@rangojs/router 0.0.0-experimental.f2337aef → 0.0.0-experimental.fa8a383a

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 (57) hide show
  1. package/dist/bin/rango.js +8 -3
  2. package/dist/vite/index.js +139 -200
  3. package/package.json +1 -1
  4. package/skills/caching/SKILL.md +37 -4
  5. package/skills/parallel/SKILL.md +59 -0
  6. package/src/browser/event-controller.ts +5 -0
  7. package/src/browser/navigation-bridge.ts +1 -3
  8. package/src/browser/navigation-client.ts +60 -27
  9. package/src/browser/navigation-transaction.ts +11 -9
  10. package/src/browser/partial-update.ts +39 -9
  11. package/src/browser/prefetch/cache.ts +57 -5
  12. package/src/browser/prefetch/fetch.ts +30 -21
  13. package/src/browser/prefetch/queue.ts +53 -13
  14. package/src/browser/react/Link.tsx +9 -1
  15. package/src/browser/react/NavigationProvider.tsx +27 -0
  16. package/src/browser/rsc-router.tsx +109 -57
  17. package/src/browser/scroll-restoration.ts +20 -7
  18. package/src/browser/segment-reconciler.ts +6 -1
  19. package/src/browser/types.ts +9 -0
  20. package/src/build/route-types/router-processing.ts +12 -2
  21. package/src/cache/cache-scope.ts +2 -2
  22. package/src/cache/cf/cf-cache-store.ts +453 -11
  23. package/src/cache/cf/index.ts +5 -1
  24. package/src/cache/document-cache.ts +17 -7
  25. package/src/cache/index.ts +1 -0
  26. package/src/debug.ts +2 -2
  27. package/src/route-definition/dsl-helpers.ts +32 -7
  28. package/src/route-definition/redirect.ts +2 -2
  29. package/src/router/lazy-includes.ts +4 -1
  30. package/src/router/logging.ts +1 -1
  31. package/src/router/manifest.ts +9 -3
  32. package/src/router/match-middleware/background-revalidation.ts +18 -1
  33. package/src/router/match-middleware/cache-lookup.ts +20 -3
  34. package/src/router/match-middleware/cache-store.ts +32 -6
  35. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  36. package/src/router/match-middleware/segment-resolution.ts +7 -5
  37. package/src/router/match-result.ts +11 -1
  38. package/src/router/middleware.ts +2 -1
  39. package/src/router/segment-resolution/fresh.ts +104 -14
  40. package/src/router/segment-resolution/loader-cache.ts +1 -0
  41. package/src/router/segment-resolution/revalidation.ts +307 -272
  42. package/src/router.ts +5 -1
  43. package/src/rsc/handler.ts +9 -0
  44. package/src/segment-system.tsx +140 -4
  45. package/src/server/context.ts +90 -13
  46. package/src/server/request-context.ts +10 -4
  47. package/src/ssr/index.tsx +1 -0
  48. package/src/types/route-entry.ts +7 -0
  49. package/src/types/segments.ts +2 -0
  50. package/src/urls/path-helper.ts +1 -1
  51. package/src/vite/discovery/state.ts +0 -2
  52. package/src/vite/plugin-types.ts +0 -83
  53. package/src/vite/plugins/expose-action-id.ts +1 -3
  54. package/src/vite/plugins/version-plugin.ts +13 -1
  55. package/src/vite/rango.ts +144 -209
  56. package/src/vite/router-discovery.ts +0 -8
  57. package/src/vite/utils/banner.ts +3 -3
@@ -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,
@@ -259,8 +263,11 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
259
263
  const allLoaderSegments: ResolvedSegment[] = [];
260
264
  const allMatchedIds: string[] = [];
261
265
 
262
- for (const entry of entries) {
263
- const belongsToRoute = entry.type === "route";
266
+ async function collectEntryLoaders(
267
+ entry: EntryData,
268
+ belongsToRoute: boolean,
269
+ shortCodeOverride?: string,
270
+ ): Promise<void> {
264
271
  const { segments, matchedIds } = await resolveLoadersWithRevalidation(
265
272
  entry,
266
273
  context,
@@ -273,11 +280,27 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
273
280
  routeKey,
274
281
  deps,
275
282
  actionContext,
276
- undefined, // shortCodeOverride
283
+ shortCodeOverride,
277
284
  stale,
278
285
  );
279
286
  allLoaderSegments.push(...segments);
280
287
  allMatchedIds.push(...matchedIds);
288
+
289
+ const seenParallelEntryIds = new Set<string>();
290
+ for (const parallelEntry of getParallelEntries(entry.parallel)) {
291
+ if (seenParallelEntryIds.has(parallelEntry.id)) continue;
292
+ seenParallelEntryIds.add(parallelEntry.id);
293
+ await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
294
+ }
295
+
296
+ const childBelongsToRoute = belongsToRoute || entry.type === "route";
297
+ for (const layoutEntry of entry.layout) {
298
+ await collectEntryLoaders(layoutEntry, childBelongsToRoute);
299
+ }
300
+ }
301
+
302
+ for (const entry of entries) {
303
+ await collectEntryLoaders(entry, entry.type === "route");
281
304
  }
282
305
 
283
306
  return { segments: allLoaderSegments, matchedIds: allMatchedIds };
@@ -301,22 +324,20 @@ export function buildEntryRevalidateMap(
301
324
  map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
302
325
 
303
326
  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
- }
327
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
328
+ entry.parallel,
329
+ )) {
330
+ const parallelParentShortCode = parentShortCode ?? entry.shortCode;
331
+ const parallelId = `${parallelParentShortCode}.${slot}`;
332
+ map.set(parallelId, {
333
+ entry: parallelEntry,
334
+ revalidate: parallelEntry.revalidate,
335
+ });
315
336
  }
316
337
  }
317
338
 
318
339
  for (const layoutEntry of entry.layout) {
319
- processEntry(layoutEntry);
340
+ processEntry(layoutEntry, entry.shortCode);
320
341
  }
321
342
  }
322
343
 
@@ -348,7 +369,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
348
369
  const segments: ResolvedSegment[] = [];
349
370
  const matchedIds: string[] = [];
350
371
 
351
- for (const parallelEntry of entry.parallel) {
372
+ const resolvedParallelEntries = new Set<string>();
373
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
374
+ entry.parallel,
375
+ )) {
352
376
  invariant(
353
377
  parallelEntry.type === "parallel",
354
378
  `Expected parallel entry, got: ${parallelEntry.type}`,
@@ -359,141 +383,61 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
359
383
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
360
384
  | ReactNode
361
385
  >;
386
+ // In production, static handler bodies are evicted and the slot value
387
+ // may be undefined. The static store holds the pre-rendered component.
388
+ // We defer the handler check until after tryStaticSlot.
389
+ const handler = slots[slot];
390
+
391
+ const parallelId = `${entry.shortCode}.${slot}`;
392
+
393
+ const isFullRefetch = clientSegmentIds.size === 0;
394
+ const isNewParent = !clientSegmentIds.has(entry.shortCode);
395
+ if (
396
+ isFullRefetch ||
397
+ clientSegmentIds.has(parallelId) ||
398
+ belongsToRoute ||
399
+ isNewParent
400
+ ) {
401
+ matchedIds.push(parallelId);
402
+ }
362
403
 
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;
404
+ const shouldResolve = await (async () => {
405
+ if (isFullRefetch) {
406
+ if (isTraceActive()) {
407
+ pushRevalidationTraceEntry({
408
+ segmentId: parallelId,
409
+ segmentType: "parallel",
410
+ belongsToRoute,
411
+ source: "parallel",
412
+ defaultShouldRevalidate: true,
413
+ finalShouldRevalidate: true,
414
+ reason: "full-refetch",
415
+ });
411
416
  }
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);
417
+ return true;
456
418
  }
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;
419
+ if (!clientSegmentIds.has(parallelId)) {
420
+ const result = belongsToRoute || isNewParent;
421
+ if (isTraceActive()) {
422
+ pushRevalidationTraceEntry({
423
+ segmentId: parallelId,
424
+ segmentType: "parallel",
425
+ belongsToRoute,
426
+ source: "parallel",
427
+ defaultShouldRevalidate: result,
428
+ finalShouldRevalidate: result,
429
+ reason: result ? "new-segment" : "skip-parent-chain",
430
+ });
486
431
  }
432
+ return result;
487
433
  }
488
434
 
489
- segments.push({
435
+ const dummySegment: ResolvedSegment = {
490
436
  id: parallelId,
491
437
  namespace: parallelEntry.id,
492
438
  type: "parallel",
493
439
  index: 0,
494
- component,
495
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
496
- transition: parallelEntry.transition,
440
+ component: null as any,
497
441
  params,
498
442
  slot,
499
443
  belongsToRoute,
@@ -501,28 +445,111 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
501
445
  ...(parallelEntry.mountPath
502
446
  ? { mountPath: parallelEntry.mountPath }
503
447
  : {}),
504
- });
505
- }
448
+ };
506
449
 
507
- if (!parallelEntry.loading) {
508
- const loaderResult = await resolveLoadersWithRevalidation(
509
- parallelEntry,
510
- context,
511
- belongsToRoute,
512
- clientSegmentIds,
450
+ return await evaluateRevalidation({
451
+ segment: dummySegment,
513
452
  prevParams,
453
+ getPrevSegment: null,
514
454
  request,
515
455
  prevUrl,
516
456
  nextUrl,
457
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
458
+ name: `revalidate${i}`,
459
+ fn,
460
+ })),
517
461
  routeKey,
518
- deps,
462
+ context,
519
463
  actionContext,
520
- entry.shortCode,
521
464
  stale,
522
- );
523
- segments.push(...loaderResult.segments);
524
- matchedIds.push(...loaderResult.matchedIds);
465
+ traceSource: "parallel",
466
+ });
467
+ })();
468
+ emitRevalidationDecision(
469
+ parallelId,
470
+ context.pathname,
471
+ routeKey,
472
+ shouldResolve,
473
+ );
474
+
475
+ let component: ReactNode | undefined;
476
+ if (shouldResolve) {
477
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
478
+ }
479
+ if (component === undefined) {
480
+ const hasLoadingFallback =
481
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
482
+ if (!shouldResolve) {
483
+ component = null;
484
+ } else if (handler === undefined) {
485
+ // Handler evicted (production static slot) but static lookup missed.
486
+ // Nothing to render — use null so the client keeps its cached version.
487
+ component = null;
488
+ } else if (hasLoadingFallback) {
489
+ const result =
490
+ typeof handler === "function" ? handler(context) : handler;
491
+ if (result instanceof Promise) {
492
+ const tracked = deps.trackHandler(result, {
493
+ segmentId: parallelId,
494
+ segmentType: "parallel",
495
+ });
496
+ observeStreamedHandler(
497
+ tracked,
498
+ parallelId,
499
+ "parallel",
500
+ context.pathname,
501
+ routeKey,
502
+ params,
503
+ );
504
+ component = tracked as ReactNode;
505
+ } else {
506
+ component = result as ReactNode;
507
+ }
508
+ } else {
509
+ component =
510
+ typeof handler === "function" ? await handler(context) : handler;
511
+ }
525
512
  }
513
+
514
+ segments.push({
515
+ id: parallelId,
516
+ namespace: parallelEntry.id,
517
+ type: "parallel",
518
+ index: 0,
519
+ component,
520
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
521
+ transition: parallelEntry.transition,
522
+ params,
523
+ slot,
524
+ belongsToRoute,
525
+ parallelName: `${parallelEntry.id}.${slot}`,
526
+ ...(parallelEntry.mountPath
527
+ ? { mountPath: parallelEntry.mountPath }
528
+ : {}),
529
+ });
530
+
531
+ if (resolvedParallelEntries.has(parallelEntry.id)) {
532
+ continue;
533
+ }
534
+
535
+ const loaderResult = await resolveLoadersWithRevalidation(
536
+ parallelEntry,
537
+ context,
538
+ belongsToRoute,
539
+ clientSegmentIds,
540
+ prevParams,
541
+ request,
542
+ prevUrl,
543
+ nextUrl,
544
+ routeKey,
545
+ deps,
546
+ actionContext,
547
+ entry.shortCode,
548
+ stale,
549
+ );
550
+ segments.push(...loaderResult.segments);
551
+ matchedIds.push(...loaderResult.matchedIds);
552
+ resolvedParallelEntries.add(parallelEntry.id);
526
553
  }
527
554
 
528
555
  return { segments, matchedIds };
@@ -997,143 +1024,72 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
997
1024
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
998
1025
  });
999
1026
 
1000
- for (const parallelEntry of orphan.parallel) {
1027
+ const resolvedParallelEntries = new Set<string>();
1028
+ for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
1029
+ orphan.parallel,
1030
+ )) {
1001
1031
  invariant(
1002
1032
  parallelEntry.type === "parallel",
1003
1033
  `Expected parallel entry, got: ${parallelEntry.type}`,
1004
1034
  );
1005
1035
 
1006
- const loaderResult = await resolveLoadersWithRevalidation(
1007
- parallelEntry,
1008
- context,
1009
- belongsToRoute,
1010
- clientSegmentIds,
1011
- prevParams,
1012
- request,
1013
- prevUrl,
1014
- nextUrl,
1015
- routeKey,
1016
- deps,
1017
- actionContext,
1018
- undefined,
1019
- stale,
1020
- );
1021
- segments.push(...loaderResult.segments);
1022
- matchedIds.push(...loaderResult.matchedIds);
1036
+ if (!resolvedParallelEntries.has(parallelEntry.id)) {
1037
+ const loaderResult = await resolveLoadersWithRevalidation(
1038
+ parallelEntry,
1039
+ context,
1040
+ belongsToRoute,
1041
+ clientSegmentIds,
1042
+ prevParams,
1043
+ request,
1044
+ prevUrl,
1045
+ nextUrl,
1046
+ routeKey,
1047
+ deps,
1048
+ actionContext,
1049
+ undefined,
1050
+ stale,
1051
+ );
1052
+ segments.push(...loaderResult.segments);
1053
+ matchedIds.push(...loaderResult.matchedIds);
1054
+ resolvedParallelEntries.add(parallelEntry.id);
1055
+ }
1023
1056
 
1024
1057
  const slots = parallelEntry.handler as Record<
1025
1058
  `@${string}`,
1026
1059
  | ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
1027
1060
  | ReactNode
1028
1061
  >;
1062
+ // Handler may be undefined in production after static handler eviction.
1063
+ const handler = slots[slot];
1029
1064
 
1030
- for (const [slot, handler] of Object.entries(slots)) {
1031
- // Use orphan.shortCode (the parent layout) to match the SSR path
1032
- // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1033
- // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1034
- const parallelId = `${orphan.shortCode}.${slot}`;
1035
- matchedIds.push(parallelId);
1036
-
1037
- const shouldResolve = await (async () => {
1038
- if (!clientSegmentIds.has(parallelId)) {
1039
- if (isTraceActive()) {
1040
- pushRevalidationTraceEntry({
1041
- segmentId: parallelId,
1042
- segmentType: "parallel",
1043
- belongsToRoute,
1044
- source: "parallel",
1045
- defaultShouldRevalidate: true,
1046
- finalShouldRevalidate: true,
1047
- reason: "new-segment",
1048
- });
1049
- }
1050
- return true;
1051
- }
1052
-
1053
- const dummySegment: ResolvedSegment = {
1054
- id: parallelId,
1055
- namespace: parallelEntry.id,
1056
- type: "parallel",
1057
- index: 0,
1058
- component: null as any,
1059
- params,
1060
- slot,
1061
- belongsToRoute,
1062
- parallelName: `${parallelEntry.id}.${slot}`,
1063
- ...(parallelEntry.mountPath
1064
- ? { mountPath: parallelEntry.mountPath }
1065
- : {}),
1066
- };
1067
-
1068
- return await evaluateRevalidation({
1069
- segment: dummySegment,
1070
- prevParams,
1071
- getPrevSegment: null,
1072
- request,
1073
- prevUrl,
1074
- nextUrl,
1075
- revalidations: parallelEntry.revalidate.map((fn, i) => ({
1076
- name: `revalidate${i}`,
1077
- fn,
1078
- })),
1079
- routeKey,
1080
- context,
1081
- actionContext,
1082
- stale,
1083
- traceSource: "parallel",
1084
- });
1085
- })();
1086
- emitRevalidationDecision(
1087
- parallelId,
1088
- context.pathname,
1089
- routeKey,
1090
- shouldResolve,
1091
- );
1065
+ // Use orphan.shortCode (the parent layout) to match the SSR path
1066
+ // (resolveParallelEntry receives parentShortCode = orphan.shortCode).
1067
+ // Using parallelEntry.shortCode would generate IDs the client doesn't know about.
1068
+ const parallelId = `${orphan.shortCode}.${slot}`;
1069
+ matchedIds.push(parallelId);
1092
1070
 
1093
- let component: ReactNode | undefined;
1094
- if (shouldResolve) {
1095
- component = await tryStaticSlot(parallelEntry, slot, parallelId);
1096
- }
1097
- if (component === undefined) {
1098
- const hasLoadingFallback =
1099
- parallelEntry.loading !== undefined &&
1100
- parallelEntry.loading !== false;
1101
- if (!shouldResolve) {
1102
- component = null;
1103
- } else if (hasLoadingFallback) {
1104
- const result =
1105
- typeof handler === "function" ? handler(context) : handler;
1106
- if (result instanceof Promise) {
1107
- const tracked = deps.trackHandler(result, {
1108
- segmentId: parallelId,
1109
- segmentType: "parallel",
1110
- });
1111
- observeStreamedHandler(
1112
- tracked,
1113
- parallelId,
1114
- "parallel",
1115
- context.pathname,
1116
- routeKey,
1117
- params,
1118
- );
1119
- component = tracked as ReactNode;
1120
- } else {
1121
- component = result as ReactNode;
1122
- }
1123
- } else {
1124
- component =
1125
- typeof handler === "function" ? await handler(context) : handler;
1071
+ const shouldResolve = await (async () => {
1072
+ if (!clientSegmentIds.has(parallelId)) {
1073
+ if (isTraceActive()) {
1074
+ pushRevalidationTraceEntry({
1075
+ segmentId: parallelId,
1076
+ segmentType: "parallel",
1077
+ belongsToRoute,
1078
+ source: "parallel",
1079
+ defaultShouldRevalidate: true,
1080
+ finalShouldRevalidate: true,
1081
+ reason: "new-segment",
1082
+ });
1126
1083
  }
1084
+ return true;
1127
1085
  }
1128
1086
 
1129
- segments.push({
1087
+ const dummySegment: ResolvedSegment = {
1130
1088
  id: parallelId,
1131
1089
  namespace: parallelEntry.id,
1132
1090
  type: "parallel",
1133
1091
  index: 0,
1134
- component,
1135
- loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1136
- transition: parallelEntry.transition,
1092
+ component: null as any,
1137
1093
  params,
1138
1094
  slot,
1139
1095
  belongsToRoute,
@@ -1141,8 +1097,87 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1141
1097
  ...(parallelEntry.mountPath
1142
1098
  ? { mountPath: parallelEntry.mountPath }
1143
1099
  : {}),
1100
+ };
1101
+
1102
+ return await evaluateRevalidation({
1103
+ segment: dummySegment,
1104
+ prevParams,
1105
+ getPrevSegment: null,
1106
+ request,
1107
+ prevUrl,
1108
+ nextUrl,
1109
+ revalidations: parallelEntry.revalidate.map((fn, i) => ({
1110
+ name: `revalidate${i}`,
1111
+ fn,
1112
+ })),
1113
+ routeKey,
1114
+ context,
1115
+ actionContext,
1116
+ stale,
1117
+ traceSource: "parallel",
1144
1118
  });
1119
+ })();
1120
+ emitRevalidationDecision(
1121
+ parallelId,
1122
+ context.pathname,
1123
+ routeKey,
1124
+ shouldResolve,
1125
+ );
1126
+
1127
+ let component: ReactNode | undefined;
1128
+ if (shouldResolve) {
1129
+ component = await tryStaticSlot(parallelEntry, slot, parallelId);
1145
1130
  }
1131
+ if (component === undefined) {
1132
+ const hasLoadingFallback =
1133
+ parallelEntry.loading !== undefined && parallelEntry.loading !== false;
1134
+ if (!shouldResolve) {
1135
+ component = null;
1136
+ } else if (handler === undefined) {
1137
+ // Handler evicted (production static slot) but static lookup missed.
1138
+ component = null;
1139
+ } else if (hasLoadingFallback) {
1140
+ const result =
1141
+ typeof handler === "function" ? handler(context) : handler;
1142
+ if (result instanceof Promise) {
1143
+ const tracked = deps.trackHandler(result, {
1144
+ segmentId: parallelId,
1145
+ segmentType: "parallel",
1146
+ });
1147
+ observeStreamedHandler(
1148
+ tracked,
1149
+ parallelId,
1150
+ "parallel",
1151
+ context.pathname,
1152
+ routeKey,
1153
+ params,
1154
+ );
1155
+ component = tracked as ReactNode;
1156
+ } else {
1157
+ component = result as ReactNode;
1158
+ }
1159
+ } else {
1160
+ component =
1161
+ typeof handler === "function" ? await handler(context) : handler;
1162
+ }
1163
+ }
1164
+
1165
+ segments.push({
1166
+ id: parallelId,
1167
+ namespace: parallelEntry.id,
1168
+ type: "parallel",
1169
+ index: 0,
1170
+ component,
1171
+ loading: parallelEntry.loading === false ? null : parallelEntry.loading,
1172
+ transition: parallelEntry.transition,
1173
+ params,
1174
+ slot,
1175
+ belongsToRoute,
1176
+ parallelName: `${parallelEntry.id}.${slot}`,
1177
+ ...(parallelEntry.mountPath
1178
+ ? { mountPath: parallelEntry.mountPath }
1179
+ : {}),
1180
+ });
1146
1181
  }
1147
1182
 
1148
1183
  return { segments, matchedIds };