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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (392) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +126 -16
  3. package/dist/bin/rango.js +319 -95
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +2724 -1053
  6. package/package.json +68 -14
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +64 -2
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +224 -32
  11. package/skills/caching/SKILL.md +279 -17
  12. package/skills/composability/SKILL.md +27 -3
  13. package/skills/css/SKILL.md +76 -0
  14. package/skills/debug-manifest/SKILL.md +4 -2
  15. package/skills/document-cache/SKILL.md +78 -55
  16. package/skills/handler-use/SKILL.md +11 -9
  17. package/skills/hooks/SKILL.md +243 -29
  18. package/skills/host-router/SKILL.md +83 -23
  19. package/skills/i18n/SKILL.md +276 -0
  20. package/skills/intercept/SKILL.md +68 -19
  21. package/skills/layout/SKILL.md +13 -9
  22. package/skills/links/SKILL.md +190 -23
  23. package/skills/loader/SKILL.md +235 -9
  24. package/skills/middleware/SKILL.md +18 -10
  25. package/skills/migrate-nextjs/SKILL.md +43 -19
  26. package/skills/migrate-react-router/SKILL.md +8 -2
  27. package/skills/mime-routes/SKILL.md +28 -1
  28. package/skills/observability/SKILL.md +172 -0
  29. package/skills/parallel/SKILL.md +18 -7
  30. package/skills/prerender/SKILL.md +65 -60
  31. package/skills/rango/SKILL.md +251 -24
  32. package/skills/react-compiler/SKILL.md +168 -0
  33. package/skills/response-routes/SKILL.md +115 -48
  34. package/skills/route/SKILL.md +46 -5
  35. package/skills/router-setup/SKILL.md +30 -8
  36. package/skills/scripts/SKILL.md +179 -0
  37. package/skills/server-actions/SKILL.md +775 -0
  38. package/skills/tailwind/SKILL.md +27 -3
  39. package/skills/testing/SKILL.md +130 -0
  40. package/skills/testing/bindings.md +103 -0
  41. package/skills/testing/cache-prerender.md +127 -0
  42. package/skills/testing/client-components.md +124 -0
  43. package/skills/testing/e2e-parity.md +125 -0
  44. package/skills/testing/flight.md +91 -0
  45. package/skills/testing/handles.md +129 -0
  46. package/skills/testing/loader.md +128 -0
  47. package/skills/testing/middleware.md +99 -0
  48. package/skills/testing/render-handler.md +122 -0
  49. package/skills/testing/response-routes.md +95 -0
  50. package/skills/testing/reverse-and-types.md +84 -0
  51. package/skills/testing/server-actions.md +107 -0
  52. package/skills/testing/server-tree.md +128 -0
  53. package/skills/testing/setup.md +123 -0
  54. package/skills/typesafety/SKILL.md +322 -29
  55. package/skills/use-cache/SKILL.md +57 -14
  56. package/skills/view-transitions/SKILL.md +337 -0
  57. package/src/__augment-tests__/augment.ts +81 -0
  58. package/src/__augment-tests__/augmented.check.ts +116 -0
  59. package/src/__internal.ts +0 -65
  60. package/src/browser/action-coordinator.ts +53 -36
  61. package/src/browser/action-fence.ts +47 -0
  62. package/src/browser/app-shell.ts +39 -0
  63. package/src/browser/connection-warmup.ts +134 -0
  64. package/src/browser/cookie-name.ts +140 -0
  65. package/src/browser/event-controller.ts +192 -150
  66. package/src/browser/history-state.ts +21 -0
  67. package/src/browser/index.ts +3 -3
  68. package/src/browser/invalidate-client-cache.ts +52 -0
  69. package/src/browser/navigation-bridge.ts +94 -25
  70. package/src/browser/navigation-client.ts +121 -84
  71. package/src/browser/navigation-store-handle.ts +38 -0
  72. package/src/browser/navigation-store.ts +115 -67
  73. package/src/browser/navigation-transaction.ts +9 -59
  74. package/src/browser/network-error-handler.ts +34 -7
  75. package/src/browser/partial-update.ts +147 -128
  76. package/src/browser/prefetch/cache.ts +107 -56
  77. package/src/browser/prefetch/fetch.ts +204 -34
  78. package/src/browser/prefetch/queue.ts +6 -3
  79. package/src/browser/rango-state.ts +158 -76
  80. package/src/browser/react/Link.tsx +30 -7
  81. package/src/browser/react/NavigationProvider.tsx +283 -118
  82. package/src/browser/react/ScrollRestoration.tsx +10 -6
  83. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  84. package/src/browser/react/filter-segment-order.ts +66 -7
  85. package/src/browser/react/index.ts +0 -48
  86. package/src/browser/react/location-state-shared.ts +178 -8
  87. package/src/browser/react/location-state.ts +39 -14
  88. package/src/browser/react/use-action.ts +6 -15
  89. package/src/browser/react/use-handle.ts +17 -14
  90. package/src/browser/react/use-href.tsx +8 -1
  91. package/src/browser/react/use-link-status.ts +33 -8
  92. package/src/browser/react/use-navigation.ts +10 -5
  93. package/src/browser/react/use-params.ts +11 -11
  94. package/src/browser/react/use-reverse.ts +106 -0
  95. package/src/browser/react/use-router.ts +25 -3
  96. package/src/browser/react/use-search-params.ts +0 -5
  97. package/src/browser/react/use-segments.ts +11 -21
  98. package/src/browser/response-adapter.ts +99 -8
  99. package/src/browser/rsc-router.tsx +91 -24
  100. package/src/browser/scroll-restoration.ts +30 -17
  101. package/src/browser/segment-structure-assert.ts +2 -2
  102. package/src/browser/server-action-bridge.ts +214 -55
  103. package/src/browser/types.ts +80 -9
  104. package/src/browser/validate-redirect-origin.ts +43 -16
  105. package/src/build/collect-fallback-refs.ts +107 -0
  106. package/src/build/generate-manifest.ts +60 -35
  107. package/src/build/generate-route-types.ts +2 -1
  108. package/src/build/index.ts +8 -2
  109. package/src/build/prefix-tree-utils.ts +123 -0
  110. package/src/build/route-trie.ts +117 -14
  111. package/src/build/route-types/ast-route-extraction.ts +15 -8
  112. package/src/build/route-types/codegen.ts +16 -5
  113. package/src/build/route-types/include-resolution.ts +117 -23
  114. package/src/build/route-types/param-extraction.ts +6 -3
  115. package/src/build/route-types/per-module-writer.ts +22 -6
  116. package/src/build/route-types/router-processing.ts +55 -28
  117. package/src/build/route-types/scan-filter.ts +1 -1
  118. package/src/build/route-types/source-scan.ts +216 -0
  119. package/src/build/runtime-discovery.ts +9 -20
  120. package/src/cache/cache-error.ts +104 -0
  121. package/src/cache/cache-key-utils.ts +29 -13
  122. package/src/cache/cache-policy.ts +108 -34
  123. package/src/cache/cache-runtime.ts +224 -41
  124. package/src/cache/cache-scope.ts +188 -82
  125. package/src/cache/cache-tag.ts +103 -0
  126. package/src/cache/cf/cf-base64.ts +33 -0
  127. package/src/cache/cf/cf-cache-constants.ts +127 -0
  128. package/src/cache/cf/cf-cache-store.ts +1989 -378
  129. package/src/cache/cf/cf-cache-types.ts +349 -0
  130. package/src/cache/cf/cf-kv-utils.ts +46 -0
  131. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  132. package/src/cache/cf/index.ts +6 -16
  133. package/src/cache/document-cache.ts +89 -21
  134. package/src/cache/handle-snapshot.ts +70 -0
  135. package/src/cache/index.ts +10 -20
  136. package/src/cache/memory-segment-store.ts +136 -37
  137. package/src/cache/profile-registry.ts +46 -31
  138. package/src/cache/read-through-swr.ts +56 -12
  139. package/src/cache/segment-codec.ts +9 -17
  140. package/src/cache/tag-invalidation.ts +230 -0
  141. package/src/cache/types.ts +37 -100
  142. package/src/client.rsc.tsx +44 -21
  143. package/src/client.tsx +36 -61
  144. package/src/cloudflare/index.ts +11 -0
  145. package/src/cloudflare/tracing.ts +109 -0
  146. package/src/component-utils.ts +19 -0
  147. package/src/components/DefaultDocument.tsx +8 -2
  148. package/src/context-var.ts +18 -6
  149. package/src/decode-loader-results.ts +52 -0
  150. package/src/defer.ts +196 -0
  151. package/src/deps/ssr.ts +0 -1
  152. package/src/encode-kv.ts +49 -0
  153. package/src/errors.ts +30 -4
  154. package/src/escape-script.ts +52 -0
  155. package/src/handle.ts +31 -23
  156. package/src/handles/MetaTags.tsx +62 -19
  157. package/src/handles/Scripts.tsx +183 -0
  158. package/src/handles/breadcrumbs.ts +37 -8
  159. package/src/handles/is-thenable.ts +19 -0
  160. package/src/handles/meta.ts +51 -40
  161. package/src/handles/script.ts +244 -0
  162. package/src/host/cookie-handler.ts +9 -60
  163. package/src/host/errors.ts +0 -24
  164. package/src/host/index.ts +8 -2
  165. package/src/host/pattern-matcher.ts +23 -52
  166. package/src/host/router.ts +107 -99
  167. package/src/host/testing.ts +40 -27
  168. package/src/host/types.ts +37 -4
  169. package/src/host/utils.ts +1 -1
  170. package/src/href-client.ts +137 -22
  171. package/src/index.rsc.ts +96 -12
  172. package/src/index.ts +94 -14
  173. package/src/internal-debug.ts +11 -10
  174. package/src/loader-store.ts +500 -0
  175. package/src/loader.rsc.ts +20 -13
  176. package/src/loader.ts +12 -11
  177. package/src/missing-id-error.ts +68 -0
  178. package/src/outlet-context.ts +1 -1
  179. package/src/outlet-provider.tsx +1 -5
  180. package/src/prerender/param-hash.ts +16 -16
  181. package/src/prerender/store.ts +32 -37
  182. package/src/prerender.ts +61 -6
  183. package/src/redirect-origin.ts +100 -0
  184. package/src/regex-escape.ts +8 -0
  185. package/src/render-error-thrower.tsx +20 -0
  186. package/src/response-utils.ts +34 -0
  187. package/src/reverse.ts +65 -40
  188. package/src/root-error-boundary.tsx +1 -19
  189. package/src/route-content-wrapper.tsx +19 -77
  190. package/src/route-definition/dsl-helpers.ts +304 -309
  191. package/src/route-definition/helper-factories.ts +28 -140
  192. package/src/route-definition/helpers-types.ts +82 -55
  193. package/src/route-definition/index.ts +1 -2
  194. package/src/route-definition/redirect.ts +44 -11
  195. package/src/route-definition/resolve-handler-use.ts +12 -1
  196. package/src/route-definition/use-item-types.ts +29 -0
  197. package/src/route-map-builder.ts +0 -16
  198. package/src/route-types.ts +19 -46
  199. package/src/router/basename.ts +14 -0
  200. package/src/router/content-negotiation.ts +73 -25
  201. package/src/router/error-handling.ts +45 -18
  202. package/src/router/find-match.ts +44 -23
  203. package/src/router/handler-context.ts +27 -43
  204. package/src/router/instrument.ts +350 -0
  205. package/src/router/intercept-resolution.ts +39 -20
  206. package/src/router/lazy-includes.ts +10 -47
  207. package/src/router/loader-resolution.ts +155 -72
  208. package/src/router/logging.ts +0 -6
  209. package/src/router/manifest.ts +18 -29
  210. package/src/router/match-api.ts +9 -24
  211. package/src/router/match-context.ts +0 -22
  212. package/src/router/match-handlers.ts +58 -58
  213. package/src/router/match-middleware/background-revalidation.ts +40 -24
  214. package/src/router/match-middleware/cache-lookup.ts +159 -285
  215. package/src/router/match-middleware/cache-store.ts +64 -52
  216. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  217. package/src/router/match-middleware/segment-resolution.ts +0 -22
  218. package/src/router/match-pipelines.ts +1 -42
  219. package/src/router/match-result.ts +44 -74
  220. package/src/router/metrics.ts +0 -34
  221. package/src/router/middleware-types.ts +7 -134
  222. package/src/router/middleware.ts +247 -166
  223. package/src/router/navigation-snapshot.ts +0 -51
  224. package/src/router/params-util.ts +23 -0
  225. package/src/router/pattern-matching.ts +85 -94
  226. package/src/router/prefetch-cache-ttl.ts +51 -0
  227. package/src/router/prerender-match.ts +104 -65
  228. package/src/router/preview-match.ts +3 -1
  229. package/src/router/request-classification.ts +28 -62
  230. package/src/router/revalidation.ts +123 -73
  231. package/src/router/route-snapshot.ts +0 -1
  232. package/src/router/router-context.ts +3 -28
  233. package/src/router/router-interfaces.ts +83 -35
  234. package/src/router/router-options.ts +136 -5
  235. package/src/router/router-registry.ts +2 -5
  236. package/src/router/segment-resolution/fresh.ts +97 -84
  237. package/src/router/segment-resolution/helpers.ts +86 -6
  238. package/src/router/segment-resolution/loader-cache.ts +76 -39
  239. package/src/router/segment-resolution/revalidation.ts +272 -320
  240. package/src/router/segment-resolution/static-store.ts +19 -5
  241. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  242. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  243. package/src/router/segment-resolution.ts +5 -1
  244. package/src/router/segment-wrappers.ts +6 -5
  245. package/src/router/state-cookie-name.ts +33 -0
  246. package/src/router/substitute-pattern-params.ts +56 -0
  247. package/src/router/telemetry-otel.ts +161 -199
  248. package/src/router/telemetry.ts +96 -19
  249. package/src/router/timeout.ts +0 -20
  250. package/src/router/tracing.ts +206 -0
  251. package/src/router/trie-matching.ts +162 -64
  252. package/src/router/types.ts +9 -63
  253. package/src/router/url-params.ts +0 -5
  254. package/src/router.ts +110 -55
  255. package/src/rsc/handler-context.ts +3 -2
  256. package/src/rsc/handler.ts +264 -220
  257. package/src/rsc/helpers.ts +100 -6
  258. package/src/rsc/index.ts +2 -5
  259. package/src/rsc/json-route-result.ts +38 -0
  260. package/src/rsc/loader-fetch.ts +114 -38
  261. package/src/rsc/manifest-init.ts +28 -41
  262. package/src/rsc/origin-guard.ts +39 -25
  263. package/src/rsc/progressive-enhancement.ts +117 -11
  264. package/src/rsc/redirect-guard.ts +99 -0
  265. package/src/rsc/response-cache-serve.ts +238 -0
  266. package/src/rsc/response-error.ts +79 -12
  267. package/src/rsc/response-route-handler.ts +88 -188
  268. package/src/rsc/rsc-rendering.ts +98 -76
  269. package/src/rsc/runtime-warnings.ts +23 -10
  270. package/src/rsc/server-action.ts +281 -117
  271. package/src/rsc/ssr-setup.ts +16 -0
  272. package/src/rsc/transition-gate.ts +89 -0
  273. package/src/rsc/types.ts +23 -5
  274. package/src/runtime-env.ts +18 -0
  275. package/src/search-params.ts +35 -30
  276. package/src/segment-loader-promise.ts +31 -4
  277. package/src/segment-system.tsx +254 -143
  278. package/src/serialize.ts +243 -0
  279. package/src/server/context.ts +163 -51
  280. package/src/server/cookie-parse.ts +32 -0
  281. package/src/server/cookie-store.ts +80 -5
  282. package/src/server/handle-store.ts +21 -38
  283. package/src/server/loader-registry.ts +33 -42
  284. package/src/server/request-context.ts +287 -178
  285. package/src/ssr/index.tsx +21 -16
  286. package/src/static-handler.ts +10 -13
  287. package/src/testing/cache-status.ts +162 -0
  288. package/src/testing/collect-handle.ts +40 -0
  289. package/src/testing/dispatch.ts +701 -0
  290. package/src/testing/dom.entry.ts +22 -0
  291. package/src/testing/e2e/fixture.ts +188 -0
  292. package/src/testing/e2e/index.ts +128 -0
  293. package/src/testing/e2e/matchers.ts +35 -0
  294. package/src/testing/e2e/page-helpers.ts +272 -0
  295. package/src/testing/e2e/parity.ts +387 -0
  296. package/src/testing/e2e/server.ts +195 -0
  297. package/src/testing/flight-matchers.ts +97 -0
  298. package/src/testing/flight-normalize.ts +11 -0
  299. package/src/testing/flight-runtime.d.ts +57 -0
  300. package/src/testing/flight-tree.ts +682 -0
  301. package/src/testing/flight.entry.ts +52 -0
  302. package/src/testing/flight.ts +257 -0
  303. package/src/testing/generated-routes.ts +183 -0
  304. package/src/testing/index.ts +105 -0
  305. package/src/testing/internal/context.ts +371 -0
  306. package/src/testing/internal/flight-client-globals.ts +30 -0
  307. package/src/testing/internal/seed-vars.ts +54 -0
  308. package/src/testing/render-handler.ts +357 -0
  309. package/src/testing/render-route.tsx +581 -0
  310. package/src/testing/run-loader.ts +385 -0
  311. package/src/testing/run-middleware.ts +205 -0
  312. package/src/testing/run-transition-when.ts +164 -0
  313. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  314. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  315. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  316. package/src/testing/vitest-stubs/version.ts +5 -0
  317. package/src/testing/vitest.ts +305 -0
  318. package/src/theme/ThemeProvider.tsx +20 -58
  319. package/src/theme/ThemeScript.tsx +7 -9
  320. package/src/theme/constants.ts +52 -13
  321. package/src/theme/index.ts +0 -7
  322. package/src/theme/theme-context.ts +1 -5
  323. package/src/theme/theme-script.ts +22 -21
  324. package/src/theme/use-theme.ts +0 -3
  325. package/src/types/boundaries.ts +0 -35
  326. package/src/types/cache-types.ts +13 -4
  327. package/src/types/error-types.ts +30 -90
  328. package/src/types/global-namespace.ts +54 -41
  329. package/src/types/handler-context.ts +110 -62
  330. package/src/types/index.ts +3 -10
  331. package/src/types/loader-types.ts +11 -9
  332. package/src/types/request-scope.ts +112 -0
  333. package/src/types/route-config.ts +6 -50
  334. package/src/types/route-entry.ts +0 -6
  335. package/src/types/segments.ts +135 -14
  336. package/src/urls/include-helper.ts +9 -56
  337. package/src/urls/index.ts +1 -11
  338. package/src/urls/path-helper-types.ts +29 -12
  339. package/src/urls/path-helper.ts +17 -106
  340. package/src/urls/pattern-types.ts +36 -19
  341. package/src/urls/response-types.ts +22 -29
  342. package/src/urls/type-extraction.ts +58 -139
  343. package/src/urls/urls-function.ts +1 -19
  344. package/src/use-loader.tsx +292 -107
  345. package/src/vite/debug.ts +185 -0
  346. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  347. package/src/vite/discovery/discover-routers.ts +126 -85
  348. package/src/vite/discovery/discovery-errors.ts +194 -0
  349. package/src/vite/discovery/gate-state.ts +171 -0
  350. package/src/vite/discovery/prerender-collection.ts +96 -68
  351. package/src/vite/discovery/route-types-writer.ts +40 -84
  352. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  353. package/src/vite/discovery/state.ts +44 -0
  354. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  355. package/src/vite/index.ts +2 -0
  356. package/src/vite/inject-client-debug.ts +36 -0
  357. package/src/vite/plugin-types.ts +126 -8
  358. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  359. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  360. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  361. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  362. package/src/vite/plugins/expose-action-id.ts +48 -95
  363. package/src/vite/plugins/expose-id-utils.ts +88 -55
  364. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  365. package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
  366. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  367. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  368. package/src/vite/plugins/expose-internal-ids.ts +505 -486
  369. package/src/vite/plugins/performance-tracks.ts +26 -25
  370. package/src/vite/plugins/refresh-cmd.ts +1 -1
  371. package/src/vite/plugins/use-cache-transform.ts +73 -83
  372. package/src/vite/plugins/version-injector.ts +40 -29
  373. package/src/vite/plugins/version-plugin.ts +37 -40
  374. package/src/vite/plugins/virtual-entries.ts +39 -25
  375. package/src/vite/rango.ts +109 -118
  376. package/src/vite/router-discovery.ts +718 -119
  377. package/src/vite/utils/ast-handler-extract.ts +26 -35
  378. package/src/vite/utils/banner.ts +1 -1
  379. package/src/vite/utils/bundle-analysis.ts +10 -15
  380. package/src/vite/utils/client-chunks.ts +184 -0
  381. package/src/vite/utils/directive-prologue.ts +40 -0
  382. package/src/vite/utils/forward-user-plugins.ts +171 -0
  383. package/src/vite/utils/manifest-utils.ts +4 -59
  384. package/src/vite/utils/package-resolution.ts +20 -52
  385. package/src/vite/utils/prerender-utils.ts +54 -39
  386. package/src/vite/utils/shared-utils.ts +90 -41
  387. package/src/browser/action-response-classifier.ts +0 -99
  388. package/src/browser/react/use-client-cache.ts +0 -58
  389. package/src/browser/shallow.ts +0 -40
  390. package/src/handles/index.ts +0 -7
  391. package/src/network-error-thrower.tsx +0 -23
  392. package/src/router/middleware-cookies.ts +0 -55
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Wire-type serialization transforms.
3
+ *
4
+ * The type a handler or loader returns on the server is frequently NOT the type
5
+ * a client receives after serialization. These transforms model that boundary so
6
+ * consumer-facing types (e.g. `Rango.PathResponse`) describe the wire value, not
7
+ * the source value.
8
+ *
9
+ * Two serializers, two transforms — they are intentionally NOT interchangeable:
10
+ *
11
+ * - `JsonSerialize<T>` models plain `JSON.stringify` (`path.json()` /
12
+ * `fetch().then(r => r.json())`). Lossy: `Date -> string`, `undefined` /
13
+ * functions / symbols dropped, `Map`/`Set` -> `{}`. `bigint` *throws* (no wire
14
+ * value), so it collapses the whole result to `never`. Honors `toJSON()`.
15
+ * - `FlightSerialize<T>` models React RSC Flight (loaders, RSC props, cache).
16
+ * High fidelity: `Date`/`Map`/`Set`/`bigint`/typed arrays/`Promise` are
17
+ * preserved; ordinary functions and non-global symbols do not cross.
18
+ *
19
+ * ## Overriding (full-transform replacement)
20
+ *
21
+ * Because `Rango.JsonSerialize` / `Rango.FlightSerialize` are type *aliases*, TS
22
+ * cannot let you redefine them directly (aliases don't merge). Instead each alias
23
+ * consults a generic override slot — augment it with a single member that is your
24
+ * complete transform. Delegate to the built-in for the cases you don't change:
25
+ *
26
+ * ```ts
27
+ * declare global {
28
+ * namespace Rango {
29
+ * interface FlightSerializeOverride<T> {
30
+ * app: T extends Money ? number : Rango.FlightSerializeBuiltin<T>;
31
+ * }
32
+ * }
33
+ * }
34
+ * // now Rango.FlightSerialize<Money> is number; everything else is the built-in.
35
+ * ```
36
+ *
37
+ * Provide exactly one member: the slot is read as `Override<T>[keyof Override<T>]`,
38
+ * so multiple members union (and conflict). The built-in recurses through the
39
+ * override-aware alias, so an override applies at every nesting level.
40
+ */
41
+
42
+ import type { ReactNode } from "react";
43
+
44
+ type JsonPrimitive = string | number | boolean | null;
45
+
46
+ type AnyFunction = (...args: never[]) => unknown;
47
+
48
+ // --- JSON ---------------------------------------------------------------------
49
+
50
+ /**
51
+ * Internal marker for a value that makes `JSON.stringify` throw (`bigint`, or a
52
+ * `toJSON()` returning one). Distinct from `never`, which means "omitted":
53
+ * `undefined`/function/symbol-valued keys are dropped, and such array slots
54
+ * become `null`. A throwing value has no valid JSON wire form, so it propagates
55
+ * up through every container and is excluded at the public boundary (`bigint`
56
+ * alone -> `never`; `{ id: bigint }` -> `never`).
57
+ */
58
+ declare const JSON_THROWS: unique symbol;
59
+ type JsonThrows = typeof JSON_THROWS;
60
+
61
+ /** True if union `U` contains the throw marker. */
62
+ type HasThrow<U> = [Extract<U, JsonThrows>] extends [never] ? false : true;
63
+
64
+ /** Map a JSON array/tuple: propagate a throw; else omitted elements become null. */
65
+ type JsonSerializeArray<T extends readonly unknown[]> =
66
+ HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[number]> extends true
67
+ ? JsonThrows
68
+ : {
69
+ [K in keyof T]: [JsonRawResolve<T[K]>] extends [never]
70
+ ? null
71
+ : JsonRawResolve<T[K]>;
72
+ };
73
+
74
+ /** Map a JSON object: propagate a throw; else drop omitted keys. */
75
+ type JsonSerializeObject<T> =
76
+ HasThrow<{ [K in keyof T]: JsonRawResolve<T[K]> }[keyof T]> extends true
77
+ ? JsonThrows
78
+ : {
79
+ [K in keyof T as [JsonRawResolve<T[K]>] extends [never]
80
+ ? never
81
+ : K]: JsonRawResolve<T[K]>;
82
+ };
83
+
84
+ /**
85
+ * Built-in JSON rules, *raw* (may yield the throw marker). Honors `toJSON()` (so
86
+ * `Date -> string` and any class with `toJSON()` serialize correctly), preserves
87
+ * JSON primitives and literals, omits functions / symbols / `undefined`,
88
+ * collapses `Map`/`Set` to `{}`, and marks `bigint` as throwing. Recurses through
89
+ * the override-aware resolver, so registered overrides apply at every level.
90
+ */
91
+ type JsonSerializeBuiltinRaw<T> = T extends {
92
+ toJSON(...args: never[]): infer R;
93
+ }
94
+ ? JsonRawResolve<R>
95
+ : T extends JsonPrimitive
96
+ ? T
97
+ : T extends bigint
98
+ ? JsonThrows
99
+ : T extends AnyFunction
100
+ ? never
101
+ : T extends symbol
102
+ ? never
103
+ : T extends undefined
104
+ ? never
105
+ : T extends readonly unknown[]
106
+ ? JsonSerializeArray<T>
107
+ : T extends ReadonlyMap<unknown, unknown>
108
+ ? {}
109
+ : T extends ReadonlySet<unknown>
110
+ ? {}
111
+ : T extends object
112
+ ? JsonSerializeObject<T>
113
+ : never;
114
+
115
+ /** Override-aware raw JSON resolution (the recursion entry). */
116
+ type JsonRawResolve<T> = [keyof Rango.JsonSerializeOverride<T>] extends [never]
117
+ ? JsonSerializeBuiltinRaw<T>
118
+ : Rango.JsonSerializeOverride<T>[keyof Rango.JsonSerializeOverride<T>];
119
+
120
+ /**
121
+ * Model the result of round-tripping a value through `JSON.stringify` /
122
+ * `JSON.parse`. A registered `Rango.JsonSerializeOverride` replaces the transform
123
+ * wholesale; otherwise the built-in rules apply. Throwing values collapse to
124
+ * `never`.
125
+ */
126
+ export type JsonSerialize<T> = Exclude<JsonRawResolve<T>, JsonThrows>;
127
+
128
+ // --- Flight -------------------------------------------------------------------
129
+
130
+ /**
131
+ * Built-in Flight rules. Mirrors React's `ReactClientValue` contract: primitives
132
+ * including `bigint`, `undefined`, `null`, symbols, `Date`, `ArrayBuffer` and
133
+ * typed-array views, `Map`, `Set`, `FormData`, `Blob`, `Promise`,
134
+ * `ReadableStream`, and (async) iterables are preserved; ordinary functions
135
+ * resolve to `never`. JSX (`ReactNode`, and the async-node union
136
+ * `ReactNode | Promise<ReactNode>`) is preserved as-is via a non-distributive
137
+ * leaf, so handle/loader returns that carry JSX round-trip unchanged. Recurses
138
+ * through the override-aware `FlightSerialize`.
139
+ *
140
+ * The source of truth is React's own contract, which is intentionally NOT
141
+ * semver-stable across RSC framework APIs — this tracks the React version Rango
142
+ * pins. See:
143
+ * https://react.dev/reference/rsc/use-client#serializable-types-returned-by-server-components
144
+ *
145
+ * Type-level limitations (not detectable structurally, so not modeled): class
146
+ * instances and null-prototype objects are rejected by React at runtime but pass
147
+ * here as their structural shape; non-global symbols are rejected at runtime but
148
+ * `symbol` is preserved here; Server Functions would need an override to be
149
+ * distinguished from ordinary functions (which resolve to `never`).
150
+ */
151
+ type FlightSerializeBuiltinRaw<T> = [T] extends [ReactNode | Promise<ReactNode>]
152
+ ? T
153
+ : T extends string | number | boolean | bigint | symbol | null | undefined
154
+ ? T
155
+ : T extends AnyFunction
156
+ ? never
157
+ : T extends Date
158
+ ? Date
159
+ : T extends ArrayBuffer
160
+ ? ArrayBuffer
161
+ : T extends ArrayBufferView
162
+ ? T
163
+ : T extends FormData
164
+ ? FormData
165
+ : T extends Blob
166
+ ? Blob
167
+ : T extends Map<infer K, infer V>
168
+ ? Map<FlightSerialize<K>, FlightSerialize<V>>
169
+ : T extends Set<infer V>
170
+ ? Set<FlightSerialize<V>>
171
+ : T extends Promise<infer V>
172
+ ? Promise<FlightSerialize<V>>
173
+ : T extends ReadableStream<infer V>
174
+ ? ReadableStream<FlightSerialize<V>>
175
+ : T extends readonly unknown[]
176
+ ? { [K in keyof T]: FlightSerialize<T[K]> }
177
+ : T extends AsyncIterable<infer V>
178
+ ? AsyncIterable<FlightSerialize<V>>
179
+ : T extends Iterable<infer V>
180
+ ? Iterable<FlightSerialize<V>>
181
+ : T extends object
182
+ ? { [K in keyof T]: FlightSerialize<T[K]> }
183
+ : never;
184
+
185
+ /**
186
+ * Model React RSC Flight serialization. A registered `Rango.FlightSerializeOverride`
187
+ * replaces the transform wholesale; otherwise the built-in rules apply.
188
+ */
189
+ export type FlightSerialize<T> = [
190
+ keyof Rango.FlightSerializeOverride<T>,
191
+ ] extends [never]
192
+ ? FlightSerializeBuiltinRaw<T>
193
+ : Rango.FlightSerializeOverride<T>[keyof Rango.FlightSerializeOverride<T>];
194
+
195
+ // Module-scoped aliases so the ambient `Rango.*` members below can reference the
196
+ // module-level transforms without the global namespace shadowing the names.
197
+ type GlobalJsonSerialize<T> = JsonSerialize<T>;
198
+ type GlobalJsonSerializeBuiltin<T> = JsonSerializeBuiltinRaw<T>;
199
+ type GlobalFlightSerialize<T> = FlightSerialize<T>;
200
+ type GlobalFlightSerializeBuiltin<T> = FlightSerializeBuiltinRaw<T>;
201
+
202
+ /**
203
+ * Ambient serialization transforms and their override slots on the `Rango`
204
+ * namespace. Available with no import wherever the router's types are in scope,
205
+ * alongside `Rango.Path` / `Rango.PathResponse`.
206
+ *
207
+ * `Rango.JsonSerialize` is what `Rango.PathResponse` applies; `Rango.FlightSerialize`
208
+ * is exposed for RSC/loader/cache wire types and must NOT be used for `path.json()`.
209
+ * `Rango.JsonSerializeBuiltin` / `Rango.FlightSerializeBuiltin` are the defaults,
210
+ * exported so an override can delegate to them.
211
+ */
212
+ declare global {
213
+ namespace Rango {
214
+ /**
215
+ * Full-transform override slot for `Rango.JsonSerialize`. Empty by default;
216
+ * augment with one member that is your complete transform (delegate to
217
+ * `Rango.JsonSerializeBuiltin<T>` for the cases you don't change).
218
+ */
219
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
220
+ interface JsonSerializeOverride<T> {}
221
+
222
+ /**
223
+ * Full-transform override slot for `Rango.FlightSerialize`. Empty by default;
224
+ * augment with one member that is your complete transform (delegate to
225
+ * `Rango.FlightSerializeBuiltin<T>` for the cases you don't change).
226
+ */
227
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
228
+ interface FlightSerializeOverride<T> {}
229
+
230
+ /** Wire type after `JSON.stringify` (`path.json()` / `fetch().json()`). */
231
+ type JsonSerialize<T> = GlobalJsonSerialize<T>;
232
+ /**
233
+ * Built-in `JsonSerialize` rules, for an override to delegate to. Raw: a
234
+ * `bigint`-bearing type yields the internal throw marker here, which
235
+ * `Rango.JsonSerialize` excludes to `never` at the boundary.
236
+ */
237
+ type JsonSerializeBuiltin<T> = GlobalJsonSerializeBuiltin<T>;
238
+ /** Wire type after RSC Flight serialization (loaders / RSC props / cache). */
239
+ type FlightSerialize<T> = GlobalFlightSerialize<T>;
240
+ /** Built-in `FlightSerialize` rules, for an override to delegate to. */
241
+ type FlightSerializeBuiltin<T> = GlobalFlightSerializeBuiltin<T>;
242
+ }
243
+ }
@@ -10,7 +10,7 @@ import type {
10
10
  ShouldRevalidateFn,
11
11
  TransitionConfig,
12
12
  } from "../types";
13
- import { invariant } from "../errors";
13
+ import { invariant, DslContextError } from "../errors";
14
14
  import type { DefaultRouteName } from "../types/global-namespace.js";
15
15
 
16
16
  // ============================================================================
@@ -40,7 +40,7 @@ export interface MetricsStore {
40
40
  metrics: PerformanceMetric[];
41
41
  }
42
42
  // ============================================================================
43
- // RSC Router Context
43
+ // Rango Context
44
44
  // ============================================================================
45
45
 
46
46
  /**
@@ -71,6 +71,10 @@ export type EntryPropCommon = {
71
71
  };
72
72
 
73
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
+ *
74
78
  * @internal This type is an implementation detail and may change without notice.
75
79
  */
76
80
  export type EntryPropDatas = {
@@ -80,6 +84,16 @@ export type EntryPropDatas = {
80
84
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
81
85
  };
82
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
+
83
97
  /**
84
98
  * Loader entry stored in EntryData
85
99
  * Contains the loader definition and its revalidation rules
@@ -136,6 +150,19 @@ export type InterceptWhenFn<TEnv = any> = (
136
150
  ctx: InterceptSelectorContext<TEnv>,
137
151
  ) => boolean;
138
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
+ }
165
+
139
166
  /**
140
167
  * Intercept entry stored in EntryData
141
168
  * Contains the slot name, route to intercept, and handler
@@ -158,11 +185,9 @@ export type InterceptEntry = {
158
185
  };
159
186
 
160
187
  export interface ParallelEntryData
161
- extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
188
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
162
189
  type: "parallel";
163
190
  handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
164
- loading?: ReactNode | false;
165
- transition?: TransitionConfig;
166
191
  /** Set when any parallel slot is a Static definition */
167
192
  isStaticPrerender?: true;
168
193
  /** Per-slot static handler $$ids for build-time store lookup */
@@ -171,6 +196,13 @@ export interface ParallelEntryData
171
196
 
172
197
  export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
173
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
+ */
174
206
  export type EntryPropSegments = {
175
207
  loader: LoaderEntry[];
176
208
  layout: EntryData[];
@@ -182,8 +214,6 @@ export type EntryData =
182
214
  | ({
183
215
  type: "route";
184
216
  handler: Handler<any, any, any>;
185
- loading?: ReactNode | false;
186
- transition?: TransitionConfig;
187
217
  /** URL pattern for this route (used by path() in urls()) */
188
218
  pattern?: string;
189
219
  /** Set when handler is a Prerender definition */
@@ -205,29 +235,28 @@ export type EntryData =
205
235
  responseType?: string;
206
236
  } & EntryPropCommon &
207
237
  EntryPropDatas &
208
- EntryPropSegments)
238
+ EntryPropSegments &
239
+ EntryPropRender)
209
240
  | ({
210
241
  type: "layout";
211
242
  handler: ReactNode | Handler<any, any, any>;
212
- loading?: ReactNode | false;
213
- transition?: TransitionConfig;
214
243
  /** Set when handler is a Static definition (build-time only) */
215
244
  isStaticPrerender?: true;
216
245
  /** Static handler $$id for build-time store lookup */
217
246
  staticHandlerId?: string;
218
247
  } & EntryPropCommon &
219
248
  EntryPropDatas &
220
- EntryPropSegments)
249
+ EntryPropSegments &
250
+ EntryPropRender)
221
251
  | ParallelEntryData
222
252
  | ({
223
253
  type: "cache";
224
254
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
225
255
  handler: ReactNode | Handler<any, any, any>;
226
- loading?: ReactNode | false;
227
- transition?: TransitionConfig;
228
256
  } & EntryPropCommon &
229
257
  EntryPropDatas &
230
- EntryPropSegments);
258
+ EntryPropSegments &
259
+ EntryPropRender);
231
260
 
232
261
  /**
233
262
  * Tracked include info for build-time manifest generation
@@ -303,10 +332,28 @@ interface HelperContext {
303
332
  // hold references to the old instance — causing getStore() to return
304
333
  // undefined even inside a run() callback.
305
334
  const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
306
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
335
+ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
307
336
  globalThis as any
308
337
  )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
309
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]++;
355
+ }
356
+
310
357
  export const getContext = (): {
311
358
  context: AsyncLocalStorage<HelperContext>;
312
359
  getStore: () => HelperContext;
@@ -330,12 +377,12 @@ export const getContext = (): {
330
377
  callback: (...args: any[]) => T,
331
378
  ) => T;
332
379
  } => {
333
- const context = RSCRouterContext;
380
+ const context = RangoContext;
334
381
 
335
382
  return {
336
383
  context,
337
384
  getOrCreateStore: (forRoute?: string): HelperContext => {
338
- let store = RSCRouterContext.getStore();
385
+ let store = RangoContext.getStore();
339
386
  if (!store) {
340
387
  store = {
341
388
  manifest: new Map<string, EntryData>(),
@@ -355,7 +402,7 @@ export const getContext = (): {
355
402
  const store = context.getStore();
356
403
  if (!store) {
357
404
  throw new Error(
358
- "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.",
359
406
  );
360
407
  }
361
408
  return store;
@@ -372,29 +419,17 @@ export const getContext = (): {
372
419
  type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
373
420
  ) => {
374
421
  const store = context.getStore();
375
- invariant(store, "No context RSCRouterContext available");
376
- store.counters[type] ??= 0;
377
- const index = store.counters[type];
378
- store.counters[type] = index + 1;
379
- return `$${type}.${index}`;
422
+ invariant(store, "No context RangoContext available");
423
+ return `$${type}.${bumpCounter(store, type)}`;
380
424
  },
381
425
  getShortCode: (
382
426
  type: "layout" | "parallel" | "route" | "loader" | "cache",
383
427
  ) => {
384
428
  const store = context.getStore();
385
- invariant(store, "No context RSCRouterContext available");
429
+ invariant(store, "No context RangoContext available");
386
430
 
387
431
  const parent = store.parent;
388
- const prefix =
389
- type === "layout"
390
- ? "L"
391
- : type === "parallel"
392
- ? "P"
393
- : type === "loader"
394
- ? "D"
395
- : type === "cache"
396
- ? "C"
397
- : "R";
432
+ const prefix = SHORT_CODE_PREFIX[type];
398
433
  const mountPrefix =
399
434
  store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
400
435
 
@@ -405,10 +440,7 @@ export const getContext = (): {
405
440
  const counterKey = mountPrefix
406
441
  ? `${mountPrefix}_root_${type}`
407
442
  : `root_${type}`;
408
- store.counters[counterKey] ??= 0;
409
- const index = store.counters[counterKey];
410
- store.counters[counterKey] = index + 1;
411
- return `${mountPrefix}${prefix}${index}`;
443
+ return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
412
444
  } else {
413
445
  // Child entry: use parent-scoped counter with includeScope appended.
414
446
  // When we're evaluating a lazy include's direct children, includeScope
@@ -416,10 +448,7 @@ export const getContext = (): {
416
448
  // parent's counter namespace so routes inside one include cannot
417
449
  // collide with siblings declared outside it.
418
450
  const counterKey = `${parent.shortCode}${includeScope}_${type}`;
419
- store.counters[counterKey] ??= 0;
420
- const index = store.counters[counterKey];
421
- store.counters[counterKey] = index + 1;
422
- return `${parent.shortCode}${includeScope}${prefix}${index}`;
451
+ return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
423
452
  }
424
453
  },
425
454
  runWithStore: <T>(
@@ -493,6 +522,31 @@ export const getContext = (): {
493
522
  };
494
523
  };
495
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
+
496
550
  /**
497
551
  * Run a callback with specific URL and name prefixes
498
552
  * Used by include() to apply prefixes to nested patterns
@@ -502,7 +556,7 @@ export function runWithPrefixes<T>(
502
556
  namePrefix: string | undefined,
503
557
  callback: () => T,
504
558
  ): T {
505
- const store = RSCRouterContext.getStore();
559
+ const store = RangoContext.getStore();
506
560
  if (!store) {
507
561
  throw new Error("runWithPrefixes must be called within router context");
508
562
  }
@@ -547,7 +601,7 @@ export function runWithPrefixes<T>(
547
601
  ? (store.rootScoped ?? false)
548
602
  : store.rootScoped;
549
603
 
550
- return RSCRouterContext.run(
604
+ return RangoContext.run(
551
605
  {
552
606
  ...store,
553
607
  urlPrefix: combinedUrlPrefix,
@@ -562,7 +616,7 @@ export function runWithPrefixes<T>(
562
616
  * Get current URL prefix from context
563
617
  */
564
618
  export function getUrlPrefix(): string {
565
- const store = RSCRouterContext.getStore();
619
+ const store = RangoContext.getStore();
566
620
  return store?.urlPrefix || "";
567
621
  }
568
622
 
@@ -570,7 +624,7 @@ export function getUrlPrefix(): string {
570
624
  * Get current name prefix from context
571
625
  */
572
626
  export function getNamePrefix(): string | undefined {
573
- const store = RSCRouterContext.getStore();
627
+ const store = RangoContext.getStore();
574
628
  return store?.namePrefix;
575
629
  }
576
630
 
@@ -579,7 +633,7 @@ export function getNamePrefix(): string | undefined {
579
633
  * Returns true at root or inside { name: "" } includes, false inside named includes.
580
634
  */
581
635
  export function getRootScoped(): boolean {
582
- const store = RSCRouterContext.getStore();
636
+ const store = RangoContext.getStore();
583
637
  return store?.rootScoped ?? true;
584
638
  }
585
639
 
@@ -676,7 +730,7 @@ export function getParallelSlotCount(
676
730
  * ```
677
731
  */
678
732
  export function track(label: string, depth?: number): () => void {
679
- const store = RSCRouterContext.getStore();
733
+ const store = RangoContext.getStore();
680
734
 
681
735
  // No-op if context unavailable or metrics not enabled
682
736
  if (!store?.metrics?.enabled) {
@@ -699,25 +753,40 @@ export function track(label: string, depth?: number): () => void {
699
753
 
700
754
  /**
701
755
  * Separate ALS for tracking loader execution scope.
702
- * Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
703
- * nested RSCRouterContext.run() calls in Vite's module runner.
756
+ * Uses a dedicated ALS (not RangoContext) to avoid issues with
757
+ * nested RangoContext.run() calls in Vite's module runner.
704
758
  */
705
759
  const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
706
760
  const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
707
761
  globalThis as any
708
762
  )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
709
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
+
710
775
  /**
711
776
  * Check if the current execution is inside a cache() DSL boundary.
712
777
  * Returns false inside loader execution — loaders are always fresh
713
778
  * (never cached), so non-cacheable reads are safe.
714
779
  */
715
780
  export function isInsideCacheScope(): boolean {
716
- if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
781
+ if (RangoContext.getStore()?.insideCacheScope !== true) return false;
717
782
  // Loaders are always fresh — even inside a cache() boundary, the loader
718
783
  // function re-executes on every request. Skip the guard when running
719
784
  // inside a loader.
720
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;
721
790
  return true;
722
791
  }
723
792
 
@@ -738,3 +807,46 @@ export function isInsideLoaderScope(): boolean {
738
807
  export function runInsideLoaderScope<T>(fn: () => T): T {
739
808
  return loaderScopeALS.run({ active: true }, fn);
740
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
+ }