@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.20dbba0c
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/AGENTS.md +4 -0
- package/README.md +172 -50
- package/dist/bin/rango.js +138 -50
- package/dist/vite/index.js +1160 -508
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +17 -16
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +61 -51
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +20 -0
- package/skills/layout/SKILL.md +22 -0
- package/skills/links/SKILL.md +91 -17
- package/skills/loader/SKILL.md +107 -24
- package/skills/middleware/SKILL.md +34 -3
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/parallel/SKILL.md +185 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +24 -23
- package/skills/response-routes/SKILL.md +8 -0
- package/skills/route/SKILL.md +58 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/typesafety/SKILL.md +38 -24
- package/src/__internal.ts +92 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +175 -17
- package/src/browser/navigation-client.ts +177 -44
- package/src/browser/navigation-store.ts +68 -9
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +113 -17
- package/src/browser/prefetch/cache.ts +275 -28
- package/src/browser/prefetch/fetch.ts +191 -46
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +123 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +53 -13
- package/src/browser/react/Link.tsx +98 -14
- package/src/browser/react/NavigationProvider.tsx +89 -14
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-navigation.ts +22 -2
- package/src/browser/react/use-params.ts +11 -1
- package/src/browser/react/use-router.ts +29 -9
- package/src/browser/rsc-router.tsx +177 -66
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/server-action-bridge.ts +8 -6
- package/src/browser/types.ts +73 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-trie.ts +67 -25
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +223 -74
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +455 -15
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +85 -276
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +9 -36
- package/src/index.ts +79 -70
- package/src/outlet-context.ts +1 -1
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +27 -2
- package/src/route-definition/dsl-helpers.ts +240 -40
- package/src/route-definition/helpers-types.ts +67 -19
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +18 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +129 -26
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +10 -7
- package/src/router/loader-resolution.ts +160 -22
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +31 -16
- package/src/router/match-api.ts +128 -193
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +94 -17
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +103 -18
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +48 -27
- package/src/router/middleware.ts +201 -86
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +77 -11
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +215 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +454 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +30 -6
- package/src/router/types.ts +1 -0
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +89 -17
- package/src/rsc/handler.ts +563 -364
- package/src/rsc/helpers.ts +69 -41
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +37 -10
- package/src/rsc/response-route-handler.ts +14 -1
- package/src/rsc/rsc-rendering.ts +47 -44
- package/src/rsc/server-action.ts +24 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +11 -1
- package/src/search-params.ts +16 -13
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +109 -23
- package/src/server/context.ts +174 -19
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +218 -65
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +140 -72
- package/src/types/loader-types.ts +41 -15
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +19 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/include-helper.ts +24 -14
- package/src/urls/path-helper-types.ts +39 -6
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +18 -16
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +7 -4
- package/src/vite/discovery/prerender-collection.ts +162 -88
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +190 -217
- package/src/vite/router-discovery.ts +241 -45
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/package-resolution.ts +34 -1
- package/src/vite/utils/prerender-utils.ts +97 -5
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
package/src/rsc/handler.ts
CHANGED
|
@@ -14,17 +14,24 @@ import {
|
|
|
14
14
|
runWithRequestContext,
|
|
15
15
|
setRequestContextParams,
|
|
16
16
|
requireRequestContext,
|
|
17
|
+
getRequestContext,
|
|
18
|
+
_getRequestContext,
|
|
17
19
|
createRequestContext,
|
|
18
20
|
} from "../server/request-context.js";
|
|
19
21
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
import type {
|
|
23
|
+
RscPayload,
|
|
24
|
+
CreateRSCHandlerOptions,
|
|
25
|
+
LoadSSRModule,
|
|
26
|
+
SSRModule,
|
|
27
|
+
} from "./types.js";
|
|
22
28
|
import {
|
|
23
29
|
createResponseWithMergedHeaders,
|
|
24
30
|
finalizeResponse,
|
|
25
31
|
interceptRedirectForPartial,
|
|
26
32
|
buildRouteMiddlewareEntries,
|
|
27
33
|
} from "./helpers.js";
|
|
34
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
28
35
|
import {
|
|
29
36
|
handleResponseRoute,
|
|
30
37
|
type ResponseRouteMatch,
|
|
@@ -50,6 +57,7 @@ import {
|
|
|
50
57
|
getRouterTrie,
|
|
51
58
|
} from "../route-map-builder.js";
|
|
52
59
|
import type { HandlerContext } from "./handler-context.js";
|
|
60
|
+
import type { SegmentCacheStore } from "../cache/types.js";
|
|
53
61
|
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
54
62
|
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
55
63
|
import {
|
|
@@ -66,6 +74,22 @@ import {
|
|
|
66
74
|
createDefaultTimeoutResponse,
|
|
67
75
|
type TimeoutPhase,
|
|
68
76
|
} from "../router/timeout.js";
|
|
77
|
+
import {
|
|
78
|
+
createMetricsStore,
|
|
79
|
+
appendMetric,
|
|
80
|
+
buildMetricsTiming,
|
|
81
|
+
} from "../router/metrics.js";
|
|
82
|
+
import {
|
|
83
|
+
startSSRSetup,
|
|
84
|
+
getSSRSetup,
|
|
85
|
+
mayNeedSSR,
|
|
86
|
+
SSR_SETUP_VAR,
|
|
87
|
+
} from "./ssr-setup.js";
|
|
88
|
+
import {
|
|
89
|
+
classifyRequest,
|
|
90
|
+
type RequestPlan,
|
|
91
|
+
type ExecutableRequestPlan,
|
|
92
|
+
} from "../router/request-classification.js";
|
|
69
93
|
|
|
70
94
|
/**
|
|
71
95
|
* Create an RSC request handler.
|
|
@@ -117,10 +141,22 @@ export function createRSCHandler<
|
|
|
117
141
|
decodeFormState,
|
|
118
142
|
} = deps;
|
|
119
143
|
|
|
120
|
-
// Use provided loadSSRModule or default to vite RSC module loader
|
|
121
|
-
|
|
144
|
+
// Use provided loadSSRModule or default to vite RSC module loader.
|
|
145
|
+
// In production the SSR module is stable across requests, so memoize
|
|
146
|
+
// the dynamic import to avoid repeated module resolution overhead.
|
|
147
|
+
// In dev mode Vite may hot-reload the module, so skip memoization.
|
|
148
|
+
const rawLoadSSRModule: LoadSSRModule =
|
|
122
149
|
options.loadSSRModule ??
|
|
123
150
|
(() => import.meta.viteRsc.loadModule("ssr", "index"));
|
|
151
|
+
let _ssrModulePromise: Promise<SSRModule> | undefined;
|
|
152
|
+
const loadSSRModule: LoadSSRModule =
|
|
153
|
+
process.env.NODE_ENV === "production"
|
|
154
|
+
? () =>
|
|
155
|
+
(_ssrModulePromise ??= rawLoadSSRModule().catch((err) => {
|
|
156
|
+
_ssrModulePromise = undefined;
|
|
157
|
+
throw err;
|
|
158
|
+
}))
|
|
159
|
+
: rawLoadSSRModule;
|
|
124
160
|
|
|
125
161
|
/**
|
|
126
162
|
* Per-request error reporter that deduplicates via the ALS request context.
|
|
@@ -133,10 +169,13 @@ export function createRSCHandler<
|
|
|
133
169
|
phase: ErrorPhase,
|
|
134
170
|
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
135
171
|
): void {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
172
|
+
// Guard: abort signal handlers fire asynchronously outside the ALS
|
|
173
|
+
// request scope, so the context may be gone. Skip dedup in that
|
|
174
|
+
// case — the error is from a cancelled stream, not a real failure.
|
|
175
|
+
const reqCtx = _getRequestContext();
|
|
176
|
+
if (error != null && typeof error === "object" && reqCtx) {
|
|
177
|
+
if (reqCtx._reportedErrors.has(error)) return;
|
|
178
|
+
reqCtx._reportedErrors.add(error);
|
|
140
179
|
}
|
|
141
180
|
invokeOnError(router.onError, error, phase, context, "RSC");
|
|
142
181
|
}
|
|
@@ -268,6 +307,11 @@ export function createRSCHandler<
|
|
|
268
307
|
input: RouterRequestInput<TEnv> = {},
|
|
269
308
|
): Promise<Response> {
|
|
270
309
|
const handlerStart = performance.now();
|
|
310
|
+
// Create the metrics store at handler start so handler:total has startTime=0
|
|
311
|
+
// and all metrics are relative to the request entry point.
|
|
312
|
+
const earlyMetricsStore = router.debugPerformance
|
|
313
|
+
? createMetricsStore(true, handlerStart)
|
|
314
|
+
: undefined;
|
|
271
315
|
|
|
272
316
|
const { env = {} as TEnv, vars: initialVars, ctx: executionCtx } = input;
|
|
273
317
|
|
|
@@ -310,7 +354,7 @@ export function createRSCHandler<
|
|
|
310
354
|
// Resolve cache store configuration
|
|
311
355
|
// Priority: options.cache (handler override) > router.cache (router default)
|
|
312
356
|
// Store is enabled only if: config provided, enabled, and no ?__no_cache query param
|
|
313
|
-
let cacheStore
|
|
357
|
+
let cacheStore: SegmentCacheStore | undefined;
|
|
314
358
|
const cacheOption = options.cache ?? router.cache;
|
|
315
359
|
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
316
360
|
const cacheConfig =
|
|
@@ -381,6 +425,10 @@ export function createRSCHandler<
|
|
|
381
425
|
executionContext: executionCtx,
|
|
382
426
|
themeConfig: router.themeConfig,
|
|
383
427
|
});
|
|
428
|
+
if (earlyMetricsStore) {
|
|
429
|
+
requestContext._debugPerformance = true;
|
|
430
|
+
requestContext._metricsStore = earlyMetricsStore;
|
|
431
|
+
}
|
|
384
432
|
// Wire background error reporting so "use cache" and other subsystems
|
|
385
433
|
// can surface non-fatal errors through the router's onError callback.
|
|
386
434
|
requestContext._reportBackgroundError = (
|
|
@@ -415,6 +463,9 @@ export function createRSCHandler<
|
|
|
415
463
|
// - Server components during rendering
|
|
416
464
|
// - Error boundaries
|
|
417
465
|
// - Streaming
|
|
466
|
+
// Store basename on request context (scoped per-request via existing ALS)
|
|
467
|
+
requestContext._basename = router.basename;
|
|
468
|
+
|
|
418
469
|
return runWithRequestContext(requestContext, async () => {
|
|
419
470
|
// Core handler logic (wrapped by middleware)
|
|
420
471
|
const coreHandler = async (): Promise<Response> => {
|
|
@@ -422,6 +473,7 @@ export function createRSCHandler<
|
|
|
422
473
|
};
|
|
423
474
|
|
|
424
475
|
// Execute middleware chain if any, otherwise call core handler directly
|
|
476
|
+
let response: Response;
|
|
425
477
|
if (matchedMiddleware.length > 0) {
|
|
426
478
|
const mwResponse = await executeMiddleware(
|
|
427
479
|
matchedMiddleware,
|
|
@@ -440,17 +492,60 @@ export function createRSCHandler<
|
|
|
440
492
|
mwResponse,
|
|
441
493
|
createRedirectFlightResponse,
|
|
442
494
|
);
|
|
443
|
-
|
|
495
|
+
response = intercepted ?? finalizeResponse(mwResponse);
|
|
496
|
+
} else {
|
|
497
|
+
response = finalizeResponse(mwResponse);
|
|
444
498
|
}
|
|
499
|
+
} else {
|
|
500
|
+
response = await coreHandler();
|
|
501
|
+
}
|
|
445
502
|
|
|
446
|
-
|
|
503
|
+
// Finalize metrics after all middleware (including post-next work)
|
|
504
|
+
// has completed so :post spans are captured in the timeline.
|
|
505
|
+
// Handler timing parts are always emitted (even without debug metrics)
|
|
506
|
+
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
507
|
+
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
508
|
+
// Preserve any existing Server-Timing set by response routes or middleware
|
|
509
|
+
const existingTiming = response.headers.get("Server-Timing");
|
|
510
|
+
const timingParts = existingTiming
|
|
511
|
+
? [existingTiming, ...handlerTimingArr]
|
|
512
|
+
: [...handlerTimingArr];
|
|
513
|
+
|
|
514
|
+
const metricsStore = requestContext._metricsStore;
|
|
515
|
+
if (metricsStore) {
|
|
516
|
+
// When the store was created at handler start (earlyMetricsStore),
|
|
517
|
+
// handler:total covers the full request. When ctx.debugPerformance()
|
|
518
|
+
// created the store mid-request, use its requestStart to avoid a
|
|
519
|
+
// negative startTime offset.
|
|
520
|
+
const totalStart = earlyMetricsStore
|
|
521
|
+
? handlerStart
|
|
522
|
+
: metricsStore.requestStart;
|
|
523
|
+
appendMetric(
|
|
524
|
+
metricsStore,
|
|
525
|
+
"handler:total",
|
|
526
|
+
totalStart,
|
|
527
|
+
performance.now() - totalStart,
|
|
528
|
+
);
|
|
529
|
+
const metricsTiming = buildMetricsTiming(
|
|
530
|
+
request.method,
|
|
531
|
+
url.pathname,
|
|
532
|
+
metricsStore,
|
|
533
|
+
);
|
|
534
|
+
if (metricsTiming) timingParts.push(metricsTiming);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const fullTiming = timingParts.join(", ");
|
|
538
|
+
if (fullTiming && !isWebSocketUpgradeResponse(response)) {
|
|
539
|
+
response.headers.set("Server-Timing", fullTiming);
|
|
447
540
|
}
|
|
448
541
|
|
|
449
|
-
return
|
|
542
|
+
return response;
|
|
450
543
|
});
|
|
451
544
|
};
|
|
452
545
|
|
|
453
|
-
// Core request handling logic (separated for middleware wrapping)
|
|
546
|
+
// Core request handling logic (separated for middleware wrapping).
|
|
547
|
+
// Uses the classify → execute model: classifyRequest produces a RequestPlan,
|
|
548
|
+
// then execution dispatches on the plan mode.
|
|
454
549
|
async function coreRequestHandler(
|
|
455
550
|
request: Request,
|
|
456
551
|
env: TEnv,
|
|
@@ -458,56 +553,112 @@ export function createRSCHandler<
|
|
|
458
553
|
variables: Record<string, any>,
|
|
459
554
|
nonce: string | undefined,
|
|
460
555
|
): Promise<Response> {
|
|
461
|
-
const previewStart = performance.now();
|
|
462
|
-
const preview = await router.previewMatch(request, { env });
|
|
463
|
-
const previewDur = performance.now() - previewStart;
|
|
464
556
|
const handlerTiming: string[] = variables.__handlerTiming || [];
|
|
465
|
-
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
557
|
+
|
|
558
|
+
// Debug manifest endpoint: handled before classification since it
|
|
559
|
+
// doesn't need a route match and needs trie access from the closure.
|
|
560
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
561
|
+
if (
|
|
562
|
+
url.searchParams.has("__debug_manifest") &&
|
|
563
|
+
(isDev || router.allowDebugManifest)
|
|
564
|
+
) {
|
|
565
|
+
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
566
|
+
const routeManifest = getRequiredRouteMap();
|
|
567
|
+
const { extractAncestryFromTrie } =
|
|
568
|
+
await import("../build/route-trie.js");
|
|
569
|
+
return new Response(
|
|
570
|
+
JSON.stringify(
|
|
571
|
+
{
|
|
572
|
+
routerId: router.id,
|
|
573
|
+
routeManifest,
|
|
574
|
+
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
575
|
+
routeTrie: trie,
|
|
576
|
+
precomputedEntries: getPrecomputedEntries(),
|
|
577
|
+
},
|
|
578
|
+
null,
|
|
579
|
+
2,
|
|
476
580
|
),
|
|
477
|
-
|
|
478
|
-
|
|
581
|
+
{
|
|
582
|
+
headers: { "Content-Type": "application/json" },
|
|
583
|
+
},
|
|
479
584
|
);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ---- 1. Classify ----
|
|
588
|
+
// classifyRequest may throw RouteNotFoundError for unknown routes.
|
|
589
|
+
// In that case, fall through to a full-render plan so the pipeline
|
|
590
|
+
// can render the 404 page via the existing error handling path.
|
|
591
|
+
const classifyStart = performance.now();
|
|
592
|
+
let plan: RequestPlan<TEnv>;
|
|
593
|
+
try {
|
|
594
|
+
plan = await classifyRequest<TEnv>(request, url, {
|
|
595
|
+
findMatch: router.findMatch,
|
|
596
|
+
routerVersion: version,
|
|
597
|
+
routerId: router.id,
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (
|
|
601
|
+
error instanceof RouteNotFoundError ||
|
|
602
|
+
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
603
|
+
) {
|
|
604
|
+
// Let the render path handle 404 — match()/matchPartial() will
|
|
605
|
+
// re-throw RouteNotFoundError and the catch block in
|
|
606
|
+
// executeRenderWithMiddleware renders the not-found page.
|
|
607
|
+
plan = {
|
|
608
|
+
mode: "full-render",
|
|
609
|
+
route: {
|
|
610
|
+
matched: null as any,
|
|
611
|
+
manifestEntry: null as any,
|
|
612
|
+
entries: [],
|
|
613
|
+
routeKey: "",
|
|
614
|
+
localRouteName: "",
|
|
615
|
+
params: {},
|
|
616
|
+
routeMiddleware: [],
|
|
617
|
+
cacheScope: null,
|
|
618
|
+
isPassthrough: false,
|
|
619
|
+
},
|
|
620
|
+
negotiated: false,
|
|
621
|
+
};
|
|
622
|
+
} else {
|
|
623
|
+
throw error;
|
|
489
624
|
}
|
|
490
|
-
|
|
625
|
+
}
|
|
626
|
+
const classifyDur = performance.now() - classifyStart;
|
|
627
|
+
handlerTiming.push(`handler-classify;dur=${classifyDur.toFixed(2)}`);
|
|
628
|
+
|
|
629
|
+
// ---- 2. Terminal plans (no execution needed) ----
|
|
630
|
+
if (plan.mode === "redirect") {
|
|
631
|
+
// Redirects are handled by the pipeline (match/matchPartial),
|
|
632
|
+
// but for partial requests we short-circuit with a Flight redirect.
|
|
633
|
+
if (url.searchParams.has("_rsc_partial")) {
|
|
634
|
+
return createRedirectFlightResponse(plan.redirectUrl);
|
|
635
|
+
}
|
|
636
|
+
// Full requests: let the pipeline handle the redirect via match()
|
|
637
|
+
// which returns { redirect: url }. Fall through to full-render.
|
|
491
638
|
}
|
|
492
639
|
|
|
493
|
-
|
|
640
|
+
if (plan.mode === "version-mismatch") {
|
|
641
|
+
console.log(
|
|
642
|
+
`[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
|
|
643
|
+
);
|
|
644
|
+
return createResponseWithMergedHeaders(null, {
|
|
645
|
+
status: 200,
|
|
646
|
+
headers: {
|
|
647
|
+
"X-RSC-Reload": plan.reloadUrl,
|
|
648
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
}
|
|
494
652
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const originPhase: OriginCheckPhase | null = isAction
|
|
505
|
-
? "action"
|
|
506
|
-
: isLoaderFetch
|
|
507
|
-
? "loader"
|
|
508
|
-
: request.method === "POST"
|
|
509
|
-
? "pe-form"
|
|
510
|
-
: null;
|
|
653
|
+
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
654
|
+
const originPhase: OriginCheckPhase | null =
|
|
655
|
+
plan.mode === "action"
|
|
656
|
+
? "action"
|
|
657
|
+
: plan.mode === "loader"
|
|
658
|
+
? "loader"
|
|
659
|
+
: plan.mode === "pe-render"
|
|
660
|
+
? "pe-form"
|
|
661
|
+
: null;
|
|
511
662
|
if (originPhase) {
|
|
512
663
|
const originResult = await checkRequestOrigin(
|
|
513
664
|
request,
|
|
@@ -557,13 +708,33 @@ export function createRSCHandler<
|
|
|
557
708
|
}
|
|
558
709
|
}
|
|
559
710
|
|
|
560
|
-
//
|
|
711
|
+
// ---- 4. Execute ----
|
|
712
|
+
return executeRequest(
|
|
713
|
+
plan as ExecutableRequestPlan<TEnv>,
|
|
714
|
+
request,
|
|
715
|
+
env,
|
|
716
|
+
url,
|
|
717
|
+
variables,
|
|
718
|
+
nonce,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Execute a classified request plan. Dispatches to the appropriate handler
|
|
723
|
+
// based on plan.mode. Lives in the createRSCHandler closure for access to
|
|
724
|
+
// handlerCtx, router, callOnError, etc.
|
|
725
|
+
// Only receives executable plans (version-mismatch is handled above).
|
|
726
|
+
async function executeRequest(
|
|
727
|
+
plan: ExecutableRequestPlan<TEnv>,
|
|
728
|
+
request: Request,
|
|
729
|
+
env: TEnv,
|
|
730
|
+
url: URL,
|
|
731
|
+
variables: Record<string, any>,
|
|
732
|
+
nonce: string | undefined,
|
|
733
|
+
): Promise<Response> {
|
|
734
|
+
// Common setup
|
|
561
735
|
const handleStore = requireRequestContext()._handleStore;
|
|
562
736
|
|
|
563
737
|
// Wire up error reporting for late streaming-handle failures
|
|
564
|
-
// (LateHandlePushError: handle pushed after stream completion).
|
|
565
|
-
// Without this, these errors are only caught by React's error boundary
|
|
566
|
-
// and never reach the router's onError callback or telemetry.
|
|
567
738
|
handleStore.onError = (error: Error) => {
|
|
568
739
|
const reqCtx = requireRequestContext();
|
|
569
740
|
callOnError(error, "handler", {
|
|
@@ -593,37 +764,106 @@ export function createRSCHandler<
|
|
|
593
764
|
};
|
|
594
765
|
|
|
595
766
|
// Set route params early so all execution paths can access ctx.params.
|
|
596
|
-
|
|
597
|
-
|
|
767
|
+
// Also store the classified snapshot so match/matchPartial can reuse it
|
|
768
|
+
// instead of calling resolveRoute again.
|
|
769
|
+
if (plan.mode !== "redirect") {
|
|
770
|
+
setRequestContextParams(plan.route.params, plan.route.routeKey);
|
|
771
|
+
requireRequestContext()._classifiedRoute = plan.route;
|
|
598
772
|
}
|
|
599
773
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
routeMiddleware:
|
|
774
|
+
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
775
|
+
|
|
776
|
+
// ---- Response route: skip entire RSC pipeline ----
|
|
777
|
+
if (plan.mode === "response") {
|
|
778
|
+
// Build ResponseRouteMatch from plan fields. handleResponseRoute
|
|
779
|
+
// expects a flat object with params at the top level.
|
|
780
|
+
const responseMatch: ResponseRouteMatch = {
|
|
781
|
+
responseType: plan.responseType,
|
|
782
|
+
handler: plan.handler,
|
|
783
|
+
params: plan.route.params,
|
|
784
|
+
negotiated: plan.negotiated,
|
|
785
|
+
manifestEntry: plan.manifestEntry,
|
|
786
|
+
routeMiddleware: plan.routeMiddleware,
|
|
787
|
+
};
|
|
788
|
+
const responseOutcome = await withTimeout(
|
|
789
|
+
handleResponseRoute(
|
|
790
|
+
handlerCtx,
|
|
791
|
+
responseMatch,
|
|
792
|
+
request,
|
|
793
|
+
env,
|
|
794
|
+
url,
|
|
795
|
+
variables,
|
|
796
|
+
),
|
|
797
|
+
router.timeouts.renderStartMs,
|
|
798
|
+
"render-start",
|
|
799
|
+
);
|
|
800
|
+
if (responseOutcome.timedOut) {
|
|
801
|
+
return handleTimeoutResponse(
|
|
802
|
+
request,
|
|
803
|
+
env,
|
|
804
|
+
url,
|
|
805
|
+
"render-start",
|
|
806
|
+
responseOutcome.durationMs,
|
|
807
|
+
plan.route.routeKey,
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const response = responseOutcome.result;
|
|
811
|
+
if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
812
|
+
response.headers.append("Vary", "Accept");
|
|
813
|
+
}
|
|
814
|
+
return response;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// SSR setup: kick off in parallel for modes that need HTML rendering.
|
|
818
|
+
// Placed after response-route short-circuit so response/mime routes
|
|
819
|
+
// never pay for SSR work.
|
|
820
|
+
if (plan.mode !== "loader" && mayNeedSSR(request, url)) {
|
|
821
|
+
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
822
|
+
handlerCtx,
|
|
823
|
+
request,
|
|
824
|
+
env,
|
|
825
|
+
url,
|
|
826
|
+
router.debugPerformance
|
|
827
|
+
? () => requireRequestContext()._metricsStore
|
|
828
|
+
: undefined,
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// ---- Loader fetch ----
|
|
833
|
+
if (plan.mode === "loader") {
|
|
834
|
+
return handleLoaderFetch(
|
|
835
|
+
handlerCtx,
|
|
836
|
+
request,
|
|
837
|
+
env,
|
|
838
|
+
url,
|
|
613
839
|
variables,
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
840
|
+
plan.route.params,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ---- Progressive enhancement ----
|
|
845
|
+
if (plan.mode === "pe-render") {
|
|
846
|
+
const peResult = await handleProgressiveEnhancement(
|
|
847
|
+
handlerCtx,
|
|
848
|
+
request,
|
|
849
|
+
env,
|
|
850
|
+
url,
|
|
851
|
+
false, // isAction = false for PE
|
|
852
|
+
handleStore,
|
|
853
|
+
nonce,
|
|
854
|
+
{
|
|
855
|
+
routeMiddleware: plan.route.routeMiddleware,
|
|
856
|
+
variables,
|
|
857
|
+
routeReverse,
|
|
858
|
+
},
|
|
859
|
+
);
|
|
860
|
+
if (peResult) return peResult;
|
|
861
|
+
// PE handler returned null (not a PE form) — fall through to render
|
|
619
862
|
}
|
|
620
863
|
|
|
621
|
-
//
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
// the revalidation pass (identical to a normal render).
|
|
625
|
-
let actionContinuation: ActionContinuation | undefined;
|
|
626
|
-
if (isAction && actionId) {
|
|
864
|
+
// ---- Action: execute action, then revalidate wrapped in route middleware ----
|
|
865
|
+
if (plan.mode === "action") {
|
|
866
|
+
let actionContinuation: ActionContinuation | undefined;
|
|
627
867
|
try {
|
|
628
868
|
const actionOutcome = await withTimeout(
|
|
629
869
|
executeServerAction(
|
|
@@ -631,7 +871,7 @@ export function createRSCHandler<
|
|
|
631
871
|
request,
|
|
632
872
|
env,
|
|
633
873
|
url,
|
|
634
|
-
actionId,
|
|
874
|
+
plan.actionId,
|
|
635
875
|
handleStore,
|
|
636
876
|
),
|
|
637
877
|
router.timeouts.actionMs,
|
|
@@ -644,8 +884,8 @@ export function createRSCHandler<
|
|
|
644
884
|
url,
|
|
645
885
|
"action",
|
|
646
886
|
actionOutcome.durationMs,
|
|
647
|
-
|
|
648
|
-
actionId,
|
|
887
|
+
plan.route.routeKey,
|
|
888
|
+
plan.actionId,
|
|
649
889
|
);
|
|
650
890
|
}
|
|
651
891
|
const result = actionOutcome.result;
|
|
@@ -657,338 +897,297 @@ export function createRSCHandler<
|
|
|
657
897
|
request,
|
|
658
898
|
url,
|
|
659
899
|
env,
|
|
660
|
-
actionId,
|
|
900
|
+
actionId: plan.actionId,
|
|
661
901
|
handledByBoundary: false,
|
|
662
902
|
});
|
|
663
903
|
console.error(`[RSC] Action error:`, error);
|
|
664
904
|
throw error;
|
|
665
905
|
}
|
|
666
|
-
}
|
|
667
906
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
907
|
+
// Revalidation render wrapped in route middleware.
|
|
908
|
+
// Actions from client-side navigation include _rsc_partial — preserve
|
|
909
|
+
// the partial flag so the revalidation returns a Flight stream, not HTML.
|
|
910
|
+
// App-switch is already excluded by classifyRequest (would be full-render).
|
|
911
|
+
const isPartialAction = url.searchParams.has("_rsc_partial");
|
|
912
|
+
return executeRenderWithMiddleware(
|
|
913
|
+
plan.route.routeMiddleware,
|
|
914
|
+
plan.negotiated,
|
|
915
|
+
plan.route.routeKey,
|
|
916
|
+
routeReverse,
|
|
672
917
|
request,
|
|
673
918
|
env,
|
|
674
919
|
url,
|
|
675
920
|
variables,
|
|
676
921
|
nonce,
|
|
677
|
-
preview?.params,
|
|
678
|
-
preview?.routeKey,
|
|
679
922
|
handleStore,
|
|
923
|
+
isPartialAction,
|
|
680
924
|
actionContinuation,
|
|
681
925
|
);
|
|
682
|
-
|
|
683
|
-
response.headers.append("Vary", "Accept");
|
|
684
|
-
}
|
|
685
|
-
return response;
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
// Wrap the render path (with or without route middleware) in a
|
|
689
|
-
// renderStartMs timeout so slow renders are caught before output.
|
|
690
|
-
const executeRender = async (): Promise<Response> => {
|
|
691
|
-
if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
|
|
692
|
-
const mwResponse = await executeMiddleware(
|
|
693
|
-
buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
|
|
694
|
-
request,
|
|
695
|
-
env,
|
|
696
|
-
variables,
|
|
697
|
-
renderHandler,
|
|
698
|
-
routeReverse,
|
|
699
|
-
);
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
url.searchParams.has("_rsc_partial") ||
|
|
703
|
-
url.searchParams.has("_rsc_action")
|
|
704
|
-
) {
|
|
705
|
-
const intercepted = interceptRedirectForPartial(
|
|
706
|
-
mwResponse,
|
|
707
|
-
createRedirectFlightResponse,
|
|
708
|
-
);
|
|
709
|
-
if (intercepted) return intercepted;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
return finalizeResponse(mwResponse);
|
|
713
|
-
}
|
|
926
|
+
}
|
|
714
927
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
928
|
+
// ---- Full render / Partial render (or PE that fell through) ----
|
|
929
|
+
if (plan.mode === "full-render" || plan.mode === "partial-render") {
|
|
930
|
+
const isPartial = plan.mode === "partial-render";
|
|
931
|
+
return executeRenderWithMiddleware(
|
|
932
|
+
plan.route.routeMiddleware,
|
|
933
|
+
plan.negotiated,
|
|
934
|
+
plan.route.routeKey,
|
|
935
|
+
routeReverse,
|
|
936
|
+
request,
|
|
937
|
+
env,
|
|
938
|
+
url,
|
|
939
|
+
variables,
|
|
940
|
+
nonce,
|
|
941
|
+
handleStore,
|
|
942
|
+
isPartial,
|
|
943
|
+
);
|
|
944
|
+
}
|
|
718
945
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
946
|
+
// PE that fell through (handleProgressiveEnhancement returned null)
|
|
947
|
+
// falls back to full render
|
|
948
|
+
if (plan.mode === "pe-render") {
|
|
949
|
+
return executeRenderWithMiddleware(
|
|
950
|
+
plan.route.routeMiddleware,
|
|
951
|
+
false,
|
|
952
|
+
plan.route.routeKey,
|
|
953
|
+
routeReverse,
|
|
726
954
|
request,
|
|
727
955
|
env,
|
|
728
956
|
url,
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
957
|
+
variables,
|
|
958
|
+
nonce,
|
|
959
|
+
handleStore,
|
|
960
|
+
false,
|
|
732
961
|
);
|
|
733
962
|
}
|
|
734
|
-
|
|
963
|
+
|
|
964
|
+
// Redirect plan that wasn't handled above (full-page redirect — let
|
|
965
|
+
// the pipeline handle it via match() which returns { redirect: url })
|
|
966
|
+
return executeRenderWithMiddleware(
|
|
967
|
+
plan.route.routeMiddleware,
|
|
968
|
+
false,
|
|
969
|
+
plan.route.routeKey,
|
|
970
|
+
routeReverse,
|
|
971
|
+
request,
|
|
972
|
+
env,
|
|
973
|
+
url,
|
|
974
|
+
variables,
|
|
975
|
+
nonce,
|
|
976
|
+
handleStore,
|
|
977
|
+
false,
|
|
978
|
+
);
|
|
735
979
|
}
|
|
736
980
|
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
async function
|
|
981
|
+
// Shared render execution: wraps handleRscRendering (or revalidateAfterAction)
|
|
982
|
+
// in route middleware and timeout handling. Consolidates the pattern used by
|
|
983
|
+
// action-revalidate, full-render, and partial-render modes.
|
|
984
|
+
async function executeRenderWithMiddleware(
|
|
985
|
+
routeMiddleware: import("../router/middleware-types.js").CollectedMiddleware[],
|
|
986
|
+
negotiated: boolean,
|
|
987
|
+
routeKey: string,
|
|
988
|
+
routeReverse: ReturnType<typeof createReverseFunction>,
|
|
741
989
|
request: Request,
|
|
742
990
|
env: TEnv,
|
|
743
991
|
url: URL,
|
|
744
992
|
variables: Record<string, any>,
|
|
745
993
|
nonce: string | undefined,
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
994
|
+
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
995
|
+
isPartial: boolean,
|
|
749
996
|
actionContinuation?: ActionContinuation,
|
|
750
997
|
): Promise<Response> {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
998
|
+
const renderHandler = async (): Promise<Response> => {
|
|
999
|
+
try {
|
|
1000
|
+
let response: Response;
|
|
1001
|
+
if (actionContinuation) {
|
|
1002
|
+
response = await revalidateAfterAction(
|
|
1003
|
+
handlerCtx,
|
|
1004
|
+
request,
|
|
1005
|
+
env,
|
|
1006
|
+
url,
|
|
1007
|
+
handleStore,
|
|
1008
|
+
actionContinuation,
|
|
1009
|
+
);
|
|
1010
|
+
} else {
|
|
1011
|
+
response = await handleRscRendering(
|
|
1012
|
+
handlerCtx,
|
|
1013
|
+
request,
|
|
1014
|
+
env,
|
|
1015
|
+
url,
|
|
1016
|
+
isPartial,
|
|
1017
|
+
handleStore,
|
|
1018
|
+
nonce,
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
if (negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
1022
|
+
response.headers.append("Vary", "Accept");
|
|
1023
|
+
}
|
|
1024
|
+
return response;
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
// Check if middleware/handler returned Response
|
|
1027
|
+
if (error instanceof Response) {
|
|
1028
|
+
// During partial (client-side navigation), a 200 Response from a handler
|
|
1029
|
+
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
1030
|
+
// Signal the browser to hard-navigate so it renders the raw response.
|
|
1031
|
+
if (isPartial && error.status === 200) {
|
|
1032
|
+
console.warn(
|
|
1033
|
+
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1034
|
+
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1035
|
+
);
|
|
1036
|
+
return createResponseWithMergedHeaders(null, {
|
|
1037
|
+
status: 200,
|
|
1038
|
+
headers: {
|
|
1039
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
1040
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
762
1044
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (referer) {
|
|
770
|
-
try {
|
|
771
|
-
const refererUrl = new URL(referer);
|
|
772
|
-
if (refererUrl.origin === url.origin) {
|
|
773
|
-
reloadUrl = referer;
|
|
774
|
-
}
|
|
775
|
-
} catch {
|
|
776
|
-
// Malformed referer, fall back to cleanUrl
|
|
1045
|
+
if (isPartial) {
|
|
1046
|
+
const intercepted = interceptRedirectForPartial(
|
|
1047
|
+
error,
|
|
1048
|
+
createRedirectFlightResponse,
|
|
1049
|
+
);
|
|
1050
|
+
if (intercepted) return intercepted;
|
|
777
1051
|
}
|
|
1052
|
+
|
|
1053
|
+
return error;
|
|
778
1054
|
}
|
|
779
|
-
}
|
|
780
1055
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
793
|
-
if (
|
|
794
|
-
url.searchParams.has("__debug_manifest") &&
|
|
795
|
-
(isDev || router.allowDebugManifest)
|
|
796
|
-
) {
|
|
797
|
-
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
798
|
-
const routeManifest = getRequiredRouteMap();
|
|
799
|
-
const { extractAncestryFromTrie } =
|
|
800
|
-
await import("../build/route-trie.js");
|
|
801
|
-
return new Response(
|
|
802
|
-
JSON.stringify(
|
|
803
|
-
{
|
|
804
|
-
routerId: router.id,
|
|
805
|
-
routeManifest,
|
|
806
|
-
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
807
|
-
routeTrie: trie,
|
|
808
|
-
precomputedEntries: getPrecomputedEntries(),
|
|
809
|
-
},
|
|
810
|
-
null,
|
|
811
|
-
2,
|
|
812
|
-
),
|
|
813
|
-
{
|
|
814
|
-
headers: { "Content-Type": "application/json" },
|
|
815
|
-
},
|
|
816
|
-
);
|
|
817
|
-
}
|
|
1056
|
+
// Render 404 page for unmatched routes
|
|
1057
|
+
const isRouteNotFound =
|
|
1058
|
+
error instanceof RouteNotFoundError ||
|
|
1059
|
+
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1060
|
+
if (isRouteNotFound) {
|
|
1061
|
+
callOnError(error, "routing", {
|
|
1062
|
+
request,
|
|
1063
|
+
url,
|
|
1064
|
+
env,
|
|
1065
|
+
handledByBoundary: true,
|
|
1066
|
+
});
|
|
818
1067
|
|
|
819
|
-
|
|
1068
|
+
const notFoundOption = router.notFound;
|
|
1069
|
+
const notFoundComponent =
|
|
1070
|
+
typeof notFoundOption === "function"
|
|
1071
|
+
? notFoundOption({ pathname: url.pathname })
|
|
1072
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
1073
|
+
|
|
1074
|
+
const notFoundSegment = {
|
|
1075
|
+
id: "notFound",
|
|
1076
|
+
namespace: "notFound",
|
|
1077
|
+
type: "route" as const,
|
|
1078
|
+
index: 0,
|
|
1079
|
+
component: notFoundComponent,
|
|
1080
|
+
params: {},
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const payload: RscPayload = {
|
|
1084
|
+
metadata: {
|
|
1085
|
+
pathname: url.pathname,
|
|
1086
|
+
routerId: router.id,
|
|
1087
|
+
basename: router.basename,
|
|
1088
|
+
segments: [notFoundSegment],
|
|
1089
|
+
matched: [],
|
|
1090
|
+
diff: [],
|
|
1091
|
+
isPartial: false,
|
|
1092
|
+
rootLayout: router.rootLayout,
|
|
1093
|
+
handles: handleStore.stream(),
|
|
1094
|
+
version,
|
|
1095
|
+
themeConfig: router.themeConfig,
|
|
1096
|
+
warmupEnabled: router.warmupEnabled,
|
|
1097
|
+
initialTheme: requireRequestContext().theme,
|
|
1098
|
+
},
|
|
1099
|
+
};
|
|
820
1100
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
}
|
|
1101
|
+
const rscStream = renderToReadableStream(payload, {
|
|
1102
|
+
onError: (error: unknown) => {
|
|
1103
|
+
callOnError(error, "rendering", { request, url, env });
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
827
1106
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
handlerCtx,
|
|
834
|
-
request,
|
|
835
|
-
env,
|
|
836
|
-
url,
|
|
837
|
-
store,
|
|
838
|
-
actionContinuation,
|
|
839
|
-
);
|
|
840
|
-
}
|
|
1107
|
+
const isRscRequest =
|
|
1108
|
+
isPartial ||
|
|
1109
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1110
|
+
!url.searchParams.has("__html")) ||
|
|
1111
|
+
url.searchParams.has("__rsc");
|
|
841
1112
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
handlerCtx,
|
|
849
|
-
request,
|
|
850
|
-
env,
|
|
851
|
-
url,
|
|
852
|
-
variables,
|
|
853
|
-
routeParams,
|
|
854
|
-
);
|
|
855
|
-
}
|
|
1113
|
+
if (isRscRequest) {
|
|
1114
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
1115
|
+
status: 404,
|
|
1116
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
856
1119
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
request,
|
|
864
|
-
env,
|
|
865
|
-
url,
|
|
866
|
-
isPartial,
|
|
867
|
-
store,
|
|
868
|
-
nonce,
|
|
869
|
-
);
|
|
870
|
-
} catch (error) {
|
|
871
|
-
// Check if middleware/handler returned Response
|
|
872
|
-
if (error instanceof Response) {
|
|
873
|
-
// During partial (client-side navigation), a 200 Response from a handler
|
|
874
|
-
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
875
|
-
// Signal the browser to hard-navigate so it renders the raw response.
|
|
876
|
-
// Only for 200 — redirects (3xx) work already because the browser follows
|
|
877
|
-
// them automatically to a URL that serves Flight data.
|
|
878
|
-
if (isPartial && error.status === 200) {
|
|
879
|
-
console.warn(
|
|
880
|
-
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
881
|
-
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1120
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1121
|
+
handlerCtx,
|
|
1122
|
+
request,
|
|
1123
|
+
env,
|
|
1124
|
+
url,
|
|
1125
|
+
requireRequestContext()._metricsStore,
|
|
882
1126
|
);
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
887
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
888
|
-
},
|
|
1127
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1128
|
+
nonce,
|
|
1129
|
+
streamMode,
|
|
889
1130
|
});
|
|
890
|
-
}
|
|
891
1131
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
);
|
|
897
|
-
if (intercepted) return intercepted;
|
|
1132
|
+
return createResponseWithMergedHeaders(htmlStream, {
|
|
1133
|
+
status: 404,
|
|
1134
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
1135
|
+
});
|
|
898
1136
|
}
|
|
899
1137
|
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// Render 404 page for unmatched routes
|
|
904
|
-
// Check both instanceof and error.name for cross-bundle compatibility
|
|
905
|
-
const isRouteNotFound =
|
|
906
|
-
error instanceof RouteNotFoundError ||
|
|
907
|
-
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
908
|
-
if (isRouteNotFound) {
|
|
1138
|
+
// Report unhandled errors
|
|
909
1139
|
callOnError(error, "routing", {
|
|
910
1140
|
request,
|
|
911
1141
|
url,
|
|
912
1142
|
env,
|
|
913
|
-
handledByBoundary:
|
|
1143
|
+
handledByBoundary: false,
|
|
914
1144
|
});
|
|
1145
|
+
console.error(`[RSC] Error:`, error);
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
915
1149
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
type: "route" as const,
|
|
928
|
-
index: 0,
|
|
929
|
-
component: notFoundComponent,
|
|
930
|
-
params: {},
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
const payload: RscPayload = {
|
|
934
|
-
metadata: {
|
|
935
|
-
pathname: url.pathname,
|
|
936
|
-
segments: [notFoundSegment],
|
|
937
|
-
matched: [],
|
|
938
|
-
diff: [],
|
|
939
|
-
isPartial: false,
|
|
940
|
-
rootLayout: router.rootLayout,
|
|
941
|
-
handles: store.stream(),
|
|
942
|
-
version,
|
|
943
|
-
themeConfig: router.themeConfig,
|
|
944
|
-
warmupEnabled: router.warmupEnabled,
|
|
945
|
-
initialTheme: requireRequestContext().theme,
|
|
946
|
-
// No routeName for not-found routes
|
|
947
|
-
},
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
const rscStream = renderToReadableStream(payload);
|
|
951
|
-
|
|
952
|
-
// Determine if this is an RSC request or HTML request.
|
|
953
|
-
// Partial requests are always RSC (see main isRscRequest comment).
|
|
954
|
-
const isRscRequest =
|
|
955
|
-
isPartial ||
|
|
956
|
-
(!request.headers.get("accept")?.includes("text/html") &&
|
|
957
|
-
!url.searchParams.has("__html")) ||
|
|
958
|
-
url.searchParams.has("__rsc");
|
|
1150
|
+
// Wrap the render path in a renderStartMs timeout
|
|
1151
|
+
const executeRender = async (): Promise<Response> => {
|
|
1152
|
+
if (routeMiddleware.length > 0) {
|
|
1153
|
+
const mwResponse = await executeMiddleware(
|
|
1154
|
+
buildRouteMiddlewareEntries<TEnv>(routeMiddleware),
|
|
1155
|
+
request,
|
|
1156
|
+
env,
|
|
1157
|
+
variables,
|
|
1158
|
+
renderHandler,
|
|
1159
|
+
routeReverse,
|
|
1160
|
+
);
|
|
959
1161
|
|
|
960
|
-
if (
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1162
|
+
if (isPartial || actionContinuation) {
|
|
1163
|
+
const intercepted = interceptRedirectForPartial(
|
|
1164
|
+
mwResponse,
|
|
1165
|
+
createRedirectFlightResponse,
|
|
1166
|
+
);
|
|
1167
|
+
if (intercepted) return intercepted;
|
|
965
1168
|
}
|
|
966
1169
|
|
|
967
|
-
|
|
968
|
-
const [ssrModule, streamMode] = await Promise.all([
|
|
969
|
-
loadSSRModule(),
|
|
970
|
-
handlerCtx.resolveStreamMode(request, env, url),
|
|
971
|
-
]);
|
|
972
|
-
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
973
|
-
nonce,
|
|
974
|
-
streamMode,
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
return createResponseWithMergedHeaders(htmlStream, {
|
|
978
|
-
status: 404,
|
|
979
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
980
|
-
});
|
|
1170
|
+
return finalizeResponse(mwResponse);
|
|
981
1171
|
}
|
|
982
1172
|
|
|
983
|
-
|
|
984
|
-
|
|
1173
|
+
return renderHandler();
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
const renderOutcome = await withTimeout(
|
|
1177
|
+
executeRender(),
|
|
1178
|
+
router.timeouts.renderStartMs,
|
|
1179
|
+
"render-start",
|
|
1180
|
+
);
|
|
1181
|
+
if (renderOutcome.timedOut) {
|
|
1182
|
+
return handleTimeoutResponse(
|
|
985
1183
|
request,
|
|
986
|
-
url,
|
|
987
1184
|
env,
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1185
|
+
url,
|
|
1186
|
+
"render-start",
|
|
1187
|
+
renderOutcome.durationMs,
|
|
1188
|
+
routeKey,
|
|
1189
|
+
);
|
|
992
1190
|
}
|
|
1191
|
+
return renderOutcome.result;
|
|
993
1192
|
}
|
|
994
1193
|
}
|