@rangojs/router 0.0.0-experimental.0f44aca1
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 +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- 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 +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +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 +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -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 +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +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 +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- 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/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +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/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Pattern Matching
|
|
3
|
+
*
|
|
4
|
+
* Route pattern compilation and matching utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
8
|
+
import type { EntryData } from "../server/context";
|
|
9
|
+
import { debugLog, isRouterDebugEnabled } from "./logging.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parsed segment info
|
|
13
|
+
*/
|
|
14
|
+
export interface ParsedSegment {
|
|
15
|
+
type: "static" | "param" | "wildcard";
|
|
16
|
+
value: string; // static text, param name, or "*"
|
|
17
|
+
optional: boolean;
|
|
18
|
+
constraint?: string[]; // enum values like ["en", "gb"]
|
|
19
|
+
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse a route pattern into segments
|
|
24
|
+
*
|
|
25
|
+
* Supports:
|
|
26
|
+
* - Static: /blog, /about
|
|
27
|
+
* - Params: /:slug, /:id
|
|
28
|
+
* - Optional: /:locale?, /:page?
|
|
29
|
+
* - Constrained: /:locale(en|gb), /:type(post|page)
|
|
30
|
+
* - Optional + Constrained: /:locale(en|gb)?
|
|
31
|
+
* - Wildcard: /*
|
|
32
|
+
*/
|
|
33
|
+
export function parsePattern(pattern: string): ParsedSegment[] {
|
|
34
|
+
const segments: ParsedSegment[] = [];
|
|
35
|
+
// Match: /segment where segment can be:
|
|
36
|
+
// - static text
|
|
37
|
+
// - :param
|
|
38
|
+
// - :param?
|
|
39
|
+
// - :param(a|b)
|
|
40
|
+
// - :param(a|b)?
|
|
41
|
+
// - *
|
|
42
|
+
const segmentRegex =
|
|
43
|
+
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
44
|
+
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
47
|
+
const [
|
|
48
|
+
,
|
|
49
|
+
,
|
|
50
|
+
paramName,
|
|
51
|
+
,
|
|
52
|
+
constraint,
|
|
53
|
+
optional,
|
|
54
|
+
suffix,
|
|
55
|
+
wildcard,
|
|
56
|
+
staticText,
|
|
57
|
+
] = match;
|
|
58
|
+
|
|
59
|
+
if (wildcard) {
|
|
60
|
+
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
61
|
+
} else if (paramName) {
|
|
62
|
+
segments.push({
|
|
63
|
+
type: "param",
|
|
64
|
+
value: paramName,
|
|
65
|
+
optional: optional === "?",
|
|
66
|
+
constraint: constraint ? constraint.split("|") : undefined,
|
|
67
|
+
suffix: suffix || undefined,
|
|
68
|
+
});
|
|
69
|
+
} else if (staticText) {
|
|
70
|
+
segments.push({ type: "static", value: staticText, optional: false });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return segments;
|
|
75
|
+
}
|
|
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
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Compile a route pattern to regex
|
|
121
|
+
*
|
|
122
|
+
* Supports:
|
|
123
|
+
* - Static segments: /blog, /about
|
|
124
|
+
* - Dynamic params: /:slug, /:id
|
|
125
|
+
* - Optional params: /:locale?, /:page?
|
|
126
|
+
* - Constrained params: /:locale(en|gb)
|
|
127
|
+
* - Optional + constrained: /:locale(en|gb)?
|
|
128
|
+
* - Wildcard: /*
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* compilePattern("/blog/:slug") // matches /blog/hello
|
|
132
|
+
* compilePattern("/:locale?/blog") // matches /blog or /en/blog
|
|
133
|
+
* compilePattern("/:locale(en|gb)/blog") // matches /en/blog or /gb/blog
|
|
134
|
+
* compilePattern("/:locale(en|gb)?/blog") // matches /blog, /en/blog, or /gb/blog
|
|
135
|
+
*/
|
|
136
|
+
export function compilePattern(pattern: string): CompiledPattern {
|
|
137
|
+
// Detect if pattern has trailing slash (but not just "/")
|
|
138
|
+
const hasTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
139
|
+
// Remove trailing slash for parsing (we'll add it back to regex if needed)
|
|
140
|
+
const normalizedPattern = hasTrailingSlash ? pattern.slice(0, -1) : pattern;
|
|
141
|
+
|
|
142
|
+
const segments = parsePattern(normalizedPattern);
|
|
143
|
+
const paramNames: string[] = [];
|
|
144
|
+
const optionalParams = new Set<string>();
|
|
145
|
+
|
|
146
|
+
let regexPattern = "";
|
|
147
|
+
|
|
148
|
+
for (const segment of segments) {
|
|
149
|
+
if (segment.type === "wildcard") {
|
|
150
|
+
paramNames.push("*");
|
|
151
|
+
regexPattern += "/(.*)";
|
|
152
|
+
} else if (segment.type === "param") {
|
|
153
|
+
paramNames.push(segment.value);
|
|
154
|
+
const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
|
|
155
|
+
const valuePattern = segment.constraint
|
|
156
|
+
? `(${segment.constraint.map(escapeRegex).join("|")})`
|
|
157
|
+
: segment.suffix
|
|
158
|
+
? "([^/]+?)"
|
|
159
|
+
: "([^/]+)";
|
|
160
|
+
|
|
161
|
+
if (segment.optional) {
|
|
162
|
+
optionalParams.add(segment.value);
|
|
163
|
+
// Optional: make the whole /segment optional
|
|
164
|
+
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
165
|
+
} else {
|
|
166
|
+
regexPattern += `/${valuePattern}${suffixPattern}`;
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// Static segment
|
|
170
|
+
regexPattern += `/${escapeRegex(segment.value)}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Handle root path
|
|
175
|
+
if (regexPattern === "") {
|
|
176
|
+
regexPattern = "/";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Add trailing slash to regex if pattern has one
|
|
180
|
+
if (hasTrailingSlash) {
|
|
181
|
+
regexPattern += "/";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
regex: new RegExp(`^${regexPattern}$`),
|
|
186
|
+
paramNames,
|
|
187
|
+
optionalParams,
|
|
188
|
+
hasTrailingSlash,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Escape special regex characters in a string
|
|
194
|
+
*/
|
|
195
|
+
function escapeRegex(str: string): string {
|
|
196
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
197
|
+
}
|
|
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
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Match a pathname against registered routes
|
|
249
|
+
*
|
|
250
|
+
* Note: Optional params that are absent in the path will have empty string value.
|
|
251
|
+
* Use the pattern definition to determine if a param is optional.
|
|
252
|
+
*
|
|
253
|
+
* Trailing slash handling (priority order):
|
|
254
|
+
* 1. Per-route `trailingSlash` config from route()
|
|
255
|
+
* 2. Pattern-based detection (pattern ending with `/`)
|
|
256
|
+
*
|
|
257
|
+
* Modes:
|
|
258
|
+
* - "never": Redirect to no trailing slash
|
|
259
|
+
* - "always": Redirect to with trailing slash
|
|
260
|
+
* - "ignore": Match both, no redirect
|
|
261
|
+
*/
|
|
262
|
+
/**
|
|
263
|
+
* Result of a route match
|
|
264
|
+
*/
|
|
265
|
+
export interface RouteMatchResult<TEnv = any> {
|
|
266
|
+
entry: RouteEntry<TEnv>;
|
|
267
|
+
routeKey: string;
|
|
268
|
+
params: Record<string, string>;
|
|
269
|
+
optionalParams: Set<string>;
|
|
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
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function findMatch<TEnv>(
|
|
325
|
+
pathname: string,
|
|
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("/");
|
|
344
|
+
// Try alternate pathname for redirect matching
|
|
345
|
+
const alternatePathname = pathnameHasTrailingSlash
|
|
346
|
+
? pathname.slice(0, -1)
|
|
347
|
+
: pathname + "/";
|
|
348
|
+
|
|
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
|
+
|
|
378
|
+
const routeEntries = Object.entries(entry.routes);
|
|
379
|
+
|
|
380
|
+
for (const [routeKey, pattern] of routeEntries) {
|
|
381
|
+
if (effectiveDebug) {
|
|
382
|
+
debugStats.routesChecked++;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Join prefix and pattern, handling edge cases
|
|
386
|
+
let fullPattern: string;
|
|
387
|
+
if (entry.prefix === "" || entry.prefix === "/") {
|
|
388
|
+
fullPattern = pattern;
|
|
389
|
+
} else if (pattern === "/" || pattern === "") {
|
|
390
|
+
fullPattern = entry.prefix;
|
|
391
|
+
} else {
|
|
392
|
+
fullPattern = entry.prefix + pattern;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const { regex, paramNames, optionalParams, hasTrailingSlash } =
|
|
396
|
+
getCompiledPattern(fullPattern);
|
|
397
|
+
|
|
398
|
+
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
399
|
+
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
400
|
+
entry.trailingSlash?.[routeKey];
|
|
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
|
+
: {};
|
|
409
|
+
|
|
410
|
+
// Try exact match first
|
|
411
|
+
const match = regex.exec(pathname);
|
|
412
|
+
if (match) {
|
|
413
|
+
const params: Record<string, string> = {};
|
|
414
|
+
paramNames.forEach((name, index) => {
|
|
415
|
+
params[name] = match[index + 1] ?? "";
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
if (effectiveDebug) {
|
|
419
|
+
debugLog("findMatch", "matched route", {
|
|
420
|
+
routeKey,
|
|
421
|
+
pattern: fullPattern,
|
|
422
|
+
stats: { ...debugStats },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if trailing slash mode requires redirect even on exact match
|
|
427
|
+
if (
|
|
428
|
+
trailingSlashMode === "always" &&
|
|
429
|
+
!pathnameHasTrailingSlash &&
|
|
430
|
+
pathname !== "/"
|
|
431
|
+
) {
|
|
432
|
+
// Mode says always have trailing slash, but pathname doesn't have it
|
|
433
|
+
return {
|
|
434
|
+
entry,
|
|
435
|
+
routeKey,
|
|
436
|
+
params,
|
|
437
|
+
optionalParams,
|
|
438
|
+
redirectTo: pathname + "/",
|
|
439
|
+
...prFlag,
|
|
440
|
+
...ptFlag,
|
|
441
|
+
};
|
|
442
|
+
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
443
|
+
// Mode says never have trailing slash, but pathname has it
|
|
444
|
+
return {
|
|
445
|
+
entry,
|
|
446
|
+
routeKey,
|
|
447
|
+
params,
|
|
448
|
+
optionalParams,
|
|
449
|
+
redirectTo: pathname.slice(0, -1),
|
|
450
|
+
...prFlag,
|
|
451
|
+
...ptFlag,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
entry,
|
|
457
|
+
routeKey,
|
|
458
|
+
params,
|
|
459
|
+
optionalParams,
|
|
460
|
+
...prFlag,
|
|
461
|
+
...ptFlag,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Try alternate pathname (opposite trailing slash)
|
|
466
|
+
const altMatch = regex.exec(alternatePathname);
|
|
467
|
+
if (altMatch) {
|
|
468
|
+
const params: Record<string, string> = {};
|
|
469
|
+
paramNames.forEach((name, index) => {
|
|
470
|
+
params[name] = altMatch[index + 1] ?? "";
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Determine redirect behavior based on mode
|
|
474
|
+
if (trailingSlashMode === "ignore") {
|
|
475
|
+
// Match without redirect
|
|
476
|
+
return {
|
|
477
|
+
entry,
|
|
478
|
+
routeKey,
|
|
479
|
+
params,
|
|
480
|
+
optionalParams,
|
|
481
|
+
...prFlag,
|
|
482
|
+
...ptFlag,
|
|
483
|
+
};
|
|
484
|
+
} else if (trailingSlashMode === "never") {
|
|
485
|
+
// Redirect to no trailing slash
|
|
486
|
+
if (pathnameHasTrailingSlash) {
|
|
487
|
+
return {
|
|
488
|
+
entry,
|
|
489
|
+
routeKey,
|
|
490
|
+
params,
|
|
491
|
+
optionalParams,
|
|
492
|
+
redirectTo: alternatePathname,
|
|
493
|
+
...prFlag,
|
|
494
|
+
...ptFlag,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
entry,
|
|
499
|
+
routeKey,
|
|
500
|
+
params,
|
|
501
|
+
optionalParams,
|
|
502
|
+
...prFlag,
|
|
503
|
+
...ptFlag,
|
|
504
|
+
};
|
|
505
|
+
} else if (trailingSlashMode === "always") {
|
|
506
|
+
// Redirect to with trailing slash
|
|
507
|
+
if (!pathnameHasTrailingSlash) {
|
|
508
|
+
return {
|
|
509
|
+
entry,
|
|
510
|
+
routeKey,
|
|
511
|
+
params,
|
|
512
|
+
optionalParams,
|
|
513
|
+
redirectTo: alternatePathname,
|
|
514
|
+
...prFlag,
|
|
515
|
+
...ptFlag,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return {
|
|
519
|
+
entry,
|
|
520
|
+
routeKey,
|
|
521
|
+
params,
|
|
522
|
+
optionalParams,
|
|
523
|
+
...prFlag,
|
|
524
|
+
...ptFlag,
|
|
525
|
+
};
|
|
526
|
+
} else {
|
|
527
|
+
// No explicit mode - use pattern-based detection
|
|
528
|
+
// Redirect to canonical form (what the pattern defines)
|
|
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
|
+
};
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Traverse from entry to bottom to top, yielding each EntryData
|
|
551
|
+
* e.g. {child -> parent -> grandparent ...}
|
|
552
|
+
*/
|
|
553
|
+
export function* traverseBack(entry: EntryData): Generator<EntryData> {
|
|
554
|
+
let current: EntryData | null = entry;
|
|
555
|
+
const items = [] as EntryData[];
|
|
556
|
+
while (current !== null) {
|
|
557
|
+
items.push(current); // Move up to next parent
|
|
558
|
+
current = current.parent;
|
|
559
|
+
}
|
|
560
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
561
|
+
yield items[i];
|
|
562
|
+
}
|
|
563
|
+
}
|