@tanstack/react-start-rsc 0.0.36 → 0.0.37
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 -2
- package/dist/esm/createCompositeComponent.js.map +1 -1
- package/dist/esm/createServerComponentFromStream.js +3 -2
- package/dist/esm/createServerComponentFromStream.js.map +1 -1
- package/dist/esm/plugin/rscCssTransform.js +79 -0
- package/dist/esm/plugin/rscCssTransform.js.map +1 -0
- package/dist/esm/plugin/vite.js +18 -1
- package/dist/esm/plugin/vite.js.map +1 -1
- package/dist/esm/renderServerComponent.js +3 -2
- package/dist/esm/renderServerComponent.js.map +1 -1
- package/dist/esm/rscCssEnvelope.js +23 -0
- package/dist/esm/rscCssEnvelope.js.map +1 -0
- package/dist/esm/serialization.server.js +6 -4
- package/dist/esm/serialization.server.js.map +1 -1
- package/dist/esm/src/createCompositeComponent.d.ts +2 -1
- package/dist/esm/src/plugin/rscCssTransform.d.ts +5 -0
- package/dist/esm/src/plugin/vite.d.ts +1 -0
- package/dist/esm/src/renderServerComponent.d.ts +2 -1
- package/dist/esm/src/rscCssEnvelope.d.ts +8 -0
- package/package.json +9 -3
- package/src/createCompositeComponent.ts +8 -2
- package/src/createServerComponentFromStream.ts +3 -2
- package/src/plugin/rscCssTransform.ts +150 -0
- package/src/plugin/vite.ts +31 -0
- package/src/renderServerComponent.ts +6 -1
- package/src/rscCssEnvelope.ts +38 -0
- package/src/serialization.server.ts +10 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RSC_SLOT_USAGES_STREAM, SERVER_COMPONENT_STREAM } from "./ServerComponentTypes.js";
|
|
2
2
|
import { ReplayableStream } from "./ReplayableStream.js";
|
|
3
|
+
import { createRscCssEnvelope } from "./rscCssEnvelope.js";
|
|
3
4
|
import { sanitizeSlotArgs } from "./slotUsageSanitizer.js";
|
|
4
5
|
import { ClientSlot } from "./ClientSlot.js";
|
|
5
6
|
import { createElement } from "react";
|
|
@@ -37,7 +38,7 @@ import { getStartContext } from "@tanstack/start-storage-context";
|
|
|
37
38
|
* )
|
|
38
39
|
* ```
|
|
39
40
|
*/
|
|
40
|
-
async function createCompositeComponent(component) {
|
|
41
|
+
async function createCompositeComponent(component, options) {
|
|
41
42
|
const isDev = process.env.NODE_ENV === "development";
|
|
42
43
|
const slotUsagesEmitter = isDev ? createReadableStreamEmitter() : null;
|
|
43
44
|
const { proxy: proxyProps } = createSlotProxy({ onSlotCall: slotUsagesEmitter ? (name, args) => {
|
|
@@ -48,7 +49,7 @@ async function createCompositeComponent(component) {
|
|
|
48
49
|
});
|
|
49
50
|
} : void 0 });
|
|
50
51
|
async function ServerComponentWrapper() {
|
|
51
|
-
return component(proxyProps);
|
|
52
|
+
return createRscCssEnvelope(await component(proxyProps), options);
|
|
52
53
|
}
|
|
53
54
|
const flightStream = renderToReadableStream(createElement(ServerComponentWrapper));
|
|
54
55
|
const isRouterRequest = getStartContext({ throwIfNotFound: false })?.handlerType === "router";
|
|
@@ -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 type {\n AnyCompositeComponent,\n CompositeComponentResult,\n RscSlotUsageEvent,\n ServerComponentStream,\n ValidateCompositeComponent,\n} from './ServerComponentTypes'\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): 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 (component as React.FC)(proxyProps)\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),\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 { 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,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { unwrapRscCssEnvelope } from "./rscCssEnvelope.js";
|
|
2
3
|
import { awaitLazyElements } from "./awaitLazyElements.js";
|
|
3
4
|
import { createRscProxy } from "./createRscProxy.js";
|
|
4
5
|
import { use } from "react";
|
|
@@ -52,9 +53,9 @@ function setupStreamDecode(stream) {
|
|
|
52
53
|
await awaitLazyElements(result, (href) => {
|
|
53
54
|
cssHrefs.add(href);
|
|
54
55
|
});
|
|
55
|
-
cachedTree = result;
|
|
56
|
+
cachedTree = unwrapRscCssEnvelope(result);
|
|
56
57
|
cacheReady = true;
|
|
57
|
-
return
|
|
58
|
+
return cachedTree;
|
|
58
59
|
});
|
|
59
60
|
trackPostProcessPromise(transformedTreePromise);
|
|
60
61
|
const streamWrapper = { createReplayStream: () => stream };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createServerComponentFromStream.js","names":[],"sources":["../../src/createServerComponentFromStream.ts"],"sourcesContent":["'use client'\n\nimport { use } from 'react'\nimport { trackPostProcessPromise } from '@tanstack/start-client-core'\nimport { createFromReadableStream as browserDecode } from 'virtual:tanstack-rsc-browser-decode'\n\nimport { awaitLazyElements } from './awaitLazyElements'\nimport { createRscProxy } from './createRscProxy'\nimport type {\n AnyCompositeComponent,\n RscSlotUsageEvent,\n ServerComponentStream,\n} from './ServerComponentTypes'\n\n/**\n * Creates a renderable RSC proxy from a raw Flight stream.\n * Client-side only - used by the client serialization adapter for `renderServerComponent`.\n *\n * Returns a Proxy that:\n * - Can be rendered directly as `{data}` in JSX\n * - Supports nested access: `{data.foo.bar}`\n * - Masquerades as a React element\n */\nexport function createRenderableFromStream(\n stream: ReadableStream<Uint8Array>,\n): any {\n const { getTree, streamWrapper, cssHrefs } = setupStreamDecode(stream)\n\n return createRscProxy(getTree, {\n stream: streamWrapper,\n cssHrefs,\n renderable: true,\n })\n}\n\n/**\n * Creates a composite RSC proxy from a raw Flight stream.\n * Client-side only - used by the client serialization adapter for `createCompositeComponent`.\n *\n * Returns a Proxy that:\n * - NOT directly renderable\n * - Supports nested access: `src.foo.bar`\n * - Must be rendered via `<CompositeComponent src={...} />`\n */\nexport function createCompositeFromStream(\n stream: ReadableStream<Uint8Array>,\n options?: {\n slotUsagesStream?: ReadableStream<RscSlotUsageEvent>\n },\n): AnyCompositeComponent {\n const { getTree, streamWrapper, cssHrefs } = setupStreamDecode(stream)\n\n return createRscProxy(getTree, {\n stream: streamWrapper,\n cssHrefs,\n renderable: false,\n slotUsagesStream: options?.slotUsagesStream,\n })\n}\n\n/**\n * Shared stream decode setup for both renderable and composite.\n */\nfunction setupStreamDecode(stream: ReadableStream<Uint8Array>): {\n getTree: () => unknown\n streamWrapper: ServerComponentStream\n cssHrefs: Set<string> | undefined\n} {\n // Start decoding eagerly during deserialization\n const decodeThenable = browserDecode(stream)\n const cssHrefs = new Set<string>()\n\n // Synchronous cache for the decoded tree.\n let cachedTree: unknown = undefined\n let cacheReady = false\n\n // Promise for the tree with lazy elements awaited.\n const transformedTreePromise = Promise.resolve(decodeThenable).then(\n async (result) => {\n await awaitLazyElements(result, (href) => {\n cssHrefs.add(href)\n })\n cachedTree = result\n cacheReady = true\n return
|
|
1
|
+
{"version":3,"file":"createServerComponentFromStream.js","names":[],"sources":["../../src/createServerComponentFromStream.ts"],"sourcesContent":["'use client'\n\nimport { use } from 'react'\nimport { trackPostProcessPromise } from '@tanstack/start-client-core'\nimport { createFromReadableStream as browserDecode } from 'virtual:tanstack-rsc-browser-decode'\n\nimport { awaitLazyElements } from './awaitLazyElements'\nimport { createRscProxy } from './createRscProxy'\nimport { unwrapRscCssEnvelope } from './rscCssEnvelope'\nimport type {\n AnyCompositeComponent,\n RscSlotUsageEvent,\n ServerComponentStream,\n} from './ServerComponentTypes'\n\n/**\n * Creates a renderable RSC proxy from a raw Flight stream.\n * Client-side only - used by the client serialization adapter for `renderServerComponent`.\n *\n * Returns a Proxy that:\n * - Can be rendered directly as `{data}` in JSX\n * - Supports nested access: `{data.foo.bar}`\n * - Masquerades as a React element\n */\nexport function createRenderableFromStream(\n stream: ReadableStream<Uint8Array>,\n): any {\n const { getTree, streamWrapper, cssHrefs } = setupStreamDecode(stream)\n\n return createRscProxy(getTree, {\n stream: streamWrapper,\n cssHrefs,\n renderable: true,\n })\n}\n\n/**\n * Creates a composite RSC proxy from a raw Flight stream.\n * Client-side only - used by the client serialization adapter for `createCompositeComponent`.\n *\n * Returns a Proxy that:\n * - NOT directly renderable\n * - Supports nested access: `src.foo.bar`\n * - Must be rendered via `<CompositeComponent src={...} />`\n */\nexport function createCompositeFromStream(\n stream: ReadableStream<Uint8Array>,\n options?: {\n slotUsagesStream?: ReadableStream<RscSlotUsageEvent>\n },\n): AnyCompositeComponent {\n const { getTree, streamWrapper, cssHrefs } = setupStreamDecode(stream)\n\n return createRscProxy(getTree, {\n stream: streamWrapper,\n cssHrefs,\n renderable: false,\n slotUsagesStream: options?.slotUsagesStream,\n })\n}\n\n/**\n * Shared stream decode setup for both renderable and composite.\n */\nfunction setupStreamDecode(stream: ReadableStream<Uint8Array>): {\n getTree: () => unknown\n streamWrapper: ServerComponentStream\n cssHrefs: Set<string> | undefined\n} {\n // Start decoding eagerly during deserialization\n const decodeThenable = browserDecode(stream)\n const cssHrefs = new Set<string>()\n\n // Synchronous cache for the decoded tree.\n let cachedTree: unknown = undefined\n let cacheReady = false\n\n // Promise for the tree with lazy elements awaited.\n const transformedTreePromise = Promise.resolve(decodeThenable).then(\n async (result) => {\n await awaitLazyElements(result, (href) => {\n cssHrefs.add(href)\n })\n cachedTree = unwrapRscCssEnvelope(result)\n cacheReady = true\n return cachedTree\n },\n )\n\n // Track the lazy element loading - prevents flash\n trackPostProcessPromise(transformedTreePromise)\n\n const streamWrapper: ServerComponentStream = {\n createReplayStream: () => stream,\n }\n\n const getTree = () => {\n if (cacheReady) return cachedTree\n // eslint-disable-next-line react-hooks/rules-of-hooks\n return use(transformedTreePromise)\n }\n\n return { getTree, streamWrapper, cssHrefs }\n}\n// Legacy export for backwards compatibility during migration\nexport const createServerComponentFromStream = createCompositeFromStream\n"],"mappings":";;;;;;;;;;;;;;;;;AAwBA,SAAgB,2BACd,QACK;CACL,MAAM,EAAE,SAAS,eAAe,aAAa,kBAAkB,OAAO;AAEtE,QAAO,eAAe,SAAS;EAC7B,QAAQ;EACR;EACA,YAAY;EACb,CAAC;;;;;;;;;;;AAYJ,SAAgB,0BACd,QACA,SAGuB;CACvB,MAAM,EAAE,SAAS,eAAe,aAAa,kBAAkB,OAAO;AAEtE,QAAO,eAAe,SAAS;EAC7B,QAAQ;EACR;EACA,YAAY;EACZ,kBAAkB,SAAS;EAC5B,CAAC;;;;;AAMJ,SAAS,kBAAkB,QAIzB;CAEA,MAAM,iBAAiB,yBAAc,OAAO;CAC5C,MAAM,2BAAW,IAAI,KAAa;CAGlC,IAAI,aAAsB,KAAA;CAC1B,IAAI,aAAa;CAGjB,MAAM,yBAAyB,QAAQ,QAAQ,eAAe,CAAC,KAC7D,OAAO,WAAW;AAChB,QAAM,kBAAkB,SAAS,SAAS;AACxC,YAAS,IAAI,KAAK;IAClB;AACF,eAAa,qBAAqB,OAAO;AACzC,eAAa;AACb,SAAO;GAEV;AAGD,yBAAwB,uBAAuB;CAE/C,MAAM,gBAAuC,EAC3C,0BAA0B,QAC3B;CAED,MAAM,gBAAgB;AACpB,MAAI,WAAY,QAAO;AAEvB,SAAO,IAAI,uBAAuB;;AAGpC,QAAO;EAAE;EAAS;EAAe;EAAU"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/plugin/rscCssTransform.ts
|
|
2
|
+
var TSS_SERVERFN_SPLIT_PARAM = "tss-serverfn-split";
|
|
3
|
+
var RSC_CSS_OPTIONS_KEY = "__tanstackStartRscCss";
|
|
4
|
+
function createRscCssCompilerTransforms(opts) {
|
|
5
|
+
let loadCssExpression;
|
|
6
|
+
const getLoadCssExpression = (context) => {
|
|
7
|
+
loadCssExpression ??= context.parseExpression(opts.loadCssExpression);
|
|
8
|
+
return loadCssExpression;
|
|
9
|
+
};
|
|
10
|
+
return [
|
|
11
|
+
createRscCssCompilerTransform({
|
|
12
|
+
serverFnProviderOnly: opts.serverFnProviderOnly,
|
|
13
|
+
getLoadCssExpression,
|
|
14
|
+
kind: "renderServerComponent",
|
|
15
|
+
name: "react-rsc-render-server-component-css"
|
|
16
|
+
}),
|
|
17
|
+
createRscCssCompilerTransform({
|
|
18
|
+
serverFnProviderOnly: opts.serverFnProviderOnly,
|
|
19
|
+
getLoadCssExpression,
|
|
20
|
+
kind: "createCompositeComponent",
|
|
21
|
+
name: "react-rsc-create-composite-component-css"
|
|
22
|
+
}),
|
|
23
|
+
createRscCssCompilerTransform({
|
|
24
|
+
serverFnProviderOnly: opts.serverFnProviderOnly,
|
|
25
|
+
getLoadCssExpression,
|
|
26
|
+
kind: "renderToReadableStream",
|
|
27
|
+
name: "react-rsc-render-to-readable-stream-css"
|
|
28
|
+
})
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
function createRscCssCompilerTransform(opts) {
|
|
32
|
+
return {
|
|
33
|
+
name: opts.name,
|
|
34
|
+
environment: "server",
|
|
35
|
+
imports: [{
|
|
36
|
+
libName: "@tanstack/react-start/rsc",
|
|
37
|
+
rootExport: opts.kind
|
|
38
|
+
}, {
|
|
39
|
+
libName: "@tanstack/react-start-rsc",
|
|
40
|
+
rootExport: opts.kind
|
|
41
|
+
}],
|
|
42
|
+
detect: new RegExp(`\\b${opts.kind}\\b`),
|
|
43
|
+
transform: (candidates, context) => {
|
|
44
|
+
if (opts.serverFnProviderOnly && !context.id.includes(TSS_SERVERFN_SPLIT_PARAM)) return;
|
|
45
|
+
const t = context.types;
|
|
46
|
+
const loadCssExpression = opts.getLoadCssExpression(context);
|
|
47
|
+
const cloneLoadCssExpression = () => t.cloneNode(loadCssExpression);
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const args = candidate.path.node.arguments;
|
|
50
|
+
if (args.length !== 1) continue;
|
|
51
|
+
if (opts.kind === "renderToReadableStream") {
|
|
52
|
+
const firstArg = args[0];
|
|
53
|
+
if (!firstArg || !t.isExpression(firstArg)) continue;
|
|
54
|
+
if (!isTopLevelJsx(t, firstArg)) continue;
|
|
55
|
+
args[0] = createCssFragment(t, firstArg, cloneLoadCssExpression());
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
args.push(t.objectExpression([t.objectProperty(t.identifier(RSC_CSS_OPTIONS_KEY), cloneLoadCssExpression())]));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function isTopLevelJsx(t, expr) {
|
|
64
|
+
const unwrapped = unwrapTransparentExpression(t, expr);
|
|
65
|
+
return t.isJSXElement(unwrapped) || t.isJSXFragment(unwrapped);
|
|
66
|
+
}
|
|
67
|
+
function unwrapTransparentExpression(t, expr) {
|
|
68
|
+
let current = expr;
|
|
69
|
+
while (t.isParenthesizedExpression(current) || t.isTSAsExpression(current) || t.isTSSatisfiesExpression(current) || t.isTSTypeAssertion(current) || t.isTSNonNullExpression(current)) current = current.expression;
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
function createCssFragment(t, original, loadCssExpression) {
|
|
73
|
+
const unwrapped = unwrapTransparentExpression(t, original);
|
|
74
|
+
return t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), [t.jsxExpressionContainer(loadCssExpression), t.isJSXElement(unwrapped) || t.isJSXFragment(unwrapped) ? unwrapped : t.jsxExpressionContainer(original)]);
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { createRscCssCompilerTransforms };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=rscCssTransform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rscCssTransform.js","names":[],"sources":["../../../src/plugin/rscCssTransform.ts"],"sourcesContent":["import type {\n StartCompilerImportTransform,\n StartCompilerTransformContext,\n} from '@tanstack/start-plugin-core'\n\nconst TSS_SERVERFN_SPLIT_PARAM = 'tss-serverfn-split'\nconst RSC_CSS_OPTIONS_KEY = '__tanstackStartRscCss'\n\ntype BabelTypes = StartCompilerTransformContext['types']\ntype BabelExpression = ReturnType<\n StartCompilerTransformContext['parseExpression']\n>\n\ntype RscCssTransformKind =\n | 'renderServerComponent'\n | 'createCompositeComponent'\n | 'renderToReadableStream'\n\nexport function createRscCssCompilerTransforms(opts: {\n loadCssExpression: string\n serverFnProviderOnly?: boolean | undefined\n}): Array<StartCompilerImportTransform> {\n let loadCssExpression: BabelExpression | undefined\n\n const getLoadCssExpression = (context: StartCompilerTransformContext) => {\n loadCssExpression ??= context.parseExpression(opts.loadCssExpression)\n return loadCssExpression\n }\n\n return [\n createRscCssCompilerTransform({\n serverFnProviderOnly: opts.serverFnProviderOnly,\n getLoadCssExpression,\n kind: 'renderServerComponent',\n name: 'react-rsc-render-server-component-css',\n }),\n createRscCssCompilerTransform({\n serverFnProviderOnly: opts.serverFnProviderOnly,\n getLoadCssExpression,\n kind: 'createCompositeComponent',\n name: 'react-rsc-create-composite-component-css',\n }),\n createRscCssCompilerTransform({\n serverFnProviderOnly: opts.serverFnProviderOnly,\n getLoadCssExpression,\n kind: 'renderToReadableStream',\n name: 'react-rsc-render-to-readable-stream-css',\n }),\n ]\n}\n\nfunction createRscCssCompilerTransform(opts: {\n name: string\n kind: RscCssTransformKind\n getLoadCssExpression: (\n context: StartCompilerTransformContext,\n ) => BabelExpression\n serverFnProviderOnly?: boolean | undefined\n}): StartCompilerImportTransform {\n return {\n name: opts.name,\n environment: 'server',\n imports: [\n {\n libName: '@tanstack/react-start/rsc',\n rootExport: opts.kind,\n },\n {\n libName: '@tanstack/react-start-rsc',\n rootExport: opts.kind,\n },\n ],\n detect: new RegExp(`\\\\b${opts.kind}\\\\b`),\n transform: (candidates, context) => {\n if (\n opts.serverFnProviderOnly &&\n !context.id.includes(TSS_SERVERFN_SPLIT_PARAM)\n ) {\n return\n }\n\n const t = context.types\n const loadCssExpression = opts.getLoadCssExpression(context)\n const cloneLoadCssExpression = () => t.cloneNode(loadCssExpression)\n\n for (const candidate of candidates) {\n const args = candidate.path.node.arguments\n if (args.length !== 1) continue\n\n if (opts.kind === 'renderToReadableStream') {\n const firstArg = args[0]\n if (!firstArg || !t.isExpression(firstArg)) continue\n if (!isTopLevelJsx(t, firstArg)) continue\n\n args[0] = createCssFragment(\n t,\n firstArg,\n cloneLoadCssExpression(),\n ) as typeof firstArg\n continue\n }\n\n args.push(\n t.objectExpression([\n t.objectProperty(\n t.identifier(RSC_CSS_OPTIONS_KEY),\n cloneLoadCssExpression(),\n ),\n ]),\n )\n }\n },\n }\n}\n\nfunction isTopLevelJsx(t: BabelTypes, expr: BabelExpression): boolean {\n const unwrapped = unwrapTransparentExpression(t, expr)\n return t.isJSXElement(unwrapped) || t.isJSXFragment(unwrapped)\n}\n\nfunction unwrapTransparentExpression(\n t: BabelTypes,\n expr: BabelExpression,\n): BabelExpression {\n let current = expr\n while (\n t.isParenthesizedExpression(current) ||\n t.isTSAsExpression(current) ||\n t.isTSSatisfiesExpression(current) ||\n t.isTSTypeAssertion(current) ||\n t.isTSNonNullExpression(current)\n ) {\n current = current.expression\n }\n return current\n}\n\nfunction createCssFragment(\n t: BabelTypes,\n original: BabelExpression,\n loadCssExpression: BabelExpression,\n) {\n const unwrapped = unwrapTransparentExpression(t, original)\n return t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), [\n t.jsxExpressionContainer(loadCssExpression),\n t.isJSXElement(unwrapped) || t.isJSXFragment(unwrapped)\n ? unwrapped\n : t.jsxExpressionContainer(original),\n ])\n}\n"],"mappings":";AAKA,IAAM,2BAA2B;AACjC,IAAM,sBAAsB;AAY5B,SAAgB,+BAA+B,MAGP;CACtC,IAAI;CAEJ,MAAM,wBAAwB,YAA2C;AACvE,wBAAsB,QAAQ,gBAAgB,KAAK,kBAAkB;AACrE,SAAO;;AAGT,QAAO;EACL,8BAA8B;GAC5B,sBAAsB,KAAK;GAC3B;GACA,MAAM;GACN,MAAM;GACP,CAAC;EACF,8BAA8B;GAC5B,sBAAsB,KAAK;GAC3B;GACA,MAAM;GACN,MAAM;GACP,CAAC;EACF,8BAA8B;GAC5B,sBAAsB,KAAK;GAC3B;GACA,MAAM;GACN,MAAM;GACP,CAAC;EACH;;AAGH,SAAS,8BAA8B,MAON;AAC/B,QAAO;EACL,MAAM,KAAK;EACX,aAAa;EACb,SAAS,CACP;GACE,SAAS;GACT,YAAY,KAAK;GAClB,EACD;GACE,SAAS;GACT,YAAY,KAAK;GAClB,CACF;EACD,QAAQ,IAAI,OAAO,MAAM,KAAK,KAAK,KAAK;EACxC,YAAY,YAAY,YAAY;AAClC,OACE,KAAK,wBACL,CAAC,QAAQ,GAAG,SAAS,yBAAyB,CAE9C;GAGF,MAAM,IAAI,QAAQ;GAClB,MAAM,oBAAoB,KAAK,qBAAqB,QAAQ;GAC5D,MAAM,+BAA+B,EAAE,UAAU,kBAAkB;AAEnE,QAAK,MAAM,aAAa,YAAY;IAClC,MAAM,OAAO,UAAU,KAAK,KAAK;AACjC,QAAI,KAAK,WAAW,EAAG;AAEvB,QAAI,KAAK,SAAS,0BAA0B;KAC1C,MAAM,WAAW,KAAK;AACtB,SAAI,CAAC,YAAY,CAAC,EAAE,aAAa,SAAS,CAAE;AAC5C,SAAI,CAAC,cAAc,GAAG,SAAS,CAAE;AAEjC,UAAK,KAAK,kBACR,GACA,UACA,wBAAwB,CACzB;AACD;;AAGF,SAAK,KACH,EAAE,iBAAiB,CACjB,EAAE,eACA,EAAE,WAAW,oBAAoB,EACjC,wBAAwB,CACzB,CACF,CAAC,CACH;;;EAGN;;AAGH,SAAS,cAAc,GAAe,MAAgC;CACpE,MAAM,YAAY,4BAA4B,GAAG,KAAK;AACtD,QAAO,EAAE,aAAa,UAAU,IAAI,EAAE,cAAc,UAAU;;AAGhE,SAAS,4BACP,GACA,MACiB;CACjB,IAAI,UAAU;AACd,QACE,EAAE,0BAA0B,QAAQ,IACpC,EAAE,iBAAiB,QAAQ,IAC3B,EAAE,wBAAwB,QAAQ,IAClC,EAAE,kBAAkB,QAAQ,IAC5B,EAAE,sBAAsB,QAAQ,CAEhC,WAAU,QAAQ;AAEpB,QAAO;;AAGT,SAAS,kBACP,GACA,UACA,mBACA;CACA,MAAM,YAAY,4BAA4B,GAAG,SAAS;AAC1D,QAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,EAAE,oBAAoB,EAAE,CACnE,EAAE,uBAAuB,kBAAkB,EAC3C,EAAE,aAAa,UAAU,IAAI,EAAE,cAAc,UAAU,GACnD,YACA,EAAE,uBAAuB,SAAS,CACvC,CAAC"}
|
package/dist/esm/plugin/vite.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createRscCssCompilerTransforms } from "./rscCssTransform.js";
|
|
1
2
|
import { fileURLToPath } from "node:url";
|
|
2
3
|
import path from "pathe";
|
|
3
4
|
import { createVirtualModule } from "@tanstack/start-plugin-core/vite";
|
|
@@ -32,7 +33,8 @@ function configureRsc() {
|
|
|
32
33
|
export: "rscSerializationAdapter",
|
|
33
34
|
isFactory: true
|
|
34
35
|
}
|
|
35
|
-
}]
|
|
36
|
+
}],
|
|
37
|
+
compilerTransforms: createRscCssCompilerTransforms({ loadCssExpression: "import.meta.viteRsc.loadCss()" })
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
function reactStartRscVitePlugin() {
|
|
@@ -64,6 +66,21 @@ function reactStartRscVitePlugin() {
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
},
|
|
69
|
+
{
|
|
70
|
+
name: "tanstack-react-start:rsc-scan-virtual-fallback",
|
|
71
|
+
apply: "build",
|
|
72
|
+
applyToEnvironment(env) {
|
|
73
|
+
return env.name === RSC_ENV_NAME;
|
|
74
|
+
},
|
|
75
|
+
load: {
|
|
76
|
+
filter: { id: /^(virtual:tanstack-rsc-runtime|virtual:vite-rsc\/encryption-key)$/ },
|
|
77
|
+
handler(id) {
|
|
78
|
+
if (this.environment.config.build.write !== false) return;
|
|
79
|
+
if (id === RSC_RUNTIME_VIRTUAL_ID) return `export { renderToReadableStream, createFromReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState } from '@vitejs/plugin-rsc/rsc'`;
|
|
80
|
+
if (id === "virtual:vite-rsc/encryption-key") return `export default () => ''`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
67
84
|
createVirtualModule({
|
|
68
85
|
name: "tanstack-react-start:rsc-runtime-virtual",
|
|
69
86
|
moduleId: RSC_RUNTIME_VIRTUAL_ID,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.js","names":[],"sources":["../../../src/plugin/vite.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url'\nimport path from 'pathe'\nimport { createVirtualModule } from '@tanstack/start-plugin-core/vite'\nimport type {\n TanStackStartVitePluginCoreOptions,\n ViteRscForwardSsrResolverStrategy,\n} from '@tanstack/start-plugin-core/vite'\nimport type { PluginOption, UserConfig } from 'vite'\n\nconst isClientEnvironment = (env: { config: { consumer: string } }) =>\n env.config.consumer === 'client'\n\n// Virtual module ids used by the React Start RSC runtime.\nconst RSC_HMR_VIRTUAL_ID = 'virtual:tanstack-rsc-hmr'\nconst RSC_RUNTIME_VIRTUAL_ID = 'virtual:tanstack-rsc-runtime'\nconst RSC_BROWSER_DECODE_VIRTUAL_ID = 'virtual:tanstack-rsc-browser-decode'\nconst RSC_SSR_DECODE_VIRTUAL_ID = 'virtual:tanstack-rsc-ssr-decode'\nconst RSC_ENV_NAME = 'rsc'\n\nconst currentDir = path.dirname(fileURLToPath(import.meta.url))\nconst entryDir = path.resolve(currentDir, '..', '..', 'plugin', 'entry')\nconst rscEntryPath = path.resolve(entryDir, 'rsc.tsx')\n\nexport function configureRsc(): {\n envName: string\n providerEnvironmentName: TanStackStartVitePluginCoreOptions['providerEnvironmentName']\n ssrResolverStrategy: TanStackStartVitePluginCoreOptions['ssrResolverStrategy']\n serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters']\n} {\n const serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters'] =\n [\n // IMPORTANT: plugin-adapters-plugin only calls the top-level factory once.\n // That factory must return a flat array of adapters (not nested arrays),\n // otherwise router-core ends up with non-adapter entries and Seroval crashes.\n {\n client: {\n module: '@tanstack/react-start/rsc/serialization/client',\n export: 'rscSerializationAdapter',\n isFactory: true,\n },\n server: {\n module: '@tanstack/react-start/rsc/serialization/server',\n export: 'rscSerializationAdapter',\n isFactory: true,\n },\n },\n ]\n const ssrResolverStrategy = {\n type: 'vite-rsc-forward',\n sourceEnvironmentName: RSC_ENV_NAME,\n sourceEntry: 'index',\n exportName: 'getServerFnById',\n } satisfies ViteRscForwardSsrResolverStrategy\n return {\n envName: RSC_ENV_NAME,\n providerEnvironmentName: RSC_ENV_NAME,\n ssrResolverStrategy,\n serializationAdapters,\n }\n}\nexport function reactStartRscVitePlugin(): PluginOption {\n return [\n // When RSC is enabled, SSR needs noExternal: true to ensure single React instance.\n // The RSC decoder's dynamic imports for client components can cause module duplication\n // without this, leading to \"Invalid hook call\" errors.\n // We use the top-level `ssr` config option as `environments.ssr.resolve.noExternal`\n // doesn't have the same effect.\n {\n name: 'tanstack-react-start:rsc-ssr-config',\n config() {\n return {\n ssr: {\n noExternal: true,\n },\n }\n },\n },\n {\n name: 'tanstack-react-start:rsc-env-config',\n config() {\n return {\n rsc: {\n // Disable @vitejs/plugin-rsc's built-in server handler middleware.\n // TanStack Start has its own request handling via the SSR environment.\n serverHandler: false,\n // Disable CSS link precedence to prevent React 19 SSR suspension\n // TanStack Start handles CSS preloading via manifest injection instead\n cssLinkPrecedence: false,\n },\n environments: {\n [RSC_ENV_NAME]: {\n consumer: 'server',\n // Force @tanstack packages to be processed by Vite as source code\n // rather than treated as external modules. This ensures:\n // 1. createIsomorphicFn transforms are applied\n // 2. Imports are resolved within the RSC environment context\n // with proper react-server conditions and pre-bundled deps\n resolve: {\n noExternal: [\n '@tanstack/start**',\n '@tanstack/react-start',\n '@tanstack/react-start-rsc',\n '@tanstack/react-router',\n ],\n },\n build: {\n rollupOptions: {\n input: {\n index: rscEntryPath,\n },\n },\n },\n },\n },\n } satisfies UserConfig & {\n rsc: {\n serverHandler: false\n cssLinkPrecedence?: boolean\n }\n }\n },\n },\n\n // Runtime bridge into the Vite RSC environment.\n createVirtualModule({\n name: 'tanstack-react-start:rsc-runtime-virtual',\n moduleId: RSC_RUNTIME_VIRTUAL_ID,\n load() {\n const envName = this.environment.name\n if (envName === RSC_ENV_NAME) {\n return `export { renderToReadableStream, createFromReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState } from '@vitejs/plugin-rsc/rsc'`\n }\n return `\nexport function renderToReadableStream() { throw new Error('renderToReadableStream can only be used in RSC environment'); }\nexport function createFromReadableStream() { throw new Error('createFromReadableStream can only be used in RSC environment'); }\nexport function createTemporaryReferenceSet() { throw new Error('createTemporaryReferenceSet can only be used in RSC environment'); }\nexport function decodeReply() { throw new Error('decodeReply can only be used in RSC environment'); }\nexport function loadServerAction() { throw new Error('loadServerAction can only be used in RSC environment'); }\nexport function decodeAction() { throw new Error('decodeAction can only be used in RSC environment'); }\nexport function decodeFormState() { throw new Error('decodeFormState can only be used in RSC environment'); }\n`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-browser-decode-virtual',\n moduleId: RSC_BROWSER_DECODE_VIRTUAL_ID,\n load() {\n return `export { createFromReadableStream, createFromFetch } from '@vitejs/plugin-rsc/browser'`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-ssr-decode-virtual',\n moduleId: RSC_SSR_DECODE_VIRTUAL_ID,\n load() {\n return `export { setOnClientReference, createFromReadableStream } from '@vitejs/plugin-rsc/ssr'`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-hmr-virtual:dev',\n moduleId: RSC_HMR_VIRTUAL_ID,\n apply: 'serve',\n applyToEnvironment: isClientEnvironment,\n load() {\n return `\nexport function setupRscHmr() {\nif (!import.meta.hot) {\n return\n}\n\n let __invalidateQueued = false\n\n function __queueInvalidate() {\n if (__invalidateQueued) return\n __invalidateQueued = true\n queueMicrotask(async () => {\n __invalidateQueued = false\n try {\n const router = window.__TSR_ROUTER__\n if (!router) {\n console.warn('[rsc:hmr] No router found on window.__TSR_ROUTER__')\n return\n }\n await router.invalidate()\n } catch (e) {\n console.warn('[rsc:hmr] Failed to invalidate router:', e)\n }\n })\n }\n\n import.meta.hot.on('rsc:update', () => {\n __queueInvalidate()\n })\n}\n`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-hmr-virtual:prod',\n moduleId: RSC_HMR_VIRTUAL_ID,\n applyToEnvironment: isClientEnvironment,\n apply: 'build',\n load() {\n return 'export function setupRscHmr() {} '\n },\n }),\n ]\n}\n"],"mappings":";;;;AASA,IAAM,uBAAuB,QAC3B,IAAI,OAAO,aAAa;AAG1B,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAC/B,IAAM,gCAAgC;AACtC,IAAM,4BAA4B;AAClC,IAAM,eAAe;AAErB,IAAM,aAAa,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAC/D,IAAM,WAAW,KAAK,QAAQ,YAAY,MAAM,MAAM,UAAU,QAAQ;AACxE,IAAM,eAAe,KAAK,QAAQ,UAAU,UAAU;AAEtD,SAAgB,eAKd;AAyBA,QAAO;EACL,SAAS;EACT,yBAAyB;EACzB,qBAT0B;GAC1B,MAAM;GACN,uBAAuB;GACvB,aAAa;GACb,YAAY;GACb;EAKC,uBA3BA,CAIE;GACE,QAAQ;IACN,QAAQ;IACR,QAAQ;IACR,WAAW;IACZ;GACD,QAAQ;IACN,QAAQ;IACR,QAAQ;IACR,WAAW;IACZ;GACF,CACF;EAYF;;AAEH,SAAgB,0BAAwC;AACtD,QAAO;EAML;GACE,MAAM;GACN,SAAS;AACP,WAAO,EACL,KAAK,EACH,YAAY,MACb,EACF;;GAEJ;EACD;GACE,MAAM;GACN,SAAS;AACP,WAAO;KACL,KAAK;MAGH,eAAe;MAGf,mBAAmB;MACpB;KACD,cAAc,GACX,eAAe;MACd,UAAU;MAMV,SAAS,EACP,YAAY;OACV;OACA;OACA;OACA;OACD,EACF;MACD,OAAO,EACL,eAAe,EACb,OAAO,EACL,OAAO,cACR,EACF,EACF;MACF,EACF;KACF;;GAOJ;EAGD,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;AAEL,QADgB,KAAK,YAAY,SACjB,aACd,QAAO;AAET,WAAO;;;;;;;;;;GAUV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;AACL,WAAO;;GAEV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;AACL,WAAO;;GAEV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;GACP,oBAAoB;GACpB,OAAO;AACL,WAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,oBAAoB;GACpB,OAAO;GACP,OAAO;AACL,WAAO;;GAEV,CAAC;EACH"}
|
|
1
|
+
{"version":3,"file":"vite.js","names":[],"sources":["../../../src/plugin/vite.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url'\nimport path from 'pathe'\nimport { createVirtualModule } from '@tanstack/start-plugin-core/vite'\nimport { createRscCssCompilerTransforms } from './rscCssTransform'\nimport type {\n TanStackStartVitePluginCoreOptions,\n ViteRscForwardSsrResolverStrategy,\n} from '@tanstack/start-plugin-core/vite'\nimport type { PluginOption, UserConfig } from 'vite'\n\nconst isClientEnvironment = (env: { config: { consumer: string } }) =>\n env.config.consumer === 'client'\n\n// Virtual module ids used by the React Start RSC runtime.\nconst RSC_HMR_VIRTUAL_ID = 'virtual:tanstack-rsc-hmr'\nconst RSC_RUNTIME_VIRTUAL_ID = 'virtual:tanstack-rsc-runtime'\nconst RSC_BROWSER_DECODE_VIRTUAL_ID = 'virtual:tanstack-rsc-browser-decode'\nconst RSC_SSR_DECODE_VIRTUAL_ID = 'virtual:tanstack-rsc-ssr-decode'\nconst RSC_ENV_NAME = 'rsc'\n\nconst currentDir = path.dirname(fileURLToPath(import.meta.url))\nconst entryDir = path.resolve(currentDir, '..', '..', 'plugin', 'entry')\nconst rscEntryPath = path.resolve(entryDir, 'rsc.tsx')\n\nexport function configureRsc(): {\n envName: string\n providerEnvironmentName: TanStackStartVitePluginCoreOptions['providerEnvironmentName']\n ssrResolverStrategy: TanStackStartVitePluginCoreOptions['ssrResolverStrategy']\n serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters']\n compilerTransforms: TanStackStartVitePluginCoreOptions['compilerTransforms']\n} {\n const serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters'] =\n [\n // IMPORTANT: plugin-adapters-plugin only calls the top-level factory once.\n // That factory must return a flat array of adapters (not nested arrays),\n // otherwise router-core ends up with non-adapter entries and Seroval crashes.\n {\n client: {\n module: '@tanstack/react-start/rsc/serialization/client',\n export: 'rscSerializationAdapter',\n isFactory: true,\n },\n server: {\n module: '@tanstack/react-start/rsc/serialization/server',\n export: 'rscSerializationAdapter',\n isFactory: true,\n },\n },\n ]\n const ssrResolverStrategy = {\n type: 'vite-rsc-forward',\n sourceEnvironmentName: RSC_ENV_NAME,\n sourceEntry: 'index',\n exportName: 'getServerFnById',\n } satisfies ViteRscForwardSsrResolverStrategy\n return {\n envName: RSC_ENV_NAME,\n providerEnvironmentName: RSC_ENV_NAME,\n ssrResolverStrategy,\n serializationAdapters,\n compilerTransforms: createRscCssCompilerTransforms({\n loadCssExpression: 'import.meta.viteRsc.loadCss()',\n }),\n }\n}\nexport function reactStartRscVitePlugin(): PluginOption {\n return [\n // When RSC is enabled, SSR needs noExternal: true to ensure single React instance.\n // The RSC decoder's dynamic imports for client components can cause module duplication\n // without this, leading to \"Invalid hook call\" errors.\n // We use the top-level `ssr` config option as `environments.ssr.resolve.noExternal`\n // doesn't have the same effect.\n {\n name: 'tanstack-react-start:rsc-ssr-config',\n config() {\n return {\n ssr: {\n noExternal: true,\n },\n }\n },\n },\n {\n name: 'tanstack-react-start:rsc-env-config',\n config() {\n return {\n rsc: {\n // Disable @vitejs/plugin-rsc's built-in server handler middleware.\n // TanStack Start has its own request handling via the SSR environment.\n serverHandler: false,\n // Disable CSS link precedence to prevent React 19 SSR suspension\n // TanStack Start handles CSS preloading via manifest injection instead\n cssLinkPrecedence: false,\n },\n environments: {\n [RSC_ENV_NAME]: {\n consumer: 'server',\n // Force @tanstack packages to be processed by Vite as source code\n // rather than treated as external modules. This ensures:\n // 1. createIsomorphicFn transforms are applied\n // 2. Imports are resolved within the RSC environment context\n // with proper react-server conditions and pre-bundled deps\n resolve: {\n noExternal: [\n '@tanstack/start**',\n '@tanstack/react-start',\n '@tanstack/react-start-rsc',\n '@tanstack/react-router',\n ],\n },\n build: {\n rollupOptions: {\n input: {\n index: rscEntryPath,\n },\n },\n },\n },\n },\n } satisfies UserConfig & {\n rsc: {\n serverHandler: false\n cssLinkPrecedence?: boolean\n }\n }\n },\n },\n\n {\n name: 'tanstack-react-start:rsc-scan-virtual-fallback',\n apply: 'build',\n applyToEnvironment(env) {\n return env.name === RSC_ENV_NAME\n },\n load: {\n filter: {\n id: /^(virtual:tanstack-rsc-runtime|virtual:vite-rsc\\/encryption-key)$/,\n },\n handler(id) {\n if (this.environment.config.build.write !== false) return\n\n if (id === RSC_RUNTIME_VIRTUAL_ID) {\n return `export { renderToReadableStream, createFromReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState } from '@vitejs/plugin-rsc/rsc'`\n }\n\n if (id === 'virtual:vite-rsc/encryption-key') {\n return `export default () => ''`\n }\n\n return undefined\n },\n },\n },\n\n // Runtime bridge into the Vite RSC environment.\n createVirtualModule({\n name: 'tanstack-react-start:rsc-runtime-virtual',\n moduleId: RSC_RUNTIME_VIRTUAL_ID,\n load() {\n const envName = this.environment.name\n if (envName === RSC_ENV_NAME) {\n return `export { renderToReadableStream, createFromReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState } from '@vitejs/plugin-rsc/rsc'`\n }\n return `\nexport function renderToReadableStream() { throw new Error('renderToReadableStream can only be used in RSC environment'); }\nexport function createFromReadableStream() { throw new Error('createFromReadableStream can only be used in RSC environment'); }\nexport function createTemporaryReferenceSet() { throw new Error('createTemporaryReferenceSet can only be used in RSC environment'); }\nexport function decodeReply() { throw new Error('decodeReply can only be used in RSC environment'); }\nexport function loadServerAction() { throw new Error('loadServerAction can only be used in RSC environment'); }\nexport function decodeAction() { throw new Error('decodeAction can only be used in RSC environment'); }\nexport function decodeFormState() { throw new Error('decodeFormState can only be used in RSC environment'); }\n`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-browser-decode-virtual',\n moduleId: RSC_BROWSER_DECODE_VIRTUAL_ID,\n load() {\n return `export { createFromReadableStream, createFromFetch } from '@vitejs/plugin-rsc/browser'`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-ssr-decode-virtual',\n moduleId: RSC_SSR_DECODE_VIRTUAL_ID,\n load() {\n return `export { setOnClientReference, createFromReadableStream } from '@vitejs/plugin-rsc/ssr'`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-hmr-virtual:dev',\n moduleId: RSC_HMR_VIRTUAL_ID,\n apply: 'serve',\n applyToEnvironment: isClientEnvironment,\n load() {\n return `\nexport function setupRscHmr() {\nif (!import.meta.hot) {\n return\n}\n\n let __invalidateQueued = false\n\n function __queueInvalidate() {\n if (__invalidateQueued) return\n __invalidateQueued = true\n queueMicrotask(async () => {\n __invalidateQueued = false\n try {\n const router = window.__TSR_ROUTER__\n if (!router) {\n console.warn('[rsc:hmr] No router found on window.__TSR_ROUTER__')\n return\n }\n await router.invalidate()\n } catch (e) {\n console.warn('[rsc:hmr] Failed to invalidate router:', e)\n }\n })\n }\n\n import.meta.hot.on('rsc:update', () => {\n __queueInvalidate()\n })\n}\n`\n },\n }),\n createVirtualModule({\n name: 'tanstack-react-start:rsc-hmr-virtual:prod',\n moduleId: RSC_HMR_VIRTUAL_ID,\n applyToEnvironment: isClientEnvironment,\n apply: 'build',\n load() {\n return 'export function setupRscHmr() {} '\n },\n }),\n ]\n}\n"],"mappings":";;;;;AAUA,IAAM,uBAAuB,QAC3B,IAAI,OAAO,aAAa;AAG1B,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAC/B,IAAM,gCAAgC;AACtC,IAAM,4BAA4B;AAClC,IAAM,eAAe;AAErB,IAAM,aAAa,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAC/D,IAAM,WAAW,KAAK,QAAQ,YAAY,MAAM,MAAM,UAAU,QAAQ;AACxE,IAAM,eAAe,KAAK,QAAQ,UAAU,UAAU;AAEtD,SAAgB,eAMd;AAyBA,QAAO;EACL,SAAS;EACT,yBAAyB;EACzB,qBAT0B;GAC1B,MAAM;GACN,uBAAuB;GACvB,aAAa;GACb,YAAY;GACb;EAKC,uBA3BA,CAIE;GACE,QAAQ;IACN,QAAQ;IACR,QAAQ;IACR,WAAW;IACZ;GACD,QAAQ;IACN,QAAQ;IACR,QAAQ;IACR,WAAW;IACZ;GACF,CACF;EAYD,oBAAoB,+BAA+B,EACjD,mBAAmB,iCACpB,CAAC;EACH;;AAEH,SAAgB,0BAAwC;AACtD,QAAO;EAML;GACE,MAAM;GACN,SAAS;AACP,WAAO,EACL,KAAK,EACH,YAAY,MACb,EACF;;GAEJ;EACD;GACE,MAAM;GACN,SAAS;AACP,WAAO;KACL,KAAK;MAGH,eAAe;MAGf,mBAAmB;MACpB;KACD,cAAc,GACX,eAAe;MACd,UAAU;MAMV,SAAS,EACP,YAAY;OACV;OACA;OACA;OACA;OACD,EACF;MACD,OAAO,EACL,eAAe,EACb,OAAO,EACL,OAAO,cACR,EACF,EACF;MACF,EACF;KACF;;GAOJ;EAED;GACE,MAAM;GACN,OAAO;GACP,mBAAmB,KAAK;AACtB,WAAO,IAAI,SAAS;;GAEtB,MAAM;IACJ,QAAQ,EACN,IAAI,qEACL;IACD,QAAQ,IAAI;AACV,SAAI,KAAK,YAAY,OAAO,MAAM,UAAU,MAAO;AAEnD,SAAI,OAAO,uBACT,QAAO;AAGT,SAAI,OAAO,kCACT,QAAO;;IAKZ;GACF;EAGD,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;AAEL,QADgB,KAAK,YAAY,SACjB,aACd,QAAO;AAET,WAAO;;;;;;;;;;GAUV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;AACL,WAAO;;GAEV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;AACL,WAAO;;GAEV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,OAAO;GACP,oBAAoB;GACpB,OAAO;AACL,WAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCV,CAAC;EACF,oBAAoB;GAClB,MAAM;GACN,UAAU;GACV,oBAAoB;GACpB,OAAO;GACP,OAAO;AACL,WAAO;;GAEV,CAAC;EACH"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from "./ServerComponentTypes.js";
|
|
2
2
|
import { ReplayableStream } from "./ReplayableStream.js";
|
|
3
|
+
import { createRscCssEnvelope } from "./rscCssEnvelope.js";
|
|
3
4
|
import { renderToReadableStream } from "virtual:tanstack-rsc-runtime";
|
|
4
5
|
import { getRequest } from "@tanstack/start-server-core";
|
|
5
6
|
import { getStartContext } from "@tanstack/start-storage-context";
|
|
@@ -27,8 +28,8 @@ import { getStartContext } from "@tanstack/start-storage-context";
|
|
|
27
28
|
* )
|
|
28
29
|
* ```
|
|
29
30
|
*/
|
|
30
|
-
async function renderServerComponent(node) {
|
|
31
|
-
const flightStream = renderToReadableStream(node);
|
|
31
|
+
async function renderServerComponent(node, options) {
|
|
32
|
+
const flightStream = renderToReadableStream(createRscCssEnvelope(node, options));
|
|
32
33
|
const isRouterRequest = getStartContext({ throwIfNotFound: false })?.handlerType === "router";
|
|
33
34
|
const ssrHandler = globalThis.__RSC_SSR__;
|
|
34
35
|
if (isRouterRequest && ssrHandler) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderServerComponent.js","names":[],"sources":["../../src/renderServerComponent.ts"],"sourcesContent":["import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'\nimport { getRequest } from '@tanstack/start-server-core'\nimport { getStartContext } from '@tanstack/start-storage-context'\nimport { ReplayableStream } from './ReplayableStream'\nimport { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from './ServerComponentTypes'\nimport type {\n AnyRenderableServerComponent,\n RenderableServerComponentBuilder,\n ServerComponentStream,\n ValidateRenderableServerComponent,\n} from './ServerComponentTypes'\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): Promise<RenderableServerComponentBuilder<TNode>> {\n const flightStream = renderToReadableStream(node)\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 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":"
|
|
1
|
+
{"version":3,"file":"renderServerComponent.js","names":[],"sources":["../../src/renderServerComponent.ts"],"sourcesContent":["import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'\nimport { getRequest } from '@tanstack/start-server-core'\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 = 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 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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,eAAsB,sBACpB,MACA,SACkD;CAClD,MAAM,eAAe,uBACnB,qBAAqB,MAAM,QAAQ,CACpC;CAID,MAAM,kBADM,gBAAgB,EAAE,iBAAiB,OAAO,CAAC,EAC1B,gBAAgB;CAC7C,MAAM,aAAa,WAAW;AAG9B,KAAI,mBAAmB,YAAY;EACjC,MAAM,SAAS,YAAY,CAAC;EAC5B,MAAM,SAAS,IAAI,iBAAiB,cAAc,EAAE,QAAQ,CAAC;EAG7D,MAAM,UAAU,MAAM,WAAW,OAAO,OAAO;AAC/C,SAAO,WAAW,sBAChB,QACA,QACD;;AAIH,QAAO,uBACL,aACD;;;;;;AAOH,SAAS,uBACP,cAC8B;CAC9B,MAAM,gBAAuC,EAC3C,0BAA0B,cAC3B;CAED,MAAM,OAAO,SAAS,oBAA2B;AAC/C,QAAM,IAAI,MACR,oHAED;;AAGD,MAAa,2BAA2B;AACxC,MAAa,kBAAkB;AACjC,QAAO"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/rscCssEnvelope.ts
|
|
2
|
+
var RSC_CSS_ENVELOPE_MARKER = "__tanstackStartRscCssEnvelope";
|
|
3
|
+
var RSC_CSS_ENVELOPE_RESOURCES = "__tanstackStartRscCss";
|
|
4
|
+
var RSC_CSS_ENVELOPE_VALUE = "__tanstackStartRscValue";
|
|
5
|
+
function createRscCssEnvelope(value, options) {
|
|
6
|
+
const resources = options?.[RSC_CSS_ENVELOPE_RESOURCES];
|
|
7
|
+
if (resources === void 0 || resources === null || resources === false) return value;
|
|
8
|
+
return {
|
|
9
|
+
[RSC_CSS_ENVELOPE_MARKER]: true,
|
|
10
|
+
[RSC_CSS_ENVELOPE_RESOURCES]: resources,
|
|
11
|
+
[RSC_CSS_ENVELOPE_VALUE]: value
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function unwrapRscCssEnvelope(value) {
|
|
15
|
+
if (!value || typeof value !== "object") return value;
|
|
16
|
+
const maybeEnvelope = value;
|
|
17
|
+
if (maybeEnvelope[RSC_CSS_ENVELOPE_MARKER] !== true) return value;
|
|
18
|
+
return maybeEnvelope[RSC_CSS_ENVELOPE_VALUE];
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { createRscCssEnvelope, unwrapRscCssEnvelope };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=rscCssEnvelope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rscCssEnvelope.js","names":[],"sources":["../../src/rscCssEnvelope.ts"],"sourcesContent":["import type React from 'react'\n\nconst RSC_CSS_ENVELOPE_MARKER = '__tanstackStartRscCssEnvelope'\nconst RSC_CSS_ENVELOPE_RESOURCES = '__tanstackStartRscCss'\nconst RSC_CSS_ENVELOPE_VALUE = '__tanstackStartRscValue'\n\nexport interface RscCssEnvelopeOptions {\n [RSC_CSS_ENVELOPE_RESOURCES]?: React.ReactNode\n}\n\nexport function createRscCssEnvelope<TValue>(\n value: TValue,\n options?: RscCssEnvelopeOptions,\n): TValue | Record<string, unknown> {\n const resources = options?.[RSC_CSS_ENVELOPE_RESOURCES]\n if (resources === undefined || resources === null || resources === false) {\n return value\n }\n\n return {\n [RSC_CSS_ENVELOPE_MARKER]: true,\n [RSC_CSS_ENVELOPE_RESOURCES]: resources,\n [RSC_CSS_ENVELOPE_VALUE]: value,\n }\n}\n\nexport function unwrapRscCssEnvelope(value: unknown): unknown {\n if (!value || typeof value !== 'object') {\n return value\n }\n\n const maybeEnvelope = value as Record<string, unknown>\n if (maybeEnvelope[RSC_CSS_ENVELOPE_MARKER] !== true) {\n return value\n }\n\n return maybeEnvelope[RSC_CSS_ENVELOPE_VALUE]\n}\n"],"mappings":";AAEA,IAAM,0BAA0B;AAChC,IAAM,6BAA6B;AACnC,IAAM,yBAAyB;AAM/B,SAAgB,qBACd,OACA,SACkC;CAClC,MAAM,YAAY,UAAU;AAC5B,KAAI,cAAc,KAAA,KAAa,cAAc,QAAQ,cAAc,MACjE,QAAO;AAGT,QAAO;GACJ,0BAA0B;GAC1B,6BAA6B;GAC7B,yBAAyB;EAC3B;;AAGH,SAAgB,qBAAqB,OAAyB;AAC5D,KAAI,CAAC,SAAS,OAAO,UAAU,SAC7B,QAAO;CAGT,MAAM,gBAAgB;AACtB,KAAI,cAAc,6BAA6B,KAC7C,QAAO;AAGT,QAAO,cAAc"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RENDERABLE_RSC, RSC_SLOT_USAGES_STREAM, SERVER_COMPONENT_STREAM, isServerComponent } from "./ServerComponentTypes.js";
|
|
2
|
+
import { unwrapRscCssEnvelope } from "./rscCssEnvelope.js";
|
|
2
3
|
import { awaitLazyElements } from "./awaitLazyElements.js";
|
|
3
4
|
import { createRscProxy } from "./createRscProxy.js";
|
|
4
5
|
import { getStartContext } from "@tanstack/start-storage-context";
|
|
@@ -17,7 +18,8 @@ setOnClientReference(({ deps, runtime }) => {
|
|
|
17
18
|
if (jsCollector) for (const href of deps.js) jsCollector.add(href);
|
|
18
19
|
if (!ctx || runtime === "rsbuild") return;
|
|
19
20
|
if (!ctx.requestAssets) ctx.requestAssets = [];
|
|
20
|
-
const seenHrefs =
|
|
21
|
+
const seenHrefs = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const asset of ctx.requestAssets) if (asset.tag === "link" && asset.attrs?.href) seenHrefs.add(asset.attrs.href);
|
|
21
23
|
for (const href of deps.js) {
|
|
22
24
|
if (seenHrefs.has(href)) continue;
|
|
23
25
|
seenHrefs.add(href);
|
|
@@ -49,12 +51,12 @@ var ssrHandler = {
|
|
|
49
51
|
const jsCollector = /* @__PURE__ */ new Set();
|
|
50
52
|
return decodeCollectorStorage.run(cssCollector, async () => {
|
|
51
53
|
return jsCollectorStorage.run(jsCollector, async () => {
|
|
52
|
-
const
|
|
53
|
-
await awaitLazyElements(
|
|
54
|
+
const decodedTree = await createFromReadableStream(readableStream);
|
|
55
|
+
await awaitLazyElements(decodedTree, (href) => {
|
|
54
56
|
cssCollector.add(href);
|
|
55
57
|
});
|
|
56
58
|
return {
|
|
57
|
-
tree,
|
|
59
|
+
tree: unwrapRscCssEnvelope(decodedTree),
|
|
58
60
|
cssHrefs: cssCollector.size > 0 ? cssCollector : void 0,
|
|
59
61
|
jsPreloads: jsCollector.size > 0 ? jsCollector : void 0
|
|
60
62
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialization.server.js","names":[],"sources":["../../src/serialization.server.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport { createSerializationAdapter } from '@tanstack/react-router'\nimport { RawStream } from '@tanstack/router-core'\nimport { getStartContext } from '@tanstack/start-storage-context'\nimport {\n setOnClientReference,\n createFromReadableStream as ssrDecode,\n} from 'virtual:tanstack-rsc-ssr-decode'\nimport {\n RENDERABLE_RSC,\n RSC_SLOT_USAGES_STREAM,\n SERVER_COMPONENT_STREAM,\n isServerComponent,\n} from './ServerComponentTypes'\nimport { createRscProxy } from './createRscProxy'\nimport { awaitLazyElements } from './awaitLazyElements'\nimport type {\n AnyCompositeComponent,\n ServerComponentStream,\n} from './ServerComponentTypes'\nimport type { RscDecodeResult, RscSsrHandler } from './rscSsrHandler'\n\n// ===== SSR Handler Registration =====\n// This handler is registered on globalThis for the RSC environment to access.\n// The RSC env calls these functions during loader execution to pre-decode streams\n// and create renderable proxies.\n//\n// This MUST happen in a module without 'use client' directive.\n// Modules with 'use client' may be transformed to client references in the\n// SSR environment when RSC is enabled, preventing the side effect from running.\n\n// AsyncLocalStorage for decode-scoped CSS collector.\n// Each decode() runs in its own async context with its own collector.\n// The onClientReference callback reads from this to write CSS hrefs.\nconst decodeCollectorStorage = new AsyncLocalStorage<Set<string>>()\nconst jsCollectorStorage = new AsyncLocalStorage<Set<string>>()\n\nsetOnClientReference(\n ({\n deps,\n runtime,\n }: {\n deps: { js: Array<string>; css: Array<string> }\n runtime?: 'rsbuild'\n }) => {\n const ctx = getStartContext({ throwIfNotFound: false })\n\n const cssCollector = decodeCollectorStorage.getStore()\n if (cssCollector) {\n for (const href of deps.css) {\n cssCollector.add(href)\n }\n }\n\n const jsCollector = jsCollectorStorage.getStore()\n if (jsCollector) {\n for (const href of deps.js) {\n jsCollector.add(href)\n }\n }\n\n if (!ctx || runtime === 'rsbuild') return\n\n if (!ctx.requestAssets) ctx.requestAssets = []\n const seenHrefs = new Set(\n
|
|
1
|
+
{"version":3,"file":"serialization.server.js","names":[],"sources":["../../src/serialization.server.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport { createSerializationAdapter } from '@tanstack/react-router'\nimport { RawStream } from '@tanstack/router-core'\nimport { getStartContext } from '@tanstack/start-storage-context'\nimport {\n setOnClientReference,\n createFromReadableStream as ssrDecode,\n} from 'virtual:tanstack-rsc-ssr-decode'\nimport {\n RENDERABLE_RSC,\n RSC_SLOT_USAGES_STREAM,\n SERVER_COMPONENT_STREAM,\n isServerComponent,\n} from './ServerComponentTypes'\nimport { createRscProxy } from './createRscProxy'\nimport { awaitLazyElements } from './awaitLazyElements'\nimport { unwrapRscCssEnvelope } from './rscCssEnvelope'\nimport type {\n AnyCompositeComponent,\n ServerComponentStream,\n} from './ServerComponentTypes'\nimport type { RscDecodeResult, RscSsrHandler } from './rscSsrHandler'\n\n// ===== SSR Handler Registration =====\n// This handler is registered on globalThis for the RSC environment to access.\n// The RSC env calls these functions during loader execution to pre-decode streams\n// and create renderable proxies.\n//\n// This MUST happen in a module without 'use client' directive.\n// Modules with 'use client' may be transformed to client references in the\n// SSR environment when RSC is enabled, preventing the side effect from running.\n\n// AsyncLocalStorage for decode-scoped CSS collector.\n// Each decode() runs in its own async context with its own collector.\n// The onClientReference callback reads from this to write CSS hrefs.\nconst decodeCollectorStorage = new AsyncLocalStorage<Set<string>>()\nconst jsCollectorStorage = new AsyncLocalStorage<Set<string>>()\n\nsetOnClientReference(\n ({\n deps,\n runtime,\n }: {\n deps: { js: Array<string>; css: Array<string> }\n runtime?: 'rsbuild'\n }) => {\n const ctx = getStartContext({ throwIfNotFound: false })\n\n const cssCollector = decodeCollectorStorage.getStore()\n if (cssCollector) {\n for (const href of deps.css) {\n cssCollector.add(href)\n }\n }\n\n const jsCollector = jsCollectorStorage.getStore()\n if (jsCollector) {\n for (const href of deps.js) {\n jsCollector.add(href)\n }\n }\n\n if (!ctx || runtime === 'rsbuild') return\n\n if (!ctx.requestAssets) ctx.requestAssets = []\n const seenHrefs = new Set<string>()\n for (const asset of ctx.requestAssets) {\n if (asset.tag === 'link' && asset.attrs?.href) {\n seenHrefs.add(asset.attrs.href as string)\n }\n }\n\n for (const href of deps.js) {\n if (seenHrefs.has(href)) continue\n seenHrefs.add(href)\n ctx.requestAssets.push({\n tag: 'link',\n attrs: { rel: 'modulepreload', href },\n })\n }\n\n for (const href of deps.css) {\n if (seenHrefs.has(href)) continue\n seenHrefs.add(href)\n ctx.requestAssets.push({\n tag: 'link',\n attrs: { rel: 'preload', href, as: 'style' },\n })\n }\n },\n)\n\nconst ssrHandler: RscSsrHandler = {\n async decode(stream: ServerComponentStream): Promise<RscDecodeResult> {\n const readableStream = stream.createReplayStream()\n\n // Create a collector for this decode operation.\n // Run the decode in an AsyncLocalStorage context so the onClientReference\n // callback can write to this specific collector even with parallel decodes.\n const cssCollector = new Set<string>()\n const jsCollector = new Set<string>()\n\n return decodeCollectorStorage.run(cssCollector, async () => {\n return jsCollectorStorage.run(jsCollector, async () => {\n const decodedTree = await ssrDecode(readableStream)\n await awaitLazyElements(decodedTree, (href) => {\n cssCollector.add(href)\n })\n\n return {\n tree: unwrapRscCssEnvelope(decodedTree),\n cssHrefs: cssCollector.size > 0 ? cssCollector : undefined,\n jsPreloads: jsCollector.size > 0 ? jsCollector : undefined,\n }\n })\n })\n },\n\n createRenderableProxy(stream, decoded): any {\n return createRscProxy(() => decoded.tree, {\n stream,\n cssHrefs: decoded.cssHrefs,\n jsPreloads: decoded.jsPreloads,\n renderable: true,\n })\n },\n\n createCompositeProxy(\n stream,\n decoded,\n slotUsagesStream,\n ): AnyCompositeComponent {\n const proxy = createRscProxy(() => decoded.tree, {\n stream,\n cssHrefs: decoded.cssHrefs,\n jsPreloads: decoded.jsPreloads,\n renderable: false,\n slotUsagesStream,\n })\n return proxy\n },\n}\n\n// Register SSR handler on globalThis for RSC environment to access.\nglobalThis.__RSC_SSR__ = ssrHandler\n\n// ===== End SSR Handler Registration =====\n\n/**\n * Helper to check if a value is a renderable RSC (from renderServerComponent).\n * The value can be either an object (proxy target) or a function (stub for server functions).\n */\nfunction isRenderableRsc(value: unknown): boolean {\n if (value === null || value === undefined) return false\n if (typeof value !== 'object' && typeof value !== 'function') return false\n return RENDERABLE_RSC in value && (value as any)[RENDERABLE_RSC] === true\n}\n\n/**\n * Server-side serialization adapter for RSC (renderable + composite).\n */\nconst adapter = createSerializationAdapter({\n key: '$RSC',\n test: (value: unknown): value is AnyCompositeComponent => {\n return isServerComponent(value)\n },\n toSerializable: (component: AnyCompositeComponent) => {\n const stream = component[SERVER_COMPONENT_STREAM]!.createReplayStream()\n\n const kind = isRenderableRsc(component) ? 'renderable' : 'composite'\n\n const slotUsagesStream =\n kind === 'composite' &&\n process.env.NODE_ENV === 'development' &&\n RSC_SLOT_USAGES_STREAM in component\n ? ((component as any)[RSC_SLOT_USAGES_STREAM] as unknown as\n | ReadableStream<any>\n | undefined)\n : undefined\n\n return {\n kind,\n stream: new RawStream(stream, { hint: 'text' }),\n slotUsagesStream,\n }\n },\n fromSerializable: (): never => {\n throw new Error('Server should never deserialize RSC data')\n },\n})\n\n/**\n * Factory function for server-side RSC serialization adapter.\n */\nexport const rscSerializationAdapter = () => [adapter]\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CompositeComponentResult, ValidateCompositeComponent } from './ServerComponentTypes.js';
|
|
2
|
+
import { RscCssEnvelopeOptions } from './rscCssEnvelope.js';
|
|
2
3
|
/**
|
|
3
4
|
* Creates a composite server component with slot support.
|
|
4
5
|
*
|
|
@@ -29,4 +30,4 @@ import { CompositeComponentResult, ValidateCompositeComponent } from './ServerCo
|
|
|
29
30
|
* )
|
|
30
31
|
* ```
|
|
31
32
|
*/
|
|
32
|
-
export declare function createCompositeComponent<TComp>(component: ValidateCompositeComponent<TComp
|
|
33
|
+
export declare function createCompositeComponent<TComp>(component: ValidateCompositeComponent<TComp>, options?: RscCssEnvelopeOptions): Promise<CompositeComponentResult<TComp>>;
|
|
@@ -5,5 +5,6 @@ export declare function configureRsc(): {
|
|
|
5
5
|
providerEnvironmentName: TanStackStartVitePluginCoreOptions['providerEnvironmentName'];
|
|
6
6
|
ssrResolverStrategy: TanStackStartVitePluginCoreOptions['ssrResolverStrategy'];
|
|
7
7
|
serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters'];
|
|
8
|
+
compilerTransforms: TanStackStartVitePluginCoreOptions['compilerTransforms'];
|
|
8
9
|
};
|
|
9
10
|
export declare function reactStartRscVitePlugin(): PluginOption;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AnyRenderableServerComponent, RenderableServerComponentBuilder, ValidateRenderableServerComponent } from './ServerComponentTypes.js';
|
|
2
|
+
import { RscCssEnvelopeOptions } from './rscCssEnvelope.js';
|
|
2
3
|
export type { RscSsrHandler, RscDecodeResult } from './rscSsrHandler.js';
|
|
3
4
|
/**
|
|
4
5
|
* Renderable RSC handle type - used for serialization detection.
|
|
@@ -30,4 +31,4 @@ export declare function isRenderableRscHandle(value: unknown): value is AnyRende
|
|
|
30
31
|
* )
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
33
|
-
export declare function renderServerComponent<TNode>(node: ValidateRenderableServerComponent<TNode
|
|
34
|
+
export declare function renderServerComponent<TNode>(node: ValidateRenderableServerComponent<TNode>, options?: RscCssEnvelopeOptions): Promise<RenderableServerComponentBuilder<TNode>>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { default as React } from 'react';
|
|
2
|
+
declare const RSC_CSS_ENVELOPE_RESOURCES = "__tanstackStartRscCss";
|
|
3
|
+
export interface RscCssEnvelopeOptions {
|
|
4
|
+
[RSC_CSS_ENVELOPE_RESOURCES]?: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
export declare function createRscCssEnvelope<TValue>(value: TValue, options?: RscCssEnvelopeOptions): TValue | Record<string, unknown>;
|
|
7
|
+
export declare function unwrapRscCssEnvelope(value: unknown): unknown;
|
|
8
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/react-start-rsc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.37",
|
|
4
4
|
"description": "React Server Components support for TanStack Start",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,6 +53,12 @@
|
|
|
53
53
|
"default": "./dist/esm/plugin/vite.js"
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
|
+
"./plugin/rscCssTransform": {
|
|
57
|
+
"import": {
|
|
58
|
+
"types": "./dist/esm/src/plugin/rscCssTransform.d.ts",
|
|
59
|
+
"default": "./dist/esm/plugin/rscCssTransform.js"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
56
62
|
"./rsbuild/ssr-decode": {
|
|
57
63
|
"import": {
|
|
58
64
|
"types": "./dist/esm/src/rsbuild/ssr-decode.d.ts",
|
|
@@ -77,12 +83,12 @@
|
|
|
77
83
|
"dependencies": {
|
|
78
84
|
"pathe": "^2.0.3",
|
|
79
85
|
"@tanstack/react-router": "1.169.1",
|
|
80
|
-
"@tanstack/router-core": "1.169.1",
|
|
81
86
|
"@tanstack/react-start-server": "1.166.48",
|
|
87
|
+
"@tanstack/router-core": "1.169.1",
|
|
82
88
|
"@tanstack/router-utils": "1.161.7",
|
|
83
89
|
"@tanstack/start-client-core": "1.168.1",
|
|
90
|
+
"@tanstack/start-plugin-core": "1.169.13",
|
|
84
91
|
"@tanstack/start-fn-stubs": "1.161.6",
|
|
85
|
-
"@tanstack/start-plugin-core": "1.169.12",
|
|
86
92
|
"@tanstack/start-server-core": "1.167.26",
|
|
87
93
|
"@tanstack/start-storage-context": "1.166.34"
|
|
88
94
|
},
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
RSC_SLOT_USAGES_STREAM,
|
|
10
10
|
SERVER_COMPONENT_STREAM,
|
|
11
11
|
} from './ServerComponentTypes'
|
|
12
|
+
import { createRscCssEnvelope } from './rscCssEnvelope'
|
|
12
13
|
import type {
|
|
13
14
|
AnyCompositeComponent,
|
|
14
15
|
CompositeComponentResult,
|
|
@@ -16,6 +17,7 @@ import type {
|
|
|
16
17
|
ServerComponentStream,
|
|
17
18
|
ValidateCompositeComponent,
|
|
18
19
|
} from './ServerComponentTypes'
|
|
20
|
+
import type { RscCssEnvelopeOptions } from './rscCssEnvelope'
|
|
19
21
|
|
|
20
22
|
import './rscSsrHandler' // Import for global declaration side effect
|
|
21
23
|
|
|
@@ -51,6 +53,7 @@ import './rscSsrHandler' // Import for global declaration side effect
|
|
|
51
53
|
*/
|
|
52
54
|
export async function createCompositeComponent<TComp>(
|
|
53
55
|
component: ValidateCompositeComponent<TComp>,
|
|
56
|
+
options?: RscCssEnvelopeOptions,
|
|
54
57
|
): Promise<CompositeComponentResult<TComp>> {
|
|
55
58
|
const isDev = process.env.NODE_ENV === 'development'
|
|
56
59
|
|
|
@@ -76,14 +79,17 @@ export async function createCompositeComponent<TComp>(
|
|
|
76
79
|
|
|
77
80
|
// Wrapper that renders the user's component inside Flight render context
|
|
78
81
|
async function ServerComponentWrapper() {
|
|
79
|
-
return (
|
|
82
|
+
return createRscCssEnvelope(
|
|
83
|
+
await (component as React.FC)(proxyProps),
|
|
84
|
+
options,
|
|
85
|
+
)
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
// Render using createElement so React calls our component during Flight rendering
|
|
83
89
|
// This is critical for React.cache to work - the component must be invoked
|
|
84
90
|
// during renderToReadableStream's execution, not before
|
|
85
91
|
const flightStream = renderToReadableStream(
|
|
86
|
-
createElement(ServerComponentWrapper),
|
|
92
|
+
createElement(ServerComponentWrapper as any),
|
|
87
93
|
)
|
|
88
94
|
|
|
89
95
|
// Check if this is an SSR request (router) or a direct server function call
|
|
@@ -6,6 +6,7 @@ import { createFromReadableStream as browserDecode } from 'virtual:tanstack-rsc-
|
|
|
6
6
|
|
|
7
7
|
import { awaitLazyElements } from './awaitLazyElements'
|
|
8
8
|
import { createRscProxy } from './createRscProxy'
|
|
9
|
+
import { unwrapRscCssEnvelope } from './rscCssEnvelope'
|
|
9
10
|
import type {
|
|
10
11
|
AnyCompositeComponent,
|
|
11
12
|
RscSlotUsageEvent,
|
|
@@ -80,9 +81,9 @@ function setupStreamDecode(stream: ReadableStream<Uint8Array>): {
|
|
|
80
81
|
await awaitLazyElements(result, (href) => {
|
|
81
82
|
cssHrefs.add(href)
|
|
82
83
|
})
|
|
83
|
-
cachedTree = result
|
|
84
|
+
cachedTree = unwrapRscCssEnvelope(result)
|
|
84
85
|
cacheReady = true
|
|
85
|
-
return
|
|
86
|
+
return cachedTree
|
|
86
87
|
},
|
|
87
88
|
)
|
|
88
89
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StartCompilerImportTransform,
|
|
3
|
+
StartCompilerTransformContext,
|
|
4
|
+
} from '@tanstack/start-plugin-core'
|
|
5
|
+
|
|
6
|
+
const TSS_SERVERFN_SPLIT_PARAM = 'tss-serverfn-split'
|
|
7
|
+
const RSC_CSS_OPTIONS_KEY = '__tanstackStartRscCss'
|
|
8
|
+
|
|
9
|
+
type BabelTypes = StartCompilerTransformContext['types']
|
|
10
|
+
type BabelExpression = ReturnType<
|
|
11
|
+
StartCompilerTransformContext['parseExpression']
|
|
12
|
+
>
|
|
13
|
+
|
|
14
|
+
type RscCssTransformKind =
|
|
15
|
+
| 'renderServerComponent'
|
|
16
|
+
| 'createCompositeComponent'
|
|
17
|
+
| 'renderToReadableStream'
|
|
18
|
+
|
|
19
|
+
export function createRscCssCompilerTransforms(opts: {
|
|
20
|
+
loadCssExpression: string
|
|
21
|
+
serverFnProviderOnly?: boolean | undefined
|
|
22
|
+
}): Array<StartCompilerImportTransform> {
|
|
23
|
+
let loadCssExpression: BabelExpression | undefined
|
|
24
|
+
|
|
25
|
+
const getLoadCssExpression = (context: StartCompilerTransformContext) => {
|
|
26
|
+
loadCssExpression ??= context.parseExpression(opts.loadCssExpression)
|
|
27
|
+
return loadCssExpression
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
createRscCssCompilerTransform({
|
|
32
|
+
serverFnProviderOnly: opts.serverFnProviderOnly,
|
|
33
|
+
getLoadCssExpression,
|
|
34
|
+
kind: 'renderServerComponent',
|
|
35
|
+
name: 'react-rsc-render-server-component-css',
|
|
36
|
+
}),
|
|
37
|
+
createRscCssCompilerTransform({
|
|
38
|
+
serverFnProviderOnly: opts.serverFnProviderOnly,
|
|
39
|
+
getLoadCssExpression,
|
|
40
|
+
kind: 'createCompositeComponent',
|
|
41
|
+
name: 'react-rsc-create-composite-component-css',
|
|
42
|
+
}),
|
|
43
|
+
createRscCssCompilerTransform({
|
|
44
|
+
serverFnProviderOnly: opts.serverFnProviderOnly,
|
|
45
|
+
getLoadCssExpression,
|
|
46
|
+
kind: 'renderToReadableStream',
|
|
47
|
+
name: 'react-rsc-render-to-readable-stream-css',
|
|
48
|
+
}),
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createRscCssCompilerTransform(opts: {
|
|
53
|
+
name: string
|
|
54
|
+
kind: RscCssTransformKind
|
|
55
|
+
getLoadCssExpression: (
|
|
56
|
+
context: StartCompilerTransformContext,
|
|
57
|
+
) => BabelExpression
|
|
58
|
+
serverFnProviderOnly?: boolean | undefined
|
|
59
|
+
}): StartCompilerImportTransform {
|
|
60
|
+
return {
|
|
61
|
+
name: opts.name,
|
|
62
|
+
environment: 'server',
|
|
63
|
+
imports: [
|
|
64
|
+
{
|
|
65
|
+
libName: '@tanstack/react-start/rsc',
|
|
66
|
+
rootExport: opts.kind,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
libName: '@tanstack/react-start-rsc',
|
|
70
|
+
rootExport: opts.kind,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
detect: new RegExp(`\\b${opts.kind}\\b`),
|
|
74
|
+
transform: (candidates, context) => {
|
|
75
|
+
if (
|
|
76
|
+
opts.serverFnProviderOnly &&
|
|
77
|
+
!context.id.includes(TSS_SERVERFN_SPLIT_PARAM)
|
|
78
|
+
) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const t = context.types
|
|
83
|
+
const loadCssExpression = opts.getLoadCssExpression(context)
|
|
84
|
+
const cloneLoadCssExpression = () => t.cloneNode(loadCssExpression)
|
|
85
|
+
|
|
86
|
+
for (const candidate of candidates) {
|
|
87
|
+
const args = candidate.path.node.arguments
|
|
88
|
+
if (args.length !== 1) continue
|
|
89
|
+
|
|
90
|
+
if (opts.kind === 'renderToReadableStream') {
|
|
91
|
+
const firstArg = args[0]
|
|
92
|
+
if (!firstArg || !t.isExpression(firstArg)) continue
|
|
93
|
+
if (!isTopLevelJsx(t, firstArg)) continue
|
|
94
|
+
|
|
95
|
+
args[0] = createCssFragment(
|
|
96
|
+
t,
|
|
97
|
+
firstArg,
|
|
98
|
+
cloneLoadCssExpression(),
|
|
99
|
+
) as typeof firstArg
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
args.push(
|
|
104
|
+
t.objectExpression([
|
|
105
|
+
t.objectProperty(
|
|
106
|
+
t.identifier(RSC_CSS_OPTIONS_KEY),
|
|
107
|
+
cloneLoadCssExpression(),
|
|
108
|
+
),
|
|
109
|
+
]),
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isTopLevelJsx(t: BabelTypes, expr: BabelExpression): boolean {
|
|
117
|
+
const unwrapped = unwrapTransparentExpression(t, expr)
|
|
118
|
+
return t.isJSXElement(unwrapped) || t.isJSXFragment(unwrapped)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function unwrapTransparentExpression(
|
|
122
|
+
t: BabelTypes,
|
|
123
|
+
expr: BabelExpression,
|
|
124
|
+
): BabelExpression {
|
|
125
|
+
let current = expr
|
|
126
|
+
while (
|
|
127
|
+
t.isParenthesizedExpression(current) ||
|
|
128
|
+
t.isTSAsExpression(current) ||
|
|
129
|
+
t.isTSSatisfiesExpression(current) ||
|
|
130
|
+
t.isTSTypeAssertion(current) ||
|
|
131
|
+
t.isTSNonNullExpression(current)
|
|
132
|
+
) {
|
|
133
|
+
current = current.expression
|
|
134
|
+
}
|
|
135
|
+
return current
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createCssFragment(
|
|
139
|
+
t: BabelTypes,
|
|
140
|
+
original: BabelExpression,
|
|
141
|
+
loadCssExpression: BabelExpression,
|
|
142
|
+
) {
|
|
143
|
+
const unwrapped = unwrapTransparentExpression(t, original)
|
|
144
|
+
return t.jsxFragment(t.jsxOpeningFragment(), t.jsxClosingFragment(), [
|
|
145
|
+
t.jsxExpressionContainer(loadCssExpression),
|
|
146
|
+
t.isJSXElement(unwrapped) || t.isJSXFragment(unwrapped)
|
|
147
|
+
? unwrapped
|
|
148
|
+
: t.jsxExpressionContainer(original),
|
|
149
|
+
])
|
|
150
|
+
}
|
package/src/plugin/vite.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url'
|
|
2
2
|
import path from 'pathe'
|
|
3
3
|
import { createVirtualModule } from '@tanstack/start-plugin-core/vite'
|
|
4
|
+
import { createRscCssCompilerTransforms } from './rscCssTransform'
|
|
4
5
|
import type {
|
|
5
6
|
TanStackStartVitePluginCoreOptions,
|
|
6
7
|
ViteRscForwardSsrResolverStrategy,
|
|
@@ -26,6 +27,7 @@ export function configureRsc(): {
|
|
|
26
27
|
providerEnvironmentName: TanStackStartVitePluginCoreOptions['providerEnvironmentName']
|
|
27
28
|
ssrResolverStrategy: TanStackStartVitePluginCoreOptions['ssrResolverStrategy']
|
|
28
29
|
serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters']
|
|
30
|
+
compilerTransforms: TanStackStartVitePluginCoreOptions['compilerTransforms']
|
|
29
31
|
} {
|
|
30
32
|
const serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters'] =
|
|
31
33
|
[
|
|
@@ -56,6 +58,9 @@ export function configureRsc(): {
|
|
|
56
58
|
providerEnvironmentName: RSC_ENV_NAME,
|
|
57
59
|
ssrResolverStrategy,
|
|
58
60
|
serializationAdapters,
|
|
61
|
+
compilerTransforms: createRscCssCompilerTransforms({
|
|
62
|
+
loadCssExpression: 'import.meta.viteRsc.loadCss()',
|
|
63
|
+
}),
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
export function reactStartRscVitePlugin(): PluginOption {
|
|
@@ -121,6 +126,32 @@ export function reactStartRscVitePlugin(): PluginOption {
|
|
|
121
126
|
},
|
|
122
127
|
},
|
|
123
128
|
|
|
129
|
+
{
|
|
130
|
+
name: 'tanstack-react-start:rsc-scan-virtual-fallback',
|
|
131
|
+
apply: 'build',
|
|
132
|
+
applyToEnvironment(env) {
|
|
133
|
+
return env.name === RSC_ENV_NAME
|
|
134
|
+
},
|
|
135
|
+
load: {
|
|
136
|
+
filter: {
|
|
137
|
+
id: /^(virtual:tanstack-rsc-runtime|virtual:vite-rsc\/encryption-key)$/,
|
|
138
|
+
},
|
|
139
|
+
handler(id) {
|
|
140
|
+
if (this.environment.config.build.write !== false) return
|
|
141
|
+
|
|
142
|
+
if (id === RSC_RUNTIME_VIRTUAL_ID) {
|
|
143
|
+
return `export { renderToReadableStream, createFromReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState } from '@vitejs/plugin-rsc/rsc'`
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (id === 'virtual:vite-rsc/encryption-key') {
|
|
147
|
+
return `export default () => ''`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return undefined
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
|
|
124
155
|
// Runtime bridge into the Vite RSC environment.
|
|
125
156
|
createVirtualModule({
|
|
126
157
|
name: 'tanstack-react-start:rsc-runtime-virtual',
|
|
@@ -3,12 +3,14 @@ import { getRequest } from '@tanstack/start-server-core'
|
|
|
3
3
|
import { getStartContext } from '@tanstack/start-storage-context'
|
|
4
4
|
import { ReplayableStream } from './ReplayableStream'
|
|
5
5
|
import { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from './ServerComponentTypes'
|
|
6
|
+
import { createRscCssEnvelope } from './rscCssEnvelope'
|
|
6
7
|
import type {
|
|
7
8
|
AnyRenderableServerComponent,
|
|
8
9
|
RenderableServerComponentBuilder,
|
|
9
10
|
ServerComponentStream,
|
|
10
11
|
ValidateRenderableServerComponent,
|
|
11
12
|
} from './ServerComponentTypes'
|
|
13
|
+
import type { RscCssEnvelopeOptions } from './rscCssEnvelope'
|
|
12
14
|
|
|
13
15
|
import './rscSsrHandler'
|
|
14
16
|
// Import for global declaration side effect
|
|
@@ -57,8 +59,11 @@ export function isRenderableRscHandle(
|
|
|
57
59
|
*/
|
|
58
60
|
export async function renderServerComponent<TNode>(
|
|
59
61
|
node: ValidateRenderableServerComponent<TNode>,
|
|
62
|
+
options?: RscCssEnvelopeOptions,
|
|
60
63
|
): Promise<RenderableServerComponentBuilder<TNode>> {
|
|
61
|
-
const flightStream = renderToReadableStream(
|
|
64
|
+
const flightStream = renderToReadableStream(
|
|
65
|
+
createRscCssEnvelope(node, options),
|
|
66
|
+
)
|
|
62
67
|
|
|
63
68
|
// Check if this is an SSR request (router) or a direct server function call
|
|
64
69
|
const ctx = getStartContext({ throwIfNotFound: false })
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
|
|
3
|
+
const RSC_CSS_ENVELOPE_MARKER = '__tanstackStartRscCssEnvelope'
|
|
4
|
+
const RSC_CSS_ENVELOPE_RESOURCES = '__tanstackStartRscCss'
|
|
5
|
+
const RSC_CSS_ENVELOPE_VALUE = '__tanstackStartRscValue'
|
|
6
|
+
|
|
7
|
+
export interface RscCssEnvelopeOptions {
|
|
8
|
+
[RSC_CSS_ENVELOPE_RESOURCES]?: React.ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createRscCssEnvelope<TValue>(
|
|
12
|
+
value: TValue,
|
|
13
|
+
options?: RscCssEnvelopeOptions,
|
|
14
|
+
): TValue | Record<string, unknown> {
|
|
15
|
+
const resources = options?.[RSC_CSS_ENVELOPE_RESOURCES]
|
|
16
|
+
if (resources === undefined || resources === null || resources === false) {
|
|
17
|
+
return value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
[RSC_CSS_ENVELOPE_MARKER]: true,
|
|
22
|
+
[RSC_CSS_ENVELOPE_RESOURCES]: resources,
|
|
23
|
+
[RSC_CSS_ENVELOPE_VALUE]: value,
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function unwrapRscCssEnvelope(value: unknown): unknown {
|
|
28
|
+
if (!value || typeof value !== 'object') {
|
|
29
|
+
return value
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const maybeEnvelope = value as Record<string, unknown>
|
|
33
|
+
if (maybeEnvelope[RSC_CSS_ENVELOPE_MARKER] !== true) {
|
|
34
|
+
return value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return maybeEnvelope[RSC_CSS_ENVELOPE_VALUE]
|
|
38
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './ServerComponentTypes'
|
|
15
15
|
import { createRscProxy } from './createRscProxy'
|
|
16
16
|
import { awaitLazyElements } from './awaitLazyElements'
|
|
17
|
+
import { unwrapRscCssEnvelope } from './rscCssEnvelope'
|
|
17
18
|
import type {
|
|
18
19
|
AnyCompositeComponent,
|
|
19
20
|
ServerComponentStream,
|
|
@@ -62,11 +63,12 @@ setOnClientReference(
|
|
|
62
63
|
if (!ctx || runtime === 'rsbuild') return
|
|
63
64
|
|
|
64
65
|
if (!ctx.requestAssets) ctx.requestAssets = []
|
|
65
|
-
const seenHrefs = new Set(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
.
|
|
69
|
-
|
|
66
|
+
const seenHrefs = new Set<string>()
|
|
67
|
+
for (const asset of ctx.requestAssets) {
|
|
68
|
+
if (asset.tag === 'link' && asset.attrs?.href) {
|
|
69
|
+
seenHrefs.add(asset.attrs.href as string)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
70
72
|
|
|
71
73
|
for (const href of deps.js) {
|
|
72
74
|
if (seenHrefs.has(href)) continue
|
|
@@ -100,13 +102,13 @@ const ssrHandler: RscSsrHandler = {
|
|
|
100
102
|
|
|
101
103
|
return decodeCollectorStorage.run(cssCollector, async () => {
|
|
102
104
|
return jsCollectorStorage.run(jsCollector, async () => {
|
|
103
|
-
const
|
|
104
|
-
await awaitLazyElements(
|
|
105
|
+
const decodedTree = await ssrDecode(readableStream)
|
|
106
|
+
await awaitLazyElements(decodedTree, (href) => {
|
|
105
107
|
cssCollector.add(href)
|
|
106
108
|
})
|
|
107
109
|
|
|
108
110
|
return {
|
|
109
|
-
tree,
|
|
111
|
+
tree: unwrapRscCssEnvelope(decodedTree),
|
|
110
112
|
cssHrefs: cssCollector.size > 0 ? cssCollector : undefined,
|
|
111
113
|
jsPreloads: jsCollector.size > 0 ? jsCollector : undefined,
|
|
112
114
|
}
|