diff --git a/packages/plugin-rsc/src/react/rsc.node.ts b/packages/plugin-rsc/src/react/rsc.node.ts new file mode 100644 index 000000000..a42f412b8 --- /dev/null +++ b/packages/plugin-rsc/src/react/rsc.node.ts @@ -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(destination: Writable): Writable +} + +export function renderToPipeableStream( + 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( + data: T, + options?: RenderToReadableStreamOptions, + extraOptions?: { + /** + * @internal + */ + onClientReference?: (metadata: { id: string; name: string }) => void + }, +): ReadableStream { + return ReactServer.renderToReadableStream( + data, + createClientManifest({ + onClientReference: extraOptions?.onClientReference, + }), + options, + ) +} + +export function createFromReadableStream( + stream: ReadableStream, + options: object = {}, +): Promise { + return ReactClient.createFromReadableStream(stream, { + serverConsumerManifest: { + serverModuleMap: createServerManifest(), + moduleMap: createServerDecodeClientManifest(), + }, + ...options, + }) +} + +export function createFromNodeStream( + stream: Readable, + options: object = {}, +): Promise { + return ReactClient.createFromNodeStream(stream, { + serverConsumerManifest: { + serverModuleMap: createServerManifest(), + moduleMap: createServerDecodeClientManifest(), + }, + ...options, + }) +} + +export function registerClientReference( + proxy: T, + id: string, + name: string, +): T { + return ReactServer.registerClientReference(proxy, id, name) +} + +export const registerServerReference: ( + 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> { + return ReactServer.decodeAction(body, createServerManifest()) +} + +export function decodeFormState( + actionResult: unknown, + body: FormData, +): Promise { + return ReactServer.decodeFormState(actionResult, body, createServerManifest()) +} + +export const createTemporaryReferenceSet: () => ServerTemporaryReferenceSet = + ReactServer.createTemporaryReferenceSet + +export const encodeReply: EncodeReplyFunction = ReactClient.encodeReply + +export const createClientTemporaryReferenceSet: () => ClientTemporaryReferenceSet = + ReactClient.createTemporaryReferenceSet diff --git a/packages/plugin-rsc/src/react/ssr.node.ts b/packages/plugin-rsc/src/react/ssr.node.ts new file mode 100644 index 000000000..9e34ac8bc --- /dev/null +++ b/packages/plugin-rsc/src/react/ssr.node.ts @@ -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( + stream: ReadableStream, + options: object = {}, +): Promise { + return ReactClient.createFromReadableStream(stream, { + serverConsumerManifest: createServerConsumerManifest(), + ...options, + }) +} + +export function createFromNodeStream( + stream: Readable, + options: object = {}, +): Promise { + 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 diff --git a/packages/plugin-rsc/src/rsc.node.tsx b/packages/plugin-rsc/src/rsc.node.tsx new file mode 100644 index 000000000..6eb5c54fe --- /dev/null +++ b/packages/plugin-rsc/src/rsc.node.tsx @@ -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( + 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, + }) + }, + }) +} diff --git a/packages/plugin-rsc/src/ssr.node.tsx b/packages/plugin-rsc/src/ssr.node.tsx new file mode 100644 index 000000000..9c3271fcb --- /dev/null +++ b/packages/plugin-rsc/src/ssr.node.tsx @@ -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, + }) + } +} diff --git a/packages/plugin-rsc/tsdown.config.ts b/packages/plugin-rsc/tsdown.config.ts index d226dd01d..f51b16f80 100644 --- a/packages/plugin-rsc/tsdown.config.ts +++ b/packages/plugin-rsc/tsdown.config.ts @@ -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',