@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2

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 (160) hide show
  1. package/{CLAUDE.md → AGENTS.md} +4 -0
  2. package/README.md +122 -30
  3. package/dist/bin/rango.js +245 -63
  4. package/dist/vite/index.js +859 -418
  5. package/package.json +3 -3
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +49 -8
  9. package/skills/document-cache/SKILL.md +2 -2
  10. package/skills/hooks/SKILL.md +33 -31
  11. package/skills/host-router/SKILL.md +218 -0
  12. package/skills/links/SKILL.md +3 -1
  13. package/skills/loader/SKILL.md +72 -22
  14. package/skills/middleware/SKILL.md +2 -0
  15. package/skills/parallel/SKILL.md +126 -0
  16. package/skills/prerender/SKILL.md +112 -70
  17. package/skills/rango/SKILL.md +0 -1
  18. package/skills/route/SKILL.md +34 -4
  19. package/skills/router-setup/SKILL.md +95 -5
  20. package/skills/typesafety/SKILL.md +35 -23
  21. package/src/__internal.ts +92 -0
  22. package/src/bin/rango.ts +18 -0
  23. package/src/browser/app-version.ts +14 -0
  24. package/src/browser/event-controller.ts +5 -0
  25. package/src/browser/link-interceptor.ts +4 -0
  26. package/src/browser/navigation-bridge.ts +114 -18
  27. package/src/browser/navigation-client.ts +126 -44
  28. package/src/browser/navigation-store.ts +43 -8
  29. package/src/browser/navigation-transaction.ts +11 -9
  30. package/src/browser/partial-update.ts +80 -15
  31. package/src/browser/prefetch/cache.ts +166 -27
  32. package/src/browser/prefetch/fetch.ts +52 -39
  33. package/src/browser/prefetch/policy.ts +6 -0
  34. package/src/browser/prefetch/queue.ts +92 -20
  35. package/src/browser/prefetch/resource-ready.ts +77 -0
  36. package/src/browser/react/Link.tsx +70 -14
  37. package/src/browser/react/NavigationProvider.tsx +40 -4
  38. package/src/browser/react/context.ts +7 -2
  39. package/src/browser/react/use-handle.ts +9 -58
  40. package/src/browser/react/use-router.ts +21 -8
  41. package/src/browser/rsc-router.tsx +143 -59
  42. package/src/browser/scroll-restoration.ts +41 -42
  43. package/src/browser/segment-reconciler.ts +6 -1
  44. package/src/browser/server-action-bridge.ts +454 -436
  45. package/src/browser/types.ts +60 -5
  46. package/src/build/generate-manifest.ts +6 -6
  47. package/src/build/generate-route-types.ts +5 -0
  48. package/src/build/route-trie.ts +19 -3
  49. package/src/build/route-types/include-resolution.ts +8 -1
  50. package/src/build/route-types/router-processing.ts +346 -87
  51. package/src/build/route-types/scan-filter.ts +8 -1
  52. package/src/cache/cache-runtime.ts +15 -11
  53. package/src/cache/cache-scope.ts +48 -7
  54. package/src/cache/cf/cf-cache-store.ts +453 -11
  55. package/src/cache/cf/index.ts +5 -1
  56. package/src/cache/document-cache.ts +17 -7
  57. package/src/cache/index.ts +1 -0
  58. package/src/cache/taint.ts +55 -0
  59. package/src/client.rsc.tsx +2 -1
  60. package/src/client.tsx +3 -102
  61. package/src/context-var.ts +72 -2
  62. package/src/debug.ts +2 -2
  63. package/src/handle.ts +40 -0
  64. package/src/handles/breadcrumbs.ts +66 -0
  65. package/src/handles/index.ts +1 -0
  66. package/src/host/index.ts +0 -3
  67. package/src/index.rsc.ts +8 -37
  68. package/src/index.ts +40 -66
  69. package/src/prerender/store.ts +57 -15
  70. package/src/prerender.ts +138 -77
  71. package/src/reverse.ts +22 -1
  72. package/src/route-definition/dsl-helpers.ts +73 -25
  73. package/src/route-definition/helpers-types.ts +10 -6
  74. package/src/route-definition/index.ts +3 -3
  75. package/src/route-definition/redirect.ts +11 -3
  76. package/src/route-definition/resolve-handler-use.ts +149 -0
  77. package/src/route-map-builder.ts +7 -1
  78. package/src/route-types.ts +11 -0
  79. package/src/router/content-negotiation.ts +100 -1
  80. package/src/router/find-match.ts +4 -2
  81. package/src/router/handler-context.ts +108 -25
  82. package/src/router/intercept-resolution.ts +11 -4
  83. package/src/router/lazy-includes.ts +4 -1
  84. package/src/router/loader-resolution.ts +123 -11
  85. package/src/router/logging.ts +5 -2
  86. package/src/router/manifest.ts +9 -3
  87. package/src/router/match-api.ts +125 -190
  88. package/src/router/match-middleware/background-revalidation.ts +30 -2
  89. package/src/router/match-middleware/cache-lookup.ts +88 -16
  90. package/src/router/match-middleware/cache-store.ts +53 -10
  91. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  92. package/src/router/match-middleware/segment-resolution.ts +61 -5
  93. package/src/router/match-result.ts +22 -15
  94. package/src/router/metrics.ts +238 -13
  95. package/src/router/middleware-types.ts +53 -12
  96. package/src/router/middleware.ts +172 -85
  97. package/src/router/navigation-snapshot.ts +182 -0
  98. package/src/router/pattern-matching.ts +20 -5
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/revalidation.ts +27 -7
  103. package/src/router/route-snapshot.ts +245 -0
  104. package/src/router/router-context.ts +6 -1
  105. package/src/router/router-interfaces.ts +50 -5
  106. package/src/router/router-options.ts +50 -19
  107. package/src/router/segment-resolution/fresh.ts +200 -19
  108. package/src/router/segment-resolution/helpers.ts +30 -25
  109. package/src/router/segment-resolution/loader-cache.ts +1 -0
  110. package/src/router/segment-resolution/revalidation.ts +429 -301
  111. package/src/router/segment-wrappers.ts +2 -0
  112. package/src/router/trie-matching.ts +20 -2
  113. package/src/router/types.ts +1 -0
  114. package/src/router.ts +88 -15
  115. package/src/rsc/handler.ts +546 -359
  116. package/src/rsc/index.ts +0 -20
  117. package/src/rsc/manifest-init.ts +5 -1
  118. package/src/rsc/progressive-enhancement.ts +25 -8
  119. package/src/rsc/rsc-rendering.ts +35 -43
  120. package/src/rsc/server-action.ts +16 -10
  121. package/src/rsc/ssr-setup.ts +128 -0
  122. package/src/rsc/types.ts +10 -1
  123. package/src/search-params.ts +16 -13
  124. package/src/segment-system.tsx +140 -4
  125. package/src/server/context.ts +148 -16
  126. package/src/server/loader-registry.ts +9 -8
  127. package/src/server/request-context.ts +182 -34
  128. package/src/server.ts +6 -0
  129. package/src/ssr/index.tsx +4 -0
  130. package/src/static-handler.ts +18 -6
  131. package/src/theme/index.ts +4 -13
  132. package/src/types/cache-types.ts +4 -4
  133. package/src/types/handler-context.ts +149 -49
  134. package/src/types/loader-types.ts +36 -9
  135. package/src/types/route-config.ts +17 -8
  136. package/src/types/route-entry.ts +8 -1
  137. package/src/types/segments.ts +2 -5
  138. package/src/urls/path-helper-types.ts +9 -2
  139. package/src/urls/path-helper.ts +48 -13
  140. package/src/urls/pattern-types.ts +12 -0
  141. package/src/urls/response-types.ts +16 -6
  142. package/src/use-loader.tsx +73 -4
  143. package/src/vite/discovery/bundle-postprocess.ts +61 -89
  144. package/src/vite/discovery/discover-routers.ts +23 -5
  145. package/src/vite/discovery/prerender-collection.ts +48 -15
  146. package/src/vite/discovery/state.ts +17 -13
  147. package/src/vite/index.ts +8 -3
  148. package/src/vite/plugin-types.ts +51 -79
  149. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  150. package/src/vite/plugins/expose-action-id.ts +1 -3
  151. package/src/vite/plugins/performance-tracks.ts +88 -0
  152. package/src/vite/plugins/refresh-cmd.ts +127 -0
  153. package/src/vite/plugins/version-plugin.ts +13 -1
  154. package/src/vite/rango.ts +174 -211
  155. package/src/vite/router-discovery.ts +169 -42
  156. package/src/vite/utils/banner.ts +3 -3
  157. package/src/vite/utils/prerender-utils.ts +78 -0
  158. package/src/vite/utils/shared-utils.ts +3 -2
  159. package/skills/testing/SKILL.md +0 -226
  160. package/src/route-definition/route-function.ts +0 -119
@@ -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,6 +41,11 @@ import {
37
41
  } from "./helpers.js";
38
42
  import { getRouterContext } from "../router-context.js";
39
43
  import { resolveSink, safeEmit } from "../telemetry.js";
44
+ import {
45
+ track,
46
+ RSCRouterContext,
47
+ runInsideLoaderScope,
48
+ } from "../../server/context.js";
40
49
 
41
50
  // ---------------------------------------------------------------------------
42
51
  // Telemetry helpers
@@ -227,7 +236,9 @@ export async function resolveLoadersWithRevalidation<TEnv>(
227
236
  params: ctx.params,
228
237
  loaderId: loader.$$id,
229
238
  loaderData: deps.wrapLoaderPromise(
230
- resolveLoaderData(loaderEntry, ctx, ctx.pathname),
239
+ runInsideLoaderScope(() =>
240
+ resolveLoaderData(loaderEntry, ctx, ctx.pathname),
241
+ ),
231
242
  entry,
232
243
  segmentId,
233
244
  ctx.pathname,
@@ -257,26 +268,75 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
257
268
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
258
269
  const allLoaderSegments: ResolvedSegment[] = [];
259
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
+ if (
324
+ entry.type === "route" &&
325
+ entry.loader &&
326
+ entry.loader.length > 0 &&
327
+ Object.keys(layoutEntry.parallel).length > 0
328
+ ) {
329
+ await collectEntryLoaders(
330
+ entry,
331
+ childBelongsToRoute,
332
+ layoutEntry.shortCode,
333
+ );
334
+ }
335
+ }
336
+ }
260
337
 
261
338
  for (const entry of entries) {
262
- const belongsToRoute = entry.type === "route";
263
- const { segments, matchedIds } = await resolveLoadersWithRevalidation(
264
- entry,
265
- context,
266
- belongsToRoute,
267
- clientSegmentIds,
268
- prevParams,
269
- request,
270
- prevUrl,
271
- nextUrl,
272
- routeKey,
273
- deps,
274
- actionContext,
275
- undefined, // shortCodeOverride
276
- stale,
277
- );
278
- allLoaderSegments.push(...segments);
279
- allMatchedIds.push(...matchedIds);
339
+ await collectEntryLoaders(entry, entry.type === "route");
280
340
  }
281
341
 
282
342
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -300,22 +360,20 @@ export function buildEntryRevalidateMap(
300
360
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
301
361
 
302
362
  if (entry.type !== "parallel") {
303
- for (const parallelEntry of entry.parallel) {
304
- if (parallelEntry.type === "parallel") {
305
- const slots = Object.keys(parallelEntry.handler) as `@${string}`[];
306
- for (const slot of slots) {
307
- const parallelId = `${parallelEntry.shortCode}.${slot}`;
308
- map.set(parallelId, {
309
- entry: parallelEntry,
310
- revalidate: parallelEntry.revalidate,
311
- });
312
- }
313
- }
363
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
364
+ entry.parallel,
365
+ )) {
366
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
367
+ const parallelId = `${parallelParentShortCode}.${slot}`;
368
+ map.set(parallelId, {
369
+ entry: parallelEntry,
370
+ revalidate: parallelEntry.revalidate,
371
+ });
314
372
  }
315
373
  }
316
374
 
317
375
  for (const layoutEntry of entry.layout) {
318
- processEntry(layoutEntry);
376
+ processEntry(layoutEntry, entry.shortCode);
319
377
  }
320
378
  }
321
379
 
@@ -347,7 +405,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
347
405
  const segments: ResolvedSegment[] = [];
348
406
  const matchedIds: string[] = [];
349
407
 
350
- for (const parallelEntry of entry.parallel) {
408
+ const resolvedParallelEntries = new Set<string>();
409
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
410
+ entry.parallel,
411
+ )) {
351
412
  invariant(
352
413
  parallelEntry.type === "parallel",
353
414
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -358,141 +419,61 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
358
419
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
359
420
  | ReactNode
360
421
  >;
422
+ // In production, static handler bodies are evicted and the slot value
423
+ // may be undefined. The static store holds the pre-rendered component.
424
+ // We defer the handler check until after tryStaticSlot.
425
+ const handler = slots[slot];
426
+
427
+ const parallelId = `${entry.shortCode}.${slot}`;
428
+
429
+ const isFullRefetch = clientSegmentIds.size === 0;
430
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
431
+ if (
432
+ isFullRefetch ||
433
+ clientSegmentIds.has(parallelId) ||
434
+ belongsToRoute ||
435
+ isNewParent
436
+ ) {
437
+ matchedIds.push(parallelId);
438
+ }
361
439
 
362
- for (const [slot, handler] of Object.entries(slots)) {
363
- const parallelId = `${entry.shortCode}.${slot}`;
364
-
365
- const isFullRefetch = clientSegmentIds.size === 0;
366
- // When the parent layout is new (not in client's segment set),
367
- // all its parallel children must be resolved and tracked.
368
- // Without this, navigating to a new layout with parallels
369
- // (e.g., BlogLayout with @sidebar) from a different route
370
- // would silently drop those parallel segments.
371
- const isNewParent = !clientSegmentIds.has(entry.shortCode);
372
- if (
373
- isFullRefetch ||
374
- clientSegmentIds.has(parallelId) ||
375
- belongsToRoute ||
376
- isNewParent
377
- ) {
378
- matchedIds.push(parallelId);
379
- }
380
-
381
- const shouldResolve = await (async () => {
382
- if (isFullRefetch) {
383
- if (isTraceActive()) {
384
- pushRevalidationTraceEntry({
385
- segmentId: parallelId,
386
- segmentType: "parallel",
387
- belongsToRoute,
388
- source: "parallel",
389
- defaultShouldRevalidate: true,
390
- finalShouldRevalidate: true,
391
- reason: "full-refetch",
392
- });
393
- }
394
- return true;
395
- }
396
- if (!clientSegmentIds.has(parallelId)) {
397
- const result = belongsToRoute || isNewParent;
398
- if (isTraceActive()) {
399
- pushRevalidationTraceEntry({
400
- segmentId: parallelId,
401
- segmentType: "parallel",
402
- belongsToRoute,
403
- source: "parallel",
404
- defaultShouldRevalidate: result,
405
- finalShouldRevalidate: result,
406
- reason: result ? "new-segment" : "skip-parent-chain",
407
- });
408
- }
409
- return result;
440
+ const shouldResolve = await (async () => {
441
+ if (isFullRefetch) {
442
+ if (isTraceActive()) {
443
+ pushRevalidationTraceEntry({
444
+ segmentId: parallelId,
445
+ segmentType: "parallel",
446
+ belongsToRoute,
447
+ source: "parallel",
448
+ defaultShouldRevalidate: true,
449
+ finalShouldRevalidate: true,
450
+ reason: "full-refetch",
451
+ });
410
452
  }
411
-
412
- const dummySegment: ResolvedSegment = {
413
- id: parallelId,
414
- namespace: parallelEntry.id,
415
- type: "parallel",
416
- index: 0,
417
- component: null as any,
418
- params,
419
- slot,
420
- belongsToRoute,
421
- parallelName: `${parallelEntry.id}.${slot}`,
422
- ...(parallelEntry.mountPath
423
- ? { mountPath: parallelEntry.mountPath }
424
- : {}),
425
- };
426
-
427
- return await evaluateRevalidation({
428
- segment: dummySegment,
429
- prevParams,
430
- getPrevSegment: null,
431
- request,
432
- prevUrl,
433
- nextUrl,
434
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
435
- name: `revalidate${i}`,
436
- fn,
437
- })),
438
- routeKey,
439
- context,
440
- actionContext,
441
- stale,
442
- traceSource: "parallel",
443
- });
444
- })();
445
- emitRevalidationDecision(
446
- parallelId,
447
- context.pathname,
448
- routeKey,
449
- shouldResolve,
450
- );
451
-
452
- let component: ReactNode | undefined;
453
- if (shouldResolve) {
454
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
453
+ return true;
455
454
  }
456
- if (component === undefined) {
457
- const hasLoadingFallback =
458
- parallelEntry.loading !== undefined &&
459
- parallelEntry.loading !== false;
460
- if (!shouldResolve) {
461
- component = null;
462
- } else if (hasLoadingFallback) {
463
- const result =
464
- typeof handler === "function" ? handler(context) : handler;
465
- if (result instanceof Promise) {
466
- const tracked = deps.trackHandler(result, {
467
- segmentId: parallelId,
468
- segmentType: "parallel",
469
- });
470
- observeStreamedHandler(
471
- tracked,
472
- parallelId,
473
- "parallel",
474
- context.pathname,
475
- routeKey,
476
- params,
477
- );
478
- component = tracked as ReactNode;
479
- } else {
480
- component = result as ReactNode;
481
- }
482
- } else {
483
- component =
484
- typeof handler === "function" ? await handler(context) : handler;
455
+ if (!clientSegmentIds.has(parallelId)) {
456
+ const result = belongsToRoute || isNewParent;
457
+ if (isTraceActive()) {
458
+ pushRevalidationTraceEntry({
459
+ segmentId: parallelId,
460
+ segmentType: "parallel",
461
+ belongsToRoute,
462
+ source: "parallel",
463
+ defaultShouldRevalidate: result,
464
+ finalShouldRevalidate: result,
465
+ reason: result ? "new-segment" : "skip-parent-chain",
466
+ });
485
467
  }
468
+ return result;
486
469
  }
487
470
 
488
- segments.push({
471
+ const dummySegment: ResolvedSegment = {
489
472
  id: parallelId,
490
473
  namespace: parallelEntry.id,
491
474
  type: "parallel",
492
475
  index: 0,
493
- component,
494
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
495
- transition: parallelEntry.transition,
476
+ component: null as any,
496
477
  params,
497
478
  slot,
498
479
  belongsToRoute,
@@ -500,28 +481,111 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
500
481
  ...(parallelEntry.mountPath
501
482
  ? { mountPath: parallelEntry.mountPath }
502
483
  : {}),
503
- });
504
- }
484
+ };
505
485
 
506
- if (!parallelEntry.loading) {
507
- const loaderResult = await resolveLoadersWithRevalidation(
508
- parallelEntry,
509
- context,
510
- belongsToRoute,
511
- clientSegmentIds,
486
+ return await evaluateRevalidation({
487
+ segment: dummySegment,
512
488
  prevParams,
489
+ getPrevSegment: null,
513
490
  request,
514
491
  prevUrl,
515
492
  nextUrl,
493
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
494
+ name: `revalidate${i}`,
495
+ fn,
496
+ })),
516
497
  routeKey,
517
- deps,
498
+ context,
518
499
  actionContext,
519
- entry.shortCode,
520
500
  stale,
521
- );
522
- segments.push(...loaderResult.segments);
523
- matchedIds.push(...loaderResult.matchedIds);
501
+ traceSource: "parallel",
502
+ });
503
+ })();
504
+ emitRevalidationDecision(
505
+ parallelId,
506
+ context.pathname,
507
+ routeKey,
508
+ shouldResolve,
509
+ );
510
+
511
+ let component: ReactNode | undefined;
512
+ if (shouldResolve) {
513
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
514
+ }
515
+ if (component === undefined) {
516
+ const hasLoadingFallback =
517
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
518
+ if (!shouldResolve) {
519
+ component = null;
520
+ } else if (handler === undefined) {
521
+ // Handler evicted (production static slot) but static lookup missed.
522
+ // Nothing to render — use null so the client keeps its cached version.
523
+ component = null;
524
+ } else if (hasLoadingFallback) {
525
+ const result =
526
+ typeof handler === "function" ? handler(context) : handler;
527
+ if (result instanceof Promise) {
528
+ const tracked = deps.trackHandler(result, {
529
+ segmentId: parallelId,
530
+ segmentType: "parallel",
531
+ });
532
+ observeStreamedHandler(
533
+ tracked,
534
+ parallelId,
535
+ "parallel",
536
+ context.pathname,
537
+ routeKey,
538
+ params,
539
+ );
540
+ component = tracked as ReactNode;
541
+ } else {
542
+ component = result as ReactNode;
543
+ }
544
+ } else {
545
+ component =
546
+ typeof handler === "function" ? await handler(context) : handler;
547
+ }
524
548
  }
549
+
550
+ segments.push({
551
+ id: parallelId,
552
+ namespace: parallelEntry.id,
553
+ type: "parallel",
554
+ index: 0,
555
+ component,
556
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
557
+ transition: parallelEntry.transition,
558
+ params,
559
+ slot,
560
+ belongsToRoute,
561
+ parallelName: `${parallelEntry.id}.${slot}`,
562
+ ...(parallelEntry.mountPath
563
+ ? { mountPath: parallelEntry.mountPath }
564
+ : {}),
565
+ });
566
+
567
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
568
+ continue;
569
+ }
570
+
571
+ const loaderResult = await resolveLoadersWithRevalidation(
572
+ parallelEntry,
573
+ context,
574
+ belongsToRoute,
575
+ clientSegmentIds,
576
+ prevParams,
577
+ request,
578
+ prevUrl,
579
+ nextUrl,
580
+ routeKey,
581
+ deps,
582
+ actionContext,
583
+ entry.shortCode,
584
+ stale,
585
+ );
586
+ segments.push(...loaderResult.segments);
587
+ matchedIds.push(...loaderResult.matchedIds);
588
+ resolvedParallelEntries.add(parallelEntry.id);
525
589
  }
526
590
 
527
591
  return { segments, matchedIds };
@@ -607,6 +671,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
607
671
  context,
608
672
  actionContext,
609
673
  stale,
674
+ traceSource:
675
+ entry.type === "route" ? "route-handler" : "layout-handler",
610
676
  });
611
677
  emitRevalidationDecision(
612
678
  entry.shortCode,
@@ -621,20 +687,36 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
621
687
  return shouldRevalidate;
622
688
  },
623
689
  async () => {
690
+ const doneHandler = track(`handler:${entry.id}`, 2);
624
691
  (context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
625
692
  entry.shortCode;
626
693
  if (entry.type === "layout" || entry.type === "cache") {
627
- return resolveLayoutComponent(entry, context);
694
+ const layoutComponent = await resolveLayoutComponent(entry, context);
695
+ doneHandler();
696
+ return layoutComponent;
628
697
  }
629
698
  const staticComponent = await tryStaticHandler(entry, entry.shortCode);
630
- if (staticComponent !== undefined) return staticComponent;
699
+ if (staticComponent !== undefined) {
700
+ doneHandler();
701
+ return staticComponent;
702
+ }
631
703
  const routeEntry = entry as Extract<EntryData, { type: "route" }>;
704
+ // For Passthrough routes at runtime, use the live handler instead of
705
+ // the build handler. At build time (context.build === true), always
706
+ // use the build handler from routeEntry.handler.
707
+ const handler =
708
+ !context.build && routeEntry.liveHandler
709
+ ? routeEntry.liveHandler
710
+ : routeEntry.handler;
632
711
  if (!routeEntry.loading) {
633
- return handleHandlerResult(await routeEntry.handler(context));
712
+ const result = handleHandlerResult(await handler(context));
713
+ doneHandler();
714
+ return result;
634
715
  }
635
716
  if (!actionContext) {
636
- const result = handleHandlerResult(routeEntry.handler(context));
717
+ const result = handleHandlerResult(handler(context));
637
718
  if (result instanceof Promise) {
719
+ result.finally(doneHandler).catch(() => {});
638
720
  const tracked = deps.trackHandler(result, {
639
721
  segmentId: entry.shortCode,
640
722
  segmentType: entry.type,
@@ -649,24 +731,27 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
649
731
  );
650
732
  return { content: tracked };
651
733
  }
734
+ doneHandler();
652
735
  return { content: result };
653
736
  }
654
737
  debugLog("segment.action", "resolving action route with awaited value", {
655
738
  entryId: entry.id,
656
739
  });
740
+ const actionResult = handleHandlerResult(await handler(context));
741
+ doneHandler();
657
742
  return {
658
- content: Promise.resolve(
659
- handleHandlerResult(await routeEntry.handler(context)),
660
- ),
743
+ content: Promise.resolve(actionResult),
661
744
  };
662
745
  },
663
746
  () => null,
664
747
  );
665
748
 
749
+ // Normalize void handlers (undefined) to null so the reconciler's
750
+ // component === null checks work consistently for both void and explicit null.
666
751
  const resolvedComponent =
667
752
  component && typeof component === "object" && "content" in component
668
- ? (component as { content: ReactNode }).content
669
- : component;
753
+ ? ((component as { content: ReactNode }).content ?? null)
754
+ : (component ?? null);
670
755
 
671
756
  const segment: ResolvedSegment = {
672
757
  id: entry.shortCode,
@@ -768,6 +853,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
768
853
  deps,
769
854
  actionContext,
770
855
  stale,
856
+ entry,
771
857
  );
772
858
  segments.push(...orphanResult.segments);
773
859
  matchedIds.push(...orphanResult.matchedIds);
@@ -879,6 +965,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
879
965
  deps: SegmentResolutionDeps<TEnv>,
880
966
  actionContext?: ActionContext,
881
967
  stale?: boolean,
968
+ /** Parent route entry — its loaders are inherited so parallel slots can access them. */
969
+ parentRouteEntry?: EntryData,
882
970
  ): Promise<SegmentRevalidationResult> {
883
971
  invariant(
884
972
  orphan.type === "layout" || orphan.type === "cache",
@@ -906,6 +994,33 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
906
994
  segments.push(...loaderResult.segments);
907
995
  matchedIds.push(...loaderResult.matchedIds);
908
996
 
997
+ // Inherit parent route's loaders so parallel slots inside this layout
998
+ // can access them via useLoader(). See resolveOrphanLayout in fresh.ts.
999
+ if (
1000
+ parentRouteEntry &&
1001
+ parentRouteEntry.loader &&
1002
+ parentRouteEntry.loader.length > 0 &&
1003
+ Object.keys(orphan.parallel).length > 0
1004
+ ) {
1005
+ const inheritedResult = await resolveLoadersWithRevalidation(
1006
+ parentRouteEntry,
1007
+ context,
1008
+ belongsToRoute,
1009
+ clientSegmentIds,
1010
+ prevParams,
1011
+ request,
1012
+ prevUrl,
1013
+ nextUrl,
1014
+ routeKey,
1015
+ deps,
1016
+ actionContext,
1017
+ orphan.shortCode,
1018
+ stale,
1019
+ );
1020
+ segments.push(...inheritedResult.segments);
1021
+ matchedIds.push(...inheritedResult.matchedIds);
1022
+ }
1023
+
909
1024
  // Handler-first: resolve orphan layout handler before its parallels
910
1025
  // so ctx.set() values are visible to parallel children.
911
1026
  matchedIds.push(orphan.shortCode);
@@ -982,143 +1097,72 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
982
1097
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
983
1098
  });
984
1099
 
985
- for (const parallelEntry of orphan.parallel) {
1100
+ const resolvedParallelEntries = new Set<string>();
1101
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1102
+ orphan.parallel,
1103
+ )) {
986
1104
  invariant(
987
1105
  parallelEntry.type === "parallel",
988
1106
  `Expected parallel entry, got: ${parallelEntry.type}`,
989
1107
  );
990
1108
 
991
- const loaderResult = await resolveLoadersWithRevalidation(
992
- parallelEntry,
993
- context,
994
- belongsToRoute,
995
- clientSegmentIds,
996
- prevParams,
997
- request,
998
- prevUrl,
999
- nextUrl,
1000
- routeKey,
1001
- deps,
1002
- actionContext,
1003
- undefined,
1004
- stale,
1005
- );
1006
- segments.push(...loaderResult.segments);
1007
- matchedIds.push(...loaderResult.matchedIds);
1109
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1110
+ const loaderResult = await resolveLoadersWithRevalidation(
1111
+ parallelEntry,
1112
+ context,
1113
+ belongsToRoute,
1114
+ clientSegmentIds,
1115
+ prevParams,
1116
+ request,
1117
+ prevUrl,
1118
+ nextUrl,
1119
+ routeKey,
1120
+ deps,
1121
+ actionContext,
1122
+ undefined,
1123
+ stale,
1124
+ );
1125
+ segments.push(...loaderResult.segments);
1126
+ matchedIds.push(...loaderResult.matchedIds);
1127
+ resolvedParallelEntries.add(parallelEntry.id);
1128
+ }
1008
1129
 
1009
1130
  const slots = parallelEntry.handler as Record<
1010
1131
  `@${string}`,
1011
1132
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1012
1133
  | ReactNode
1013
1134
  >;
1135
+ // Handler may be undefined in production after static handler eviction.
1136
+ const handler = slots[slot];
1014
1137
 
1015
- for (const [slot, handler] of Object.entries(slots)) {
1016
- // Use orphan.shortCode (the parent layout) to match the SSR path
1017
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1018
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1019
- const parallelId = `${orphan.shortCode}.${slot}`;
1020
- matchedIds.push(parallelId);
1021
-
1022
- const shouldResolve = await (async () => {
1023
- if (!clientSegmentIds.has(parallelId)) {
1024
- if (isTraceActive()) {
1025
- pushRevalidationTraceEntry({
1026
- segmentId: parallelId,
1027
- segmentType: "parallel",
1028
- belongsToRoute,
1029
- source: "parallel",
1030
- defaultShouldRevalidate: true,
1031
- finalShouldRevalidate: true,
1032
- reason: "new-segment",
1033
- });
1034
- }
1035
- return true;
1036
- }
1037
-
1038
- const dummySegment: ResolvedSegment = {
1039
- id: parallelId,
1040
- namespace: parallelEntry.id,
1041
- type: "parallel",
1042
- index: 0,
1043
- component: null as any,
1044
- params,
1045
- slot,
1046
- belongsToRoute,
1047
- parallelName: `${parallelEntry.id}.${slot}`,
1048
- ...(parallelEntry.mountPath
1049
- ? { mountPath: parallelEntry.mountPath }
1050
- : {}),
1051
- };
1052
-
1053
- return await evaluateRevalidation({
1054
- segment: dummySegment,
1055
- prevParams,
1056
- getPrevSegment: null,
1057
- request,
1058
- prevUrl,
1059
- nextUrl,
1060
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1061
- name: `revalidate${i}`,
1062
- fn,
1063
- })),
1064
- routeKey,
1065
- context,
1066
- actionContext,
1067
- stale,
1068
- traceSource: "parallel",
1069
- });
1070
- })();
1071
- emitRevalidationDecision(
1072
- parallelId,
1073
- context.pathname,
1074
- routeKey,
1075
- shouldResolve,
1076
- );
1138
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1139
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1140
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1141
+ const parallelId = `${orphan.shortCode}.${slot}`;
1142
+ matchedIds.push(parallelId);
1077
1143
 
1078
- let component: ReactNode | undefined;
1079
- if (shouldResolve) {
1080
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1081
- }
1082
- if (component === undefined) {
1083
- const hasLoadingFallback =
1084
- parallelEntry.loading !== undefined &&
1085
- parallelEntry.loading !== false;
1086
- if (!shouldResolve) {
1087
- component = null;
1088
- } else if (hasLoadingFallback) {
1089
- const result =
1090
- typeof handler === "function" ? handler(context) : handler;
1091
- if (result instanceof Promise) {
1092
- const tracked = deps.trackHandler(result, {
1093
- segmentId: parallelId,
1094
- segmentType: "parallel",
1095
- });
1096
- observeStreamedHandler(
1097
- tracked,
1098
- parallelId,
1099
- "parallel",
1100
- context.pathname,
1101
- routeKey,
1102
- params,
1103
- );
1104
- component = tracked as ReactNode;
1105
- } else {
1106
- component = result as ReactNode;
1107
- }
1108
- } else {
1109
- component =
1110
- typeof handler === "function" ? await handler(context) : handler;
1144
+ const shouldResolve = await (async () => {
1145
+ if (!clientSegmentIds.has(parallelId)) {
1146
+ if (isTraceActive()) {
1147
+ pushRevalidationTraceEntry({
1148
+ segmentId: parallelId,
1149
+ segmentType: "parallel",
1150
+ belongsToRoute,
1151
+ source: "parallel",
1152
+ defaultShouldRevalidate: true,
1153
+ finalShouldRevalidate: true,
1154
+ reason: "new-segment",
1155
+ });
1111
1156
  }
1157
+ return true;
1112
1158
  }
1113
1159
 
1114
- segments.push({
1160
+ const dummySegment: ResolvedSegment = {
1115
1161
  id: parallelId,
1116
1162
  namespace: parallelEntry.id,
1117
1163
  type: "parallel",
1118
1164
  index: 0,
1119
- component,
1120
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1121
- transition: parallelEntry.transition,
1165
+ component: null as any,
1122
1166
  params,
1123
1167
  slot,
1124
1168
  belongsToRoute,
@@ -1126,8 +1170,87 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1126
1170
  ...(parallelEntry.mountPath
1127
1171
  ? { mountPath: parallelEntry.mountPath }
1128
1172
  : {}),
1173
+ };
1174
+
1175
+ return await evaluateRevalidation({
1176
+ segment: dummySegment,
1177
+ prevParams,
1178
+ getPrevSegment: null,
1179
+ request,
1180
+ prevUrl,
1181
+ nextUrl,
1182
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1183
+ name: `revalidate${i}`,
1184
+ fn,
1185
+ })),
1186
+ routeKey,
1187
+ context,
1188
+ actionContext,
1189
+ stale,
1190
+ traceSource: "parallel",
1129
1191
  });
1192
+ })();
1193
+ emitRevalidationDecision(
1194
+ parallelId,
1195
+ context.pathname,
1196
+ routeKey,
1197
+ shouldResolve,
1198
+ );
1199
+
1200
+ let component: ReactNode | undefined;
1201
+ if (shouldResolve) {
1202
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1203
+ }
1204
+ if (component === undefined) {
1205
+ const hasLoadingFallback =
1206
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1207
+ if (!shouldResolve) {
1208
+ component = null;
1209
+ } else if (handler === undefined) {
1210
+ // Handler evicted (production static slot) but static lookup missed.
1211
+ component = null;
1212
+ } else if (hasLoadingFallback) {
1213
+ const result =
1214
+ typeof handler === "function" ? handler(context) : handler;
1215
+ if (result instanceof Promise) {
1216
+ const tracked = deps.trackHandler(result, {
1217
+ segmentId: parallelId,
1218
+ segmentType: "parallel",
1219
+ });
1220
+ observeStreamedHandler(
1221
+ tracked,
1222
+ parallelId,
1223
+ "parallel",
1224
+ context.pathname,
1225
+ routeKey,
1226
+ params,
1227
+ );
1228
+ component = tracked as ReactNode;
1229
+ } else {
1230
+ component = result as ReactNode;
1231
+ }
1232
+ } else {
1233
+ component =
1234
+ typeof handler === "function" ? await handler(context) : handler;
1235
+ }
1130
1236
  }
1237
+
1238
+ segments.push({
1239
+ id: parallelId,
1240
+ namespace: parallelEntry.id,
1241
+ type: "parallel",
1242
+ index: 0,
1243
+ component,
1244
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1245
+ transition: parallelEntry.transition,
1246
+ params,
1247
+ slot,
1248
+ belongsToRoute,
1249
+ parallelName: `${parallelEntry.id}.${slot}`,
1250
+ ...(parallelEntry.mountPath
1251
+ ? { mountPath: parallelEntry.mountPath }
1252
+ : {}),
1253
+ });
1131
1254
  }
1132
1255
 
1133
1256
  return { segments, matchedIds };
@@ -1152,6 +1275,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1152
1275
  localRouteName: string,
1153
1276
  pathname: string,
1154
1277
  deps: SegmentResolutionDeps<TEnv>,
1278
+ stale?: boolean,
1155
1279
  ): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
1156
1280
  const allSegments: ResolvedSegment[] = [];
1157
1281
  const matchedIds: string[] = [];
@@ -1178,6 +1302,11 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1178
1302
  }
1179
1303
 
1180
1304
  const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
1305
+ if (entry.type === "cache") {
1306
+ const store = RSCRouterContext.getStore();
1307
+ if (store) store.insideCacheScope = true;
1308
+ }
1309
+ const doneEntry = track(`segment:${entry.id}`, 1);
1181
1310
  const resolved = await resolveWithErrorBoundary(
1182
1311
  nonParallelEntry,
1183
1312
  params,
@@ -1195,15 +1324,14 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
1195
1324
  loaderPromises,
1196
1325
  deps,
1197
1326
  actionContext,
1198
- false,
1327
+ stale,
1199
1328
  ),
1200
1329
  (seg) => ({ segments: [seg], matchedIds: [seg.id] }),
1201
1330
  deps,
1202
- telemetry
1203
- ? { request, url: context.url, routeKey, isPartial: true, telemetry }
1204
- : undefined,
1331
+ { request, url: context.url, routeKey, isPartial: true, telemetry },
1205
1332
  pathname,
1206
1333
  );
1334
+ doneEntry();
1207
1335
 
1208
1336
  // Deduplicate segments and matchedIds by ID, matching resolveAllSegments.
1209
1337
  // include() scopes can produce entries that resolve the same shared