diff --git a/packages/plugin-rsc/e2e/react-router.test.ts b/packages/plugin-rsc/e2e/react-router.test.ts index e62352e84..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,10 +35,10 @@ test.describe('build-cloudflare', () => { buildCommand: 'pnpm cf-build', command: 'pnpm cf-preview', }) - defineTest(f) + defineTest(f, 'cloudflare') }) -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), ) 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..239fd51b8 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,16 @@ export default defineConfig({ }, }, ssr: { - optimizeDeps: { - exclude: ['react-router'], + keepProcessEnv: false, + build: { + outDir: './dist/rsc/ssr', }, + resolve: + env.command === 'build' + ? { + noExternal: true, + } + : undefined, }, rsc: { optimizeDeps: { @@ -52,4 +53,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/package.json b/packages/plugin-rsc/examples/react-router/package.json index 22f6757f0..d9db7a03a 100644 --- a/packages/plugin-rsc/examples/react-router/package.json +++ b/packages/plugin-rsc/examples/react-router/package.json @@ -10,12 +10,12 @@ "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", "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/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..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 @@ -9,7 +9,20 @@ 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) { + 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..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,15 +5,20 @@ 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( - 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..5ba2e532f 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,40 @@ export function createRpcClient(options: { endpoint: string }): T { }, ) as any } + +const encodePlugin: EncodePlugin = (value) => { + if (value instanceof Response) { + const data: ConstructorParameters = [ + value.body, + { + status: value.status, + statusText: value.statusText, + headers: value.headers, + }, + ] + return ['vite-rsc/response', ...data] + } + if (value instanceof Headers) { + const data: ConstructorParameters = [[...value]] + return ['vite-rsc/headers', ...data] + } +} + +const decodePlugin: DecodePlugin = (type, ...data) => { + if (type === 'vite-rsc/response') { + const value = new Response(...(data as any)) + return { value } + } + if (type === 'vite-rsc/headers') { + const value = new Headers(...(data as any)) + return { value } + } +} + +const encodeOptions: EncodeOptions = { + plugins: [encodePlugin], +} + +const decodeOptions: DecodeOptions = { + plugins: [decodePlugin], +} 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