From d60f64f13bb478712f222de0c4ebc98b0c70f498 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 12:31:03 +0000
Subject: [PATCH 01/22] Initial plan
From c1bbe3c374b855df9c7446c29df46f03f73b9e42 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 12:41:16 +0000
Subject: [PATCH 02/22] Add browser-mode2 example with hybrid RSC + module
runner setup
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
.../examples/browser-mode2/README.md | 47 +++++
.../examples/browser-mode2/index.html | 13 ++
.../examples/browser-mode2/package.json | 23 +++
.../examples/browser-mode2/public/vite.svg | 1 +
.../examples/browser-mode2/src/action.tsx | 11 ++
.../browser-mode2/src/assets/react.svg | 1 +
.../examples/browser-mode2/src/client.tsx | 13 ++
.../src/framework/entry.browser.tsx | 135 +++++++++++++
.../browser-mode2/src/framework/entry.rsc.tsx | 56 ++++++
.../src/framework/load-rsc-dev.tsx | 25 +++
.../browser-mode2/src/framework/main.tsx | 10 +
.../browser-mode2/src/framework/virtual.d.ts | 4 +
.../examples/browser-mode2/src/index.css | 112 +++++++++++
.../examples/browser-mode2/src/root.tsx | 44 +++++
.../examples/browser-mode2/tsconfig.json | 18 ++
.../examples/browser-mode2/vite.config.ts | 185 ++++++++++++++++++
pnpm-lock.yaml | 25 +++
17 files changed, 723 insertions(+)
create mode 100644 packages/plugin-rsc/examples/browser-mode2/README.md
create mode 100644 packages/plugin-rsc/examples/browser-mode2/index.html
create mode 100644 packages/plugin-rsc/examples/browser-mode2/package.json
create mode 100644 packages/plugin-rsc/examples/browser-mode2/public/vite.svg
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/action.tsx
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/assets/react.svg
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/client.tsx
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/index.css
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/root.tsx
create mode 100644 packages/plugin-rsc/examples/browser-mode2/tsconfig.json
create mode 100644 packages/plugin-rsc/examples/browser-mode2/vite.config.ts
diff --git a/packages/plugin-rsc/examples/browser-mode2/README.md b/packages/plugin-rsc/examples/browser-mode2/README.md
new file mode 100644
index 000000000..da89f3279
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/README.md
@@ -0,0 +1,47 @@
+# browser-mode2
+
+Hybrid RSC example that combines:
+
+- The `rsc` environment from [examples/no-ssr](../no-ssr) (simple RSC setup with `entry.rsc.tsx`)
+- The module runner approach from [examples/browser-mode](../browser-mode) (runs RSC on browser via module runner)
+
+This example demonstrates how to run React Server Components entirely in the browser using Vite's module runner API, without requiring a Node.js server environment. The RSC rendering logic that would normally run on the server is executed in the browser through the module runner.
+
+## Key Features
+
+- **No Node.js server required**: The RSC environment runs in the browser
+- **Module Runner**: Uses Vite's module runner to load and execute the RSC environment in the browser
+- **HMR Support**: Hot module replacement for both client and RSC code
+- **Server Actions**: Full support for server actions, executed in the browser context
+
+## How it works
+
+1. The main entry point (`src/framework/main.tsx`) loads both the RSC and client environments
+2. In dev mode, the RSC environment is loaded via module runner (`load-rsc-dev.tsx`)
+3. The client environment consumes the RSC output through the standard RSC protocol
+4. Server actions are handled by the RSC environment running in the browser
+
+## Architecture
+
+```
+Browser Context
+├── Client Environment (src/framework/entry.browser.tsx)
+│ ├── React Client Components ('use client')
+│ └── RSC Consumer (renders server components)
+└── RSC Environment (via Module Runner)
+ ├── Server Components (src/framework/entry.rsc.tsx)
+ └── Server Actions (src/action.tsx)
+```
+
+## Development
+
+```bash
+pnpm dev
+```
+
+## Build
+
+```bash
+pnpm build
+pnpm preview
+```
diff --git a/packages/plugin-rsc/examples/browser-mode2/index.html b/packages/plugin-rsc/examples/browser-mode2/index.html
new file mode 100644
index 000000000..ec960c1a5
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ RSC Browser Mode 2
+
+
+
+
+
+
+
+
diff --git a/packages/plugin-rsc/examples/browser-mode2/package.json b/packages/plugin-rsc/examples/browser-mode2/package.json
new file mode 100644
index 000000000..9d9e3fa38
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@vitejs/plugin-rsc-examples-browser-mode2",
+ "version": "0.0.0",
+ "private": true,
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/react": "^19.2.2",
+ "@types/react-dom": "^19.2.2",
+ "@vitejs/plugin-react": "latest",
+ "@vitejs/plugin-rsc": "latest",
+ "vite": "^7.1.10"
+ }
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/public/vite.svg b/packages/plugin-rsc/examples/browser-mode2/public/vite.svg
new file mode 100644
index 000000000..e7b8dfb1b
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/action.tsx b/packages/plugin-rsc/examples/browser-mode2/src/action.tsx
new file mode 100644
index 000000000..4fc55d65b
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/action.tsx
@@ -0,0 +1,11 @@
+'use server'
+
+let serverCounter = 0
+
+export async function getServerCounter() {
+ return serverCounter
+}
+
+export async function updateServerCounter(change: number) {
+ serverCounter += change
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/assets/react.svg b/packages/plugin-rsc/examples/browser-mode2/src/assets/react.svg
new file mode 100644
index 000000000..6c87de9bb
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/client.tsx b/packages/plugin-rsc/examples/browser-mode2/src/client.tsx
new file mode 100644
index 000000000..29bb5d367
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/client.tsx
@@ -0,0 +1,13 @@
+'use client'
+
+import React from 'react'
+
+export function ClientCounter() {
+ const [count, setCount] = React.useState(0)
+
+ return (
+
+ )
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
new file mode 100644
index 000000000..e4d4d7757
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
@@ -0,0 +1,135 @@
+import React from 'react'
+import { createRoot } from 'react-dom/client'
+import {
+ createFromFetch,
+ setServerCallback,
+ createTemporaryReferenceSet,
+ encodeReply,
+} from '@vitejs/plugin-rsc/browser'
+import type { RscPayload } from './entry.rsc'
+
+let fetchRsc: (request: Request) => Promise
+
+export function initialize(options: {
+ fetchRsc: (request: Request) => Promise
+}) {
+ fetchRsc = options.fetchRsc
+}
+
+export async function main() {
+ // stash `setPayload` function to trigger re-rendering
+ // from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr)
+ let setPayload: (v: RscPayload) => void
+
+ const initialPayload = await createFromFetch(
+ fetchRsc(new Request(window.location.href)),
+ )
+
+ // browser root component to (re-)render RSC payload as state
+ function BrowserRoot() {
+ const [payload, setPayload_] = React.useState(initialPayload)
+
+ React.useEffect(() => {
+ setPayload = (v) => React.startTransition(() => setPayload_(v))
+ }, [setPayload_])
+
+ // re-fetch/render on client side navigation
+ React.useEffect(() => {
+ return listenNavigation(() => fetchRscPayload())
+ }, [])
+
+ return payload.root
+ }
+
+ // re-fetch RSC and trigger re-rendering
+ async function fetchRscPayload() {
+ const payload = await createFromFetch(
+ fetchRsc(new Request(window.location.href)),
+ )
+ setPayload(payload)
+ }
+
+ // register a handler which will be internally called by React
+ // on server function request after hydration.
+ setServerCallback(async (id, args) => {
+ const url = new URL(window.location.href)
+ const temporaryReferences = createTemporaryReferenceSet()
+ const payload = await createFromFetch(
+ fetchRsc(
+ new Request(url, {
+ method: 'POST',
+ body: await encodeReply(args, { temporaryReferences }),
+ headers: {
+ 'x-rsc-action': id,
+ },
+ }),
+ ),
+ { temporaryReferences },
+ )
+ setPayload(payload)
+ return payload.returnValue
+ })
+
+ // hydration
+ const browserRoot = (
+
+
+
+ )
+ createRoot(document.body).render(browserRoot)
+
+ // implement server HMR by trigering re-fetch/render of RSC upon server code change
+ if (import.meta.hot) {
+ import.meta.hot.on('rsc:update', () => {
+ fetchRscPayload()
+ })
+ }
+}
+
+// a little helper to setup events interception for client side navigation
+function listenNavigation(onNavigation: () => void) {
+ window.addEventListener('popstate', onNavigation)
+
+ const oldPushState = window.history.pushState
+ window.history.pushState = function (...args) {
+ const res = oldPushState.apply(this, args)
+ onNavigation()
+ return res
+ }
+
+ const oldReplaceState = window.history.replaceState
+ window.history.replaceState = function (...args) {
+ const res = oldReplaceState.apply(this, args)
+ onNavigation()
+ return res
+ }
+
+ function onClick(e: MouseEvent) {
+ let link = (e.target as Element).closest('a')
+ if (
+ link &&
+ link instanceof HTMLAnchorElement &&
+ link.href &&
+ (!link.target || link.target === '_self') &&
+ link.origin === location.origin &&
+ !link.hasAttribute('download') &&
+ e.button === 0 && // left clicks only
+ !e.metaKey && // open in new tab (mac)
+ !e.ctrlKey && // open in new tab (windows)
+ !e.altKey && // download
+ !e.shiftKey &&
+ !e.defaultPrevented
+ ) {
+ e.preventDefault()
+ history.pushState(null, '', link.href)
+ }
+ }
+ document.addEventListener('click', onClick)
+
+ return () => {
+ document.removeEventListener('click', onClick)
+ window.removeEventListener('popstate', onNavigation)
+ window.history.pushState = oldPushState
+ window.history.replaceState = oldReplaceState
+ }
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
new file mode 100644
index 000000000..27a5ce931
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
@@ -0,0 +1,56 @@
+import {
+ renderToReadableStream,
+ createTemporaryReferenceSet,
+ decodeReply,
+ loadServerAction,
+ decodeAction,
+ decodeFormState,
+} from '@vitejs/plugin-rsc/rsc'
+import type { ReactFormState } from 'react-dom/client'
+import { Root } from '../root.tsx'
+
+export type RscPayload = {
+ root: React.ReactNode
+ returnValue?: unknown
+ formState?: ReactFormState
+}
+
+export default async function handler(request: Request): Promise {
+ const isAction = request.method === 'POST'
+ let returnValue: unknown | undefined
+ let formState: ReactFormState | undefined
+ let temporaryReferences: unknown | undefined
+ if (isAction) {
+ const actionId = request.headers.get('x-rsc-action')
+ if (actionId) {
+ const contentType = request.headers.get('content-type')
+ const body = contentType?.startsWith('multipart/form-data')
+ ? await request.formData()
+ : await request.text()
+ temporaryReferences = createTemporaryReferenceSet()
+ const args = await decodeReply(body, { temporaryReferences })
+ const action = await loadServerAction(actionId)
+ returnValue = await action.apply(null, args)
+ } else {
+ const formData = await request.formData()
+ const decodedAction = await decodeAction(formData)
+ const result = await decodedAction()
+ formState = await decodeFormState(result, formData)
+ }
+ }
+
+ const rscPayload: RscPayload = { root: , formState, returnValue }
+ const rscOptions = { temporaryReferences }
+ const rscStream = renderToReadableStream(rscPayload, rscOptions)
+
+ return new Response(rscStream, {
+ headers: {
+ 'content-type': 'text/x-component;charset=utf-8',
+ vary: 'accept',
+ },
+ })
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept()
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
new file mode 100644
index 000000000..a9af62eae
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
@@ -0,0 +1,25 @@
+import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
+
+export default async function loadRsc() {
+ const runner = new ModuleRunner(
+ {
+ sourcemapInterceptor: false,
+ transport: {
+ invoke: async (payload) => {
+ const response = await fetch(
+ '/@vite/invoke-rsc?' +
+ new URLSearchParams({
+ data: JSON.stringify(payload),
+ }),
+ )
+ return response.json()
+ },
+ },
+ hmr: false,
+ },
+ new ESModulesEvaluator(),
+ )
+ return await runner.import(
+ '/src/framework/entry.rsc.tsx',
+ )
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
new file mode 100644
index 000000000..38066dcff
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
@@ -0,0 +1,10 @@
+import * as client from './entry.browser'
+import loadRsc from 'virtual:vite-rsc-browser-mode2/load-rsc'
+
+async function main() {
+ const rsc = await loadRsc()
+ client.initialize({ fetchRsc: rsc.default })
+ await client.main()
+}
+
+main()
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
new file mode 100644
index 000000000..4c61bcad4
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
@@ -0,0 +1,4 @@
+declare module 'virtual:vite-rsc-browser-mode2/load-rsc' {
+ const loadRsc: () => Promise
+ export default loadRsc
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/index.css b/packages/plugin-rsc/examples/browser-mode2/src/index.css
new file mode 100644
index 000000000..f4d2128c0
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/index.css
@@ -0,0 +1,112 @@
+:root {
+ font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ font-weight: 500;
+ color: #646cff;
+ text-decoration: inherit;
+}
+a:hover {
+ color: #535bf2;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 1rem;
+}
+
+.read-the-docs {
+ color: #888;
+ text-align: left;
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/root.tsx b/packages/plugin-rsc/examples/browser-mode2/src/root.tsx
new file mode 100644
index 000000000..9baa7b9c2
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/root.tsx
@@ -0,0 +1,44 @@
+import './index.css' // css import is automatically injected in exported server components
+import viteLogo from '/vite.svg'
+import { getServerCounter, updateServerCounter } from './action.tsx'
+import reactLogo from './assets/react.svg'
+import { ClientCounter } from './client.tsx'
+
+export function Root() {
+ return
+}
+
+function App() {
+ return (
+
+
+
Vite + RSC
+
+
+
+
+
+
+
+ -
+ Edit
src/client.tsx to test client HMR.
+
+ -
+ Edit
src/root.tsx to test server HMR.
+
+
+
+ )
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/tsconfig.json b/packages/plugin-rsc/examples/browser-mode2/tsconfig.json
new file mode 100644
index 000000000..4c355ed3c
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "erasableSyntaxOnly": true,
+ "allowImportingTsExtensions": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "skipLibCheck": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "target": "ESNext",
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
+ "types": ["vite/client", "@vitejs/plugin-rsc/types"],
+ "jsx": "react-jsx"
+ }
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
new file mode 100644
index 000000000..9579455c8
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -0,0 +1,185 @@
+import { defaultClientConditions, defineConfig, type Plugin } from 'vite'
+import react from '@vitejs/plugin-react'
+import {
+ vitePluginRscMinimal,
+ getPluginApi,
+ type PluginApi,
+} from '@vitejs/plugin-rsc/plugin'
+
+export default defineConfig({
+ plugins: [react(), rscBrowserMode2Plugin()],
+ environments: {
+ client: {
+ build: {
+ minify: false,
+ },
+ },
+ },
+})
+
+function rscBrowserMode2Plugin(): Plugin[] {
+ let manager: PluginApi['manager']
+
+ return [
+ ...vitePluginRscMinimal({
+ environment: {
+ rsc: 'rsc',
+ browser: 'client',
+ },
+ }),
+ {
+ name: 'rsc-browser-mode2',
+ config(userConfig, env) {
+ return {
+ define: {
+ 'import.meta.env.__vite_rsc_build__': JSON.stringify(
+ env.command === 'build',
+ ),
+ },
+ environments: {
+ client: {
+ keepProcessEnv: false,
+ resolve: {
+ conditions: [...defaultClientConditions],
+ },
+ optimizeDeps: {
+ include: [
+ 'react',
+ 'react-dom',
+ 'react-dom/client',
+ 'react/jsx-runtime',
+ 'react/jsx-dev-runtime',
+ '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser',
+ ],
+ exclude: ['@vitejs/plugin-rsc'],
+ },
+ build: {
+ outDir: 'dist/client',
+ },
+ },
+ rsc: {
+ keepProcessEnv: false,
+ resolve: {
+ conditions: ['react-server', ...defaultClientConditions],
+ noExternal: true,
+ },
+ optimizeDeps: {
+ include: [
+ 'react',
+ 'react-dom',
+ 'react/jsx-runtime',
+ 'react/jsx-dev-runtime',
+ '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge',
+ ],
+ exclude: ['@vitejs/plugin-rsc'],
+ esbuildOptions: {
+ platform: 'browser',
+ },
+ },
+ build: {
+ outDir: 'dist/rsc',
+ copyPublicDir: false,
+ emitAssets: true,
+ rollupOptions: {
+ input: {
+ 'entry.rsc': './src/framework/entry.rsc.tsx',
+ },
+ },
+ },
+ },
+ },
+ builder: {
+ sharedPlugins: true,
+ sharedConfigBuild: true,
+ },
+ build: {
+ rollupOptions: {
+ onwarn(warning, defaultHandler) {
+ if (
+ warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
+ (warning.message.includes('use client') ||
+ warning.message.includes('use server'))
+ ) {
+ return
+ }
+ if (
+ warning.code === 'SOURCEMAP_ERROR' &&
+ warning.message.includes('resolve original location') &&
+ warning.pos === 0
+ ) {
+ return
+ }
+ if (userConfig.build?.rollupOptions?.onwarn) {
+ userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
+ } else {
+ defaultHandler(warning)
+ }
+ },
+ },
+ },
+ }
+ },
+ configResolved(config) {
+ manager = getPluginApi(config)!.manager
+ },
+ configureServer(server) {
+ server.middlewares.use(async (req, res, next) => {
+ const url = new URL(req.url ?? '/', 'https://any.local')
+ if (url.pathname === '/@vite/invoke-rsc') {
+ const payload = JSON.parse(url.searchParams.get('data')!)
+ const result =
+ await server.environments['rsc']!.hot.handleInvoke(payload)
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify(result))
+ return
+ }
+ next()
+ })
+ },
+ hotUpdate(ctx) {
+ if (this.environment.name === 'rsc') {
+ if (ctx.modules.length > 0) {
+ ctx.server.environments.client.hot.send({
+ type: 'custom',
+ event: 'rsc:update',
+ })
+ }
+ }
+ },
+ async buildApp(builder) {
+ const rscEnv = builder.environments.rsc!
+ const clientEnv = builder.environments.client!
+ manager.isScanBuild = true
+ rscEnv.config.build.write = false
+ await builder.build(rscEnv)
+ manager.isScanBuild = false
+ rscEnv.config.build.write = true
+ await builder.build(rscEnv)
+ await builder.build(clientEnv)
+ },
+ },
+ {
+ name: 'rsc-browser-mode2:load-rsc',
+ resolveId(source) {
+ if (source === 'virtual:vite-rsc-browser-mode2/load-rsc') {
+ if (this.environment.mode === 'dev') {
+ return this.resolve('/src/framework/load-rsc-dev')
+ }
+ return '\0' + source
+ }
+ },
+ load(id) {
+ if (id === '\0virtual:vite-rsc-browser-mode2/load-rsc') {
+ // In build mode, return a function that dynamically imports the built RSC module
+ if (manager.isScanBuild) {
+ return `export default async () => {}`
+ } else {
+ return `export default async () => {
+ return await import("/dist/rsc/entry.rsc.js")
+ }`
+ }
+ }
+ },
+ },
+ ]
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index db0a7a5c2..d5474f240 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -584,6 +584,31 @@ importers:
specifier: ^7.1.10
version: 7.1.10(@types/node@22.18.11)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)
+ packages/plugin-rsc/examples/browser-mode2:
+ dependencies:
+ react:
+ specifier: ^19.2.0
+ version: 19.2.0
+ react-dom:
+ specifier: ^19.2.0
+ version: 19.2.0(react@19.2.0)
+ devDependencies:
+ '@types/react':
+ specifier: ^19.2.2
+ version: 19.2.2
+ '@types/react-dom':
+ specifier: ^19.2.2
+ version: 19.2.2(@types/react@19.2.2)
+ '@vitejs/plugin-react':
+ specifier: latest
+ version: link:../../../plugin-react
+ '@vitejs/plugin-rsc':
+ specifier: latest
+ version: link:../..
+ vite:
+ specifier: ^7.1.10
+ version: 7.1.10(@types/node@22.18.11)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)
+
packages/plugin-rsc/examples/e2e:
devDependencies:
'@vitejs/plugin-react':
From 67d09defa06952b85bfabfd5659d2069bc3e4021 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 12:59:28 +0000
Subject: [PATCH 03/22] Fix browser-mode2: add client/server reference mappings
for build mode
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
.../src/framework/entry.browser.tsx | 19 +++++++-
.../browser-mode2/src/framework/entry.rsc.tsx | 22 ++++++++-
.../browser-mode2/src/framework/main.tsx | 1 +
.../browser-mode2/src/framework/virtual.d.ts | 10 +++++
.../examples/browser-mode2/vite.config.ts | 45 +++++++++++++++++++
5 files changed, 95 insertions(+), 2 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
index e4d4d7757..154f2e4ea 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
@@ -3,10 +3,12 @@ import { createRoot } from 'react-dom/client'
import {
createFromFetch,
setServerCallback,
+ setRequireModule,
createTemporaryReferenceSet,
encodeReply,
-} from '@vitejs/plugin-rsc/browser'
+} from '@vitejs/plugin-rsc/react/browser'
import type { RscPayload } from './entry.rsc'
+import buildClientReferences from 'virtual:vite-rsc-browser-mode2/build-client-references'
let fetchRsc: (request: Request) => Promise
@@ -14,6 +16,21 @@ export function initialize(options: {
fetchRsc: (request: Request) => Promise
}) {
fetchRsc = options.fetchRsc
+
+ // Setup client reference loading
+ setRequireModule({
+ load: async (id) => {
+ if (import.meta.env.__vite_rsc_build__) {
+ const import_ = buildClientReferences[id]
+ if (!import_) {
+ throw new Error(`invalid client reference: ${id}`)
+ }
+ return import_()
+ } else {
+ return import(/* @vite-ignore */ id)
+ }
+ },
+ })
}
export async function main() {
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
index 27a5ce931..eb0be3fd8 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
@@ -1,13 +1,15 @@
import {
+ setRequireModule,
renderToReadableStream,
createTemporaryReferenceSet,
decodeReply,
loadServerAction,
decodeAction,
decodeFormState,
-} from '@vitejs/plugin-rsc/rsc'
+} from '@vitejs/plugin-rsc/react/rsc'
import type { ReactFormState } from 'react-dom/client'
import { Root } from '../root.tsx'
+import buildServerReferences from 'virtual:vite-rsc-browser-mode2/build-server-references'
export type RscPayload = {
root: React.ReactNode
@@ -15,6 +17,24 @@ export type RscPayload = {
formState?: ReactFormState
}
+declare let __vite_rsc_raw_import__: (id: string) => Promise
+
+export function initialize() {
+ setRequireModule({
+ load: (id) => {
+ if (import.meta.env.__vite_rsc_build__) {
+ const import_ = buildServerReferences[id]
+ if (!import_) {
+ throw new Error(`invalid server reference: ${id}`)
+ }
+ return import_()
+ } else {
+ return __vite_rsc_raw_import__(/* @vite-ignore */ id)
+ }
+ },
+ })
+}
+
export default async function handler(request: Request): Promise {
const isAction = request.method === 'POST'
let returnValue: unknown | undefined
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
index 38066dcff..d83888ecf 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
@@ -3,6 +3,7 @@ import loadRsc from 'virtual:vite-rsc-browser-mode2/load-rsc'
async function main() {
const rsc = await loadRsc()
+ rsc.initialize()
client.initialize({ fetchRsc: rsc.default })
await client.main()
}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
index 4c61bcad4..cc4a80c39 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
@@ -2,3 +2,13 @@ declare module 'virtual:vite-rsc-browser-mode2/load-rsc' {
const loadRsc: () => Promise
export default loadRsc
}
+
+declare module 'virtual:vite-rsc-browser-mode2/build-client-references' {
+ const buildClientReferences: Record Promise>
+ export default buildClientReferences
+}
+
+declare module 'virtual:vite-rsc-browser-mode2/build-server-references' {
+ const buildServerReferences: Record Promise>
+ export default buildServerReferences
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index 9579455c8..6f4fd20aa 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -70,6 +70,7 @@ function rscBrowserMode2Plugin(): Plugin[] {
'react/jsx-runtime',
'react/jsx-dev-runtime',
'@vitejs/plugin-rsc/vendor/react-server-dom/server.edge',
+ '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge',
],
exclude: ['@vitejs/plugin-rsc'],
esbuildOptions: {
@@ -181,5 +182,49 @@ function rscBrowserMode2Plugin(): Plugin[] {
}
},
},
+ {
+ name: 'rsc-browser-mode2:build-client-references',
+ resolveId(source) {
+ if (
+ source === 'virtual:vite-rsc-browser-mode2/build-client-references'
+ ) {
+ return '\0' + source
+ }
+ },
+ load(id) {
+ if (id === '\0virtual:vite-rsc-browser-mode2/build-client-references') {
+ if (this.environment.mode === 'dev') {
+ return `export default {}` // no-op during dev
+ }
+ let code = ''
+ for (const meta of Object.values(manager.clientReferenceMetaMap)) {
+ code += `${JSON.stringify(meta.referenceKey)}: () => import(${JSON.stringify(meta.importId)}),`
+ }
+ return `export default {${code}}`
+ }
+ },
+ },
+ {
+ name: 'rsc-browser-mode2:build-server-references',
+ resolveId(source) {
+ if (
+ source === 'virtual:vite-rsc-browser-mode2/build-server-references'
+ ) {
+ return '\0' + source
+ }
+ },
+ load(id) {
+ if (id === '\0virtual:vite-rsc-browser-mode2/build-server-references') {
+ if (this.environment.mode === 'dev') {
+ return `export default {}` // no-op during dev
+ }
+ let code = ''
+ for (const meta of Object.values(manager.serverReferenceMetaMap)) {
+ code += `${JSON.stringify(meta.referenceKey)}: () => import(${JSON.stringify(meta.importId)}),`
+ }
+ return `export default {${code}}`
+ }
+ },
+ },
]
}
From 2a3c3bfb47d96c41d5b9d845a26ce3c03c7b4195 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 13:13:11 +0000
Subject: [PATCH 04/22] Simplify vite.config.ts: use main rsc plugin and keep
only middleware
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
.../examples/browser-mode2/vite.config.ts | 140 ++----------------
1 file changed, 14 insertions(+), 126 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index 6f4fd20aa..fa5e8fb16 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -1,13 +1,17 @@
-import { defaultClientConditions, defineConfig, type Plugin } from 'vite'
+import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
-import {
- vitePluginRscMinimal,
- getPluginApi,
- type PluginApi,
-} from '@vitejs/plugin-rsc/plugin'
+import rsc, { getPluginApi, type PluginApi } from '@vitejs/plugin-rsc'
export default defineConfig({
- plugins: [react(), rscBrowserMode2Plugin()],
+ plugins: [
+ react(),
+ rsc({
+ entries: {
+ rsc: './src/framework/entry.rsc.tsx',
+ },
+ }),
+ rscBrowserMode2Plugin(),
+ ],
environments: {
client: {
build: {
@@ -21,105 +25,8 @@ function rscBrowserMode2Plugin(): Plugin[] {
let manager: PluginApi['manager']
return [
- ...vitePluginRscMinimal({
- environment: {
- rsc: 'rsc',
- browser: 'client',
- },
- }),
{
name: 'rsc-browser-mode2',
- config(userConfig, env) {
- return {
- define: {
- 'import.meta.env.__vite_rsc_build__': JSON.stringify(
- env.command === 'build',
- ),
- },
- environments: {
- client: {
- keepProcessEnv: false,
- resolve: {
- conditions: [...defaultClientConditions],
- },
- optimizeDeps: {
- include: [
- 'react',
- 'react-dom',
- 'react-dom/client',
- 'react/jsx-runtime',
- 'react/jsx-dev-runtime',
- '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser',
- ],
- exclude: ['@vitejs/plugin-rsc'],
- },
- build: {
- outDir: 'dist/client',
- },
- },
- rsc: {
- keepProcessEnv: false,
- resolve: {
- conditions: ['react-server', ...defaultClientConditions],
- noExternal: true,
- },
- optimizeDeps: {
- include: [
- 'react',
- 'react-dom',
- 'react/jsx-runtime',
- 'react/jsx-dev-runtime',
- '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge',
- '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge',
- ],
- exclude: ['@vitejs/plugin-rsc'],
- esbuildOptions: {
- platform: 'browser',
- },
- },
- build: {
- outDir: 'dist/rsc',
- copyPublicDir: false,
- emitAssets: true,
- rollupOptions: {
- input: {
- 'entry.rsc': './src/framework/entry.rsc.tsx',
- },
- },
- },
- },
- },
- builder: {
- sharedPlugins: true,
- sharedConfigBuild: true,
- },
- build: {
- rollupOptions: {
- onwarn(warning, defaultHandler) {
- if (
- warning.code === 'MODULE_LEVEL_DIRECTIVE' &&
- (warning.message.includes('use client') ||
- warning.message.includes('use server'))
- ) {
- return
- }
- if (
- warning.code === 'SOURCEMAP_ERROR' &&
- warning.message.includes('resolve original location') &&
- warning.pos === 0
- ) {
- return
- }
- if (userConfig.build?.rollupOptions?.onwarn) {
- userConfig.build.rollupOptions.onwarn(warning, defaultHandler)
- } else {
- defaultHandler(warning)
- }
- },
- },
- },
- }
- },
configResolved(config) {
manager = getPluginApi(config)!.manager
},
@@ -137,27 +44,6 @@ function rscBrowserMode2Plugin(): Plugin[] {
next()
})
},
- hotUpdate(ctx) {
- if (this.environment.name === 'rsc') {
- if (ctx.modules.length > 0) {
- ctx.server.environments.client.hot.send({
- type: 'custom',
- event: 'rsc:update',
- })
- }
- }
- },
- async buildApp(builder) {
- const rscEnv = builder.environments.rsc!
- const clientEnv = builder.environments.client!
- manager.isScanBuild = true
- rscEnv.config.build.write = false
- await builder.build(rscEnv)
- manager.isScanBuild = false
- rscEnv.config.build.write = true
- await builder.build(rscEnv)
- await builder.build(clientEnv)
- },
},
{
name: 'rsc-browser-mode2:load-rsc',
@@ -175,8 +61,10 @@ function rscBrowserMode2Plugin(): Plugin[] {
if (manager.isScanBuild) {
return `export default async () => {}`
} else {
+ // Use a dynamic import expression that won't be statically analyzed
return `export default async () => {
- return await import("/dist/rsc/entry.rsc.js")
+ const path = "/dist/rsc/index.js"
+ return await import(/* @vite-ignore */ path)
}`
}
}
From 47b1e7b8006dd6d2deb927dbc240e17a77e630aa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 13:20:19 +0000
Subject: [PATCH 05/22] Focus on dev-only: remove getPluginApi and build-time
virtual modules
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
.../src/framework/entry.browser.tsx | 13 +---
.../browser-mode2/src/framework/entry.rsc.tsx | 11 +--
.../browser-mode2/src/framework/virtual.d.ts | 10 ---
.../examples/browser-mode2/vite.config.ts | 77 +------------------
4 files changed, 5 insertions(+), 106 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
index 154f2e4ea..6811bcd9e 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
@@ -8,7 +8,6 @@ import {
encodeReply,
} from '@vitejs/plugin-rsc/react/browser'
import type { RscPayload } from './entry.rsc'
-import buildClientReferences from 'virtual:vite-rsc-browser-mode2/build-client-references'
let fetchRsc: (request: Request) => Promise
@@ -17,18 +16,10 @@ export function initialize(options: {
}) {
fetchRsc = options.fetchRsc
- // Setup client reference loading
+ // Setup client reference loading for dev mode
setRequireModule({
load: async (id) => {
- if (import.meta.env.__vite_rsc_build__) {
- const import_ = buildClientReferences[id]
- if (!import_) {
- throw new Error(`invalid client reference: ${id}`)
- }
- return import_()
- } else {
- return import(/* @vite-ignore */ id)
- }
+ return import(/* @vite-ignore */ id)
},
})
}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
index eb0be3fd8..bbcee93c1 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
@@ -9,7 +9,6 @@ import {
} from '@vitejs/plugin-rsc/react/rsc'
import type { ReactFormState } from 'react-dom/client'
import { Root } from '../root.tsx'
-import buildServerReferences from 'virtual:vite-rsc-browser-mode2/build-server-references'
export type RscPayload = {
root: React.ReactNode
@@ -22,15 +21,7 @@ declare let __vite_rsc_raw_import__: (id: string) => Promise
export function initialize() {
setRequireModule({
load: (id) => {
- if (import.meta.env.__vite_rsc_build__) {
- const import_ = buildServerReferences[id]
- if (!import_) {
- throw new Error(`invalid server reference: ${id}`)
- }
- return import_()
- } else {
- return __vite_rsc_raw_import__(/* @vite-ignore */ id)
- }
+ return __vite_rsc_raw_import__(/* @vite-ignore */ id)
},
})
}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
index cc4a80c39..4c61bcad4 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
@@ -2,13 +2,3 @@ declare module 'virtual:vite-rsc-browser-mode2/load-rsc' {
const loadRsc: () => Promise
export default loadRsc
}
-
-declare module 'virtual:vite-rsc-browser-mode2/build-client-references' {
- const buildClientReferences: Record Promise>
- export default buildClientReferences
-}
-
-declare module 'virtual:vite-rsc-browser-mode2/build-server-references' {
- const buildServerReferences: Record Promise>
- export default buildServerReferences
-}
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index fa5e8fb16..16253ca82 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -1,6 +1,6 @@
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
-import rsc, { getPluginApi, type PluginApi } from '@vitejs/plugin-rsc'
+import rsc from '@vitejs/plugin-rsc'
export default defineConfig({
plugins: [
@@ -12,24 +12,12 @@ export default defineConfig({
}),
rscBrowserMode2Plugin(),
],
- environments: {
- client: {
- build: {
- minify: false,
- },
- },
- },
})
function rscBrowserMode2Plugin(): Plugin[] {
- let manager: PluginApi['manager']
-
return [
{
name: 'rsc-browser-mode2',
- configResolved(config) {
- manager = getPluginApi(config)!.manager
- },
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url ?? '/', 'https://any.local')
@@ -49,68 +37,7 @@ function rscBrowserMode2Plugin(): Plugin[] {
name: 'rsc-browser-mode2:load-rsc',
resolveId(source) {
if (source === 'virtual:vite-rsc-browser-mode2/load-rsc') {
- if (this.environment.mode === 'dev') {
- return this.resolve('/src/framework/load-rsc-dev')
- }
- return '\0' + source
- }
- },
- load(id) {
- if (id === '\0virtual:vite-rsc-browser-mode2/load-rsc') {
- // In build mode, return a function that dynamically imports the built RSC module
- if (manager.isScanBuild) {
- return `export default async () => {}`
- } else {
- // Use a dynamic import expression that won't be statically analyzed
- return `export default async () => {
- const path = "/dist/rsc/index.js"
- return await import(/* @vite-ignore */ path)
- }`
- }
- }
- },
- },
- {
- name: 'rsc-browser-mode2:build-client-references',
- resolveId(source) {
- if (
- source === 'virtual:vite-rsc-browser-mode2/build-client-references'
- ) {
- return '\0' + source
- }
- },
- load(id) {
- if (id === '\0virtual:vite-rsc-browser-mode2/build-client-references') {
- if (this.environment.mode === 'dev') {
- return `export default {}` // no-op during dev
- }
- let code = ''
- for (const meta of Object.values(manager.clientReferenceMetaMap)) {
- code += `${JSON.stringify(meta.referenceKey)}: () => import(${JSON.stringify(meta.importId)}),`
- }
- return `export default {${code}}`
- }
- },
- },
- {
- name: 'rsc-browser-mode2:build-server-references',
- resolveId(source) {
- if (
- source === 'virtual:vite-rsc-browser-mode2/build-server-references'
- ) {
- return '\0' + source
- }
- },
- load(id) {
- if (id === '\0virtual:vite-rsc-browser-mode2/build-server-references') {
- if (this.environment.mode === 'dev') {
- return `export default {}` // no-op during dev
- }
- let code = ''
- for (const meta of Object.values(manager.serverReferenceMetaMap)) {
- code += `${JSON.stringify(meta.referenceKey)}: () => import(${JSON.stringify(meta.importId)}),`
- }
- return `export default {${code}}`
+ return this.resolve('/src/framework/load-rsc-dev')
}
},
},
From 6594ccc8d3829635d59b2802b26c05d50ea5e32b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 13:25:26 +0000
Subject: [PATCH 06/22] Use @vitejs/plugin-rsc/browser and /rsc, remove
setRequireModule
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
.../browser-mode2/src/framework/entry.browser.tsx | 10 +---------
.../browser-mode2/src/framework/entry.rsc.tsx | 13 +------------
.../examples/browser-mode2/src/framework/main.tsx | 1 -
3 files changed, 2 insertions(+), 22 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
index 6811bcd9e..e4d4d7757 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
@@ -3,10 +3,9 @@ import { createRoot } from 'react-dom/client'
import {
createFromFetch,
setServerCallback,
- setRequireModule,
createTemporaryReferenceSet,
encodeReply,
-} from '@vitejs/plugin-rsc/react/browser'
+} from '@vitejs/plugin-rsc/browser'
import type { RscPayload } from './entry.rsc'
let fetchRsc: (request: Request) => Promise
@@ -15,13 +14,6 @@ export function initialize(options: {
fetchRsc: (request: Request) => Promise
}) {
fetchRsc = options.fetchRsc
-
- // Setup client reference loading for dev mode
- setRequireModule({
- load: async (id) => {
- return import(/* @vite-ignore */ id)
- },
- })
}
export async function main() {
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
index bbcee93c1..27a5ce931 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
@@ -1,12 +1,11 @@
import {
- setRequireModule,
renderToReadableStream,
createTemporaryReferenceSet,
decodeReply,
loadServerAction,
decodeAction,
decodeFormState,
-} from '@vitejs/plugin-rsc/react/rsc'
+} from '@vitejs/plugin-rsc/rsc'
import type { ReactFormState } from 'react-dom/client'
import { Root } from '../root.tsx'
@@ -16,16 +15,6 @@ export type RscPayload = {
formState?: ReactFormState
}
-declare let __vite_rsc_raw_import__: (id: string) => Promise
-
-export function initialize() {
- setRequireModule({
- load: (id) => {
- return __vite_rsc_raw_import__(/* @vite-ignore */ id)
- },
- })
-}
-
export default async function handler(request: Request): Promise {
const isAction = request.method === 'POST'
let returnValue: unknown | undefined
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
index d83888ecf..38066dcff 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
@@ -3,7 +3,6 @@ import loadRsc from 'virtual:vite-rsc-browser-mode2/load-rsc'
async function main() {
const rsc = await loadRsc()
- rsc.initialize()
client.initialize({ fetchRsc: rsc.default })
await client.main()
}
From 925e985bed5ff1bae416a56a2e9941780749e1b0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 13:33:07 +0000
Subject: [PATCH 07/22] Changes before error encountered
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
.../examples/browser-mode2/vite.config.ts | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index 16253ca82..1a0bb8807 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -37,7 +37,18 @@ function rscBrowserMode2Plugin(): Plugin[] {
name: 'rsc-browser-mode2:load-rsc',
resolveId(source) {
if (source === 'virtual:vite-rsc-browser-mode2/load-rsc') {
- return this.resolve('/src/framework/load-rsc-dev')
+ if (this.environment.mode === 'dev') {
+ return this.resolve('/src/framework/load-rsc-dev')
+ }
+ return '\0' + source
+ }
+ },
+ load(id) {
+ if (id === '\0virtual:vite-rsc-browser-mode2/load-rsc') {
+ // In build mode, return a function that dynamically imports the built RSC module
+ return `export default async () => {
+ return await import("/dist/rsc/index.js")
+ }`
}
},
},
From 29ad4e040b1721a7d2fa5d45470ea5017558aec6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 16:03:06 +0000
Subject: [PATCH 08/22] Add e2e test for browser-mode2 example
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
packages/plugin-rsc/e2e/browser-mode2.test.ts | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
create mode 100644 packages/plugin-rsc/e2e/browser-mode2.test.ts
diff --git a/packages/plugin-rsc/e2e/browser-mode2.test.ts b/packages/plugin-rsc/e2e/browser-mode2.test.ts
new file mode 100644
index 000000000..2b7bb70b9
--- /dev/null
+++ b/packages/plugin-rsc/e2e/browser-mode2.test.ts
@@ -0,0 +1,23 @@
+import { expect, test } from '@playwright/test'
+import { useFixture } from './fixture'
+import { defineStarterTest } from './starter'
+import path from 'node:path'
+import fs from 'node:fs'
+
+// Webkit fails by
+// > TypeError: ReadableByteStreamController is not implemented
+test.skip(({ browserName }) => browserName === 'webkit')
+
+test.describe('dev-browser-mode2', () => {
+ const f = useFixture({ root: 'examples/browser-mode2', mode: 'dev' })
+ defineStarterTest(f, 'browser-mode')
+})
+
+test.describe('build-browser-mode2', () => {
+ const f = useFixture({ root: 'examples/browser-mode2', mode: 'build' })
+ defineStarterTest(f, 'browser-mode')
+
+ test('no ssr build', () => {
+ expect(fs.existsSync(path.join(f.root, 'dist/ssr'))).toBe(false)
+ })
+})
From 572cbc8129d52915ee3d0e63490ff89aaab06ecc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 21 Oct 2025 16:25:02 +0000
Subject: [PATCH 09/22] Add spaPlugin and async_hooks polyfill to fix page
loading
Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com>
---
.../src/framework/async-hooks-polyfill.js | 20 +++++
.../examples/browser-mode2/vite.config.ts | 78 +++++++++++++++++++
2 files changed, 98 insertions(+)
create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js b/packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js
new file mode 100644
index 000000000..15225e9af
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js
@@ -0,0 +1,20 @@
+// Browser polyfill for node:async_hooks
+export class AsyncLocalStorage {
+ constructor() {
+ this.store = undefined
+ }
+
+ run(store, callback, ...args) {
+ const prev = this.store
+ this.store = store
+ try {
+ return callback(...args)
+ } finally {
+ this.store = prev
+ }
+ }
+
+ getStore() {
+ return this.store
+ }
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index 1a0bb8807..f24eab69d 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -1,9 +1,11 @@
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import rsc from '@vitejs/plugin-rsc'
+import fsp from 'node:fs/promises'
export default defineConfig({
plugins: [
+ spaPlugin(),
react(),
rsc({
entries: {
@@ -12,8 +14,84 @@ export default defineConfig({
}),
rscBrowserMode2Plugin(),
],
+ define: {
+ 'process.env.NODE_ENV': JSON.stringify('development'),
+ },
+ environments: {
+ rsc: {
+ resolve: {
+ noExternal: true,
+ alias: {
+ 'node:async_hooks': '/src/framework/async-hooks-polyfill.js',
+ },
+ },
+ optimizeDeps: {
+ include: [
+ 'react',
+ 'react-dom',
+ 'react/jsx-runtime',
+ 'react/jsx-dev-runtime',
+ '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge',
+ '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge',
+ ],
+ exclude: ['@vitejs/plugin-rsc'],
+ esbuildOptions: {
+ platform: 'browser',
+ },
+ },
+ },
+ },
})
+function spaPlugin(): Plugin[] {
+ // serve index.html before rsc server
+ return [
+ {
+ name: 'serve-spa',
+ configureServer(server) {
+ return () => {
+ server.middlewares.use(async (req, res, next) => {
+ try {
+ if (req.headers.accept?.includes('text/html')) {
+ const html = await fsp.readFile('index.html', 'utf-8')
+ const transformed = await server.transformIndexHtml('/', html)
+ res.setHeader('Content-type', 'text/html')
+ res.setHeader('Vary', 'accept')
+ res.end(transformed)
+ return
+ }
+ } catch (error) {
+ next(error)
+ return
+ }
+ next()
+ })
+ }
+ },
+ configurePreviewServer(server) {
+ return () => {
+ server.middlewares.use(async (req, res, next) => {
+ try {
+ if (req.headers.accept?.includes('text/html')) {
+ const html = await fsp.readFile(
+ 'dist/client/index.html',
+ 'utf-8',
+ )
+ res.end(html)
+ return
+ }
+ } catch (error) {
+ next(error)
+ return
+ }
+ next()
+ })
+ }
+ },
+ },
+ ]
+}
+
function rscBrowserMode2Plugin(): Plugin[] {
return [
{
From 49966c414840178756e6b10e5158a772368fadc7 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Wed, 22 Oct 2025 11:41:43 +0900
Subject: [PATCH 10/22] fix: patch `rsc:inject-async-local-storage`
---
.../examples/browser-mode2/vite.config.ts | 80 +++----------------
1 file changed, 13 insertions(+), 67 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index f24eab69d..d5ec200dc 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -1,97 +1,43 @@
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import rsc from '@vitejs/plugin-rsc'
-import fsp from 'node:fs/promises'
export default defineConfig({
+ appType: 'spa',
plugins: [
- spaPlugin(),
react(),
rsc({
+ serverHandler: false,
entries: {
rsc: './src/framework/entry.rsc.tsx',
},
}),
rscBrowserMode2Plugin(),
+ {
+ name: 'patch-rsc',
+ configResolved(config) {
+ const plugin = config.plugins.find(
+ (p) => p.name === 'rsc:inject-async-local-storage',
+ )
+ delete plugin!.transform
+ },
+ },
],
- define: {
- 'process.env.NODE_ENV': JSON.stringify('development'),
- },
environments: {
rsc: {
+ keepProcessEnv: false,
resolve: {
noExternal: true,
- alias: {
- 'node:async_hooks': '/src/framework/async-hooks-polyfill.js',
- },
},
optimizeDeps: {
- include: [
- 'react',
- 'react-dom',
- 'react/jsx-runtime',
- 'react/jsx-dev-runtime',
- '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge',
- '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge',
- ],
- exclude: ['@vitejs/plugin-rsc'],
esbuildOptions: {
- platform: 'browser',
+ platform: 'neutral',
},
},
},
},
})
-function spaPlugin(): Plugin[] {
- // serve index.html before rsc server
- return [
- {
- name: 'serve-spa',
- configureServer(server) {
- return () => {
- server.middlewares.use(async (req, res, next) => {
- try {
- if (req.headers.accept?.includes('text/html')) {
- const html = await fsp.readFile('index.html', 'utf-8')
- const transformed = await server.transformIndexHtml('/', html)
- res.setHeader('Content-type', 'text/html')
- res.setHeader('Vary', 'accept')
- res.end(transformed)
- return
- }
- } catch (error) {
- next(error)
- return
- }
- next()
- })
- }
- },
- configurePreviewServer(server) {
- return () => {
- server.middlewares.use(async (req, res, next) => {
- try {
- if (req.headers.accept?.includes('text/html')) {
- const html = await fsp.readFile(
- 'dist/client/index.html',
- 'utf-8',
- )
- res.end(html)
- return
- }
- } catch (error) {
- next(error)
- return
- }
- next()
- })
- }
- },
- },
- ]
-}
-
function rscBrowserMode2Plugin(): Plugin[] {
return [
{
From f6fa6ebf112269dd6baad3dd13bdfd19eed1f15a Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Wed, 22 Oct 2025 12:10:01 +0900
Subject: [PATCH 11/22] cleanup
---
.../examples/browser-mode2/vite.config.ts | 102 ++++++++++++------
1 file changed, 71 insertions(+), 31 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index d5ec200dc..a00268463 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -1,47 +1,68 @@
-import { defineConfig, type Plugin } from 'vite'
+import { defineConfig, normalizePath, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import rsc from '@vitejs/plugin-rsc'
+import path from 'node:path'
+import { rmSync } from 'node:fs'
export default defineConfig({
- appType: 'spa',
plugins: [
react(),
+ rscBrowserModePlugin(),
rsc({
- serverHandler: false,
entries: {
rsc: './src/framework/entry.rsc.tsx',
},
}),
- rscBrowserMode2Plugin(),
+ ],
+})
+
+function rscBrowserModePlugin(): Plugin[] {
+ return [
{
- name: 'patch-rsc',
+ name: 'rsc-browser-mode2',
+ config() {
+ return {
+ appType: 'spa',
+ environments: {
+ client: {
+ build: {
+ emptyOutDir: false,
+ },
+ },
+ rsc: {
+ build: {
+ outDir: 'dist/client/__server',
+ },
+ keepProcessEnv: false,
+ resolve: {
+ noExternal: true,
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ platform: 'neutral',
+ },
+ },
+ },
+ },
+ rsc: {
+ serverHandler: false,
+ },
+ }
+ },
configResolved(config) {
+ // avoid globalThis.AsyncLocalStorage injection in browser mode
const plugin = config.plugins.find(
(p) => p.name === 'rsc:inject-async-local-storage',
)
delete plugin!.transform
},
- },
- ],
- environments: {
- rsc: {
- keepProcessEnv: false,
- resolve: {
- noExternal: true,
- },
- optimizeDeps: {
- esbuildOptions: {
- platform: 'neutral',
+ buildApp: {
+ order: 'pre',
+ async handler() {
+ // clean up nested outDir
+ rmSync('./dist', { recursive: true, force: true })
},
},
- },
- },
-})
-
-function rscBrowserMode2Plugin(): Plugin[] {
- return [
- {
- name: 'rsc-browser-mode2',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const url = new URL(req.url ?? '/', 'https://any.local')
@@ -64,17 +85,36 @@ function rscBrowserMode2Plugin(): Plugin[] {
if (this.environment.mode === 'dev') {
return this.resolve('/src/framework/load-rsc-dev')
}
- return '\0' + source
+ return { id: source, external: true }
}
},
- load(id) {
- if (id === '\0virtual:vite-rsc-browser-mode2/load-rsc') {
- // In build mode, return a function that dynamically imports the built RSC module
- return `export default async () => {
- return await import("/dist/rsc/index.js")
- }`
+ renderChunk(code, chunk) {
+ if (code.includes('virtual:vite-rsc-browser-mode2/load-rsc')) {
+ const config = this.environment.getTopLevelConfig()
+ const replacement = normalizeRelativePath(
+ path.relative(
+ path.join(
+ config.environments.client.build.outDir,
+ chunk.fileName,
+ '..',
+ ),
+ path.join(config.environments.rsc.build.outDir, 'index.js'),
+ ),
+ )
+ code = code.replaceAll(
+ 'virtual:vite-rsc-browser-mode2/load-rsc',
+ () => replacement,
+ )
+ return { code }
}
+
+ code
},
},
]
}
+
+function normalizeRelativePath(s: string): string {
+ s = normalizePath(s)
+ return s[0] === '.' ? s : './' + s
+}
From b1aa4706da364b2e3d7d50e5fbb1a584300a5c49 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Wed, 22 Oct 2025 12:16:01 +0900
Subject: [PATCH 12/22] cleanup
---
.../src/framework/async-hooks-polyfill.js | 20 -------------------
1 file changed, 20 deletions(-)
delete mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js b/packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js
deleted file mode 100644
index 15225e9af..000000000
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/async-hooks-polyfill.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Browser polyfill for node:async_hooks
-export class AsyncLocalStorage {
- constructor() {
- this.store = undefined
- }
-
- run(store, callback, ...args) {
- const prev = this.store
- this.store = store
- try {
- return callback(...args)
- } finally {
- this.store = prev
- }
- }
-
- getStore() {
- return this.store
- }
-}
From 487af7f85e44568af04a9ac1da4a7b83f5867f8c Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Wed, 22 Oct 2025 12:17:53 +0900
Subject: [PATCH 13/22] cleanup
---
packages/plugin-rsc/examples/browser-mode2/vite.config.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index a00268463..8fcfd001d 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -107,8 +107,6 @@ function rscBrowserModePlugin(): Plugin[] {
)
return { code }
}
-
- code
},
},
]
From 750090cfba9649e51d9c01fd324d3c2272e134a1 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Wed, 22 Oct 2025 12:19:42 +0900
Subject: [PATCH 14/22] todo
---
packages/plugin-rsc/examples/browser-mode2/vite.config.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index 8fcfd001d..f813e24f1 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -29,6 +29,7 @@ function rscBrowserModePlugin(): Plugin[] {
emptyOutDir: false,
},
},
+ // TODO: server build is not hashed
rsc: {
build: {
outDir: 'dist/client/__server',
From 89022541e4cd916b52de1ea7e613a4115a8ac135 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Wed, 22 Oct 2025 13:24:23 +0900
Subject: [PATCH 15/22] fix: simplify load-rsc virtual
---
.../src/framework/entry.browser.tsx | 9 ++--
.../browser-mode2/src/framework/entry.rsc.tsx | 6 ++-
.../src/framework/load-rsc-dev.tsx | 49 +++++++++++--------
.../browser-mode2/src/framework/main.tsx | 10 +---
.../browser-mode2/src/framework/virtual.d.ts | 4 +-
5 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
index e4d4d7757..cd665d2fa 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
@@ -8,12 +8,9 @@ import {
} from '@vitejs/plugin-rsc/browser'
import type { RscPayload } from './entry.rsc'
-let fetchRsc: (request: Request) => Promise
-
-export function initialize(options: {
- fetchRsc: (request: Request) => Promise
-}) {
- fetchRsc = options.fetchRsc
+async function fetchRsc(request: Request): Promise {
+ const module = await import('virtual:vite-rsc-browser-mode2/load-rsc')
+ return module.default.fetch(request)
}
export async function main() {
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
index 27a5ce931..8d0195b9f 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
@@ -15,7 +15,7 @@ export type RscPayload = {
formState?: ReactFormState
}
-export default async function handler(request: Request): Promise {
+async function handler(request: Request): Promise {
const isAction = request.method === 'POST'
let returnValue: unknown | undefined
let formState: ReactFormState | undefined
@@ -51,6 +51,10 @@ export default async function handler(request: Request): Promise {
})
}
+export default {
+ fetch: handler,
+}
+
if (import.meta.hot) {
import.meta.hot.accept()
}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
index a9af62eae..b5e71909a 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
@@ -1,25 +1,32 @@
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'
-export default async function loadRsc() {
- const runner = new ModuleRunner(
- {
- sourcemapInterceptor: false,
- transport: {
- invoke: async (payload) => {
- const response = await fetch(
- '/@vite/invoke-rsc?' +
- new URLSearchParams({
- data: JSON.stringify(payload),
- }),
- )
- return response.json()
- },
+const runner = new ModuleRunner(
+ {
+ sourcemapInterceptor: false,
+ transport: {
+ invoke: async (payload) => {
+ const response = await fetch(
+ '/@vite/invoke-rsc?' +
+ new URLSearchParams({
+ data: JSON.stringify(payload),
+ }),
+ )
+ return response.json()
},
- hmr: false,
},
- new ESModulesEvaluator(),
- )
- return await runner.import(
- '/src/framework/entry.rsc.tsx',
- )
-}
+ hmr: false,
+ },
+ new ESModulesEvaluator(),
+)
+
+export default new Proxy(
+ {},
+ {
+ get(_target, p, _receiver) {
+ return async (...args: any[]) => {
+ const module = await runner.import('/src/framework/entry.rsc.tsx')
+ return module.default[p](...args)
+ }
+ },
+ },
+)
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
index 38066dcff..8156a4818 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
@@ -1,10 +1,2 @@
-import * as client from './entry.browser'
-import loadRsc from 'virtual:vite-rsc-browser-mode2/load-rsc'
-
-async function main() {
- const rsc = await loadRsc()
- client.initialize({ fetchRsc: rsc.default })
- await client.main()
-}
-
+import { main } from './entry.browser'
main()
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
index 4c61bcad4..f1edb1c05 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
@@ -1,4 +1,4 @@
declare module 'virtual:vite-rsc-browser-mode2/load-rsc' {
- const loadRsc: () => Promise
- export default loadRsc
+ const default_: typeof import('./entry.rsc.tsx').default
+ export default default_
}
From 8c1b793090a2a052a5d933345da35bc0a328e404 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Wed, 22 Oct 2025 13:28:07 +0900
Subject: [PATCH 16/22] test: update
---
packages/plugin-rsc/e2e/browser-mode2.test.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/plugin-rsc/e2e/browser-mode2.test.ts b/packages/plugin-rsc/e2e/browser-mode2.test.ts
index 2b7bb70b9..22ef634fc 100644
--- a/packages/plugin-rsc/e2e/browser-mode2.test.ts
+++ b/packages/plugin-rsc/e2e/browser-mode2.test.ts
@@ -10,12 +10,12 @@ test.skip(({ browserName }) => browserName === 'webkit')
test.describe('dev-browser-mode2', () => {
const f = useFixture({ root: 'examples/browser-mode2', mode: 'dev' })
- defineStarterTest(f, 'browser-mode')
+ defineStarterTest(f, 'no-ssr')
})
test.describe('build-browser-mode2', () => {
const f = useFixture({ root: 'examples/browser-mode2', mode: 'build' })
- defineStarterTest(f, 'browser-mode')
+ defineStarterTest(f, 'no-ssr')
test('no ssr build', () => {
expect(fs.existsSync(path.join(f.root, 'dist/ssr'))).toBe(false)
From 52a903d457337dd8f2a064f18c81740cbfb10dc7 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Thu, 23 Oct 2025 10:27:28 +0900
Subject: [PATCH 17/22] refactor: move code
---
.../examples/browser-mode2/index.html | 2 +-
.../load-rsc-dev.tsx => lib/dev-proxy.ts} | 2 +-
.../examples/browser-mode2/lib/plugin.ts | 105 +++++++++++++++++
.../examples/browser-mode2/lib/runtime.ts | 3 +
.../src/framework/entry.browser.tsx | 7 +-
.../browser-mode2/src/framework/main.tsx | 2 -
.../browser-mode2/src/framework/virtual.d.ts | 4 -
.../examples/browser-mode2/vite.config.ts | 109 +-----------------
8 files changed, 118 insertions(+), 116 deletions(-)
rename packages/plugin-rsc/examples/browser-mode2/{src/framework/load-rsc-dev.tsx => lib/dev-proxy.ts} (98%)
create mode 100644 packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts
create mode 100644 packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts
delete mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
delete mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
diff --git a/packages/plugin-rsc/examples/browser-mode2/index.html b/packages/plugin-rsc/examples/browser-mode2/index.html
index ec960c1a5..f8165f1bb 100644
--- a/packages/plugin-rsc/examples/browser-mode2/index.html
+++ b/packages/plugin-rsc/examples/browser-mode2/index.html
@@ -5,7 +5,7 @@
RSC Browser Mode 2
-
+
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx b/packages/plugin-rsc/examples/browser-mode2/lib/dev-proxy.ts
similarity index 98%
rename from packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
rename to packages/plugin-rsc/examples/browser-mode2/lib/dev-proxy.ts
index b5e71909a..0a08e6995 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-rsc-dev.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/lib/dev-proxy.ts
@@ -24,7 +24,7 @@ export default new Proxy(
{
get(_target, p, _receiver) {
return async (...args: any[]) => {
- const module = await runner.import('/src/framework/entry.rsc.tsx')
+ const module = await runner.import('/src/framework/entry.rsc')
return module.default[p](...args)
}
},
diff --git a/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts b/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts
new file mode 100644
index 000000000..0ac8f6617
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts
@@ -0,0 +1,105 @@
+import { rmSync } from 'node:fs'
+import path from 'node:path'
+import { normalizePath, type Plugin } from 'vite'
+
+export default function vitePluginRscBrowser(): Plugin[] {
+ return [
+ {
+ name: 'rsc-browser-mode2',
+ config() {
+ return {
+ appType: 'spa',
+ environments: {
+ client: {
+ build: {
+ emptyOutDir: false,
+ },
+ },
+ // TODO: server build is not hashed
+ rsc: {
+ build: {
+ outDir: 'dist/client/__server',
+ },
+ keepProcessEnv: false,
+ resolve: {
+ noExternal: true,
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ platform: 'neutral',
+ },
+ },
+ },
+ },
+ rsc: {
+ serverHandler: false,
+ },
+ }
+ },
+ configResolved(config) {
+ // avoid globalThis.AsyncLocalStorage injection in browser mode
+ const plugin = config.plugins.find(
+ (p) => p.name === 'rsc:inject-async-local-storage',
+ )
+ delete plugin!.transform
+ },
+ buildApp: {
+ order: 'pre',
+ async handler() {
+ // clean up nested outDir
+ rmSync('./dist', { recursive: true, force: true })
+ },
+ },
+ configureServer(server) {
+ server.middlewares.use(async (req, res, next) => {
+ const url = new URL(req.url ?? '/', 'https://any.local')
+ if (url.pathname === '/@vite/invoke-rsc') {
+ const payload = JSON.parse(url.searchParams.get('data')!)
+ const result =
+ await server.environments['rsc']!.hot.handleInvoke(payload)
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify(result))
+ return
+ }
+ next()
+ })
+ },
+ },
+ {
+ name: 'rsc-browser-mode2:load-rsc',
+ resolveId(source) {
+ if (source === 'virtual:vite-rsc-browser-mode2/load-rsc') {
+ if (this.environment.mode === 'dev') {
+ return this.resolve('/lib/dev-proxy')
+ }
+ return { id: source, external: true }
+ }
+ },
+ renderChunk(code, chunk) {
+ if (code.includes('virtual:vite-rsc-browser-mode2/load-rsc')) {
+ const config = this.environment.getTopLevelConfig()
+ const replacement = normalizeRelativePath(
+ path.relative(
+ path.join(
+ config.environments.client.build.outDir,
+ chunk.fileName,
+ '..',
+ ),
+ path.join(config.environments.rsc.build.outDir, 'index.js'),
+ ),
+ )
+ code = code.replaceAll(
+ 'virtual:vite-rsc-browser-mode2/load-rsc',
+ () => replacement,
+ )
+ return { code }
+ }
+ },
+ },
+ ]
+}
+
+function normalizeRelativePath(s: string): string {
+ s = normalizePath(s)
+ return s[0] === '.' ? s : './' + s
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts b/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts
new file mode 100644
index 000000000..113539ab1
--- /dev/null
+++ b/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts
@@ -0,0 +1,3 @@
+export function loadEntryRsc() {
+ return import('virtual:vite-rsc-browser-mode2/load-rsc' as any)
+}
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
index cd665d2fa..ee086c8b1 100644
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
+++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
@@ -7,13 +7,14 @@ import {
encodeReply,
} from '@vitejs/plugin-rsc/browser'
import type { RscPayload } from './entry.rsc'
+import { loadEntryRsc } from '../../lib/runtime'
async function fetchRsc(request: Request): Promise {
- const module = await import('virtual:vite-rsc-browser-mode2/load-rsc')
+ const module = await loadEntryRsc()
return module.default.fetch(request)
}
-export async function main() {
+async function main() {
// stash `setPayload` function to trigger re-rendering
// from outside of `BrowserRoot` component (e.g. server function call, navigation, hmr)
let setPayload: (v: RscPayload) => void
@@ -130,3 +131,5 @@ function listenNavigation(onNavigation: () => void) {
window.history.replaceState = oldReplaceState
}
}
+
+main()
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
deleted file mode 100644
index 8156a4818..000000000
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx
+++ /dev/null
@@ -1,2 +0,0 @@
-import { main } from './entry.browser'
-main()
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts b/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
deleted file mode 100644
index f1edb1c05..000000000
--- a/packages/plugin-rsc/examples/browser-mode2/src/framework/virtual.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module 'virtual:vite-rsc-browser-mode2/load-rsc' {
- const default_: typeof import('./entry.rsc.tsx').default
- export default default_
-}
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
index f813e24f1..47e029848 100644
--- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts
@@ -1,13 +1,12 @@
-import { defineConfig, normalizePath, type Plugin } from 'vite'
+import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import rsc from '@vitejs/plugin-rsc'
-import path from 'node:path'
-import { rmSync } from 'node:fs'
+import rscBrowser from './lib/plugin'
export default defineConfig({
plugins: [
react(),
- rscBrowserModePlugin(),
+ rscBrowser(),
rsc({
entries: {
rsc: './src/framework/entry.rsc.tsx',
@@ -15,105 +14,3 @@ export default defineConfig({
}),
],
})
-
-function rscBrowserModePlugin(): Plugin[] {
- return [
- {
- name: 'rsc-browser-mode2',
- config() {
- return {
- appType: 'spa',
- environments: {
- client: {
- build: {
- emptyOutDir: false,
- },
- },
- // TODO: server build is not hashed
- rsc: {
- build: {
- outDir: 'dist/client/__server',
- },
- keepProcessEnv: false,
- resolve: {
- noExternal: true,
- },
- optimizeDeps: {
- esbuildOptions: {
- platform: 'neutral',
- },
- },
- },
- },
- rsc: {
- serverHandler: false,
- },
- }
- },
- configResolved(config) {
- // avoid globalThis.AsyncLocalStorage injection in browser mode
- const plugin = config.plugins.find(
- (p) => p.name === 'rsc:inject-async-local-storage',
- )
- delete plugin!.transform
- },
- buildApp: {
- order: 'pre',
- async handler() {
- // clean up nested outDir
- rmSync('./dist', { recursive: true, force: true })
- },
- },
- configureServer(server) {
- server.middlewares.use(async (req, res, next) => {
- const url = new URL(req.url ?? '/', 'https://any.local')
- if (url.pathname === '/@vite/invoke-rsc') {
- const payload = JSON.parse(url.searchParams.get('data')!)
- const result =
- await server.environments['rsc']!.hot.handleInvoke(payload)
- res.setHeader('Content-Type', 'application/json')
- res.end(JSON.stringify(result))
- return
- }
- next()
- })
- },
- },
- {
- name: 'rsc-browser-mode2:load-rsc',
- resolveId(source) {
- if (source === 'virtual:vite-rsc-browser-mode2/load-rsc') {
- if (this.environment.mode === 'dev') {
- return this.resolve('/src/framework/load-rsc-dev')
- }
- return { id: source, external: true }
- }
- },
- renderChunk(code, chunk) {
- if (code.includes('virtual:vite-rsc-browser-mode2/load-rsc')) {
- const config = this.environment.getTopLevelConfig()
- const replacement = normalizeRelativePath(
- path.relative(
- path.join(
- config.environments.client.build.outDir,
- chunk.fileName,
- '..',
- ),
- path.join(config.environments.rsc.build.outDir, 'index.js'),
- ),
- )
- code = code.replaceAll(
- 'virtual:vite-rsc-browser-mode2/load-rsc',
- () => replacement,
- )
- return { code }
- }
- },
- },
- ]
-}
-
-function normalizeRelativePath(s: string): string {
- s = normalizePath(s)
- return s[0] === '.' ? s : './' + s
-}
From 9a50768d8a25b67f1c6f6b80140c2de5948e5831 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Thu, 23 Oct 2025 10:38:01 +0900
Subject: [PATCH 18/22] cleanup
---
.../plugin-rsc/examples/browser-mode2/lib/plugin.ts | 10 +++++-----
.../plugin-rsc/examples/browser-mode2/lib/runtime.ts | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts b/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts
index 0ac8f6617..c12582c01 100644
--- a/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts
@@ -5,7 +5,7 @@ import { normalizePath, type Plugin } from 'vite'
export default function vitePluginRscBrowser(): Plugin[] {
return [
{
- name: 'rsc-browser-mode2',
+ name: 'rsc-browser',
config() {
return {
appType: 'spa',
@@ -66,9 +66,9 @@ export default function vitePluginRscBrowser(): Plugin[] {
},
},
{
- name: 'rsc-browser-mode2:load-rsc',
+ name: 'rsc-browser:load-rsc',
resolveId(source) {
- if (source === 'virtual:vite-rsc-browser-mode2/load-rsc') {
+ if (source === 'virtual:vite-rsc-browser/load-rsc') {
if (this.environment.mode === 'dev') {
return this.resolve('/lib/dev-proxy')
}
@@ -76,7 +76,7 @@ export default function vitePluginRscBrowser(): Plugin[] {
}
},
renderChunk(code, chunk) {
- if (code.includes('virtual:vite-rsc-browser-mode2/load-rsc')) {
+ if (code.includes('virtual:vite-rsc-browser/load-rsc')) {
const config = this.environment.getTopLevelConfig()
const replacement = normalizeRelativePath(
path.relative(
@@ -89,7 +89,7 @@ export default function vitePluginRscBrowser(): Plugin[] {
),
)
code = code.replaceAll(
- 'virtual:vite-rsc-browser-mode2/load-rsc',
+ 'virtual:vite-rsc-browser/load-rsc',
() => replacement,
)
return { code }
diff --git a/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts b/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts
index 113539ab1..5c505946b 100644
--- a/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts
+++ b/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts
@@ -1,3 +1,3 @@
export function loadEntryRsc() {
- return import('virtual:vite-rsc-browser-mode2/load-rsc' as any)
+ return import('virtual:vite-rsc-browser/load-rsc' as any)
}
From 32e8e2c7b1241ea65f14e337e0c9f61930a87701 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Thu, 23 Oct 2025 10:39:14 +0900
Subject: [PATCH 19/22] chore: rename
---
.../e2e/{browser-mode2.test.ts => browser.test.ts} | 8 ++++----
.../examples/{browser-mode2 => browser}/README.md | 0
.../examples/{browser-mode2 => browser}/index.html | 0
.../examples/{browser-mode2 => browser}/lib/dev-proxy.ts | 0
.../examples/{browser-mode2 => browser}/lib/plugin.ts | 0
.../examples/{browser-mode2 => browser}/lib/runtime.ts | 0
.../examples/{browser-mode2 => browser}/package.json | 2 +-
.../examples/{browser-mode2 => browser}/public/vite.svg | 0
.../examples/{browser-mode2 => browser}/src/action.tsx | 0
.../{browser-mode2 => browser}/src/assets/react.svg | 0
.../examples/{browser-mode2 => browser}/src/client.tsx | 0
.../src/framework/entry.browser.tsx | 0
.../src/framework/entry.rsc.tsx | 0
.../examples/{browser-mode2 => browser}/src/index.css | 0
.../examples/{browser-mode2 => browser}/src/root.tsx | 0
.../examples/{browser-mode2 => browser}/tsconfig.json | 0
.../examples/{browser-mode2 => browser}/vite.config.ts | 0
pnpm-lock.yaml | 4 ++--
18 files changed, 7 insertions(+), 7 deletions(-)
rename packages/plugin-rsc/e2e/{browser-mode2.test.ts => browser.test.ts} (68%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/README.md (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/index.html (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/lib/dev-proxy.ts (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/lib/plugin.ts (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/lib/runtime.ts (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/package.json (89%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/public/vite.svg (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/src/action.tsx (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/src/assets/react.svg (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/src/client.tsx (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/src/framework/entry.browser.tsx (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/src/framework/entry.rsc.tsx (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/src/index.css (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/src/root.tsx (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/tsconfig.json (100%)
rename packages/plugin-rsc/examples/{browser-mode2 => browser}/vite.config.ts (100%)
diff --git a/packages/plugin-rsc/e2e/browser-mode2.test.ts b/packages/plugin-rsc/e2e/browser.test.ts
similarity index 68%
rename from packages/plugin-rsc/e2e/browser-mode2.test.ts
rename to packages/plugin-rsc/e2e/browser.test.ts
index 22ef634fc..3b5898047 100644
--- a/packages/plugin-rsc/e2e/browser-mode2.test.ts
+++ b/packages/plugin-rsc/e2e/browser.test.ts
@@ -8,13 +8,13 @@ import fs from 'node:fs'
// > TypeError: ReadableByteStreamController is not implemented
test.skip(({ browserName }) => browserName === 'webkit')
-test.describe('dev-browser-mode2', () => {
- const f = useFixture({ root: 'examples/browser-mode2', mode: 'dev' })
+test.describe('dev-browser', () => {
+ const f = useFixture({ root: 'examples/browser', mode: 'dev' })
defineStarterTest(f, 'no-ssr')
})
-test.describe('build-browser-mode2', () => {
- const f = useFixture({ root: 'examples/browser-mode2', mode: 'build' })
+test.describe('build-browser', () => {
+ const f = useFixture({ root: 'examples/browser', mode: 'build' })
defineStarterTest(f, 'no-ssr')
test('no ssr build', () => {
diff --git a/packages/plugin-rsc/examples/browser-mode2/README.md b/packages/plugin-rsc/examples/browser/README.md
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/README.md
rename to packages/plugin-rsc/examples/browser/README.md
diff --git a/packages/plugin-rsc/examples/browser-mode2/index.html b/packages/plugin-rsc/examples/browser/index.html
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/index.html
rename to packages/plugin-rsc/examples/browser/index.html
diff --git a/packages/plugin-rsc/examples/browser-mode2/lib/dev-proxy.ts b/packages/plugin-rsc/examples/browser/lib/dev-proxy.ts
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/lib/dev-proxy.ts
rename to packages/plugin-rsc/examples/browser/lib/dev-proxy.ts
diff --git a/packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts b/packages/plugin-rsc/examples/browser/lib/plugin.ts
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/lib/plugin.ts
rename to packages/plugin-rsc/examples/browser/lib/plugin.ts
diff --git a/packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts b/packages/plugin-rsc/examples/browser/lib/runtime.ts
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/lib/runtime.ts
rename to packages/plugin-rsc/examples/browser/lib/runtime.ts
diff --git a/packages/plugin-rsc/examples/browser-mode2/package.json b/packages/plugin-rsc/examples/browser/package.json
similarity index 89%
rename from packages/plugin-rsc/examples/browser-mode2/package.json
rename to packages/plugin-rsc/examples/browser/package.json
index 9d9e3fa38..17c85382b 100644
--- a/packages/plugin-rsc/examples/browser-mode2/package.json
+++ b/packages/plugin-rsc/examples/browser/package.json
@@ -1,5 +1,5 @@
{
- "name": "@vitejs/plugin-rsc-examples-browser-mode2",
+ "name": "@vitejs/plugin-rsc-examples-browser",
"version": "0.0.0",
"private": true,
"license": "MIT",
diff --git a/packages/plugin-rsc/examples/browser-mode2/public/vite.svg b/packages/plugin-rsc/examples/browser/public/vite.svg
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/public/vite.svg
rename to packages/plugin-rsc/examples/browser/public/vite.svg
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/action.tsx b/packages/plugin-rsc/examples/browser/src/action.tsx
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/src/action.tsx
rename to packages/plugin-rsc/examples/browser/src/action.tsx
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/assets/react.svg b/packages/plugin-rsc/examples/browser/src/assets/react.svg
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/src/assets/react.svg
rename to packages/plugin-rsc/examples/browser/src/assets/react.svg
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/client.tsx b/packages/plugin-rsc/examples/browser/src/client.tsx
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/src/client.tsx
rename to packages/plugin-rsc/examples/browser/src/client.tsx
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser/src/framework/entry.browser.tsx
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx
rename to packages/plugin-rsc/examples/browser/src/framework/entry.browser.tsx
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser/src/framework/entry.rsc.tsx
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/src/framework/entry.rsc.tsx
rename to packages/plugin-rsc/examples/browser/src/framework/entry.rsc.tsx
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/index.css b/packages/plugin-rsc/examples/browser/src/index.css
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/src/index.css
rename to packages/plugin-rsc/examples/browser/src/index.css
diff --git a/packages/plugin-rsc/examples/browser-mode2/src/root.tsx b/packages/plugin-rsc/examples/browser/src/root.tsx
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/src/root.tsx
rename to packages/plugin-rsc/examples/browser/src/root.tsx
diff --git a/packages/plugin-rsc/examples/browser-mode2/tsconfig.json b/packages/plugin-rsc/examples/browser/tsconfig.json
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/tsconfig.json
rename to packages/plugin-rsc/examples/browser/tsconfig.json
diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser/vite.config.ts
similarity index 100%
rename from packages/plugin-rsc/examples/browser-mode2/vite.config.ts
rename to packages/plugin-rsc/examples/browser/vite.config.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 85e08fc1c..886392989 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -565,7 +565,7 @@ importers:
specifier: ^4.43.0
version: 4.43.0
- packages/plugin-rsc/examples/browser-mode:
+ packages/plugin-rsc/examples/browser:
dependencies:
react:
specifier: ^19.2.0
@@ -590,7 +590,7 @@ importers:
specifier: ^7.1.10
version: 7.1.10(@types/node@22.18.11)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)
- packages/plugin-rsc/examples/browser-mode2:
+ packages/plugin-rsc/examples/browser-mode:
dependencies:
react:
specifier: ^19.2.0
From 9f7bf37071bba6b243693dc9d388ebb069157416 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Thu, 23 Oct 2025 10:42:22 +0900
Subject: [PATCH 20/22] readme
---
packages/plugin-rsc/examples/browser/README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/plugin-rsc/examples/browser/README.md b/packages/plugin-rsc/examples/browser/README.md
index da89f3279..ed0d20f30 100644
--- a/packages/plugin-rsc/examples/browser/README.md
+++ b/packages/plugin-rsc/examples/browser/README.md
@@ -1,4 +1,4 @@
-# browser-mode2
+# `rsc` environment on browser
Hybrid RSC example that combines:
From 70821ad8bee88fcd1bc7b9dd2f0905efd6e09017 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Thu, 23 Oct 2025 10:43:46 +0900
Subject: [PATCH 21/22] tweak
---
packages/plugin-rsc/examples/browser/index.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/plugin-rsc/examples/browser/index.html b/packages/plugin-rsc/examples/browser/index.html
index f8165f1bb..590485e64 100644
--- a/packages/plugin-rsc/examples/browser/index.html
+++ b/packages/plugin-rsc/examples/browser/index.html
@@ -2,7 +2,7 @@
- RSC Browser Mode 2
+ RSC on Browser
From 27d43ee07af4c9c2ac8dc761dce2e2dc455cb2d6 Mon Sep 17 00:00:00 2001
From: Hiroshi Ogawa
Date: Thu, 23 Oct 2025 11:06:43 +0900
Subject: [PATCH 22/22] cleanup
---
.../plugin-rsc/examples/browser/README.md | 46 +------------------
1 file changed, 1 insertion(+), 45 deletions(-)
diff --git a/packages/plugin-rsc/examples/browser/README.md b/packages/plugin-rsc/examples/browser/README.md
index ed0d20f30..84f3c673e 100644
--- a/packages/plugin-rsc/examples/browser/README.md
+++ b/packages/plugin-rsc/examples/browser/README.md
@@ -1,47 +1,3 @@
# `rsc` environment on browser
-Hybrid RSC example that combines:
-
-- The `rsc` environment from [examples/no-ssr](../no-ssr) (simple RSC setup with `entry.rsc.tsx`)
-- The module runner approach from [examples/browser-mode](../browser-mode) (runs RSC on browser via module runner)
-
-This example demonstrates how to run React Server Components entirely in the browser using Vite's module runner API, without requiring a Node.js server environment. The RSC rendering logic that would normally run on the server is executed in the browser through the module runner.
-
-## Key Features
-
-- **No Node.js server required**: The RSC environment runs in the browser
-- **Module Runner**: Uses Vite's module runner to load and execute the RSC environment in the browser
-- **HMR Support**: Hot module replacement for both client and RSC code
-- **Server Actions**: Full support for server actions, executed in the browser context
-
-## How it works
-
-1. The main entry point (`src/framework/main.tsx`) loads both the RSC and client environments
-2. In dev mode, the RSC environment is loaded via module runner (`load-rsc-dev.tsx`)
-3. The client environment consumes the RSC output through the standard RSC protocol
-4. Server actions are handled by the RSC environment running in the browser
-
-## Architecture
-
-```
-Browser Context
-├── Client Environment (src/framework/entry.browser.tsx)
-│ ├── React Client Components ('use client')
-│ └── RSC Consumer (renders server components)
-└── RSC Environment (via Module Runner)
- ├── Server Components (src/framework/entry.rsc.tsx)
- └── Server Actions (src/action.tsx)
-```
-
-## Development
-
-```bash
-pnpm dev
-```
-
-## Build
-
-```bash
-pnpm build
-pnpm preview
-```
+See also https://github.com/hi-ogawa/vite-rsc-browser-example/