From a477fd78f89b8b1a2457211028c4e8e919a7a7f8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:10:36 +0900 Subject: [PATCH 1/8] feat(rsc): react-router cloudflare single worker example --- .../examples/react-router/cf/entry.rsc.tsx | 4 +- .../examples/react-router/cf/entry.ssr.tsx | 9 --- .../examples/react-router/cf/vite.config.ts | 32 +++++----- .../cf/{wrangler.rsc.jsonc => wrangler.jsonc} | 2 +- .../react-router/cf/wrangler.ssr.jsonc | 9 --- .../react-router-vite/entry.rsc.single.tsx | 10 --- .../react-router-vite/entry.rsc.tsx | 16 ++++- .../react-router-vite/entry.ssr.tsx | 11 ++-- .../examples/react-router/vite.config.ts | 2 +- packages/plugin-rsc/src/utils/rpc.ts | 62 ++++++++++++++++++- 10 files changed, 103 insertions(+), 54 deletions(-) delete mode 100644 packages/plugin-rsc/examples/react-router/cf/entry.ssr.tsx rename packages/plugin-rsc/examples/react-router/cf/{wrangler.rsc.jsonc => wrangler.jsonc} (84%) delete mode 100644 packages/plugin-rsc/examples/react-router/cf/wrangler.ssr.jsonc delete mode 100644 packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.single.tsx diff --git a/packages/plugin-rsc/examples/react-router/cf/entry.rsc.tsx b/packages/plugin-rsc/examples/react-router/cf/entry.rsc.tsx index 103b41ed1..6f6a20fb1 100644 --- a/packages/plugin-rsc/examples/react-router/cf/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/react-router/cf/entry.rsc.tsx @@ -1,9 +1,9 @@ -import { fetchServer } from '../react-router-vite/entry.rsc' +import handler from '../react-router-vite/entry.rsc' console.log('[debug:cf-rsc-entry]') export default { fetch(request: Request) { - return fetchServer(request) + return handler(request) }, } diff --git a/packages/plugin-rsc/examples/react-router/cf/entry.ssr.tsx b/packages/plugin-rsc/examples/react-router/cf/entry.ssr.tsx deleted file mode 100644 index 689e7ac25..000000000 --- a/packages/plugin-rsc/examples/react-router/cf/entry.ssr.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { generateHTML } from '../react-router-vite/entry.ssr' - -console.log('[debug:cf-ssr-entry]') - -export default { - fetch(request: Request, env: any) { - return generateHTML(request, (request) => env.RSC.fetch(request)) - }, -} diff --git a/packages/plugin-rsc/examples/react-router/cf/vite.config.ts b/packages/plugin-rsc/examples/react-router/cf/vite.config.ts index 4f77cb175..952d15381 100644 --- a/packages/plugin-rsc/examples/react-router/cf/vite.config.ts +++ b/packages/plugin-rsc/examples/react-router/cf/vite.config.ts @@ -5,7 +5,7 @@ import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' // import inspect from 'vite-plugin-inspect' -export default defineConfig({ +export default defineConfig((env) => ({ clearScreen: false, build: { minify: false, @@ -17,22 +17,16 @@ export default defineConfig({ rsc({ entries: { client: './react-router-vite/entry.browser.tsx', + ssr: './react-router-vite/entry.ssr.tsx', }, serverHandler: false, + loadModuleDevProxy: true, }), cloudflare({ - configPath: './cf/wrangler.ssr.jsonc', + configPath: './cf/wrangler.jsonc', viteEnvironment: { - name: 'ssr', + name: 'rsc', }, - auxiliaryWorkers: [ - { - configPath: './cf/wrangler.rsc.jsonc', - viteEnvironment: { - name: 'rsc', - }, - }, - ], }), ], environments: { @@ -42,9 +36,19 @@ export default defineConfig({ }, }, ssr: { - optimizeDeps: { - exclude: ['react-router'], + keepProcessEnv: false, + build: { + outDir: './dist/rsc/ssr', }, + resolve: + env.command === 'build' + ? { + noExternal: true, + } + : undefined, + // optimizeDeps: { + // exclude: ['react-router'], + // }, }, rsc: { optimizeDeps: { @@ -52,4 +56,4 @@ export default defineConfig({ }, }, }, -}) +})) diff --git a/packages/plugin-rsc/examples/react-router/cf/wrangler.rsc.jsonc b/packages/plugin-rsc/examples/react-router/cf/wrangler.jsonc similarity index 84% rename from packages/plugin-rsc/examples/react-router/cf/wrangler.rsc.jsonc rename to packages/plugin-rsc/examples/react-router/cf/wrangler.jsonc index 67cdb5435..879000cd0 100644 --- a/packages/plugin-rsc/examples/react-router/cf/wrangler.rsc.jsonc +++ b/packages/plugin-rsc/examples/react-router/cf/wrangler.jsonc @@ -1,6 +1,6 @@ { "$schema": "https://www.unpkg.com/wrangler@4.19.1/config-schema.json", - "name": "vite-rsc-react-router-rsc", + "name": "vite-rsc-react-router", "main": "./entry.rsc.tsx", "workers_dev": true, "compatibility_date": "2025-04-01", diff --git a/packages/plugin-rsc/examples/react-router/cf/wrangler.ssr.jsonc b/packages/plugin-rsc/examples/react-router/cf/wrangler.ssr.jsonc deleted file mode 100644 index 22b718b03..000000000 --- a/packages/plugin-rsc/examples/react-router/cf/wrangler.ssr.jsonc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://www.unpkg.com/wrangler@4.19.1/config-schema.json", - "name": "vite-rsc-react-router", - "main": "./entry.ssr.tsx", - "workers_dev": true, - "services": [{ "binding": "RSC", "service": "vite-rsc-react-router-rsc" }], - "compatibility_date": "2025-04-01", - "compatibility_flags": ["nodejs_als"], -} diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.single.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.single.tsx deleted file mode 100644 index da3895388..000000000 --- a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.single.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { fetchServer } from './entry.rsc' - -export default async function handler(request: Request) { - // Import the generateHTML function from the client environment - const ssr = await import.meta.viteRsc.loadModule< - typeof import('./entry.ssr') - >('ssr', 'index') - - return ssr.generateHTML(request, fetchServer) -} diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx index 847211bfd..8aa0a8b06 100644 --- a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx @@ -9,7 +9,21 @@ import { import { unstable_matchRSCServerRequest as matchRSCServerRequest } from 'react-router' import { routes } from '../app/routes' -export function fetchServer(request: Request) { +export default async function handler(request: Request) { + // Import the generateHTML function from the client environment + const ssr = await import.meta.viteRsc.loadModule< + typeof import('./entry.ssr') + >('ssr', 'index') + const rscResponse = await fetchServer(request) + const ssrResponse = await ssr.generateHTML( + request.url, + request.headers, + rscResponse, + ) + return ssrResponse +} + +function fetchServer(request: Request) { return matchRSCServerRequest({ // Provide the React Server touchpoints. createTemporaryReferenceSet, diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx index c80cde622..7492d3188 100644 --- a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx @@ -5,15 +5,18 @@ import { unstable_RSCStaticRouter as RSCStaticRouter, } from 'react-router' +// https://github.com/remix-run/react-router/blob/20d8307d4a51c219f6e13e0b66461e7162d944e4/packages/react-router/lib/rsc/server.ssr.tsx#L95-L102 + export async function generateHTML( - request: Request, - fetchServer: (request: Request) => Promise, + url: string, + headers: Headers, + rscResponse: Response, ): Promise { return await routeRSCServerRequest({ // The incoming request. - request, + request: new Request(url, { headers }), // How to call the React Server. - fetchServer, + fetchServer: async () => rscResponse, // Provide the React Server touchpoints. createFromReadableStream, // Render the router to HTML. diff --git a/packages/plugin-rsc/examples/react-router/vite.config.ts b/packages/plugin-rsc/examples/react-router/vite.config.ts index 7b9dbb7c0..4ac2404c3 100644 --- a/packages/plugin-rsc/examples/react-router/vite.config.ts +++ b/packages/plugin-rsc/examples/react-router/vite.config.ts @@ -17,7 +17,7 @@ export default defineConfig({ entries: { client: './react-router-vite/entry.browser.tsx', ssr: './react-router-vite/entry.ssr.tsx', - rsc: './react-router-vite/entry.rsc.single.tsx', + rsc: './react-router-vite/entry.rsc.tsx', }, }), ], diff --git a/packages/plugin-rsc/src/utils/rpc.ts b/packages/plugin-rsc/src/utils/rpc.ts index 483b9a2e7..d75c767d9 100644 --- a/packages/plugin-rsc/src/utils/rpc.ts +++ b/packages/plugin-rsc/src/utils/rpc.ts @@ -1,4 +1,11 @@ -import { decode, encode } from 'turbo-stream' +import { + decode, + encode, + type DecodeOptions, + type DecodePlugin, + type EncodeOptions, + type EncodePlugin, +} from 'turbo-stream' type RequestPayload = { method: string @@ -17,6 +24,7 @@ export function createRpcServer(handlers: T) { } const reqPayload = await decode( request.body.pipeThrough(new TextDecoderStream()), + decodeOptions, ) const handler = (handlers as any)[reqPayload.method] if (!handler) { @@ -31,7 +39,7 @@ export function createRpcServer(handlers: T) { resPayload.ok = false resPayload.data = e } - return new Response(encode(resPayload)) + return new Response(encode(resPayload, encodeOptions)) } } @@ -41,7 +49,9 @@ export function createRpcClient(options: { endpoint: string }): T { method, args, } - const body = encode(reqPayload).pipeThrough(new TextEncoderStream()) + const body = encode(reqPayload, encodeOptions).pipeThrough( + new TextEncoderStream(), + ) const res = await fetch(options.endpoint, { method: 'POST', body, @@ -55,6 +65,7 @@ export function createRpcClient(options: { endpoint: string }): T { } const resPayload = await decode( res.body.pipeThrough(new TextDecoderStream()), + decodeOptions, ) if (!resPayload.ok) { throw resPayload.data @@ -74,3 +85,48 @@ export function createRpcClient(options: { endpoint: string }): T { }, ) as any } + +const encodePlugin: EncodePlugin = (value) => { + if (value instanceof Response) { + return [ + 'vite-rsc/response', + value.status, + value.statusText, + [...value.headers], + value.body, + ] + } + if (value instanceof Headers) { + return ['vite-rsc/headers', [...value]] + } +} + +const decodePlugin: DecodePlugin = (type, ...data) => { + if (type === 'vite-rsc/response') { + const [status, statusText, headers, body] = data as [ + number, + string, + [string, string][], + ReadableStream | null, + ] + const value = new Response(body, { + status, + statusText, + headers, + }) + return { value } + } + if (type === 'vite-rsc/headers') { + const [headers] = data as [[string, string][]] + const value = new Headers(headers) + return { value } + } +} + +const encodeOptions: EncodeOptions = { + plugins: [encodePlugin], +} + +const decodeOptions: DecodeOptions = { + plugins: [decodePlugin], +} From 787f7b230b07cdbf74ffd227785264bfd1aad5c7 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:16:10 +0900 Subject: [PATCH 2/8] refactor: simplify turbo-stream plugins --- packages/plugin-rsc/src/utils/rpc.ts | 30 ++++++++++------------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/packages/plugin-rsc/src/utils/rpc.ts b/packages/plugin-rsc/src/utils/rpc.ts index d75c767d9..5ba2e532f 100644 --- a/packages/plugin-rsc/src/utils/rpc.ts +++ b/packages/plugin-rsc/src/utils/rpc.ts @@ -88,37 +88,29 @@ export function createRpcClient(options: { endpoint: string }): T { const encodePlugin: EncodePlugin = (value) => { if (value instanceof Response) { - return [ - 'vite-rsc/response', - value.status, - value.statusText, - [...value.headers], + const data: ConstructorParameters = [ value.body, + { + status: value.status, + statusText: value.statusText, + headers: value.headers, + }, ] + return ['vite-rsc/response', ...data] } if (value instanceof Headers) { - return ['vite-rsc/headers', [...value]] + const data: ConstructorParameters = [[...value]] + return ['vite-rsc/headers', ...data] } } const decodePlugin: DecodePlugin = (type, ...data) => { if (type === 'vite-rsc/response') { - const [status, statusText, headers, body] = data as [ - number, - string, - [string, string][], - ReadableStream | null, - ] - const value = new Response(body, { - status, - statusText, - headers, - }) + const value = new Response(...(data as any)) return { value } } if (type === 'vite-rsc/headers') { - const [headers] = data as [[string, string][]] - const value = new Headers(headers) + const value = new Headers(...(data as any)) return { value } } } From b34c08620a67148058da2bbddf7eca5848222144 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:17:03 +0900 Subject: [PATCH 3/8] chore: cleanup --- packages/plugin-rsc/examples/react-router/cf/vite.config.ts | 3 --- .../examples/react-router/react-router-vite/entry.rsc.tsx | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/plugin-rsc/examples/react-router/cf/vite.config.ts b/packages/plugin-rsc/examples/react-router/cf/vite.config.ts index 952d15381..239fd51b8 100644 --- a/packages/plugin-rsc/examples/react-router/cf/vite.config.ts +++ b/packages/plugin-rsc/examples/react-router/cf/vite.config.ts @@ -46,9 +46,6 @@ export default defineConfig((env) => ({ noExternal: true, } : undefined, - // optimizeDeps: { - // exclude: ['react-router'], - // }, }, rsc: { optimizeDeps: { diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx index 8aa0a8b06..81b1ef0f3 100644 --- a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.rsc.tsx @@ -10,7 +10,6 @@ import { unstable_matchRSCServerRequest as matchRSCServerRequest } from 'react-r import { routes } from '../app/routes' export default async function handler(request: Request) { - // Import the generateHTML function from the client environment const ssr = await import.meta.viteRsc.loadModule< typeof import('./entry.ssr') >('ssr', 'index') From 1f0aeb6a4ec99b6c10b7b9de10b86a64d6657a71 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:21:11 +0900 Subject: [PATCH 4/8] chore: comment --- .../examples/react-router/react-router-vite/entry.ssr.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx index 7492d3188..9ca597adb 100644 --- a/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx +++ b/packages/plugin-rsc/examples/react-router/react-router-vite/entry.ssr.tsx @@ -5,6 +5,8 @@ import { unstable_RSCStaticRouter as RSCStaticRouter, } from 'react-router' +// pass serializable values (via turbo-stream) to ssr environment. +// passing entire `request` and `fetchServer` are not necessary since `routeRSCServerRequest` works like this // https://github.com/remix-run/react-router/blob/20d8307d4a51c219f6e13e0b66461e7162d944e4/packages/react-router/lib/rsc/server.ssr.tsx#L95-L102 export async function generateHTML( From 937cb8dde8d47acb69029d508c37d2be64fdf6e9 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:39:06 +0900 Subject: [PATCH 5/8] test: update --- packages/plugin-rsc/e2e/react-router.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/e2e/react-router.test.ts b/packages/plugin-rsc/e2e/react-router.test.ts index e62352e84..46004a86b 100644 --- a/packages/plugin-rsc/e2e/react-router.test.ts +++ b/packages/plugin-rsc/e2e/react-router.test.ts @@ -38,7 +38,7 @@ test.describe('build-cloudflare', () => { defineTest(f) }) -function defineTest(f: Fixture) { +function defineTest(f: Fixture, variant?: 'cloudflare') { test('loader', async ({ page }) => { await page.goto(f.url()) await expect(page.getByText(`loaderData: {"name":"Unknown"}`)).toBeVisible() @@ -81,7 +81,7 @@ function defineTest(f: Fixture) { ) const manifest = JSON.parse( readFileSync( - f.root + '/dist/ssr/__vite_rsc_assets_manifest.js', + `${f.root}/${variant === 'cloudflare' ? 'dist/rsc/ssr' : 'dist/ssr'}/__vite_rsc_assets_manifest.js`, 'utf-8', ).slice('export default '.length), ) From f813ef3fcc92384cc4c9c78a06632a7d731d63d5 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:41:47 +0900 Subject: [PATCH 6/8] chore: update cf-release --- packages/plugin-rsc/examples/react-router/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/examples/react-router/package.json b/packages/plugin-rsc/examples/react-router/package.json index 22f6757f0..1eac961e9 100644 --- a/packages/plugin-rsc/examples/react-router/package.json +++ b/packages/plugin-rsc/examples/react-router/package.json @@ -10,7 +10,7 @@ "cf-dev": "vite -c ./cf/vite.config.ts", "cf-build": "vite -c ./cf/vite.config.ts build", "cf-preview": "vite -c ./cf/vite.config.ts preview", - "cf-release": "wrangler deploy -c dist/rsc/wrangler.json && wrangler deploy" + "cf-release": "wrangler deploy" }, "dependencies": { "react": "^19.1.1", From be3918552737d5be52308db2b19d07c6d1f547d3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:47:16 +0900 Subject: [PATCH 7/8] chore: update react-router for getPayload bug fix --- packages/plugin-rsc/examples/react-router/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/plugin-rsc/examples/react-router/package.json b/packages/plugin-rsc/examples/react-router/package.json index 1eac961e9..d9db7a03a 100644 --- a/packages/plugin-rsc/examples/react-router/package.json +++ b/packages/plugin-rsc/examples/react-router/package.json @@ -15,7 +15,7 @@ "dependencies": { "react": "^19.1.1", "react-dom": "^19.1.1", - "react-router": "7.8.2" + "react-router": "0.0.0-nightly-353d05fd9-20250830" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.11.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af510d5bc..bed5d8511 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -632,8 +632,8 @@ importers: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) react-router: - specifier: 7.8.2 - version: 7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: 0.0.0-nightly-353d05fd9-20250830 + version: 0.0.0-nightly-353d05fd9-20250830(react-dom@19.1.1(react@19.1.1))(react@19.1.1) devDependencies: '@cloudflare/vite-plugin': specifier: ^1.11.7 @@ -3951,8 +3951,8 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} - react-router@7.8.2: - resolution: {integrity: sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==} + react-router@0.0.0-nightly-353d05fd9-20250830: + resolution: {integrity: sha512-A8RDNWdzjnAwsiAZfqoTa6Um4rAZmf8Ump4DB1yqjUTjk7jt3EuSoNynNbog50zH47k2iXrnkFLuFPz/ayZUQg==} engines: {node: '>=20.0.0'} peerDependencies: react: '>=18' @@ -7618,7 +7618,7 @@ snapshots: react-refresh@0.17.0: {} - react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-router@0.0.0-nightly-353d05fd9-20250830(react-dom@19.1.1(react@19.1.1))(react@19.1.1): dependencies: cookie: 1.0.2 react: 19.1.1 From 8070629976ecad1e2126c890296bdfa2c24fa258 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Sun, 31 Aug 2025 16:55:06 +0900 Subject: [PATCH 8/8] test: update --- packages/plugin-rsc/e2e/react-router.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/e2e/react-router.test.ts b/packages/plugin-rsc/e2e/react-router.test.ts index 46004a86b..20f2d6133 100644 --- a/packages/plugin-rsc/e2e/react-router.test.ts +++ b/packages/plugin-rsc/e2e/react-router.test.ts @@ -25,7 +25,7 @@ test.describe('dev-cloudflare', () => { mode: 'dev', command: 'pnpm cf-dev', }) - defineTest(f) + defineTest(f, 'cloudflare') }) test.describe('build-cloudflare', () => { @@ -35,7 +35,7 @@ test.describe('build-cloudflare', () => { buildCommand: 'pnpm cf-build', command: 'pnpm cf-preview', }) - defineTest(f) + defineTest(f, 'cloudflare') }) function defineTest(f: Fixture, variant?: 'cloudflare') {