@tanstack/react-start-rsc 0.1.32 → 0.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/createCompositeComponent.js +3 -3
- package/dist/esm/createCompositeComponent.js.map +1 -1
- package/dist/esm/renderServerComponent.js +3 -3
- package/dist/esm/renderServerComponent.js.map +1 -1
- package/package.json +2 -3
- package/src/createCompositeComponent.ts +1 -2
- package/src/renderServerComponent.ts +1 -2
|
@@ -5,7 +5,6 @@ import { sanitizeSlotArgs } from "./slotUsageSanitizer.js";
|
|
|
5
5
|
import { ClientSlot } from "./ClientSlot.js";
|
|
6
6
|
import { createElement } from "react";
|
|
7
7
|
import { renderToReadableStream } from "virtual:tanstack-rsc-runtime";
|
|
8
|
-
import { getRequest } from "@tanstack/start-server-core";
|
|
9
8
|
import { getStartContext } from "@tanstack/start-storage-context";
|
|
10
9
|
//#region src/createCompositeComponent.ts
|
|
11
10
|
/**
|
|
@@ -52,10 +51,11 @@ async function createCompositeComponent(component, options) {
|
|
|
52
51
|
return createRscCssEnvelope(await component(proxyProps), options);
|
|
53
52
|
}
|
|
54
53
|
const flightStream = renderToReadableStream(createElement(ServerComponentWrapper));
|
|
55
|
-
const
|
|
54
|
+
const ctx = getStartContext({ throwIfNotFound: false });
|
|
55
|
+
const isRouterRequest = ctx?.handlerType === "router";
|
|
56
56
|
const ssrHandler = globalThis.__RSC_SSR__;
|
|
57
57
|
if (isRouterRequest && ssrHandler) {
|
|
58
|
-
const signal =
|
|
58
|
+
const signal = ctx.request.signal;
|
|
59
59
|
const stream = new ReplayableStream(flightStream, { signal });
|
|
60
60
|
const decoded = await ssrHandler.decode(stream);
|
|
61
61
|
slotUsagesEmitter?.close();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createCompositeComponent.js","names":[],"sources":["../../src/createCompositeComponent.ts"],"sourcesContent":["import { createElement } from 'react'\nimport { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'\nimport { getRequest } from '@tanstack/start-server-core'\nimport { getStartContext } from '@tanstack/start-storage-context'\nimport { sanitizeSlotArgs } from './slotUsageSanitizer'\nimport { ReplayableStream } from './ReplayableStream'\nimport { ClientSlot } from './ClientSlot'\nimport {\n RSC_SLOT_USAGES_STREAM,\n SERVER_COMPONENT_STREAM,\n} from './ServerComponentTypes'\nimport { createRscCssEnvelope } from './rscCssEnvelope'\nimport type {\n AnyCompositeComponent,\n CompositeComponentResult,\n RscSlotUsageEvent,\n ServerComponentStream,\n ValidateCompositeComponent,\n} from './ServerComponentTypes'\nimport type { RscCssEnvelopeOptions } from './rscCssEnvelope'\n\nimport './rscSsrHandler' // Import for global declaration side effect\n\n/**\n * Creates a composite server component with slot support.\n *\n * Supports returning:\n * - A ReactNode directly\n * - An object structure with ReactNodes: accessed as `src.Foo`\n * - Nested structures: accessed as `src.x.Bar`\n *\n * Props that are functions become slots - they render as ClientSlot placeholders\n * in the RSC output, filled in by the consumer with actual implementations.\n *\n * The returned value is NOT directly renderable. Use `<CompositeComponent src={...} />`.\n *\n * @example\n * ```tsx\n * const src = await createCompositeComponent((props) => (\n * <div>\n * <header>{props.header('Dashboard')}</header>\n * <main>{props.children}</main>\n * </div>\n * ))\n *\n * // In route component\n * return (\n * <CompositeComponent src={src} header={(title) => <h1>{title}</h1>}>\n * <p>Main content</p>\n * </CompositeComponent>\n * )\n * ```\n */\nexport async function createCompositeComponent<TComp>(\n component: ValidateCompositeComponent<TComp>,\n options?: RscCssEnvelopeOptions,\n): Promise<CompositeComponentResult<TComp>> {\n const isDev = process.env.NODE_ENV === 'development'\n\n // Dev-only: stream slot usage events (slot + raw args)\n const slotUsagesEmitter = isDev\n ? createReadableStreamEmitter<RscSlotUsageEvent>()\n : null\n\n // Create a wrapper component that will be rendered inside React's Flight context.\n // This ensures React.cache works properly since the component is called during\n // renderToReadableStream's render phase, not before it.\n const { proxy: proxyProps } = createSlotProxy<{}>({\n onSlotCall: slotUsagesEmitter\n ? (name, args) => {\n const sanitizedArgs = sanitizeSlotArgs(args)\n slotUsagesEmitter.emit({\n slot: name,\n args: sanitizedArgs.length ? sanitizedArgs : undefined,\n })\n }\n : undefined,\n })\n\n // Wrapper that renders the user's component inside Flight render context\n async function ServerComponentWrapper() {\n return createRscCssEnvelope(\n await (component as React.FC)(proxyProps),\n options,\n )\n }\n\n // Render using createElement so React calls our component during Flight rendering\n // This is critical for React.cache to work - the component must be invoked\n // during renderToReadableStream's execution, not before\n const flightStream = renderToReadableStream(\n createElement(ServerComponentWrapper as any),\n )\n\n // Check if this is an SSR request (router) or a direct server function call\n const ctx = getStartContext({ throwIfNotFound: false })\n const isRouterRequest = ctx?.handlerType === 'router'\n const ssrHandler = globalThis.__RSC_SSR__\n\n // SSR path: buffer stream for replay, pre-decode for synchronous rendering\n if (isRouterRequest && ssrHandler) {\n const signal = getRequest().signal\n const stream = new ReplayableStream(flightStream, { signal })\n\n // Pre-decode during loader phase for synchronous SSR rendering\n const decoded = await ssrHandler.decode(stream)\n\n // For SSR we know decode fully consumed the Flight stream.\n slotUsagesEmitter?.close()\n\n return ssrHandler.createCompositeProxy(\n stream,\n decoded,\n slotUsagesEmitter?.stream,\n ) as CompositeComponentResult<TComp>\n }\n\n // Server function call path:\n // The serialization adapter will stream to the client.\n const monitoredFlightStream =\n isDev && slotUsagesEmitter\n ? wrapReadableStream(flightStream, {\n onDone: () => {\n slotUsagesEmitter.close()\n },\n onCancel: () => {\n slotUsagesEmitter.close()\n },\n onError: () => {\n slotUsagesEmitter.close()\n },\n })\n : flightStream\n\n return createCompositeHandle(monitoredFlightStream, {\n slotUsagesStream: slotUsagesEmitter?.stream,\n }) as CompositeComponentResult<TComp>\n}\n\n/**\n * Creates a composite handle for server function responses.\n * No proxy needed - the client will decode and create its own proxy.\n */\nfunction createCompositeHandle(\n flightStream: ReadableStream<Uint8Array>,\n options?: {\n slotUsagesStream?: ReadableStream<RscSlotUsageEvent>\n },\n): AnyCompositeComponent {\n // Simple single-use stream wrapper. For server function calls, the stream\n // is consumed exactly once by the serialization adapter for transport.\n const streamWrapper: ServerComponentStream = {\n createReplayStream: () => flightStream,\n }\n\n // Create a stub function with the stream attached for serialization.\n // This will never be rendered directly - it goes through serialization\n // which extracts the stream and sends it to the client.\n const stub = function CompositeComponentStub(): never {\n throw new Error(\n 'CompositeComponent from server function cannot be rendered on server. ' +\n 'It should be serialized and sent to the client.',\n )\n }\n\n ;(stub as any)[SERVER_COMPONENT_STREAM] = streamWrapper\n // Note: RENDERABLE_RSC is not set (or implicitly false), indicating this is a composite component\n\n if (options?.slotUsagesStream) {\n ;(stub as any)[RSC_SLOT_USAGES_STREAM] = options.slotUsagesStream\n }\n\n return stub as unknown as AnyCompositeComponent\n}\n\n/**\n * Base slot props type - functions that become ClientSlot placeholders\n */\ninterface SlotPropsBase {\n [key: string]:\n | ((...args: Array<any>) => React.ReactNode)\n | React.ReactNode\n | undefined\n children?: React.ReactNode\n}\n\ninterface SlotProxyResult<TSlotProps extends object> {\n proxy: TSlotProps & SlotPropsBase\n}\n\n/**\n * Proxy that turns property access into ClientSlot renders.\n * Also tracks accessed slot names for devtools.\n */\nfunction createSlotProxy<TSlotProps extends object>(options?: {\n onSlotCall?: (name: string, args: Array<any>) => void\n}): SlotProxyResult<TSlotProps> {\n const cache = new Map<string, (...args: Array<any>) => React.ReactNode>()\n\n const proxy = new Proxy({} as TSlotProps & SlotPropsBase, {\n get(_target, prop) {\n if (prop === 'then' || typeof prop !== 'string') return undefined\n\n if (prop === 'children') {\n options?.onSlotCall?.('children', [])\n return createElement(ClientSlot, { slot: 'children', args: [] })\n }\n\n let fn = cache.get(prop)\n if (!fn) {\n fn = (...args: Array<any>) => {\n options?.onSlotCall?.(prop, args)\n return createElement(ClientSlot, { slot: prop, args })\n }\n cache.set(prop, fn)\n }\n return fn\n },\n })\n\n return {\n proxy,\n }\n}\n\nfunction createReadableStreamEmitter<T>(): {\n stream: ReadableStream<T>\n emit: (value: T) => void\n close: () => void\n} {\n let closed = false\n const queue: Array<T> = []\n let controller: ReadableStreamDefaultController<T> | null = null\n\n const stream = new ReadableStream<T>({\n start(ctrl) {\n controller = ctrl\n for (const value of queue) {\n try {\n ctrl.enqueue(value)\n } catch {\n // Ignore\n }\n }\n queue.length = 0\n if (closed) {\n try {\n ctrl.close()\n } catch {\n // Ignore\n }\n }\n },\n cancel() {\n closed = true\n controller = null\n queue.length = 0\n },\n })\n\n const emit = (value: T) => {\n if (closed) return\n if (!controller) {\n queue.push(value)\n return\n }\n try {\n controller.enqueue(value)\n } catch {\n // Ignore\n }\n }\n\n const close = () => {\n if (closed) return\n closed = true\n if (controller) {\n try {\n controller.close()\n } catch {\n // Ignore\n }\n controller = null\n }\n }\n\n return { stream, emit, close }\n}\n\nfunction wrapReadableStream<T>(\n source: ReadableStream<T>,\n handlers: {\n onDone?: () => void\n onCancel?: () => void\n onError?: () => void\n },\n): ReadableStream<T> {\n const reader = source.getReader()\n let finished = false\n\n const finish = () => {\n if (finished) return\n finished = true\n handlers.onDone?.()\n try {\n reader.releaseLock()\n } catch {\n // Ignore\n }\n }\n\n return new ReadableStream<T>({\n async pull(controller) {\n try {\n const { value, done } = await reader.read()\n if (done) {\n controller.close()\n finish()\n return\n }\n controller.enqueue(value)\n } catch (err) {\n try {\n controller.error(err)\n } catch {\n // Ignore\n }\n handlers.onError?.()\n finish()\n }\n },\n async cancel(reason) {\n handlers.onCancel?.()\n try {\n await reader.cancel(reason)\n } catch {\n // Ignore\n }\n finish()\n },\n })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"createCompositeComponent.js","names":[],"sources":["../../src/createCompositeComponent.ts"],"sourcesContent":["import { createElement } from 'react'\nimport { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'\nimport { getStartContext } from '@tanstack/start-storage-context'\nimport { sanitizeSlotArgs } from './slotUsageSanitizer'\nimport { ReplayableStream } from './ReplayableStream'\nimport { ClientSlot } from './ClientSlot'\nimport {\n RSC_SLOT_USAGES_STREAM,\n SERVER_COMPONENT_STREAM,\n} from './ServerComponentTypes'\nimport { createRscCssEnvelope } from './rscCssEnvelope'\nimport type {\n AnyCompositeComponent,\n CompositeComponentResult,\n RscSlotUsageEvent,\n ServerComponentStream,\n ValidateCompositeComponent,\n} from './ServerComponentTypes'\nimport type { RscCssEnvelopeOptions } from './rscCssEnvelope'\n\nimport './rscSsrHandler' // Import for global declaration side effect\n\n/**\n * Creates a composite server component with slot support.\n *\n * Supports returning:\n * - A ReactNode directly\n * - An object structure with ReactNodes: accessed as `src.Foo`\n * - Nested structures: accessed as `src.x.Bar`\n *\n * Props that are functions become slots - they render as ClientSlot placeholders\n * in the RSC output, filled in by the consumer with actual implementations.\n *\n * The returned value is NOT directly renderable. Use `<CompositeComponent src={...} />`.\n *\n * @example\n * ```tsx\n * const src = await createCompositeComponent((props) => (\n * <div>\n * <header>{props.header('Dashboard')}</header>\n * <main>{props.children}</main>\n * </div>\n * ))\n *\n * // In route component\n * return (\n * <CompositeComponent src={src} header={(title) => <h1>{title}</h1>}>\n * <p>Main content</p>\n * </CompositeComponent>\n * )\n * ```\n */\nexport async function createCompositeComponent<TComp>(\n component: ValidateCompositeComponent<TComp>,\n options?: RscCssEnvelopeOptions,\n): Promise<CompositeComponentResult<TComp>> {\n const isDev = process.env.NODE_ENV === 'development'\n\n // Dev-only: stream slot usage events (slot + raw args)\n const slotUsagesEmitter = isDev\n ? createReadableStreamEmitter<RscSlotUsageEvent>()\n : null\n\n // Create a wrapper component that will be rendered inside React's Flight context.\n // This ensures React.cache works properly since the component is called during\n // renderToReadableStream's render phase, not before it.\n const { proxy: proxyProps } = createSlotProxy<{}>({\n onSlotCall: slotUsagesEmitter\n ? (name, args) => {\n const sanitizedArgs = sanitizeSlotArgs(args)\n slotUsagesEmitter.emit({\n slot: name,\n args: sanitizedArgs.length ? sanitizedArgs : undefined,\n })\n }\n : undefined,\n })\n\n // Wrapper that renders the user's component inside Flight render context\n async function ServerComponentWrapper() {\n return createRscCssEnvelope(\n await (component as React.FC)(proxyProps),\n options,\n )\n }\n\n // Render using createElement so React calls our component during Flight rendering\n // This is critical for React.cache to work - the component must be invoked\n // during renderToReadableStream's execution, not before\n const flightStream = renderToReadableStream(\n createElement(ServerComponentWrapper as any),\n )\n\n // Check if this is an SSR request (router) or a direct server function call\n const ctx = getStartContext({ throwIfNotFound: false })\n const isRouterRequest = ctx?.handlerType === 'router'\n const ssrHandler = globalThis.__RSC_SSR__\n\n // SSR path: buffer stream for replay, pre-decode for synchronous rendering\n if (isRouterRequest && ssrHandler) {\n const signal = ctx.request.signal\n const stream = new ReplayableStream(flightStream, { signal })\n\n // Pre-decode during loader phase for synchronous SSR rendering\n const decoded = await ssrHandler.decode(stream)\n\n // For SSR we know decode fully consumed the Flight stream.\n slotUsagesEmitter?.close()\n\n return ssrHandler.createCompositeProxy(\n stream,\n decoded,\n slotUsagesEmitter?.stream,\n ) as CompositeComponentResult<TComp>\n }\n\n // Server function call path:\n // The serialization adapter will stream to the client.\n const monitoredFlightStream =\n isDev && slotUsagesEmitter\n ? wrapReadableStream(flightStream, {\n onDone: () => {\n slotUsagesEmitter.close()\n },\n onCancel: () => {\n slotUsagesEmitter.close()\n },\n onError: () => {\n slotUsagesEmitter.close()\n },\n })\n : flightStream\n\n return createCompositeHandle(monitoredFlightStream, {\n slotUsagesStream: slotUsagesEmitter?.stream,\n }) as CompositeComponentResult<TComp>\n}\n\n/**\n * Creates a composite handle for server function responses.\n * No proxy needed - the client will decode and create its own proxy.\n */\nfunction createCompositeHandle(\n flightStream: ReadableStream<Uint8Array>,\n options?: {\n slotUsagesStream?: ReadableStream<RscSlotUsageEvent>\n },\n): AnyCompositeComponent {\n // Simple single-use stream wrapper. For server function calls, the stream\n // is consumed exactly once by the serialization adapter for transport.\n const streamWrapper: ServerComponentStream = {\n createReplayStream: () => flightStream,\n }\n\n // Create a stub function with the stream attached for serialization.\n // This will never be rendered directly - it goes through serialization\n // which extracts the stream and sends it to the client.\n const stub = function CompositeComponentStub(): never {\n throw new Error(\n 'CompositeComponent from server function cannot be rendered on server. ' +\n 'It should be serialized and sent to the client.',\n )\n }\n\n ;(stub as any)[SERVER_COMPONENT_STREAM] = streamWrapper\n // Note: RENDERABLE_RSC is not set (or implicitly false), indicating this is a composite component\n\n if (options?.slotUsagesStream) {\n ;(stub as any)[RSC_SLOT_USAGES_STREAM] = options.slotUsagesStream\n }\n\n return stub as unknown as AnyCompositeComponent\n}\n\n/**\n * Base slot props type - functions that become ClientSlot placeholders\n */\ninterface SlotPropsBase {\n [key: string]:\n | ((...args: Array<any>) => React.ReactNode)\n | React.ReactNode\n | undefined\n children?: React.ReactNode\n}\n\ninterface SlotProxyResult<TSlotProps extends object> {\n proxy: TSlotProps & SlotPropsBase\n}\n\n/**\n * Proxy that turns property access into ClientSlot renders.\n * Also tracks accessed slot names for devtools.\n */\nfunction createSlotProxy<TSlotProps extends object>(options?: {\n onSlotCall?: (name: string, args: Array<any>) => void\n}): SlotProxyResult<TSlotProps> {\n const cache = new Map<string, (...args: Array<any>) => React.ReactNode>()\n\n const proxy = new Proxy({} as TSlotProps & SlotPropsBase, {\n get(_target, prop) {\n if (prop === 'then' || typeof prop !== 'string') return undefined\n\n if (prop === 'children') {\n options?.onSlotCall?.('children', [])\n return createElement(ClientSlot, { slot: 'children', args: [] })\n }\n\n let fn = cache.get(prop)\n if (!fn) {\n fn = (...args: Array<any>) => {\n options?.onSlotCall?.(prop, args)\n return createElement(ClientSlot, { slot: prop, args })\n }\n cache.set(prop, fn)\n }\n return fn\n },\n })\n\n return {\n proxy,\n }\n}\n\nfunction createReadableStreamEmitter<T>(): {\n stream: ReadableStream<T>\n emit: (value: T) => void\n close: () => void\n} {\n let closed = false\n const queue: Array<T> = []\n let controller: ReadableStreamDefaultController<T> | null = null\n\n const stream = new ReadableStream<T>({\n start(ctrl) {\n controller = ctrl\n for (const value of queue) {\n try {\n ctrl.enqueue(value)\n } catch {\n // Ignore\n }\n }\n queue.length = 0\n if (closed) {\n try {\n ctrl.close()\n } catch {\n // Ignore\n }\n }\n },\n cancel() {\n closed = true\n controller = null\n queue.length = 0\n },\n })\n\n const emit = (value: T) => {\n if (closed) return\n if (!controller) {\n queue.push(value)\n return\n }\n try {\n controller.enqueue(value)\n } catch {\n // Ignore\n }\n }\n\n const close = () => {\n if (closed) return\n closed = true\n if (controller) {\n try {\n controller.close()\n } catch {\n // Ignore\n }\n controller = null\n }\n }\n\n return { stream, emit, close }\n}\n\nfunction wrapReadableStream<T>(\n source: ReadableStream<T>,\n handlers: {\n onDone?: () => void\n onCancel?: () => void\n onError?: () => void\n },\n): ReadableStream<T> {\n const reader = source.getReader()\n let finished = false\n\n const finish = () => {\n if (finished) return\n finished = true\n handlers.onDone?.()\n try {\n reader.releaseLock()\n } catch {\n // Ignore\n }\n }\n\n return new ReadableStream<T>({\n async pull(controller) {\n try {\n const { value, done } = await reader.read()\n if (done) {\n controller.close()\n finish()\n return\n }\n controller.enqueue(value)\n } catch (err) {\n try {\n controller.error(err)\n } catch {\n // Ignore\n }\n handlers.onError?.()\n finish()\n }\n },\n async cancel(reason) {\n handlers.onCancel?.()\n try {\n await reader.cancel(reason)\n } catch {\n // Ignore\n }\n finish()\n },\n })\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,6 @@ import { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from "./ServerComponentTypes.
|
|
|
2
2
|
import { ReplayableStream } from "./ReplayableStream.js";
|
|
3
3
|
import { createRscCssEnvelope } from "./rscCssEnvelope.js";
|
|
4
4
|
import { renderToReadableStream } from "virtual:tanstack-rsc-runtime";
|
|
5
|
-
import { getRequest } from "@tanstack/start-server-core";
|
|
6
5
|
import { getStartContext } from "@tanstack/start-storage-context";
|
|
7
6
|
//#region src/renderServerComponent.ts
|
|
8
7
|
/**
|
|
@@ -30,10 +29,11 @@ import { getStartContext } from "@tanstack/start-storage-context";
|
|
|
30
29
|
*/
|
|
31
30
|
async function renderServerComponent(node, options) {
|
|
32
31
|
const flightStream = renderToReadableStream(createRscCssEnvelope(node, options));
|
|
33
|
-
const
|
|
32
|
+
const ctx = getStartContext({ throwIfNotFound: false });
|
|
33
|
+
const isRouterRequest = ctx?.handlerType === "router";
|
|
34
34
|
const ssrHandler = globalThis.__RSC_SSR__;
|
|
35
35
|
if (isRouterRequest && ssrHandler) {
|
|
36
|
-
const signal =
|
|
36
|
+
const signal = ctx.request.signal;
|
|
37
37
|
const stream = new ReplayableStream(flightStream, { signal });
|
|
38
38
|
const decoded = await ssrHandler.decode(stream);
|
|
39
39
|
return ssrHandler.createRenderableProxy(stream, decoded);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderServerComponent.js","names":[],"sources":["../../src/renderServerComponent.ts"],"sourcesContent":["import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'\nimport {
|
|
1
|
+
{"version":3,"file":"renderServerComponent.js","names":[],"sources":["../../src/renderServerComponent.ts"],"sourcesContent":["import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'\nimport { getStartContext } from '@tanstack/start-storage-context'\nimport { ReplayableStream } from './ReplayableStream'\nimport { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from './ServerComponentTypes'\nimport { createRscCssEnvelope } from './rscCssEnvelope'\nimport type {\n AnyRenderableServerComponent,\n RenderableServerComponentBuilder,\n ServerComponentStream,\n ValidateRenderableServerComponent,\n} from './ServerComponentTypes'\nimport type { RscCssEnvelopeOptions } from './rscCssEnvelope'\n\nimport './rscSsrHandler'\n// Import for global declaration side effect\nexport type { RscSsrHandler, RscDecodeResult } from './rscSsrHandler'\n\n/**\n * Renderable RSC handle type - used for serialization detection.\n */\n\n/**\n * Type guard for renderable RSC handle.\n */\nexport function isRenderableRscHandle(\n value: unknown,\n): value is AnyRenderableServerComponent {\n return (\n typeof value === 'function' &&\n SERVER_COMPONENT_STREAM in value &&\n RENDERABLE_RSC in value &&\n (value as any)[RENDERABLE_RSC] === true\n )\n}\n\n/**\n * Renders a React element to an RSC Flight stream.\n *\n * Returns a \"renderable proxy\" that can be:\n * - Rendered directly as `{data}` in JSX\n * - Accessed for nested selections: `{data.foo.bar.Hello}`\n *\n * No slot support - for slots use `createCompositeComponent`.\n *\n * @example\n * ```tsx\n * // In a loader or server function\n * const data = await renderServerComponent(<MyServerComponent foo=\"bar\" />)\n *\n * // In the route component\n * return (\n * <div>\n * {data}\n * {data.sidebar.Menu}\n * </div>\n * )\n * ```\n */\nexport async function renderServerComponent<TNode>(\n node: ValidateRenderableServerComponent<TNode>,\n options?: RscCssEnvelopeOptions,\n): Promise<RenderableServerComponentBuilder<TNode>> {\n const flightStream = renderToReadableStream(\n createRscCssEnvelope(node, options),\n )\n\n // Check if this is an SSR request (router) or a direct server function call\n const ctx = getStartContext({ throwIfNotFound: false })\n const isRouterRequest = ctx?.handlerType === 'router'\n const ssrHandler = globalThis.__RSC_SSR__\n\n // SSR path: buffer stream for replay, pre-decode for synchronous rendering\n if (isRouterRequest && ssrHandler) {\n const signal = ctx.request.signal\n const stream = new ReplayableStream(flightStream, { signal })\n\n // Pre-decode during loader phase for synchronous SSR rendering\n const decoded = await ssrHandler.decode(stream)\n return ssrHandler.createRenderableProxy(\n stream,\n decoded,\n ) as RenderableServerComponentBuilder<TNode>\n }\n\n // Server function call path: return a handle for serialization\n return createRenderableHandle(\n flightStream,\n ) as unknown as RenderableServerComponentBuilder<TNode>\n}\n\n/**\n * Creates a renderable handle for server function responses.\n * Tagged with RENDERABLE_RSC for the serialization adapter.\n */\nfunction createRenderableHandle(\n flightStream: ReadableStream<Uint8Array>,\n): AnyRenderableServerComponent {\n const streamWrapper: ServerComponentStream = {\n createReplayStream: () => flightStream,\n }\n\n const stub = function RenderableRscStub(): never {\n throw new Error(\n 'Renderable RSC from server function cannot be rendered on server. ' +\n 'It should be serialized and sent to the client.',\n )\n }\n\n ;(stub as any)[SERVER_COMPONENT_STREAM] = streamWrapper\n ;(stub as any)[RENDERABLE_RSC] = true\n return stub as unknown as AnyRenderableServerComponent\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DA,eAAsB,sBACpB,MACA,SACkD;CAClD,MAAM,eAAe,uBACnB,qBAAqB,MAAM,OAAO,CACpC;CAGA,MAAM,MAAM,gBAAgB,EAAE,iBAAiB,MAAM,CAAC;CACtD,MAAM,kBAAkB,KAAK,gBAAgB;CAC7C,MAAM,aAAa,WAAW;CAG9B,IAAI,mBAAmB,YAAY;EACjC,MAAM,SAAS,IAAI,QAAQ;EAC3B,MAAM,SAAS,IAAI,iBAAiB,cAAc,EAAE,OAAO,CAAC;EAG5D,MAAM,UAAU,MAAM,WAAW,OAAO,MAAM;EAC9C,OAAO,WAAW,sBAChB,QACA,OACF;CACF;CAGA,OAAO,uBACL,YACF;AACF;;;;;AAMA,SAAS,uBACP,cAC8B;CAC9B,MAAM,gBAAuC,EAC3C,0BAA0B,aAC5B;CAEA,MAAM,OAAO,SAAS,oBAA2B;EAC/C,MAAM,IAAI,MACR,mHAEF;CACF;CAEC,KAAc,2BAA2B;CACzC,KAAc,kBAAkB;CACjC,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-start-rsc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.34",
|
|
4
4
|
"description": "React Server Components support for TanStack Start",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -87,8 +87,7 @@
|
|
|
87
87
|
"@tanstack/router-utils": "1.162.2",
|
|
88
88
|
"@tanstack/start-client-core": "1.170.14",
|
|
89
89
|
"@tanstack/start-fn-stubs": "1.162.0",
|
|
90
|
-
"@tanstack/start-plugin-core": "1.171.
|
|
91
|
-
"@tanstack/start-server-core": "1.169.17",
|
|
90
|
+
"@tanstack/start-plugin-core": "1.171.26",
|
|
92
91
|
"@tanstack/start-storage-context": "1.167.17"
|
|
93
92
|
},
|
|
94
93
|
"devDependencies": {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createElement } from 'react'
|
|
2
2
|
import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
|
|
3
|
-
import { getRequest } from '@tanstack/start-server-core'
|
|
4
3
|
import { getStartContext } from '@tanstack/start-storage-context'
|
|
5
4
|
import { sanitizeSlotArgs } from './slotUsageSanitizer'
|
|
6
5
|
import { ReplayableStream } from './ReplayableStream'
|
|
@@ -99,7 +98,7 @@ export async function createCompositeComponent<TComp>(
|
|
|
99
98
|
|
|
100
99
|
// SSR path: buffer stream for replay, pre-decode for synchronous rendering
|
|
101
100
|
if (isRouterRequest && ssrHandler) {
|
|
102
|
-
const signal =
|
|
101
|
+
const signal = ctx.request.signal
|
|
103
102
|
const stream = new ReplayableStream(flightStream, { signal })
|
|
104
103
|
|
|
105
104
|
// Pre-decode during loader phase for synchronous SSR rendering
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
|
|
2
|
-
import { getRequest } from '@tanstack/start-server-core'
|
|
3
2
|
import { getStartContext } from '@tanstack/start-storage-context'
|
|
4
3
|
import { ReplayableStream } from './ReplayableStream'
|
|
5
4
|
import { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from './ServerComponentTypes'
|
|
@@ -72,7 +71,7 @@ export async function renderServerComponent<TNode>(
|
|
|
72
71
|
|
|
73
72
|
// SSR path: buffer stream for replay, pre-decode for synchronous rendering
|
|
74
73
|
if (isRouterRequest && ssrHandler) {
|
|
75
|
-
const signal =
|
|
74
|
+
const signal = ctx.request.signal
|
|
76
75
|
const stream = new ReplayableStream(flightStream, { signal })
|
|
77
76
|
|
|
78
77
|
// Pre-decode during loader phase for synchronous SSR rendering
|