Skip to content

Commit 8314caa

Browse files
hi-ogawaclaude
andcommitted
feat(plugin-rsc): expose onClientReference callback in renderToReadableStream
Add an experimental `onClientReference` callback option to `renderToReadableStream` that fires when a client reference is encountered during RSC rendering. The callback provides the reference id, name, and resolved asset dependencies (JS/CSS files). This enables use cases like: - Tracking which client components are used in a render - Collecting asset dependencies for preloading - Debugging client reference resolution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent de0aebe commit 8314caa

5 files changed

Lines changed: 76 additions & 5 deletions

File tree

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: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,19 @@ 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, {
84+
...rscOptions,
85+
onClientReference(metadata) {
86+
debugClientReferences.push(metadata)
87+
},
88+
})
89+
90+
// test `onClientReference` callback
91+
if (renderRequest.url.pathname === '/__test_onClientReference') {
92+
await rscStream.pipeTo(new WritableStream({ write() {} }))
93+
return Response.json(debugClientReferences)
94+
}
8395

8496
// Respond RSC stream without HTML rendering as decided by `RenderRequest`
8597
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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ export { loadServerAction, setRequireModule } from '../core/rsc'
1313

1414
export function renderToReadableStream<T>(
1515
data: T,
16-
options?: object,
16+
options?: object & {
17+
/**
18+
* @internal
19+
*/
20+
onClientReference?: (metadata: { id: string; name: string }) => void
21+
},
1722
): ReadableStream<Uint8Array> {
23+
const { onClientReference, ...restOptions } = options || {}
1824
return ReactServer.renderToReadableStream(
1925
data,
20-
createClientManifest(),
21-
options,
26+
createClientManifest({ onClientReference }),
27+
restOptions,
2228
)
2329
}
2430

packages/plugin-rsc/src/rsc.tsx

Lines changed: 29 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,29 @@ function initialize(): void {
3639
},
3740
})
3841
}
42+
43+
export function renderToReadableStream<T>(
44+
data: T,
45+
options?: object & {
46+
/**
47+
* @experimental
48+
*/
49+
onClientReference?: (metadata: {
50+
id: string
51+
name: string
52+
deps: ResolvedAssetDeps
53+
}) => void
54+
},
55+
): ReadableStream<Uint8Array> {
56+
const { onClientReference, ...restOptions } = options || {}
57+
return originalRenderToReadableStream(data, {
58+
...restOptions,
59+
onClientReference(metadata) {
60+
const deps = assetsManifest.clientReferenceDeps[metadata.id] ?? {
61+
js: [],
62+
css: [],
63+
}
64+
onClientReference?.({ id: metadata.id, name: metadata.name, deps })
65+
},
66+
})
67+
}

0 commit comments

Comments
 (0)