From 251fe27544028219dbe3d5c26b526eda4e98719e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:43:29 +0000 Subject: [PATCH 1/6] Initial plan From c473a8745ed0c16b18293e634776abaff911e2af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 12:50:56 +0000 Subject: [PATCH 2/6] Add browser-mode2 example with basic structure Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- .../examples/browser-mode2/README.md | 1 + .../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 | 59 ++++++++ .../browser-mode2/src/framework/entry.rsc.tsx | 56 +++++++ .../src/framework/load-client-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 | 139 ++++++++++++++++++ pnpm-lock.yaml | 25 ++++ 17 files changed, 555 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-client-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..83e723d1a --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode2/README.md @@ -0,0 +1 @@ +[examples/no-ssr](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc/examples/no-ssr) with `rsc` environment running on browser via module runner 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..28349d072 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/entry.browser.tsx @@ -0,0 +1,59 @@ +import * as 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 fetchServer: typeof import('./entry.rsc').fetchServer + +export function initialize(options: { fetchServer: typeof fetchServer }) { + fetchServer = options.fetchServer +} + +export async function main() { + let setPayload: (v: RscPayload) => void + + const initialPayload = await createFromFetch( + fetchServer(new Request(window.location.href)), + ) + + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)) + }, [setPayload_]) + + return payload.root + } + + setServerCallback(async (id, args) => { + const url = new URL(window.location.href) + const temporaryReferences = createTemporaryReferenceSet() + const payload = await createFromFetch( + fetchServer( + new Request(url, { + method: 'POST', + body: await encodeReply(args, { temporaryReferences }), + headers: { + 'x-rsc-action': id, + }, + }), + ), + { temporaryReferences }, + ) + setPayload(payload) + return payload.returnValue + }) + + const browserRoot = ( + + + + ) + createRoot(document.body).render(browserRoot) +} 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..cef4e5871 --- /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 async function fetchServer(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-client-dev.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-client-dev.tsx new file mode 100644 index 000000000..8ca40ae43 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-client-dev.tsx @@ -0,0 +1,25 @@ +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' + +export default async function loadClient() { + const runner = new ModuleRunner( + { + sourcemapInterceptor: false, + transport: { + invoke: async (payload) => { + const response = await fetch( + '/@vite/invoke-react-client?' + + new URLSearchParams({ + data: JSON.stringify(payload), + }), + ) + return response.json() + }, + }, + hmr: false, + }, + new ESModulesEvaluator(), + ) + return await runner.import( + '/src/framework/entry.browser.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..152061827 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx @@ -0,0 +1,10 @@ +import * as server from './entry.rsc' +import loadClient from 'virtual:vite-rsc-browser-mode2/load-client' + +async function main() { + const client = await loadClient() + client.initialize({ fetchServer: server.fetchServer }) + 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..2f251e6fe --- /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-client' { + const loadClient: () => Promise + export default loadClient +} 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..e8d912527 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode2/src/root.tsx @@ -0,0 +1,44 @@ +import './index.css' +import viteLogo from '/vite.svg' +import { getServerCounter, updateServerCounter } from './action.tsx' +import reactLogo from './assets/react.svg' +import { ClientCounter } from './client.tsx' +import { TestUseActionState } from './action-from-client/client.tsx' +import { TestActionBind } from './action-bind/server.tsx' + +export function Root() { + return +} + +function App() { + return ( +
+ +

Vite + RSC

+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ ) +} 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..4d22b13ae --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts @@ -0,0 +1,139 @@ +import rsc from '@vitejs/plugin-rsc' +import react from '@vitejs/plugin-react' +import { defaultClientConditions, defineConfig, type Plugin } from 'vite' + +export default defineConfig({ + plugins: [ + react(), + rsc({ + entries: { + rsc: './src/framework/entry.rsc.tsx', + }, + }), + rscBrowserMode2Plugin(), + ], + environments: { + client: { + build: { + minify: false, + }, + }, + }, +}) + +function rscBrowserMode2Plugin(): Plugin[] { + return [ + { + 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: ['react-server', ...defaultClientConditions], + }, + optimizeDeps: { + include: [ + 'react', + 'react-dom', + 'react-dom/client', + '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: ['vite', '@vitejs/plugin-rsc'], + }, + build: { + outDir: 'dist/client', + }, + }, + react_client: { + keepProcessEnv: false, + resolve: { + conditions: [...defaultClientConditions], + noExternal: true, + }, + 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'], + esbuildOptions: { + platform: 'browser', + }, + }, + build: { + outDir: 'dist/react_client', + copyPublicDir: false, + emitAssets: true, + rollupOptions: { + input: { + index: './src/framework/entry.browser.tsx', + }, + }, + }, + }, + }, + builder: { + sharedPlugins: true, + sharedConfigBuild: true, + }, + } + }, + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + const url = new URL(req.url ?? '/', 'https://any.local') + if (url.pathname === '/@vite/invoke-react-client') { + const payload = JSON.parse(url.searchParams.get('data')!) + const result = + await server.environments['react_client']!.hot.handleInvoke( + payload, + ) + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify(result)) + return + } + next() + }) + }, + hotUpdate(ctx) { + if (this.environment.name === 'react_client') { + if (ctx.modules.length > 0) { + ctx.server.environments.client.hot.send({ + type: 'full-reload', + path: ctx.file, + }) + } + } + }, + }, + { + name: 'rsc-browser-mode2:load-client', + resolveId(source) { + if (source === 'virtual:vite-rsc-browser-mode2/load-client') { + if (this.environment.mode === 'dev') { + return this.resolve('/src/framework/load-client-dev') + } + return '\0' + source + } + }, + load(id) { + if (id === '\0virtual:vite-rsc-browser-mode2/load-client') { + return `export default async () => import("/dist/react_client/index.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 aee84e635a0c07602ce7d50477d15a2b938fb3ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:03:59 +0000 Subject: [PATCH 3/6] Work in progress on browser-mode2 example Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- .../browser-mode2/src/framework/entry.rsc.tsx | 17 +++++- .../src/framework/load-server-dev.tsx | 25 +++++++++ .../browser-mode2/src/framework/main.tsx | 4 +- .../browser-mode2/src/framework/virtual.d.ts | 5 ++ .../examples/browser-mode2/src/root.tsx | 18 +++--- .../examples/browser-mode2/vite.config.ts | 55 +++++++++++++++++++ 6 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx 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 cef4e5871..c15f13546 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 @@ -5,7 +5,8 @@ import { loadServerAction, decodeAction, decodeFormState, -} from '@vitejs/plugin-rsc/rsc' + setRequireModule, +} from '@vitejs/plugin-rsc/react/rsc' import type { ReactFormState } from 'react-dom/client' import { Root } from '../root.tsx' @@ -15,7 +16,14 @@ export type RscPayload = { formState?: ReactFormState } -export async function fetchServer(request: Request): Promise { +// Initialize module loading for RSC +setRequireModule({ + load: async (id) => { + return import(/* @vite-ignore */ id) + }, +}) + +async function handler(request: Request): Promise { const isAction = request.method === 'POST' let returnValue: unknown | undefined let formState: ReactFormState | undefined @@ -51,6 +59,11 @@ export async function fetchServer(request: Request): Promise { }) } +export default handler + +// Export named function for browser mode usage +export const fetchServer = handler + if (import.meta.hot) { import.meta.hot.accept() } diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx new file mode 100644 index 000000000..f30fa6017 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx @@ -0,0 +1,25 @@ +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' + +export default async function loadServer() { + 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 index 152061827..d20a2f6d6 100644 --- a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx +++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx @@ -1,8 +1,8 @@ -import * as server from './entry.rsc' import loadClient from 'virtual:vite-rsc-browser-mode2/load-client' +import loadServer from 'virtual:vite-rsc-browser-mode2/load-server' async function main() { - const client = await loadClient() + const [client, server] = await Promise.all([loadClient(), loadServer()]) client.initialize({ fetchServer: server.fetchServer }) 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 2f251e6fe..bbd03f226 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,8 @@ declare module 'virtual:vite-rsc-browser-mode2/load-client' { const loadClient: () => Promise export default loadClient } + +declare module 'virtual:vite-rsc-browser-mode2/load-server' { + const loadServer: () => Promise + export default loadServer +} diff --git a/packages/plugin-rsc/examples/browser-mode2/src/root.tsx b/packages/plugin-rsc/examples/browser-mode2/src/root.tsx index e8d912527..9baa7b9c2 100644 --- a/packages/plugin-rsc/examples/browser-mode2/src/root.tsx +++ b/packages/plugin-rsc/examples/browser-mode2/src/root.tsx @@ -1,10 +1,8 @@ -import './index.css' +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' -import { TestUseActionState } from './action-from-client/client.tsx' -import { TestActionBind } from './action-bind/server.tsx' export function Root() { return @@ -33,12 +31,14 @@ function App() { -
- -
-
- -
+
    +
  • + 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/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts index 4d22b13ae..985f8788f 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 rsc from '@vitejs/plugin-rsc' import react from '@vitejs/plugin-react' import { defaultClientConditions, defineConfig, type Plugin } from 'vite' +import fsp from 'node:fs/promises' export default defineConfig({ plugins: [ + spaPlugin(), react(), rsc({ entries: { @@ -21,6 +23,35 @@ export default defineConfig({ }, }) +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() + }) + } + }, + }, + ] +} + function rscBrowserMode2Plugin(): Plugin[] { return [ { @@ -105,6 +136,14 @@ function rscBrowserMode2Plugin(): Plugin[] { res.end(JSON.stringify(result)) return } + 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() }) }, @@ -135,5 +174,21 @@ function rscBrowserMode2Plugin(): Plugin[] { } }, }, + { + name: 'rsc-browser-mode2:load-server', + resolveId(source) { + if (source === 'virtual:vite-rsc-browser-mode2/load-server') { + if (this.environment.mode === 'dev') { + return this.resolve('/src/framework/load-server-dev') + } + return '\0' + source + } + }, + load(id) { + if (id === '\0virtual:vite-rsc-browser-mode2/load-server') { + return `export default async () => import("/dist/rsc/entry.js")` + } + }, + }, ] } From e973e90241c270e21d12b10f870057ae09d6e75e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:09:44 +0000 Subject: [PATCH 4/6] Simplify browser-mode2 approach to match browser-mode pattern Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- .../browser-mode2/src/framework/entry.rsc.tsx | 21 ++++++++-------- .../src/framework/load-server-dev.tsx | 25 ------------------- .../browser-mode2/src/framework/main.tsx | 5 ++-- .../browser-mode2/src/framework/virtual.d.ts | 5 ---- .../examples/browser-mode2/vite.config.ts | 24 ------------------ 5 files changed, 13 insertions(+), 67 deletions(-) delete mode 100644 packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx 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 c15f13546..1820c6e66 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 @@ -16,14 +16,15 @@ export type RscPayload = { formState?: ReactFormState } -// Initialize module loading for RSC -setRequireModule({ - load: async (id) => { - return import(/* @vite-ignore */ id) - }, -}) +export function initialize() { + setRequireModule({ + load: async (id) => { + return import(/* @vite-ignore */ id) + }, + }) +} -async function handler(request: Request): Promise { +export async function fetchServer(request: Request): Promise { const isAction = request.method === 'POST' let returnValue: unknown | undefined let formState: ReactFormState | undefined @@ -59,10 +60,8 @@ async function handler(request: Request): Promise { }) } -export default handler - -// Export named function for browser mode usage -export const fetchServer = handler +// Default export for no-ssr mode compatibility +export default fetchServer if (import.meta.hot) { import.meta.hot.accept() diff --git a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx b/packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx deleted file mode 100644 index f30fa6017..000000000 --- a/packages/plugin-rsc/examples/browser-mode2/src/framework/load-server-dev.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' - -export default async function loadServer() { - 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 index d20a2f6d6..050ed1d08 100644 --- a/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx +++ b/packages/plugin-rsc/examples/browser-mode2/src/framework/main.tsx @@ -1,8 +1,9 @@ +import * as server from './entry.rsc' import loadClient from 'virtual:vite-rsc-browser-mode2/load-client' -import loadServer from 'virtual:vite-rsc-browser-mode2/load-server' async function main() { - const [client, server] = await Promise.all([loadClient(), loadServer()]) + const client = await loadClient() + server.initialize() client.initialize({ fetchServer: server.fetchServer }) 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 bbd03f226..2f251e6fe 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,8 +2,3 @@ declare module 'virtual:vite-rsc-browser-mode2/load-client' { const loadClient: () => Promise export default loadClient } - -declare module 'virtual:vite-rsc-browser-mode2/load-server' { - const loadServer: () => Promise - export default loadServer -} diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts index 985f8788f..55ea0f2b6 100644 --- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts @@ -136,14 +136,6 @@ function rscBrowserMode2Plugin(): Plugin[] { res.end(JSON.stringify(result)) return } - 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() }) }, @@ -174,21 +166,5 @@ function rscBrowserMode2Plugin(): Plugin[] { } }, }, - { - name: 'rsc-browser-mode2:load-server', - resolveId(source) { - if (source === 'virtual:vite-rsc-browser-mode2/load-server') { - if (this.environment.mode === 'dev') { - return this.resolve('/src/framework/load-server-dev') - } - return '\0' + source - } - }, - load(id) { - if (id === '\0virtual:vite-rsc-browser-mode2/load-server') { - return `export default async () => import("/dist/rsc/entry.js")` - } - }, - }, ] } From 2d6c3a01c1e1232bb8042729043dad49ac089255 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:17:54 +0000 Subject: [PATCH 5/6] Add browser-mode2 example - WIP with known issues Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- .../src/framework/entry.browser.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 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 28349d072..0e5075bcb 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 @@ -17,20 +17,7 @@ export function initialize(options: { fetchServer: typeof fetchServer }) { export async function main() { let setPayload: (v: RscPayload) => void - const initialPayload = await createFromFetch( - fetchServer(new Request(window.location.href)), - ) - - function BrowserRoot() { - const [payload, setPayload_] = React.useState(initialPayload) - - React.useEffect(() => { - setPayload = (v) => React.startTransition(() => setPayload_(v)) - }, [setPayload_]) - - return payload.root - } - + // Set server callback BEFORE processing the initial payload setServerCallback(async (id, args) => { const url = new URL(window.location.href) const temporaryReferences = createTemporaryReferenceSet() @@ -50,6 +37,20 @@ export async function main() { return payload.returnValue }) + const initialPayload = await createFromFetch( + fetchServer(new Request(window.location.href)), + ) + + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + + React.useEffect(() => { + setPayload = (v) => React.startTransition(() => setPayload_(v)) + }, [setPayload_]) + + return payload.root + } + const browserRoot = ( From c0f1aaa37f329645e3719620f30f4eb28775e827 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:29:21 +0000 Subject: [PATCH 6/6] Simplify rscBrowserMode2Plugin as suggested in review Co-authored-by: hi-ogawa <4232207+hi-ogawa@users.noreply.github.com> --- .../examples/browser-mode2/vite.config.ts | 48 +------------------ 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts index 55ea0f2b6..5123022b1 100644 --- a/packages/plugin-rsc/examples/browser-mode2/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode2/vite.config.ts @@ -56,55 +56,13 @@ function rscBrowserMode2Plugin(): Plugin[] { return [ { name: 'rsc-browser-mode2', - config(userConfig, env) { + config() { return { - define: { - 'import.meta.env.__vite_rsc_build__': JSON.stringify( - env.command === 'build', - ), - }, environments: { - client: { - keepProcessEnv: false, - resolve: { - conditions: ['react-server', ...defaultClientConditions], - }, - optimizeDeps: { - include: [ - 'react', - 'react-dom', - 'react-dom/client', - '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: ['vite', '@vitejs/plugin-rsc'], - }, - build: { - outDir: 'dist/client', - }, - }, react_client: { - keepProcessEnv: false, resolve: { - conditions: [...defaultClientConditions], noExternal: true, }, - 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'], - esbuildOptions: { - platform: 'browser', - }, - }, build: { outDir: 'dist/react_client', copyPublicDir: false, @@ -117,10 +75,6 @@ function rscBrowserMode2Plugin(): Plugin[] { }, }, }, - builder: { - sharedPlugins: true, - sharedConfigBuild: true, - }, } }, configureServer(server) {