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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +441 -134
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3453 -1240
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +76 -21
  8. package/skills/api-client/SKILL.md +211 -0
  9. package/skills/breadcrumbs/SKILL.md +64 -2
  10. package/skills/bundle-analysis/SKILL.md +159 -0
  11. package/skills/cache-guide/SKILL.md +247 -23
  12. package/skills/caching/SKILL.md +318 -15
  13. package/skills/composability/SKILL.md +27 -2
  14. package/skills/css/SKILL.md +76 -0
  15. package/skills/debug-manifest/SKILL.md +4 -2
  16. package/skills/document-cache/SKILL.md +78 -55
  17. package/skills/handler-use/SKILL.md +364 -0
  18. package/skills/hooks/SKILL.md +250 -30
  19. package/skills/host-router/SKILL.md +83 -23
  20. package/skills/i18n/SKILL.md +276 -0
  21. package/skills/intercept/SKILL.md +50 -6
  22. package/skills/layout/SKILL.md +35 -9
  23. package/skills/links/SKILL.md +249 -17
  24. package/skills/loader/SKILL.md +279 -53
  25. package/skills/middleware/SKILL.md +52 -13
  26. package/skills/migrate-nextjs/SKILL.md +584 -0
  27. package/skills/migrate-react-router/SKILL.md +771 -0
  28. package/skills/mime-routes/SKILL.md +28 -1
  29. package/skills/observability/SKILL.md +172 -0
  30. package/skills/parallel/SKILL.md +203 -7
  31. package/skills/prerender/SKILL.md +153 -109
  32. package/skills/rango/SKILL.md +251 -22
  33. package/skills/react-compiler/SKILL.md +168 -0
  34. package/skills/response-routes/SKILL.md +123 -48
  35. package/skills/route/SKILL.md +101 -5
  36. package/skills/router-setup/SKILL.md +116 -8
  37. package/skills/scripts/SKILL.md +179 -0
  38. package/skills/server-actions/SKILL.md +775 -0
  39. package/skills/streams-and-websockets/SKILL.md +283 -0
  40. package/skills/tailwind/SKILL.md +27 -3
  41. package/skills/testing/SKILL.md +129 -0
  42. package/skills/testing/bindings.md +103 -0
  43. package/skills/testing/cache-prerender.md +127 -0
  44. package/skills/testing/client-components.md +124 -0
  45. package/skills/testing/e2e-parity.md +125 -0
  46. package/skills/testing/flight.md +91 -0
  47. package/skills/testing/handles.md +129 -0
  48. package/skills/testing/loader.md +128 -0
  49. package/skills/testing/middleware.md +99 -0
  50. package/skills/testing/render-handler.md +121 -0
  51. package/skills/testing/response-routes.md +95 -0
  52. package/skills/testing/reverse-and-types.md +84 -0
  53. package/skills/testing/server-actions.md +107 -0
  54. package/skills/testing/server-tree.md +128 -0
  55. package/skills/testing/setup.md +123 -0
  56. package/skills/typesafety/SKILL.md +332 -29
  57. package/skills/use-cache/SKILL.md +46 -14
  58. package/skills/view-transitions/SKILL.md +294 -0
  59. package/src/__augment-tests__/augment.ts +81 -0
  60. package/src/__augment-tests__/augmented.check.ts +116 -0
  61. package/src/__internal.ts +1 -66
  62. package/src/browser/action-coordinator.ts +53 -36
  63. package/src/browser/action-fence.ts +47 -0
  64. package/src/browser/app-shell.ts +39 -0
  65. package/src/browser/app-version.ts +14 -0
  66. package/src/browser/connection-warmup.ts +134 -0
  67. package/src/browser/cookie-name.ts +140 -0
  68. package/src/browser/event-controller.ts +197 -150
  69. package/src/browser/history-state.ts +21 -0
  70. package/src/browser/index.ts +3 -3
  71. package/src/browser/invalidate-client-cache.ts +52 -0
  72. package/src/browser/navigation-bridge.ts +111 -31
  73. package/src/browser/navigation-client.ts +201 -67
  74. package/src/browser/navigation-store-handle.ts +38 -0
  75. package/src/browser/navigation-store.ts +76 -67
  76. package/src/browser/navigation-transaction.ts +18 -66
  77. package/src/browser/network-error-handler.ts +34 -7
  78. package/src/browser/partial-update.ts +187 -112
  79. package/src/browser/prefetch/cache.ts +230 -35
  80. package/src/browser/prefetch/fetch.ts +338 -39
  81. package/src/browser/prefetch/queue.ts +126 -20
  82. package/src/browser/prefetch/resource-ready.ts +77 -0
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +111 -16
  85. package/src/browser/react/NavigationProvider.tsx +135 -120
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/filter-segment-order.ts +66 -7
  89. package/src/browser/react/index.ts +0 -48
  90. package/src/browser/react/location-state-shared.ts +178 -8
  91. package/src/browser/react/location-state.ts +39 -14
  92. package/src/browser/react/use-action.ts +6 -15
  93. package/src/browser/react/use-handle.ts +23 -69
  94. package/src/browser/react/use-href.tsx +8 -1
  95. package/src/browser/react/use-link-status.ts +33 -8
  96. package/src/browser/react/use-navigation.ts +32 -7
  97. package/src/browser/react/use-params.ts +20 -10
  98. package/src/browser/react/use-reverse.ts +106 -0
  99. package/src/browser/react/use-router.ts +46 -11
  100. package/src/browser/react/use-search-params.ts +0 -5
  101. package/src/browser/react/use-segments.ts +11 -21
  102. package/src/browser/response-adapter.ts +80 -5
  103. package/src/browser/rsc-router.tsx +218 -76
  104. package/src/browser/scroll-restoration.ts +54 -42
  105. package/src/browser/segment-reconciler.ts +36 -9
  106. package/src/browser/segment-structure-assert.ts +2 -2
  107. package/src/browser/server-action-bridge.ts +222 -61
  108. package/src/browser/types.ts +91 -11
  109. package/src/browser/validate-redirect-origin.ts +43 -16
  110. package/src/build/collect-fallback-refs.ts +107 -0
  111. package/src/build/generate-manifest.ts +65 -40
  112. package/src/build/generate-route-types.ts +5 -1
  113. package/src/build/index.ts +8 -2
  114. package/src/build/prefix-tree-utils.ts +123 -0
  115. package/src/build/route-trie.ts +165 -36
  116. package/src/build/route-types/ast-route-extraction.ts +15 -8
  117. package/src/build/route-types/codegen.ts +16 -5
  118. package/src/build/route-types/include-resolution.ts +125 -24
  119. package/src/build/route-types/param-extraction.ts +6 -3
  120. package/src/build/route-types/per-module-writer.ts +22 -6
  121. package/src/build/route-types/router-processing.ts +272 -96
  122. package/src/build/route-types/scan-filter.ts +9 -2
  123. package/src/build/route-types/source-scan.ts +216 -0
  124. package/src/build/runtime-discovery.ts +9 -20
  125. package/src/cache/cache-error.ts +104 -0
  126. package/src/cache/cache-key-utils.ts +29 -13
  127. package/src/cache/cache-policy.ts +108 -34
  128. package/src/cache/cache-runtime.ts +214 -48
  129. package/src/cache/cache-scope.ts +236 -89
  130. package/src/cache/cache-tag.ts +103 -0
  131. package/src/cache/cf/cf-base64.ts +33 -0
  132. package/src/cache/cf/cf-cache-constants.ts +127 -0
  133. package/src/cache/cf/cf-cache-store.ts +2224 -171
  134. package/src/cache/cf/cf-cache-types.ts +349 -0
  135. package/src/cache/cf/cf-kv-utils.ts +46 -0
  136. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  137. package/src/cache/cf/index.ts +11 -17
  138. package/src/cache/document-cache.ts +89 -27
  139. package/src/cache/handle-snapshot.ts +70 -0
  140. package/src/cache/index.ts +11 -20
  141. package/src/cache/memory-segment-store.ts +136 -37
  142. package/src/cache/profile-registry.ts +31 -31
  143. package/src/cache/read-through-swr.ts +41 -11
  144. package/src/cache/segment-codec.ts +9 -17
  145. package/src/cache/tag-invalidation.ts +230 -0
  146. package/src/cache/taint.ts +55 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +84 -2
  155. package/src/debug.ts +2 -2
  156. package/src/decode-loader-results.ts +52 -0
  157. package/src/defer.ts +196 -0
  158. package/src/deps/ssr.ts +0 -1
  159. package/src/encode-kv.ts +49 -0
  160. package/src/errors.ts +30 -4
  161. package/src/escape-script.ts +52 -0
  162. package/src/handle.ts +70 -22
  163. package/src/handles/MetaTags.tsx +56 -19
  164. package/src/handles/Scripts.tsx +183 -0
  165. package/src/handles/breadcrumbs.ts +37 -8
  166. package/src/handles/is-thenable.ts +19 -0
  167. package/src/handles/meta.ts +51 -40
  168. package/src/handles/script.ts +244 -0
  169. package/src/host/cookie-handler.ts +9 -60
  170. package/src/host/errors.ts +0 -24
  171. package/src/host/index.ts +8 -2
  172. package/src/host/pattern-matcher.ts +23 -52
  173. package/src/host/router.ts +107 -99
  174. package/src/host/testing.ts +40 -27
  175. package/src/host/types.ts +37 -4
  176. package/src/host/utils.ts +1 -1
  177. package/src/href-client.ts +137 -22
  178. package/src/index.rsc.ts +93 -12
  179. package/src/index.ts +133 -15
  180. package/src/internal-debug.ts +11 -10
  181. package/src/loader-store.ts +500 -0
  182. package/src/loader.rsc.ts +20 -13
  183. package/src/loader.ts +12 -11
  184. package/src/missing-id-error.ts +68 -0
  185. package/src/outlet-context.ts +1 -1
  186. package/src/outlet-provider.tsx +1 -5
  187. package/src/prerender/param-hash.ts +16 -16
  188. package/src/prerender/store.ts +37 -41
  189. package/src/prerender.ts +198 -82
  190. package/src/redirect-origin.ts +100 -0
  191. package/src/regex-escape.ts +8 -0
  192. package/src/render-error-thrower.tsx +20 -0
  193. package/src/response-utils.ts +62 -0
  194. package/src/reverse.ts +65 -15
  195. package/src/root-error-boundary.tsx +1 -19
  196. package/src/route-content-wrapper.tsx +7 -72
  197. package/src/route-definition/dsl-helpers.ts +469 -276
  198. package/src/route-definition/helper-factories.ts +29 -139
  199. package/src/route-definition/helpers-types.ts +113 -37
  200. package/src/route-definition/index.ts +3 -0
  201. package/src/route-definition/redirect.ts +53 -12
  202. package/src/route-definition/resolve-handler-use.ts +161 -0
  203. package/src/route-definition/use-item-types.ts +32 -0
  204. package/src/route-map-builder.ts +7 -17
  205. package/src/route-types.ts +37 -41
  206. package/src/router/basename.ts +14 -0
  207. package/src/router/content-negotiation.ts +164 -17
  208. package/src/router/error-handling.ts +45 -18
  209. package/src/router/find-match.ts +45 -22
  210. package/src/router/handler-context.ts +83 -39
  211. package/src/router/instrument.ts +350 -0
  212. package/src/router/intercept-resolution.ts +50 -24
  213. package/src/router/lazy-includes.ts +19 -53
  214. package/src/router/loader-resolution.ts +274 -56
  215. package/src/router/logging.ts +5 -8
  216. package/src/router/manifest.ts +49 -45
  217. package/src/router/match-api.ts +120 -204
  218. package/src/router/match-context.ts +0 -22
  219. package/src/router/match-handlers.ts +58 -58
  220. package/src/router/match-middleware/background-revalidation.ts +33 -6
  221. package/src/router/match-middleware/cache-lookup.ts +214 -263
  222. package/src/router/match-middleware/cache-store.ts +73 -33
  223. package/src/router/match-middleware/intercept-resolution.ts +8 -28
  224. package/src/router/match-middleware/segment-resolution.ts +52 -18
  225. package/src/router/match-pipelines.ts +1 -42
  226. package/src/router/match-result.ts +104 -40
  227. package/src/router/metrics.ts +5 -34
  228. package/src/router/middleware-types.ts +13 -142
  229. package/src/router/middleware.ts +270 -172
  230. package/src/router/navigation-snapshot.ts +131 -0
  231. package/src/router/params-util.ts +23 -0
  232. package/src/router/pattern-matching.ts +132 -90
  233. package/src/router/prefetch-cache-ttl.ts +51 -0
  234. package/src/router/prerender-match.ts +195 -56
  235. package/src/router/preview-match.ts +32 -102
  236. package/src/router/request-classification.ts +276 -0
  237. package/src/router/revalidation.ts +123 -73
  238. package/src/router/route-snapshot.ts +244 -0
  239. package/src/router/router-context.ts +8 -28
  240. package/src/router/router-interfaces.ts +115 -35
  241. package/src/router/router-options.ts +172 -15
  242. package/src/router/router-registry.ts +2 -5
  243. package/src/router/segment-resolution/fresh.ts +264 -77
  244. package/src/router/segment-resolution/helpers.ts +115 -30
  245. package/src/router/segment-resolution/loader-cache.ts +63 -37
  246. package/src/router/segment-resolution/revalidation.ts +474 -385
  247. package/src/router/segment-resolution/static-store.ts +19 -5
  248. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  249. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  250. package/src/router/segment-resolution.ts +5 -1
  251. package/src/router/segment-wrappers.ts +8 -5
  252. package/src/router/state-cookie-name.ts +33 -0
  253. package/src/router/substitute-pattern-params.ts +56 -0
  254. package/src/router/telemetry-otel.ts +161 -199
  255. package/src/router/telemetry.ts +96 -19
  256. package/src/router/timeout.ts +0 -20
  257. package/src/router/tracing.ts +206 -0
  258. package/src/router/trie-matching.ts +163 -59
  259. package/src/router/types.ts +10 -63
  260. package/src/router/url-params.ts +44 -0
  261. package/src/router.ts +163 -55
  262. package/src/rsc/handler-context.ts +3 -2
  263. package/src/rsc/handler.ts +658 -511
  264. package/src/rsc/helpers.ts +168 -46
  265. package/src/rsc/index.ts +2 -5
  266. package/src/rsc/json-route-result.ts +38 -0
  267. package/src/rsc/loader-fetch.ts +127 -31
  268. package/src/rsc/manifest-init.ts +33 -42
  269. package/src/rsc/origin-guard.ts +39 -25
  270. package/src/rsc/progressive-enhancement.ts +77 -11
  271. package/src/rsc/redirect-guard.ts +99 -0
  272. package/src/rsc/response-cache-serve.ts +238 -0
  273. package/src/rsc/response-error.ts +79 -12
  274. package/src/rsc/response-route-handler.ts +99 -189
  275. package/src/rsc/rsc-rendering.ts +105 -72
  276. package/src/rsc/runtime-warnings.ts +23 -10
  277. package/src/rsc/server-action.ts +263 -112
  278. package/src/rsc/ssr-setup.ts +18 -2
  279. package/src/rsc/types.ts +32 -6
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +281 -129
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +309 -61
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +26 -46
  291. package/src/server/request-context.ts +398 -172
  292. package/src/ssr/index.tsx +25 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +99 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +343 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  320. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  321. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  322. package/src/testing/vitest-stubs/version.ts +5 -0
  323. package/src/testing/vitest.ts +305 -0
  324. package/src/theme/ThemeProvider.tsx +20 -58
  325. package/src/theme/ThemeScript.tsx +7 -9
  326. package/src/theme/constants.ts +52 -13
  327. package/src/theme/index.ts +0 -7
  328. package/src/theme/theme-context.ts +1 -5
  329. package/src/theme/theme-script.ts +22 -21
  330. package/src/theme/use-theme.ts +0 -3
  331. package/src/types/boundaries.ts +0 -35
  332. package/src/types/cache-types.ts +17 -8
  333. package/src/types/error-types.ts +30 -90
  334. package/src/types/global-namespace.ts +54 -41
  335. package/src/types/handler-context.ts +233 -81
  336. package/src/types/index.ts +1 -10
  337. package/src/types/loader-types.ts +44 -15
  338. package/src/types/request-scope.ts +112 -0
  339. package/src/types/route-config.ts +6 -50
  340. package/src/types/route-entry.ts +19 -7
  341. package/src/types/segments.ts +37 -14
  342. package/src/urls/include-helper.ts +33 -70
  343. package/src/urls/index.ts +1 -11
  344. package/src/urls/path-helper-types.ts +58 -11
  345. package/src/urls/path-helper.ts +57 -111
  346. package/src/urls/pattern-types.ts +48 -19
  347. package/src/urls/response-types.ts +25 -22
  348. package/src/urls/type-extraction.ts +58 -139
  349. package/src/urls/urls-function.ts +1 -18
  350. package/src/use-loader.tsx +346 -89
  351. package/src/vite/debug.ts +185 -0
  352. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  353. package/src/vite/discovery/discover-routers.ts +130 -85
  354. package/src/vite/discovery/discovery-errors.ts +194 -0
  355. package/src/vite/discovery/gate-state.ts +171 -0
  356. package/src/vite/discovery/prerender-collection.ts +214 -132
  357. package/src/vite/discovery/route-types-writer.ts +40 -84
  358. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  359. package/src/vite/discovery/state.ts +57 -6
  360. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  361. package/src/vite/index.ts +6 -0
  362. package/src/vite/inject-client-debug.ts +36 -0
  363. package/src/vite/plugin-types.ts +155 -65
  364. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  365. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  366. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  367. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  369. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  370. package/src/vite/plugins/expose-action-id.ts +49 -98
  371. package/src/vite/plugins/expose-id-utils.ts +96 -51
  372. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  373. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  374. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  375. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  376. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  377. package/src/vite/plugins/performance-tracks.ts +89 -0
  378. package/src/vite/plugins/refresh-cmd.ts +89 -27
  379. package/src/vite/plugins/use-cache-transform.ts +73 -83
  380. package/src/vite/plugins/version-injector.ts +21 -25
  381. package/src/vite/plugins/version-plugin.ts +46 -37
  382. package/src/vite/plugins/virtual-entries.ts +13 -18
  383. package/src/vite/rango.ts +238 -295
  384. package/src/vite/router-discovery.ts +940 -149
  385. package/src/vite/utils/ast-handler-extract.ts +26 -35
  386. package/src/vite/utils/banner.ts +4 -4
  387. package/src/vite/utils/bundle-analysis.ts +10 -15
  388. package/src/vite/utils/client-chunks.ts +184 -0
  389. package/src/vite/utils/directive-prologue.ts +40 -0
  390. package/src/vite/utils/forward-user-plugins.ts +171 -0
  391. package/src/vite/utils/manifest-utils.ts +4 -59
  392. package/src/vite/utils/package-resolution.ts +20 -52
  393. package/src/vite/utils/prerender-utils.ts +81 -34
  394. package/src/vite/utils/shared-utils.ts +92 -42
  395. package/src/browser/action-response-classifier.ts +0 -99
  396. package/src/browser/react/use-client-cache.ts +0 -58
  397. package/src/browser/shallow.ts +0 -40
  398. package/src/handles/index.ts +0 -7
  399. package/src/network-error-thrower.tsx +0 -23
  400. package/src/router/middleware-cookies.ts +0 -55
@@ -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,54 +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
- import { track } from "../../server/context.js";
41
-
42
- // ---------------------------------------------------------------------------
43
- // Telemetry helpers
44
- // ---------------------------------------------------------------------------
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";
45
54
 
46
55
  /**
47
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
48
- * 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.
49
59
  */
50
- function observeStreamedHandler(
51
- promise: Promise<ReactNode>,
52
- segmentId: string,
53
- segmentType: string,
54
- pathname?: string,
55
- routeKey?: string,
56
- params?: Record<string, string>,
60
+ function traceFullRefetchedParallelSlot(
61
+ parallelId: string,
62
+ belongsToRoute: boolean,
57
63
  ): void {
58
- let routerCtx;
59
- try {
60
- routerCtx = getRouterContext();
61
- } catch {
62
- return;
63
- }
64
- if (!routerCtx?.telemetry) return;
65
- const sink = resolveSink(routerCtx.telemetry);
66
- const reqId = routerCtx.requestId;
67
- promise.catch((err: unknown) => {
68
- const errorObj = err instanceof Error ? err : new Error(String(err));
69
- safeEmit(sink, {
70
- type: "handler.error",
71
- timestamp: performance.now(),
72
- requestId: reqId,
73
- segmentId,
74
- segmentType,
75
- error: errorObj,
76
- handledByBoundary: true,
77
- pathname,
78
- routeKey,
79
- params,
80
- });
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",
81
73
  });
82
74
  }
83
75
 
@@ -96,23 +88,14 @@ function emitRevalidationDecision(
96
88
  routeKey: string,
97
89
  shouldRevalidate: boolean,
98
90
  ): void {
99
- let routerCtx;
100
- try {
101
- routerCtx = getRouterContext();
102
- } catch {
103
- return;
104
- }
105
- if (routerCtx?.telemetry) {
106
- safeEmit(resolveSink(routerCtx.telemetry), {
107
- type: "revalidation.decision",
108
- timestamp: performance.now(),
109
- requestId: routerCtx.requestId,
110
- segmentId,
111
- pathname,
112
- routeKey,
113
- shouldRevalidate,
114
- });
115
- }
91
+ observeEvent({
92
+ type: "revalidation.decision",
93
+ timestamp: performance.now(),
94
+ segmentId,
95
+ pathname,
96
+ routeKey,
97
+ shouldRevalidate,
98
+ });
116
99
  }
117
100
 
118
101
  // ---------------------------------------------------------------------------
@@ -217,6 +200,10 @@ export async function resolveLoadersWithRevalidation<TEnv>(
217
200
  ),
218
201
  );
219
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
+
220
207
  const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
221
208
  const segments: ResolvedSegment[] = loadersToRun.map(
222
209
  ({ loaderEntry, loader, segmentId, index }) => ({
@@ -228,10 +215,13 @@ export async function resolveLoadersWithRevalidation<TEnv>(
228
215
  params: ctx.params,
229
216
  loaderId: loader.$$id,
230
217
  loaderData: deps.wrapLoaderPromise(
231
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
218
+ runInsideLoaderScope(() =>
219
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
220
+ ),
232
221
  entry,
233
222
  segmentId,
234
223
  ctx.pathname,
224
+ errorContext,
235
225
  ),
236
226
  belongsToRoute,
237
227
  }),
@@ -258,26 +248,95 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
258
248
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
259
249
  const allLoaderSegments: ResolvedSegment[] = [];
260
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
+ }
261
337
 
262
338
  for (const entry of entries) {
263
- const belongsToRoute = entry.type === "route";
264
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
265
- entry,
266
- context,
267
- belongsToRoute,
268
- clientSegmentIds,
269
- prevParams,
270
- request,
271
- prevUrl,
272
- nextUrl,
273
- routeKey,
274
- deps,
275
- actionContext,
276
- undefined, // shortCodeOverride
277
- stale,
278
- );
279
- allLoaderSegments.push(...segments);
280
- allMatchedIds.push(...matchedIds);
339
+ await collectEntryLoaders(entry, entry.type === "route");
281
340
  }
282
341
 
283
342
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -301,22 +360,20 @@ export function buildEntryRevalidateMap(
301
360
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
302
361
 
303
362
  if (entry.type !== "parallel") {
304
- for (const parallelEntry of entry.parallel) {
305
- if (parallelEntry.type === "parallel") {
306
- const slots = Object.keys(parallelEntry.handler) as `@${string}`[];
307
- for (const slot of slots) {
308
- const parallelId = `${parallelEntry.shortCode}.${slot}`;
309
- map.set(parallelId, {
310
- entry: parallelEntry,
311
- revalidate: parallelEntry.revalidate,
312
- });
313
- }
314
- }
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
+ });
315
372
  }
316
373
  }
317
374
 
318
375
  for (const layoutEntry of entry.layout) {
319
- processEntry(layoutEntry);
376
+ processEntry(layoutEntry, entry.shortCode);
320
377
  }
321
378
  }
322
379
 
@@ -327,6 +384,97 @@ export function buildEntryRevalidateMap(
327
384
  return map;
328
385
  }
329
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
+
330
478
  /**
331
479
  * Resolve parallel segments with revalidation.
332
480
  */
@@ -344,11 +492,35 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
344
492
  deps: SegmentResolutionDeps<TEnv>,
345
493
  actionContext?: ActionContext,
346
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
+ },
347
514
  ): Promise<SegmentRevalidationResult> {
348
515
  const segments: ResolvedSegment[] = [];
349
516
  const matchedIds: string[] = [];
517
+ const parentChainDefault = options?.parentChainDefault ?? "type-derived";
518
+ const loaderOrder = options?.loaderOrder ?? "after";
350
519
 
351
- for (const parallelEntry of entry.parallel) {
520
+ const resolvedParallelEntries = new Set<string>();
521
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
522
+ entry.parallel,
523
+ )) {
352
524
  invariant(
353
525
  parallelEntry.type === "parallel",
354
526
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -359,141 +531,78 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
359
531
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
360
532
  | ReactNode
361
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];
362
538
 
363
- for (const [slot, handler] of Object.entries(slots)) {
364
- const parallelId = `${entry.shortCode}.${slot}`;
365
-
366
- const isFullRefetch = clientSegmentIds.size === 0;
367
- // When the parent layout is new (not in client's segment set),
368
- // all its parallel children must be resolved and tracked.
369
- // Without this, navigating to a new layout with parallels
370
- // (e.g., BlogLayout with @sidebar) from a different route
371
- // would silently drop those parallel segments.
372
- const isNewParent = !clientSegmentIds.has(entry.shortCode);
373
- if (
374
- isFullRefetch ||
375
- clientSegmentIds.has(parallelId) ||
376
- belongsToRoute ||
377
- isNewParent
378
- ) {
379
- matchedIds.push(parallelId);
380
- }
381
-
382
- const shouldResolve = await (async () => {
383
- if (isFullRefetch) {
384
- if (isTraceActive()) {
385
- pushRevalidationTraceEntry({
386
- segmentId: parallelId,
387
- segmentType: "parallel",
388
- belongsToRoute,
389
- source: "parallel",
390
- defaultShouldRevalidate: true,
391
- finalShouldRevalidate: true,
392
- reason: "full-refetch",
393
- });
394
- }
395
- return true;
396
- }
397
- if (!clientSegmentIds.has(parallelId)) {
398
- const result = belongsToRoute || isNewParent;
399
- if (isTraceActive()) {
400
- pushRevalidationTraceEntry({
401
- segmentId: parallelId,
402
- segmentType: "parallel",
403
- belongsToRoute,
404
- source: "parallel",
405
- defaultShouldRevalidate: result,
406
- finalShouldRevalidate: result,
407
- reason: result ? "new-segment" : "skip-parent-chain",
408
- });
409
- }
410
- return result;
411
- }
539
+ const parallelId = `${entry.shortCode}.${slot}`;
412
540
 
413
- const dummySegment: ResolvedSegment = {
414
- id: parallelId,
415
- namespace: parallelEntry.id,
416
- type: "parallel",
417
- index: 0,
418
- component: null as any,
419
- params,
420
- slot,
421
- belongsToRoute,
422
- parallelName: `${parallelEntry.id}.${slot}`,
423
- ...(parallelEntry.mountPath
424
- ? { mountPath: parallelEntry.mountPath }
425
- : {}),
426
- };
541
+ const isFullRefetch = clientSegmentIds.size === 0;
542
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
427
543
 
428
- return await evaluateRevalidation({
429
- segment: dummySegment,
430
- prevParams,
431
- getPrevSegment: null,
432
- request,
433
- prevUrl,
434
- nextUrl,
435
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
436
- name: `revalidate${i}`,
437
- fn,
438
- })),
439
- routeKey,
440
- context,
441
- actionContext,
442
- stale,
443
- traceSource: "parallel",
444
- });
445
- })();
446
- emitRevalidationDecision(
447
- parallelId,
448
- 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,
449
557
  routeKey,
450
- shouldResolve,
558
+ deps,
559
+ actionContext,
560
+ entry.shortCode,
561
+ stale,
451
562
  );
563
+ segments.push(...loaderResult.segments);
564
+ matchedIds.push(...loaderResult.matchedIds);
565
+ resolvedParallelEntries.add(parallelEntry.id);
566
+ };
452
567
 
453
- let component: ReactNode | undefined;
454
- if (shouldResolve) {
455
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
456
- }
457
- if (component === undefined) {
458
- const hasLoadingFallback =
459
- parallelEntry.loading !== undefined &&
460
- parallelEntry.loading !== false;
461
- if (!shouldResolve) {
462
- component = null;
463
- } else if (hasLoadingFallback) {
464
- const result =
465
- typeof handler === "function" ? handler(context) : handler;
466
- if (result instanceof Promise) {
467
- const tracked = deps.trackHandler(result, {
468
- segmentId: parallelId,
469
- segmentType: "parallel",
470
- });
471
- observeStreamedHandler(
472
- tracked,
473
- parallelId,
474
- "parallel",
475
- context.pathname,
476
- routeKey,
477
- params,
478
- );
479
- component = tracked as ReactNode;
480
- } else {
481
- component = result as ReactNode;
482
- }
483
- } else {
484
- component =
485
- typeof handler === "function" ? await handler(context) : handler;
486
- }
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
+ };
487
598
  }
488
599
 
489
- segments.push({
600
+ const dummySegment: ResolvedSegment = {
490
601
  id: parallelId,
491
602
  namespace: parallelEntry.id,
492
603
  type: "parallel",
493
604
  index: 0,
494
- component,
495
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
496
- transition: parallelEntry.transition,
605
+ component: null as any,
497
606
  params,
498
607
  slot,
499
608
  belongsToRoute,
@@ -501,27 +610,69 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
501
610
  ...(parallelEntry.mountPath
502
611
  ? { mountPath: parallelEntry.mountPath }
503
612
  : {}),
504
- });
505
- }
613
+ };
506
614
 
507
- if (!parallelEntry.loading) {
508
- const loaderResult = await resolveLoadersWithRevalidation(
509
- parallelEntry,
510
- context,
511
- belongsToRoute,
512
- clientSegmentIds,
615
+ shouldResolve = await evaluateRevalidation({
616
+ segment: dummySegment,
513
617
  prevParams,
618
+ getPrevSegment: null,
514
619
  request,
515
620
  prevUrl,
516
621
  nextUrl,
622
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
623
+ name: `revalidate${i}`,
624
+ fn,
625
+ })),
517
626
  routeKey,
518
- deps,
627
+ context,
519
628
  actionContext,
520
- entry.shortCode,
521
629
  stale,
522
- );
523
- segments.push(...loaderResult.segments);
524
- 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();
525
676
  }
526
677
  }
527
678
 
@@ -548,6 +699,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
548
699
  ): Promise<{ segment: ResolvedSegment; matchedId: string }> {
549
700
  const matchedId = entry.shortCode;
550
701
 
702
+ let handlerRan = false;
551
703
  const component = await revalidate(
552
704
  async () => {
553
705
  const hasSegment = clientSegmentIds.has(entry.shortCode);
@@ -608,6 +760,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
608
760
  context,
609
761
  actionContext,
610
762
  stale,
763
+ traceSource:
764
+ entry.type === "route" ? "route-handler" : "layout-handler",
611
765
  });
612
766
  emitRevalidationDecision(
613
767
  entry.shortCode,
@@ -622,6 +776,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
622
776
  return shouldRevalidate;
623
777
  },
624
778
  async () => {
779
+ handlerRan = true;
625
780
  const doneHandler = track(`handler:${entry.id}`, 2);
626
781
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
627
782
  entry.shortCode;
@@ -636,14 +791,26 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
636
791
  return staticComponent;
637
792
  }
638
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;
639
801
  if (!routeEntry.loading) {
640
- const result = handleHandlerResult(await routeEntry.handler(context));
802
+ const result = handleHandlerResult(
803
+ await observeHandler(entry.id, handler, context),
804
+ );
641
805
  doneHandler();
642
806
  return result;
643
807
  }
644
808
  if (!actionContext) {
645
- const result = handleHandlerResult(routeEntry.handler(context));
809
+ const result = handleHandlerResult(
810
+ observeHandler(entry.id, handler, context),
811
+ );
646
812
  if (result instanceof Promise) {
813
+ warnOnStreamedResponse(result, routeEntry.id);
647
814
  result.finally(doneHandler).catch(() => {});
648
815
  const tracked = deps.trackHandler(result, {
649
816
  segmentId: entry.shortCode,
@@ -666,7 +833,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
666
833
  entryId: entry.id,
667
834
  });
668
835
  const actionResult = handleHandlerResult(
669
- await routeEntry.handler(context),
836
+ await observeHandler(entry.id, handler, context),
670
837
  );
671
838
  doneHandler();
672
839
  return {
@@ -676,10 +843,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
676
843
  () => null,
677
844
  );
678
845
 
846
+ // Normalize void handlers (undefined) to null so the reconciler's
847
+ // component === null checks work consistently for both void and explicit null.
679
848
  const resolvedComponent =
680
849
  component && typeof component === "object" && "content" in component
681
- ? (component as { content: ReactNode }).content
682
- : component;
850
+ ? ((component as { content: ReactNode }).content ?? null)
851
+ : (component ?? null);
683
852
 
684
853
  const segment: ResolvedSegment = {
685
854
  id: entry.shortCode,
@@ -689,13 +858,17 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
689
858
  index: 0,
690
859
  component: resolvedComponent,
691
860
  loading: entry.loading === false ? null : entry.loading,
692
- transition: entry.transition,
861
+ transition: applyViewTransitionDefault(
862
+ entry.transition,
863
+ deps.viewTransitionDefault,
864
+ ),
693
865
  params,
694
866
  belongsToRoute,
695
867
  ...(entry.type === "layout" || entry.type === "cache"
696
868
  ? { layoutName: entry.id }
697
869
  : {}),
698
870
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
871
+ _handlerRan: handlerRan,
699
872
  };
700
873
 
701
874
  return { segment, matchedId };
@@ -714,7 +887,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
714
887
  request: Request,
715
888
  prevUrl: URL,
716
889
  nextUrl: URL,
717
- loaderPromises: Map<string, Promise<any>>,
718
890
  deps: SegmentResolutionDeps<TEnv>,
719
891
  actionContext?: ActionContext,
720
892
  stale?: boolean,
@@ -776,11 +948,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
776
948
  prevUrl,
777
949
  nextUrl,
778
950
  routeKey,
779
- loaderPromises,
780
951
  true,
781
952
  deps,
782
953
  actionContext,
783
954
  stale,
955
+ entry,
784
956
  );
785
957
  segments.push(...orphanResult.segments);
786
958
  matchedIds.push(...orphanResult.matchedIds);
@@ -860,7 +1032,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
860
1032
  prevUrl,
861
1033
  nextUrl,
862
1034
  routeKey,
863
- loaderPromises,
864
1035
  false,
865
1036
  deps,
866
1037
  actionContext,
@@ -887,11 +1058,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
887
1058
  prevUrl: URL,
888
1059
  nextUrl: URL,
889
1060
  routeKey: string,
890
- loaderPromises: Map<string, Promise<any>>,
891
1061
  belongsToRoute: boolean,
892
1062
  deps: SegmentResolutionDeps<TEnv>,
893
1063
  actionContext?: ActionContext,
894
1064
  stale?: boolean,
1065
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
1066
+ parentRouteEntry?: EntryData,
895
1067
  ): Promise<SegmentRevalidationResult> {
896
1068
  invariant(
897
1069
  orphan.type === "layout" || orphan.type === "cache",
@@ -919,6 +1091,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
919
1091
  segments.push(...loaderResult.segments);
920
1092
  matchedIds.push(...loaderResult.matchedIds);
921
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
+
922
1125
  // Handler-first: resolve orphan layout handler before its parallels
923
1126
  // so ctx.set() values are visible to parallel children.
924
1127
  matchedIds.push(orphan.shortCode);
@@ -991,157 +1194,40 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
991
1194
  belongsToRoute,
992
1195
  layoutName: orphan.id,
993
1196
  loading: orphan.loading === false ? null : orphan.loading,
994
- transition: orphan.transition,
1197
+ transition: applyViewTransitionDefault(
1198
+ orphan.transition,
1199
+ deps.viewTransitionDefault,
1200
+ ),
995
1201
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
996
1202
  });
997
1203
 
998
- for (const parallelEntry of orphan.parallel) {
999
- invariant(
1000
- parallelEntry.type === "parallel",
1001
- `Expected parallel entry, got: ${parallelEntry.type}`,
1002
- );
1003
-
1004
- const loaderResult = await resolveLoadersWithRevalidation(
1005
- parallelEntry,
1006
- context,
1007
- belongsToRoute,
1008
- clientSegmentIds,
1009
- prevParams,
1010
- request,
1011
- prevUrl,
1012
- nextUrl,
1013
- routeKey,
1014
- deps,
1015
- actionContext,
1016
- undefined,
1017
- stale,
1018
- );
1019
- segments.push(...loaderResult.segments);
1020
- matchedIds.push(...loaderResult.matchedIds);
1021
-
1022
- const slots = parallelEntry.handler as Record<
1023
- `@${string}`,
1024
- | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1025
- | ReactNode
1026
- >;
1027
-
1028
- for (const [slot, handler] of Object.entries(slots)) {
1029
- // Use orphan.shortCode (the parent layout) to match the SSR path
1030
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1031
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1032
- const parallelId = `${orphan.shortCode}.${slot}`;
1033
- matchedIds.push(parallelId);
1034
-
1035
- const shouldResolve = await (async () => {
1036
- if (!clientSegmentIds.has(parallelId)) {
1037
- if (isTraceActive()) {
1038
- pushRevalidationTraceEntry({
1039
- segmentId: parallelId,
1040
- segmentType: "parallel",
1041
- belongsToRoute,
1042
- source: "parallel",
1043
- defaultShouldRevalidate: true,
1044
- finalShouldRevalidate: true,
1045
- reason: "new-segment",
1046
- });
1047
- }
1048
- return true;
1049
- }
1050
-
1051
- const dummySegment: ResolvedSegment = {
1052
- id: parallelId,
1053
- namespace: parallelEntry.id,
1054
- type: "parallel",
1055
- index: 0,
1056
- component: null as any,
1057
- params,
1058
- slot,
1059
- belongsToRoute,
1060
- parallelName: `${parallelEntry.id}.${slot}`,
1061
- ...(parallelEntry.mountPath
1062
- ? { mountPath: parallelEntry.mountPath }
1063
- : {}),
1064
- };
1065
-
1066
- return await evaluateRevalidation({
1067
- segment: dummySegment,
1068
- prevParams,
1069
- getPrevSegment: null,
1070
- request,
1071
- prevUrl,
1072
- nextUrl,
1073
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1074
- name: `revalidate${i}`,
1075
- fn,
1076
- })),
1077
- routeKey,
1078
- context,
1079
- actionContext,
1080
- stale,
1081
- traceSource: "parallel",
1082
- });
1083
- })();
1084
- emitRevalidationDecision(
1085
- parallelId,
1086
- context.pathname,
1087
- routeKey,
1088
- shouldResolve,
1089
- );
1090
-
1091
- let component: ReactNode | undefined;
1092
- if (shouldResolve) {
1093
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1094
- }
1095
- if (component === undefined) {
1096
- const hasLoadingFallback =
1097
- parallelEntry.loading !== undefined &&
1098
- parallelEntry.loading !== false;
1099
- if (!shouldResolve) {
1100
- component = null;
1101
- } else if (hasLoadingFallback) {
1102
- const result =
1103
- typeof handler === "function" ? handler(context) : handler;
1104
- if (result instanceof Promise) {
1105
- const tracked = deps.trackHandler(result, {
1106
- segmentId: parallelId,
1107
- segmentType: "parallel",
1108
- });
1109
- observeStreamedHandler(
1110
- tracked,
1111
- parallelId,
1112
- "parallel",
1113
- context.pathname,
1114
- routeKey,
1115
- params,
1116
- );
1117
- component = tracked as ReactNode;
1118
- } else {
1119
- component = result as ReactNode;
1120
- }
1121
- } else {
1122
- component =
1123
- typeof handler === "function" ? await handler(context) : handler;
1124
- }
1125
- }
1126
-
1127
- segments.push({
1128
- id: parallelId,
1129
- namespace: parallelEntry.id,
1130
- type: "parallel",
1131
- index: 0,
1132
- component,
1133
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1134
- transition: parallelEntry.transition,
1135
- params,
1136
- slot,
1137
- belongsToRoute,
1138
- parallelName: `${parallelEntry.id}.${slot}`,
1139
- ...(parallelEntry.mountPath
1140
- ? { mountPath: parallelEntry.mountPath }
1141
- : {}),
1142
- });
1143
- }
1144
- }
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);
1145
1231
 
1146
1232
  return { segments, matchedIds };
1147
1233
  }
@@ -1159,12 +1245,12 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1159
1245
  request: Request,
1160
1246
  prevUrl: URL,
1161
1247
  nextUrl: URL,
1162
- loaderPromises: Map<string, Promise<any>>,
1163
1248
  actionContext: ActionContext | undefined,
1164
1249
  interceptResult: { intercept: any; entry: EntryData } | null,
1165
1250
  localRouteName: string,
1166
1251
  pathname: string,
1167
1252
  deps: SegmentResolutionDeps<TEnv>,
1253
+ stale?: boolean,
1168
1254
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1169
1255
  const allSegments: ResolvedSegment[] = [];
1170
1256
  const matchedIds: string[] = [];
@@ -1191,6 +1277,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1191
1277
  }
1192
1278
 
1193
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
+ }
1194
1284
  const doneEntry = track(`segment:${entry.id}`, 1);
1195
1285
  const resolved = await resolveWithErrorBoundary(
1196
1286
  nonParallelEntry,
@@ -1206,10 +1296,9 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1206
1296
  request,
1207
1297
  prevUrl,
1208
1298
  nextUrl,
1209
- loaderPromises,
1210
1299
  deps,
1211
1300
  actionContext,
1212
- false,
1301
+ stale,
1213
1302
  ),
1214
1303
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1215
1304
  deps,