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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +441 -134
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3453 -1240
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +76 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +64 -2
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +318 -15
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +250 -30
  19. package/skills/host-router/SKILL.md +83 -23
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +279 -53
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +153 -109
  32. package/skills/rango/SKILL.md +251 -22
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +101 -5
  36. package/skills/router-setup/SKILL.md +116 -8
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +129 -0
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +332 -29
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +1 -66
  62. package/src/browser/action-coordinator.ts +53 -36
  63. package/src/browser/action-fence.ts +47 -0
  64. package/src/browser/app-shell.ts +39 -0
  65. package/src/browser/app-version.ts +14 -0
  66. package/src/browser/connection-warmup.ts +134 -0
  67. package/src/browser/cookie-name.ts +140 -0
  68. package/src/browser/event-controller.ts +197 -150
  69. package/src/browser/history-state.ts +21 -0
  70. package/src/browser/index.ts +3 -3
  71. package/src/browser/invalidate-client-cache.ts +52 -0
  72. package/src/browser/navigation-bridge.ts +111 -31
  73. package/src/browser/navigation-client.ts +201 -67
  74. package/src/browser/navigation-store-handle.ts +38 -0
  75. package/src/browser/navigation-store.ts +76 -67
  76. package/src/browser/navigation-transaction.ts +18 -66
  77. package/src/browser/network-error-handler.ts +34 -7
  78. package/src/browser/partial-update.ts +187 -112
  79. package/src/browser/prefetch/cache.ts +230 -35
  80. package/src/browser/prefetch/fetch.ts +338 -39
  81. package/src/browser/prefetch/queue.ts +126 -20
  82. package/src/browser/prefetch/resource-ready.ts +77 -0
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +111 -16
  85. package/src/browser/react/NavigationProvider.tsx +135 -120
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/filter-segment-order.ts +66 -7
  89. package/src/browser/react/index.ts +0 -48
  90. package/src/browser/react/location-state-shared.ts +178 -8
  91. package/src/browser/react/location-state.ts +39 -14
  92. package/src/browser/react/use-action.ts +6 -15
  93. package/src/browser/react/use-handle.ts +23 -69
  94. package/src/browser/react/use-href.tsx +8 -1
  95. package/src/browser/react/use-link-status.ts +33 -8
  96. package/src/browser/react/use-navigation.ts +32 -7
  97. package/src/browser/react/use-params.ts +20 -10
  98. package/src/browser/react/use-reverse.ts +106 -0
  99. package/src/browser/react/use-router.ts +46 -11
  100. package/src/browser/react/use-search-params.ts +0 -5
  101. package/src/browser/react/use-segments.ts +11 -21
  102. package/src/browser/response-adapter.ts +80 -5
  103. package/src/browser/rsc-router.tsx +218 -76
  104. package/src/browser/scroll-restoration.ts +54 -42
  105. package/src/browser/segment-reconciler.ts +36 -9
  106. package/src/browser/segment-structure-assert.ts +2 -2
  107. package/src/browser/server-action-bridge.ts +222 -61
  108. package/src/browser/types.ts +91 -11
  109. package/src/browser/validate-redirect-origin.ts +43 -16
  110. package/src/build/collect-fallback-refs.ts +107 -0
  111. package/src/build/generate-manifest.ts +65 -40
  112. package/src/build/generate-route-types.ts +5 -1
  113. package/src/build/index.ts +8 -2
  114. package/src/build/prefix-tree-utils.ts +123 -0
  115. package/src/build/route-trie.ts +165 -36
  116. package/src/build/route-types/ast-route-extraction.ts +15 -8
  117. package/src/build/route-types/codegen.ts +16 -5
  118. package/src/build/route-types/include-resolution.ts +125 -24
  119. package/src/build/route-types/param-extraction.ts +6 -3
  120. package/src/build/route-types/per-module-writer.ts +22 -6
  121. package/src/build/route-types/router-processing.ts +272 -96
  122. package/src/build/route-types/scan-filter.ts +9 -2
  123. package/src/build/route-types/source-scan.ts +216 -0
  124. package/src/build/runtime-discovery.ts +9 -20
  125. package/src/cache/cache-error.ts +104 -0
  126. package/src/cache/cache-key-utils.ts +29 -13
  127. package/src/cache/cache-policy.ts +108 -34
  128. package/src/cache/cache-runtime.ts +214 -48
  129. package/src/cache/cache-scope.ts +236 -89
  130. package/src/cache/cache-tag.ts +103 -0
  131. package/src/cache/cf/cf-base64.ts +33 -0
  132. package/src/cache/cf/cf-cache-constants.ts +127 -0
  133. package/src/cache/cf/cf-cache-store.ts +2224 -171
  134. package/src/cache/cf/cf-cache-types.ts +349 -0
  135. package/src/cache/cf/cf-kv-utils.ts +46 -0
  136. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  137. package/src/cache/cf/index.ts +11 -17
  138. package/src/cache/document-cache.ts +89 -27
  139. package/src/cache/handle-snapshot.ts +70 -0
  140. package/src/cache/index.ts +11 -20
  141. package/src/cache/memory-segment-store.ts +136 -37
  142. package/src/cache/profile-registry.ts +31 -31
  143. package/src/cache/read-through-swr.ts +41 -11
  144. package/src/cache/segment-codec.ts +9 -17
  145. package/src/cache/tag-invalidation.ts +230 -0
  146. package/src/cache/taint.ts +55 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +84 -2
  155. package/src/debug.ts +2 -2
  156. package/src/decode-loader-results.ts +52 -0
  157. package/src/defer.ts +196 -0
  158. package/src/deps/ssr.ts +0 -1
  159. package/src/encode-kv.ts +49 -0
  160. package/src/errors.ts +30 -4
  161. package/src/escape-script.ts +52 -0
  162. package/src/handle.ts +70 -22
  163. package/src/handles/MetaTags.tsx +56 -19
  164. package/src/handles/Scripts.tsx +183 -0
  165. package/src/handles/breadcrumbs.ts +37 -8
  166. package/src/handles/is-thenable.ts +19 -0
  167. package/src/handles/meta.ts +51 -40
  168. package/src/handles/script.ts +244 -0
  169. package/src/host/cookie-handler.ts +9 -60
  170. package/src/host/errors.ts +0 -24
  171. package/src/host/index.ts +8 -2
  172. package/src/host/pattern-matcher.ts +23 -52
  173. package/src/host/router.ts +107 -99
  174. package/src/host/testing.ts +40 -27
  175. package/src/host/types.ts +37 -4
  176. package/src/host/utils.ts +1 -1
  177. package/src/href-client.ts +137 -22
  178. package/src/index.rsc.ts +93 -12
  179. package/src/index.ts +133 -15
  180. package/src/internal-debug.ts +11 -10
  181. package/src/loader-store.ts +500 -0
  182. package/src/loader.rsc.ts +20 -13
  183. package/src/loader.ts +12 -11
  184. package/src/missing-id-error.ts +68 -0
  185. package/src/outlet-context.ts +1 -1
  186. package/src/outlet-provider.tsx +1 -5
  187. package/src/prerender/param-hash.ts +16 -16
  188. package/src/prerender/store.ts +37 -41
  189. package/src/prerender.ts +198 -82
  190. package/src/redirect-origin.ts +100 -0
  191. package/src/regex-escape.ts +8 -0
  192. package/src/render-error-thrower.tsx +20 -0
  193. package/src/response-utils.ts +62 -0
  194. package/src/reverse.ts +65 -15
  195. package/src/root-error-boundary.tsx +1 -19
  196. package/src/route-content-wrapper.tsx +7 -72
  197. package/src/route-definition/dsl-helpers.ts +469 -276
  198. package/src/route-definition/helper-factories.ts +29 -139
  199. package/src/route-definition/helpers-types.ts +113 -37
  200. package/src/route-definition/index.ts +3 -0
  201. package/src/route-definition/redirect.ts +53 -12
  202. package/src/route-definition/resolve-handler-use.ts +161 -0
  203. package/src/route-definition/use-item-types.ts +32 -0
  204. package/src/route-map-builder.ts +7 -17
  205. package/src/route-types.ts +37 -41
  206. package/src/router/basename.ts +14 -0
  207. package/src/router/content-negotiation.ts +164 -17
  208. package/src/router/error-handling.ts +45 -18
  209. package/src/router/find-match.ts +45 -22
  210. package/src/router/handler-context.ts +83 -39
  211. package/src/router/instrument.ts +350 -0
  212. package/src/router/intercept-resolution.ts +50 -24
  213. package/src/router/lazy-includes.ts +19 -53
  214. package/src/router/loader-resolution.ts +274 -56
  215. package/src/router/logging.ts +5 -8
  216. package/src/router/manifest.ts +49 -45
  217. package/src/router/match-api.ts +120 -204
  218. package/src/router/match-context.ts +0 -22
  219. package/src/router/match-handlers.ts +58 -58
  220. package/src/router/match-middleware/background-revalidation.ts +33 -6
  221. package/src/router/match-middleware/cache-lookup.ts +214 -263
  222. package/src/router/match-middleware/cache-store.ts +73 -33
  223. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  224. package/src/router/match-middleware/segment-resolution.ts +52 -18
  225. package/src/router/match-pipelines.ts +1 -42
  226. package/src/router/match-result.ts +104 -40
  227. package/src/router/metrics.ts +5 -34
  228. package/src/router/middleware-types.ts +13 -142
  229. package/src/router/middleware.ts +270 -172
  230. package/src/router/navigation-snapshot.ts +131 -0
  231. package/src/router/params-util.ts +23 -0
  232. package/src/router/pattern-matching.ts +132 -90
  233. package/src/router/prefetch-cache-ttl.ts +51 -0
  234. package/src/router/prerender-match.ts +195 -56
  235. package/src/router/preview-match.ts +32 -102
  236. package/src/router/request-classification.ts +276 -0
  237. package/src/router/revalidation.ts +123 -73
  238. package/src/router/route-snapshot.ts +244 -0
  239. package/src/router/router-context.ts +8 -28
  240. package/src/router/router-interfaces.ts +115 -35
  241. package/src/router/router-options.ts +172 -15
  242. package/src/router/router-registry.ts +2 -5
  243. package/src/router/segment-resolution/fresh.ts +264 -77
  244. package/src/router/segment-resolution/helpers.ts +115 -30
  245. package/src/router/segment-resolution/loader-cache.ts +63 -37
  246. package/src/router/segment-resolution/revalidation.ts +474 -385
  247. package/src/router/segment-resolution/static-store.ts +19 -5
  248. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  249. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  250. package/src/router/segment-resolution.ts +5 -1
  251. package/src/router/segment-wrappers.ts +8 -5
  252. package/src/router/state-cookie-name.ts +33 -0
  253. package/src/router/substitute-pattern-params.ts +56 -0
  254. package/src/router/telemetry-otel.ts +161 -199
  255. package/src/router/telemetry.ts +96 -19
  256. package/src/router/timeout.ts +0 -20
  257. package/src/router/tracing.ts +206 -0
  258. package/src/router/trie-matching.ts +163 -59
  259. package/src/router/types.ts +10 -63
  260. package/src/router/url-params.ts +44 -0
  261. package/src/router.ts +163 -55
  262. package/src/rsc/handler-context.ts +3 -2
  263. package/src/rsc/handler.ts +658 -511
  264. package/src/rsc/helpers.ts +168 -46
  265. package/src/rsc/index.ts +2 -5
  266. package/src/rsc/json-route-result.ts +38 -0
  267. package/src/rsc/loader-fetch.ts +127 -31
  268. package/src/rsc/manifest-init.ts +33 -42
  269. package/src/rsc/origin-guard.ts +39 -25
  270. package/src/rsc/progressive-enhancement.ts +77 -11
  271. package/src/rsc/redirect-guard.ts +99 -0
  272. package/src/rsc/response-cache-serve.ts +238 -0
  273. package/src/rsc/response-error.ts +79 -12
  274. package/src/rsc/response-route-handler.ts +99 -189
  275. package/src/rsc/rsc-rendering.ts +105 -72
  276. package/src/rsc/runtime-warnings.ts +23 -10
  277. package/src/rsc/server-action.ts +263 -112
  278. package/src/rsc/ssr-setup.ts +18 -2
  279. package/src/rsc/types.ts +32 -6
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +281 -129
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +309 -61
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +26 -46
  291. package/src/server/request-context.ts +398 -172
  292. package/src/ssr/index.tsx +25 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +99 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +343 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  320. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  321. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  322. package/src/testing/vitest-stubs/version.ts +5 -0
  323. package/src/testing/vitest.ts +305 -0
  324. package/src/theme/ThemeProvider.tsx +20 -58
  325. package/src/theme/ThemeScript.tsx +7 -9
  326. package/src/theme/constants.ts +52 -13
  327. package/src/theme/index.ts +0 -7
  328. package/src/theme/theme-context.ts +1 -5
  329. package/src/theme/theme-script.ts +22 -21
  330. package/src/theme/use-theme.ts +0 -3
  331. package/src/types/boundaries.ts +0 -35
  332. package/src/types/cache-types.ts +17 -8
  333. package/src/types/error-types.ts +30 -90
  334. package/src/types/global-namespace.ts +54 -41
  335. package/src/types/handler-context.ts +233 -81
  336. package/src/types/index.ts +1 -10
  337. package/src/types/loader-types.ts +44 -15
  338. package/src/types/request-scope.ts +112 -0
  339. package/src/types/route-config.ts +6 -50
  340. package/src/types/route-entry.ts +19 -7
  341. package/src/types/segments.ts +37 -14
  342. package/src/urls/include-helper.ts +33 -70
  343. package/src/urls/index.ts +1 -11
  344. package/src/urls/path-helper-types.ts +58 -11
  345. package/src/urls/path-helper.ts +57 -111
  346. package/src/urls/pattern-types.ts +48 -19
  347. package/src/urls/response-types.ts +25 -22
  348. package/src/urls/type-extraction.ts +58 -139
  349. package/src/urls/urls-function.ts +1 -18
  350. package/src/use-loader.tsx +346 -89
  351. package/src/vite/debug.ts +185 -0
  352. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  353. package/src/vite/discovery/discover-routers.ts +130 -85
  354. package/src/vite/discovery/discovery-errors.ts +194 -0
  355. package/src/vite/discovery/gate-state.ts +171 -0
  356. package/src/vite/discovery/prerender-collection.ts +214 -132
  357. package/src/vite/discovery/route-types-writer.ts +40 -84
  358. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  359. package/src/vite/discovery/state.ts +57 -6
  360. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  361. package/src/vite/index.ts +6 -0
  362. package/src/vite/inject-client-debug.ts +36 -0
  363. package/src/vite/plugin-types.ts +155 -65
  364. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  365. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  366. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  367. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  369. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  370. package/src/vite/plugins/expose-action-id.ts +49 -98
  371. package/src/vite/plugins/expose-id-utils.ts +96 -51
  372. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  373. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  374. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  375. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  376. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  377. package/src/vite/plugins/performance-tracks.ts +89 -0
  378. package/src/vite/plugins/refresh-cmd.ts +89 -27
  379. package/src/vite/plugins/use-cache-transform.ts +73 -83
  380. package/src/vite/plugins/version-injector.ts +21 -25
  381. package/src/vite/plugins/version-plugin.ts +46 -37
  382. package/src/vite/plugins/virtual-entries.ts +13 -18
  383. package/src/vite/rango.ts +238 -295
  384. package/src/vite/router-discovery.ts +940 -149
  385. package/src/vite/utils/ast-handler-extract.ts +26 -35
  386. package/src/vite/utils/banner.ts +4 -4
  387. package/src/vite/utils/bundle-analysis.ts +10 -15
  388. package/src/vite/utils/client-chunks.ts +184 -0
  389. package/src/vite/utils/directive-prologue.ts +40 -0
  390. package/src/vite/utils/forward-user-plugins.ts +171 -0
  391. package/src/vite/utils/manifest-utils.ts +4 -59
  392. package/src/vite/utils/package-resolution.ts +20 -52
  393. package/src/vite/utils/prerender-utils.ts +81 -34
  394. package/src/vite/utils/shared-utils.ts +92 -42
  395. package/src/browser/action-response-classifier.ts +0 -99
  396. package/src/browser/react/use-client-cache.ts +0 -58
  397. package/src/browser/shallow.ts +0 -40
  398. package/src/handles/index.ts +0 -7
  399. package/src/network-error-thrower.tsx +0 -23
  400. package/src/router/middleware-cookies.ts +0 -55
@@ -5,8 +5,9 @@
5
5
  */
6
6
 
7
7
  import type { ReactNode } from "react";
8
- import { track } from "../server/context";
9
8
  import type { EntryData } from "../server/context";
9
+ import { observePhase, PHASES } from "./instrument.js";
10
+ import { contextGet } from "../context-var.js";
10
11
  import type {
11
12
  ResolvedSegment,
12
13
  HandlerContext,
@@ -18,11 +19,17 @@ import type {
18
19
  ErrorBoundaryFallbackProps,
19
20
  ErrorInfo,
20
21
  } from "../types";
21
- import type { LoaderRevalidationResult, ActionContext } from "./types";
22
- import { isHandle, type Handle } from "../handle.js";
23
- import type { HandleStore } from "../server/handle-store.js";
22
+ import { isHandle, collectHandleData, type Handle } from "../handle.js";
23
+ import { withDefer } from "../defer.js";
24
+ import { buildHandleSnapshot } from "../server/handle-store.js";
24
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
25
26
  import { _getRequestContext } from "../server/request-context.js";
27
+ import {
28
+ isInsideLoaderScope,
29
+ runInsideLoaderBodyScope,
30
+ isInsidePushCallbackScope,
31
+ runInsidePushCallbackScope,
32
+ } from "../server/context.js";
26
33
  import { debugLog } from "./logging.js";
27
34
 
28
35
  /**
@@ -64,7 +71,9 @@ export function wrapLoaderWithErrorHandling<T>(
64
71
  ) => ErrorInfo,
65
72
  onError?: LoaderErrorCallback,
66
73
  ): Promise<LoaderDataResult<T>> {
67
- // Extract loader name from segmentId (format: "M1L0D0.loaderName")
74
+ // Extract the trailing token from segmentId (format: "<shortCode>D<i>.<loaderId>").
75
+ // The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
76
+ // not a clean display name.
68
77
  const loaderName = segmentId.split(".").pop() || "unknown";
69
78
 
70
79
  return Promise.resolve(promise)
@@ -100,16 +109,40 @@ export function wrapLoaderWithErrorHandling<T>(
100
109
  };
101
110
  }
102
111
 
103
- // Render fallback on server
112
+ // Render fallback on server. The user ErrorBoundaryHandler may throw
113
+ // synchronously; if it does we must NOT let that rejection escape — the
114
+ // wrapped LoaderDataResult promise is contracted to never reject (see
115
+ // segment-resolution/fresh.ts `await Promise.all(...wrapped)`), and a
116
+ // rejection here would collapse the whole entry and discard healthy
117
+ // sibling loader data. On a fallback-render throw, fall back to the
118
+ // no-boundary result (fallback: null) so the client throws the ORIGINAL
119
+ // error, and the wrapped promise still resolves to a LoaderDataResult.
104
120
  let renderedFallback: ReactNode;
105
- if (typeof fallback === "function") {
106
- // ErrorBoundaryHandler - call with error info
107
- const props: ErrorBoundaryFallbackProps = {
121
+ try {
122
+ if (typeof fallback === "function") {
123
+ // ErrorBoundaryHandler - call with error info
124
+ const props: ErrorBoundaryFallbackProps = {
125
+ error: errorInfo,
126
+ };
127
+ renderedFallback = fallback(props);
128
+ } else {
129
+ renderedFallback = fallback;
130
+ }
131
+ } catch (fallbackError) {
132
+ debugLog("loader", "error boundary fallback render threw", {
133
+ segmentId,
134
+ message: errorInfo.message,
135
+ fallbackError:
136
+ fallbackError instanceof Error
137
+ ? fallbackError.message
138
+ : String(fallbackError),
139
+ });
140
+ return {
141
+ __loaderResult: true,
142
+ ok: false,
108
143
  error: errorInfo,
144
+ fallback: null,
109
145
  };
110
- renderedFallback = fallback(props);
111
- } else {
112
- renderedFallback = fallback;
113
146
  }
114
147
 
115
148
  debugLog("loader", "loader error wrapped with boundary fallback", {
@@ -241,6 +274,21 @@ function createLoaderExecutor<TEnv>(
241
274
  pendingLoaders.add(loader.$$id);
242
275
 
243
276
  const currentLoaderId = loader.$$id;
277
+ const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
278
+
279
+ // Capture whether this loader is being started from a DSL loader scope
280
+ // (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
281
+ // inside loader scope. This determines whether rendered() is allowed.
282
+ const isDslLoader = isInsideLoaderScope();
283
+
284
+ let renderedResolved = false;
285
+ let renderedPromise: Promise<void> | null = null;
286
+
287
+ // Loader functions are always fresh (never cached), so they get an
288
+ // unguarded get that bypasses non-cacheable read guards. This applies
289
+ // to ALL loaders — DSL and handler-called — because the loader
290
+ // function itself always re-executes. Also handles nested deps
291
+ // (loaderA → use(loaderB)) since all share this unguarded get.
244
292
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
245
293
  params: ctx.params,
246
294
  routeParams: (ctx.params ?? {}) as Record<string, string>,
@@ -249,26 +297,138 @@ function createLoaderExecutor<TEnv>(
249
297
  search: (ctx as any).search,
250
298
  pathname: ctx.pathname,
251
299
  url: ctx.url,
300
+ originalUrl: ctx.originalUrl,
252
301
  env: ctx.env,
253
- var: ctx.var,
254
- get: ctx.get,
255
- use: <TDep, TDepParams = any>(
256
- dep: LoaderDefinition<TDep, TDepParams>,
257
- ): Promise<TDep> => {
258
- return useLoader(dep, currentLoaderId);
259
- },
302
+ waitUntil: ctx.waitUntil.bind(ctx),
303
+ executionContext: ctx.executionContext,
304
+ get: ((keyOrVar: any) =>
305
+ contextGet(variables, keyOrVar)) as typeof ctx.get,
306
+ use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
307
+ if (isHandle(item)) {
308
+ if (!renderedResolved) {
309
+ throw new Error(
310
+ `ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
311
+ `Handle "${item.$$id}" cannot be read until the render tree has settled.`,
312
+ );
313
+ }
314
+ const reqCtx = reqCtxRef ?? _getRequestContext();
315
+ if (!reqCtx) {
316
+ throw new Error(
317
+ `ctx.use(handle) failed: request context not available.`,
318
+ );
319
+ }
320
+ const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
321
+ // The complete snapshot is cached at barrier resolution for
322
+ // non-streaming trees, and by rendered() after handleStore.settled for
323
+ // streaming trees (where the eager snapshot would have been incomplete
324
+ // because loading() handlers were still in flight). Either way it is
325
+ // present by the time a loader reads a handle; the fresh build is only
326
+ // a defensive fallback.
327
+ const snapshot =
328
+ reqCtx._renderBarrierHandleSnapshot ??
329
+ buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
330
+ return collectHandleData(item, snapshot, segmentOrder);
331
+ }
332
+
333
+ // Loader case
334
+ return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
335
+ }) as LoaderContext["use"],
260
336
  method: "GET",
261
337
  body: undefined,
262
338
  reverse: ctx.reverse as LoaderContext["reverse"],
339
+ rendered: (): Promise<void> => {
340
+ // Guard: only DSL loaders may use rendered()
341
+ if (!isDslLoader) {
342
+ throw new Error(
343
+ `ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
344
+ `Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
345
+ );
346
+ }
347
+
348
+ const reqCtx = reqCtxRef ?? _getRequestContext();
349
+
350
+ if (renderedPromise) return renderedPromise;
351
+
352
+ if (!reqCtx) {
353
+ throw new Error(
354
+ `ctx.rendered() failed: request context not available.`,
355
+ );
356
+ }
357
+
358
+ // Bidirectional deadlock check: if a handler already started
359
+ // awaiting this loader, calling rendered() would deadlock. This is the
360
+ // real cycle guard (it holds for both streaming and non-streaming): the
361
+ // handler blocks segment resolution, which blocks the barrier, which
362
+ // blocks this loader.
363
+ if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
364
+ throw new Error(
365
+ `Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
366
+ `is already awaiting this loader via ctx.use(). The handler blocks ` +
367
+ `segment resolution, which blocks the barrier, which blocks this loader. ` +
368
+ `Move the data dependency to a loader-to-loader pattern instead.`,
369
+ );
370
+ }
371
+
372
+ // Register this loader as waiting for the barrier so that
373
+ // setupLoaderAccess can detect deadlocks when a handler
374
+ // tries to await the same loader via ctx.use().
375
+ if (!reqCtx._renderBarrierWaiters) {
376
+ reqCtx._renderBarrierWaiters = new Set();
377
+ }
378
+ reqCtx._renderBarrierWaiters.add(currentLoaderId);
379
+
380
+ // Streaming trees (loading()): the barrier resolves once the segment
381
+ // tree is resolved, but loading() handlers stream behind Suspense and
382
+ // their handle pushes are still in flight then. Their async execution
383
+ // IS tracked in the handle store (trackHandler -> store.track), so after
384
+ // the barrier we seal (no further handlers register once the tree is
385
+ // resolved) and wait for settled — every tracked handler, streaming
386
+ // included, has finished pushing. The loader's own segment streams in
387
+ // after, so this does not block the shell; the deadlock guard above
388
+ // keeps a handler from depending on this loader.
389
+ const streaming = reqCtx._treeHasStreaming === true;
390
+ renderedPromise = reqCtx._renderBarrier.then(async () => {
391
+ if (streaming) {
392
+ reqCtx._handleStore.seal();
393
+ await reqCtx._handleStore.settled;
394
+ // The eager snapshot was intentionally left unbuilt for streaming
395
+ // (it would have been incomplete). Build the complete one once, now
396
+ // that the store has settled, so every ctx.use(handle) reads the
397
+ // cached snapshot instead of rebuilding it per call.
398
+ reqCtx._renderBarrierHandleSnapshot ??= buildHandleSnapshot(
399
+ reqCtx._handleStore,
400
+ reqCtx._renderBarrierSegmentOrder ?? [],
401
+ );
402
+ }
403
+ renderedResolved = true;
404
+ });
405
+ return renderedPromise;
406
+ },
263
407
  };
264
408
 
265
- const doneLoader = track(`loader:${loader.$$id}`, 2);
266
- const promise = Promise.resolve(
267
- loaderFn(loaderCtx as LoaderContext<any, TEnv>),
268
- ).finally(() => {
269
- pendingLoaders.delete(loader.$$id);
270
- doneLoader();
271
- });
409
+ // Meter this loader once via observePhase (loader:<id> perf metric +
410
+ // rango.loader span); loaderFn runs inside the span callback so its KV/D1/
411
+ // fetch spans nest under it. This is one of the observePhase loader funnels —
412
+ // see instrument.ts for the single-metering contract.
413
+ //
414
+ // Run the loader body inside loader scope so request-scoped reads
415
+ // (cookies()/headers() and non-cacheable ctx.get) are exempt from the
416
+ // cache-purity guards: loaders always run fresh, so their reads never leak
417
+ // into a cached segment. DSL loaders are already wrapped by fresh.ts; this
418
+ // also covers handler-invoked loaders (ctx.use(Loader) from a handler),
419
+ // which otherwise execute in the caller's cache scope and would wrongly
420
+ // throw. rendered() gating uses the captured isDslLoader (above), so this
421
+ // does not grant rendered() to handler-invoked loaders. Uses a body-only
422
+ // scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
423
+ const promise = observePhase(PHASES.loader(loader.$$id), () =>
424
+ Promise.resolve(
425
+ runInsideLoaderBodyScope(() =>
426
+ loaderFn(loaderCtx as LoaderContext<any, TEnv>),
427
+ ),
428
+ ).finally(() => {
429
+ pendingLoaders.delete(loader.$$id);
430
+ }),
431
+ );
272
432
 
273
433
  loaderPromises.set(loader.$$id, promise);
274
434
  return promise;
@@ -290,12 +450,13 @@ export function setupLoaderAccess<TEnv>(
290
450
  ctx: HandlerContext<any, TEnv>,
291
451
  loaderPromises: Map<string, Promise<any>>,
292
452
  ): void {
293
- // Eagerly capture the HandleStore at setup time (before pipeline async ops).
294
- // In workerd/Cloudflare, dynamic imports and fetch() in the match pipeline
295
- // can disrupt AsyncLocalStorage, causing getRequestContext() to return
296
- // undefined when handlers later call ctx.use(handle). Capturing early
297
- // ensures the store reference survives ALS disruption.
298
- const handleStoreRef = _getRequestContext()?._handleStore;
453
+ // Eagerly capture the request context and HandleStore at setup time
454
+ // (before pipeline async ops). In workerd/Cloudflare, dynamic imports and
455
+ // fetch() in the match pipeline can disrupt AsyncLocalStorage, causing
456
+ // getRequestContext() to return undefined when handlers later call
457
+ // ctx.use(handle). Capturing early ensures references survive ALS disruption.
458
+ const reqCtxRef = _getRequestContext();
459
+ const handleStoreRef = reqCtxRef?._handleStore;
299
460
 
300
461
  const useLoader = createLoaderExecutor(ctx, loaderPromises);
301
462
 
@@ -313,21 +474,72 @@ export function setupLoaderAccess<TEnv>(
313
474
  );
314
475
  }
315
476
 
316
- return (
317
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
318
- ) => {
319
- if (!store) return;
320
-
321
- const valueOrPromise =
322
- typeof dataOrFn === "function"
323
- ? (dataOrFn as () => Promise<unknown>)()
324
- : dataOrFn;
477
+ return withDefer(
478
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
479
+ if (!store) return;
480
+
481
+ if (typeof dataOrFn === "function") {
482
+ // Run the callback inside the push-callback scope so ctx.use(loader)
483
+ // calls it makes — including after its own awaits, for an async
484
+ // callback — are not registered as handler-to-loader deps and do not
485
+ // trip the deadlock guard. A pushed promise value is not tracked by
486
+ // handleStore.settled and does not block segment resolution, so it
487
+ // cannot form a rendered() deadlock. The ALS scope (not a plain
488
+ // boolean) is what survives the callback's awaits.
489
+ const result = runInsidePushCallbackScope(() =>
490
+ (dataOrFn as () => Promise<unknown>)(),
491
+ );
492
+ store.push(handle.$$id, segmentId, result);
493
+ return;
494
+ }
495
+
496
+ store.push(handle.$$id, segmentId, dataOrFn);
497
+ },
498
+ );
499
+ }
325
500
 
326
- store.push(handle.$$id, segmentId, valueOrPromise);
327
- };
501
+ // Deadlock guard and handler-to-loader dependency tracking.
502
+ // Skip when inside a DSL loader scope (resolveLoaderData also calls
503
+ // ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
504
+ // inside a handle push callback (push callbacks don't block segment
505
+ // resolution so they can't cause rendered() deadlocks). The push-callback
506
+ // check is an ALS scope so it also exempts an ASYNC callback's continuation
507
+ // after its first await — relevant on streaming trees, where the guard
508
+ // state now stays live until handleStore.settled.
509
+ const loader = item as LoaderDefinition<any, any>;
510
+ if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
511
+ const reqCtx = reqCtxRef ?? _getRequestContext();
512
+ if (reqCtx) {
513
+ // Direction 1: handler awaits loader that already called rendered()
514
+ if (
515
+ loaderPromises.has(loader.$$id) &&
516
+ reqCtx._renderBarrierWaiters?.has(loader.$$id)
517
+ ) {
518
+ throw new Error(
519
+ `Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
520
+ `The loader is waiting for segment resolution, but the handler blocks resolution. ` +
521
+ `Move the data dependency to a loader-to-loader pattern instead.`,
522
+ );
523
+ }
524
+ // Direction 2: track dep so rendered() can detect the deadlock if the
525
+ // loader calls it later. Skip once the guard window is CLOSED — for a
526
+ // non-streaming tree that is when the barrier resolves (rendered()
527
+ // resolves immediately), and for a streaming tree it is when
528
+ // handleStore.settled completes (rendered() keeps waiting until then, so
529
+ // a loading() handler resuming after the barrier can still form a
530
+ // cycle). Using the explicit guard-closed flag rather than
531
+ // _renderBarrierSegmentOrder keeps tracking live across the streaming
532
+ // settle wait. (Handle push callbacks are already excluded above via
533
+ // isInsidePushCallbackScope(), so they cannot produce false positives
534
+ // here.)
535
+ if (!reqCtx._renderBarrierGuardClosed) {
536
+ if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
537
+ reqCtx._handlerLoaderDeps.add(loader.$$id);
538
+ }
539
+ }
328
540
  }
329
541
 
330
- return useLoader(item as LoaderDefinition<any, any>, null);
542
+ return useLoader(loader, null);
331
543
  }) as typeof ctx.use;
332
544
  }
333
545
 
@@ -354,18 +566,21 @@ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
354
566
  );
355
567
  }
356
568
 
357
- return (
358
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
359
- ) => {
360
- if (!store) return;
569
+ // Wrap with withDefer so ctx.use(Handle).defer(...) works on the build /
570
+ // prerender path, matching production setupLoaderAccess. Without it a
571
+ // prerender handler calling .defer() throws "defer is not a function".
572
+ return withDefer(
573
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
574
+ if (!store) return;
361
575
 
362
- const valueOrPromise =
363
- typeof dataOrFn === "function"
364
- ? (dataOrFn as () => Promise<unknown>)()
365
- : dataOrFn;
576
+ const valueOrPromise =
577
+ typeof dataOrFn === "function"
578
+ ? (dataOrFn as () => Promise<unknown>)()
579
+ : dataOrFn;
366
580
 
367
- store.push(handle.$$id, segmentId, valueOrPromise);
368
- };
581
+ store.push(handle.$$id, segmentId, valueOrPromise);
582
+ },
583
+ );
369
584
  }
370
585
 
371
586
  // Loader case: not available during pre-rendering
@@ -393,8 +608,11 @@ export function setupLoaderAccessSilent<TEnv>(
393
608
 
394
609
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
395
610
  if (isHandle(item)) {
396
- // Silent mode - return a no-op so handle data is not pushed during caching
397
- return (_dataOrFn: unknown) => {};
611
+ // Silent mode - return a no-op so handle data is not pushed during caching.
612
+ // Wrap with withDefer so ctx.use(Handle).defer(...) still resolves to a
613
+ // callable resolver (also a no-op here), matching production's push shape
614
+ // instead of throwing "defer is not a function".
615
+ return withDefer((_dataOrFn: unknown) => {});
398
616
  }
399
617
 
400
618
  return useLoader(item as LoaderDefinition<any, any>, null);
@@ -1,8 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
3
3
 
4
- // -- Revalidation trace types --
5
-
6
4
  export interface RevalidationTraceEntry {
7
5
  segmentId: string;
8
6
  segmentType: string;
@@ -12,7 +10,10 @@ export interface RevalidationTraceEntry {
12
10
  | "cache-hit"
13
11
  | "loader"
14
12
  | "parallel"
15
- | "orphan-layout";
13
+ | "orphan-layout"
14
+ | "route-handler"
15
+ | "layout-handler"
16
+ | "intercept-loader";
16
17
  defaultShouldRevalidate: boolean;
17
18
  finalShouldRevalidate: boolean;
18
19
  reason: string;
@@ -33,8 +34,6 @@ export interface RevalidationTrace {
33
34
  entries: RevalidationTraceEntry[];
34
35
  }
35
36
 
36
- // -- Log context --
37
-
38
37
  interface RouterLogContext {
39
38
  requestId: string;
40
39
  transactionId: string;
@@ -71,7 +70,7 @@ function getHeaderRequestId(request: Request): string | null {
71
70
  return trimmed.length > 0 ? trimmed : null;
72
71
  }
73
72
 
74
- function getOrCreateRequestId(request: Request): string {
73
+ export function getOrCreateRequestId(request: Request): string {
75
74
  const existing = requestIds.get(request);
76
75
  if (existing) return existing;
77
76
 
@@ -192,8 +191,6 @@ export function debugWarn(
192
191
  console.warn(`${prefix} ${message}`);
193
192
  }
194
193
 
195
- // -- Revalidation trace helpers --
196
-
197
194
  export function isTraceActive(): boolean {
198
195
  if (!INTERNAL_RANGO_DEBUG) return false;
199
196
  const ctx = routerLogContext.getStore();
@@ -1,18 +1,14 @@
1
- /**
2
- * Router Manifest Loading
3
- *
4
- * Handles lazy loading and validation of route manifests.
5
- */
6
-
7
1
  import { invariant, RouteNotFoundError } from "../errors";
8
2
  import { createRouteHelpers } from "../route-definition";
9
3
  import {
10
4
  getContext,
11
5
  runWithPrefixes,
6
+ getIsolatedLazyParent,
12
7
  type EntryData,
13
8
  type MetricsStore,
14
9
  } from "../server/context";
15
10
  import MapRootLayout from "../server/root-layout";
11
+ import { joinPrefix } from "./pattern-matching.js";
16
12
  import type { RouteEntry } from "../types";
17
13
  import type { UrlPatterns } from "../urls";
18
14
  import { VERSION } from "@rangojs/router:version";
@@ -22,20 +18,19 @@ import { VERSION } from "@rangojs/router:version";
22
18
  // stable references), so the resulting EntryData tree can be safely cached and reused
23
19
  // across requests within the same isolate.
24
20
  //
25
- // Cache is keyed by (VERSION, mountIndex, routeKey, isSSR). VERSION comes from the
21
+ // Cache is keyed by (VERSION, routerId, mountIndex, routeKey, isSSR). routeKey is
22
+ // REQUIRED in the key: loadManifest() runs the handler with forRoute=routeKey, and
23
+ // path-helper.ts prunes (skips registering) every route except forRoute, so the
24
+ // resulting Store.manifest is pruned to the requested route — NOT the full include.
25
+ // Dropping routeKey would make a sibling route miss and overwrite this entry with its
26
+ // own pruned manifest, so alternating sibling requests would thrash (re-run the
27
+ // handler every time). Running the include handler once per isolate instead of once
28
+ // per route is possible but needs an unpruned manifest cache with prune-on-read — see
29
+ // LP1 in docs/internal/matching-and-lazy-discovery.md. VERSION comes from the
26
30
  // @rangojs/router:version virtual module which Vite invalidates on RSC module HMR.
27
31
  // When VERSION changes, this module re-evaluates and the cache is recreated empty.
28
- // Including VERSION in the key is additional defense against stale entries.
29
32
  const manifestModuleCache = new Map<string, Map<string, EntryData>>();
30
33
 
31
- /**
32
- * Load manifest from route entry with AsyncLocalStorage context
33
- * Handles lazy imports, unwrapping, and validation
34
- *
35
- * Results are cached at module level after first execution. Subsequent calls
36
- * for the same (routeKey, isSSR) within the same isolate return cached data
37
- * without re-executing the DSL handler.
38
- */
39
34
  /**
40
35
  * Clear the module-level manifest cache.
41
36
  * Called on HMR to ensure stale handler references are discarded.
@@ -64,8 +59,12 @@ export async function loadManifest(
64
59
 
65
60
  const mountIndex = entry.mountIndex;
66
61
 
67
- // Check module-level cache (persists across requests within same isolate)
68
- const cacheKey = `${VERSION}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
62
+ // Check module-level cache (persists across requests within same isolate).
63
+ // Include routerId so multi-router setups (host routing) don't share cached
64
+ // EntryData across routers with overlapping mountIndex + routeKey combinations.
65
+ // routeKey is in the key because loadManifest() builds a manifest pruned to
66
+ // forRoute=routeKey (see path-helper.ts) — see the cache comment above.
67
+ const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
69
68
  const cached = manifestModuleCache.get(cacheKey);
70
69
  if (cached) {
71
70
  const cacheStart = performance.now();
@@ -85,20 +84,15 @@ export async function loadManifest(
85
84
  const storeSetupStart = performance.now();
86
85
  const Store = getContext().getOrCreateStore(routeKey);
87
86
 
88
- // Set mount index in store for unique shortCode prefixes
89
87
  Store.mountIndex = mountIndex;
90
-
91
- // Set isSSR flag so loading() can check if we're in SSR
92
88
  Store.isSSR = isSSR;
93
89
 
94
- // Attach metrics store to context if provided
95
90
  if (metricsStore) {
96
91
  Store.metrics = metricsStore;
97
92
  }
98
93
 
99
94
  pushMetric?.("manifest:store-setup", storeSetupStart);
100
95
 
101
- // Clear manifest before rebuilding to prevent stale entry mutations
102
96
  const clearStart = performance.now();
103
97
  Store.manifest.clear();
104
98
  pushMetric?.("manifest:clear", clearStart);
@@ -112,36 +106,48 @@ export async function loadManifest(
112
106
  // This ensures routes are registered under the correct layout hierarchy
113
107
  const lazyContext =
114
108
  entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
115
- const parentForContext =
116
- (lazyContext?.parent as EntryData | null) ?? Store.parent;
109
+ const parentForContext = lazyContext
110
+ ? getIsolatedLazyParent(
111
+ (lazyContext.parent as EntryData | null) ?? Store.parent,
112
+ )
113
+ : Store.parent;
117
114
 
118
115
  // For lazy entries, merge captured counters from include() so the
119
116
  // handler's entries get shortCode indices after sibling entries that
120
117
  // were created during pattern extraction. This prevents shortCode
121
118
  // collisions between lazy and non-lazy entries under the same parent
122
119
  // (e.g., ArticlesLayout and BlogLayout both under NavLayout).
123
- if (lazyContext && (lazyContext as any).counters) {
124
- const captured = (lazyContext as any).counters as Record<string, number>;
125
- for (const [key, value] of Object.entries(captured)) {
120
+ if (lazyContext?.counters) {
121
+ for (const [key, value] of Object.entries(lazyContext.counters)) {
126
122
  Store.counters[key] = Math.max(Store.counters[key] ?? 0, value);
127
123
  }
128
124
  }
129
125
 
130
126
  // Propagate cache profiles for DSL-time cache("profileName") resolution.
131
127
  // Non-lazy entries carry profiles directly; lazy entries carry them
132
- // in the captured lazyContext from include() time.
133
- const entryProfiles =
134
- entry.cacheProfiles ?? (lazyContext as any)?.cacheProfiles;
135
- if (entryProfiles) {
136
- Store.cacheProfiles = entryProfiles;
137
- }
128
+ // in the captured lazyContext from include() time. Always write
129
+ // (including clearing to undefined) so a prior lazy build's profile
130
+ // map cannot leak into a later non-lazy build on the same ALS-backed
131
+ // Store — which would otherwise let cache("name") resolve a profile
132
+ // from an unrelated entry.
133
+ Store.cacheProfiles = entry.cacheProfiles ?? lazyContext?.cacheProfiles;
138
134
 
139
135
  // Propagate rootScoped from lazyContext so that routes inside
140
136
  // nested { name: "sub" } under { name: "" } keep inherited root scope
141
- // when the manifest is rebuilt on each request.
142
- if (lazyContext && (lazyContext as any).rootScoped !== undefined) {
143
- Store.rootScoped = (lazyContext as any).rootScoped;
144
- }
137
+ // when the manifest is rebuilt on each request. Always write
138
+ // (including clearing to undefined, which makes getRootScoped()
139
+ // return its true default) so a prior lazy build's scope cannot leak
140
+ // into a later non-lazy build on the same ALS-backed Store — which
141
+ // would otherwise mis-register plain routes as non-root-scoped and
142
+ // break dot-local reverse resolution.
143
+ Store.rootScoped = lazyContext?.rootScoped;
144
+
145
+ // Propagate includeScope from lazyContext so that direct-descendant
146
+ // shortCodes of this include use the correct scoped counter namespace
147
+ // on every manifest rebuild. Always write (including clearing to
148
+ // undefined) so a prior lazy build's scope cannot leak into a later
149
+ // non-lazy build on the same ALS-backed Store.
150
+ Store.includeScope = lazyContext?.includeScope;
145
151
 
146
152
  const handlerExecStart = performance.now();
147
153
  const useItems = await getContext().runWithStore(
@@ -161,7 +167,10 @@ export async function loadManifest(
161
167
  if (entry.lazy && entry.lazyPatterns) {
162
168
  const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
163
169
  const includePrefix = (entry as any)._lazyPrefix || "";
164
- const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
170
+ // Slash-collapsing join so a trailing-slash parent prefix does not
171
+ // bake a double slash into the registered route patterns (must match
172
+ // the same join in evaluateLazyEntry / the build-time runWithPrefixes).
173
+ const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
165
174
 
166
175
  if (fullPrefix || lazyContext?.namePrefix) {
167
176
  return runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () =>
@@ -171,20 +180,16 @@ export async function loadManifest(
171
180
  return lazyPatterns.handler();
172
181
  }
173
182
 
174
- // Wrap handler execution in root layout so routes get correct parent
175
- // This ensures all routes are registered with the layout as their parent
176
183
  let promiseResult: Promise<any> | null = null;
177
184
  const wrappedItems = helpers.layout(MapRootLayout, () => {
178
185
  const result = entry.handler();
179
186
  if (result instanceof Promise) {
180
- // Lazy handler detected - capture promise for async handling
181
187
  promiseResult = result;
182
- return []; // Return empty, we'll discard this wrapped result
188
+ return [];
183
189
  }
184
190
  return result;
185
191
  });
186
192
 
187
- // Handle lazy (Promise-based) handlers
188
193
  if (promiseResult !== null) {
189
194
  const load = await (promiseResult as Promise<any>);
190
195
  if (
@@ -218,7 +223,6 @@ export async function loadManifest(
218
223
  );
219
224
  }
220
225
 
221
- // Inline handler - routes were registered with correct parent inside layout
222
226
  return [wrappedItems].flat(3);
223
227
  },
224
228
  );