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/