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

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 +293 -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 +2508 -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 +24 -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-snapshot.ts +368 -0
  168. package/src/cache/tag-invalidation.ts +230 -0
  169. package/src/cache/taint.ts +153 -0
  170. package/src/cache/types.ts +222 -211
  171. package/src/cache/vercel/index.ts +11 -0
  172. package/src/cache/vercel/vercel-cache-store.ts +1113 -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 +173 -35
  205. package/src/index.ts +241 -73
  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 +527 -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 +897 -0
  313. package/src/rsc/shell-serve.ts +124 -0
  314. package/src/rsc/ssr-setup.ts +144 -0
  315. package/src/rsc/transition-gate.ts +89 -0
  316. package/src/rsc/types.ts +95 -12
  317. package/src/runtime-env.ts +18 -0
  318. package/src/search-params.ts +99 -82
  319. package/src/segment-content-promise.ts +67 -0
  320. package/src/segment-loader-promise.ts +149 -0
  321. package/src/segment-system.tsx +349 -134
  322. package/src/serialize.ts +243 -0
  323. package/src/server/context.ts +459 -85
  324. package/src/server/cookie-parse.ts +32 -0
  325. package/src/server/cookie-store.ts +310 -0
  326. package/src/server/fetchable-loader-store.ts +11 -6
  327. package/src/server/handle-store.ts +123 -42
  328. package/src/server/loader-registry.ts +51 -100
  329. package/src/server/request-context.ts +848 -157
  330. package/src/server.ts +15 -8
  331. package/src/ssr/index.tsx +443 -135
  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 +76 -98
  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 +44 -0
  387. package/src/urls/path-helper-types.ts +413 -0
  388. package/src/urls/path-helper.ts +280 -0
  389. package/src/urls/pattern-types.ts +160 -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,56 @@ 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;
236
+ /**
237
+ * PPR (partial pre-rendering) opt-in from the path() `ppr` option. A
238
+ * document-level property of the page route: `true` uses the default
239
+ * shell policy, an object carries ttl/swr/tags. Read by the integrated
240
+ * PPR serve path (rsc/shell-serve.ts resolvePprConfig).
241
+ */
242
+ ppr?: boolean | import("../urls/pattern-types.js").PartialPrerenderProps;
166
243
  } & EntryPropCommon &
167
244
  EntryPropDatas &
168
- EntryPropSegments)
245
+ EntryPropSegments &
246
+ EntryPropRender)
169
247
  | ({
170
248
  type: "layout";
171
249
  handler: ReactNode | Handler<any, any, any>;
172
- loading?: ReactNode | false;
173
250
  /** Set when handler is a Static definition (build-time only) */
174
251
  isStaticPrerender?: true;
252
+ /** Static handler $$id for build-time store lookup */
253
+ staticHandlerId?: string;
175
254
  } & EntryPropCommon &
176
255
  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)
256
+ EntryPropSegments &
257
+ EntryPropRender)
258
+ | ParallelEntryData
187
259
  | ({
188
260
  type: "cache";
189
261
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
190
262
  handler: ReactNode | Handler<any, any, any>;
191
- loading?: ReactNode | false;
192
263
  } & EntryPropCommon &
193
264
  EntryPropDatas &
194
- EntryPropSegments);
265
+ EntryPropSegments &
266
+ EntryPropRender);
195
267
 
196
268
  /**
197
269
  * Tracked include info for build-time manifest generation
@@ -229,13 +301,65 @@ interface HelperContext {
229
301
  urlPrefix?: string;
230
302
  /** Name prefix from include() - applied to all named routes */
231
303
  namePrefix?: string;
304
+ /** True when this scope is at root level (no named include boundary above).
305
+ * Routes at root scope allow dot-local reverse to fall back to bare names. */
306
+ rootScoped?: boolean;
232
307
  /** Run helper for cleaner middleware code */
233
308
  run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
234
309
  /** Tracked includes for build-time manifest generation */
235
310
  trackedIncludes?: TrackedInclude[];
311
+ /** Cache profiles for DSL-time cache("profileName") resolution */
312
+ cacheProfiles?: Record<
313
+ string,
314
+ import("../cache/profile-registry.js").CacheProfile
315
+ >;
316
+ /** True when resolving handlers inside a cache() DSL boundary.
317
+ * Read by ctx.get() to guard non-cacheable variable reads. */
318
+ insideCacheScope?: boolean;
319
+ /**
320
+ * Include scope string applied to direct-descendant shortCodes.
321
+ *
322
+ * Each `include(...)` call allocates a sibling-positional token like `I0`,
323
+ * `I1` from its parent's include counter and stores the composed scope
324
+ * (`${parentScope}I${idx}`) in its lazyContext. When the include's handler
325
+ * evaluates lazily, the store's `includeScope` is set from that context so
326
+ * every direct-descendant shortCode is generated as
327
+ * `${parent.shortCode}${includeScope}${prefix}${index}` — preventing
328
+ * collisions with siblings declared outside the include.
329
+ *
330
+ * The scope is NOT propagated through `store.run(...)`, so layouts /
331
+ * parallels / caches inside the include absorb the scope into their own
332
+ * shortCodes and their children start fresh.
333
+ */
334
+ includeScope?: string;
335
+ }
336
+ // Use a global symbol key so the AsyncLocalStorage instance survives HMR
337
+ // module re-evaluation. Without this, Vite's RSC module runner may create
338
+ // a new instance when context.ts is re-evaluated, while other modules still
339
+ // hold references to the old instance — causing getStore() to return
340
+ // undefined even inside a run() callback.
341
+ const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
342
+ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
343
+ globalThis as any
344
+ )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
345
+
346
+ /** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
347
+ const SHORT_CODE_PREFIX: Record<
348
+ "layout" | "parallel" | "route" | "loader" | "cache",
349
+ string
350
+ > = {
351
+ layout: "L",
352
+ parallel: "P",
353
+ route: "R",
354
+ loader: "D",
355
+ cache: "C",
356
+ };
357
+
358
+ /** Post-increment a named per-store counter, returning the prior value. */
359
+ function bumpCounter(store: HelperContext, key: string): number {
360
+ store.counters[key] ??= 0;
361
+ return store.counters[key]++;
236
362
  }
237
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
238
- new AsyncLocalStorage<HelperContext>();
239
363
 
240
364
  export const getContext = (): {
241
365
  context: AsyncLocalStorage<HelperContext>;
@@ -243,29 +367,29 @@ export const getContext = (): {
243
367
  getParent: () => EntryData | null;
244
368
  getOrCreateStore: (forRoute?: string) => HelperContext;
245
369
  getNextIndex: (
246
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
370
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
247
371
  ) => string;
248
372
  getShortCode: (
249
- type: "layout" | "parallel" | "route" | "loader" | "cache"
373
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
250
374
  ) => string;
251
375
  run: <T>(
252
376
  namespace: string,
253
377
  parent: EntryData | null,
254
- callback: (...args: any[]) => T
378
+ callback: (...args: any[]) => T,
255
379
  ) => T;
256
380
  runWithStore: <T>(
257
381
  store: HelperContext,
258
382
  namespace: string,
259
383
  parent: EntryData | null,
260
- callback: (...args: any[]) => T
384
+ callback: (...args: any[]) => T,
261
385
  ) => T;
262
386
  } => {
263
- const context = RSCRouterContext;
387
+ const context = RangoContext;
264
388
 
265
389
  return {
266
390
  context,
267
391
  getOrCreateStore: (forRoute?: string): HelperContext => {
268
- let store = RSCRouterContext.getStore();
392
+ let store = RangoContext.getStore();
269
393
  if (!store) {
270
394
  store = {
271
395
  manifest: new Map<string, EntryData>(),
@@ -285,7 +409,7 @@ export const getContext = (): {
285
409
  const store = context.getStore();
286
410
  if (!store) {
287
411
  throw new Error(
288
- "RSC Router context store is not available. Make sure to run within RSC Router context."
412
+ "Rango context store is not available. Make sure to run within Rango context.",
289
413
  );
290
414
  }
291
415
  return store;
@@ -299,44 +423,46 @@ export const getContext = (): {
299
423
  return store.parent;
300
424
  },
301
425
  getNextIndex: (
302
- type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
426
+ type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
303
427
  ) => {
304
428
  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}`;
429
+ invariant(store, "No context RangoContext available");
430
+ return `$${type}.${bumpCounter(store, type)}`;
310
431
  },
311
- getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
432
+ getShortCode: (
433
+ type: "layout" | "parallel" | "route" | "loader" | "cache",
434
+ ) => {
312
435
  const store = context.getStore();
313
- invariant(store, "No context RSCRouterContext available");
436
+ invariant(store, "No context RangoContext available");
314
437
 
315
438
  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}` : "";
439
+ const prefix = SHORT_CODE_PREFIX[type];
440
+ const mountPrefix =
441
+ store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
442
+
443
+ const includeScope = store.includeScope ?? "";
318
444
 
319
445
  if (!parent) {
320
446
  // 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}`;
447
+ const counterKey = mountPrefix
448
+ ? `${mountPrefix}_root_${type}`
449
+ : `root_${type}`;
450
+ return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
326
451
  } 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}`;
452
+ // Child entry: use parent-scoped counter with includeScope appended.
453
+ // When we're evaluating a lazy include's direct children, includeScope
454
+ // is a per-include token like "I0" / "I1I0" that partitions the
455
+ // parent's counter namespace so routes inside one include cannot
456
+ // collide with siblings declared outside it.
457
+ const counterKey = `${parent.shortCode}${includeScope}_${type}`;
458
+ return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
333
459
  }
334
460
  },
335
461
  runWithStore: <T>(
336
462
  store: HelperContext,
337
463
  namespace: string,
338
464
  parent: EntryData | null,
339
- callback: (...args: any[]) => T
465
+ callback: (...args: any[]) => T,
340
466
  ): T => {
341
467
  return context.run(
342
468
  {
@@ -353,23 +479,30 @@ export const getContext = (): {
353
479
  searchSchemas: store.searchSchemas,
354
480
  urlPrefix: store.urlPrefix,
355
481
  namePrefix: store.namePrefix,
482
+ rootScoped: store.rootScoped,
356
483
  trackedIncludes: store.trackedIncludes,
484
+ cacheProfiles: store.cacheProfiles,
485
+ includeScope: store.includeScope,
357
486
  },
358
- callback
487
+ callback,
359
488
  );
360
489
  },
361
490
  run: <T>(
362
491
  namespace: string,
363
492
  parent: EntryData | null,
364
- callback: (...args: any[]) => T
493
+ callback: (...args: any[]) => T,
365
494
  ) => {
366
495
  const store = context.getStore();
367
496
  // Preserve parent counters to ensure globally unique shortCodes
368
497
  const counters = store?.counters || {};
369
498
  const manifest = store ? store.manifest : new Map<string, EntryData>();
370
499
  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>>();
500
+ const patternsByPrefix = store?.patternsByPrefix;
501
+ const trailingSlash =
502
+ store?.trailingSlash ||
503
+ new Map<string, "never" | "always" | "ignore">();
504
+ const searchSchemas =
505
+ store?.searchSchemas || new Map<string, Record<string, string>>();
373
506
  return context.run(
374
507
  {
375
508
  manifest,
@@ -381,18 +514,46 @@ export const getContext = (): {
381
514
  metrics: store?.metrics,
382
515
  isSSR: store?.isSSR,
383
516
  patterns,
517
+ patternsByPrefix,
384
518
  trailingSlash,
385
519
  searchSchemas,
386
520
  urlPrefix: store?.urlPrefix,
387
521
  namePrefix: store?.namePrefix,
522
+ rootScoped: store?.rootScoped,
388
523
  trackedIncludes: store?.trackedIncludes,
524
+ cacheProfiles: store?.cacheProfiles,
389
525
  },
390
- callback
526
+ callback,
391
527
  );
392
528
  },
393
529
  };
394
530
  };
395
531
 
532
+ /**
533
+ * Acquire the active DSL build context, throwing `message` if a helper was
534
+ * called outside a urls()/map() builder. Returns the store API and the live
535
+ * HelperContext so callers avoid a second getContext() lookup.
536
+ */
537
+ export function requireDslContext(message: string): {
538
+ store: ReturnType<typeof getContext>;
539
+ ctx: HelperContext;
540
+ } {
541
+ const store = getContext();
542
+ const ctx = store.context.getStore();
543
+ if (!ctx) {
544
+ // The only reason the store is absent here is that a route-definition helper
545
+ // ran with no active RangoContext — i.e. outside a urls()/map() builder.
546
+ // Record that as the cause so the throw is self-explanatory, not a bare
547
+ // "must be called inside urls()" with no indication of the mechanism.
548
+ throw new DslContextError(message, {
549
+ cause:
550
+ "RangoContext store is undefined: a route-definition helper was called " +
551
+ "outside an active urls()/map() builder.",
552
+ });
553
+ }
554
+ return { store, ctx };
555
+ }
556
+
396
557
  /**
397
558
  * Run a callback with specific URL and name prefixes
398
559
  * Used by include() to apply prefixes to nested patterns
@@ -400,9 +561,9 @@ export const getContext = (): {
400
561
  export function runWithPrefixes<T>(
401
562
  urlPrefix: string,
402
563
  namePrefix: string | undefined,
403
- callback: () => T
564
+ callback: () => T,
404
565
  ): T {
405
- const store = RSCRouterContext.getStore();
566
+ const store = RangoContext.getStore();
406
567
  if (!store) {
407
568
  throw new Error("runWithPrefixes must be called within router context");
408
569
  }
@@ -418,19 +579,43 @@ export function runWithPrefixes<T>(
418
579
  } else {
419
580
  combinedUrlPrefix = urlPrefix;
420
581
  }
421
- const combinedNamePrefix = namePrefix
422
- ? store.namePrefix
423
- ? `${store.namePrefix}.${namePrefix}`
424
- : namePrefix
425
- : store.namePrefix;
582
+ const combinedNamePrefix =
583
+ namePrefix !== undefined
584
+ ? namePrefix === ""
585
+ ? store.namePrefix
586
+ : store.namePrefix
587
+ ? `${store.namePrefix}.${namePrefix}`
588
+ : namePrefix
589
+ : store.namePrefix;
590
+
591
+ // Track root scope for dot-local reverse resolution.
592
+ //
593
+ // The flag answers: "can this route reach bare names at root scope?"
594
+ // It propagates through the include chain:
595
+ //
596
+ // { name: "" } — transparent: inherit parent, default true
597
+ // { name: "foo" } — inherit parent if already set, else create boundary (false)
598
+ // no name — inherit parent unchanged
599
+ //
600
+ // This means { name: "" } + nested { name: "sub" } keeps rootScoped=true
601
+ // (the outer transparent include establishes root access, and the inner
602
+ // named include inherits it). But a direct { name: "sub" } at root gets
603
+ // rootScoped=false (no prior root-access grant, so it creates a boundary).
604
+ const combinedRootScoped =
605
+ namePrefix === ""
606
+ ? (store.rootScoped ?? true)
607
+ : namePrefix !== undefined
608
+ ? (store.rootScoped ?? false)
609
+ : store.rootScoped;
426
610
 
427
- return RSCRouterContext.run(
611
+ return RangoContext.run(
428
612
  {
429
613
  ...store,
430
614
  urlPrefix: combinedUrlPrefix,
431
615
  namePrefix: combinedNamePrefix,
616
+ rootScoped: combinedRootScoped,
432
617
  },
433
- callback
618
+ callback,
434
619
  );
435
620
  }
436
621
 
@@ -438,7 +623,7 @@ export function runWithPrefixes<T>(
438
623
  * Get current URL prefix from context
439
624
  */
440
625
  export function getUrlPrefix(): string {
441
- const store = RSCRouterContext.getStore();
626
+ const store = RangoContext.getStore();
442
627
  return store?.urlPrefix || "";
443
628
  }
444
629
 
@@ -446,13 +631,96 @@ export function getUrlPrefix(): string {
446
631
  * Get current name prefix from context
447
632
  */
448
633
  export function getNamePrefix(): string | undefined {
449
- const store = RSCRouterContext.getStore();
634
+ const store = RangoContext.getStore();
450
635
  return store?.namePrefix;
451
636
  }
452
637
 
638
+ /**
639
+ * Get whether the current scope is at root level (no named include boundary above).
640
+ * Returns true at root or inside { name: "" } includes, false inside named includes.
641
+ */
642
+ export function getRootScoped(): boolean {
643
+ const store = RangoContext.getStore();
644
+ return store?.rootScoped ?? true;
645
+ }
646
+
453
647
  // Export HelperContext type for use in other modules
454
648
  export type { HelperContext };
455
649
 
650
+ /**
651
+ * Return an isolated copy of a lazy include's captured parent entry.
652
+ *
653
+ * DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
654
+ * Multiple include() scopes capture the *same* syntheticMapRoot as their
655
+ * parent, so without isolation one include's loaders/middleware leak into
656
+ * every other route that shares that root.
657
+ *
658
+ * The clone is shallow: only the mutable arrays are copied so each
659
+ * include pushes to its own list. The rest of the entry (id, shortCode,
660
+ * parent pointer, handler) stays shared, which is correct and cheap.
661
+ */
662
+ export function getIsolatedLazyParent(
663
+ captured: EntryData | null | undefined,
664
+ ): EntryData | null {
665
+ if (!captured) return null;
666
+ return {
667
+ ...captured,
668
+ loader: [...captured.loader],
669
+ middleware: [...captured.middleware],
670
+ revalidate: [...captured.revalidate],
671
+ errorBoundary: [...captured.errorBoundary],
672
+ notFoundBoundary: [...captured.notFoundBoundary],
673
+ layout: [...captured.layout],
674
+ parallel: { ...captured.parallel },
675
+ intercept: [...captured.intercept],
676
+ };
677
+ }
678
+
679
+ export function getParallelEntries(
680
+ parallels: ParallelEntries | EntryData[] | undefined,
681
+ ): ParallelEntryData[] {
682
+ if (!parallels) return [];
683
+ if (Array.isArray(parallels)) {
684
+ return parallels.filter(
685
+ (entry): entry is ParallelEntryData => entry.type === "parallel",
686
+ );
687
+ }
688
+ return Object.values(parallels).filter(
689
+ (entry): entry is ParallelEntryData => !!entry,
690
+ );
691
+ }
692
+
693
+ export function getParallelSlotEntries(
694
+ parallels: ParallelEntries | EntryData[] | undefined,
695
+ ): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
696
+ if (!parallels) return [];
697
+
698
+ if (Array.isArray(parallels)) {
699
+ return getParallelEntries(parallels).flatMap((entry) =>
700
+ (Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
701
+ slot,
702
+ entry,
703
+ })),
704
+ );
705
+ }
706
+
707
+ return Object.entries(parallels)
708
+ .filter(([, entry]) => !!entry)
709
+ .map(([slot, entry]) => ({
710
+ slot: slot as `@${string}`,
711
+ entry: entry!,
712
+ }));
713
+ }
714
+
715
+ export function getParallelSlotCount(
716
+ parallels: ParallelEntries | EntryData[] | undefined,
717
+ ): number {
718
+ if (!parallels) return 0;
719
+ return Array.isArray(parallels)
720
+ ? parallels.filter((entry) => entry?.type === "parallel").length
721
+ : Object.keys(parallels).length;
722
+ }
723
+
456
724
  // ============================================================================
457
725
  // Performance Metrics Helpers
458
726
  // ============================================================================
@@ -468,8 +736,8 @@ export type { HelperContext };
468
736
  * done(); // Records duration
469
737
  * ```
470
738
  */
471
- export function track(label: string): () => void {
472
- const store = RSCRouterContext.getStore();
739
+ export function track(label: string, depth?: number): () => void {
740
+ const store = RangoContext.getStore();
473
741
 
474
742
  // No-op if context unavailable or metrics not enabled
475
743
  if (!store?.metrics?.enabled) {
@@ -479,7 +747,113 @@ export function track(label: string): () => void {
479
747
  const startTime = performance.now() - store.metrics.requestStart;
480
748
 
481
749
  return () => {
482
- const duration = performance.now() - store.metrics!.requestStart - startTime;
483
- store.metrics!.metrics.push({ label, duration, startTime });
750
+ const duration =
751
+ performance.now() - store.metrics!.requestStart - startTime;
752
+ store.metrics!.metrics.push({
753
+ label,
754
+ duration,
755
+ startTime,
756
+ ...(depth != null ? { depth } : {}),
757
+ });
484
758
  };
485
759
  }
760
+
761
+ /**
762
+ * Separate ALS for tracking loader execution scope.
763
+ * Uses a dedicated ALS (not RangoContext) to avoid issues with
764
+ * nested RangoContext.run() calls in Vite's module runner.
765
+ */
766
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
767
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
768
+ globalThis as any
769
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
770
+
771
+ // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
772
+ // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
773
+ // invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
774
+ // request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
775
+ // so rendered()/barrier/deadlock gating (which must distinguish DSL from
776
+ // handler-invoked loaders) is unchanged.
777
+ const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
778
+ const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
779
+ globalThis as any
780
+ )[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
781
+
782
+ /**
783
+ * Check if the current execution is inside a cache() DSL boundary.
784
+ * Returns false inside loader execution — loaders are always fresh
785
+ * (never cached), so non-cacheable reads are safe.
786
+ */
787
+ export function isInsideCacheScope(): boolean {
788
+ if (RangoContext.getStore()?.insideCacheScope !== true) return false;
789
+ // Loaders are always fresh — even inside a cache() boundary, the loader
790
+ // function re-executes on every request. Skip the guard when running
791
+ // inside a loader.
792
+ if (loaderScopeALS.getStore()?.active) return false;
793
+ // Also exempt handler-invoked loaders: their bodies run in a loader-body
794
+ // scope (not the DSL loader scope above), so request-scoped reads inside any
795
+ // loader — however invoked — are safe (loaders always re-run fresh).
796
+ if (loaderBodyScopeALS.getStore()?.active) return false;
797
+ return true;
798
+ }
799
+
800
+ /**
801
+ * Check if the current execution is inside a DSL loader scope
802
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
803
+ * to distinguish DSL loaders from handler-invoked loaders.
804
+ */
805
+ export function isInsideLoaderScope(): boolean {
806
+ return loaderScopeALS.getStore()?.active === true;
807
+ }
808
+
809
+ /**
810
+ * Run `fn` inside a loader scope. While active, cache-scope guards
811
+ * are bypassed because loaders are always fresh (never cached) and
812
+ * their side effects (setCookie, header, etc.) are safe.
813
+ */
814
+ export function runInsideLoaderScope<T>(fn: () => T): T {
815
+ return loaderScopeALS.run({ active: true }, fn);
816
+ }
817
+
818
+ /**
819
+ * Run `fn` inside a loader BODY scope. Marks loader-function execution for the
820
+ * cache-purity guard only (isInsideCacheScope), WITHOUT affecting
821
+ * isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
822
+ * and handler-invoked via ctx.use) so request-scoped reads inside a loader
823
+ * never trip the cache-scope guards — loaders always run fresh.
824
+ */
825
+ export function runInsideLoaderBodyScope<T>(fn: () => T): T {
826
+ return loaderBodyScopeALS.run({ active: true }, fn);
827
+ }
828
+
829
+ // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
830
+ // A push callback's value is stored as-is; if it is a promise it is NOT tracked
831
+ // by handleStore.settled and does not block segment resolution, so a
832
+ // ctx.use(loader) made from inside such a callback can never form a rendered()
833
+ // deadlock. This is an ALS (not a plain boolean) so the exemption survives the
834
+ // callback's own awaits — an async push callback that resumes after `await`
835
+ // still reads as "inside a push callback" and stays out of the deadlock guard.
836
+ const PUSH_CALLBACK_SCOPE_KEY = Symbol.for(
837
+ "rangojs-router:push-callback-scope",
838
+ );
839
+ const pushCallbackScopeALS: AsyncLocalStorage<{ active: true }> = ((
840
+ globalThis as any
841
+ )[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
842
+
843
+ /**
844
+ * Check if the current execution is inside a handle push callback (sync or an
845
+ * async callback's continuation). Used by the handler-to-loader deadlock guard
846
+ * to exempt push-callback continuations.
847
+ */
848
+ export function isInsidePushCallbackScope(): boolean {
849
+ return pushCallbackScopeALS.getStore()?.active === true;
850
+ }
851
+
852
+ /**
853
+ * Run `fn` inside a push-callback scope. Wraps the invocation of a handle push
854
+ * callback so that any ctx.use(loader) it makes — including after one of its own
855
+ * awaits — is exempt from the deadlock guard.
856
+ */
857
+ export function runInsidePushCallbackScope<T>(fn: () => T): T {
858
+ return pushCallbackScopeALS.run({ active: true }, fn);
859
+ }