@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,42 @@
1
+ 'use client'
2
+
3
+ import { createContext, use } from 'react'
4
+ import type { SlotImplementations } from './types'
5
+
6
+ export interface SlotContextValue {
7
+ implementations: SlotImplementations
8
+ strict: boolean
9
+ }
10
+
11
+ const SlotContext = createContext<SlotContextValue | null>(null)
12
+
13
+ /**
14
+ * Hook to access slot implementations from within ClientSlot.
15
+ */
16
+ export function useSlotContext(): SlotContextValue | null {
17
+ return use(SlotContext)
18
+ }
19
+
20
+ export interface SlotProviderProps {
21
+ implementations: SlotImplementations
22
+ strict?: boolean
23
+ children?: React.ReactNode
24
+ }
25
+
26
+ /**
27
+ * SlotProvider - makes slot implementations available to ClientSlot components.
28
+ *
29
+ * Must wrap the decoded RSC content so that ClientSlot components can
30
+ * access their slot implementations via React Context.
31
+ */
32
+ export function SlotProvider({
33
+ implementations,
34
+ strict,
35
+ children,
36
+ }: SlotProviderProps) {
37
+ return (
38
+ <SlotContext value={{ implementations, strict: strict ?? false }}>
39
+ {children}
40
+ </SlotContext>
41
+ )
42
+ }
@@ -0,0 +1,91 @@
1
+ import { ReactElement, ReactLazy, ReactSuspense } from './reactSymbols'
2
+
3
+ /**
4
+ * Optional callback for collecting CSS hrefs during tree traversal.
5
+ * Only called server-side when processing <link rel="stylesheet" data-rsc-css-href>
6
+ */
7
+ export type CssHrefCollector = (href: string) => void
8
+
9
+ /**
10
+ * Yields pending lazy element payloads from a tree, stopping at Suspense boundaries.
11
+ * Also collects CSS hrefs from <link rel="stylesheet" data-rsc-css-href> elements.
12
+ */
13
+ function* findPendingLazyPayloads(
14
+ obj: unknown,
15
+ seen = new Set(),
16
+ cssCollector?: CssHrefCollector,
17
+ ): Generator<PromiseLike<unknown>> {
18
+ if (!obj || typeof obj !== 'object') return
19
+ if (seen.has(obj)) return
20
+ seen.add(obj)
21
+
22
+ const el = obj as any
23
+
24
+ // Stop at Suspense boundaries - lazy elements inside are intentionally deferred
25
+ if (el.$$typeof === ReactElement && el.type === ReactSuspense) {
26
+ return
27
+ }
28
+
29
+ // Collect CSS hrefs from <link rel="stylesheet" data-rsc-css-href>
30
+ // The active RSC bundler adapter injects these for CSS module imports
31
+ if (
32
+ el.$$typeof === ReactElement &&
33
+ el.type === 'link' &&
34
+ el.props?.rel === 'stylesheet'
35
+ ) {
36
+ const cssHref = el.props['data-rsc-css-href'] as string | undefined
37
+ if (cssHref && cssCollector) {
38
+ cssCollector(cssHref)
39
+ }
40
+ }
41
+
42
+ // Yield pending lazy element payload
43
+ if (el.$$typeof === ReactLazy) {
44
+ const payload = el._payload
45
+ if (
46
+ payload &&
47
+ typeof payload === 'object' &&
48
+ (payload.status === 'pending' || payload.status === 'blocked') &&
49
+ typeof payload.then === 'function'
50
+ ) {
51
+ yield payload
52
+ }
53
+ }
54
+
55
+ // Recurse into children
56
+ if (Array.isArray(obj)) {
57
+ for (const item of obj) {
58
+ yield* findPendingLazyPayloads(item, seen, cssCollector)
59
+ }
60
+ } else {
61
+ for (const key of Object.keys(obj)) {
62
+ if (key !== '_owner' && key !== '_store') {
63
+ yield* findPendingLazyPayloads(el[key], seen, cssCollector)
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Wait for all lazy elements in a tree to be resolved.
71
+ * This ensures client component chunks are fully loaded before rendering,
72
+ * preventing Suspense boundaries from flashing during SWR navigation.
73
+ *
74
+ * Also collects CSS hrefs from <link rel="stylesheet" data-rsc-css-href>
75
+ * elements for preloading in <head>.
76
+ *
77
+ * @param tree - The tree to process
78
+ * @param cssCollector - Optional callback to collect CSS hrefs (server-only)
79
+ */
80
+ export async function awaitLazyElements(
81
+ tree: unknown,
82
+ cssCollector?: CssHrefCollector,
83
+ ): Promise<void> {
84
+ for (const payload of findPendingLazyPayloads(
85
+ tree,
86
+ new Set(),
87
+ cssCollector,
88
+ )) {
89
+ await Promise.resolve(payload).catch(() => {})
90
+ }
91
+ }
@@ -0,0 +1,20 @@
1
+ import type {
2
+ CompositeComponentResult,
3
+ ValidateCompositeComponent,
4
+ } from './ServerComponentTypes'
5
+
6
+ /**
7
+ * Client stub for createCompositeComponent.
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 createCompositeComponent<TComp>(
14
+ _component: ValidateCompositeComponent<TComp>,
15
+ ): Promise<CompositeComponentResult<TComp>> {
16
+ throw new Error(
17
+ 'createCompositeComponent cannot be called on the client. ' +
18
+ 'This function should only be called inside a server function or route loader.',
19
+ )
20
+ }
@@ -0,0 +1,338 @@
1
+ import { createElement } from 'react'
2
+ import { renderToReadableStream } from 'virtual:tanstack-rsc-runtime'
3
+ import { getRequest } from '@tanstack/start-server-core'
4
+ import { getStartContext } from '@tanstack/start-storage-context'
5
+ import { sanitizeSlotArgs } from './slotUsageSanitizer'
6
+
7
+ import { ReplayableStream } from './ReplayableStream'
8
+ import { ClientSlot } from './ClientSlot'
9
+ import {
10
+ RSC_SLOT_USAGES_STREAM,
11
+ SERVER_COMPONENT_STREAM,
12
+ } from './ServerComponentTypes'
13
+ import type {
14
+ AnyCompositeComponent,
15
+ CompositeComponentResult,
16
+ RscSlotUsageEvent,
17
+ ServerComponentStream,
18
+ ValidateCompositeComponent,
19
+ } from './ServerComponentTypes'
20
+
21
+ import './rscSsrHandler' // Import for global declaration side effect
22
+
23
+ /**
24
+ * Creates a composite server component with slot support.
25
+ *
26
+ * Supports returning:
27
+ * - A ReactNode directly
28
+ * - An object structure with ReactNodes: accessed as `src.Foo`
29
+ * - Nested structures: accessed as `src.x.Bar`
30
+ *
31
+ * Props that are functions become slots - they render as ClientSlot placeholders
32
+ * in the RSC output, filled in by the consumer with actual implementations.
33
+ *
34
+ * The returned value is NOT directly renderable. Use `<CompositeComponent src={...} />`.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * const src = await createCompositeComponent((props) => (
39
+ * <div>
40
+ * <header>{props.header('Dashboard')}</header>
41
+ * <main>{props.children}</main>
42
+ * </div>
43
+ * ))
44
+ *
45
+ * // In route component
46
+ * return (
47
+ * <CompositeComponent src={src} header={(title) => <h1>{title}</h1>}>
48
+ * <p>Main content</p>
49
+ * </CompositeComponent>
50
+ * )
51
+ * ```
52
+ */
53
+ export async function createCompositeComponent<TComp>(
54
+ component: ValidateCompositeComponent<TComp>,
55
+ ): Promise<CompositeComponentResult<TComp>> {
56
+ const isDev = process.env.NODE_ENV === 'development'
57
+
58
+ // Dev-only: stream slot usage events (slot + raw args)
59
+ const slotUsagesEmitter = isDev
60
+ ? createReadableStreamEmitter<RscSlotUsageEvent>()
61
+ : null
62
+
63
+ // Create a wrapper component that will be rendered inside React's Flight context.
64
+ // This ensures React.cache works properly since the component is called during
65
+ // renderToReadableStream's render phase, not before it.
66
+ const { proxy: proxyProps } = createSlotProxy<{}>({
67
+ onSlotCall: slotUsagesEmitter
68
+ ? (name, args) => {
69
+ const sanitizedArgs = sanitizeSlotArgs(args)
70
+ slotUsagesEmitter.emit({
71
+ slot: name,
72
+ args: sanitizedArgs.length ? sanitizedArgs : undefined,
73
+ })
74
+ }
75
+ : undefined,
76
+ })
77
+
78
+ // Wrapper that renders the user's component inside Flight render context
79
+ async function ServerComponentWrapper() {
80
+ return (component as React.FC)(proxyProps)
81
+ }
82
+
83
+ // Render using createElement so React calls our component during Flight rendering
84
+ // This is critical for React.cache to work - the component must be invoked
85
+ // during renderToReadableStream's execution, not before
86
+ const flightStream = renderToReadableStream(
87
+ createElement(ServerComponentWrapper),
88
+ )
89
+
90
+ // Check if this is an SSR request (router) or a direct server function call
91
+ const ctx = getStartContext({ throwIfNotFound: false })
92
+ const isRouterRequest = ctx?.handlerType === 'router'
93
+ const ssrHandler = globalThis.__RSC_SSR__
94
+
95
+ // SSR path: buffer stream for replay, pre-decode for synchronous rendering
96
+ if (isRouterRequest && ssrHandler) {
97
+ const signal = getRequest().signal
98
+ const stream = new ReplayableStream(flightStream, { signal })
99
+
100
+ // Pre-decode during loader phase for synchronous SSR rendering
101
+ const decoded = await ssrHandler.decode(stream)
102
+
103
+ // For SSR we know decode fully consumed the Flight stream.
104
+ slotUsagesEmitter?.close()
105
+
106
+ const proxy = ssrHandler.createCompositeProxy(
107
+ stream,
108
+ decoded,
109
+ slotUsagesEmitter?.stream,
110
+ )
111
+ return proxy as CompositeComponentResult<TComp>
112
+ }
113
+
114
+ // Server function call path:
115
+ // The serialization adapter will stream to the client.
116
+ const monitoredFlightStream =
117
+ isDev && slotUsagesEmitter
118
+ ? wrapReadableStream(flightStream, {
119
+ onDone: () => {
120
+ slotUsagesEmitter.close()
121
+ },
122
+ onCancel: () => {
123
+ slotUsagesEmitter.close()
124
+ },
125
+ onError: () => {
126
+ slotUsagesEmitter.close()
127
+ },
128
+ })
129
+ : flightStream
130
+
131
+ return createCompositeHandle(monitoredFlightStream, {
132
+ slotUsagesStream: slotUsagesEmitter?.stream,
133
+ }) as CompositeComponentResult<TComp>
134
+ }
135
+
136
+ /**
137
+ * Creates a composite handle for server function responses.
138
+ * No proxy needed - the client will decode and create its own proxy.
139
+ */
140
+ function createCompositeHandle(
141
+ flightStream: ReadableStream<Uint8Array>,
142
+ options?: {
143
+ slotUsagesStream?: ReadableStream<RscSlotUsageEvent>
144
+ },
145
+ ): AnyCompositeComponent {
146
+ // Simple single-use stream wrapper. For server function calls, the stream
147
+ // is consumed exactly once by the serialization adapter for transport.
148
+ const streamWrapper: ServerComponentStream = {
149
+ createReplayStream: () => flightStream,
150
+ }
151
+
152
+ // Create a stub function with the stream attached for serialization.
153
+ // This will never be rendered directly - it goes through serialization
154
+ // which extracts the stream and sends it to the client.
155
+ const stub = function CompositeComponentStub(): never {
156
+ throw new Error(
157
+ 'CompositeComponent from server function cannot be rendered on server. ' +
158
+ 'It should be serialized and sent to the client.',
159
+ )
160
+ }
161
+
162
+ ;(stub as any)[SERVER_COMPONENT_STREAM] = streamWrapper
163
+ // Note: RENDERABLE_RSC is not set (or implicitly false), indicating this is a composite component
164
+
165
+ if (options?.slotUsagesStream) {
166
+ ;(stub as any)[RSC_SLOT_USAGES_STREAM] = options.slotUsagesStream
167
+ }
168
+
169
+ return stub as unknown as AnyCompositeComponent
170
+ }
171
+
172
+ /**
173
+ * Base slot props type - functions that become ClientSlot placeholders
174
+ */
175
+ interface SlotPropsBase {
176
+ [key: string]:
177
+ | ((...args: Array<any>) => React.ReactNode)
178
+ | React.ReactNode
179
+ | undefined
180
+ children?: React.ReactNode
181
+ }
182
+
183
+ interface SlotProxyResult<TSlotProps extends object> {
184
+ proxy: TSlotProps & SlotPropsBase
185
+ }
186
+
187
+ /**
188
+ * Proxy that turns property access into ClientSlot renders.
189
+ * Also tracks accessed slot names for devtools.
190
+ */
191
+ function createSlotProxy<TSlotProps extends object>(options?: {
192
+ onSlotCall?: (name: string, args: Array<any>) => void
193
+ }): SlotProxyResult<TSlotProps> {
194
+ const cache = new Map<string, (...args: Array<any>) => React.ReactNode>()
195
+
196
+ const proxy = new Proxy({} as TSlotProps & SlotPropsBase, {
197
+ get(_target, prop) {
198
+ if (prop === 'then' || typeof prop !== 'string') return undefined
199
+
200
+ if (prop === 'children') {
201
+ options?.onSlotCall?.('children', [])
202
+ return createElement(ClientSlot, { slot: 'children', args: [] })
203
+ }
204
+
205
+ let fn = cache.get(prop)
206
+ if (!fn) {
207
+ fn = (...args: Array<any>) => {
208
+ options?.onSlotCall?.(prop, args)
209
+ return createElement(ClientSlot, { slot: prop, args })
210
+ }
211
+ cache.set(prop, fn)
212
+ }
213
+ return fn
214
+ },
215
+ })
216
+
217
+ return {
218
+ proxy,
219
+ }
220
+ }
221
+
222
+ function createReadableStreamEmitter<T>(): {
223
+ stream: ReadableStream<T>
224
+ emit: (value: T) => void
225
+ close: () => void
226
+ } {
227
+ let closed = false
228
+ const queue: Array<T> = []
229
+ let controller: ReadableStreamDefaultController<T> | null = null
230
+
231
+ const stream = new ReadableStream<T>({
232
+ start(ctrl) {
233
+ controller = ctrl
234
+ for (const value of queue) {
235
+ try {
236
+ ctrl.enqueue(value)
237
+ } catch {
238
+ // Ignore
239
+ }
240
+ }
241
+ queue.length = 0
242
+ if (closed) {
243
+ try {
244
+ ctrl.close()
245
+ } catch {
246
+ // Ignore
247
+ }
248
+ }
249
+ },
250
+ cancel() {
251
+ closed = true
252
+ controller = null
253
+ queue.length = 0
254
+ },
255
+ })
256
+
257
+ const emit = (value: T) => {
258
+ if (closed) return
259
+ if (!controller) {
260
+ queue.push(value)
261
+ return
262
+ }
263
+ try {
264
+ controller.enqueue(value)
265
+ } catch {
266
+ // Ignore
267
+ }
268
+ }
269
+
270
+ const close = () => {
271
+ if (closed) return
272
+ closed = true
273
+ if (controller) {
274
+ try {
275
+ controller.close()
276
+ } catch {
277
+ // Ignore
278
+ }
279
+ controller = null
280
+ }
281
+ }
282
+
283
+ return { stream, emit, close }
284
+ }
285
+
286
+ function wrapReadableStream<T>(
287
+ source: ReadableStream<T>,
288
+ handlers: {
289
+ onDone?: () => void
290
+ onCancel?: () => void
291
+ onError?: () => void
292
+ },
293
+ ): ReadableStream<T> {
294
+ const reader = source.getReader()
295
+ let finished = false
296
+
297
+ const finish = () => {
298
+ if (finished) return
299
+ finished = true
300
+ handlers.onDone?.()
301
+ try {
302
+ reader.releaseLock()
303
+ } catch {
304
+ // Ignore
305
+ }
306
+ }
307
+
308
+ return new ReadableStream<T>({
309
+ async pull(controller) {
310
+ try {
311
+ const { value, done } = await reader.read()
312
+ if (done) {
313
+ controller.close()
314
+ finish()
315
+ return
316
+ }
317
+ controller.enqueue(value)
318
+ } catch (err) {
319
+ try {
320
+ controller.error(err)
321
+ } catch {
322
+ // Ignore
323
+ }
324
+ handlers.onError?.()
325
+ finish()
326
+ }
327
+ },
328
+ async cancel(reason) {
329
+ handlers.onCancel?.()
330
+ try {
331
+ await reader.cancel(reason)
332
+ } catch {
333
+ // Ignore
334
+ }
335
+ finish()
336
+ },
337
+ })
338
+ }