@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
@@ -1,8 +1,8 @@
1
1
  import { registerRouteMap } from "../route-map-builder.js";
2
- import { extractStaticPrefix } from "./pattern-matching.js";
2
+ import { extractStaticPrefix, joinPrefix } from "./pattern-matching.js";
3
3
  import {
4
- EntryData,
5
- RSCRouterContext,
4
+ type EntryData,
5
+ RangoContext,
6
6
  runWithPrefixes,
7
7
  getIsolatedLazyParent,
8
8
  } from "../server/context";
@@ -18,9 +18,6 @@ export interface LazyEvalDeps<TEnv = any> {
18
18
  routerId?: string;
19
19
  }
20
20
 
21
- // Detect lazy includes in handler result and create placeholder entries
22
- // Lazy includes are IncludeItem with lazy: true and _lazyContext
23
- // Moved to outer scope so it can be reused by evaluateLazyEntry for nested includes
24
21
  export function findLazyIncludes<TEnv = any>(
25
22
  items: AllUseItems[],
26
23
  ): Array<{
@@ -56,7 +53,6 @@ export function findLazyIncludes<TEnv = any>(
56
53
  });
57
54
  }
58
55
  }
59
- // Recursively check nested items (in layouts, etc.)
60
56
  if ((item as any).uses && Array.isArray((item as any).uses)) {
61
57
  lazyItems.push(...findLazyIncludes((item as any).uses));
62
58
  }
@@ -78,14 +74,6 @@ export function evaluateLazyEntry<TEnv = any>(
78
74
  return;
79
75
  }
80
76
 
81
- // Check for pre-computed routes from build-time data.
82
- // Only leaf nodes (no nested includes) are precomputed, so entries with
83
- // nested lazy includes fall through to the handler below.
84
- // When multiple entries share the same staticPrefix (e.g., several
85
- // include("/", ...) calls), the precomputed data merges all their routes
86
- // into one entry. Assigning that merged set to the first matching entry
87
- // causes findMatch to pick the wrong handler for routes belonging to a
88
- // different include. Skip the shortcut when the prefix is shared.
89
77
  const currentPrecomputed = deps.getPrecomputedByPrefix();
90
78
  if (currentPrecomputed) {
91
79
  const routes = currentPrecomputed.get(entry.staticPrefix);
@@ -105,25 +93,18 @@ export function evaluateLazyEntry<TEnv = any>(
105
93
  }
106
94
  }
107
95
 
108
- // Mark as evaluated immediately to prevent concurrent evaluation.
109
- // JS is single-threaded but handlers.handler() could theoretically yield,
110
- // and the while-loop in findMatch retries after evaluation.
111
96
  entry.lazyEvaluated = true;
112
97
 
113
98
  const lazyPatterns = entry.lazyPatterns as UrlPatterns<TEnv>;
114
99
  const lazyContext = entry.lazyContext;
115
100
 
116
- // Create a new context for evaluating the lazy patterns
117
101
  const manifest = new Map<string, EntryData>();
118
102
  const patterns = new Map<string, string>();
119
103
  const patternsByPrefix = new Map<string, Map<string, string>>();
120
104
  const trailingSlashMap = new Map<string, TrailingSlashMode>();
121
105
 
122
- // Capture the handler result to detect nested lazy includes
123
106
  let handlerResult: AllUseItems[] = [];
124
107
 
125
- // Merge captured counters from include() to maintain consistent
126
- // shortCode indices with sibling entries from pattern extraction
127
108
  const lazyCounters: Record<string, number> = {};
128
109
  if (lazyContext?.counters) {
129
110
  for (const [key, value] of Object.entries(lazyContext.counters)) {
@@ -131,7 +112,7 @@ export function evaluateLazyEntry<TEnv = any>(
131
112
  }
132
113
  }
133
114
 
134
- RSCRouterContext.run(
115
+ RangoContext.run(
135
116
  {
136
117
  manifest,
137
118
  patterns,
@@ -145,10 +126,8 @@ export function evaluateLazyEntry<TEnv = any>(
145
126
  includeScope: lazyContext?.includeScope,
146
127
  },
147
128
  () => {
148
- // Run the lazy patterns handler with the original context prefixes
149
- // The prefix comes from the IncludeItem stored in lazyPatterns
150
129
  const includePrefix = (entry as any)._lazyPrefix || "";
151
- const fullPrefix = (lazyContext?.urlPrefix || "") + includePrefix;
130
+ const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
152
131
 
153
132
  if (fullPrefix || lazyContext?.namePrefix) {
154
133
  runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () => {
@@ -160,11 +139,9 @@ export function evaluateLazyEntry<TEnv = any>(
160
139
  },
161
140
  );
162
141
 
163
- // Populate the entry's routes from the patterns
164
142
  const routesObject: Record<string, string> = {};
165
143
  for (const [name, pattern] of patterns.entries()) {
166
144
  routesObject[name] = pattern;
167
- // Also add to merged route map for reverse() support
168
145
  const existingPattern = deps.mergedRouteMap[name];
169
146
  if (existingPattern !== undefined && existingPattern !== pattern) {
170
147
  console.warn(
@@ -175,46 +152,33 @@ export function evaluateLazyEntry<TEnv = any>(
175
152
  deps.mergedRouteMap[name] = pattern;
176
153
  }
177
154
 
178
- // Update the entry in-place
179
155
  entry.routes = routesObject as ResolvedRouteMap<any>;
180
156
 
181
- // Note: Do NOT clear lazyPatterns/lazyContext here.
182
- // loadManifest() needs them on every request to re-run the handler
183
- // in the correct AsyncLocalStorage context (Store.manifest).
184
-
185
- // Update trailing slash config if available
186
157
  if (trailingSlashMap.size > 0) {
187
158
  entry.trailingSlash = Object.fromEntries(trailingSlashMap);
188
159
  }
189
160
 
190
- // Detect nested lazy includes and register them as new entries
191
161
  const nestedLazyIncludes = findLazyIncludes(handlerResult);
192
162
  for (const lazyInclude of nestedLazyIncludes) {
193
- // Compute the full URL prefix (combining parent prefix if any)
194
- const fullPrefix = lazyInclude.context.urlPrefix
195
- ? lazyInclude.context.urlPrefix + lazyInclude.prefix
196
- : lazyInclude.prefix;
163
+ const fullPrefix = joinPrefix(
164
+ lazyInclude.context.urlPrefix,
165
+ lazyInclude.prefix,
166
+ );
197
167
 
198
168
  const nestedEntry: RouteEntry<TEnv> & { _lazyPrefix?: string } = {
199
169
  prefix: "",
200
170
  staticPrefix: extractStaticPrefix(fullPrefix),
201
- routes: {} as ResolvedRouteMap<any>, // Empty until first match
171
+ routes: {} as ResolvedRouteMap<any>,
202
172
  trailingSlash: entry.trailingSlash,
203
173
  handler: (lazyInclude.patterns as UrlPatterns<TEnv>).handler,
204
174
  mountIndex: deps.nextMountIndex(),
205
175
  routerId: deps.routerId,
206
- // Lazy evaluation fields
207
176
  lazy: true,
208
177
  lazyPatterns: lazyInclude.patterns,
209
178
  lazyContext: lazyInclude.context,
210
179
  lazyEvaluated: false,
211
- // Store the include prefix for evaluation
212
180
  _lazyPrefix: lazyInclude.prefix,
213
181
  };
214
- // Insert nested lazy entry before any entry whose staticPrefix is a
215
- // prefix of (but shorter than) this lazy entry's staticPrefix.
216
- // This ensures more specific lazy includes are matched before
217
- // less specific eager entries (e.g., "/href/nested" before "/href/:id").
218
182
  const nestedPrefix = nestedEntry.staticPrefix;
219
183
  let insertIndex = deps.routesEntries.length;
220
184
  if (nestedPrefix) {
@@ -232,6 +196,5 @@ export function evaluateLazyEntry<TEnv = any>(
232
196
  deps.routesEntries.splice(insertIndex, 0, nestedEntry);
233
197
  }
234
198
 
235
- // Re-register route map for runtime reverse() usage
236
199
  registerRouteMap(deps.mergedRouteMap);
237
200
  }
@@ -5,8 +5,8 @@
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
10
  import { contextGet } from "../context-var.js";
11
11
  import type {
12
12
  ResolvedSegment,
@@ -19,12 +19,17 @@ import type {
19
19
  ErrorBoundaryFallbackProps,
20
20
  ErrorInfo,
21
21
  } from "../types";
22
- import type { LoaderRevalidationResult, ActionContext } from "./types";
23
22
  import { isHandle, collectHandleData, type Handle } from "../handle.js";
23
+ import { withDefer } from "../defer.js";
24
24
  import { buildHandleSnapshot } from "../server/handle-store.js";
25
25
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
26
26
  import { _getRequestContext } from "../server/request-context.js";
27
- import { isInsideLoaderScope } from "../server/context.js";
27
+ import {
28
+ isInsideLoaderScope,
29
+ runInsideLoaderBodyScope,
30
+ isInsidePushCallbackScope,
31
+ runInsidePushCallbackScope,
32
+ } from "../server/context.js";
28
33
  import { debugLog } from "./logging.js";
29
34
 
30
35
  /**
@@ -66,7 +71,9 @@ export function wrapLoaderWithErrorHandling<T>(
66
71
  ) => ErrorInfo,
67
72
  onError?: LoaderErrorCallback,
68
73
  ): Promise<LoaderDataResult<T>> {
69
- // 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.
70
77
  const loaderName = segmentId.split(".").pop() || "unknown";
71
78
 
72
79
  return Promise.resolve(promise)
@@ -102,16 +109,40 @@ export function wrapLoaderWithErrorHandling<T>(
102
109
  };
103
110
  }
104
111
 
105
- // 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.
106
120
  let renderedFallback: ReactNode;
107
- if (typeof fallback === "function") {
108
- // ErrorBoundaryHandler - call with error info
109
- 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,
110
143
  error: errorInfo,
144
+ fallback: null,
111
145
  };
112
- renderedFallback = fallback(props);
113
- } else {
114
- renderedFallback = fallback;
115
146
  }
116
147
 
117
148
  debugLog("loader", "loader error wrapped with boundary fallback", {
@@ -266,7 +297,10 @@ function createLoaderExecutor<TEnv>(
266
297
  search: (ctx as any).search,
267
298
  pathname: ctx.pathname,
268
299
  url: ctx.url,
300
+ originalUrl: ctx.originalUrl,
269
301
  env: ctx.env,
302
+ waitUntil: ctx.waitUntil.bind(ctx),
303
+ executionContext: ctx.executionContext,
270
304
  get: ((keyOrVar: any) =>
271
305
  contextGet(variables, keyOrVar)) as typeof ctx.get,
272
306
  use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
@@ -284,6 +318,12 @@ function createLoaderExecutor<TEnv>(
284
318
  );
285
319
  }
286
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.
287
327
  const snapshot =
288
328
  reqCtx._renderBarrierHandleSnapshot ??
289
329
  buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
@@ -305,15 +345,7 @@ function createLoaderExecutor<TEnv>(
305
345
  );
306
346
  }
307
347
 
308
- // Guard: reject streaming trees
309
348
  const reqCtx = reqCtxRef ?? _getRequestContext();
310
- if (reqCtx?._treeHasStreaming) {
311
- throw new Error(
312
- `ctx.rendered() is not supported when the matched route tree uses loading(). ` +
313
- `Streaming handlers may not have settled when rendered() resolves. ` +
314
- `Remove loading() from the route tree or restructure to avoid rendered().`,
315
- );
316
- }
317
349
 
318
350
  if (renderedPromise) return renderedPromise;
319
351
 
@@ -324,7 +356,10 @@ function createLoaderExecutor<TEnv>(
324
356
  }
325
357
 
326
358
  // Bidirectional deadlock check: if a handler already started
327
- // awaiting this loader, calling rendered() would deadlock.
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.
328
363
  if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
329
364
  throw new Error(
330
365
  `Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
@@ -342,20 +377,58 @@ function createLoaderExecutor<TEnv>(
342
377
  }
343
378
  reqCtx._renderBarrierWaiters.add(currentLoaderId);
344
379
 
345
- renderedPromise = reqCtx._renderBarrier.then(() => {
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
+ }
346
403
  renderedResolved = true;
347
404
  });
348
405
  return renderedPromise;
349
406
  },
350
407
  };
351
408
 
352
- const doneLoader = track(`loader:${loader.$$id}`, 2);
353
- const promise = Promise.resolve(
354
- loaderFn(loaderCtx as LoaderContext<any, TEnv>),
355
- ).finally(() => {
356
- pendingLoaders.delete(loader.$$id);
357
- doneLoader();
358
- });
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
+ );
359
432
 
360
433
  loaderPromises.set(loader.$$id, promise);
361
434
  return promise;
@@ -387,12 +460,6 @@ export function setupLoaderAccess<TEnv>(
387
460
 
388
461
  const useLoader = createLoaderExecutor(ctx, loaderPromises);
389
462
 
390
- // Track whether we're inside a handle push callback. Loaders started
391
- // from push callbacks (e.g. push(async () => ctx.use(Loader))) do NOT
392
- // block segment resolution, so they must not be registered as handler
393
- // dependencies for deadlock detection.
394
- let insideHandlePush = false;
395
-
396
463
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
397
464
  if (isHandle(item)) {
398
465
  const handle = item;
@@ -407,35 +474,40 @@ export function setupLoaderAccess<TEnv>(
407
474
  );
408
475
  }
409
476
 
410
- return (
411
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
412
- ) => {
413
- if (!store) return;
414
-
415
- if (typeof dataOrFn === "function") {
416
- // Mark scope so ctx.use(loader) calls inside the callback
417
- // are not registered as handler-to-loader deps.
418
- insideHandlePush = true;
419
- try {
420
- const result = (dataOrFn as () => Promise<unknown>)();
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
+ );
421
492
  store.push(handle.$$id, segmentId, result);
422
- } finally {
423
- insideHandlePush = false;
493
+ return;
424
494
  }
425
- return;
426
- }
427
495
 
428
- store.push(handle.$$id, segmentId, dataOrFn);
429
- };
496
+ store.push(handle.$$id, segmentId, dataOrFn);
497
+ },
498
+ );
430
499
  }
431
500
 
432
501
  // Deadlock guard and handler-to-loader dependency tracking.
433
502
  // Skip when inside a DSL loader scope (resolveLoaderData also calls
434
503
  // ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
435
504
  // inside a handle push callback (push callbacks don't block segment
436
- // resolution so they can't cause rendered() deadlocks).
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.
437
509
  const loader = item as LoaderDefinition<any, any>;
438
- if (!isInsideLoaderScope() && !insideHandlePush) {
510
+ if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
439
511
  const reqCtx = reqCtxRef ?? _getRequestContext();
440
512
  if (reqCtx) {
441
513
  // Direction 1: handler awaits loader that already called rendered()
@@ -449,13 +521,18 @@ export function setupLoaderAccess<TEnv>(
449
521
  `Move the data dependency to a loader-to-loader pattern instead.`,
450
522
  );
451
523
  }
452
- // Direction 2: track dep so rendered() can detect the deadlock
453
- // if the loader calls it later. Skip when the barrier has already
454
- // resolved no deadlock is possible (rendered() resolves immediately).
455
- // _renderBarrierSegmentOrder is undefined before resolution, string[]
456
- // after. This also prevents false positives from handle push callbacks
457
- // that resume after their first await (post-barrier-resolution).
458
- if (reqCtx._renderBarrierSegmentOrder === undefined) {
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) {
459
536
  if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
460
537
  reqCtx._handlerLoaderDeps.add(loader.$$id);
461
538
  }
@@ -489,18 +566,21 @@ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
489
566
  );
490
567
  }
491
568
 
492
- return (
493
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
494
- ) => {
495
- 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;
496
575
 
497
- const valueOrPromise =
498
- typeof dataOrFn === "function"
499
- ? (dataOrFn as () => Promise<unknown>)()
500
- : dataOrFn;
576
+ const valueOrPromise =
577
+ typeof dataOrFn === "function"
578
+ ? (dataOrFn as () => Promise<unknown>)()
579
+ : dataOrFn;
501
580
 
502
- store.push(handle.$$id, segmentId, valueOrPromise);
503
- };
581
+ store.push(handle.$$id, segmentId, valueOrPromise);
582
+ },
583
+ );
504
584
  }
505
585
 
506
586
  // Loader case: not available during pre-rendering
@@ -528,8 +608,11 @@ export function setupLoaderAccessSilent<TEnv>(
528
608
 
529
609
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
530
610
  if (isHandle(item)) {
531
- // Silent mode - return a no-op so handle data is not pushed during caching
532
- 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) => {});
533
616
  }
534
617
 
535
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;
@@ -36,8 +34,6 @@ export interface RevalidationTrace {
36
34
  entries: RevalidationTraceEntry[];
37
35
  }
38
36
 
39
- // -- Log context --
40
-
41
37
  interface RouterLogContext {
42
38
  requestId: string;
43
39
  transactionId: string;
@@ -195,8 +191,6 @@ export function debugWarn(
195
191
  console.warn(`${prefix} ${message}`);
196
192
  }
197
193
 
198
- // -- Revalidation trace helpers --
199
-
200
194
  export function isTraceActive(): boolean {
201
195
  if (!INTERNAL_RANGO_DEBUG) return false;
202
196
  const ctx = routerLogContext.getStore();
@@ -1,9 +1,3 @@
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 {
@@ -14,6 +8,7 @@ import {
14
8
  type MetricsStore,
15
9
  } from "../server/context";
16
10
  import MapRootLayout from "../server/root-layout";
11
+ import { joinPrefix } from "./pattern-matching.js";
17
12
  import type { RouteEntry } from "../types";
18
13
  import type { UrlPatterns } from "../urls";
19
14
  import { VERSION } from "@rangojs/router:version";
@@ -23,20 +18,19 @@ import { VERSION } from "@rangojs/router:version";
23
18
  // stable references), so the resulting EntryData tree can be safely cached and reused
24
19
  // across requests within the same isolate.
25
20
  //
26
- // 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
27
30
  // @rangojs/router:version virtual module which Vite invalidates on RSC module HMR.
28
31
  // When VERSION changes, this module re-evaluates and the cache is recreated empty.
29
- // Including VERSION in the key is additional defense against stale entries.
30
32
  const manifestModuleCache = new Map<string, Map<string, EntryData>>();
31
33
 
32
- /**
33
- * Load manifest from route entry with AsyncLocalStorage context
34
- * Handles lazy imports, unwrapping, and validation
35
- *
36
- * Results are cached at module level after first execution. Subsequent calls
37
- * for the same (routeKey, isSSR) within the same isolate return cached data
38
- * without re-executing the DSL handler.
39
- */
40
34
  /**
41
35
  * Clear the module-level manifest cache.
42
36
  * Called on HMR to ensure stale handler references are discarded.
@@ -65,9 +59,11 @@ export async function loadManifest(
65
59
 
66
60
  const mountIndex = entry.mountIndex;
67
61
 
68
- // Check module-level cache (persists across requests within same isolate)
62
+ // Check module-level cache (persists across requests within same isolate).
69
63
  // Include routerId so multi-router setups (host routing) don't share cached
70
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.
71
67
  const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
72
68
  const cached = manifestModuleCache.get(cacheKey);
73
69
  if (cached) {
@@ -88,20 +84,15 @@ export async function loadManifest(
88
84
  const storeSetupStart = performance.now();
89
85
  const Store = getContext().getOrCreateStore(routeKey);
90
86
 
91
- // Set mount index in store for unique shortCode prefixes
92
87
  Store.mountIndex = mountIndex;
93
-
94
- // Set isSSR flag so loading() can check if we're in SSR
95
88
  Store.isSSR = isSSR;
96
89
 
97
- // Attach metrics store to context if provided
98
90
  if (metricsStore) {
99
91
  Store.metrics = metricsStore;
100
92
  }
101
93
 
102
94
  pushMetric?.("manifest:store-setup", storeSetupStart);
103
95
 
104
- // Clear manifest before rebuilding to prevent stale entry mutations
105
96
  const clearStart = performance.now();
106
97
  Store.manifest.clear();
107
98
  pushMetric?.("manifest:clear", clearStart);
@@ -176,7 +167,10 @@ export async function loadManifest(
176
167
  if (entry.lazy && entry.lazyPatterns) {
177
168
  const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
178
169
  const includePrefix = (entry as any)._lazyPrefix || "";
179
- 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);
180
174
 
181
175
  if (fullPrefix || lazyContext?.namePrefix) {
182
176
  return runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () =>
@@ -186,20 +180,16 @@ export async function loadManifest(
186
180
  return lazyPatterns.handler();
187
181
  }
188
182
 
189
- // Wrap handler execution in root layout so routes get correct parent
190
- // This ensures all routes are registered with the layout as their parent
191
183
  let promiseResult: Promise<any> | null = null;
192
184
  const wrappedItems = helpers.layout(MapRootLayout, () => {
193
185
  const result = entry.handler();
194
186
  if (result instanceof Promise) {
195
- // Lazy handler detected - capture promise for async handling
196
187
  promiseResult = result;
197
- return []; // Return empty, we'll discard this wrapped result
188
+ return [];
198
189
  }
199
190
  return result;
200
191
  });
201
192
 
202
- // Handle lazy (Promise-based) handlers
203
193
  if (promiseResult !== null) {
204
194
  const load = await (promiseResult as Promise<any>);
205
195
  if (
@@ -233,7 +223,6 @@ export async function loadManifest(
233
223
  );
234
224
  }
235
225
 
236
- // Inline handler - routes were registered with correct parent inside layout
237
226
  return [wrappedItems].flat(3);
238
227
  },
239
228
  );