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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (406) hide show
  1. package/AGENTS.md +17 -0
  2. package/README.md +291 -61
  3. package/dist/bin/rango.js +544 -143
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3744 -1329
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +67 -13
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +312 -0
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +322 -19
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +282 -60
  19. package/skills/host-router/SKILL.md +278 -0
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +297 -31
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +155 -111
  32. package/skills/rango/SKILL.md +251 -23
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +104 -9
  36. package/skills/router-setup/SKILL.md +124 -11
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +125 -222
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +357 -52
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +67 -40
  62. package/src/bin/rango.ts +18 -0
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +197 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/link-interceptor.ts +4 -0
  74. package/src/browser/navigation-bridge.ts +200 -30
  75. package/src/browser/navigation-client.ts +217 -58
  76. package/src/browser/navigation-store-handle.ts +38 -0
  77. package/src/browser/navigation-store.ts +76 -67
  78. package/src/browser/navigation-transaction.ts +18 -66
  79. package/src/browser/network-error-handler.ts +34 -7
  80. package/src/browser/partial-update.ts +187 -112
  81. package/src/browser/prefetch/cache.ts +312 -30
  82. package/src/browser/prefetch/fetch.ts +344 -47
  83. package/src/browser/prefetch/policy.ts +6 -0
  84. package/src/browser/prefetch/queue.ts +126 -20
  85. package/src/browser/prefetch/resource-ready.ts +77 -0
  86. package/src/browser/rango-state.ts +158 -76
  87. package/src/browser/react/Link.tsx +125 -18
  88. package/src/browser/react/NavigationProvider.tsx +135 -120
  89. package/src/browser/react/ScrollRestoration.tsx +10 -6
  90. package/src/browser/react/context.ts +7 -2
  91. package/src/browser/react/filter-segment-order.ts +66 -7
  92. package/src/browser/react/index.ts +0 -48
  93. package/src/browser/react/location-state-shared.ts +178 -8
  94. package/src/browser/react/location-state.ts +39 -14
  95. package/src/browser/react/use-action.ts +6 -15
  96. package/src/browser/react/use-handle.ts +23 -69
  97. package/src/browser/react/use-href.tsx +8 -1
  98. package/src/browser/react/use-link-status.ts +33 -8
  99. package/src/browser/react/use-navigation.ts +32 -7
  100. package/src/browser/react/use-params.ts +20 -10
  101. package/src/browser/react/use-reverse.ts +106 -0
  102. package/src/browser/react/use-router.ts +46 -11
  103. package/src/browser/react/use-search-params.ts +0 -5
  104. package/src/browser/react/use-segments.ts +11 -21
  105. package/src/browser/response-adapter.ts +80 -5
  106. package/src/browser/rsc-router.tsx +226 -75
  107. package/src/browser/scroll-restoration.ts +54 -42
  108. package/src/browser/segment-reconciler.ts +36 -9
  109. package/src/browser/segment-structure-assert.ts +2 -2
  110. package/src/browser/server-action-bridge.ts +619 -442
  111. package/src/browser/types.ts +115 -11
  112. package/src/browser/validate-redirect-origin.ts +43 -16
  113. package/src/build/collect-fallback-refs.ts +107 -0
  114. package/src/build/generate-manifest.ts +65 -40
  115. package/src/build/generate-route-types.ts +7 -1
  116. package/src/build/index.ts +8 -2
  117. package/src/build/prefix-tree-utils.ts +123 -0
  118. package/src/build/route-trie.ts +182 -37
  119. package/src/build/route-types/ast-route-extraction.ts +15 -8
  120. package/src/build/route-types/codegen.ts +16 -5
  121. package/src/build/route-types/include-resolution.ts +125 -24
  122. package/src/build/route-types/param-extraction.ts +6 -3
  123. package/src/build/route-types/per-module-writer.ts +22 -6
  124. package/src/build/route-types/router-processing.ts +392 -106
  125. package/src/build/route-types/scan-filter.ts +9 -2
  126. package/src/build/route-types/source-scan.ts +216 -0
  127. package/src/build/runtime-discovery.ts +9 -20
  128. package/src/cache/cache-error.ts +104 -0
  129. package/src/cache/cache-key-utils.ts +29 -13
  130. package/src/cache/cache-policy.ts +108 -34
  131. package/src/cache/cache-runtime.ts +214 -48
  132. package/src/cache/cache-scope.ts +236 -89
  133. package/src/cache/cache-tag.ts +103 -0
  134. package/src/cache/cf/cf-base64.ts +33 -0
  135. package/src/cache/cf/cf-cache-constants.ts +127 -0
  136. package/src/cache/cf/cf-cache-store.ts +2224 -171
  137. package/src/cache/cf/cf-cache-types.ts +349 -0
  138. package/src/cache/cf/cf-kv-utils.ts +46 -0
  139. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  140. package/src/cache/cf/index.ts +11 -17
  141. package/src/cache/document-cache.ts +89 -27
  142. package/src/cache/handle-snapshot.ts +70 -0
  143. package/src/cache/index.ts +11 -20
  144. package/src/cache/memory-segment-store.ts +136 -37
  145. package/src/cache/profile-registry.ts +31 -31
  146. package/src/cache/read-through-swr.ts +41 -11
  147. package/src/cache/segment-codec.ts +9 -17
  148. package/src/cache/tag-invalidation.ts +230 -0
  149. package/src/cache/taint.ts +55 -0
  150. package/src/cache/types.ts +37 -100
  151. package/src/client.rsc.tsx +45 -21
  152. package/src/client.tsx +120 -336
  153. package/src/cloudflare/index.ts +11 -0
  154. package/src/cloudflare/tracing.ts +109 -0
  155. package/src/component-utils.ts +19 -0
  156. package/src/components/DefaultDocument.tsx +8 -2
  157. package/src/context-var.ts +84 -2
  158. package/src/debug.ts +2 -2
  159. package/src/decode-loader-results.ts +52 -0
  160. package/src/defer.ts +196 -0
  161. package/src/deps/ssr.ts +0 -1
  162. package/src/encode-kv.ts +49 -0
  163. package/src/errors.ts +30 -4
  164. package/src/escape-script.ts +52 -0
  165. package/src/handle.ts +70 -22
  166. package/src/handles/MetaTags.tsx +56 -19
  167. package/src/handles/Scripts.tsx +183 -0
  168. package/src/handles/breadcrumbs.ts +95 -0
  169. package/src/handles/is-thenable.ts +19 -0
  170. package/src/handles/meta.ts +51 -40
  171. package/src/handles/script.ts +244 -0
  172. package/src/host/cookie-handler.ts +9 -60
  173. package/src/host/errors.ts +0 -24
  174. package/src/host/index.ts +8 -5
  175. package/src/host/pattern-matcher.ts +23 -52
  176. package/src/host/router.ts +107 -99
  177. package/src/host/testing.ts +40 -27
  178. package/src/host/types.ts +37 -4
  179. package/src/host/utils.ts +1 -1
  180. package/src/href-client.ts +137 -22
  181. package/src/index.rsc.ts +79 -29
  182. package/src/index.ts +149 -65
  183. package/src/internal-debug.ts +11 -10
  184. package/src/loader-store.ts +500 -0
  185. package/src/loader.rsc.ts +20 -13
  186. package/src/loader.ts +12 -11
  187. package/src/missing-id-error.ts +68 -0
  188. package/src/outlet-context.ts +1 -1
  189. package/src/outlet-provider.tsx +1 -5
  190. package/src/prerender/param-hash.ts +16 -16
  191. package/src/prerender/store.ts +63 -26
  192. package/src/prerender.ts +198 -82
  193. package/src/redirect-origin.ts +100 -0
  194. package/src/regex-escape.ts +8 -0
  195. package/src/render-error-thrower.tsx +20 -0
  196. package/src/response-utils.ts +62 -0
  197. package/src/reverse.ts +65 -15
  198. package/src/root-error-boundary.tsx +1 -19
  199. package/src/route-content-wrapper.tsx +7 -72
  200. package/src/route-definition/dsl-helpers.ts +469 -276
  201. package/src/route-definition/helper-factories.ts +29 -139
  202. package/src/route-definition/helpers-types.ts +113 -37
  203. package/src/route-definition/index.ts +3 -3
  204. package/src/route-definition/redirect.ts +53 -12
  205. package/src/route-definition/resolve-handler-use.ts +161 -0
  206. package/src/route-definition/use-item-types.ts +32 -0
  207. package/src/route-map-builder.ts +7 -17
  208. package/src/route-types.ts +37 -41
  209. package/src/router/basename.ts +14 -0
  210. package/src/router/content-negotiation.ts +164 -17
  211. package/src/router/error-handling.ts +45 -18
  212. package/src/router/find-match.ts +45 -22
  213. package/src/router/handler-context.ts +110 -39
  214. package/src/router/instrument.ts +350 -0
  215. package/src/router/intercept-resolution.ts +50 -24
  216. package/src/router/lazy-includes.ts +19 -53
  217. package/src/router/loader-resolution.ts +274 -56
  218. package/src/router/logging.ts +5 -8
  219. package/src/router/manifest.ts +49 -45
  220. package/src/router/match-api.ts +121 -205
  221. package/src/router/match-context.ts +0 -22
  222. package/src/router/match-handlers.ts +58 -58
  223. package/src/router/match-middleware/background-revalidation.ts +33 -6
  224. package/src/router/match-middleware/cache-lookup.ts +214 -263
  225. package/src/router/match-middleware/cache-store.ts +73 -33
  226. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  227. package/src/router/match-middleware/segment-resolution.ts +52 -18
  228. package/src/router/match-pipelines.ts +1 -42
  229. package/src/router/match-result.ts +104 -49
  230. package/src/router/metrics.ts +217 -26
  231. package/src/router/middleware-types.ts +24 -110
  232. package/src/router/middleware.ts +384 -197
  233. package/src/router/navigation-snapshot.ts +131 -0
  234. package/src/router/params-util.ts +23 -0
  235. package/src/router/pattern-matching.ts +148 -91
  236. package/src/router/prefetch-cache-ttl.ts +51 -0
  237. package/src/router/prerender-match.ts +199 -56
  238. package/src/router/preview-match.ts +32 -102
  239. package/src/router/request-classification.ts +276 -0
  240. package/src/router/revalidation.ts +144 -74
  241. package/src/router/route-snapshot.ts +244 -0
  242. package/src/router/router-context.ts +8 -28
  243. package/src/router/router-interfaces.ts +129 -36
  244. package/src/router/router-options.ts +185 -23
  245. package/src/router/router-registry.ts +2 -5
  246. package/src/router/segment-resolution/fresh.ts +281 -76
  247. package/src/router/segment-resolution/helpers.ts +116 -31
  248. package/src/router/segment-resolution/loader-cache.ts +63 -37
  249. package/src/router/segment-resolution/revalidation.ts +493 -391
  250. package/src/router/segment-resolution/static-store.ts +19 -5
  251. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  252. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  253. package/src/router/segment-resolution.ts +5 -1
  254. package/src/router/segment-wrappers.ts +8 -5
  255. package/src/router/state-cookie-name.ts +33 -0
  256. package/src/router/substitute-pattern-params.ts +56 -0
  257. package/src/router/telemetry-otel.ts +161 -199
  258. package/src/router/telemetry.ts +96 -19
  259. package/src/router/timeout.ts +0 -20
  260. package/src/router/tracing.ts +206 -0
  261. package/src/router/trie-matching.ts +180 -58
  262. package/src/router/types.ts +10 -63
  263. package/src/router/url-params.ts +44 -0
  264. package/src/router.ts +182 -54
  265. package/src/rsc/handler-context.ts +3 -2
  266. package/src/rsc/handler.ts +702 -460
  267. package/src/rsc/helpers.ts +168 -46
  268. package/src/rsc/index.ts +2 -25
  269. package/src/rsc/json-route-result.ts +38 -0
  270. package/src/rsc/loader-fetch.ts +127 -31
  271. package/src/rsc/manifest-init.ts +33 -42
  272. package/src/rsc/origin-guard.ts +39 -25
  273. package/src/rsc/progressive-enhancement.ts +98 -19
  274. package/src/rsc/redirect-guard.ts +99 -0
  275. package/src/rsc/response-cache-serve.ts +238 -0
  276. package/src/rsc/response-error.ts +79 -12
  277. package/src/rsc/response-route-handler.ts +99 -189
  278. package/src/rsc/rsc-rendering.ts +126 -106
  279. package/src/rsc/runtime-warnings.ts +23 -10
  280. package/src/rsc/server-action.ts +269 -114
  281. package/src/rsc/ssr-setup.ts +144 -0
  282. package/src/rsc/types.ts +34 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +49 -41
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +281 -129
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +317 -63
  290. package/src/server/cookie-parse.ts +32 -0
  291. package/src/server/cookie-store.ts +80 -5
  292. package/src/server/handle-store.ts +40 -38
  293. package/src/server/loader-registry.ts +26 -46
  294. package/src/server/request-context.ts +425 -177
  295. package/src/server.ts +6 -0
  296. package/src/ssr/index.tsx +25 -16
  297. package/src/static-handler.ts +27 -18
  298. package/src/testing/cache-status.ts +162 -0
  299. package/src/testing/collect-handle.ts +40 -0
  300. package/src/testing/dispatch.ts +701 -0
  301. package/src/testing/dom.entry.ts +22 -0
  302. package/src/testing/e2e/fixture.ts +188 -0
  303. package/src/testing/e2e/index.ts +128 -0
  304. package/src/testing/e2e/matchers.ts +35 -0
  305. package/src/testing/e2e/page-helpers.ts +272 -0
  306. package/src/testing/e2e/parity.ts +387 -0
  307. package/src/testing/e2e/server.ts +195 -0
  308. package/src/testing/flight-matchers.ts +97 -0
  309. package/src/testing/flight-normalize.ts +11 -0
  310. package/src/testing/flight-runtime.d.ts +57 -0
  311. package/src/testing/flight-tree.ts +682 -0
  312. package/src/testing/flight.entry.ts +52 -0
  313. package/src/testing/flight.ts +257 -0
  314. package/src/testing/generated-routes.ts +183 -0
  315. package/src/testing/index.ts +99 -0
  316. package/src/testing/internal/context.ts +371 -0
  317. package/src/testing/internal/flight-client-globals.ts +30 -0
  318. package/src/testing/internal/seed-vars.ts +54 -0
  319. package/src/testing/render-handler.ts +343 -0
  320. package/src/testing/render-route.tsx +581 -0
  321. package/src/testing/run-loader.ts +385 -0
  322. package/src/testing/run-middleware.ts +205 -0
  323. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  324. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  325. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  326. package/src/testing/vitest-stubs/version.ts +5 -0
  327. package/src/testing/vitest.ts +305 -0
  328. package/src/theme/ThemeProvider.tsx +20 -58
  329. package/src/theme/ThemeScript.tsx +7 -9
  330. package/src/theme/constants.ts +52 -13
  331. package/src/theme/index.ts +3 -19
  332. package/src/theme/theme-context.ts +1 -5
  333. package/src/theme/theme-script.ts +22 -21
  334. package/src/theme/use-theme.ts +0 -3
  335. package/src/types/boundaries.ts +0 -35
  336. package/src/types/cache-types.ts +17 -8
  337. package/src/types/error-types.ts +30 -90
  338. package/src/types/global-namespace.ts +54 -41
  339. package/src/types/handler-context.ts +236 -88
  340. package/src/types/index.ts +1 -10
  341. package/src/types/loader-types.ts +44 -15
  342. package/src/types/request-scope.ts +112 -0
  343. package/src/types/route-config.ts +10 -45
  344. package/src/types/route-entry.ts +19 -7
  345. package/src/types/segments.ts +37 -19
  346. package/src/urls/include-helper.ts +33 -70
  347. package/src/urls/index.ts +1 -11
  348. package/src/urls/path-helper-types.ts +58 -11
  349. package/src/urls/path-helper.ts +57 -111
  350. package/src/urls/pattern-types.ts +48 -19
  351. package/src/urls/response-types.ts +25 -22
  352. package/src/urls/type-extraction.ts +58 -139
  353. package/src/urls/urls-function.ts +1 -18
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +64 -91
  357. package/src/vite/discovery/discover-routers.ts +147 -88
  358. package/src/vite/discovery/discovery-errors.ts +194 -0
  359. package/src/vite/discovery/gate-state.ts +171 -0
  360. package/src/vite/discovery/prerender-collection.ts +247 -145
  361. package/src/vite/discovery/route-types-writer.ts +40 -84
  362. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  363. package/src/vite/discovery/state.ts +61 -13
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +10 -3
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +155 -65
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +120 -0
  370. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  371. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  372. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  373. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  374. package/src/vite/plugins/expose-action-id.ts +49 -98
  375. package/src/vite/plugins/expose-id-utils.ts +96 -51
  376. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  377. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  378. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  379. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  380. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  381. package/src/vite/plugins/performance-tracks.ts +89 -0
  382. package/src/vite/plugins/refresh-cmd.ts +127 -0
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +21 -25
  385. package/src/vite/plugins/version-plugin.ts +46 -37
  386. package/src/vite/plugins/virtual-entries.ts +13 -18
  387. package/src/vite/rango.ts +241 -287
  388. package/src/vite/router-discovery.ts +956 -149
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +4 -4
  391. package/src/vite/utils/bundle-analysis.ts +10 -15
  392. package/src/vite/utils/client-chunks.ts +184 -0
  393. package/src/vite/utils/directive-prologue.ts +40 -0
  394. package/src/vite/utils/forward-user-plugins.ts +171 -0
  395. package/src/vite/utils/manifest-utils.ts +4 -59
  396. package/src/vite/utils/package-resolution.ts +20 -52
  397. package/src/vite/utils/prerender-utils.ts +141 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/CLAUDE.md +0 -5
  400. package/src/browser/action-response-classifier.ts +0 -99
  401. package/src/browser/react/use-client-cache.ts +0 -58
  402. package/src/browser/shallow.ts +0 -40
  403. package/src/handles/index.ts +0 -6
  404. package/src/network-error-thrower.tsx +0 -23
  405. package/src/route-definition/route-function.ts +0 -119
  406. package/src/router/middleware-cookies.ts +0 -55
@@ -10,7 +10,12 @@ import type { ReactNode } from "react";
10
10
  import { invariant } from "../../errors";
11
11
  import { revalidate } from "../loader-resolution.js";
12
12
  import { evaluateRevalidation } from "../revalidation.js";
13
- import type { EntryData } from "../../server/context";
13
+ import {
14
+ getParallelEntries,
15
+ getParallelSlotEntries,
16
+ type EntryData,
17
+ type ParallelEntryData,
18
+ } from "../../server/context";
14
19
  import type {
15
20
  HandlerContext,
16
21
  InternalHandlerContext,
@@ -30,53 +35,41 @@ import {
30
35
  import { resolveLoaderData } from "./loader-cache.js";
31
36
  import {
32
37
  handleHandlerResult,
38
+ warnOnStreamedResponse,
33
39
  tryStaticHandler,
34
40
  tryStaticSlot,
35
41
  resolveLayoutComponent,
36
42
  resolveWithErrorBoundary,
43
+ buildLoaderErrorContext,
37
44
  } from "./helpers.js";
45
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
38
46
  import { getRouterContext } from "../router-context.js";
39
- import { resolveSink, safeEmit } from "../telemetry.js";
40
-
41
- // ---------------------------------------------------------------------------
42
- // Telemetry helpers
43
- // ---------------------------------------------------------------------------
47
+ import { observeEvent, observeHandler } from "../instrument.js";
48
+ import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
49
+ import {
50
+ track,
51
+ RangoContext,
52
+ runInsideLoaderScope,
53
+ } from "../../server/context.js";
44
54
 
45
55
  /**
46
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
47
- * Silently no-ops when called outside RouterContext (e.g. in unit tests).
56
+ * Trace a parallel slot that's being force-rendered on a full refetch (client
57
+ * has no cached state). User revalidate fns are bypassed in this case — see
58
+ * the call sites for the load-bearing rationale.
48
59
  */
49
- function observeStreamedHandler(
50
- promise: Promise<ReactNode>,
51
- segmentId: string,
52
- segmentType: string,
53
- pathname?: string,
54
- routeKey?: string,
55
- params?: Record<string, string>,
60
+ function traceFullRefetchedParallelSlot(
61
+ parallelId: string,
62
+ belongsToRoute: boolean,
56
63
  ): void {
57
- let routerCtx;
58
- try {
59
- routerCtx = getRouterContext();
60
- } catch {
61
- return;
62
- }
63
- if (!routerCtx?.telemetry) return;
64
- const sink = resolveSink(routerCtx.telemetry);
65
- const reqId = routerCtx.requestId;
66
- promise.catch((err: unknown) => {
67
- const errorObj = err instanceof Error ? err : new Error(String(err));
68
- safeEmit(sink, {
69
- type: "handler.error",
70
- timestamp: performance.now(),
71
- requestId: reqId,
72
- segmentId,
73
- segmentType,
74
- error: errorObj,
75
- handledByBoundary: true,
76
- pathname,
77
- routeKey,
78
- params,
79
- });
64
+ if (!isTraceActive()) return;
65
+ pushRevalidationTraceEntry({
66
+ segmentId: parallelId,
67
+ segmentType: "parallel",
68
+ belongsToRoute,
69
+ source: "parallel",
70
+ defaultShouldRevalidate: true,
71
+ finalShouldRevalidate: true,
72
+ reason: "full-refetch",
80
73
  });
81
74
  }
82
75
 
@@ -95,23 +88,14 @@ function emitRevalidationDecision(
95
88
  routeKey: string,
96
89
  shouldRevalidate: boolean,
97
90
  ): void {
98
- let routerCtx;
99
- try {
100
- routerCtx = getRouterContext();
101
- } catch {
102
- return;
103
- }
104
- if (routerCtx?.telemetry) {
105
- safeEmit(resolveSink(routerCtx.telemetry), {
106
- type: "revalidation.decision",
107
- timestamp: performance.now(),
108
- requestId: routerCtx.requestId,
109
- segmentId,
110
- pathname,
111
- routeKey,
112
- shouldRevalidate,
113
- });
114
- }
91
+ observeEvent({
92
+ type: "revalidation.decision",
93
+ timestamp: performance.now(),
94
+ segmentId,
95
+ pathname,
96
+ routeKey,
97
+ shouldRevalidate,
98
+ });
115
99
  }
116
100
 
117
101
  // ---------------------------------------------------------------------------
@@ -216,6 +200,10 @@ export async function resolveLoadersWithRevalidation<TEnv>(
216
200
  ),
217
201
  );
218
202
 
203
+ // Partial (revalidation) render path: a throwing DSL loader must still fire
204
+ // onError/loader.error. isPartial flags the reporting phase accordingly.
205
+ const errorContext = { ...buildLoaderErrorContext(ctx), isPartial: true };
206
+
219
207
  const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
220
208
  const segments: ResolvedSegment[] = loadersToRun.map(
221
209
  ({ loaderEntry, loader, segmentId, index }) => ({
@@ -227,10 +215,13 @@ export async function resolveLoadersWithRevalidation<TEnv>(
227
215
  params: ctx.params,
228
216
  loaderId: loader.$$id,
229
217
  loaderData: deps.wrapLoaderPromise(
230
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
218
+ runInsideLoaderScope(() =>
219
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
220
+ ),
231
221
  entry,
232
222
  segmentId,
233
223
  ctx.pathname,
224
+ errorContext,
234
225
  ),
235
226
  belongsToRoute,
236
227
  }),
@@ -257,26 +248,95 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
257
248
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
258
249
  const allLoaderSegments: ResolvedSegment[] = [];
259
250
  const allMatchedIds: string[] = [];
251
+ const seenIds = new Set<string>();
252
+
253
+ async function collectEntryLoaders(
254
+ entry: EntryData,
255
+ belongsToRoute: boolean,
256
+ shortCodeOverride?: string,
257
+ ): Promise<void> {
258
+ // Skip if all loaders from this entry have already been resolved
259
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
260
+ const loaderEntries = entry.loader ?? [];
261
+ const sc = shortCodeOverride ?? entry.shortCode;
262
+ const allAlreadySeen =
263
+ loaderEntries.length > 0 &&
264
+ loaderEntries.every((le, i) =>
265
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
266
+ );
267
+ if (!allAlreadySeen) {
268
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
269
+ entry,
270
+ context,
271
+ belongsToRoute,
272
+ clientSegmentIds,
273
+ prevParams,
274
+ request,
275
+ prevUrl,
276
+ nextUrl,
277
+ routeKey,
278
+ deps,
279
+ actionContext,
280
+ shortCodeOverride,
281
+ stale,
282
+ );
283
+ for (const seg of segments) {
284
+ if (!seenIds.has(seg.id)) {
285
+ seenIds.add(seg.id);
286
+ allLoaderSegments.push(seg);
287
+ }
288
+ }
289
+ allMatchedIds.push(...matchedIds);
290
+ }
291
+
292
+ const seenParallelEntryIds = new Set<string>();
293
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
294
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
295
+ seenParallelEntryIds.add(parallelEntry.id);
296
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
297
+ }
298
+
299
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
300
+ for (const layoutEntry of entry.layout) {
301
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
302
+ // Inherit route loaders for orphan layouts with parallels.
303
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
304
+ // route entry, as that would re-iterate route.layout and loop.
305
+ if (
306
+ entry.type === "route" &&
307
+ entry.loader &&
308
+ entry.loader.length > 0 &&
309
+ Object.keys(layoutEntry.parallel).length > 0
310
+ ) {
311
+ const inherited = await resolveLoadersWithRevalidation(
312
+ entry,
313
+ context,
314
+ childBelongsToRoute,
315
+ clientSegmentIds,
316
+ prevParams,
317
+ request,
318
+ prevUrl,
319
+ nextUrl,
320
+ routeKey,
321
+ deps,
322
+ actionContext,
323
+ layoutEntry.shortCode,
324
+ stale,
325
+ );
326
+ for (const seg of inherited.segments) {
327
+ if (!seenIds.has(seg.id)) {
328
+ seenIds.add(seg.id);
329
+ seg._inherited = true;
330
+ allLoaderSegments.push(seg);
331
+ }
332
+ }
333
+ allMatchedIds.push(...inherited.matchedIds);
334
+ }
335
+ }
336
+ }
260
337
 
261
338
  for (const entry of entries) {
262
- const belongsToRoute = entry.type === "route";
263
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
264
- entry,
265
- context,
266
- belongsToRoute,
267
- clientSegmentIds,
268
- prevParams,
269
- request,
270
- prevUrl,
271
- nextUrl,
272
- routeKey,
273
- deps,
274
- actionContext,
275
- undefined, // shortCodeOverride
276
- stale,
277
- );
278
- allLoaderSegments.push(...segments);
279
- allMatchedIds.push(...matchedIds);
339
+ await collectEntryLoaders(entry, entry.type === "route");
280
340
  }
281
341
 
282
342
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -300,22 +360,20 @@ export function buildEntryRevalidateMap(
300
360
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
301
361
 
302
362
  if (entry.type !== "parallel") {
303
- for (const parallelEntry of entry.parallel) {
304
- if (parallelEntry.type === "parallel") {
305
- const slots = Object.keys(parallelEntry.handler) as `@${string}`[];
306
- for (const slot of slots) {
307
- const parallelId = `${parallelEntry.shortCode}.${slot}`;
308
- map.set(parallelId, {
309
- entry: parallelEntry,
310
- revalidate: parallelEntry.revalidate,
311
- });
312
- }
313
- }
363
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
364
+ entry.parallel,
365
+ )) {
366
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
367
+ const parallelId = `${parallelParentShortCode}.${slot}`;
368
+ map.set(parallelId, {
369
+ entry: parallelEntry,
370
+ revalidate: parallelEntry.revalidate,
371
+ });
314
372
  }
315
373
  }
316
374
 
317
375
  for (const layoutEntry of entry.layout) {
318
- processEntry(layoutEntry);
376
+ processEntry(layoutEntry, entry.shortCode);
319
377
  }
320
378
  }
321
379
 
@@ -326,6 +384,97 @@ export function buildEntryRevalidateMap(
326
384
  return map;
327
385
  }
328
386
 
387
+ /**
388
+ * Resolve the component for a single parallel slot on the revalidation path.
389
+ * Pure component resolution shared verbatim by
390
+ * resolveParallelSegmentsWithRevalidation and the orphan-inlined loop in
391
+ * resolveOrphanLayoutWithRevalidation: try the static slot cache, else run the
392
+ * slot handler (pinning _currentSegmentId to the slot id so handle pushes land
393
+ * in the slot's own bucket, and wrapping a streamed handler). Returns the
394
+ * resolved component and whether the handler actually ran. Does NOT touch the
395
+ * revalidate-default policy (the caller decides shouldResolve, including the
396
+ * orphan-vs-main defaultOverride divergence) or loader-resolution ordering.
397
+ */
398
+ async function resolveParallelSlotComponent<TEnv>(args: {
399
+ shouldResolve: boolean;
400
+ parallelEntry: ParallelEntryData;
401
+ slot: string;
402
+ parallelId: string;
403
+ handler:
404
+ | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
405
+ | ReactNode
406
+ | undefined;
407
+ context: HandlerContext<any, TEnv>;
408
+ deps: SegmentResolutionDeps<TEnv>;
409
+ routeKey: string;
410
+ params: Record<string, string>;
411
+ }): Promise<{ component: ReactNode | undefined; handlerRan: boolean }> {
412
+ const {
413
+ shouldResolve,
414
+ parallelEntry,
415
+ slot,
416
+ parallelId,
417
+ handler,
418
+ context,
419
+ deps,
420
+ routeKey,
421
+ params,
422
+ } = args;
423
+
424
+ let component: ReactNode | undefined;
425
+ let handlerRan = false;
426
+ if (shouldResolve) {
427
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
428
+ // tryStaticSlot returning a value means the static cache supplied the
429
+ // component — handler did NOT run. handlerRan stays false.
430
+ }
431
+ if (component === undefined) {
432
+ const hasLoadingFallback =
433
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
434
+ if (!shouldResolve) {
435
+ component = null;
436
+ } else if (handler === undefined) {
437
+ // Handler evicted (production static slot) but static lookup missed.
438
+ // Nothing to render — use null so the client keeps its cached version.
439
+ component = null;
440
+ } else {
441
+ // Slot-keyed pushes — slot owns its own bucket, parent layout owns its
442
+ // own. On slot-only revalidations the partial merge updates only the
443
+ // slot's bucket; the parent's bucket stays intact.
444
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
445
+ parallelId;
446
+ handlerRan = true;
447
+ if (hasLoadingFallback) {
448
+ const result =
449
+ typeof handler === "function" ? handler(context) : handler;
450
+ if (result instanceof Promise) {
451
+ warnOnStreamedResponse(result, parallelId);
452
+ const tracked = deps.trackHandler(result, {
453
+ segmentId: parallelId,
454
+ segmentType: "parallel",
455
+ });
456
+ observeStreamedHandler(
457
+ tracked,
458
+ parallelId,
459
+ "parallel",
460
+ context.pathname,
461
+ routeKey,
462
+ params,
463
+ );
464
+ component = tracked as ReactNode;
465
+ } else {
466
+ component = result as ReactNode;
467
+ }
468
+ } else {
469
+ component =
470
+ typeof handler === "function" ? await handler(context) : handler;
471
+ }
472
+ }
473
+ }
474
+
475
+ return { component, handlerRan };
476
+ }
477
+
329
478
  /**
330
479
  * Resolve parallel segments with revalidation.
331
480
  */
@@ -343,11 +492,35 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
343
492
  deps: SegmentResolutionDeps<TEnv>,
344
493
  actionContext?: ActionContext,
345
494
  stale?: boolean,
495
+ options?: {
496
+ /**
497
+ * Seed for an unknown parent-chain slot (slot not in clientSegmentIds) when
498
+ * there are no deciding revalidate fns. "type-derived" (default, main path):
499
+ * `belongsToRoute || isNewParent`. "force-render" (orphan path): always
500
+ * `true` — orphan parallels always belong to the route and must render
501
+ * unless the user opts out via revalidate(); the #482 blank-parent-chain-
502
+ * slot guard.
503
+ */
504
+ parentChainDefault?: "type-derived" | "force-render";
505
+ /**
506
+ * When a slot's loaders are resolved relative to the slot segment push.
507
+ * "after" (default, main path) pushes the slot segment first; "before"
508
+ * (orphan path) resolves loaders first. This only changes the
509
+ * segments/matchedIds emission ORDER (the client reconciler is insensitive
510
+ * to it: loader sub-ids are filtered out and slots are re-grouped by parent).
511
+ */
512
+ loaderOrder?: "after" | "before";
513
+ },
346
514
  ): Promise<SegmentRevalidationResult> {
347
515
  const segments: ResolvedSegment[] = [];
348
516
  const matchedIds: string[] = [];
517
+ const parentChainDefault = options?.parentChainDefault ?? "type-derived";
518
+ const loaderOrder = options?.loaderOrder ?? "after";
349
519
 
350
- for (const parallelEntry of entry.parallel) {
520
+ const resolvedParallelEntries = new Set<string>();
521
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
522
+ entry.parallel,
523
+ )) {
351
524
  invariant(
352
525
  parallelEntry.type === "parallel",
353
526
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -358,141 +531,78 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
358
531
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
359
532
  | ReactNode
360
533
  >;
534
+ // In production, static handler bodies are evicted and the slot value
535
+ // may be undefined. The static store holds the pre-rendered component.
536
+ // We defer the handler check until after tryStaticSlot.
537
+ const handler = slots[slot];
361
538
 
362
- for (const [slot, handler] of Object.entries(slots)) {
363
- const parallelId = `${entry.shortCode}.${slot}`;
539
+ const parallelId = `${entry.shortCode}.${slot}`;
364
540
 
365
- const isFullRefetch = clientSegmentIds.size === 0;
366
- // When the parent layout is new (not in client's segment set),
367
- // all its parallel children must be resolved and tracked.
368
- // Without this, navigating to a new layout with parallels
369
- // (e.g., BlogLayout with @sidebar) from a different route
370
- // would silently drop those parallel segments.
371
- const isNewParent = !clientSegmentIds.has(entry.shortCode);
372
- if (
373
- isFullRefetch ||
374
- clientSegmentIds.has(parallelId) ||
375
- belongsToRoute ||
376
- isNewParent
377
- ) {
378
- matchedIds.push(parallelId);
379
- }
541
+ const isFullRefetch = clientSegmentIds.size === 0;
542
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
380
543
 
381
- const shouldResolve = await (async () => {
382
- if (isFullRefetch) {
383
- if (isTraceActive()) {
384
- pushRevalidationTraceEntry({
385
- segmentId: parallelId,
386
- segmentType: "parallel",
387
- belongsToRoute,
388
- source: "parallel",
389
- defaultShouldRevalidate: true,
390
- finalShouldRevalidate: true,
391
- reason: "full-refetch",
392
- });
393
- }
394
- return true;
395
- }
396
- if (!clientSegmentIds.has(parallelId)) {
397
- const result = belongsToRoute || isNewParent;
398
- if (isTraceActive()) {
399
- pushRevalidationTraceEntry({
400
- segmentId: parallelId,
401
- segmentType: "parallel",
402
- belongsToRoute,
403
- source: "parallel",
404
- defaultShouldRevalidate: result,
405
- finalShouldRevalidate: result,
406
- reason: result ? "new-segment" : "skip-parent-chain",
407
- });
408
- }
409
- return result;
410
- }
411
-
412
- const dummySegment: ResolvedSegment = {
413
- id: parallelId,
414
- namespace: parallelEntry.id,
415
- type: "parallel",
416
- index: 0,
417
- component: null as any,
418
- params,
419
- slot,
420
- belongsToRoute,
421
- parallelName: `${parallelEntry.id}.${slot}`,
422
- ...(parallelEntry.mountPath
423
- ? { mountPath: parallelEntry.mountPath }
424
- : {}),
425
- };
426
-
427
- return await evaluateRevalidation({
428
- segment: dummySegment,
429
- prevParams,
430
- getPrevSegment: null,
431
- request,
432
- prevUrl,
433
- nextUrl,
434
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
435
- name: `revalidate${i}`,
436
- fn,
437
- })),
438
- routeKey,
439
- context,
440
- actionContext,
441
- stale,
442
- traceSource: "parallel",
443
- });
444
- })();
445
- emitRevalidationDecision(
446
- parallelId,
447
- context.pathname,
544
+ // A slot's loaders (never cached) are deduped per parallel entry and
545
+ // emitted either before or after the slot segment per loaderOrder.
546
+ const resolveSlotLoaders = async () => {
547
+ if (resolvedParallelEntries.has(parallelEntry.id)) return;
548
+ const loaderResult = await resolveLoadersWithRevalidation(
549
+ parallelEntry,
550
+ context,
551
+ belongsToRoute,
552
+ clientSegmentIds,
553
+ prevParams,
554
+ request,
555
+ prevUrl,
556
+ nextUrl,
448
557
  routeKey,
449
- shouldResolve,
558
+ deps,
559
+ actionContext,
560
+ entry.shortCode,
561
+ stale,
450
562
  );
563
+ segments.push(...loaderResult.segments);
564
+ matchedIds.push(...loaderResult.matchedIds);
565
+ resolvedParallelEntries.add(parallelEntry.id);
566
+ };
451
567
 
452
- let component: ReactNode | undefined;
453
- if (shouldResolve) {
454
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
455
- }
456
- if (component === undefined) {
457
- const hasLoadingFallback =
458
- parallelEntry.loading !== undefined &&
459
- parallelEntry.loading !== false;
460
- if (!shouldResolve) {
461
- component = null;
462
- } else if (hasLoadingFallback) {
463
- const result =
464
- typeof handler === "function" ? handler(context) : handler;
465
- if (result instanceof Promise) {
466
- const tracked = deps.trackHandler(result, {
467
- segmentId: parallelId,
468
- segmentType: "parallel",
469
- });
470
- observeStreamedHandler(
471
- tracked,
472
- parallelId,
473
- "parallel",
474
- context.pathname,
475
- routeKey,
476
- params,
477
- );
478
- component = tracked as ReactNode;
479
- } else {
480
- component = result as ReactNode;
481
- }
482
- } else {
483
- component =
484
- typeof handler === "function" ? await handler(context) : handler;
485
- }
568
+ if (loaderOrder === "before") {
569
+ await resolveSlotLoaders();
570
+ }
571
+ // Always announce the slot in matchedIds — it's unconditionally appended
572
+ // to `segments` below, and a segment present in segments but missing from
573
+ // matched lets the client prune it (then it's missing from clientSegmentIds
574
+ // on the next request, perpetuating the staleness).
575
+ matchedIds.push(parallelId);
576
+
577
+ let shouldResolve: boolean;
578
+ if (isFullRefetch) {
579
+ // Client has nothing cached — slot MUST render. User revalidate fns are
580
+ // bypassed here because returning false would leave the segment blank
581
+ // with no client-side fallback.
582
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
583
+ shouldResolve = true;
584
+ } else {
585
+ // For non-empty client sets, consult user revalidate fns. When the slot
586
+ // is unknown to the client, override the type-derived default so the
587
+ // soft chain seeds with the right "new segment" / "parent-chain" value.
588
+ let defaultOverride: { value: boolean; reason: string } | undefined;
589
+ if (!clientSegmentIds.has(parallelId)) {
590
+ const value =
591
+ parentChainDefault === "force-render"
592
+ ? true
593
+ : belongsToRoute || isNewParent;
594
+ defaultOverride = {
595
+ value,
596
+ reason: value ? "new-segment" : "skip-parent-chain",
597
+ };
486
598
  }
487
599
 
488
- segments.push({
600
+ const dummySegment: ResolvedSegment = {
489
601
  id: parallelId,
490
602
  namespace: parallelEntry.id,
491
603
  type: "parallel",
492
604
  index: 0,
493
- component,
494
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
495
- transition: parallelEntry.transition,
605
+ component: null as any,
496
606
  params,
497
607
  slot,
498
608
  belongsToRoute,
@@ -500,27 +610,69 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
500
610
  ...(parallelEntry.mountPath
501
611
  ? { mountPath: parallelEntry.mountPath }
502
612
  : {}),
503
- });
504
- }
613
+ };
505
614
 
506
- if (!parallelEntry.loading) {
507
- const loaderResult = await resolveLoadersWithRevalidation(
508
- parallelEntry,
509
- context,
510
- belongsToRoute,
511
- clientSegmentIds,
615
+ shouldResolve = await evaluateRevalidation({
616
+ segment: dummySegment,
512
617
  prevParams,
618
+ getPrevSegment: null,
513
619
  request,
514
620
  prevUrl,
515
621
  nextUrl,
622
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
623
+ name: `revalidate${i}`,
624
+ fn,
625
+ })),
516
626
  routeKey,
517
- deps,
627
+ context,
518
628
  actionContext,
519
- entry.shortCode,
520
629
  stale,
521
- );
522
- segments.push(...loaderResult.segments);
523
- matchedIds.push(...loaderResult.matchedIds);
630
+ traceSource: "parallel",
631
+ defaultOverride,
632
+ });
633
+ }
634
+ emitRevalidationDecision(
635
+ parallelId,
636
+ context.pathname,
637
+ routeKey,
638
+ shouldResolve,
639
+ );
640
+
641
+ const { component, handlerRan } = await resolveParallelSlotComponent({
642
+ shouldResolve,
643
+ parallelEntry,
644
+ slot,
645
+ parallelId,
646
+ handler,
647
+ context,
648
+ deps,
649
+ routeKey,
650
+ params,
651
+ });
652
+
653
+ segments.push({
654
+ id: parallelId,
655
+ namespace: parallelEntry.id,
656
+ type: "parallel",
657
+ index: 0,
658
+ component,
659
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
660
+ transition: applyViewTransitionDefault(
661
+ parallelEntry.transition,
662
+ deps.viewTransitionDefault,
663
+ ),
664
+ params,
665
+ slot,
666
+ _handlerRan: handlerRan,
667
+ belongsToRoute,
668
+ parallelName: `${parallelEntry.id}.${slot}`,
669
+ ...(parallelEntry.mountPath
670
+ ? { mountPath: parallelEntry.mountPath }
671
+ : {}),
672
+ });
673
+
674
+ if (loaderOrder === "after") {
675
+ await resolveSlotLoaders();
524
676
  }
525
677
  }
526
678
 
@@ -547,6 +699,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
547
699
  ): Promise<{ segment: ResolvedSegment; matchedId: string }> {
548
700
  const matchedId = entry.shortCode;
549
701
 
702
+ let handlerRan = false;
550
703
  const component = await revalidate(
551
704
  async () => {
552
705
  const hasSegment = clientSegmentIds.has(entry.shortCode);
@@ -607,6 +760,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
607
760
  context,
608
761
  actionContext,
609
762
  stale,
763
+ traceSource:
764
+ entry.type === "route" ? "route-handler" : "layout-handler",
610
765
  });
611
766
  emitRevalidationDecision(
612
767
  entry.shortCode,
@@ -621,20 +776,42 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
621
776
  return shouldRevalidate;
622
777
  },
623
778
  async () => {
779
+ handlerRan = true;
780
+ const doneHandler = track(`handler:${entry.id}`, 2);
624
781
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
625
782
  entry.shortCode;
626
783
  if (entry.type === "layout" || entry.type === "cache") {
627
- return resolveLayoutComponent(entry, context);
784
+ const layoutComponent = await resolveLayoutComponent(entry, context);
785
+ doneHandler();
786
+ return layoutComponent;
628
787
  }
629
788
  const staticComponent = await tryStaticHandler(entry, entry.shortCode);
630
- if (staticComponent !== undefined) return staticComponent;
789
+ if (staticComponent !== undefined) {
790
+ doneHandler();
791
+ return staticComponent;
792
+ }
631
793
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
794
+ // For Passthrough routes at runtime, use the live handler instead of
795
+ // the build handler. At build time (context.build === true), always
796
+ // use the build handler from routeEntry.handler.
797
+ const handler =
798
+ !context.build && routeEntry.liveHandler
799
+ ? routeEntry.liveHandler
800
+ : routeEntry.handler;
632
801
  if (!routeEntry.loading) {
633
- return handleHandlerResult(await routeEntry.handler(context));
802
+ const result = handleHandlerResult(
803
+ await observeHandler(entry.id, handler, context),
804
+ );
805
+ doneHandler();
806
+ return result;
634
807
  }
635
808
  if (!actionContext) {
636
- const result = handleHandlerResult(routeEntry.handler(context));
809
+ const result = handleHandlerResult(
810
+ observeHandler(entry.id, handler, context),
811
+ );
637
812
  if (result instanceof Promise) {
813
+ warnOnStreamedResponse(result, routeEntry.id);
814
+ result.finally(doneHandler).catch(() => {});
638
815
  const tracked = deps.trackHandler(result, {
639
816
  segmentId: entry.shortCode,
640
817
  segmentType: entry.type,
@@ -649,24 +826,29 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
649
826
  );
650
827
  return { content: tracked };
651
828
  }
829
+ doneHandler();
652
830
  return { content: result };
653
831
  }
654
832
  debugLog("segment.action", "resolving action route with awaited value", {
655
833
  entryId: entry.id,
656
834
  });
835
+ const actionResult = handleHandlerResult(
836
+ await observeHandler(entry.id, handler, context),
837
+ );
838
+ doneHandler();
657
839
  return {
658
- content: Promise.resolve(
659
- handleHandlerResult(await routeEntry.handler(context)),
660
- ),
840
+ content: Promise.resolve(actionResult),
661
841
  };
662
842
  },
663
843
  () => null,
664
844
  );
665
845
 
846
+ // Normalize void handlers (undefined) to null so the reconciler's
847
+ // component === null checks work consistently for both void and explicit null.
666
848
  const resolvedComponent =
667
849
  component && typeof component === "object" && "content" in component
668
- ? (component as { content: ReactNode }).content
669
- : component;
850
+ ? ((component as { content: ReactNode }).content ?? null)
851
+ : (component ?? null);
670
852
 
671
853
  const segment: ResolvedSegment = {
672
854
  id: entry.shortCode,
@@ -676,13 +858,17 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
676
858
  index: 0,
677
859
  component: resolvedComponent,
678
860
  loading: entry.loading === false ? null : entry.loading,
679
- transition: entry.transition,
861
+ transition: applyViewTransitionDefault(
862
+ entry.transition,
863
+ deps.viewTransitionDefault,
864
+ ),
680
865
  params,
681
866
  belongsToRoute,
682
867
  ...(entry.type === "layout" || entry.type === "cache"
683
868
  ? { layoutName: entry.id }
684
869
  : {}),
685
870
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
871
+ _handlerRan: handlerRan,
686
872
  };
687
873
 
688
874
  return { segment, matchedId };
@@ -701,7 +887,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
701
887
  request: Request,
702
888
  prevUrl: URL,
703
889
  nextUrl: URL,
704
- loaderPromises: Map<string, Promise<any>>,
705
890
  deps: SegmentResolutionDeps<TEnv>,
706
891
  actionContext?: ActionContext,
707
892
  stale?: boolean,
@@ -763,11 +948,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
763
948
  prevUrl,
764
949
  nextUrl,
765
950
  routeKey,
766
- loaderPromises,
767
951
  true,
768
952
  deps,
769
953
  actionContext,
770
954
  stale,
955
+ entry,
771
956
  );
772
957
  segments.push(...orphanResult.segments);
773
958
  matchedIds.push(...orphanResult.matchedIds);
@@ -847,7 +1032,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
847
1032
  prevUrl,
848
1033
  nextUrl,
849
1034
  routeKey,
850
- loaderPromises,
851
1035
  false,
852
1036
  deps,
853
1037
  actionContext,
@@ -874,11 +1058,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
874
1058
  prevUrl: URL,
875
1059
  nextUrl: URL,
876
1060
  routeKey: string,
877
- loaderPromises: Map<string, Promise<any>>,
878
1061
  belongsToRoute: boolean,
879
1062
  deps: SegmentResolutionDeps<TEnv>,
880
1063
  actionContext?: ActionContext,
881
1064
  stale?: boolean,
1065
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
1066
+ parentRouteEntry?: EntryData,
882
1067
  ): Promise<SegmentRevalidationResult> {
883
1068
  invariant(
884
1069
  orphan.type === "layout" || orphan.type === "cache",
@@ -906,6 +1091,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
906
1091
  segments.push(...loaderResult.segments);
907
1092
  matchedIds.push(...loaderResult.matchedIds);
908
1093
 
1094
+ // Inherit parent route's loaders so parallel slots inside this layout
1095
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1096
+ if (
1097
+ parentRouteEntry &&
1098
+ parentRouteEntry.loader &&
1099
+ parentRouteEntry.loader.length > 0 &&
1100
+ Object.keys(orphan.parallel).length > 0
1101
+ ) {
1102
+ const inheritedResult = await resolveLoadersWithRevalidation(
1103
+ parentRouteEntry,
1104
+ context,
1105
+ belongsToRoute,
1106
+ clientSegmentIds,
1107
+ prevParams,
1108
+ request,
1109
+ prevUrl,
1110
+ nextUrl,
1111
+ routeKey,
1112
+ deps,
1113
+ actionContext,
1114
+ orphan.shortCode,
1115
+ stale,
1116
+ );
1117
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1118
+ for (const s of inheritedResult.segments) {
1119
+ s._inherited = true;
1120
+ }
1121
+ segments.push(...inheritedResult.segments);
1122
+ matchedIds.push(...inheritedResult.matchedIds);
1123
+ }
1124
+
909
1125
  // Handler-first: resolve orphan layout handler before its parallels
910
1126
  // so ctx.set() values are visible to parallel children.
911
1127
  matchedIds.push(orphan.shortCode);
@@ -978,157 +1194,40 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
978
1194
  belongsToRoute,
979
1195
  layoutName: orphan.id,
980
1196
  loading: orphan.loading === false ? null : orphan.loading,
981
- transition: orphan.transition,
1197
+ transition: applyViewTransitionDefault(
1198
+ orphan.transition,
1199
+ deps.viewTransitionDefault,
1200
+ ),
982
1201
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
983
1202
  });
984
1203
 
985
- for (const parallelEntry of orphan.parallel) {
986
- invariant(
987
- parallelEntry.type === "parallel",
988
- `Expected parallel entry, got: ${parallelEntry.type}`,
989
- );
990
-
991
- const loaderResult = await resolveLoadersWithRevalidation(
992
- parallelEntry,
993
- context,
994
- belongsToRoute,
995
- clientSegmentIds,
996
- prevParams,
997
- request,
998
- prevUrl,
999
- nextUrl,
1000
- routeKey,
1001
- deps,
1002
- actionContext,
1003
- undefined,
1004
- stale,
1005
- );
1006
- segments.push(...loaderResult.segments);
1007
- matchedIds.push(...loaderResult.matchedIds);
1008
-
1009
- const slots = parallelEntry.handler as Record<
1010
- `@${string}`,
1011
- | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1012
- | ReactNode
1013
- >;
1014
-
1015
- for (const [slot, handler] of Object.entries(slots)) {
1016
- // Use orphan.shortCode (the parent layout) to match the SSR path
1017
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1018
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1019
- const parallelId = `${orphan.shortCode}.${slot}`;
1020
- matchedIds.push(parallelId);
1021
-
1022
- const shouldResolve = await (async () => {
1023
- if (!clientSegmentIds.has(parallelId)) {
1024
- if (isTraceActive()) {
1025
- pushRevalidationTraceEntry({
1026
- segmentId: parallelId,
1027
- segmentType: "parallel",
1028
- belongsToRoute,
1029
- source: "parallel",
1030
- defaultShouldRevalidate: true,
1031
- finalShouldRevalidate: true,
1032
- reason: "new-segment",
1033
- });
1034
- }
1035
- return true;
1036
- }
1037
-
1038
- const dummySegment: ResolvedSegment = {
1039
- id: parallelId,
1040
- namespace: parallelEntry.id,
1041
- type: "parallel",
1042
- index: 0,
1043
- component: null as any,
1044
- params,
1045
- slot,
1046
- belongsToRoute,
1047
- parallelName: `${parallelEntry.id}.${slot}`,
1048
- ...(parallelEntry.mountPath
1049
- ? { mountPath: parallelEntry.mountPath }
1050
- : {}),
1051
- };
1052
-
1053
- return await evaluateRevalidation({
1054
- segment: dummySegment,
1055
- prevParams,
1056
- getPrevSegment: null,
1057
- request,
1058
- prevUrl,
1059
- nextUrl,
1060
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1061
- name: `revalidate${i}`,
1062
- fn,
1063
- })),
1064
- routeKey,
1065
- context,
1066
- actionContext,
1067
- stale,
1068
- traceSource: "parallel",
1069
- });
1070
- })();
1071
- emitRevalidationDecision(
1072
- parallelId,
1073
- context.pathname,
1074
- routeKey,
1075
- shouldResolve,
1076
- );
1077
-
1078
- let component: ReactNode | undefined;
1079
- if (shouldResolve) {
1080
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1081
- }
1082
- if (component === undefined) {
1083
- const hasLoadingFallback =
1084
- parallelEntry.loading !== undefined &&
1085
- parallelEntry.loading !== false;
1086
- if (!shouldResolve) {
1087
- component = null;
1088
- } else if (hasLoadingFallback) {
1089
- const result =
1090
- typeof handler === "function" ? handler(context) : handler;
1091
- if (result instanceof Promise) {
1092
- const tracked = deps.trackHandler(result, {
1093
- segmentId: parallelId,
1094
- segmentType: "parallel",
1095
- });
1096
- observeStreamedHandler(
1097
- tracked,
1098
- parallelId,
1099
- "parallel",
1100
- context.pathname,
1101
- routeKey,
1102
- params,
1103
- );
1104
- component = tracked as ReactNode;
1105
- } else {
1106
- component = result as ReactNode;
1107
- }
1108
- } else {
1109
- component =
1110
- typeof handler === "function" ? await handler(context) : handler;
1111
- }
1112
- }
1113
-
1114
- segments.push({
1115
- id: parallelId,
1116
- namespace: parallelEntry.id,
1117
- type: "parallel",
1118
- index: 0,
1119
- component,
1120
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1121
- transition: parallelEntry.transition,
1122
- params,
1123
- slot,
1124
- belongsToRoute,
1125
- parallelName: `${parallelEntry.id}.${slot}`,
1126
- ...(parallelEntry.mountPath
1127
- ? { mountPath: parallelEntry.mountPath }
1128
- : {}),
1129
- });
1130
- }
1131
- }
1204
+ // Resolve the orphan layout's parallel slots through the shared main-path
1205
+ // helper. The orphan policy is carried by explicit args, byte-for-byte:
1206
+ // - parentChainDefault "force-render": an unknown parent-chain slot seeds
1207
+ // `true` (orphan parallels always belong to the route — the #482 guard),
1208
+ // where the main path would seed `belongsToRoute || isNewParent`.
1209
+ // - loaderOrder "before": a slot's loaders are emitted before the slot
1210
+ // segment, matching the prior inlined order.
1211
+ // `entry.shortCode` inside the helper is `orphan.shortCode` (orphan is passed
1212
+ // as `entry`), so the parallel ids + loader shortCodeOverride are unchanged.
1213
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
1214
+ orphan,
1215
+ params,
1216
+ context,
1217
+ belongsToRoute,
1218
+ clientSegmentIds,
1219
+ prevParams,
1220
+ request,
1221
+ prevUrl,
1222
+ nextUrl,
1223
+ routeKey,
1224
+ deps,
1225
+ actionContext,
1226
+ stale,
1227
+ { parentChainDefault: "force-render", loaderOrder: "before" },
1228
+ );
1229
+ segments.push(...parallelResult.segments);
1230
+ matchedIds.push(...parallelResult.matchedIds);
1132
1231
 
1133
1232
  return { segments, matchedIds };
1134
1233
  }
@@ -1146,12 +1245,12 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1146
1245
  request: Request,
1147
1246
  prevUrl: URL,
1148
1247
  nextUrl: URL,
1149
- loaderPromises: Map<string, Promise<any>>,
1150
1248
  actionContext: ActionContext | undefined,
1151
1249
  interceptResult: { intercept: any; entry: EntryData } | null,
1152
1250
  localRouteName: string,
1153
1251
  pathname: string,
1154
1252
  deps: SegmentResolutionDeps<TEnv>,
1253
+ stale?: boolean,
1155
1254
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1156
1255
  const allSegments: ResolvedSegment[] = [];
1157
1256
  const matchedIds: string[] = [];
@@ -1178,6 +1277,11 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1178
1277
  }
1179
1278
 
1180
1279
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1280
+ if (entry.type === "cache") {
1281
+ const store = RangoContext.getStore();
1282
+ if (store) store.insideCacheScope = true;
1283
+ }
1284
+ const doneEntry = track(`segment:${entry.id}`, 1);
1181
1285
  const resolved = await resolveWithErrorBoundary(
1182
1286
  nonParallelEntry,
1183
1287
  params,
@@ -1192,18 +1296,16 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1192
1296
  request,
1193
1297
  prevUrl,
1194
1298
  nextUrl,
1195
- loaderPromises,
1196
1299
  deps,
1197
1300
  actionContext,
1198
- false,
1301
+ stale,
1199
1302
  ),
1200
1303
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1201
1304
  deps,
1202
- telemetry
1203
- ? { request, url: context.url, routeKey, isPartial: true, telemetry }
1204
- : undefined,
1305
+ { request, url: context.url, routeKey, isPartial: true, telemetry },
1205
1306
  pathname,
1206
1307
  );
1308
+ doneEntry();
1207
1309
 
1208
1310
  // Deduplicate segments and matchedIds by ID, matching resolveAllSegments.
1209
1311
  // include() scopes can produce entries that resolve the same shared