@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,294 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createElement } from 'react'
|
|
4
|
+
import { RscNodeRenderer } from './RscNodeRenderer'
|
|
5
|
+
import {
|
|
6
|
+
RENDERABLE_RSC,
|
|
7
|
+
RSC_PROXY_GET_TREE,
|
|
8
|
+
RSC_PROXY_PATH,
|
|
9
|
+
RSC_SLOT_USAGES,
|
|
10
|
+
RSC_SLOT_USAGES_STREAM,
|
|
11
|
+
SERVER_COMPONENT_CSS_HREFS,
|
|
12
|
+
SERVER_COMPONENT_JS_PRELOADS,
|
|
13
|
+
SERVER_COMPONENT_STREAM,
|
|
14
|
+
} from './ServerComponentTypes'
|
|
15
|
+
import type { RscSlotUsageEvent } from './ServerComponentTypes'
|
|
16
|
+
|
|
17
|
+
export interface RscProxyOptions {
|
|
18
|
+
stream?: unknown // The stream to attach for serialization
|
|
19
|
+
cssHrefs?: ReadonlySet<string> // CSS hrefs collected from the RSC stream
|
|
20
|
+
jsPreloads?: ReadonlySet<string> // JS hrefs collected from the RSC stream
|
|
21
|
+
renderable?: boolean // If true, proxy masquerades as React element
|
|
22
|
+
slotUsagesStream?: ReadableStream<RscSlotUsageEvent> // Dev only: slot usage event stream
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Creates a recursive Proxy for RSC data.
|
|
27
|
+
*
|
|
28
|
+
* If `renderable: true`, returns a React element that can be rendered as `{data}`.
|
|
29
|
+
* The element also has proxy-like behavior for nested access like `data.foo.bar`.
|
|
30
|
+
*
|
|
31
|
+
* If `renderable: false` (default), the proxy is NOT directly renderable and
|
|
32
|
+
* must be used with `<CompositeComponent src={...} />`.
|
|
33
|
+
*/
|
|
34
|
+
export function createRscProxy<T>(
|
|
35
|
+
getTree: () => T,
|
|
36
|
+
options: RscProxyOptions = {},
|
|
37
|
+
): any {
|
|
38
|
+
if (options.renderable) {
|
|
39
|
+
// For renderable mode, create a real React element with proxy access
|
|
40
|
+
return createRenderableElement(
|
|
41
|
+
getTree,
|
|
42
|
+
[],
|
|
43
|
+
options.stream,
|
|
44
|
+
options.cssHrefs,
|
|
45
|
+
options.jsPreloads,
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
let slotUsages: Array<RscSlotUsageEvent> | undefined = undefined
|
|
49
|
+
|
|
50
|
+
if (
|
|
51
|
+
process.env.NODE_ENV === 'development' &&
|
|
52
|
+
typeof window !== 'undefined' &&
|
|
53
|
+
options.slotUsagesStream
|
|
54
|
+
) {
|
|
55
|
+
slotUsages = []
|
|
56
|
+
void consumeSlotUsages(options.slotUsagesStream, slotUsages)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Non-renderable mode: plain proxy
|
|
60
|
+
return createRscProxyWithPath(
|
|
61
|
+
getTree,
|
|
62
|
+
[],
|
|
63
|
+
options.stream,
|
|
64
|
+
options.cssHrefs,
|
|
65
|
+
options.jsPreloads,
|
|
66
|
+
slotUsages,
|
|
67
|
+
options.slotUsagesStream,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates a React element that's also a Proxy for nested access.
|
|
73
|
+
* This is used by renderable RSCs so they work as both {data} and {data.foo.bar}.
|
|
74
|
+
*/
|
|
75
|
+
type CreateProxyOptions = {
|
|
76
|
+
getTree: () => unknown
|
|
77
|
+
path: Array<string>
|
|
78
|
+
stream: unknown | undefined
|
|
79
|
+
cssHrefs: ReadonlySet<string> | undefined
|
|
80
|
+
jsPreloads: ReadonlySet<string> | undefined
|
|
81
|
+
renderable: boolean
|
|
82
|
+
slotUsages: Array<RscSlotUsageEvent> | undefined
|
|
83
|
+
slotUsagesStream: ReadableStream<RscSlotUsageEvent> | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const UNHANDLED = Symbol('tanstack.rsc.proxy.unhandled')
|
|
87
|
+
|
|
88
|
+
function handleProxyTrap(
|
|
89
|
+
kind: 'get' | 'has',
|
|
90
|
+
prop: PropertyKey,
|
|
91
|
+
options: CreateProxyOptions,
|
|
92
|
+
): unknown | boolean | typeof UNHANDLED {
|
|
93
|
+
switch (prop) {
|
|
94
|
+
// Seroval (>=1.5) uses string sentinels and `in` checks for internal types.
|
|
95
|
+
// These proxies must never look like streams/sequences.
|
|
96
|
+
case '__SEROVAL_STREAM__':
|
|
97
|
+
case '__SEROVAL_SEQUENCE__':
|
|
98
|
+
return kind === 'get' ? undefined : false
|
|
99
|
+
|
|
100
|
+
// Seroval >=1.5 also checks iterability via `Symbol.iterator in value`.
|
|
101
|
+
case Symbol.iterator:
|
|
102
|
+
case Symbol.asyncIterator:
|
|
103
|
+
return kind === 'get' ? undefined : false
|
|
104
|
+
|
|
105
|
+
// Our proxy branding/properties
|
|
106
|
+
case SERVER_COMPONENT_STREAM:
|
|
107
|
+
return kind === 'get' ? options.stream : options.stream !== undefined
|
|
108
|
+
case SERVER_COMPONENT_CSS_HREFS:
|
|
109
|
+
return kind === 'get' ? options.cssHrefs : options.cssHrefs !== undefined
|
|
110
|
+
case SERVER_COMPONENT_JS_PRELOADS:
|
|
111
|
+
return kind === 'get'
|
|
112
|
+
? options.jsPreloads
|
|
113
|
+
: options.jsPreloads !== undefined
|
|
114
|
+
case RSC_PROXY_GET_TREE:
|
|
115
|
+
return kind === 'get' ? options.getTree : true
|
|
116
|
+
case RSC_PROXY_PATH:
|
|
117
|
+
return kind === 'get' ? options.path : true
|
|
118
|
+
case RENDERABLE_RSC:
|
|
119
|
+
return kind === 'get' ? options.renderable : true
|
|
120
|
+
case RSC_SLOT_USAGES:
|
|
121
|
+
return kind === 'get'
|
|
122
|
+
? options.slotUsages
|
|
123
|
+
: options.slotUsages !== undefined
|
|
124
|
+
|
|
125
|
+
case RSC_SLOT_USAGES_STREAM:
|
|
126
|
+
return kind === 'get'
|
|
127
|
+
? options.slotUsagesStream
|
|
128
|
+
: options.slotUsagesStream !== undefined
|
|
129
|
+
|
|
130
|
+
// Avoid promise-like checks
|
|
131
|
+
case 'then':
|
|
132
|
+
return kind === 'get' ? undefined : UNHANDLED
|
|
133
|
+
|
|
134
|
+
// Avoid breaking primitive coercion (eg String(proxy)).
|
|
135
|
+
// Without these, nested-selection proxies can shadow these keys and
|
|
136
|
+
// cause `Cannot convert object to primitive value` errors.
|
|
137
|
+
case 'toString':
|
|
138
|
+
return kind === 'get' ? Object.prototype.toString : UNHANDLED
|
|
139
|
+
case 'valueOf':
|
|
140
|
+
return kind === 'get' ? Object.prototype.valueOf : UNHANDLED
|
|
141
|
+
case 'constructor':
|
|
142
|
+
return kind === 'get' ? Object : UNHANDLED
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Non-renderable proxies claim all string keys exist for nested selection,
|
|
146
|
+
// but symbol presence checks should be accurate.
|
|
147
|
+
if (typeof prop === 'symbol') {
|
|
148
|
+
return kind === 'get' ? undefined : false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return UNHANDLED
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createRscProxyInternal(options: CreateProxyOptions): any {
|
|
155
|
+
// Per-proxy cache so repeated property access is referentially stable.
|
|
156
|
+
const childCache = new Map<string, any>()
|
|
157
|
+
|
|
158
|
+
const getChild = (key: string) => {
|
|
159
|
+
const cached = childCache.get(key)
|
|
160
|
+
if (cached) return cached
|
|
161
|
+
|
|
162
|
+
const next = createRscProxyInternal({
|
|
163
|
+
...options,
|
|
164
|
+
path: [...options.path, key],
|
|
165
|
+
})
|
|
166
|
+
childCache.set(key, next)
|
|
167
|
+
return next
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const dataProxy = options.renderable
|
|
171
|
+
? createRscProxyInternal({ ...options, renderable: false })
|
|
172
|
+
: undefined
|
|
173
|
+
|
|
174
|
+
// Use a React element (renderable) or plain object (non-renderable) as Proxy target.
|
|
175
|
+
const proxyTarget = options.renderable
|
|
176
|
+
? createElement(RscNodeRenderer, { data: dataProxy })
|
|
177
|
+
: ({} as any)
|
|
178
|
+
|
|
179
|
+
return new Proxy(proxyTarget, {
|
|
180
|
+
get(target, prop) {
|
|
181
|
+
const handled = handleProxyTrap('get', prop, options)
|
|
182
|
+
if (handled !== UNHANDLED) return handled
|
|
183
|
+
|
|
184
|
+
if (options.renderable) {
|
|
185
|
+
// Proxy invariants: if target has a non-configurable, read-only property,
|
|
186
|
+
// we must return the *exact* value for that property.
|
|
187
|
+
if (prop === 'props') {
|
|
188
|
+
return target.props
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (prop === 'data') {
|
|
192
|
+
return dataProxy
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (prop in target) {
|
|
196
|
+
return target[prop]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return getChild(String(prop))
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
has(target, prop) {
|
|
204
|
+
const handled = handleProxyTrap('has', prop, options)
|
|
205
|
+
if (handled !== UNHANDLED) return handled as boolean
|
|
206
|
+
|
|
207
|
+
if (options.renderable) {
|
|
208
|
+
if (prop in target) return true
|
|
209
|
+
if (typeof prop === 'string') return true
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Allow any property access for nested selection.
|
|
214
|
+
return true
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
getPrototypeOf(target) {
|
|
218
|
+
return options.renderable
|
|
219
|
+
? Object.getPrototypeOf(target)
|
|
220
|
+
: Object.prototype
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
224
|
+
return options.renderable
|
|
225
|
+
? Object.getOwnPropertyDescriptor(target, prop)
|
|
226
|
+
: undefined
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
ownKeys(target) {
|
|
230
|
+
return options.renderable ? Reflect.ownKeys(target) : []
|
|
231
|
+
},
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Creates a React element that's also a Proxy for nested access.
|
|
237
|
+
* This is used by renderable RSCs so they work as both {data} and {data.foo.bar}.
|
|
238
|
+
*/
|
|
239
|
+
function createRenderableElement(
|
|
240
|
+
getTree: () => unknown,
|
|
241
|
+
path: Array<string>,
|
|
242
|
+
stream: unknown | undefined,
|
|
243
|
+
cssHrefs: ReadonlySet<string> | undefined,
|
|
244
|
+
jsPreloads: ReadonlySet<string> | undefined,
|
|
245
|
+
): any {
|
|
246
|
+
return createRscProxyInternal({
|
|
247
|
+
getTree,
|
|
248
|
+
path,
|
|
249
|
+
stream,
|
|
250
|
+
cssHrefs,
|
|
251
|
+
jsPreloads,
|
|
252
|
+
renderable: true,
|
|
253
|
+
slotUsages: undefined,
|
|
254
|
+
slotUsagesStream: undefined,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function createRscProxyWithPath(
|
|
259
|
+
getTree: () => unknown,
|
|
260
|
+
path: Array<string>,
|
|
261
|
+
stream: unknown | undefined,
|
|
262
|
+
cssHrefs: ReadonlySet<string> | undefined,
|
|
263
|
+
jsPreloads: ReadonlySet<string> | undefined,
|
|
264
|
+
slotUsages: Array<RscSlotUsageEvent> | undefined,
|
|
265
|
+
slotUsagesStream: ReadableStream<RscSlotUsageEvent> | undefined,
|
|
266
|
+
): any {
|
|
267
|
+
return createRscProxyInternal({
|
|
268
|
+
getTree,
|
|
269
|
+
path,
|
|
270
|
+
stream,
|
|
271
|
+
cssHrefs,
|
|
272
|
+
jsPreloads,
|
|
273
|
+
renderable: false,
|
|
274
|
+
slotUsages,
|
|
275
|
+
slotUsagesStream,
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function consumeSlotUsages(
|
|
280
|
+
stream: ReadableStream<RscSlotUsageEvent>,
|
|
281
|
+
slotUsages: Array<RscSlotUsageEvent>,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
try {
|
|
284
|
+
const reader = stream.getReader()
|
|
285
|
+
for (;;) {
|
|
286
|
+
const { value, done } = await reader.read()
|
|
287
|
+
if (done) break
|
|
288
|
+
if (!value.slot) continue
|
|
289
|
+
slotUsages.push(value)
|
|
290
|
+
}
|
|
291
|
+
} catch {
|
|
292
|
+
// Ignore - dev-only best effort
|
|
293
|
+
}
|
|
294
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { use } from 'react'
|
|
4
|
+
import { trackPostProcessPromise } from '@tanstack/start-client-core'
|
|
5
|
+
import { createFromReadableStream as browserDecode } from 'virtual:tanstack-rsc-browser-decode'
|
|
6
|
+
|
|
7
|
+
import { awaitLazyElements } from './awaitLazyElements'
|
|
8
|
+
import { createRscProxy } from './createRscProxy'
|
|
9
|
+
import type {
|
|
10
|
+
AnyCompositeComponent,
|
|
11
|
+
RscSlotUsageEvent,
|
|
12
|
+
ServerComponentStream,
|
|
13
|
+
} from './ServerComponentTypes'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a renderable RSC proxy from a raw Flight stream.
|
|
17
|
+
* Client-side only - used by the client serialization adapter for `renderServerComponent`.
|
|
18
|
+
*
|
|
19
|
+
* Returns a Proxy that:
|
|
20
|
+
* - Can be rendered directly as `{data}` in JSX
|
|
21
|
+
* - Supports nested access: `{data.foo.bar}`
|
|
22
|
+
* - Masquerades as a React element
|
|
23
|
+
*/
|
|
24
|
+
export function createRenderableFromStream(
|
|
25
|
+
stream: ReadableStream<Uint8Array>,
|
|
26
|
+
): any {
|
|
27
|
+
const { getTree, streamWrapper, cssHrefs } = setupStreamDecode(stream)
|
|
28
|
+
|
|
29
|
+
return createRscProxy(getTree, {
|
|
30
|
+
stream: streamWrapper,
|
|
31
|
+
cssHrefs,
|
|
32
|
+
renderable: true,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Creates a composite RSC proxy from a raw Flight stream.
|
|
38
|
+
* Client-side only - used by the client serialization adapter for `createCompositeComponent`.
|
|
39
|
+
*
|
|
40
|
+
* Returns a Proxy that:
|
|
41
|
+
* - NOT directly renderable
|
|
42
|
+
* - Supports nested access: `src.foo.bar`
|
|
43
|
+
* - Must be rendered via `<CompositeComponent src={...} />`
|
|
44
|
+
*/
|
|
45
|
+
export function createCompositeFromStream(
|
|
46
|
+
stream: ReadableStream<Uint8Array>,
|
|
47
|
+
options?: {
|
|
48
|
+
slotUsagesStream?: ReadableStream<RscSlotUsageEvent>
|
|
49
|
+
},
|
|
50
|
+
): AnyCompositeComponent {
|
|
51
|
+
const { getTree, streamWrapper, cssHrefs } = setupStreamDecode(stream)
|
|
52
|
+
|
|
53
|
+
return createRscProxy(getTree, {
|
|
54
|
+
stream: streamWrapper,
|
|
55
|
+
cssHrefs,
|
|
56
|
+
renderable: false,
|
|
57
|
+
slotUsagesStream: options?.slotUsagesStream,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Shared stream decode setup for both renderable and composite.
|
|
63
|
+
*/
|
|
64
|
+
function setupStreamDecode(stream: ReadableStream<Uint8Array>): {
|
|
65
|
+
getTree: () => unknown
|
|
66
|
+
streamWrapper: ServerComponentStream
|
|
67
|
+
cssHrefs: Set<string> | undefined
|
|
68
|
+
} {
|
|
69
|
+
// Start decoding eagerly during deserialization
|
|
70
|
+
const decodeThenable = browserDecode(stream)
|
|
71
|
+
const cssHrefs = new Set<string>()
|
|
72
|
+
|
|
73
|
+
// Synchronous cache for the decoded tree.
|
|
74
|
+
let cachedTree: unknown = undefined
|
|
75
|
+
let cacheReady = false
|
|
76
|
+
|
|
77
|
+
// Promise for the tree with lazy elements awaited.
|
|
78
|
+
const transformedTreePromise = Promise.resolve(decodeThenable).then(
|
|
79
|
+
async (result) => {
|
|
80
|
+
await awaitLazyElements(result, (href) => {
|
|
81
|
+
cssHrefs.add(href)
|
|
82
|
+
})
|
|
83
|
+
cachedTree = result
|
|
84
|
+
cacheReady = true
|
|
85
|
+
return result
|
|
86
|
+
},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// Track the lazy element loading - prevents flash
|
|
90
|
+
trackPostProcessPromise(transformedTreePromise)
|
|
91
|
+
|
|
92
|
+
const streamWrapper: ServerComponentStream = {
|
|
93
|
+
createReplayStream: () => stream,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const getTree = () => {
|
|
97
|
+
if (cacheReady) return cachedTree
|
|
98
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
99
|
+
return use(transformedTreePromise)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { getTree, streamWrapper, cssHrefs }
|
|
103
|
+
}
|
|
104
|
+
// Legacy export for backwards compatibility during migration
|
|
105
|
+
export const createServerComponentFromStream = createCompositeFromStream
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared RSC (React Server Components) entry point.
|
|
3
|
+
*
|
|
4
|
+
* This file exports the functions needed for the active RSC environment:
|
|
5
|
+
* - getServerFnById: Resolves server functions by their encoded ID
|
|
6
|
+
* - render: Renders a React node to an RSC Flight stream
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
|
|
10
|
+
import type React from 'react'
|
|
11
|
+
|
|
12
|
+
// Re-export getServerFnById from the virtual module which handles both dev and production
|
|
13
|
+
// In dev: dynamic import with base64url-decoded file path
|
|
14
|
+
// In production: manifest-based lookup with bundled chunks
|
|
15
|
+
export { getServerFnById } from '#tanstack-start-server-fn-resolver'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Renders a React node to an RSC Flight stream.
|
|
19
|
+
* Used internally for streaming server component output.
|
|
20
|
+
*/
|
|
21
|
+
export function render(node: React.ReactNode): ReadableStream<Uint8Array> {
|
|
22
|
+
return renderToReadableStream(node)
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Type declarations for virtual modules used in RSC entry
|
|
2
|
+
// These modules are provided by the active bundler adapter at runtime
|
|
3
|
+
|
|
4
|
+
declare module '#tanstack-start-server-fn-resolver' {
|
|
5
|
+
export type ServerFnLookupAccess = { origin: 'client' } | { origin: 'server' }
|
|
6
|
+
|
|
7
|
+
export type ServerFn = (...args: Array<any>) => Promise<any>
|
|
8
|
+
export function getServerFnById(
|
|
9
|
+
id: string,
|
|
10
|
+
access: ServerFnLookupAccess,
|
|
11
|
+
): Promise<ServerFn>
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level Flight stream API for RSC (React Server Components) environment.
|
|
3
|
+
*
|
|
4
|
+
* This exports renderToReadableStream which generates a Flight stream from
|
|
5
|
+
* React elements. Only available in RSC context (react-server condition).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { renderToReadableStream } from '@tanstack/react-start/rsc'
|
|
10
|
+
*
|
|
11
|
+
* const stream = renderToReadableStream(<MyServerComponent />)
|
|
12
|
+
* return new Response(stream, {
|
|
13
|
+
* headers: { 'Content-Type': 'text/x-component' }
|
|
14
|
+
* })
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client stub for renderToReadableStream.
|
|
3
|
+
*
|
|
4
|
+
* This function should never be called at runtime on the client.
|
|
5
|
+
* It exists only to provide types for bundler imports in client bundles.
|
|
6
|
+
* The real implementation only runs inside RSC context (server functions).
|
|
7
|
+
*/
|
|
8
|
+
export function renderToReadableStream(
|
|
9
|
+
_node: React.ReactNode,
|
|
10
|
+
): ReadableStream<Uint8Array> {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'renderToReadableStream cannot be called on the client. ' +
|
|
13
|
+
'This function should only be called inside RSC context (server functions).',
|
|
14
|
+
)
|
|
15
|
+
}
|
package/src/flight.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level Flight stream APIs for decoding RSC streams.
|
|
3
|
+
*
|
|
4
|
+
* These functions provide direct access to RSC Flight stream decoding,
|
|
5
|
+
* allowing advanced use cases like:
|
|
6
|
+
* - Server functions returning raw Flight Response
|
|
7
|
+
* - API routes streaming Flight payloads
|
|
8
|
+
* - Custom Flight stream handling via RawStream
|
|
9
|
+
*
|
|
10
|
+
* `createFromReadableStream` works in both SSR and browser contexts.
|
|
11
|
+
* `createFromFetch` is browser-only.
|
|
12
|
+
*
|
|
13
|
+
* NOTE: Dynamic imports keep decode initialisation runtime-specific. The
|
|
14
|
+
* concrete implementation comes from bundler-owned virtual modules.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
createClientOnlyFn,
|
|
19
|
+
createIsomorphicFn,
|
|
20
|
+
} from '@tanstack/start-fn-stubs'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Decode a Flight stream into React elements.
|
|
24
|
+
* Works in both SSR and browser contexts.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const rawStream = await getRscRawStream()
|
|
29
|
+
* const tree = await createFromReadableStream(rawStream)
|
|
30
|
+
* return <>{tree}</>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const createFromReadableStream = createIsomorphicFn()
|
|
34
|
+
.server(
|
|
35
|
+
async (stream: ReadableStream<Uint8Array>): Promise<React.ReactNode> => {
|
|
36
|
+
const { createFromReadableStream: decode } =
|
|
37
|
+
await import('virtual:tanstack-rsc-ssr-decode')
|
|
38
|
+
return decode(stream) as React.ReactNode
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
.client(
|
|
42
|
+
async (stream: ReadableStream<Uint8Array>): Promise<React.ReactNode> => {
|
|
43
|
+
const { createFromReadableStream: decode } =
|
|
44
|
+
await import('virtual:tanstack-rsc-browser-decode')
|
|
45
|
+
return decode(stream)
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decode a Flight stream from a fetch Response.
|
|
51
|
+
* Browser only - will throw if called on the server.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* // From server function returning raw Response
|
|
56
|
+
* const tree = await createFromFetch(getFlightResponse())
|
|
57
|
+
*
|
|
58
|
+
* // From API route
|
|
59
|
+
* const tree = await createFromFetch(fetch('/api/rsc-flight'))
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export const createFromFetch = createClientOnlyFn(
|
|
63
|
+
async (fetchPromise: Promise<Response>): Promise<React.ReactNode> => {
|
|
64
|
+
const { createFromFetch: decode } =
|
|
65
|
+
await import('virtual:tanstack-rsc-browser-decode')
|
|
66
|
+
return decode(fetchPromise)
|
|
67
|
+
},
|
|
68
|
+
)
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type declarations for virtual:tanstack-rsc-runtime
|
|
5
|
+
*
|
|
6
|
+
* This virtual module is provided by the active Start bundler adapter
|
|
7
|
+
* and re-exports RSC runtime functions from that adapter's Flight runtime.
|
|
8
|
+
*
|
|
9
|
+
* Using a virtual module allows the imports to be resolved at runtime within
|
|
10
|
+
* the correct server environment context (react-server conditions).
|
|
11
|
+
*/
|
|
12
|
+
declare module 'virtual:tanstack-rsc-runtime' {
|
|
13
|
+
export function renderToReadableStream<T>(
|
|
14
|
+
data: T,
|
|
15
|
+
options?: object,
|
|
16
|
+
): ReadableStream<Uint8Array>
|
|
17
|
+
export function createFromReadableStream<T = unknown>(
|
|
18
|
+
stream: ReadableStream<Uint8Array>,
|
|
19
|
+
options?: object,
|
|
20
|
+
): Promise<T>
|
|
21
|
+
export function createTemporaryReferenceSet(): object
|
|
22
|
+
export function decodeReply<T = unknown>(
|
|
23
|
+
body: string | FormData,
|
|
24
|
+
options?: object,
|
|
25
|
+
): Promise<T>
|
|
26
|
+
export function loadServerAction(id: string): Promise<(...args: any) => any>
|
|
27
|
+
export function decodeAction<T = unknown>(
|
|
28
|
+
body: FormData,
|
|
29
|
+
serverManifest?: unknown,
|
|
30
|
+
): Promise<() => T>
|
|
31
|
+
export function decodeFormState<T = unknown>(
|
|
32
|
+
actionResult: T,
|
|
33
|
+
body: FormData,
|
|
34
|
+
serverManifest?: unknown,
|
|
35
|
+
): Promise<unknown>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare module 'virtual:tanstack-rsc-browser-decode' {
|
|
39
|
+
export function createFromReadableStream<T = unknown>(
|
|
40
|
+
stream: ReadableStream<Uint8Array>,
|
|
41
|
+
options?: object,
|
|
42
|
+
): Promise<T>
|
|
43
|
+
export function createFromFetch<T = unknown>(
|
|
44
|
+
response: Promise<Response>,
|
|
45
|
+
): Promise<T>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
declare module 'virtual:tanstack-rsc-ssr-decode' {
|
|
49
|
+
export function setOnClientReference(
|
|
50
|
+
callback:
|
|
51
|
+
| ((reference: {
|
|
52
|
+
id: string
|
|
53
|
+
deps: { js: Array<string>; css: Array<string> }
|
|
54
|
+
runtime?: 'rsbuild'
|
|
55
|
+
}) => void)
|
|
56
|
+
| undefined,
|
|
57
|
+
): void
|
|
58
|
+
export function createFromReadableStream<T = unknown>(
|
|
59
|
+
stream: ReadableStream<Uint8Array>,
|
|
60
|
+
options?: object,
|
|
61
|
+
): Promise<T>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Type declarations for virtual:tanstack-rsc-hmr
|
|
66
|
+
*
|
|
67
|
+
* This virtual module is provided by the active Start bundler adapter
|
|
68
|
+
* and sets up the RSC HMR listener in dev mode. It listens for 'rsc:update'
|
|
69
|
+
* events and invalidates the router to refetch server components.
|
|
70
|
+
*
|
|
71
|
+
* In production builds, this module is empty.
|
|
72
|
+
*/
|
|
73
|
+
declare module 'virtual:tanstack-rsc-hmr' {
|
|
74
|
+
export function setupRscHmr(): void
|
|
75
|
+
}
|
package/src/index.rsc.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Server-side exports (react-server condition)
|
|
2
|
+
// This file is used when importing from RSC (React Server Components) context
|
|
3
|
+
|
|
4
|
+
// Types are always available
|
|
5
|
+
export type { AnyCompositeComponent } from './ServerComponentTypes'
|
|
6
|
+
|
|
7
|
+
// New API: renderServerComponent - renders element to renderable proxy
|
|
8
|
+
export { renderServerComponent } from './renderServerComponent.js'
|
|
9
|
+
|
|
10
|
+
// New API: createCompositeComponent - creates composite with slot support
|
|
11
|
+
export { createCompositeComponent } from './createCompositeComponent.js'
|
|
12
|
+
|
|
13
|
+
// Renderer for composite RSC data (client/SSR)
|
|
14
|
+
export { CompositeComponent } from './CompositeComponent.js'
|
|
15
|
+
|
|
16
|
+
// Low-level Flight stream API (RSC only)
|
|
17
|
+
export { renderToReadableStream } from './flight.rsc.js'
|
|
18
|
+
|
|
19
|
+
// Low-level Flight stream APIs (also available in RSC for decode operations)
|
|
20
|
+
export { createFromReadableStream, createFromFetch } from './flight'
|
|
21
|
+
|
|
22
|
+
// Note: rscSerializationAdapter is intentionally NOT exported here.
|
|
23
|
+
// It uses createSerializationAdapter from react-router which is marked as
|
|
24
|
+
// client-only in RSC environment. The adapter is only needed by SSR/client
|
|
25
|
+
// for serializing/deserializing server components, not by RSC itself.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Default exports (used by client bundles and SSR)
|
|
2
|
+
// This file is used when importing outside of RSC (React Server Components) context
|
|
3
|
+
|
|
4
|
+
// Types are always available
|
|
5
|
+
export type { AnyCompositeComponent } from './ServerComponentTypes'
|
|
6
|
+
|
|
7
|
+
// CSS hrefs symbol for type-safe access
|
|
8
|
+
export { SERVER_COMPONENT_CSS_HREFS } from './ServerComponentTypes'
|
|
9
|
+
|
|
10
|
+
// Stubs for RSC-only functions - throw if called outside RSC context
|
|
11
|
+
export { renderServerComponent } from './renderServerComponent.stub.js'
|
|
12
|
+
export { createCompositeComponent } from './createCompositeComponent.stub.js'
|
|
13
|
+
|
|
14
|
+
// Renderer for composite RSC data (client/SSR)
|
|
15
|
+
export { CompositeComponent } from './CompositeComponent.js'
|
|
16
|
+
|
|
17
|
+
// Low-level Flight stream APIs (client/SSR)
|
|
18
|
+
export { createFromReadableStream, createFromFetch } from './flight'
|
|
19
|
+
|
|
20
|
+
// Stub for renderToReadableStream - throws if called outside RSC context
|
|
21
|
+
export { renderToReadableStream } from './flight.stub.js'
|
|
22
|
+
|
|
23
|
+
// Note: rscSerializationAdapter is intentionally NOT exported here.
|
|
24
|
+
// It imports virtual:tanstack-rsc-hmr which is client-only (not available in SSR).
|
|
25
|
+
// Import directly from '@tanstack/react-start-rsc/serialization.client' or
|
|
26
|
+
// '@tanstack/react-start-rsc/serialization.server' as needed.
|