Skip to content

Commit a0b2af7

Browse files
committed
feat(rsc): preserve decoded server references
1 parent 0135881 commit a0b2af7

4 files changed

Lines changed: 81 additions & 5 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { beforeAll, describe, expect, it, vi } from 'vitest'
2+
3+
vi.mock('@vitejs/plugin-rsc/vendor/react-server-dom/server.edge', () => ({
4+
registerClientReference: vi.fn(),
5+
registerServerReference(reference: Function, id: string, name: string) {
6+
return Object.defineProperties(reference, {
7+
$$typeof: { value: Symbol.for('react.server.reference') },
8+
$$id: { value: `${id}#${name}` },
9+
$$bound: { value: null, writable: true },
10+
})
11+
},
12+
}))
13+
14+
const { createServerManifest, setRequireModule } = await import('./rsc')
15+
16+
beforeAll(() => {
17+
setRequireModule({
18+
load() {
19+
throw new Error('preserved references must not load their implementation')
20+
},
21+
})
22+
})
23+
24+
describe('createServerManifest', () => {
25+
it('preserves server references without loading their implementation', async () => {
26+
const manifest = createServerManifest({ preserveServerReferences: true })
27+
const entry = manifest['module-id#action']!
28+
expect(entry.id).toContain('$$decode-server-reference:module-id')
29+
30+
const module = await (globalThis as any).__vite_rsc_require__(entry.id)
31+
expect(Object.prototype.hasOwnProperty.call(module, 'action')).toBe(true)
32+
const reference = module.action
33+
expect(reference.$$typeof).toBe(Symbol.for('react.server.reference'))
34+
expect(reference.$$id).toBe('module-id#action')
35+
expect(reference.$$bound).toBeNull()
36+
})
37+
})

packages/plugin-rsc/src/core/rsc.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { memoize, tinyassert } from '@hiogawa/utils'
22
import type { BundlerConfig, ImportManifestEntry, ModuleMap } from '../types'
33
import {
44
SERVER_DECODE_CLIENT_PREFIX,
5+
SERVER_DECODE_REFERENCE_PREFIX,
56
SERVER_REFERENCE_PREFIX,
67
createReferenceCacheTag,
78
removeReferenceCacheTag,
@@ -27,6 +28,28 @@ export function setRequireModule(options: {
2728
// need memoize to return stable promise from __webpack_require__
2829
;(globalThis as any).__vite_rsc_server_require__ = memoize(
2930
async (id: string) => {
31+
if (id.startsWith(SERVER_DECODE_REFERENCE_PREFIX)) {
32+
id = id.slice(SERVER_DECODE_REFERENCE_PREFIX.length)
33+
id = removeReferenceCacheTag(id)
34+
const target = {} as Record<string, unknown>
35+
return new Proxy(target, {
36+
getOwnPropertyDescriptor(_target, name) {
37+
if (typeof name !== 'string' || name === 'then') {
38+
return Reflect.getOwnPropertyDescriptor(target, name)
39+
}
40+
target[name] ??= ReactServer.registerServerReference(
41+
() => {
42+
throw new Error(
43+
`Unexpectedly preserved server reference '${id}#${name}' is called on server`,
44+
)
45+
},
46+
id,
47+
name,
48+
)
49+
return Reflect.getOwnPropertyDescriptor(target, name)
50+
},
51+
})
52+
}
3053
if (id.startsWith(SERVER_DECODE_CLIENT_PREFIX)) {
3154
// decode client reference on the server
3255
id = id.slice(SERVER_DECODE_CLIENT_PREFIX.length)
@@ -71,7 +94,9 @@ export async function loadServerAction(id: string): Promise<Function> {
7194
return mod[name]
7295
}
7396

74-
export function createServerManifest(): BundlerConfig {
97+
export function createServerManifest(options?: {
98+
preserveServerReferences?: boolean
99+
}): BundlerConfig {
75100
const cacheTag = import.meta.env.DEV ? createReferenceCacheTag() : ''
76101

77102
return new Proxy(
@@ -83,7 +108,13 @@ export function createServerManifest(): BundlerConfig {
83108
tinyassert(id)
84109
tinyassert(name)
85110
return {
86-
id: SERVER_REFERENCE_PREFIX + id + cacheTag,
111+
id:
112+
SERVER_REFERENCE_PREFIX +
113+
(options?.preserveServerReferences
114+
? SERVER_DECODE_REFERENCE_PREFIX
115+
: '') +
116+
id +
117+
cacheTag,
87118
name,
88119
chunks: [],
89120
async: true,

packages/plugin-rsc/src/core/shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ export const SERVER_REFERENCE_PREFIX = '$$server:'
33

44
export const SERVER_DECODE_CLIENT_PREFIX = '$$decode-client:'
55

6+
export const SERVER_DECODE_REFERENCE_PREFIX = '$$decode-server-reference:'
7+
68
// cache bust memoized require promise during dev
79
export function createReferenceCacheTag(): string {
810
const cache = Math.random().toString(36).slice(2)

packages/plugin-rsc/src/react/rsc.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,22 @@ export function renderToReadableStream<T>(
4040

4141
export function createFromReadableStream<T>(
4242
stream: ReadableStream<Uint8Array>,
43-
options: CreateFromReadableStreamEdgeOptions = {},
43+
options: CreateFromReadableStreamEdgeOptions & {
44+
/** Preserve decoded server references so the value can be rendered again. */
45+
serverReferences?: 'resolve' | 'preserve'
46+
} = {},
4447
): Promise<T> {
48+
const { serverReferences = 'resolve', ...reactOptions } = options
4549
return ReactClient.createFromReadableStream(stream, {
4650
serverConsumerManifest: {
4751
// https://github.com/facebook/react/pull/31300
4852
// https://github.com/vercel/next.js/pull/71527
49-
serverModuleMap: createServerManifest(),
53+
serverModuleMap: createServerManifest({
54+
preserveServerReferences: serverReferences === 'preserve',
55+
}),
5056
moduleMap: createServerDecodeClientManifest(),
5157
},
52-
...options,
58+
...reactOptions,
5359
})
5460
}
5561

0 commit comments

Comments
 (0)