@rangojs/router 0.0.0-experimental.8a4d0430 → 0.0.0-experimental.8bcfea43

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 (174) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +126 -38
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +1171 -461
  5. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  6. package/package.json +19 -16
  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 +88 -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 +185 -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 +90 -16
  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 +113 -17
  36. package/src/browser/prefetch/cache.ts +184 -16
  37. package/src/browser/prefetch/fetch.ts +180 -33
  38. package/src/browser/prefetch/policy.ts +6 -0
  39. package/src/browser/prefetch/queue.ts +123 -20
  40. package/src/browser/prefetch/resource-ready.ts +77 -0
  41. package/src/browser/rango-state.ts +53 -13
  42. package/src/browser/react/Link.tsx +81 -9
  43. package/src/browser/react/NavigationProvider.tsx +89 -14
  44. package/src/browser/react/context.ts +7 -2
  45. package/src/browser/react/use-handle.ts +9 -58
  46. package/src/browser/react/use-navigation.ts +22 -2
  47. package/src/browser/react/use-params.ts +11 -1
  48. package/src/browser/react/use-router.ts +29 -9
  49. package/src/browser/rsc-router.tsx +168 -65
  50. package/src/browser/scroll-restoration.ts +41 -42
  51. package/src/browser/segment-reconciler.ts +36 -9
  52. package/src/browser/server-action-bridge.ts +8 -6
  53. package/src/browser/types.ts +49 -5
  54. package/src/build/generate-manifest.ts +6 -6
  55. package/src/build/generate-route-types.ts +3 -0
  56. package/src/build/route-trie.ts +50 -24
  57. package/src/build/route-types/include-resolution.ts +8 -1
  58. package/src/build/route-types/router-processing.ts +223 -74
  59. package/src/build/route-types/scan-filter.ts +8 -1
  60. package/src/cache/cache-runtime.ts +15 -11
  61. package/src/cache/cache-scope.ts +48 -7
  62. package/src/cache/cf/cf-cache-store.ts +455 -15
  63. package/src/cache/cf/index.ts +5 -1
  64. package/src/cache/document-cache.ts +17 -7
  65. package/src/cache/index.ts +1 -0
  66. package/src/cache/taint.ts +55 -0
  67. package/src/client.tsx +84 -230
  68. package/src/context-var.ts +72 -2
  69. package/src/debug.ts +2 -2
  70. package/src/handle.ts +40 -0
  71. package/src/index.rsc.ts +6 -1
  72. package/src/index.ts +49 -6
  73. package/src/outlet-context.ts +1 -1
  74. package/src/prerender/store.ts +5 -4
  75. package/src/prerender.ts +138 -77
  76. package/src/response-utils.ts +28 -0
  77. package/src/reverse.ts +27 -2
  78. package/src/route-definition/dsl-helpers.ts +240 -40
  79. package/src/route-definition/helpers-types.ts +67 -19
  80. package/src/route-definition/index.ts +3 -0
  81. package/src/route-definition/redirect.ts +11 -3
  82. package/src/route-definition/resolve-handler-use.ts +155 -0
  83. package/src/route-map-builder.ts +7 -1
  84. package/src/route-types.ts +18 -0
  85. package/src/router/content-negotiation.ts +100 -1
  86. package/src/router/find-match.ts +4 -2
  87. package/src/router/handler-context.ts +101 -25
  88. package/src/router/intercept-resolution.ts +11 -4
  89. package/src/router/lazy-includes.ts +10 -7
  90. package/src/router/loader-resolution.ts +159 -21
  91. package/src/router/logging.ts +5 -2
  92. package/src/router/manifest.ts +31 -16
  93. package/src/router/match-api.ts +127 -192
  94. package/src/router/match-middleware/background-revalidation.ts +30 -2
  95. package/src/router/match-middleware/cache-lookup.ts +94 -17
  96. package/src/router/match-middleware/cache-store.ts +53 -10
  97. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  98. package/src/router/match-middleware/segment-resolution.ts +61 -5
  99. package/src/router/match-result.ts +104 -10
  100. package/src/router/metrics.ts +6 -1
  101. package/src/router/middleware-types.ts +8 -30
  102. package/src/router/middleware.ts +36 -10
  103. package/src/router/navigation-snapshot.ts +182 -0
  104. package/src/router/pattern-matching.ts +60 -9
  105. package/src/router/prerender-match.ts +110 -10
  106. package/src/router/preview-match.ts +30 -102
  107. package/src/router/request-classification.ts +310 -0
  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 +438 -300
  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 +19 -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 +137 -65
  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 +55 -0
  153. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  154. package/src/vite/discovery/discover-routers.ts +5 -1
  155. package/src/vite/discovery/prerender-collection.ts +128 -74
  156. package/src/vite/discovery/state.ts +13 -6
  157. package/src/vite/index.ts +4 -0
  158. package/src/vite/plugin-types.ts +51 -79
  159. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  160. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  161. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  162. package/src/vite/plugins/expose-action-id.ts +1 -3
  163. package/src/vite/plugins/expose-id-utils.ts +12 -0
  164. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  165. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  166. package/src/vite/plugins/performance-tracks.ts +86 -0
  167. package/src/vite/plugins/refresh-cmd.ts +88 -26
  168. package/src/vite/plugins/version-plugin.ts +13 -1
  169. package/src/vite/rango.ts +204 -217
  170. package/src/vite/router-discovery.ts +335 -64
  171. package/src/vite/utils/banner.ts +4 -4
  172. package/src/vite/utils/package-resolution.ts +41 -1
  173. package/src/vite/utils/prerender-utils.ts +37 -5
  174. 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
@@ -228,7 +236,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
228
236
  params: ctx.params,
229
237
  loaderId: loader.$$id,
230
238
  loaderData: deps.wrapLoaderPromise(
231
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
239
+ runInsideLoaderScope(() =>
240
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
241
+ ),
232
242
  entry,
233
243
  segmentId,
234
244
  ctx.pathname,
@@ -258,26 +268,95 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
258
268
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
259
269
  const allLoaderSegments: ResolvedSegment[] = [];
260
270
  const allMatchedIds: string[] = [];
271
+ const seenIds = new Set<string>();
272
+
273
+ async function collectEntryLoaders(
274
+ entry: EntryData,
275
+ belongsToRoute: boolean,
276
+ shortCodeOverride?: string,
277
+ ): Promise<void> {
278
+ // Skip if all loaders from this entry have already been resolved
279
+ // via a parent (e.g., cache boundary wrapping a layout with shared loaders).
280
+ const loaderEntries = entry.loader ?? [];
281
+ const sc = shortCodeOverride ?? entry.shortCode;
282
+ const allAlreadySeen =
283
+ loaderEntries.length > 0 &&
284
+ loaderEntries.every((le, i) =>
285
+ seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
286
+ );
287
+ if (!allAlreadySeen) {
288
+ const { segments, matchedIds } = await resolveLoadersWithRevalidation(
289
+ entry,
290
+ context,
291
+ belongsToRoute,
292
+ clientSegmentIds,
293
+ prevParams,
294
+ request,
295
+ prevUrl,
296
+ nextUrl,
297
+ routeKey,
298
+ deps,
299
+ actionContext,
300
+ shortCodeOverride,
301
+ stale,
302
+ );
303
+ for (const seg of segments) {
304
+ if (!seenIds.has(seg.id)) {
305
+ seenIds.add(seg.id);
306
+ allLoaderSegments.push(seg);
307
+ }
308
+ }
309
+ allMatchedIds.push(...matchedIds);
310
+ }
311
+
312
+ const seenParallelEntryIds = new Set<string>();
313
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
314
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
315
+ seenParallelEntryIds.add(parallelEntry.id);
316
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
317
+ }
318
+
319
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
320
+ for (const layoutEntry of entry.layout) {
321
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
322
+ // Inherit route loaders for orphan layouts with parallels.
323
+ // Resolve directly — do NOT re-enter collectEntryLoaders with the
324
+ // route entry, as that would re-iterate route.layout and loop.
325
+ if (
326
+ entry.type === "route" &&
327
+ entry.loader &&
328
+ entry.loader.length > 0 &&
329
+ Object.keys(layoutEntry.parallel).length > 0
330
+ ) {
331
+ const inherited = await resolveLoadersWithRevalidation(
332
+ entry,
333
+ context,
334
+ childBelongsToRoute,
335
+ clientSegmentIds,
336
+ prevParams,
337
+ request,
338
+ prevUrl,
339
+ nextUrl,
340
+ routeKey,
341
+ deps,
342
+ actionContext,
343
+ layoutEntry.shortCode,
344
+ stale,
345
+ );
346
+ for (const seg of inherited.segments) {
347
+ if (!seenIds.has(seg.id)) {
348
+ seenIds.add(seg.id);
349
+ seg._inherited = true;
350
+ allLoaderSegments.push(seg);
351
+ }
352
+ }
353
+ allMatchedIds.push(...inherited.matchedIds);
354
+ }
355
+ }
356
+ }
261
357
 
262
358
  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);
359
+ await collectEntryLoaders(entry, entry.type === "route");
281
360
  }
282
361
 
283
362
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -301,22 +380,20 @@ export function buildEntryRevalidateMap(
301
380
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
302
381
 
303
382
  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
- }
383
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
384
+ entry.parallel,
385
+ )) {
386
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
387
+ const parallelId = `${parallelParentShortCode}.${slot}`;
388
+ map.set(parallelId, {
389
+ entry: parallelEntry,
390
+ revalidate: parallelEntry.revalidate,
391
+ });
315
392
  }
316
393
  }
317
394
 
318
395
  for (const layoutEntry of entry.layout) {
319
- processEntry(layoutEntry);
396
+ processEntry(layoutEntry, entry.shortCode);
320
397
  }
321
398
  }
322
399
 
@@ -348,7 +425,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
348
425
  const segments: ResolvedSegment[] = [];
349
426
  const matchedIds: string[] = [];
350
427
 
351
- for (const parallelEntry of entry.parallel) {
428
+ const resolvedParallelEntries = new Set<string>();
429
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
430
+ entry.parallel,
431
+ )) {
352
432
  invariant(
353
433
  parallelEntry.type === "parallel",
354
434
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -359,141 +439,61 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
359
439
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
360
440
  | ReactNode
361
441
  >;
442
+ // In production, static handler bodies are evicted and the slot value
443
+ // may be undefined. The static store holds the pre-rendered component.
444
+ // We defer the handler check until after tryStaticSlot.
445
+ const handler = slots[slot];
446
+
447
+ const parallelId = `${entry.shortCode}.${slot}`;
448
+
449
+ const isFullRefetch = clientSegmentIds.size === 0;
450
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
451
+ if (
452
+ isFullRefetch ||
453
+ clientSegmentIds.has(parallelId) ||
454
+ belongsToRoute ||
455
+ isNewParent
456
+ ) {
457
+ matchedIds.push(parallelId);
458
+ }
362
459
 
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;
460
+ const shouldResolve = await (async () => {
461
+ if (isFullRefetch) {
462
+ if (isTraceActive()) {
463
+ pushRevalidationTraceEntry({
464
+ segmentId: parallelId,
465
+ segmentType: "parallel",
466
+ belongsToRoute,
467
+ source: "parallel",
468
+ defaultShouldRevalidate: true,
469
+ finalShouldRevalidate: true,
470
+ reason: "full-refetch",
471
+ });
411
472
  }
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
- };
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);
473
+ return true;
456
474
  }
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;
475
+ if (!clientSegmentIds.has(parallelId)) {
476
+ const result = belongsToRoute || isNewParent;
477
+ if (isTraceActive()) {
478
+ pushRevalidationTraceEntry({
479
+ segmentId: parallelId,
480
+ segmentType: "parallel",
481
+ belongsToRoute,
482
+ source: "parallel",
483
+ defaultShouldRevalidate: result,
484
+ finalShouldRevalidate: result,
485
+ reason: result ? "new-segment" : "skip-parent-chain",
486
+ });
486
487
  }
488
+ return result;
487
489
  }
488
490
 
489
- segments.push({
491
+ const dummySegment: ResolvedSegment = {
490
492
  id: parallelId,
491
493
  namespace: parallelEntry.id,
492
494
  type: "parallel",
493
495
  index: 0,
494
- component,
495
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
496
- transition: parallelEntry.transition,
496
+ component: null as any,
497
497
  params,
498
498
  slot,
499
499
  belongsToRoute,
@@ -501,28 +501,111 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
501
501
  ...(parallelEntry.mountPath
502
502
  ? { mountPath: parallelEntry.mountPath }
503
503
  : {}),
504
- });
505
- }
504
+ };
506
505
 
507
- if (!parallelEntry.loading) {
508
- const loaderResult = await resolveLoadersWithRevalidation(
509
- parallelEntry,
510
- context,
511
- belongsToRoute,
512
- clientSegmentIds,
506
+ return await evaluateRevalidation({
507
+ segment: dummySegment,
513
508
  prevParams,
509
+ getPrevSegment: null,
514
510
  request,
515
511
  prevUrl,
516
512
  nextUrl,
513
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
514
+ name: `revalidate${i}`,
515
+ fn,
516
+ })),
517
517
  routeKey,
518
- deps,
518
+ context,
519
519
  actionContext,
520
- entry.shortCode,
521
520
  stale,
522
- );
523
- segments.push(...loaderResult.segments);
524
- matchedIds.push(...loaderResult.matchedIds);
521
+ traceSource: "parallel",
522
+ });
523
+ })();
524
+ emitRevalidationDecision(
525
+ parallelId,
526
+ context.pathname,
527
+ routeKey,
528
+ shouldResolve,
529
+ );
530
+
531
+ let component: ReactNode | undefined;
532
+ if (shouldResolve) {
533
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
534
+ }
535
+ if (component === undefined) {
536
+ const hasLoadingFallback =
537
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
538
+ if (!shouldResolve) {
539
+ component = null;
540
+ } else if (handler === undefined) {
541
+ // Handler evicted (production static slot) but static lookup missed.
542
+ // Nothing to render — use null so the client keeps its cached version.
543
+ component = null;
544
+ } else if (hasLoadingFallback) {
545
+ const result =
546
+ typeof handler === "function" ? handler(context) : handler;
547
+ if (result instanceof Promise) {
548
+ const tracked = deps.trackHandler(result, {
549
+ segmentId: parallelId,
550
+ segmentType: "parallel",
551
+ });
552
+ observeStreamedHandler(
553
+ tracked,
554
+ parallelId,
555
+ "parallel",
556
+ context.pathname,
557
+ routeKey,
558
+ params,
559
+ );
560
+ component = tracked as ReactNode;
561
+ } else {
562
+ component = result as ReactNode;
563
+ }
564
+ } else {
565
+ component =
566
+ typeof handler === "function" ? await handler(context) : handler;
567
+ }
525
568
  }
569
+
570
+ segments.push({
571
+ id: parallelId,
572
+ namespace: parallelEntry.id,
573
+ type: "parallel",
574
+ index: 0,
575
+ component,
576
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
577
+ transition: parallelEntry.transition,
578
+ params,
579
+ slot,
580
+ belongsToRoute,
581
+ parallelName: `${parallelEntry.id}.${slot}`,
582
+ ...(parallelEntry.mountPath
583
+ ? { mountPath: parallelEntry.mountPath }
584
+ : {}),
585
+ });
586
+
587
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
588
+ continue;
589
+ }
590
+
591
+ const loaderResult = await resolveLoadersWithRevalidation(
592
+ parallelEntry,
593
+ context,
594
+ belongsToRoute,
595
+ clientSegmentIds,
596
+ prevParams,
597
+ request,
598
+ prevUrl,
599
+ nextUrl,
600
+ routeKey,
601
+ deps,
602
+ actionContext,
603
+ entry.shortCode,
604
+ stale,
605
+ );
606
+ segments.push(...loaderResult.segments);
607
+ matchedIds.push(...loaderResult.matchedIds);
608
+ resolvedParallelEntries.add(parallelEntry.id);
526
609
  }
527
610
 
528
611
  return { segments, matchedIds };
@@ -608,6 +691,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
608
691
  context,
609
692
  actionContext,
610
693
  stale,
694
+ traceSource:
695
+ entry.type === "route" ? "route-handler" : "layout-handler",
611
696
  });
612
697
  emitRevalidationDecision(
613
698
  entry.shortCode,
@@ -636,13 +721,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
636
721
  return staticComponent;
637
722
  }
638
723
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
724
+ // For Passthrough routes at runtime, use the live handler instead of
725
+ // the build handler. At build time (context.build === true), always
726
+ // use the build handler from routeEntry.handler.
727
+ const handler =
728
+ !context.build && routeEntry.liveHandler
729
+ ? routeEntry.liveHandler
730
+ : routeEntry.handler;
639
731
  if (!routeEntry.loading) {
640
- const result = handleHandlerResult(await routeEntry.handler(context));
732
+ const result = handleHandlerResult(await handler(context));
641
733
  doneHandler();
642
734
  return result;
643
735
  }
644
736
  if (!actionContext) {
645
- const result = handleHandlerResult(routeEntry.handler(context));
737
+ const result = handleHandlerResult(handler(context));
646
738
  if (result instanceof Promise) {
647
739
  result.finally(doneHandler).catch(() => {});
648
740
  const tracked = deps.trackHandler(result, {
@@ -665,9 +757,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
665
757
  debugLog("segment.action", "resolving action route with awaited value", {
666
758
  entryId: entry.id,
667
759
  });
668
- const actionResult = handleHandlerResult(
669
- await routeEntry.handler(context),
670
- );
760
+ const actionResult = handleHandlerResult(await handler(context));
671
761
  doneHandler();
672
762
  return {
673
763
  content: Promise.resolve(actionResult),
@@ -676,10 +766,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
676
766
  () => null,
677
767
  );
678
768
 
769
+ // Normalize void handlers (undefined) to null so the reconciler's
770
+ // component === null checks work consistently for both void and explicit null.
679
771
  const resolvedComponent =
680
772
  component && typeof component === "object" && "content" in component
681
- ? (component as { content: ReactNode }).content
682
- : component;
773
+ ? ((component as { content: ReactNode }).content ?? null)
774
+ : (component ?? null);
683
775
 
684
776
  const segment: ResolvedSegment = {
685
777
  id: entry.shortCode,
@@ -781,6 +873,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
781
873
  deps,
782
874
  actionContext,
783
875
  stale,
876
+ entry,
784
877
  );
785
878
  segments.push(...orphanResult.segments);
786
879
  matchedIds.push(...orphanResult.matchedIds);
@@ -892,6 +985,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
892
985
  deps: SegmentResolutionDeps<TEnv>,
893
986
  actionContext?: ActionContext,
894
987
  stale?: boolean,
988
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
989
+ parentRouteEntry?: EntryData,
895
990
  ): Promise<SegmentRevalidationResult> {
896
991
  invariant(
897
992
  orphan.type === "layout" || orphan.type === "cache",
@@ -919,6 +1014,37 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
919
1014
  segments.push(...loaderResult.segments);
920
1015
  matchedIds.push(...loaderResult.matchedIds);
921
1016
 
1017
+ // Inherit parent route's loaders so parallel slots inside this layout
1018
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
1019
+ if (
1020
+ parentRouteEntry &&
1021
+ parentRouteEntry.loader &&
1022
+ parentRouteEntry.loader.length > 0 &&
1023
+ Object.keys(orphan.parallel).length > 0
1024
+ ) {
1025
+ const inheritedResult = await resolveLoadersWithRevalidation(
1026
+ parentRouteEntry,
1027
+ context,
1028
+ belongsToRoute,
1029
+ clientSegmentIds,
1030
+ prevParams,
1031
+ request,
1032
+ prevUrl,
1033
+ nextUrl,
1034
+ routeKey,
1035
+ deps,
1036
+ actionContext,
1037
+ orphan.shortCode,
1038
+ stale,
1039
+ );
1040
+ // Tag as inherited so buildMatchResult can deduplicate when safe
1041
+ for (const s of inheritedResult.segments) {
1042
+ s._inherited = true;
1043
+ }
1044
+ segments.push(...inheritedResult.segments);
1045
+ matchedIds.push(...inheritedResult.matchedIds);
1046
+ }
1047
+
922
1048
  // Handler-first: resolve orphan layout handler before its parallels
923
1049
  // so ctx.set() values are visible to parallel children.
924
1050
  matchedIds.push(orphan.shortCode);
@@ -995,143 +1121,73 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
995
1121
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
996
1122
  });
997
1123
 
998
- for (const parallelEntry of orphan.parallel) {
1124
+ const resolvedParallelEntries = new Set<string>();
1125
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1126
+ orphan.parallel,
1127
+ )) {
999
1128
  invariant(
1000
1129
  parallelEntry.type === "parallel",
1001
1130
  `Expected parallel entry, got: ${parallelEntry.type}`,
1002
1131
  );
1003
1132
 
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);
1133
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1134
+ // shortCodeOverride must match the parent layout, not the parallel entry.
1135
+ const loaderResult = await resolveLoadersWithRevalidation(
1136
+ parallelEntry,
1137
+ context,
1138
+ belongsToRoute,
1139
+ clientSegmentIds,
1140
+ prevParams,
1141
+ request,
1142
+ prevUrl,
1143
+ nextUrl,
1144
+ routeKey,
1145
+ deps,
1146
+ actionContext,
1147
+ orphan.shortCode,
1148
+ stale,
1149
+ );
1150
+ segments.push(...loaderResult.segments);
1151
+ matchedIds.push(...loaderResult.matchedIds);
1152
+ resolvedParallelEntries.add(parallelEntry.id);
1153
+ }
1021
1154
 
1022
1155
  const slots = parallelEntry.handler as Record<
1023
1156
  `@${string}`,
1024
1157
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1025
1158
  | ReactNode
1026
1159
  >;
1160
+ // Handler may be undefined in production after static handler eviction.
1161
+ const handler = slots[slot];
1027
1162
 
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);
1163
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1164
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1165
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1166
+ const parallelId = `${orphan.shortCode}.${slot}`;
1167
+ matchedIds.push(parallelId);
1034
1168
 
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;
1169
+ const shouldResolve = await (async () => {
1170
+ if (!clientSegmentIds.has(parallelId)) {
1171
+ if (isTraceActive()) {
1172
+ pushRevalidationTraceEntry({
1173
+ segmentId: parallelId,
1174
+ segmentType: "parallel",
1175
+ belongsToRoute,
1176
+ source: "parallel",
1177
+ defaultShouldRevalidate: true,
1178
+ finalShouldRevalidate: true,
1179
+ reason: "new-segment",
1180
+ });
1124
1181
  }
1182
+ return true;
1125
1183
  }
1126
1184
 
1127
- segments.push({
1185
+ const dummySegment: ResolvedSegment = {
1128
1186
  id: parallelId,
1129
1187
  namespace: parallelEntry.id,
1130
1188
  type: "parallel",
1131
1189
  index: 0,
1132
- component,
1133
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1134
- transition: parallelEntry.transition,
1190
+ component: null as any,
1135
1191
  params,
1136
1192
  slot,
1137
1193
  belongsToRoute,
@@ -1139,8 +1195,87 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1139
1195
  ...(parallelEntry.mountPath
1140
1196
  ? { mountPath: parallelEntry.mountPath }
1141
1197
  : {}),
1198
+ };
1199
+
1200
+ return await evaluateRevalidation({
1201
+ segment: dummySegment,
1202
+ prevParams,
1203
+ getPrevSegment: null,
1204
+ request,
1205
+ prevUrl,
1206
+ nextUrl,
1207
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1208
+ name: `revalidate${i}`,
1209
+ fn,
1210
+ })),
1211
+ routeKey,
1212
+ context,
1213
+ actionContext,
1214
+ stale,
1215
+ traceSource: "parallel",
1142
1216
  });
1217
+ })();
1218
+ emitRevalidationDecision(
1219
+ parallelId,
1220
+ context.pathname,
1221
+ routeKey,
1222
+ shouldResolve,
1223
+ );
1224
+
1225
+ let component: ReactNode | undefined;
1226
+ if (shouldResolve) {
1227
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1228
+ }
1229
+ if (component === undefined) {
1230
+ const hasLoadingFallback =
1231
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1232
+ if (!shouldResolve) {
1233
+ component = null;
1234
+ } else if (handler === undefined) {
1235
+ // Handler evicted (production static slot) but static lookup missed.
1236
+ component = null;
1237
+ } else if (hasLoadingFallback) {
1238
+ const result =
1239
+ typeof handler === "function" ? handler(context) : handler;
1240
+ if (result instanceof Promise) {
1241
+ const tracked = deps.trackHandler(result, {
1242
+ segmentId: parallelId,
1243
+ segmentType: "parallel",
1244
+ });
1245
+ observeStreamedHandler(
1246
+ tracked,
1247
+ parallelId,
1248
+ "parallel",
1249
+ context.pathname,
1250
+ routeKey,
1251
+ params,
1252
+ );
1253
+ component = tracked as ReactNode;
1254
+ } else {
1255
+ component = result as ReactNode;
1256
+ }
1257
+ } else {
1258
+ component =
1259
+ typeof handler === "function" ? await handler(context) : handler;
1260
+ }
1143
1261
  }
1262
+
1263
+ segments.push({
1264
+ id: parallelId,
1265
+ namespace: parallelEntry.id,
1266
+ type: "parallel",
1267
+ index: 0,
1268
+ component,
1269
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1270
+ transition: parallelEntry.transition,
1271
+ params,
1272
+ slot,
1273
+ belongsToRoute,
1274
+ parallelName: `${parallelEntry.id}.${slot}`,
1275
+ ...(parallelEntry.mountPath
1276
+ ? { mountPath: parallelEntry.mountPath }
1277
+ : {}),
1278
+ });
1144
1279
  }
1145
1280
 
1146
1281
  return { segments, matchedIds };
@@ -1165,6 +1300,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1165
1300
  localRouteName: string,
1166
1301
  pathname: string,
1167
1302
  deps: SegmentResolutionDeps<TEnv>,
1303
+ stale?: boolean,
1168
1304
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1169
1305
  const allSegments: ResolvedSegment[] = [];
1170
1306
  const matchedIds: string[] = [];
@@ -1191,6 +1327,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1191
1327
  }
1192
1328
 
1193
1329
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1330
+ if (entry.type === "cache") {
1331
+ const store = RSCRouterContext.getStore();
1332
+ if (store) store.insideCacheScope = true;
1333
+ }
1194
1334
  const doneEntry = track(`segment:${entry.id}`, 1);
1195
1335
  const resolved = await resolveWithErrorBoundary(
1196
1336
  nonParallelEntry,
@@ -1209,13 +1349,11 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1209
1349
  loaderPromises,
1210
1350
  deps,
1211
1351
  actionContext,
1212
- false,
1352
+ stale,
1213
1353
  ),
1214
1354
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1215
1355
  deps,
1216
- telemetry
1217
- ? { request, url: context.url, routeKey, isPartial: true, telemetry }
1218
- : undefined,
1356
+ { request, url: context.url, routeKey, isPartial: true, telemetry },
1219
1357
  pathname,
1220
1358
  );
1221
1359
  doneEntry();