@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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 (376) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +198 -44
  3. package/dist/bin/rango.js +287 -105
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3248 -1117
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +73 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +107 -1
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +245 -21
  12. package/skills/caching/SKILL.md +302 -6
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +364 -0
  17. package/skills/hooks/SKILL.md +270 -30
  18. package/skills/host-router/SKILL.md +82 -22
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +49 -5
  21. package/skills/layout/SKILL.md +35 -9
  22. package/skills/links/SKILL.md +249 -17
  23. package/skills/loader/SKILL.md +294 -30
  24. package/skills/middleware/SKILL.md +52 -13
  25. package/skills/migrate-nextjs/SKILL.md +584 -0
  26. package/skills/migrate-react-router/SKILL.md +769 -0
  27. package/skills/mime-routes/SKILL.md +27 -0
  28. package/skills/observability/SKILL.md +137 -0
  29. package/skills/parallel/SKILL.md +203 -7
  30. package/skills/prerender/SKILL.md +123 -100
  31. package/skills/rango/SKILL.md +250 -22
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +122 -47
  34. package/skills/route/SKILL.md +97 -5
  35. package/skills/router-setup/SKILL.md +90 -5
  36. package/skills/server-actions/SKILL.md +775 -0
  37. package/skills/streams-and-websockets/SKILL.md +283 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +129 -0
  40. package/skills/testing/bindings.md +89 -0
  41. package/skills/testing/cache-prerender.md +124 -0
  42. package/skills/testing/client-components.md +122 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +92 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +121 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +120 -0
  54. package/skills/typesafety/SKILL.md +329 -27
  55. package/skills/use-cache/SKILL.md +36 -5
  56. package/skills/view-transitions/SKILL.md +294 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +67 -40
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/app-version.ts +14 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +86 -147
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/link-interceptor.ts +4 -0
  70. package/src/browser/navigation-bridge.ts +148 -19
  71. package/src/browser/navigation-client.ts +187 -67
  72. package/src/browser/navigation-store-handle.ts +38 -0
  73. package/src/browser/navigation-store.ts +76 -67
  74. package/src/browser/navigation-transaction.ts +18 -66
  75. package/src/browser/partial-update.ts +123 -94
  76. package/src/browser/prefetch/cache.ts +214 -36
  77. package/src/browser/prefetch/fetch.ts +260 -38
  78. package/src/browser/prefetch/policy.ts +6 -0
  79. package/src/browser/prefetch/queue.ts +126 -20
  80. package/src/browser/prefetch/resource-ready.ts +77 -0
  81. package/src/browser/rango-state.ts +158 -76
  82. package/src/browser/react/Link.tsx +93 -11
  83. package/src/browser/react/NavigationProvider.tsx +115 -34
  84. package/src/browser/react/ScrollRestoration.tsx +10 -6
  85. package/src/browser/react/context.ts +7 -2
  86. package/src/browser/react/filter-segment-order.ts +49 -7
  87. package/src/browser/react/index.ts +0 -48
  88. package/src/browser/react/location-state-shared.ts +166 -8
  89. package/src/browser/react/location-state.ts +39 -14
  90. package/src/browser/react/use-action.ts +6 -15
  91. package/src/browser/react/use-handle.ts +23 -69
  92. package/src/browser/react/use-link-status.ts +0 -4
  93. package/src/browser/react/use-navigation.ts +22 -5
  94. package/src/browser/react/use-params.ts +20 -10
  95. package/src/browser/react/use-reverse.ts +106 -0
  96. package/src/browser/react/use-router.ts +46 -11
  97. package/src/browser/react/use-search-params.ts +0 -5
  98. package/src/browser/react/use-segments.ts +11 -21
  99. package/src/browser/response-adapter.ts +52 -1
  100. package/src/browser/rsc-router.tsx +215 -76
  101. package/src/browser/scroll-restoration.ts +46 -39
  102. package/src/browser/segment-reconciler.ts +36 -9
  103. package/src/browser/segment-structure-assert.ts +2 -2
  104. package/src/browser/server-action-bridge.ts +176 -50
  105. package/src/browser/types.ts +95 -11
  106. package/src/browser/validate-redirect-origin.ts +43 -16
  107. package/src/build/collect-fallback-refs.ts +107 -0
  108. package/src/build/generate-manifest.ts +65 -40
  109. package/src/build/generate-route-types.ts +5 -0
  110. package/src/build/index.ts +8 -2
  111. package/src/build/prefix-tree-utils.ts +123 -0
  112. package/src/build/route-trie.ts +137 -32
  113. package/src/build/route-types/codegen.ts +4 -4
  114. package/src/build/route-types/include-resolution.ts +9 -2
  115. package/src/build/route-types/param-extraction.ts +6 -3
  116. package/src/build/route-types/per-module-writer.ts +7 -4
  117. package/src/build/route-types/router-processing.ts +278 -96
  118. package/src/build/route-types/scan-filter.ts +9 -2
  119. package/src/build/route-types/source-scan.ts +118 -0
  120. package/src/build/runtime-discovery.ts +9 -20
  121. package/src/cache/cache-error.ts +104 -0
  122. package/src/cache/cache-policy.ts +68 -28
  123. package/src/cache/cache-runtime.ts +149 -43
  124. package/src/cache/cache-scope.ts +148 -81
  125. package/src/cache/cache-tag.ts +98 -0
  126. package/src/cache/cf/cf-cache-store.ts +2550 -93
  127. package/src/cache/cf/index.ts +11 -17
  128. package/src/cache/document-cache.ts +78 -27
  129. package/src/cache/handle-snapshot.ts +63 -0
  130. package/src/cache/index.ts +23 -20
  131. package/src/cache/memory-segment-store.ts +136 -37
  132. package/src/cache/profile-registry.ts +6 -30
  133. package/src/cache/read-through-swr.ts +41 -11
  134. package/src/cache/segment-codec.ts +0 -16
  135. package/src/cache/tag-invalidation.ts +230 -0
  136. package/src/cache/taint.ts +55 -0
  137. package/src/cache/types.ts +33 -100
  138. package/src/cache/vercel/index.ts +11 -0
  139. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  140. package/src/client.rsc.tsx +6 -21
  141. package/src/client.tsx +108 -290
  142. package/src/component-utils.ts +19 -0
  143. package/src/context-var.ts +84 -2
  144. package/src/debug.ts +2 -2
  145. package/src/decode-loader-results.ts +36 -0
  146. package/src/defer.ts +196 -0
  147. package/src/deps/ssr.ts +0 -1
  148. package/src/errors.ts +30 -4
  149. package/src/handle.ts +70 -22
  150. package/src/handles/MetaTags.tsx +0 -14
  151. package/src/handles/breadcrumbs.ts +16 -5
  152. package/src/handles/meta.ts +0 -39
  153. package/src/host/cookie-handler.ts +0 -36
  154. package/src/host/errors.ts +0 -24
  155. package/src/host/index.ts +8 -2
  156. package/src/host/pattern-matcher.ts +7 -50
  157. package/src/host/router.ts +107 -99
  158. package/src/host/testing.ts +40 -27
  159. package/src/host/types.ts +37 -4
  160. package/src/host/utils.ts +1 -1
  161. package/src/href-client.ts +137 -22
  162. package/src/index.rsc.ts +52 -26
  163. package/src/index.ts +100 -38
  164. package/src/internal-debug.ts +2 -4
  165. package/src/loader-store.ts +500 -0
  166. package/src/loader.rsc.ts +20 -13
  167. package/src/loader.ts +12 -11
  168. package/src/missing-id-error.ts +68 -0
  169. package/src/network-error-thrower.tsx +1 -6
  170. package/src/outlet-context.ts +1 -1
  171. package/src/outlet-provider.tsx +1 -5
  172. package/src/prerender/param-hash.ts +10 -11
  173. package/src/prerender/store.ts +37 -41
  174. package/src/prerender.ts +198 -82
  175. package/src/redirect-origin.ts +100 -0
  176. package/src/response-utils.ts +37 -0
  177. package/src/reverse.ts +65 -15
  178. package/src/root-error-boundary.tsx +1 -19
  179. package/src/route-content-wrapper.tsx +7 -72
  180. package/src/route-definition/dsl-helpers.ts +437 -274
  181. package/src/route-definition/helper-factories.ts +29 -139
  182. package/src/route-definition/helpers-types.ts +113 -37
  183. package/src/route-definition/index.ts +3 -0
  184. package/src/route-definition/redirect.ts +52 -10
  185. package/src/route-definition/resolve-handler-use.ts +161 -0
  186. package/src/route-definition/use-item-types.ts +32 -0
  187. package/src/route-map-builder.ts +7 -17
  188. package/src/route-types.ts +37 -41
  189. package/src/router/basename.ts +14 -0
  190. package/src/router/content-negotiation.ts +108 -9
  191. package/src/router/error-handling.ts +13 -17
  192. package/src/router/find-match.ts +45 -22
  193. package/src/router/handler-context.ts +83 -41
  194. package/src/router/intercept-resolution.ts +25 -23
  195. package/src/router/lazy-includes.ts +19 -53
  196. package/src/router/loader-resolution.ts +213 -30
  197. package/src/router/logging.ts +5 -8
  198. package/src/router/manifest.ts +49 -45
  199. package/src/router/match-api.ts +121 -205
  200. package/src/router/match-context.ts +0 -22
  201. package/src/router/match-handlers.ts +58 -58
  202. package/src/router/match-middleware/background-revalidation.ts +27 -6
  203. package/src/router/match-middleware/cache-lookup.ts +205 -249
  204. package/src/router/match-middleware/cache-store.ts +45 -32
  205. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  206. package/src/router/match-middleware/segment-resolution.ts +52 -18
  207. package/src/router/match-pipelines.ts +1 -42
  208. package/src/router/match-result.ts +104 -40
  209. package/src/router/metrics.ts +5 -34
  210. package/src/router/middleware-types.ts +13 -142
  211. package/src/router/middleware.ts +173 -143
  212. package/src/router/navigation-snapshot.ts +131 -0
  213. package/src/router/params-util.ts +23 -0
  214. package/src/router/pattern-matching.ts +109 -63
  215. package/src/router/prerender-match.ts +192 -54
  216. package/src/router/preview-match.ts +32 -102
  217. package/src/router/request-classification.ts +276 -0
  218. package/src/router/revalidation.ts +63 -55
  219. package/src/router/route-snapshot.ts +244 -0
  220. package/src/router/router-context.ts +6 -28
  221. package/src/router/router-interfaces.ts +100 -35
  222. package/src/router/router-options.ts +91 -11
  223. package/src/router/router-registry.ts +2 -5
  224. package/src/router/segment-resolution/fresh.ts +242 -75
  225. package/src/router/segment-resolution/helpers.ts +64 -25
  226. package/src/router/segment-resolution/loader-cache.ts +41 -37
  227. package/src/router/segment-resolution/revalidation.ts +456 -372
  228. package/src/router/segment-resolution/static-store.ts +19 -5
  229. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  230. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  231. package/src/router/segment-resolution.ts +4 -1
  232. package/src/router/segment-wrappers.ts +2 -3
  233. package/src/router/state-cookie-name.ts +33 -0
  234. package/src/router/substitute-pattern-params.ts +56 -0
  235. package/src/router/telemetry-otel.ts +0 -20
  236. package/src/router/telemetry.ts +96 -19
  237. package/src/router/timeout.ts +0 -20
  238. package/src/router/trie-matching.ts +91 -46
  239. package/src/router/types.ts +10 -63
  240. package/src/router/url-params.ts +44 -0
  241. package/src/router.ts +134 -43
  242. package/src/rsc/handler-context.ts +3 -2
  243. package/src/rsc/handler.ts +492 -383
  244. package/src/rsc/helpers.ts +162 -46
  245. package/src/rsc/index.ts +1 -1
  246. package/src/rsc/json-route-result.ts +38 -0
  247. package/src/rsc/loader-fetch.ts +23 -3
  248. package/src/rsc/manifest-init.ts +33 -42
  249. package/src/rsc/origin-guard.ts +39 -25
  250. package/src/rsc/progressive-enhancement.ts +30 -3
  251. package/src/rsc/redirect-guard.ts +99 -0
  252. package/src/rsc/response-error.ts +79 -12
  253. package/src/rsc/response-route-handler.ts +90 -63
  254. package/src/rsc/rsc-rendering.ts +56 -54
  255. package/src/rsc/runtime-warnings.ts +23 -10
  256. package/src/rsc/server-action.ts +74 -67
  257. package/src/rsc/ssr-setup.ts +18 -2
  258. package/src/rsc/types.ts +25 -6
  259. package/src/runtime-env.ts +18 -0
  260. package/src/search-params.ts +4 -20
  261. package/src/segment-content-promise.ts +67 -0
  262. package/src/segment-loader-promise.ts +134 -0
  263. package/src/segment-system.tsx +272 -129
  264. package/src/serialize.ts +243 -0
  265. package/src/server/context.ts +309 -61
  266. package/src/server/cookie-store.ts +80 -5
  267. package/src/server/handle-store.ts +26 -24
  268. package/src/server/loader-registry.ts +10 -28
  269. package/src/server/request-context.ts +348 -128
  270. package/src/ssr/index.tsx +23 -15
  271. package/src/static-handler.ts +27 -18
  272. package/src/testing/cache-status.ts +162 -0
  273. package/src/testing/collect-handle.ts +40 -0
  274. package/src/testing/dispatch.ts +618 -0
  275. package/src/testing/dom.entry.ts +22 -0
  276. package/src/testing/e2e/fixture.ts +188 -0
  277. package/src/testing/e2e/index.ts +128 -0
  278. package/src/testing/e2e/matchers.ts +35 -0
  279. package/src/testing/e2e/page-helpers.ts +272 -0
  280. package/src/testing/e2e/parity.ts +387 -0
  281. package/src/testing/e2e/server.ts +195 -0
  282. package/src/testing/flight-matchers.ts +97 -0
  283. package/src/testing/flight-normalize.ts +11 -0
  284. package/src/testing/flight-runtime.d.ts +57 -0
  285. package/src/testing/flight-tree.ts +682 -0
  286. package/src/testing/flight.entry.ts +52 -0
  287. package/src/testing/flight.ts +232 -0
  288. package/src/testing/generated-routes.ts +183 -0
  289. package/src/testing/index.ts +99 -0
  290. package/src/testing/internal/context.ts +348 -0
  291. package/src/testing/internal/flight-client-globals.ts +30 -0
  292. package/src/testing/internal/seed-vars.ts +54 -0
  293. package/src/testing/render-handler.ts +330 -0
  294. package/src/testing/render-route.tsx +566 -0
  295. package/src/testing/run-loader.ts +378 -0
  296. package/src/testing/run-middleware.ts +205 -0
  297. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  298. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  299. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  300. package/src/testing/vitest-stubs/version.ts +5 -0
  301. package/src/testing/vitest.ts +305 -0
  302. package/src/theme/ThemeProvider.tsx +0 -52
  303. package/src/theme/ThemeScript.tsx +0 -6
  304. package/src/theme/constants.ts +0 -12
  305. package/src/theme/index.ts +0 -7
  306. package/src/theme/theme-context.ts +1 -5
  307. package/src/theme/theme-script.ts +0 -14
  308. package/src/theme/use-theme.ts +0 -3
  309. package/src/types/boundaries.ts +0 -35
  310. package/src/types/cache-types.ts +17 -8
  311. package/src/types/error-types.ts +30 -90
  312. package/src/types/global-namespace.ts +54 -41
  313. package/src/types/handler-context.ts +233 -81
  314. package/src/types/index.ts +1 -10
  315. package/src/types/loader-types.ts +44 -15
  316. package/src/types/request-scope.ts +107 -0
  317. package/src/types/route-config.ts +6 -50
  318. package/src/types/route-entry.ts +19 -7
  319. package/src/types/segments.ts +37 -14
  320. package/src/urls/include-helper.ts +33 -70
  321. package/src/urls/index.ts +1 -11
  322. package/src/urls/path-helper-types.ts +58 -11
  323. package/src/urls/path-helper.ts +57 -111
  324. package/src/urls/pattern-types.ts +48 -19
  325. package/src/urls/response-types.ts +25 -22
  326. package/src/urls/type-extraction.ts +58 -139
  327. package/src/urls/urls-function.ts +1 -18
  328. package/src/use-loader.tsx +346 -89
  329. package/src/vite/debug.ts +185 -0
  330. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  331. package/src/vite/discovery/discover-routers.ts +130 -85
  332. package/src/vite/discovery/discovery-errors.ts +194 -0
  333. package/src/vite/discovery/gate-state.ts +171 -0
  334. package/src/vite/discovery/prerender-collection.ts +192 -99
  335. package/src/vite/discovery/route-types-writer.ts +40 -84
  336. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  337. package/src/vite/discovery/state.ts +51 -6
  338. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  339. package/src/vite/index.ts +8 -0
  340. package/src/vite/plugin-types.ts +187 -69
  341. package/src/vite/plugins/cjs-to-esm.ts +8 -18
  342. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  343. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  344. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  345. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  346. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  347. package/src/vite/plugins/expose-action-id.ts +49 -98
  348. package/src/vite/plugins/expose-id-utils.ts +11 -50
  349. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  350. package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
  351. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  352. package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
  353. package/src/vite/plugins/expose-internal-ids.ts +554 -317
  354. package/src/vite/plugins/performance-tracks.ts +89 -0
  355. package/src/vite/plugins/refresh-cmd.ts +89 -27
  356. package/src/vite/plugins/use-cache-transform.ts +73 -83
  357. package/src/vite/plugins/vercel-output.ts +258 -0
  358. package/src/vite/plugins/version-injector.ts +21 -25
  359. package/src/vite/plugins/version-plugin.ts +41 -20
  360. package/src/vite/plugins/virtual-entries.ts +2 -17
  361. package/src/vite/rango.ts +257 -289
  362. package/src/vite/router-discovery.ts +930 -140
  363. package/src/vite/utils/ast-handler-extract.ts +15 -31
  364. package/src/vite/utils/banner.ts +4 -4
  365. package/src/vite/utils/bundle-analysis.ts +10 -15
  366. package/src/vite/utils/client-chunks.ts +184 -0
  367. package/src/vite/utils/forward-user-plugins.ts +171 -0
  368. package/src/vite/utils/manifest-utils.ts +4 -59
  369. package/src/vite/utils/package-resolution.ts +20 -52
  370. package/src/vite/utils/prerender-utils.ts +27 -29
  371. package/src/vite/utils/shared-utils.ts +92 -42
  372. package/src/browser/action-response-classifier.ts +0 -99
  373. package/src/browser/react/use-client-cache.ts +0 -58
  374. package/src/browser/shallow.ts +0 -40
  375. package/src/handles/index.ts +0 -7
  376. package/src/router/middleware-cookies.ts +0 -55
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { TrieNode, TrieLeaf } from "../build/route-trie.js";
9
+ import { safeDecodeURIComponent } from "./url-params.js";
9
10
 
10
11
  export interface TrieMatchResult {
11
12
  /** Route name */
@@ -14,10 +15,6 @@ export interface TrieMatchResult {
14
15
  sp: string;
15
16
  /** Matched route params */
16
17
  params: Record<string, string>;
17
- /** Optional param names (absent params have empty string value) */
18
- optionalParams?: string[];
19
- /** Ancestry shortCodes for layout pruning */
20
- ancestry: string[];
21
18
  /** Redirect target if trailing slash requires it */
22
19
  redirectTo?: string;
23
20
  /** Route has pre-rendered data available */
@@ -42,14 +39,12 @@ export function tryTrieMatch(
42
39
  ): TrieMatchResult | null {
43
40
  if (!trie) return null;
44
41
 
45
- // Split pathname into segments, filtering empty strings from leading/trailing slashes
46
42
  const pathnameHasTrailingSlash =
47
43
  pathname.length > 1 && pathname.endsWith("/");
48
44
  const normalizedPath = pathnameHasTrailingSlash
49
45
  ? pathname.slice(0, -1)
50
46
  : pathname;
51
47
 
52
- // Handle root path
53
48
  if (normalizedPath === "" || normalizedPath === "/") {
54
49
  if (trie.r) {
55
50
  return validateAndBuild(
@@ -60,13 +55,24 @@ export function tryTrieMatch(
60
55
  pathnameHasTrailingSlash,
61
56
  );
62
57
  }
58
+ // A root-level wildcard ("/*") matches "/" with an empty remainder, the
59
+ // same value the regex matcher produces for the bare prefix. Without this
60
+ // the trie misses, the regex fallback runs, and its no-config branch emits
61
+ // a corrupt slice-off redirect. The static terminal still wins above.
62
+ if (trie.w) {
63
+ return validateAndBuild(
64
+ trie.w,
65
+ [],
66
+ "",
67
+ pathname,
68
+ pathnameHasTrailingSlash,
69
+ );
70
+ }
63
71
  return null;
64
72
  }
65
73
 
66
- // Remove leading slash and split
67
74
  const segments = normalizedPath.slice(1).split("/");
68
75
 
69
- // Try exact match with normalized path (no trailing slash)
70
76
  const result = walkTrie(trie, segments, 0, []);
71
77
  if (result) {
72
78
  return validateAndBuild(
@@ -88,8 +94,58 @@ interface WalkResult {
88
94
  }
89
95
 
90
96
  /**
91
- * Walk the trie by segments with priority: static > param > wildcard.
92
- * Uses backtracking to try all possible matches.
97
+ * Check a leaf's constraints (leaf.cv) against already-resolved named params.
98
+ * Empty/undefined values are exempt (optional params that were not bound).
99
+ */
100
+ function constraintsSatisfied(
101
+ leaf: TrieLeaf,
102
+ params: Record<string, string>,
103
+ ): boolean {
104
+ if (!leaf.cv) return true;
105
+ for (const paramName in leaf.cv) {
106
+ const allowed = leaf.cv[paramName]!;
107
+ const value = params[paramName];
108
+ if (value !== undefined && value !== "" && !allowed.includes(value)) {
109
+ return false;
110
+ }
111
+ }
112
+ return true;
113
+ }
114
+
115
+ /**
116
+ * Constraint check for a candidate terminal DURING the walk. Builds the named
117
+ * params from positional walk values (decoded the same way validateAndBuild
118
+ * does) and validates leaf.cv. Returning false lets walkTrie unwind to a
119
+ * lower-priority sibling instead of committing to a leaf that would only be
120
+ * rejected post-walk — that post-walk rejection is what forced the regex
121
+ * fallback (and its false "trie gap" R3 warning) for perfectly valid configs.
122
+ */
123
+ function leafConstraintsPass(
124
+ leaf: TrieLeaf,
125
+ paramValues: string[],
126
+ wildcardValue: string | undefined,
127
+ ): boolean {
128
+ if (!leaf.cv) return true;
129
+ const params: Record<string, string> = {};
130
+ if (leaf.pa) {
131
+ for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
132
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
133
+ }
134
+ }
135
+ if (wildcardValue !== undefined && "pn" in leaf) {
136
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
137
+ safeDecodeURIComponent(wildcardValue);
138
+ }
139
+ return constraintsSatisfied(leaf, params);
140
+ }
141
+
142
+ /**
143
+ * Walk the trie by segments with priority: static > suffix-param > param >
144
+ * wildcard (Priority 1-4 below; matches the canonical M4 ordering in
145
+ * docs/internal/matching-and-lazy-discovery.md).
146
+ * Uses backtracking to try all possible matches. Per-leaf constraints are
147
+ * enforced at each candidate terminal so a constraint miss backtracks to a
148
+ * lower-priority sibling rather than aborting the whole match.
93
149
  */
94
150
  function walkTrie(
95
151
  node: TrieNode,
@@ -97,25 +153,34 @@ function walkTrie(
97
153
  index: number,
98
154
  paramValues: string[],
99
155
  ): WalkResult | null {
100
- // All segments consumed: check for terminal
101
156
  if (index === segments.length) {
102
- if (node.r) {
157
+ if (node.r && leafConstraintsPass(node.r, paramValues, undefined)) {
103
158
  return { leaf: node.r, paramValues: [...paramValues] };
104
159
  }
160
+ // A wildcard at this node matches the bare prefix with an empty remainder
161
+ // (e.g. "/files" against "/files/*"), mirroring the regex matcher's `*=""`.
162
+ // walkTrie otherwise only reaches node.w in the index<length branch below,
163
+ // so without this a request to the wildcard's own prefix misses the trie
164
+ // and the regex fallback emits a corrupt redirect. A static terminal
165
+ // (node.r) still wins.
166
+ if (node.w && leafConstraintsPass(node.w, paramValues, "")) {
167
+ return { leaf: node.w, paramValues: [...paramValues], wildcardValue: "" };
168
+ }
105
169
  return null;
106
170
  }
107
171
 
108
172
  const segment = segments[index];
109
173
  const staticChild = node.s?.[segment];
110
174
 
111
- // Priority 1: Static match
112
175
  if (staticChild) {
113
176
  const result = walkTrie(staticChild, segments, index + 1, paramValues);
114
177
  if (result) return result;
115
178
  }
116
179
 
117
- // Priority 2: Suffix-param match (e.g., :productId.html)
118
180
  if (node.xp) {
181
+ // node.xp keys are pre-sorted longest-suffix-first at build time
182
+ // (route-trie.ts sortSuffixParams), so the first match is the most specific
183
+ // suffix: `/app.min.js` matches `:file.min.js` before `:file.js`.
119
184
  for (const suffix in node.xp) {
120
185
  if (segment.endsWith(suffix) && segment.length > suffix.length) {
121
186
  const paramValue = segment.slice(0, -suffix.length);
@@ -132,7 +197,6 @@ function walkTrie(
132
197
  }
133
198
  }
134
199
 
135
- // Priority 3: Param match
136
200
  if (node.p) {
137
201
  paramValues.push(segment);
138
202
  const result = walkTrie(node.p.c, segments, index + 1, paramValues);
@@ -140,14 +204,15 @@ function walkTrie(
140
204
  if (result) return result;
141
205
  }
142
206
 
143
- // Priority 4: Wildcard match (consumes rest)
144
207
  if (node.w) {
145
208
  const rest = joinRemainingSegments(segments, index);
146
- return {
147
- leaf: node.w,
148
- paramValues: [...paramValues],
149
- wildcardValue: rest,
150
- };
209
+ if (leafConstraintsPass(node.w, paramValues, rest)) {
210
+ return {
211
+ leaf: node.w,
212
+ paramValues: [...paramValues],
213
+ wildcardValue: rest,
214
+ };
215
+ }
151
216
  }
152
217
 
153
218
  return null;
@@ -173,40 +238,22 @@ function validateAndBuild(
173
238
  originalPathname: string,
174
239
  pathnameHasTrailingSlash: boolean,
175
240
  ): TrieMatchResult | null {
176
- // Build named params by zipping leaf.pa with positional paramValues
177
241
  const params: Record<string, string> = {};
178
242
  if (leaf.pa) {
179
243
  for (let i = 0; i < leaf.pa.length && i < paramValues.length; i++) {
180
- params[leaf.pa[i]] = paramValues[i];
244
+ params[leaf.pa[i]] = safeDecodeURIComponent(paramValues[i]);
181
245
  }
182
246
  }
183
247
 
184
- // Add wildcard param (wildcard leaves have pn from TrieNode.w type)
185
248
  if (wildcardValue !== undefined && "pn" in leaf) {
186
- params[(leaf as TrieLeaf & { pn: string }).pn] = wildcardValue;
249
+ params[(leaf as TrieLeaf & { pn: string }).pn] =
250
+ safeDecodeURIComponent(wildcardValue);
187
251
  }
188
252
 
189
- // Validate constraints
190
- if (leaf.cv) {
191
- for (const paramName in leaf.cv) {
192
- const allowed = leaf.cv[paramName]!;
193
- const value = params[paramName];
194
- if (value !== undefined && value !== "" && !allowed.includes(value)) {
195
- return null;
196
- }
197
- }
198
- }
199
-
200
- // Fill in empty strings for optional params that weren't matched
201
- if (leaf.op) {
202
- for (const name of leaf.op) {
203
- if (!(name in params)) {
204
- params[name] = "";
205
- }
206
- }
253
+ if (!constraintsSatisfied(leaf, params)) {
254
+ return null;
207
255
  }
208
256
 
209
- // Trailing slash handling
210
257
  const tsMode = leaf.ts as "never" | "always" | "ignore" | undefined;
211
258
  let redirectTo: string | undefined;
212
259
 
@@ -224,10 +271,8 @@ function validateAndBuild(
224
271
  routeKey: leaf.n,
225
272
  sp: leaf.sp,
226
273
  params,
227
- ancestry: leaf.a,
228
274
  };
229
275
 
230
- if (leaf.op) result.optionalParams = leaf.op;
231
276
  if (redirectTo) result.redirectTo = redirectTo;
232
277
  if (leaf.pr) result.pr = true;
233
278
  if (leaf.pt) result.pt = true;
@@ -22,27 +22,11 @@ import type {
22
22
  ShouldRevalidateFn,
23
23
  } from "../types";
24
24
 
25
- /**
26
- * Result of resolving loaders with revalidation
27
- * Contains both segments to render and all matched segment IDs
28
- */
29
- export interface LoaderRevalidationResult {
30
- segments: ResolvedSegment[];
31
- matchedIds: string[];
32
- }
33
-
34
- /**
35
- * Result of resolving segments with revalidation
36
- * Contains both segments to render and all matched segment IDs
37
- */
38
25
  export interface SegmentRevalidationResult {
39
26
  segments: ResolvedSegment[];
40
27
  matchedIds: string[];
41
28
  }
42
29
 
43
- /**
44
- * Action context type for revalidation
45
- */
46
30
  export type ActionContext = {
47
31
  actionId?: string;
48
32
  actionUrl?: URL;
@@ -50,23 +34,6 @@ export type ActionContext = {
50
34
  formData?: FormData;
51
35
  };
52
36
 
53
- /**
54
- * Dependencies passed to segment resolution functions
55
- * These are created within createRouter and passed to extracted utilities
56
- */
57
- export interface RouterDependencies<TEnv> {
58
- findNearestErrorBoundary: (
59
- entry: EntryData | null,
60
- ) => ReactNode | ErrorBoundaryHandler | null;
61
- findNearestNotFoundBoundary: (
62
- entry: EntryData | null,
63
- ) => ReactNode | NotFoundBoundaryHandler | null;
64
- }
65
-
66
- /**
67
- * Dependencies injected from createRouter closure into extracted segment resolution functions.
68
- * These are the closure-bound helpers that cannot be imported directly.
69
- */
70
37
  export interface SegmentResolutionDeps<TEnv = any> {
71
38
  wrapLoaderPromise: <T>(
72
39
  promise: Promise<T>,
@@ -96,24 +63,18 @@ export interface SegmentResolutionDeps<TEnv = any> {
96
63
  findNearestNotFoundBoundary: (
97
64
  entry: EntryData | null,
98
65
  ) => ReactNode | NotFoundBoundaryHandler | null;
66
+ notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
99
67
  callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
68
+ /**
69
+ * Router-level default for the per-segment `transition({ viewTransition })`
70
+ * flag, from createRouter({ viewTransition }). Resolved into each segment's
71
+ * transition config during resolution (only `false` is stamped) so the render
72
+ * gate reads the boundary decision off the segment on both server and client.
73
+ * Undefined is treated as "auto" (wrap).
74
+ */
75
+ viewTransitionDefault?: "auto" | false;
100
76
  }
101
77
 
102
- /**
103
- * Dependencies injected from createRouter closure into extracted intercept resolution functions.
104
- */
105
- export interface InterceptResolutionDeps<TEnv = any> {
106
- wrapLoaderPromise: SegmentResolutionDeps<TEnv>["wrapLoaderPromise"];
107
- evaluateInterceptWhen: (
108
- intercept: InterceptEntry,
109
- selectorContext: InterceptSelectorContext | null,
110
- isAction: boolean,
111
- ) => boolean;
112
- }
113
-
114
- /**
115
- * Dependencies injected from createRouter closure into extracted match API functions.
116
- */
117
78
  export interface MatchApiDeps<TEnv = any> {
118
79
  findMatch: (pathname: string, ms?: any) => any;
119
80
  getMetricsStore: () => any;
@@ -128,23 +89,13 @@ export interface MatchApiDeps<TEnv = any> {
128
89
  getRouteMap: () => Record<string, string>;
129
90
  }
130
91
 
131
- /**
132
- * Title descriptor types for template support
133
- */
134
92
  export type TitleDescriptor =
135
93
  | string
136
94
  | { template: string; default: string } // For layouts - template applied to child titles
137
- | { absolute: string }; // Bypass parent template
95
+ | { absolute: string };
138
96
 
139
- /**
140
- * Unset descriptor to remove inherited meta
141
- * Key format matches getMetaKey output: "title", "name:description", "property:og:image"
142
- */
143
97
  export type UnsetDescriptor = { unset: string };
144
98
 
145
- /**
146
- * Base meta descriptor types (sync values)
147
- */
148
99
  export type MetaDescriptorBase =
149
100
  | { charSet: "utf-8" }
150
101
  | { title: TitleDescriptor }
@@ -156,10 +107,6 @@ export type MetaDescriptorBase =
156
107
  | UnsetDescriptor
157
108
  | { [name: string]: unknown };
158
109
 
159
- /**
160
- * Meta descriptor that can be sync or async.
161
- * Use Promise<MetaDescriptorBase> for streaming meta that resolves after initial render.
162
- */
163
110
  export type MetaDescriptor = MetaDescriptorBase | Promise<MetaDescriptorBase>;
164
111
 
165
112
  type LdJsonObject = { [Key in string]: LdJsonValue } & {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * URL param encode/decode at the route boundary.
3
+ *
4
+ * Extraction (decode): regex/trie matchers keep param values URL-encoded;
5
+ * `safeDecodeURIComponent` turns them back into raw strings so `ctx.params`
6
+ * matches the contract apps expect (Express/React Router/Fastify/Koa) and
7
+ * round-trips through reverse stay stable. Malformed %-encoding is
8
+ * preserved as-is so a broken URL doesn't crash matching.
9
+ *
10
+ * Reversal (encode): `encodePathSegment` escapes only what RFC 3986
11
+ * requires for a path segment — `/`, `?`, `#`, space, control chars,
12
+ * non-ASCII — and leaves pchar sub-delims (`@ : $ & + , ; =` and friends)
13
+ * readable. `encodeURIComponent` over-encodes for path segments, which
14
+ * makes generated URLs harder for humans to read in the address bar
15
+ * (e.g. mailbox IDs like `ivo@example.com` would become
16
+ * `ivo%40example.com` even though `@` is path-legal).
17
+ */
18
+
19
+ export function safeDecodeURIComponent(raw: string): string {
20
+ if (raw === "" || raw.indexOf("%") === -1) return raw;
21
+ try {
22
+ return decodeURIComponent(raw);
23
+ } catch {
24
+ return raw;
25
+ }
26
+ }
27
+
28
+ const PATH_SAFE_ESCAPES: Record<string, string> = {
29
+ "%3A": ":",
30
+ "%40": "@",
31
+ "%24": "$",
32
+ "%26": "&",
33
+ "%2B": "+",
34
+ "%2C": ",",
35
+ "%3B": ";",
36
+ "%3D": "=",
37
+ };
38
+
39
+ export function encodePathSegment(value: string): string {
40
+ return encodeURIComponent(value).replace(
41
+ /%(?:3A|40|24|26|2B|2C|3B|3D)/gi,
42
+ (match) => PATH_SAFE_ESCAPES[match.toUpperCase()] ?? match,
43
+ );
44
+ }