@rangojs/router 0.0.0-experimental.39 → 0.0.0-experimental.3b1deca8
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.
- package/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +292 -204
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +45 -4
- package/skills/loader/SKILL.md +53 -43
- package/skills/parallel/SKILL.md +126 -0
- package/skills/route/SKILL.md +31 -0
- package/skills/router-setup/SKILL.md +52 -2
- package/skills/typesafety/SKILL.md +10 -0
- package/src/browser/debug-channel.ts +93 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -5
- package/src/browser/navigation-client.ts +84 -27
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +50 -9
- package/src/browser/prefetch/cache.ts +57 -5
- package/src/browser/prefetch/fetch.ts +30 -21
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +32 -3
- package/src/browser/rsc-router.tsx +109 -57
- package/src/browser/scroll-restoration.ts +31 -34
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +12 -0
- package/src/browser/types.ts +17 -1
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/deps/browser.ts +1 -0
- package/src/route-definition/dsl-helpers.ts +32 -7
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/route-definition/redirect.ts +2 -2
- package/src/route-map-builder.ts +7 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +31 -8
- package/src/router/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +7 -1
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +66 -9
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +8 -5
- package/src/router/match-result.ts +22 -6
- package/src/router/metrics.ts +6 -1
- package/src/router/middleware-types.ts +6 -2
- package/src/router/middleware.ts +4 -3
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +130 -17
- package/src/router/segment-resolution/helpers.ts +29 -24
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +352 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/types.ts +1 -0
- package/src/router.ts +6 -1
- package/src/rsc/handler.ts +28 -2
- package/src/rsc/loader-fetch.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -1
- package/src/rsc/server-action.ts +2 -0
- package/src/rsc/types.ts +7 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +102 -13
- package/src/server/request-context.ts +59 -12
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +120 -22
- package/src/types/loader-types.ts +4 -4
- package/src/types/route-entry.ts +7 -0
- package/src/types/segments.ts +2 -0
- package/src/urls/path-helper.ts +1 -1
- package/src/vite/discovery/state.ts +0 -2
- package/src/vite/plugin-types.ts +0 -83
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +235 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +148 -209
- package/src/vite/router-discovery.ts +0 -8
- 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
|
|
13
|
+
import {
|
|
14
|
+
getParallelEntries,
|
|
15
|
+
getParallelSlotEntries,
|
|
16
|
+
type EntryData,
|
|
17
|
+
} from "../../server/context";
|
|
14
18
|
import type {
|
|
15
19
|
HandlerContext,
|
|
16
20
|
InternalHandlerContext,
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
import { getRouterContext } from "../router-context.js";
|
|
39
43
|
import { resolveSink, safeEmit } from "../telemetry.js";
|
|
40
44
|
import { track } from "../../server/context.js";
|
|
45
|
+
import { RSCRouterContext } from "../../server/context.js";
|
|
41
46
|
|
|
42
47
|
// ---------------------------------------------------------------------------
|
|
43
48
|
// Telemetry helpers
|
|
@@ -258,26 +263,62 @@ export async function resolveLoadersOnlyWithRevalidation<TEnv>(
|
|
|
258
263
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
259
264
|
const allLoaderSegments: ResolvedSegment[] = [];
|
|
260
265
|
const allMatchedIds: string[] = [];
|
|
266
|
+
const seenIds = new Set<string>();
|
|
267
|
+
|
|
268
|
+
async function collectEntryLoaders(
|
|
269
|
+
entry: EntryData,
|
|
270
|
+
belongsToRoute: boolean,
|
|
271
|
+
shortCodeOverride?: string,
|
|
272
|
+
): Promise<void> {
|
|
273
|
+
// Skip if all loaders from this entry have already been resolved
|
|
274
|
+
// via a parent (e.g., cache boundary wrapping a layout with shared loaders).
|
|
275
|
+
const loaderEntries = entry.loader ?? [];
|
|
276
|
+
const sc = shortCodeOverride ?? entry.shortCode;
|
|
277
|
+
const allAlreadySeen =
|
|
278
|
+
loaderEntries.length > 0 &&
|
|
279
|
+
loaderEntries.every((le, i) =>
|
|
280
|
+
seenIds.has(`${sc}D${i}.${le.loader.$$id}`),
|
|
281
|
+
);
|
|
282
|
+
if (!allAlreadySeen) {
|
|
283
|
+
const { segments, matchedIds } = await resolveLoadersWithRevalidation(
|
|
284
|
+
entry,
|
|
285
|
+
context,
|
|
286
|
+
belongsToRoute,
|
|
287
|
+
clientSegmentIds,
|
|
288
|
+
prevParams,
|
|
289
|
+
request,
|
|
290
|
+
prevUrl,
|
|
291
|
+
nextUrl,
|
|
292
|
+
routeKey,
|
|
293
|
+
deps,
|
|
294
|
+
actionContext,
|
|
295
|
+
shortCodeOverride,
|
|
296
|
+
stale,
|
|
297
|
+
);
|
|
298
|
+
for (const seg of segments) {
|
|
299
|
+
if (!seenIds.has(seg.id)) {
|
|
300
|
+
seenIds.add(seg.id);
|
|
301
|
+
allLoaderSegments.push(seg);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
allMatchedIds.push(...matchedIds);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const seenParallelEntryIds = new Set<string>();
|
|
308
|
+
for (const parallelEntry of getParallelEntries(entry.parallel)) {
|
|
309
|
+
if (seenParallelEntryIds.has(parallelEntry.id)) continue;
|
|
310
|
+
seenParallelEntryIds.add(parallelEntry.id);
|
|
311
|
+
await collectEntryLoaders(parallelEntry, belongsToRoute, entry.shortCode);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const childBelongsToRoute = belongsToRoute || entry.type === "route";
|
|
315
|
+
for (const layoutEntry of entry.layout) {
|
|
316
|
+
await collectEntryLoaders(layoutEntry, childBelongsToRoute);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
261
319
|
|
|
262
320
|
for (const entry of entries) {
|
|
263
|
-
|
|
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);
|
|
321
|
+
await collectEntryLoaders(entry, entry.type === "route");
|
|
281
322
|
}
|
|
282
323
|
|
|
283
324
|
return { segments: allLoaderSegments, matchedIds: allMatchedIds };
|
|
@@ -301,22 +342,20 @@ export function buildEntryRevalidateMap(
|
|
|
301
342
|
map.set(entry.shortCode, { entry, revalidate: entry.revalidate });
|
|
302
343
|
|
|
303
344
|
if (entry.type !== "parallel") {
|
|
304
|
-
for (const parallelEntry of
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
}
|
|
345
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
346
|
+
entry.parallel,
|
|
347
|
+
)) {
|
|
348
|
+
const parallelParentShortCode = parentShortCode ?? entry.shortCode;
|
|
349
|
+
const parallelId = `${parallelParentShortCode}.${slot}`;
|
|
350
|
+
map.set(parallelId, {
|
|
351
|
+
entry: parallelEntry,
|
|
352
|
+
revalidate: parallelEntry.revalidate,
|
|
353
|
+
});
|
|
315
354
|
}
|
|
316
355
|
}
|
|
317
356
|
|
|
318
357
|
for (const layoutEntry of entry.layout) {
|
|
319
|
-
processEntry(layoutEntry);
|
|
358
|
+
processEntry(layoutEntry, entry.shortCode);
|
|
320
359
|
}
|
|
321
360
|
}
|
|
322
361
|
|
|
@@ -348,7 +387,10 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
348
387
|
const segments: ResolvedSegment[] = [];
|
|
349
388
|
const matchedIds: string[] = [];
|
|
350
389
|
|
|
351
|
-
|
|
390
|
+
const resolvedParallelEntries = new Set<string>();
|
|
391
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
392
|
+
entry.parallel,
|
|
393
|
+
)) {
|
|
352
394
|
invariant(
|
|
353
395
|
parallelEntry.type === "parallel",
|
|
354
396
|
`Expected parallel entry, got: ${parallelEntry.type}`,
|
|
@@ -359,141 +401,61 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
359
401
|
| ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
360
402
|
| ReactNode
|
|
361
403
|
>;
|
|
404
|
+
// In production, static handler bodies are evicted and the slot value
|
|
405
|
+
// may be undefined. The static store holds the pre-rendered component.
|
|
406
|
+
// We defer the handler check until after tryStaticSlot.
|
|
407
|
+
const handler = slots[slot];
|
|
408
|
+
|
|
409
|
+
const parallelId = `${entry.shortCode}.${slot}`;
|
|
410
|
+
|
|
411
|
+
const isFullRefetch = clientSegmentIds.size === 0;
|
|
412
|
+
const isNewParent = !clientSegmentIds.has(entry.shortCode);
|
|
413
|
+
if (
|
|
414
|
+
isFullRefetch ||
|
|
415
|
+
clientSegmentIds.has(parallelId) ||
|
|
416
|
+
belongsToRoute ||
|
|
417
|
+
isNewParent
|
|
418
|
+
) {
|
|
419
|
+
matchedIds.push(parallelId);
|
|
420
|
+
}
|
|
362
421
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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;
|
|
422
|
+
const shouldResolve = await (async () => {
|
|
423
|
+
if (isFullRefetch) {
|
|
424
|
+
if (isTraceActive()) {
|
|
425
|
+
pushRevalidationTraceEntry({
|
|
426
|
+
segmentId: parallelId,
|
|
427
|
+
segmentType: "parallel",
|
|
428
|
+
belongsToRoute,
|
|
429
|
+
source: "parallel",
|
|
430
|
+
defaultShouldRevalidate: true,
|
|
431
|
+
finalShouldRevalidate: true,
|
|
432
|
+
reason: "full-refetch",
|
|
433
|
+
});
|
|
411
434
|
}
|
|
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);
|
|
435
|
+
return true;
|
|
456
436
|
}
|
|
457
|
-
if (
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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;
|
|
437
|
+
if (!clientSegmentIds.has(parallelId)) {
|
|
438
|
+
const result = belongsToRoute || isNewParent;
|
|
439
|
+
if (isTraceActive()) {
|
|
440
|
+
pushRevalidationTraceEntry({
|
|
441
|
+
segmentId: parallelId,
|
|
442
|
+
segmentType: "parallel",
|
|
443
|
+
belongsToRoute,
|
|
444
|
+
source: "parallel",
|
|
445
|
+
defaultShouldRevalidate: result,
|
|
446
|
+
finalShouldRevalidate: result,
|
|
447
|
+
reason: result ? "new-segment" : "skip-parent-chain",
|
|
448
|
+
});
|
|
486
449
|
}
|
|
450
|
+
return result;
|
|
487
451
|
}
|
|
488
452
|
|
|
489
|
-
|
|
453
|
+
const dummySegment: ResolvedSegment = {
|
|
490
454
|
id: parallelId,
|
|
491
455
|
namespace: parallelEntry.id,
|
|
492
456
|
type: "parallel",
|
|
493
457
|
index: 0,
|
|
494
|
-
component,
|
|
495
|
-
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
496
|
-
transition: parallelEntry.transition,
|
|
458
|
+
component: null as any,
|
|
497
459
|
params,
|
|
498
460
|
slot,
|
|
499
461
|
belongsToRoute,
|
|
@@ -501,28 +463,111 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
501
463
|
...(parallelEntry.mountPath
|
|
502
464
|
? { mountPath: parallelEntry.mountPath }
|
|
503
465
|
: {}),
|
|
504
|
-
}
|
|
505
|
-
}
|
|
466
|
+
};
|
|
506
467
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
parallelEntry,
|
|
510
|
-
context,
|
|
511
|
-
belongsToRoute,
|
|
512
|
-
clientSegmentIds,
|
|
468
|
+
return await evaluateRevalidation({
|
|
469
|
+
segment: dummySegment,
|
|
513
470
|
prevParams,
|
|
471
|
+
getPrevSegment: null,
|
|
514
472
|
request,
|
|
515
473
|
prevUrl,
|
|
516
474
|
nextUrl,
|
|
475
|
+
revalidations: parallelEntry.revalidate.map((fn, i) => ({
|
|
476
|
+
name: `revalidate${i}`,
|
|
477
|
+
fn,
|
|
478
|
+
})),
|
|
517
479
|
routeKey,
|
|
518
|
-
|
|
480
|
+
context,
|
|
519
481
|
actionContext,
|
|
520
|
-
entry.shortCode,
|
|
521
482
|
stale,
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
483
|
+
traceSource: "parallel",
|
|
484
|
+
});
|
|
485
|
+
})();
|
|
486
|
+
emitRevalidationDecision(
|
|
487
|
+
parallelId,
|
|
488
|
+
context.pathname,
|
|
489
|
+
routeKey,
|
|
490
|
+
shouldResolve,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
let component: ReactNode | undefined;
|
|
494
|
+
if (shouldResolve) {
|
|
495
|
+
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
525
496
|
}
|
|
497
|
+
if (component === undefined) {
|
|
498
|
+
const hasLoadingFallback =
|
|
499
|
+
parallelEntry.loading !== undefined && parallelEntry.loading !== false;
|
|
500
|
+
if (!shouldResolve) {
|
|
501
|
+
component = null;
|
|
502
|
+
} else if (handler === undefined) {
|
|
503
|
+
// Handler evicted (production static slot) but static lookup missed.
|
|
504
|
+
// Nothing to render — use null so the client keeps its cached version.
|
|
505
|
+
component = null;
|
|
506
|
+
} else if (hasLoadingFallback) {
|
|
507
|
+
const result =
|
|
508
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
509
|
+
if (result instanceof Promise) {
|
|
510
|
+
const tracked = deps.trackHandler(result, {
|
|
511
|
+
segmentId: parallelId,
|
|
512
|
+
segmentType: "parallel",
|
|
513
|
+
});
|
|
514
|
+
observeStreamedHandler(
|
|
515
|
+
tracked,
|
|
516
|
+
parallelId,
|
|
517
|
+
"parallel",
|
|
518
|
+
context.pathname,
|
|
519
|
+
routeKey,
|
|
520
|
+
params,
|
|
521
|
+
);
|
|
522
|
+
component = tracked as ReactNode;
|
|
523
|
+
} else {
|
|
524
|
+
component = result as ReactNode;
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
component =
|
|
528
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
segments.push({
|
|
533
|
+
id: parallelId,
|
|
534
|
+
namespace: parallelEntry.id,
|
|
535
|
+
type: "parallel",
|
|
536
|
+
index: 0,
|
|
537
|
+
component,
|
|
538
|
+
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
539
|
+
transition: parallelEntry.transition,
|
|
540
|
+
params,
|
|
541
|
+
slot,
|
|
542
|
+
belongsToRoute,
|
|
543
|
+
parallelName: `${parallelEntry.id}.${slot}`,
|
|
544
|
+
...(parallelEntry.mountPath
|
|
545
|
+
? { mountPath: parallelEntry.mountPath }
|
|
546
|
+
: {}),
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if (resolvedParallelEntries.has(parallelEntry.id)) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const loaderResult = await resolveLoadersWithRevalidation(
|
|
554
|
+
parallelEntry,
|
|
555
|
+
context,
|
|
556
|
+
belongsToRoute,
|
|
557
|
+
clientSegmentIds,
|
|
558
|
+
prevParams,
|
|
559
|
+
request,
|
|
560
|
+
prevUrl,
|
|
561
|
+
nextUrl,
|
|
562
|
+
routeKey,
|
|
563
|
+
deps,
|
|
564
|
+
actionContext,
|
|
565
|
+
entry.shortCode,
|
|
566
|
+
stale,
|
|
567
|
+
);
|
|
568
|
+
segments.push(...loaderResult.segments);
|
|
569
|
+
matchedIds.push(...loaderResult.matchedIds);
|
|
570
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
526
571
|
}
|
|
527
572
|
|
|
528
573
|
return { segments, matchedIds };
|
|
@@ -608,6 +653,8 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
608
653
|
context,
|
|
609
654
|
actionContext,
|
|
610
655
|
stale,
|
|
656
|
+
traceSource:
|
|
657
|
+
entry.type === "route" ? "route-handler" : "layout-handler",
|
|
611
658
|
});
|
|
612
659
|
emitRevalidationDecision(
|
|
613
660
|
entry.shortCode,
|
|
@@ -676,10 +723,12 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
676
723
|
() => null,
|
|
677
724
|
);
|
|
678
725
|
|
|
726
|
+
// Normalize void handlers (undefined) to null so the reconciler's
|
|
727
|
+
// component === null checks work consistently for both void and explicit null.
|
|
679
728
|
const resolvedComponent =
|
|
680
729
|
component && typeof component === "object" && "content" in component
|
|
681
|
-
? (component as { content: ReactNode }).content
|
|
682
|
-
: component;
|
|
730
|
+
? ((component as { content: ReactNode }).content ?? null)
|
|
731
|
+
: (component ?? null);
|
|
683
732
|
|
|
684
733
|
const segment: ResolvedSegment = {
|
|
685
734
|
id: entry.shortCode,
|
|
@@ -995,143 +1044,72 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
995
1044
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
996
1045
|
});
|
|
997
1046
|
|
|
998
|
-
|
|
1047
|
+
const resolvedParallelEntries = new Set<string>();
|
|
1048
|
+
for (const { slot, entry: parallelEntry } of getParallelSlotEntries(
|
|
1049
|
+
orphan.parallel,
|
|
1050
|
+
)) {
|
|
999
1051
|
invariant(
|
|
1000
1052
|
parallelEntry.type === "parallel",
|
|
1001
1053
|
`Expected parallel entry, got: ${parallelEntry.type}`,
|
|
1002
1054
|
);
|
|
1003
1055
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1056
|
+
if (!resolvedParallelEntries.has(parallelEntry.id)) {
|
|
1057
|
+
const loaderResult = await resolveLoadersWithRevalidation(
|
|
1058
|
+
parallelEntry,
|
|
1059
|
+
context,
|
|
1060
|
+
belongsToRoute,
|
|
1061
|
+
clientSegmentIds,
|
|
1062
|
+
prevParams,
|
|
1063
|
+
request,
|
|
1064
|
+
prevUrl,
|
|
1065
|
+
nextUrl,
|
|
1066
|
+
routeKey,
|
|
1067
|
+
deps,
|
|
1068
|
+
actionContext,
|
|
1069
|
+
undefined,
|
|
1070
|
+
stale,
|
|
1071
|
+
);
|
|
1072
|
+
segments.push(...loaderResult.segments);
|
|
1073
|
+
matchedIds.push(...loaderResult.matchedIds);
|
|
1074
|
+
resolvedParallelEntries.add(parallelEntry.id);
|
|
1075
|
+
}
|
|
1021
1076
|
|
|
1022
1077
|
const slots = parallelEntry.handler as Record<
|
|
1023
1078
|
`@${string}`,
|
|
1024
1079
|
| ((ctx: HandlerContext<any, TEnv>) => ReactNode | Promise<ReactNode>)
|
|
1025
1080
|
| ReactNode
|
|
1026
1081
|
>;
|
|
1082
|
+
// Handler may be undefined in production after static handler eviction.
|
|
1083
|
+
const handler = slots[slot];
|
|
1027
1084
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
matchedIds.push(parallelId);
|
|
1085
|
+
// Use orphan.shortCode (the parent layout) to match the SSR path
|
|
1086
|
+
// (resolveParallelEntry receives parentShortCode = orphan.shortCode).
|
|
1087
|
+
// Using parallelEntry.shortCode would generate IDs the client doesn't know about.
|
|
1088
|
+
const parallelId = `${orphan.shortCode}.${slot}`;
|
|
1089
|
+
matchedIds.push(parallelId);
|
|
1034
1090
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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;
|
|
1091
|
+
const shouldResolve = await (async () => {
|
|
1092
|
+
if (!clientSegmentIds.has(parallelId)) {
|
|
1093
|
+
if (isTraceActive()) {
|
|
1094
|
+
pushRevalidationTraceEntry({
|
|
1095
|
+
segmentId: parallelId,
|
|
1096
|
+
segmentType: "parallel",
|
|
1097
|
+
belongsToRoute,
|
|
1098
|
+
source: "parallel",
|
|
1099
|
+
defaultShouldRevalidate: true,
|
|
1100
|
+
finalShouldRevalidate: true,
|
|
1101
|
+
reason: "new-segment",
|
|
1102
|
+
});
|
|
1124
1103
|
}
|
|
1104
|
+
return true;
|
|
1125
1105
|
}
|
|
1126
1106
|
|
|
1127
|
-
|
|
1107
|
+
const dummySegment: ResolvedSegment = {
|
|
1128
1108
|
id: parallelId,
|
|
1129
1109
|
namespace: parallelEntry.id,
|
|
1130
1110
|
type: "parallel",
|
|
1131
1111
|
index: 0,
|
|
1132
|
-
component,
|
|
1133
|
-
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
1134
|
-
transition: parallelEntry.transition,
|
|
1112
|
+
component: null as any,
|
|
1135
1113
|
params,
|
|
1136
1114
|
slot,
|
|
1137
1115
|
belongsToRoute,
|
|
@@ -1139,8 +1117,87 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1139
1117
|
...(parallelEntry.mountPath
|
|
1140
1118
|
? { mountPath: parallelEntry.mountPath }
|
|
1141
1119
|
: {}),
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
return await evaluateRevalidation({
|
|
1123
|
+
segment: dummySegment,
|
|
1124
|
+
prevParams,
|
|
1125
|
+
getPrevSegment: null,
|
|
1126
|
+
request,
|
|
1127
|
+
prevUrl,
|
|
1128
|
+
nextUrl,
|
|
1129
|
+
revalidations: parallelEntry.revalidate.map((fn, i) => ({
|
|
1130
|
+
name: `revalidate${i}`,
|
|
1131
|
+
fn,
|
|
1132
|
+
})),
|
|
1133
|
+
routeKey,
|
|
1134
|
+
context,
|
|
1135
|
+
actionContext,
|
|
1136
|
+
stale,
|
|
1137
|
+
traceSource: "parallel",
|
|
1142
1138
|
});
|
|
1139
|
+
})();
|
|
1140
|
+
emitRevalidationDecision(
|
|
1141
|
+
parallelId,
|
|
1142
|
+
context.pathname,
|
|
1143
|
+
routeKey,
|
|
1144
|
+
shouldResolve,
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
let component: ReactNode | undefined;
|
|
1148
|
+
if (shouldResolve) {
|
|
1149
|
+
component = await tryStaticSlot(parallelEntry, slot, parallelId);
|
|
1150
|
+
}
|
|
1151
|
+
if (component === undefined) {
|
|
1152
|
+
const hasLoadingFallback =
|
|
1153
|
+
parallelEntry.loading !== undefined && parallelEntry.loading !== false;
|
|
1154
|
+
if (!shouldResolve) {
|
|
1155
|
+
component = null;
|
|
1156
|
+
} else if (handler === undefined) {
|
|
1157
|
+
// Handler evicted (production static slot) but static lookup missed.
|
|
1158
|
+
component = null;
|
|
1159
|
+
} else if (hasLoadingFallback) {
|
|
1160
|
+
const result =
|
|
1161
|
+
typeof handler === "function" ? handler(context) : handler;
|
|
1162
|
+
if (result instanceof Promise) {
|
|
1163
|
+
const tracked = deps.trackHandler(result, {
|
|
1164
|
+
segmentId: parallelId,
|
|
1165
|
+
segmentType: "parallel",
|
|
1166
|
+
});
|
|
1167
|
+
observeStreamedHandler(
|
|
1168
|
+
tracked,
|
|
1169
|
+
parallelId,
|
|
1170
|
+
"parallel",
|
|
1171
|
+
context.pathname,
|
|
1172
|
+
routeKey,
|
|
1173
|
+
params,
|
|
1174
|
+
);
|
|
1175
|
+
component = tracked as ReactNode;
|
|
1176
|
+
} else {
|
|
1177
|
+
component = result as ReactNode;
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
1180
|
+
component =
|
|
1181
|
+
typeof handler === "function" ? await handler(context) : handler;
|
|
1182
|
+
}
|
|
1143
1183
|
}
|
|
1184
|
+
|
|
1185
|
+
segments.push({
|
|
1186
|
+
id: parallelId,
|
|
1187
|
+
namespace: parallelEntry.id,
|
|
1188
|
+
type: "parallel",
|
|
1189
|
+
index: 0,
|
|
1190
|
+
component,
|
|
1191
|
+
loading: parallelEntry.loading === false ? null : parallelEntry.loading,
|
|
1192
|
+
transition: parallelEntry.transition,
|
|
1193
|
+
params,
|
|
1194
|
+
slot,
|
|
1195
|
+
belongsToRoute,
|
|
1196
|
+
parallelName: `${parallelEntry.id}.${slot}`,
|
|
1197
|
+
...(parallelEntry.mountPath
|
|
1198
|
+
? { mountPath: parallelEntry.mountPath }
|
|
1199
|
+
: {}),
|
|
1200
|
+
});
|
|
1144
1201
|
}
|
|
1145
1202
|
|
|
1146
1203
|
return { segments, matchedIds };
|
|
@@ -1165,6 +1222,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1165
1222
|
localRouteName: string,
|
|
1166
1223
|
pathname: string,
|
|
1167
1224
|
deps: SegmentResolutionDeps<TEnv>,
|
|
1225
|
+
stale?: boolean,
|
|
1168
1226
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
1169
1227
|
const allSegments: ResolvedSegment[] = [];
|
|
1170
1228
|
const matchedIds: string[] = [];
|
|
@@ -1191,6 +1249,10 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1191
1249
|
}
|
|
1192
1250
|
|
|
1193
1251
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1252
|
+
if (entry.type === "cache") {
|
|
1253
|
+
const store = RSCRouterContext.getStore();
|
|
1254
|
+
if (store) store.insideCacheScope = true;
|
|
1255
|
+
}
|
|
1194
1256
|
const doneEntry = track(`segment:${entry.id}`, 1);
|
|
1195
1257
|
const resolved = await resolveWithErrorBoundary(
|
|
1196
1258
|
nonParallelEntry,
|
|
@@ -1209,7 +1271,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1209
1271
|
loaderPromises,
|
|
1210
1272
|
deps,
|
|
1211
1273
|
actionContext,
|
|
1212
|
-
|
|
1274
|
+
stale,
|
|
1213
1275
|
),
|
|
1214
1276
|
(seg) => ({ segments: [seg], matchedIds: [seg.id] }),
|
|
1215
1277
|
deps,
|