@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.
- package/dist/esm/ClientSlot.js +19 -0
- package/dist/esm/ClientSlot.js.map +1 -0
- package/dist/esm/CompositeComponent.js +93 -0
- package/dist/esm/CompositeComponent.js.map +1 -0
- package/dist/esm/ReplayableStream.js +147 -0
- package/dist/esm/ReplayableStream.js.map +1 -0
- package/dist/esm/RscNodeRenderer.js +46 -0
- package/dist/esm/RscNodeRenderer.js.map +1 -0
- package/dist/esm/ServerComponentTypes.js +22 -0
- package/dist/esm/ServerComponentTypes.js.map +1 -0
- package/dist/esm/SlotContext.js +30 -0
- package/dist/esm/SlotContext.js.map +1 -0
- package/dist/esm/awaitLazyElements.js +41 -0
- package/dist/esm/awaitLazyElements.js.map +1 -0
- package/dist/esm/createCompositeComponent.js +205 -0
- package/dist/esm/createCompositeComponent.js.map +1 -0
- package/dist/esm/createCompositeComponent.stub.js +15 -0
- package/dist/esm/createCompositeComponent.stub.js.map +1 -0
- package/dist/esm/createRscProxy.js +138 -0
- package/dist/esm/createRscProxy.js.map +1 -0
- package/dist/esm/createServerComponentFromStream.js +74 -0
- package/dist/esm/createServerComponentFromStream.js.map +1 -0
- package/dist/esm/entry/rsc.js +21 -0
- package/dist/esm/entry/rsc.js.map +1 -0
- package/dist/esm/flight.js +56 -0
- package/dist/esm/flight.js.map +1 -0
- package/dist/esm/flight.rsc.js +2 -0
- package/dist/esm/flight.stub.js +15 -0
- package/dist/esm/flight.stub.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.rsc.js +6 -0
- package/dist/esm/plugin/vite.js +172 -0
- package/dist/esm/plugin/vite.js.map +1 -0
- package/dist/esm/reactSymbols.js +8 -0
- package/dist/esm/reactSymbols.js.map +1 -0
- package/dist/esm/renderServerComponent.js +58 -0
- package/dist/esm/renderServerComponent.js.map +1 -0
- package/dist/esm/renderServerComponent.stub.js +16 -0
- package/dist/esm/renderServerComponent.stub.js.map +1 -0
- package/dist/esm/serialization.client.js +21 -0
- package/dist/esm/serialization.client.js.map +1 -0
- package/dist/esm/serialization.server.js +121 -0
- package/dist/esm/serialization.server.js.map +1 -0
- package/dist/esm/slotUsageSanitizer.js +33 -0
- package/dist/esm/slotUsageSanitizer.js.map +1 -0
- package/dist/esm/src/ClientSlot.d.ts +5 -0
- package/dist/esm/src/CompositeComponent.d.ts +28 -0
- package/dist/esm/src/ReplayableStream.d.ts +76 -0
- package/dist/esm/src/RscNodeRenderer.d.ts +7 -0
- package/dist/esm/src/ServerComponentTypes.d.ts +99 -0
- package/dist/esm/src/SlotContext.d.ts +21 -0
- package/dist/esm/src/awaitLazyElements.d.ts +17 -0
- package/dist/esm/src/createCompositeComponent.d.ts +32 -0
- package/dist/esm/src/createCompositeComponent.stub.d.ts +9 -0
- package/dist/esm/src/createRscProxy.d.ts +18 -0
- package/dist/esm/src/createServerComponentFromStream.d.ts +24 -0
- package/dist/esm/src/entry/rsc.d.ts +7 -0
- package/dist/esm/src/flight.d.ts +41 -0
- package/dist/esm/src/flight.rsc.d.ts +17 -0
- package/dist/esm/src/flight.stub.d.ts +8 -0
- package/dist/esm/src/index.d.ts +7 -0
- package/dist/esm/src/index.rsc.d.ts +6 -0
- package/dist/esm/src/plugin/vite.d.ts +9 -0
- package/dist/esm/src/reactSymbols.d.ts +3 -0
- package/dist/esm/src/renderServerComponent.d.ts +33 -0
- package/dist/esm/src/renderServerComponent.stub.d.ts +9 -0
- package/dist/esm/src/rscSsrHandler.d.ts +24 -0
- package/dist/esm/src/serialization.client.d.ts +11 -0
- package/dist/esm/src/serialization.server.d.ts +10 -0
- package/dist/esm/src/slotUsageSanitizer.d.ts +1 -0
- package/dist/esm/src/types.d.ts +13 -0
- package/dist/plugin/entry/rsc.tsx +23 -0
- package/package.json +108 -0
- package/src/ClientSlot.tsx +34 -0
- package/src/CompositeComponent.tsx +165 -0
- package/src/ReplayableStream.ts +249 -0
- package/src/RscNodeRenderer.tsx +76 -0
- package/src/ServerComponentTypes.ts +226 -0
- package/src/SlotContext.tsx +42 -0
- package/src/awaitLazyElements.ts +91 -0
- package/src/createCompositeComponent.stub.ts +20 -0
- package/src/createCompositeComponent.ts +338 -0
- package/src/createRscProxy.tsx +294 -0
- package/src/createServerComponentFromStream.ts +105 -0
- package/src/entry/rsc.tsx +23 -0
- package/src/entry/virtual-modules.d.ts +12 -0
- package/src/flight.rsc.ts +17 -0
- package/src/flight.stub.ts +15 -0
- package/src/flight.ts +68 -0
- package/src/global.d.ts +75 -0
- package/src/index.rsc.ts +25 -0
- package/src/index.ts +26 -0
- package/src/plugin/vite.ts +241 -0
- package/src/reactSymbols.ts +6 -0
- package/src/renderServerComponent.stub.ts +26 -0
- package/src/renderServerComponent.ts +110 -0
- package/src/rscSsrHandler.ts +39 -0
- package/src/serialization.client.ts +43 -0
- package/src/serialization.server.ts +193 -0
- package/src/slotUsageSanitizer.ts +62 -0
- 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>
|