Skip to content

Commit 3706026

Browse files
feat(plugin-rsc): expose Node.js stream APIs (renderToPipeableStream, createFromNodeStream)
Closes #1162
1 parent 707a46c commit 3706026

5 files changed

Lines changed: 333 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type { Readable } from 'node:stream'
2+
// @ts-ignore
3+
import * as ReactClient from '@vitejs/plugin-rsc/vendor/react-server-dom/client.node'
4+
// @ts-ignore
5+
import * as ReactServer from '@vitejs/plugin-rsc/vendor/react-server-dom/server.node'
6+
import type { ReactFormState } from 'react-dom/client'
7+
import {
8+
createClientManifest,
9+
createServerDecodeClientManifest,
10+
createServerManifest,
11+
} from '../core/rsc'
12+
import type {
13+
ClientTemporaryReferenceSet,
14+
DecodeReplyFunction,
15+
EncodeReplyFunction,
16+
RenderToReadableStreamOptions,
17+
ServerTemporaryReferenceSet,
18+
} from '../types'
19+
20+
export { loadServerAction, setRequireModule } from '../core/rsc'
21+
22+
export interface PipeableStream {
23+
abort(reason: unknown): void
24+
pipe<Writable extends NodeJS.WritableStream>(destination: Writable): Writable
25+
}
26+
27+
export function renderToPipeableStream<T>(
28+
data: T,
29+
options?: RenderToReadableStreamOptions,
30+
extraOptions?: {
31+
/**
32+
* @internal
33+
*/
34+
onClientReference?: (metadata: { id: string; name: string }) => void
35+
},
36+
): PipeableStream {
37+
return ReactServer.renderToPipeableStream(
38+
data,
39+
createClientManifest({
40+
onClientReference: extraOptions?.onClientReference,
41+
}),
42+
options,
43+
)
44+
}
45+
46+
export function renderToReadableStream<T>(
47+
data: T,
48+
options?: RenderToReadableStreamOptions,
49+
extraOptions?: {
50+
/**
51+
* @internal
52+
*/
53+
onClientReference?: (metadata: { id: string; name: string }) => void
54+
},
55+
): ReadableStream<Uint8Array> {
56+
return ReactServer.renderToReadableStream(
57+
data,
58+
createClientManifest({
59+
onClientReference: extraOptions?.onClientReference,
60+
}),
61+
options,
62+
)
63+
}
64+
65+
export function createFromReadableStream<T>(
66+
stream: ReadableStream<Uint8Array>,
67+
options: object = {},
68+
): Promise<T> {
69+
return ReactClient.createFromReadableStream(stream, {
70+
serverConsumerManifest: {
71+
serverModuleMap: createServerManifest(),
72+
moduleMap: createServerDecodeClientManifest(),
73+
},
74+
...options,
75+
})
76+
}
77+
78+
export function createFromNodeStream<T>(
79+
stream: Readable,
80+
options: object = {},
81+
): Promise<T> {
82+
return ReactClient.createFromNodeStream(stream, {
83+
serverConsumerManifest: {
84+
serverModuleMap: createServerManifest(),
85+
moduleMap: createServerDecodeClientManifest(),
86+
},
87+
...options,
88+
})
89+
}
90+
91+
export function registerClientReference<T>(
92+
proxy: T,
93+
id: string,
94+
name: string,
95+
): T {
96+
return ReactServer.registerClientReference(proxy, id, name)
97+
}
98+
99+
export const registerServerReference: <T>(
100+
ref: T,
101+
id: string,
102+
name: string,
103+
) => T = ReactServer.registerServerReference
104+
105+
export const decodeReply: DecodeReplyFunction = (body, options) =>
106+
ReactServer.decodeReply(body, createServerManifest(), options)
107+
108+
export function decodeAction(body: FormData): Promise<() => Promise<void>> {
109+
return ReactServer.decodeAction(body, createServerManifest())
110+
}
111+
112+
export function decodeFormState(
113+
actionResult: unknown,
114+
body: FormData,
115+
): Promise<ReactFormState | undefined> {
116+
return ReactServer.decodeFormState(actionResult, body, createServerManifest())
117+
}
118+
119+
export const createTemporaryReferenceSet: () => ServerTemporaryReferenceSet =
120+
ReactServer.createTemporaryReferenceSet
121+
122+
export const encodeReply: EncodeReplyFunction = ReactClient.encodeReply
123+
124+
export const createClientTemporaryReferenceSet: () => ClientTemporaryReferenceSet =
125+
ReactClient.createTemporaryReferenceSet
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Readable } from 'node:stream'
2+
// @ts-ignore
3+
import * as ReactClient from '@vitejs/plugin-rsc/vendor/react-server-dom/client.node'
4+
import { createServerConsumerManifest } from '../core/ssr'
5+
6+
export { setRequireModule } from '../core/ssr'
7+
8+
export function createFromReadableStream<T>(
9+
stream: ReadableStream<Uint8Array>,
10+
options: object = {},
11+
): Promise<T> {
12+
return ReactClient.createFromReadableStream(stream, {
13+
serverConsumerManifest: createServerConsumerManifest(),
14+
...options,
15+
})
16+
}
17+
18+
export function createFromNodeStream<T>(
19+
stream: Readable,
20+
options: object = {},
21+
): Promise<T> {
22+
return ReactClient.createFromNodeStream(stream, {
23+
serverConsumerManifest: createServerConsumerManifest(),
24+
...options,
25+
})
26+
}
27+
28+
export function createServerReference(id: string): unknown {
29+
return ReactClient.createServerReference(id)
30+
}
31+
32+
export const callServer = null
33+
export const findSourceMapURL = null
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import assetsManifest from 'virtual:vite-rsc/assets-manifest'
2+
import serverReferences from 'virtual:vite-rsc/server-references'
3+
import { setRequireModule } from './core/rsc'
4+
import type { ResolvedAssetDeps } from './plugin'
5+
import { toReferenceValidationVirtual } from './plugins/shared'
6+
import { renderToPipeableStream as originalRenderToPipeableStream } from './react/rsc.node'
7+
import type { PipeableStream } from './react/rsc.node'
8+
9+
export {
10+
createClientManifest,
11+
createServerManifest,
12+
loadServerAction,
13+
} from './core/rsc'
14+
15+
export {
16+
encryptActionBoundArgs,
17+
decryptActionBoundArgs,
18+
} from './utils/encryption-runtime'
19+
20+
export * from './react/rsc.node'
21+
22+
initialize()
23+
24+
function initialize(): void {
25+
setRequireModule({
26+
load: async (id) => {
27+
if (!import.meta.env.__vite_rsc_build__) {
28+
await import(
29+
/* @vite-ignore */ '/@id/__x00__' +
30+
toReferenceValidationVirtual({ id, type: 'server' })
31+
)
32+
return import(/* @vite-ignore */ id)
33+
} else {
34+
const import_ = serverReferences[id]
35+
if (!import_) {
36+
throw new Error(`server reference not found '${id}'`)
37+
}
38+
return import_()
39+
}
40+
},
41+
})
42+
}
43+
44+
export function renderToPipeableStream<T>(
45+
data: T,
46+
options?: object,
47+
extraOptions?: {
48+
/**
49+
* @experimental
50+
*/
51+
onClientReference?: (metadata: {
52+
id: string
53+
name: string
54+
deps: ResolvedAssetDeps
55+
}) => void
56+
},
57+
): PipeableStream {
58+
return originalRenderToPipeableStream(data, options, {
59+
onClientReference(metadata) {
60+
const deps = assetsManifest.clientReferenceDeps[metadata.id] ?? {
61+
js: [],
62+
css: [],
63+
}
64+
extraOptions?.onClientReference?.({
65+
id: metadata.id,
66+
name: metadata.name,
67+
deps,
68+
})
69+
},
70+
})
71+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as ReactDOM from 'react-dom'
2+
import assetsManifest from 'virtual:vite-rsc/assets-manifest'
3+
import * as clientReferences from 'virtual:vite-rsc/client-references'
4+
import { setRequireModule } from './core/ssr'
5+
import type { ResolvedAssetDeps } from './plugin'
6+
import { toCssVirtual, toReferenceValidationVirtual } from './plugins/shared'
7+
8+
export { createServerConsumerManifest } from './core/ssr'
9+
10+
export * from './react/ssr.node'
11+
12+
/**
13+
* Callback type for client reference dependency notifications.
14+
* Called during SSR when a client component's dependencies are loaded.
15+
* @experimental
16+
*/
17+
export type OnClientReference = (reference: {
18+
id: string
19+
deps: ResolvedAssetDeps
20+
}) => void
21+
22+
// Registered callback for client reference deps
23+
let onClientReference: OnClientReference | undefined
24+
25+
/**
26+
* Register a callback to be notified when client reference dependencies are loaded.
27+
* Called during SSR when a client component is accessed.
28+
* @experimental
29+
*/
30+
export function setOnClientReference(
31+
callback: OnClientReference | undefined,
32+
): void {
33+
onClientReference = callback
34+
}
35+
36+
initialize()
37+
38+
function initialize(): void {
39+
setRequireModule({
40+
load: async (id) => {
41+
if (!import.meta.env.__vite_rsc_build__) {
42+
await import(
43+
/* @vite-ignore */ '/@id/__x00__' +
44+
toReferenceValidationVirtual({ id, type: 'client' })
45+
)
46+
const mod = await import(/* @vite-ignore */ id)
47+
const modCss = await import(
48+
/* @vite-ignore */ '/@id/__x00__' + toCssVirtual({ id, type: 'ssr' })
49+
)
50+
return wrapResourceProxy(mod, id, { js: [], css: modCss.default })
51+
} else {
52+
const import_ = clientReferences.default[id]
53+
if (!import_) {
54+
throw new Error(`client reference not found '${id}'`)
55+
}
56+
const deps = assetsManifest.clientReferenceDeps[id] ?? {
57+
js: [],
58+
css: [],
59+
}
60+
// kick off preload/notify before initial async import, which is not sync-cached
61+
preloadDeps(deps)
62+
onClientReference?.({ id, deps })
63+
const mod: any = await import_()
64+
return wrapResourceProxy(mod, id, deps)
65+
}
66+
},
67+
})
68+
}
69+
70+
// preload/preinit during getter access since `load` is cached on production.
71+
// also notify `onClientReference` callback here since module export access is not memoized by React.
72+
function wrapResourceProxy(mod: any, id: string, deps: ResolvedAssetDeps) {
73+
return new Proxy(mod, {
74+
get(target, p, receiver) {
75+
if (p in mod) {
76+
preloadDeps(deps)
77+
onClientReference?.({ id, deps })
78+
}
79+
return Reflect.get(target, p, receiver)
80+
},
81+
})
82+
}
83+
84+
function preloadDeps(deps: ResolvedAssetDeps) {
85+
for (const href of deps.js) {
86+
ReactDOM.preloadModule(href, {
87+
as: 'script',
88+
crossOrigin: '',
89+
})
90+
}
91+
for (const href of deps.css) {
92+
ReactDOM.preinit(href, {
93+
as: 'style',
94+
precedence:
95+
assetsManifest.cssLinkPrecedence !== false
96+
? 'vite-rsc/client-reference'
97+
: undefined,
98+
})
99+
}
100+
}

packages/plugin-rsc/tsdown.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ export default defineConfig({
77
'src/plugin.ts',
88
'src/browser.ts',
99
'src/ssr.tsx',
10+
'src/ssr.node.tsx',
1011
'src/rsc.tsx',
12+
'src/rsc.node.tsx',
1113
'src/core/browser.ts',
1214
'src/core/ssr.ts',
1315
'src/core/rsc.ts',
1416
'src/core/plugin.ts',
1517
'src/react/browser.ts',
1618
'src/react/ssr.ts',
19+
'src/react/ssr.node.ts',
1720
'src/react/rsc.ts',
21+
'src/react/rsc.node.ts',
1822
'src/transforms/index.ts',
1923
'src/plugins/cjs.ts',
2024
'src/utils/rpc.ts',

0 commit comments

Comments
 (0)