From a0b2af7da11f0877380998a05e8c5e2c73d0bda8 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 12 Jun 2026 02:47:06 +0100 Subject: [PATCH 1/6] feat(rsc): preserve decoded server references --- packages/plugin-rsc/src/core/rsc.test.ts | 37 ++++++++++++++++++++++++ packages/plugin-rsc/src/core/rsc.ts | 35 ++++++++++++++++++++-- packages/plugin-rsc/src/core/shared.ts | 2 ++ packages/plugin-rsc/src/react/rsc.ts | 12 ++++++-- 4 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-rsc/src/core/rsc.test.ts diff --git a/packages/plugin-rsc/src/core/rsc.test.ts b/packages/plugin-rsc/src/core/rsc.test.ts new file mode 100644 index 000000000..8f2eb55a7 --- /dev/null +++ b/packages/plugin-rsc/src/core/rsc.test.ts @@ -0,0 +1,37 @@ +import { beforeAll, describe, expect, it, vi } from 'vitest' + +vi.mock('@vitejs/plugin-rsc/vendor/react-server-dom/server.edge', () => ({ + registerClientReference: vi.fn(), + registerServerReference(reference: Function, id: string, name: string) { + return Object.defineProperties(reference, { + $$typeof: { value: Symbol.for('react.server.reference') }, + $$id: { value: `${id}#${name}` }, + $$bound: { value: null, writable: true }, + }) + }, +})) + +const { createServerManifest, setRequireModule } = await import('./rsc') + +beforeAll(() => { + setRequireModule({ + load() { + throw new Error('preserved references must not load their implementation') + }, + }) +}) + +describe('createServerManifest', () => { + it('preserves server references without loading their implementation', async () => { + const manifest = createServerManifest({ preserveServerReferences: true }) + const entry = manifest['module-id#action']! + expect(entry.id).toContain('$$decode-server-reference:module-id') + + const module = await (globalThis as any).__vite_rsc_require__(entry.id) + expect(Object.prototype.hasOwnProperty.call(module, 'action')).toBe(true) + const reference = module.action + expect(reference.$$typeof).toBe(Symbol.for('react.server.reference')) + expect(reference.$$id).toBe('module-id#action') + expect(reference.$$bound).toBeNull() + }) +}) diff --git a/packages/plugin-rsc/src/core/rsc.ts b/packages/plugin-rsc/src/core/rsc.ts index ecd46ea47..ad74b3a66 100644 --- a/packages/plugin-rsc/src/core/rsc.ts +++ b/packages/plugin-rsc/src/core/rsc.ts @@ -2,6 +2,7 @@ import { memoize, tinyassert } from '@hiogawa/utils' import type { BundlerConfig, ImportManifestEntry, ModuleMap } from '../types' import { SERVER_DECODE_CLIENT_PREFIX, + SERVER_DECODE_REFERENCE_PREFIX, SERVER_REFERENCE_PREFIX, createReferenceCacheTag, removeReferenceCacheTag, @@ -27,6 +28,28 @@ export function setRequireModule(options: { // need memoize to return stable promise from __webpack_require__ ;(globalThis as any).__vite_rsc_server_require__ = memoize( async (id: string) => { + if (id.startsWith(SERVER_DECODE_REFERENCE_PREFIX)) { + id = id.slice(SERVER_DECODE_REFERENCE_PREFIX.length) + id = removeReferenceCacheTag(id) + const target = {} as Record + return new Proxy(target, { + getOwnPropertyDescriptor(_target, name) { + if (typeof name !== 'string' || name === 'then') { + return Reflect.getOwnPropertyDescriptor(target, name) + } + target[name] ??= ReactServer.registerServerReference( + () => { + throw new Error( + `Unexpectedly preserved server reference '${id}#${name}' is called on server`, + ) + }, + id, + name, + ) + return Reflect.getOwnPropertyDescriptor(target, name) + }, + }) + } if (id.startsWith(SERVER_DECODE_CLIENT_PREFIX)) { // decode client reference on the server id = id.slice(SERVER_DECODE_CLIENT_PREFIX.length) @@ -71,7 +94,9 @@ export async function loadServerAction(id: string): Promise { return mod[name] } -export function createServerManifest(): BundlerConfig { +export function createServerManifest(options?: { + preserveServerReferences?: boolean +}): BundlerConfig { const cacheTag = import.meta.env.DEV ? createReferenceCacheTag() : '' return new Proxy( @@ -83,7 +108,13 @@ export function createServerManifest(): BundlerConfig { tinyassert(id) tinyassert(name) return { - id: SERVER_REFERENCE_PREFIX + id + cacheTag, + id: + SERVER_REFERENCE_PREFIX + + (options?.preserveServerReferences + ? SERVER_DECODE_REFERENCE_PREFIX + : '') + + id + + cacheTag, name, chunks: [], async: true, diff --git a/packages/plugin-rsc/src/core/shared.ts b/packages/plugin-rsc/src/core/shared.ts index bd3d18af3..937455395 100644 --- a/packages/plugin-rsc/src/core/shared.ts +++ b/packages/plugin-rsc/src/core/shared.ts @@ -3,6 +3,8 @@ export const SERVER_REFERENCE_PREFIX = '$$server:' export const SERVER_DECODE_CLIENT_PREFIX = '$$decode-client:' +export const SERVER_DECODE_REFERENCE_PREFIX = '$$decode-server-reference:' + // cache bust memoized require promise during dev export function createReferenceCacheTag(): string { const cache = Math.random().toString(36).slice(2) diff --git a/packages/plugin-rsc/src/react/rsc.ts b/packages/plugin-rsc/src/react/rsc.ts index 117fe15ec..33aaca289 100644 --- a/packages/plugin-rsc/src/react/rsc.ts +++ b/packages/plugin-rsc/src/react/rsc.ts @@ -40,16 +40,22 @@ export function renderToReadableStream( export function createFromReadableStream( stream: ReadableStream, - options: CreateFromReadableStreamEdgeOptions = {}, + options: CreateFromReadableStreamEdgeOptions & { + /** Preserve decoded server references so the value can be rendered again. */ + serverReferences?: 'resolve' | 'preserve' + } = {}, ): Promise { + const { serverReferences = 'resolve', ...reactOptions } = options return ReactClient.createFromReadableStream(stream, { serverConsumerManifest: { // https://github.com/facebook/react/pull/31300 // https://github.com/vercel/next.js/pull/71527 - serverModuleMap: createServerManifest(), + serverModuleMap: createServerManifest({ + preserveServerReferences: serverReferences === 'preserve', + }), moduleMap: createServerDecodeClientManifest(), }, - ...options, + ...reactOptions, }) } From 715864b28443d9ba7034073d4a2b56cc9f09dbc1 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 19 Jun 2026 00:37:10 +0100 Subject: [PATCH 2/6] fix(rsc): preserve server manifest export names --- packages/plugin-rsc/src/plugin.test.ts | 56 ++++++++++++++++++++++++++ packages/plugin-rsc/src/plugin.ts | 11 +++-- 2 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-rsc/src/plugin.test.ts diff --git a/packages/plugin-rsc/src/plugin.test.ts b/packages/plugin-rsc/src/plugin.test.ts new file mode 100644 index 000000000..a3d180083 --- /dev/null +++ b/packages/plugin-rsc/src/plugin.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest' +import { vitePluginRscMinimal } from './plugin' + +describe('server reference manifest', () => { + it('preserves and deduplicates existing export names', async () => { + const plugins = vitePluginRscMinimal({ enableActionEncryption: false }) + const minimalPlugin = plugins.find( + (plugin) => plugin.name === 'rsc:minimal', + )! + const manager = (minimalPlugin.api as any).manager + manager.config = { + command: 'build', + root: '/root', + } + + const id = '/root/actions.ts' + manager.serverReferenceMetaMap[id] = { + importId: id, + referenceKey: 'existing', + exportNames: ['cached', 'action', 'cached'], + } + + const useServerPlugin = plugins.find( + (plugin) => plugin.name === 'rsc:use-server', + )! + const transformHandler = (useServerPlugin.transform as any).handler + await transformHandler.call( + { + environment: { name: 'rsc', mode: 'build' }, + error(error: unknown) { + throw error + }, + }, + `"use server"; export async function action() {}`, + id, + ) + + expect(manager.serverReferenceMetaMap[id].exportNames).toEqual([ + 'cached', + 'action', + ]) + + const manifestPlugin = plugins.find( + (plugin) => plugin.name === 'rsc:virtual-vite-rsc/server-references', + )! + const loadHandler = (manifestPlugin.load as any).handler + const manifest = await loadHandler.call( + { environment: { mode: 'build' } }, + '\0virtual:vite-rsc/server-references', + {}, + ) + + expect(manifest.code.match(/\bcached\b/g)).toHaveLength(2) + expect(manifest.code.match(/\baction\b/g)).toHaveLength(2) + }) +}) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 01ec7b083..abe617d0b 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -2017,11 +2017,16 @@ function vitePluginUseServer( delete manager.serverReferenceMetaMap[id] return } + const existingExportNames = + manager.serverReferenceMetaMap[id]?.exportNames ?? [] + const exportNames = + 'names' in result ? result.names : result.exportNames manager.serverReferenceMetaMap[id] = { importId: id, referenceKey: getNormalizedId(), - exportNames: - 'names' in result ? result.names : result.exportNames, + exportNames: [ + ...new Set([...existingExportNames, ...exportNames]), + ], } const importSource = resolvePackage(`${PKG_NAME}/react/rsc`) output.prepend( @@ -2094,7 +2099,7 @@ function vitePluginUseServer( for (const meta of Object.values(manager.serverReferenceMetaMap)) { const key = JSON.stringify(meta.referenceKey) const id = JSON.stringify(meta.importId) - const exports = meta.exportNames + const exports = [...new Set(meta.exportNames)] .map((name) => (name === 'default' ? 'default: _default' : name)) .sort() code += ` From 1d91f40a1ae7bb3147ff24ea84df76285ab28b03 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 19 Jun 2026 00:40:53 +0100 Subject: [PATCH 3/6] feat(rsc): configure server reference markers --- packages/plugin-rsc/src/plugin.test.ts | 44 ++++++++++++++++++++++++++ packages/plugin-rsc/src/plugin.ts | 17 ++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.test.ts b/packages/plugin-rsc/src/plugin.test.ts index a3d180083..3b8280d6c 100644 --- a/packages/plugin-rsc/src/plugin.test.ts +++ b/packages/plugin-rsc/src/plugin.test.ts @@ -53,4 +53,48 @@ describe('server reference manifest', () => { expect(manifest.code.match(/\bcached\b/g)).toHaveLength(2) expect(manifest.code.match(/\baction\b/g)).toHaveLength(2) }) + + it('preserves metadata for configured server reference markers', async () => { + const marker = '/* framework-server-reference */' + const plugins = vitePluginRscMinimal({ + enableActionEncryption: false, + serverReferenceMarkers: [marker, ''], + }) + const minimalPlugin = plugins.find( + (plugin) => plugin.name === 'rsc:minimal', + )! + const manager = (minimalPlugin.api as any).manager + const useServerPlugin = plugins.find( + (plugin) => plugin.name === 'rsc:use-server', + )! + const transformHandler = (useServerPlugin.transform as any).handler + const transformContext = { + environment: { name: 'rsc', mode: 'build' }, + error(error: unknown) { + throw error + }, + } + + const markedId = '/root/marked.ts' + manager.serverReferenceMetaMap[markedId] = { + importId: markedId, + referenceKey: 'marked', + exportNames: ['cached'], + } + await transformHandler.call( + transformContext, + `${marker}\nexport {}`, + markedId, + ) + expect(manager.serverReferenceMetaMap[markedId]).toBeDefined() + + const unmarkedId = '/root/unmarked.ts' + manager.serverReferenceMetaMap[unmarkedId] = { + importId: unmarkedId, + referenceKey: 'unmarked', + exportNames: ['stale'], + } + await transformHandler.call(transformContext, 'export {}', unmarkedId) + expect(manager.serverReferenceMetaMap[unmarkedId]).toBeUndefined() + }) }) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index abe617d0b..9c6dc0bff 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -171,6 +171,13 @@ class RscPluginManager { } export type RscPluginOptions = { + /** + * Source markers emitted by preceding transforms that register server + * references in the plugin manager. Modules containing a marker keep their + * existing server-reference metadata when they do not contain `"use server"`. + * @experimental + */ + serverReferenceMarkers?: string[] /** * shorthand for configuring `environments.(name).build.rollupOptions.input.index` */ @@ -1926,7 +1933,7 @@ function vitePluginDefineEncryptionKey( function vitePluginUseServer( useServerPluginOptions: Pick< RscPluginOptions, - 'enableActionEncryption' | 'environment' + 'enableActionEncryption' | 'environment' | 'serverReferenceMarkers' >, manager: RscPluginManager, ): Plugin[] { @@ -1946,7 +1953,13 @@ function vitePluginUseServer( // filter: { code: 'use server' }, async handler(code, id) { if (!code.includes('use server')) { - delete manager.serverReferenceMetaMap[id] + const hasServerReferenceMarker = + useServerPluginOptions.serverReferenceMarkers?.some( + (marker) => marker.length > 0 && code.includes(marker), + ) ?? false + if (!hasServerReferenceMarker) { + delete manager.serverReferenceMetaMap[id] + } return } let ast = await parseAstAsync(code) From 7c28da34499c9e2b97e6acde95348af9ad02c853 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 30 Jun 2026 23:22:30 +0100 Subject: [PATCH 4/6] test(rsc): prove preserved server references e2e --- packages/plugin-rsc/e2e/basic.test.ts | 3 ++ .../basic/src/routes/serialization/client.tsx | 10 ++++++ .../basic/src/routes/serialization/server.tsx | 36 +++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index 01cadf3b0..ffa3794f5 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -1602,6 +1602,9 @@ function defineTest(f: Fixture) { await page.goto(f.url()) await waitForHydration(page) await expect(page.getByTestId('serialization')).toHaveText('?') + await expect( + page.getByTestId('serialization-preserved-reference'), + ).toHaveText('preserved') await page.getByTestId('serialization').click() await expect(page.getByTestId('serialization')).toHaveText('ok') }) diff --git a/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx index 7815f8072..a32f1345a 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx @@ -16,3 +16,13 @@ export function TestSerializationClient(props: { action: () => Promise }) { ) } + +export function TestPreservedServerReference(props: { + action: () => Promise +}) { + return ( +
+ {typeof props.action === 'function' ? 'preserved' : 'missing'} +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx index ffb7b9699..c818a8645 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx @@ -1,9 +1,10 @@ import { createFromReadableStream, + registerServerReference, renderToReadableStream, } from '@vitejs/plugin-rsc/rsc' import { testSerializationAction } from './action' -import { TestSerializationClient } from './client' +import { TestPreservedServerReference, TestSerializationClient } from './client' export function TestSerializationServer() { const original = @@ -23,5 +24,36 @@ export function TestSerializationServer() { .pipeThrough(new TextEncoderStream()) } const deserialized = createFromReadableStream(serialized) - return
test-serialization:{deserialized}
+ return ( +
+ test-serialization:{deserialized} + +
+ ) +} + +async function TestSerializationPreservedServerReference() { + const deserialized = createFromReadableStream( + renderToReadableStream(createSyntheticServerReferenceElement()), + { + serverReferences: 'preserve', + }, + ) + return <>{deserialized} +} + +function createSyntheticServerReferenceElement() { + return ( + { + throw new Error('This synthetic reference should never be called') + }, + // This id intentionally has no backing module; re-rendering only works + // when the decoded server reference is preserved as an opaque reference. + 'virtual:test-preserved-server-reference', + 'action', + )} + /> + ) } From 47a698c1d3f6ff689881ab3229431afd9bcd4453 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 30 Jun 2026 23:46:33 +0100 Subject: [PATCH 5/6] test(rsc): demonstrate preserved cached server references --- packages/plugin-rsc/e2e/basic.test.ts | 25 +++++++++++-- .../basic/src/framework/entry.rsc.tsx | 5 ++- .../src/framework/server-reference-runtime.ts | 14 ++++++++ .../basic/src/framework/use-cache-runtime.tsx | 3 ++ .../basic/src/routes/serialization/client.tsx | 10 ------ .../basic/src/routes/serialization/server.tsx | 36 ++----------------- .../basic/src/routes/use-cache/client.tsx | 29 +++++++++++++++ .../basic/src/routes/use-cache/server.tsx | 32 +++++++++++++++++ 8 files changed, 106 insertions(+), 48 deletions(-) create mode 100644 packages/plugin-rsc/examples/basic/src/framework/server-reference-runtime.ts create mode 100644 packages/plugin-rsc/examples/basic/src/routes/use-cache/client.tsx diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index ffa3794f5..de8fa75e1 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -1602,9 +1602,6 @@ function defineTest(f: Fixture) { await page.goto(f.url()) await waitForHydration(page) await expect(page.getByTestId('serialization')).toHaveText('?') - await expect( - page.getByTestId('serialization-preserved-reference'), - ).toHaveText('preserved') await page.getByTestId('serialization').click() await expect(page.getByTestId('serialization')).toHaveText('ok') }) @@ -1784,6 +1781,28 @@ function defineTest(f: Fixture) { ) }) + test('use cache replays Flight with framework server action', async ({ + page, + }) => { + await page.goto(f.url()) + await waitForHydration(page) + const locator = page.getByTestId( + 'test-use-cache-flight-replay-server-action', + ) + await expect( + locator.getByTestId('test-use-cache-flight-replay-server-action-cache'), + ).toHaveText('cached product card render count: 1') + await locator.getByRole('button', { name: 'add cached product' }).click() + await expect( + locator.getByTestId('test-use-cache-flight-replay-server-action-result'), + ).toHaveText( + /^added rsc-product-1 with framework action \([1-9]\d*\)$/, + ) + await expect( + locator.getByTestId('test-use-cache-flight-replay-server-action-cache'), + ).toHaveText('cached product card render count: 1') + }) + test('hydration mismatch', async ({ page }) => { const errors: Error[] = [] page.on('pageerror', (error) => { diff --git a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx index e26f0b674..c75bc93a5 100644 --- a/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx +++ b/packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx @@ -9,6 +9,7 @@ import { import type React from 'react' import type { ReactFormState } from 'react-dom/client' import { parseRenderRequest } from './request.tsx' +import { loadFrameworkServerReference } from './server-reference-runtime.ts' // The schema of payload which is serialized into RSC stream on rsc environment // and deserialized on ssr/client environments. @@ -50,7 +51,9 @@ async function handleRequest({ : await request.text() temporaryReferences = createTemporaryReferenceSet() const args = await decodeReply(body, { temporaryReferences }) - const action = await loadServerAction(renderRequest.actionId) + const action = + loadFrameworkServerReference(renderRequest.actionId) ?? + (await loadServerAction(renderRequest.actionId)) try { const data = await action.apply(null, args) returnValue = { ok: true, data } diff --git a/packages/plugin-rsc/examples/basic/src/framework/server-reference-runtime.ts b/packages/plugin-rsc/examples/basic/src/framework/server-reference-runtime.ts new file mode 100644 index 000000000..dc2c94a96 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/framework/server-reference-runtime.ts @@ -0,0 +1,14 @@ +import { registerServerReference } from '@vitejs/plugin-rsc/rsc' + +const frameworkServerReferences = new Map() + +export function registerFrameworkServerReference< + T extends (...args: any[]) => unknown, +>(reference: T, id: string, name: string): T { + frameworkServerReferences.set(`${id}#${name}`, reference) + return registerServerReference(reference, id, name) as T +} + +export function loadFrameworkServerReference(id: string): Function | undefined { + return frameworkServerReferences.get(id) +} diff --git a/packages/plugin-rsc/examples/basic/src/framework/use-cache-runtime.tsx b/packages/plugin-rsc/examples/basic/src/framework/use-cache-runtime.tsx index a25e56c6c..9f43b1be4 100644 --- a/packages/plugin-rsc/examples/basic/src/framework/use-cache-runtime.tsx +++ b/packages/plugin-rsc/examples/basic/src/framework/use-cache-runtime.tsx @@ -65,6 +65,9 @@ export default function cacheWrapper(fn: (...args: any[]) => Promise) { const result = createFromReadableStream(stream, { environmentName: 'Cache', replayConsoleLogs: true, + // Cached RSC streams can contain framework-owned server references whose + // implementation modules are not resolvable by the app bundler runtime. + serverReferences: 'preserve', temporaryReferences: clientTemporaryReferences, }) return result diff --git a/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx index a32f1345a..7815f8072 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/serialization/client.tsx @@ -16,13 +16,3 @@ export function TestSerializationClient(props: { action: () => Promise }) { ) } - -export function TestPreservedServerReference(props: { - action: () => Promise -}) { - return ( -
- {typeof props.action === 'function' ? 'preserved' : 'missing'} -
- ) -} diff --git a/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx index c818a8645..ffb7b9699 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/serialization/server.tsx @@ -1,10 +1,9 @@ import { createFromReadableStream, - registerServerReference, renderToReadableStream, } from '@vitejs/plugin-rsc/rsc' import { testSerializationAction } from './action' -import { TestPreservedServerReference, TestSerializationClient } from './client' +import { TestSerializationClient } from './client' export function TestSerializationServer() { const original = @@ -24,36 +23,5 @@ export function TestSerializationServer() { .pipeThrough(new TextEncoderStream()) } const deserialized = createFromReadableStream(serialized) - return ( -
- test-serialization:{deserialized} - -
- ) -} - -async function TestSerializationPreservedServerReference() { - const deserialized = createFromReadableStream( - renderToReadableStream(createSyntheticServerReferenceElement()), - { - serverReferences: 'preserve', - }, - ) - return <>{deserialized} -} - -function createSyntheticServerReferenceElement() { - return ( - { - throw new Error('This synthetic reference should never be called') - }, - // This id intentionally has no backing module; re-rendering only works - // when the decoded server reference is preserved as an opaque reference. - 'virtual:test-preserved-server-reference', - 'action', - )} - /> - ) + return
test-serialization:{deserialized}
} diff --git a/packages/plugin-rsc/examples/basic/src/routes/use-cache/client.tsx b/packages/plugin-rsc/examples/basic/src/routes/use-cache/client.tsx new file mode 100644 index 000000000..0b6c00236 --- /dev/null +++ b/packages/plugin-rsc/examples/basic/src/routes/use-cache/client.tsx @@ -0,0 +1,29 @@ +'use client' + +import React from 'react' + +export function TestUseCacheFlightReplayServerActionClient(props: { + addToCart: (productId: string) => Promise + productId: string + renderCount: number +}) { + const [result, setResult] = React.useState('idle') + + return ( +
+ + cached product card render count: {props.renderCount} + + + + {result} + +
+ ) +} diff --git a/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx b/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx index 9c5e09f4c..d09808edf 100644 --- a/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx +++ b/packages/plugin-rsc/examples/basic/src/routes/use-cache/server.tsx @@ -1,4 +1,6 @@ +import { registerFrameworkServerReference } from '../../framework/server-reference-runtime' import { revalidateCache } from '../../framework/use-cache-runtime' +import { TestUseCacheFlightReplayServerActionClient } from './client' export function TestUseCache() { return ( @@ -6,6 +8,7 @@ export function TestUseCache() { + ) } @@ -103,3 +106,32 @@ let outerFnArg = '' let innerFnArg = '' let innerFnCount = 0 let actionCount2 = 0 + +async function TestUseCacheFlightReplayServerAction() { + return +} + +async function CachedProductCard(props: { productId: string }) { + 'use cache' + cachedProductCardRenderCount++ + return ( + + ) +} + +let cachedProductCardRenderCount = 0 +let cartActionCount = 0 + +const addCachedProductToCart = registerFrameworkServerReference( + async (productId: string) => { + cartActionCount++ + return `added ${productId} with framework action (${cartActionCount})` + }, + 'framework:cached-flight-product-card', + 'addToCart', +) From 4196777c3c75e833f33848ea21a3faf2d0836e28 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 30 Jun 2026 23:54:37 +0100 Subject: [PATCH 6/6] chore: format preserved reference e2e --- packages/plugin-rsc/e2e/basic.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/plugin-rsc/e2e/basic.test.ts b/packages/plugin-rsc/e2e/basic.test.ts index de8fa75e1..8bc3470d9 100644 --- a/packages/plugin-rsc/e2e/basic.test.ts +++ b/packages/plugin-rsc/e2e/basic.test.ts @@ -1795,9 +1795,7 @@ function defineTest(f: Fixture) { await locator.getByRole('button', { name: 'add cached product' }).click() await expect( locator.getByTestId('test-use-cache-flight-replay-server-action-result'), - ).toHaveText( - /^added rsc-product-1 with framework action \([1-9]\d*\)$/, - ) + ).toHaveText(/^added rsc-product-1 with framework action \([1-9]\d*\)$/) await expect( locator.getByTestId('test-use-cache-flight-replay-server-action-cache'), ).toHaveText('cached product card render count: 1')