@tanstack/react-start-rsc 0.0.0

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.
Files changed (101) hide show
  1. package/dist/esm/ClientSlot.js +19 -0
  2. package/dist/esm/ClientSlot.js.map +1 -0
  3. package/dist/esm/CompositeComponent.js +93 -0
  4. package/dist/esm/CompositeComponent.js.map +1 -0
  5. package/dist/esm/ReplayableStream.js +147 -0
  6. package/dist/esm/ReplayableStream.js.map +1 -0
  7. package/dist/esm/RscNodeRenderer.js +46 -0
  8. package/dist/esm/RscNodeRenderer.js.map +1 -0
  9. package/dist/esm/ServerComponentTypes.js +22 -0
  10. package/dist/esm/ServerComponentTypes.js.map +1 -0
  11. package/dist/esm/SlotContext.js +30 -0
  12. package/dist/esm/SlotContext.js.map +1 -0
  13. package/dist/esm/awaitLazyElements.js +41 -0
  14. package/dist/esm/awaitLazyElements.js.map +1 -0
  15. package/dist/esm/createCompositeComponent.js +205 -0
  16. package/dist/esm/createCompositeComponent.js.map +1 -0
  17. package/dist/esm/createCompositeComponent.stub.js +15 -0
  18. package/dist/esm/createCompositeComponent.stub.js.map +1 -0
  19. package/dist/esm/createRscProxy.js +138 -0
  20. package/dist/esm/createRscProxy.js.map +1 -0
  21. package/dist/esm/createServerComponentFromStream.js +74 -0
  22. package/dist/esm/createServerComponentFromStream.js.map +1 -0
  23. package/dist/esm/entry/rsc.js +21 -0
  24. package/dist/esm/entry/rsc.js.map +1 -0
  25. package/dist/esm/flight.js +56 -0
  26. package/dist/esm/flight.js.map +1 -0
  27. package/dist/esm/flight.rsc.js +2 -0
  28. package/dist/esm/flight.stub.js +15 -0
  29. package/dist/esm/flight.stub.js.map +1 -0
  30. package/dist/esm/index.js +7 -0
  31. package/dist/esm/index.rsc.js +6 -0
  32. package/dist/esm/plugin/vite.js +172 -0
  33. package/dist/esm/plugin/vite.js.map +1 -0
  34. package/dist/esm/reactSymbols.js +8 -0
  35. package/dist/esm/reactSymbols.js.map +1 -0
  36. package/dist/esm/renderServerComponent.js +58 -0
  37. package/dist/esm/renderServerComponent.js.map +1 -0
  38. package/dist/esm/renderServerComponent.stub.js +16 -0
  39. package/dist/esm/renderServerComponent.stub.js.map +1 -0
  40. package/dist/esm/serialization.client.js +21 -0
  41. package/dist/esm/serialization.client.js.map +1 -0
  42. package/dist/esm/serialization.server.js +121 -0
  43. package/dist/esm/serialization.server.js.map +1 -0
  44. package/dist/esm/slotUsageSanitizer.js +33 -0
  45. package/dist/esm/slotUsageSanitizer.js.map +1 -0
  46. package/dist/esm/src/ClientSlot.d.ts +5 -0
  47. package/dist/esm/src/CompositeComponent.d.ts +28 -0
  48. package/dist/esm/src/ReplayableStream.d.ts +76 -0
  49. package/dist/esm/src/RscNodeRenderer.d.ts +7 -0
  50. package/dist/esm/src/ServerComponentTypes.d.ts +99 -0
  51. package/dist/esm/src/SlotContext.d.ts +21 -0
  52. package/dist/esm/src/awaitLazyElements.d.ts +17 -0
  53. package/dist/esm/src/createCompositeComponent.d.ts +32 -0
  54. package/dist/esm/src/createCompositeComponent.stub.d.ts +9 -0
  55. package/dist/esm/src/createRscProxy.d.ts +18 -0
  56. package/dist/esm/src/createServerComponentFromStream.d.ts +24 -0
  57. package/dist/esm/src/entry/rsc.d.ts +7 -0
  58. package/dist/esm/src/flight.d.ts +41 -0
  59. package/dist/esm/src/flight.rsc.d.ts +17 -0
  60. package/dist/esm/src/flight.stub.d.ts +8 -0
  61. package/dist/esm/src/index.d.ts +7 -0
  62. package/dist/esm/src/index.rsc.d.ts +6 -0
  63. package/dist/esm/src/plugin/vite.d.ts +9 -0
  64. package/dist/esm/src/reactSymbols.d.ts +3 -0
  65. package/dist/esm/src/renderServerComponent.d.ts +33 -0
  66. package/dist/esm/src/renderServerComponent.stub.d.ts +9 -0
  67. package/dist/esm/src/rscSsrHandler.d.ts +24 -0
  68. package/dist/esm/src/serialization.client.d.ts +11 -0
  69. package/dist/esm/src/serialization.server.d.ts +10 -0
  70. package/dist/esm/src/slotUsageSanitizer.d.ts +1 -0
  71. package/dist/esm/src/types.d.ts +13 -0
  72. package/dist/plugin/entry/rsc.tsx +23 -0
  73. package/package.json +108 -0
  74. package/src/ClientSlot.tsx +34 -0
  75. package/src/CompositeComponent.tsx +165 -0
  76. package/src/ReplayableStream.ts +249 -0
  77. package/src/RscNodeRenderer.tsx +76 -0
  78. package/src/ServerComponentTypes.ts +226 -0
  79. package/src/SlotContext.tsx +42 -0
  80. package/src/awaitLazyElements.ts +91 -0
  81. package/src/createCompositeComponent.stub.ts +20 -0
  82. package/src/createCompositeComponent.ts +338 -0
  83. package/src/createRscProxy.tsx +294 -0
  84. package/src/createServerComponentFromStream.ts +105 -0
  85. package/src/entry/rsc.tsx +23 -0
  86. package/src/entry/virtual-modules.d.ts +12 -0
  87. package/src/flight.rsc.ts +17 -0
  88. package/src/flight.stub.ts +15 -0
  89. package/src/flight.ts +68 -0
  90. package/src/global.d.ts +75 -0
  91. package/src/index.rsc.ts +25 -0
  92. package/src/index.ts +26 -0
  93. package/src/plugin/vite.ts +241 -0
  94. package/src/reactSymbols.ts +6 -0
  95. package/src/renderServerComponent.stub.ts +26 -0
  96. package/src/renderServerComponent.ts +110 -0
  97. package/src/rscSsrHandler.ts +39 -0
  98. package/src/serialization.client.ts +43 -0
  99. package/src/serialization.server.ts +193 -0
  100. package/src/slotUsageSanitizer.ts +62 -0
  101. package/src/types.ts +15 -0
@@ -0,0 +1,241 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import path from 'pathe'
3
+ import { resolveViteId } from '@tanstack/start-plugin-core/utils'
4
+ import type {
5
+ TanStackStartVitePluginCoreOptions,
6
+ ViteRscForwardSsrResolverStrategy,
7
+ } from '@tanstack/start-plugin-core/vite/types'
8
+ import type { Plugin, PluginOption, UserConfig } from 'vite'
9
+
10
+ type VirtualModuleLoadHandler = (this: {
11
+ environment: { name: string }
12
+ }) => string
13
+ const isClientEnvironment = (env: { config: { consumer: string } }) =>
14
+ env.config.consumer === 'client'
15
+
16
+ function escapeRegExp(value: string): string {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
18
+ }
19
+
20
+ function createVirtualModule(opts: {
21
+ name: string
22
+ moduleId: string
23
+ load: VirtualModuleLoadHandler
24
+ apply?: Plugin['apply']
25
+ applyToEnvironment?: Plugin['applyToEnvironment']
26
+ }): Plugin {
27
+ const resolvedId = resolveViteId(opts.moduleId)
28
+ const idFilter = { id: new RegExp(escapeRegExp(opts.moduleId)) }
29
+
30
+ return {
31
+ name: opts.name,
32
+ apply: opts.apply,
33
+ applyToEnvironment: opts.applyToEnvironment,
34
+ resolveId: {
35
+ filter: idFilter,
36
+ handler() {
37
+ return resolvedId
38
+ },
39
+ },
40
+ load: {
41
+ filter: idFilter,
42
+ handler: opts.load,
43
+ },
44
+ }
45
+ }
46
+
47
+ // Virtual module ids used by the React Start RSC runtime.
48
+ const RSC_HMR_VIRTUAL_ID = 'virtual:tanstack-rsc-hmr'
49
+ const RSC_RUNTIME_VIRTUAL_ID = 'virtual:tanstack-rsc-runtime'
50
+ const RSC_BROWSER_DECODE_VIRTUAL_ID = 'virtual:tanstack-rsc-browser-decode'
51
+ const RSC_SSR_DECODE_VIRTUAL_ID = 'virtual:tanstack-rsc-ssr-decode'
52
+ const RSC_ENV_NAME = 'rsc'
53
+
54
+ const currentDir = path.dirname(fileURLToPath(import.meta.url))
55
+ const entryDir = path.resolve(currentDir, '..', '..', 'plugin', 'entry')
56
+ const rscEntryPath = path.resolve(entryDir, 'rsc.tsx')
57
+
58
+ export function configureRsc(): {
59
+ envName: string
60
+ providerEnvironmentName: TanStackStartVitePluginCoreOptions['providerEnvironmentName']
61
+ ssrResolverStrategy: TanStackStartVitePluginCoreOptions['ssrResolverStrategy']
62
+ serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters']
63
+ } {
64
+ const serializationAdapters: TanStackStartVitePluginCoreOptions['serializationAdapters'] =
65
+ [
66
+ // IMPORTANT: plugin-adapters-plugin only calls the top-level factory once.
67
+ // That factory must return a flat array of adapters (not nested arrays),
68
+ // otherwise router-core ends up with non-adapter entries and Seroval crashes.
69
+ {
70
+ client: {
71
+ module: '@tanstack/react-start/rsc/serialization/client',
72
+ export: 'rscSerializationAdapter',
73
+ isFactory: true,
74
+ },
75
+ server: {
76
+ module: '@tanstack/react-start/rsc/serialization/server',
77
+ export: 'rscSerializationAdapter',
78
+ isFactory: true,
79
+ },
80
+ },
81
+ ]
82
+ const ssrResolverStrategy = {
83
+ type: 'vite-rsc-forward',
84
+ sourceEnvironmentName: RSC_ENV_NAME,
85
+ sourceEntry: 'index',
86
+ exportName: 'getServerFnById',
87
+ } satisfies ViteRscForwardSsrResolverStrategy
88
+ return {
89
+ envName: RSC_ENV_NAME,
90
+ providerEnvironmentName: RSC_ENV_NAME,
91
+ ssrResolverStrategy,
92
+ serializationAdapters,
93
+ }
94
+ }
95
+ export function reactStartRscVitePlugin(): PluginOption {
96
+ return [
97
+ // When RSC is enabled, SSR needs noExternal: true to ensure single React instance.
98
+ // The RSC decoder's dynamic imports for client components can cause module duplication
99
+ // without this, leading to "Invalid hook call" errors.
100
+ // We use the top-level `ssr` config option as `environments.ssr.resolve.noExternal`
101
+ // doesn't have the same effect.
102
+ {
103
+ name: 'tanstack-react-start:rsc-ssr-config',
104
+ config() {
105
+ return {
106
+ ssr: {
107
+ noExternal: true,
108
+ },
109
+ }
110
+ },
111
+ },
112
+ {
113
+ name: 'tanstack-react-start:rsc-env-config',
114
+ config() {
115
+ return {
116
+ rsc: {
117
+ // Disable @vitejs/plugin-rsc's built-in server handler middleware.
118
+ // TanStack Start has its own request handling via the SSR environment.
119
+ serverHandler: false,
120
+ // Disable CSS link precedence to prevent React 19 SSR suspension
121
+ // TanStack Start handles CSS preloading via manifest injection instead
122
+ cssLinkPrecedence: false,
123
+ },
124
+ environments: {
125
+ [RSC_ENV_NAME]: {
126
+ consumer: 'server',
127
+ // Force @tanstack packages to be processed by Vite as source code
128
+ // rather than treated as external modules. This ensures:
129
+ // 1. createIsomorphicFn transforms are applied
130
+ // 2. Imports are resolved within the RSC environment context
131
+ // with proper react-server conditions and pre-bundled deps
132
+ resolve: {
133
+ noExternal: [
134
+ '@tanstack/start**',
135
+ '@tanstack/react-start',
136
+ '@tanstack/react-start-rsc',
137
+ '@tanstack/react-router',
138
+ ],
139
+ },
140
+ build: {
141
+ rollupOptions: {
142
+ input: {
143
+ index: rscEntryPath,
144
+ },
145
+ },
146
+ },
147
+ },
148
+ },
149
+ } satisfies UserConfig & {
150
+ rsc: {
151
+ serverHandler: false
152
+ cssLinkPrecedence?: boolean
153
+ }
154
+ }
155
+ },
156
+ },
157
+
158
+ // Runtime bridge into the Vite RSC environment.
159
+ createVirtualModule({
160
+ name: 'tanstack-react-start:rsc-runtime-virtual',
161
+ moduleId: RSC_RUNTIME_VIRTUAL_ID,
162
+ load() {
163
+ const envName = this.environment.name
164
+ if (envName === RSC_ENV_NAME) {
165
+ return `export { renderToReadableStream, createFromReadableStream, createTemporaryReferenceSet, decodeReply, loadServerAction, decodeAction, decodeFormState } from '@vitejs/plugin-rsc/rsc'`
166
+ }
167
+ return `
168
+ export function renderToReadableStream() { throw new Error('renderToReadableStream can only be used in RSC environment'); }
169
+ export function createFromReadableStream() { throw new Error('createFromReadableStream can only be used in RSC environment'); }
170
+ export function createTemporaryReferenceSet() { throw new Error('createTemporaryReferenceSet can only be used in RSC environment'); }
171
+ export function decodeReply() { throw new Error('decodeReply can only be used in RSC environment'); }
172
+ export function loadServerAction() { throw new Error('loadServerAction can only be used in RSC environment'); }
173
+ export function decodeAction() { throw new Error('decodeAction can only be used in RSC environment'); }
174
+ export function decodeFormState() { throw new Error('decodeFormState can only be used in RSC environment'); }
175
+ `
176
+ },
177
+ }),
178
+ createVirtualModule({
179
+ name: 'tanstack-react-start:rsc-browser-decode-virtual',
180
+ moduleId: RSC_BROWSER_DECODE_VIRTUAL_ID,
181
+ load() {
182
+ return `export { createFromReadableStream, createFromFetch } from '@vitejs/plugin-rsc/browser'`
183
+ },
184
+ }),
185
+ createVirtualModule({
186
+ name: 'tanstack-react-start:rsc-ssr-decode-virtual',
187
+ moduleId: RSC_SSR_DECODE_VIRTUAL_ID,
188
+ load() {
189
+ return `export { setOnClientReference, createFromReadableStream } from '@vitejs/plugin-rsc/ssr'`
190
+ },
191
+ }),
192
+ createVirtualModule({
193
+ name: 'tanstack-react-start:rsc-hmr-virtual:dev',
194
+ moduleId: RSC_HMR_VIRTUAL_ID,
195
+ apply: 'serve',
196
+ applyToEnvironment: isClientEnvironment,
197
+ load() {
198
+ return `
199
+ export function setupRscHmr() {
200
+ if (!import.meta.hot) {
201
+ return
202
+ }
203
+
204
+ let __invalidateQueued = false
205
+
206
+ function __queueInvalidate() {
207
+ if (__invalidateQueued) return
208
+ __invalidateQueued = true
209
+ queueMicrotask(async () => {
210
+ __invalidateQueued = false
211
+ try {
212
+ const router = window.__TSR_ROUTER__
213
+ if (!router) {
214
+ console.warn('[rsc:hmr] No router found on window.__TSR_ROUTER__')
215
+ return
216
+ }
217
+ await router.invalidate()
218
+ } catch (e) {
219
+ console.warn('[rsc:hmr] Failed to invalidate router:', e)
220
+ }
221
+ })
222
+ }
223
+
224
+ import.meta.hot.on('rsc:update', () => {
225
+ __queueInvalidate()
226
+ })
227
+ }
228
+ `
229
+ },
230
+ }),
231
+ createVirtualModule({
232
+ name: 'tanstack-react-start:rsc-hmr-virtual:prod',
233
+ moduleId: RSC_HMR_VIRTUAL_ID,
234
+ applyToEnvironment: isClientEnvironment,
235
+ apply: 'build',
236
+ load() {
237
+ return 'export function setupRscHmr() {} '
238
+ },
239
+ }),
240
+ ]
241
+ }
@@ -0,0 +1,6 @@
1
+ // Inline React 19 internal $$typeof symbols to avoid depending on `react-is`.
2
+ // `react-is` is CJS-only, causing module resolution failures in strict pnpm
3
+ // setups where it can't be resolved from the consumer's node_modules.
4
+ export const ReactElement = Symbol.for('react.transitional.element')
5
+ export const ReactLazy = Symbol.for('react.lazy')
6
+ export const ReactSuspense = Symbol.for('react.suspense')
@@ -0,0 +1,26 @@
1
+ import type {
2
+ RenderableServerComponentBuilder,
3
+ ValidateRenderableServerComponent,
4
+ } from './ServerComponentTypes'
5
+
6
+ /**
7
+ * Client stub for renderServerComponent.
8
+ *
9
+ * This function should never be called at runtime on the client.
10
+ * It exists only to satisfy bundler imports in client bundles.
11
+ * The real implementation only runs inside server functions.
12
+ */
13
+ export function renderServerComponent<TNode>(
14
+ _node: ValidateRenderableServerComponent<TNode>,
15
+ ): Promise<RenderableServerComponentBuilder<TNode>> {
16
+ // Unit/type tests import this stub directly and call it.
17
+ // Avoid throwing in that environment while keeping a hard runtime guard elsewhere.
18
+ if (process.env.NODE_ENV === 'test') {
19
+ return Promise.resolve(null as any)
20
+ }
21
+
22
+ throw new Error(
23
+ 'renderServerComponent cannot be called on the client. ' +
24
+ 'This function should only be called inside a server function or route loader.',
25
+ )
26
+ }
@@ -0,0 +1,110 @@
1
+ import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
2
+ import { getRequest } from '@tanstack/start-server-core'
3
+ import { getStartContext } from '@tanstack/start-storage-context'
4
+
5
+ import { ReplayableStream } from './ReplayableStream'
6
+ import { RENDERABLE_RSC, SERVER_COMPONENT_STREAM } from './ServerComponentTypes'
7
+ import type {
8
+ AnyRenderableServerComponent,
9
+ RenderableServerComponentBuilder,
10
+ ServerComponentStream,
11
+ ValidateRenderableServerComponent,
12
+ } from './ServerComponentTypes'
13
+
14
+ import './rscSsrHandler'
15
+ // Import for global declaration side effect
16
+ export type { RscSsrHandler, RscDecodeResult } from './rscSsrHandler'
17
+
18
+ /**
19
+ * Renderable RSC handle type - used for serialization detection.
20
+ */
21
+
22
+ /**
23
+ * Type guard for renderable RSC handle.
24
+ */
25
+ export function isRenderableRscHandle(
26
+ value: unknown,
27
+ ): value is AnyRenderableServerComponent {
28
+ return (
29
+ typeof value === 'function' &&
30
+ SERVER_COMPONENT_STREAM in value &&
31
+ RENDERABLE_RSC in value &&
32
+ (value as any)[RENDERABLE_RSC] === true
33
+ )
34
+ }
35
+
36
+ /**
37
+ * Renders a React element to an RSC Flight stream.
38
+ *
39
+ * Returns a "renderable proxy" that can be:
40
+ * - Rendered directly as `{data}` in JSX
41
+ * - Accessed for nested selections: `{data.foo.bar.Hello}`
42
+ *
43
+ * No slot support - for slots use `createCompositeComponent`.
44
+ *
45
+ * @example
46
+ * ```tsx
47
+ * // In a loader or server function
48
+ * const data = await renderServerComponent(<MyServerComponent foo="bar" />)
49
+ *
50
+ * // In the route component
51
+ * return (
52
+ * <div>
53
+ * {data}
54
+ * {data.sidebar.Menu}
55
+ * </div>
56
+ * )
57
+ * ```
58
+ */
59
+ export async function renderServerComponent<TNode>(
60
+ node: ValidateRenderableServerComponent<TNode>,
61
+ ): Promise<RenderableServerComponentBuilder<TNode>> {
62
+ // Render the element directly to a Flight stream
63
+ const flightStream = renderToReadableStream(node)
64
+
65
+ // Check if this is an SSR request (router) or a direct server function call
66
+ const ctx = getStartContext({ throwIfNotFound: false })
67
+ const isRouterRequest = ctx?.handlerType === 'router'
68
+ const ssrHandler = globalThis.__RSC_SSR__
69
+
70
+ // SSR path: buffer stream for replay, pre-decode for synchronous rendering
71
+ if (isRouterRequest && ssrHandler) {
72
+ const signal = getRequest().signal
73
+ const stream = new ReplayableStream(flightStream, { signal })
74
+
75
+ // Pre-decode during loader phase for synchronous SSR rendering
76
+ const decoded = await ssrHandler.decode(stream)
77
+ return ssrHandler.createRenderableProxy(
78
+ stream,
79
+ decoded,
80
+ ) as RenderableServerComponentBuilder<TNode>
81
+ }
82
+
83
+ // Server function call path: return a handle for serialization
84
+ return createRenderableHandle(
85
+ flightStream,
86
+ ) as unknown as RenderableServerComponentBuilder<TNode>
87
+ }
88
+
89
+ /**
90
+ * Creates a renderable handle for server function responses.
91
+ * Tagged with RENDERABLE_RSC for the serialization adapter.
92
+ */
93
+ function createRenderableHandle(
94
+ flightStream: ReadableStream<Uint8Array>,
95
+ ): AnyRenderableServerComponent {
96
+ const streamWrapper: ServerComponentStream = {
97
+ createReplayStream: () => flightStream,
98
+ }
99
+
100
+ const stub = function RenderableRscStub(): never {
101
+ throw new Error(
102
+ 'Renderable RSC from server function cannot be rendered on server. ' +
103
+ 'It should be serialized and sent to the client.',
104
+ )
105
+ }
106
+
107
+ ;(stub as any)[SERVER_COMPONENT_STREAM] = streamWrapper
108
+ ;(stub as any)[RENDERABLE_RSC] = true
109
+ return stub as unknown as AnyRenderableServerComponent
110
+ }
@@ -0,0 +1,39 @@
1
+ import type {
2
+ RscSlotUsageEvent,
3
+ ServerComponentStream,
4
+ } from './ServerComponentTypes'
5
+
6
+ /**
7
+ * Result from decoding an RSC stream for SSR.
8
+ */
9
+ export interface RscDecodeResult {
10
+ tree: unknown
11
+ cssHrefs?: Set<string>
12
+ jsPreloads?: Set<string>
13
+ }
14
+
15
+ /**
16
+ * SSR handler interface - registered by serialization.server.ts in SSR environment.
17
+ * Supports both renderable and composite proxy creation.
18
+ */
19
+ export interface RscSsrHandler {
20
+ /** Pre-decode the stream for synchronous SSR rendering */
21
+ decode: (stream: ServerComponentStream) => Promise<RscDecodeResult>
22
+ /** Create a renderable proxy (for renderServerComponent) */
23
+ createRenderableProxy: (
24
+ stream: ServerComponentStream,
25
+ decoded: RscDecodeResult,
26
+ ) => any
27
+ /** Create a composite proxy (for createCompositeComponent) */
28
+ createCompositeProxy: (
29
+ stream: ServerComponentStream,
30
+ decoded: RscDecodeResult,
31
+ slotUsagesStream?: ReadableStream<RscSlotUsageEvent>,
32
+ ) => any
33
+ }
34
+
35
+ // Single global for SSR environment communication
36
+ declare global {
37
+ // eslint-disable-next-line no-var
38
+ var __RSC_SSR__: RscSsrHandler | undefined
39
+ }
@@ -0,0 +1,43 @@
1
+ import { createSerializationAdapter } from '@tanstack/react-router'
2
+ // RSC HMR setup (dev-only, provided by the active Start bundler adapter).
3
+ import { setupRscHmr } from 'virtual:tanstack-rsc-hmr'
4
+ import {
5
+ createCompositeFromStream,
6
+ createRenderableFromStream,
7
+ } from './createServerComponentFromStream'
8
+ import type {
9
+ AnyCompositeComponent,
10
+ RscSlotUsageEvent,
11
+ } from './ServerComponentTypes'
12
+
13
+ if (process.env.NODE_ENV === 'development') {
14
+ setupRscHmr()
15
+ }
16
+
17
+ /**
18
+ * Client-side serialization adapter for RSC (renderable + composite).
19
+ */
20
+ type SerializedRsc = {
21
+ kind: 'renderable' | 'composite'
22
+ stream: ReadableStream<Uint8Array>
23
+ slotUsagesStream?: ReadableStream<RscSlotUsageEvent>
24
+ }
25
+
26
+ const adapter = createSerializationAdapter({
27
+ key: '$RSC',
28
+ test: (_value: unknown): _value is never => false,
29
+ toSerializable: (): never => {
30
+ throw new Error('RSC cannot be serialized on client')
31
+ },
32
+ fromSerializable: (value: SerializedRsc): AnyCompositeComponent => {
33
+ if (value.kind === 'renderable') {
34
+ return createRenderableFromStream(value.stream)
35
+ }
36
+
37
+ return createCompositeFromStream(value.stream, {
38
+ slotUsagesStream: value.slotUsagesStream,
39
+ })
40
+ },
41
+ })
42
+
43
+ export const rscSerializationAdapter = () => [adapter]
@@ -0,0 +1,193 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { createSerializationAdapter } from '@tanstack/react-router'
3
+ import { RawStream } from '@tanstack/router-core'
4
+ import { getStartContext } from '@tanstack/start-storage-context'
5
+ import {
6
+ setOnClientReference,
7
+ createFromReadableStream as ssrDecode,
8
+ } from 'virtual:tanstack-rsc-ssr-decode'
9
+ import {
10
+ RENDERABLE_RSC,
11
+ RSC_SLOT_USAGES_STREAM,
12
+ SERVER_COMPONENT_STREAM,
13
+ isServerComponent,
14
+ } from './ServerComponentTypes'
15
+ import { createRscProxy } from './createRscProxy'
16
+ import { awaitLazyElements } from './awaitLazyElements'
17
+ import type {
18
+ AnyCompositeComponent,
19
+ ServerComponentStream,
20
+ } from './ServerComponentTypes'
21
+ import type { RscDecodeResult, RscSsrHandler } from './rscSsrHandler'
22
+
23
+ // ===== SSR Handler Registration =====
24
+ // This handler is registered on globalThis for the RSC environment to access.
25
+ // The RSC env calls these functions during loader execution to pre-decode streams
26
+ // and create renderable proxies.
27
+ //
28
+ // This MUST happen in a module without 'use client' directive.
29
+ // Modules with 'use client' may be transformed to client references in the
30
+ // SSR environment when RSC is enabled, preventing the side effect from running.
31
+
32
+ // AsyncLocalStorage for decode-scoped CSS collector.
33
+ // Each decode() runs in its own async context with its own collector.
34
+ // The onClientReference callback reads from this to write CSS hrefs.
35
+ const decodeCollectorStorage = new AsyncLocalStorage<Set<string>>()
36
+ const jsCollectorStorage = new AsyncLocalStorage<Set<string>>()
37
+
38
+ setOnClientReference(
39
+ ({
40
+ deps,
41
+ runtime,
42
+ }: {
43
+ deps: { js: Array<string>; css: Array<string> }
44
+ runtime?: 'rsbuild'
45
+ }) => {
46
+ const ctx = getStartContext({ throwIfNotFound: false })
47
+
48
+ const cssCollector = decodeCollectorStorage.getStore()
49
+ if (cssCollector) {
50
+ for (const href of deps.css) {
51
+ cssCollector.add(href)
52
+ }
53
+ }
54
+
55
+ const jsCollector = jsCollectorStorage.getStore()
56
+ if (jsCollector) {
57
+ for (const href of deps.js) {
58
+ jsCollector.add(href)
59
+ }
60
+ }
61
+
62
+ if (!ctx || runtime === 'rsbuild') return
63
+
64
+ if (!ctx.requestAssets) ctx.requestAssets = []
65
+ const seenHrefs = new Set(
66
+ ctx.requestAssets
67
+ .filter((a) => a.tag === 'link' && a.attrs?.href)
68
+ .map((a) => a.attrs!.href as string),
69
+ )
70
+
71
+ for (const href of deps.js) {
72
+ if (seenHrefs.has(href)) continue
73
+ seenHrefs.add(href)
74
+ ctx.requestAssets.push({
75
+ tag: 'link',
76
+ attrs: { rel: 'modulepreload', href },
77
+ })
78
+ }
79
+
80
+ for (const href of deps.css) {
81
+ if (seenHrefs.has(href)) continue
82
+ seenHrefs.add(href)
83
+ ctx.requestAssets.push({
84
+ tag: 'link',
85
+ attrs: { rel: 'preload', href, as: 'style' },
86
+ })
87
+ }
88
+ },
89
+ )
90
+
91
+ const ssrHandler: RscSsrHandler = {
92
+ async decode(stream: ServerComponentStream): Promise<RscDecodeResult> {
93
+ const readableStream = stream.createReplayStream()
94
+
95
+ // Create a collector for this decode operation.
96
+ // Run the decode in an AsyncLocalStorage context so the onClientReference
97
+ // callback can write to this specific collector even with parallel decodes.
98
+ const cssCollector = new Set<string>()
99
+ const jsCollector = new Set<string>()
100
+
101
+ return decodeCollectorStorage.run(cssCollector, async () => {
102
+ return jsCollectorStorage.run(jsCollector, async () => {
103
+ const tree = await ssrDecode(readableStream)
104
+ await awaitLazyElements(tree, (href) => {
105
+ cssCollector.add(href)
106
+ })
107
+
108
+ return {
109
+ tree,
110
+ cssHrefs: cssCollector.size > 0 ? cssCollector : undefined,
111
+ jsPreloads: jsCollector.size > 0 ? jsCollector : undefined,
112
+ }
113
+ })
114
+ })
115
+ },
116
+
117
+ createRenderableProxy(stream, decoded): any {
118
+ return createRscProxy(() => decoded.tree, {
119
+ stream,
120
+ cssHrefs: decoded.cssHrefs,
121
+ jsPreloads: decoded.jsPreloads,
122
+ renderable: true,
123
+ })
124
+ },
125
+
126
+ createCompositeProxy(
127
+ stream,
128
+ decoded,
129
+ slotUsagesStream,
130
+ ): AnyCompositeComponent {
131
+ const proxy = createRscProxy(() => decoded.tree, {
132
+ stream,
133
+ cssHrefs: decoded.cssHrefs,
134
+ jsPreloads: decoded.jsPreloads,
135
+ renderable: false,
136
+ slotUsagesStream,
137
+ })
138
+ return proxy
139
+ },
140
+ }
141
+
142
+ // Register SSR handler on globalThis for RSC environment to access.
143
+ globalThis.__RSC_SSR__ = ssrHandler
144
+
145
+ // ===== End SSR Handler Registration =====
146
+
147
+ /**
148
+ * Helper to check if a value is a renderable RSC (from renderServerComponent).
149
+ * The value can be either an object (proxy target) or a function (stub for server functions).
150
+ */
151
+ function isRenderableRsc(value: unknown): boolean {
152
+ if (value === null || value === undefined) return false
153
+ if (typeof value !== 'object' && typeof value !== 'function') return false
154
+ return RENDERABLE_RSC in value && (value as any)[RENDERABLE_RSC] === true
155
+ }
156
+
157
+ /**
158
+ * Server-side serialization adapter for RSC (renderable + composite).
159
+ */
160
+ const adapter = createSerializationAdapter({
161
+ key: '$RSC',
162
+ test: (value: unknown): value is AnyCompositeComponent => {
163
+ return isServerComponent(value)
164
+ },
165
+ toSerializable: (component: AnyCompositeComponent) => {
166
+ const stream = component[SERVER_COMPONENT_STREAM]!.createReplayStream()
167
+
168
+ const kind = isRenderableRsc(component) ? 'renderable' : 'composite'
169
+
170
+ const slotUsagesStream =
171
+ kind === 'composite' &&
172
+ process.env.NODE_ENV === 'development' &&
173
+ RSC_SLOT_USAGES_STREAM in component
174
+ ? ((component as any)[RSC_SLOT_USAGES_STREAM] as unknown as
175
+ | ReadableStream<any>
176
+ | undefined)
177
+ : undefined
178
+
179
+ return {
180
+ kind,
181
+ stream: new RawStream(stream, { hint: 'text' }),
182
+ slotUsagesStream,
183
+ }
184
+ },
185
+ fromSerializable: (): never => {
186
+ throw new Error('Server should never deserialize RSC data')
187
+ },
188
+ })
189
+
190
+ /**
191
+ * Factory function for server-side RSC serialization adapter.
192
+ */
193
+ export const rscSerializationAdapter = () => [adapter]