Skip to content

Commit 454c742

Browse files
hi-ogawaclaude
andauthored
feat(plugin-rsc): expose onClientReference callback in renderToReadableStream (#1079)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent de0aebe commit 454c742

File tree

6 files changed

+102
-3
lines changed

6 files changed

+102
-3
lines changed

packages/plugin-rsc/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,30 @@ This module re-exports RSC runtime API provided by `react-server-dom/server.edge
453453
- `decodeAction/decodeReply/decodeFormState/loadServerAction/createTemporaryReferenceSet`
454454
- `encodeReply/createClientTemporaryReferenceSet`
455455

456+
#### Vite-specific extension: `renderToReadableStream` (experimental)
457+
458+
> [!NOTE]
459+
> This is a Vite-specific extension to the standard React RSC API. The official `react-server-dom` does not provide this callback mechanism.
460+
461+
`renderToReadableStream` API is extended with an optional third parameter with `onClientReference` callback.
462+
This is invoked whenever a client reference is used in RSC stream rendering.
463+
464+
```ts
465+
function renderToReadableStream<T>(
466+
data: T,
467+
// standard options (e.g. temporaryReferences, onError, etc.)
468+
options?: object,
469+
// vite-specific options
470+
extraOptions?: {
471+
onClientReference?: (metadata: {
472+
id: string
473+
name: string
474+
deps: { js: string[]; css: string[] }
475+
}) => void
476+
},
477+
): ReadableStream<Uint8Array>
478+
```
479+
456480
### `@vitejs/plugin-rsc/ssr`
457481

458482
This module re-exports RSC runtime API provided by `react-server-dom/client.edge`

packages/plugin-rsc/e2e/basic.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ function defineTest(f: Fixture) {
237237
expect(f.proc().stderr()).toBe('')
238238
})
239239

240+
test('onClientReference callback', async ({ page }) => {
241+
const response = await page.request.get(f.url('__test_onClientReference'))
242+
expect(response.ok()).toBe(true)
243+
const data = await response.json()
244+
expect(data).toEqual(
245+
expect.arrayContaining([
246+
expect.objectContaining({
247+
id: expect.any(String),
248+
name: expect.any(String),
249+
deps: expect.objectContaining({
250+
js: expect.any(Array),
251+
css: expect.any(Array),
252+
}),
253+
}),
254+
]),
255+
)
256+
})
257+
240258
test('client component', async ({ page }) => {
241259
await page.goto(f.url())
242260
await waitForHydration(page)

packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,18 @@ async function handleRequest({
7979

8080
const rscPayload: RscPayload = { root: getRoot(), formState, returnValue }
8181
const rscOptions = { temporaryReferences }
82-
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions)
82+
const debugClientReferences: unknown[] = []
83+
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions, {
84+
onClientReference(metadata) {
85+
debugClientReferences.push(metadata)
86+
},
87+
})
88+
89+
// test `onClientReference` callback
90+
if (renderRequest.url.pathname === '/__test_onClientReference') {
91+
await rscStream.pipeTo(new WritableStream({ write() {} }))
92+
return Response.json(debugClientReferences)
93+
}
8394

8495
// Respond RSC stream without HTML rendering as decided by `RenderRequest`
8596
if (renderRequest.isRsc) {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,12 @@ export function createServerDecodeClientManifest(): ModuleMap {
116116
)
117117
}
118118

119-
export function createClientManifest(): BundlerConfig {
119+
export function createClientManifest(options?: {
120+
/**
121+
* @internal
122+
*/
123+
onClientReference?: (metadata: { id: string; name: string }) => void
124+
}): BundlerConfig {
120125
const cacheTag = import.meta.env.DEV ? createReferenceCacheTag() : ''
121126

122127
return new Proxy(
@@ -127,6 +132,7 @@ export function createClientManifest(): BundlerConfig {
127132
let [id, name] = $$id.split('#')
128133
tinyassert(id)
129134
tinyassert(name)
135+
options?.onClientReference?.({ id, name })
130136
return {
131137
id: id + cacheTag,
132138
name,

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@ export { loadServerAction, setRequireModule } from '../core/rsc'
1414
export function renderToReadableStream<T>(
1515
data: T,
1616
options?: object,
17+
extraOptions?: {
18+
/**
19+
* @internal
20+
*/
21+
onClientReference?: (metadata: { id: string; name: string }) => void
22+
},
1723
): ReadableStream<Uint8Array> {
1824
return ReactServer.renderToReadableStream(
1925
data,
20-
createClientManifest(),
26+
createClientManifest({
27+
onClientReference: extraOptions?.onClientReference,
28+
}),
2129
options,
2230
)
2331
}

packages/plugin-rsc/src/rsc.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import assetsManifest from 'virtual:vite-rsc/assets-manifest'
12
import serverReferences from 'virtual:vite-rsc/server-references'
23
import { setRequireModule } from './core/rsc'
4+
import type { ResolvedAssetDeps } from './plugin'
35
import { toReferenceValidationVirtual } from './plugins/shared'
6+
import { renderToReadableStream as originalRenderToReadableStream } from './react/rsc'
47

58
export {
69
createClientManifest,
@@ -36,3 +39,32 @@ function initialize(): void {
3639
},
3740
})
3841
}
42+
43+
export function renderToReadableStream<T>(
44+
data: T,
45+
options?: object,
46+
extraOptions?: {
47+
/**
48+
* @experimental
49+
*/
50+
onClientReference?: (metadata: {
51+
id: string
52+
name: string
53+
deps: ResolvedAssetDeps
54+
}) => void
55+
},
56+
): ReadableStream<Uint8Array> {
57+
return originalRenderToReadableStream(data, options, {
58+
onClientReference(metadata) {
59+
const deps = assetsManifest.clientReferenceDeps[metadata.id] ?? {
60+
js: [],
61+
css: [],
62+
}
63+
extraOptions?.onClientReference?.({
64+
id: metadata.id,
65+
name: metadata.name,
66+
deps,
67+
})
68+
},
69+
})
70+
}

0 commit comments

Comments
 (0)