@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17

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