@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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
8
8
|
import type { EntryData } from "../server/context";
|
|
9
|
+
import { debugLog, isRouterDebugEnabled } from "./logging.js";
|
|
10
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Parsed segment info
|
|
@@ -15,6 +17,7 @@ export interface ParsedSegment {
|
|
|
15
17
|
value: string; // static text, param name, or "*"
|
|
16
18
|
optional: boolean;
|
|
17
19
|
constraint?: string[]; // enum values like ["en", "gb"]
|
|
20
|
+
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -37,11 +40,22 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
37
40
|
// - :param(a|b)
|
|
38
41
|
// - :param(a|b)?
|
|
39
42
|
// - *
|
|
40
|
-
const segmentRegex =
|
|
43
|
+
const segmentRegex =
|
|
44
|
+
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
41
45
|
|
|
42
46
|
let match;
|
|
43
47
|
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
44
|
-
const [
|
|
48
|
+
const [
|
|
49
|
+
,
|
|
50
|
+
,
|
|
51
|
+
paramName,
|
|
52
|
+
,
|
|
53
|
+
constraint,
|
|
54
|
+
optional,
|
|
55
|
+
suffix,
|
|
56
|
+
wildcard,
|
|
57
|
+
staticText,
|
|
58
|
+
] = match;
|
|
45
59
|
|
|
46
60
|
if (wildcard) {
|
|
47
61
|
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
@@ -51,6 +65,7 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
51
65
|
value: paramName,
|
|
52
66
|
optional: optional === "?",
|
|
53
67
|
constraint: constraint ? constraint.split("|") : undefined,
|
|
68
|
+
suffix: suffix || undefined,
|
|
54
69
|
});
|
|
55
70
|
} else if (staticText) {
|
|
56
71
|
segments.push({ type: "static", value: staticText, optional: false });
|
|
@@ -60,6 +75,55 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
60
75
|
return segments;
|
|
61
76
|
}
|
|
62
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Compiled pattern result containing regex, param metadata, and trailing slash info.
|
|
80
|
+
*/
|
|
81
|
+
export interface CompiledPattern {
|
|
82
|
+
regex: RegExp;
|
|
83
|
+
paramNames: string[];
|
|
84
|
+
optionalParams: Set<string>;
|
|
85
|
+
hasTrailingSlash: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
|
|
88
|
+
* Validated against the **decoded** param value after regex extraction so
|
|
89
|
+
* a URL like `/en%20GB` still matches `:lang(en GB)` — matching the trie
|
|
90
|
+
* path's behavior (trie-matching.ts:validateAndBuild).
|
|
91
|
+
*/
|
|
92
|
+
constraints?: Record<string, string[]>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
96
|
+
// defined at build time, so this map is bounded by the number of routes.
|
|
97
|
+
const compiledPatternCache = new Map<string, CompiledPattern>();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get a compiled pattern from cache or compile and cache it.
|
|
101
|
+
* Avoids O(routes) regex compilations per request in the fallback path.
|
|
102
|
+
*/
|
|
103
|
+
export function getCompiledPattern(pattern: string): CompiledPattern {
|
|
104
|
+
let compiled = compiledPatternCache.get(pattern);
|
|
105
|
+
if (compiled) return compiled;
|
|
106
|
+
compiled = compilePattern(pattern);
|
|
107
|
+
compiledPatternCache.set(pattern, compiled);
|
|
108
|
+
return compiled;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Return the current size of the compiled pattern cache.
|
|
113
|
+
* Exposed for testing.
|
|
114
|
+
*/
|
|
115
|
+
export function getPatternCacheSize(): number {
|
|
116
|
+
return compiledPatternCache.size;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Clear the compiled pattern cache.
|
|
121
|
+
* Exposed for testing.
|
|
122
|
+
*/
|
|
123
|
+
export function clearPatternCache(): void {
|
|
124
|
+
compiledPatternCache.clear();
|
|
125
|
+
}
|
|
126
|
+
|
|
63
127
|
/**
|
|
64
128
|
* Compile a route pattern to regex
|
|
65
129
|
*
|
|
@@ -77,12 +141,7 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
77
141
|
* compilePattern("/:locale(en|gb)/blog") // matches /en/blog or /gb/blog
|
|
78
142
|
* compilePattern("/:locale(en|gb)?/blog") // matches /blog, /en/blog, or /gb/blog
|
|
79
143
|
*/
|
|
80
|
-
export function compilePattern(pattern: string): {
|
|
81
|
-
regex: RegExp;
|
|
82
|
-
paramNames: string[];
|
|
83
|
-
optionalParams: Set<string>;
|
|
84
|
-
hasTrailingSlash: boolean;
|
|
85
|
-
} {
|
|
144
|
+
export function compilePattern(pattern: string): CompiledPattern {
|
|
86
145
|
// Detect if pattern has trailing slash (but not just "/")
|
|
87
146
|
const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
88
147
|
// Remove trailing slash for parsing (we'll add it back to regex if needed)
|
|
@@ -91,6 +150,7 @@ export function compilePattern(pattern: string): {
|
|
|
91
150
|
const segments = parsePattern(normalizedPattern);
|
|
92
151
|
const paramNames: string[] = [];
|
|
93
152
|
const optionalParams = new Set<string>();
|
|
153
|
+
let constraints: Record<string, string[]> | undefined;
|
|
94
154
|
|
|
95
155
|
let regexPattern = "";
|
|
96
156
|
|
|
@@ -100,16 +160,22 @@ export function compilePattern(pattern: string): {
|
|
|
100
160
|
regexPattern += "/(.*)";
|
|
101
161
|
} else if (segment.type === "param") {
|
|
102
162
|
paramNames.push(segment.value);
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
163
|
+
const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
|
|
164
|
+
// Constrained params capture anything here; the allowed values are
|
|
165
|
+
// checked post-decode in findMatch so URL-encoded constraint values
|
|
166
|
+
// (e.g. `:lang(en GB)` via `/en%20GB`) still match.
|
|
167
|
+
const valuePattern = segment.suffix ? "([^/]+?)" : "([^/]+)";
|
|
168
|
+
|
|
169
|
+
if (segment.constraint) {
|
|
170
|
+
(constraints ??= {})[segment.value] = segment.constraint;
|
|
171
|
+
}
|
|
106
172
|
|
|
107
173
|
if (segment.optional) {
|
|
108
174
|
optionalParams.add(segment.value);
|
|
109
175
|
// Optional: make the whole /segment optional
|
|
110
|
-
regexPattern += `(?:/${valuePattern})?`;
|
|
176
|
+
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
111
177
|
} else {
|
|
112
|
-
regexPattern += `/${valuePattern}`;
|
|
178
|
+
regexPattern += `/${valuePattern}${suffixPattern}`;
|
|
113
179
|
}
|
|
114
180
|
} else {
|
|
115
181
|
// Static segment
|
|
@@ -122,6 +188,20 @@ export function compilePattern(pattern: string): {
|
|
|
122
188
|
regexPattern = "/";
|
|
123
189
|
}
|
|
124
190
|
|
|
191
|
+
// Patterns of only optional segments (e.g. `/:locale?`, `/:a?/:b?`) need
|
|
192
|
+
// an explicit `/` alternative so a bare `/` matches the absent form. The
|
|
193
|
+
// optional template `(?:/X)?` matches `/X` or empty string, but pathnames
|
|
194
|
+
// are never empty. Arises from `include("/:locale?", routes)` + inner
|
|
195
|
+
// `path("/")`. Skip when an explicit trailing slash already anchors the
|
|
196
|
+
// match.
|
|
197
|
+
const hasOnlyOptionalSegments =
|
|
198
|
+
!hasTrailingSlash &&
|
|
199
|
+
segments.length > 0 &&
|
|
200
|
+
segments.every((segment) => segment.type === "param" && segment.optional);
|
|
201
|
+
if (hasOnlyOptionalSegments) {
|
|
202
|
+
regexPattern = `(?:/|${regexPattern})`;
|
|
203
|
+
}
|
|
204
|
+
|
|
125
205
|
// Add trailing slash to regex if pattern has one
|
|
126
206
|
if (hasTrailingSlash) {
|
|
127
207
|
regexPattern += "/";
|
|
@@ -132,9 +212,35 @@ export function compilePattern(pattern: string): {
|
|
|
132
212
|
paramNames,
|
|
133
213
|
optionalParams,
|
|
134
214
|
hasTrailingSlash,
|
|
215
|
+
...(constraints ? { constraints } : {}),
|
|
135
216
|
};
|
|
136
217
|
}
|
|
137
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Validate decoded params against a compiled pattern's constraints.
|
|
221
|
+
* Returns false if any constrained param has a non-empty value not in the
|
|
222
|
+
* allowed list. Absent optionals (key missing or `undefined`) are allowed;
|
|
223
|
+
* `""` is also tolerated as "absent" so user-provided params or fixtures
|
|
224
|
+
* that pass empty strings explicitly behave the same way.
|
|
225
|
+
*/
|
|
226
|
+
function satisfiesConstraints(
|
|
227
|
+
params: Record<string, string>,
|
|
228
|
+
constraints: Record<string, string[]> | undefined,
|
|
229
|
+
): boolean {
|
|
230
|
+
if (!constraints) return true;
|
|
231
|
+
for (const name in constraints) {
|
|
232
|
+
const value = params[name];
|
|
233
|
+
if (
|
|
234
|
+
value !== undefined &&
|
|
235
|
+
value !== "" &&
|
|
236
|
+
!constraints[name].includes(value)
|
|
237
|
+
) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
138
244
|
/**
|
|
139
245
|
* Escape special regex characters in a string
|
|
140
246
|
*/
|
|
@@ -142,6 +248,27 @@ function escapeRegex(str: string): string {
|
|
|
142
248
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
143
249
|
}
|
|
144
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Build the named-params record from a regex match. Optional segments that
|
|
253
|
+
* didn't capture leave the corresponding group `undefined`; we skip those
|
|
254
|
+
* keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
|
|
255
|
+
* keeps the runtime aligned with the `ExtractParams` type and matches the
|
|
256
|
+
* trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
|
|
257
|
+
*/
|
|
258
|
+
function buildParamsFromMatch(
|
|
259
|
+
match: RegExpExecArray,
|
|
260
|
+
paramNames: string[],
|
|
261
|
+
): Record<string, string> {
|
|
262
|
+
const params: Record<string, string> = {};
|
|
263
|
+
paramNames.forEach((name, index) => {
|
|
264
|
+
const captured = match[index + 1];
|
|
265
|
+
if (captured !== undefined) {
|
|
266
|
+
params[name] = safeDecodeURIComponent(captured);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return params;
|
|
270
|
+
}
|
|
271
|
+
|
|
145
272
|
/**
|
|
146
273
|
* Extract the static prefix from a route pattern.
|
|
147
274
|
* Returns everything before the first param/wildcard.
|
|
@@ -193,8 +320,10 @@ export function extractStaticPrefix(pattern: string): string {
|
|
|
193
320
|
/**
|
|
194
321
|
* Match a pathname against registered routes
|
|
195
322
|
*
|
|
196
|
-
* Note: Optional params that are absent in the path
|
|
197
|
-
*
|
|
323
|
+
* Note: Optional params that are absent in the path are omitted from the
|
|
324
|
+
* returned `params` (read as `undefined`), matching the trie matcher and
|
|
325
|
+
* the `ExtractParams<"/:locale?/...">` type. Use the pattern definition or
|
|
326
|
+
* `optionalParams` to determine which keys are optional.
|
|
198
327
|
*
|
|
199
328
|
* Trailing slash handling (priority order):
|
|
200
329
|
* 1. Per-route `trailingSlash` config from route()
|
|
@@ -239,7 +368,7 @@ export interface LazyEvaluationNeeded<TEnv = any> {
|
|
|
239
368
|
* Type guard to check if result is a lazy evaluation needed response
|
|
240
369
|
*/
|
|
241
370
|
export function isLazyEvaluationNeeded<TEnv>(
|
|
242
|
-
result: RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null
|
|
371
|
+
result: RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null,
|
|
243
372
|
): result is LazyEvaluationNeeded<TEnv> {
|
|
244
373
|
return result !== null && "lazyEntry" in result;
|
|
245
374
|
}
|
|
@@ -260,22 +389,33 @@ export function enableMatchDebug(enabled: boolean): void {
|
|
|
260
389
|
}
|
|
261
390
|
|
|
262
391
|
export function getMatchDebugStats(): MatchDebugStats {
|
|
263
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
entriesChecked: debugStats.entriesChecked,
|
|
394
|
+
entriesSkipped: debugStats.entriesSkipped,
|
|
395
|
+
routesChecked: debugStats.routesChecked,
|
|
396
|
+
};
|
|
264
397
|
}
|
|
265
398
|
|
|
266
399
|
export function findMatch<TEnv>(
|
|
267
400
|
pathname: string,
|
|
268
|
-
routesEntries: RouteEntry<TEnv>[]
|
|
401
|
+
routesEntries: RouteEntry<TEnv>[],
|
|
269
402
|
): RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null {
|
|
270
|
-
|
|
403
|
+
const effectiveDebug = debugEnabled || isRouterDebugEnabled();
|
|
404
|
+
|
|
405
|
+
if (effectiveDebug) {
|
|
271
406
|
debugStats = { entriesChecked: 0, entriesSkipped: 0, routesChecked: 0 };
|
|
272
|
-
|
|
407
|
+
debugLog("findMatch", "start", { pathname, entries: routesEntries.length });
|
|
273
408
|
for (const e of routesEntries) {
|
|
274
|
-
|
|
409
|
+
debugLog("findMatch", "entry", {
|
|
410
|
+
prefix: e.prefix,
|
|
411
|
+
staticPrefix: e.staticPrefix,
|
|
412
|
+
routeCount: Object.keys(e.routes).length,
|
|
413
|
+
});
|
|
275
414
|
}
|
|
276
415
|
}
|
|
277
416
|
|
|
278
|
-
const pathnameHasTrailingSlash =
|
|
417
|
+
const pathnameHasTrailingSlash =
|
|
418
|
+
pathname.length > 1 && pathname.endsWith("/");
|
|
279
419
|
// Try alternate pathname for redirect matching
|
|
280
420
|
const alternatePathname = pathnameHasTrailingSlash
|
|
281
421
|
? pathname.slice(0, -1)
|
|
@@ -285,9 +425,12 @@ export function findMatch<TEnv>(
|
|
|
285
425
|
// Short-circuit: skip entry if pathname doesn't start with static prefix
|
|
286
426
|
// staticPrefix is pre-computed at registration time, so this is O(1)
|
|
287
427
|
if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
|
|
288
|
-
if (
|
|
428
|
+
if (effectiveDebug) {
|
|
289
429
|
debugStats.entriesSkipped++;
|
|
290
|
-
|
|
430
|
+
debugLog("findMatch", "skipped entry", {
|
|
431
|
+
prefix: entry.prefix,
|
|
432
|
+
staticPrefix: entry.staticPrefix,
|
|
433
|
+
});
|
|
291
434
|
}
|
|
292
435
|
continue;
|
|
293
436
|
}
|
|
@@ -295,20 +438,22 @@ export function findMatch<TEnv>(
|
|
|
295
438
|
// Check if this is a lazy entry that needs evaluation
|
|
296
439
|
// When staticPrefix matches but routes are not yet populated, signal caller to evaluate
|
|
297
440
|
if (entry.lazy && !entry.lazyEvaluated) {
|
|
298
|
-
if (
|
|
299
|
-
|
|
441
|
+
if (effectiveDebug) {
|
|
442
|
+
debugLog("findMatch", "lazy entry requires evaluation", {
|
|
443
|
+
staticPrefix: entry.staticPrefix,
|
|
444
|
+
});
|
|
300
445
|
}
|
|
301
446
|
return { lazyEntry: entry };
|
|
302
447
|
}
|
|
303
448
|
|
|
304
|
-
if (
|
|
449
|
+
if (effectiveDebug) {
|
|
305
450
|
debugStats.entriesChecked++;
|
|
306
451
|
}
|
|
307
452
|
|
|
308
453
|
const routeEntries = Object.entries(entry.routes);
|
|
309
454
|
|
|
310
455
|
for (const [routeKey, pattern] of routeEntries) {
|
|
311
|
-
if (
|
|
456
|
+
if (effectiveDebug) {
|
|
312
457
|
debugStats.routesChecked++;
|
|
313
458
|
}
|
|
314
459
|
|
|
@@ -322,66 +467,161 @@ export function findMatch<TEnv>(
|
|
|
322
467
|
fullPattern = entry.prefix + pattern;
|
|
323
468
|
}
|
|
324
469
|
|
|
325
|
-
const {
|
|
470
|
+
const {
|
|
471
|
+
regex,
|
|
472
|
+
paramNames,
|
|
473
|
+
optionalParams,
|
|
474
|
+
hasTrailingSlash,
|
|
475
|
+
constraints,
|
|
476
|
+
} = getCompiledPattern(fullPattern);
|
|
326
477
|
|
|
327
478
|
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
328
|
-
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
479
|
+
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
480
|
+
entry.trailingSlash?.[routeKey];
|
|
329
481
|
|
|
482
|
+
// Prerender flag from entry metadata (set by urls() for prerender handlers)
|
|
483
|
+
const prFlag = entry.prerenderRouteKeys?.has(routeKey)
|
|
484
|
+
? { pr: true as const }
|
|
485
|
+
: {};
|
|
486
|
+
const ptFlag = entry.passthroughRouteKeys?.has(routeKey)
|
|
487
|
+
? { pt: true as const }
|
|
488
|
+
: {};
|
|
330
489
|
|
|
331
490
|
// Try exact match first
|
|
332
491
|
const match = regex.exec(pathname);
|
|
333
492
|
if (match) {
|
|
334
|
-
const params
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
493
|
+
const params = buildParamsFromMatch(match, paramNames);
|
|
494
|
+
|
|
495
|
+
// Validate constraints against decoded values; a failure falls
|
|
496
|
+
// through to the next route so other patterns can still match.
|
|
497
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
338
500
|
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
501
|
+
if (effectiveDebug) {
|
|
502
|
+
debugLog("findMatch", "matched route", {
|
|
503
|
+
routeKey,
|
|
504
|
+
pattern: fullPattern,
|
|
505
|
+
stats: { ...debugStats },
|
|
506
|
+
});
|
|
342
507
|
}
|
|
343
508
|
|
|
344
509
|
// Check if trailing slash mode requires redirect even on exact match
|
|
345
|
-
if (
|
|
510
|
+
if (
|
|
511
|
+
trailingSlashMode === "always" &&
|
|
512
|
+
!pathnameHasTrailingSlash &&
|
|
513
|
+
pathname !== "/"
|
|
514
|
+
) {
|
|
346
515
|
// Mode says always have trailing slash, but pathname doesn't have it
|
|
347
|
-
return {
|
|
516
|
+
return {
|
|
517
|
+
entry,
|
|
518
|
+
routeKey,
|
|
519
|
+
params,
|
|
520
|
+
optionalParams,
|
|
521
|
+
redirectTo: pathname + "/",
|
|
522
|
+
...prFlag,
|
|
523
|
+
...ptFlag,
|
|
524
|
+
};
|
|
348
525
|
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
349
526
|
// Mode says never have trailing slash, but pathname has it
|
|
350
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
entry,
|
|
529
|
+
routeKey,
|
|
530
|
+
params,
|
|
531
|
+
optionalParams,
|
|
532
|
+
redirectTo: pathname.slice(0, -1),
|
|
533
|
+
...prFlag,
|
|
534
|
+
...ptFlag,
|
|
535
|
+
};
|
|
351
536
|
}
|
|
352
537
|
|
|
353
|
-
return {
|
|
538
|
+
return {
|
|
539
|
+
entry,
|
|
540
|
+
routeKey,
|
|
541
|
+
params,
|
|
542
|
+
optionalParams,
|
|
543
|
+
...prFlag,
|
|
544
|
+
...ptFlag,
|
|
545
|
+
};
|
|
354
546
|
}
|
|
355
547
|
|
|
356
548
|
// Try alternate pathname (opposite trailing slash)
|
|
357
549
|
const altMatch = regex.exec(alternatePathname);
|
|
358
550
|
if (altMatch) {
|
|
359
|
-
const params
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
551
|
+
const params = buildParamsFromMatch(altMatch, paramNames);
|
|
552
|
+
|
|
553
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
363
556
|
|
|
364
557
|
// Determine redirect behavior based on mode
|
|
365
558
|
if (trailingSlashMode === "ignore") {
|
|
366
559
|
// Match without redirect
|
|
367
|
-
return {
|
|
560
|
+
return {
|
|
561
|
+
entry,
|
|
562
|
+
routeKey,
|
|
563
|
+
params,
|
|
564
|
+
optionalParams,
|
|
565
|
+
...prFlag,
|
|
566
|
+
...ptFlag,
|
|
567
|
+
};
|
|
368
568
|
} else if (trailingSlashMode === "never") {
|
|
369
569
|
// Redirect to no trailing slash
|
|
370
570
|
if (pathnameHasTrailingSlash) {
|
|
371
|
-
return {
|
|
571
|
+
return {
|
|
572
|
+
entry,
|
|
573
|
+
routeKey,
|
|
574
|
+
params,
|
|
575
|
+
optionalParams,
|
|
576
|
+
redirectTo: alternatePathname,
|
|
577
|
+
...prFlag,
|
|
578
|
+
...ptFlag,
|
|
579
|
+
};
|
|
372
580
|
}
|
|
373
|
-
return {
|
|
581
|
+
return {
|
|
582
|
+
entry,
|
|
583
|
+
routeKey,
|
|
584
|
+
params,
|
|
585
|
+
optionalParams,
|
|
586
|
+
...prFlag,
|
|
587
|
+
...ptFlag,
|
|
588
|
+
};
|
|
374
589
|
} else if (trailingSlashMode === "always") {
|
|
375
590
|
// Redirect to with trailing slash
|
|
376
591
|
if (!pathnameHasTrailingSlash) {
|
|
377
|
-
return {
|
|
592
|
+
return {
|
|
593
|
+
entry,
|
|
594
|
+
routeKey,
|
|
595
|
+
params,
|
|
596
|
+
optionalParams,
|
|
597
|
+
redirectTo: alternatePathname,
|
|
598
|
+
...prFlag,
|
|
599
|
+
...ptFlag,
|
|
600
|
+
};
|
|
378
601
|
}
|
|
379
|
-
return {
|
|
602
|
+
return {
|
|
603
|
+
entry,
|
|
604
|
+
routeKey,
|
|
605
|
+
params,
|
|
606
|
+
optionalParams,
|
|
607
|
+
...prFlag,
|
|
608
|
+
...ptFlag,
|
|
609
|
+
};
|
|
380
610
|
} else {
|
|
381
611
|
// No explicit mode - use pattern-based detection
|
|
382
612
|
// Redirect to canonical form (what the pattern defines)
|
|
383
|
-
const canonicalPath = hasTrailingSlash
|
|
384
|
-
|
|
613
|
+
const canonicalPath = hasTrailingSlash
|
|
614
|
+
? alternatePathname
|
|
615
|
+
: pathname.slice(0, -1);
|
|
616
|
+
return {
|
|
617
|
+
entry,
|
|
618
|
+
routeKey,
|
|
619
|
+
params,
|
|
620
|
+
optionalParams,
|
|
621
|
+
redirectTo: canonicalPath,
|
|
622
|
+
...prFlag,
|
|
623
|
+
...ptFlag,
|
|
624
|
+
};
|
|
385
625
|
}
|
|
386
626
|
}
|
|
387
627
|
}
|