@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1c0bdfad

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 (406) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +291 -61
  3. package/dist/bin/rango.js +544 -143
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3744 -1329
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +67 -13
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +312 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +322 -19
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +282 -60
  19. package/skills/host-router/SKILL.md +278 -0
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +297 -31
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +155 -111
  32. package/skills/rango/SKILL.md +251 -23
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +104 -9
  36. package/skills/router-setup/SKILL.md +124 -11
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +125 -222
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +357 -52
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +67 -40
  62. package/src/bin/rango.ts +18 -0
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +197 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/link-interceptor.ts +4 -0
  74. package/src/browser/navigation-bridge.ts +200 -30
  75. package/src/browser/navigation-client.ts +217 -58
  76. package/src/browser/navigation-store-handle.ts +38 -0
  77. package/src/browser/navigation-store.ts +76 -67
  78. package/src/browser/navigation-transaction.ts +18 -66
  79. package/src/browser/network-error-handler.ts +34 -7
  80. package/src/browser/partial-update.ts +187 -112
  81. package/src/browser/prefetch/cache.ts +312 -30
  82. package/src/browser/prefetch/fetch.ts +344 -47
  83. package/src/browser/prefetch/policy.ts +6 -0
  84. package/src/browser/prefetch/queue.ts +126 -20
  85. package/src/browser/prefetch/resource-ready.ts +77 -0
  86. package/src/browser/rango-state.ts +158 -76
  87. package/src/browser/react/Link.tsx +125 -18
  88. package/src/browser/react/NavigationProvider.tsx +135 -120
  89. package/src/browser/react/ScrollRestoration.tsx +10 -6
  90. package/src/browser/react/context.ts +7 -2
  91. package/src/browser/react/filter-segment-order.ts +66 -7
  92. package/src/browser/react/index.ts +0 -48
  93. package/src/browser/react/location-state-shared.ts +178 -8
  94. package/src/browser/react/location-state.ts +39 -14
  95. package/src/browser/react/use-action.ts +6 -15
  96. package/src/browser/react/use-handle.ts +23 -69
  97. package/src/browser/react/use-href.tsx +8 -1
  98. package/src/browser/react/use-link-status.ts +33 -8
  99. package/src/browser/react/use-navigation.ts +32 -7
  100. package/src/browser/react/use-params.ts +20 -10
  101. package/src/browser/react/use-reverse.ts +106 -0
  102. package/src/browser/react/use-router.ts +46 -11
  103. package/src/browser/react/use-search-params.ts +0 -5
  104. package/src/browser/react/use-segments.ts +11 -21
  105. package/src/browser/response-adapter.ts +80 -5
  106. package/src/browser/rsc-router.tsx +226 -75
  107. package/src/browser/scroll-restoration.ts +54 -42
  108. package/src/browser/segment-reconciler.ts +36 -9
  109. package/src/browser/segment-structure-assert.ts +2 -2
  110. package/src/browser/server-action-bridge.ts +619 -442
  111. package/src/browser/types.ts +115 -11
  112. package/src/browser/validate-redirect-origin.ts +43 -16
  113. package/src/build/collect-fallback-refs.ts +107 -0
  114. package/src/build/generate-manifest.ts +65 -40
  115. package/src/build/generate-route-types.ts +7 -1
  116. package/src/build/index.ts +8 -2
  117. package/src/build/prefix-tree-utils.ts +123 -0
  118. package/src/build/route-trie.ts +182 -37
  119. package/src/build/route-types/ast-route-extraction.ts +15 -8
  120. package/src/build/route-types/codegen.ts +16 -5
  121. package/src/build/route-types/include-resolution.ts +125 -24
  122. package/src/build/route-types/param-extraction.ts +6 -3
  123. package/src/build/route-types/per-module-writer.ts +22 -6
  124. package/src/build/route-types/router-processing.ts +392 -106
  125. package/src/build/route-types/scan-filter.ts +9 -2
  126. package/src/build/route-types/source-scan.ts +216 -0
  127. package/src/build/runtime-discovery.ts +9 -20
  128. package/src/cache/cache-error.ts +104 -0
  129. package/src/cache/cache-key-utils.ts +29 -13
  130. package/src/cache/cache-policy.ts +108 -34
  131. package/src/cache/cache-runtime.ts +214 -48
  132. package/src/cache/cache-scope.ts +236 -89
  133. package/src/cache/cache-tag.ts +103 -0
  134. package/src/cache/cf/cf-base64.ts +33 -0
  135. package/src/cache/cf/cf-cache-constants.ts +127 -0
  136. package/src/cache/cf/cf-cache-store.ts +2224 -171
  137. package/src/cache/cf/cf-cache-types.ts +349 -0
  138. package/src/cache/cf/cf-kv-utils.ts +46 -0
  139. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  140. package/src/cache/cf/index.ts +11 -17
  141. package/src/cache/document-cache.ts +89 -27
  142. package/src/cache/handle-snapshot.ts +70 -0
  143. package/src/cache/index.ts +11 -20
  144. package/src/cache/memory-segment-store.ts +136 -37
  145. package/src/cache/profile-registry.ts +31 -31
  146. package/src/cache/read-through-swr.ts +41 -11
  147. package/src/cache/segment-codec.ts +9 -17
  148. package/src/cache/tag-invalidation.ts +230 -0
  149. package/src/cache/taint.ts +55 -0
  150. package/src/cache/types.ts +37 -100
  151. package/src/client.rsc.tsx +45 -21
  152. package/src/client.tsx +120 -336
  153. package/src/cloudflare/index.ts +11 -0
  154. package/src/cloudflare/tracing.ts +109 -0
  155. package/src/component-utils.ts +19 -0
  156. package/src/components/DefaultDocument.tsx +8 -2
  157. package/src/context-var.ts +84 -2
  158. package/src/debug.ts +2 -2
  159. package/src/decode-loader-results.ts +52 -0
  160. package/src/defer.ts +196 -0
  161. package/src/deps/ssr.ts +0 -1
  162. package/src/encode-kv.ts +49 -0
  163. package/src/errors.ts +30 -4
  164. package/src/escape-script.ts +52 -0
  165. package/src/handle.ts +70 -22
  166. package/src/handles/MetaTags.tsx +56 -19
  167. package/src/handles/Scripts.tsx +183 -0
  168. package/src/handles/breadcrumbs.ts +95 -0
  169. package/src/handles/is-thenable.ts +19 -0
  170. package/src/handles/meta.ts +51 -40
  171. package/src/handles/script.ts +244 -0
  172. package/src/host/cookie-handler.ts +9 -60
  173. package/src/host/errors.ts +0 -24
  174. package/src/host/index.ts +8 -5
  175. package/src/host/pattern-matcher.ts +23 -52
  176. package/src/host/router.ts +107 -99
  177. package/src/host/testing.ts +40 -27
  178. package/src/host/types.ts +37 -4
  179. package/src/host/utils.ts +1 -1
  180. package/src/href-client.ts +137 -22
  181. package/src/index.rsc.ts +79 -29
  182. package/src/index.ts +149 -65
  183. package/src/internal-debug.ts +11 -10
  184. package/src/loader-store.ts +500 -0
  185. package/src/loader.rsc.ts +20 -13
  186. package/src/loader.ts +12 -11
  187. package/src/missing-id-error.ts +68 -0
  188. package/src/outlet-context.ts +1 -1
  189. package/src/outlet-provider.tsx +1 -5
  190. package/src/prerender/param-hash.ts +16 -16
  191. package/src/prerender/store.ts +63 -26
  192. package/src/prerender.ts +198 -82
  193. package/src/redirect-origin.ts +100 -0
  194. package/src/regex-escape.ts +8 -0
  195. package/src/render-error-thrower.tsx +20 -0
  196. package/src/response-utils.ts +62 -0
  197. package/src/reverse.ts +65 -15
  198. package/src/root-error-boundary.tsx +1 -19
  199. package/src/route-content-wrapper.tsx +7 -72
  200. package/src/route-definition/dsl-helpers.ts +469 -276
  201. package/src/route-definition/helper-factories.ts +29 -139
  202. package/src/route-definition/helpers-types.ts +113 -37
  203. package/src/route-definition/index.ts +3 -3
  204. package/src/route-definition/redirect.ts +53 -12
  205. package/src/route-definition/resolve-handler-use.ts +161 -0
  206. package/src/route-definition/use-item-types.ts +32 -0
  207. package/src/route-map-builder.ts +7 -17
  208. package/src/route-types.ts +37 -41
  209. package/src/router/basename.ts +14 -0
  210. package/src/router/content-negotiation.ts +164 -17
  211. package/src/router/error-handling.ts +45 -18
  212. package/src/router/find-match.ts +45 -22
  213. package/src/router/handler-context.ts +110 -39
  214. package/src/router/instrument.ts +350 -0
  215. package/src/router/intercept-resolution.ts +50 -24
  216. package/src/router/lazy-includes.ts +19 -53
  217. package/src/router/loader-resolution.ts +274 -56
  218. package/src/router/logging.ts +5 -8
  219. package/src/router/manifest.ts +49 -45
  220. package/src/router/match-api.ts +121 -205
  221. package/src/router/match-context.ts +0 -22
  222. package/src/router/match-handlers.ts +58 -58
  223. package/src/router/match-middleware/background-revalidation.ts +33 -6
  224. package/src/router/match-middleware/cache-lookup.ts +214 -263
  225. package/src/router/match-middleware/cache-store.ts +73 -33
  226. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  227. package/src/router/match-middleware/segment-resolution.ts +52 -18
  228. package/src/router/match-pipelines.ts +1 -42
  229. package/src/router/match-result.ts +104 -49
  230. package/src/router/metrics.ts +217 -26
  231. package/src/router/middleware-types.ts +24 -110
  232. package/src/router/middleware.ts +384 -197
  233. package/src/router/navigation-snapshot.ts +131 -0
  234. package/src/router/params-util.ts +23 -0
  235. package/src/router/pattern-matching.ts +148 -91
  236. package/src/router/prefetch-cache-ttl.ts +51 -0
  237. package/src/router/prerender-match.ts +199 -56
  238. package/src/router/preview-match.ts +32 -102
  239. package/src/router/request-classification.ts +276 -0
  240. package/src/router/revalidation.ts +144 -74
  241. package/src/router/route-snapshot.ts +244 -0
  242. package/src/router/router-context.ts +8 -28
  243. package/src/router/router-interfaces.ts +129 -36
  244. package/src/router/router-options.ts +185 -23
  245. package/src/router/router-registry.ts +2 -5
  246. package/src/router/segment-resolution/fresh.ts +281 -76
  247. package/src/router/segment-resolution/helpers.ts +116 -31
  248. package/src/router/segment-resolution/loader-cache.ts +63 -37
  249. package/src/router/segment-resolution/revalidation.ts +493 -391
  250. package/src/router/segment-resolution/static-store.ts +19 -5
  251. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  252. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  253. package/src/router/segment-resolution.ts +5 -1
  254. package/src/router/segment-wrappers.ts +8 -5
  255. package/src/router/state-cookie-name.ts +33 -0
  256. package/src/router/substitute-pattern-params.ts +56 -0
  257. package/src/router/telemetry-otel.ts +161 -199
  258. package/src/router/telemetry.ts +96 -19
  259. package/src/router/timeout.ts +0 -20
  260. package/src/router/tracing.ts +206 -0
  261. package/src/router/trie-matching.ts +180 -58
  262. package/src/router/types.ts +10 -63
  263. package/src/router/url-params.ts +44 -0
  264. package/src/router.ts +182 -54
  265. package/src/rsc/handler-context.ts +3 -2
  266. package/src/rsc/handler.ts +702 -460
  267. package/src/rsc/helpers.ts +168 -46
  268. package/src/rsc/index.ts +2 -25
  269. package/src/rsc/json-route-result.ts +38 -0
  270. package/src/rsc/loader-fetch.ts +127 -31
  271. package/src/rsc/manifest-init.ts +33 -42
  272. package/src/rsc/origin-guard.ts +39 -25
  273. package/src/rsc/progressive-enhancement.ts +98 -19
  274. package/src/rsc/redirect-guard.ts +99 -0
  275. package/src/rsc/response-cache-serve.ts +238 -0
  276. package/src/rsc/response-error.ts +79 -12
  277. package/src/rsc/response-route-handler.ts +99 -189
  278. package/src/rsc/rsc-rendering.ts +126 -106
  279. package/src/rsc/runtime-warnings.ts +23 -10
  280. package/src/rsc/server-action.ts +269 -114
  281. package/src/rsc/ssr-setup.ts +144 -0
  282. package/src/rsc/types.ts +34 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +49 -41
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +281 -129
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +317 -63
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +26 -46
  294. package/src/server/request-context.ts +425 -177
  295. package/src/server.ts +6 -0
  296. package/src/ssr/index.tsx +25 -16
  297. package/src/static-handler.ts +27 -18
  298. package/src/testing/cache-status.ts +162 -0
  299. package/src/testing/collect-handle.ts +40 -0
  300. package/src/testing/dispatch.ts +701 -0
  301. package/src/testing/dom.entry.ts +22 -0
  302. package/src/testing/e2e/fixture.ts +188 -0
  303. package/src/testing/e2e/index.ts +128 -0
  304. package/src/testing/e2e/matchers.ts +35 -0
  305. package/src/testing/e2e/page-helpers.ts +272 -0
  306. package/src/testing/e2e/parity.ts +387 -0
  307. package/src/testing/e2e/server.ts +195 -0
  308. package/src/testing/flight-matchers.ts +97 -0
  309. package/src/testing/flight-normalize.ts +11 -0
  310. package/src/testing/flight-runtime.d.ts +57 -0
  311. package/src/testing/flight-tree.ts +682 -0
  312. package/src/testing/flight.entry.ts +52 -0
  313. package/src/testing/flight.ts +257 -0
  314. package/src/testing/generated-routes.ts +183 -0
  315. package/src/testing/index.ts +99 -0
  316. package/src/testing/internal/context.ts +371 -0
  317. package/src/testing/internal/flight-client-globals.ts +30 -0
  318. package/src/testing/internal/seed-vars.ts +54 -0
  319. package/src/testing/render-handler.ts +343 -0
  320. package/src/testing/render-route.tsx +581 -0
  321. package/src/testing/run-loader.ts +385 -0
  322. package/src/testing/run-middleware.ts +205 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +3 -19
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +236 -88
  340. package/src/types/index.ts +1 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +10 -45
  344. package/src/types/route-entry.ts +19 -7
  345. package/src/types/segments.ts +37 -19
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +58 -11
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -18
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +64 -91
  357. package/src/vite/discovery/discover-routers.ts +147 -88
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +247 -145
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +61 -13
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +10 -3
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +155 -65
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +49 -98
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +127 -0
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +21 -25
  385. package/src/vite/plugins/version-plugin.ts +46 -37
  386. package/src/vite/plugins/virtual-entries.ts +13 -18
  387. package/src/vite/rango.ts +241 -287
  388. package/src/vite/router-discovery.ts +956 -149
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +4 -4
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +141 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/CLAUDE.md +0 -5
  400. package/src/browser/action-response-classifier.ts +0 -99
  401. package/src/browser/react/use-client-cache.ts +0 -58
  402. package/src/browser/shallow.ts +0 -40
  403. package/src/handles/index.ts +0 -6
  404. package/src/network-error-thrower.tsx +0 -23
  405. package/src/route-definition/route-function.ts +0 -119
  406. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Pure prefix-tree walks shared by the build/discovery layer and the runtime
3
+ * trie builder. Kept in `build/` (not `vite/utils`) so runtime code
4
+ * (rsc/manifest-init via build/route-trie) can consume them without importing
5
+ * from the vite layer. `vite/utils/manifest-utils` re-exports them so existing
6
+ * vite-side imports stay unchanged.
7
+ */
8
+
9
+ /**
10
+ * Flatten prefix tree leaf nodes into precomputed route entries.
11
+ * Leaf nodes have no children (no nested includes), so their routes can be
12
+ * used directly by evaluateLazyEntry() without running the handler.
13
+ * Non-leaf nodes are skipped because they have nested lazy includes that
14
+ * require the handler to run for discovery.
15
+ *
16
+ * A leaf is also skipped when its staticPrefix collides with an ancestor
17
+ * include node's staticPrefix. That happens when a dynamic param collapses the
18
+ * staticPrefix of nested includes onto the parent's (e.g. `/m/:id/edit` -> sp
19
+ * `/m`): precomputing such a leaf under the collapsed prefix would let the
20
+ * ancestor's lazy entry claim a route it cannot register (the route is behind
21
+ * further nested lazy includes), producing a RouteNotFoundError at request time
22
+ * (issue #506). Those routes are resolved via the handler chain instead.
23
+ */
24
+ export function flattenLeafEntries(
25
+ prefixTree: Record<string, any>,
26
+ routeManifest: Record<string, string>,
27
+ result: Array<{ staticPrefix: string; routes: Record<string, string> }>,
28
+ ): void {
29
+ function visit(node: any, ancestorStaticPrefixes: Set<string>): void {
30
+ const children = node.children || {};
31
+ if (
32
+ Object.keys(children).length === 0 &&
33
+ node.routes &&
34
+ node.routes.length > 0
35
+ ) {
36
+ // Leaf node. Skip if its staticPrefix collides with an ancestor include
37
+ // node's staticPrefix (dynamic-param collapse) — see doc comment above.
38
+ if (ancestorStaticPrefixes.has(node.staticPrefix)) {
39
+ return;
40
+ }
41
+ // Collect its routes from the manifest
42
+ const routes: Record<string, string> = {};
43
+ for (const name of node.routes) {
44
+ if (name in routeManifest) {
45
+ routes[name] = routeManifest[name];
46
+ }
47
+ }
48
+ result.push({ staticPrefix: node.staticPrefix, routes });
49
+ } else {
50
+ // Non-leaf: recurse into children, tracking this node's staticPrefix as
51
+ // an ancestor so a collapsed nested leaf below it is not over-claimed.
52
+ const nextAncestors = new Set(ancestorStaticPrefixes);
53
+ nextAncestors.add(node.staticPrefix);
54
+ for (const child of Object.values(children)) {
55
+ visit(child, nextAncestors);
56
+ }
57
+ }
58
+ }
59
+ for (const node of Object.values(prefixTree)) {
60
+ visit(node, new Set());
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Build the staticPrefix -> routes lookup the runtime shortcut consumes from a
66
+ * flat precomputed-entry array.
67
+ *
68
+ * A staticPrefix owned by MORE THAN ONE leaf include cannot be collapsed to a
69
+ * single routes object: `new Map(entries.map(e => [e.staticPrefix, e.routes]))`
70
+ * is last-wins, so one include's routes are silently dropped and mis-assigned
71
+ * to whichever entry evaluates first. Two distinct includes legitimately share a
72
+ * staticPrefix when a dynamic param collapses their literal prefixes onto the
73
+ * same value (e.g. `include("/shop/:cat", ...)` and a nested
74
+ * `include("/shop/:brand", ...)` both extract "/shop/"). Merging them is also
75
+ * wrong — assigning the merged set to the first matching entry makes findMatch
76
+ * pick the wrong handler for routes belonging to the other include, which then
77
+ * fails its `Store.manifest.has(routeKey)` invariant at render (500 on a valid
78
+ * route, dev/prod identical).
79
+ *
80
+ * So any shared staticPrefix is OMITTED from the shortcut entirely. Those
81
+ * includes fall through to the handler path in evaluateLazyEntry(), which is the
82
+ * ground truth (identical to pre-precomputed behavior). The shortcut is purely an
83
+ * optimization, so dropping a prefix can only cost a handler run, never change a
84
+ * result.
85
+ */
86
+ export function buildPrecomputedByPrefix(
87
+ entries: Array<{ staticPrefix: string; routes: Record<string, string> }>,
88
+ ): Map<string, Record<string, string>> {
89
+ const byPrefix = new Map<string, Record<string, string>>();
90
+ const shared = new Set<string>();
91
+ for (const e of entries) {
92
+ if (byPrefix.has(e.staticPrefix)) {
93
+ shared.add(e.staticPrefix);
94
+ } else {
95
+ byPrefix.set(e.staticPrefix, e.routes);
96
+ }
97
+ }
98
+ for (const sp of shared) {
99
+ byPrefix.delete(sp);
100
+ }
101
+ return byPrefix;
102
+ }
103
+
104
+ /**
105
+ * Walk prefix tree to map each route name to its scope's staticPrefix.
106
+ */
107
+ export function buildRouteToStaticPrefix(
108
+ prefixTree: Record<string, any>,
109
+ result: Record<string, string>,
110
+ ): void {
111
+ function visit(node: any): void {
112
+ const sp = node.staticPrefix || "";
113
+ for (const name of node.routes || []) {
114
+ result[name] = sp;
115
+ }
116
+ for (const child of Object.values(node.children || {})) {
117
+ visit(child);
118
+ }
119
+ }
120
+ for (const node of Object.values(prefixTree)) {
121
+ visit(node);
122
+ }
123
+ }
@@ -10,9 +10,24 @@ import {
10
10
  parsePattern,
11
11
  type ParsedSegment,
12
12
  } from "../router/pattern-matching.js";
13
+ import { buildRouteToStaticPrefix } from "./prefix-tree-utils.js";
14
+ import type { FullManifest } from "./generate-manifest.js";
13
15
 
14
16
  // -- Trie data structures (compact keys for JSON serialization) --
15
17
 
18
+ /**
19
+ * A response-type variant folded into a primary leaf's negotiate list. `pa` is
20
+ * the variant's own positional param-name array, carried so the runtime can
21
+ * re-key the matched params under the variant's names when it wins negotiation
22
+ * (the trie match extracts params under the PRIMARY leaf's pa). Omitted when the
23
+ * variant has no params; absent/identical pa means no re-key is needed.
24
+ */
25
+ export interface NegotiateVariant {
26
+ routeKey: string;
27
+ responseType: string;
28
+ pa?: string[];
29
+ }
30
+
16
31
  export interface TrieLeaf {
17
32
  /** Route name (e.g., "site.l1_500") */
18
33
  n: string;
@@ -20,8 +35,6 @@ export interface TrieLeaf {
20
35
  sp: string;
21
36
  /** Ancestry shortCodes from root to route [M0L0, M0L0L0, M0L0L0R499] */
22
37
  a: string[];
23
- /** Optional param names (absent params get empty string value) */
24
- op?: string[];
25
38
  /** Constraint validation: paramName -> allowed values */
26
39
  cv?: Record<string, string[]>;
27
40
  /** Ordered param names for this route (positional) */
@@ -35,7 +48,7 @@ export interface TrieLeaf {
35
48
  /** Response type for non-RSC routes (json, text, image, any) */
36
49
  rt?: string;
37
50
  /** Negotiate variants: response-type routes sharing this path */
38
- nv?: Array<{ routeKey: string; responseType: string }>;
51
+ nv?: NegotiateVariant[];
39
52
  /** RSC-first: RSC route was defined before response-type variants */
40
53
  rf?: true;
41
54
  }
@@ -47,6 +60,8 @@ export interface TrieNode {
47
60
  s?: Record<string, TrieNode>;
48
61
  /** Param child: { n: paramName, c: child node } */
49
62
  p?: { n: string; c: TrieNode };
63
+ /** Suffix-param children keyed by suffix (e.g., ".html" → { n: "productId", c: ... }) */
64
+ xp?: Record<string, { n: string; c: TrieNode }>;
50
65
  /** Wildcard terminal: leaf + paramName */
51
66
  w?: TrieLeaf & { pn: string };
52
67
  }
@@ -58,6 +73,9 @@ export interface TrieNode {
58
73
  * @param routeAncestry - Map of route name to ancestry shortCodes
59
74
  * @param routeToStaticPrefix - Map of route name to its entry's staticPrefix
60
75
  * @param routeTrailingSlash - Optional map of route name to trailing slash mode
76
+ * @param prerenderRouteNames - Optional set of prerendered route names (sets leaf.pr)
77
+ * @param passthroughRouteNames - Optional set of passthrough route names (sets leaf.pt)
78
+ * @param responseTypeRoutes - Optional map of route name to response type (sets leaf.rt)
61
79
  */
62
80
  export function buildRouteTrie(
63
81
  routeManifest: Record<string, string>,
@@ -92,51 +110,124 @@ export function buildRouteTrie(
92
110
  });
93
111
  }
94
112
 
113
+ sortSuffixParams(root);
95
114
  return root;
96
115
  }
97
116
 
98
117
  /**
99
- * Insert a route into the trie, handling optional params by forking
100
- * the insertion path (one terminal without the param, one with).
118
+ * Sort every node's suffix-param map (`node.xp`) by descending suffix length so
119
+ * the matcher tries the most specific suffix first. Overlapping suffixes like
120
+ * `.min.js` and `.js` must resolve by specificity, not route declaration order:
121
+ * a request for `/app.min.js` should match `:file.min.js`, not `:file.js`.
122
+ *
123
+ * This started as a bug — `walkTrie` iterates `node.xp` in object order and
124
+ * returns the first suffix the segment ends with, so the winner depended on
125
+ * which route was declared first. Sorting at build time fixes it allocation-free
126
+ * on the match hot path: the serialized production trie preserves this key order
127
+ * through JSON.parse, so dev (per-request rebuild) and production match
128
+ * identically. Array.prototype.sort is stable (ES2019+), so equal-length
129
+ * suffixes keep their declaration order — the router's existing tiebreak.
130
+ */
131
+ function sortSuffixParams(node: TrieNode): void {
132
+ if (node.xp) {
133
+ const sorted: Record<string, { n: string; c: TrieNode }> = {};
134
+ for (const suffix of Object.keys(node.xp).sort(
135
+ (a, b) => b.length - a.length,
136
+ )) {
137
+ sorted[suffix] = node.xp[suffix];
138
+ }
139
+ node.xp = sorted;
140
+ for (const child of Object.values(node.xp)) {
141
+ sortSuffixParams(child.c);
142
+ }
143
+ }
144
+ if (node.s) {
145
+ for (const child of Object.values(node.s)) {
146
+ sortSuffixParams(child);
147
+ }
148
+ }
149
+ if (node.p) {
150
+ sortSuffixParams(node.p.c);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Build a per-router trie from a generated manifest. This is the single
156
+ * construction path shared by build/discovery (discover-routers.ts, serialized
157
+ * into the production chunk) and the dev/HMR runtime rebuild
158
+ * (rsc/manifest-init.ts). Keeping one code path is what guarantees the dev
159
+ * runtime trie and the production serialized trie are byte-for-byte identical
160
+ * (modulo `leaf.a` ancestry, which embeds the mount index and is debug-only).
161
+ *
162
+ * Returns null when the manifest has no route ancestry (no routes), matching
163
+ * the prior guard at both call sites.
164
+ */
165
+ export function buildPerRouterTrie(manifest: FullManifest): TrieNode | null {
166
+ const ancestry = manifest._routeAncestry;
167
+ if (!ancestry || Object.keys(ancestry).length === 0) {
168
+ return null;
169
+ }
170
+
171
+ // Seed every route to the root static prefix (""), then override with each
172
+ // route's include() scope prefix from the prefix tree so the trie returns the
173
+ // correct `sp` for lazy-entry lookup in find-match.
174
+ const routeToStaticPrefix: Record<string, string> = {};
175
+ for (const name of Object.keys(manifest.routeManifest)) {
176
+ routeToStaticPrefix[name] = "";
177
+ }
178
+ if (manifest.prefixTree) {
179
+ buildRouteToStaticPrefix(manifest.prefixTree, routeToStaticPrefix);
180
+ }
181
+
182
+ return buildRouteTrie(
183
+ manifest.routeManifest,
184
+ ancestry,
185
+ routeToStaticPrefix,
186
+ manifest.routeTrailingSlash,
187
+ manifest.prerenderRoutes ? new Set(manifest.prerenderRoutes) : undefined,
188
+ manifest.passthroughRoutes
189
+ ? new Set(manifest.passthroughRoutes)
190
+ : undefined,
191
+ manifest.responseTypeRoutes,
192
+ );
193
+ }
194
+
195
+ /**
196
+ * Insert a route into the trie. Optional params expand into two branches at
197
+ * registration time (skip-first, then present), so each terminal lives at the
198
+ * correct depth for its number of bound params and carries a branch-local
199
+ * `pa` listing only those names. The trie's single-slot `node.p` is reused
200
+ * across branches because matching ignores `node.p.n` — the leaf's `pa` is
201
+ * the source of truth for naming. Skip-first ordering lets `mergeLeaf`'s
202
+ * last-wins rule produce greedy-leftmost semantics for free at any shared
203
+ * terminal depth.
101
204
  */
102
205
  function insertRoute(
103
206
  node: TrieNode,
104
207
  segments: ParsedSegment[],
105
208
  index: number,
106
- leaf: Omit<TrieLeaf, "op" | "cv" | "pa">,
209
+ leaf: Omit<TrieLeaf, "cv" | "pa">,
107
210
  ): void {
108
- // Collect param names, optional param names, and constraints across all segments
109
- const paramNames: string[] = [];
110
- const optionalParams: string[] = [];
211
+ // cv (full constraint map) is route-level and identical on every terminal,
212
+ // so compute it once on the shared base.
111
213
  const constraints: Record<string, string[]> = {};
112
214
 
113
215
  for (const seg of segments) {
114
216
  if (seg.type === "param") {
115
- paramNames.push(seg.value);
116
- if (seg.optional) {
117
- optionalParams.push(seg.value);
118
- }
119
217
  if (seg.constraint) {
120
218
  constraints[seg.value] = seg.constraint;
121
219
  }
122
220
  }
123
221
  }
124
222
 
125
- const fullLeaf: TrieLeaf = {
223
+ const leafBase: Omit<TrieLeaf, "pa"> = {
126
224
  ...leaf,
127
- ...(paramNames.length > 0 ? { pa: paramNames } : {}),
128
- ...(optionalParams.length > 0 ? { op: optionalParams } : {}),
129
225
  ...(Object.keys(constraints).length > 0 ? { cv: constraints } : {}),
130
226
  };
131
227
 
132
- insertSegments(node, segments, index, fullLeaf);
228
+ insertSegments(node, segments, index, leafBase, []);
133
229
  }
134
230
 
135
- /**
136
- * Recursively insert segments into the trie.
137
- * For optional params, we add a terminal at the current node (param absent)
138
- * AND continue inserting into the param child (param present).
139
- */
140
231
  /**
141
232
  * Extract ancestry map from a built trie by visiting all leaf nodes.
142
233
  * Returns { routeName: ancestryShortCodes[] } for every route in the trie.
@@ -158,6 +249,11 @@ export function extractAncestryFromTrie(
158
249
  visit(child);
159
250
  }
160
251
  }
252
+ if (node.xp) {
253
+ for (const child of Object.values(node.xp)) {
254
+ visit(child.c);
255
+ }
256
+ }
161
257
  if (node.p) {
162
258
  visit(node.p.c);
163
259
  }
@@ -174,6 +270,19 @@ export function extractAncestryFromTrie(
174
270
  * appended to the nv (negotiate variants) array.
175
271
  * Multiple response types on the same path are supported (json + text + xml).
176
272
  */
273
+ /**
274
+ * Build a negotiate-variant entry from a leaf being folded into another leaf's
275
+ * nv list. Carries the variant's positional param names (`pa`) so the runtime
276
+ * can re-key matched params under the variant's names; omitted when the variant
277
+ * has none (the common case where primary and variant share the same names is a
278
+ * no-op re-key regardless).
279
+ */
280
+ function toVariant(leaf: TrieLeaf, responseType: string): NegotiateVariant {
281
+ return leaf.pa
282
+ ? { routeKey: leaf.n, responseType, pa: leaf.pa }
283
+ : { routeKey: leaf.n, responseType };
284
+ }
285
+
177
286
  function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
178
287
  if (!existing) return leaf;
179
288
 
@@ -181,7 +290,7 @@ function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
181
290
  // Both are response-type: preserve old as variant
182
291
  const merged = leaf;
183
292
  merged.nv = existing.nv || [];
184
- merged.nv.push({ routeKey: existing.n, responseType: existing.rt });
293
+ merged.nv.push(toVariant(existing, existing.rt));
185
294
  return merged;
186
295
  }
187
296
  if (leaf.rt && !existing.rt) {
@@ -191,7 +300,7 @@ function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
191
300
  existing.nv = [];
192
301
  existing.rf = true;
193
302
  }
194
- existing.nv.push({ routeKey: leaf.n, responseType: leaf.rt });
303
+ existing.nv.push(toVariant(leaf, leaf.rt));
195
304
  return existing;
196
305
  }
197
306
  if (!leaf.rt && existing.rt) {
@@ -199,7 +308,7 @@ function mergeLeaves(existing: TrieLeaf | undefined, leaf: TrieLeaf): TrieLeaf {
199
308
  // RSC was defined second (response-type was already the existing leaf)
200
309
  if (!leaf.nv) leaf.nv = [];
201
310
  if (existing.nv) leaf.nv.push(...existing.nv);
202
- leaf.nv.push({ routeKey: existing.n, responseType: existing.rt });
311
+ leaf.nv.push(toVariant(existing, existing.rt));
203
312
  // rf intentionally not set — RSC came after response-type variants
204
313
  return leaf;
205
314
  }
@@ -211,15 +320,25 @@ function mergeLeaf(node: TrieNode, leaf: TrieLeaf): void {
211
320
  node.r = mergeLeaves(node.r, leaf);
212
321
  }
213
322
 
323
+ function buildLeaf(
324
+ leafBase: Omit<TrieLeaf, "pa">,
325
+ paramNames: string[],
326
+ ): TrieLeaf {
327
+ return paramNames.length > 0
328
+ ? { ...leafBase, pa: [...paramNames] }
329
+ : { ...leafBase };
330
+ }
331
+
214
332
  function insertSegments(
215
333
  node: TrieNode,
216
334
  segments: ParsedSegment[],
217
335
  index: number,
218
- leaf: TrieLeaf,
336
+ leafBase: Omit<TrieLeaf, "pa">,
337
+ paramNames: string[],
219
338
  ): void {
220
- // Base case: all segments consumed, add terminal
339
+ // Base case: all segments consumed, add terminal with branch-local pa
221
340
  if (index >= segments.length) {
222
- mergeLeaf(node, leaf);
341
+ mergeLeaf(node, buildLeaf(leafBase, paramNames));
223
342
  return;
224
343
  }
225
344
 
@@ -228,20 +347,46 @@ function insertSegments(
228
347
  if (segment.type === "static") {
229
348
  if (!node.s) node.s = {};
230
349
  if (!node.s[segment.value]) node.s[segment.value] = {};
231
- insertSegments(node.s[segment.value], segments, index + 1, leaf);
350
+ insertSegments(
351
+ node.s[segment.value],
352
+ segments,
353
+ index + 1,
354
+ leafBase,
355
+ paramNames,
356
+ );
232
357
  } else if (segment.type === "param") {
233
358
  if (segment.optional) {
234
- // Optional param: add terminal at current node (param absent)
235
- mergeLeaf(node, leaf);
236
- // AND continue with param child (param present)
359
+ // SKIP first: continue at the same node without binding this name.
360
+ // Skip-first ordering means the present-branch's TAKE overwrites any
361
+ // shared terminal later, giving greedy-leftmost semantics.
362
+ insertSegments(node, segments, index + 1, leafBase, paramNames);
237
363
  }
238
- if (!node.p) {
239
- node.p = { n: segment.value, c: {} };
364
+ if (segment.suffix) {
365
+ // Suffix param: keyed by suffix string (e.g., ".html")
366
+ if (!node.xp) node.xp = {};
367
+ if (!node.xp[segment.suffix]) {
368
+ node.xp[segment.suffix] = { n: segment.value, c: {} };
369
+ }
370
+ insertSegments(node.xp[segment.suffix].c, segments, index + 1, leafBase, [
371
+ ...paramNames,
372
+ segment.value,
373
+ ]);
374
+ } else {
375
+ if (!node.p) {
376
+ node.p = { n: segment.value, c: {} };
377
+ }
378
+ insertSegments(node.p.c, segments, index + 1, leafBase, [
379
+ ...paramNames,
380
+ segment.value,
381
+ ]);
240
382
  }
241
- insertSegments(node.p.c, segments, index + 1, leaf);
242
383
  } else if (segment.type === "wildcard") {
243
- // Wildcard consumes all remaining segments
244
- const wildLeaf = { ...leaf, pn: "*" };
384
+ // Wildcard consumes all remaining segments. Carry any params bound before
385
+ // the wildcard in pa so they zip correctly against paramValues at match.
386
+ const wildLeaf: TrieLeaf & { pn: string } = {
387
+ ...buildLeaf(leafBase, paramNames),
388
+ pn: "*",
389
+ };
245
390
  const existing = node.w ? ({ ...node.w } as TrieLeaf) : undefined;
246
391
  const merged = mergeLeaves(existing, wildLeaf);
247
392
  node.w = merged as TrieLeaf & { pn: string };
@@ -15,19 +15,26 @@ import { extractParamsFromPattern } from "./param-extraction.js";
15
15
  * the pattern, name, params, and optional search schema from each.
16
16
  * Skips unnamed paths (no { name: "..." }).
17
17
  */
18
- export function extractRoutesFromSource(code: string): Array<{
18
+ export function extractRoutesFromSource(
19
+ code: string,
20
+ sourceFileArg?: ts.SourceFile,
21
+ ): Array<{
19
22
  name: string;
20
23
  pattern: string;
21
24
  params?: Record<string, string>;
22
25
  search?: Record<string, string>;
23
26
  }> {
24
- const sourceFile = ts.createSourceFile(
25
- "input.tsx",
26
- code,
27
- ts.ScriptTarget.Latest,
28
- true,
29
- ts.ScriptKind.TSX,
30
- );
27
+ // Reuse a caller-provided SourceFile (parsed once per scan) when given;
28
+ // otherwise parse the block here. The walk does not mutate the tree.
29
+ const sourceFile =
30
+ sourceFileArg ??
31
+ ts.createSourceFile(
32
+ "input.tsx",
33
+ code,
34
+ ts.ScriptTarget.Latest,
35
+ true,
36
+ ts.ScriptKind.TSX,
37
+ );
31
38
  const routes: Array<{
32
39
  name: string;
33
40
  pattern: string;
@@ -23,7 +23,7 @@ export function generatePerModuleTypesSource(
23
23
  const valid = routes.filter(({ name }) => {
24
24
  if (!name || /["'\\`\n\r]/.test(name)) {
25
25
  console.warn(
26
- `[rsc-router] Skipping route with invalid name: ${JSON.stringify(name)}`,
26
+ `[rango] Skipping route with invalid name: ${JSON.stringify(name)}`,
27
27
  );
28
28
  return false;
29
29
  }
@@ -42,7 +42,7 @@ export function generatePerModuleTypesSource(
42
42
  for (const { name, pattern, params, search } of valid) {
43
43
  if (deduped.has(name)) {
44
44
  console.warn(
45
- `[rsc-router] Duplicate route name "${name}" — keeping first definition`,
45
+ `[rango] Duplicate route name "${name}" — keeping first definition`,
46
46
  );
47
47
  continue;
48
48
  }
@@ -59,7 +59,7 @@ export function generatePerModuleTypesSource(
59
59
  }
60
60
 
61
61
  /**
62
- * Generates a .ts file that augments RSCRouter.GeneratedRouteMap
62
+ * Generates a .ts file that augments Rango.GeneratedRouteMap
63
63
  * with route name -> pattern mappings. This enables Handler<"routeName">
64
64
  * without circular references since the file has no imports from the app.
65
65
  */
@@ -88,14 +88,25 @@ export function generateRouteTypesSource(
88
88
  })
89
89
  .join("\n");
90
90
 
91
+ // The global augmentation extends an alias of `typeof NamedRoutes` rather than
92
+ // `Readonly<typeof NamedRoutes>`. `as const` already makes the members readonly,
93
+ // so the two are behaviour-identical, but `Readonly<>` is a homomorphic mapped
94
+ // type the compiler instantiates once per route at the augmentation site
95
+ // (~4 instantiations/route). An interface `extends` clause cannot name a bare
96
+ // `typeof` query, so the alias is what lets us drop the wrapper. Measured: ~35%
97
+ // fewer type instantiations on a 14k-route app; negligible (but free) on small.
91
98
  return `// Auto-generated by @rangojs/router - do not edit
92
99
  export const NamedRoutes = {
93
100
  ${objectBody}
94
101
  } as const;
95
102
 
103
+ // Aliased so the augmentation below does not pay a homomorphic mapped-type
104
+ // instantiation per route; \`as const\` already makes the members readonly.
105
+ type NamedRoutesShape = typeof NamedRoutes;
106
+
96
107
  declare global {
97
- namespace RSCRouter {
98
- interface GeneratedRouteMap extends Readonly<typeof NamedRoutes> {}
108
+ namespace Rango {
109
+ interface GeneratedRouteMap extends NamedRoutesShape {}
99
110
  }
100
111
  }
101
112
  `;