From 95eaa8b13dfa3d15e774692228b19d72b4ea4d55 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 10:07:07 +0900 Subject: [PATCH 01/43] feat(rsc): support customizing `react-server` conditioned environment --- packages/plugin-rsc/src/plugin.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index a8b946ea2..cfed1d9f7 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -131,6 +131,15 @@ export type RscPluginOptions = { * @default false */ useBuildAppHook?: boolean + + /** + * This configuration allows configuring `react-server` conditioned environment. + * @experimental + * @default { server: ['rsc'] } + */ + environment?: { + server?: string[] + } } export default function vitePluginRsc( From 21183ba2d0b199698cac48deac4c7a811bdfb9d3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 10:08:53 +0900 Subject: [PATCH 02/43] chore: comment --- packages/plugin-rsc/src/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index cfed1d9f7..f9d74b17d 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -133,7 +133,7 @@ export type RscPluginOptions = { useBuildAppHook?: boolean /** - * This configuration allows configuring `react-server` conditioned environment. + * This allows configuring `react-server` condition environment. * @experimental * @default { server: ['rsc'] } */ From 32d4756fc07080e3cabf4f46272e6712c6df6616 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 10:52:02 +0900 Subject: [PATCH 03/43] wip: vitePluginUseClient --- packages/plugin-rsc/src/plugin.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index f9d74b17d..b030f9044 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -945,10 +945,10 @@ function normalizeReferenceId(id: string, name: 'client' | 'rsc') { return normalizeViteImportAnalysisUrl(environment, id) } -function vitePluginUseClient( +export function vitePluginUseClient( useClientPluginOptions: Pick< RscPluginOptions, - 'ignoredPackageWarnings' | 'keepUseCientProxy' + 'ignoredPackageWarnings' | 'keepUseCientProxy' | 'environment' >, ): Plugin[] { const packageSources = new Map() @@ -956,11 +956,16 @@ function vitePluginUseClient( // https://github.com/vitejs/vite/blob/4bcf45863b5f46aa2b41f261283d08f12d3e8675/packages/vite/src/node/utils.ts#L175 const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/ + const serverEnvironments = useClientPluginOptions.environment?.server ?? [ + 'rsc', + ] + const isServer = (name: string) => serverEnvironments.includes(name) + return [ { name: 'rsc:use-client', async transform(code, id) { - if (this.environment.name !== 'rsc') return + if (!isServer(this.environment.name)) return if (!code.includes('use client')) return const ast = await parseAstAsync(code) @@ -1082,7 +1087,7 @@ function vitePluginUseClient( id.startsWith('\0virtual:vite-rsc/client-in-server-package-proxy/') ) { assert.equal(this.environment.mode, 'dev') - assert.notEqual(this.environment.name, 'rsc') + assert(!isServer(this.environment.name)) id = decodeURIComponent( id.slice( '\0virtual:vite-rsc/client-in-server-package-proxy/'.length, @@ -1102,7 +1107,7 @@ function vitePluginUseClient( resolveId: { order: 'pre', async handler(source, importer, options) { - if (this.environment.name === 'rsc' && bareImportRE.test(source)) { + if (isServer(this.environment.name) && bareImportRE.test(source)) { const resolved = await this.resolve(source, importer, options) if (resolved && resolved.id.includes('/node_modules/')) { packageSources.set(resolved.id, source) @@ -1127,7 +1132,7 @@ function vitePluginUseClient( } }, generateBundle(_options, bundle) { - if (this.environment.name !== 'rsc') return + if (!isServer(this.environment.name)) return // track used exports of client references in rsc build // to tree shake unused exports in browser and ssr build From 2e36de8064a368a054a2bfbfe241238eec914c4e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 11:06:51 +0900 Subject: [PATCH 04/43] wip vitePluginUseServer --- packages/plugin-rsc/src/plugin.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index b030f9044..59c9b5643 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1210,12 +1210,17 @@ function vitePluginDefineEncryptionKey( ] } -function vitePluginUseServer( +export function vitePluginUseServer( useServerPluginOptions: Pick< RscPluginOptions, - 'ignoredPackageWarnings' | 'enableActionEncryption' + 'ignoredPackageWarnings' | 'enableActionEncryption' | 'environment' >, ): Plugin[] { + const serverEnvironments = useServerPluginOptions.environment?.server ?? [ + 'rsc', + ] + const isServer = (name: string) => serverEnvironments.includes(name) + return [ { name: 'rsc:use-server', @@ -1249,7 +1254,7 @@ function vitePluginUseServer( return normalizedId_ } - if (this.environment.name === 'rsc') { + if (isServer(this.environment.name)) { const transformServerActionServer_ = withRollupError( this, transformServerActionServer, From f480f3f738fad2660998ffc96395c727d36a5d4a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 11:22:11 +0900 Subject: [PATCH 05/43] feat: expose more --- packages/plugin-rsc/src/plugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 59c9b5643..32aeb77bd 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -945,6 +945,7 @@ function normalizeReferenceId(id: string, name: 'client' | 'rsc') { return normalizeViteImportAnalysisUrl(environment, id) } +/** @experimental */ export function vitePluginUseClient( useClientPluginOptions: Pick< RscPluginOptions, @@ -1151,7 +1152,8 @@ export function vitePluginUseClient( ] } -function vitePluginDefineEncryptionKey( +/** @experimental */ +export function vitePluginDefineEncryptionKey( useServerPluginOptions: Pick, ): Plugin[] { let defineEncryptionKey: string @@ -1210,6 +1212,7 @@ function vitePluginDefineEncryptionKey( ] } +/** @experimental */ export function vitePluginUseServer( useServerPluginOptions: Pick< RscPluginOptions, From fa6353dd4107b0b482e0a8a5b82133716acb5910 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 11:25:30 +0900 Subject: [PATCH 06/43] feat: expose more --- packages/plugin-rsc/src/core/plugin.ts | 1 + packages/plugin-rsc/src/plugin.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/plugin-rsc/src/core/plugin.ts b/packages/plugin-rsc/src/core/plugin.ts index d22d2ab0a..a13002379 100644 --- a/packages/plugin-rsc/src/core/plugin.ts +++ b/packages/plugin-rsc/src/core/plugin.ts @@ -1,5 +1,6 @@ import type { Plugin } from 'vite' +/** @experimental */ export default function vitePluginRscCore(): Plugin[] { return [ { diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 32aeb77bd..2868d363a 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -70,6 +70,8 @@ function resolvePackage(name: string) { return pathToFileURL(require.resolve(name)).href } +export { vitePluginRscCore } + export type RscPluginOptions = { /** * shorthand for configuring `environments.(name).build.rollupOptions.input.index` From 795f9fd7257310c5c234e9b46527f3c179ec09f8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 11:27:14 +0900 Subject: [PATCH 07/43] chore: tweak --- packages/plugin-rsc/src/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 2868d363a..141d0045f 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1156,7 +1156,7 @@ export function vitePluginUseClient( /** @experimental */ export function vitePluginDefineEncryptionKey( - useServerPluginOptions: Pick, + useServerPluginOptions?: Pick, ): Plugin[] { let defineEncryptionKey: string let emitEncryptionKey = false @@ -1169,7 +1169,7 @@ export function vitePluginDefineEncryptionKey( async configEnvironment(name, _config, env) { if (name === 'rsc' && !env.isPreview) { defineEncryptionKey = - useServerPluginOptions.defineEncryptionKey ?? + useServerPluginOptions?.defineEncryptionKey ?? JSON.stringify(toBase64(await generateEncryptionKey())) } }, From 21c8cd663d23cf656abb8b20f021c34bef7b92ee Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 11:45:01 +0900 Subject: [PATCH 08/43] feat: add vitePluginRscMinimal --- packages/plugin-rsc/src/core/plugin.ts | 1 - packages/plugin-rsc/src/plugin.ts | 31 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/plugin-rsc/src/core/plugin.ts b/packages/plugin-rsc/src/core/plugin.ts index a13002379..d22d2ab0a 100644 --- a/packages/plugin-rsc/src/core/plugin.ts +++ b/packages/plugin-rsc/src/core/plugin.ts @@ -1,6 +1,5 @@ import type { Plugin } from 'vite' -/** @experimental */ export default function vitePluginRscCore(): Plugin[] { return [ { diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 141d0045f..f31d9aae2 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -70,8 +70,6 @@ function resolvePackage(name: string) { return pathToFileURL(require.resolve(name)).href } -export { vitePluginRscCore } - export type RscPluginOptions = { /** * shorthand for configuring `environments.(name).build.rollupOptions.input.index` @@ -144,6 +142,25 @@ export type RscPluginOptions = { } } +/** @experimental */ +export function vitePluginRscMinimal(): Plugin[] { + return [ + { + name: 'rsc:minimal', + async config() { + await esModuleLexer.init + }, + configResolved(config_) { + config = config_ + }, + configureServer(server_) { + server = server_ + }, + }, + ...vitePluginRscCore(), + ] +} + export default function vitePluginRsc( rscPluginOptions: RscPluginOptions = {}, ): Plugin[] { @@ -194,11 +211,10 @@ export default function vitePluginRsc( } return [ + ...vitePluginRscMinimal(), { name: 'rsc', async config(config, env) { - await esModuleLexer.init - // crawl packages with "react" in "peerDependencies" to bundle react deps on server // see https://github.com/svitejs/vitefu/blob/d8d82fa121e3b2215ba437107093c77bde51b63b/src/index.js#L95-L101 const result = await crawlFrameworkPkgs({ @@ -306,11 +322,7 @@ export default function vitePluginRsc( } }, buildApp: rscPluginOptions.useBuildAppHook ? buildApp : undefined, - configResolved(config_) { - config = config_ - }, - configureServer(server_) { - server = server_ + configureServer() { ;(globalThis as any).__viteRscDevServer = server if (rscPluginOptions.disableServerHandler) return @@ -849,7 +861,6 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; return '' }, }, - ...vitePluginRscCore(), ...vitePluginUseClient(rscPluginOptions), ...vitePluginUseServer(rscPluginOptions), ...vitePluginDefineEncryptionKey(rscPluginOptions), From bb392646ef13a4a33a1111960579492a7e9068e6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 11:52:31 +0900 Subject: [PATCH 09/43] fix: replace more --- packages/plugin-rsc/src/plugin.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index f31d9aae2..fef3f220f 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -947,7 +947,10 @@ function hashString(v: string) { return createHash('sha256').update(v).digest().toString('hex').slice(0, 12) } -function normalizeReferenceId(id: string, name: 'client' | 'rsc') { +function normalizeReferenceId( + id: string, + name: 'client' | 'rsc' | (string & {}), +) { if (!server) { return hashString(path.relative(config.root, id)) } @@ -1022,6 +1025,7 @@ export function vitePluginUseClient( } else { if (this.environment.mode === 'dev') { importId = normalizeViteImportAnalysisUrl( + // TODO server.environments.client, id, ) @@ -1265,7 +1269,8 @@ export function vitePluginUseServer( // module identity of `import(id)` like browser, so we simply strip it off. id = id.split('?v=')[0]! } - normalizedId_ = normalizeReferenceId(id, 'rsc') + // TODO + normalizedId_ = normalizeReferenceId(id, serverEnvironments[0]!) } return normalizedId_ } From ea048d2ffac783f1b2bf7727e82ecdf92dacfefd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 14:00:05 +0900 Subject: [PATCH 10/43] chore: add examples/browser-mode --- .../examples/browser-mode/README.md | 2 + .../examples/browser-mode/index.html | 12 ++ .../examples/browser-mode/package.json | 23 ++++ .../examples/browser-mode/public/vite.svg | 1 + .../examples/browser-mode/src/action.tsx | 11 ++ .../browser-mode/src/assets/react.svg | 1 + .../examples/browser-mode/src/client.tsx | 13 ++ .../browser-mode/src/framework/client.tsx | 21 ++++ .../browser-mode/src/framework/main.tsx | 1 + .../browser-mode/src/framework/server.tsx | 48 ++++++++ .../examples/browser-mode/src/index.css | 112 ++++++++++++++++++ .../examples/browser-mode/src/root.tsx | 44 +++++++ .../examples/browser-mode/tsconfig.json | 18 +++ .../examples/browser-mode/vite.config.ts | 100 ++++++++++++++++ pnpm-lock.yaml | 25 ++++ 15 files changed, 432 insertions(+) create mode 100644 packages/plugin-rsc/examples/browser-mode/README.md create mode 100644 packages/plugin-rsc/examples/browser-mode/index.html create mode 100644 packages/plugin-rsc/examples/browser-mode/package.json create mode 100644 packages/plugin-rsc/examples/browser-mode/public/vite.svg create mode 100644 packages/plugin-rsc/examples/browser-mode/src/action.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/assets/react.svg create mode 100644 packages/plugin-rsc/examples/browser-mode/src/client.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/index.css create mode 100644 packages/plugin-rsc/examples/browser-mode/src/root.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/tsconfig.json create mode 100644 packages/plugin-rsc/examples/browser-mode/vite.config.ts diff --git a/packages/plugin-rsc/examples/browser-mode/README.md b/packages/plugin-rsc/examples/browser-mode/README.md new file mode 100644 index 000000000..2dd43bc70 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/README.md @@ -0,0 +1,2 @@ +- https://github.com/kasperpeulen/vitest-plugin-rsc/ +- https://github.com/kasperpeulen/rsc-browser-vite-demo diff --git a/packages/plugin-rsc/examples/browser-mode/index.html b/packages/plugin-rsc/examples/browser-mode/index.html new file mode 100644 index 000000000..bb7507e37 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/index.html @@ -0,0 +1,12 @@ + + + + + + + + + +
+ + diff --git a/packages/plugin-rsc/examples/browser-mode/package.json b/packages/plugin-rsc/examples/browser-mode/package.json new file mode 100644 index 000000000..f314b7333 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/package.json @@ -0,0 +1,23 @@ +{ + "name": "@vitejs/plugin-rsc-examples-browser-mode", + "version": "0.0.0", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@vitejs/plugin-rsc": "latest", + "react": "^19.1.0", + "react-dom": "^19.1.0" + }, + "devDependencies": { + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "latest", + "vite": "^7.0.6" + } +} diff --git a/packages/plugin-rsc/examples/browser-mode/public/vite.svg b/packages/plugin-rsc/examples/browser-mode/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-rsc/examples/browser-mode/src/action.tsx b/packages/plugin-rsc/examples/browser-mode/src/action.tsx new file mode 100644 index 000000000..4fc55d65b --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/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-mode/src/assets/react.svg b/packages/plugin-rsc/examples/browser-mode/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/plugin-rsc/examples/browser-mode/src/client.tsx b/packages/plugin-rsc/examples/browser-mode/src/client.tsx new file mode 100644 index 000000000..29bb5d367 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/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-mode/src/framework/client.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx new file mode 100644 index 000000000..e89322289 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' +import * as ReactDOMClient from 'react-dom/client' +import * as ReactClient from '@vitejs/plugin-rsc/react/browser' +import type { RscPayload } from './server' + +export function render(rscStream: ReadableStream) { + let rscPaylod: Promise + + function ClientRoot() { + rscPaylod ??= ReactClient.createFromReadableStream(rscStream) + return React.use(rscPaylod).root + } + + const domRoot = document.getElementById('root')! + const reactRoot = ReactDOMClient.createRoot(domRoot) + reactRoot.render( + + + , + ) +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx new file mode 100644 index 000000000..6c46e9b17 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx @@ -0,0 +1 @@ +import './server' diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx new file mode 100644 index 000000000..2e1b1237e --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx @@ -0,0 +1,48 @@ +import { renderToReadableStream } from '@vitejs/plugin-rsc/react/rsc' +import type React from 'react' +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' + +export type RscPayload = { + root: React.ReactNode +} + +async function main() { + const rscRoot = ( +
+

RSC Browser Mode

+
+ ) + const rscStream = renderToReadableStream({ + root: rscRoot, + }) + + const clientRunner = createClientRunner() + const clientEntry = await clientRunner.import( + '/src/framework/client.tsx', + ) + clientEntry.render(rscStream) +} + +function createClientRunner() { + 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 runner +} + +main() diff --git a/packages/plugin-rsc/examples/browser-mode/src/index.css b/packages/plugin-rsc/examples/browser-mode/src/index.css new file mode 100644 index 000000000..f4d2128c0 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/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-mode/src/root.tsx b/packages/plugin-rsc/examples/browser-mode/src/root.tsx new file mode 100644 index 000000000..9baa7b9c2 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/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-mode/tsconfig.json b/packages/plugin-rsc/examples/browser-mode/tsconfig.json new file mode 100644 index 000000000..4c355ed3c --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/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-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts new file mode 100644 index 000000000..77297f545 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -0,0 +1,100 @@ +import { defaultClientConditions, defineConfig } from 'vite' +import { + vitePluginRscMinimal, + vitePluginUseClient, + vitePluginUseServer, + vitePluginDefineEncryptionKey, +} from '@vitejs/plugin-rsc/plugin' + +export default defineConfig({ + plugins: [ + vitePluginRscMinimal(), + vitePluginUseClient({ + environment: { + server: ['client'], + }, + }), + vitePluginUseServer({ + environment: { + server: ['client'], + }, + }), + vitePluginDefineEncryptionKey(), + { + name: 'rsc:run-in-browser', + 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.end(JSON.stringify(result)) + return + } + next() + }) + }, + // hotUpdate(ctx) { + // // TODO find out how to do HMR + // ctx.server.ws.send({ type: "full-reload", path: ctx.file }); + // }, + config() { + return { + 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.browser', + '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge', + '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge', + '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', + ], + }, + }, + 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: ['fsevents'], + esbuildOptions: { + platform: 'browser', + }, + }, + }, + }, + resolve: { + alias: { + '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge': + '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', + '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge': + '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', + }, + }, + } + }, + }, + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0406d1e0d..2f83a1ff8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -561,6 +561,31 @@ importers: specifier: ^4.26.0 version: 4.26.0 + packages/plugin-rsc/examples/browser-mode: + dependencies: + '@vitejs/plugin-rsc': + specifier: latest + version: link:../.. + react: + specifier: ^19.1.0 + version: 19.1.0 + react-dom: + specifier: ^19.1.0 + version: 19.1.0(react@19.1.0) + devDependencies: + '@types/react': + specifier: ^19.1.8 + version: 19.1.8 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.1.6(@types/react@19.1.8) + '@vitejs/plugin-react': + specifier: latest + version: link:../../../plugin-react + vite: + specifier: ^7.0.6 + version: 7.0.6(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1) + packages/plugin-rsc/examples/e2e: devDependencies: '@vitejs/plugin-react': From 743eaedda6f9cf3c0852aefaeb375f14fa1fcd31 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 14:07:27 +0900 Subject: [PATCH 11/43] chore: cleanup --- .../examples/browser-mode/vite.config.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 77297f545..96b440b6c 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ }), vitePluginDefineEncryptionKey(), { - name: 'rsc:run-in-browser', + name: 'rsc:browser-mode', configureServer(server) { server.middlewares.use(async (req, res, next) => { const url = new URL(req.url ?? '/', 'https://any.local') @@ -37,10 +37,16 @@ export default defineConfig({ next() }) }, - // hotUpdate(ctx) { - // // TODO find out how to do HMR - // ctx.server.ws.send({ type: "full-reload", path: ctx.file }); - // }, + 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, + }) + } + } + }, config() { return { environments: { From d3b411a754eb3fe16c7ee4924d913da1baa6ae7d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 14:14:00 +0900 Subject: [PATCH 12/43] wip: client component --- .../plugin-rsc/examples/browser-mode/index.html | 2 +- .../examples/browser-mode/package.json | 4 ++-- .../browser-mode/src/framework/client.tsx | 6 ++++++ .../examples/browser-mode/src/framework/main.tsx | 1 - .../browser-mode/src/framework/server.tsx | 16 +++++++++------- .../examples/browser-mode/src/root.tsx | 14 +++----------- 6 files changed, 21 insertions(+), 22 deletions(-) delete mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx diff --git a/packages/plugin-rsc/examples/browser-mode/index.html b/packages/plugin-rsc/examples/browser-mode/index.html index bb7507e37..c7db4de43 100644 --- a/packages/plugin-rsc/examples/browser-mode/index.html +++ b/packages/plugin-rsc/examples/browser-mode/index.html @@ -4,7 +4,7 @@ - +
diff --git a/packages/plugin-rsc/examples/browser-mode/package.json b/packages/plugin-rsc/examples/browser-mode/package.json index f314b7333..a4f517d27 100644 --- a/packages/plugin-rsc/examples/browser-mode/package.json +++ b/packages/plugin-rsc/examples/browser-mode/package.json @@ -6,8 +6,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", - "preview": "vite preview" + "build": "false && vite build", + "preview": "false && vite preview" }, "dependencies": { "@vitejs/plugin-rsc": "latest", diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx index e89322289..1b5305505 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx @@ -3,6 +3,12 @@ import * as ReactDOMClient from 'react-dom/client' import * as ReactClient from '@vitejs/plugin-rsc/react/browser' import type { RscPayload } from './server' +export function initialize() { + ReactClient.setRequireModule({ + load: (id) => import(/* @vite-ignore */ id), + }) +} + export function render(rscStream: ReadableStream) { let rscPaylod: Promise diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx deleted file mode 100644 index 6c46e9b17..000000000 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx +++ /dev/null @@ -1 +0,0 @@ -import './server' diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx index 2e1b1237e..78afb1807 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx @@ -1,25 +1,27 @@ -import { renderToReadableStream } from '@vitejs/plugin-rsc/react/rsc' +import { + renderToReadableStream, + setRequireModule, +} from '@vitejs/plugin-rsc/react/rsc' import type React from 'react' import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' +import { Root } from '../root' export type RscPayload = { root: React.ReactNode } async function main() { - const rscRoot = ( -
-

RSC Browser Mode

-
- ) + setRequireModule({ load: (id) => import(/* @vite-ignore */ id) }) + const rscStream = renderToReadableStream({ - root: rscRoot, + root: , }) const clientRunner = createClientRunner() const clientEntry = await clientRunner.import( '/src/framework/client.tsx', ) + clientEntry.initialize() clientEntry.render(rscStream) } diff --git a/packages/plugin-rsc/examples/browser-mode/src/root.tsx b/packages/plugin-rsc/examples/browser-mode/src/root.tsx index 9baa7b9c2..78ce9d516 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/root.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/root.tsx @@ -1,6 +1,6 @@ import './index.css' // css import is automatically injected in exported server components import viteLogo from '/vite.svg' -import { getServerCounter, updateServerCounter } from './action.tsx' +// import { getServerCounter, updateServerCounter } from './action.tsx' import reactLogo from './assets/react.svg' import { ClientCounter } from './client.tsx' @@ -26,19 +26,11 @@ function App() {
-
+ {/*
-
-
    -
  • - Edit src/client.tsx to test client HMR. -
  • -
  • - Edit src/root.tsx to test server HMR. -
  • -
+
*/} ) } From 3e2f652ec8046061dfc47f4a593bd53201d906aa Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 14:15:15 +0900 Subject: [PATCH 13/43] chore: cleanup --- packages/plugin-rsc/examples/browser-mode/index.html | 1 + .../examples/browser-mode/src/framework/client-runner.tsx | 0 .../examples/browser-mode/src/framework/client.tsx | 6 ++++++ .../plugin-rsc/examples/browser-mode/src/framework/main.tsx | 0 .../examples/browser-mode/src/framework/server.tsx | 6 +++++- 5 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/client-runner.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx diff --git a/packages/plugin-rsc/examples/browser-mode/index.html b/packages/plugin-rsc/examples/browser-mode/index.html index c7db4de43..5b2a0fe0f 100644 --- a/packages/plugin-rsc/examples/browser-mode/index.html +++ b/packages/plugin-rsc/examples/browser-mode/index.html @@ -2,6 +2,7 @@ + RSC Browser Mode diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/client-runner.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/client-runner.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx index 1b5305505..ebae92f9e 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx @@ -7,6 +7,12 @@ export function initialize() { ReactClient.setRequireModule({ load: (id) => import(/* @vite-ignore */ id), }) + + // TODO: + ReactClient.setServerCallback(async (id, args) => { + id + args + }) } export function render(rscStream: ReadableStream) { diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx index 78afb1807..1b2399b0c 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx @@ -10,8 +10,12 @@ export type RscPayload = { root: React.ReactNode } -async function main() { +function initialize() { setRequireModule({ load: (id) => import(/* @vite-ignore */ id) }) +} + +async function main() { + initialize() const rscStream = renderToReadableStream({ root: , From 162f50160322dbf7d31051bb863713361e508877 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:18:48 +0900 Subject: [PATCH 14/43] refactor: align entry.{rsc,browser} pattern --- .../examples/browser-mode/index.html | 2 +- .../src/framework/client-runner.tsx | 0 .../browser-mode/src/framework/client.tsx | 33 ----------- .../src/framework/entry.browser.tsx | 57 ++++++++++++++++++ .../browser-mode/src/framework/entry.rsc.tsx | 59 +++++++++++++++++++ .../browser-mode/src/framework/main.tsx | 35 +++++++++++ .../browser-mode/src/framework/server.tsx | 54 ----------------- 7 files changed, 152 insertions(+), 88 deletions(-) delete mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/client-runner.tsx delete mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/entry.browser.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx delete mode 100644 packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx diff --git a/packages/plugin-rsc/examples/browser-mode/index.html b/packages/plugin-rsc/examples/browser-mode/index.html index 5b2a0fe0f..6323c94f5 100644 --- a/packages/plugin-rsc/examples/browser-mode/index.html +++ b/packages/plugin-rsc/examples/browser-mode/index.html @@ -5,7 +5,7 @@ RSC Browser Mode - +
diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/client-runner.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/client-runner.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx deleted file mode 100644 index ebae92f9e..000000000 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/client.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import * as React from 'react' -import * as ReactDOMClient from 'react-dom/client' -import * as ReactClient from '@vitejs/plugin-rsc/react/browser' -import type { RscPayload } from './server' - -export function initialize() { - ReactClient.setRequireModule({ - load: (id) => import(/* @vite-ignore */ id), - }) - - // TODO: - ReactClient.setServerCallback(async (id, args) => { - id - args - }) -} - -export function render(rscStream: ReadableStream) { - let rscPaylod: Promise - - function ClientRoot() { - rscPaylod ??= ReactClient.createFromReadableStream(rscStream) - return React.use(rscPaylod).root - } - - const domRoot = document.getElementById('root')! - const reactRoot = ReactDOMClient.createRoot(domRoot) - reactRoot.render( - - - , - ) -} diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/entry.browser.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.browser.tsx new file mode 100644 index 000000000..bb36e3626 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.browser.tsx @@ -0,0 +1,57 @@ +import * as React from 'react' +import * as ReactDOMClient from 'react-dom/client' +import * as ReactClient from '@vitejs/plugin-rsc/react/browser' +import type { RscPayload } from './entry.rsc' + +let fetchServer: typeof import('./entry.rsc').fetchServer + +export function initialize(options: { fetchServer: typeof fetchServer }) { + fetchServer = options.fetchServer + ReactClient.setRequireModule({ + load: (id) => import(/* @vite-ignore */ id), + }) +} + +export async function main() { + let setPayload: (v: RscPayload) => void + + const initialPayload = await ReactClient.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 + } + + ReactClient.setServerCallback(async (id, args) => { + const url = new URL(window.location.href) + const temporaryReferences = ReactClient.createTemporaryReferenceSet() + const payload = await ReactClient.createFromFetch( + fetchServer( + new Request(url, { + method: 'POST', + body: await ReactClient.encodeReply(args, { temporaryReferences }), + headers: { + 'x-rsc-action': id, + }, + }), + ), + { temporaryReferences }, + ) + setPayload(payload) + return payload.returnValue + }) + + const browserRoot = ( + + + + ) + ReactDOMClient.createRoot(document.body).render(browserRoot) +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx new file mode 100644 index 000000000..5f6b2e997 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx @@ -0,0 +1,59 @@ +import * as ReactServer from '@vitejs/plugin-rsc/react/rsc' +import type React from 'react' +import { Root } from '../root' +import type { ReactFormState } from 'react-dom/client' + +export type RscPayload = { + root: React.ReactNode + returnValue?: unknown + formState?: ReactFormState +} + +export function initialize() { + ReactServer.setRequireModule({ load: (id) => import(/* @vite-ignore */ id) }) +} + +let root: React.ReactNode + +export function setRoot(root_: React.ReactNode) { + root = root_ +} + +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 = ReactServer.createTemporaryReferenceSet() + const args = await ReactServer.decodeReply(body, { temporaryReferences }) + const action = await ReactServer.loadServerAction(actionId) + returnValue = await action.apply(null, args) + } else { + const formData = await request.formData() + const decodedAction = await ReactServer.decodeAction(formData) + const result = await decodedAction() + formState = await ReactServer.decodeFormState(result, formData) + } + } + + const rscPayload: RscPayload = { root: , formState, returnValue } + const rscOptions = { temporaryReferences } + const rscStream = ReactServer.renderToReadableStream( + rscPayload, + rscOptions, + ) + + return new Response(rscStream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + vary: 'accept', + }, + }) +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx index e69de29bb..12d5c7546 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/main.tsx @@ -0,0 +1,35 @@ +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' +import * as server from './entry.rsc' + +async function main() { + const client = await importClient() + server.initialize() + client.initialize({ fetchServer: server.fetchServer }) + await client.main() +} + +async function importClient() { + 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', + ) +} + +main() diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx deleted file mode 100644 index 1b2399b0c..000000000 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/server.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - renderToReadableStream, - setRequireModule, -} from '@vitejs/plugin-rsc/react/rsc' -import type React from 'react' -import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' -import { Root } from '../root' - -export type RscPayload = { - root: React.ReactNode -} - -function initialize() { - setRequireModule({ load: (id) => import(/* @vite-ignore */ id) }) -} - -async function main() { - initialize() - - const rscStream = renderToReadableStream({ - root: , - }) - - const clientRunner = createClientRunner() - const clientEntry = await clientRunner.import( - '/src/framework/client.tsx', - ) - clientEntry.initialize() - clientEntry.render(rscStream) -} - -function createClientRunner() { - 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 runner -} - -main() From 976afb614c948a9da6caf70ae0488843080c3fb1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:20:11 +0900 Subject: [PATCH 15/43] chore: cleanup --- .../examples/browser-mode/src/framework/entry.rsc.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx index 5f6b2e997..089c1981d 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx @@ -13,12 +13,6 @@ export function initialize() { ReactServer.setRequireModule({ load: (id) => import(/* @vite-ignore */ id) }) } -let root: React.ReactNode - -export function setRoot(root_: React.ReactNode) { - root = root_ -} - export async function fetchServer(request: Request): Promise { const isAction = request.method === 'POST' let returnValue: unknown | undefined From 357e219224e240af9166f026ee5dd254b588fd7a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:26:23 +0900 Subject: [PATCH 16/43] fix(rsc): use `/react/rsc` for use server transform runtime import --- packages/plugin-rsc/src/plugin.ts | 15 ++++++++++++--- packages/plugin-rsc/tsdown.config.ts | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index fef3f220f..a709dbf80 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1289,17 +1289,26 @@ export function vitePluginUseServer( )}, ${JSON.stringify(name)})`, rejectNonAsyncFunction: true, encode: enableEncryption - ? (value) => `$$ReactServer.encryptActionBoundArgs(${value})` + ? (value) => + `__vite_rsc_encryption_runtime.encryptActionBoundArgs(${value})` : undefined, decode: enableEncryption ? (value) => - `await $$ReactServer.decryptActionBoundArgs(${value})` + `await __vite_rsc_encryption_runtime.decryptActionBoundArgs(${value})` : undefined, }) if (!output.hasChanged()) return serverReferences[getNormalizedId()] = id - const importSource = resolvePackage(`${PKG_NAME}/rsc`) + const importSource = resolvePackage(`${PKG_NAME}/react/rsc`) output.prepend(`import * as $$ReactServer from "${importSource}";\n`) + if (enableEncryption) { + const importSource = resolvePackage( + `${PKG_NAME}/utils/encryption-runtime`, + ) + output.prepend( + `import * as __vite_rsc_encryption_runtime from ${JSON.stringify(importSource)};\n`, + ) + } return { code: output.toString(), map: output.generateMap({ hires: 'boundary' }), diff --git a/packages/plugin-rsc/tsdown.config.ts b/packages/plugin-rsc/tsdown.config.ts index 0c39520ab..e5a8c06d7 100644 --- a/packages/plugin-rsc/tsdown.config.ts +++ b/packages/plugin-rsc/tsdown.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ 'src/rsc-html-stream/ssr.ts', 'src/rsc-html-stream/browser.ts', 'src/utils/rpc.ts', + 'src/utils/encryption-runtime.ts', ], format: ['esm'], external: [/^virtual:/, /^@vitejs\/plugin-rsc\/vendor\//], From 2fa3a35cfe6320ea6bd27a5c829f47e664395cfd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:30:48 +0900 Subject: [PATCH 17/43] wip: server action --- packages/plugin-rsc/examples/browser-mode/src/root.tsx | 8 ++++---- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 4 ++++ packages/plugin-rsc/package.json | 3 ++- pnpm-lock.yaml | 5 ++++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/src/root.tsx b/packages/plugin-rsc/examples/browser-mode/src/root.tsx index 78ce9d516..fb9d49061 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/root.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/root.tsx @@ -1,6 +1,6 @@ -import './index.css' // css import is automatically injected in exported server components +import './index.css' import viteLogo from '/vite.svg' -// import { getServerCounter, updateServerCounter } from './action.tsx' +import { getServerCounter, updateServerCounter } from './action.tsx' import reactLogo from './assets/react.svg' import { ClientCounter } from './client.tsx' @@ -26,11 +26,11 @@ function App() {
- {/*
+
-
*/} +
) } diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 96b440b6c..3beae8ea5 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -5,9 +5,11 @@ import { vitePluginUseServer, vitePluginDefineEncryptionKey, } from '@vitejs/plugin-rsc/plugin' +import inspect from 'vite-plugin-inspect' export default defineConfig({ plugins: [ + inspect(), vitePluginRscMinimal(), vitePluginUseClient({ environment: { @@ -93,6 +95,8 @@ export default defineConfig({ }, resolve: { alias: { + '@vitejs/plugin-rsc/rsc': + '@vitejs/plugin-rsc/react/rsc', '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge': '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge': diff --git a/packages/plugin-rsc/package.json b/packages/plugin-rsc/package.json index 01e4e08be..41a8f18a0 100644 --- a/packages/plugin-rsc/package.json +++ b/packages/plugin-rsc/package.json @@ -60,7 +60,8 @@ "react-server-dom-webpack": "^19.1.0", "rsc-html-stream": "^0.0.7", "tinyexec": "^1.0.1", - "tsdown": "^0.13.0" + "tsdown": "^0.13.0", + "vite-plugin-inspect": "^11.3.2" }, "peerDependencies": { "react": "*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f83a1ff8..889b162a4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -502,6 +502,9 @@ importers: tsdown: specifier: ^0.13.0 version: 0.13.0(publint@0.3.12)(typescript@5.8.3) + vite-plugin-inspect: + specifier: ^11.3.2 + version: 11.3.2(vite@7.0.6(@types/node@22.16.5)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3)(yaml@2.7.1)) packages/plugin-rsc/examples/basic: dependencies: @@ -8640,7 +8643,7 @@ snapshots: unplugin-utils@0.2.4: dependencies: pathe: 2.0.3 - picomatch: 4.0.2 + picomatch: 4.0.3 unrs-resolver@1.9.2: dependencies: From f3f493273b25e5cc6f7fe7058b020ae726abcd36 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:31:13 +0900 Subject: [PATCH 18/43] chore: cleanup --- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 3beae8ea5..eed6c8ae9 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -95,8 +95,6 @@ export default defineConfig({ }, resolve: { alias: { - '@vitejs/plugin-rsc/rsc': - '@vitejs/plugin-rsc/react/rsc', '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge': '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge': From d84bd2a529b9bdbc58ba624a93f447cbeb6a53fd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:36:45 +0900 Subject: [PATCH 19/43] fix: __vite_rsc_raw_import__ for server action --- .../browser-mode/src/framework/entry.rsc.tsx | 4 +++- packages/plugin-rsc/src/plugin.ts | 24 +++++++++---------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx index 089c1981d..343e6751c 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/framework/entry.rsc.tsx @@ -9,8 +9,10 @@ export type RscPayload = { formState?: ReactFormState } +declare let __vite_rsc_raw_import__: (id: string) => Promise + export function initialize() { - ReactServer.setRequireModule({ load: (id) => import(/* @vite-ignore */ id) }) + ReactServer.setRequireModule({ load: (id) => __vite_rsc_raw_import__(id) }) } export async function fetchServer(request: Request): Promise { diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index a709dbf80..32fe2b897 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -157,6 +157,18 @@ export function vitePluginRscMinimal(): Plugin[] { server = server_ }, }, + { + name: 'rsc:patch-browser-raw-import', + transform: { + order: 'post', + handler(code) { + if (code.includes('__vite_rsc_raw_import__')) { + // inject dynamic import last to avoid Vite adding `?import` query to client references + return code.replace('__vite_rsc_raw_import__', 'import') + } + }, + }, + }, ...vitePluginRscCore(), ] } @@ -465,18 +477,6 @@ export default function vitePluginRsc( } }, }, - { - name: 'rsc:patch-browser-raw-import', - transform: { - order: 'post', - handler(code) { - if (code.includes('__vite_rsc_raw_import__')) { - // inject dynamic import last to avoid Vite adding `?import` query to client references - return code.replace('__vite_rsc_raw_import__', 'import') - } - }, - }, - }, { // backward compat: `loadSsrModule(name)` implemented as `loadModule("ssr", name)` name: 'rsc:load-ssr-module', From f54f591b281e670947aa468f539726a2b6043c97 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:38:43 +0900 Subject: [PATCH 20/43] chore: tweak --- packages/plugin-rsc/src/plugin.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 32fe2b897..c53b657a8 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -158,12 +158,13 @@ export function vitePluginRscMinimal(): Plugin[] { }, }, { - name: 'rsc:patch-browser-raw-import', + name: 'rsc:vite-client-raw-import', transform: { order: 'post', handler(code) { if (code.includes('__vite_rsc_raw_import__')) { - // inject dynamic import last to avoid Vite adding `?import` query to client references + // inject dynamic import last to avoid Vite adding `?import` query + // to client references (and browser mode server references) return code.replace('__vite_rsc_raw_import__', 'import') } }, From c9270c25e9f5922ce40a44f6bf73e97ed274bbf3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:44:11 +0900 Subject: [PATCH 21/43] chore: readme --- packages/plugin-rsc/examples/browser-mode/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/README.md b/packages/plugin-rsc/examples/browser-mode/README.md index 2dd43bc70..2e9ed6455 100644 --- a/packages/plugin-rsc/examples/browser-mode/README.md +++ b/packages/plugin-rsc/examples/browser-mode/README.md @@ -1,2 +1 @@ -- https://github.com/kasperpeulen/vitest-plugin-rsc/ -- https://github.com/kasperpeulen/rsc-browser-vite-demo +[examples/starter](https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-rsc/examples/starter) but entirely on Browser. Inspired by https://github.com/kasperpeulen/vitest-plugin-rsc/ From 9e4ced6bba00f26138cdf07182a6c50a3a5f69a4 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:46:49 +0900 Subject: [PATCH 22/43] chore: cleanup --- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index eed6c8ae9..c3aeb1190 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -5,11 +5,11 @@ import { vitePluginUseServer, vitePluginDefineEncryptionKey, } from '@vitejs/plugin-rsc/plugin' -import inspect from 'vite-plugin-inspect' +// import inspect from 'vite-plugin-inspect' export default defineConfig({ plugins: [ - inspect(), + // inspect(), vitePluginRscMinimal(), vitePluginUseClient({ environment: { From 30e8dc58cbba7384b079ff81a464469cea1936b9 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 15:56:27 +0900 Subject: [PATCH 23/43] test: add e2e --- packages/plugin-rsc/e2e/browser-mode.test.ts | 8 ++++++ packages/plugin-rsc/e2e/starter.ts | 30 +++++++++++--------- 2 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 packages/plugin-rsc/e2e/browser-mode.test.ts diff --git a/packages/plugin-rsc/e2e/browser-mode.test.ts b/packages/plugin-rsc/e2e/browser-mode.test.ts new file mode 100644 index 000000000..0c248eede --- /dev/null +++ b/packages/plugin-rsc/e2e/browser-mode.test.ts @@ -0,0 +1,8 @@ +import { test } from '@playwright/test' +import { useFixture } from './fixture' +import { defineStarterTest } from './starter' + +test.describe('dev-browser-mode', () => { + const f = useFixture({ root: 'examples/browser-mode', mode: 'dev' }) + defineStarterTest(f, 'browser-mode') +}) diff --git a/packages/plugin-rsc/e2e/starter.ts b/packages/plugin-rsc/e2e/starter.ts index 68b05706f..3f2e02378 100644 --- a/packages/plugin-rsc/e2e/starter.ts +++ b/packages/plugin-rsc/e2e/starter.ts @@ -9,10 +9,13 @@ import { export function defineStarterTest( f: Fixture, - variant?: 'no-ssr' | 'dev-production', + variant?: 'no-ssr' | 'dev-production' | 'browser-mode', ) { const waitForHydration: typeof waitForHydration_ = (page) => - waitForHydration_(page, variant === 'no-ssr' ? '#root' : 'body') + waitForHydration_( + page, + variant === 'no-ssr' || variant === 'browser-mode' ? '#root' : 'body', + ) test('basic', async ({ page }) => { using _ = expectNoPageError(page) @@ -40,7 +43,7 @@ export function defineStarterTest( }) testNoJs('server action @nojs', async ({ page }) => { - test.skip(variant === 'no-ssr') + test.skip(variant === 'no-ssr' || variant === 'browser-mode') await page.goto(f.url()) await page.getByRole('button', { name: 'Server Counter: 1' }).click() @@ -50,7 +53,11 @@ export function defineStarterTest( }) test('client hmr', async ({ page }) => { - test.skip(f.mode === 'build' || variant === 'dev-production') + test.skip( + f.mode === 'build' || + variant === 'dev-production' || + variant === 'browser-mode', + ) await page.goto(f.url()) await waitForHydration(page) @@ -80,7 +87,7 @@ export function defineStarterTest( }) test.describe(() => { - test.skip(f.mode === 'build') + test.skip(f.mode === 'build' || variant === 'browser-mode') test('server hmr', async ({ page }) => { await page.goto(f.url()) @@ -113,20 +120,17 @@ export function defineStarterTest( test('css @js', async ({ page }) => { await page.goto(f.url()) await waitForHydration(page) - await expect(page.locator('.read-the-docs')).toHaveCSS( - 'color', - 'rgb(136, 136, 136)', - ) + await expect(page.locator('.card').nth(0)).toHaveCSS('padding-left', '16px') }) test.describe(() => { - test.skip(variant === 'no-ssr') + test.skip(variant === 'no-ssr' || variant === 'browser-mode') testNoJs('css @nojs', async ({ page }) => { await page.goto(f.url()) - await expect(page.locator('.read-the-docs')).toHaveCSS( - 'color', - 'rgb(136, 136, 136)', + await expect(page.locator('.card').nth(0)).toHaveCSS( + 'padding-left', + '16px', ) }) }) From a66299f8afc8b368927f21224d435e366ac77b86 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 16:04:00 +0900 Subject: [PATCH 24/43] test: tweak timeout --- packages/plugin-rsc/e2e/helper.ts | 2 +- packages/plugin-rsc/playwright.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/e2e/helper.ts b/packages/plugin-rsc/e2e/helper.ts index 702c5b7ec..60c3aa4f2 100644 --- a/packages/plugin-rsc/e2e/helper.ts +++ b/packages/plugin-rsc/e2e/helper.ts @@ -15,7 +15,7 @@ export async function waitForHydration(page: Page, locator: string = 'body') { el && Object.keys(el).some((key) => key.startsWith('__reactFiber')), ), - { timeout: 3000 }, + { timeout: 10000 }, ) .toBeTruthy() } diff --git a/packages/plugin-rsc/playwright.config.ts b/packages/plugin-rsc/playwright.config.ts index 13549cb7e..5c390e9de 100644 --- a/packages/plugin-rsc/playwright.config.ts +++ b/packages/plugin-rsc/playwright.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ trace: 'on-first-retry', }, expect: { - toPass: { timeout: 5000 }, + toPass: { timeout: 10000 }, }, projects: [ { From 2f809135076cb79746a413bfabe91016f6548a49 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 16:18:08 +0900 Subject: [PATCH 25/43] test: bye webkit --- packages/plugin-rsc/e2e/browser-mode.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/plugin-rsc/e2e/browser-mode.test.ts b/packages/plugin-rsc/e2e/browser-mode.test.ts index 0c248eede..155b0543e 100644 --- a/packages/plugin-rsc/e2e/browser-mode.test.ts +++ b/packages/plugin-rsc/e2e/browser-mode.test.ts @@ -3,6 +3,10 @@ import { useFixture } from './fixture' import { defineStarterTest } from './starter' test.describe('dev-browser-mode', () => { + // Webkit fails by + // > TypeError: ReadableByteStreamController is not implemented + test.skip((ctx) => ctx.browserName === 'webkit') + const f = useFixture({ root: 'examples/browser-mode', mode: 'dev' }) defineStarterTest(f, 'browser-mode') }) From 801c349c0a1eafe0a5dad775e7857d7db294451e Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 16:26:53 +0900 Subject: [PATCH 26/43] fix: correct webkit test skip syntax in browser-mode tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the test.skip syntax to properly destructure browserName from the test context parameter. The previous syntax was causing webkit tests to still run despite the skip condition. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- packages/plugin-rsc/e2e/browser-mode.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/browser-mode.test.ts b/packages/plugin-rsc/e2e/browser-mode.test.ts index 155b0543e..c4c55e1de 100644 --- a/packages/plugin-rsc/e2e/browser-mode.test.ts +++ b/packages/plugin-rsc/e2e/browser-mode.test.ts @@ -5,7 +5,7 @@ import { defineStarterTest } from './starter' test.describe('dev-browser-mode', () => { // Webkit fails by // > TypeError: ReadableByteStreamController is not implemented - test.skip((ctx) => ctx.browserName === 'webkit') + test.skip(({ browserName }) => browserName === 'webkit') const f = useFixture({ root: 'examples/browser-mode', mode: 'dev' }) defineStarterTest(f, 'browser-mode') From 0ba59e38c60cace5ac6f3a1d1615dd027d4c26cc Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 16:52:34 +0900 Subject: [PATCH 27/43] fix: fix action from client --- .../browser-mode/src/action-from-client/action.tsx | 5 +++++ .../browser-mode/src/action-from-client/client.tsx | 14 ++++++++++++++ .../plugin-rsc/examples/browser-mode/src/root.tsx | 4 ++++ packages/plugin-rsc/src/plugin.ts | 3 ++- 4 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 packages/plugin-rsc/examples/browser-mode/src/action-from-client/action.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/action-from-client/client.tsx diff --git a/packages/plugin-rsc/examples/browser-mode/src/action-from-client/action.tsx b/packages/plugin-rsc/examples/browser-mode/src/action-from-client/action.tsx new file mode 100644 index 000000000..a72eb0bda --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/action-from-client/action.tsx @@ -0,0 +1,5 @@ +'use server' + +export async function testActionState(prev: number) { + return prev + 1 +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/action-from-client/client.tsx b/packages/plugin-rsc/examples/browser-mode/src/action-from-client/client.tsx new file mode 100644 index 000000000..aca850f2f --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/action-from-client/client.tsx @@ -0,0 +1,14 @@ +'use client' + +import React from 'react' +import { testActionState } from './action' + +export function TestUseActionState() { + const [state, formAction] = React.useActionState(testActionState, 0) + + return ( +
+ +
+ ) +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/root.tsx b/packages/plugin-rsc/examples/browser-mode/src/root.tsx index fb9d49061..6436e5869 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/root.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/root.tsx @@ -3,6 +3,7 @@ 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' export function Root() { return @@ -31,6 +32,9 @@ function App() { +
+ +
) } diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index c53b657a8..fdeb788d0 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1337,7 +1337,8 @@ export function vitePluginUseServer( const output = result?.output if (!output?.hasChanged()) return serverReferences[getNormalizedId()] = id - const name = this.environment.name === 'client' ? 'browser' : 'ssr' + // TODO + const name = this.environment.name === 'ssr' ? 'ssr' : 'browser' const importSource = resolvePackage(`${PKG_NAME}/react/${name}`) output.prepend(`import * as $$ReactClient from "${importSource}";\n`) return { From 24c57695a78358a806448e86c838f1339e66d5f6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 17:03:30 +0900 Subject: [PATCH 28/43] chore: cleanup --- packages/plugin-rsc/src/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index fdeb788d0..5c5a05238 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1172,7 +1172,7 @@ export function vitePluginUseClient( /** @experimental */ export function vitePluginDefineEncryptionKey( - useServerPluginOptions?: Pick, + useServerPluginOptions: Pick, ): Plugin[] { let defineEncryptionKey: string let emitEncryptionKey = false @@ -1185,7 +1185,7 @@ export function vitePluginDefineEncryptionKey( async configEnvironment(name, _config, env) { if (name === 'rsc' && !env.isPreview) { defineEncryptionKey = - useServerPluginOptions?.defineEncryptionKey ?? + useServerPluginOptions.defineEncryptionKey ?? JSON.stringify(toBase64(await generateEncryptionKey())) } }, From adbeee0009769c5a46fdb2700f56c7abed7815d6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 17:04:27 +0900 Subject: [PATCH 29/43] chore: cleanup --- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index c3aeb1190..599801eb8 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ server: ['client'], }, }), - vitePluginDefineEncryptionKey(), + vitePluginDefineEncryptionKey({}), { name: 'rsc:browser-mode', configureServer(server) { From b17dcd857e9a45ac9fe7f64031235c47a36ae2bf Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 18:37:13 +0900 Subject: [PATCH 30/43] chore: cleanup --- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 599801eb8..888b65406 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -65,8 +65,6 @@ export default defineConfig({ 'react/jsx-runtime', 'react/jsx-dev-runtime', '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', - '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge', - '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge', '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', ], }, From edbdfca4836050b77dbbe9b4e37716a52fb6b6f0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 18:52:18 +0900 Subject: [PATCH 31/43] fix: fix vitePluginDefineEncryptionKey --- .../browser-mode/src/action-bind/client.tsx | 12 ++ .../browser-mode/src/action-bind/form.tsx | 16 +++ .../browser-mode/src/action-bind/server.tsx | 107 ++++++++++++++++++ .../examples/browser-mode/src/root.tsx | 4 + .../examples/browser-mode/vite.config.ts | 13 ++- packages/plugin-rsc/src/plugin.ts | 12 +- 6 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 packages/plugin-rsc/examples/browser-mode/src/action-bind/client.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/action-bind/form.tsx create mode 100644 packages/plugin-rsc/examples/browser-mode/src/action-bind/server.tsx diff --git a/packages/plugin-rsc/examples/browser-mode/src/action-bind/client.tsx b/packages/plugin-rsc/examples/browser-mode/src/action-bind/client.tsx new file mode 100644 index 000000000..2fe0c81c6 --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/action-bind/client.tsx @@ -0,0 +1,12 @@ +'use client' + +import React from 'react' + +export function ActionBindClient() { + const hydrated = React.useSyncExternalStore( + React.useCallback(() => () => {}, []), + () => true, + () => false, + ) + return <>{String(hydrated)} +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/action-bind/form.tsx b/packages/plugin-rsc/examples/browser-mode/src/action-bind/form.tsx new file mode 100644 index 000000000..1b1675c3a --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/action-bind/form.tsx @@ -0,0 +1,16 @@ +'use client' + +import React from 'react' + +export function TestServerActionBindClientForm(props: { + action: () => Promise +}) { + const [result, formAction] = React.useActionState(props.action, '[?]') + + return ( +
+ + {result} +
+ ) +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/action-bind/server.tsx b/packages/plugin-rsc/examples/browser-mode/src/action-bind/server.tsx new file mode 100644 index 000000000..dda9ee2ed --- /dev/null +++ b/packages/plugin-rsc/examples/browser-mode/src/action-bind/server.tsx @@ -0,0 +1,107 @@ +// based on test cases in +// https://github.com/vercel/next.js/blob/ad898de735c393d98960a68c8d9eaeee32206c57/test/e2e/app-dir/actions/app/encryption/page.js + +import { ActionBindClient } from './client' +import { TestServerActionBindClientForm } from './form' + +export function TestActionBind() { + return ( + <> + + + + + + ) +} + +export function TestServerActionBindReset() { + return ( +
{ + 'use server' + testServerActionBindSimpleState = '[?]' + testServerActionBindActionState = '[?]' + testServerActionBindClientState++ + }} + > + +
+ ) +} + +let testServerActionBindSimpleState = '[?]' + +export function TestServerActionBindSimple() { + const outerValue = 'outerValue' + + return ( +
{ + 'use server' + const result = String(formData.get('value')) === outerValue + testServerActionBindSimpleState = JSON.stringify(result) + }} + > + + + + {testServerActionBindSimpleState} + +
+ ) +} + +let testServerActionBindClientState = 0 + +export function TestServerActionBindClient() { + // client element as server action bound argument + const client = + + const action = async () => { + 'use server' + return client + } + + return ( + + ) +} + +let testServerActionBindActionState = '[?]' + +export function TestServerActionBindAction() { + async function otherAction() { + 'use server' + return 'otherActionValue' + } + + function wrapAction(value: string, action: () => Promise) { + return async function (formValue: string) { + 'use server' + const actionValue = await action() + return [actionValue === 'otherActionValue', formValue === value] + } + } + + const action = wrapAction('ok', otherAction) + + return ( +
{ + 'use server' + const result = await action(String(formData.get('value'))) + testServerActionBindActionState = JSON.stringify(result) + }} + > + + + + {testServerActionBindActionState} + +
+ ) +} diff --git a/packages/plugin-rsc/examples/browser-mode/src/root.tsx b/packages/plugin-rsc/examples/browser-mode/src/root.tsx index 6436e5869..e8d912527 100644 --- a/packages/plugin-rsc/examples/browser-mode/src/root.tsx +++ b/packages/plugin-rsc/examples/browser-mode/src/root.tsx @@ -4,6 +4,7 @@ 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 @@ -35,6 +36,9 @@ function App() {
+
+ +
) } diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 888b65406..88785d1be 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -5,11 +5,11 @@ import { vitePluginUseServer, vitePluginDefineEncryptionKey, } from '@vitejs/plugin-rsc/plugin' -// import inspect from 'vite-plugin-inspect' +import inspect from 'vite-plugin-inspect' export default defineConfig({ plugins: [ - // inspect(), + inspect(), vitePluginRscMinimal(), vitePluginUseClient({ environment: { @@ -21,7 +21,11 @@ export default defineConfig({ server: ['client'], }, }), - vitePluginDefineEncryptionKey({}), + vitePluginDefineEncryptionKey({ + environment: { + server: ['client'], + }, + }), { name: 'rsc:browser-mode', configureServer(server) { @@ -67,6 +71,7 @@ export default defineConfig({ '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', ], + exclude: ['vite', '@vitejs/plugin-rsc'], }, }, react_client: { @@ -84,7 +89,7 @@ export default defineConfig({ 'react/jsx-dev-runtime', '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', ], - exclude: ['fsevents'], + exclude: ['@vitejs/plugin-rsc', 'fsevents'], esbuildOptions: { platform: 'browser', }, diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 5c5a05238..0026526fc 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1172,18 +1172,26 @@ export function vitePluginUseClient( /** @experimental */ export function vitePluginDefineEncryptionKey( - useServerPluginOptions: Pick, + useServerPluginOptions: Pick< + RscPluginOptions, + 'defineEncryptionKey' | 'environment' + >, ): Plugin[] { let defineEncryptionKey: string let emitEncryptionKey = false const KEY_PLACEHOLDER = '__vite_rsc_define_encryption_key' const KEY_FILE = '__vite_rsc_encryption_key.js' + const serverEnvironments = useServerPluginOptions.environment?.server ?? [ + 'rsc', + ] + const isServer = (name: string) => serverEnvironments.includes(name) + return [ { name: 'rsc:encryption-key', async configEnvironment(name, _config, env) { - if (name === 'rsc' && !env.isPreview) { + if (isServer(name) && !env.isPreview) { defineEncryptionKey = useServerPluginOptions.defineEncryptionKey ?? JSON.stringify(toBase64(await generateEncryptionKey())) From 07cc571dd586427dd2bca46baa5469ab47bdf465 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 18:53:57 +0900 Subject: [PATCH 32/43] chore: cleanup --- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 88785d1be..a79aaaf60 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -89,7 +89,7 @@ export default defineConfig({ 'react/jsx-dev-runtime', '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', ], - exclude: ['@vitejs/plugin-rsc', 'fsevents'], + exclude: ['@vitejs/plugin-rsc'], esbuildOptions: { platform: 'browser', }, From 8acadc47f0732858a357a301847ddf2fac502f0c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 19:02:40 +0900 Subject: [PATCH 33/43] chore: tweak --- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index a79aaaf60..03474f33e 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -37,6 +37,7 @@ export default defineConfig({ await server.environments['react_client']!.hot.handleInvoke( payload, ) + res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify(result)) return } From 2799acb06347aa4fcf1aa4674cc790c7b6283151 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Thu, 31 Jul 2025 19:21:50 +0900 Subject: [PATCH 34/43] fix: use edge build --- .../examples/browser-mode/vite.config.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 03474f33e..23fb7a9ac 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -69,8 +69,11 @@ export default defineConfig({ 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime', - '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', - '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', + '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge', + '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge', + // TODO: browser build breaks `src/actin-bind` examples + // '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', + // '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', ], exclude: ['vite', '@vitejs/plugin-rsc'], }, @@ -98,12 +101,12 @@ export default defineConfig({ }, }, resolve: { - alias: { - '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge': - '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', - '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge': - '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', - }, + // alias: { + // '@vitejs/plugin-rsc/vendor/react-server-dom/server.edge': + // '@vitejs/plugin-rsc/vendor/react-server-dom/server.browser', + // '@vitejs/plugin-rsc/vendor/react-server-dom/client.edge': + // '@vitejs/plugin-rsc/vendor/react-server-dom/client.browser', + // }, }, } }, From 07b3fbeb3556641e8eeb5e6f35530433b873e1a5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 09:42:02 +0900 Subject: [PATCH 35/43] chore: comment --- packages/plugin-rsc/examples/browser-mode/vite.config.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 23fb7a9ac..654d2c7b2 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -5,11 +5,11 @@ import { vitePluginUseServer, vitePluginDefineEncryptionKey, } from '@vitejs/plugin-rsc/plugin' -import inspect from 'vite-plugin-inspect' +// import inspect from 'vite-plugin-inspect' export default defineConfig({ plugins: [ - inspect(), + // inspect(), vitePluginRscMinimal(), vitePluginUseClient({ environment: { @@ -44,6 +44,10 @@ export default defineConfig({ next() }) }, + // for "react_client" hmr, it requires: + // - enable fast-refresh transform on `react_client` environment + // - currently `@vitejs/plugin-react` doesn't support it + // - implement and enable module runner hmr hotUpdate(ctx) { if (this.environment.name === 'react_client') { if (ctx.modules.length > 0) { From 8e28d1884fc02e5255f8dcd2bff066216da3d67c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 11:29:19 +0900 Subject: [PATCH 36/43] test: tweak flaky --- packages/plugin-rsc/e2e/syntax-error.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/e2e/syntax-error.test.ts b/packages/plugin-rsc/e2e/syntax-error.test.ts index 75425713c..a44980d95 100644 --- a/packages/plugin-rsc/e2e/syntax-error.test.ts +++ b/packages/plugin-rsc/e2e/syntax-error.test.ts @@ -164,11 +164,11 @@ test.describe(() => { ) await expect(async () => { await page.goto(f.url()) - await waitForHydration(page) await expect(page.getByTestId('client-content')).toHaveText( 'client:fixed', ) }).toPass() + await waitForHydration(page) }) }) @@ -197,11 +197,11 @@ test.describe(() => { ) await expect(async () => { await page.goto(f.url()) - await waitForHydration(page) await expect(page.getByTestId('server-content')).toHaveText( 'server:fixed', ) }).toPass() + await waitForHydration(page) }) }) }) From 7975e6dd4492d87f753ccea9a364f06135a06323 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 11:50:49 +0900 Subject: [PATCH 37/43] refactor: rename option to `serverEnvironmentName` --- .../examples/browser-mode/vite.config.ts | 6 +-- packages/plugin-rsc/src/plugin.ts | 42 +++++++++---------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 654d2c7b2..8d384c007 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -13,17 +13,17 @@ export default defineConfig({ vitePluginRscMinimal(), vitePluginUseClient({ environment: { - server: ['client'], + serverEnvironmentName: 'client', }, }), vitePluginUseServer({ environment: { - server: ['client'], + serverEnvironmentName: 'client', }, }), vitePluginDefineEncryptionKey({ environment: { - server: ['client'], + serverEnvironmentName: 'client', }, }), { diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 0026526fc..1646bec39 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -133,12 +133,12 @@ export type RscPluginOptions = { useBuildAppHook?: boolean /** - * This allows configuring `react-server` condition environment. + * Custom environment configuration * @experimental - * @default { server: ['rsc'] } + * @default { serverEnvironmentName: 'rsc' } */ environment?: { - server?: string[] + serverEnvironmentName?: string } } @@ -974,16 +974,14 @@ export function vitePluginUseClient( // https://github.com/vitejs/vite/blob/4bcf45863b5f46aa2b41f261283d08f12d3e8675/packages/vite/src/node/utils.ts#L175 const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/ - const serverEnvironments = useClientPluginOptions.environment?.server ?? [ - 'rsc', - ] - const isServer = (name: string) => serverEnvironments.includes(name) + const serverEnvironmentName = + useClientPluginOptions.environment?.serverEnvironmentName ?? 'rsc' return [ { name: 'rsc:use-client', async transform(code, id) { - if (!isServer(this.environment.name)) return + if (this.environment.name !== serverEnvironmentName) return if (!code.includes('use client')) return const ast = await parseAstAsync(code) @@ -1106,7 +1104,7 @@ export function vitePluginUseClient( id.startsWith('\0virtual:vite-rsc/client-in-server-package-proxy/') ) { assert.equal(this.environment.mode, 'dev') - assert(!isServer(this.environment.name)) + assert(this.environment.name !== serverEnvironmentName) id = decodeURIComponent( id.slice( '\0virtual:vite-rsc/client-in-server-package-proxy/'.length, @@ -1126,7 +1124,10 @@ export function vitePluginUseClient( resolveId: { order: 'pre', async handler(source, importer, options) { - if (isServer(this.environment.name) && bareImportRE.test(source)) { + if ( + this.environment.name === serverEnvironmentName && + bareImportRE.test(source) + ) { const resolved = await this.resolve(source, importer, options) if (resolved && resolved.id.includes('/node_modules/')) { packageSources.set(resolved.id, source) @@ -1151,7 +1152,7 @@ export function vitePluginUseClient( } }, generateBundle(_options, bundle) { - if (!isServer(this.environment.name)) return + if (this.environment.name !== serverEnvironmentName) return // track used exports of client references in rsc build // to tree shake unused exports in browser and ssr build @@ -1182,16 +1183,14 @@ export function vitePluginDefineEncryptionKey( const KEY_PLACEHOLDER = '__vite_rsc_define_encryption_key' const KEY_FILE = '__vite_rsc_encryption_key.js' - const serverEnvironments = useServerPluginOptions.environment?.server ?? [ - 'rsc', - ] - const isServer = (name: string) => serverEnvironments.includes(name) + const serverEnvironmentName = + useServerPluginOptions.environment?.serverEnvironmentName ?? 'rsc' return [ { name: 'rsc:encryption-key', async configEnvironment(name, _config, env) { - if (isServer(name) && !env.isPreview) { + if (name === serverEnvironmentName && !env.isPreview) { defineEncryptionKey = useServerPluginOptions.defineEncryptionKey ?? JSON.stringify(toBase64(await generateEncryptionKey())) @@ -1245,10 +1244,8 @@ export function vitePluginUseServer( 'ignoredPackageWarnings' | 'enableActionEncryption' | 'environment' >, ): Plugin[] { - const serverEnvironments = useServerPluginOptions.environment?.server ?? [ - 'rsc', - ] - const isServer = (name: string) => serverEnvironments.includes(name) + const serverEnvironmentName = + useServerPluginOptions.environment?.serverEnvironmentName ?? 'rsc' return [ { @@ -1278,13 +1275,12 @@ export function vitePluginUseServer( // module identity of `import(id)` like browser, so we simply strip it off. id = id.split('?v=')[0]! } - // TODO - normalizedId_ = normalizeReferenceId(id, serverEnvironments[0]!) + normalizedId_ = normalizeReferenceId(id, serverEnvironmentName) } return normalizedId_ } - if (isServer(this.environment.name)) { + if (this.environment.name === serverEnvironmentName) { const transformServerActionServer_ = withRollupError( this, transformServerActionServer, From 3798dc96b7c7afbd97fd3ffc7c330f2307bd6dac Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 12:07:58 +0900 Subject: [PATCH 38/43] refactor: expose `environment.browser` option --- .../examples/browser-mode/vite.config.ts | 9 +++++--- packages/plugin-rsc/src/plugin.ts | 21 ++++++++++--------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index 8d384c007..ee78a5f7e 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -13,17 +13,20 @@ export default defineConfig({ vitePluginRscMinimal(), vitePluginUseClient({ environment: { - serverEnvironmentName: 'client', + rsc: 'client', + browser: 'react_client', }, }), vitePluginUseServer({ environment: { - serverEnvironmentName: 'client', + rsc: 'client', + browser: 'react_client', }, }), vitePluginDefineEncryptionKey({ environment: { - serverEnvironmentName: 'client', + rsc: 'client', + browser: 'react_client', }, }), { diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 1646bec39..cc739aa77 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -135,10 +135,12 @@ export type RscPluginOptions = { /** * Custom environment configuration * @experimental - * @default { serverEnvironmentName: 'rsc' } + * @default { browser: 'client', ssr: 'ssr', rsc: 'rsc' } */ environment?: { - serverEnvironmentName?: string + browser?: string + ssr?: string + rsc?: string } } @@ -974,8 +976,7 @@ export function vitePluginUseClient( // https://github.com/vitejs/vite/blob/4bcf45863b5f46aa2b41f261283d08f12d3e8675/packages/vite/src/node/utils.ts#L175 const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/ - const serverEnvironmentName = - useClientPluginOptions.environment?.serverEnvironmentName ?? 'rsc' + const serverEnvironmentName = useClientPluginOptions.environment?.rsc ?? 'rsc' return [ { @@ -1183,8 +1184,7 @@ export function vitePluginDefineEncryptionKey( const KEY_PLACEHOLDER = '__vite_rsc_define_encryption_key' const KEY_FILE = '__vite_rsc_encryption_key.js' - const serverEnvironmentName = - useServerPluginOptions.environment?.serverEnvironmentName ?? 'rsc' + const serverEnvironmentName = useServerPluginOptions.environment?.rsc ?? 'rsc' return [ { @@ -1244,8 +1244,9 @@ export function vitePluginUseServer( 'ignoredPackageWarnings' | 'enableActionEncryption' | 'environment' >, ): Plugin[] { - const serverEnvironmentName = - useServerPluginOptions.environment?.serverEnvironmentName ?? 'rsc' + const serverEnvironmentName = useServerPluginOptions.environment?.rsc ?? 'rsc' + const browserEnvironmentName = + useServerPluginOptions.environment?.browser ?? 'browser' return [ { @@ -1341,8 +1342,8 @@ export function vitePluginUseServer( const output = result?.output if (!output?.hasChanged()) return serverReferences[getNormalizedId()] = id - // TODO - const name = this.environment.name === 'ssr' ? 'ssr' : 'browser' + const name = + this.environment.name === browserEnvironmentName ? 'browser' : 'ssr' const importSource = resolvePackage(`${PKG_NAME}/react/${name}`) output.prepend(`import * as $$ReactClient from "${importSource}";\n`) return { From d35cf81ebb6e93d0ac20299237d095b56eba692d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 12:32:28 +0900 Subject: [PATCH 39/43] refactor: expose only vitePluginRscMinimal --- .../examples/browser-mode/vite.config.ts | 22 ++----------------- packages/plugin-rsc/src/plugin.ts | 21 +++++++++--------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/packages/plugin-rsc/examples/browser-mode/vite.config.ts b/packages/plugin-rsc/examples/browser-mode/vite.config.ts index ee78a5f7e..c8bf161f4 100644 --- a/packages/plugin-rsc/examples/browser-mode/vite.config.ts +++ b/packages/plugin-rsc/examples/browser-mode/vite.config.ts @@ -1,29 +1,11 @@ import { defaultClientConditions, defineConfig } from 'vite' -import { - vitePluginRscMinimal, - vitePluginUseClient, - vitePluginUseServer, - vitePluginDefineEncryptionKey, -} from '@vitejs/plugin-rsc/plugin' +import { vitePluginRscMinimal } from '@vitejs/plugin-rsc/plugin' // import inspect from 'vite-plugin-inspect' export default defineConfig({ plugins: [ // inspect(), - vitePluginRscMinimal(), - vitePluginUseClient({ - environment: { - rsc: 'client', - browser: 'react_client', - }, - }), - vitePluginUseServer({ - environment: { - rsc: 'client', - browser: 'react_client', - }, - }), - vitePluginDefineEncryptionKey({ + vitePluginRscMinimal({ environment: { rsc: 'client', browser: 'react_client', diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 538e42ef2..c82ba6cb0 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -145,10 +145,13 @@ export type RscPluginOptions = { } /** @experimental */ -export function vitePluginRscMinimal(): Plugin[] { +export function vitePluginRscMinimal( + rscPluginOptions: RscPluginOptions = {}, +): Plugin[] { return [ { name: 'rsc:minimal', + enforce: 'pre', async config() { await esModuleLexer.init }, @@ -173,6 +176,9 @@ export function vitePluginRscMinimal(): Plugin[] { }, }, ...vitePluginRscCore(), + ...vitePluginUseClient(rscPluginOptions), + ...vitePluginUseServer(rscPluginOptions), + ...vitePluginDefineEncryptionKey(rscPluginOptions), ] } @@ -877,9 +883,7 @@ globalThis.AsyncLocalStorage = __viteRscAyncHooks.AsyncLocalStorage; return '' }, }, - ...vitePluginUseClient(rscPluginOptions), - ...vitePluginUseServer(rscPluginOptions), - ...vitePluginDefineEncryptionKey(rscPluginOptions), + ...vitePluginRscMinimal(rscPluginOptions), ...vitePluginFindSourceMapURL(), ...vitePluginRscCss({ rscCssTransform: rscPluginOptions.rscCssTransform }), ...(rscPluginOptions.validateImports !== false @@ -963,8 +967,7 @@ function hashString(v: string) { return createHash('sha256').update(v).digest().toString('hex').slice(0, 12) } -/** @experimental */ -export function vitePluginUseClient( +function vitePluginUseClient( useClientPluginOptions: Pick< RscPluginOptions, 'ignoredPackageWarnings' | 'keepUseCientProxy' | 'environment' @@ -1171,8 +1174,7 @@ export function vitePluginUseClient( ] } -/** @experimental */ -export function vitePluginDefineEncryptionKey( +function vitePluginDefineEncryptionKey( useServerPluginOptions: Pick< RscPluginOptions, 'defineEncryptionKey' | 'environment' @@ -1236,8 +1238,7 @@ export function vitePluginDefineEncryptionKey( ] } -/** @experimental */ -export function vitePluginUseServer( +function vitePluginUseServer( useServerPluginOptions: Pick< RscPluginOptions, 'ignoredPackageWarnings' | 'enableActionEncryption' | 'environment' From 0b556030cd1772cabf0b07d41ac7afc2f637771b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 12:33:14 +0900 Subject: [PATCH 40/43] fix: fix double --- packages/plugin-rsc/src/plugin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index c82ba6cb0..03d68f496 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -232,7 +232,6 @@ export default function vitePluginRsc( } return [ - ...vitePluginRscMinimal(), { name: 'rsc', async config(config, env) { From 7e6b57a63d033f5bb8d6d27226466e36f0d2be17 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 12:36:23 +0900 Subject: [PATCH 41/43] test: add e2e --- packages/plugin-rsc/e2e/browser-mode.test.ts | 64 +++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/e2e/browser-mode.test.ts b/packages/plugin-rsc/e2e/browser-mode.test.ts index c4c55e1de..42e2e6d6a 100644 --- a/packages/plugin-rsc/e2e/browser-mode.test.ts +++ b/packages/plugin-rsc/e2e/browser-mode.test.ts @@ -1,4 +1,4 @@ -import { test } from '@playwright/test' +import { expect, test, type Page } from '@playwright/test' import { useFixture } from './fixture' import { defineStarterTest } from './starter' @@ -9,4 +9,66 @@ test.describe('dev-browser-mode', () => { const f = useFixture({ root: 'examples/browser-mode', mode: 'dev' }) defineStarterTest(f, 'browser-mode') + + // action-bind tests copied from basic.test.ts + + test('action bind simple', async ({ page }) => { + await page.goto(f.url()) + await testActionBindSimple(page) + }) + + async function testActionBindSimple(page: Page) { + await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText( + '[?]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-simple' }) + .click() + await expect(page.getByTestId('test-server-action-bind-simple')).toHaveText( + 'true', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-reset' }) + .click() + } + + test('action bind client', async ({ page }) => { + await page.goto(f.url()) + await testActionBindClient(page) + }) + + async function testActionBindClient(page: Page) { + await expect(page.getByTestId('test-server-action-bind-client')).toHaveText( + '[?]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-client' }) + .click() + await expect(page.getByTestId('test-server-action-bind-client')).toHaveText( + 'true', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-reset' }) + .click() + } + + test('action bind action', async ({ page }) => { + await page.goto(f.url()) + await testActionBindAction(page) + }) + + async function testActionBindAction(page: Page) { + await expect(page.getByTestId('test-server-action-bind-action')).toHaveText( + '[?]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-action' }) + .click() + await expect(page.getByTestId('test-server-action-bind-action')).toHaveText( + '[true,true]', + ) + await page + .getByRole('button', { name: 'test-server-action-bind-reset' }) + .click() + } }) From 86fc3de5ae18d5147cabbb90185ed3fa58361211 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 12:39:13 +0900 Subject: [PATCH 42/43] refactor: fix todo --- packages/plugin-rsc/src/plugin.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 03d68f496..ffb1d8f50 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -978,6 +978,8 @@ function vitePluginUseClient( const bareImportRE = /^(?![a-zA-Z]:)[\w@](?!.*:\/\/)/ const serverEnvironmentName = useClientPluginOptions.environment?.rsc ?? 'rsc' + const browserEnvironmentName = + useClientPluginOptions.environment?.browser ?? 'client' return [ { @@ -1026,8 +1028,7 @@ function vitePluginUseClient( } else { if (this.environment.mode === 'dev') { importId = normalizeViteImportAnalysisUrl( - // TODO - server.environments.client, + server.environments[browserEnvironmentName]!, id, ) referenceKey = importId From bf73261e761149c5e0b5061c575e150f752a9525 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Fri, 1 Aug 2025 12:47:01 +0900 Subject: [PATCH 43/43] fix: wrong browserEnvironmentName --- packages/plugin-rsc/src/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index ffb1d8f50..b9b28de71 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -1246,7 +1246,7 @@ function vitePluginUseServer( ): Plugin[] { const serverEnvironmentName = useServerPluginOptions.environment?.rsc ?? 'rsc' const browserEnvironmentName = - useServerPluginOptions.environment?.browser ?? 'browser' + useServerPluginOptions.environment?.browser ?? 'client' return [ {