@rangojs/router 0.0.0-experimental.002d056c

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +547 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +479 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +982 -0
  105. package/src/cache/cf/index.ts +29 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +44 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +281 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +193 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +749 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +320 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1242 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +291 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1006 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +237 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +920 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +109 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +108 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +48 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +363 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +266 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +445 -0
  298. package/src/vite/router-discovery.ts +777 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. 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
+ }