@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c

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 (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -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 +122 -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 +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -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 +0 -65
  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/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  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/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -5,47 +5,43 @@
5
5
  */
6
6
 
7
7
  /**
8
- * Branded return types for route helpers
8
+ * Brand for UrlPatterns nominal typing (see pattern-types.ts). The route-item
9
+ * types below are discriminated by their `type` literal, so they carry no brand.
9
10
  */
10
- export declare const LayoutBrand: unique symbol;
11
- export declare const RouteBrand: unique symbol;
12
- export declare const ParallelBrand: unique symbol;
13
- export declare const InterceptBrand: unique symbol;
14
- export declare const MiddlewareBrand: unique symbol;
15
- export declare const RevalidateBrand: unique symbol;
16
- export declare const LoaderBrand: unique symbol;
17
- export declare const LoadingBrand: unique symbol;
18
- export declare const ErrorBoundaryBrand: unique symbol;
19
- export declare const NotFoundBoundaryBrand: unique symbol;
20
- export declare const WhenBrand: unique symbol;
21
- export declare const CacheBrand: unique symbol;
22
- export declare const TransitionBrand: unique symbol;
23
- export declare const IncludeBrand: unique symbol;
24
11
  export declare const UrlPatternsBrand: unique symbol;
25
12
 
26
13
  export type LayoutItem = {
27
14
  name: string;
28
15
  type: "layout";
29
16
  uses?: AllUseItems[];
30
- [LayoutBrand]: void;
31
17
  };
32
18
 
33
19
  /**
34
- * Typed layout item that carries child routes as phantom type
35
- * Used for type inference in urls() API
20
+ * Phantom inference fields attached to wrapper items (layout/cache/transition)
21
+ * so the urls() type extractor can read their child routes/responses. The fields
22
+ * never exist at runtime.
36
23
  */
37
- export type TypedLayoutItem<
24
+ type WithChildren<
25
+ TBase,
38
26
  TChildRoutes extends Record<string, any> = Record<string, string>,
39
27
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
40
- > = LayoutItem & {
28
+ > = TBase & {
41
29
  readonly __childRoutes?: TChildRoutes;
42
30
  readonly __childResponses?: TChildResponses;
43
31
  };
32
+
33
+ /**
34
+ * Typed layout item that carries child routes as phantom type
35
+ * Used for type inference in urls() API
36
+ */
37
+ export type TypedLayoutItem<
38
+ TChildRoutes extends Record<string, any> = Record<string, string>,
39
+ TChildResponses extends Record<string, unknown> = Record<string, unknown>,
40
+ > = WithChildren<LayoutItem, TChildRoutes, TChildResponses>;
44
41
  export type RouteItem = {
45
42
  name: string;
46
43
  type: "route";
47
44
  uses?: AllUseItems[];
48
- [RouteBrand]: void;
49
45
  };
50
46
 
51
47
  /**
@@ -67,64 +63,49 @@ export type ParallelItem = {
67
63
  name: string;
68
64
  type: "parallel";
69
65
  uses?: ParallelUseItem[];
70
- [ParallelBrand]: void;
71
66
  };
72
67
  export type InterceptItem = {
73
68
  name: string;
74
69
  type: "intercept";
75
70
  uses?: InterceptUseItem[];
76
- [InterceptBrand]: void;
77
71
  };
78
72
  export type LoaderItem = {
79
73
  name: string;
80
74
  type: "loader";
81
75
  uses?: LoaderUseItem[];
82
- [LoaderBrand]: void;
83
76
  };
84
77
  export type MiddlewareItem = {
85
78
  name: string;
86
79
  type: "middleware";
87
80
  uses?: AllUseItems[];
88
- [MiddlewareBrand]: void;
89
81
  };
90
82
  export type RevalidateItem = {
91
83
  name: string;
92
84
  type: "revalidate";
93
85
  uses?: AllUseItems[];
94
- [RevalidateBrand]: void;
95
86
  };
96
87
  export type LoadingItem = {
97
88
  name: string;
98
89
  type: "loading";
99
- [LoadingBrand]: void;
100
90
  };
101
91
  export type ErrorBoundaryItem = {
102
92
  name: string;
103
93
  type: "errorBoundary";
104
94
  uses?: AllUseItems[];
105
- [ErrorBoundaryBrand]: void;
106
95
  };
107
96
  export type NotFoundBoundaryItem = {
108
97
  name: string;
109
98
  type: "notFoundBoundary";
110
99
  uses?: AllUseItems[];
111
- [NotFoundBoundaryBrand]: void;
112
- };
113
- export type WhenItem = {
114
- name: string;
115
- type: "when";
116
- [WhenBrand]: void;
117
100
  };
118
101
  export type CacheItem = {
119
102
  name: string;
120
103
  type: "cache";
121
104
  uses?: AllUseItems[];
122
- [CacheBrand]: void;
123
105
  };
124
106
  export type TransitionItem = {
125
107
  name: string;
126
108
  type: "transition";
127
- [TransitionBrand]: void;
128
109
  };
129
110
 
130
111
  /**
@@ -134,10 +115,7 @@ export type TransitionItem = {
134
115
  export type TypedTransitionItem<
135
116
  TChildRoutes extends Record<string, any> = Record<string, string>,
136
117
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
137
- > = TransitionItem & {
138
- readonly __childRoutes?: TChildRoutes;
139
- readonly __childResponses?: TChildResponses;
140
- };
118
+ > = WithChildren<TransitionItem, TChildRoutes, TChildResponses>;
141
119
 
142
120
  /**
143
121
  * Typed cache item that carries child routes as phantom type
@@ -146,10 +124,7 @@ export type TypedTransitionItem<
146
124
  export type TypedCacheItem<
147
125
  TChildRoutes extends Record<string, any> = Record<string, string>,
148
126
  TChildResponses extends Record<string, unknown> = Record<string, unknown>,
149
- > = CacheItem & {
150
- readonly __childRoutes?: TChildRoutes;
151
- readonly __childResponses?: TChildResponses;
152
- };
127
+ > = WithChildren<CacheItem, TChildRoutes, TChildResponses>;
153
128
 
154
129
  /**
155
130
  * Include item for URL pattern composition (used by urls() API)
@@ -184,7 +159,6 @@ export type IncludeItem = {
184
159
  */
185
160
  includeScope?: string;
186
161
  };
187
- [IncludeBrand]: void;
188
162
  };
189
163
 
190
164
  /**
@@ -253,7 +227,6 @@ export type InterceptUseItem =
253
227
  | NotFoundBoundaryItem
254
228
  | LayoutItem
255
229
  | RouteItem
256
- | WhenItem
257
230
  | TransitionItem;
258
231
  export type LoaderUseItem = RevalidateItem | CacheItem;
259
232
 
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Normalize a router basename to its canonical form: a single leading slash,
3
+ * no trailing slash, and `undefined` for an empty or bare-"/" value.
4
+ *
5
+ * This is the single source of truth used by both createRouter() (so the RSC
6
+ * handler stores a canonical basename on the request context) and the testing
7
+ * primitives (so a consumer can pass the same un-normalized string their
8
+ * createRouter() accepts and observe the same redirect() prefixing).
9
+ */
10
+ export function normalizeBasename(basename?: string): string | undefined {
11
+ if (!basename) return undefined;
12
+ const trimmed = basename.replace(/^\/+|\/+$/g, "");
13
+ return trimmed ? "/" + trimmed : undefined;
14
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { EntryData } from "../server/context.js";
10
+ import type { NegotiateVariant } from "../build/route-trie.js";
10
11
  import type { CollectedMiddleware } from "./middleware-types.js";
11
12
  import { collectRouteMiddleware } from "./middleware.js";
12
13
  import { loadManifest } from "./manifest.js";
@@ -14,7 +15,6 @@ import { traverseBack } from "./pattern-matching.js";
14
15
  import type { RouteMatchResult } from "./pattern-matching.js";
15
16
  import type { RouteSnapshot } from "./route-snapshot.js";
16
17
 
17
- // Response type -> MIME type used for Accept header matching
18
18
  export const RESPONSE_TYPE_MIME: Record<string, string> = {
19
19
  json: "application/json",
20
20
  text: "text/plain",
@@ -23,7 +23,6 @@ export const RESPONSE_TYPE_MIME: Record<string, string> = {
23
23
  md: "text/markdown",
24
24
  };
25
25
 
26
- // Reverse lookup: MIME type -> response type tag (e.g. "text/html" -> "html")
27
26
  export const MIME_RESPONSE_TYPE: Record<string, string> = Object.fromEntries(
28
27
  Object.entries(RESPONSE_TYPE_MIME).map(([tag, mime]) => [mime, tag]),
29
28
  );
@@ -71,12 +70,10 @@ export function parseAcceptTypes(accept: string): AcceptEntry[] {
71
70
  }
72
71
  entries.push({ mime, q, order: i });
73
72
  }
74
- // Sort: highest q first, then lowest client order first (stable)
75
73
  entries.sort((a, b) => b.q - a.q || a.order - b.order);
76
74
  return entries;
77
75
  }
78
76
 
79
- // Sentinel response type for RSC routes in negotiation candidates
80
77
  export const RSC_RESPONSE_TYPE = "__rsc__";
81
78
 
82
79
  /**
@@ -85,15 +82,10 @@ export const RSC_RESPONSE_TYPE = "__rsc__";
85
82
  * candidate serves that type. Wildcards match the first candidate.
86
83
  * Falls back to the first candidate if nothing matches.
87
84
  */
88
- export function pickNegotiateVariant(
89
- acceptEntries: AcceptEntry[],
90
- candidates: Array<{ routeKey: string; responseType: string }>,
91
- ): { routeKey: string; responseType: string } {
92
- // Build a MIME -> candidate lookup for O(1) matching
93
- const byCandidateMime = new Map<
94
- string,
95
- { routeKey: string; responseType: string }
96
- >();
85
+ export function pickNegotiateVariant<
86
+ T extends { routeKey: string; responseType: string },
87
+ >(acceptEntries: AcceptEntry[], candidates: T[]): T {
88
+ const byCandidateMime = new Map<string, T>();
97
89
  for (const c of candidates) {
98
90
  const mime =
99
91
  c.responseType === RSC_RESPONSE_TYPE
@@ -106,9 +98,7 @@ export function pickNegotiateVariant(
106
98
 
107
99
  for (const entry of acceptEntries) {
108
100
  if (entry.q === 0) continue;
109
- // Wildcard matches first candidate
110
101
  if (entry.mime === "*/*") return candidates[0]!;
111
- // Type wildcard (e.g. "text/*") -- match first candidate with that type
112
102
  if (entry.mime.endsWith("/*")) {
113
103
  const typePrefix = entry.mime.slice(0, entry.mime.indexOf("/"));
114
104
  for (const [mime, candidate] of byCandidateMime) {
@@ -119,10 +109,52 @@ export function pickNegotiateVariant(
119
109
  const match = byCandidateMime.get(entry.mime);
120
110
  if (match) return match;
121
111
  }
122
- // No match -- use first candidate as default
123
112
  return candidates[0]!;
124
113
  }
125
114
 
115
+ /**
116
+ * Re-key params from the primary leaf's names to a winning variant's names.
117
+ *
118
+ * The trie match builds `params` positionally under the primary leaf's pa, so
119
+ * `/widgets/:id` matched as the primary yields `{ id }` even when the winning
120
+ * `/widgets/:file` response variant expects `{ file }`. Both share the same trie
121
+ * terminal, so they bind the same number of positional named params; we zip the
122
+ * variant's pa against the named values in insertion order (which is the
123
+ * primary's pa order). The wildcard key (`*`) is positional-independent and left
124
+ * untouched.
125
+ *
126
+ * Mutates `params` in place. No-op when `variantPa` is absent, when names already
127
+ * match (the common case), or when the positional count diverges (defensive:
128
+ * never corrupt params by zipping mismatched lengths).
129
+ */
130
+ export function rekeyParamsForVariant(
131
+ params: Record<string, string>,
132
+ variantPa: string[] | undefined,
133
+ ): void {
134
+ if (!variantPa || variantPa.length === 0) return;
135
+
136
+ const namedKeys: string[] = [];
137
+ for (const key in params) {
138
+ if (key !== "*") namedKeys.push(key);
139
+ }
140
+ if (namedKeys.length !== variantPa.length) return;
141
+
142
+ let identical = true;
143
+ for (let i = 0; i < variantPa.length; i++) {
144
+ if (namedKeys[i] !== variantPa[i]) {
145
+ identical = false;
146
+ break;
147
+ }
148
+ }
149
+ if (identical) return;
150
+
151
+ const values = namedKeys.map((k) => params[k]!);
152
+ for (const key of namedKeys) delete params[key];
153
+ for (let i = 0; i < variantPa.length; i++) {
154
+ params[variantPa[i]!] = values[i]!;
155
+ }
156
+ }
157
+
126
158
  /**
127
159
  * Result of content negotiation for a route with negotiate variants.
128
160
  */
@@ -135,8 +167,8 @@ export interface NegotiationResult {
135
167
  manifestEntry: EntryData;
136
168
  /** Route middleware for the winning variant */
137
169
  routeMiddleware: CollectedMiddleware[];
138
- /** Always true negotiation occurred */
139
- negotiated: true;
170
+ /** True when negotiation selected a variant; false for a plain response route. */
171
+ negotiated: boolean;
140
172
  }
141
173
 
142
174
  /**
@@ -155,14 +187,28 @@ export async function negotiateRoute(
155
187
  ): Promise<NegotiationResult | null> {
156
188
  const { matched, manifestEntry, routeMiddleware, responseType } = snapshot;
157
189
  if (!matched.negotiateVariants || matched.negotiateVariants.length === 0) {
190
+ // No variants: a plain response route still yields a result (negotiated:false)
191
+ // so callers don't re-derive it; RSC routes (no responseType/handler) -> null.
192
+ const handler =
193
+ manifestEntry.type === "route" ? manifestEntry.handler : undefined;
194
+ if (responseType && handler) {
195
+ return {
196
+ responseType,
197
+ handler: handler as Function,
198
+ manifestEntry,
199
+ routeMiddleware,
200
+ negotiated: false,
201
+ };
202
+ }
158
203
  return null;
159
204
  }
160
205
 
161
206
  const acceptEntries = parseAcceptTypes(request.headers.get("accept") || "");
162
207
 
163
- // Build candidate list preserving definition order.
164
- const variants = matched.negotiateVariants;
165
- let candidates: Array<{ routeKey: string; responseType: string }>;
208
+ // Variants carry the variant's own pa (positional param names); the synthetic
209
+ // primary/RSC candidates have none (their params are already keyed correctly).
210
+ const variants = matched.negotiateVariants as NegotiateVariant[];
211
+ let candidates: NegotiateVariant[];
166
212
  if (responseType) {
167
213
  candidates = [...variants, { routeKey: matched.routeKey, responseType }];
168
214
  } else {
@@ -177,12 +223,10 @@ export async function negotiateRoute(
177
223
 
178
224
  const variant = pickNegotiateVariant(acceptEntries, candidates);
179
225
 
180
- // RSC won negotiation
181
226
  if (variant.responseType === RSC_RESPONSE_TYPE) {
182
227
  return null;
183
228
  }
184
229
 
185
- // Primary response-type won — use existing manifest entry and middleware
186
230
  if (responseType && variant.routeKey === matched.routeKey) {
187
231
  return {
188
232
  responseType,
@@ -192,8 +236,12 @@ export async function negotiateRoute(
192
236
  negotiated: true,
193
237
  };
194
238
  }
195
-
196
- // Different variant won load its manifest entry
239
+ // The trie extracted params under the PRIMARY leaf's pa, but the winning
240
+ // variant's handler is keyed by the variant's own param names. Re-key in place
241
+ // so plan.route.params (and the variant middleware collected just below) see
242
+ // the variant's names. No-op when the variant has no pa or shares the primary's
243
+ // names (the common case).
244
+ rekeyParamsForVariant(matched.params, variant.pa);
197
245
  const negotiateEntry = await loadManifest(
198
246
  matched.entry,
199
247
  variant.routeKey,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Router Error Handling Utilities
3
3
  *
4
- * Error boundary and not-found boundary handling for RSC Router.
4
+ * Error boundary and not-found boundary handling for Rango.
5
5
  * Also includes the shared invokeOnError utility for error callback invocation.
6
6
  */
7
7
 
@@ -117,16 +117,10 @@ export function findNearestErrorBoundary(
117
117
  let current: EntryData | null = entry;
118
118
 
119
119
  while (current) {
120
- // Check if this entry has error boundaries defined
121
120
  if (current.errorBoundary && current.errorBoundary.length > 0) {
122
- // Return the last error boundary (most recently defined takes precedence)
123
121
  return current.errorBoundary[current.errorBoundary.length - 1];
124
122
  }
125
123
 
126
- // Check orphan layouts for error boundaries
127
- // Orphan layouts are siblings that render alongside the main route chain
128
- // They can define error boundaries that catch errors from routes in the same route group
129
- // Check from first to last (first sibling takes precedence as the "outer" wrapper)
130
124
  if (current.layout && current.layout.length > 0) {
131
125
  for (const orphan of current.layout) {
132
126
  if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
@@ -153,11 +147,21 @@ export function findNearestNotFoundBoundary(
153
147
  let current: EntryData | null = entry;
154
148
 
155
149
  while (current) {
156
- // Check if this entry has notFound boundaries defined
157
150
  if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
158
- // Return the last notFound boundary (most recently defined takes precedence)
159
151
  return current.notFoundBoundary[current.notFoundBoundary.length - 1];
160
152
  }
153
+
154
+ // Check orphan layouts mirroring findNearestErrorBoundary: notFoundBoundary
155
+ // attaches identically (onto parent.notFoundBoundary), and an orphan layout
156
+ // (parent=null) is reachable only via this scan. First sibling is "outer".
157
+ if (current.layout && current.layout.length > 0) {
158
+ for (const orphan of current.layout) {
159
+ if (orphan.notFoundBoundary && orphan.notFoundBoundary.length > 0) {
160
+ return orphan.notFoundBoundary[orphan.notFoundBoundary.length - 1];
161
+ }
162
+ }
163
+ }
164
+
161
165
  current = current.parent;
162
166
  }
163
167
 
@@ -165,6 +169,37 @@ export function findNearestNotFoundBoundary(
165
169
  return defaultNotFoundBoundary || null;
166
170
  }
167
171
 
172
+ /**
173
+ * Normalize an error's cause into a Flight-serializable shape.
174
+ * ErrorInfo.cause crosses the RSC serialization boundary (via
175
+ * LoaderDataResult.error + error-segment fallback props); a non-serializable
176
+ * cause (function, class instance, circular object) would make Flight
177
+ * serialization throw and mask the original loader error.
178
+ */
179
+ function normalizeCause(cause: unknown): unknown {
180
+ if (cause == null) return undefined;
181
+ const t = typeof cause;
182
+ if (t === "string" || t === "number" || t === "boolean") return cause;
183
+ // The whole body is guarded: even `instanceof`/clone can run user code (a
184
+ // Proxy trap, a throwing getter), and this helper must never throw —
185
+ // throwing here would mask the original loader error it exists to protect.
186
+ try {
187
+ if (cause instanceof Error) {
188
+ return { name: cause.name, message: cause.message, stack: cause.stack };
189
+ }
190
+ // Prefer preserving a serializable object/array intact (Flight uses
191
+ // structured-clone-like semantics); fall back to a string when the value
192
+ // is circular, host-bound, or otherwise non-serializable.
193
+ return structuredClone(cause);
194
+ } catch {
195
+ try {
196
+ return String(cause);
197
+ } catch {
198
+ return "[unstringifiable cause]";
199
+ }
200
+ }
201
+ }
202
+
168
203
  /**
169
204
  * Create ErrorInfo from an error object
170
205
  * Sanitizes error details in production
@@ -182,7 +217,7 @@ export function createErrorInfo(
182
217
  name: error.name,
183
218
  code: (error as any).code,
184
219
  stack: isDev ? error.stack : undefined,
185
- cause: isDev ? error.cause : undefined,
220
+ cause: isDev ? normalizeCause(error.cause) : undefined,
186
221
  segmentId,
187
222
  segmentType,
188
223
  };
@@ -207,22 +242,17 @@ export function createErrorSegment(
207
242
  entry: EntryData,
208
243
  params: Record<string, string>,
209
244
  ): ResolvedSegment {
210
- // Determine the component to render
211
245
  let component: ReactNode;
212
246
 
213
247
  if (typeof fallback === "function") {
214
- // ErrorBoundaryHandler - call with error info
215
248
  const props: ErrorBoundaryFallbackProps = {
216
249
  error: errorInfo,
217
250
  };
218
251
  component = fallback(props);
219
252
  } else {
220
- // Static ReactNode fallback
221
253
  component = fallback;
222
254
  }
223
255
 
224
- // Error segment uses the same ID as the layout that has the error boundary
225
- // The error boundary content replaces the layout's outlet content
226
256
  return {
227
257
  id: entry.shortCode,
228
258
  namespace: entry.id,
@@ -261,17 +291,14 @@ export function createNotFoundSegment(
261
291
  entry: EntryData,
262
292
  params: Record<string, string>,
263
293
  ): ResolvedSegment {
264
- // Determine the component to render
265
294
  let component: ReactNode;
266
295
 
267
296
  if (typeof fallback === "function") {
268
- // NotFoundBoundaryHandler - call with props
269
297
  const props: NotFoundBoundaryFallbackProps = {
270
298
  notFound: notFoundInfo,
271
299
  };
272
300
  component = fallback(props);
273
301
  } else {
274
- // Static ReactNode fallback
275
302
  component = fallback;
276
303
  }
277
304
 
@@ -1,5 +1,5 @@
1
1
  import { tryTrieMatch } from "./trie-matching.js";
2
- import { getRouteTrie, getRouterTrie } from "../route-map-builder.js";
2
+ import { getRouterTrie } from "../route-map-builder.js";
3
3
  import {
4
4
  findMatch as findRouteMatch,
5
5
  isLazyEvaluationNeeded,
@@ -8,6 +8,16 @@ import {
8
8
  import type { MetricsStore } from "../server/context";
9
9
  import type { RouteEntry } from "../types";
10
10
 
11
+ // The single-entry cache is module-lifetime, keyed only on pathname, so the same
12
+ // result object is handed to every same-pathname request. ctx.params aliases
13
+ // this object, so handlers mutating it would corrupt the cache for later requests.
14
+ // Clone params; entry/flags are read-only and shared safely.
15
+ function cloneMatchResult<TEnv>(
16
+ r: RouteMatchResult<TEnv> | null,
17
+ ): RouteMatchResult<TEnv> | null {
18
+ return r ? { ...r, params: { ...r.params } } : null;
19
+ }
20
+
11
21
  export interface FindMatchDeps<TEnv = any> {
12
22
  routesEntries: RouteEntry<TEnv>[];
13
23
  evaluateLazyEntry: (entry: RouteEntry<TEnv>) => void;
@@ -27,20 +37,14 @@ export function createFindMatch<TEnv = any>(
27
37
  let lastFindMatchPathname: string | null = null;
28
38
  let lastFindMatchResult: RouteMatchResult<TEnv> | null = null;
29
39
 
30
- // Wrapper for findMatch that uses routesEntries
31
- // Handles lazy evaluation by evaluating lazy entries on first match.
32
- // Phase 1: try O(path_length) trie match.
33
- // Phase 2: fall back to regex iteration.
34
40
  return function findMatch(
35
41
  pathname: string,
36
42
  ms?: MetricsStore,
37
43
  ): RouteMatchResult<TEnv> | null {
38
- // Return cached result if same pathname (avoids double-match per request)
39
44
  if (lastFindMatchPathname === pathname) {
40
- return lastFindMatchResult;
45
+ return cloneMatchResult(lastFindMatchResult);
41
46
  }
42
47
 
43
- // Helper to push sub-metrics
44
48
  const pushMetric = ms
45
49
  ? (label: string, start: number) => {
46
50
  ms.metrics.push({
@@ -51,17 +55,15 @@ export function createFindMatch<TEnv = any>(
51
55
  }
52
56
  : undefined;
53
57
 
54
- // Phase 1: Try trie match (O(path_length))
55
- // Only use the per-router trie. The global trie merges routes from ALL
56
- // routers and must not be used — in multi-router setups (host routing)
57
- // overlapping paths like "/" would match the wrong app's route.
58
58
  const routeTrie = getRouterTrie(deps.routerId);
59
+ let trieMatched = false;
59
60
  if (routeTrie) {
60
61
  const trieStart = performance.now();
61
62
  const trieResult = tryTrieMatch(routeTrie, pathname);
62
63
  pushMetric?.("match:trie", trieStart);
63
64
 
64
65
  if (trieResult) {
66
+ trieMatched = true;
65
67
  // Find the RouteEntry that contains this route.
66
68
  // Multiple entries can share the same staticPrefix (e.g., several
67
69
  // include("/", patterns) calls all produce staticPrefix=""). Evaluate
@@ -83,12 +85,8 @@ export function createFindMatch<TEnv = any>(
83
85
  }
84
86
  }
85
87
 
86
- // If no entry had the route in its routes map, use the first matching
87
- // entry as fallback (handles main entry with inline routes not yet
88
- // reflected in its routes object).
89
88
  if (!entry) entry = fallbackEntry;
90
89
 
91
- // If entry not found (nested include not yet discovered), evaluate parent
92
90
  if (!entry) {
93
91
  const parent = deps.routesEntries.find(
94
92
  (e) =>
@@ -112,9 +110,7 @@ export function createFindMatch<TEnv = any>(
112
110
  entry,
113
111
  routeKey: trieResult.routeKey,
114
112
  params: trieResult.params,
115
- optionalParams: new Set(trieResult.optionalParams || []),
116
113
  redirectTo: trieResult.redirectTo,
117
- ancestry: trieResult.ancestry,
118
114
  ...(trieResult.pr ? { pr: true } : {}),
119
115
  ...(trieResult.pt ? { pt: true } : {}),
120
116
  ...(trieResult.responseType
@@ -125,17 +121,14 @@ export function createFindMatch<TEnv = any>(
125
121
  : {}),
126
122
  ...(trieResult.rscFirst ? { rscFirst: true } : {}),
127
123
  };
128
- return lastFindMatchResult;
124
+ return cloneMatchResult(lastFindMatchResult);
129
125
  }
130
126
  }
131
127
  }
132
128
 
133
- // Phase 2: Fall back to existing matching (regex iteration)
134
129
  const regexStart = performance.now();
135
130
  let result = findRouteMatch(pathname, deps.routesEntries);
136
131
 
137
- // If we hit a lazy entry that needs evaluation, evaluate and retry.
138
- // Cap iterations to prevent infinite loops from pathological nesting.
139
132
  const MAX_LAZY_ITERATIONS = 100;
140
133
  let iterations = 0;
141
134
  while (isLazyEvaluationNeeded(result)) {
@@ -153,8 +146,36 @@ export function createFindMatch<TEnv = any>(
153
146
  }
154
147
  pushMetric?.("match:regex-fallback", regexStart);
155
148
 
149
+ // The trie is the single source of truth and is built before findMatch in
150
+ // both dev (handler rebuild) and production (ensureRouterManifest). If the
151
+ // trie was present yet the regex fallback resolved a real match, the trie
152
+ // has a gap (e.g. a route shape it cannot represent) and dev/prod could
153
+ // diverge if the trie were ever absent. Surface it in dev; folded out in
154
+ // production builds.
155
+ //
156
+ // Suppress when the trie DID match (`trieMatched`): that path falls through
157
+ // to the regex fallback only on the first request to a not-yet-spliced lazy
158
+ // entry (e.g. a 2+-level nested include whose deeper parent has not been
159
+ // evaluated). The trie knew the route; runtime lazy discovery simply lagged.
160
+ // That is the supported lazy-include flow, not a trie gap, so warning on it
161
+ // is a false positive (it manufactures bug reports and erodes the signal).
162
+ if (
163
+ process.env.NODE_ENV !== "production" &&
164
+ routeTrie &&
165
+ !trieMatched &&
166
+ result &&
167
+ !isLazyEvaluationNeeded(result)
168
+ ) {
169
+ console.warn(
170
+ `[@rangojs/router] Route "${pathname}" resolved via the regex fallback ` +
171
+ `even though the route trie was present. The trie should be the single ` +
172
+ `matching source of truth; this indicates a trie gap. Please report this ` +
173
+ `with your route configuration.`,
174
+ );
175
+ }
176
+
156
177
  lastFindMatchPathname = pathname;
157
178
  lastFindMatchResult = result;
158
- return result;
179
+ return cloneMatchResult(result);
159
180
  };
160
181
  }