@rangojs/router 0.0.0-experimental.002d056c
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 +9 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -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 +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +363 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
2
|
+
/// <reference path="../vite/plugins/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 { createElement } from "react";
|
|
11
|
+
import { RouteNotFoundError } from "../errors.js";
|
|
12
|
+
import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
13
|
+
import {
|
|
14
|
+
runWithRequestContext,
|
|
15
|
+
setRequestContextParams,
|
|
16
|
+
requireRequestContext,
|
|
17
|
+
createRequestContext,
|
|
18
|
+
} from "../server/request-context.js";
|
|
19
|
+
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
RscPayload,
|
|
23
|
+
CreateRSCHandlerOptions,
|
|
24
|
+
LoadSSRModule,
|
|
25
|
+
SSRModule,
|
|
26
|
+
} from "./types.js";
|
|
27
|
+
import {
|
|
28
|
+
createResponseWithMergedHeaders,
|
|
29
|
+
finalizeResponse,
|
|
30
|
+
interceptRedirectForPartial,
|
|
31
|
+
buildRouteMiddlewareEntries,
|
|
32
|
+
} from "./helpers.js";
|
|
33
|
+
import {
|
|
34
|
+
handleResponseRoute,
|
|
35
|
+
type ResponseRouteMatch,
|
|
36
|
+
} from "./response-route-handler.js";
|
|
37
|
+
import { generateNonce, nonce as nonceToken } from "./nonce.js";
|
|
38
|
+
import { VERSION } from "@rangojs/router:version";
|
|
39
|
+
import type { ErrorPhase } from "../types.js";
|
|
40
|
+
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
41
|
+
import { invokeOnError } from "../router/error-handling.js";
|
|
42
|
+
import {
|
|
43
|
+
createReverseFunction,
|
|
44
|
+
stripInternalParams,
|
|
45
|
+
} from "../router/handler-context.js";
|
|
46
|
+
import { getRouterContext } from "../router/router-context.js";
|
|
47
|
+
import { resolveSink, safeEmit } from "../router/telemetry.js";
|
|
48
|
+
import { contextSet } from "../context-var.js";
|
|
49
|
+
import {
|
|
50
|
+
hasCachedManifest,
|
|
51
|
+
getRouteTrie,
|
|
52
|
+
getPrecomputedEntries,
|
|
53
|
+
waitForManifestReady,
|
|
54
|
+
getRouterManifest,
|
|
55
|
+
getRouterTrie,
|
|
56
|
+
} from "../route-map-builder.js";
|
|
57
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
58
|
+
import { buildRouterTrieFromUrlpatterns } from "./manifest-init.js";
|
|
59
|
+
import { handleProgressiveEnhancement } from "./progressive-enhancement.js";
|
|
60
|
+
import {
|
|
61
|
+
executeServerAction,
|
|
62
|
+
revalidateAfterAction,
|
|
63
|
+
type ActionContinuation,
|
|
64
|
+
} from "./server-action.js";
|
|
65
|
+
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
66
|
+
import { checkRequestOrigin, type OriginCheckPhase } from "./origin-guard.js";
|
|
67
|
+
import { handleRscRendering } from "./rsc-rendering.js";
|
|
68
|
+
import {
|
|
69
|
+
withTimeout,
|
|
70
|
+
RouterTimeoutError,
|
|
71
|
+
createDefaultTimeoutResponse,
|
|
72
|
+
type TimeoutPhase,
|
|
73
|
+
} from "../router/timeout.js";
|
|
74
|
+
import {
|
|
75
|
+
createMetricsStore,
|
|
76
|
+
appendMetric,
|
|
77
|
+
buildMetricsTiming,
|
|
78
|
+
} from "../router/metrics.js";
|
|
79
|
+
import {
|
|
80
|
+
startSSRSetup,
|
|
81
|
+
getSSRSetup,
|
|
82
|
+
mayNeedSSR,
|
|
83
|
+
SSR_SETUP_VAR,
|
|
84
|
+
} from "./ssr-setup.js";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Create an RSC request handler.
|
|
88
|
+
*
|
|
89
|
+
* **Recommended:** Use `router.createHandler()` instead for simpler setup:
|
|
90
|
+
* ```tsx
|
|
91
|
+
* const router = createRouter({ document, urls, nonce: () => true });
|
|
92
|
+
* export const fetch = router.createHandler();
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* This function is still useful for advanced cases like per-request cache
|
|
96
|
+
* configuration (e.g., Cloudflare Workers with ExecutionContext).
|
|
97
|
+
*
|
|
98
|
+
* @example Basic usage (deps and loadSSRModule have sensible defaults)
|
|
99
|
+
* ```tsx
|
|
100
|
+
* import { createRSCHandler } from "@rangojs/router/rsc";
|
|
101
|
+
* import { router } from "./router.js";
|
|
102
|
+
*
|
|
103
|
+
* export default createRSCHandler({ router });
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example With custom deps (advanced)
|
|
107
|
+
* ```tsx
|
|
108
|
+
* import { createRSCHandler } from "@rangojs/router/rsc";
|
|
109
|
+
* import * as rsc from "@vitejs/plugin-rsc/rsc";
|
|
110
|
+
* import { router } from "./router.js";
|
|
111
|
+
*
|
|
112
|
+
* export default createRSCHandler({
|
|
113
|
+
* router,
|
|
114
|
+
* deps: rsc,
|
|
115
|
+
* loadSSRModule: () => import.meta.viteRsc.loadModule("ssr", "index"),
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function createRSCHandler<
|
|
120
|
+
TEnv = unknown,
|
|
121
|
+
TRoutes extends Record<string, string> = Record<string, string>,
|
|
122
|
+
>(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
|
|
123
|
+
const { router, version = VERSION, nonce: nonceProvider } = options;
|
|
124
|
+
|
|
125
|
+
// Use provided deps or default to @vitejs/plugin-rsc/rsc exports
|
|
126
|
+
const deps = options.deps ?? rscDeps;
|
|
127
|
+
const {
|
|
128
|
+
renderToReadableStream,
|
|
129
|
+
decodeReply,
|
|
130
|
+
createTemporaryReferenceSet,
|
|
131
|
+
loadServerAction,
|
|
132
|
+
decodeAction,
|
|
133
|
+
decodeFormState,
|
|
134
|
+
} = deps;
|
|
135
|
+
|
|
136
|
+
// Use provided loadSSRModule or default to vite RSC module loader.
|
|
137
|
+
// In production the SSR module is stable across requests, so memoize
|
|
138
|
+
// the dynamic import to avoid repeated module resolution overhead.
|
|
139
|
+
// In dev mode Vite may hot-reload the module, so skip memoization.
|
|
140
|
+
const rawLoadSSRModule: LoadSSRModule =
|
|
141
|
+
options.loadSSRModule ??
|
|
142
|
+
(() => import.meta.viteRsc.loadModule("ssr", "index"));
|
|
143
|
+
let _ssrModulePromise: Promise<SSRModule> | undefined;
|
|
144
|
+
const loadSSRModule: LoadSSRModule =
|
|
145
|
+
process.env.NODE_ENV === "production"
|
|
146
|
+
? () =>
|
|
147
|
+
(_ssrModulePromise ??= rawLoadSSRModule().catch((err) => {
|
|
148
|
+
_ssrModulePromise = undefined;
|
|
149
|
+
throw err;
|
|
150
|
+
}))
|
|
151
|
+
: rawLoadSSRModule;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Per-request error reporter that deduplicates via the ALS request context.
|
|
155
|
+
*
|
|
156
|
+
* Uses the same _reportedErrors WeakSet as the router layer so errors
|
|
157
|
+
* that propagate across layers are only reported once per request.
|
|
158
|
+
*/
|
|
159
|
+
function callOnError(
|
|
160
|
+
error: unknown,
|
|
161
|
+
phase: ErrorPhase,
|
|
162
|
+
context: Parameters<typeof invokeOnError<TEnv>>[3],
|
|
163
|
+
): void {
|
|
164
|
+
if (error != null && typeof error === "object") {
|
|
165
|
+
const reportedErrors = requireRequestContext()._reportedErrors;
|
|
166
|
+
if (reportedErrors.has(error)) return;
|
|
167
|
+
reportedErrors.add(error);
|
|
168
|
+
}
|
|
169
|
+
invokeOnError(router.onError, error, phase, context, "RSC");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getRequiredRouteMap(): Record<string, string> {
|
|
173
|
+
const routeMap = getRouterManifest(router.id);
|
|
174
|
+
if (!routeMap) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Route manifest for router "${router.id}" is not available.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return routeMap;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Handle a timeout by reporting the error, emitting telemetry,
|
|
184
|
+
* and returning either the custom onTimeout response or a default 504.
|
|
185
|
+
*/
|
|
186
|
+
async function handleTimeoutResponse(
|
|
187
|
+
request: Request,
|
|
188
|
+
env: TEnv,
|
|
189
|
+
url: URL,
|
|
190
|
+
phase: TimeoutPhase,
|
|
191
|
+
durationMs: number,
|
|
192
|
+
routeKey?: string,
|
|
193
|
+
actionId?: string,
|
|
194
|
+
): Promise<Response> {
|
|
195
|
+
const timeoutError = new RouterTimeoutError(phase, durationMs);
|
|
196
|
+
|
|
197
|
+
callOnError(timeoutError, phase === "action" ? "action" : "handler", {
|
|
198
|
+
request,
|
|
199
|
+
url,
|
|
200
|
+
env,
|
|
201
|
+
routeKey,
|
|
202
|
+
actionId,
|
|
203
|
+
handledByBoundary: false,
|
|
204
|
+
metadata: { timeout: true, phase, durationMs },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const routerCtx = getRouterContext();
|
|
209
|
+
if (routerCtx?.telemetry) {
|
|
210
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
211
|
+
type: "request.timeout" as const,
|
|
212
|
+
timestamp: performance.now(),
|
|
213
|
+
requestId: routerCtx.requestId,
|
|
214
|
+
phase,
|
|
215
|
+
pathname: url.pathname,
|
|
216
|
+
routeKey,
|
|
217
|
+
actionId,
|
|
218
|
+
durationMs,
|
|
219
|
+
customHandler: !!router.onTimeout,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
// Router context may not be available
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (router.onTimeout) {
|
|
227
|
+
try {
|
|
228
|
+
return await router.onTimeout({
|
|
229
|
+
phase,
|
|
230
|
+
request,
|
|
231
|
+
url,
|
|
232
|
+
env,
|
|
233
|
+
routeKey,
|
|
234
|
+
actionId,
|
|
235
|
+
durationMs,
|
|
236
|
+
});
|
|
237
|
+
} catch (e) {
|
|
238
|
+
if (process.env.NODE_ENV !== "production") {
|
|
239
|
+
console.error("[RSC] onTimeout callback error:", e);
|
|
240
|
+
}
|
|
241
|
+
return createDefaultTimeoutResponse(phase);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return createDefaultTimeoutResponse(phase);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Build a 200 Flight response that carries a redirect URL and optional state.
|
|
250
|
+
* Used when a partial/action request results in a redirect -- fetch
|
|
251
|
+
* auto-follows 3xx so we send the redirect as payload metadata instead.
|
|
252
|
+
*/
|
|
253
|
+
function createRedirectFlightResponse(
|
|
254
|
+
redirectUrl: string,
|
|
255
|
+
locationState?: Record<string, unknown>,
|
|
256
|
+
): Response {
|
|
257
|
+
const redirectPayload: RscPayload = {
|
|
258
|
+
metadata: {
|
|
259
|
+
pathname: redirectUrl,
|
|
260
|
+
segments: [],
|
|
261
|
+
redirect: { url: redirectUrl },
|
|
262
|
+
...(locationState && { locationState }),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
const rscStream = renderToReadableStream<RscPayload>(redirectPayload);
|
|
266
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
267
|
+
status: 200,
|
|
268
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Bundle shared dependencies for extracted handler functions.
|
|
273
|
+
// callOnError reads from ALS so it's inherently per-request scoped.
|
|
274
|
+
const handlerCtx: HandlerContext<TEnv> = {
|
|
275
|
+
router,
|
|
276
|
+
version,
|
|
277
|
+
renderToReadableStream,
|
|
278
|
+
decodeReply,
|
|
279
|
+
createTemporaryReferenceSet,
|
|
280
|
+
loadServerAction,
|
|
281
|
+
decodeAction,
|
|
282
|
+
decodeFormState,
|
|
283
|
+
loadSSRModule,
|
|
284
|
+
callOnError,
|
|
285
|
+
getRequiredRouteMap,
|
|
286
|
+
createRedirectFlightResponse,
|
|
287
|
+
resolveStreamMode: async (request, env, url) => {
|
|
288
|
+
const resolver = router.ssr?.resolveStreaming;
|
|
289
|
+
if (!resolver) return "stream";
|
|
290
|
+
return resolver({ request, env, url });
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return async function handler(
|
|
295
|
+
request: Request,
|
|
296
|
+
input: RouterRequestInput<TEnv> = {},
|
|
297
|
+
): Promise<Response> {
|
|
298
|
+
const handlerStart = performance.now();
|
|
299
|
+
// Create the metrics store at handler start so handler:total has startTime=0
|
|
300
|
+
// and all metrics are relative to the request entry point.
|
|
301
|
+
const earlyMetricsStore = router.debugPerformance
|
|
302
|
+
? createMetricsStore(true, handlerStart)
|
|
303
|
+
: undefined;
|
|
304
|
+
|
|
305
|
+
const { env = {} as TEnv, vars: initialVars, ctx: executionCtx } = input;
|
|
306
|
+
|
|
307
|
+
// Connection warmup: return 204 immediately before any processing
|
|
308
|
+
if (router?.warmupEnabled && request.method === "HEAD") {
|
|
309
|
+
const warmupUrl = new URL(request.url);
|
|
310
|
+
if (warmupUrl.searchParams.has("_rsc_warmup")) {
|
|
311
|
+
return new Response(null, { status: 204 });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Resolve nonce if provider is set
|
|
316
|
+
const nonceStart = performance.now();
|
|
317
|
+
let nonce: string | undefined;
|
|
318
|
+
if (nonceProvider) {
|
|
319
|
+
const result = await nonceProvider(request, env);
|
|
320
|
+
nonce = result === true ? generateNonce() : result;
|
|
321
|
+
}
|
|
322
|
+
const nonceDur = performance.now() - nonceStart;
|
|
323
|
+
|
|
324
|
+
const url = new URL(request.url);
|
|
325
|
+
|
|
326
|
+
// Match global middleware
|
|
327
|
+
const mwMatchStart = performance.now();
|
|
328
|
+
const matchedMiddleware = matchMiddleware(url.pathname, router.middleware);
|
|
329
|
+
const mwMatchDur = performance.now() - mwMatchStart;
|
|
330
|
+
|
|
331
|
+
// Shared variables between middleware and route handlers
|
|
332
|
+
// Initialize from input.vars if provided (allows pre-seeding from worker entry)
|
|
333
|
+
const variables: Record<string, any> = initialVars
|
|
334
|
+
? { ...initialVars }
|
|
335
|
+
: {};
|
|
336
|
+
|
|
337
|
+
// Store nonce via ContextVar token and string key for backward compat
|
|
338
|
+
if (nonce) {
|
|
339
|
+
contextSet(variables, nonceToken, nonce);
|
|
340
|
+
variables.nonce = nonce;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Resolve cache store configuration
|
|
344
|
+
// Priority: options.cache (handler override) > router.cache (router default)
|
|
345
|
+
// Store is enabled only if: config provided, enabled, and no ?__no_cache query param
|
|
346
|
+
let cacheStore = undefined;
|
|
347
|
+
const cacheOption = options.cache ?? router.cache;
|
|
348
|
+
if (cacheOption && !url.searchParams.has("__no_cache")) {
|
|
349
|
+
const cacheConfig =
|
|
350
|
+
typeof cacheOption === "function"
|
|
351
|
+
? cacheOption(env, executionCtx)
|
|
352
|
+
: cacheOption;
|
|
353
|
+
|
|
354
|
+
if (cacheConfig.enabled !== false) {
|
|
355
|
+
cacheStore = cacheConfig.store;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Route manifest is populated at startup via the virtual module
|
|
360
|
+
// (virtual:rsc-router/routes-manifest). In build/production, it's inlined
|
|
361
|
+
// into the bundle. In dev mode (Node), the discovery plugin populates it
|
|
362
|
+
// via setManifestReadyPromise(). In dev mode (Cloudflare), Miniflare runs
|
|
363
|
+
// in a separate isolate where module-level state doesn't carry over, so
|
|
364
|
+
// we generate inline from the router's urlpatterns.
|
|
365
|
+
//
|
|
366
|
+
// In multi-router setups (e.g. createHostRouter), each router must have
|
|
367
|
+
// its own per-router manifest. We check per-router data first: even if
|
|
368
|
+
// the global manifest was set by a different router, this router still
|
|
369
|
+
// needs its own trie and manifest for correct matching.
|
|
370
|
+
const manifestCacheStart = performance.now();
|
|
371
|
+
const hasRouterData = getRouterManifest(router.id) !== undefined;
|
|
372
|
+
if (!hasRouterData) {
|
|
373
|
+
if (!hasCachedManifest()) {
|
|
374
|
+
const readyPromise = waitForManifestReady();
|
|
375
|
+
if (readyPromise) {
|
|
376
|
+
await readyPromise;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (!getRouterManifest(router.id) && router.urlpatterns) {
|
|
380
|
+
// Cloudflare dev: generate manifest inline for this router.
|
|
381
|
+
// Each router generates its own manifest independently so
|
|
382
|
+
// multi-router setups (host routing) work correctly.
|
|
383
|
+
await buildRouterTrieFromUrlpatterns(router);
|
|
384
|
+
}
|
|
385
|
+
if (!getRouterManifest(router.id) && !hasCachedManifest()) {
|
|
386
|
+
throw new Error(
|
|
387
|
+
'Route manifest not available. Ensure "virtual:rsc-router/routes-manifest" is imported in your entry file.',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Rebuild the trie when the manifest exists but the per-router trie is
|
|
393
|
+
// missing. This happens in dev mode after HMR: the virtual module sets
|
|
394
|
+
// the manifest (from fresh gen files) but the trie is intentionally not
|
|
395
|
+
// injected to avoid stale discovery-time data. Without the trie, route
|
|
396
|
+
// matching falls back to regex iteration which does not handle wildcard
|
|
397
|
+
// priority correctly (catch-all patterns match before specific routes).
|
|
398
|
+
if (!getRouterTrie(router.id) && router.urlpatterns) {
|
|
399
|
+
await buildRouterTrieFromUrlpatterns(router);
|
|
400
|
+
}
|
|
401
|
+
const manifestCacheDur = performance.now() - manifestCacheStart;
|
|
402
|
+
|
|
403
|
+
// Create unified request context with all methods
|
|
404
|
+
// Includes: stub response, handle store, loader memoization, use(), cookies, headers, cache store
|
|
405
|
+
// params starts empty, populated after route matching via setRequestContextParams
|
|
406
|
+
const ctxCreateStart = performance.now();
|
|
407
|
+
const requestContext = createRequestContext({
|
|
408
|
+
env,
|
|
409
|
+
request,
|
|
410
|
+
url,
|
|
411
|
+
variables,
|
|
412
|
+
cacheStore,
|
|
413
|
+
cacheProfiles: router.cacheProfiles,
|
|
414
|
+
executionContext: executionCtx,
|
|
415
|
+
themeConfig: router.themeConfig,
|
|
416
|
+
});
|
|
417
|
+
if (earlyMetricsStore) {
|
|
418
|
+
requestContext._debugPerformance = true;
|
|
419
|
+
requestContext._metricsStore = earlyMetricsStore;
|
|
420
|
+
}
|
|
421
|
+
// Wire background error reporting so "use cache" and other subsystems
|
|
422
|
+
// can surface non-fatal errors through the router's onError callback.
|
|
423
|
+
requestContext._reportBackgroundError = (
|
|
424
|
+
error: unknown,
|
|
425
|
+
category: string,
|
|
426
|
+
) => {
|
|
427
|
+
callOnError(error, "cache", {
|
|
428
|
+
request,
|
|
429
|
+
url,
|
|
430
|
+
metadata: { category },
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const ctxCreateDur = performance.now() - ctxCreateStart;
|
|
435
|
+
|
|
436
|
+
// Accumulate handler-level timing for Server-Timing header
|
|
437
|
+
const handlerTiming = [
|
|
438
|
+
`handler-nonce;dur=${nonceDur.toFixed(2)}`,
|
|
439
|
+
`handler-mw-match;dur=${mwMatchDur.toFixed(2)}`,
|
|
440
|
+
`handler-manifest-cache;dur=${manifestCacheDur.toFixed(2)}`,
|
|
441
|
+
`handler-ctx-create;dur=${ctxCreateDur.toFixed(2)}`,
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
// Store timing data in variables for downstream access
|
|
445
|
+
variables.__handlerTiming = handlerTiming;
|
|
446
|
+
variables.__handlerStart = handlerStart;
|
|
447
|
+
|
|
448
|
+
// Wrap entire request handling in request context
|
|
449
|
+
// Makes context available via getRequestContext() throughout:
|
|
450
|
+
// - Middleware execution
|
|
451
|
+
// - Route handlers and loaders
|
|
452
|
+
// - Server components during rendering
|
|
453
|
+
// - Error boundaries
|
|
454
|
+
// - Streaming
|
|
455
|
+
return runWithRequestContext(requestContext, async () => {
|
|
456
|
+
// Core handler logic (wrapped by middleware)
|
|
457
|
+
const coreHandler = async (): Promise<Response> => {
|
|
458
|
+
return coreRequestHandler(request, env, url, variables, nonce);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Execute middleware chain if any, otherwise call core handler directly
|
|
462
|
+
let response: Response;
|
|
463
|
+
if (matchedMiddleware.length > 0) {
|
|
464
|
+
const mwResponse = await executeMiddleware(
|
|
465
|
+
matchedMiddleware,
|
|
466
|
+
request,
|
|
467
|
+
env,
|
|
468
|
+
variables,
|
|
469
|
+
coreHandler,
|
|
470
|
+
createReverseFunction(getRequiredRouteMap()),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
if (
|
|
474
|
+
url.searchParams.has("_rsc_partial") ||
|
|
475
|
+
url.searchParams.has("_rsc_action")
|
|
476
|
+
) {
|
|
477
|
+
const intercepted = interceptRedirectForPartial(
|
|
478
|
+
mwResponse,
|
|
479
|
+
createRedirectFlightResponse,
|
|
480
|
+
);
|
|
481
|
+
response = intercepted ?? finalizeResponse(mwResponse);
|
|
482
|
+
} else {
|
|
483
|
+
response = finalizeResponse(mwResponse);
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
response = await coreHandler();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Finalize metrics after all middleware (including post-next work)
|
|
490
|
+
// has completed so :post spans are captured in the timeline.
|
|
491
|
+
// Handler timing parts are always emitted (even without debug metrics)
|
|
492
|
+
// so non-debug requests still get bootstrap Server-Timing entries.
|
|
493
|
+
const handlerTimingArr: string[] = variables.__handlerTiming || [];
|
|
494
|
+
// Preserve any existing Server-Timing set by response routes or middleware
|
|
495
|
+
const existingTiming = response.headers.get("Server-Timing");
|
|
496
|
+
const timingParts = existingTiming
|
|
497
|
+
? [existingTiming, ...handlerTimingArr]
|
|
498
|
+
: [...handlerTimingArr];
|
|
499
|
+
|
|
500
|
+
const metricsStore = requestContext._metricsStore;
|
|
501
|
+
if (metricsStore) {
|
|
502
|
+
// When the store was created at handler start (earlyMetricsStore),
|
|
503
|
+
// handler:total covers the full request. When ctx.debugPerformance()
|
|
504
|
+
// created the store mid-request, use its requestStart to avoid a
|
|
505
|
+
// negative startTime offset.
|
|
506
|
+
const totalStart = earlyMetricsStore
|
|
507
|
+
? handlerStart
|
|
508
|
+
: metricsStore.requestStart;
|
|
509
|
+
appendMetric(
|
|
510
|
+
metricsStore,
|
|
511
|
+
"handler:total",
|
|
512
|
+
totalStart,
|
|
513
|
+
performance.now() - totalStart,
|
|
514
|
+
);
|
|
515
|
+
const metricsTiming = buildMetricsTiming(
|
|
516
|
+
request.method,
|
|
517
|
+
url.pathname,
|
|
518
|
+
metricsStore,
|
|
519
|
+
);
|
|
520
|
+
if (metricsTiming) timingParts.push(metricsTiming);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const fullTiming = timingParts.join(", ");
|
|
524
|
+
if (fullTiming) response.headers.set("Server-Timing", fullTiming);
|
|
525
|
+
|
|
526
|
+
return response;
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// Core request handling logic (separated for middleware wrapping)
|
|
531
|
+
async function coreRequestHandler(
|
|
532
|
+
request: Request,
|
|
533
|
+
env: TEnv,
|
|
534
|
+
url: URL,
|
|
535
|
+
variables: Record<string, any>,
|
|
536
|
+
nonce: string | undefined,
|
|
537
|
+
): Promise<Response> {
|
|
538
|
+
const previewStart = performance.now();
|
|
539
|
+
const preview = await router.previewMatch(request, { env });
|
|
540
|
+
const previewDur = performance.now() - previewStart;
|
|
541
|
+
const handlerTiming: string[] = variables.__handlerTiming || [];
|
|
542
|
+
handlerTiming.push(`handler-preview-match;dur=${previewDur.toFixed(2)}`);
|
|
543
|
+
// Response route short-circuit: skip entire RSC pipeline
|
|
544
|
+
if (preview?.responseType && preview.handler) {
|
|
545
|
+
const responseOutcome = await withTimeout(
|
|
546
|
+
handleResponseRoute(
|
|
547
|
+
handlerCtx,
|
|
548
|
+
preview as ResponseRouteMatch,
|
|
549
|
+
request,
|
|
550
|
+
env,
|
|
551
|
+
url,
|
|
552
|
+
variables,
|
|
553
|
+
),
|
|
554
|
+
router.timeouts.renderStartMs,
|
|
555
|
+
"render-start",
|
|
556
|
+
);
|
|
557
|
+
if (responseOutcome.timedOut) {
|
|
558
|
+
return handleTimeoutResponse(
|
|
559
|
+
request,
|
|
560
|
+
env,
|
|
561
|
+
url,
|
|
562
|
+
"render-start",
|
|
563
|
+
responseOutcome.durationMs,
|
|
564
|
+
preview?.routeKey,
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return responseOutcome.result;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Kick off SSR module loading + stream mode resolution in parallel with
|
|
571
|
+
// segment resolution. Placed after the response-route short-circuit so
|
|
572
|
+
// response/mime routes never pay for SSR work.
|
|
573
|
+
if (mayNeedSSR(request, url)) {
|
|
574
|
+
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
575
|
+
handlerCtx,
|
|
576
|
+
request,
|
|
577
|
+
env,
|
|
578
|
+
url,
|
|
579
|
+
router.debugPerformance
|
|
580
|
+
? () => requireRequestContext()._metricsStore
|
|
581
|
+
: undefined,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
586
|
+
|
|
587
|
+
const isAction =
|
|
588
|
+
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
589
|
+
const isLoaderFetch = url.searchParams.has("_rsc_loader");
|
|
590
|
+
const actionId =
|
|
591
|
+
request.headers.get("rsc-action") || url.searchParams.get("_rsc_action");
|
|
592
|
+
|
|
593
|
+
// Origin guard: reject cross-origin actions, loader fetches, and
|
|
594
|
+
// PE form submissions before any execution. Regular page navigations
|
|
595
|
+
// (GET without _rsc_loader/_rsc_action) are not affected.
|
|
596
|
+
const originPhase: OriginCheckPhase | null = isAction
|
|
597
|
+
? "action"
|
|
598
|
+
: isLoaderFetch
|
|
599
|
+
? "loader"
|
|
600
|
+
: request.method === "POST"
|
|
601
|
+
? "pe-form"
|
|
602
|
+
: null;
|
|
603
|
+
if (originPhase) {
|
|
604
|
+
const originResult = await checkRequestOrigin(
|
|
605
|
+
request,
|
|
606
|
+
url,
|
|
607
|
+
router.originCheck,
|
|
608
|
+
env,
|
|
609
|
+
router.id,
|
|
610
|
+
originPhase,
|
|
611
|
+
);
|
|
612
|
+
if (originResult) {
|
|
613
|
+
const originError = new Error(
|
|
614
|
+
`Origin check rejected: ${request.headers.get("origin") ?? "none"} vs ${request.headers.get("host") ?? "none"}`,
|
|
615
|
+
);
|
|
616
|
+
originError.name = "OriginCheckError";
|
|
617
|
+
|
|
618
|
+
callOnError(originError, "origin", {
|
|
619
|
+
request,
|
|
620
|
+
url,
|
|
621
|
+
env,
|
|
622
|
+
handledByBoundary: false,
|
|
623
|
+
metadata: {
|
|
624
|
+
phase: originPhase,
|
|
625
|
+
origin: request.headers.get("origin"),
|
|
626
|
+
host: request.headers.get("host"),
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const routerCtx = getRouterContext();
|
|
632
|
+
if (routerCtx?.telemetry) {
|
|
633
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
634
|
+
type: "request.origin-rejected" as const,
|
|
635
|
+
timestamp: performance.now(),
|
|
636
|
+
requestId: routerCtx.requestId,
|
|
637
|
+
method: request.method,
|
|
638
|
+
pathname: url.pathname,
|
|
639
|
+
phase: originPhase,
|
|
640
|
+
origin: request.headers.get("origin"),
|
|
641
|
+
host: request.headers.get("host"),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
} catch {
|
|
645
|
+
// Router context may not be available
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return originResult;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Get handle store from request context
|
|
653
|
+
const handleStore = requireRequestContext()._handleStore;
|
|
654
|
+
|
|
655
|
+
// Wire up error reporting for late streaming-handle failures
|
|
656
|
+
// (LateHandlePushError: handle pushed after stream completion).
|
|
657
|
+
// Without this, these errors are only caught by React's error boundary
|
|
658
|
+
// and never reach the router's onError callback or telemetry.
|
|
659
|
+
handleStore.onError = (error: Error) => {
|
|
660
|
+
const reqCtx = requireRequestContext();
|
|
661
|
+
callOnError(error, "handler", {
|
|
662
|
+
request,
|
|
663
|
+
url,
|
|
664
|
+
routeKey: reqCtx._routeName,
|
|
665
|
+
params: reqCtx.params as Record<string, string>,
|
|
666
|
+
handledByBoundary: true,
|
|
667
|
+
});
|
|
668
|
+
try {
|
|
669
|
+
const routerCtx = getRouterContext();
|
|
670
|
+
if (routerCtx?.telemetry) {
|
|
671
|
+
safeEmit(resolveSink(routerCtx.telemetry), {
|
|
672
|
+
type: "handler.error" as const,
|
|
673
|
+
timestamp: performance.now(),
|
|
674
|
+
requestId: routerCtx.requestId,
|
|
675
|
+
error,
|
|
676
|
+
handledByBoundary: true,
|
|
677
|
+
pathname: url.pathname,
|
|
678
|
+
routeKey: reqCtx._routeName,
|
|
679
|
+
params: reqCtx.params as Record<string, string>,
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
} catch {
|
|
683
|
+
// Router context may not be available (e.g. prerender path)
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// Set route params early so all execution paths can access ctx.params.
|
|
688
|
+
if (preview?.params) {
|
|
689
|
+
setRequestContextParams(preview.params, preview.routeKey);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Progressive enhancement runs before the normal action/render paths.
|
|
693
|
+
// Route middleware wraps the PE re-render so handlers see the same
|
|
694
|
+
// context variables regardless of JS/no-JS transport.
|
|
695
|
+
const progressiveResult = await handleProgressiveEnhancement(
|
|
696
|
+
handlerCtx,
|
|
697
|
+
request,
|
|
698
|
+
env,
|
|
699
|
+
url,
|
|
700
|
+
isAction,
|
|
701
|
+
handleStore,
|
|
702
|
+
nonce,
|
|
703
|
+
{
|
|
704
|
+
routeMiddleware: preview?.routeMiddleware,
|
|
705
|
+
variables,
|
|
706
|
+
routeReverse,
|
|
707
|
+
},
|
|
708
|
+
);
|
|
709
|
+
if (progressiveResult) {
|
|
710
|
+
return progressiveResult;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// --- Action execution: runs BEFORE route middleware ---
|
|
714
|
+
// Route middleware wraps rendering only. For actions, the action runs
|
|
715
|
+
// first in the global middleware context, then route middleware wraps
|
|
716
|
+
// the revalidation pass (identical to a normal render).
|
|
717
|
+
let actionContinuation: ActionContinuation | undefined;
|
|
718
|
+
if (isAction && actionId) {
|
|
719
|
+
try {
|
|
720
|
+
const actionOutcome = await withTimeout(
|
|
721
|
+
executeServerAction(
|
|
722
|
+
handlerCtx,
|
|
723
|
+
request,
|
|
724
|
+
env,
|
|
725
|
+
url,
|
|
726
|
+
actionId,
|
|
727
|
+
handleStore,
|
|
728
|
+
),
|
|
729
|
+
router.timeouts.actionMs,
|
|
730
|
+
"action",
|
|
731
|
+
);
|
|
732
|
+
if (actionOutcome.timedOut) {
|
|
733
|
+
return handleTimeoutResponse(
|
|
734
|
+
request,
|
|
735
|
+
env,
|
|
736
|
+
url,
|
|
737
|
+
"action",
|
|
738
|
+
actionOutcome.durationMs,
|
|
739
|
+
preview?.routeKey,
|
|
740
|
+
actionId,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
const result = actionOutcome.result;
|
|
744
|
+
// Response means redirect or error boundary — done.
|
|
745
|
+
if (result instanceof Response) return result;
|
|
746
|
+
actionContinuation = result;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
callOnError(error, "action", {
|
|
749
|
+
request,
|
|
750
|
+
url,
|
|
751
|
+
env,
|
|
752
|
+
actionId,
|
|
753
|
+
handledByBoundary: false,
|
|
754
|
+
});
|
|
755
|
+
console.error(`[RSC] Action error:`, error);
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// --- Rendering (action revalidation or navigation) ---
|
|
761
|
+
// Route middleware wraps this — same code path for both cases.
|
|
762
|
+
const renderHandler = async () => {
|
|
763
|
+
const response = await coreRequestHandlerInner(
|
|
764
|
+
request,
|
|
765
|
+
env,
|
|
766
|
+
url,
|
|
767
|
+
variables,
|
|
768
|
+
nonce,
|
|
769
|
+
preview?.params,
|
|
770
|
+
preview?.routeKey,
|
|
771
|
+
handleStore,
|
|
772
|
+
actionContinuation,
|
|
773
|
+
);
|
|
774
|
+
if (preview?.negotiated) {
|
|
775
|
+
response.headers.append("Vary", "Accept");
|
|
776
|
+
}
|
|
777
|
+
return response;
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
// Wrap the render path (with or without route middleware) in a
|
|
781
|
+
// renderStartMs timeout so slow renders are caught before output.
|
|
782
|
+
const executeRender = async (): Promise<Response> => {
|
|
783
|
+
if (preview?.routeMiddleware && preview.routeMiddleware.length > 0) {
|
|
784
|
+
const mwResponse = await executeMiddleware(
|
|
785
|
+
buildRouteMiddlewareEntries<TEnv>(preview.routeMiddleware),
|
|
786
|
+
request,
|
|
787
|
+
env,
|
|
788
|
+
variables,
|
|
789
|
+
renderHandler,
|
|
790
|
+
routeReverse,
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
if (
|
|
794
|
+
url.searchParams.has("_rsc_partial") ||
|
|
795
|
+
url.searchParams.has("_rsc_action")
|
|
796
|
+
) {
|
|
797
|
+
const intercepted = interceptRedirectForPartial(
|
|
798
|
+
mwResponse,
|
|
799
|
+
createRedirectFlightResponse,
|
|
800
|
+
);
|
|
801
|
+
if (intercepted) return intercepted;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return finalizeResponse(mwResponse);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// No route middleware, proceed directly
|
|
808
|
+
return renderHandler();
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
const renderOutcome = await withTimeout(
|
|
812
|
+
executeRender(),
|
|
813
|
+
router.timeouts.renderStartMs,
|
|
814
|
+
"render-start",
|
|
815
|
+
);
|
|
816
|
+
if (renderOutcome.timedOut) {
|
|
817
|
+
return handleTimeoutResponse(
|
|
818
|
+
request,
|
|
819
|
+
env,
|
|
820
|
+
url,
|
|
821
|
+
"render-start",
|
|
822
|
+
renderOutcome.durationMs,
|
|
823
|
+
preview?.routeKey,
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return renderOutcome.result;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Inner request handler: rendering logic wrapped by route middleware.
|
|
830
|
+
// Handles action revalidation (when actionContinuation is present),
|
|
831
|
+
// loader fetches, and regular RSC rendering.
|
|
832
|
+
async function coreRequestHandlerInner(
|
|
833
|
+
request: Request,
|
|
834
|
+
env: TEnv,
|
|
835
|
+
url: URL,
|
|
836
|
+
variables: Record<string, any>,
|
|
837
|
+
nonce: string | undefined,
|
|
838
|
+
routeParams?: Record<string, string>,
|
|
839
|
+
routeKey?: string,
|
|
840
|
+
handleStore?: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
841
|
+
actionContinuation?: ActionContinuation,
|
|
842
|
+
): Promise<Response> {
|
|
843
|
+
const isPartial = url.searchParams.has("_rsc_partial");
|
|
844
|
+
const isAction =
|
|
845
|
+
request.headers.has("rsc-action") || url.searchParams.has("_rsc_action");
|
|
846
|
+
|
|
847
|
+
// Version mismatch detection - client may have stale code after HMR/deployment
|
|
848
|
+
// If versions don't match, tell the client to reload
|
|
849
|
+
const clientVersion = url.searchParams.get("_rsc_v");
|
|
850
|
+
if (version && clientVersion && clientVersion !== version) {
|
|
851
|
+
console.log(
|
|
852
|
+
`[RSC] Version mismatch: client=${clientVersion}, server=${version}. Forcing reload.`,
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
// For actions, reload current page (referer) if same origin.
|
|
856
|
+
// For navigation, load the target URL.
|
|
857
|
+
// Validate referer origin to prevent open redirect via crafted header.
|
|
858
|
+
let reloadUrl = stripInternalParams(url).toString();
|
|
859
|
+
if (isAction) {
|
|
860
|
+
const referer = request.headers.get("referer");
|
|
861
|
+
if (referer) {
|
|
862
|
+
try {
|
|
863
|
+
const refererUrl = new URL(referer);
|
|
864
|
+
if (refererUrl.origin === url.origin) {
|
|
865
|
+
reloadUrl = referer;
|
|
866
|
+
}
|
|
867
|
+
} catch {
|
|
868
|
+
// Malformed referer, fall back to cleanUrl
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Return special response that tells client to reload
|
|
874
|
+
return createResponseWithMergedHeaders(null, {
|
|
875
|
+
status: 200,
|
|
876
|
+
headers: {
|
|
877
|
+
"X-RSC-Reload": reloadUrl,
|
|
878
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
// Debug manifest endpoint: ?__debug_manifest on any route.
|
|
883
|
+
// Always available in dev, requires allowDebugManifest option in production.
|
|
884
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
885
|
+
if (
|
|
886
|
+
url.searchParams.has("__debug_manifest") &&
|
|
887
|
+
(isDev || router.allowDebugManifest)
|
|
888
|
+
) {
|
|
889
|
+
const trie = getRouterTrie(router.id) ?? getRouteTrie();
|
|
890
|
+
const routeManifest = getRequiredRouteMap();
|
|
891
|
+
const { extractAncestryFromTrie } =
|
|
892
|
+
await import("../build/route-trie.js");
|
|
893
|
+
return new Response(
|
|
894
|
+
JSON.stringify(
|
|
895
|
+
{
|
|
896
|
+
routerId: router.id,
|
|
897
|
+
routeManifest,
|
|
898
|
+
routeAncestry: trie ? extractAncestryFromTrie(trie) : {},
|
|
899
|
+
routeTrie: trie,
|
|
900
|
+
precomputedEntries: getPrecomputedEntries(),
|
|
901
|
+
},
|
|
902
|
+
null,
|
|
903
|
+
2,
|
|
904
|
+
),
|
|
905
|
+
{
|
|
906
|
+
headers: { "Content-Type": "application/json" },
|
|
907
|
+
},
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const store = handleStore ?? requireRequestContext()._handleStore;
|
|
912
|
+
|
|
913
|
+
try {
|
|
914
|
+
// Route params were already set in coreRequestHandler, but set again
|
|
915
|
+
// for callers that enter coreRequestHandlerInner directly.
|
|
916
|
+
if (routeParams) {
|
|
917
|
+
setRequestContextParams(routeParams, routeKey);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ============================================================================
|
|
921
|
+
// ACTION REVALIDATION (action already executed, revalidate segments)
|
|
922
|
+
// ============================================================================
|
|
923
|
+
if (actionContinuation) {
|
|
924
|
+
return await revalidateAfterAction(
|
|
925
|
+
handlerCtx,
|
|
926
|
+
request,
|
|
927
|
+
env,
|
|
928
|
+
url,
|
|
929
|
+
store,
|
|
930
|
+
actionContinuation,
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ============================================================================
|
|
935
|
+
// LOADER FETCH EXECUTION (data fetching with RSC serialization)
|
|
936
|
+
// ============================================================================
|
|
937
|
+
const isLoaderRequest = url.searchParams.has("_rsc_loader");
|
|
938
|
+
if (isLoaderRequest) {
|
|
939
|
+
return handleLoaderFetch(
|
|
940
|
+
handlerCtx,
|
|
941
|
+
request,
|
|
942
|
+
env,
|
|
943
|
+
url,
|
|
944
|
+
variables,
|
|
945
|
+
routeParams,
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ============================================================================
|
|
950
|
+
// REGULAR RSC RENDERING (Navigation)
|
|
951
|
+
// ============================================================================
|
|
952
|
+
// Note: Must use "return await" for try/catch to catch async rejections
|
|
953
|
+
return await handleRscRendering(
|
|
954
|
+
handlerCtx,
|
|
955
|
+
request,
|
|
956
|
+
env,
|
|
957
|
+
url,
|
|
958
|
+
isPartial,
|
|
959
|
+
store,
|
|
960
|
+
nonce,
|
|
961
|
+
);
|
|
962
|
+
} catch (error) {
|
|
963
|
+
// Check if middleware/handler returned Response
|
|
964
|
+
if (error instanceof Response) {
|
|
965
|
+
// During partial (client-side navigation), a 200 Response from a handler
|
|
966
|
+
// means the route serves raw content (JSON, text, etc.), not JSX.
|
|
967
|
+
// Signal the browser to hard-navigate so it renders the raw response.
|
|
968
|
+
// Only for 200 — redirects (3xx) work already because the browser follows
|
|
969
|
+
// them automatically to a URL that serves Flight data.
|
|
970
|
+
if (isPartial && error.status === 200) {
|
|
971
|
+
console.warn(
|
|
972
|
+
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
973
|
+
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
974
|
+
);
|
|
975
|
+
return createResponseWithMergedHeaders(null, {
|
|
976
|
+
status: 200,
|
|
977
|
+
headers: {
|
|
978
|
+
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
979
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (isPartial) {
|
|
985
|
+
const intercepted = interceptRedirectForPartial(
|
|
986
|
+
error,
|
|
987
|
+
createRedirectFlightResponse,
|
|
988
|
+
);
|
|
989
|
+
if (intercepted) return intercepted;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return error;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Render 404 page for unmatched routes
|
|
996
|
+
// Check both instanceof and error.name for cross-bundle compatibility
|
|
997
|
+
const isRouteNotFound =
|
|
998
|
+
error instanceof RouteNotFoundError ||
|
|
999
|
+
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1000
|
+
if (isRouteNotFound) {
|
|
1001
|
+
callOnError(error, "routing", {
|
|
1002
|
+
request,
|
|
1003
|
+
url,
|
|
1004
|
+
env,
|
|
1005
|
+
handledByBoundary: true, // Handled by notFound component
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
// Get notFound component from router options or use default
|
|
1009
|
+
const notFoundOption = router.notFound;
|
|
1010
|
+
const notFoundComponent =
|
|
1011
|
+
typeof notFoundOption === "function"
|
|
1012
|
+
? notFoundOption({ pathname: url.pathname })
|
|
1013
|
+
: (notFoundOption ?? createElement("h1", null, "Not Found"));
|
|
1014
|
+
|
|
1015
|
+
// Create a simple segment for the 404 page
|
|
1016
|
+
const notFoundSegment = {
|
|
1017
|
+
id: "notFound",
|
|
1018
|
+
namespace: "notFound",
|
|
1019
|
+
type: "route" as const,
|
|
1020
|
+
index: 0,
|
|
1021
|
+
component: notFoundComponent,
|
|
1022
|
+
params: {},
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const payload: RscPayload = {
|
|
1026
|
+
metadata: {
|
|
1027
|
+
pathname: url.pathname,
|
|
1028
|
+
segments: [notFoundSegment],
|
|
1029
|
+
matched: [],
|
|
1030
|
+
diff: [],
|
|
1031
|
+
isPartial: false,
|
|
1032
|
+
rootLayout: router.rootLayout,
|
|
1033
|
+
handles: store.stream(),
|
|
1034
|
+
version,
|
|
1035
|
+
themeConfig: router.themeConfig,
|
|
1036
|
+
warmupEnabled: router.warmupEnabled,
|
|
1037
|
+
initialTheme: requireRequestContext().theme,
|
|
1038
|
+
// No routeName for not-found routes
|
|
1039
|
+
},
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
const rscStream = renderToReadableStream(payload);
|
|
1043
|
+
|
|
1044
|
+
// Determine if this is an RSC request or HTML request.
|
|
1045
|
+
// Partial requests are always RSC (see main isRscRequest comment).
|
|
1046
|
+
const isRscRequest =
|
|
1047
|
+
isPartial ||
|
|
1048
|
+
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1049
|
+
!url.searchParams.has("__html")) ||
|
|
1050
|
+
url.searchParams.has("__rsc");
|
|
1051
|
+
|
|
1052
|
+
if (isRscRequest) {
|
|
1053
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
1054
|
+
status: 404,
|
|
1055
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
1060
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
1061
|
+
handlerCtx,
|
|
1062
|
+
request,
|
|
1063
|
+
env,
|
|
1064
|
+
url,
|
|
1065
|
+
requireRequestContext()._metricsStore,
|
|
1066
|
+
);
|
|
1067
|
+
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1068
|
+
nonce,
|
|
1069
|
+
streamMode,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
return createResponseWithMergedHeaders(htmlStream, {
|
|
1073
|
+
status: 404,
|
|
1074
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Report unhandled errors
|
|
1079
|
+
callOnError(error, "routing", {
|
|
1080
|
+
request,
|
|
1081
|
+
url,
|
|
1082
|
+
env,
|
|
1083
|
+
handledByBoundary: false,
|
|
1084
|
+
});
|
|
1085
|
+
console.error(`[RSC] Error:`, error);
|
|
1086
|
+
throw error;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|