@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.30
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 +5 -0
- package/README.md +883 -4
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +4655 -747
- package/package.json +78 -50
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +54 -25
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +23 -21
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +390 -63
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +133 -10
- package/skills/layout/SKILL.md +102 -5
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +366 -29
- package/skills/middleware/SKILL.md +173 -36
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +80 -3
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +86 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +227 -14
- package/skills/router-setup/SKILL.md +225 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +12 -11
- package/skills/typesafety/SKILL.md +401 -75
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +10 -4
- 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 +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +20 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +201 -553
- package/src/browser/navigation-client.ts +124 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +267 -317
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +173 -73
- package/src/browser/react/NavigationProvider.tsx +138 -27
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -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 +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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +49 -65
- package/src/browser/react/use-href.tsx +20 -188
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +27 -78
- 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 +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +111 -26
- package/src/browser/scroll-restoration.ts +92 -16
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +504 -584
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +92 -57
- 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 +469 -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 +120 -303
- package/src/cache/cf/cf-cache-store.ts +119 -7
- package/src/cache/cf/index.ts +8 -2
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +0 -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 +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +10 -15
- package/src/client.tsx +114 -135
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +34 -19
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/meta.ts +30 -13
- 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 +135 -49
- package/src/index.rsc.ts +182 -17
- package/src/index.ts +238 -24
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +27 -142
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- 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 +41 -29
- package/src/route-content-wrapper.tsx +9 -11
- 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 -1388
- package/src/route-map-builder.ts +241 -112
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +70 -9
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +371 -81
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +155 -32
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +24 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -29
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +324 -367
- package/src/router/pattern-matching.ts +321 -30
- package/src/router/prerender-match.ts +400 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +36 -21
- 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 +1241 -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 +289 -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 +77 -3
- package/src/router.ts +688 -3656
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +786 -760
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +5 -25
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -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 +235 -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 +40 -14
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +57 -61
- package/src/server/context.ts +202 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +422 -70
- package/src/server.ts +36 -120
- package/src/ssr/index.tsx +157 -26
- package/src/static-handler.ts +114 -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 +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 +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1577
- 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 -726
- package/src/use-loader.tsx +85 -77
- 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 +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +131 -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/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
- 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 +254 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -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/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -3
- 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/href.ts +0 -255
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -357
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -6,15 +6,17 @@
|
|
|
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";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Parsed segment info
|
|
12
13
|
*/
|
|
13
|
-
interface ParsedSegment {
|
|
14
|
+
export interface ParsedSegment {
|
|
14
15
|
type: "static" | "param" | "wildcard";
|
|
15
16
|
value: string; // static text, param name, or "*"
|
|
16
17
|
optional: boolean;
|
|
17
18
|
constraint?: string[]; // enum values like ["en", "gb"]
|
|
19
|
+
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -28,7 +30,7 @@ interface ParsedSegment {
|
|
|
28
30
|
* - Optional + Constrained: /:locale(en|gb)?
|
|
29
31
|
* - Wildcard: /*
|
|
30
32
|
*/
|
|
31
|
-
function parsePattern(pattern: string): ParsedSegment[] {
|
|
33
|
+
export function parsePattern(pattern: string): ParsedSegment[] {
|
|
32
34
|
const segments: ParsedSegment[] = [];
|
|
33
35
|
// Match: /segment where segment can be:
|
|
34
36
|
// - static text
|
|
@@ -37,11 +39,22 @@ function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
37
39
|
// - :param(a|b)
|
|
38
40
|
// - :param(a|b)?
|
|
39
41
|
// - *
|
|
40
|
-
const segmentRegex =
|
|
42
|
+
const segmentRegex =
|
|
43
|
+
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
41
44
|
|
|
42
45
|
let match;
|
|
43
46
|
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
44
|
-
const [
|
|
47
|
+
const [
|
|
48
|
+
,
|
|
49
|
+
,
|
|
50
|
+
paramName,
|
|
51
|
+
,
|
|
52
|
+
constraint,
|
|
53
|
+
optional,
|
|
54
|
+
suffix,
|
|
55
|
+
wildcard,
|
|
56
|
+
staticText,
|
|
57
|
+
] = match;
|
|
45
58
|
|
|
46
59
|
if (wildcard) {
|
|
47
60
|
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
@@ -51,6 +64,7 @@ function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
51
64
|
value: paramName,
|
|
52
65
|
optional: optional === "?",
|
|
53
66
|
constraint: constraint ? constraint.split("|") : undefined,
|
|
67
|
+
suffix: suffix || undefined,
|
|
54
68
|
});
|
|
55
69
|
} else if (staticText) {
|
|
56
70
|
segments.push({ type: "static", value: staticText, optional: false });
|
|
@@ -60,6 +74,48 @@ function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
60
74
|
return segments;
|
|
61
75
|
}
|
|
62
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Compiled pattern result containing regex, param metadata, and trailing slash info.
|
|
79
|
+
*/
|
|
80
|
+
export interface CompiledPattern {
|
|
81
|
+
regex: RegExp;
|
|
82
|
+
paramNames: string[];
|
|
83
|
+
optionalParams: Set<string>;
|
|
84
|
+
hasTrailingSlash: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
88
|
+
// defined at build time, so this map is bounded by the number of routes.
|
|
89
|
+
const compiledPatternCache = new Map<string, CompiledPattern>();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a compiled pattern from cache or compile and cache it.
|
|
93
|
+
* Avoids O(routes) regex compilations per request in the fallback path.
|
|
94
|
+
*/
|
|
95
|
+
export function getCompiledPattern(pattern: string): CompiledPattern {
|
|
96
|
+
let compiled = compiledPatternCache.get(pattern);
|
|
97
|
+
if (compiled) return compiled;
|
|
98
|
+
compiled = compilePattern(pattern);
|
|
99
|
+
compiledPatternCache.set(pattern, compiled);
|
|
100
|
+
return compiled;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Return the current size of the compiled pattern cache.
|
|
105
|
+
* Exposed for testing.
|
|
106
|
+
*/
|
|
107
|
+
export function getPatternCacheSize(): number {
|
|
108
|
+
return compiledPatternCache.size;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Clear the compiled pattern cache.
|
|
113
|
+
* Exposed for testing.
|
|
114
|
+
*/
|
|
115
|
+
export function clearPatternCache(): void {
|
|
116
|
+
compiledPatternCache.clear();
|
|
117
|
+
}
|
|
118
|
+
|
|
63
119
|
/**
|
|
64
120
|
* Compile a route pattern to regex
|
|
65
121
|
*
|
|
@@ -77,12 +133,7 @@ function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
77
133
|
* compilePattern("/:locale(en|gb)/blog") // matches /en/blog or /gb/blog
|
|
78
134
|
* compilePattern("/:locale(en|gb)?/blog") // matches /blog, /en/blog, or /gb/blog
|
|
79
135
|
*/
|
|
80
|
-
export function compilePattern(pattern: string): {
|
|
81
|
-
regex: RegExp;
|
|
82
|
-
paramNames: string[];
|
|
83
|
-
optionalParams: Set<string>;
|
|
84
|
-
hasTrailingSlash: boolean;
|
|
85
|
-
} {
|
|
136
|
+
export function compilePattern(pattern: string): CompiledPattern {
|
|
86
137
|
// Detect if pattern has trailing slash (but not just "/")
|
|
87
138
|
const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
88
139
|
// Remove trailing slash for parsing (we'll add it back to regex if needed)
|
|
@@ -100,16 +151,19 @@ export function compilePattern(pattern: string): {
|
|
|
100
151
|
regexPattern += "/(.*)";
|
|
101
152
|
} else if (segment.type === "param") {
|
|
102
153
|
paramNames.push(segment.value);
|
|
154
|
+
const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
|
|
103
155
|
const valuePattern = segment.constraint
|
|
104
|
-
? `(${segment.constraint.join("|")})`
|
|
105
|
-
:
|
|
156
|
+
? `(${segment.constraint.map(escapeRegex).join("|")})`
|
|
157
|
+
: segment.suffix
|
|
158
|
+
? "([^/]+?)"
|
|
159
|
+
: "([^/]+)";
|
|
106
160
|
|
|
107
161
|
if (segment.optional) {
|
|
108
162
|
optionalParams.add(segment.value);
|
|
109
163
|
// Optional: make the whole /segment optional
|
|
110
|
-
regexPattern += `(?:/${valuePattern})?`;
|
|
164
|
+
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
111
165
|
} else {
|
|
112
|
-
regexPattern += `/${valuePattern}`;
|
|
166
|
+
regexPattern += `/${valuePattern}${suffixPattern}`;
|
|
113
167
|
}
|
|
114
168
|
} else {
|
|
115
169
|
// Static segment
|
|
@@ -142,6 +196,54 @@ function escapeRegex(str: string): string {
|
|
|
142
196
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
143
197
|
}
|
|
144
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Extract the static prefix from a route pattern.
|
|
201
|
+
* Returns everything before the first param/wildcard.
|
|
202
|
+
*
|
|
203
|
+
* Called ONCE at registration time, not at match time.
|
|
204
|
+
*
|
|
205
|
+
* Examples:
|
|
206
|
+
* - "/api" → "/api"
|
|
207
|
+
* - "/site/:locale" → "/site"
|
|
208
|
+
* - "/:locale" → ""
|
|
209
|
+
* - "/admin/users/:id" → "/admin/users"
|
|
210
|
+
* - "/api/*" → "/api"
|
|
211
|
+
*/
|
|
212
|
+
export function extractStaticPrefix(pattern: string): string {
|
|
213
|
+
if (!pattern || pattern === "/") return "";
|
|
214
|
+
|
|
215
|
+
// Find the first occurrence of : or *
|
|
216
|
+
const paramIndex = pattern.indexOf(":");
|
|
217
|
+
const wildcardIndex = pattern.indexOf("*");
|
|
218
|
+
|
|
219
|
+
let cutIndex = -1;
|
|
220
|
+
if (paramIndex !== -1 && wildcardIndex !== -1) {
|
|
221
|
+
cutIndex = Math.min(paramIndex, wildcardIndex);
|
|
222
|
+
} else if (paramIndex !== -1) {
|
|
223
|
+
cutIndex = paramIndex;
|
|
224
|
+
} else if (wildcardIndex !== -1) {
|
|
225
|
+
cutIndex = wildcardIndex;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (cutIndex === -1) {
|
|
229
|
+
// No params or wildcards - entire pattern is static
|
|
230
|
+
return pattern;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (cutIndex === 0) {
|
|
234
|
+
// Pattern starts with : or * - no static prefix
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Find the last / before the param
|
|
239
|
+
const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
|
|
240
|
+
if (lastSlash === -1 || lastSlash === 0) {
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return pattern.slice(0, lastSlash);
|
|
245
|
+
}
|
|
246
|
+
|
|
145
247
|
/**
|
|
146
248
|
* Match a pathname against registered routes
|
|
147
249
|
*
|
|
@@ -166,22 +268,120 @@ export interface RouteMatchResult<TEnv = any> {
|
|
|
166
268
|
params: Record<string, string>;
|
|
167
269
|
optionalParams: Set<string>;
|
|
168
270
|
redirectTo?: string;
|
|
271
|
+
/** Ancestry shortCodes for layout pruning (from trie match) */
|
|
272
|
+
ancestry?: string[];
|
|
273
|
+
/** Route has pre-rendered data available (from trie) */
|
|
274
|
+
pr?: true;
|
|
275
|
+
/** Passthrough: handler kept for live fallback on unknown params (from trie) */
|
|
276
|
+
pt?: true;
|
|
277
|
+
/** Response type for non-RSC routes (json, text, image, any) */
|
|
278
|
+
responseType?: string;
|
|
279
|
+
/** Negotiate variants: response-type routes sharing this path */
|
|
280
|
+
negotiateVariants?: Array<{ routeKey: string; responseType: string }>;
|
|
281
|
+
/** RSC-first: RSC route was defined before response-type variants */
|
|
282
|
+
rscFirst?: true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Result when a lazy entry needs evaluation before matching
|
|
287
|
+
*/
|
|
288
|
+
export interface LazyEvaluationNeeded<TEnv = any> {
|
|
289
|
+
lazyEntry: RouteEntry<TEnv>;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Type guard to check if result is a lazy evaluation needed response
|
|
294
|
+
*/
|
|
295
|
+
export function isLazyEvaluationNeeded<TEnv>(
|
|
296
|
+
result: RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null,
|
|
297
|
+
): result is LazyEvaluationNeeded<TEnv> {
|
|
298
|
+
return result !== null && "lazyEntry" in result;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Debug stats type for exports
|
|
302
|
+
interface MatchDebugStats {
|
|
303
|
+
entriesChecked: number;
|
|
304
|
+
entriesSkipped: number;
|
|
305
|
+
routesChecked: number;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Debug stats for route matching (only in debug mode)
|
|
309
|
+
let debugEnabled = false;
|
|
310
|
+
let debugStats = { entriesChecked: 0, entriesSkipped: 0, routesChecked: 0 };
|
|
311
|
+
|
|
312
|
+
export function enableMatchDebug(enabled: boolean): void {
|
|
313
|
+
debugEnabled = enabled;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function getMatchDebugStats(): MatchDebugStats {
|
|
317
|
+
return {
|
|
318
|
+
entriesChecked: debugStats.entriesChecked,
|
|
319
|
+
entriesSkipped: debugStats.entriesSkipped,
|
|
320
|
+
routesChecked: debugStats.routesChecked,
|
|
321
|
+
};
|
|
169
322
|
}
|
|
170
323
|
|
|
171
324
|
export function findMatch<TEnv>(
|
|
172
325
|
pathname: string,
|
|
173
|
-
routesEntries: RouteEntry<TEnv>[]
|
|
174
|
-
): RouteMatchResult<TEnv> | null {
|
|
175
|
-
const
|
|
326
|
+
routesEntries: RouteEntry<TEnv>[],
|
|
327
|
+
): RouteMatchResult<TEnv> | LazyEvaluationNeeded<TEnv> | null {
|
|
328
|
+
const effectiveDebug = debugEnabled || isRouterDebugEnabled();
|
|
329
|
+
|
|
330
|
+
if (effectiveDebug) {
|
|
331
|
+
debugStats = { entriesChecked: 0, entriesSkipped: 0, routesChecked: 0 };
|
|
332
|
+
debugLog("findMatch", "start", { pathname, entries: routesEntries.length });
|
|
333
|
+
for (const e of routesEntries) {
|
|
334
|
+
debugLog("findMatch", "entry", {
|
|
335
|
+
prefix: e.prefix,
|
|
336
|
+
staticPrefix: e.staticPrefix,
|
|
337
|
+
routeCount: Object.keys(e.routes).length,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const pathnameHasTrailingSlash =
|
|
343
|
+
pathname.length > 1 && pathname.endsWith("/");
|
|
176
344
|
// Try alternate pathname for redirect matching
|
|
177
345
|
const alternatePathname = pathnameHasTrailingSlash
|
|
178
346
|
? pathname.slice(0, -1)
|
|
179
347
|
: pathname + "/";
|
|
180
348
|
|
|
181
349
|
for (const entry of routesEntries) {
|
|
350
|
+
// Short-circuit: skip entry if pathname doesn't start with static prefix
|
|
351
|
+
// staticPrefix is pre-computed at registration time, so this is O(1)
|
|
352
|
+
if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
|
|
353
|
+
if (effectiveDebug) {
|
|
354
|
+
debugStats.entriesSkipped++;
|
|
355
|
+
debugLog("findMatch", "skipped entry", {
|
|
356
|
+
prefix: entry.prefix,
|
|
357
|
+
staticPrefix: entry.staticPrefix,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check if this is a lazy entry that needs evaluation
|
|
364
|
+
// When staticPrefix matches but routes are not yet populated, signal caller to evaluate
|
|
365
|
+
if (entry.lazy && !entry.lazyEvaluated) {
|
|
366
|
+
if (effectiveDebug) {
|
|
367
|
+
debugLog("findMatch", "lazy entry requires evaluation", {
|
|
368
|
+
staticPrefix: entry.staticPrefix,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return { lazyEntry: entry };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (effectiveDebug) {
|
|
375
|
+
debugStats.entriesChecked++;
|
|
376
|
+
}
|
|
377
|
+
|
|
182
378
|
const routeEntries = Object.entries(entry.routes);
|
|
183
379
|
|
|
184
380
|
for (const [routeKey, pattern] of routeEntries) {
|
|
381
|
+
if (effectiveDebug) {
|
|
382
|
+
debugStats.routesChecked++;
|
|
383
|
+
}
|
|
384
|
+
|
|
185
385
|
// Join prefix and pattern, handling edge cases
|
|
186
386
|
let fullPattern: string;
|
|
187
387
|
if (entry.prefix === "" || entry.prefix === "/") {
|
|
@@ -192,11 +392,20 @@ export function findMatch<TEnv>(
|
|
|
192
392
|
fullPattern = entry.prefix + pattern;
|
|
193
393
|
}
|
|
194
394
|
|
|
195
|
-
const { regex, paramNames, optionalParams, hasTrailingSlash } =
|
|
395
|
+
const { regex, paramNames, optionalParams, hasTrailingSlash } =
|
|
396
|
+
getCompiledPattern(fullPattern);
|
|
196
397
|
|
|
197
398
|
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
198
|
-
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
399
|
+
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
400
|
+
entry.trailingSlash?.[routeKey];
|
|
199
401
|
|
|
402
|
+
// Prerender flag from entry metadata (set by urls() for prerender handlers)
|
|
403
|
+
const prFlag = entry.prerenderRouteKeys?.has(routeKey)
|
|
404
|
+
? { pr: true as const }
|
|
405
|
+
: {};
|
|
406
|
+
const ptFlag = entry.passthroughRouteKeys?.has(routeKey)
|
|
407
|
+
? { pt: true as const }
|
|
408
|
+
: {};
|
|
200
409
|
|
|
201
410
|
// Try exact match first
|
|
202
411
|
const match = regex.exec(pathname);
|
|
@@ -206,16 +415,51 @@ export function findMatch<TEnv>(
|
|
|
206
415
|
params[name] = match[index + 1] ?? "";
|
|
207
416
|
});
|
|
208
417
|
|
|
418
|
+
if (effectiveDebug) {
|
|
419
|
+
debugLog("findMatch", "matched route", {
|
|
420
|
+
routeKey,
|
|
421
|
+
pattern: fullPattern,
|
|
422
|
+
stats: { ...debugStats },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
209
426
|
// Check if trailing slash mode requires redirect even on exact match
|
|
210
|
-
if (
|
|
427
|
+
if (
|
|
428
|
+
trailingSlashMode === "always" &&
|
|
429
|
+
!pathnameHasTrailingSlash &&
|
|
430
|
+
pathname !== "/"
|
|
431
|
+
) {
|
|
211
432
|
// Mode says always have trailing slash, but pathname doesn't have it
|
|
212
|
-
return {
|
|
433
|
+
return {
|
|
434
|
+
entry,
|
|
435
|
+
routeKey,
|
|
436
|
+
params,
|
|
437
|
+
optionalParams,
|
|
438
|
+
redirectTo: pathname + "/",
|
|
439
|
+
...prFlag,
|
|
440
|
+
...ptFlag,
|
|
441
|
+
};
|
|
213
442
|
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
214
443
|
// Mode says never have trailing slash, but pathname has it
|
|
215
|
-
return {
|
|
444
|
+
return {
|
|
445
|
+
entry,
|
|
446
|
+
routeKey,
|
|
447
|
+
params,
|
|
448
|
+
optionalParams,
|
|
449
|
+
redirectTo: pathname.slice(0, -1),
|
|
450
|
+
...prFlag,
|
|
451
|
+
...ptFlag,
|
|
452
|
+
};
|
|
216
453
|
}
|
|
217
454
|
|
|
218
|
-
return {
|
|
455
|
+
return {
|
|
456
|
+
entry,
|
|
457
|
+
routeKey,
|
|
458
|
+
params,
|
|
459
|
+
optionalParams,
|
|
460
|
+
...prFlag,
|
|
461
|
+
...ptFlag,
|
|
462
|
+
};
|
|
219
463
|
}
|
|
220
464
|
|
|
221
465
|
// Try alternate pathname (opposite trailing slash)
|
|
@@ -229,24 +473,71 @@ export function findMatch<TEnv>(
|
|
|
229
473
|
// Determine redirect behavior based on mode
|
|
230
474
|
if (trailingSlashMode === "ignore") {
|
|
231
475
|
// Match without redirect
|
|
232
|
-
return {
|
|
476
|
+
return {
|
|
477
|
+
entry,
|
|
478
|
+
routeKey,
|
|
479
|
+
params,
|
|
480
|
+
optionalParams,
|
|
481
|
+
...prFlag,
|
|
482
|
+
...ptFlag,
|
|
483
|
+
};
|
|
233
484
|
} else if (trailingSlashMode === "never") {
|
|
234
485
|
// Redirect to no trailing slash
|
|
235
486
|
if (pathnameHasTrailingSlash) {
|
|
236
|
-
return {
|
|
487
|
+
return {
|
|
488
|
+
entry,
|
|
489
|
+
routeKey,
|
|
490
|
+
params,
|
|
491
|
+
optionalParams,
|
|
492
|
+
redirectTo: alternatePathname,
|
|
493
|
+
...prFlag,
|
|
494
|
+
...ptFlag,
|
|
495
|
+
};
|
|
237
496
|
}
|
|
238
|
-
return {
|
|
497
|
+
return {
|
|
498
|
+
entry,
|
|
499
|
+
routeKey,
|
|
500
|
+
params,
|
|
501
|
+
optionalParams,
|
|
502
|
+
...prFlag,
|
|
503
|
+
...ptFlag,
|
|
504
|
+
};
|
|
239
505
|
} else if (trailingSlashMode === "always") {
|
|
240
506
|
// Redirect to with trailing slash
|
|
241
507
|
if (!pathnameHasTrailingSlash) {
|
|
242
|
-
return {
|
|
508
|
+
return {
|
|
509
|
+
entry,
|
|
510
|
+
routeKey,
|
|
511
|
+
params,
|
|
512
|
+
optionalParams,
|
|
513
|
+
redirectTo: alternatePathname,
|
|
514
|
+
...prFlag,
|
|
515
|
+
...ptFlag,
|
|
516
|
+
};
|
|
243
517
|
}
|
|
244
|
-
return {
|
|
518
|
+
return {
|
|
519
|
+
entry,
|
|
520
|
+
routeKey,
|
|
521
|
+
params,
|
|
522
|
+
optionalParams,
|
|
523
|
+
...prFlag,
|
|
524
|
+
...ptFlag,
|
|
525
|
+
};
|
|
245
526
|
} else {
|
|
246
527
|
// No explicit mode - use pattern-based detection
|
|
247
528
|
// Redirect to canonical form (what the pattern defines)
|
|
248
|
-
const canonicalPath = hasTrailingSlash
|
|
249
|
-
|
|
529
|
+
const canonicalPath = hasTrailingSlash
|
|
530
|
+
? alternatePathname
|
|
531
|
+
: pathname.slice(0, -1);
|
|
532
|
+
return {
|
|
533
|
+
entry,
|
|
534
|
+
routeKey,
|
|
535
|
+
params,
|
|
536
|
+
optionalParams,
|
|
537
|
+
redirectTo: canonicalPath,
|
|
538
|
+
...prFlag,
|
|
539
|
+
...ptFlag,
|
|
540
|
+
};
|
|
250
541
|
}
|
|
251
542
|
}
|
|
252
543
|
}
|