@rangojs/router 0.0.0-experimental.cb54cbba → 0.0.0-experimental.debug-cache-2383ca26
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/AGENTS.md +4 -0
- package/dist/bin/rango.js +8 -3
- package/dist/vite/index.js +139 -200
- package/package.json +15 -14
- package/skills/caching/SKILL.md +37 -4
- package/skills/parallel/SKILL.md +126 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/navigation-bridge.ts +1 -3
- package/src/browser/navigation-client.ts +60 -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 +53 -13
- package/src/browser/react/Link.tsx +9 -1
- package/src/browser/react/NavigationProvider.tsx +27 -0
- 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/types.ts +9 -0
- package/src/build/route-types/router-processing.ts +12 -2
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +43 -3
- 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/debug.ts +2 -2
- package/src/route-definition/dsl-helpers.ts +32 -7
- 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/intercept-resolution.ts +2 -0
- package/src/router/lazy-includes.ts +4 -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.ts +2 -1
- package/src/router/router-context.ts +6 -1
- package/src/router/segment-resolution/fresh.ts +122 -15
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +347 -290
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router.ts +5 -1
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +90 -13
- package/src/server/request-context.ts +10 -4
- package/src/ssr/index.tsx +1 -0
- package/src/types/handler-context.ts +103 -17
- 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/version-plugin.ts +13 -1
- package/src/vite/rango.ts +144 -209
- package/src/vite/router-discovery.ts +0 -8
- package/src/vite/utils/banner.ts +3 -3
|
@@ -204,6 +204,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
204
204
|
interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
|
|
205
205
|
localRouteName: string,
|
|
206
206
|
pathname: string,
|
|
207
|
+
stale?: boolean,
|
|
207
208
|
): ReturnType<typeof _resolveAllSegmentsWithRevalidation> {
|
|
208
209
|
return _resolveAllSegmentsWithRevalidation(
|
|
209
210
|
entries,
|
|
@@ -221,6 +222,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
221
222
|
localRouteName,
|
|
222
223
|
pathname,
|
|
223
224
|
segmentDeps,
|
|
225
|
+
stale,
|
|
224
226
|
);
|
|
225
227
|
}
|
|
226
228
|
|
package/src/router.ts
CHANGED
|
@@ -560,6 +560,7 @@ export function createRouter<TEnv = any>(
|
|
|
560
560
|
mergedRouteMap,
|
|
561
561
|
nextMountIndex: () => mountIndex++,
|
|
562
562
|
getPrecomputedByPrefix,
|
|
563
|
+
routerId,
|
|
563
564
|
};
|
|
564
565
|
|
|
565
566
|
function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
|
|
@@ -689,7 +690,7 @@ export function createRouter<TEnv = any>(
|
|
|
689
690
|
errorBoundary: [],
|
|
690
691
|
notFoundBoundary: [],
|
|
691
692
|
layout: [],
|
|
692
|
-
parallel:
|
|
693
|
+
parallel: {},
|
|
693
694
|
intercept: [],
|
|
694
695
|
loader: [],
|
|
695
696
|
};
|
|
@@ -751,6 +752,7 @@ export function createRouter<TEnv = any>(
|
|
|
751
752
|
trailingSlash: trailingSlashConfig,
|
|
752
753
|
handler: urlPatterns.handler,
|
|
753
754
|
mountIndex: currentMountIndex,
|
|
755
|
+
routerId,
|
|
754
756
|
cacheProfiles: resolvedCacheProfiles,
|
|
755
757
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
756
758
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -770,6 +772,7 @@ export function createRouter<TEnv = any>(
|
|
|
770
772
|
trailingSlash: trailingSlashConfig,
|
|
771
773
|
handler: urlPatterns.handler,
|
|
772
774
|
mountIndex: currentMountIndex,
|
|
775
|
+
routerId,
|
|
773
776
|
cacheProfiles: resolvedCacheProfiles,
|
|
774
777
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
775
778
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -813,6 +816,7 @@ export function createRouter<TEnv = any>(
|
|
|
813
816
|
trailingSlash: trailingSlashConfig,
|
|
814
817
|
handler: urlPatterns.handler,
|
|
815
818
|
mountIndex: mountIndex++,
|
|
819
|
+
routerId,
|
|
816
820
|
// Lazy evaluation fields
|
|
817
821
|
lazy: true,
|
|
818
822
|
lazyPatterns: lazyInclude.patterns,
|
package/src/segment-system.tsx
CHANGED
|
@@ -20,6 +20,61 @@ import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
|
20
20
|
const ReactViewTransition: any =
|
|
21
21
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
22
22
|
|
|
23
|
+
function restoreParallelLoaderMarkers(
|
|
24
|
+
segments: ResolvedSegment[],
|
|
25
|
+
): ResolvedSegment[] {
|
|
26
|
+
const parallelLoadingByNamespace = new Map<string, ReactNode>();
|
|
27
|
+
let nextSegments: ResolvedSegment[] | null = null;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < segments.length; i++) {
|
|
30
|
+
const segment = segments[i];
|
|
31
|
+
|
|
32
|
+
if (segment.type === "parallel") {
|
|
33
|
+
if (
|
|
34
|
+
segment.namespace &&
|
|
35
|
+
segment.loading !== undefined &&
|
|
36
|
+
segment.loading !== null &&
|
|
37
|
+
segment.loading !== false
|
|
38
|
+
) {
|
|
39
|
+
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parallelLoading = segment.namespace
|
|
49
|
+
? parallelLoadingByNamespace.get(segment.namespace)
|
|
50
|
+
: undefined;
|
|
51
|
+
if (parallelLoading === undefined) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!nextSegments) {
|
|
56
|
+
nextSegments = segments.slice();
|
|
57
|
+
}
|
|
58
|
+
nextSegments[i] = { ...segment, parallelLoading };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return nextSegments ?? segments;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
|
|
65
|
+
if (!a || a.length !== b.length) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < a.length; i++) {
|
|
70
|
+
if (a[i] !== b[i]) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
23
78
|
/**
|
|
24
79
|
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
25
80
|
*/
|
|
@@ -143,6 +198,10 @@ export async function renderSegments(
|
|
|
143
198
|
} = options || {};
|
|
144
199
|
|
|
145
200
|
const temporalLazyRefs: Promise<any>[] = [];
|
|
201
|
+
const normalizedSegments = restoreParallelLoaderMarkers(segments);
|
|
202
|
+
const normalizedInterceptSegments = interceptSegments
|
|
203
|
+
? restoreParallelLoaderMarkers(interceptSegments)
|
|
204
|
+
: undefined;
|
|
146
205
|
|
|
147
206
|
/**
|
|
148
207
|
* Registers promises from lazy/async components for awaiting.
|
|
@@ -167,7 +226,7 @@ export async function renderSegments(
|
|
|
167
226
|
);
|
|
168
227
|
}
|
|
169
228
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
170
|
-
const tree = segmentTreeWalk(
|
|
229
|
+
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
171
230
|
// Render content segments as siblings
|
|
172
231
|
let content: ReactNode = null;
|
|
173
232
|
for (const node of tree) {
|
|
@@ -284,13 +343,90 @@ export async function renderSegments(
|
|
|
284
343
|
children: nodeContent,
|
|
285
344
|
});
|
|
286
345
|
} else {
|
|
287
|
-
// Has loaders but no loading skeleton
|
|
288
|
-
|
|
346
|
+
// Has loaders but no loading skeleton.
|
|
347
|
+
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
348
|
+
// layout-owned loaders are awaited (they gate the layout content).
|
|
349
|
+
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
350
|
+
const parallelOwnedLoaders = loaderEntries.filter(
|
|
351
|
+
(l) => !!l.parallelLoading,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
// Await only layout-owned loaders
|
|
355
|
+
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
356
|
+
const layoutLoaderDataPromise =
|
|
357
|
+
layoutLoaders.length > 0
|
|
358
|
+
? Promise.all(
|
|
359
|
+
layoutLoaders.map((l) =>
|
|
360
|
+
l.loaderData instanceof Promise
|
|
361
|
+
? l.loaderData
|
|
362
|
+
: Promise.resolve(l.loaderData),
|
|
363
|
+
),
|
|
364
|
+
)
|
|
365
|
+
: Promise.resolve([]);
|
|
366
|
+
const resolvedData = await layoutLoaderDataPromise;
|
|
289
367
|
const { loaderData, errorFallback } = resolveLoaderData(
|
|
290
368
|
resolvedData,
|
|
291
|
-
|
|
369
|
+
layoutLoaderIds,
|
|
292
370
|
);
|
|
293
371
|
|
|
372
|
+
// Parallel-owned loaders: attach to their owning parallel segment
|
|
373
|
+
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
374
|
+
if (parallelOwnedLoaders.length > 0) {
|
|
375
|
+
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
376
|
+
|
|
377
|
+
for (const loader of parallelOwnedLoaders) {
|
|
378
|
+
if (!loader.namespace) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const existing = loadersByParallelNamespace.get(loader.namespace);
|
|
382
|
+
if (existing) {
|
|
383
|
+
existing.push(loader);
|
|
384
|
+
} else {
|
|
385
|
+
loadersByParallelNamespace.set(loader.namespace, [loader]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const p of node.parallel) {
|
|
390
|
+
if (!p.loading || !p.namespace) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
|
|
395
|
+
if (!ownedLoaders || ownedLoaders.length === 0) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const parallelLoaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
400
|
+
const parallelLoaderSources = ownedLoaders.map((l) => l.loaderData);
|
|
401
|
+
p.loaderIds = parallelLoaderIds;
|
|
402
|
+
|
|
403
|
+
const shouldReuseParallelPromise =
|
|
404
|
+
p.loaderDataPromise !== undefined &&
|
|
405
|
+
hasSameReferences(p.parallelLoaderSources, parallelLoaderSources);
|
|
406
|
+
|
|
407
|
+
const parallelLoaderDataPromise = shouldReuseParallelPromise
|
|
408
|
+
? p.loaderDataPromise
|
|
409
|
+
: forceAwait || isAction
|
|
410
|
+
? await Promise.all(
|
|
411
|
+
ownedLoaders.map((l) =>
|
|
412
|
+
l.loaderData instanceof Promise
|
|
413
|
+
? l.loaderData
|
|
414
|
+
: Promise.resolve(l.loaderData),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
: Promise.all(
|
|
418
|
+
ownedLoaders.map((l) =>
|
|
419
|
+
l.loaderData instanceof Promise
|
|
420
|
+
? l.loaderData
|
|
421
|
+
: Promise.resolve(l.loaderData),
|
|
422
|
+
),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
p.loaderDataPromise = parallelLoaderDataPromise;
|
|
426
|
+
p.parallelLoaderSources = parallelLoaderSources;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
294
430
|
content = createElement(OutletProvider, {
|
|
295
431
|
key,
|
|
296
432
|
content: outletContent,
|
package/src/server/context.ts
CHANGED
|
@@ -157,10 +157,24 @@ export type InterceptEntry = {
|
|
|
157
157
|
when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
+
export interface ParallelEntryData
|
|
161
|
+
extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
|
|
162
|
+
type: "parallel";
|
|
163
|
+
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
164
|
+
loading?: ReactNode | false;
|
|
165
|
+
transition?: TransitionConfig;
|
|
166
|
+
/** Set when any parallel slot is a Static definition */
|
|
167
|
+
isStaticPrerender?: true;
|
|
168
|
+
/** Per-slot static handler $$ids for build-time store lookup */
|
|
169
|
+
staticHandlerIds?: Record<string, string>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
|
|
173
|
+
|
|
160
174
|
export type EntryPropSegments = {
|
|
161
175
|
loader: LoaderEntry[];
|
|
162
176
|
layout: EntryData[];
|
|
163
|
-
parallel:
|
|
177
|
+
parallel: ParallelEntries; // slot -> parallel entry (same entry may back multiple slots)
|
|
164
178
|
intercept: InterceptEntry[]; // intercept definitions for soft navigation
|
|
165
179
|
};
|
|
166
180
|
|
|
@@ -200,18 +214,7 @@ export type EntryData =
|
|
|
200
214
|
} & EntryPropCommon &
|
|
201
215
|
EntryPropDatas &
|
|
202
216
|
EntryPropSegments)
|
|
203
|
-
|
|
|
204
|
-
type: "parallel";
|
|
205
|
-
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
206
|
-
loading?: ReactNode | false;
|
|
207
|
-
transition?: TransitionConfig;
|
|
208
|
-
/** Set when any parallel slot is a Static definition */
|
|
209
|
-
isStaticPrerender?: true;
|
|
210
|
-
/** Per-slot static handler $$ids for build-time store lookup */
|
|
211
|
-
staticHandlerIds?: Record<string, string>;
|
|
212
|
-
} & EntryPropCommon &
|
|
213
|
-
EntryPropDatas &
|
|
214
|
-
EntryPropSegments)
|
|
217
|
+
| ParallelEntryData
|
|
215
218
|
| ({
|
|
216
219
|
type: "cache";
|
|
217
220
|
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
@@ -553,6 +556,80 @@ export function getRootScoped(): boolean {
|
|
|
553
556
|
// Export HelperContext type for use in other modules
|
|
554
557
|
export type { HelperContext };
|
|
555
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Return an isolated copy of a lazy include's captured parent entry.
|
|
561
|
+
*
|
|
562
|
+
* DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
|
|
563
|
+
* Multiple include() scopes capture the *same* syntheticMapRoot as their
|
|
564
|
+
* parent, so without isolation one include's loaders/middleware leak into
|
|
565
|
+
* every other route that shares that root.
|
|
566
|
+
*
|
|
567
|
+
* The clone is shallow: only the mutable arrays are copied so each
|
|
568
|
+
* include pushes to its own list. The rest of the entry (id, shortCode,
|
|
569
|
+
* parent pointer, handler) stays shared, which is correct and cheap.
|
|
570
|
+
*/
|
|
571
|
+
export function getIsolatedLazyParent(
|
|
572
|
+
captured: EntryData | null | undefined,
|
|
573
|
+
): EntryData | null {
|
|
574
|
+
if (!captured) return null;
|
|
575
|
+
return {
|
|
576
|
+
...captured,
|
|
577
|
+
loader: [...captured.loader],
|
|
578
|
+
middleware: [...captured.middleware],
|
|
579
|
+
revalidate: [...captured.revalidate],
|
|
580
|
+
errorBoundary: [...captured.errorBoundary],
|
|
581
|
+
notFoundBoundary: [...captured.notFoundBoundary],
|
|
582
|
+
layout: [...captured.layout],
|
|
583
|
+
parallel: { ...captured.parallel },
|
|
584
|
+
intercept: [...captured.intercept],
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function getParallelEntries(
|
|
589
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
590
|
+
): ParallelEntryData[] {
|
|
591
|
+
if (!parallels) return [];
|
|
592
|
+
if (Array.isArray(parallels)) {
|
|
593
|
+
return parallels.filter(
|
|
594
|
+
(entry): entry is ParallelEntryData => entry.type === "parallel",
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
return Object.values(parallels).filter(
|
|
598
|
+
(entry): entry is ParallelEntryData => !!entry,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function getParallelSlotEntries(
|
|
603
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
604
|
+
): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
|
|
605
|
+
if (!parallels) return [];
|
|
606
|
+
|
|
607
|
+
if (Array.isArray(parallels)) {
|
|
608
|
+
return getParallelEntries(parallels).flatMap((entry) =>
|
|
609
|
+
(Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
|
|
610
|
+
slot,
|
|
611
|
+
entry,
|
|
612
|
+
})),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return Object.entries(parallels)
|
|
617
|
+
.filter(([, entry]) => !!entry)
|
|
618
|
+
.map(([slot, entry]) => ({
|
|
619
|
+
slot: slot as `@${string}`,
|
|
620
|
+
entry: entry!,
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function getParallelSlotCount(
|
|
625
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
626
|
+
): number {
|
|
627
|
+
if (!parallels) return 0;
|
|
628
|
+
return Array.isArray(parallels)
|
|
629
|
+
? parallels.filter((entry) => entry?.type === "parallel").length
|
|
630
|
+
: Object.keys(parallels).length;
|
|
631
|
+
}
|
|
632
|
+
|
|
556
633
|
// ============================================================================
|
|
557
634
|
// Performance Metrics Helpers
|
|
558
635
|
// ============================================================================
|
|
@@ -30,7 +30,10 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
|
30
30
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
31
31
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
32
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
createReverseFunction,
|
|
35
|
+
stripInternalParams,
|
|
36
|
+
} from "../router/handler-context.js";
|
|
34
37
|
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
35
38
|
import { invariant } from "../errors.js";
|
|
36
39
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
@@ -58,7 +61,7 @@ export interface RequestContext<
|
|
|
58
61
|
originalUrl: URL;
|
|
59
62
|
/** URL pathname */
|
|
60
63
|
pathname: string;
|
|
61
|
-
/** URL search params (
|
|
64
|
+
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
62
65
|
searchParams: URLSearchParams;
|
|
63
66
|
/** Variables set by middleware (same as ctx.var) */
|
|
64
67
|
var: Record<string, any>;
|
|
@@ -555,14 +558,17 @@ export function createRequestContext<TEnv>(
|
|
|
555
558
|
invalidateResponseCookieCache();
|
|
556
559
|
};
|
|
557
560
|
|
|
561
|
+
// Strip internal _rsc* params so userland sees a clean URL.
|
|
562
|
+
const cleanUrl = stripInternalParams(url);
|
|
563
|
+
|
|
558
564
|
// Build the context object first (without use), then add use
|
|
559
565
|
const ctx: RequestContext<TEnv> = {
|
|
560
566
|
env,
|
|
561
567
|
request,
|
|
562
|
-
url,
|
|
568
|
+
url: cleanUrl,
|
|
563
569
|
originalUrl: new URL(request.url),
|
|
564
570
|
pathname: url.pathname,
|
|
565
|
-
searchParams:
|
|
571
|
+
searchParams: cleanUrl.searchParams,
|
|
566
572
|
var: variables,
|
|
567
573
|
get: ((keyOrVar: any) =>
|
|
568
574
|
contextGet(variables, keyOrVar)) as RequestContext<TEnv>["get"],
|
package/src/ssr/index.tsx
CHANGED
|
@@ -289,8 +289,12 @@ export type HandlerContext<
|
|
|
289
289
|
/**
|
|
290
290
|
* Access loader data or push handle data.
|
|
291
291
|
*
|
|
292
|
+
* Available in route handlers, layout handlers, middleware, server actions,
|
|
293
|
+
* and server components rendered within the request context.
|
|
294
|
+
*
|
|
292
295
|
* For loaders: Returns a promise that resolves to the loader data.
|
|
293
|
-
* Loaders are executed in parallel and memoized per request
|
|
296
|
+
* Loaders are executed in parallel and memoized per request — calling
|
|
297
|
+
* `ctx.use(SameLoader)` multiple times returns the same promise.
|
|
294
298
|
*
|
|
295
299
|
* For handles: Returns a push function to add data for this segment.
|
|
296
300
|
* Handle data accumulates across all matched route segments.
|
|
@@ -519,30 +523,112 @@ export type RevalidateParams<TParams = GenericParams, TEnv = any> = Parameters<
|
|
|
519
523
|
* })
|
|
520
524
|
* ```
|
|
521
525
|
*/
|
|
526
|
+
/**
|
|
527
|
+
* Revalidation function called during client-side navigation to decide whether
|
|
528
|
+
* a segment (layout, route, parallel slot, or loader) should be re-rendered.
|
|
529
|
+
*
|
|
530
|
+
* Return `true` to re-render, `false` to skip (keep client's current version),
|
|
531
|
+
* or `{ defaultShouldRevalidate: boolean }` to override the default for
|
|
532
|
+
* downstream segments.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* // Re-render only when a cart action happened or browser signals staleness
|
|
537
|
+
* revalidate(({ actionId, stale }) =>
|
|
538
|
+
* actionId?.includes("cart") || stale || false
|
|
539
|
+
* )
|
|
540
|
+
*
|
|
541
|
+
* // Always re-render when params change (default behavior made explicit)
|
|
542
|
+
* revalidate(({ defaultShouldRevalidate }) => defaultShouldRevalidate)
|
|
543
|
+
* ```
|
|
544
|
+
*/
|
|
522
545
|
export type ShouldRevalidateFn<TParams = GenericParams, TEnv = any> = (args: {
|
|
546
|
+
/** Route params from the page being navigated away from. */
|
|
523
547
|
currentParams: TParams;
|
|
548
|
+
/** Full URL of the page being navigated away from. */
|
|
524
549
|
currentUrl: URL;
|
|
550
|
+
/** Route params for the navigation target. */
|
|
525
551
|
nextParams: TParams;
|
|
552
|
+
/** Full URL of the navigation target. */
|
|
526
553
|
nextUrl: URL;
|
|
554
|
+
/**
|
|
555
|
+
* The router's default revalidation decision for this segment.
|
|
556
|
+
* `true` when params changed or the segment is new to the client.
|
|
557
|
+
* Return this when you want default behavior plus your own conditions.
|
|
558
|
+
*/
|
|
527
559
|
defaultShouldRevalidate: boolean;
|
|
560
|
+
/** Full handler context — access to `ctx.use()`, `ctx.env`, `ctx.params`, etc. */
|
|
528
561
|
context: HandlerContext<TParams, TEnv>;
|
|
529
|
-
|
|
562
|
+
|
|
563
|
+
// ── Segment metadata (which segment is being evaluated) ──────────────
|
|
564
|
+
|
|
565
|
+
/** The type of segment being revalidated. */
|
|
530
566
|
segmentType: "layout" | "route" | "parallel";
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
567
|
+
/** Layout name (e.g., `"root"`, `"shop"`, `"auth"`). Only set for layout segments. */
|
|
568
|
+
layoutName?: string;
|
|
569
|
+
/** Slot name (e.g., `"@sidebar"`, `"@modal"`). Only set for parallel segments. */
|
|
570
|
+
slotName?: string;
|
|
571
|
+
|
|
572
|
+
// ── Action context (populated when revalidation is triggered by a server action) ──
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Identifier of the server action that triggered revalidation.
|
|
576
|
+
* `undefined` during normal navigation (no action involved).
|
|
577
|
+
*
|
|
578
|
+
* Format: `"src/<path>#<exportName>"` — the file path is the source path
|
|
579
|
+
* relative to the project root, followed by `#` and the exported function name.
|
|
580
|
+
*
|
|
581
|
+
* This is stable and can be used for path-based matching to revalidate
|
|
582
|
+
* when any action in a module or directory fires:
|
|
583
|
+
*
|
|
584
|
+
* @example
|
|
585
|
+
* ```ts
|
|
586
|
+
* // Match a specific action
|
|
587
|
+
* revalidate(({ actionId }) => actionId === "src/actions/cart.ts#addToCart")
|
|
588
|
+
*
|
|
589
|
+
* // Match any action in the cart module
|
|
590
|
+
* revalidate(({ actionId }) => actionId?.includes("cart") ?? false)
|
|
591
|
+
*
|
|
592
|
+
* // Match any action under src/apps/store/actions/
|
|
593
|
+
* revalidate(({ actionId }) => actionId?.startsWith("src/apps/store/actions/") ?? false)
|
|
594
|
+
* ```
|
|
595
|
+
*/
|
|
596
|
+
actionId?: string;
|
|
597
|
+
/** URL where the action was executed (the page the user was on when they triggered the action). */
|
|
598
|
+
actionUrl?: URL;
|
|
599
|
+
/** Return value from the action execution. Can be used to conditionally revalidate based on the action's outcome. */
|
|
600
|
+
actionResult?: any;
|
|
601
|
+
/** FormData from the action request body. Only set for form-based actions (not inline `"use server"` actions). */
|
|
602
|
+
formData?: FormData;
|
|
603
|
+
/** HTTP method: `"GET"` for navigation, `"POST"` for server actions. */
|
|
604
|
+
method?: string;
|
|
605
|
+
|
|
606
|
+
// ── Route identity ───────────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
/** Route name of the navigation target. Alias for `toRouteName`. */
|
|
609
|
+
routeName?: DefaultRouteName;
|
|
610
|
+
/**
|
|
611
|
+
* Route name being navigated away from.
|
|
612
|
+
* `undefined` for unnamed internal routes (those without a `name` option).
|
|
613
|
+
*/
|
|
614
|
+
fromRouteName?: DefaultRouteName;
|
|
615
|
+
/**
|
|
616
|
+
* Route name being navigated to.
|
|
617
|
+
* `undefined` for unnamed internal routes (those without a `name` option).
|
|
618
|
+
*/
|
|
619
|
+
toRouteName?: DefaultRouteName;
|
|
620
|
+
|
|
621
|
+
// ── Staleness signal ─────────────────────────────────────────────────
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* `true` when the browser signals that data may be stale — typically because
|
|
625
|
+
* a server action was executed in this or another tab (`_rsc_stale` header).
|
|
626
|
+
*
|
|
627
|
+
* This is NOT segment cache staleness (loaders are never segment-cached).
|
|
628
|
+
* Use this to decide whether loader data should be re-fetched after an
|
|
629
|
+
* action that may have mutated backend state.
|
|
630
|
+
*/
|
|
631
|
+
stale?: boolean;
|
|
546
632
|
}) => boolean | { defaultShouldRevalidate: boolean };
|
|
547
633
|
|
|
548
634
|
// MiddlewareFn is imported from "../router/middleware.js" and re-exported
|
package/src/types/route-entry.ts
CHANGED
|
@@ -55,6 +55,13 @@ export interface RouteEntry<TEnv = any> {
|
|
|
55
55
|
| Promise<() => Array<AllUseItems>>;
|
|
56
56
|
mountIndex: number;
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Router ID that owns this entry. Used to namespace the manifest cache
|
|
60
|
+
* so multi-router setups (host routing) don't share cached EntryData
|
|
61
|
+
* across routers with overlapping mountIndex + routeKey combinations.
|
|
62
|
+
*/
|
|
63
|
+
routerId?: string;
|
|
64
|
+
|
|
58
65
|
/**
|
|
59
66
|
* Route keys in this entry that have pre-render handlers.
|
|
60
67
|
* Used by the non-trie match path to set the `pr` flag.
|
package/src/types/segments.ts
CHANGED
|
@@ -51,9 +51,11 @@ export interface ResolvedSegment {
|
|
|
51
51
|
// Loader-specific fields
|
|
52
52
|
loaderId?: string; // For loaders: the loader $$id identifier
|
|
53
53
|
loaderData?: any; // For loaders: the resolved data from loader execution
|
|
54
|
+
parallelLoading?: ReactNode; // For parallel-owned loaders: the parallel's loading fallback
|
|
54
55
|
// Intercept loader fields (for streaming loader data in parallel segments)
|
|
55
56
|
loaderDataPromise?: Promise<any[]> | any[]; // Loader data promise or resolved array
|
|
56
57
|
loaderIds?: string[]; // IDs ($$id) of loaders for this segment
|
|
58
|
+
parallelLoaderSources?: any[]; // Internal: preserves stable aggregate promise across renders
|
|
57
59
|
// Error-specific fields
|
|
58
60
|
error?: ErrorInfo; // For error segments: the error information
|
|
59
61
|
// NotFound-specific fields
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -13,8 +13,6 @@ export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
|
|
|
13
13
|
export interface PluginOptions {
|
|
14
14
|
enableBuildPrerender?: boolean;
|
|
15
15
|
staticRouteTypesGeneration?: boolean;
|
|
16
|
-
include?: string[];
|
|
17
|
-
exclude?: string[];
|
|
18
16
|
// Mutable ref for deferred auto-discovery (node preset).
|
|
19
17
|
// The auto-discover config() hook populates this before configResolved.
|
|
20
18
|
routerPathRef?: { path?: string };
|