@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa

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 (411) hide show
  1. package/AGENTS.md +8 -4
  2. package/README.md +296 -887
  3. package/dist/bin/rango.js +459 -91
  4. package/dist/testing/vitest.js +36 -2
  5. package/dist/vite/index.js +1708 -414
  6. package/package.json +35 -10
  7. package/skills/api-client/SKILL.md +211 -0
  8. package/skills/breadcrumbs/SKILL.md +82 -5
  9. package/skills/bundle-analysis/SKILL.md +2 -2
  10. package/skills/cache-guide/SKILL.md +14 -9
  11. package/skills/caching/SKILL.md +221 -12
  12. package/skills/catalog.json +271 -0
  13. package/skills/comparison/SKILL.md +50 -0
  14. package/skills/comparison/agents/openai.yaml +4 -0
  15. package/skills/comparison/references/framework-comparison.md +837 -0
  16. package/skills/composability/SKILL.md +83 -2
  17. package/skills/css/SKILL.md +76 -0
  18. package/skills/debug-manifest/SKILL.md +5 -3
  19. package/skills/defer-hydration/SKILL.md +235 -0
  20. package/skills/document-cache/SKILL.md +11 -3
  21. package/skills/fonts/SKILL.md +1 -1
  22. package/skills/handler-use/SKILL.md +9 -9
  23. package/skills/hooks/SKILL.md +73 -900
  24. package/skills/hooks/data.md +273 -0
  25. package/skills/hooks/handle-and-actions.md +103 -0
  26. package/skills/hooks/navigation.md +110 -0
  27. package/skills/hooks/outlets.md +41 -0
  28. package/skills/hooks/state.md +228 -0
  29. package/skills/hooks/urls.md +135 -0
  30. package/skills/host-router/SKILL.md +84 -7
  31. package/skills/i18n/SKILL.md +1 -1
  32. package/skills/intercept/SKILL.md +51 -17
  33. package/skills/layout/SKILL.md +38 -16
  34. package/skills/links/SKILL.md +1 -1
  35. package/skills/loader/SKILL.md +48 -20
  36. package/skills/middleware/SKILL.md +11 -5
  37. package/skills/migrate-nextjs/SKILL.md +203 -20
  38. package/skills/migrate-react-router/SKILL.md +59 -675
  39. package/skills/migrate-react-router/cloudflare-workers.md +129 -0
  40. package/skills/migrate-react-router/component-migration.md +196 -0
  41. package/skills/migrate-react-router/data-and-actions.md +225 -0
  42. package/skills/migrate-react-router/route-mapping.md +271 -0
  43. package/skills/mime-routes/SKILL.md +3 -3
  44. package/skills/observability/SKILL.md +70 -5
  45. package/skills/parallel/SKILL.md +32 -8
  46. package/skills/ppr/SKILL.md +622 -0
  47. package/skills/prerender/SKILL.md +59 -28
  48. package/skills/rango/SKILL.md +124 -50
  49. package/skills/response-routes/SKILL.md +78 -46
  50. package/skills/route/SKILL.md +85 -6
  51. package/skills/router-setup/SKILL.md +41 -6
  52. package/skills/scripts/SKILL.md +179 -0
  53. package/skills/server-actions/SKILL.md +28 -3
  54. package/skills/shell-manifest/SKILL.md +185 -0
  55. package/skills/streams-and-websockets/SKILL.md +1 -1
  56. package/skills/tailwind/SKILL.md +28 -4
  57. package/skills/testing/SKILL.md +68 -654
  58. package/skills/testing/bindings.md +103 -0
  59. package/skills/testing/cache-prerender.md +127 -0
  60. package/skills/testing/client-components.md +124 -0
  61. package/skills/testing/e2e-parity.md +125 -0
  62. package/skills/testing/flight.md +91 -0
  63. package/skills/testing/handles.md +131 -0
  64. package/skills/testing/loader.md +128 -0
  65. package/skills/testing/middleware.md +99 -0
  66. package/skills/testing/render-handler.md +122 -0
  67. package/skills/testing/response-routes.md +95 -0
  68. package/skills/testing/reverse-and-types.md +85 -0
  69. package/skills/testing/server-actions.md +107 -0
  70. package/skills/testing/server-tree.md +128 -0
  71. package/skills/testing/setup.md +123 -0
  72. package/skills/theme/SKILL.md +1 -1
  73. package/skills/typesafety/SKILL.md +45 -918
  74. package/skills/typesafety/env-and-bindings.md +254 -0
  75. package/skills/typesafety/generated-files-and-cli.md +335 -0
  76. package/skills/typesafety/params-and-search.md +153 -0
  77. package/skills/typesafety/route-types.md +209 -0
  78. package/skills/use-cache/SKILL.md +47 -17
  79. package/skills/vercel/SKILL.md +128 -0
  80. package/skills/view-transitions/SKILL.md +44 -1
  81. package/src/__augment-tests__/augmented.check.ts +2 -3
  82. package/src/__internal.ts +0 -65
  83. package/src/browser/action-coordinator.ts +1 -1
  84. package/src/browser/action-fence.ts +47 -0
  85. package/src/browser/app-shell.ts +14 -27
  86. package/src/browser/connection-warmup.ts +134 -0
  87. package/src/browser/cookie-name.ts +140 -0
  88. package/src/browser/event-controller.ts +178 -100
  89. package/src/browser/invalidate-client-cache.ts +52 -0
  90. package/src/browser/logging.ts +28 -0
  91. package/src/browser/merge-segment-loaders.ts +6 -4
  92. package/src/browser/navigation-bridge.ts +81 -68
  93. package/src/browser/navigation-client.ts +115 -70
  94. package/src/browser/navigation-store-handle.ts +38 -0
  95. package/src/browser/navigation-store.ts +153 -88
  96. package/src/browser/navigation-transaction.ts +0 -32
  97. package/src/browser/network-error-handler.ts +34 -7
  98. package/src/browser/partial-update.ts +157 -144
  99. package/src/browser/prefetch/cache.ts +148 -81
  100. package/src/browser/prefetch/fetch.ts +231 -51
  101. package/src/browser/prefetch/queue.ts +25 -7
  102. package/src/browser/rango-state.ts +157 -115
  103. package/src/browser/react/Link.tsx +40 -7
  104. package/src/browser/react/NavigationProvider.tsx +140 -99
  105. package/src/browser/react/ScrollRestoration.tsx +10 -6
  106. package/src/browser/react/filter-segment-order.ts +17 -2
  107. package/src/browser/react/index.ts +0 -51
  108. package/src/browser/react/location-state-shared.ts +14 -15
  109. package/src/browser/react/location-state.ts +0 -1
  110. package/src/browser/react/use-action.ts +6 -15
  111. package/src/browser/react/use-handle.ts +0 -5
  112. package/src/browser/react/use-href.tsx +8 -1
  113. package/src/browser/react/use-link-status.ts +33 -8
  114. package/src/browser/react/use-navigation.ts +10 -5
  115. package/src/browser/react/use-params.ts +0 -2
  116. package/src/browser/react/use-router.ts +6 -4
  117. package/src/browser/react/use-search-params.ts +0 -5
  118. package/src/browser/react/use-segments.ts +0 -13
  119. package/src/browser/response-adapter.ts +74 -8
  120. package/src/browser/rsc-router.tsx +97 -22
  121. package/src/browser/scroll-restoration.ts +15 -8
  122. package/src/browser/segment-reconciler.ts +31 -21
  123. package/src/browser/server-action-bridge.ts +216 -38
  124. package/src/browser/types.ts +94 -22
  125. package/src/browser/validate-redirect-origin.ts +43 -16
  126. package/src/build/generate-manifest.ts +155 -131
  127. package/src/build/generate-route-types.ts +1 -1
  128. package/src/build/index.ts +11 -5
  129. package/src/build/prefix-tree-utils.ts +123 -0
  130. package/src/build/route-trie.ts +152 -22
  131. package/src/build/route-types/ast-route-extraction.ts +15 -8
  132. package/src/build/route-types/codegen.ts +12 -1
  133. package/src/build/route-types/include-resolution.ts +455 -61
  134. package/src/build/route-types/param-extraction.ts +6 -3
  135. package/src/build/route-types/per-module-writer.ts +15 -2
  136. package/src/build/route-types/router-processing.ts +77 -41
  137. package/src/build/route-types/source-scan.ts +105 -7
  138. package/src/build/runtime-discovery.ts +4 -1
  139. package/src/cache/cache-error.ts +104 -0
  140. package/src/cache/cache-key-utils.ts +58 -13
  141. package/src/cache/cache-policy.ts +108 -34
  142. package/src/cache/cache-runtime.ts +454 -101
  143. package/src/cache/cache-scope.ts +159 -54
  144. package/src/cache/cache-tag.ts +149 -0
  145. package/src/cache/cf/cf-base64.ts +33 -0
  146. package/src/cache/cf/cf-cache-constants.ts +127 -0
  147. package/src/cache/cf/cf-cache-store.ts +2170 -377
  148. package/src/cache/cf/cf-cache-types.ts +349 -0
  149. package/src/cache/cf/cf-kv-utils.ts +46 -0
  150. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  151. package/src/cache/cf/index.ts +6 -16
  152. package/src/cache/document-cache.ts +126 -41
  153. package/src/cache/handle-snapshot.ts +70 -0
  154. package/src/cache/index.ts +23 -20
  155. package/src/cache/memory-segment-store.ts +243 -37
  156. package/src/cache/profile-registry.ts +46 -31
  157. package/src/cache/read-through-swr.ts +56 -12
  158. package/src/cache/segment-codec.ts +13 -21
  159. package/src/cache/shell-snapshot.ts +417 -0
  160. package/src/cache/tag-invalidation.ts +230 -0
  161. package/src/cache/types.ts +194 -99
  162. package/src/cache/vercel/index.ts +11 -0
  163. package/src/cache/vercel/vercel-cache-store.ts +1132 -0
  164. package/src/client.rsc.tsx +39 -22
  165. package/src/client.tsx +28 -58
  166. package/src/cloudflare/index.ts +11 -0
  167. package/src/cloudflare/tracing.ts +108 -0
  168. package/src/component-utils.ts +19 -0
  169. package/src/components/DefaultDocument.tsx +8 -2
  170. package/src/context-var.ts +13 -1
  171. package/src/decode-loader-results.ts +18 -2
  172. package/src/defer.ts +185 -0
  173. package/src/deps/ssr.ts +0 -1
  174. package/src/encode-kv.ts +49 -0
  175. package/src/errors.ts +0 -3
  176. package/src/escape-script.ts +52 -0
  177. package/src/handle.ts +57 -40
  178. package/src/handles/MetaTags.tsx +24 -53
  179. package/src/handles/Scripts.tsx +183 -0
  180. package/src/handles/breadcrumbs.ts +35 -8
  181. package/src/handles/deferred-resolution.ts +127 -0
  182. package/src/handles/is-thenable.ts +18 -0
  183. package/src/handles/meta.ts +14 -40
  184. package/src/handles/script.ts +244 -0
  185. package/src/host/cookie-handler.ts +9 -60
  186. package/src/host/errors.ts +13 -22
  187. package/src/host/index.ts +7 -0
  188. package/src/host/pattern-matcher.ts +23 -52
  189. package/src/host/router.ts +1 -65
  190. package/src/host/testing.ts +40 -27
  191. package/src/host/types.ts +6 -2
  192. package/src/href-client.ts +7 -12
  193. package/src/index.rsc.ts +88 -8
  194. package/src/index.ts +90 -16
  195. package/src/internal-debug.ts +11 -10
  196. package/src/loader.rsc.ts +19 -9
  197. package/src/loader.ts +12 -4
  198. package/src/outlet-provider.tsx +1 -5
  199. package/src/prerender/param-hash.ts +16 -16
  200. package/src/prerender/store.ts +32 -37
  201. package/src/prerender.ts +75 -7
  202. package/src/redirect-origin.ts +114 -0
  203. package/src/regex-escape.ts +8 -0
  204. package/src/render-error-thrower.tsx +20 -0
  205. package/src/response-utils.ts +25 -0
  206. package/src/root-error-boundary.tsx +1 -19
  207. package/src/route-content-wrapper.tsx +13 -49
  208. package/src/route-definition/dsl-helpers.ts +60 -53
  209. package/src/route-definition/helper-factories.ts +0 -2
  210. package/src/route-definition/helpers-types.ts +46 -46
  211. package/src/route-definition/index.ts +1 -2
  212. package/src/route-definition/redirect.ts +44 -11
  213. package/src/route-definition/resolve-handler-use.ts +6 -1
  214. package/src/route-definition/use-item-types.ts +3 -6
  215. package/src/route-map-builder.ts +41 -20
  216. package/src/route-types.ts +0 -5
  217. package/src/router/content-negotiation.ts +58 -23
  218. package/src/router/error-handling.ts +44 -17
  219. package/src/router/find-match.ts +129 -30
  220. package/src/router/handler-context.ts +6 -1
  221. package/src/router/instrument.ts +355 -0
  222. package/src/router/intercept-resolution.ts +35 -2
  223. package/src/router/lazy-includes.ts +79 -56
  224. package/src/router/loader-resolution.ts +151 -73
  225. package/src/router/logging.ts +0 -6
  226. package/src/router/manifest.ts +74 -40
  227. package/src/router/match-api.ts +76 -52
  228. package/src/router/match-context.ts +0 -22
  229. package/src/router/match-handlers.ts +181 -178
  230. package/src/router/match-middleware/background-revalidation.ts +40 -24
  231. package/src/router/match-middleware/cache-lookup.ts +115 -194
  232. package/src/router/match-middleware/cache-store.ts +61 -50
  233. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  234. package/src/router/match-middleware/segment-resolution.ts +0 -22
  235. package/src/router/match-pipelines.ts +1 -42
  236. package/src/router/match-result.ts +36 -67
  237. package/src/router/metrics.ts +0 -34
  238. package/src/router/middleware-types.ts +0 -116
  239. package/src/router/middleware.ts +231 -120
  240. package/src/router/navigation-snapshot.ts +7 -56
  241. package/src/router/params-util.ts +23 -0
  242. package/src/router/parse-pattern.ts +115 -0
  243. package/src/router/pattern-matching.ts +99 -152
  244. package/src/router/prefetch-cache-ttl.ts +51 -0
  245. package/src/router/prefetch-limits.ts +37 -0
  246. package/src/router/prerender-match.ts +111 -66
  247. package/src/router/preview-match.ts +3 -1
  248. package/src/router/request-classification.ts +47 -42
  249. package/src/router/revalidation.ts +75 -81
  250. package/src/router/route-snapshot.ts +14 -3
  251. package/src/router/router-context.ts +6 -29
  252. package/src/router/router-interfaces.ts +70 -8
  253. package/src/router/router-options.ts +126 -4
  254. package/src/router/segment-resolution/fresh.ts +104 -80
  255. package/src/router/segment-resolution/helpers.ts +86 -6
  256. package/src/router/segment-resolution/loader-cache.ts +155 -39
  257. package/src/router/segment-resolution/loader-mask.ts +60 -0
  258. package/src/router/segment-resolution/loader-snapshot.ts +259 -0
  259. package/src/router/segment-resolution/mask-nested.ts +83 -0
  260. package/src/router/segment-resolution/revalidation.ts +215 -304
  261. package/src/router/segment-resolution/static-store.ts +19 -5
  262. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  263. package/src/router/segment-resolution/view-transition-default.ts +35 -15
  264. package/src/router/segment-resolution.ts +5 -1
  265. package/src/router/segment-wrappers.ts +6 -5
  266. package/src/router/state-cookie-name.ts +33 -0
  267. package/src/router/substitute-pattern-params.ts +54 -35
  268. package/src/router/telemetry-otel.ts +160 -200
  269. package/src/router/telemetry.ts +9 -23
  270. package/src/router/timeout.ts +0 -20
  271. package/src/router/tracing.ts +215 -0
  272. package/src/router/trie-matching.ts +171 -64
  273. package/src/router/types.ts +1 -63
  274. package/src/router/url-params.ts +13 -5
  275. package/src/router.ts +119 -48
  276. package/src/rsc/full-payload.ts +70 -0
  277. package/src/rsc/handler-context.ts +1 -0
  278. package/src/rsc/handler.ts +267 -152
  279. package/src/rsc/helpers.ts +78 -4
  280. package/src/rsc/index.ts +1 -4
  281. package/src/rsc/json-route-result.ts +38 -0
  282. package/src/rsc/loader-fetch.ts +114 -38
  283. package/src/rsc/manifest-init.ts +29 -42
  284. package/src/rsc/nonce.ts +10 -1
  285. package/src/rsc/origin-guard.ts +11 -15
  286. package/src/rsc/progressive-enhancement.ts +120 -13
  287. package/src/rsc/redirect-guard.ts +100 -0
  288. package/src/rsc/response-cache-serve.ts +238 -0
  289. package/src/rsc/response-error.ts +79 -12
  290. package/src/rsc/response-route-handler.ts +58 -141
  291. package/src/rsc/rsc-rendering.ts +492 -49
  292. package/src/rsc/runtime-warnings.ts +14 -0
  293. package/src/rsc/server-action.ts +268 -82
  294. package/src/rsc/shell-capture.ts +1190 -0
  295. package/src/rsc/shell-serve.ts +181 -0
  296. package/src/rsc/transition-gate.ts +89 -0
  297. package/src/rsc/types.ts +45 -3
  298. package/src/runtime-env.ts +18 -0
  299. package/src/search-params.ts +31 -26
  300. package/src/segment-loader-promise.ts +49 -4
  301. package/src/segment-system.tsx +260 -95
  302. package/src/server/context.ts +99 -9
  303. package/src/server/cookie-parse.ts +32 -0
  304. package/src/server/cookie-store.ts +125 -2
  305. package/src/server/handle-store.ts +21 -38
  306. package/src/server/loader-registry.ts +33 -42
  307. package/src/server/request-context.ts +379 -138
  308. package/src/ssr/index.tsx +491 -182
  309. package/src/ssr/inject-rsc-eager.ts +167 -0
  310. package/src/ssr/ssr-root.tsx +228 -0
  311. package/src/static-handler.ts +10 -13
  312. package/src/testing/cache-status.ts +44 -48
  313. package/src/testing/collect-handle.ts +14 -31
  314. package/src/testing/dispatch.ts +533 -160
  315. package/src/testing/e2e/fixture.ts +45 -11
  316. package/src/testing/e2e/index.ts +1 -22
  317. package/src/testing/e2e/matchers.ts +0 -16
  318. package/src/testing/e2e/parity.ts +85 -4
  319. package/src/testing/e2e/server.ts +12 -0
  320. package/src/testing/flight-matchers.ts +7 -14
  321. package/src/testing/flight-normalize.ts +11 -0
  322. package/src/testing/flight-runtime.d.ts +36 -0
  323. package/src/testing/flight-tree.ts +682 -0
  324. package/src/testing/flight.entry.ts +30 -0
  325. package/src/testing/flight.ts +145 -70
  326. package/src/testing/generated-routes.ts +26 -50
  327. package/src/testing/index.ts +18 -19
  328. package/src/testing/internal/context.ts +184 -68
  329. package/src/testing/internal/flight-client-globals.ts +30 -0
  330. package/src/testing/internal/seed-vars.ts +54 -0
  331. package/src/testing/render-handler.ts +357 -0
  332. package/src/testing/render-route.tsx +134 -115
  333. package/src/testing/run-loader.ts +140 -51
  334. package/src/testing/run-middleware.ts +59 -33
  335. package/src/testing/run-transition-when.ts +164 -0
  336. package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
  337. package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
  338. package/src/testing/vitest.ts +138 -16
  339. package/src/theme/ThemeProvider.tsx +56 -84
  340. package/src/theme/ThemeScript.tsx +7 -9
  341. package/src/theme/constants.ts +52 -13
  342. package/src/theme/index.ts +0 -7
  343. package/src/theme/theme-context.ts +1 -5
  344. package/src/theme/theme-script.ts +22 -21
  345. package/src/theme/use-theme.ts +0 -3
  346. package/src/types/boundaries.ts +0 -35
  347. package/src/types/cache-types.ts +13 -4
  348. package/src/types/error-types.ts +30 -90
  349. package/src/types/global-namespace.ts +15 -15
  350. package/src/types/handler-context.ts +45 -15
  351. package/src/types/index.ts +2 -10
  352. package/src/types/loader-types.ts +6 -3
  353. package/src/types/request-scope.ts +8 -22
  354. package/src/types/route-config.ts +20 -52
  355. package/src/types/route-entry.ts +0 -6
  356. package/src/types/segments.ts +100 -13
  357. package/src/urls/include-helper.ts +10 -12
  358. package/src/urls/include-provider.ts +71 -0
  359. package/src/urls/index.ts +2 -8
  360. package/src/urls/path-helper-types.ts +52 -14
  361. package/src/urls/path-helper.ts +5 -54
  362. package/src/urls/pattern-types.ts +36 -0
  363. package/src/urls/type-extraction.ts +76 -42
  364. package/src/urls/urls-function.ts +0 -14
  365. package/src/use-loader.tsx +0 -186
  366. package/src/vercel/index.ts +11 -0
  367. package/src/vercel/tracing.ts +88 -0
  368. package/src/vite/discovery/bundle-postprocess.ts +2 -1
  369. package/src/vite/discovery/dev-prerender-cache.ts +117 -0
  370. package/src/vite/discovery/discover-routers.ts +34 -43
  371. package/src/vite/discovery/discovery-errors.ts +61 -0
  372. package/src/vite/discovery/prerender-collection.ts +33 -46
  373. package/src/vite/discovery/state.ts +12 -1
  374. package/src/vite/discovery/virtual-module-codegen.ts +1 -11
  375. package/src/vite/index.ts +9 -0
  376. package/src/vite/inject-client-debug.ts +88 -0
  377. package/src/vite/plugin-types.ts +143 -10
  378. package/src/vite/plugins/cjs-to-esm.ts +8 -12
  379. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  380. package/src/vite/plugins/client-ref-hashing.ts +0 -10
  381. package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
  382. package/src/vite/plugins/expose-action-id.ts +2 -73
  383. package/src/vite/plugins/expose-id-utils.ts +85 -56
  384. package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
  385. package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
  386. package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
  387. package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
  388. package/src/vite/plugins/expose-internal-ids.ts +10 -1
  389. package/src/vite/plugins/performance-tracks.ts +0 -3
  390. package/src/vite/plugins/refresh-cmd.ts +1 -1
  391. package/src/vite/plugins/use-cache-transform.ts +21 -46
  392. package/src/vite/plugins/vercel-output.ts +384 -0
  393. package/src/vite/plugins/version-injector.ts +22 -27
  394. package/src/vite/plugins/version-plugin.ts +6 -66
  395. package/src/vite/plugins/virtual-entries.ts +137 -26
  396. package/src/vite/rango.ts +146 -135
  397. package/src/vite/router-discovery.ts +189 -48
  398. package/src/vite/utils/ast-handler-extract.ts +11 -20
  399. package/src/vite/utils/bundle-analysis.ts +6 -13
  400. package/src/vite/utils/client-chunks.ts +0 -6
  401. package/src/vite/utils/directive-prologue.ts +40 -0
  402. package/src/vite/utils/forward-user-plugins.ts +0 -22
  403. package/src/vite/utils/manifest-utils.ts +4 -75
  404. package/src/vite/utils/package-resolution.ts +1 -73
  405. package/src/vite/utils/prerender-utils.ts +71 -44
  406. package/src/vite/utils/shared-utils.ts +55 -37
  407. package/src/browser/react/use-client-cache.ts +0 -58
  408. package/src/browser/shallow.ts +0 -40
  409. package/src/handles/index.ts +0 -7
  410. package/src/network-error-thrower.tsx +0 -23
  411. package/src/router/middleware-cookies.ts +0 -55
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { createElement, type ReactNode, type ComponentType } from "react";
3
- import { OutletProvider } from "./client.js";
3
+ import { OutletProvider } from "./outlet-provider.js";
4
4
  import { MountContextProvider } from "./browser/react/mount-context.js";
5
5
  import type { ResolvedSegment, RootLayoutProps } from "./types.js";
6
6
  import { decodeLoaderResults } from "./decode-loader-results.js";
@@ -10,17 +10,58 @@ import {
10
10
  LoaderBoundary,
11
11
  } from "./route-content-wrapper.js";
12
12
  import { RootErrorBoundary } from "./root-error-boundary.js";
13
+ import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
13
14
  import { getMemoizedContentPromise } from "./segment-content-promise.js";
14
- import { getMemoizedLoaderPromise } from "./segment-loader-promise.js";
15
+ import {
16
+ buildLoaderPromise,
17
+ getMemoizedLoaderPromise,
18
+ } from "./segment-loader-promise.js";
19
+
20
+ /**
21
+ * Debug log for the segment tree build, gated on the baked flag. Runs on BOTH
22
+ * sides now, environment-tagged: `[Browser][segments]` lines up with the
23
+ * `[Browser][boot]` sequence around hydrateRoot; `[Server][segments]` exposes
24
+ * the SSR/RSC tree-build stalls (blocking loader awaits during fizz are what
25
+ * dominate MISS TTFB) that used to be invisible because the logs were
26
+ * window-gated. Server lines have no request correlation — segment-system is
27
+ * shared client code and cannot import request-context (node:async_hooks
28
+ * would enter the browser bundle) — so on a busy server, correlate by
29
+ * timestamp + segment ids.
30
+ */
31
+ function segDebugLog(msg: string, details?: Record<string, unknown>): void {
32
+ if (!INTERNAL_RANGO_DEBUG) return;
33
+ const env = typeof window === "object" ? "[Browser]" : "[Server]";
34
+ const prefix = `${env}[segments] ${msg} @ ${Math.round(performance.now())}ms`;
35
+ if (details) {
36
+ console.log(prefix, details);
37
+ return;
38
+ }
39
+ console.log(prefix);
40
+ }
15
41
 
16
42
  // ViewTransition is only available in React experimental.
17
43
  // Access via namespace import to avoid compile-time errors on stable React.
18
44
  const ReactViewTransition: any =
19
45
  "ViewTransition" in React ? (React as any).ViewTransition : null;
20
46
 
21
- function restoreParallelLoaderMarkers(
47
+ // A loading skeleton is renderable only when it is a real ReactNode value.
48
+ // `false` is treated as "not renderable" here. This is the three-term gate;
49
+ // the distinct two-term gate at the LoaderBoundary site deliberately treats
50
+ // `false` as "create a boundary without a RouteContentWrapper"
51
+ // (tree-structure.md), so it must NOT use this helper.
52
+ function isRenderableLoading(loading: ReactNode): boolean {
53
+ return loading !== undefined && loading !== null && loading !== false;
54
+ }
55
+
56
+ // Exported for unit testing the no-parallel fast path (D6); internal otherwise.
57
+ export function restoreParallelLoaderMarkers(
22
58
  segments: ResolvedSegment[],
23
59
  ): ResolvedSegment[] {
60
+ // Parallel-loading markers only exist when a parallel segment is present, so
61
+ // a list with no parallel slot has nothing to restore. Skip the Map alloc and
62
+ // full scan in that (common) case — this runs on every render.
63
+ if (!segments.some((s) => s.type === "parallel")) return segments;
64
+
24
65
  const parallelLoadingByNamespace = new Map<string, ReactNode>();
25
66
  let nextSegments: ResolvedSegment[] | null = null;
26
67
 
@@ -28,12 +69,7 @@ function restoreParallelLoaderMarkers(
28
69
  const segment = segments[i];
29
70
 
30
71
  if (segment.type === "parallel") {
31
- if (
32
- segment.namespace &&
33
- segment.loading !== undefined &&
34
- segment.loading !== null &&
35
- segment.loading !== false
36
- ) {
72
+ if (segment.namespace && isRenderableLoading(segment.loading)) {
37
73
  parallelLoadingByNamespace.set(segment.namespace, segment.loading);
38
74
  }
39
75
  continue;
@@ -142,8 +178,14 @@ function wrapDefaultOutletContent(
142
178
  /**
143
179
  * Render segments into a React tree with proper layout nesting
144
180
  *
145
- * Layouts nest using OutletProvider, while route + parallel + error + notFound segments
146
- * render as siblings in a Fragment.
181
+ * Layouts nest using OutletProvider; a layout receives the inner content via
182
+ * its `<Outlet />`. Parallel segments do NOT render as inline Fragment siblings
183
+ * — they flow through OutletContext.parallel and are resolved where a layout
184
+ * places `<ParallelOutlet name="@sidebar" />` (or `<Outlet name="@sidebar" />`).
185
+ *
186
+ * The result is always wrapped in RootErrorBoundary so unhandled errors never
187
+ * blank the screen. When `options.rootLayout` is provided it wraps the error
188
+ * boundary at the OUTERMOST level (so the app shell survives errors).
147
189
  *
148
190
  * Error segments are treated like route segments - they render their fallback
149
191
  * component in place of the failed segment. When an error occurs in a handler,
@@ -155,27 +197,30 @@ function wrapDefaultOutletContent(
155
197
  * notFoundBoundary's fallback component.
156
198
  *
157
199
  * @param segments - Array of resolved segments to render
158
- * @returns ReactNode representing the component tree
200
+ * @returns Promise resolving to the ReactNode tree (the function is async)
159
201
  *
160
202
  * @example
161
203
  * ```typescript
162
204
  * const segments = [
163
- * { id: 'L0.0', type: 'layout', component: <RootLayout /> },
164
- * { id: 'L1.0', type: 'layout', component: <BlogLayout /> },
165
- * { id: 'R2.0', type: 'route', component: <BlogPost /> },
166
- * { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
205
+ * { id: 'L0.0', type: 'layout', component: <BlogLayout /> },
206
+ * { id: 'L0R1', type: 'route', component: <BlogPost /> },
207
+ * { id: 'L0R1.@sidebar', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
167
208
  * ];
168
209
  *
169
- * const tree = renderSegments(segments);
170
- * // Results in:
171
- * // <OutletProvider><RootLayout>
172
- * // <OutletProvider><BlogLayout>
173
- * // <><BlogPost /><Sidebar /></>
174
- * // </BlogLayout></OutletProvider>
175
- * // </RootLayout></OutletProvider>
210
+ * // BlogLayout renders <Outlet /> for the route and
211
+ * // <ParallelOutlet name="@sidebar" /> for the parallel slot.
212
+ * const tree = await renderSegments(segments, { rootLayout: RootLayout });
213
+ * // Results in (outermost first):
214
+ * // <RootLayout>
215
+ * // <RootErrorBoundary>
216
+ * // <OutletProvider segment={BlogLayout} parallel={[Sidebar]}>
217
+ * // <BlogPost />
218
+ * // </OutletProvider>
219
+ * // </RootErrorBoundary>
220
+ * // </RootLayout>
176
221
  *
177
222
  * // For server actions, pass isAction to await components:
178
- * const tree = renderSegments(segments, { isAction: true });
223
+ * const tree = await renderSegments(segments, { isAction: true });
179
224
  * ```
180
225
  */
181
226
  export async function renderSegments(
@@ -189,6 +234,17 @@ export async function renderSegments(
189
234
  rootLayout: RootLayout,
190
235
  } = options || {};
191
236
 
237
+ const segDebug = INTERNAL_RANGO_DEBUG;
238
+ const segDebugStart = segDebug ? performance.now() : 0;
239
+ if (segDebug) {
240
+ segDebugLog("renderSegments start", {
241
+ segments: segments.map((s) => `${s.id}:${s.type}`),
242
+ isAction: !!isAction,
243
+ forceAwait: !!forceAwait,
244
+ intercepts: interceptSegments?.length ?? 0,
245
+ });
246
+ }
247
+
192
248
  const temporalLazyRefs: Promise<any>[] = [];
193
249
  const normalizedSegments = restoreParallelLoaderMarkers(segments);
194
250
  const normalizedInterceptSegments = interceptSegments
@@ -249,6 +305,7 @@ export async function renderSegments(
249
305
  `Expected layout, route, error, or notFound segment, got ${node.segment.type}`,
250
306
  );
251
307
  const { component, id, params, loading } = node.segment;
308
+ const segNodeStart = segDebug ? performance.now() : 0;
252
309
 
253
310
  // Param-agnostic keys are opt-in via the transition() DSL (see
254
311
  // inTransitionScope above). A route (and its route-owned layouts) inside a
@@ -283,31 +340,71 @@ export async function renderSegments(
283
340
  .map(([k, v]) => `${k}=${v}`)
284
341
  .join(",")
285
342
  : "";
286
- const key = `${paramStr ? `${id}-${paramStr}` : id}`;
343
+ const key = paramStr ? `${id}-${paramStr}` : id;
287
344
 
288
- // Get loader entries for this node
289
345
  const loaderEntries = node.loaders.filter(
290
346
  (loader) => loader.loaderId && loader.loaderData !== undefined,
291
347
  );
292
348
 
293
- // Determine the component content (with or without Suspense wrapper)
294
- // Wrap when loading skeleton defined OR component is Promise (needs Suspense)
295
- // During actions, await component Promise to prevent Suspense from triggering
296
- // This keeps existing content visible instead of showing loading skeleton
297
349
  let resolvedComponent = component;
298
350
  if (isAction && component instanceof Promise) {
351
+ const componentAwaitStart = segDebug ? performance.now() : 0;
299
352
  resolvedComponent = await component;
353
+ if (segDebug) {
354
+ segDebugLog(`segment ${id}: component awaited (action)`, {
355
+ ms: Math.round(performance.now() - componentAwaitStart),
356
+ });
357
+ }
300
358
  }
301
359
 
302
- let nodeContent: ReactNode =
303
- loading !== null && loading !== undefined && loading !== false
304
- ? createElement(RouteContentWrapper, {
305
- key: `suspense-loading-${id}`,
306
- content: getMemoizedContentPromise(resolvedComponent),
307
- fallback: loading,
308
- segmentId: id,
309
- })
310
- : registerLazyRef(resolvedComponent);
360
+ let nodeContent: ReactNode = null;
361
+ if (isRenderableLoading(loading)) {
362
+ // forceAwait (popstate, stale-revalidation, fully-prefetched nav) renders a
363
+ // loading() route with the route content ALREADY resolved, so its
364
+ // RouteContentWrapper Suspender does not suspend for a microtask and flash
365
+ // the loading() fallback on a NORMAL (non-transition) commit. The router
366
+ // data is known-ready on these paths, so awaiting the content here is free.
367
+ // The wrapper tree is unchanged (RouteContentWrapper is still created with
368
+ // the same key/fallback) — only the `content` prop is a resolved node
369
+ // instead of a pending promise, which Suspender renders synchronously. This
370
+ // mirrors the forceAwait loaderData unwrap above; a CLIENT component that
371
+ // suspends on mount inside the content still reveals a fallback (it is not
372
+ // pre-resolved).
373
+ const contentPromise = getMemoizedContentPromise(resolvedComponent);
374
+ let loadingContent: Promise<ReactNode> | ReactNode = contentPromise;
375
+ if (forceAwait) {
376
+ const contentAwaitStart = segDebug ? performance.now() : 0;
377
+ loadingContent = await contentPromise;
378
+ if (segDebug) {
379
+ segDebugLog(`segment ${id}: content awaited (forceAwait)`, {
380
+ ms: Math.round(performance.now() - contentAwaitStart),
381
+ });
382
+ }
383
+ }
384
+ nodeContent = createElement(RouteContentWrapper, {
385
+ key: `suspense-loading-${id}`,
386
+ content: loadingContent,
387
+ fallback: loading,
388
+ segmentId: id,
389
+ });
390
+ } else {
391
+ // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. A segment in the no-loading()
392
+ // branch whose component decodes as a Promise/lazy gets registered into
393
+ // temporalLazyRefs and awaited before commit (see below) — which on builds
394
+ // where the segment component arrives deferred defeats client-nav streaming.
395
+ if (INTERNAL_RANGO_DEBUG && typeof window === "object") {
396
+ const c = resolvedComponent as unknown;
397
+ console.log("[VT-DIAG] renderSegments no-loading-branch segment", {
398
+ id,
399
+ type: node.segment.type,
400
+ componentIsPromise: c instanceof Promise,
401
+ componentIsLazy:
402
+ c != null && typeof c === "object" && "_payload" in c,
403
+ componentTypeof: typeof c,
404
+ });
405
+ }
406
+ nodeContent = registerLazyRef(resolvedComponent);
407
+ }
311
408
 
312
409
  // Wrap with <ViewTransition> if transition config exists (React experimental only).
313
410
  // An empty config ({}) creates a bare <ViewTransition> boundary that participates
@@ -351,18 +448,27 @@ export async function renderSegments(
351
448
  // Prepare loader data if there are loaders
352
449
  const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
353
450
 
354
- // Use LoaderBoundary when loading is defined to maintain consistent tree structure
355
- // This ensures cached segments (which may not have loader segments) have the same
356
- // tree structure as fresh segments, preventing React remounts
357
- // If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
358
451
  if (loading !== undefined && loading !== null) {
359
- // Aggregate built here only — the loaderless and no-loading branches don't
360
- // read it (the latter builds its own per-parallel promises).
361
452
  const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
453
+ let boundaryLoaderData: Promise<any[]> | any[] = loaderDataPromise;
454
+ if (forceAwait || isAction) {
455
+ const awaitStart = segDebug ? performance.now() : 0;
456
+ boundaryLoaderData = await loaderDataPromise;
457
+ if (segDebug) {
458
+ segDebugLog(`segment ${id}: loaders awaited (forceAwait/action)`, {
459
+ loaderIds,
460
+ ms: Math.round(performance.now() - awaitStart),
461
+ });
462
+ }
463
+ } else if (segDebug) {
464
+ segDebugLog(
465
+ `segment ${id}: streaming loaders via LoaderBoundary (suspense)`,
466
+ { loaderIds },
467
+ );
468
+ }
362
469
  content = createElement(LoaderBoundary, {
363
470
  key: `loader-boundary-${key}`,
364
- loaderDataPromise:
365
- forceAwait || isAction ? await loaderDataPromise : loaderDataPromise,
471
+ loaderDataPromise: boundaryLoaderData,
366
472
  loaderIds,
367
473
  fallback: loading,
368
474
  outletKey: key,
@@ -372,7 +478,6 @@ export async function renderSegments(
372
478
  children: nodeContent,
373
479
  });
374
480
  } else if (loaderEntries.length === 0) {
375
- // No loaders, no loading - simple OutletProvider
376
481
  content = createElement(OutletProvider, {
377
482
  key,
378
483
  content: outletContent,
@@ -381,34 +486,37 @@ export async function renderSegments(
381
486
  children: nodeContent,
382
487
  });
383
488
  } else {
384
- // Has loaders but no loading skeleton.
385
- // Split: parallel-owned loaders stream (their parallel has loading()),
386
- // layout-owned loaders are awaited (they gate the layout content).
387
489
  const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
388
490
  const parallelOwnedLoaders = loaderEntries.filter(
389
491
  (l) => !!l.parallelLoading,
390
492
  );
391
493
 
392
- // Await only layout-owned loaders
393
494
  const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
394
- const layoutLoaderDataPromise =
395
- layoutLoaders.length > 0
396
- ? Promise.all(
397
- layoutLoaders.map((l) =>
398
- l.loaderData instanceof Promise
399
- ? l.loaderData
400
- : Promise.resolve(l.loaderData),
401
- ),
402
- )
403
- : Promise.resolve([]);
404
- const resolvedData = await layoutLoaderDataPromise;
495
+ // No loading() on this segment, so its loader data cannot stream behind
496
+ // a Suspense fallback — the tree build BLOCKS here until the data
497
+ // arrives. On the initial document this await runs before hydrateRoot.
498
+ const layoutAwaitStart = segDebug ? performance.now() : 0;
499
+ const resolvedData = await buildLoaderPromise(layoutLoaders);
500
+ if (segDebug) {
501
+ segDebugLog(`segment ${id}: layout loaders awaited (blocking)`, {
502
+ loaderIds: layoutLoaderIds,
503
+ ms: Math.round(performance.now() - layoutAwaitStart),
504
+ });
505
+ }
506
+ const decodeStart = segDebug ? performance.now() : 0;
405
507
  const { loaderData, errorFallback } = decodeLoaderResults(
406
508
  resolvedData,
407
509
  layoutLoaderIds,
408
510
  );
511
+ if (segDebug) {
512
+ const decodeMs = Math.round(performance.now() - decodeStart);
513
+ if (decodeMs > 0) {
514
+ segDebugLog(`segment ${id}: loader results decoded`, {
515
+ ms: decodeMs,
516
+ });
517
+ }
518
+ }
409
519
 
410
- // Parallel-owned loaders: attach to their owning parallel segment
411
- // as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
412
520
  if (parallelOwnedLoaders.length > 0) {
413
521
  const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
414
522
 
@@ -436,10 +544,27 @@ export async function renderSegments(
436
544
 
437
545
  p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
438
546
  const aggregated = getMemoizedLoaderPromise(ownedLoaders);
439
- p.loaderDataPromise =
440
- (forceAwait || isAction) && aggregated instanceof Promise
441
- ? await aggregated
442
- : aggregated;
547
+ if ((forceAwait || isAction) && aggregated instanceof Promise) {
548
+ const parallelAwaitStart = segDebug ? performance.now() : 0;
549
+ p.loaderDataPromise = await aggregated;
550
+ if (segDebug) {
551
+ segDebugLog(
552
+ `segment ${id}: parallel ${p.id} loaders awaited (forceAwait/action)`,
553
+ {
554
+ loaderIds: p.loaderIds,
555
+ ms: Math.round(performance.now() - parallelAwaitStart),
556
+ },
557
+ );
558
+ }
559
+ } else {
560
+ p.loaderDataPromise = aggregated;
561
+ if (segDebug) {
562
+ segDebugLog(
563
+ `segment ${id}: parallel ${p.id} loaders streaming (suspense)`,
564
+ { loaderIds: p.loaderIds },
565
+ );
566
+ }
567
+ }
443
568
  }
444
569
  }
445
570
 
@@ -463,28 +588,56 @@ export async function renderSegments(
463
588
  children: content,
464
589
  });
465
590
  }
591
+
592
+ if (segDebug) {
593
+ segDebugLog(`segment ${id} built`, {
594
+ type: node.segment.type,
595
+ ms: Math.round(performance.now() - segNodeStart),
596
+ loaders: node.loaders.map((l) => l.loaderId).filter(Boolean),
597
+ hasLoading: loading !== undefined && loading !== null,
598
+ parallel: node.parallel.map((p) => p.id),
599
+ });
600
+ }
466
601
  }
467
602
 
468
- // Always wrap with root error boundary to prevent white screens
469
- // This catches any unhandled errors that bubble up from the segment tree
470
603
  const errorBoundaryWrapped = createElement(RootErrorBoundary, {
471
604
  children: content,
472
605
  });
473
606
  if (typeof window === "object") {
607
+ // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. If this await dominates the
608
+ // navigation time, a deferred/lazy segment component is being fully resolved
609
+ // before commit, which defeats client-nav streaming. The await itself is
610
+ // functional (it preloads lazy chunk refs); only the timing log is gated.
611
+ const vtDebug = INTERNAL_RANGO_DEBUG && temporalLazyRefs.length > 0;
612
+ const vtDebugStart = vtDebug ? performance.now() : 0;
613
+ if (vtDebug) {
614
+ console.log("[VT-DIAG] renderSegments awaiting temporalLazyRefs", {
615
+ count: temporalLazyRefs.length,
616
+ });
617
+ }
474
618
  await Promise.allSettled(temporalLazyRefs);
619
+ if (vtDebug) {
620
+ console.log("[VT-DIAG] renderSegments temporalLazyRefs settled", {
621
+ count: temporalLazyRefs.length,
622
+ ms: Math.round(performance.now() - vtDebugStart),
623
+ });
624
+ }
475
625
  }
476
626
 
477
- // Build the final result, optionally wrapped with root layout
478
627
  let result: ReactNode = errorBoundaryWrapped;
479
628
 
480
- // If rootLayout is provided, wrap the error boundary with it
481
- // This ensures the app shell stays mounted even during errors (prevents FOUC)
482
629
  if (RootLayout) {
483
630
  result = createElement(RootLayout, {
484
631
  children: errorBoundaryWrapped,
485
632
  });
486
633
  }
487
634
 
635
+ if (segDebug) {
636
+ segDebugLog("renderSegments complete", {
637
+ ms: Math.round(performance.now() - segDebugStart),
638
+ });
639
+ }
640
+
488
641
  return result;
489
642
  }
490
643
 
@@ -516,6 +669,31 @@ export async function renderSegments(
516
669
  * @param segments - Main segments from the route tree
517
670
  * @param interceptSegments - Optional intercept segments to inject
518
671
  */
672
+ // Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
673
+ // parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
674
+ // loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
675
+ // slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
676
+ // (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
677
+ // is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
678
+ // dropped the loader's data. The first-`D<index>.` strip is only correct because
679
+ // slot names cannot contain "." -- assertValidSlotName (route-definition/
680
+ // dsl-helpers.ts) rejects a "." at definition time, so a name like "@D3.foo"
681
+ // (which WOULD mis-cut here) can never reach this function.
682
+ function loaderParentId(loaderSegmentId: string): string {
683
+ return loaderSegmentId.replace(/D\d+\..*$/, "");
684
+ }
685
+
686
+ // Append a value to the array stored under `key`, creating the array on first
687
+ // use. Single Map lookup (vs the has/get!().push double-lookup idiom).
688
+ function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
689
+ const arr = map.get(key);
690
+ if (arr) {
691
+ arr.push(value);
692
+ } else {
693
+ map.set(key, [value]);
694
+ }
695
+ }
696
+
519
697
  function* segmentTreeWalk(
520
698
  segments: ResolvedSegment[],
521
699
  interceptSegments?: ResolvedSegment[],
@@ -536,19 +714,12 @@ function* segmentTreeWalk(
536
714
  // Extract parent ID from parallel ID
537
715
  // Example: "L0R1L0.@sidebar" → "L0R1L0"
538
716
  const parentId = segment.id.split(".")[0];
539
- if (!parallelsByParent.has(parentId)) {
540
- parallelsByParent.set(parentId, []);
541
- }
542
- parallelsByParent.get(parentId)!.push(segment);
717
+ pushToGroup(parallelsByParent, parentId, segment);
543
718
  } else if (segment.type === "loader") {
544
719
  // Extract parent ID from loader ID
545
- // Example: "L0D0.cart" → "L0"
546
- // Loader ID format: {parentShortCode}D{index}.{loaderId}
547
- const parentId = segment.id.split("D")[0];
548
- if (!loadersByParent.has(parentId)) {
549
- loadersByParent.set(parentId, []);
550
- }
551
- loadersByParent.get(parentId)!.push(segment);
720
+ // Example: "L0D0.cart" → "L0"; "L0.@DetailD0.x" → "L0.@Detail"
721
+ const parentId = loaderParentId(segment.id);
722
+ pushToGroup(loadersByParent, parentId, segment);
552
723
  } else {
553
724
  // Layout, route, error, and notFound segments are all rendered in the tree
554
725
  // Error/notFound segments replace the failed segment with fallback UI
@@ -563,17 +734,11 @@ function* segmentTreeWalk(
563
734
  if (intercept.type === "parallel" && intercept.slot) {
564
735
  // Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
565
736
  const parentId = intercept.id.split(".")[0];
566
- if (!parallelsByParent.has(parentId)) {
567
- parallelsByParent.set(parentId, []);
568
- }
569
- parallelsByParent.get(parentId)!.push(intercept);
737
+ pushToGroup(parallelsByParent, parentId, intercept);
570
738
  } else if (intercept.type === "loader") {
571
- // Intercept loaders - extract parent from loader ID
572
- const parentId = intercept.id.split("D")[0];
573
- if (!loadersByParent.has(parentId)) {
574
- loadersByParent.set(parentId, []);
575
- }
576
- loadersByParent.get(parentId)!.push(intercept);
739
+ // Intercept loaders - extract parent from loader ID (slot name preserved)
740
+ const parentId = loaderParentId(intercept.id);
741
+ pushToGroup(loadersByParent, parentId, intercept);
577
742
  }
578
743
  }
579
744
  }
@@ -150,6 +150,19 @@ export type InterceptWhenFn<TEnv = any> = (
150
150
  ctx: InterceptSelectorContext<TEnv>,
151
151
  ) => boolean;
152
152
 
153
+ /**
154
+ * Config object passed to intercept() (its 4th argument). `when` gates whether
155
+ * the intercept activates on a soft navigation — a single match-time selector or
156
+ * an array of them (ALL must return true; omit to always activate). This is the
157
+ * intercept counterpart to transition({ when }); both express conditional
158
+ * behavior as a config field rather than a separate DSL helper.
159
+ *
160
+ * @internal This type is an implementation detail and may change without notice.
161
+ */
162
+ export interface InterceptConfig<TEnv = any> {
163
+ when?: InterceptWhenFn<TEnv> | InterceptWhenFn<TEnv>[];
164
+ }
165
+
153
166
  /**
154
167
  * Intercept entry stored in EntryData
155
168
  * Contains the slot name, route to intercept, and handler
@@ -220,6 +233,13 @@ export type EntryData =
220
233
  staticHandlerId?: string;
221
234
  /** Response type for non-RSC routes (json, text, image, any) */
222
235
  responseType?: string;
236
+ /**
237
+ * PPR (partial pre-rendering) opt-in from the path() `ppr` option. A
238
+ * document-level property of the page route: `true` uses the default
239
+ * shell policy, an object carries ttl/swr/tags. Read by the integrated
240
+ * PPR serve path (rsc/shell-serve.ts resolvePprConfig).
241
+ */
242
+ ppr?: boolean | import("../urls/pattern-types.js").PartialPrerenderProps;
223
243
  } & EntryPropCommon &
224
244
  EntryPropDatas &
225
245
  EntryPropSegments &
@@ -750,14 +770,23 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
750
770
 
751
771
  // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
752
772
  // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
753
- // invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
754
- // request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
755
- // so rendered()/barrier/deadlock gating (which must distinguish DSL from
756
- // handler-invoked loaders) is unchanged.
773
+ // invoked via ctx.use). Consulted by isInsideCacheScope() to exempt
774
+ // request-scoped reads, by getCurrentLoaderBodyId() for guard-warning
775
+ // attribution, and by isInsideHandlerInvokedLoaderBody() for the
776
+ // consumption-lane rule (the shell-capture guard exemption). It deliberately
777
+ // does NOT affect isInsideLoaderScope(), so rendered()/barrier/deadlock
778
+ // gating (which must distinguish DSL from handler-invoked loaders) is
779
+ // unchanged.
757
780
  const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
758
- const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
759
- globalThis as any
760
- )[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
781
+ const loaderBodyScopeALS: AsyncLocalStorage<{
782
+ active: true;
783
+ loaderId?: string;
784
+ handlerInvoked?: boolean;
785
+ }> = ((globalThis as any)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{
786
+ active: true;
787
+ loaderId?: string;
788
+ handlerInvoked?: boolean;
789
+ }>());
761
790
 
762
791
  /**
763
792
  * Check if the current execution is inside a cache() DSL boundary.
@@ -802,6 +831,67 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
802
831
  * and handler-invoked via ctx.use) so request-scoped reads inside a loader
803
832
  * never trip the cache-scope guards — loaders always run fresh.
804
833
  */
805
- export function runInsideLoaderBodyScope<T>(fn: () => T): T {
806
- return loaderBodyScopeALS.run({ active: true }, fn);
834
+ export function runInsideLoaderBodyScope<T>(
835
+ fn: () => T,
836
+ loaderId?: string,
837
+ handlerInvoked?: boolean,
838
+ ): T {
839
+ return loaderBodyScopeALS.run({ active: true, loaderId, handlerInvoked }, fn);
840
+ }
841
+
842
+ /**
843
+ * The $$id of the loader whose body is currently executing, or undefined
844
+ * outside any loader body. Used by the shell-capture identity guard
845
+ * (cookie-store.ts) so its refusal warning can name the loader that read
846
+ * cookies()/headers() instead of blaming a lane it cannot see — the old
847
+ * hardcoded "bake-lane loader" text misled a live-lane debugging session
848
+ * (issue #672, secondary).
849
+ */
850
+ export function getCurrentLoaderBodyId(): string | undefined {
851
+ return loaderBodyScopeALS.getStore()?.loaderId;
852
+ }
853
+
854
+ /**
855
+ * True while a HANDLER-invoked loader body (`await ctx.use(Loader)` from a
856
+ * handler, not the DSL segment funnel) is executing. The consumption-lane
857
+ * rule keys off this: handler consumption yields a BAKED copy in every shared
858
+ * artifact — cache(), "use cache", and the PPR shell — so the shell-capture
859
+ * identity guard (cookie-store.ts) permits cookies()/headers() here, exactly
860
+ * like the cache-purity guards do. DSL segment loaders (live lane masked at
861
+ * capture, bake lane guarded) never set the flag.
862
+ */
863
+ export function isInsideHandlerInvokedLoaderBody(): boolean {
864
+ return loaderBodyScopeALS.getStore()?.handlerInvoked === true;
865
+ }
866
+
867
+ // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
868
+ // A push callback's value is stored as-is; if it is a promise it is NOT tracked
869
+ // by handleStore.settled and does not block segment resolution, so a
870
+ // ctx.use(loader) made from inside such a callback can never form a rendered()
871
+ // deadlock. This is an ALS (not a plain boolean) so the exemption survives the
872
+ // callback's own awaits — an async push callback that resumes after `await`
873
+ // still reads as "inside a push callback" and stays out of the deadlock guard.
874
+ const PUSH_CALLBACK_SCOPE_KEY = Symbol.for(
875
+ "rangojs-router:push-callback-scope",
876
+ );
877
+ const pushCallbackScopeALS: AsyncLocalStorage<{ active: true }> = ((
878
+ globalThis as any
879
+ )[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
880
+
881
+ /**
882
+ * Check if the current execution is inside a handle push callback (sync or an
883
+ * async callback's continuation). Used by the handler-to-loader deadlock guard
884
+ * to exempt push-callback continuations.
885
+ */
886
+ export function isInsidePushCallbackScope(): boolean {
887
+ return pushCallbackScopeALS.getStore()?.active === true;
888
+ }
889
+
890
+ /**
891
+ * Run `fn` inside a push-callback scope. Wraps the invocation of a handle push
892
+ * callback so that any ctx.use(loader) it makes — including after one of its own
893
+ * awaits — is exempt from the deadlock guard.
894
+ */
895
+ export function runInsidePushCallbackScope<T>(fn: () => T): T {
896
+ return pushCallbackScopeALS.run({ active: true }, fn);
807
897
  }