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

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 (182) hide show
  1. package/README.md +126 -38
  2. package/dist/bin/rango.js +138 -50
  3. package/dist/vite/index.js +2018 -732
  4. package/dist/vite/index.js.bak +5448 -0
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +7 -5
  7. package/skills/breadcrumbs/SKILL.md +3 -1
  8. package/skills/cache-guide/SKILL.md +32 -0
  9. package/skills/caching/SKILL.md +45 -4
  10. package/skills/handler-use/SKILL.md +362 -0
  11. package/skills/hooks/SKILL.md +28 -20
  12. package/skills/intercept/SKILL.md +20 -0
  13. package/skills/layout/SKILL.md +22 -0
  14. package/skills/links/SKILL.md +91 -17
  15. package/skills/loader/SKILL.md +119 -45
  16. package/skills/middleware/SKILL.md +34 -3
  17. package/skills/migrate-nextjs/SKILL.md +560 -0
  18. package/skills/migrate-react-router/SKILL.md +765 -0
  19. package/skills/parallel/SKILL.md +192 -0
  20. package/skills/prerender/SKILL.md +110 -68
  21. package/skills/rango/SKILL.md +24 -22
  22. package/skills/response-routes/SKILL.md +8 -0
  23. package/skills/route/SKILL.md +55 -0
  24. package/skills/router-setup/SKILL.md +87 -2
  25. package/skills/streams-and-websockets/SKILL.md +283 -0
  26. package/skills/typesafety/SKILL.md +13 -1
  27. package/src/__internal.ts +1 -1
  28. package/src/browser/app-shell.ts +52 -0
  29. package/src/browser/app-version.ts +14 -0
  30. package/src/browser/event-controller.ts +5 -0
  31. package/src/browser/navigation-bridge.ts +88 -9
  32. package/src/browser/navigation-client.ts +167 -59
  33. package/src/browser/navigation-store.ts +68 -9
  34. package/src/browser/navigation-transaction.ts +11 -9
  35. package/src/browser/partial-update.ts +109 -15
  36. package/src/browser/prefetch/cache.ts +175 -15
  37. package/src/browser/prefetch/fetch.ts +180 -33
  38. package/src/browser/prefetch/queue.ts +123 -20
  39. package/src/browser/prefetch/resource-ready.ts +77 -0
  40. package/src/browser/rango-state.ts +53 -13
  41. package/src/browser/react/Link.tsx +81 -9
  42. package/src/browser/react/NavigationProvider.tsx +89 -14
  43. package/src/browser/react/context.ts +7 -2
  44. package/src/browser/react/use-handle.ts +9 -58
  45. package/src/browser/react/use-navigation.ts +22 -2
  46. package/src/browser/react/use-params.ts +11 -1
  47. package/src/browser/react/use-router.ts +29 -9
  48. package/src/browser/rsc-router.tsx +168 -65
  49. package/src/browser/scroll-restoration.ts +36 -17
  50. package/src/browser/segment-reconciler.ts +36 -9
  51. package/src/browser/server-action-bridge.ts +8 -6
  52. package/src/browser/types.ts +49 -5
  53. package/src/build/generate-manifest.ts +6 -6
  54. package/src/build/generate-route-types.ts +3 -0
  55. package/src/build/route-trie.ts +50 -24
  56. package/src/build/route-types/include-resolution.ts +8 -1
  57. package/src/build/route-types/router-processing.ts +223 -74
  58. package/src/build/route-types/scan-filter.ts +8 -1
  59. package/src/cache/cache-runtime.ts +15 -11
  60. package/src/cache/cache-scope.ts +48 -7
  61. package/src/cache/cf/cf-cache-store.ts +455 -15
  62. package/src/cache/cf/index.ts +5 -1
  63. package/src/cache/document-cache.ts +17 -7
  64. package/src/cache/index.ts +1 -0
  65. package/src/cache/taint.ts +55 -0
  66. package/src/client.tsx +84 -230
  67. package/src/context-var.ts +72 -2
  68. package/src/debug.ts +2 -2
  69. package/src/handle.ts +40 -0
  70. package/src/index.rsc.ts +6 -1
  71. package/src/index.ts +49 -6
  72. package/src/outlet-context.ts +1 -1
  73. package/src/prerender/store.ts +5 -4
  74. package/src/prerender.ts +138 -77
  75. package/src/response-utils.ts +28 -0
  76. package/src/reverse.ts +27 -2
  77. package/src/route-definition/dsl-helpers.ts +240 -40
  78. package/src/route-definition/helpers-types.ts +73 -20
  79. package/src/route-definition/index.ts +3 -0
  80. package/src/route-definition/redirect.ts +11 -3
  81. package/src/route-definition/resolve-handler-use.ts +155 -0
  82. package/src/route-map-builder.ts +7 -1
  83. package/src/route-types.ts +18 -0
  84. package/src/router/content-negotiation.ts +100 -1
  85. package/src/router/find-match.ts +4 -2
  86. package/src/router/handler-context.ts +101 -25
  87. package/src/router/intercept-resolution.ts +11 -4
  88. package/src/router/lazy-includes.ts +10 -7
  89. package/src/router/loader-resolution.ts +159 -21
  90. package/src/router/logging.ts +5 -2
  91. package/src/router/manifest.ts +31 -16
  92. package/src/router/match-api.ts +127 -192
  93. package/src/router/match-middleware/background-revalidation.ts +30 -2
  94. package/src/router/match-middleware/cache-lookup.ts +94 -17
  95. package/src/router/match-middleware/cache-store.ts +53 -10
  96. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  97. package/src/router/match-middleware/segment-resolution.ts +61 -5
  98. package/src/router/match-result.ts +104 -10
  99. package/src/router/metrics.ts +6 -1
  100. package/src/router/middleware-types.ts +8 -30
  101. package/src/router/middleware.ts +58 -13
  102. package/src/router/navigation-snapshot.ts +182 -0
  103. package/src/router/pattern-matching.ts +60 -9
  104. package/src/router/prerender-match.ts +110 -10
  105. package/src/router/preview-match.ts +30 -102
  106. package/src/router/request-classification.ts +310 -0
  107. package/src/router/revalidation.ts +15 -1
  108. package/src/router/route-snapshot.ts +245 -0
  109. package/src/router/router-context.ts +6 -1
  110. package/src/router/router-interfaces.ts +36 -4
  111. package/src/router/router-options.ts +37 -11
  112. package/src/router/segment-resolution/fresh.ts +198 -20
  113. package/src/router/segment-resolution/helpers.ts +29 -24
  114. package/src/router/segment-resolution/loader-cache.ts +1 -0
  115. package/src/router/segment-resolution/revalidation.ts +452 -307
  116. package/src/router/segment-wrappers.ts +2 -0
  117. package/src/router/trie-matching.ts +10 -4
  118. package/src/router/types.ts +1 -0
  119. package/src/router/url-params.ts +49 -0
  120. package/src/router.ts +60 -8
  121. package/src/rsc/handler.ts +478 -374
  122. package/src/rsc/helpers.ts +69 -41
  123. package/src/rsc/loader-fetch.ts +23 -3
  124. package/src/rsc/manifest-init.ts +5 -1
  125. package/src/rsc/progressive-enhancement.ts +16 -2
  126. package/src/rsc/response-route-handler.ts +14 -1
  127. package/src/rsc/rsc-rendering.ts +17 -1
  128. package/src/rsc/server-action.ts +10 -0
  129. package/src/rsc/ssr-setup.ts +2 -2
  130. package/src/rsc/types.ts +9 -1
  131. package/src/segment-content-promise.ts +67 -0
  132. package/src/segment-loader-promise.ts +122 -0
  133. package/src/segment-system.tsx +109 -23
  134. package/src/server/context.ts +166 -17
  135. package/src/server/handle-store.ts +19 -0
  136. package/src/server/loader-registry.ts +9 -8
  137. package/src/server/request-context.ts +194 -60
  138. package/src/ssr/index.tsx +4 -0
  139. package/src/static-handler.ts +18 -6
  140. package/src/types/cache-types.ts +4 -4
  141. package/src/types/handler-context.ts +145 -68
  142. package/src/types/loader-types.ts +41 -15
  143. package/src/types/request-scope.ts +126 -0
  144. package/src/types/route-entry.ts +19 -1
  145. package/src/types/segments.ts +2 -0
  146. package/src/urls/include-helper.ts +24 -14
  147. package/src/urls/path-helper-types.ts +39 -6
  148. package/src/urls/path-helper.ts +48 -13
  149. package/src/urls/pattern-types.ts +12 -0
  150. package/src/urls/response-types.ts +18 -16
  151. package/src/use-loader.tsx +77 -5
  152. package/src/vite/debug.ts +184 -0
  153. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  154. package/src/vite/discovery/discover-routers.ts +36 -4
  155. package/src/vite/discovery/gate-state.ts +171 -0
  156. package/src/vite/discovery/prerender-collection.ts +175 -74
  157. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  158. package/src/vite/discovery/state.ts +13 -6
  159. package/src/vite/index.ts +4 -0
  160. package/src/vite/plugin-types.ts +51 -79
  161. package/src/vite/plugins/cjs-to-esm.ts +5 -0
  162. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  163. package/src/vite/plugins/client-ref-hashing.ts +16 -4
  164. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  165. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  166. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  167. package/src/vite/plugins/expose-action-id.ts +53 -31
  168. package/src/vite/plugins/expose-id-utils.ts +12 -0
  169. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  170. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  171. package/src/vite/plugins/expose-internal-ids.ts +563 -316
  172. package/src/vite/plugins/performance-tracks.ts +96 -0
  173. package/src/vite/plugins/refresh-cmd.ts +88 -26
  174. package/src/vite/plugins/use-cache-transform.ts +56 -43
  175. package/src/vite/plugins/version-injector.ts +37 -11
  176. package/src/vite/plugins/version-plugin.ts +13 -1
  177. package/src/vite/rango.ts +204 -217
  178. package/src/vite/router-discovery.ts +732 -94
  179. package/src/vite/utils/banner.ts +4 -4
  180. package/src/vite/utils/package-resolution.ts +41 -1
  181. package/src/vite/utils/prerender-utils.ts +37 -5
  182. package/src/vite/utils/shared-utils.ts +3 -2
@@ -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,
@@ -37,7 +41,11 @@ import {
37
41
  } from "./helpers.js";
38
42
  import { getRouterContext } from "../router-context.js";
39
43
  import { resolveSink, safeEmit } from "../telemetry.js";
40
- import { track } from "../../server/context.js";
44
+ import {
45
+ track,
46
+ RSCRouterContext,
47
+ runInsideLoaderScope,
48
+ } from "../../server/context.js";
41
49
 
42
50
  // ---------------------------------------------------------------------------
43
51
  // Telemetry helpers
@@ -81,6 +89,27 @@ function observeStreamedHandler(
81
89
  });
82
90
  }
83
91
 
92
+ /**
93
+ * Trace a parallel slot that's being force-rendered on a full refetch (client
94
+ * has no cached state). User revalidate fns are bypassed in this case — see
95
+ * the call sites for the load-bearing rationale.
96
+ */
97
+ function traceFullRefetchedParallelSlot(
98
+ parallelId: string,
99
+ belongsToRoute: boolean,
100
+ ): void {
101
+ if (!isTraceActive()) return;
102
+ pushRevalidationTraceEntry({
103
+ segmentId: parallelId,
104
+ segmentType: "parallel",
105
+ belongsToRoute,
106
+ source: "parallel",
107
+ defaultShouldRevalidate: true,
108
+ finalShouldRevalidate: true,
109
+ reason: "full-refetch",
110
+ });
111
+ }
112
+
84
113
  // ---------------------------------------------------------------------------
85
114
  // Revalidation telemetry helper
86
115
  // ---------------------------------------------------------------------------
@@ -228,7 +257,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
228
257
  params: ctx.params,
229
258
  loaderId: loader.$$id,
230
259
  loaderData: deps.wrapLoaderPromise(
231
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
260
+ runInsideLoaderScope(() =>
261
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
262
+ ),
232
263
  entry,
233
264
  segmentId,
234
265
  ctx.pathname,
@@ -258,26 +289,95 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
258
289
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
259
290
  const allLoaderSegments: ResolvedSegment[] = [];
260
291
  const allMatchedIds: string[] = [];
292
+ const seenIds = new Set<string>();
293
+
294
+ async function collectEntryLoaders(
295
+ entry: EntryData,
296
+ belongsToRoute: boolean,
297
+ shortCodeOverride?: string,
298
+ ): Promise<void> {
299
+ // Skip if all loaders from this entry have already been resolved
300
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
301
+ const loaderEntries = entry.loader ?? [];
302
+ const sc = shortCodeOverride ?? entry.shortCode;
303
+ const allAlreadySeen =
304
+ loaderEntries.length > 0 &&
305
+ loaderEntries.every((le, i) =>
306
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
307
+ );
308
+ if (!allAlreadySeen) {
309
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
310
+ entry,
311
+ context,
312
+ belongsToRoute,
313
+ clientSegmentIds,
314
+ prevParams,
315
+ request,
316
+ prevUrl,
317
+ nextUrl,
318
+ routeKey,
319
+ deps,
320
+ actionContext,
321
+ shortCodeOverride,
322
+ stale,
323
+ );
324
+ for (const seg of segments) {
325
+ if (!seenIds.has(seg.id)) {
326
+ seenIds.add(seg.id);
327
+ allLoaderSegments.push(seg);
328
+ }
329
+ }
330
+ allMatchedIds.push(...matchedIds);
331
+ }
332
+
333
+ const seenParallelEntryIds = new Set<string>();
334
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
335
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
336
+ seenParallelEntryIds.add(parallelEntry.id);
337
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
338
+ }
339
+
340
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
341
+ for (const layoutEntry of entry.layout) {
342
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
343
+ // Inherit route loaders for orphan layouts with parallels.
344
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
345
+ // route entry, as that would re-iterate route.layout and loop.
346
+ if (
347
+ entry.type === "route" &&
348
+ entry.loader &&
349
+ entry.loader.length > 0 &&
350
+ Object.keys(layoutEntry.parallel).length > 0
351
+ ) {
352
+ const inherited = await resolveLoadersWithRevalidation(
353
+ entry,
354
+ context,
355
+ childBelongsToRoute,
356
+ clientSegmentIds,
357
+ prevParams,
358
+ request,
359
+ prevUrl,
360
+ nextUrl,
361
+ routeKey,
362
+ deps,
363
+ actionContext,
364
+ layoutEntry.shortCode,
365
+ stale,
366
+ );
367
+ for (const seg of inherited.segments) {
368
+ if (!seenIds.has(seg.id)) {
369
+ seenIds.add(seg.id);
370
+ seg._inherited = true;
371
+ allLoaderSegments.push(seg);
372
+ }
373
+ }
374
+ allMatchedIds.push(...inherited.matchedIds);
375
+ }
376
+ }
377
+ }
261
378
 
262
379
  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);
380
+ await collectEntryLoaders(entry, entry.type === "route");
281
381
  }
282
382
 
283
383
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -301,22 +401,20 @@ export function buildEntryRevalidateMap(
301
401
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
302
402
 
303
403
  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
- }
404
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
405
+ entry.parallel,
406
+ )) {
407
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
408
+ const parallelId = `${parallelParentShortCode}.${slot}`;
409
+ map.set(parallelId, {
410
+ entry: parallelEntry,
411
+ revalidate: parallelEntry.revalidate,
412
+ });
315
413
  }
316
414
  }
317
415
 
318
416
  for (const layoutEntry of entry.layout) {
319
- processEntry(layoutEntry);
417
+ processEntry(layoutEntry, entry.shortCode);
320
418
  }
321
419
  }
322
420
 
@@ -348,7 +446,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
348
446
  const segments: ResolvedSegment[] = [];
349
447
  const matchedIds: string[] = [];
350
448
 
351
- for (const parallelEntry of entry.parallel) {
449
+ const resolvedParallelEntries = new Set<string>();
450
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
451
+ entry.parallel,
452
+ )) {
352
453
  invariant(
353
454
  parallelEntry.type === "parallel",
354
455
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -359,141 +460,47 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
359
460
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
360
461
  | ReactNode
361
462
  >;
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);
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
- }
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
- : {}),
463
+ // In production, static handler bodies are evicted and the slot value
464
+ // may be undefined. The static store holds the pre-rendered component.
465
+ // We defer the handler check until after tryStaticSlot.
466
+ const handler = slots[slot];
467
+
468
+ const parallelId = `${entry.shortCode}.${slot}`;
469
+
470
+ const isFullRefetch = clientSegmentIds.size === 0;
471
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
472
+ // Always announce the slot in matchedIds — it's unconditionally appended
473
+ // to `segments` below, and a segment present in segments but missing from
474
+ // matched lets the client prune it (then it's missing from clientSegmentIds
475
+ // on the next request, perpetuating the staleness).
476
+ matchedIds.push(parallelId);
477
+
478
+ let shouldResolve: boolean;
479
+ if (isFullRefetch) {
480
+ // Client has nothing cached — slot MUST render. User revalidate fns are
481
+ // bypassed here because returning false would leave the segment blank
482
+ // with no client-side fallback.
483
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
484
+ shouldResolve = true;
485
+ } else {
486
+ // For non-empty client sets, consult user revalidate fns. When the slot
487
+ // is unknown to the client, override the type-derived default so the
488
+ // soft chain seeds with the right "new segment" / "parent-chain" value.
489
+ let defaultOverride: { value: boolean; reason: string } | undefined;
490
+ if (!clientSegmentIds.has(parallelId)) {
491
+ const value = belongsToRoute || isNewParent;
492
+ defaultOverride = {
493
+ value,
494
+ reason: value ? "new-segment" : "skip-parent-chain",
426
495
  };
427
-
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,
449
- routeKey,
450
- shouldResolve,
451
- );
452
-
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
- }
487
496
  }
488
497
 
489
- segments.push({
498
+ const dummySegment: ResolvedSegment = {
490
499
  id: parallelId,
491
500
  namespace: parallelEntry.id,
492
501
  type: "parallel",
493
502
  index: 0,
494
- component,
495
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
496
- transition: parallelEntry.transition,
503
+ component: null as any,
497
504
  params,
498
505
  slot,
499
506
  belongsToRoute,
@@ -501,28 +508,112 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
501
508
  ...(parallelEntry.mountPath
502
509
  ? { mountPath: parallelEntry.mountPath }
503
510
  : {}),
504
- });
505
- }
511
+ };
506
512
 
507
- if (!parallelEntry.loading) {
508
- const loaderResult = await resolveLoadersWithRevalidation(
509
- parallelEntry,
510
- context,
511
- belongsToRoute,
512
- clientSegmentIds,
513
+ shouldResolve = await evaluateRevalidation({
514
+ segment: dummySegment,
513
515
  prevParams,
516
+ getPrevSegment: null,
514
517
  request,
515
518
  prevUrl,
516
519
  nextUrl,
520
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
521
+ name: `revalidate${i}`,
522
+ fn,
523
+ })),
517
524
  routeKey,
518
- deps,
525
+ context,
519
526
  actionContext,
520
- entry.shortCode,
521
527
  stale,
522
- );
523
- segments.push(...loaderResult.segments);
524
- matchedIds.push(...loaderResult.matchedIds);
528
+ traceSource: "parallel",
529
+ defaultOverride,
530
+ });
531
+ }
532
+ emitRevalidationDecision(
533
+ parallelId,
534
+ context.pathname,
535
+ routeKey,
536
+ shouldResolve,
537
+ );
538
+
539
+ let component: ReactNode | undefined;
540
+ if (shouldResolve) {
541
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
542
+ }
543
+ if (component === undefined) {
544
+ const hasLoadingFallback =
545
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
546
+ if (!shouldResolve) {
547
+ component = null;
548
+ } else if (handler === undefined) {
549
+ // Handler evicted (production static slot) but static lookup missed.
550
+ // Nothing to render — use null so the client keeps its cached version.
551
+ component = null;
552
+ } else if (hasLoadingFallback) {
553
+ const result =
554
+ typeof handler === "function" ? handler(context) : handler;
555
+ if (result instanceof Promise) {
556
+ const tracked = deps.trackHandler(result, {
557
+ segmentId: parallelId,
558
+ segmentType: "parallel",
559
+ });
560
+ observeStreamedHandler(
561
+ tracked,
562
+ parallelId,
563
+ "parallel",
564
+ context.pathname,
565
+ routeKey,
566
+ params,
567
+ );
568
+ component = tracked as ReactNode;
569
+ } else {
570
+ component = result as ReactNode;
571
+ }
572
+ } else {
573
+ component =
574
+ typeof handler === "function" ? await handler(context) : handler;
575
+ }
525
576
  }
577
+
578
+ segments.push({
579
+ id: parallelId,
580
+ namespace: parallelEntry.id,
581
+ type: "parallel",
582
+ index: 0,
583
+ component,
584
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
585
+ transition: parallelEntry.transition,
586
+ params,
587
+ slot,
588
+ belongsToRoute,
589
+ parallelName: `${parallelEntry.id}.${slot}`,
590
+ ...(parallelEntry.mountPath
591
+ ? { mountPath: parallelEntry.mountPath }
592
+ : {}),
593
+ });
594
+
595
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
596
+ continue;
597
+ }
598
+
599
+ const loaderResult = await resolveLoadersWithRevalidation(
600
+ parallelEntry,
601
+ context,
602
+ belongsToRoute,
603
+ clientSegmentIds,
604
+ prevParams,
605
+ request,
606
+ prevUrl,
607
+ nextUrl,
608
+ routeKey,
609
+ deps,
610
+ actionContext,
611
+ entry.shortCode,
612
+ stale,
613
+ );
614
+ segments.push(...loaderResult.segments);
615
+ matchedIds.push(...loaderResult.matchedIds);
616
+ resolvedParallelEntries.add(parallelEntry.id);
526
617
  }
527
618
 
528
619
  return { segments, matchedIds };
@@ -608,6 +699,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
608
699
  context,
609
700
  actionContext,
610
701
  stale,
702
+ traceSource:
703
+ entry.type === "route" ? "route-handler" : "layout-handler",
611
704
  });
612
705
  emitRevalidationDecision(
613
706
  entry.shortCode,
@@ -636,13 +729,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
636
729
  return staticComponent;
637
730
  }
638
731
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
732
+ // For Passthrough routes at runtime, use the live handler instead of
733
+ // the build handler. At build time (context.build === true), always
734
+ // use the build handler from routeEntry.handler.
735
+ const handler =
736
+ !context.build && routeEntry.liveHandler
737
+ ? routeEntry.liveHandler
738
+ : routeEntry.handler;
639
739
  if (!routeEntry.loading) {
640
- const result = handleHandlerResult(await routeEntry.handler(context));
740
+ const result = handleHandlerResult(await handler(context));
641
741
  doneHandler();
642
742
  return result;
643
743
  }
644
744
  if (!actionContext) {
645
- const result = handleHandlerResult(routeEntry.handler(context));
745
+ const result = handleHandlerResult(handler(context));
646
746
  if (result instanceof Promise) {
647
747
  result.finally(doneHandler).catch(() => {});
648
748
  const tracked = deps.trackHandler(result, {
@@ -665,9 +765,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
665
765
  debugLog("segment.action", "resolving action route with awaited value", {
666
766
  entryId: entry.id,
667
767
  });
668
- const actionResult = handleHandlerResult(
669
- await routeEntry.handler(context),
670
- );
768
+ const actionResult = handleHandlerResult(await handler(context));
671
769
  doneHandler();
672
770
  return {
673
771
  content: Promise.resolve(actionResult),
@@ -676,10 +774,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
676
774
  () => null,
677
775
  );
678
776
 
777
+ // Normalize void handlers (undefined) to null so the reconciler's
778
+ // component === null checks work consistently for both void and explicit null.
679
779
  const resolvedComponent =
680
780
  component && typeof component === "object" && "content" in component
681
- ? (component as { content: ReactNode }).content
682
- : component;
781
+ ? ((component as { content: ReactNode }).content ?? null)
782
+ : (component ?? null);
683
783
 
684
784
  const segment: ResolvedSegment = {
685
785
  id: entry.shortCode,
@@ -776,11 +876,11 @@ export async function resolveSegmentWithRevalidation<TEnv>(
776
876
  prevUrl,
777
877
  nextUrl,
778
878
  routeKey,
779
- loaderPromises,
780
879
  true,
781
880
  deps,
782
881
  actionContext,
783
882
  stale,
883
+ entry,
784
884
  );
785
885
  segments.push(...orphanResult.segments);
786
886
  matchedIds.push(...orphanResult.matchedIds);
@@ -860,7 +960,6 @@ export async function resolveSegmentWithRevalidation<TEnv>(
860
960
  prevUrl,
861
961
  nextUrl,
862
962
  routeKey,
863
- loaderPromises,
864
963
  false,
865
964
  deps,
866
965
  actionContext,
@@ -887,11 +986,12 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
887
986
  prevUrl: URL,
888
987
  nextUrl: URL,
889
988
  routeKey: string,
890
- loaderPromises: Map<string, Promise<any>>,
891
989
  belongsToRoute: boolean,
892
990
  deps: SegmentResolutionDeps<TEnv>,
893
991
  actionContext?: ActionContext,
894
992
  stale?: boolean,
993
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
994
+ parentRouteEntry?: EntryData,
895
995
  ): Promise<SegmentRevalidationResult> {
896
996
  invariant(
897
997
  orphan.type === "layout" || orphan.type === "cache",
@@ -919,6 +1019,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
919
1019
  segments.push(...loaderResult.segments);
920
1020
  matchedIds.push(...loaderResult.matchedIds);
921
1021
 
1022
+ // Inherit parent route's loaders so parallel slots inside this layout
1023
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1024
+ if (
1025
+ parentRouteEntry &&
1026
+ parentRouteEntry.loader &&
1027
+ parentRouteEntry.loader.length > 0 &&
1028
+ Object.keys(orphan.parallel).length > 0
1029
+ ) {
1030
+ const inheritedResult = await resolveLoadersWithRevalidation(
1031
+ parentRouteEntry,
1032
+ context,
1033
+ belongsToRoute,
1034
+ clientSegmentIds,
1035
+ prevParams,
1036
+ request,
1037
+ prevUrl,
1038
+ nextUrl,
1039
+ routeKey,
1040
+ deps,
1041
+ actionContext,
1042
+ orphan.shortCode,
1043
+ stale,
1044
+ );
1045
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1046
+ for (const s of inheritedResult.segments) {
1047
+ s._inherited = true;
1048
+ }
1049
+ segments.push(...inheritedResult.segments);
1050
+ matchedIds.push(...inheritedResult.matchedIds);
1051
+ }
1052
+
922
1053
  // Handler-first: resolve orphan layout handler before its parallels
923
1054
  // so ctx.set() values are visible to parallel children.
924
1055
  matchedIds.push(orphan.shortCode);
@@ -995,143 +1126,72 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
995
1126
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
996
1127
  });
997
1128
 
998
- for (const parallelEntry of orphan.parallel) {
1129
+ const resolvedParallelEntries = new Set<string>();
1130
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1131
+ orphan.parallel,
1132
+ )) {
999
1133
  invariant(
1000
1134
  parallelEntry.type === "parallel",
1001
1135
  `Expected parallel entry, got: ${parallelEntry.type}`,
1002
1136
  );
1003
1137
 
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);
1138
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1139
+ // shortCodeOverride must match the parent layout, not the parallel entry.
1140
+ const loaderResult = await resolveLoadersWithRevalidation(
1141
+ parallelEntry,
1142
+ context,
1143
+ belongsToRoute,
1144
+ clientSegmentIds,
1145
+ prevParams,
1146
+ request,
1147
+ prevUrl,
1148
+ nextUrl,
1149
+ routeKey,
1150
+ deps,
1151
+ actionContext,
1152
+ orphan.shortCode,
1153
+ stale,
1154
+ );
1155
+ segments.push(...loaderResult.segments);
1156
+ matchedIds.push(...loaderResult.matchedIds);
1157
+ resolvedParallelEntries.add(parallelEntry.id);
1158
+ }
1021
1159
 
1022
1160
  const slots = parallelEntry.handler as Record<
1023
1161
  `@${string}`,
1024
1162
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1025
1163
  | ReactNode
1026
1164
  >;
1165
+ // Handler may be undefined in production after static handler eviction.
1166
+ const handler = slots[slot];
1167
+
1168
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1169
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1170
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1171
+ const parallelId = `${orphan.shortCode}.${slot}`;
1172
+ matchedIds.push(parallelId);
1173
+
1174
+ const isFullRefetch = clientSegmentIds.size === 0;
1175
+ let shouldResolve: boolean;
1176
+ if (isFullRefetch) {
1177
+ // Same load-bearing rationale as the main parallel path: full refetch
1178
+ // means the client has nothing to fall back to, so the slot must render.
1179
+ traceFullRefetchedParallelSlot(parallelId, belongsToRoute);
1180
+ shouldResolve = true;
1181
+ } else {
1182
+ // When slot is unknown to the client, seed the soft chain with `true`
1183
+ // (orphan parallels always belong to the route — we want them rendered
1184
+ // unless the user explicitly opts out via revalidate()).
1185
+ const defaultOverride = clientSegmentIds.has(parallelId)
1186
+ ? undefined
1187
+ : { value: true, reason: "new-segment" };
1027
1188
 
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({
1189
+ const dummySegment: ResolvedSegment = {
1128
1190
  id: parallelId,
1129
1191
  namespace: parallelEntry.id,
1130
1192
  type: "parallel",
1131
1193
  index: 0,
1132
- component,
1133
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1134
- transition: parallelEntry.transition,
1194
+ component: null as any,
1135
1195
  params,
1136
1196
  slot,
1137
1197
  belongsToRoute,
@@ -1139,8 +1199,88 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1139
1199
  ...(parallelEntry.mountPath
1140
1200
  ? { mountPath: parallelEntry.mountPath }
1141
1201
  : {}),
1202
+ };
1203
+
1204
+ shouldResolve = await evaluateRevalidation({
1205
+ segment: dummySegment,
1206
+ prevParams,
1207
+ getPrevSegment: null,
1208
+ request,
1209
+ prevUrl,
1210
+ nextUrl,
1211
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1212
+ name: `revalidate${i}`,
1213
+ fn,
1214
+ })),
1215
+ routeKey,
1216
+ context,
1217
+ actionContext,
1218
+ stale,
1219
+ traceSource: "parallel",
1220
+ defaultOverride,
1142
1221
  });
1143
1222
  }
1223
+ emitRevalidationDecision(
1224
+ parallelId,
1225
+ context.pathname,
1226
+ routeKey,
1227
+ shouldResolve,
1228
+ );
1229
+
1230
+ let component: ReactNode | undefined;
1231
+ if (shouldResolve) {
1232
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1233
+ }
1234
+ if (component === undefined) {
1235
+ const hasLoadingFallback =
1236
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1237
+ if (!shouldResolve) {
1238
+ component = null;
1239
+ } else if (handler === undefined) {
1240
+ // Handler evicted (production static slot) but static lookup missed.
1241
+ component = null;
1242
+ } else if (hasLoadingFallback) {
1243
+ const result =
1244
+ typeof handler === "function" ? handler(context) : handler;
1245
+ if (result instanceof Promise) {
1246
+ const tracked = deps.trackHandler(result, {
1247
+ segmentId: parallelId,
1248
+ segmentType: "parallel",
1249
+ });
1250
+ observeStreamedHandler(
1251
+ tracked,
1252
+ parallelId,
1253
+ "parallel",
1254
+ context.pathname,
1255
+ routeKey,
1256
+ params,
1257
+ );
1258
+ component = tracked as ReactNode;
1259
+ } else {
1260
+ component = result as ReactNode;
1261
+ }
1262
+ } else {
1263
+ component =
1264
+ typeof handler === "function" ? await handler(context) : handler;
1265
+ }
1266
+ }
1267
+
1268
+ segments.push({
1269
+ id: parallelId,
1270
+ namespace: parallelEntry.id,
1271
+ type: "parallel",
1272
+ index: 0,
1273
+ component,
1274
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1275
+ transition: parallelEntry.transition,
1276
+ params,
1277
+ slot,
1278
+ belongsToRoute,
1279
+ parallelName: `${parallelEntry.id}.${slot}`,
1280
+ ...(parallelEntry.mountPath
1281
+ ? { mountPath: parallelEntry.mountPath }
1282
+ : {}),
1283
+ });
1144
1284
  }
1145
1285
 
1146
1286
  return { segments, matchedIds };
@@ -1165,6 +1305,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1165
1305
  localRouteName: string,
1166
1306
  pathname: string,
1167
1307
  deps: SegmentResolutionDeps<TEnv>,
1308
+ stale?: boolean,
1168
1309
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1169
1310
  const allSegments: ResolvedSegment[] = [];
1170
1311
  const matchedIds: string[] = [];
@@ -1191,6 +1332,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1191
1332
  }
1192
1333
 
1193
1334
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1335
+ if (entry.type === "cache") {
1336
+ const store = RSCRouterContext.getStore();
1337
+ if (store) store.insideCacheScope = true;
1338
+ }
1194
1339
  const doneEntry = track(`segment:${entry.id}`, 1);
1195
1340
  const resolved = await resolveWithErrorBoundary(
1196
1341
  nonParallelEntry,
@@ -1209,7 +1354,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1209
1354
  loaderPromises,
1210
1355
  deps,
1211
1356
  actionContext,
1212
- false,
1357
+ stale,
1213
1358
  ),
1214
1359
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1215
1360
  deps,