@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,165 @@
1
+ 'use client'
2
+
3
+ import { Suspense } from 'react'
4
+ import ReactDOM from 'react-dom'
5
+
6
+ import { SlotProvider } from './SlotContext'
7
+ import {
8
+ RSC_PROXY_GET_TREE,
9
+ RSC_PROXY_PATH,
10
+ SERVER_COMPONENT_CSS_HREFS,
11
+ SERVER_COMPONENT_JS_PRELOADS,
12
+ SERVER_COMPONENT_STREAM,
13
+ } from './ServerComponentTypes'
14
+ import type { AnyCompositeComponent } from './ServerComponentTypes'
15
+ import type { SlotImplementations } from './types'
16
+
17
+ function splitSlotProps(props?: Record<string, unknown>): {
18
+ implementations: SlotImplementations
19
+ strict: boolean
20
+ } {
21
+ const safeProps = props ?? {}
22
+ const { children, strict, ...slotProps } = safeProps as {
23
+ children?: unknown
24
+ strict?: unknown
25
+ [key: string]: unknown
26
+ }
27
+
28
+ return {
29
+ implementations: { ...slotProps, children },
30
+ strict: strict === true,
31
+ }
32
+ }
33
+
34
+ function EmptyFallback() {
35
+ return null
36
+ }
37
+
38
+ function CompositeRenderInner({
39
+ getTree,
40
+ path,
41
+ slotProps,
42
+ }: {
43
+ getTree: () => unknown
44
+ path: Array<string>
45
+ slotProps?: Record<string, unknown>
46
+ }): React.ReactNode {
47
+ let tree: unknown = getTree()
48
+
49
+ for (const key of path) {
50
+ if (tree === null || tree === undefined) return null
51
+ if (typeof tree !== 'object') return null
52
+ tree = (tree as Record<string, unknown>)[key]
53
+ }
54
+
55
+ if (tree === null || tree === undefined) return null
56
+
57
+ const { implementations, strict } = splitSlotProps(slotProps)
58
+
59
+ return (
60
+ <SlotProvider implementations={implementations} strict={strict}>
61
+ {tree as React.ReactNode}
62
+ </SlotProvider>
63
+ )
64
+ }
65
+
66
+ function CompositeRenderComponent({
67
+ getTree,
68
+ path,
69
+ slotProps,
70
+ cssHrefs,
71
+ jsPreloads,
72
+ }: {
73
+ getTree: () => unknown
74
+ path: Array<string>
75
+ slotProps?: Record<string, unknown>
76
+ cssHrefs?: ReadonlySet<string>
77
+ jsPreloads?: ReadonlySet<string>
78
+ }): React.ReactNode {
79
+ for (const href of cssHrefs ?? []) {
80
+ ReactDOM.preinit(href, { as: 'style', precedence: 'high' })
81
+ }
82
+
83
+ if (jsPreloads) {
84
+ for (const href of jsPreloads) {
85
+ ReactDOM.preloadModule(href)
86
+ }
87
+ }
88
+
89
+ return (
90
+ <>
91
+ <Suspense fallback={<EmptyFallback />}>
92
+ <CompositeRenderInner
93
+ getTree={getTree}
94
+ path={path}
95
+ slotProps={slotProps}
96
+ />
97
+ </Suspense>
98
+ </>
99
+ )
100
+ }
101
+
102
+ /**
103
+ * Renders composite RSC data with slot support.
104
+ *
105
+ * Use this component to render data from `createCompositeComponent`.
106
+ * Pass slot implementations as props to fill in ClientSlot placeholders.
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * const src = await createCompositeComponent((props) => (
111
+ * <div>
112
+ * <header>{props.header('Dashboard')}</header>
113
+ * <main>{props.children}</main>
114
+ * </div>
115
+ * ))
116
+ *
117
+ * // In route component
118
+ * return (
119
+ * <CompositeComponent src={src} header={(title) => <h1>{title}</h1>}>
120
+ * <p>Main content</p>
121
+ * </CompositeComponent>
122
+ * )
123
+ * ```
124
+ */
125
+ export function CompositeComponent<TComp extends AnyCompositeComponent>(
126
+ props: CompositeComponentProps<TComp>,
127
+ ): TComp['~types']['return'] {
128
+ const { src, ...slotProps } = props
129
+
130
+ const stream = src[SERVER_COMPONENT_STREAM]
131
+
132
+ if (!stream) {
133
+ throw new Error(
134
+ '[tanstack/start] <CompositeComponent> missing RSC stream on src',
135
+ )
136
+ }
137
+
138
+ const cssHrefs = src[SERVER_COMPONENT_CSS_HREFS]
139
+ const jsPreloads = src[SERVER_COMPONENT_JS_PRELOADS]
140
+
141
+ const path = src[RSC_PROXY_PATH] ?? []
142
+
143
+ const getTree = src[RSC_PROXY_GET_TREE]
144
+
145
+ if (!getTree) {
146
+ throw new Error(
147
+ '[tanstack/start] <CompositeComponent> missing getTree on RSC src. ' +
148
+ 'Make sure src comes from createCompositeComponent().',
149
+ )
150
+ }
151
+
152
+ return (
153
+ <CompositeRenderComponent
154
+ getTree={getTree}
155
+ path={path}
156
+ slotProps={slotProps}
157
+ cssHrefs={cssHrefs}
158
+ jsPreloads={jsPreloads}
159
+ />
160
+ )
161
+ }
162
+
163
+ export type CompositeComponentProps<TComp extends AnyCompositeComponent> = {
164
+ src: TComp
165
+ } & TComp['~types']['props']
@@ -0,0 +1,249 @@
1
+ /**
2
+ * ReplayableStream is used for React Server Components (RSC) / Flight streams.
3
+ *
4
+ * In this package the same Flight payload may need to be:
5
+ * - decoded for SSR (render path), and/or
6
+ * - serialized for transport to the client for client-side decoding.
7
+ *
8
+ * Call sites:
9
+ * - `src/createServerComponent.ts`: wraps the produced Flight stream once.
10
+ * - `src/serialization.ts`: uses `createReplayStream()` to transport a fresh stream
11
+ * to the client via `RawStream`.
12
+ *
13
+ * Constraints:
14
+ * - Consumption order isn't fixed: SSR decode might start first or transport might
15
+ * start first depending on the request path.
16
+ * - Sometimes only one happens (e.g. when client calls server function directly).
17
+ *
18
+ * Why not just `ReadableStream.tee()`?
19
+ * - tee() must be called up-front before the stream is consumed/locked and only
20
+ * creates two live branches (no late "replay from byte 0").
21
+ * - If one branch is slower or never consumed, the runtime may buffer internally
22
+ * to keep branches consistent, which can retain large Flight payloads longer
23
+ * than intended and makes cleanup less explicit.
24
+ *
25
+ * ReplayableStream reads once, buffers explicitly, can mint replay streams on
26
+ * demand, and centralizes cancellation so aborting can stop upstream work and
27
+ * free buffered data deterministically.
28
+ *
29
+ * Memory Management:
30
+ * - Memory is released when the abort signal fires (request cancelled)
31
+ * - Call `release()` to force immediate cleanup if no more replays are needed
32
+ * - No automatic release: replays can be created at unpredictable times (SSR decode
33
+ * finishes before serialization starts), so we can't safely auto-release
34
+ */
35
+
36
+ export interface ReplayableStreamOptions {
37
+ signal?: AbortSignal
38
+ }
39
+
40
+ // Use Symbol.for to ensure the same symbol across different module instances
41
+ export const REPLAYABLE_STREAM_MARKER = Symbol.for(
42
+ 'tanstack.rsc.ReplayableStream',
43
+ )
44
+
45
+ export class ReplayableStream<T = Uint8Array> {
46
+ // Marker for cross-environment instance checking
47
+ readonly [REPLAYABLE_STREAM_MARKER] = true
48
+
49
+ private chunks: Array<T> = []
50
+ private done = false
51
+ private error: unknown = null
52
+ private waiter: PromiseWithResolvers<void> | null = null
53
+ private aborted = false
54
+ private released = false
55
+
56
+ private sourceReader: ReadableStreamDefaultReader<T> | null = null
57
+ private abortSignal: AbortSignal | undefined
58
+ private abortListener: (() => void) | null = null
59
+
60
+ constructor(
61
+ private source: ReadableStream<T>,
62
+ private options: ReplayableStreamOptions = {},
63
+ ) {
64
+ this.abortSignal = options.signal
65
+ this.start()
66
+ }
67
+
68
+ private start(): void {
69
+ const signal = this.abortSignal
70
+
71
+ if (signal?.aborted) {
72
+ this.handleAbort()
73
+ return
74
+ }
75
+
76
+ const onAbort = () => this.handleAbort()
77
+ this.abortListener = onAbort
78
+ signal?.addEventListener('abort', onAbort, { once: true })
79
+
80
+ const reader = this.source.getReader()
81
+ this.sourceReader = reader
82
+
83
+ const pump = async () => {
84
+ try {
85
+ // Keep reading until upstream ends or we are stopped.
86
+ while (!this.aborted && !this.released) {
87
+ const { done, value } = await reader.read()
88
+ if (done) break
89
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
90
+ if (this.aborted || this.released) break
91
+ this.chunks.push(value)
92
+ this.notify()
93
+ }
94
+ this.done = true
95
+ } catch (err) {
96
+ if (!this.aborted && !this.released) {
97
+ this.error = err
98
+ }
99
+ this.done = true
100
+ } finally {
101
+ this.detachAbortListener()
102
+ try {
103
+ reader.releaseLock()
104
+ } catch {
105
+ // Ignore
106
+ }
107
+ if (this.sourceReader === reader) {
108
+ this.sourceReader = null
109
+ }
110
+ this.notify()
111
+ }
112
+ }
113
+
114
+ pump()
115
+ }
116
+
117
+ private detachAbortListener(): void {
118
+ const signal = this.abortSignal
119
+ const listener = this.abortListener
120
+ if (signal && listener) {
121
+ signal.removeEventListener('abort', listener)
122
+ }
123
+ this.abortListener = null
124
+ }
125
+
126
+ private cancelSource(reason: unknown): void {
127
+ const reader = this.sourceReader
128
+ this.sourceReader = null
129
+
130
+ try {
131
+ reader?.cancel(reason).catch(() => {})
132
+ } catch {
133
+ // Ignore
134
+ }
135
+ }
136
+
137
+ private handleAbort(): void {
138
+ if (this.aborted) return
139
+ this.aborted = true
140
+ this.done = true
141
+
142
+ // Try to stop upstream work immediately.
143
+ // Cancellation during an abort can throw synchronously in some runtimes
144
+ // (eg when the underlying cancel algorithm throws). Never let that escape
145
+ // an AbortSignal event handler.
146
+ const reason =
147
+ this.abortSignal?.reason ?? new Error('ReplayableStream aborted')
148
+
149
+ this.detachAbortListener()
150
+ this.abortSignal = undefined
151
+ this.cancelSource(reason)
152
+
153
+ this.chunks = [] // Free memory immediately on abort
154
+ this.released = true
155
+ this.notify()
156
+ }
157
+
158
+ private notify(): void {
159
+ if (this.waiter) {
160
+ this.waiter.resolve()
161
+ this.waiter = null
162
+ }
163
+ }
164
+
165
+ private wait(): Promise<void> {
166
+ if (this.done || this.released) return Promise.resolve()
167
+ if (!this.waiter) {
168
+ this.waiter = Promise.withResolvers<void>()
169
+ }
170
+ return this.waiter.promise
171
+ }
172
+
173
+ /**
174
+ * Explicitly release buffered chunks.
175
+ * Call this when you know no more replay streams will be created.
176
+ * After calling release(), createReplayStream() will return empty streams.
177
+ */
178
+ release(): void {
179
+ if (this.released) return
180
+
181
+ this.released = true
182
+ this.chunks = []
183
+
184
+ // Release should also stop upstream work and wake any waiters.
185
+ // This is important when a Flight payload is never consumed again
186
+ // (eg. cached loader data that evicts) to avoid retaining upstream resources.
187
+ this.detachAbortListener()
188
+ this.abortSignal = undefined
189
+ this.cancelSource(new Error('ReplayableStream released'))
190
+ this.notify()
191
+ }
192
+
193
+ /**
194
+ * Check if the stream data has been released
195
+ */
196
+ isReleased(): boolean {
197
+ return this.released
198
+ }
199
+
200
+ /**
201
+ * Create an independent replay stream. Each call returns a fresh reader
202
+ * that starts from the beginning of the buffered data.
203
+ *
204
+ * If the stream has been released, returns a stream that closes immediately.
205
+ */
206
+ createReplayStream(): ReadableStream<T> {
207
+ if (this.released) {
208
+ return new ReadableStream<T>({
209
+ start(controller) {
210
+ controller.close()
211
+ },
212
+ })
213
+ }
214
+
215
+ let index = 0
216
+
217
+ return new ReadableStream<T>({
218
+ pull: async (controller) => {
219
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
220
+ while (true) {
221
+ if (this.released) {
222
+ controller.close()
223
+ return
224
+ }
225
+
226
+ if (index < this.chunks.length) {
227
+ controller.enqueue(this.chunks[index++] as T)
228
+ return
229
+ }
230
+
231
+ if (this.done) {
232
+ if (this.error && !this.aborted) {
233
+ controller.error(this.error)
234
+ } else {
235
+ controller.close()
236
+ }
237
+ return
238
+ }
239
+
240
+ await this.wait()
241
+ }
242
+ },
243
+ cancel: () => {
244
+ // No-op: consumers canceling a replay should not cancel upstream.
245
+ // Upstream cancellation is controlled by abort/release.
246
+ },
247
+ })
248
+ }
249
+ }
@@ -0,0 +1,76 @@
1
+ 'use client'
2
+
3
+ import { Suspense } from 'react'
4
+ import ReactDOM from 'react-dom'
5
+
6
+ import {
7
+ RSC_PROXY_GET_TREE,
8
+ RSC_PROXY_PATH,
9
+ SERVER_COMPONENT_CSS_HREFS,
10
+ SERVER_COMPONENT_JS_PRELOADS,
11
+ } from './ServerComponentTypes'
12
+
13
+ function EmptyFallback() {
14
+ return null
15
+ }
16
+
17
+ function RscNodeRenderInner({
18
+ getTree,
19
+ path,
20
+ }: {
21
+ getTree: () => unknown
22
+ path: Array<string>
23
+ }): React.ReactNode {
24
+ let tree: unknown = getTree()
25
+
26
+ for (const key of path) {
27
+ if (tree === null || tree === undefined) return null
28
+ if (typeof tree !== 'object') return null
29
+ tree = (tree as Record<string, unknown>)[key]
30
+ }
31
+
32
+ if (tree === null || tree === undefined) return null
33
+
34
+ // No SlotProvider - just return the tree directly
35
+ return tree as React.ReactNode
36
+ }
37
+
38
+ /**
39
+ * Renders a renderable RSC proxy without slot support.
40
+ * Used internally by the renderable proxy's $$typeof/type masquerade.
41
+ */
42
+ export function RscNodeRenderer({ data }: { data: any }): React.ReactNode {
43
+ const cssHrefs = data[SERVER_COMPONENT_CSS_HREFS] as
44
+ | ReadonlySet<string>
45
+ | undefined
46
+ const jsPreloads = data[SERVER_COMPONENT_JS_PRELOADS] as
47
+ | ReadonlySet<string>
48
+ | undefined
49
+
50
+ const path = (data[RSC_PROXY_PATH] as Array<string> | undefined) ?? []
51
+
52
+ const getTree = data[RSC_PROXY_GET_TREE] as (() => unknown) | undefined
53
+ if (!getTree) {
54
+ throw new Error(
55
+ '[tanstack/start] RscNodeRenderer missing getTree on RSC data.',
56
+ )
57
+ }
58
+
59
+ for (const href of cssHrefs ?? []) {
60
+ ReactDOM.preinit(href, { as: 'style', precedence: 'high' })
61
+ }
62
+
63
+ if (jsPreloads) {
64
+ for (const href of jsPreloads) {
65
+ ReactDOM.preloadModule(href)
66
+ }
67
+ }
68
+
69
+ return (
70
+ <>
71
+ <Suspense fallback={<EmptyFallback />}>
72
+ <RscNodeRenderInner getTree={getTree} path={path} />
73
+ </Suspense>
74
+ </>
75
+ )
76
+ }
@@ -0,0 +1,226 @@
1
+ import type {
2
+ Constrain,
3
+ LooseAsyncReturnType,
4
+ LooseReturnType,
5
+ ValidateSerializable,
6
+ } from '@tanstack/router-core'
7
+ import type { ComponentProps, ComponentType } from 'react'
8
+
9
+ export interface ServerComponentStream {
10
+ createReplayStream: () => ReadableStream<Uint8Array>
11
+ }
12
+
13
+ // Symbol to attach stream to component for serialization
14
+ export const SERVER_COMPONENT_STREAM = Symbol.for('tanstack.rsc.stream')
15
+
16
+ // Symbol to attach collected CSS hrefs to component
17
+ export const SERVER_COMPONENT_CSS_HREFS = Symbol.for('tanstack.rsc.cssHrefs')
18
+
19
+ // Symbol to attach collected JS modulepreload hrefs to component
20
+ export const SERVER_COMPONENT_JS_PRELOADS = Symbol.for(
21
+ 'tanstack.rsc.jsPreloads',
22
+ )
23
+
24
+ // Symbol to attach a nested selection path to the RSC data proxy
25
+ export const RSC_PROXY_PATH = Symbol.for('tanstack.rsc.path')
26
+
27
+ // Symbol to attach the root tree getter to every nested proxy
28
+ export const RSC_PROXY_GET_TREE = Symbol.for('tanstack.rsc.getTree')
29
+
30
+ // Symbol to mark a proxy as "renderable" (for renderServerComponent output)
31
+ // When true: from renderServerComponent (directly renderable)
32
+ // When false/undefined: from createCompositeComponent (needs <CompositeComponent src={...} />)
33
+ export const RENDERABLE_RSC = Symbol.for('tanstack.rsc.renderable')
34
+
35
+ // Dev-only: collected slot usage data for devtools (client-side cache)
36
+ export const RSC_SLOT_USAGES = Symbol.for('tanstack.rsc.slotUsages')
37
+
38
+ // Dev-only: stream of slot usage preview events for devtools
39
+ export const RSC_SLOT_USAGES_STREAM = Symbol.for(
40
+ 'tanstack.rsc.slotUsages.stream',
41
+ )
42
+
43
+ export type RscSlotUsageEvent = {
44
+ slot: string
45
+ // Raw args passed to the slot call (must be serializable by the transport)
46
+ args?: Array<any>
47
+ }
48
+
49
+ /**
50
+ * Type guard to check if a value is a ServerComponent (Proxy with attached stream).
51
+ * The value can be either an object (proxy target) or a function (stub for server functions).
52
+ */
53
+ export function isServerComponent(
54
+ value: unknown,
55
+ ): value is AnyCompositeComponent {
56
+ if (value === null || value === undefined) return false
57
+ if (typeof value !== 'object' && typeof value !== 'function') return false
58
+ return (
59
+ SERVER_COMPONENT_STREAM in value &&
60
+ (value as any)[SERVER_COMPONENT_STREAM] !== undefined
61
+ )
62
+ }
63
+
64
+ /**
65
+ * Type guard to check if a value is a RenderableRsc (renderable proxy from renderServerComponent).
66
+ * The value can be either an object (proxy target) or a function (stub for server functions).
67
+ */
68
+ export function isRenderableRsc(value: unknown): boolean {
69
+ if (value === null || value === undefined) return false
70
+ if (typeof value !== 'object' && typeof value !== 'function') return false
71
+ return RENDERABLE_RSC in value && (value as any)[RENDERABLE_RSC] === true
72
+ }
73
+
74
+ export type ValidateCompositeComponent<TComp> = Constrain<
75
+ TComp,
76
+ (
77
+ props: ValidateCompositeComponentProps<TComp>,
78
+ ) => ValidateCompositeComponentReturnType<TComp>
79
+ >
80
+
81
+ export type ValidateCompositeComponentProps<TComp> = unknown extends TComp
82
+ ? TComp
83
+ : ValidateCompositeComponentPropsObject<CompositeComponentProps<TComp>>
84
+
85
+ export type ValidateCompositeComponentPropsObject<TProps> =
86
+ unknown extends TProps
87
+ ? TProps
88
+ : {
89
+ [TKey in keyof TProps]: ValidateCompositeComponentProp<TProps[TKey]>
90
+ }
91
+
92
+ export type CompositeComponentProps<TComp> = TComp extends (
93
+ props: infer TProps,
94
+ ) => any
95
+ ? TProps
96
+ : unknown
97
+
98
+ export type ValidateCompositeComponentProp<TProp> = TProp extends (
99
+ ...args: Array<any>
100
+ ) => any
101
+ ? (...args: ValidateReactSerializable<Parameters<TProp>>) => React.ReactNode
102
+ : TProp extends ComponentType<any>
103
+ ? ComponentType<ValidateReactSerializable<ComponentProps<TProp>>>
104
+ : TProp extends React.ReactNode
105
+ ? TProp
106
+ : React.ReactNode
107
+
108
+ export type ValidateReactSerializable<T> = ValidateSerializable<
109
+ T,
110
+ ReactSerializable
111
+ >
112
+
113
+ export type ReactSerializable =
114
+ | number
115
+ | string
116
+ | bigint
117
+ | boolean
118
+ | null
119
+ | undefined
120
+ | React.ReactNode
121
+
122
+ export type ValidateCompositeComponentReturnType<TComp> = unknown extends TComp
123
+ ? React.ReactNode
124
+ : ValidateCompositeComponentResult<LooseReturnType<TComp>>
125
+
126
+ export type ValidateCompositeComponentResult<TNode> =
127
+ ValidateServerComponentResult<TNode>
128
+
129
+ export type ValidateServerComponentResult<TNode> =
130
+ TNode extends Promise<any>
131
+ ? ValidateCompositeComponentPromiseResult<TNode>
132
+ : TNode extends React.ReactNode
133
+ ? TNode
134
+ : TNode extends (...args: Array<any>) => any
135
+ ? React.ReactNode
136
+ : TNode extends object
137
+ ? ValidateCompositeComponentObjectResult<TNode>
138
+ : React.ReactNode
139
+
140
+ export type ValidateCompositeComponentPromiseResult<TPromise> =
141
+ TPromise extends Promise<infer T>
142
+ ? Promise<ValidateCompositeComponentResult<T>>
143
+ : never
144
+
145
+ export type ValidateCompositeComponentObjectResult<TObject> = {
146
+ [TKey in keyof TObject]: ValidateCompositeComponentResult<TObject[TKey]>
147
+ }
148
+
149
+ export type CompositeComponentResult<TComp> = CompositeComponentBuilder<
150
+ TComp,
151
+ LooseAsyncReturnType<TComp>
152
+ >
153
+
154
+ export type CompositeComponentBuilder<TComp, TReturn> =
155
+ TReturn extends React.ReactNode
156
+ ? CompositeComponent<TComp, TReturn>
157
+ : {
158
+ [TKey in keyof TReturn]: CompositeComponentBuilder<TComp, TReturn[TKey]>
159
+ }
160
+
161
+ export interface CompositeComponent<in out TComp, in out TReturn> {
162
+ '~types': {
163
+ props: CompositeComponentProps<TComp>
164
+ return: TReturn
165
+ }
166
+
167
+ [SERVER_COMPONENT_STREAM]?: ServerComponentStream
168
+
169
+ /**
170
+ * Root decoded tree getter.
171
+ */
172
+ [RSC_PROXY_GET_TREE]?: () => unknown
173
+ /**
174
+ * Nested selection path (eg ['content','Stats']).
175
+ * Used by <CompositeComponent/> to render a sub-tree.
176
+ */
177
+ [RSC_PROXY_PATH]?: Array<string>
178
+ /**
179
+ * CSS hrefs collected from the RSC stream.
180
+ * Can be used for preloading in <head> or emitting 103 Early Hints.
181
+ */
182
+ [SERVER_COMPONENT_CSS_HREFS]?: ReadonlySet<string>
183
+
184
+ /**
185
+ * JS hrefs collected from the RSC stream.
186
+ * Emitted as modulepreload links only if the decoded tree is rendered in SSR.
187
+ */
188
+ [SERVER_COMPONENT_JS_PRELOADS]?: ReadonlySet<string>
189
+
190
+ /**
191
+ * Dev-only: async stream of slot usage preview events.
192
+ * Used by devtools to show slot names and previewed call args without
193
+ * buffering/draining the Flight stream.
194
+ */
195
+ [RSC_SLOT_USAGES_STREAM]?: ReadableStream<RscSlotUsageEvent>
196
+ }
197
+
198
+ export type ValidateRenderableServerComponent<TNode> =
199
+ ValidateServerComponentResult<TNode>
200
+
201
+ export type RenderableServerComponentBuilder<T> = T extends React.ReactNode
202
+ ? RenderableServerComponent<T>
203
+ : { [TKey in keyof T]: RenderableServerComponentBuilder<T[TKey]> }
204
+
205
+ export type RenderableServerComponent<TNode extends React.ReactNode> = TNode &
206
+ RenderableServerComponentAttributes<TNode>
207
+
208
+ export interface RenderableServerComponentAttributes<TNode> {
209
+ '~types': {
210
+ node: TNode
211
+ }
212
+ [SERVER_COMPONENT_STREAM]: ServerComponentStream
213
+ [RENDERABLE_RSC]: true
214
+ }
215
+
216
+ declare module '@tanstack/router-core' {
217
+ export interface SerializableExtensions {
218
+ CompositeComponent: AnyCompositeComponent
219
+ RenderableServerComponent: AnyRenderableServerComponent
220
+ }
221
+ }
222
+
223
+ export type AnyCompositeComponent = CompositeComponent<any, any>
224
+
225
+ export type AnyRenderableServerComponent =
226
+ RenderableServerComponentAttributes<any>