@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared route-param comparison helpers.
3
+ */
4
+
5
+ /**
6
+ * Shallow equality for two route-param records. Same-reference is a fast path;
7
+ * otherwise compares key count then each value.
8
+ */
9
+ export function paramsEqual(
10
+ a: Record<string, string>,
11
+ b: Record<string, string>,
12
+ ): boolean {
13
+ if (a === b) return true;
14
+
15
+ const keysA = Object.keys(a);
16
+ if (keysA.length !== Object.keys(b).length) return false;
17
+
18
+ for (const key of keysA) {
19
+ if (a[key] !== b[key]) return false;
20
+ }
21
+
22
+ return true;
23
+ }
@@ -33,13 +33,6 @@ export interface ParsedSegment {
33
33
  */
34
34
  export function parsePattern(pattern: string): ParsedSegment[] {
35
35
  const segments: ParsedSegment[] = [];
36
- // Match: /segment where segment can be:
37
- // - static text
38
- // - :param
39
- // - :param?
40
- // - :param(a|b)
41
- // - :param(a|b)?
42
- // - *
43
36
  const segmentRegex =
44
37
  /\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
45
38
 
@@ -81,7 +74,6 @@ export function parsePattern(pattern: string): ParsedSegment[] {
81
74
  export interface CompiledPattern {
82
75
  regex: RegExp;
83
76
  paramNames: string[];
84
- optionalParams: Set<string>;
85
77
  hasTrailingSlash: boolean;
86
78
  /**
87
79
  * Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
@@ -149,7 +141,6 @@ export function compilePattern(pattern: string): CompiledPattern {
149
141
 
150
142
  const segments = parsePattern(normalizedPattern);
151
143
  const paramNames: string[] = [];
152
- const optionalParams = new Set<string>();
153
144
  let constraints: Record<string, string[]> | undefined;
154
145
 
155
146
  let regexPattern = "";
@@ -171,7 +162,6 @@ export function compilePattern(pattern: string): CompiledPattern {
171
162
  }
172
163
 
173
164
  if (segment.optional) {
174
- optionalParams.add(segment.value);
175
165
  // Optional: make the whole /segment optional
176
166
  regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
177
167
  } else {
@@ -183,11 +173,24 @@ export function compilePattern(pattern: string): CompiledPattern {
183
173
  }
184
174
  }
185
175
 
186
- // Handle root path
187
176
  if (regexPattern === "") {
188
177
  regexPattern = "/";
189
178
  }
190
179
 
180
+ // Patterns of only optional segments (e.g. `/:locale?`, `/:a?/:b?`) need
181
+ // an explicit `/` alternative so a bare `/` matches the absent form. The
182
+ // optional template `(?:/X)?` matches `/X` or empty string, but pathnames
183
+ // are never empty. Arises from `include("/:locale?", routes)` + inner
184
+ // `path("/")`. Skip when an explicit trailing slash already anchors the
185
+ // match.
186
+ const hasOnlyOptionalSegments =
187
+ !hasTrailingSlash &&
188
+ segments.length > 0 &&
189
+ segments.every((segment) => segment.type === "param" && segment.optional);
190
+ if (hasOnlyOptionalSegments) {
191
+ regexPattern = `(?:/|${regexPattern})`;
192
+ }
193
+
191
194
  // Add trailing slash to regex if pattern has one
192
195
  if (hasTrailingSlash) {
193
196
  regexPattern += "/";
@@ -196,7 +199,6 @@ export function compilePattern(pattern: string): CompiledPattern {
196
199
  return {
197
200
  regex: new RegExp(`^${regexPattern}$`),
198
201
  paramNames,
199
- optionalParams,
200
202
  hasTrailingSlash,
201
203
  ...(constraints ? { constraints } : {}),
202
204
  };
@@ -205,7 +207,9 @@ export function compilePattern(pattern: string): CompiledPattern {
205
207
  /**
206
208
  * Validate decoded params against a compiled pattern's constraints.
207
209
  * Returns false if any constrained param has a non-empty value not in the
208
- * allowed list (empty-string = absent optional, which is allowed).
210
+ * allowed list. Absent optionals (key missing or `undefined`) are allowed;
211
+ * `""` is also tolerated as "absent" so user-provided params or fixtures
212
+ * that pass empty strings explicitly behave the same way.
209
213
  */
210
214
  function satisfiesConstraints(
211
215
  params: Record<string, string>,
@@ -232,6 +236,27 @@ function escapeRegex(str: string): string {
232
236
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
233
237
  }
234
238
 
239
+ /**
240
+ * Build the named-params record from a regex match. Optional segments that
241
+ * didn't capture leave the corresponding group `undefined`; we skip those
242
+ * keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
243
+ * keeps the runtime aligned with the `ExtractParams` type and matches the
244
+ * trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
245
+ */
246
+ function buildParamsFromMatch(
247
+ match: RegExpExecArray,
248
+ paramNames: string[],
249
+ ): Record<string, string> {
250
+ const params: Record<string, string> = {};
251
+ paramNames.forEach((name, index) => {
252
+ const captured = match[index + 1];
253
+ if (captured !== undefined) {
254
+ params[name] = safeDecodeURIComponent(captured);
255
+ }
256
+ });
257
+ return params;
258
+ }
259
+
235
260
  /**
236
261
  * Extract the static prefix from a route pattern.
237
262
  * Returns everything before the first param/wildcard.
@@ -248,7 +273,6 @@ function escapeRegex(str: string): string {
248
273
  export function extractStaticPrefix(pattern: string): string {
249
274
  if (!pattern || pattern === "/") return "";
250
275
 
251
- // Find the first occurrence of : or *
252
276
  const paramIndex = pattern.indexOf(":");
253
277
  const wildcardIndex = pattern.indexOf("*");
254
278
 
@@ -262,16 +286,13 @@ export function extractStaticPrefix(pattern: string): string {
262
286
  }
263
287
 
264
288
  if (cutIndex === -1) {
265
- // No params or wildcards - entire pattern is static
266
289
  return pattern;
267
290
  }
268
291
 
269
292
  if (cutIndex === 0) {
270
- // Pattern starts with : or * - no static prefix
271
293
  return "";
272
294
  }
273
295
 
274
- // Find the last / before the param
275
296
  const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
276
297
  if (lastSlash === -1 || lastSlash === 0) {
277
298
  return "";
@@ -280,11 +301,28 @@ export function extractStaticPrefix(pattern: string): string {
280
301
  return pattern.slice(0, lastSlash);
281
302
  }
282
303
 
304
+ /**
305
+ * Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
306
+ * base ends with "/" and the sub-prefix starts with "/". This mirrors the
307
+ * canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
308
+ * (server/context.ts) so a nested lazy include's runtime staticPrefix matches
309
+ * the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
310
+ * `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
311
+ */
312
+ export function joinPrefix(base: string | undefined, prefix: string): string {
313
+ if (!base) return prefix;
314
+ return base.endsWith("/") && prefix.startsWith("/")
315
+ ? base + prefix.slice(1)
316
+ : base + prefix;
317
+ }
318
+
283
319
  /**
284
320
  * Match a pathname against registered routes
285
321
  *
286
- * Note: Optional params that are absent in the path will have empty string value.
287
- * Use the pattern definition to determine if a param is optional.
322
+ * Note: Optional params that are absent in the path are omitted from the
323
+ * returned `params` (read as `undefined`), matching the trie matcher and
324
+ * the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
325
+ * determine which keys are optional.
288
326
  *
289
327
  * Trailing slash handling (priority order):
290
328
  * 1. Per-route `trailingSlash` config from route()
@@ -302,10 +340,7 @@ export interface RouteMatchResult<TEnv = any> {
302
340
  entry: RouteEntry<TEnv>;
303
341
  routeKey: string;
304
342
  params: Record<string, string>;
305
- optionalParams: Set<string>;
306
343
  redirectTo?: string;
307
- /** Ancestry shortCodes for layout pruning (from trie match) */
308
- ancestry?: string[];
309
344
  /** Route has pre-rendered data available (from trie) */
310
345
  pr?: true;
311
346
  /** Passthrough: handler kept for live fallback on unknown params (from trie) */
@@ -383,8 +418,6 @@ export function findMatch<TEnv>(
383
418
  : pathname + "/";
384
419
 
385
420
  for (const entry of routesEntries) {
386
- // Short-circuit: skip entry if pathname doesn't start with static prefix
387
- // staticPrefix is pre-computed at registration time, so this is O(1)
388
421
  if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
389
422
  if (effectiveDebug) {
390
423
  debugStats.entriesSkipped++;
@@ -396,8 +429,6 @@ export function findMatch<TEnv>(
396
429
  continue;
397
430
  }
398
431
 
399
- // Check if this is a lazy entry that needs evaluation
400
- // When staticPrefix matches but routes are not yet populated, signal caller to evaluate
401
432
  if (entry.lazy && !entry.lazyEvaluated) {
402
433
  if (effectiveDebug) {
403
434
  debugLog("findMatch", "lazy entry requires evaluation", {
@@ -418,7 +449,6 @@ export function findMatch<TEnv>(
418
449
  debugStats.routesChecked++;
419
450
  }
420
451
 
421
- // Join prefix and pattern, handling edge cases
422
452
  let fullPattern: string;
423
453
  if (entry.prefix === "" || entry.prefix === "/") {
424
454
  fullPattern = pattern;
@@ -428,19 +458,12 @@ export function findMatch<TEnv>(
428
458
  fullPattern = entry.prefix + pattern;
429
459
  }
430
460
 
431
- const {
432
- regex,
433
- paramNames,
434
- optionalParams,
435
- hasTrailingSlash,
436
- constraints,
437
- } = getCompiledPattern(fullPattern);
461
+ const { regex, paramNames, hasTrailingSlash, constraints } =
462
+ getCompiledPattern(fullPattern);
438
463
 
439
- // Get trailing slash mode for this route (per-route config or pattern-based)
440
464
  const trailingSlashMode: TrailingSlashMode | undefined =
441
465
  entry.trailingSlash?.[routeKey];
442
466
 
443
- // Prerender flag from entry metadata (set by urls() for prerender handlers)
444
467
  const prFlag = entry.prerenderRouteKeys?.has(routeKey)
445
468
  ? { pr: true as const }
446
469
  : {};
@@ -448,16 +471,10 @@ export function findMatch<TEnv>(
448
471
  ? { pt: true as const }
449
472
  : {};
450
473
 
451
- // Try exact match first
452
474
  const match = regex.exec(pathname);
453
475
  if (match) {
454
- const params: Record<string, string> = {};
455
- paramNames.forEach((name, index) => {
456
- params[name] = safeDecodeURIComponent(match[index + 1] ?? "");
457
- });
476
+ const params = buildParamsFromMatch(match, paramNames);
458
477
 
459
- // Validate constraints against decoded values; a failure falls
460
- // through to the next route so other patterns can still match.
461
478
  if (!satisfiesConstraints(params, constraints)) {
462
479
  continue;
463
480
  }
@@ -470,29 +487,24 @@ export function findMatch<TEnv>(
470
487
  });
471
488
  }
472
489
 
473
- // Check if trailing slash mode requires redirect even on exact match
474
490
  if (
475
491
  trailingSlashMode === "always" &&
476
492
  !pathnameHasTrailingSlash &&
477
493
  pathname !== "/"
478
494
  ) {
479
- // Mode says always have trailing slash, but pathname doesn't have it
480
495
  return {
481
496
  entry,
482
497
  routeKey,
483
498
  params,
484
- optionalParams,
485
499
  redirectTo: pathname + "/",
486
500
  ...prFlag,
487
501
  ...ptFlag,
488
502
  };
489
503
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
490
- // Mode says never have trailing slash, but pathname has it
491
504
  return {
492
505
  entry,
493
506
  routeKey,
494
507
  params,
495
- optionalParams,
496
508
  redirectTo: pathname.slice(0, -1),
497
509
  ...prFlag,
498
510
  ...ptFlag,
@@ -503,43 +515,33 @@ export function findMatch<TEnv>(
503
515
  entry,
504
516
  routeKey,
505
517
  params,
506
- optionalParams,
507
518
  ...prFlag,
508
519
  ...ptFlag,
509
520
  };
510
521
  }
511
522
 
512
- // Try alternate pathname (opposite trailing slash)
513
523
  const altMatch = regex.exec(alternatePathname);
514
524
  if (altMatch) {
515
- const params: Record<string, string> = {};
516
- paramNames.forEach((name, index) => {
517
- params[name] = safeDecodeURIComponent(altMatch[index + 1] ?? "");
518
- });
525
+ const params = buildParamsFromMatch(altMatch, paramNames);
519
526
 
520
527
  if (!satisfiesConstraints(params, constraints)) {
521
528
  continue;
522
529
  }
523
530
 
524
- // Determine redirect behavior based on mode
525
531
  if (trailingSlashMode === "ignore") {
526
- // Match without redirect
527
532
  return {
528
533
  entry,
529
534
  routeKey,
530
535
  params,
531
- optionalParams,
532
536
  ...prFlag,
533
537
  ...ptFlag,
534
538
  };
535
539
  } else if (trailingSlashMode === "never") {
536
- // Redirect to no trailing slash
537
540
  if (pathnameHasTrailingSlash) {
538
541
  return {
539
542
  entry,
540
543
  routeKey,
541
544
  params,
542
- optionalParams,
543
545
  redirectTo: alternatePathname,
544
546
  ...prFlag,
545
547
  ...ptFlag,
@@ -549,18 +551,15 @@ export function findMatch<TEnv>(
549
551
  entry,
550
552
  routeKey,
551
553
  params,
552
- optionalParams,
553
554
  ...prFlag,
554
555
  ...ptFlag,
555
556
  };
556
557
  } else if (trailingSlashMode === "always") {
557
- // Redirect to with trailing slash
558
558
  if (!pathnameHasTrailingSlash) {
559
559
  return {
560
560
  entry,
561
561
  routeKey,
562
562
  params,
563
- optionalParams,
564
563
  redirectTo: alternatePathname,
565
564
  ...prFlag,
566
565
  ...ptFlag,
@@ -570,13 +569,10 @@ export function findMatch<TEnv>(
570
569
  entry,
571
570
  routeKey,
572
571
  params,
573
- optionalParams,
574
572
  ...prFlag,
575
573
  ...ptFlag,
576
574
  };
577
575
  } else {
578
- // No explicit mode - use pattern-based detection
579
- // Redirect to canonical form (what the pattern defines)
580
576
  const canonicalPath = hasTrailingSlash
581
577
  ? alternatePathname
582
578
  : pathname.slice(0, -1);
@@ -584,7 +580,6 @@ export function findMatch<TEnv>(
584
580
  entry,
585
581
  routeKey,
586
582
  params,
587
- optionalParams,
588
583
  redirectTo: canonicalPath,
589
584
  ...prFlag,
590
585
  ...ptFlag,
@@ -605,7 +600,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
605
600
  let current: EntryData | null = entry;
606
601
  const items = [] as EntryData[];
607
602
  while (current !== null) {
608
- items.push(current); // Move up to next parent
603
+ items.push(current);
609
604
  current = current.parent;
610
605
  }
611
606
  for (let i = items.length - 1; i >= 0; i--) {