@ivogt/rsc-router 0.0.0-experimental.1
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/README.md +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
2
|
+
/// <reference path="../vite/version.d.ts" />
|
|
3
|
+
/**
|
|
4
|
+
* RSC Request Handler
|
|
5
|
+
*
|
|
6
|
+
* Main request handler for RSC rendering, server actions, loader fetching,
|
|
7
|
+
* and progressive enhancement (no-JS form submissions).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { renderSegments } from "../segment-system.js";
|
|
11
|
+
import { RouteNotFoundError } from "../errors.js";
|
|
12
|
+
import { getLoaderLazy } from "../server/loader-registry.js";
|
|
13
|
+
import {
|
|
14
|
+
matchMiddleware,
|
|
15
|
+
executeMiddleware,
|
|
16
|
+
executeLoaderMiddleware,
|
|
17
|
+
} from "../router/middleware.js";
|
|
18
|
+
import {
|
|
19
|
+
runWithRequestContext,
|
|
20
|
+
setRequestContextParams,
|
|
21
|
+
requireRequestContext,
|
|
22
|
+
createRequestContext,
|
|
23
|
+
} from "../server/request-context.js";
|
|
24
|
+
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
import type {
|
|
28
|
+
RscPayload,
|
|
29
|
+
ReactFormState,
|
|
30
|
+
CreateRSCHandlerOptions,
|
|
31
|
+
} from "./types.js";
|
|
32
|
+
import { hasBodyContent, createResponseWithMergedHeaders } from "./helpers.js";
|
|
33
|
+
import { generateNonce } from "./nonce.js";
|
|
34
|
+
import { VERSION } from "rsc-router:version";
|
|
35
|
+
import type { ErrorPhase } from "../types.js";
|
|
36
|
+
import { invokeOnError } from "../router/error-handling.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create an RSC request handler.
|
|
40
|
+
*
|
|
41
|
+
* @example Basic usage (deps and loadSSRModule have sensible defaults)
|
|
42
|
+
* ```tsx
|
|
43
|
+
* import { createRSCHandler } from "rsc-router/rsc";
|
|
44
|
+
* import { router } from "./router.js";
|
|
45
|
+
*
|
|
46
|
+
* export default createRSCHandler({ router });
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example With custom deps (advanced)
|
|
50
|
+
* ```tsx
|
|
51
|
+
* import { createRSCHandler } from "rsc-router/rsc";
|
|
52
|
+
* import * as rsc from "@vitejs/plugin-rsc/rsc";
|
|
53
|
+
* import { router } from "./router.js";
|
|
54
|
+
*
|
|
55
|
+
* export default createRSCHandler({
|
|
56
|
+
* router,
|
|
57
|
+
* deps: rsc,
|
|
58
|
+
* loadSSRModule: () => import.meta.viteRsc.loadModule("ssr", "index"),
|
|
59
|
+
* });
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function createRSCHandler<TEnv = unknown>(
|
|
63
|
+
options: CreateRSCHandlerOptions<TEnv>
|
|
64
|
+
) {
|
|
65
|
+
const { router, version = VERSION, nonce: nonceProvider } = options;
|
|
66
|
+
|
|
67
|
+
// Use provided deps or default to @vitejs/plugin-rsc/rsc exports
|
|
68
|
+
const deps = options.deps ?? rscDeps;
|
|
69
|
+
const {
|
|
70
|
+
renderToReadableStream,
|
|
71
|
+
decodeReply,
|
|
72
|
+
createTemporaryReferenceSet,
|
|
73
|
+
loadServerAction,
|
|
74
|
+
decodeAction,
|
|
75
|
+
decodeFormState,
|
|
76
|
+
} = deps;
|
|
77
|
+
|
|
78
|
+
// Use provided loadSSRModule or default to vite RSC module loader
|
|
79
|
+
const loadSSRModule =
|
|
80
|
+
options.loadSSRModule ??
|
|
81
|
+
(() => import.meta.viteRsc.loadModule("ssr", "index"));
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wrapper for invokeOnError that binds the router's onError callback.
|
|
85
|
+
* Uses the shared utility from router/error-handling.ts for consistent behavior.
|
|
86
|
+
*/
|
|
87
|
+
function callOnError(
|
|
88
|
+
error: unknown,
|
|
89
|
+
phase: ErrorPhase,
|
|
90
|
+
context: Parameters<typeof invokeOnError<TEnv>>[3]
|
|
91
|
+
): void {
|
|
92
|
+
invokeOnError(router.onError, error, phase, context, "RSC");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return async function handler(
|
|
96
|
+
request: Request,
|
|
97
|
+
env: TEnv = {} as TEnv
|
|
98
|
+
): Promise<Response> {
|
|
99
|
+
// Resolve nonce if provider is set
|
|
100
|
+
let nonce: string | undefined;
|
|
101
|
+
if (nonceProvider) {
|
|
102
|
+
const result = await nonceProvider(request, env);
|
|
103
|
+
nonce = result === true ? generateNonce() : result;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const url = new URL(request.url);
|
|
107
|
+
|
|
108
|
+
// Match global middleware
|
|
109
|
+
const matchedMiddleware = matchMiddleware(url.pathname, router.middleware);
|
|
110
|
+
|
|
111
|
+
// Shared variables between middleware and route handlers
|
|
112
|
+
const variables: Record<string, any> = {};
|
|
113
|
+
|
|
114
|
+
// Store nonce in variables so middleware can access via ctx.get('nonce')
|
|
115
|
+
if (nonce) {
|
|
116
|
+
variables.nonce = nonce;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Resolve cache store configuration
|
|
120
|
+
// Priority: options.cache (handler override) > router.cache (router default)
|
|
121
|
+
// Store is enabled only if: config provided, enabled, and no ?__no_cache query param
|
|
122
|
+
let cacheStore = undefined;
|
|
123
|
+
const cacheOption = options.cache ?? router.cache;
|
|
124
|
+
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
125
|
+
const cacheConfig =
|
|
126
|
+
typeof cacheOption === "function" ? cacheOption(env) : cacheOption;
|
|
127
|
+
|
|
128
|
+
if (cacheConfig.enabled !== false) {
|
|
129
|
+
cacheStore = cacheConfig.store;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Create unified request context with all methods
|
|
134
|
+
// Includes: stub response, handle store, loader memoization, use(), cookies, headers, cache store
|
|
135
|
+
// params starts empty, populated after route matching via setRequestContextParams
|
|
136
|
+
const requestContext = createRequestContext({
|
|
137
|
+
env,
|
|
138
|
+
request,
|
|
139
|
+
url,
|
|
140
|
+
variables,
|
|
141
|
+
cacheStore,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Wrap entire request handling in request context
|
|
145
|
+
// Makes context available via getRequestContext() throughout:
|
|
146
|
+
// - Middleware execution
|
|
147
|
+
// - Route handlers and loaders
|
|
148
|
+
// - Server components during rendering
|
|
149
|
+
// - Error boundaries
|
|
150
|
+
// - Streaming
|
|
151
|
+
return runWithRequestContext(requestContext, async () => {
|
|
152
|
+
// Core handler logic (wrapped by middleware)
|
|
153
|
+
const coreHandler = async (): Promise<Response> => {
|
|
154
|
+
return coreRequestHandler(request, env, url, variables, nonce);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Execute middleware chain if any, otherwise call core handler directly
|
|
158
|
+
if (matchedMiddleware.length > 0) {
|
|
159
|
+
return executeMiddleware(
|
|
160
|
+
matchedMiddleware,
|
|
161
|
+
request,
|
|
162
|
+
env,
|
|
163
|
+
variables,
|
|
164
|
+
coreHandler
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return coreHandler();
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Core request handling logic (separated for middleware wrapping)
|
|
173
|
+
async function coreRequestHandler(
|
|
174
|
+
request: Request,
|
|
175
|
+
env: TEnv,
|
|
176
|
+
url: URL,
|
|
177
|
+
variables: Record<string, any>,
|
|
178
|
+
nonce: string | undefined
|
|
179
|
+
): Promise<Response> {
|
|
180
|
+
// First, check for route-level middleware
|
|
181
|
+
const preview = await router.previewMatch(request, env);
|
|
182
|
+
if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
|
|
183
|
+
// Convert route middleware to app middleware format for execution
|
|
184
|
+
const middlewareEntries = preview.routeMiddleware.map((mw) => ({
|
|
185
|
+
entry: {
|
|
186
|
+
pattern: null,
|
|
187
|
+
regex: null,
|
|
188
|
+
paramNames: [],
|
|
189
|
+
handler: mw.handler,
|
|
190
|
+
mountPrefix: null,
|
|
191
|
+
},
|
|
192
|
+
params: mw.params,
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
// Execute route middleware wrapping the actual request handling
|
|
196
|
+
return executeMiddleware(middlewareEntries, request, env, variables, () =>
|
|
197
|
+
coreRequestHandlerInner(request, env, url, variables, nonce)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// No route middleware, proceed directly
|
|
202
|
+
return coreRequestHandlerInner(request, env, url, variables, nonce);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Inner request handler (actual RSC logic, wrapped by route middleware if any)
|
|
206
|
+
async function coreRequestHandlerInner(
|
|
207
|
+
request: Request,
|
|
208
|
+
env: TEnv,
|
|
209
|
+
url: URL,
|
|
210
|
+
variables: Record<string, any>,
|
|
211
|
+
nonce: string | undefined
|
|
212
|
+
): Promise<Response> {
|
|
213
|
+
// Early return for static file requests that don't need RSC handling
|
|
214
|
+
if (url.pathname === "/favicon.ico" || url.pathname === "/robots.txt") {
|
|
215
|
+
return new Response(null, { status: 404 });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const isPartial = url.searchParams.has("_rsc_partial");
|
|
219
|
+
const isAction =
|
|
220
|
+
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
221
|
+
const actionId =
|
|
222
|
+
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
223
|
+
|
|
224
|
+
// Version mismatch detection - client may have stale code after HMR/deployment
|
|
225
|
+
// If versions don't match, tell the client to reload
|
|
226
|
+
const clientVersion = url.searchParams.get("_rsc_v");
|
|
227
|
+
if (version && clientVersion && clientVersion !== version) {
|
|
228
|
+
console.log(
|
|
229
|
+
`[RSC] Version mismatch: client=${clientVersion}, server=${version}. Forcing reload.`
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Clean URL by removing RSC params
|
|
233
|
+
const cleanUrl = new URL(url);
|
|
234
|
+
cleanUrl.searchParams.delete("_rsc_partial");
|
|
235
|
+
cleanUrl.searchParams.delete("_rsc_segments");
|
|
236
|
+
cleanUrl.searchParams.delete("_rsc_v");
|
|
237
|
+
cleanUrl.searchParams.delete("_rsc_stale");
|
|
238
|
+
cleanUrl.searchParams.delete("_rsc_action");
|
|
239
|
+
cleanUrl.searchParams.delete("_rsc_prev");
|
|
240
|
+
|
|
241
|
+
// For actions, reload current page (referer)
|
|
242
|
+
// For navigation, load the target URL
|
|
243
|
+
const reloadUrl = isAction
|
|
244
|
+
? request.headers.get("referer") || cleanUrl.toString()
|
|
245
|
+
: cleanUrl.toString();
|
|
246
|
+
|
|
247
|
+
// Return special response that tells client to reload
|
|
248
|
+
return createResponseWithMergedHeaders(null, {
|
|
249
|
+
status: 200,
|
|
250
|
+
headers: {
|
|
251
|
+
"X-RSC-Reload": reloadUrl,
|
|
252
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Get handle store from request context (created at start of request)
|
|
258
|
+
const handleStore = requireRequestContext()._handleStore;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// PROGRESSIVE ENHANCEMENT: No-JS Form Submissions
|
|
263
|
+
// ============================================================================
|
|
264
|
+
const progressiveResult = await handleProgressiveEnhancement(
|
|
265
|
+
request,
|
|
266
|
+
env,
|
|
267
|
+
url,
|
|
268
|
+
isAction,
|
|
269
|
+
handleStore,
|
|
270
|
+
nonce
|
|
271
|
+
);
|
|
272
|
+
if (progressiveResult) {
|
|
273
|
+
return progressiveResult;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// SERVER ACTION EXECUTION (JavaScript-enabled client)
|
|
278
|
+
// ============================================================================
|
|
279
|
+
if (isAction && actionId) {
|
|
280
|
+
return handleServerAction(request, env, url, actionId, handleStore);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// LOADER FETCH EXECUTION (data fetching with RSC serialization)
|
|
285
|
+
// ============================================================================
|
|
286
|
+
const isLoaderRequest = url.searchParams.has("_rsc_loader");
|
|
287
|
+
if (isLoaderRequest) {
|
|
288
|
+
return handleLoaderFetch(request, env, url, variables);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// REGULAR RSC RENDERING (Navigation)
|
|
293
|
+
// ============================================================================
|
|
294
|
+
return handleRscRendering(request, env, url, isPartial, handleStore, nonce);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
// Check if middleware/handler returned Response
|
|
297
|
+
if (error instanceof Response) {
|
|
298
|
+
return error;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Return 404 for unmatched routes instead of 500
|
|
302
|
+
if (error instanceof RouteNotFoundError) {
|
|
303
|
+
callOnError(error, "routing", {
|
|
304
|
+
request,
|
|
305
|
+
url,
|
|
306
|
+
env,
|
|
307
|
+
handledByBoundary: false,
|
|
308
|
+
});
|
|
309
|
+
return createResponseWithMergedHeaders("Not Found", { status: 404 });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Report unhandled errors
|
|
313
|
+
callOnError(error, "routing", {
|
|
314
|
+
request,
|
|
315
|
+
url,
|
|
316
|
+
env,
|
|
317
|
+
handledByBoundary: false,
|
|
318
|
+
});
|
|
319
|
+
console.error(`[RSC] Error:`, error);
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// PROGRESSIVE ENHANCEMENT HANDLER
|
|
326
|
+
// When JavaScript is disabled, React renders forms with hidden fields
|
|
327
|
+
// ($ACTION_REF_*, $ACTION_KEY) containing the action reference.
|
|
328
|
+
// We detect these and return HTML instead of RSC stream.
|
|
329
|
+
// ============================================================================
|
|
330
|
+
async function handleProgressiveEnhancement(
|
|
331
|
+
request: Request,
|
|
332
|
+
env: TEnv,
|
|
333
|
+
url: URL,
|
|
334
|
+
isAction: boolean,
|
|
335
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
336
|
+
nonce: string | undefined
|
|
337
|
+
): Promise<Response | null> {
|
|
338
|
+
const contentType = request.headers.get("content-type") || "";
|
|
339
|
+
const isFormSubmission =
|
|
340
|
+
contentType.includes("multipart/form-data") ||
|
|
341
|
+
contentType.includes("application/x-www-form-urlencoded");
|
|
342
|
+
|
|
343
|
+
if (request.method !== "POST" || isAction || !isFormSubmission) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Clone the request to read FormData without consuming it
|
|
348
|
+
const formData = await request.clone().formData();
|
|
349
|
+
|
|
350
|
+
// Look for React's progressive enhancement hidden fields
|
|
351
|
+
let isDirectAction = false;
|
|
352
|
+
let isUseActionState = false;
|
|
353
|
+
let directActionId: string | null = null;
|
|
354
|
+
|
|
355
|
+
formData.forEach((_value, key) => {
|
|
356
|
+
if (key.startsWith("$ACTION_ID_")) {
|
|
357
|
+
isDirectAction = true;
|
|
358
|
+
directActionId = key.slice("$ACTION_ID_".length);
|
|
359
|
+
} else if (key.startsWith("$ACTION_REF_")) {
|
|
360
|
+
isUseActionState = true;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (!isDirectAction && !isUseActionState) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Execute action and return HTML
|
|
369
|
+
let actionResult: unknown = undefined;
|
|
370
|
+
let reactFormState: ReactFormState | null = null;
|
|
371
|
+
|
|
372
|
+
if (isUseActionState) {
|
|
373
|
+
try {
|
|
374
|
+
const boundAction = await decodeAction(formData);
|
|
375
|
+
actionResult = await boundAction();
|
|
376
|
+
} catch (error) {
|
|
377
|
+
callOnError(error, "action", {
|
|
378
|
+
request,
|
|
379
|
+
url,
|
|
380
|
+
env,
|
|
381
|
+
handledByBoundary: false,
|
|
382
|
+
});
|
|
383
|
+
console.error("[RSC] Progressive enhancement action error:", error);
|
|
384
|
+
}
|
|
385
|
+
} else if (isDirectAction && directActionId) {
|
|
386
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
387
|
+
|
|
388
|
+
let args: unknown[] = [];
|
|
389
|
+
try {
|
|
390
|
+
args = await decodeReply(formData, { temporaryReferences });
|
|
391
|
+
} catch {
|
|
392
|
+
args = [formData];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const loadedAction = await loadServerAction(directActionId);
|
|
397
|
+
actionResult = await loadedAction.apply(null, args);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
callOnError(error, "action", {
|
|
400
|
+
request,
|
|
401
|
+
url,
|
|
402
|
+
env,
|
|
403
|
+
actionId: directActionId,
|
|
404
|
+
handledByBoundary: false,
|
|
405
|
+
});
|
|
406
|
+
console.error("[RSC] Progressive enhancement action error:", error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Decode form state for useActionState progressive enhancement
|
|
411
|
+
try {
|
|
412
|
+
reactFormState = await decodeFormState(actionResult, formData);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
callOnError(error, "action", {
|
|
415
|
+
request,
|
|
416
|
+
url,
|
|
417
|
+
env,
|
|
418
|
+
handledByBoundary: false,
|
|
419
|
+
});
|
|
420
|
+
console.error("[RSC] Failed to decode form state:", error);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Re-render the page and return HTML
|
|
424
|
+
const renderRequest = new Request(url.toString(), {
|
|
425
|
+
method: "GET",
|
|
426
|
+
headers: new Headers({ accept: "text/html" }),
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const match = await router.match(renderRequest, env);
|
|
430
|
+
|
|
431
|
+
if (match.redirect) {
|
|
432
|
+
return new Response(null, {
|
|
433
|
+
status: 308,
|
|
434
|
+
headers: { Location: match.redirect },
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const root = renderSegments(match.segments, {
|
|
439
|
+
rootLayout: router.rootLayout,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const payload: RscPayload = {
|
|
443
|
+
root,
|
|
444
|
+
metadata: {
|
|
445
|
+
pathname: url.pathname,
|
|
446
|
+
segments: match.segments,
|
|
447
|
+
matched: match.matched,
|
|
448
|
+
diff: match.diff,
|
|
449
|
+
isPartial: false,
|
|
450
|
+
rootLayout: router.rootLayout,
|
|
451
|
+
handles: handleStore.stream(),
|
|
452
|
+
version,
|
|
453
|
+
},
|
|
454
|
+
formState: actionResult,
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const rscStream = renderToReadableStream<RscPayload>(payload);
|
|
458
|
+
const ssrModule = await loadSSRModule();
|
|
459
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
460
|
+
formState: reactFormState,
|
|
461
|
+
nonce,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
return new Response(htmlStream, {
|
|
465
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// SERVER ACTION HANDLER
|
|
471
|
+
// ============================================================================
|
|
472
|
+
async function handleServerAction(
|
|
473
|
+
request: Request,
|
|
474
|
+
env: TEnv,
|
|
475
|
+
url: URL,
|
|
476
|
+
actionId: string,
|
|
477
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"]
|
|
478
|
+
): Promise<Response> {
|
|
479
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
480
|
+
|
|
481
|
+
// Decode action arguments from request body
|
|
482
|
+
const contentType = request.headers.get("content-type") || "";
|
|
483
|
+
let args: unknown[] = [];
|
|
484
|
+
let actionFormData: FormData | undefined;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const body = contentType.includes("multipart/form-data")
|
|
488
|
+
? await request.formData()
|
|
489
|
+
: await request.text();
|
|
490
|
+
|
|
491
|
+
if (body instanceof FormData) {
|
|
492
|
+
actionFormData = body;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (hasBodyContent(body)) {
|
|
496
|
+
args = await decodeReply(body, { temporaryReferences });
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
callOnError(error, "action", {
|
|
500
|
+
request,
|
|
501
|
+
url,
|
|
502
|
+
env,
|
|
503
|
+
actionId,
|
|
504
|
+
handledByBoundary: false,
|
|
505
|
+
});
|
|
506
|
+
throw new Error(`Failed to decode action arguments: ${error}`, {
|
|
507
|
+
cause: error,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Execute the server action
|
|
512
|
+
let returnValue: { ok: boolean; data: unknown };
|
|
513
|
+
let actionStatus = 200;
|
|
514
|
+
let loadedAction: Function | undefined;
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
loadedAction = await loadServerAction(actionId);
|
|
518
|
+
const data = await loadedAction!.apply(null, args);
|
|
519
|
+
returnValue = { ok: true, data };
|
|
520
|
+
} catch (error) {
|
|
521
|
+
returnValue = { ok: false, data: error };
|
|
522
|
+
actionStatus = 500;
|
|
523
|
+
|
|
524
|
+
// Try to render error boundary
|
|
525
|
+
const errorResult = await router.matchError(request, env, error, "route");
|
|
526
|
+
|
|
527
|
+
// Report the action error (handledByBoundary indicates if error boundary will render)
|
|
528
|
+
callOnError(error, "action", {
|
|
529
|
+
request,
|
|
530
|
+
url,
|
|
531
|
+
env,
|
|
532
|
+
actionId,
|
|
533
|
+
handledByBoundary: !!errorResult,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
if (errorResult) {
|
|
537
|
+
setRequestContextParams(errorResult.params);
|
|
538
|
+
|
|
539
|
+
const payload: RscPayload = {
|
|
540
|
+
root: null,
|
|
541
|
+
metadata: {
|
|
542
|
+
pathname: url.pathname,
|
|
543
|
+
segments: errorResult.segments,
|
|
544
|
+
isPartial: true,
|
|
545
|
+
matched: errorResult.matched,
|
|
546
|
+
diff: errorResult.diff,
|
|
547
|
+
isError: true,
|
|
548
|
+
handles: handleStore.stream(),
|
|
549
|
+
version,
|
|
550
|
+
},
|
|
551
|
+
returnValue,
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const rscStream = renderToReadableStream<RscPayload>(payload, {
|
|
555
|
+
temporaryReferences,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
559
|
+
status: actionStatus,
|
|
560
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Revalidate after action
|
|
566
|
+
const resolvedActionId =
|
|
567
|
+
(loadedAction as { $id?: string; $$id?: string } | undefined)?.$id ??
|
|
568
|
+
(loadedAction as { $$id?: string } | undefined)?.$$id ??
|
|
569
|
+
actionId;
|
|
570
|
+
const actionContext = {
|
|
571
|
+
actionId: resolvedActionId,
|
|
572
|
+
actionUrl: new URL(request.url),
|
|
573
|
+
actionResult: returnValue.data,
|
|
574
|
+
formData: actionFormData,
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const matchResult = await router.matchPartial(request, env, actionContext);
|
|
578
|
+
|
|
579
|
+
if (!matchResult) {
|
|
580
|
+
// Fall back to full render
|
|
581
|
+
const fullMatch = await router.match(request, env);
|
|
582
|
+
setRequestContextParams(fullMatch.params);
|
|
583
|
+
|
|
584
|
+
if (fullMatch.redirect) {
|
|
585
|
+
return createResponseWithMergedHeaders(null, {
|
|
586
|
+
status: 308,
|
|
587
|
+
headers: { Location: fullMatch.redirect },
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const renderStart = performance.now();
|
|
592
|
+
const root = renderSegments(fullMatch.segments, {
|
|
593
|
+
rootLayout: router.rootLayout,
|
|
594
|
+
isAction: true,
|
|
595
|
+
});
|
|
596
|
+
const renderDuration = performance.now() - renderStart;
|
|
597
|
+
const serverTiming = fullMatch.serverTiming
|
|
598
|
+
? `${fullMatch.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
|
|
599
|
+
: `rendering;dur=${renderDuration.toFixed(2)}`;
|
|
600
|
+
|
|
601
|
+
const payload: RscPayload = {
|
|
602
|
+
root,
|
|
603
|
+
metadata: {
|
|
604
|
+
pathname: url.pathname,
|
|
605
|
+
segments: fullMatch.segments,
|
|
606
|
+
matched: fullMatch.matched,
|
|
607
|
+
diff: fullMatch.diff,
|
|
608
|
+
handles: handleStore.stream(),
|
|
609
|
+
version,
|
|
610
|
+
},
|
|
611
|
+
returnValue,
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const rscStream = renderToReadableStream<RscPayload>(payload, {
|
|
615
|
+
temporaryReferences,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const headers: Record<string, string> = {
|
|
619
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
620
|
+
};
|
|
621
|
+
if (serverTiming) {
|
|
622
|
+
headers["Server-Timing"] = serverTiming;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
626
|
+
status: actionStatus,
|
|
627
|
+
headers,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Return updated segments
|
|
632
|
+
setRequestContextParams(matchResult.params);
|
|
633
|
+
|
|
634
|
+
const renderStart = performance.now();
|
|
635
|
+
renderSegments(matchResult.segments, { rootLayout: router.rootLayout, isAction: true });
|
|
636
|
+
const renderDuration = performance.now() - renderStart;
|
|
637
|
+
const serverTiming = matchResult.serverTiming
|
|
638
|
+
? `${matchResult.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
|
|
639
|
+
: `rendering;dur=${renderDuration.toFixed(2)}`;
|
|
640
|
+
|
|
641
|
+
const payload: RscPayload = {
|
|
642
|
+
root: null,
|
|
643
|
+
metadata: {
|
|
644
|
+
pathname: url.pathname,
|
|
645
|
+
segments: matchResult.segments,
|
|
646
|
+
isPartial: true,
|
|
647
|
+
matched: matchResult.matched,
|
|
648
|
+
diff: matchResult.diff,
|
|
649
|
+
slots: matchResult.slots,
|
|
650
|
+
handles: handleStore.stream(),
|
|
651
|
+
version,
|
|
652
|
+
},
|
|
653
|
+
returnValue,
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const rscStream = renderToReadableStream<RscPayload>(payload, {
|
|
657
|
+
temporaryReferences,
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
const actionHeaders: Record<string, string> = {
|
|
661
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
662
|
+
};
|
|
663
|
+
if (serverTiming) {
|
|
664
|
+
actionHeaders["Server-Timing"] = serverTiming;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
668
|
+
status: actionStatus,
|
|
669
|
+
headers: actionHeaders,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// LOADER FETCH HANDLER
|
|
675
|
+
// Supports GET (params in query string) and POST/PUT/PATCH/DELETE (JSON body)
|
|
676
|
+
// ============================================================================
|
|
677
|
+
async function handleLoaderFetch(
|
|
678
|
+
request: Request,
|
|
679
|
+
env: TEnv,
|
|
680
|
+
url: URL,
|
|
681
|
+
variables: Record<string, any>
|
|
682
|
+
): Promise<Response> {
|
|
683
|
+
const loaderId = url.searchParams.get("_rsc_loader");
|
|
684
|
+
|
|
685
|
+
if (!loaderId) {
|
|
686
|
+
return createResponseWithMergedHeaders("Missing _rsc_loader parameter", {
|
|
687
|
+
status: 400,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Look up loader lazily
|
|
692
|
+
const registeredLoader = await getLoaderLazy(loaderId);
|
|
693
|
+
if (!registeredLoader) {
|
|
694
|
+
return createResponseWithMergedHeaders(
|
|
695
|
+
`Loader "${loaderId}" not found in registry`,
|
|
696
|
+
{ status: 404 }
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Parse params and body based on request method
|
|
701
|
+
let loaderParams: Record<string, string> = {};
|
|
702
|
+
let loaderBody: unknown = undefined;
|
|
703
|
+
const isBodyMethod = request.method !== "GET" && request.method !== "HEAD";
|
|
704
|
+
|
|
705
|
+
if (isBodyMethod) {
|
|
706
|
+
try {
|
|
707
|
+
const contentType = request.headers.get("content-type") || "";
|
|
708
|
+
if (contentType.includes("application/json")) {
|
|
709
|
+
const jsonBody = (await request.json()) as {
|
|
710
|
+
params?: Record<string, string>;
|
|
711
|
+
body?: unknown;
|
|
712
|
+
};
|
|
713
|
+
loaderParams = jsonBody.params ?? {};
|
|
714
|
+
loaderBody = jsonBody.body;
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
return createResponseWithMergedHeaders("Invalid JSON body", {
|
|
718
|
+
status: 400,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
const loaderParamsJson = url.searchParams.get("_rsc_loader_params");
|
|
723
|
+
if (loaderParamsJson) {
|
|
724
|
+
try {
|
|
725
|
+
loaderParams = JSON.parse(loaderParamsJson);
|
|
726
|
+
} catch {
|
|
727
|
+
return createResponseWithMergedHeaders(
|
|
728
|
+
"Invalid _rsc_loader_params JSON",
|
|
729
|
+
{ status: 400 }
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Execute the loader with middleware
|
|
736
|
+
try {
|
|
737
|
+
const { fn, middleware } = registeredLoader;
|
|
738
|
+
|
|
739
|
+
return await executeLoaderMiddleware(
|
|
740
|
+
middleware,
|
|
741
|
+
request,
|
|
742
|
+
env,
|
|
743
|
+
loaderParams,
|
|
744
|
+
variables,
|
|
745
|
+
async () => {
|
|
746
|
+
const ctx = requireRequestContext();
|
|
747
|
+
const loaderCtx: any = {
|
|
748
|
+
...ctx,
|
|
749
|
+
params: loaderParams,
|
|
750
|
+
body: loaderBody,
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
const result = await fn(loaderCtx);
|
|
754
|
+
|
|
755
|
+
interface LoaderPayload {
|
|
756
|
+
loaderResult: unknown;
|
|
757
|
+
}
|
|
758
|
+
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
759
|
+
const rscStream =
|
|
760
|
+
renderToReadableStream<LoaderPayload>(loaderPayload);
|
|
761
|
+
|
|
762
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
763
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
769
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
770
|
+
|
|
771
|
+
console.error("[RSC] Loader error:", error);
|
|
772
|
+
|
|
773
|
+
callOnError(error, "loader", {
|
|
774
|
+
request,
|
|
775
|
+
url,
|
|
776
|
+
env,
|
|
777
|
+
loaderName: loaderId,
|
|
778
|
+
handledByBoundary: false,
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const errorPayload = {
|
|
782
|
+
loaderResult: null,
|
|
783
|
+
loaderError: {
|
|
784
|
+
message: isDev ? err.message : "An error occurred",
|
|
785
|
+
name: err.name,
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
const rscStream = renderToReadableStream(errorPayload);
|
|
789
|
+
|
|
790
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
791
|
+
status: 500,
|
|
792
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ============================================================================
|
|
798
|
+
// RSC RENDERING HANDLER (Navigation)
|
|
799
|
+
// ============================================================================
|
|
800
|
+
async function handleRscRendering(
|
|
801
|
+
request: Request,
|
|
802
|
+
env: TEnv,
|
|
803
|
+
url: URL,
|
|
804
|
+
isPartial: boolean,
|
|
805
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
806
|
+
nonce: string | undefined
|
|
807
|
+
): Promise<Response> {
|
|
808
|
+
let payload: RscPayload;
|
|
809
|
+
let serverTiming: string | undefined;
|
|
810
|
+
|
|
811
|
+
if (isPartial) {
|
|
812
|
+
// Partial render (navigation)
|
|
813
|
+
const result = await router.matchPartial(request, env);
|
|
814
|
+
|
|
815
|
+
if (!result) {
|
|
816
|
+
// Fall back to full render
|
|
817
|
+
const match = await router.match(request, env);
|
|
818
|
+
setRequestContextParams(match.params);
|
|
819
|
+
|
|
820
|
+
if (match.redirect) {
|
|
821
|
+
return createResponseWithMergedHeaders(null, {
|
|
822
|
+
status: 308,
|
|
823
|
+
headers: { Location: match.redirect },
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const renderStart = performance.now();
|
|
828
|
+
const root = renderSegments(match.segments, {
|
|
829
|
+
rootLayout: router.rootLayout,
|
|
830
|
+
});
|
|
831
|
+
const renderDuration = performance.now() - renderStart;
|
|
832
|
+
serverTiming = match.serverTiming
|
|
833
|
+
? `${match.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
|
|
834
|
+
: `rendering;dur=${renderDuration.toFixed(2)}`;
|
|
835
|
+
|
|
836
|
+
payload = {
|
|
837
|
+
root,
|
|
838
|
+
metadata: {
|
|
839
|
+
pathname: url.pathname,
|
|
840
|
+
segments: match.segments,
|
|
841
|
+
matched: match.matched,
|
|
842
|
+
diff: match.diff,
|
|
843
|
+
isPartial: false,
|
|
844
|
+
handles: handleStore.stream(),
|
|
845
|
+
version,
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
} else {
|
|
849
|
+
setRequestContextParams(result.params);
|
|
850
|
+
serverTiming = result.serverTiming;
|
|
851
|
+
|
|
852
|
+
payload = {
|
|
853
|
+
root: null,
|
|
854
|
+
metadata: {
|
|
855
|
+
pathname: url.pathname,
|
|
856
|
+
segments: result.segments,
|
|
857
|
+
matched: result.matched,
|
|
858
|
+
diff: result.diff,
|
|
859
|
+
isPartial: true,
|
|
860
|
+
slots: result.slots,
|
|
861
|
+
handles: handleStore.stream(),
|
|
862
|
+
version,
|
|
863
|
+
},
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
} else {
|
|
867
|
+
// Full render (initial page load)
|
|
868
|
+
const match = await router.match(request, env);
|
|
869
|
+
setRequestContextParams(match.params);
|
|
870
|
+
|
|
871
|
+
if (match.redirect) {
|
|
872
|
+
return createResponseWithMergedHeaders(null, {
|
|
873
|
+
status: 308,
|
|
874
|
+
headers: { Location: match.redirect },
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Caching is now handled in router.match() via cache provider in request context
|
|
879
|
+
// match.segments already contains cached or fresh segments as appropriate
|
|
880
|
+
|
|
881
|
+
const renderStart = performance.now();
|
|
882
|
+
const root = renderSegments(match.segments, {
|
|
883
|
+
rootLayout: router.rootLayout,
|
|
884
|
+
});
|
|
885
|
+
const renderDuration = performance.now() - renderStart;
|
|
886
|
+
serverTiming = match.serverTiming
|
|
887
|
+
? `${match.serverTiming}, rendering;dur=${renderDuration.toFixed(2)}`
|
|
888
|
+
: `rendering;dur=${renderDuration.toFixed(2)}`;
|
|
889
|
+
|
|
890
|
+
payload = {
|
|
891
|
+
root,
|
|
892
|
+
metadata: {
|
|
893
|
+
pathname: url.pathname,
|
|
894
|
+
segments: match.segments,
|
|
895
|
+
matched: match.matched,
|
|
896
|
+
diff: match.diff,
|
|
897
|
+
isPartial: false,
|
|
898
|
+
rootLayout: router.rootLayout,
|
|
899
|
+
handles: handleStore.stream(),
|
|
900
|
+
version,
|
|
901
|
+
},
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Serialize to RSC stream
|
|
906
|
+
const rscStream = renderToReadableStream<RscPayload>(payload);
|
|
907
|
+
|
|
908
|
+
// Determine if this is an RSC request or HTML request
|
|
909
|
+
const isRscRequest =
|
|
910
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
911
|
+
!url.searchParams.has("__html")) ||
|
|
912
|
+
url.searchParams.has("__rsc");
|
|
913
|
+
|
|
914
|
+
if (isRscRequest) {
|
|
915
|
+
const rscHeaders: Record<string, string> = {
|
|
916
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
917
|
+
vary: "accept",
|
|
918
|
+
};
|
|
919
|
+
if (serverTiming) {
|
|
920
|
+
rscHeaders["Server-Timing"] = serverTiming;
|
|
921
|
+
}
|
|
922
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
923
|
+
headers: rscHeaders,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Delegate to SSR for HTML response
|
|
928
|
+
const ssrModule = await loadSSRModule();
|
|
929
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, { nonce });
|
|
930
|
+
|
|
931
|
+
const htmlHeaders: Record<string, string> = {
|
|
932
|
+
"content-type": "text/html;charset=utf-8",
|
|
933
|
+
};
|
|
934
|
+
if (serverTiming) {
|
|
935
|
+
htmlHeaders["Server-Timing"] = serverTiming;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return createResponseWithMergedHeaders(htmlStream, {
|
|
939
|
+
headers: htmlHeaders,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|