@rangojs/router 0.0.0-experimental.14 → 0.0.0-experimental.140

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 (460) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +432 -7
  3. package/dist/bin/rango.js +2073 -213
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +7258 -2714
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +140 -67
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +329 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +487 -0
  12. package/skills/caching/SKILL.md +357 -25
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +246 -0
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +16 -10
  19. package/skills/document-cache/SKILL.md +87 -62
  20. package/skills/fonts/SKILL.md +6 -4
  21. package/skills/handler-use/SKILL.md +364 -0
  22. package/skills/hooks/SKILL.md +557 -79
  23. package/skills/host-router/SKILL.md +320 -0
  24. package/skills/i18n/SKILL.md +276 -0
  25. package/skills/intercept/SKILL.md +207 -15
  26. package/skills/layout/SKILL.md +146 -6
  27. package/skills/links/SKILL.md +304 -25
  28. package/skills/loader/SKILL.md +616 -54
  29. package/skills/middleware/SKILL.md +217 -37
  30. package/skills/migrate-nextjs/SKILL.md +611 -0
  31. package/skills/migrate-react-router/SKILL.md +927 -0
  32. package/skills/mime-routes/SKILL.md +42 -11
  33. package/skills/observability/SKILL.md +194 -0
  34. package/skills/parallel/SKILL.md +284 -3
  35. package/skills/ppr/SKILL.md +426 -0
  36. package/skills/prerender/SKILL.md +437 -52
  37. package/skills/rango/SKILL.md +369 -22
  38. package/skills/react-compiler/SKILL.md +168 -0
  39. package/skills/response-routes/SKILL.md +263 -121
  40. package/skills/route/SKILL.md +350 -21
  41. package/skills/router-setup/SKILL.md +246 -33
  42. package/skills/scripts/SKILL.md +179 -0
  43. package/skills/server-actions/SKILL.md +775 -0
  44. package/skills/shell-manifest/SKILL.md +185 -0
  45. package/skills/streams-and-websockets/SKILL.md +283 -0
  46. package/skills/tailwind/SKILL.md +27 -3
  47. package/skills/testing/SKILL.md +126 -222
  48. package/skills/testing/bindings.md +103 -0
  49. package/skills/testing/cache-prerender.md +127 -0
  50. package/skills/testing/client-components.md +124 -0
  51. package/skills/testing/e2e-parity.md +125 -0
  52. package/skills/testing/flight.md +91 -0
  53. package/skills/testing/handles.md +131 -0
  54. package/skills/testing/loader.md +128 -0
  55. package/skills/testing/middleware.md +99 -0
  56. package/skills/testing/render-handler.md +122 -0
  57. package/skills/testing/response-routes.md +95 -0
  58. package/skills/testing/reverse-and-types.md +85 -0
  59. package/skills/testing/server-actions.md +107 -0
  60. package/skills/testing/server-tree.md +128 -0
  61. package/skills/testing/setup.md +123 -0
  62. package/skills/theme/SKILL.md +9 -8
  63. package/skills/typesafety/SKILL.md +532 -103
  64. package/skills/use-cache/SKILL.md +367 -0
  65. package/skills/vercel/SKILL.md +128 -0
  66. package/skills/view-transitions/SKILL.md +337 -0
  67. package/src/__augment-tests__/augment.ts +81 -0
  68. package/src/__augment-tests__/augmented.check.ts +116 -0
  69. package/src/__internal.ts +77 -44
  70. package/src/bin/rango.ts +312 -15
  71. package/src/browser/action-coordinator.ts +114 -0
  72. package/src/browser/action-fence.ts +47 -0
  73. package/src/browser/app-shell.ts +39 -0
  74. package/src/browser/app-version.ts +14 -0
  75. package/src/browser/connection-warmup.ts +134 -0
  76. package/src/browser/cookie-name.ts +140 -0
  77. package/src/browser/event-controller.ts +293 -202
  78. package/src/browser/history-state.ts +101 -0
  79. package/src/browser/index.ts +3 -3
  80. package/src/browser/intercept-utils.ts +52 -0
  81. package/src/browser/invalidate-client-cache.ts +52 -0
  82. package/src/browser/link-interceptor.ts +24 -4
  83. package/src/browser/logging.ts +11 -0
  84. package/src/browser/merge-segment-loaders.ts +20 -12
  85. package/src/browser/navigation-bridge.ts +385 -576
  86. package/src/browser/navigation-client.ts +245 -75
  87. package/src/browser/navigation-store-handle.ts +38 -0
  88. package/src/browser/navigation-store.ts +184 -118
  89. package/src/browser/navigation-transaction.ts +247 -0
  90. package/src/browser/network-error-handler.ts +88 -0
  91. package/src/browser/partial-update.ts +412 -364
  92. package/src/browser/prefetch/cache.ts +359 -0
  93. package/src/browser/prefetch/fetch.ts +452 -0
  94. package/src/browser/prefetch/observer.ts +65 -0
  95. package/src/browser/prefetch/policy.ts +48 -0
  96. package/src/browser/prefetch/queue.ts +209 -0
  97. package/src/browser/prefetch/resource-ready.ts +77 -0
  98. package/src/browser/rango-state.ts +194 -0
  99. package/src/browser/react/Link.tsx +275 -68
  100. package/src/browser/react/NavigationProvider.tsx +265 -109
  101. package/src/browser/react/ScrollRestoration.tsx +10 -6
  102. package/src/browser/react/context.ts +11 -0
  103. package/src/browser/react/filter-segment-order.ts +70 -0
  104. package/src/browser/react/index.ts +0 -48
  105. package/src/browser/react/location-state-shared.ts +272 -60
  106. package/src/browser/react/location-state.ts +90 -20
  107. package/src/browser/react/mount-context.ts +6 -1
  108. package/src/browser/react/nonce-context.ts +23 -0
  109. package/src/browser/react/shallow-equal.ts +27 -0
  110. package/src/browser/react/use-action.ts +35 -66
  111. package/src/browser/react/use-handle.ts +39 -126
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +39 -13
  114. package/src/browser/react/use-navigation.ts +53 -69
  115. package/src/browser/react/use-params.ts +75 -0
  116. package/src/browser/react/use-pathname.ts +47 -0
  117. package/src/browser/react/use-reverse.ts +106 -0
  118. package/src/browser/react/use-router.ts +98 -0
  119. package/src/browser/react/use-search-params.ts +51 -0
  120. package/src/browser/react/use-segments.ts +72 -99
  121. package/src/browser/response-adapter.ts +164 -0
  122. package/src/browser/rsc-router.tsx +300 -72
  123. package/src/browser/scroll-restoration.ts +138 -50
  124. package/src/browser/segment-reconciler.ts +243 -0
  125. package/src/browser/segment-structure-assert.ts +17 -1
  126. package/src/browser/server-action-bridge.ts +668 -613
  127. package/src/browser/types.ts +223 -51
  128. package/src/browser/validate-redirect-origin.ts +56 -0
  129. package/src/build/collect-fallback-refs.ts +107 -0
  130. package/src/build/generate-manifest.ts +252 -161
  131. package/src/build/generate-route-types.ts +41 -1038
  132. package/src/build/index.ts +12 -7
  133. package/src/build/prefix-tree-utils.ts +123 -0
  134. package/src/build/route-trie.ts +225 -42
  135. package/src/build/route-types/ast-helpers.ts +25 -0
  136. package/src/build/route-types/ast-route-extraction.ts +105 -0
  137. package/src/build/route-types/codegen.ts +113 -0
  138. package/src/build/route-types/include-resolution.ts +812 -0
  139. package/src/build/route-types/param-extraction.ts +51 -0
  140. package/src/build/route-types/per-module-writer.ts +144 -0
  141. package/src/build/route-types/router-processing.ts +695 -0
  142. package/src/build/route-types/scan-filter.ts +85 -0
  143. package/src/build/route-types/source-scan.ts +216 -0
  144. package/src/build/runtime-discovery.ts +223 -0
  145. package/src/cache/background-task.ts +34 -0
  146. package/src/cache/cache-error.ts +104 -0
  147. package/src/cache/cache-key-utils.ts +60 -0
  148. package/src/cache/cache-policy.ts +199 -0
  149. package/src/cache/cache-runtime.ts +525 -0
  150. package/src/cache/cache-scope.ts +298 -332
  151. package/src/cache/cache-tag.ts +103 -0
  152. package/src/cache/cf/cf-base64.ts +33 -0
  153. package/src/cache/cf/cf-cache-constants.ts +127 -0
  154. package/src/cache/cf/cf-cache-store.ts +2500 -158
  155. package/src/cache/cf/cf-cache-types.ts +349 -0
  156. package/src/cache/cf/cf-kv-utils.ts +46 -0
  157. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  158. package/src/cache/cf/index.ts +17 -17
  159. package/src/cache/document-cache.ts +199 -92
  160. package/src/cache/handle-capture.ts +81 -0
  161. package/src/cache/handle-snapshot.ts +111 -0
  162. package/src/cache/index.ts +29 -35
  163. package/src/cache/memory-segment-store.ts +363 -30
  164. package/src/cache/profile-registry.ts +88 -0
  165. package/src/cache/read-through-swr.ts +178 -0
  166. package/src/cache/segment-codec.ts +248 -0
  167. package/src/cache/shell-cache.ts +386 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +156 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1102 -0
  173. package/src/client.rsc.tsx +43 -21
  174. package/src/client.tsx +131 -347
  175. package/src/cloudflare/index.ts +11 -0
  176. package/src/cloudflare/tracing.ts +109 -0
  177. package/src/component-utils.ts +23 -4
  178. package/src/components/DefaultDocument.tsx +13 -3
  179. package/src/context-var.ts +168 -0
  180. package/src/debug.ts +19 -9
  181. package/src/decode-loader-results.ts +52 -0
  182. package/src/defer.ts +185 -0
  183. package/src/deps/ssr.ts +0 -1
  184. package/src/encode-kv.ts +49 -0
  185. package/src/errors.ts +106 -10
  186. package/src/escape-script.ts +52 -0
  187. package/src/handle.ts +110 -35
  188. package/src/handles/MetaTags.tsx +83 -59
  189. package/src/handles/Scripts.tsx +183 -0
  190. package/src/handles/breadcrumbs.ts +93 -0
  191. package/src/handles/deferred-resolution.ts +127 -0
  192. package/src/handles/is-thenable.ts +18 -0
  193. package/src/handles/meta.ts +44 -53
  194. package/src/handles/script.ts +244 -0
  195. package/src/host/cookie-handler.ts +20 -65
  196. package/src/host/errors.ts +21 -30
  197. package/src/host/index.ts +13 -9
  198. package/src/host/pattern-matcher.ts +50 -79
  199. package/src/host/router.ts +151 -121
  200. package/src/host/testing.ts +45 -32
  201. package/src/host/types.ts +52 -11
  202. package/src/host/utils.ts +2 -2
  203. package/src/href-client.ts +192 -57
  204. package/src/index.rsc.ts +177 -35
  205. package/src/index.ts +255 -71
  206. package/src/internal-debug.ts +9 -2
  207. package/src/loader-store.ts +500 -0
  208. package/src/loader.rsc.ts +31 -99
  209. package/src/loader.ts +30 -12
  210. package/src/missing-id-error.ts +68 -0
  211. package/src/outlet-context.ts +1 -1
  212. package/src/outlet-provider.tsx +41 -0
  213. package/src/prerender/param-hash.ts +16 -14
  214. package/src/prerender/store.ts +121 -21
  215. package/src/prerender.ts +460 -26
  216. package/src/redirect-origin.ts +100 -0
  217. package/src/regex-escape.ts +8 -0
  218. package/src/render-error-thrower.tsx +20 -0
  219. package/src/response-utils.ts +62 -0
  220. package/src/reverse.ts +198 -128
  221. package/src/root-error-boundary.tsx +42 -48
  222. package/src/route-content-wrapper.tsx +22 -77
  223. package/src/route-definition/dsl-helpers.ts +1116 -0
  224. package/src/route-definition/helper-factories.ts +88 -0
  225. package/src/route-definition/helpers-types.ts +505 -0
  226. package/src/route-definition/index.ts +54 -0
  227. package/src/route-definition/redirect.ts +134 -0
  228. package/src/route-definition/resolve-handler-use.ts +160 -0
  229. package/src/route-definition/use-item-types.ts +29 -0
  230. package/src/route-definition.ts +1 -1481
  231. package/src/route-map-builder.ts +82 -144
  232. package/src/route-name.ts +53 -0
  233. package/src/route-types.ts +71 -45
  234. package/src/router/basename.ts +14 -0
  235. package/src/router/content-negotiation.ts +263 -0
  236. package/src/router/debug-manifest.ts +72 -0
  237. package/src/router/error-handling.ts +54 -27
  238. package/src/router/find-match.ts +245 -0
  239. package/src/router/handler-context.ts +377 -125
  240. package/src/router/instrument.ts +350 -0
  241. package/src/router/intercept-resolution.ts +59 -28
  242. package/src/router/lazy-includes.ts +254 -0
  243. package/src/router/loader-resolution.ts +421 -157
  244. package/src/router/logging.ts +106 -6
  245. package/src/router/manifest.ts +131 -57
  246. package/src/router/match-api.ts +167 -246
  247. package/src/router/match-context.ts +4 -24
  248. package/src/router/match-handlers.ts +440 -0
  249. package/src/router/match-middleware/background-revalidation.ts +117 -93
  250. package/src/router/match-middleware/cache-lookup.ts +297 -150
  251. package/src/router/match-middleware/cache-store.ts +123 -51
  252. package/src/router/match-middleware/intercept-resolution.ts +44 -43
  253. package/src/router/match-middleware/segment-resolution.ts +64 -22
  254. package/src/router/match-pipelines.ts +11 -87
  255. package/src/router/match-result.ts +121 -50
  256. package/src/router/metrics.ts +219 -28
  257. package/src/router/middleware-types.ts +93 -0
  258. package/src/router/middleware.ts +505 -441
  259. package/src/router/navigation-snapshot.ts +133 -0
  260. package/src/router/params-util.ts +23 -0
  261. package/src/router/parse-pattern.ts +115 -0
  262. package/src/router/pattern-matching.ts +311 -142
  263. package/src/router/prefetch-cache-ttl.ts +51 -0
  264. package/src/router/prefetch-limits.ts +37 -0
  265. package/src/router/prerender-match.ts +547 -0
  266. package/src/router/preview-match.ts +102 -0
  267. package/src/router/request-classification.ts +278 -0
  268. package/src/router/revalidation.ts +203 -62
  269. package/src/router/route-snapshot.ts +246 -0
  270. package/src/router/router-context.ts +45 -48
  271. package/src/router/router-interfaces.ts +554 -0
  272. package/src/router/router-options.ts +779 -0
  273. package/src/router/router-registry.ts +21 -0
  274. package/src/router/segment-resolution/fresh.ts +772 -0
  275. package/src/router/segment-resolution/helpers.ts +348 -0
  276. package/src/router/segment-resolution/loader-cache.ts +250 -0
  277. package/src/router/segment-resolution/loader-mask.ts +44 -0
  278. package/src/router/segment-resolution/revalidation.ts +1331 -0
  279. package/src/router/segment-resolution/static-store.ts +81 -0
  280. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  281. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  282. package/src/router/segment-resolution.ts +25 -1354
  283. package/src/router/segment-wrappers.ts +292 -0
  284. package/src/router/state-cookie-name.ts +33 -0
  285. package/src/router/substitute-pattern-params.ts +75 -0
  286. package/src/router/telemetry-otel.ts +261 -0
  287. package/src/router/telemetry.ts +377 -0
  288. package/src/router/timeout.ts +128 -0
  289. package/src/router/tracing.ts +206 -0
  290. package/src/router/trie-matching.ts +240 -61
  291. package/src/router/types.ts +23 -70
  292. package/src/router/url-params.ts +57 -0
  293. package/src/router.ts +781 -2378
  294. package/src/rsc/full-payload.ts +70 -0
  295. package/src/rsc/handler-context.ts +46 -0
  296. package/src/rsc/handler.ts +905 -1142
  297. package/src/rsc/helpers.ts +275 -19
  298. package/src/rsc/index.ts +2 -25
  299. package/src/rsc/json-route-result.ts +38 -0
  300. package/src/rsc/loader-fetch.ts +305 -0
  301. package/src/rsc/manifest-init.ts +77 -0
  302. package/src/rsc/nonce.ts +14 -0
  303. package/src/rsc/origin-guard.ts +155 -0
  304. package/src/rsc/progressive-enhancement.ts +502 -0
  305. package/src/rsc/redirect-guard.ts +99 -0
  306. package/src/rsc/response-cache-serve.ts +238 -0
  307. package/src/rsc/response-error.ts +104 -0
  308. package/src/rsc/response-route-handler.ts +257 -0
  309. package/src/rsc/rsc-rendering.ts +337 -0
  310. package/src/rsc/runtime-warnings.ts +55 -0
  311. package/src/rsc/server-action.ts +522 -0
  312. package/src/rsc/shell-capture.ts +439 -0
  313. package/src/rsc/ssr-setup.ts +144 -0
  314. package/src/rsc/transition-gate.ts +89 -0
  315. package/src/rsc/types.ts +95 -12
  316. package/src/runtime-env.ts +18 -0
  317. package/src/search-params.ts +99 -82
  318. package/src/segment-content-promise.ts +67 -0
  319. package/src/segment-loader-promise.ts +149 -0
  320. package/src/segment-system.tsx +349 -134
  321. package/src/serialize.ts +243 -0
  322. package/src/server/context.ts +452 -85
  323. package/src/server/cookie-parse.ts +32 -0
  324. package/src/server/cookie-store.ts +310 -0
  325. package/src/server/fetchable-loader-store.ts +11 -6
  326. package/src/server/handle-store.ts +123 -42
  327. package/src/server/live.ts +130 -0
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +842 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +412 -136
  332. package/src/ssr/ssr-root.tsx +228 -0
  333. package/src/static-handler.ts +45 -18
  334. package/src/testing/cache-status.ts +162 -0
  335. package/src/testing/collect-handle.ts +46 -0
  336. package/src/testing/dispatch.ts +701 -0
  337. package/src/testing/dom.entry.ts +22 -0
  338. package/src/testing/e2e/fixture.ts +188 -0
  339. package/src/testing/e2e/index.ts +128 -0
  340. package/src/testing/e2e/matchers.ts +35 -0
  341. package/src/testing/e2e/page-helpers.ts +272 -0
  342. package/src/testing/e2e/parity.ts +387 -0
  343. package/src/testing/e2e/server.ts +195 -0
  344. package/src/testing/flight-matchers.ts +97 -0
  345. package/src/testing/flight-normalize.ts +11 -0
  346. package/src/testing/flight-runtime.d.ts +57 -0
  347. package/src/testing/flight-tree.ts +682 -0
  348. package/src/testing/flight.entry.ts +52 -0
  349. package/src/testing/flight.ts +257 -0
  350. package/src/testing/generated-routes.ts +199 -0
  351. package/src/testing/index.ts +105 -0
  352. package/src/testing/internal/context.ts +371 -0
  353. package/src/testing/internal/flight-client-globals.ts +30 -0
  354. package/src/testing/internal/seed-vars.ts +54 -0
  355. package/src/testing/render-handler.ts +357 -0
  356. package/src/testing/render-route.tsx +584 -0
  357. package/src/testing/run-loader.ts +385 -0
  358. package/src/testing/run-middleware.ts +205 -0
  359. package/src/testing/run-transition-when.ts +164 -0
  360. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  361. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  362. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  363. package/src/testing/vitest-stubs/version.ts +5 -0
  364. package/src/testing/vitest.ts +305 -0
  365. package/src/theme/ThemeProvider.tsx +40 -72
  366. package/src/theme/ThemeScript.tsx +12 -14
  367. package/src/theme/constants.ts +57 -15
  368. package/src/theme/index.ts +3 -20
  369. package/src/theme/theme-context.ts +5 -35
  370. package/src/theme/theme-script.ts +43 -39
  371. package/src/theme/use-theme.ts +0 -3
  372. package/src/types/boundaries.ts +123 -0
  373. package/src/types/cache-types.ts +207 -0
  374. package/src/types/error-types.ts +132 -0
  375. package/src/types/global-namespace.ts +113 -0
  376. package/src/types/handler-context.ts +839 -0
  377. package/src/types/index.ts +81 -0
  378. package/src/types/loader-types.ts +212 -0
  379. package/src/types/request-scope.ts +112 -0
  380. package/src/types/route-config.ts +138 -0
  381. package/src/types/route-entry.ts +114 -0
  382. package/src/types/segments.ts +271 -0
  383. package/src/types.ts +1 -1795
  384. package/src/urls/include-helper.ts +162 -0
  385. package/src/urls/include-provider.ts +71 -0
  386. package/src/urls/index.ts +43 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +275 -0
  389. package/src/urls/pattern-types.ts +124 -0
  390. package/src/urls/response-types.ts +109 -0
  391. package/src/urls/type-extraction.ts +316 -0
  392. package/src/urls/urls-function.ts +80 -0
  393. package/src/urls.ts +1 -1341
  394. package/src/use-loader.tsx +406 -141
  395. package/src/vercel/index.ts +11 -0
  396. package/src/vercel/tracing.ts +88 -0
  397. package/src/vite/debug.ts +185 -0
  398. package/src/vite/discovery/bundle-postprocess.ts +182 -0
  399. package/src/vite/discovery/discover-routers.ts +389 -0
  400. package/src/vite/discovery/discovery-errors.ts +255 -0
  401. package/src/vite/discovery/gate-state.ts +171 -0
  402. package/src/vite/discovery/prerender-collection.ts +467 -0
  403. package/src/vite/discovery/route-types-writer.ts +214 -0
  404. package/src/vite/discovery/self-gen-tracking.ts +73 -0
  405. package/src/vite/discovery/state.ts +161 -0
  406. package/src/vite/discovery/virtual-module-codegen.ts +183 -0
  407. package/src/vite/index.ts +23 -2255
  408. package/src/vite/inject-client-debug.ts +36 -0
  409. package/src/vite/plugin-types.ts +303 -0
  410. package/src/vite/plugins/cjs-to-esm.ts +90 -0
  411. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  412. package/src/vite/plugins/client-ref-hashing.ts +118 -0
  413. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  414. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  415. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  416. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +88 -110
  417. package/src/vite/{expose-id-utils.ts → plugins/expose-id-utils.ts} +89 -79
  418. package/src/vite/plugins/expose-ids/export-analysis.ts +363 -0
  419. package/src/vite/plugins/expose-ids/handler-transform.ts +130 -0
  420. package/src/vite/plugins/expose-ids/loader-transform.ts +64 -0
  421. package/src/vite/plugins/expose-ids/router-transform.ts +199 -0
  422. package/src/vite/plugins/expose-ids/types.ts +45 -0
  423. package/src/vite/plugins/expose-internal-ids.ts +805 -0
  424. package/src/vite/plugins/performance-tracks.ts +89 -0
  425. package/src/vite/plugins/refresh-cmd.ts +127 -0
  426. package/src/vite/plugins/use-cache-transform.ts +313 -0
  427. package/src/vite/plugins/vercel-output.ts +384 -0
  428. package/src/vite/plugins/version-injector.ts +94 -0
  429. package/src/vite/plugins/version-plugin.ts +263 -0
  430. package/src/vite/plugins/virtual-entries.ts +234 -0
  431. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  432. package/src/vite/rango.ts +560 -0
  433. package/src/vite/router-discovery.ts +1638 -0
  434. package/src/vite/{ast-handler-extract.ts → utils/ast-handler-extract.ts} +200 -37
  435. package/src/vite/utils/banner.ts +36 -0
  436. package/src/vite/utils/bundle-analysis.ts +132 -0
  437. package/src/vite/utils/client-chunks.ts +184 -0
  438. package/src/vite/utils/directive-prologue.ts +40 -0
  439. package/src/vite/utils/forward-user-plugins.ts +171 -0
  440. package/src/vite/utils/manifest-utils.ts +15 -0
  441. package/src/vite/utils/package-resolution.ts +89 -0
  442. package/src/vite/utils/prerender-utils.ts +249 -0
  443. package/src/vite/utils/shared-utils.ts +269 -0
  444. package/CLAUDE.md +0 -43
  445. package/dist/vite/index.named-routes.gen.ts +0 -103
  446. package/src/browser/lru-cache.ts +0 -69
  447. package/src/browser/react/use-client-cache.ts +0 -56
  448. package/src/browser/request-controller.ts +0 -164
  449. package/src/browser/shallow.ts +0 -35
  450. package/src/cache/memory-store.ts +0 -253
  451. package/src/handles/index.ts +0 -6
  452. package/src/href-context.ts +0 -33
  453. package/src/network-error-thrower.tsx +0 -21
  454. package/src/router.gen.ts +0 -6
  455. package/src/static-handler.gen.ts +0 -5
  456. package/src/urls.gen.ts +0 -8
  457. package/src/vite/expose-internal-ids.ts +0 -1167
  458. package/src/vite/package-resolution.ts +0 -125
  459. package/src/vite/virtual-entries.ts +0 -114
  460. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -1,7 +1,17 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import type { ReactNode } from "react";
3
- import type { PartialCacheOptions, ErrorBoundaryHandler, Handler, LoaderDefinition, MiddlewareFn, NotFoundBoundaryHandler, ShouldRevalidateFn } from "../types";
4
- import { invariant } from "../errors";
3
+ import type {
4
+ PartialCacheOptions,
5
+ ErrorBoundaryHandler,
6
+ Handler,
7
+ LoaderDefinition,
8
+ MiddlewareFn,
9
+ NotFoundBoundaryHandler,
10
+ ShouldRevalidateFn,
11
+ TransitionConfig,
12
+ } from "../types";
13
+ import { invariant, DslContextError } from "../errors";
14
+ import type { DefaultRouteName } from "../types/global-namespace.js";
5
15
 
6
16
  // ============================================================================
7
17
  // Performance Metrics Types
@@ -13,9 +23,10 @@ import { invariant } from "../errors";
13
23
  * @internal This type is an implementation detail and may change without notice.
14
24
  */
15
25
  export interface PerformanceMetric {
16
- label: string; // e.g., "route-matching", "loader:UserLoader"
17
- duration: number; // milliseconds
18
- startTime: number; // relative to request start
26
+ label: string; // e.g., "route-matching", "loader:UserLoader"
27
+ duration: number; // milliseconds
28
+ startTime: number; // relative to request start
29
+ depth?: number; // nesting level for hierarchical display (0 = top-level)
19
30
  }
20
31
 
21
32
  /**
@@ -29,7 +40,7 @@ export interface MetricsStore {
29
40
  metrics: PerformanceMetric[];
30
41
  }
31
42
  // ============================================================================
32
- // RSC Router Context
43
+ // Rango Context
33
44
  // ============================================================================
34
45
 
35
46
  /**
@@ -60,6 +71,10 @@ export type EntryPropCommon = {
60
71
  };
61
72
 
62
73
  /**
74
+ * Attachments resolved by walking the parent chain, not owned by the entry:
75
+ * middleware composes downward; revalidate and the error/notFound boundaries are
76
+ * resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
77
+ *
63
78
  * @internal This type is an implementation detail and may change without notice.
64
79
  */
65
80
  export type EntryPropDatas = {
@@ -69,6 +84,16 @@ export type EntryPropDatas = {
69
84
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
70
85
  };
71
86
 
87
+ /**
88
+ * Render-time presentation fields shared by every entry variant.
89
+ *
90
+ * @internal This type is an implementation detail and may change without notice.
91
+ */
92
+ export type EntryPropRender = {
93
+ loading?: ReactNode | false;
94
+ transition?: TransitionConfig;
95
+ };
96
+
72
97
  /**
73
98
  * Loader entry stored in EntryData
74
99
  * Contains the loader definition and its revalidation rules
@@ -105,12 +130,14 @@ export type InterceptSegmentsState = {
105
130
  * @internal This type is an implementation detail and may change without notice.
106
131
  */
107
132
  export type InterceptSelectorContext<TEnv = any> = {
108
- from: URL; // Source URL (where user is coming from)
109
- to: URL; // Destination URL (where user is navigating to)
110
- params: Record<string, string>; // Matched route params
111
- request: Request; // The HTTP request object
112
- env: TEnv; // Platform bindings (Cloudflare env, etc.)
113
- segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
133
+ from: URL; // Source URL (where user is coming from)
134
+ to: URL; // Destination URL (where user is navigating to)
135
+ params: Record<string, string>; // Matched route params
136
+ request: Request; // The HTTP request object
137
+ env: TEnv; // Platform bindings (Cloudflare env, etc.)
138
+ segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
139
+ fromRouteName?: DefaultRouteName; // Named route being navigated away from (undefined for unnamed routes)
140
+ toRouteName?: DefaultRouteName; // Named route being navigated to (undefined for unnamed routes)
114
141
  };
115
142
 
116
143
  /**
@@ -119,7 +146,22 @@ export type InterceptSelectorContext<TEnv = any> = {
119
146
  *
120
147
  * @internal This type is an implementation detail and may change without notice.
121
148
  */
122
- export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>) => boolean;
149
+ export type InterceptWhenFn<TEnv = any> = (
150
+ ctx: InterceptSelectorContext<TEnv>,
151
+ ) => boolean;
152
+
153
+ /**
154
+ * Config object passed to intercept() (its 4th argument). `when` gates whether
155
+ * the intercept activates on a soft navigation — a single match-time selector or
156
+ * an array of them (ALL must return true; omit to always activate). This is the
157
+ * intercept counterpart to transition({ when }); both express conditional
158
+ * behavior as a config field rather than a separate DSL helper.
159
+ *
160
+ * @internal This type is an implementation detail and may change without notice.
161
+ */
162
+ export interface InterceptConfig<TEnv = any> {
163
+ when?: InterceptWhenFn<TEnv> | InterceptWhenFn<TEnv>[];
164
+ }
123
165
 
124
166
  /**
125
167
  * Intercept entry stored in EntryData
@@ -128,8 +170,8 @@ export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>)
128
170
  * @internal This type is an implementation detail and may change without notice.
129
171
  */
130
172
  export type InterceptEntry = {
131
- slotName: `@${string}`; // e.g., "@modal"
132
- routeName: string; // e.g., "card"
173
+ slotName: `@${string}`; // e.g., "@modal"
174
+ routeName: string; // e.g., "card"
133
175
  handler: ReactNode | Handler<any, any, any>;
134
176
  middleware: MiddlewareFn<any, any>[];
135
177
  revalidate: ShouldRevalidateFn<any, any>[];
@@ -137,14 +179,34 @@ export type InterceptEntry = {
137
179
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
138
180
  loader: LoaderEntry[];
139
181
  loading?: ReactNode | false;
140
- layout?: ReactNode | Handler<any, any, any>; // Wrapper layout with <Outlet /> for content
141
- when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
182
+ transition?: TransitionConfig;
183
+ layout?: ReactNode | Handler<any, any, any>; // Wrapper layout with <Outlet /> for content
184
+ when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
142
185
  };
143
186
 
187
+ export interface ParallelEntryData
188
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
189
+ type: "parallel";
190
+ handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
191
+ /** Set when any parallel slot is a Static definition */
192
+ isStaticPrerender?: true;
193
+ /** Per-slot static handler $$ids for build-time store lookup */
194
+ staticHandlerIds?: Record<string, string>;
195
+ }
196
+
197
+ export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
198
+
199
+ /**
200
+ * This entry's own structural children plus its owned loaders. `loader` lives
201
+ * here (not in EntryPropDatas) because loaders are owned by the entry, not
202
+ * inherited from ancestors.
203
+ *
204
+ * @internal This type is an implementation detail and may change without notice.
205
+ */
144
206
  export type EntryPropSegments = {
145
207
  loader: LoaderEntry[];
146
208
  layout: EntryData[];
147
- parallel: EntryData[]; // type: "parallel" entries with their own loaders/revalidate/loading
209
+ parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
148
210
  intercept: InterceptEntry[]; // intercept definitions for soft navigation
149
211
  };
150
212
 
@@ -152,46 +214,49 @@ export type EntryData =
152
214
  | ({
153
215
  type: "route";
154
216
  handler: Handler<any, any, any>;
155
- loading?: ReactNode | false;
156
217
  /** URL pattern for this route (used by path() in urls()) */
157
218
  pattern?: string;
158
219
  /** Set when handler is a Prerender definition */
159
220
  isPrerender?: true;
160
221
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
161
- prerenderDef?: { getParams?: () => Promise<any[]> | any[]; options?: { passthrough?: boolean } };
222
+ prerenderDef?: {
223
+ getParams?: (ctx: any) => Promise<any[]> | any[];
224
+ options?: { concurrency?: number };
225
+ };
226
+ /** Set when route is wrapped with Passthrough() — has a separate live handler */
227
+ isPassthrough?: true;
228
+ /** Live handler for runtime fallback (only set on Passthrough routes) */
229
+ liveHandler?: Handler<any, any, any>;
162
230
  /** Set when handler is a Static definition (build-time only) */
163
231
  isStaticPrerender?: true;
232
+ /** Static handler $$id for build-time store lookup */
233
+ staticHandlerId?: string;
164
234
  /** Response type for non-RSC routes (json, text, image, any) */
165
235
  responseType?: string;
166
236
  } & EntryPropCommon &
167
237
  EntryPropDatas &
168
- EntryPropSegments)
238
+ EntryPropSegments &
239
+ EntryPropRender)
169
240
  | ({
170
241
  type: "layout";
171
242
  handler: ReactNode | Handler<any, any, any>;
172
- loading?: ReactNode | false;
173
243
  /** Set when handler is a Static definition (build-time only) */
174
244
  isStaticPrerender?: true;
245
+ /** Static handler $$id for build-time store lookup */
246
+ staticHandlerId?: string;
175
247
  } & EntryPropCommon &
176
248
  EntryPropDatas &
177
- EntryPropSegments)
178
- | ({
179
- type: "parallel";
180
- handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
181
- loading?: ReactNode | false;
182
- /** Set when any parallel slot is a Static definition */
183
- isStaticPrerender?: true;
184
- } & EntryPropCommon &
185
- EntryPropDatas &
186
- EntryPropSegments)
249
+ EntryPropSegments &
250
+ EntryPropRender)
251
+ | ParallelEntryData
187
252
  | ({
188
253
  type: "cache";
189
254
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
190
255
  handler: ReactNode | Handler<any, any, any>;
191
- loading?: ReactNode | false;
192
256
  } & EntryPropCommon &
193
257
  EntryPropDatas &
194
- EntryPropSegments);
258
+ EntryPropSegments &
259
+ EntryPropRender);
195
260
 
196
261
  /**
197
262
  * Tracked include info for build-time manifest generation
@@ -229,13 +294,65 @@ interface HelperContext {
229
294
  urlPrefix?: string;
230
295
  /** Name prefix from include() - applied to all named routes */
231
296
  namePrefix?: string;
297
+ /** True when this scope is at root level (no named include boundary above).
298
+ * Routes at root scope allow dot-local reverse to fall back to bare names. */
299
+ rootScoped?: boolean;
232
300
  /** Run helper for cleaner middleware code */
233
301
  run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
234
302
  /** Tracked includes for build-time manifest generation */
235
303
  trackedIncludes?: TrackedInclude[];
304
+ /** Cache profiles for DSL-time cache("profileName") resolution */
305
+ cacheProfiles?: Record<
306
+ string,
307
+ import("../cache/profile-registry.js").CacheProfile
308
+ >;
309
+ /** True when resolving handlers inside a cache() DSL boundary.
310
+ * Read by ctx.get() to guard non-cacheable variable reads. */
311
+ insideCacheScope?: boolean;
312
+ /**
313
+ * Include scope string applied to direct-descendant shortCodes.
314
+ *
315
+ * Each `include(...)` call allocates a sibling-positional token like `I0`,
316
+ * `I1` from its parent's include counter and stores the composed scope
317
+ * (`${parentScope}I${idx}`) in its lazyContext. When the include's handler
318
+ * evaluates lazily, the store's `includeScope` is set from that context so
319
+ * every direct-descendant shortCode is generated as
320
+ * `${parent.shortCode}${includeScope}${prefix}${index}` — preventing
321
+ * collisions with siblings declared outside the include.
322
+ *
323
+ * The scope is NOT propagated through `store.run(...)`, so layouts /
324
+ * parallels / caches inside the include absorb the scope into their own
325
+ * shortCodes and their children start fresh.
326
+ */
327
+ includeScope?: string;
328
+ }
329
+ // Use a global symbol key so the AsyncLocalStorage instance survives HMR
330
+ // module re-evaluation. Without this, Vite's RSC module runner may create
331
+ // a new instance when context.ts is re-evaluated, while other modules still
332
+ // hold references to the old instance — causing getStore() to return
333
+ // undefined even inside a run() callback.
334
+ const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
335
+ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
336
+ globalThis as any
337
+ )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
338
+
339
+ /** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
340
+ const SHORT_CODE_PREFIX: Record<
341
+ "layout" | "parallel" | "route" | "loader" | "cache",
342
+ string
343
+ > = {
344
+ layout: "L",
345
+ parallel: "P",
346
+ route: "R",
347
+ loader: "D",
348
+ cache: "C",
349
+ };
350
+
351
+ /** Post-increment a named per-store counter, returning the prior value. */
352
+ function bumpCounter(store: HelperContext, key: string): number {
353
+ store.counters[key] ??= 0;
354
+ return store.counters[key]++;
236
355
  }
237
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
238
- new AsyncLocalStorage<HelperContext>();
239
356
 
240
357
  export const getContext = (): {
241
358
  context: AsyncLocalStorage<HelperContext>;
@@ -243,29 +360,29 @@ export const getContext = (): {
243
360
  getParent: () => EntryData | null;
244
361
  getOrCreateStore: (forRoute?: string) => HelperContext;
245
362
  getNextIndex: (
246
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
363
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
247
364
  ) => string;
248
365
  getShortCode: (
249
- type: "layout" | "parallel" | "route" | "loader" | "cache"
366
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
250
367
  ) => string;
251
368
  run: <T>(
252
369
  namespace: string,
253
370
  parent: EntryData | null,
254
- callback: (...args: any[]) => T
371
+ callback: (...args: any[]) => T,
255
372
  ) => T;
256
373
  runWithStore: <T>(
257
374
  store: HelperContext,
258
375
  namespace: string,
259
376
  parent: EntryData | null,
260
- callback: (...args: any[]) => T
377
+ callback: (...args: any[]) => T,
261
378
  ) => T;
262
379
  } => {
263
- const context = RSCRouterContext;
380
+ const context = RangoContext;
264
381
 
265
382
  return {
266
383
  context,
267
384
  getOrCreateStore: (forRoute?: string): HelperContext => {
268
- let store = RSCRouterContext.getStore();
385
+ let store = RangoContext.getStore();
269
386
  if (!store) {
270
387
  store = {
271
388
  manifest: new Map<string, EntryData>(),
@@ -285,7 +402,7 @@ export const getContext = (): {
285
402
  const store = context.getStore();
286
403
  if (!store) {
287
404
  throw new Error(
288
- "RSC Router context store is not available. Make sure to run within RSC Router context."
405
+ "Rango context store is not available. Make sure to run within Rango context.",
289
406
  );
290
407
  }
291
408
  return store;
@@ -299,44 +416,46 @@ export const getContext = (): {
299
416
  return store.parent;
300
417
  },
301
418
  getNextIndex: (
302
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
419
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
303
420
  ) => {
304
421
  const store = context.getStore();
305
- invariant(store, "No context RSCRouterContext available");
306
- store.counters[type] ??= 0;
307
- const index = store.counters[type];
308
- store.counters[type] = index + 1;
309
- return `$${type}.${index}`;
422
+ invariant(store, "No context RangoContext available");
423
+ return `$${type}.${bumpCounter(store, type)}`;
310
424
  },
311
- getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
425
+ getShortCode: (
426
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
427
+ ) => {
312
428
  const store = context.getStore();
313
- invariant(store, "No context RSCRouterContext available");
429
+ invariant(store, "No context RangoContext available");
314
430
 
315
431
  const parent = store.parent;
316
- const prefix = type === "layout" ? "L" : type === "parallel" ? "P" : type === "loader" ? "D" : type === "cache" ? "C" : "R";
317
- const mountPrefix = store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
432
+ const prefix = SHORT_CODE_PREFIX[type];
433
+ const mountPrefix =
434
+ store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
435
+
436
+ const includeScope = store.includeScope ?? "";
318
437
 
319
438
  if (!parent) {
320
439
  // Root entry: prefix with mount index and use mount-scoped counter
321
- const counterKey = mountPrefix ? `${mountPrefix}_root_${type}` : `root_${type}`;
322
- store.counters[counterKey] ??= 0;
323
- const index = store.counters[counterKey];
324
- store.counters[counterKey] = index + 1;
325
- return `${mountPrefix}${prefix}${index}`;
440
+ const counterKey = mountPrefix
441
+ ? `${mountPrefix}_root_${type}`
442
+ : `root_${type}`;
443
+ return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
326
444
  } else {
327
- // Child entry: use parent-scoped counter (parent already has M prefix)
328
- const counterKey = `${parent.shortCode}_${type}`;
329
- store.counters[counterKey] ??= 0;
330
- const index = store.counters[counterKey];
331
- store.counters[counterKey] = index + 1;
332
- return `${parent.shortCode}${prefix}${index}`;
445
+ // Child entry: use parent-scoped counter with includeScope appended.
446
+ // When we're evaluating a lazy include's direct children, includeScope
447
+ // is a per-include token like "I0" / "I1I0" that partitions the
448
+ // parent's counter namespace so routes inside one include cannot
449
+ // collide with siblings declared outside it.
450
+ const counterKey = `${parent.shortCode}${includeScope}_${type}`;
451
+ return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
333
452
  }
334
453
  },
335
454
  runWithStore: <T>(
336
455
  store: HelperContext,
337
456
  namespace: string,
338
457
  parent: EntryData | null,
339
- callback: (...args: any[]) => T
458
+ callback: (...args: any[]) => T,
340
459
  ): T => {
341
460
  return context.run(
342
461
  {
@@ -353,23 +472,30 @@ export const getContext = (): {
353
472
  searchSchemas: store.searchSchemas,
354
473
  urlPrefix: store.urlPrefix,
355
474
  namePrefix: store.namePrefix,
475
+ rootScoped: store.rootScoped,
356
476
  trackedIncludes: store.trackedIncludes,
477
+ cacheProfiles: store.cacheProfiles,
478
+ includeScope: store.includeScope,
357
479
  },
358
- callback
480
+ callback,
359
481
  );
360
482
  },
361
483
  run: <T>(
362
484
  namespace: string,
363
485
  parent: EntryData | null,
364
- callback: (...args: any[]) => T
486
+ callback: (...args: any[]) => T,
365
487
  ) => {
366
488
  const store = context.getStore();
367
489
  // Preserve parent counters to ensure globally unique shortCodes
368
490
  const counters = store?.counters || {};
369
491
  const manifest = store ? store.manifest : new Map<string, EntryData>();
370
492
  const patterns = store?.patterns || new Map<string, string>();
371
- const trailingSlash = store?.trailingSlash || new Map<string, "never" | "always" | "ignore">();
372
- const searchSchemas = store?.searchSchemas || new Map<string, Record<string, string>>();
493
+ const patternsByPrefix = store?.patternsByPrefix;
494
+ const trailingSlash =
495
+ store?.trailingSlash ||
496
+ new Map<string, "never" | "always" | "ignore">();
497
+ const searchSchemas =
498
+ store?.searchSchemas || new Map<string, Record<string, string>>();
373
499
  return context.run(
374
500
  {
375
501
  manifest,
@@ -381,18 +507,46 @@ export const getContext = (): {
381
507
  metrics: store?.metrics,
382
508
  isSSR: store?.isSSR,
383
509
  patterns,
510
+ patternsByPrefix,
384
511
  trailingSlash,
385
512
  searchSchemas,
386
513
  urlPrefix: store?.urlPrefix,
387
514
  namePrefix: store?.namePrefix,
515
+ rootScoped: store?.rootScoped,
388
516
  trackedIncludes: store?.trackedIncludes,
517
+ cacheProfiles: store?.cacheProfiles,
389
518
  },
390
- callback
519
+ callback,
391
520
  );
392
521
  },
393
522
  };
394
523
  };
395
524
 
525
+ /**
526
+ * Acquire the active DSL build context, throwing `message` if a helper was
527
+ * called outside a urls()/map() builder. Returns the store API and the live
528
+ * HelperContext so callers avoid a second getContext() lookup.
529
+ */
530
+ export function requireDslContext(message: string): {
531
+ store: ReturnType<typeof getContext>;
532
+ ctx: HelperContext;
533
+ } {
534
+ const store = getContext();
535
+ const ctx = store.context.getStore();
536
+ if (!ctx) {
537
+ // The only reason the store is absent here is that a route-definition helper
538
+ // ran with no active RangoContext — i.e. outside a urls()/map() builder.
539
+ // Record that as the cause so the throw is self-explanatory, not a bare
540
+ // "must be called inside urls()" with no indication of the mechanism.
541
+ throw new DslContextError(message, {
542
+ cause:
543
+ "RangoContext store is undefined: a route-definition helper was called " +
544
+ "outside an active urls()/map() builder.",
545
+ });
546
+ }
547
+ return { store, ctx };
548
+ }
549
+
396
550
  /**
397
551
  * Run a callback with specific URL and name prefixes
398
552
  * Used by include() to apply prefixes to nested patterns
@@ -400,9 +554,9 @@ export const getContext = (): {
400
554
  export function runWithPrefixes<T>(
401
555
  urlPrefix: string,
402
556
  namePrefix: string | undefined,
403
- callback: () => T
557
+ callback: () => T,
404
558
  ): T {
405
- const store = RSCRouterContext.getStore();
559
+ const store = RangoContext.getStore();
406
560
  if (!store) {
407
561
  throw new Error("runWithPrefixes must be called within router context");
408
562
  }
@@ -418,19 +572,43 @@ export function runWithPrefixes<T>(
418
572
  } else {
419
573
  combinedUrlPrefix = urlPrefix;
420
574
  }
421
- const combinedNamePrefix = namePrefix
422
- ? store.namePrefix
423
- ? `${store.namePrefix}.${namePrefix}`
424
- : namePrefix
425
- : store.namePrefix;
575
+ const combinedNamePrefix =
576
+ namePrefix !== undefined
577
+ ? namePrefix === ""
578
+ ? store.namePrefix
579
+ : store.namePrefix
580
+ ? `${store.namePrefix}.${namePrefix}`
581
+ : namePrefix
582
+ : store.namePrefix;
583
+
584
+ // Track root scope for dot-local reverse resolution.
585
+ //
586
+ // The flag answers: "can this route reach bare names at root scope?"
587
+ // It propagates through the include chain:
588
+ //
589
+ // { name: "" } — transparent: inherit parent, default true
590
+ // { name: "foo" } — inherit parent if already set, else create boundary (false)
591
+ // no name — inherit parent unchanged
592
+ //
593
+ // This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
594
+ // (the outer transparent include establishes root access, and the inner
595
+ // named include inherits it). But a direct { name: "sub" } at root gets
596
+ // rootScoped=false (no prior root-access grant, so it creates a boundary).
597
+ const combinedRootScoped =
598
+ namePrefix === ""
599
+ ? (store.rootScoped ?? true)
600
+ : namePrefix !== undefined
601
+ ? (store.rootScoped ?? false)
602
+ : store.rootScoped;
426
603
 
427
- return RSCRouterContext.run(
604
+ return RangoContext.run(
428
605
  {
429
606
  ...store,
430
607
  urlPrefix: combinedUrlPrefix,
431
608
  namePrefix: combinedNamePrefix,
609
+ rootScoped: combinedRootScoped,
432
610
  },
433
- callback
611
+ callback,
434
612
  );
435
613
  }
436
614
 
@@ -438,7 +616,7 @@ export function runWithPrefixes<T>(
438
616
  * Get current URL prefix from context
439
617
  */
440
618
  export function getUrlPrefix(): string {
441
- const store = RSCRouterContext.getStore();
619
+ const store = RangoContext.getStore();
442
620
  return store?.urlPrefix || "";
443
621
  }
444
622
 
@@ -446,13 +624,96 @@ export function getUrlPrefix(): string {
446
624
  * Get current name prefix from context
447
625
  */
448
626
  export function getNamePrefix(): string | undefined {
449
- const store = RSCRouterContext.getStore();
627
+ const store = RangoContext.getStore();
450
628
  return store?.namePrefix;
451
629
  }
452
630
 
631
+ /**
632
+ * Get whether the current scope is at root level (no named include boundary above).
633
+ * Returns true at root or inside { name: "" } includes, false inside named includes.
634
+ */
635
+ export function getRootScoped(): boolean {
636
+ const store = RangoContext.getStore();
637
+ return store?.rootScoped ?? true;
638
+ }
639
+
453
640
  // Export HelperContext type for use in other modules
454
641
  export type { HelperContext };
455
642
 
643
+ /**
644
+ * Return an isolated copy of a lazy include's captured parent entry.
645
+ *
646
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
647
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
648
+ * parent, so without isolation one include's loaders/middleware leak into
649
+ * every other route that shares that root.
650
+ *
651
+ * The clone is shallow: only the mutable arrays are copied so each
652
+ * include pushes to its own list. The rest of the entry (id, shortCode,
653
+ * parent pointer, handler) stays shared, which is correct and cheap.
654
+ */
655
+ export function getIsolatedLazyParent(
656
+ captured: EntryData | null | undefined,
657
+ ): EntryData | null {
658
+ if (!captured) return null;
659
+ return {
660
+ ...captured,
661
+ loader: [...captured.loader],
662
+ middleware: [...captured.middleware],
663
+ revalidate: [...captured.revalidate],
664
+ errorBoundary: [...captured.errorBoundary],
665
+ notFoundBoundary: [...captured.notFoundBoundary],
666
+ layout: [...captured.layout],
667
+ parallel: { ...captured.parallel },
668
+ intercept: [...captured.intercept],
669
+ };
670
+ }
671
+
672
+ export function getParallelEntries(
673
+ parallels: ParallelEntries | EntryData[] | undefined,
674
+ ): ParallelEntryData[] {
675
+ if (!parallels) return [];
676
+ if (Array.isArray(parallels)) {
677
+ return parallels.filter(
678
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
679
+ );
680
+ }
681
+ return Object.values(parallels).filter(
682
+ (entry): entry is ParallelEntryData => !!entry,
683
+ );
684
+ }
685
+
686
+ export function getParallelSlotEntries(
687
+ parallels: ParallelEntries | EntryData[] | undefined,
688
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
689
+ if (!parallels) return [];
690
+
691
+ if (Array.isArray(parallels)) {
692
+ return getParallelEntries(parallels).flatMap((entry) =>
693
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
694
+ slot,
695
+ entry,
696
+ })),
697
+ );
698
+ }
699
+
700
+ return Object.entries(parallels)
701
+ .filter(([, entry]) => !!entry)
702
+ .map(([slot, entry]) => ({
703
+ slot: slot as `@${string}`,
704
+ entry: entry!,
705
+ }));
706
+ }
707
+
708
+ export function getParallelSlotCount(
709
+ parallels: ParallelEntries | EntryData[] | undefined,
710
+ ): number {
711
+ if (!parallels) return 0;
712
+ return Array.isArray(parallels)
713
+ ? parallels.filter((entry) => entry?.type === "parallel").length
714
+ : Object.keys(parallels).length;
715
+ }
716
+
456
717
  // ============================================================================
457
718
  // Performance Metrics Helpers
458
719
  // ============================================================================
@@ -468,8 +729,8 @@ export type { HelperContext };
468
729
  * done(); // Records duration
469
730
  * ```
470
731
  */
471
- export function track(label: string): () => void {
472
- const store = RSCRouterContext.getStore();
732
+ export function track(label: string, depth?: number): () => void {
733
+ const store = RangoContext.getStore();
473
734
 
474
735
  // No-op if context unavailable or metrics not enabled
475
736
  if (!store?.metrics?.enabled) {
@@ -479,7 +740,113 @@ export function track(label: string): () => void {
479
740
  const startTime = performance.now() - store.metrics.requestStart;
480
741
 
481
742
  return () => {
482
- const duration = performance.now() - store.metrics!.requestStart - startTime;
483
- store.metrics!.metrics.push({ label, duration, startTime });
743
+ const duration =
744
+ performance.now() - store.metrics!.requestStart - startTime;
745
+ store.metrics!.metrics.push({
746
+ label,
747
+ duration,
748
+ startTime,
749
+ ...(depth != null ? { depth } : {}),
750
+ });
484
751
  };
485
752
  }
753
+
754
+ /**
755
+ * Separate ALS for tracking loader execution scope.
756
+ * Uses a dedicated ALS (not RangoContext) to avoid issues with
757
+ * nested RangoContext.run() calls in Vite's module runner.
758
+ */
759
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
760
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
761
+ globalThis as any
762
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
763
+
764
+ // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
765
+ // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
766
+ // invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
767
+ // request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
768
+ // so rendered()/barrier/deadlock gating (which must distinguish DSL from
769
+ // handler-invoked loaders) is unchanged.
770
+ const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
771
+ const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
772
+ globalThis as any
773
+ )[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
774
+
775
+ /**
776
+ * Check if the current execution is inside a cache() DSL boundary.
777
+ * Returns false inside loader execution — loaders are always fresh
778
+ * (never cached), so non-cacheable reads are safe.
779
+ */
780
+ export function isInsideCacheScope(): boolean {
781
+ if (RangoContext.getStore()?.insideCacheScope !== true) return false;
782
+ // Loaders are always fresh — even inside a cache() boundary, the loader
783
+ // function re-executes on every request. Skip the guard when running
784
+ // inside a loader.
785
+ if (loaderScopeALS.getStore()?.active) return false;
786
+ // Also exempt handler-invoked loaders: their bodies run in a loader-body
787
+ // scope (not the DSL loader scope above), so request-scoped reads inside any
788
+ // loader — however invoked — are safe (loaders always re-run fresh).
789
+ if (loaderBodyScopeALS.getStore()?.active) return false;
790
+ return true;
791
+ }
792
+
793
+ /**
794
+ * Check if the current execution is inside a DSL loader scope
795
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
796
+ * to distinguish DSL loaders from handler-invoked loaders.
797
+ */
798
+ export function isInsideLoaderScope(): boolean {
799
+ return loaderScopeALS.getStore()?.active === true;
800
+ }
801
+
802
+ /**
803
+ * Run `fn` inside a loader scope. While active, cache-scope guards
804
+ * are bypassed because loaders are always fresh (never cached) and
805
+ * their side effects (setCookie, header, etc.) are safe.
806
+ */
807
+ export function runInsideLoaderScope<T>(fn: () => T): T {
808
+ return loaderScopeALS.run({ active: true }, fn);
809
+ }
810
+
811
+ /**
812
+ * Run `fn` inside a loader BODY scope. Marks loader-function execution for the
813
+ * cache-purity guard only (isInsideCacheScope), WITHOUT affecting
814
+ * isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
815
+ * and handler-invoked via ctx.use) so request-scoped reads inside a loader
816
+ * never trip the cache-scope guards — loaders always run fresh.
817
+ */
818
+ export function runInsideLoaderBodyScope<T>(fn: () => T): T {
819
+ return loaderBodyScopeALS.run({ active: true }, fn);
820
+ }
821
+
822
+ // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
823
+ // A push callback's value is stored as-is; if it is a promise it is NOT tracked
824
+ // by handleStore.settled and does not block segment resolution, so a
825
+ // ctx.use(loader) made from inside such a callback can never form a rendered()
826
+ // deadlock. This is an ALS (not a plain boolean) so the exemption survives the
827
+ // callback's own awaits — an async push callback that resumes after `await`
828
+ // still reads as "inside a push callback" and stays out of the deadlock guard.
829
+ const PUSH_CALLBACK_SCOPE_KEY = Symbol.for(
830
+ "rangojs-router:push-callback-scope",
831
+ );
832
+ const pushCallbackScopeALS: AsyncLocalStorage<{ active: true }> = ((
833
+ globalThis as any
834
+ )[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
835
+
836
+ /**
837
+ * Check if the current execution is inside a handle push callback (sync or an
838
+ * async callback's continuation). Used by the handler-to-loader deadlock guard
839
+ * to exempt push-callback continuations.
840
+ */
841
+ export function isInsidePushCallbackScope(): boolean {
842
+ return pushCallbackScopeALS.getStore()?.active === true;
843
+ }
844
+
845
+ /**
846
+ * Run `fn` inside a push-callback scope. Wraps the invocation of a handle push
847
+ * callback so that any ctx.use(loader) it makes — including after one of its own
848
+ * awaits — is exempt from the deadlock guard.
849
+ */
850
+ export function runInsidePushCallbackScope<T>(fn: () => T): T {
851
+ return pushCallbackScopeALS.run({ active: true }, fn);
852
+ }