@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.
Files changed (297) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +883 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4655 -747
  5. package/package.json +78 -50
  6. package/skills/cache-guide/SKILL.md +262 -0
  7. package/skills/caching/SKILL.md +54 -25
  8. package/skills/composability/SKILL.md +172 -0
  9. package/skills/debug-manifest/SKILL.md +12 -8
  10. package/skills/document-cache/SKILL.md +23 -21
  11. package/skills/fonts/SKILL.md +167 -0
  12. package/skills/hooks/SKILL.md +390 -63
  13. package/skills/host-router/SKILL.md +218 -0
  14. package/skills/intercept/SKILL.md +133 -10
  15. package/skills/layout/SKILL.md +102 -5
  16. package/skills/links/SKILL.md +239 -0
  17. package/skills/loader/SKILL.md +366 -29
  18. package/skills/middleware/SKILL.md +173 -36
  19. package/skills/mime-routes/SKILL.md +128 -0
  20. package/skills/parallel/SKILL.md +80 -3
  21. package/skills/prerender/SKILL.md +643 -0
  22. package/skills/rango/SKILL.md +86 -16
  23. package/skills/response-routes/SKILL.md +411 -0
  24. package/skills/route/SKILL.md +227 -14
  25. package/skills/router-setup/SKILL.md +225 -32
  26. package/skills/tailwind/SKILL.md +129 -0
  27. package/skills/theme/SKILL.md +12 -11
  28. package/skills/typesafety/SKILL.md +401 -75
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +10 -4
  31. package/src/bin/rango.ts +321 -0
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +20 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +201 -553
  41. package/src/browser/navigation-client.ts +124 -71
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +295 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +267 -317
  46. package/src/browser/prefetch/cache.ts +146 -0
  47. package/src/browser/prefetch/fetch.ts +135 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +42 -0
  50. package/src/browser/prefetch/queue.ts +88 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +173 -73
  53. package/src/browser/react/NavigationProvider.tsx +138 -27
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +37 -0
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +49 -65
  65. package/src/browser/react/use-href.tsx +20 -188
  66. package/src/browser/react/use-link-status.ts +6 -5
  67. package/src/browser/react/use-mount.ts +31 -0
  68. package/src/browser/react/use-navigation.ts +27 -78
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +111 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +83 -0
  79. package/src/browser/server-action-bridge.ts +504 -584
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +92 -57
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +438 -0
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +35 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +10 -15
  114. package/src/client.tsx +114 -135
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +34 -19
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +165 -0
  124. package/src/host/errors.ts +97 -0
  125. package/src/host/index.ts +53 -0
  126. package/src/host/pattern-matcher.ts +214 -0
  127. package/src/host/router.ts +352 -0
  128. package/src/host/testing.ts +79 -0
  129. package/src/host/types.ts +146 -0
  130. package/src/host/utils.ts +25 -0
  131. package/src/href-client.ts +135 -49
  132. package/src/index.rsc.ts +182 -17
  133. package/src/index.ts +238 -24
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +27 -142
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +37 -0
  140. package/src/prerender/store.ts +185 -0
  141. package/src/prerender.ts +463 -0
  142. package/src/reverse.ts +330 -0
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +9 -11
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1388
  151. package/src/route-map-builder.ts +241 -112
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +70 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +158 -0
  158. package/src/router/handler-context.ts +371 -81
  159. package/src/router/intercept-resolution.ts +395 -0
  160. package/src/router/lazy-includes.ts +234 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +248 -0
  163. package/src/router/manifest.ts +155 -32
  164. package/src/router/match-api.ts +620 -0
  165. package/src/router/match-context.ts +5 -3
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +382 -9
  169. package/src/router/match-middleware/cache-store.ts +51 -22
  170. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  171. package/src/router/match-middleware/segment-resolution.ts +24 -6
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -29
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +324 -367
  178. package/src/router/pattern-matching.ts +321 -30
  179. package/src/router/prerender-match.ts +400 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +36 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1241 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -0
  192. package/src/router/segment-wrappers.ts +289 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +239 -0
  197. package/src/router/types.ts +77 -3
  198. package/src/router.ts +688 -3656
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +786 -760
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +5 -25
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +235 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +40 -14
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +57 -61
  217. package/src/server/context.ts +202 -51
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +422 -70
  223. package/src/server.ts +36 -120
  224. package/src/ssr/index.tsx +157 -26
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +102 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1577
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -726
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +110 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -782
  261. package/src/vite/plugin-types.ts +131 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +29 -15
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +510 -0
  280. package/src/vite/router-discovery.ts +785 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -3
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/href.ts +0 -255
  294. package/src/vite/expose-handle-id.ts +0 -209
  295. package/src/vite/expose-loader-id.ts +0 -357
  296. package/src/vite/expose-location-state-id.ts +0 -177
  297. /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 = /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?|(\*)|([^/]+))/g;
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 [, , paramName, , constraint, optional, wildcard, staticText] = match;
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 pathnameHasTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
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 } = compilePattern(fullPattern);
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 = entry.trailingSlash?.[routeKey];
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 (trailingSlashMode === "always" && !pathnameHasTrailingSlash && pathname !== "/") {
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 { entry, routeKey, params, optionalParams, redirectTo: pathname + "/" };
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 { entry, routeKey, params, optionalParams, redirectTo: pathname.slice(0, -1) };
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 { entry, routeKey, params, optionalParams };
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 { entry, routeKey, params, optionalParams };
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 { entry, routeKey, params, optionalParams, redirectTo: alternatePathname };
487
+ return {
488
+ entry,
489
+ routeKey,
490
+ params,
491
+ optionalParams,
492
+ redirectTo: alternatePathname,
493
+ ...prFlag,
494
+ ...ptFlag,
495
+ };
237
496
  }
238
- return { entry, routeKey, params, optionalParams };
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 { entry, routeKey, params, optionalParams, redirectTo: alternatePathname };
508
+ return {
509
+ entry,
510
+ routeKey,
511
+ params,
512
+ optionalParams,
513
+ redirectTo: alternatePathname,
514
+ ...prFlag,
515
+ ...ptFlag,
516
+ };
243
517
  }
244
- return { entry, routeKey, params, optionalParams };
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 ? alternatePathname : pathname.slice(0, -1);
249
- return { entry, routeKey, params, optionalParams, redirectTo: canonicalPath };
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
  }