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

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 (285) hide show
  1. package/README.md +196 -43
  2. package/dist/bin/rango.js +277 -99
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2779 -1064
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +57 -11
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +155 -6
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +273 -53
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +123 -100
  29. package/skills/rango/SKILL.md +242 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +88 -4
  33. package/skills/router-setup/SKILL.md +90 -5
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +716 -0
  37. package/skills/typesafety/SKILL.md +329 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +1 -1
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/navigation-bridge.ts +102 -16
  50. package/src/browser/navigation-client.ts +164 -59
  51. package/src/browser/navigation-store.ts +75 -17
  52. package/src/browser/navigation-transaction.ts +21 -37
  53. package/src/browser/partial-update.ts +139 -38
  54. package/src/browser/prefetch/cache.ts +175 -15
  55. package/src/browser/prefetch/fetch.ts +180 -33
  56. package/src/browser/prefetch/queue.ts +123 -20
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +53 -13
  59. package/src/browser/react/Link.tsx +81 -9
  60. package/src/browser/react/NavigationProvider.tsx +110 -33
  61. package/src/browser/react/context.ts +7 -2
  62. package/src/browser/react/filter-segment-order.ts +51 -7
  63. package/src/browser/react/index.ts +3 -0
  64. package/src/browser/react/location-state-shared.ts +175 -4
  65. package/src/browser/react/location-state.ts +39 -13
  66. package/src/browser/react/use-handle.ts +23 -64
  67. package/src/browser/react/use-navigation.ts +22 -2
  68. package/src/browser/react/use-params.ts +20 -8
  69. package/src/browser/react/use-reverse.ts +106 -0
  70. package/src/browser/react/use-router.ts +43 -10
  71. package/src/browser/react/use-segments.ts +11 -8
  72. package/src/browser/response-adapter.ts +25 -0
  73. package/src/browser/rsc-router.tsx +191 -74
  74. package/src/browser/scroll-restoration.ts +41 -14
  75. package/src/browser/segment-reconciler.ts +36 -9
  76. package/src/browser/segment-structure-assert.ts +2 -2
  77. package/src/browser/server-action-bridge.ts +31 -36
  78. package/src/browser/types.ts +57 -5
  79. package/src/build/collect-fallback-refs.ts +107 -0
  80. package/src/build/generate-manifest.ts +65 -40
  81. package/src/build/generate-route-types.ts +5 -0
  82. package/src/build/index.ts +2 -0
  83. package/src/build/route-trie.ts +52 -25
  84. package/src/build/route-types/codegen.ts +4 -4
  85. package/src/build/route-types/include-resolution.ts +9 -2
  86. package/src/build/route-types/per-module-writer.ts +7 -4
  87. package/src/build/route-types/router-processing.ts +278 -88
  88. package/src/build/route-types/scan-filter.ts +9 -2
  89. package/src/build/route-types/source-scan.ts +118 -0
  90. package/src/build/runtime-discovery.ts +9 -20
  91. package/src/cache/cache-runtime.ts +15 -11
  92. package/src/cache/cache-scope.ts +76 -49
  93. package/src/cache/cf/cf-cache-store.ts +501 -18
  94. package/src/cache/cf/index.ts +5 -1
  95. package/src/cache/document-cache.ts +17 -7
  96. package/src/cache/index.ts +1 -0
  97. package/src/cache/taint.ts +55 -0
  98. package/src/client.rsc.tsx +3 -0
  99. package/src/client.tsx +94 -238
  100. package/src/context-var.ts +72 -2
  101. package/src/debug.ts +2 -2
  102. package/src/decode-loader-results.ts +36 -0
  103. package/src/errors.ts +30 -1
  104. package/src/handle.ts +65 -12
  105. package/src/host/index.ts +2 -2
  106. package/src/host/router.ts +129 -57
  107. package/src/host/types.ts +31 -2
  108. package/src/host/utils.ts +1 -1
  109. package/src/href-client.ts +140 -20
  110. package/src/index.rsc.ts +12 -5
  111. package/src/index.ts +61 -11
  112. package/src/loader-store.ts +500 -0
  113. package/src/loader.rsc.ts +2 -5
  114. package/src/loader.ts +3 -10
  115. package/src/missing-id-error.ts +68 -0
  116. package/src/outlet-context.ts +1 -1
  117. package/src/prerender/store.ts +5 -4
  118. package/src/prerender.ts +141 -80
  119. package/src/response-utils.ts +37 -0
  120. package/src/reverse.ts +65 -15
  121. package/src/route-content-wrapper.tsx +6 -28
  122. package/src/route-definition/dsl-helpers.ts +435 -260
  123. package/src/route-definition/helper-factories.ts +29 -139
  124. package/src/route-definition/helpers-types.ts +110 -34
  125. package/src/route-definition/index.ts +3 -0
  126. package/src/route-definition/redirect.ts +11 -3
  127. package/src/route-definition/resolve-handler-use.ts +155 -0
  128. package/src/route-definition/use-item-types.ts +32 -0
  129. package/src/route-map-builder.ts +7 -1
  130. package/src/route-types.ts +37 -41
  131. package/src/router/basename.ts +14 -0
  132. package/src/router/content-negotiation.ts +113 -1
  133. package/src/router/error-handling.ts +1 -1
  134. package/src/router/find-match.ts +4 -2
  135. package/src/router/handler-context.ts +77 -38
  136. package/src/router/intercept-resolution.ts +15 -22
  137. package/src/router/lazy-includes.ts +12 -9
  138. package/src/router/loader-resolution.ts +174 -22
  139. package/src/router/logging.ts +5 -2
  140. package/src/router/manifest.ts +31 -16
  141. package/src/router/match-api.ts +128 -192
  142. package/src/router/match-handlers.ts +63 -20
  143. package/src/router/match-middleware/background-revalidation.ts +30 -2
  144. package/src/router/match-middleware/cache-lookup.ts +136 -106
  145. package/src/router/match-middleware/cache-store.ts +54 -10
  146. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  147. package/src/router/match-middleware/segment-resolution.ts +61 -5
  148. package/src/router/match-result.ts +125 -10
  149. package/src/router/metrics.ts +7 -2
  150. package/src/router/middleware-types.ts +21 -34
  151. package/src/router/middleware.ts +103 -90
  152. package/src/router/navigation-snapshot.ts +182 -0
  153. package/src/router/pattern-matching.ts +101 -17
  154. package/src/router/prerender-match.ts +110 -10
  155. package/src/router/preview-match.ts +32 -102
  156. package/src/router/request-classification.ts +286 -0
  157. package/src/router/revalidation.ts +58 -2
  158. package/src/router/route-snapshot.ts +245 -0
  159. package/src/router/router-context.ts +6 -1
  160. package/src/router/router-interfaces.ts +77 -28
  161. package/src/router/router-options.ts +76 -11
  162. package/src/router/router-registry.ts +2 -5
  163. package/src/router/segment-resolution/fresh.ts +223 -24
  164. package/src/router/segment-resolution/helpers.ts +29 -24
  165. package/src/router/segment-resolution/loader-cache.ts +1 -0
  166. package/src/router/segment-resolution/revalidation.ts +466 -285
  167. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  168. package/src/router/segment-wrappers.ts +2 -0
  169. package/src/router/substitute-pattern-params.ts +56 -0
  170. package/src/router/telemetry.ts +99 -0
  171. package/src/router/trie-matching.ts +18 -13
  172. package/src/router/types.ts +9 -0
  173. package/src/router/url-params.ts +49 -0
  174. package/src/router.ts +91 -23
  175. package/src/rsc/handler-context.ts +2 -2
  176. package/src/rsc/handler.ts +440 -381
  177. package/src/rsc/helpers.ts +91 -43
  178. package/src/rsc/index.ts +1 -1
  179. package/src/rsc/loader-fetch.ts +23 -3
  180. package/src/rsc/manifest-init.ts +5 -1
  181. package/src/rsc/origin-guard.ts +28 -10
  182. package/src/rsc/progressive-enhancement.ts +18 -2
  183. package/src/rsc/response-route-handler.ts +46 -53
  184. package/src/rsc/rsc-rendering.ts +41 -48
  185. package/src/rsc/runtime-warnings.ts +9 -10
  186. package/src/rsc/server-action.ts +25 -37
  187. package/src/rsc/ssr-setup.ts +18 -2
  188. package/src/rsc/types.ts +17 -3
  189. package/src/search-params.ts +4 -4
  190. package/src/segment-content-promise.ts +67 -0
  191. package/src/segment-loader-promise.ts +122 -0
  192. package/src/segment-system.tsx +219 -67
  193. package/src/serialize.ts +243 -0
  194. package/src/server/context.ts +277 -61
  195. package/src/server/cookie-store.ts +28 -4
  196. package/src/server/handle-store.ts +19 -0
  197. package/src/server/loader-registry.ts +9 -8
  198. package/src/server/request-context.ts +204 -60
  199. package/src/ssr/index.tsx +9 -1
  200. package/src/static-handler.ts +19 -7
  201. package/src/testing/cache-status.ts +166 -0
  202. package/src/testing/collect-handle.ts +63 -0
  203. package/src/testing/dispatch.ts +440 -0
  204. package/src/testing/dom.entry.ts +22 -0
  205. package/src/testing/e2e/fixture.ts +154 -0
  206. package/src/testing/e2e/index.ts +149 -0
  207. package/src/testing/e2e/matchers.ts +51 -0
  208. package/src/testing/e2e/page-helpers.ts +272 -0
  209. package/src/testing/e2e/parity.ts +306 -0
  210. package/src/testing/e2e/server.ts +183 -0
  211. package/src/testing/flight-matchers.ts +104 -0
  212. package/src/testing/flight-runtime.d.ts +21 -0
  213. package/src/testing/flight.entry.ts +22 -0
  214. package/src/testing/flight.ts +182 -0
  215. package/src/testing/generated-routes.ts +223 -0
  216. package/src/testing/index.ts +106 -0
  217. package/src/testing/internal/context.ts +255 -0
  218. package/src/testing/render-route.tsx +565 -0
  219. package/src/testing/run-loader.ts +296 -0
  220. package/src/testing/run-middleware.ts +179 -0
  221. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  222. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  223. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  224. package/src/testing/vitest-stubs/version.ts +5 -0
  225. package/src/testing/vitest.ts +183 -0
  226. package/src/types/cache-types.ts +4 -4
  227. package/src/types/global-namespace.ts +39 -26
  228. package/src/types/handler-context.ts +194 -72
  229. package/src/types/index.ts +1 -0
  230. package/src/types/loader-types.ts +41 -15
  231. package/src/types/request-scope.ts +126 -0
  232. package/src/types/route-entry.ts +19 -1
  233. package/src/types/segments.ts +37 -1
  234. package/src/urls/include-helper.ts +34 -67
  235. package/src/urls/index.ts +0 -3
  236. package/src/urls/path-helper-types.ts +50 -9
  237. package/src/urls/path-helper.ts +63 -63
  238. package/src/urls/pattern-types.ts +48 -19
  239. package/src/urls/response-types.ts +25 -22
  240. package/src/urls/type-extraction.ts +26 -116
  241. package/src/urls/urls-function.ts +1 -5
  242. package/src/use-loader.tsx +487 -44
  243. package/src/vite/debug.ts +185 -0
  244. package/src/vite/discovery/bundle-postprocess.ts +34 -37
  245. package/src/vite/discovery/discover-routers.ts +105 -51
  246. package/src/vite/discovery/discovery-errors.ts +194 -0
  247. package/src/vite/discovery/gate-state.ts +171 -0
  248. package/src/vite/discovery/prerender-collection.ts +188 -93
  249. package/src/vite/discovery/route-types-writer.ts +40 -84
  250. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  251. package/src/vite/discovery/state.ts +46 -6
  252. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  253. package/src/vite/index.ts +6 -0
  254. package/src/vite/plugin-types.ts +111 -72
  255. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  256. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  257. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  258. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  259. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  260. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  261. package/src/vite/plugins/expose-action-id.ts +55 -33
  262. package/src/vite/plugins/expose-id-utils.ts +24 -8
  263. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  264. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  265. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  266. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  267. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  268. package/src/vite/plugins/performance-tracks.ts +92 -0
  269. package/src/vite/plugins/refresh-cmd.ts +88 -26
  270. package/src/vite/plugins/use-cache-transform.ts +65 -50
  271. package/src/vite/plugins/version-injector.ts +39 -23
  272. package/src/vite/plugins/version-plugin.ts +72 -3
  273. package/src/vite/plugins/virtual-entries.ts +2 -2
  274. package/src/vite/rango.ts +265 -226
  275. package/src/vite/router-discovery.ts +920 -137
  276. package/src/vite/utils/ast-handler-extract.ts +15 -15
  277. package/src/vite/utils/banner.ts +4 -4
  278. package/src/vite/utils/bundle-analysis.ts +4 -2
  279. package/src/vite/utils/client-chunks.ts +190 -0
  280. package/src/vite/utils/forward-user-plugins.ts +193 -0
  281. package/src/vite/utils/manifest-utils.ts +21 -5
  282. package/src/vite/utils/package-resolution.ts +41 -1
  283. package/src/vite/utils/prerender-utils.ts +38 -5
  284. package/src/vite/utils/shared-utils.ts +109 -27
  285. package/src/browser/action-response-classifier.ts +0 -99
@@ -10,7 +10,11 @@ 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
+ } from "../../server/context";
14
18
  import type {
15
19
  HandlerContext,
16
20
  InternalHandlerContext,
@@ -35,9 +39,14 @@ import {
35
39
  resolveLayoutComponent,
36
40
  resolveWithErrorBoundary,
37
41
  } from "./helpers.js";
42
+ import { applyViewTransitionDefault } from "./view-transition-default.js";
38
43
  import { getRouterContext } from "../router-context.js";
39
44
  import { resolveSink, safeEmit } from "../telemetry.js";
40
- import { track } from "../../server/context.js";
45
+ import {
46
+ track,
47
+ RangoContext,
48
+ runInsideLoaderScope,
49
+ } from "../../server/context.js";
41
50
 
42
51
  // ---------------------------------------------------------------------------
43
52
  // Telemetry helpers
@@ -81,6 +90,27 @@ function observeStreamedHandler(
81
90
  });
82
91
  }
83
92
 
93
+ /**
94
+ * Trace a parallel slot that's being force-rendered on a full refetch (client
95
+ * has no cached state). User revalidate fns are bypassed in this case — see
96
+ * the call sites for the load-bearing rationale.
97
+ */
98
+ function traceFullRefetchedParallelSlot(
99
+ parallelId: string,
100
+ belongsToRoute: boolean,
101
+ ): void {
102
+ if (!isTraceActive()) return;
103
+ pushRevalidationTraceEntry({
104
+ segmentId: parallelId,
105
+ segmentType: "parallel",
106
+ belongsToRoute,
107
+ source: "parallel",
108
+ defaultShouldRevalidate: true,
109
+ finalShouldRevalidate: true,
110
+ reason: "full-refetch",
111
+ });
112
+ }
113
+
84
114
  // ---------------------------------------------------------------------------
85
115
  // Revalidation telemetry helper
86
116
  // ---------------------------------------------------------------------------
@@ -228,7 +258,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
228
258
  params: ctx.params,
229
259
  loaderId: loader.$$id,
230
260
  loaderData: deps.wrapLoaderPromise(
231
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
261
+ runInsideLoaderScope(() =>
262
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
263
+ ),
232
264
  entry,
233
265
  segmentId,
234
266
  ctx.pathname,
@@ -258,26 +290,95 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
258
290
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
259
291
  const allLoaderSegments: ResolvedSegment[] = [];
260
292
  const allMatchedIds: string[] = [];
293
+ const seenIds = new Set<string>();
294
+
295
+ async function collectEntryLoaders(
296
+ entry: EntryData,
297
+ belongsToRoute: boolean,
298
+ shortCodeOverride?: string,
299
+ ): Promise<void> {
300
+ // Skip if all loaders from this entry have already been resolved
301
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
302
+ const loaderEntries = entry.loader ?? [];
303
+ const sc = shortCodeOverride ?? entry.shortCode;
304
+ const allAlreadySeen =
305
+ loaderEntries.length > 0 &&
306
+ loaderEntries.every((le, i) =>
307
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
308
+ );
309
+ if (!allAlreadySeen) {
310
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
311
+ entry,
312
+ context,
313
+ belongsToRoute,
314
+ clientSegmentIds,
315
+ prevParams,
316
+ request,
317
+ prevUrl,
318
+ nextUrl,
319
+ routeKey,
320
+ deps,
321
+ actionContext,
322
+ shortCodeOverride,
323
+ stale,
324
+ );
325
+ for (const seg of segments) {
326
+ if (!seenIds.has(seg.id)) {
327
+ seenIds.add(seg.id);
328
+ allLoaderSegments.push(seg);
329
+ }
330
+ }
331
+ allMatchedIds.push(...matchedIds);
332
+ }
333
+
334
+ const seenParallelEntryIds = new Set<string>();
335
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
336
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
337
+ seenParallelEntryIds.add(parallelEntry.id);
338
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
339
+ }
340
+
341
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
342
+ for (const layoutEntry of entry.layout) {
343
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
344
+ // Inherit route loaders for orphan layouts with parallels.
345
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
346
+ // route entry, as that would re-iterate route.layout and loop.
347
+ if (
348
+ entry.type === "route" &&
349
+ entry.loader &&
350
+ entry.loader.length > 0 &&
351
+ Object.keys(layoutEntry.parallel).length > 0
352
+ ) {
353
+ const inherited = await resolveLoadersWithRevalidation(
354
+ entry,
355
+ context,
356
+ childBelongsToRoute,
357
+ clientSegmentIds,
358
+ prevParams,
359
+ request,
360
+ prevUrl,
361
+ nextUrl,
362
+ routeKey,
363
+ deps,
364
+ actionContext,
365
+ layoutEntry.shortCode,
366
+ stale,
367
+ );
368
+ for (const seg of inherited.segments) {
369
+ if (!seenIds.has(seg.id)) {
370
+ seenIds.add(seg.id);
371
+ seg._inherited = true;
372
+ allLoaderSegments.push(seg);
373
+ }
374
+ }
375
+ allMatchedIds.push(...inherited.matchedIds);
376
+ }
377
+ }
378
+ }
261
379
 
262
380
  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);
381
+ await collectEntryLoaders(entry, entry.type === "route");
281
382
  }
282
383
 
283
384
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -301,22 +402,20 @@ export function buildEntryRevalidateMap(
301
402
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
302
403
 
303
404
  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
- }
405
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
406
+ entry.parallel,
407
+ )) {
408
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
409
+ const parallelId = `${parallelParentShortCode}.${slot}`;
410
+ map.set(parallelId, {
411
+ entry: parallelEntry,
412
+ revalidate: parallelEntry.revalidate,
413
+ });
315
414
  }
316
415
  }
317
416
 
318
417
  for (const layoutEntry of entry.layout) {
319
- processEntry(layoutEntry);
418
+ processEntry(layoutEntry, entry.shortCode);
320
419
  }
321
420
  }
322
421
 
@@ -348,7 +447,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
348
447
  const segments: ResolvedSegment[] = [];
349
448
  const matchedIds: string[] = [];
350
449
 
351
- for (const parallelEntry of entry.parallel) {
450
+ const resolvedParallelEntries = new Set<string>();
451
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
452
+ entry.parallel,
453
+ )) {
352
454
  invariant(
353
455
  parallelEntry.type === "parallel",
354
456
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -359,108 +461,106 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
359
461
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
360
462
  | ReactNode
361
463
  >;
362
-
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);
464
+ // In production, static handler bodies are evicted and the slot value
465
+ // may be undefined. The static store holds the pre-rendered component.
466
+ // We defer the handler check until after tryStaticSlot.
467
+ const handler = slots[slot];
468
+
469
+ const parallelId = `${entry.shortCode}.${slot}`;
470
+
471
+ const isFullRefetch = clientSegmentIds.size === 0;
472
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
473
+ // Always announce the slot in matchedIds — it's unconditionally appended
474
+ // to `segments` below, and a segment present in segments but missing from
475
+ // matched lets the client prune it (then it's missing from clientSegmentIds
476
+ // on the next request, perpetuating the staleness).
477
+ matchedIds.push(parallelId);
478
+
479
+ let shouldResolve: boolean;
480
+ if (isFullRefetch) {
481
+ // Client has nothing cached — slot MUST render. User revalidate fns are
482
+ // bypassed here because returning false would leave the segment blank
483
+ // with no client-side fallback.
484
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
485
+ shouldResolve = true;
486
+ } else {
487
+ // For non-empty client sets, consult user revalidate fns. When the slot
488
+ // is unknown to the client, override the type-derived default so the
489
+ // soft chain seeds with the right "new segment" / "parent-chain" value.
490
+ let defaultOverride: { value: boolean; reason: string } | undefined;
491
+ if (!clientSegmentIds.has(parallelId)) {
492
+ const value = belongsToRoute || isNewParent;
493
+ defaultOverride = {
494
+ value,
495
+ reason: value ? "new-segment" : "skip-parent-chain",
496
+ };
380
497
  }
381
498
 
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
- }
412
-
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
- };
499
+ const dummySegment: ResolvedSegment = {
500
+ id: parallelId,
501
+ namespace: parallelEntry.id,
502
+ type: "parallel",
503
+ index: 0,
504
+ component: null as any,
505
+ params,
506
+ slot,
507
+ belongsToRoute,
508
+ parallelName: `${parallelEntry.id}.${slot}`,
509
+ ...(parallelEntry.mountPath
510
+ ? { mountPath: parallelEntry.mountPath }
511
+ : {}),
512
+ };
427
513
 
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,
514
+ shouldResolve = await evaluateRevalidation({
515
+ segment: dummySegment,
516
+ prevParams,
517
+ getPrevSegment: null,
518
+ request,
519
+ prevUrl,
520
+ nextUrl,
521
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
522
+ name: `revalidate${i}`,
523
+ fn,
524
+ })),
449
525
  routeKey,
450
- shouldResolve,
451
- );
526
+ context,
527
+ actionContext,
528
+ stale,
529
+ traceSource: "parallel",
530
+ defaultOverride,
531
+ });
532
+ }
533
+ emitRevalidationDecision(
534
+ parallelId,
535
+ context.pathname,
536
+ routeKey,
537
+ shouldResolve,
538
+ );
452
539
 
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) {
540
+ let component: ReactNode | undefined;
541
+ let handlerRan = false;
542
+ if (shouldResolve) {
543
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
544
+ // tryStaticSlot returning a value means the static cache supplied the
545
+ // component — handler did NOT run. handlerRan stays false.
546
+ }
547
+ if (component === undefined) {
548
+ const hasLoadingFallback =
549
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
550
+ if (!shouldResolve) {
551
+ component = null;
552
+ } else if (handler === undefined) {
553
+ // Handler evicted (production static slot) but static lookup missed.
554
+ // Nothing to render — use null so the client keeps its cached version.
555
+ component = null;
556
+ } else {
557
+ // Slot-keyed pushes — slot owns its own bucket, parent layout owns
558
+ // its own. On slot-only revalidations the partial merge updates only
559
+ // the slot's bucket; the parent's bucket stays intact.
560
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
561
+ parallelId;
562
+ handlerRan = true;
563
+ if (hasLoadingFallback) {
464
564
  const result =
465
565
  typeof handler === "function" ? handler(context) : handler;
466
566
  if (result instanceof Promise) {
@@ -485,44 +585,51 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
485
585
  typeof handler === "function" ? await handler(context) : handler;
486
586
  }
487
587
  }
488
-
489
- segments.push({
490
- id: parallelId,
491
- namespace: parallelEntry.id,
492
- type: "parallel",
493
- index: 0,
494
- component,
495
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
496
- transition: parallelEntry.transition,
497
- params,
498
- slot,
499
- belongsToRoute,
500
- parallelName: `${parallelEntry.id}.${slot}`,
501
- ...(parallelEntry.mountPath
502
- ? { mountPath: parallelEntry.mountPath }
503
- : {}),
504
- });
505
588
  }
506
589
 
507
- if (!parallelEntry.loading) {
508
- const loaderResult = await resolveLoadersWithRevalidation(
509
- parallelEntry,
510
- context,
511
- belongsToRoute,
512
- clientSegmentIds,
513
- prevParams,
514
- request,
515
- prevUrl,
516
- nextUrl,
517
- routeKey,
518
- deps,
519
- actionContext,
520
- entry.shortCode,
521
- stale,
522
- );
523
- segments.push(...loaderResult.segments);
524
- matchedIds.push(...loaderResult.matchedIds);
590
+ segments.push({
591
+ id: parallelId,
592
+ namespace: parallelEntry.id,
593
+ type: "parallel",
594
+ index: 0,
595
+ component,
596
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
597
+ transition: applyViewTransitionDefault(
598
+ parallelEntry.transition,
599
+ deps.viewTransitionDefault,
600
+ ),
601
+ params,
602
+ slot,
603
+ _handlerRan: handlerRan,
604
+ belongsToRoute,
605
+ parallelName: `${parallelEntry.id}.${slot}`,
606
+ ...(parallelEntry.mountPath
607
+ ? { mountPath: parallelEntry.mountPath }
608
+ : {}),
609
+ });
610
+
611
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
612
+ continue;
525
613
  }
614
+
615
+ const loaderResult = await resolveLoadersWithRevalidation(
616
+ parallelEntry,
617
+ context,
618
+ belongsToRoute,
619
+ clientSegmentIds,
620
+ prevParams,
621
+ request,
622
+ prevUrl,
623
+ nextUrl,
624
+ routeKey,
625
+ deps,
626
+ actionContext,
627
+ entry.shortCode,
628
+ stale,
629
+ );
630
+ segments.push(...loaderResult.segments);
631
+ matchedIds.push(...loaderResult.matchedIds);
632
+ resolvedParallelEntries.add(parallelEntry.id);
526
633
  }
527
634
 
528
635
  return { segments, matchedIds };
@@ -548,6 +655,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
548
655
  ): Promise<{ segment: ResolvedSegment; matchedId: string }> {
549
656
  const matchedId = entry.shortCode;
550
657
 
658
+ let handlerRan = false;
551
659
  const component = await revalidate(
552
660
  async () => {
553
661
  const hasSegment = clientSegmentIds.has(entry.shortCode);
@@ -608,6 +716,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
608
716
  context,
609
717
  actionContext,
610
718
  stale,
719
+ traceSource:
720
+ entry.type === "route" ? "route-handler" : "layout-handler",
611
721
  });
612
722
  emitRevalidationDecision(
613
723
  entry.shortCode,
@@ -622,6 +732,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
622
732
  return shouldRevalidate;
623
733
  },
624
734
  async () => {
735
+ handlerRan = true;
625
736
  const doneHandler = track(`handler:${entry.id}`, 2);
626
737
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
627
738
  entry.shortCode;
@@ -636,13 +747,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
636
747
  return staticComponent;
637
748
  }
638
749
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
750
+ // For Passthrough routes at runtime, use the live handler instead of
751
+ // the build handler. At build time (context.build === true), always
752
+ // use the build handler from routeEntry.handler.
753
+ const handler =
754
+ !context.build && routeEntry.liveHandler
755
+ ? routeEntry.liveHandler
756
+ : routeEntry.handler;
639
757
  if (!routeEntry.loading) {
640
- const result = handleHandlerResult(await routeEntry.handler(context));
758
+ const result = handleHandlerResult(await handler(context));
641
759
  doneHandler();
642
760
  return result;
643
761
  }
644
762
  if (!actionContext) {
645
- const result = handleHandlerResult(routeEntry.handler(context));
763
+ const result = handleHandlerResult(handler(context));
646
764
  if (result instanceof Promise) {
647
765
  result.finally(doneHandler).catch(() => {});
648
766
  const tracked = deps.trackHandler(result, {
@@ -665,9 +783,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
665
783
  debugLog("segment.action", "resolving action route with awaited value", {
666
784
  entryId: entry.id,
667
785
  });
668
- const actionResult = handleHandlerResult(
669
- await routeEntry.handler(context),
670
- );
786
+ const actionResult = handleHandlerResult(await handler(context));
671
787
  doneHandler();
672
788
  return {
673
789
  content: Promise.resolve(actionResult),
@@ -676,10 +792,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
676
792
  () => null,
677
793
  );
678
794
 
795
+ // Normalize void handlers (undefined) to null so the reconciler's
796
+ // component === null checks work consistently for both void and explicit null.
679
797
  const resolvedComponent =
680
798
  component && typeof component === "object" && "content" in component
681
- ? (component as { content: ReactNode }).content
682
- : component;
799
+ ? ((component as { content: ReactNode }).content ?? null)
800
+ : (component ?? null);
683
801
 
684
802
  const segment: ResolvedSegment = {
685
803
  id: entry.shortCode,
@@ -689,13 +807,17 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
689
807
  index: 0,
690
808
  component: resolvedComponent,
691
809
  loading: entry.loading === false ? null : entry.loading,
692
- transition: entry.transition,
810
+ transition: applyViewTransitionDefault(
811
+ entry.transition,
812
+ deps.viewTransitionDefault,
813
+ ),
693
814
  params,
694
815
  belongsToRoute,
695
816
  ...(entry.type === "layout" || entry.type === "cache"
696
817
  ? { layoutName: entry.id }
697
818
  : {}),
698
819
  ...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
820
+ _handlerRan: handlerRan,
699
821
  };
700
822
 
701
823
  return { segment, matchedId };
@@ -776,11 +898,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
776
898
  prevUrl,
777
899
  nextUrl,
778
900
  routeKey,
779
- loaderPromises,
780
901
  true,
781
902
  deps,
782
903
  actionContext,
783
904
  stale,
905
+ entry,
784
906
  );
785
907
  segments.push(...orphanResult.segments);
786
908
  matchedIds.push(...orphanResult.matchedIds);
@@ -860,7 +982,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
860
982
  prevUrl,
861
983
  nextUrl,
862
984
  routeKey,
863
- loaderPromises,
864
985
  false,
865
986
  deps,
866
987
  actionContext,
@@ -887,11 +1008,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
887
1008
  prevUrl: URL,
888
1009
  nextUrl: URL,
889
1010
  routeKey: string,
890
- loaderPromises: Map<string, Promise<any>>,
891
1011
  belongsToRoute: boolean,
892
1012
  deps: SegmentResolutionDeps<TEnv>,
893
1013
  actionContext?: ActionContext,
894
1014
  stale?: boolean,
1015
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
1016
+ parentRouteEntry?: EntryData,
895
1017
  ): Promise<SegmentRevalidationResult> {
896
1018
  invariant(
897
1019
  orphan.type === "layout" || orphan.type === "cache",
@@ -919,6 +1041,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
919
1041
  segments.push(...loaderResult.segments);
920
1042
  matchedIds.push(...loaderResult.matchedIds);
921
1043
 
1044
+ // Inherit parent route's loaders so parallel slots inside this layout
1045
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1046
+ if (
1047
+ parentRouteEntry &&
1048
+ parentRouteEntry.loader &&
1049
+ parentRouteEntry.loader.length > 0 &&
1050
+ Object.keys(orphan.parallel).length > 0
1051
+ ) {
1052
+ const inheritedResult = await resolveLoadersWithRevalidation(
1053
+ parentRouteEntry,
1054
+ context,
1055
+ belongsToRoute,
1056
+ clientSegmentIds,
1057
+ prevParams,
1058
+ request,
1059
+ prevUrl,
1060
+ nextUrl,
1061
+ routeKey,
1062
+ deps,
1063
+ actionContext,
1064
+ orphan.shortCode,
1065
+ stale,
1066
+ );
1067
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1068
+ for (const s of inheritedResult.segments) {
1069
+ s._inherited = true;
1070
+ }
1071
+ segments.push(...inheritedResult.segments);
1072
+ matchedIds.push(...inheritedResult.matchedIds);
1073
+ }
1074
+
922
1075
  // Handler-first: resolve orphan layout handler before its parallels
923
1076
  // so ctx.set() values are visible to parallel children.
924
1077
  matchedIds.push(orphan.shortCode);
@@ -991,114 +1144,133 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
991
1144
  belongsToRoute,
992
1145
  layoutName: orphan.id,
993
1146
  loading: orphan.loading === false ? null : orphan.loading,
994
- transition: orphan.transition,
1147
+ transition: applyViewTransitionDefault(
1148
+ orphan.transition,
1149
+ deps.viewTransitionDefault,
1150
+ ),
995
1151
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
996
1152
  });
997
1153
 
998
- for (const parallelEntry of orphan.parallel) {
1154
+ const resolvedParallelEntries = new Set<string>();
1155
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1156
+ orphan.parallel,
1157
+ )) {
999
1158
  invariant(
1000
1159
  parallelEntry.type === "parallel",
1001
1160
  `Expected parallel entry, got: ${parallelEntry.type}`,
1002
1161
  );
1003
1162
 
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);
1163
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1164
+ // shortCodeOverride must match the parent layout, not the parallel entry.
1165
+ const loaderResult = await resolveLoadersWithRevalidation(
1166
+ parallelEntry,
1167
+ context,
1168
+ belongsToRoute,
1169
+ clientSegmentIds,
1170
+ prevParams,
1171
+ request,
1172
+ prevUrl,
1173
+ nextUrl,
1174
+ routeKey,
1175
+ deps,
1176
+ actionContext,
1177
+ orphan.shortCode,
1178
+ stale,
1179
+ );
1180
+ segments.push(...loaderResult.segments);
1181
+ matchedIds.push(...loaderResult.matchedIds);
1182
+ resolvedParallelEntries.add(parallelEntry.id);
1183
+ }
1021
1184
 
1022
1185
  const slots = parallelEntry.handler as Record<
1023
1186
  `@${string}`,
1024
1187
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1025
1188
  | ReactNode
1026
1189
  >;
1190
+ // Handler may be undefined in production after static handler eviction.
1191
+ const handler = slots[slot];
1192
+
1193
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1194
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1195
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1196
+ const parallelId = `${orphan.shortCode}.${slot}`;
1197
+ matchedIds.push(parallelId);
1198
+
1199
+ const isFullRefetch = clientSegmentIds.size === 0;
1200
+ let shouldResolve: boolean;
1201
+ if (isFullRefetch) {
1202
+ // Same load-bearing rationale as the main parallel path: full refetch
1203
+ // means the client has nothing to fall back to, so the slot must render.
1204
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
1205
+ shouldResolve = true;
1206
+ } else {
1207
+ // When slot is unknown to the client, seed the soft chain with `true`
1208
+ // (orphan parallels always belong to the route — we want them rendered
1209
+ // unless the user explicitly opts out via revalidate()).
1210
+ const defaultOverride = clientSegmentIds.has(parallelId)
1211
+ ? undefined
1212
+ : { value: true, reason: "new-segment" };
1027
1213
 
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
- };
1214
+ const dummySegment: ResolvedSegment = {
1215
+ id: parallelId,
1216
+ namespace: parallelEntry.id,
1217
+ type: "parallel",
1218
+ index: 0,
1219
+ component: null as any,
1220
+ params,
1221
+ slot,
1222
+ belongsToRoute,
1223
+ parallelName: `${parallelEntry.id}.${slot}`,
1224
+ ...(parallelEntry.mountPath
1225
+ ? { mountPath: parallelEntry.mountPath }
1226
+ : {}),
1227
+ };
1065
1228
 
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,
1229
+ shouldResolve = await evaluateRevalidation({
1230
+ segment: dummySegment,
1231
+ prevParams,
1232
+ getPrevSegment: null,
1233
+ request,
1234
+ prevUrl,
1235
+ nextUrl,
1236
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1237
+ name: `revalidate${i}`,
1238
+ fn,
1239
+ })),
1087
1240
  routeKey,
1088
- shouldResolve,
1089
- );
1241
+ context,
1242
+ actionContext,
1243
+ stale,
1244
+ traceSource: "parallel",
1245
+ defaultOverride,
1246
+ });
1247
+ }
1248
+ emitRevalidationDecision(
1249
+ parallelId,
1250
+ context.pathname,
1251
+ routeKey,
1252
+ shouldResolve,
1253
+ );
1090
1254
 
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) {
1255
+ let component: ReactNode | undefined;
1256
+ let handlerRan = false;
1257
+ if (shouldResolve) {
1258
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1259
+ }
1260
+ if (component === undefined) {
1261
+ const hasLoadingFallback =
1262
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1263
+ if (!shouldResolve) {
1264
+ component = null;
1265
+ } else if (handler === undefined) {
1266
+ // Handler evicted (production static slot) but static lookup missed.
1267
+ component = null;
1268
+ } else {
1269
+ // Slot-keyed pushes — see resolveParallelSegmentsWithRevalidation.
1270
+ (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
1271
+ parallelId;
1272
+ handlerRan = true;
1273
+ if (hasLoadingFallback) {
1102
1274
  const result =
1103
1275
  typeof handler === "function" ? handler(context) : handler;
1104
1276
  if (result instanceof Promise) {
@@ -1123,24 +1295,28 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1123
1295
  typeof handler === "function" ? await handler(context) : handler;
1124
1296
  }
1125
1297
  }
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
1298
  }
1299
+
1300
+ segments.push({
1301
+ id: parallelId,
1302
+ namespace: parallelEntry.id,
1303
+ type: "parallel",
1304
+ index: 0,
1305
+ component,
1306
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1307
+ transition: applyViewTransitionDefault(
1308
+ parallelEntry.transition,
1309
+ deps.viewTransitionDefault,
1310
+ ),
1311
+ params,
1312
+ slot,
1313
+ _handlerRan: handlerRan,
1314
+ belongsToRoute,
1315
+ parallelName: `${parallelEntry.id}.${slot}`,
1316
+ ...(parallelEntry.mountPath
1317
+ ? { mountPath: parallelEntry.mountPath }
1318
+ : {}),
1319
+ });
1144
1320
  }
1145
1321
 
1146
1322
  return { segments, matchedIds };
@@ -1165,6 +1341,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1165
1341
  localRouteName: string,
1166
1342
  pathname: string,
1167
1343
  deps: SegmentResolutionDeps<TEnv>,
1344
+ stale?: boolean,
1168
1345
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1169
1346
  const allSegments: ResolvedSegment[] = [];
1170
1347
  const matchedIds: string[] = [];
@@ -1191,6 +1368,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1191
1368
  }
1192
1369
 
1193
1370
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1371
+ if (entry.type === "cache") {
1372
+ const store = RangoContext.getStore();
1373
+ if (store) store.insideCacheScope = true;
1374
+ }
1194
1375
  const doneEntry = track(`segment:${entry.id}`, 1);
1195
1376
  const resolved = await resolveWithErrorBoundary(
1196
1377
  nonParallelEntry,
@@ -1209,7 +1390,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1209
1390
  loaderPromises,
1210
1391
  deps,
1211
1392
  actionContext,
1212
- false,
1393
+ stale,
1213
1394
  ),
1214
1395
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1215
1396
  deps,