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