Skip to content
Open
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
125 changes: 125 additions & 0 deletions packages/plugin-rsc/src/react/rsc.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { Readable } from 'node:stream'
// @ts-ignore
import * as ReactClient from '@vitejs/plugin-rsc/vendor/react-server-dom/client.node'
// @ts-ignore
import * as ReactServer from '@vitejs/plugin-rsc/vendor/react-server-dom/server.node'
import type { ReactFormState } from 'react-dom/client'
import {
createClientManifest,
createServerDecodeClientManifest,
createServerManifest,
} from '../core/rsc'
import type {
ClientTemporaryReferenceSet,
DecodeReplyFunction,
EncodeReplyFunction,
RenderToReadableStreamOptions,
ServerTemporaryReferenceSet,
} from '../types'

export { loadServerAction, setRequireModule } from '../core/rsc'

export interface PipeableStream {
abort(reason: unknown): void
pipe<Writable extends NodeJS.WritableStream>(destination: Writable): Writable
}

export function renderToPipeableStream<T>(
data: T,
options?: RenderToReadableStreamOptions,
extraOptions?: {
/**
* @internal
*/
onClientReference?: (metadata: { id: string; name: string }) => void
},
): PipeableStream {
return ReactServer.renderToPipeableStream(
data,
createClientManifest({
onClientReference: extraOptions?.onClientReference,
}),
options,
)
}

export function renderToReadableStream<T>(
data: T,
options?: RenderToReadableStreamOptions,
extraOptions?: {
/**
* @internal
*/
onClientReference?: (metadata: { id: string; name: string }) => void
},
): ReadableStream<Uint8Array> {
return ReactServer.renderToReadableStream(
data,
createClientManifest({
onClientReference: extraOptions?.onClientReference,
}),
options,
)
}

export function createFromReadableStream<T>(
stream: ReadableStream<Uint8Array>,
options: object = {},
): Promise<T> {
return ReactClient.createFromReadableStream(stream, {
serverConsumerManifest: {
serverModuleMap: createServerManifest(),
moduleMap: createServerDecodeClientManifest(),
},
...options,
})
}

export function createFromNodeStream<T>(
stream: Readable,
options: object = {},
): Promise<T> {
return ReactClient.createFromNodeStream(stream, {
serverConsumerManifest: {
serverModuleMap: createServerManifest(),
moduleMap: createServerDecodeClientManifest(),
},
...options,
})
}

export function registerClientReference<T>(
proxy: T,
id: string,
name: string,
): T {
return ReactServer.registerClientReference(proxy, id, name)
}

export const registerServerReference: <T>(
ref: T,
id: string,
name: string,
) => T = ReactServer.registerServerReference

export const decodeReply: DecodeReplyFunction = (body, options) =>
ReactServer.decodeReply(body, createServerManifest(), options)

export function decodeAction(body: FormData): Promise<() => Promise<void>> {
return ReactServer.decodeAction(body, createServerManifest())
}

export function decodeFormState(
actionResult: unknown,
body: FormData,
): Promise<ReactFormState | undefined> {
return ReactServer.decodeFormState(actionResult, body, createServerManifest())
}

export const createTemporaryReferenceSet: () => ServerTemporaryReferenceSet =
ReactServer.createTemporaryReferenceSet

export const encodeReply: EncodeReplyFunction = ReactClient.encodeReply

export const createClientTemporaryReferenceSet: () => ClientTemporaryReferenceSet =
ReactClient.createTemporaryReferenceSet
33 changes: 33 additions & 0 deletions packages/plugin-rsc/src/react/ssr.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Readable } from 'node:stream'
// @ts-ignore
import * as ReactClient from '@vitejs/plugin-rsc/vendor/react-server-dom/client.node'
import { createServerConsumerManifest } from '../core/ssr'

export { setRequireModule } from '../core/ssr'

export function createFromReadableStream<T>(
stream: ReadableStream<Uint8Array>,
options: object = {},
): Promise<T> {
return ReactClient.createFromReadableStream(stream, {
serverConsumerManifest: createServerConsumerManifest(),
...options,
})
}

export function createFromNodeStream<T>(
stream: Readable,
options: object = {},
): Promise<T> {
return ReactClient.createFromNodeStream(stream, {
serverConsumerManifest: createServerConsumerManifest(),
...options,
})
}

export function createServerReference(id: string): unknown {
return ReactClient.createServerReference(id)
}

export const callServer = null
export const findSourceMapURL = null
71 changes: 71 additions & 0 deletions packages/plugin-rsc/src/rsc.node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 { renderToPipeableStream as originalRenderToPipeableStream } from './react/rsc.node'
import type { PipeableStream } from './react/rsc.node'

export {
createClientManifest,
createServerManifest,
loadServerAction,
} from './core/rsc'

export {
encryptActionBoundArgs,
decryptActionBoundArgs,
} from './utils/encryption-runtime'

export * from './react/rsc.node'

initialize()

function initialize(): void {
setRequireModule({
load: async (id) => {
if (!import.meta.env.__vite_rsc_build__) {
await import(
/* @vite-ignore */ '/@id/__x00__' +
toReferenceValidationVirtual({ id, type: 'server' })
)
return import(/* @vite-ignore */ id)
} else {
const import_ = serverReferences[id]
if (!import_) {
throw new Error(`server reference not found '${id}'`)
}
return import_()
}
},
})
}

export function renderToPipeableStream<T>(
data: T,
options?: object,
extraOptions?: {
/**
* @experimental
*/
onClientReference?: (metadata: {
id: string
name: string
deps: ResolvedAssetDeps
}) => void
},
): PipeableStream {
return originalRenderToPipeableStream(data, options, {
onClientReference(metadata) {
const deps = assetsManifest.clientReferenceDeps[metadata.id] ?? {
js: [],
css: [],
}
extraOptions?.onClientReference?.({
id: metadata.id,
name: metadata.name,
deps,
})
},
})
}
100 changes: 100 additions & 0 deletions packages/plugin-rsc/src/ssr.node.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as ReactDOM from 'react-dom'
import assetsManifest from 'virtual:vite-rsc/assets-manifest'
import * as clientReferences from 'virtual:vite-rsc/client-references'
import { setRequireModule } from './core/ssr'
import type { ResolvedAssetDeps } from './plugin'
import { toCssVirtual, toReferenceValidationVirtual } from './plugins/shared'

export { createServerConsumerManifest } from './core/ssr'

export * from './react/ssr.node'

/**
* Callback type for client reference dependency notifications.
* Called during SSR when a client component's dependencies are loaded.
* @experimental
*/
export type OnClientReference = (reference: {
id: string
deps: ResolvedAssetDeps
}) => void

// Registered callback for client reference deps
let onClientReference: OnClientReference | undefined

/**
* Register a callback to be notified when client reference dependencies are loaded.
* Called during SSR when a client component is accessed.
* @experimental
*/
export function setOnClientReference(
callback: OnClientReference | undefined,
): void {
onClientReference = callback
}

initialize()

function initialize(): void {
setRequireModule({
load: async (id) => {
if (!import.meta.env.__vite_rsc_build__) {
await import(
/* @vite-ignore */ '/@id/__x00__' +
toReferenceValidationVirtual({ id, type: 'client' })
)
const mod = await import(/* @vite-ignore */ id)
const modCss = await import(
/* @vite-ignore */ '/@id/__x00__' + toCssVirtual({ id, type: 'ssr' })
)
return wrapResourceProxy(mod, id, { js: [], css: modCss.default })
} else {
const import_ = clientReferences.default[id]
if (!import_) {
throw new Error(`client reference not found '${id}'`)
}
const deps = assetsManifest.clientReferenceDeps[id] ?? {
js: [],
css: [],
}
// kick off preload/notify before initial async import, which is not sync-cached
preloadDeps(deps)
onClientReference?.({ id, deps })
const mod: any = await import_()
return wrapResourceProxy(mod, id, deps)
}
},
})
}

// preload/preinit during getter access since `load` is cached on production.
// also notify `onClientReference` callback here since module export access is not memoized by React.
function wrapResourceProxy(mod: any, id: string, deps: ResolvedAssetDeps) {
return new Proxy(mod, {
get(target, p, receiver) {
if (p in mod) {
preloadDeps(deps)
onClientReference?.({ id, deps })
}
return Reflect.get(target, p, receiver)
},
})
}

function preloadDeps(deps: ResolvedAssetDeps) {
for (const href of deps.js) {
ReactDOM.preloadModule(href, {
as: 'script',
crossOrigin: '',
})
}
for (const href of deps.css) {
ReactDOM.preinit(href, {
as: 'style',
precedence:
assetsManifest.cssLinkPrecedence !== false
? 'vite-rsc/client-reference'
: undefined,
})
}
}
4 changes: 4 additions & 0 deletions packages/plugin-rsc/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ export default defineConfig({
'src/plugin.ts',
'src/browser.ts',
'src/ssr.tsx',
'src/ssr.node.tsx',
'src/rsc.tsx',
'src/rsc.node.tsx',
'src/core/browser.ts',
'src/core/ssr.ts',
'src/core/rsc.ts',
'src/core/plugin.ts',
'src/react/browser.ts',
'src/react/ssr.ts',
'src/react/ssr.node.ts',
'src/react/rsc.ts',
'src/react/rsc.node.ts',
'src/transforms/index.ts',
'src/plugins/cjs.ts',
'src/utils/rpc.ts',
Expand Down