@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136

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 (404) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3386 -1111
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +78 -25
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +247 -23
  13. package/skills/caching/SKILL.md +281 -11
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +289 -53
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +914 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +144 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +101 -5
  37. package/skills/router-setup/SKILL.md +117 -10
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +332 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  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 +192 -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/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +199 -86
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +174 -109
  80. package/src/browser/prefetch/cache.ts +222 -69
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +113 -32
  83. package/src/browser/prefetch/resource-ready.ts +77 -0
  84. package/src/browser/rango-state.ts +158 -76
  85. package/src/browser/react/Link.tsx +102 -15
  86. package/src/browser/react/NavigationProvider.tsx +300 -122
  87. package/src/browser/react/ScrollRestoration.tsx +10 -6
  88. package/src/browser/react/context.ts +7 -2
  89. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  90. package/src/browser/react/filter-segment-order.ts +66 -7
  91. package/src/browser/react/index.ts +0 -48
  92. package/src/browser/react/location-state-shared.ts +178 -8
  93. package/src/browser/react/location-state.ts +39 -14
  94. package/src/browser/react/use-action.ts +6 -15
  95. package/src/browser/react/use-handle.ts +23 -69
  96. package/src/browser/react/use-href.tsx +8 -1
  97. package/src/browser/react/use-link-status.ts +33 -8
  98. package/src/browser/react/use-navigation.ts +32 -7
  99. package/src/browser/react/use-params.ts +20 -10
  100. package/src/browser/react/use-reverse.ts +106 -0
  101. package/src/browser/react/use-router.ts +46 -11
  102. package/src/browser/react/use-search-params.ts +0 -5
  103. package/src/browser/react/use-segments.ts +11 -21
  104. package/src/browser/response-adapter.ts +99 -8
  105. package/src/browser/rsc-router.tsx +125 -28
  106. package/src/browser/scroll-restoration.ts +37 -22
  107. package/src/browser/segment-reconciler.ts +36 -14
  108. package/src/browser/segment-structure-assert.ts +2 -2
  109. package/src/browser/server-action-bridge.ts +222 -61
  110. package/src/browser/types.ts +115 -12
  111. package/src/browser/validate-redirect-origin.ts +43 -16
  112. package/src/build/collect-fallback-refs.ts +107 -0
  113. package/src/build/generate-manifest.ts +65 -40
  114. package/src/build/generate-route-types.ts +5 -1
  115. package/src/build/index.ts +8 -2
  116. package/src/build/prefix-tree-utils.ts +123 -0
  117. package/src/build/route-trie.ts +165 -36
  118. package/src/build/route-types/ast-route-extraction.ts +15 -8
  119. package/src/build/route-types/codegen.ts +16 -5
  120. package/src/build/route-types/include-resolution.ts +125 -24
  121. package/src/build/route-types/param-extraction.ts +6 -3
  122. package/src/build/route-types/per-module-writer.ts +22 -6
  123. package/src/build/route-types/router-processing.ts +260 -94
  124. package/src/build/route-types/scan-filter.ts +9 -2
  125. package/src/build/route-types/source-scan.ts +216 -0
  126. package/src/build/runtime-discovery.ts +9 -20
  127. package/src/cache/cache-error.ts +104 -0
  128. package/src/cache/cache-key-utils.ts +29 -13
  129. package/src/cache/cache-policy.ts +108 -34
  130. package/src/cache/cache-runtime.ts +239 -52
  131. package/src/cache/cache-scope.ts +234 -87
  132. package/src/cache/cache-tag.ts +103 -0
  133. package/src/cache/cf/cf-base64.ts +33 -0
  134. package/src/cache/cf/cf-cache-constants.ts +127 -0
  135. package/src/cache/cf/cf-cache-store.ts +1989 -378
  136. package/src/cache/cf/cf-cache-types.ts +349 -0
  137. package/src/cache/cf/cf-kv-utils.ts +46 -0
  138. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  139. package/src/cache/cf/index.ts +6 -16
  140. package/src/cache/document-cache.ts +89 -21
  141. package/src/cache/handle-snapshot.ts +70 -0
  142. package/src/cache/index.ts +10 -20
  143. package/src/cache/memory-segment-store.ts +136 -37
  144. package/src/cache/profile-registry.ts +46 -31
  145. package/src/cache/read-through-swr.ts +56 -12
  146. package/src/cache/segment-codec.ts +9 -17
  147. package/src/cache/tag-invalidation.ts +230 -0
  148. package/src/cache/taint.ts +55 -0
  149. package/src/cache/types.ts +37 -100
  150. package/src/client.rsc.tsx +44 -21
  151. package/src/client.tsx +119 -290
  152. package/src/cloudflare/index.ts +11 -0
  153. package/src/cloudflare/tracing.ts +109 -0
  154. package/src/component-utils.ts +19 -0
  155. package/src/components/DefaultDocument.tsx +8 -2
  156. package/src/context-var.ts +84 -2
  157. package/src/decode-loader-results.ts +52 -0
  158. package/src/defer.ts +196 -0
  159. package/src/deps/ssr.ts +0 -1
  160. package/src/encode-kv.ts +49 -0
  161. package/src/errors.ts +30 -4
  162. package/src/escape-script.ts +52 -0
  163. package/src/handle.ts +70 -22
  164. package/src/handles/MetaTags.tsx +62 -19
  165. package/src/handles/Scripts.tsx +183 -0
  166. package/src/handles/breadcrumbs.ts +37 -8
  167. package/src/handles/is-thenable.ts +19 -0
  168. package/src/handles/meta.ts +51 -40
  169. package/src/handles/script.ts +244 -0
  170. package/src/host/cookie-handler.ts +9 -60
  171. package/src/host/errors.ts +0 -24
  172. package/src/host/index.ts +8 -2
  173. package/src/host/pattern-matcher.ts +23 -52
  174. package/src/host/router.ts +107 -99
  175. package/src/host/testing.ts +40 -27
  176. package/src/host/types.ts +37 -4
  177. package/src/host/utils.ts +1 -1
  178. package/src/href-client.ts +137 -22
  179. package/src/index.rsc.ts +99 -13
  180. package/src/index.ts +139 -19
  181. package/src/internal-debug.ts +11 -10
  182. package/src/loader-store.ts +500 -0
  183. package/src/loader.rsc.ts +20 -13
  184. package/src/loader.ts +12 -11
  185. package/src/missing-id-error.ts +68 -0
  186. package/src/outlet-context.ts +1 -1
  187. package/src/outlet-provider.tsx +1 -5
  188. package/src/prerender/param-hash.ts +16 -16
  189. package/src/prerender/store.ts +37 -41
  190. package/src/prerender.ts +198 -82
  191. package/src/redirect-origin.ts +100 -0
  192. package/src/regex-escape.ts +8 -0
  193. package/src/render-error-thrower.tsx +20 -0
  194. package/src/response-utils.ts +62 -0
  195. package/src/reverse.ts +65 -15
  196. package/src/root-error-boundary.tsx +1 -19
  197. package/src/route-content-wrapper.tsx +19 -77
  198. package/src/route-definition/dsl-helpers.ts +461 -304
  199. package/src/route-definition/helper-factories.ts +28 -140
  200. package/src/route-definition/helpers-types.ts +149 -74
  201. package/src/route-definition/index.ts +4 -2
  202. package/src/route-definition/redirect.ts +51 -10
  203. package/src/route-definition/resolve-handler-use.ts +160 -0
  204. package/src/route-definition/use-item-types.ts +29 -0
  205. package/src/route-map-builder.ts +0 -16
  206. package/src/route-types.ts +37 -46
  207. package/src/router/basename.ts +14 -0
  208. package/src/router/content-negotiation.ts +164 -17
  209. package/src/router/error-handling.ts +45 -18
  210. package/src/router/find-match.ts +44 -23
  211. package/src/router/handler-context.ts +83 -39
  212. package/src/router/instrument.ts +350 -0
  213. package/src/router/intercept-resolution.ts +48 -24
  214. package/src/router/lazy-includes.ts +15 -52
  215. package/src/router/loader-resolution.ts +274 -56
  216. package/src/router/logging.ts +0 -6
  217. package/src/router/manifest.ts +40 -42
  218. package/src/router/match-api.ts +124 -204
  219. package/src/router/match-context.ts +0 -22
  220. package/src/router/match-handlers.ts +58 -58
  221. package/src/router/match-middleware/background-revalidation.ts +54 -25
  222. package/src/router/match-middleware/cache-lookup.ts +205 -271
  223. package/src/router/match-middleware/cache-store.ts +81 -50
  224. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  225. package/src/router/match-middleware/segment-resolution.ts +45 -14
  226. package/src/router/match-pipelines.ts +1 -42
  227. package/src/router/match-result.ts +93 -39
  228. package/src/router/metrics.ts +5 -34
  229. package/src/router/middleware-types.ts +13 -142
  230. package/src/router/middleware.ts +268 -171
  231. package/src/router/navigation-snapshot.ts +131 -0
  232. package/src/router/params-util.ts +23 -0
  233. package/src/router/pattern-matching.ts +132 -90
  234. package/src/router/prefetch-cache-ttl.ts +51 -0
  235. package/src/router/prefetch-limits.ts +37 -0
  236. package/src/router/prerender-match.ts +195 -56
  237. package/src/router/preview-match.ts +32 -102
  238. package/src/router/request-classification.ts +276 -0
  239. package/src/router/revalidation.ts +123 -73
  240. package/src/router/route-snapshot.ts +244 -0
  241. package/src/router/router-context.ts +3 -27
  242. package/src/router/router-interfaces.ts +129 -35
  243. package/src/router/router-options.ts +202 -15
  244. package/src/router/router-registry.ts +2 -5
  245. package/src/router/segment-resolution/fresh.ts +197 -92
  246. package/src/router/segment-resolution/helpers.ts +115 -30
  247. package/src/router/segment-resolution/loader-cache.ts +76 -39
  248. package/src/router/segment-resolution/revalidation.ts +392 -338
  249. package/src/router/segment-resolution/static-store.ts +19 -5
  250. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  251. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  252. package/src/router/segment-resolution.ts +5 -1
  253. package/src/router/segment-wrappers.ts +6 -5
  254. package/src/router/state-cookie-name.ts +33 -0
  255. package/src/router/substitute-pattern-params.ts +56 -0
  256. package/src/router/telemetry-otel.ts +161 -199
  257. package/src/router/telemetry.ts +96 -19
  258. package/src/router/timeout.ts +0 -20
  259. package/src/router/tracing.ts +206 -0
  260. package/src/router/trie-matching.ts +163 -59
  261. package/src/router/types.ts +10 -63
  262. package/src/router/url-params.ts +44 -0
  263. package/src/router.ts +174 -54
  264. package/src/rsc/handler-context.ts +3 -2
  265. package/src/rsc/handler.ts +660 -518
  266. package/src/rsc/helpers.ts +168 -46
  267. package/src/rsc/index.ts +2 -5
  268. package/src/rsc/json-route-result.ts +38 -0
  269. package/src/rsc/loader-fetch.ts +127 -31
  270. package/src/rsc/manifest-init.ts +33 -42
  271. package/src/rsc/origin-guard.ts +39 -25
  272. package/src/rsc/progressive-enhancement.ts +133 -13
  273. package/src/rsc/redirect-guard.ts +99 -0
  274. package/src/rsc/response-cache-serve.ts +238 -0
  275. package/src/rsc/response-error.ts +79 -12
  276. package/src/rsc/response-route-handler.ts +99 -189
  277. package/src/rsc/rsc-rendering.ts +115 -73
  278. package/src/rsc/runtime-warnings.ts +23 -10
  279. package/src/rsc/server-action.ts +287 -113
  280. package/src/rsc/ssr-setup.ts +18 -2
  281. package/src/rsc/transition-gate.ts +89 -0
  282. package/src/rsc/types.ts +36 -6
  283. package/src/runtime-env.ts +18 -0
  284. package/src/search-params.ts +35 -30
  285. package/src/segment-content-promise.ts +67 -0
  286. package/src/segment-loader-promise.ts +149 -0
  287. package/src/segment-system.tsx +269 -202
  288. package/src/serialize.ts +243 -0
  289. package/src/server/context.ts +235 -51
  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 +38 -46
  294. package/src/server/request-context.ts +442 -173
  295. package/src/ssr/index.tsx +24 -16
  296. package/src/static-handler.ts +27 -18
  297. package/src/testing/cache-status.ts +162 -0
  298. package/src/testing/collect-handle.ts +40 -0
  299. package/src/testing/dispatch.ts +701 -0
  300. package/src/testing/dom.entry.ts +22 -0
  301. package/src/testing/e2e/fixture.ts +188 -0
  302. package/src/testing/e2e/index.ts +128 -0
  303. package/src/testing/e2e/matchers.ts +35 -0
  304. package/src/testing/e2e/page-helpers.ts +272 -0
  305. package/src/testing/e2e/parity.ts +387 -0
  306. package/src/testing/e2e/server.ts +195 -0
  307. package/src/testing/flight-matchers.ts +97 -0
  308. package/src/testing/flight-normalize.ts +11 -0
  309. package/src/testing/flight-runtime.d.ts +57 -0
  310. package/src/testing/flight-tree.ts +682 -0
  311. package/src/testing/flight.entry.ts +52 -0
  312. package/src/testing/flight.ts +257 -0
  313. package/src/testing/generated-routes.ts +183 -0
  314. package/src/testing/index.ts +105 -0
  315. package/src/testing/internal/context.ts +371 -0
  316. package/src/testing/internal/flight-client-globals.ts +30 -0
  317. package/src/testing/internal/seed-vars.ts +54 -0
  318. package/src/testing/render-handler.ts +357 -0
  319. package/src/testing/render-route.tsx +581 -0
  320. package/src/testing/run-loader.ts +385 -0
  321. package/src/testing/run-middleware.ts +205 -0
  322. package/src/testing/run-transition-when.ts +164 -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 +0 -7
  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 +234 -82
  340. package/src/types/index.ts +3 -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 +6 -50
  344. package/src/types/route-entry.ts +12 -7
  345. package/src/types/segments.ts +136 -15
  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 +68 -18
  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 -19
  354. package/src/use-loader.tsx +346 -89
  355. package/src/vite/debug.ts +185 -0
  356. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  357. package/src/vite/discovery/discover-routers.ts +130 -85
  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 +214 -132
  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 +57 -4
  364. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  365. package/src/vite/index.ts +6 -0
  366. package/src/vite/inject-client-debug.ts +36 -0
  367. package/src/vite/plugin-types.ts +178 -5
  368. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  369. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  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 +48 -95
  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 +89 -27
  383. package/src/vite/plugins/use-cache-transform.ts +73 -83
  384. package/src/vite/plugins/version-injector.ts +40 -29
  385. package/src/vite/plugins/version-plugin.ts +37 -40
  386. package/src/vite/plugins/virtual-entries.ts +39 -25
  387. package/src/vite/rango.ts +119 -111
  388. package/src/vite/router-discovery.ts +941 -142
  389. package/src/vite/utils/ast-handler-extract.ts +26 -35
  390. package/src/vite/utils/banner.ts +1 -1
  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 +81 -34
  398. package/src/vite/utils/shared-utils.ts +92 -42
  399. package/src/browser/action-response-classifier.ts +0 -99
  400. package/src/browser/react/use-client-cache.ts +0 -58
  401. package/src/browser/shallow.ts +0 -40
  402. package/src/handles/index.ts +0 -7
  403. package/src/network-error-thrower.tsx +0 -23
  404. package/src/router/middleware-cookies.ts +0 -55
@@ -14,6 +14,7 @@ import {
14
14
  getParallelEntries,
15
15
  getParallelSlotEntries,
16
16
  type EntryData,
17
+ type ParallelEntryData,
17
18
  } from "../../server/context";
18
19
  import type {
19
20
  HandlerContext,
@@ -34,54 +35,41 @@ import {
34
35
  import { resolveLoaderData } from "./loader-cache.js";
35
36
  import {
36
37
  handleHandlerResult,
38
+ warnOnStreamedResponse,
37
39
  tryStaticHandler,
38
40
  tryStaticSlot,
39
41
  resolveLayoutComponent,
40
42
  resolveWithErrorBoundary,
43
+ buildLoaderErrorContext,
41
44
  } from "./helpers.js";
45
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
42
46
  import { getRouterContext } from "../router-context.js";
43
- import { resolveSink, safeEmit } from "../telemetry.js";
44
- import { track } from "../../server/context.js";
45
-
46
- // ---------------------------------------------------------------------------
47
- // Telemetry helpers
48
- // ---------------------------------------------------------------------------
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";
49
54
 
50
55
  /**
51
- * Attach a fire-and-forget rejection observer to a streamed handler promise.
52
- * 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.
53
59
  */
54
- function observeStreamedHandler(
55
- promise: Promise<ReactNode>,
56
- segmentId: string,
57
- segmentType: string,
58
- pathname?: string,
59
- routeKey?: string,
60
- params?: Record<string, string>,
60
+ function traceFullRefetchedParallelSlot(
61
+ parallelId: string,
62
+ belongsToRoute: boolean,
61
63
  ): void {
62
- let routerCtx;
63
- try {
64
- routerCtx = getRouterContext();
65
- } catch {
66
- return;
67
- }
68
- if (!routerCtx?.telemetry) return;
69
- const sink = resolveSink(routerCtx.telemetry);
70
- const reqId = routerCtx.requestId;
71
- promise.catch((err: unknown) => {
72
- const errorObj = err instanceof Error ? err : new Error(String(err));
73
- safeEmit(sink, {
74
- type: "handler.error",
75
- timestamp: performance.now(),
76
- requestId: reqId,
77
- segmentId,
78
- segmentType,
79
- error: errorObj,
80
- handledByBoundary: true,
81
- pathname,
82
- routeKey,
83
- params,
84
- });
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",
85
73
  });
86
74
  }
87
75
 
@@ -100,23 +88,14 @@ function emitRevalidationDecision(
100
88
  routeKey: string,
101
89
  shouldRevalidate: boolean,
102
90
  ): void {
103
- let routerCtx;
104
- try {
105
- routerCtx = getRouterContext();
106
- } catch {
107
- return;
108
- }
109
- if (routerCtx?.telemetry) {
110
- safeEmit(resolveSink(routerCtx.telemetry), {
111
- type: "revalidation.decision",
112
- timestamp: performance.now(),
113
- requestId: routerCtx.requestId,
114
- segmentId,
115
- pathname,
116
- routeKey,
117
- shouldRevalidate,
118
- });
119
- }
91
+ observeEvent({
92
+ type: "revalidation.decision",
93
+ timestamp: performance.now(),
94
+ segmentId,
95
+ pathname,
96
+ routeKey,
97
+ shouldRevalidate,
98
+ });
120
99
  }
121
100
 
122
101
  // ---------------------------------------------------------------------------
@@ -221,6 +200,10 @@ export async function resolveLoadersWithRevalidation<TEnv>(
221
200
  ),
222
201
  );
223
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
+
224
207
  const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
225
208
  const segments: ResolvedSegment[] = loadersToRun.map(
226
209
  ({ loaderEntry, loader, segmentId, index }) => ({
@@ -232,10 +215,13 @@ export async function resolveLoadersWithRevalidation<TEnv>(
232
215
  params: ctx.params,
233
216
  loaderId: loader.$$id,
234
217
  loaderData: deps.wrapLoaderPromise(
235
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
218
+ runInsideLoaderScope(() =>
219
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
220
+ ),
236
221
  entry,
237
222
  segmentId,
238
223
  ctx.pathname,
224
+ errorContext,
239
225
  ),
240
226
  belongsToRoute,
241
227
  }),
@@ -262,29 +248,46 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
262
248
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
263
249
  const allLoaderSegments: ResolvedSegment[] = [];
264
250
  const allMatchedIds: string[] = [];
251
+ const seenIds = new Set<string>();
265
252
 
266
253
  async function collectEntryLoaders(
267
254
  entry: EntryData,
268
255
  belongsToRoute: boolean,
269
256
  shortCodeOverride?: string,
270
257
  ): Promise<void> {
271
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
272
- entry,
273
- context,
274
- belongsToRoute,
275
- clientSegmentIds,
276
- prevParams,
277
- request,
278
- prevUrl,
279
- nextUrl,
280
- routeKey,
281
- deps,
282
- actionContext,
283
- shortCodeOverride,
284
- stale,
285
- );
286
- allLoaderSegments.push(...segments);
287
- allMatchedIds.push(...matchedIds);
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
+ }
288
291
 
289
292
  const seenParallelEntryIds = new Set<string>();
290
293
  for (const parallelEntry of getParallelEntries(entry.parallel)) {
@@ -296,6 +299,39 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
296
299
  const childBelongsToRoute = belongsToRoute || entry.type === "route";
297
300
  for (const layoutEntry of entry.layout) {
298
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
+ }
299
335
  }
300
336
  }
301
337
 
@@ -348,6 +384,97 @@ export function buildEntryRevalidateMap(
348
384
  return map;
349
385
  }
350
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
+
351
478
  /**
352
479
  * Resolve parallel segments with revalidation.
353
480
  */
@@ -365,9 +492,30 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
365
492
  deps: SegmentResolutionDeps<TEnv>,
366
493
  actionContext?: ActionContext,
367
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
+ },
368
514
  ): Promise<SegmentRevalidationResult> {
369
515
  const segments: ResolvedSegment[] = [];
370
516
  const matchedIds: string[] = [];
517
+ const parentChainDefault = options?.parentChainDefault ?? "type-derived";
518
+ const loaderOrder = options?.loaderOrder ?? "after";
371
519
 
372
520
  const resolvedParallelEntries = new Set<string>();
373
521
  for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
@@ -392,44 +540,61 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
392
540
 
393
541
  const isFullRefetch = clientSegmentIds.size === 0;
394
542
  const isNewParent = !clientSegmentIds.has(entry.shortCode);
395
- if (
396
- isFullRefetch ||
397
- clientSegmentIds.has(parallelId) ||
398
- belongsToRoute ||
399
- isNewParent
400
- ) {
401
- matchedIds.push(parallelId);
543
+
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,
557
+ routeKey,
558
+ deps,
559
+ actionContext,
560
+ entry.shortCode,
561
+ stale,
562
+ );
563
+ segments.push(...loaderResult.segments);
564
+ matchedIds.push(...loaderResult.matchedIds);
565
+ resolvedParallelEntries.add(parallelEntry.id);
566
+ };
567
+
568
+ if (loaderOrder === "before") {
569
+ await resolveSlotLoaders();
402
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);
403
576
 
404
- const shouldResolve = await (async () => {
405
- if (isFullRefetch) {
406
- if (isTraceActive()) {
407
- pushRevalidationTraceEntry({
408
- segmentId: parallelId,
409
- segmentType: "parallel",
410
- belongsToRoute,
411
- source: "parallel",
412
- defaultShouldRevalidate: true,
413
- finalShouldRevalidate: true,
414
- reason: "full-refetch",
415
- });
416
- }
417
- return true;
418
- }
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;
419
589
  if (!clientSegmentIds.has(parallelId)) {
420
- const result = belongsToRoute || isNewParent;
421
- if (isTraceActive()) {
422
- pushRevalidationTraceEntry({
423
- segmentId: parallelId,
424
- segmentType: "parallel",
425
- belongsToRoute,
426
- source: "parallel",
427
- defaultShouldRevalidate: result,
428
- finalShouldRevalidate: result,
429
- reason: result ? "new-segment" : "skip-parent-chain",
430
- });
431
- }
432
- return result;
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
+ };
433
598
  }
434
599
 
435
600
  const dummySegment: ResolvedSegment = {
@@ -447,7 +612,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
447
612
  : {}),
448
613
  };
449
614
 
450
- return await evaluateRevalidation({
615
+ shouldResolve = await evaluateRevalidation({
451
616
  segment: dummySegment,
452
617
  prevParams,
453
618
  getPrevSegment: null,
@@ -463,8 +628,9 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
463
628
  actionContext,
464
629
  stale,
465
630
  traceSource: "parallel",
631
+ defaultOverride,
466
632
  });
467
- })();
633
+ }
468
634
  emitRevalidationDecision(
469
635
  parallelId,
470
636
  context.pathname,
@@ -472,44 +638,17 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
472
638
  shouldResolve,
473
639
  );
474
640
 
475
- let component: ReactNode | undefined;
476
- if (shouldResolve) {
477
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
478
- }
479
- if (component === undefined) {
480
- const hasLoadingFallback =
481
- parallelEntry.loading !== undefined && parallelEntry.loading !== false;
482
- if (!shouldResolve) {
483
- component = null;
484
- } else if (handler === undefined) {
485
- // Handler evicted (production static slot) but static lookup missed.
486
- // Nothing to render — use null so the client keeps its cached version.
487
- component = null;
488
- } else if (hasLoadingFallback) {
489
- const result =
490
- typeof handler === "function" ? handler(context) : handler;
491
- if (result instanceof Promise) {
492
- const tracked = deps.trackHandler(result, {
493
- segmentId: parallelId,
494
- segmentType: "parallel",
495
- });
496
- observeStreamedHandler(
497
- tracked,
498
- parallelId,
499
- "parallel",
500
- context.pathname,
501
- routeKey,
502
- params,
503
- );
504
- component = tracked as ReactNode;
505
- } else {
506
- component = result as ReactNode;
507
- }
508
- } else {
509
- component =
510
- typeof handler === "function" ? await handler(context) : handler;
511
- }
512
- }
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
+ });
513
652
 
514
653
  segments.push({
515
654
  id: parallelId,
@@ -518,9 +657,14 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
518
657
  index: 0,
519
658
  component,
520
659
  loading: parallelEntry.loading === false ? null : parallelEntry.loading,
521
- transition: parallelEntry.transition,
660
+ transition: applyViewTransitionDefault(
661
+ parallelEntry.transition,
662
+ deps.viewTransitionDefault,
663
+ parallelId,
664
+ ),
522
665
  params,
523
666
  slot,
667
+ _handlerRan: handlerRan,
524
668
  belongsToRoute,
525
669
  parallelName: `${parallelEntry.id}.${slot}`,
526
670
  ...(parallelEntry.mountPath
@@ -528,28 +672,9 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
528
672
  : {}),
529
673
  });
530
674
 
531
- if (resolvedParallelEntries.has(parallelEntry.id)) {
532
- continue;
675
+ if (loaderOrder === "after") {
676
+ await resolveSlotLoaders();
533
677
  }
534
-
535
- const loaderResult = await resolveLoadersWithRevalidation(
536
- parallelEntry,
537
- context,
538
- belongsToRoute,
539
- clientSegmentIds,
540
- prevParams,
541
- request,
542
- prevUrl,
543
- nextUrl,
544
- routeKey,
545
- deps,
546
- actionContext,
547
- entry.shortCode,
548
- stale,
549
- );
550
- segments.push(...loaderResult.segments);
551
- matchedIds.push(...loaderResult.matchedIds);
552
- resolvedParallelEntries.add(parallelEntry.id);
553
678
  }
554
679
 
555
680
  return { segments, matchedIds };
@@ -575,6 +700,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
575
700
  ): Promise<{ segment: ResolvedSegment; matchedId: string }> {
576
701
  const matchedId = entry.shortCode;
577
702
 
703
+ let handlerRan = false;
578
704
  const component = await revalidate(
579
705
  async () => {
580
706
  const hasSegment = clientSegmentIds.has(entry.shortCode);
@@ -651,6 +777,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
651
777
  return shouldRevalidate;
652
778
  },
653
779
  async () => {
780
+ handlerRan = true;
654
781
  const doneHandler = track(`handler:${entry.id}`, 2);
655
782
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
656
783
  entry.shortCode;
@@ -665,14 +792,26 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
665
792
  return staticComponent;
666
793
  }
667
794
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
795
+ // For Passthrough routes at runtime, use the live handler instead of
796
+ // the build handler. At build time (context.build === true), always
797
+ // use the build handler from routeEntry.handler.
798
+ const handler =
799
+ !context.build && routeEntry.liveHandler
800
+ ? routeEntry.liveHandler
801
+ : routeEntry.handler;
668
802
  if (!routeEntry.loading) {
669
- const result = handleHandlerResult(await routeEntry.handler(context));
803
+ const result = handleHandlerResult(
804
+ await observeHandler(entry.id, handler, context),
805
+ );
670
806
  doneHandler();
671
807
  return result;
672
808
  }
673
809
  if (!actionContext) {
674
- const result = handleHandlerResult(routeEntry.handler(context));
810
+ const result = handleHandlerResult(
811
+ observeHandler(entry.id, handler, context),
812
+ );
675
813
  if (result instanceof Promise) {
814
+ warnOnStreamedResponse(result, routeEntry.id);
676
815
  result.finally(doneHandler).catch(() => {});
677
816
  const tracked = deps.trackHandler(result, {
678
817
  segmentId: entry.shortCode,
@@ -695,7 +834,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
695
834
  entryId: entry.id,
696
835
  });
697
836
  const actionResult = handleHandlerResult(
698
- await routeEntry.handler(context),
837
+ await observeHandler(entry.id, handler, context),
699
838
  );
700
839
  doneHandler();
701
840
  return {
@@ -705,10 +844,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
705
844
  () => null,
706
845
  );
707
846
 
847
+ // Normalize void handlers (undefined) to null so the reconciler's
848
+ // component === null checks work consistently for both void and explicit null.
708
849
  const resolvedComponent =
709
850
  component && typeof component === "object" && "content" in component
710
- ? (component as { content: ReactNode }).content
711
- : component;
851
+ ? ((component as { content: ReactNode }).content ?? null)
852
+ : (component ?? null);
712
853
 
713
854
  const segment: ResolvedSegment = {
714
855
  id: entry.shortCode,
@@ -718,13 +859,18 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
718
859
  index: 0,
719
860
  component: resolvedComponent,
720
861
  loading: entry.loading === false ? null : entry.loading,
721
- transition: entry.transition,
862
+ transition: applyViewTransitionDefault(
863
+ entry.transition,
864
+ deps.viewTransitionDefault,
865
+ entry.shortCode,
866
+ ),
722
867
  params,
723
868
  belongsToRoute,
724
869
  ...(entry.type === "layout" || entry.type === "cache"
725
870
  ? { layoutName: entry.id }
726
871
  : {}),
727
872
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
873
+ _handlerRan: handlerRan,
728
874
  };
729
875
 
730
876
  return { segment, matchedId };
@@ -743,7 +889,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
743
889
  request: Request,
744
890
  prevUrl: URL,
745
891
  nextUrl: URL,
746
- loaderPromises: Map<string, Promise<any>>,
747
892
  deps: SegmentResolutionDeps<TEnv>,
748
893
  actionContext?: ActionContext,
749
894
  stale?: boolean,
@@ -805,11 +950,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
805
950
  prevUrl,
806
951
  nextUrl,
807
952
  routeKey,
808
- loaderPromises,
809
953
  true,
810
954
  deps,
811
955
  actionContext,
812
956
  stale,
957
+ entry,
813
958
  );
814
959
  segments.push(...orphanResult.segments);
815
960
  matchedIds.push(...orphanResult.matchedIds);
@@ -889,7 +1034,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
889
1034
  prevUrl,
890
1035
  nextUrl,
891
1036
  routeKey,
892
- loaderPromises,
893
1037
  false,
894
1038
  deps,
895
1039
  actionContext,
@@ -916,11 +1060,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
916
1060
  prevUrl: URL,
917
1061
  nextUrl: URL,
918
1062
  routeKey: string,
919
- loaderPromises: Map<string, Promise<any>>,
920
1063
  belongsToRoute: boolean,
921
1064
  deps: SegmentResolutionDeps<TEnv>,
922
1065
  actionContext?: ActionContext,
923
1066
  stale?: boolean,
1067
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
1068
+ parentRouteEntry?: EntryData,
924
1069
  ): Promise<SegmentRevalidationResult> {
925
1070
  invariant(
926
1071
  orphan.type === "layout" || orphan.type === "cache",
@@ -948,6 +1093,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
948
1093
  segments.push(...loaderResult.segments);
949
1094
  matchedIds.push(...loaderResult.matchedIds);
950
1095
 
1096
+ // Inherit parent route's loaders so parallel slots inside this layout
1097
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1098
+ if (
1099
+ parentRouteEntry &&
1100
+ parentRouteEntry.loader &&
1101
+ parentRouteEntry.loader.length > 0 &&
1102
+ Object.keys(orphan.parallel).length > 0
1103
+ ) {
1104
+ const inheritedResult = await resolveLoadersWithRevalidation(
1105
+ parentRouteEntry,
1106
+ context,
1107
+ belongsToRoute,
1108
+ clientSegmentIds,
1109
+ prevParams,
1110
+ request,
1111
+ prevUrl,
1112
+ nextUrl,
1113
+ routeKey,
1114
+ deps,
1115
+ actionContext,
1116
+ orphan.shortCode,
1117
+ stale,
1118
+ );
1119
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1120
+ for (const s of inheritedResult.segments) {
1121
+ s._inherited = true;
1122
+ }
1123
+ segments.push(...inheritedResult.segments);
1124
+ matchedIds.push(...inheritedResult.matchedIds);
1125
+ }
1126
+
951
1127
  // Handler-first: resolve orphan layout handler before its parallels
952
1128
  // so ctx.set() values are visible to parallel children.
953
1129
  matchedIds.push(orphan.shortCode);
@@ -1020,165 +1196,41 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1020
1196
  belongsToRoute,
1021
1197
  layoutName: orphan.id,
1022
1198
  loading: orphan.loading === false ? null : orphan.loading,
1023
- transition: orphan.transition,
1199
+ transition: applyViewTransitionDefault(
1200
+ orphan.transition,
1201
+ deps.viewTransitionDefault,
1202
+ orphan.shortCode,
1203
+ ),
1024
1204
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1025
1205
  });
1026
1206
 
1027
- const resolvedParallelEntries = new Set<string>();
1028
- for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1029
- orphan.parallel,
1030
- )) {
1031
- invariant(
1032
- parallelEntry.type === "parallel",
1033
- `Expected parallel entry, got: ${parallelEntry.type}`,
1034
- );
1035
-
1036
- if (!resolvedParallelEntries.has(parallelEntry.id)) {
1037
- const loaderResult = await resolveLoadersWithRevalidation(
1038
- parallelEntry,
1039
- context,
1040
- belongsToRoute,
1041
- clientSegmentIds,
1042
- prevParams,
1043
- request,
1044
- prevUrl,
1045
- nextUrl,
1046
- routeKey,
1047
- deps,
1048
- actionContext,
1049
- undefined,
1050
- stale,
1051
- );
1052
- segments.push(...loaderResult.segments);
1053
- matchedIds.push(...loaderResult.matchedIds);
1054
- resolvedParallelEntries.add(parallelEntry.id);
1055
- }
1056
-
1057
- const slots = parallelEntry.handler as Record<
1058
- `@${string}`,
1059
- | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1060
- | ReactNode
1061
- >;
1062
- // Handler may be undefined in production after static handler eviction.
1063
- const handler = slots[slot];
1064
-
1065
- // Use orphan.shortCode (the parent layout) to match the SSR path
1066
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1067
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1068
- const parallelId = `${orphan.shortCode}.${slot}`;
1069
- matchedIds.push(parallelId);
1070
-
1071
- const shouldResolve = await (async () => {
1072
- if (!clientSegmentIds.has(parallelId)) {
1073
- if (isTraceActive()) {
1074
- pushRevalidationTraceEntry({
1075
- segmentId: parallelId,
1076
- segmentType: "parallel",
1077
- belongsToRoute,
1078
- source: "parallel",
1079
- defaultShouldRevalidate: true,
1080
- finalShouldRevalidate: true,
1081
- reason: "new-segment",
1082
- });
1083
- }
1084
- return true;
1085
- }
1086
-
1087
- const dummySegment: ResolvedSegment = {
1088
- id: parallelId,
1089
- namespace: parallelEntry.id,
1090
- type: "parallel",
1091
- index: 0,
1092
- component: null as any,
1093
- params,
1094
- slot,
1095
- belongsToRoute,
1096
- parallelName: `${parallelEntry.id}.${slot}`,
1097
- ...(parallelEntry.mountPath
1098
- ? { mountPath: parallelEntry.mountPath }
1099
- : {}),
1100
- };
1101
-
1102
- return await evaluateRevalidation({
1103
- segment: dummySegment,
1104
- prevParams,
1105
- getPrevSegment: null,
1106
- request,
1107
- prevUrl,
1108
- nextUrl,
1109
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1110
- name: `revalidate${i}`,
1111
- fn,
1112
- })),
1113
- routeKey,
1114
- context,
1115
- actionContext,
1116
- stale,
1117
- traceSource: "parallel",
1118
- });
1119
- })();
1120
- emitRevalidationDecision(
1121
- parallelId,
1122
- context.pathname,
1123
- routeKey,
1124
- shouldResolve,
1125
- );
1126
-
1127
- let component: ReactNode | undefined;
1128
- if (shouldResolve) {
1129
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1130
- }
1131
- if (component === undefined) {
1132
- const hasLoadingFallback =
1133
- parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1134
- if (!shouldResolve) {
1135
- component = null;
1136
- } else if (handler === undefined) {
1137
- // Handler evicted (production static slot) but static lookup missed.
1138
- component = null;
1139
- } else if (hasLoadingFallback) {
1140
- const result =
1141
- typeof handler === "function" ? handler(context) : handler;
1142
- if (result instanceof Promise) {
1143
- const tracked = deps.trackHandler(result, {
1144
- segmentId: parallelId,
1145
- segmentType: "parallel",
1146
- });
1147
- observeStreamedHandler(
1148
- tracked,
1149
- parallelId,
1150
- "parallel",
1151
- context.pathname,
1152
- routeKey,
1153
- params,
1154
- );
1155
- component = tracked as ReactNode;
1156
- } else {
1157
- component = result as ReactNode;
1158
- }
1159
- } else {
1160
- component =
1161
- typeof handler === "function" ? await handler(context) : handler;
1162
- }
1163
- }
1164
-
1165
- segments.push({
1166
- id: parallelId,
1167
- namespace: parallelEntry.id,
1168
- type: "parallel",
1169
- index: 0,
1170
- component,
1171
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1172
- transition: parallelEntry.transition,
1173
- params,
1174
- slot,
1175
- belongsToRoute,
1176
- parallelName: `${parallelEntry.id}.${slot}`,
1177
- ...(parallelEntry.mountPath
1178
- ? { mountPath: parallelEntry.mountPath }
1179
- : {}),
1180
- });
1181
- }
1207
+ // Resolve the orphan layout's parallel slots through the shared main-path
1208
+ // helper. The orphan policy is carried by explicit args, byte-for-byte:
1209
+ // - parentChainDefault "force-render": an unknown parent-chain slot seeds
1210
+ // `true` (orphan parallels always belong to the route — the #482 guard),
1211
+ // where the main path would seed `belongsToRoute || isNewParent`.
1212
+ // - loaderOrder "before": a slot's loaders are emitted before the slot
1213
+ // segment, matching the prior inlined order.
1214
+ // `entry.shortCode` inside the helper is `orphan.shortCode` (orphan is passed
1215
+ // as `entry`), so the parallel ids + loader shortCodeOverride are unchanged.
1216
+ const parallelResult = await resolveParallelSegmentsWithRevalidation(
1217
+ orphan,
1218
+ params,
1219
+ context,
1220
+ belongsToRoute,
1221
+ clientSegmentIds,
1222
+ prevParams,
1223
+ request,
1224
+ prevUrl,
1225
+ nextUrl,
1226
+ routeKey,
1227
+ deps,
1228
+ actionContext,
1229
+ stale,
1230
+ { parentChainDefault: "force-render", loaderOrder: "before" },
1231
+ );
1232
+ segments.push(...parallelResult.segments);
1233
+ matchedIds.push(...parallelResult.matchedIds);
1182
1234
 
1183
1235
  return { segments, matchedIds };
1184
1236
  }
@@ -1196,7 +1248,6 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1196
1248
  request: Request,
1197
1249
  prevUrl: URL,
1198
1250
  nextUrl: URL,
1199
- loaderPromises: Map<string, Promise<any>>,
1200
1251
  actionContext: ActionContext | undefined,
1201
1252
  interceptResult: { intercept: any; entry: EntryData } | null,
1202
1253
  localRouteName: string,
@@ -1229,6 +1280,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1229
1280
  }
1230
1281
 
1231
1282
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1283
+ if (entry.type === "cache") {
1284
+ const store = RangoContext.getStore();
1285
+ if (store) store.insideCacheScope = true;
1286
+ }
1232
1287
  const doneEntry = track(`segment:${entry.id}`, 1);
1233
1288
  const resolved = await resolveWithErrorBoundary(
1234
1289
  nonParallelEntry,
@@ -1244,7 +1299,6 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1244
1299
  request,
1245
1300
  prevUrl,
1246
1301
  nextUrl,
1247
- loaderPromises,
1248
1302
  deps,
1249
1303
  actionContext,
1250
1304
  stale,