Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/plugin-rsc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,30 @@ This module re-exports RSC runtime API provided by `react-server-dom/server.edge
- `decodeAction/decodeReply/decodeFormState/loadServerAction/createTemporaryReferenceSet`
- `encodeReply/createClientTemporaryReferenceSet`

#### Vite-specific extension: `renderToReadableStream` (experimental)

> [!NOTE]
> This is a Vite-specific extension to the standard React RSC API. The official `react-server-dom` does not provide this callback mechanism.

`renderToReadableStream` API is extended with an optional third parameter with `onClientReference` callback.
This is invoked whenever a client reference is used in RSC stream rendering.

```ts
function renderToReadableStream<T>(
data: T,
// standard options (e.g. temporaryReferences, onError, etc.)
options?: object,
// vite-specific options
extraOptions?: {
onClientReference?: (metadata: {
id: string
name: string
deps: { js: string[]; css: string[] }
}) => void
},
): ReadableStream<Uint8Array>
```

### `@vitejs/plugin-rsc/ssr`

This module re-exports RSC runtime API provided by `react-server-dom/client.edge`
Expand Down
18 changes: 18 additions & 0 deletions packages/plugin-rsc/e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import path from 'node:path'
Expand Down Expand Up @@ -237,6 +237,24 @@
expect(f.proc().stderr()).toBe('')
})

test('onClientReference callback', async ({ page }) => {
const response = await page.request.get(f.url('__test_onClientReference'))
expect(response.ok()).toBe(true)
const data = await response.json()
expect(data).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: expect.any(String),
deps: expect.objectContaining({
js: expect.any(Array),
css: expect.any(Array),
}),
}),
]),
)
})

test('client component', async ({ page }) => {
await page.goto(f.url())
await waitForHydration(page)
Expand Down
13 changes: 12 additions & 1 deletion packages/plugin-rsc/examples/basic/src/framework/entry.rsc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,18 @@ async function handleRequest({

const rscPayload: RscPayload = { root: getRoot(), formState, returnValue }
const rscOptions = { temporaryReferences }
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions)
const debugClientReferences: unknown[] = []
const rscStream = renderToReadableStream<RscPayload>(rscPayload, rscOptions, {
onClientReference(metadata) {
debugClientReferences.push(metadata)
},
})

// test `onClientReference` callback
if (renderRequest.url.pathname === '/__test_onClientReference') {
await rscStream.pipeTo(new WritableStream({ write() {} }))
return Response.json(debugClientReferences)
}

// Respond RSC stream without HTML rendering as decided by `RenderRequest`
if (renderRequest.isRsc) {
Expand Down
8 changes: 7 additions & 1 deletion packages/plugin-rsc/src/core/rsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ export function createServerDecodeClientManifest(): ModuleMap {
)
}

export function createClientManifest(): BundlerConfig {
export function createClientManifest(options?: {
/**
* @internal
*/
onClientReference?: (metadata: { id: string; name: string }) => void
}): BundlerConfig {
const cacheTag = import.meta.env.DEV ? createReferenceCacheTag() : ''

return new Proxy(
Expand All @@ -127,6 +132,7 @@ export function createClientManifest(): BundlerConfig {
let [id, name] = $$id.split('#')
tinyassert(id)
tinyassert(name)
options?.onClientReference?.({ id, name })
return {
id: id + cacheTag,
name,
Expand Down
10 changes: 9 additions & 1 deletion packages/plugin-rsc/src/react/rsc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,18 @@ export { loadServerAction, setRequireModule } from '../core/rsc'
export function renderToReadableStream<T>(
data: T,
options?: object,
extraOptions?: {
/**
* @internal
*/
onClientReference?: (metadata: { id: string; name: string }) => void
},
): ReadableStream<Uint8Array> {
return ReactServer.renderToReadableStream(
data,
createClientManifest(),
createClientManifest({
onClientReference: extraOptions?.onClientReference,
}),
options,
)
}
Expand Down
32 changes: 32 additions & 0 deletions packages/plugin-rsc/src/rsc.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import assetsManifest from 'virtual:vite-rsc/assets-manifest'
import serverReferences from 'virtual:vite-rsc/server-references'
import { setRequireModule } from './core/rsc'
import type { ResolvedAssetDeps } from './plugin'
import { toReferenceValidationVirtual } from './plugins/shared'
import { renderToReadableStream as originalRenderToReadableStream } from './react/rsc'

export {
createClientManifest,
Expand Down Expand Up @@ -36,3 +39,32 @@ function initialize(): void {
},
})
}

export function renderToReadableStream<T>(
data: T,
options?: object,
extraOptions?: {
/**
* @experimental
*/
onClientReference?: (metadata: {
id: string
name: string
deps: ResolvedAssetDeps
}) => void
},
): ReadableStream<Uint8Array> {
return originalRenderToReadableStream(data, options, {
onClientReference(metadata) {
const deps = assetsManifest.clientReferenceDeps[metadata.id] ?? {
js: [],
css: [],
}
extraOptions?.onClientReference?.({
id: metadata.id,
name: metadata.name,
deps,
})
},
})
}
Loading