@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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 +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- 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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +342 -0
- package/src/cache/cache-scope.ts +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- 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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- 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 +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- 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/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +497 -0
- package/src/vite/router-discovery.ts +1423 -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 +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Telemetry Sink
|
|
3
|
+
*
|
|
4
|
+
* Internal event model for structured lifecycle events.
|
|
5
|
+
* The sink is optional and zero-cost when not configured.
|
|
6
|
+
*
|
|
7
|
+
* Emit points:
|
|
8
|
+
* - request.start / request.end (match-handlers.ts)
|
|
9
|
+
* - request.error (match-handlers.ts catch blocks)
|
|
10
|
+
* - request.origin-rejected (rsc/handler.ts origin guard)
|
|
11
|
+
* - loader.start / loader.end / loader.error (loader-resolution.ts)
|
|
12
|
+
* - handler.error (trackHandler catch, segment-resolution/helpers.ts)
|
|
13
|
+
* - cache.decision (cache-lookup middleware)
|
|
14
|
+
* - revalidation.decision (revalidation evaluation)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Event types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
interface BaseEvent {
|
|
22
|
+
/** Monotonic timestamp from performance.now() */
|
|
23
|
+
timestamp: number;
|
|
24
|
+
/** Request ID (from header or generated) */
|
|
25
|
+
requestId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RequestStartEvent extends BaseEvent {
|
|
29
|
+
type: "request.start";
|
|
30
|
+
method: string;
|
|
31
|
+
pathname: string;
|
|
32
|
+
/** "match" for full document requests, "matchPartial" for navigation */
|
|
33
|
+
transaction: "match" | "matchPartial";
|
|
34
|
+
isPartial: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RequestEndEvent extends BaseEvent {
|
|
38
|
+
type: "request.end";
|
|
39
|
+
method: string;
|
|
40
|
+
pathname: string;
|
|
41
|
+
transaction: "match" | "matchPartial";
|
|
42
|
+
durationMs: number;
|
|
43
|
+
segmentCount: number;
|
|
44
|
+
cacheHit: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RequestErrorEvent extends BaseEvent {
|
|
48
|
+
type: "request.error";
|
|
49
|
+
method: string;
|
|
50
|
+
pathname: string;
|
|
51
|
+
transaction: "match" | "matchPartial";
|
|
52
|
+
error: Error;
|
|
53
|
+
phase: string;
|
|
54
|
+
durationMs: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LoaderStartEvent extends BaseEvent {
|
|
58
|
+
type: "loader.start";
|
|
59
|
+
segmentId: string;
|
|
60
|
+
loaderName: string;
|
|
61
|
+
pathname: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LoaderEndEvent extends BaseEvent {
|
|
65
|
+
type: "loader.end";
|
|
66
|
+
segmentId: string;
|
|
67
|
+
loaderName: string;
|
|
68
|
+
pathname: string;
|
|
69
|
+
durationMs: number;
|
|
70
|
+
ok: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LoaderErrorEvent extends BaseEvent {
|
|
74
|
+
type: "loader.error";
|
|
75
|
+
segmentId: string;
|
|
76
|
+
loaderName: string;
|
|
77
|
+
pathname: string;
|
|
78
|
+
error: Error;
|
|
79
|
+
handledByBoundary: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface HandlerErrorEvent extends BaseEvent {
|
|
83
|
+
type: "handler.error";
|
|
84
|
+
segmentId?: string;
|
|
85
|
+
segmentType?: string;
|
|
86
|
+
error: Error;
|
|
87
|
+
handledByBoundary: boolean;
|
|
88
|
+
pathname?: string;
|
|
89
|
+
routeKey?: string;
|
|
90
|
+
params?: Record<string, string>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CacheDecisionEvent extends BaseEvent {
|
|
94
|
+
type: "cache.decision";
|
|
95
|
+
pathname: string;
|
|
96
|
+
routeKey: string;
|
|
97
|
+
hit: boolean;
|
|
98
|
+
/** Whether stale-while-revalidate was triggered */
|
|
99
|
+
shouldRevalidate: boolean;
|
|
100
|
+
source?: "runtime" | "prerender";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface RevalidationDecisionEvent extends BaseEvent {
|
|
104
|
+
type: "revalidation.decision";
|
|
105
|
+
segmentId: string;
|
|
106
|
+
pathname: string;
|
|
107
|
+
routeKey: string;
|
|
108
|
+
shouldRevalidate: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface RequestTimeoutEvent extends BaseEvent {
|
|
112
|
+
type: "request.timeout";
|
|
113
|
+
phase: import("./timeout.js").TimeoutPhase;
|
|
114
|
+
pathname: string;
|
|
115
|
+
routeKey?: string;
|
|
116
|
+
actionId?: string;
|
|
117
|
+
durationMs: number;
|
|
118
|
+
customHandler: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface OriginCheckRejectedEvent extends BaseEvent {
|
|
122
|
+
type: "request.origin-rejected";
|
|
123
|
+
method: string;
|
|
124
|
+
pathname: string;
|
|
125
|
+
phase: import("../rsc/origin-guard.js").OriginCheckPhase;
|
|
126
|
+
origin: string | null;
|
|
127
|
+
host: string | null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type TelemetryEvent =
|
|
131
|
+
| RequestStartEvent
|
|
132
|
+
| RequestEndEvent
|
|
133
|
+
| RequestErrorEvent
|
|
134
|
+
| LoaderStartEvent
|
|
135
|
+
| LoaderEndEvent
|
|
136
|
+
| LoaderErrorEvent
|
|
137
|
+
| HandlerErrorEvent
|
|
138
|
+
| CacheDecisionEvent
|
|
139
|
+
| RevalidationDecisionEvent
|
|
140
|
+
| RequestTimeoutEvent
|
|
141
|
+
| OriginCheckRejectedEvent;
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Sink interface
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Telemetry sink receives structured lifecycle events from the router.
|
|
149
|
+
* Implement this interface to integrate with any observability backend.
|
|
150
|
+
*
|
|
151
|
+
* All methods are fire-and-forget — exceptions are caught and logged.
|
|
152
|
+
*/
|
|
153
|
+
export interface TelemetrySink {
|
|
154
|
+
emit(event: TelemetryEvent): void;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// No-op singleton (zero-cost disabled state)
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
const noopSink: TelemetrySink = {
|
|
162
|
+
emit() {},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Returns the configured sink, or the no-op singleton.
|
|
167
|
+
* Call sites use this so they don't need null checks.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveSink(sink: TelemetrySink | undefined): TelemetrySink {
|
|
170
|
+
return sink ?? noopSink;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Safe emit — catches any error thrown by the sink to prevent
|
|
175
|
+
* telemetry failures from affecting request handling.
|
|
176
|
+
*/
|
|
177
|
+
export function safeEmit(sink: TelemetrySink, event: TelemetryEvent): void {
|
|
178
|
+
try {
|
|
179
|
+
sink.emit(event);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
// Telemetry must never break request handling
|
|
182
|
+
if (process.env.NODE_ENV !== "production") {
|
|
183
|
+
console.error("[Router.telemetry] Sink error:", e);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Request ID extraction (for span correlation)
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
// Per-request memoization so the same Request object always maps to the
|
|
193
|
+
// same ID. WeakMap allows GC when the Request is no longer referenced.
|
|
194
|
+
const requestIds = new WeakMap<Request, string>();
|
|
195
|
+
let telemetryRequestCounter = 0;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Get or create a request ID for telemetry correlation.
|
|
199
|
+
* Checks standard headers first (x-rsc-router-request-id, x-request-id,
|
|
200
|
+
* cf-ray), then generates an internal ID when none is present.
|
|
201
|
+
* Generated IDs use format "t-{base36}" to distinguish from header values.
|
|
202
|
+
*/
|
|
203
|
+
export function getRequestId(request: Request): string {
|
|
204
|
+
const existing = requestIds.get(request);
|
|
205
|
+
if (existing) return existing;
|
|
206
|
+
|
|
207
|
+
const candidate =
|
|
208
|
+
request.headers.get("x-rsc-router-request-id") ??
|
|
209
|
+
request.headers.get("x-request-id") ??
|
|
210
|
+
request.headers.get("cf-ray");
|
|
211
|
+
|
|
212
|
+
let id: string;
|
|
213
|
+
if (candidate) {
|
|
214
|
+
const trimmed = candidate.trim();
|
|
215
|
+
id =
|
|
216
|
+
trimmed.length > 0
|
|
217
|
+
? trimmed
|
|
218
|
+
: `t-${(++telemetryRequestCounter).toString(36)}`;
|
|
219
|
+
} else {
|
|
220
|
+
id = `t-${(++telemetryRequestCounter).toString(36)}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
requestIds.set(request, id);
|
|
224
|
+
return id;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// Console sink (built-in, replaces ad-hoc console.log debug traces)
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Built-in console sink that logs events in a structured format.
|
|
233
|
+
* Designed as the default sink for development / debugging.
|
|
234
|
+
*/
|
|
235
|
+
export function createConsoleSink(): TelemetrySink {
|
|
236
|
+
return {
|
|
237
|
+
emit(event: TelemetryEvent): void {
|
|
238
|
+
switch (event.type) {
|
|
239
|
+
case "request.start":
|
|
240
|
+
console.log(
|
|
241
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} (${event.transaction})`,
|
|
242
|
+
);
|
|
243
|
+
break;
|
|
244
|
+
case "request.end":
|
|
245
|
+
console.log(
|
|
246
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} ${event.durationMs.toFixed(1)}ms segments=${event.segmentCount} cache=${event.cacheHit}`,
|
|
247
|
+
);
|
|
248
|
+
break;
|
|
249
|
+
case "request.error":
|
|
250
|
+
console.log(
|
|
251
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} phase=${event.phase} ${event.durationMs.toFixed(1)}ms`,
|
|
252
|
+
event.error.message,
|
|
253
|
+
);
|
|
254
|
+
break;
|
|
255
|
+
case "loader.start":
|
|
256
|
+
console.log(
|
|
257
|
+
`[telemetry] ${event.type} ${event.loaderName} (${event.segmentId})`,
|
|
258
|
+
);
|
|
259
|
+
break;
|
|
260
|
+
case "loader.end":
|
|
261
|
+
console.log(
|
|
262
|
+
`[telemetry] ${event.type} ${event.loaderName} ${event.durationMs.toFixed(1)}ms ok=${event.ok}`,
|
|
263
|
+
);
|
|
264
|
+
break;
|
|
265
|
+
case "loader.error":
|
|
266
|
+
console.log(
|
|
267
|
+
`[telemetry] ${event.type} ${event.loaderName} boundary=${event.handledByBoundary}`,
|
|
268
|
+
event.error.message,
|
|
269
|
+
);
|
|
270
|
+
break;
|
|
271
|
+
case "handler.error":
|
|
272
|
+
console.log(
|
|
273
|
+
`[telemetry] ${event.type} segment=${event.segmentId ?? "unknown"} boundary=${event.handledByBoundary}${event.pathname ? ` ${event.pathname}` : ""}`,
|
|
274
|
+
event.error.message,
|
|
275
|
+
);
|
|
276
|
+
break;
|
|
277
|
+
case "cache.decision":
|
|
278
|
+
console.log(
|
|
279
|
+
`[telemetry] ${event.type} ${event.pathname} hit=${event.hit} swr=${event.shouldRevalidate}${event.source ? ` source=${event.source}` : ""}`,
|
|
280
|
+
);
|
|
281
|
+
break;
|
|
282
|
+
case "revalidation.decision":
|
|
283
|
+
console.log(
|
|
284
|
+
`[telemetry] ${event.type} ${event.segmentId} revalidate=${event.shouldRevalidate}`,
|
|
285
|
+
);
|
|
286
|
+
break;
|
|
287
|
+
case "request.timeout":
|
|
288
|
+
console.log(
|
|
289
|
+
`[telemetry] ${event.type} phase=${event.phase} ${event.pathname} ${event.durationMs.toFixed(1)}ms custom=${event.customHandler}`,
|
|
290
|
+
);
|
|
291
|
+
break;
|
|
292
|
+
case "request.origin-rejected":
|
|
293
|
+
console.log(
|
|
294
|
+
`[telemetry] ${event.type} ${event.method} ${event.pathname} phase=${event.phase} origin=${event.origin ?? "none"} host=${event.host ?? "none"}`,
|
|
295
|
+
);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Timeout
|
|
3
|
+
*
|
|
4
|
+
* Types, resolution logic, and helpers for request-level timeouts.
|
|
5
|
+
* Timeouts wrap action execution and render-start phases with
|
|
6
|
+
* a Promise.race mechanism, returning 504 on expiry.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Public types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface RouterTimeouts {
|
|
14
|
+
/** Timeout for server action execution (ms). */
|
|
15
|
+
actionMs?: number;
|
|
16
|
+
/** Timeout for initial render/response production (ms). */
|
|
17
|
+
renderStartMs?: number;
|
|
18
|
+
/** Timeout for idle streaming after render starts (ms). Reserved for PR 2. */
|
|
19
|
+
streamIdleMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type TimeoutPhase = "action" | "render-start" | "stream-idle";
|
|
23
|
+
|
|
24
|
+
export interface TimeoutContext<TEnv = any> {
|
|
25
|
+
phase: TimeoutPhase;
|
|
26
|
+
request: Request;
|
|
27
|
+
url: URL;
|
|
28
|
+
env: TEnv;
|
|
29
|
+
routeKey?: string;
|
|
30
|
+
actionId?: string;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type OnTimeoutCallback<TEnv = any> = (
|
|
35
|
+
ctx: TimeoutContext<TEnv>,
|
|
36
|
+
) => Response | Promise<Response>;
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Internal resolved form
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export interface ResolvedTimeouts {
|
|
43
|
+
actionMs: number | undefined;
|
|
44
|
+
renderStartMs: number | undefined;
|
|
45
|
+
streamIdleMs: number | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge the `timeout` shorthand with the structured `timeouts` object.
|
|
50
|
+
*
|
|
51
|
+
* - `timeout` applies to `actionMs` and `renderStartMs` (NOT `streamIdleMs`).
|
|
52
|
+
* - Explicit `timeouts.*` values override the shorthand.
|
|
53
|
+
* - Returns `undefined` for any phase that has no configured value.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveTimeouts(
|
|
56
|
+
timeout?: number,
|
|
57
|
+
timeouts?: RouterTimeouts,
|
|
58
|
+
): ResolvedTimeouts {
|
|
59
|
+
return {
|
|
60
|
+
actionMs: timeouts?.actionMs ?? timeout ?? undefined,
|
|
61
|
+
renderStartMs: timeouts?.renderStartMs ?? timeout ?? undefined,
|
|
62
|
+
streamIdleMs: timeouts?.streamIdleMs ?? undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Error class
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
export class RouterTimeoutError extends Error {
|
|
71
|
+
override name = "RouterTimeoutError" as const;
|
|
72
|
+
phase: TimeoutPhase;
|
|
73
|
+
durationMs: number;
|
|
74
|
+
|
|
75
|
+
constructor(phase: TimeoutPhase, durationMs: number) {
|
|
76
|
+
super(
|
|
77
|
+
`Request timed out during ${phase} after ${Math.round(durationMs)}ms`,
|
|
78
|
+
);
|
|
79
|
+
this.phase = phase;
|
|
80
|
+
this.durationMs = durationMs;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Race helper
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
type TimeoutResult<T> =
|
|
89
|
+
| { result: T; timedOut: false }
|
|
90
|
+
| { timedOut: true; durationMs: number };
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Race an operation against a deadline.
|
|
94
|
+
*
|
|
95
|
+
* Returns a discriminated union so callers handle the timeout case
|
|
96
|
+
* without try/catch. Non-timeout errors from the operation re-throw.
|
|
97
|
+
*
|
|
98
|
+
* When `timeoutMs` is `undefined` or `<= 0`, the operation runs
|
|
99
|
+
* without any deadline (pass-through).
|
|
100
|
+
*/
|
|
101
|
+
export async function withTimeout<T>(
|
|
102
|
+
operation: Promise<T>,
|
|
103
|
+
timeoutMs: number | undefined,
|
|
104
|
+
phase: TimeoutPhase,
|
|
105
|
+
): Promise<TimeoutResult<T>> {
|
|
106
|
+
if (timeoutMs == null || timeoutMs <= 0) {
|
|
107
|
+
return { result: await operation, timedOut: false };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const start = performance.now();
|
|
111
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
112
|
+
|
|
113
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
reject(new RouterTimeoutError(phase, performance.now() - start));
|
|
116
|
+
}, timeoutMs);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const result = await Promise.race([operation, timeoutPromise]);
|
|
121
|
+
clearTimeout(timer!);
|
|
122
|
+
return { result, timedOut: false };
|
|
123
|
+
} catch (error) {
|
|
124
|
+
clearTimeout(timer!);
|
|
125
|
+
if (error instanceof RouterTimeoutError) {
|
|
126
|
+
return { timedOut: true, durationMs: error.durationMs };
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Default response
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create the default 504 response for a timed-out request.
|
|
138
|
+
* Includes `X-Rango-Timeout-Phase` header for observability.
|
|
139
|
+
*/
|
|
140
|
+
export function createDefaultTimeoutResponse(phase: TimeoutPhase): Response {
|
|
141
|
+
return new Response("Request timed out", {
|
|
142
|
+
status: 504,
|
|
143
|
+
headers: {
|
|
144
|
+
"Content-Type": "text/plain;charset=utf-8",
|
|
145
|
+
"X-Rango-Timeout-Phase": phase,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { TrieNode, TrieLeaf } from "../build/route-trie.js";
|
|
9
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
9
10
|
|
|
10
11
|
export interface TrieMatchResult {
|
|
11
12
|
/** Route name */
|
|
@@ -14,7 +15,9 @@ export interface TrieMatchResult {
|
|
|
14
15
|
sp: string;
|
|
15
16
|
/** Matched route params */
|
|
16
17
|
params: Record<string, string>;
|
|
17
|
-
/** Optional param names
|
|
18
|
+
/** Optional param names declared on the route. Absent params are omitted
|
|
19
|
+
* from `params` (read as `undefined`), matching the
|
|
20
|
+
* `ExtractParams<"/:locale?/...">` type. */
|
|
18
21
|
optionalParams?: string[];
|
|
19
22
|
/** Ancestry shortCodes for layout pruning */
|
|
20
23
|
ancestry: string[];
|
|
@@ -43,13 +46,22 @@ export function tryTrieMatch(
|
|
|
43
46
|
if (!trie) return null;
|
|
44
47
|
|
|
45
48
|
// Split pathname into segments, filtering empty strings from leading/trailing slashes
|
|
46
|
-
const pathnameHasTrailingSlash =
|
|
47
|
-
|
|
49
|
+
const pathnameHasTrailingSlash =
|
|
50
|
+
pathname.length > 1 && pathname.endsWith("/");
|
|
51
|
+
const normalizedPath = pathnameHasTrailingSlash
|
|
52
|
+
? pathname.slice(0, -1)
|
|
53
|
+
: pathname;
|
|
48
54
|
|
|
49
55
|
// Handle root path
|
|
50
56
|
if (normalizedPath === "" || normalizedPath === "/") {
|
|
51
57
|
if (trie.r) {
|
|
52
|
-
return validateAndBuild(
|
|
58
|
+
return validateAndBuild(
|
|
59
|
+
trie.r,
|
|
60
|
+
[],
|
|
61
|
+
undefined,
|
|
62
|
+
pathname,
|
|
63
|
+
pathnameHasTrailingSlash,
|
|
64
|
+
);
|
|
53
65
|
}
|
|
54
66
|
return null;
|
|
55
67
|
}
|
|
@@ -58,9 +70,15 @@ export function tryTrieMatch(
|
|
|
58
70
|
const segments = normalizedPath.slice(1).split("/");
|
|
59
71
|
|
|
60
72
|
// Try exact match with normalized path (no trailing slash)
|
|
61
|
-
const result = walkTrie(trie, segments, 0,
|
|
73
|
+
const result = walkTrie(trie, segments, 0, []);
|
|
62
74
|
if (result) {
|
|
63
|
-
return validateAndBuild(
|
|
75
|
+
return validateAndBuild(
|
|
76
|
+
result.leaf,
|
|
77
|
+
result.paramValues,
|
|
78
|
+
result.wildcardValue,
|
|
79
|
+
pathname,
|
|
80
|
+
pathnameHasTrailingSlash,
|
|
81
|
+
);
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
return null;
|
|
@@ -68,7 +86,8 @@ export function tryTrieMatch(
|
|
|
68
86
|
|
|
69
87
|
interface WalkResult {
|
|
70
88
|
leaf: TrieLeaf;
|
|
71
|
-
|
|
89
|
+
paramValues: string[];
|
|
90
|
+
wildcardValue?: string;
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
/**
|
|
@@ -79,57 +98,106 @@ function walkTrie(
|
|
|
79
98
|
node: TrieNode,
|
|
80
99
|
segments: string[],
|
|
81
100
|
index: number,
|
|
82
|
-
|
|
101
|
+
paramValues: string[],
|
|
83
102
|
): WalkResult | null {
|
|
84
103
|
// All segments consumed: check for terminal
|
|
85
104
|
if (index === segments.length) {
|
|
86
105
|
if (node.r) {
|
|
87
|
-
return { leaf: node.r,
|
|
106
|
+
return { leaf: node.r, paramValues: [...paramValues] };
|
|
88
107
|
}
|
|
89
108
|
return null;
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
const segment = segments[index];
|
|
112
|
+
const staticChild = node.s?.[segment];
|
|
93
113
|
|
|
94
114
|
// Priority 1: Static match
|
|
95
|
-
if (
|
|
96
|
-
const result = walkTrie(
|
|
115
|
+
if (staticChild) {
|
|
116
|
+
const result = walkTrie(staticChild, segments, index + 1, paramValues);
|
|
97
117
|
if (result) return result;
|
|
98
118
|
}
|
|
99
119
|
|
|
100
|
-
// Priority 2:
|
|
120
|
+
// Priority 2: Suffix-param match (e.g., :productId.html)
|
|
121
|
+
if (node.xp) {
|
|
122
|
+
for (const suffix in node.xp) {
|
|
123
|
+
if (segment.endsWith(suffix) && segment.length > suffix.length) {
|
|
124
|
+
const paramValue = segment.slice(0, -suffix.length);
|
|
125
|
+
paramValues.push(paramValue);
|
|
126
|
+
const result = walkTrie(
|
|
127
|
+
node.xp[suffix].c,
|
|
128
|
+
segments,
|
|
129
|
+
index + 1,
|
|
130
|
+
paramValues,
|
|
131
|
+
);
|
|
132
|
+
paramValues.pop();
|
|
133
|
+
if (result) return result;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Priority 3: Param match
|
|
101
139
|
if (node.p) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
140
|
+
paramValues.push(segment);
|
|
141
|
+
const result = walkTrie(node.p.c, segments, index + 1, paramValues);
|
|
142
|
+
paramValues.pop();
|
|
106
143
|
if (result) return result;
|
|
107
144
|
}
|
|
108
145
|
|
|
109
|
-
// Priority
|
|
146
|
+
// Priority 4: Wildcard match (consumes rest)
|
|
110
147
|
if (node.w) {
|
|
111
|
-
const rest = segments
|
|
148
|
+
const rest = joinRemainingSegments(segments, index);
|
|
112
149
|
return {
|
|
113
150
|
leaf: node.w,
|
|
114
|
-
|
|
151
|
+
paramValues: [...paramValues],
|
|
152
|
+
wildcardValue: rest,
|
|
115
153
|
};
|
|
116
154
|
}
|
|
117
155
|
|
|
118
156
|
return null;
|
|
119
157
|
}
|
|
120
158
|
|
|
159
|
+
function joinRemainingSegments(segments: string[], start: number): string {
|
|
160
|
+
if (start >= segments.length) return "";
|
|
161
|
+
|
|
162
|
+
let rest = segments[start]!;
|
|
163
|
+
for (let i = start + 1; i < segments.length; i++) {
|
|
164
|
+
rest += "/" + segments[i]!;
|
|
165
|
+
}
|
|
166
|
+
return rest;
|
|
167
|
+
}
|
|
168
|
+
|
|
121
169
|
/**
|
|
122
170
|
* Post-match: validate constraints and handle trailing slash logic.
|
|
123
171
|
*/
|
|
124
172
|
function validateAndBuild(
|
|
125
173
|
leaf: TrieLeaf,
|
|
126
|
-
|
|
174
|
+
paramValues: string[],
|
|
175
|
+
wildcardValue: string | undefined,
|
|
127
176
|
originalPathname: string,
|
|
128
177
|
pathnameHasTrailingSlash: boolean,
|
|
129
178
|
): TrieMatchResult | null {
|
|
130
|
-
//
|
|
179
|
+
// Build named params by zipping leaf.pa with positional paramValues.
|
|
180
|
+
// Params are URL-decoded at this boundary so ctx.params holds the values
|
|
181
|
+
// apps expect (matching Express/React Router) and round-trip cleanly
|
|
182
|
+
// through ctx.reverse.
|
|
183
|
+
const params: Record<string, string> = {};
|
|
184
|
+
if (leaf.pa) {
|
|
185
|
+
for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
|
|
186
|
+
params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Add wildcard param (wildcard leaves have pn from TrieNode.w type)
|
|
191
|
+
if (wildcardValue !== undefined && "pn" in leaf) {
|
|
192
|
+
params[(leaf as TrieLeaf & { pn: string }).pn] =
|
|
193
|
+
safeDecodeURIComponent(wildcardValue);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Validate constraints against decoded values so constraint lists can be
|
|
197
|
+
// written in decoded form (e.g. ["en-GB", "en US"]).
|
|
131
198
|
if (leaf.cv) {
|
|
132
|
-
for (const
|
|
199
|
+
for (const paramName in leaf.cv) {
|
|
200
|
+
const allowed = leaf.cv[paramName]!;
|
|
133
201
|
const value = params[paramName];
|
|
134
202
|
if (value !== undefined && value !== "" && !allowed.includes(value)) {
|
|
135
203
|
return null;
|
|
@@ -137,36 +205,40 @@ function validateAndBuild(
|
|
|
137
205
|
}
|
|
138
206
|
}
|
|
139
207
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
208
|
+
// Optional params that weren't matched are left absent from `params` so
|
|
209
|
+
// `ctx.params.locale` reads as `undefined`, matching the
|
|
210
|
+
// `ExtractParams<"/:locale?/...">` type (`{ locale?: string }`). Both
|
|
211
|
+
// internal consumers — the constraint check above and `reverse()` —
|
|
212
|
+
// already treat missing/undefined as the absent form.
|
|
148
213
|
|
|
149
214
|
// Trailing slash handling
|
|
150
215
|
const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
|
|
151
216
|
let redirectTo: string | undefined;
|
|
152
217
|
|
|
153
|
-
if (
|
|
218
|
+
if (
|
|
219
|
+
tsMode === "always" &&
|
|
220
|
+
!pathnameHasTrailingSlash &&
|
|
221
|
+
originalPathname !== "/"
|
|
222
|
+
) {
|
|
154
223
|
redirectTo = originalPathname + "/";
|
|
155
224
|
} else if (tsMode === "never" && pathnameHasTrailingSlash) {
|
|
156
225
|
redirectTo = originalPathname.slice(0, -1);
|
|
157
226
|
}
|
|
158
227
|
|
|
159
|
-
|
|
228
|
+
const result: TrieMatchResult = {
|
|
160
229
|
routeKey: leaf.n,
|
|
161
230
|
sp: leaf.sp,
|
|
162
231
|
params,
|
|
163
|
-
optionalParams: leaf.op,
|
|
164
232
|
ancestry: leaf.a,
|
|
165
|
-
...(redirectTo ? { redirectTo } : {}),
|
|
166
|
-
...(leaf.pr ? { pr: true } : {}),
|
|
167
|
-
...(leaf.pt ? { pt: true } : {}),
|
|
168
|
-
...(leaf.rt ? { responseType: leaf.rt } : {}),
|
|
169
|
-
...(leaf.nv ? { negotiateVariants: leaf.nv } : {}),
|
|
170
|
-
...(leaf.rf ? { rscFirst: true } : {}),
|
|
171
233
|
};
|
|
234
|
+
|
|
235
|
+
if (leaf.op) result.optionalParams = leaf.op;
|
|
236
|
+
if (redirectTo) result.redirectTo = redirectTo;
|
|
237
|
+
if (leaf.pr) result.pr = true;
|
|
238
|
+
if (leaf.pt) result.pt = true;
|
|
239
|
+
if (leaf.rt) result.responseType = leaf.rt;
|
|
240
|
+
if (leaf.nv) result.negotiateVariants = leaf.nv;
|
|
241
|
+
if (leaf.rf) result.rscFirst = true;
|
|
242
|
+
|
|
243
|
+
return result;
|
|
172
244
|
}
|