@rangojs/router 0.0.0-experimental.f2337aef → 0.0.0-experimental.f681f24a
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 +291 -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 -3
- package/src/browser/navigation-client.ts +76 -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 +20 -7
- 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/router/handler-context.ts +31 -8
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +7 -1
- package/src/router/logging.ts +1 -1
- 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 +7 -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 +1 -0
- 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 +348 -289
- 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 +231 -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
package/src/router/types.ts
CHANGED
|
@@ -96,6 +96,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
96
96
|
findNearestNotFoundBoundary: (
|
|
97
97
|
entry: EntryData | null,
|
|
98
98
|
) => ReactNode | NotFoundBoundaryHandler | null;
|
|
99
|
+
notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
|
|
99
100
|
callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
|
|
100
101
|
}
|
|
101
102
|
|
package/src/router.ts
CHANGED
|
@@ -526,6 +526,7 @@ export function createRouter<TEnv = any>(
|
|
|
526
526
|
trackHandler,
|
|
527
527
|
findNearestErrorBoundary,
|
|
528
528
|
findNearestNotFoundBoundary,
|
|
529
|
+
notFoundComponent: notFound,
|
|
529
530
|
callOnError,
|
|
530
531
|
};
|
|
531
532
|
|
|
@@ -560,6 +561,7 @@ export function createRouter<TEnv = any>(
|
|
|
560
561
|
mergedRouteMap,
|
|
561
562
|
nextMountIndex: () => mountIndex++,
|
|
562
563
|
getPrecomputedByPrefix,
|
|
564
|
+
routerId,
|
|
563
565
|
};
|
|
564
566
|
|
|
565
567
|
function evaluateLazyEntry(entry: RouteEntry<TEnv>): void {
|
|
@@ -689,7 +691,7 @@ export function createRouter<TEnv = any>(
|
|
|
689
691
|
errorBoundary: [],
|
|
690
692
|
notFoundBoundary: [],
|
|
691
693
|
layout: [],
|
|
692
|
-
parallel:
|
|
694
|
+
parallel: {},
|
|
693
695
|
intercept: [],
|
|
694
696
|
loader: [],
|
|
695
697
|
};
|
|
@@ -751,6 +753,7 @@ export function createRouter<TEnv = any>(
|
|
|
751
753
|
trailingSlash: trailingSlashConfig,
|
|
752
754
|
handler: urlPatterns.handler,
|
|
753
755
|
mountIndex: currentMountIndex,
|
|
756
|
+
routerId,
|
|
754
757
|
cacheProfiles: resolvedCacheProfiles,
|
|
755
758
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
756
759
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -770,6 +773,7 @@ export function createRouter<TEnv = any>(
|
|
|
770
773
|
trailingSlash: trailingSlashConfig,
|
|
771
774
|
handler: urlPatterns.handler,
|
|
772
775
|
mountIndex: currentMountIndex,
|
|
776
|
+
routerId,
|
|
773
777
|
cacheProfiles: resolvedCacheProfiles,
|
|
774
778
|
...(prerenderRouteKeys ? { prerenderRouteKeys } : {}),
|
|
775
779
|
...(passthroughRouteKeys ? { passthroughRouteKeys } : {}),
|
|
@@ -813,6 +817,7 @@ export function createRouter<TEnv = any>(
|
|
|
813
817
|
trailingSlash: trailingSlashConfig,
|
|
814
818
|
handler: urlPatterns.handler,
|
|
815
819
|
mountIndex: mountIndex++,
|
|
820
|
+
routerId,
|
|
816
821
|
// Lazy evaluation fields
|
|
817
822
|
lazy: true,
|
|
818
823
|
lazyPatterns: lazyInclude.patterns,
|
package/src/rsc/handler.ts
CHANGED
|
@@ -14,9 +14,14 @@ import {
|
|
|
14
14
|
runWithRequestContext,
|
|
15
15
|
setRequestContextParams,
|
|
16
16
|
requireRequestContext,
|
|
17
|
+
getRequestContext,
|
|
17
18
|
createRequestContext,
|
|
18
19
|
} from "../server/request-context.js";
|
|
19
20
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc";
|
|
21
|
+
import {
|
|
22
|
+
DEBUG_ID_HEADER,
|
|
23
|
+
createServerDebugChannel,
|
|
24
|
+
} from "../vite/plugins/performance-tracks.js";
|
|
20
25
|
|
|
21
26
|
import type {
|
|
22
27
|
RscPayload,
|
|
@@ -262,7 +267,10 @@ export function createRSCHandler<
|
|
|
262
267
|
...(locationState && { locationState }),
|
|
263
268
|
},
|
|
264
269
|
};
|
|
265
|
-
const
|
|
270
|
+
const debugChannel = getRequestContext()?._debugChannel;
|
|
271
|
+
const rscStream = renderToReadableStream<RscPayload>(redirectPayload, {
|
|
272
|
+
...(debugChannel && { debugChannel }),
|
|
273
|
+
});
|
|
266
274
|
return createResponseWithMergedHeaders(rscStream, {
|
|
267
275
|
status: 200,
|
|
268
276
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -418,6 +426,21 @@ export function createRSCHandler<
|
|
|
418
426
|
requestContext._debugPerformance = true;
|
|
419
427
|
requestContext._metricsStore = earlyMetricsStore;
|
|
420
428
|
}
|
|
429
|
+
// Dev-only: wire debug channel for React Performance Tracks
|
|
430
|
+
if (process.env.NODE_ENV !== "production") {
|
|
431
|
+
const debugId = request.headers.get(DEBUG_ID_HEADER);
|
|
432
|
+
console.log("[perf-tracks] handler: debugId header =", debugId);
|
|
433
|
+
if (debugId) {
|
|
434
|
+
const channel = createServerDebugChannel(debugId);
|
|
435
|
+
console.log(
|
|
436
|
+
"[perf-tracks] handler: channel =",
|
|
437
|
+
channel ? "created" : "NOT FOUND",
|
|
438
|
+
);
|
|
439
|
+
if (channel) {
|
|
440
|
+
requestContext._debugChannel = channel;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
421
444
|
// Wire background error reporting so "use cache" and other subsystems
|
|
422
445
|
// can surface non-fatal errors through the router's onError callback.
|
|
423
446
|
requestContext._reportBackgroundError = (
|
|
@@ -1039,7 +1062,10 @@ export function createRSCHandler<
|
|
|
1039
1062
|
},
|
|
1040
1063
|
};
|
|
1041
1064
|
|
|
1042
|
-
const
|
|
1065
|
+
const debugChannel = requireRequestContext()._debugChannel;
|
|
1066
|
+
const rscStream = renderToReadableStream(payload, {
|
|
1067
|
+
...(debugChannel && { debugChannel }),
|
|
1068
|
+
});
|
|
1043
1069
|
|
|
1044
1070
|
// Determine if this is an RSC request or HTML request.
|
|
1045
1071
|
// Partial requests are always RSC (see main isRscRequest comment).
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -168,8 +168,13 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
168
168
|
loaderResult: unknown;
|
|
169
169
|
}
|
|
170
170
|
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
-
const
|
|
172
|
-
|
|
171
|
+
const debugChannel = reqCtx._debugChannel;
|
|
172
|
+
const rscStream = ctx.renderToReadableStream<LoaderPayload>(
|
|
173
|
+
loaderPayload,
|
|
174
|
+
{
|
|
175
|
+
...(debugChannel && { debugChannel }),
|
|
176
|
+
},
|
|
177
|
+
);
|
|
173
178
|
|
|
174
179
|
return createResponseWithMergedHeaders(rscStream, {
|
|
175
180
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -257,7 +257,10 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
257
257
|
formState: actionResult,
|
|
258
258
|
};
|
|
259
259
|
|
|
260
|
-
const
|
|
260
|
+
const debugChannel = requireRequestContext()._debugChannel;
|
|
261
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
262
|
+
...(debugChannel && { debugChannel }),
|
|
263
|
+
});
|
|
261
264
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
262
265
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
263
266
|
// without falling back to a fresh startSSRSetup.
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -168,7 +168,10 @@ export async function handleRscRendering<TEnv>(
|
|
|
168
168
|
|
|
169
169
|
// Serialize to RSC stream
|
|
170
170
|
const rscSerializeStart = performance.now();
|
|
171
|
-
const
|
|
171
|
+
const debugChannel = reqCtx._debugChannel;
|
|
172
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
173
|
+
...(debugChannel && { debugChannel }),
|
|
174
|
+
});
|
|
172
175
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
173
176
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
174
177
|
appendMetric(
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -223,8 +223,10 @@ export async function executeServerAction<TEnv>(
|
|
|
223
223
|
// location state is a success-only semantic. Error boundary responses
|
|
224
224
|
// update the error UI but should not mutate browser history state.
|
|
225
225
|
|
|
226
|
+
const debugChannel = requireRequestContext()._debugChannel;
|
|
226
227
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
227
228
|
temporaryReferences,
|
|
229
|
+
...(debugChannel && { debugChannel }),
|
|
228
230
|
});
|
|
229
231
|
|
|
230
232
|
return createResponseWithMergedHeaders(rscStream, {
|
package/src/rsc/types.ts
CHANGED
|
@@ -63,7 +63,13 @@ export interface RSCDependencies {
|
|
|
63
63
|
*/
|
|
64
64
|
renderToReadableStream: <T>(
|
|
65
65
|
payload: T,
|
|
66
|
-
options?: {
|
|
66
|
+
options?: {
|
|
67
|
+
temporaryReferences?: unknown;
|
|
68
|
+
debugChannel?: {
|
|
69
|
+
readable?: ReadableStream;
|
|
70
|
+
writable?: WritableStream;
|
|
71
|
+
};
|
|
72
|
+
},
|
|
67
73
|
) => ReadableStream<Uint8Array>;
|
|
68
74
|
|
|
69
75
|
/**
|
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) */
|
|
@@ -270,6 +273,9 @@ interface HelperContext {
|
|
|
270
273
|
string,
|
|
271
274
|
import("../cache/profile-registry.js").CacheProfile
|
|
272
275
|
>;
|
|
276
|
+
/** True when resolving handlers inside a cache() DSL boundary.
|
|
277
|
+
* Read by ctx.get() to guard non-cacheable variable reads. */
|
|
278
|
+
insideCacheScope?: boolean;
|
|
273
279
|
}
|
|
274
280
|
// Use a global symbol key so the AsyncLocalStorage instance survives HMR
|
|
275
281
|
// module re-evaluation. Without this, Vite's RSC module runner may create
|
|
@@ -553,6 +559,80 @@ export function getRootScoped(): boolean {
|
|
|
553
559
|
// Export HelperContext type for use in other modules
|
|
554
560
|
export type { HelperContext };
|
|
555
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Return an isolated copy of a lazy include's captured parent entry.
|
|
564
|
+
*
|
|
565
|
+
* DSL helpers (loader(), middleware(), etc.) mutate ctx.parent in place.
|
|
566
|
+
* Multiple include() scopes capture the *same* syntheticMapRoot as their
|
|
567
|
+
* parent, so without isolation one include's loaders/middleware leak into
|
|
568
|
+
* every other route that shares that root.
|
|
569
|
+
*
|
|
570
|
+
* The clone is shallow: only the mutable arrays are copied so each
|
|
571
|
+
* include pushes to its own list. The rest of the entry (id, shortCode,
|
|
572
|
+
* parent pointer, handler) stays shared, which is correct and cheap.
|
|
573
|
+
*/
|
|
574
|
+
export function getIsolatedLazyParent(
|
|
575
|
+
captured: EntryData | null | undefined,
|
|
576
|
+
): EntryData | null {
|
|
577
|
+
if (!captured) return null;
|
|
578
|
+
return {
|
|
579
|
+
...captured,
|
|
580
|
+
loader: [...captured.loader],
|
|
581
|
+
middleware: [...captured.middleware],
|
|
582
|
+
revalidate: [...captured.revalidate],
|
|
583
|
+
errorBoundary: [...captured.errorBoundary],
|
|
584
|
+
notFoundBoundary: [...captured.notFoundBoundary],
|
|
585
|
+
layout: [...captured.layout],
|
|
586
|
+
parallel: { ...captured.parallel },
|
|
587
|
+
intercept: [...captured.intercept],
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function getParallelEntries(
|
|
592
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
593
|
+
): ParallelEntryData[] {
|
|
594
|
+
if (!parallels) return [];
|
|
595
|
+
if (Array.isArray(parallels)) {
|
|
596
|
+
return parallels.filter(
|
|
597
|
+
(entry): entry is ParallelEntryData => entry.type === "parallel",
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return Object.values(parallels).filter(
|
|
601
|
+
(entry): entry is ParallelEntryData => !!entry,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function getParallelSlotEntries(
|
|
606
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
607
|
+
): Array<{ slot: `@${string}`; entry: ParallelEntryData }> {
|
|
608
|
+
if (!parallels) return [];
|
|
609
|
+
|
|
610
|
+
if (Array.isArray(parallels)) {
|
|
611
|
+
return getParallelEntries(parallels).flatMap((entry) =>
|
|
612
|
+
(Object.keys(entry.handler) as `@${string}`[]).map((slot) => ({
|
|
613
|
+
slot,
|
|
614
|
+
entry,
|
|
615
|
+
})),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return Object.entries(parallels)
|
|
620
|
+
.filter(([, entry]) => !!entry)
|
|
621
|
+
.map(([slot, entry]) => ({
|
|
622
|
+
slot: slot as `@${string}`,
|
|
623
|
+
entry: entry!,
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
export function getParallelSlotCount(
|
|
628
|
+
parallels: ParallelEntries | EntryData[] | undefined,
|
|
629
|
+
): number {
|
|
630
|
+
if (!parallels) return 0;
|
|
631
|
+
return Array.isArray(parallels)
|
|
632
|
+
? parallels.filter((entry) => entry?.type === "parallel").length
|
|
633
|
+
: Object.keys(parallels).length;
|
|
634
|
+
}
|
|
635
|
+
|
|
556
636
|
// ============================================================================
|
|
557
637
|
// Performance Metrics Helpers
|
|
558
638
|
// ============================================================================
|
|
@@ -589,3 +669,12 @@ export function track(label: string, depth?: number): () => void {
|
|
|
589
669
|
});
|
|
590
670
|
};
|
|
591
671
|
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Check if the current execution is inside a cache() DSL boundary.
|
|
675
|
+
* Returns false inside loader execution — loaders are always fresh
|
|
676
|
+
* (never cached), so non-cacheable reads are safe.
|
|
677
|
+
*/
|
|
678
|
+
export function isInsideCacheScope(): boolean {
|
|
679
|
+
return RSCRouterContext.getStore()?.insideCacheScope === true;
|
|
680
|
+
}
|
|
@@ -20,7 +20,12 @@ import type {
|
|
|
20
20
|
DefaultRouteName,
|
|
21
21
|
} from "../types/global-namespace.js";
|
|
22
22
|
import type { Handle } from "../handle.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
type ContextVar,
|
|
25
|
+
contextGet,
|
|
26
|
+
contextSet,
|
|
27
|
+
isNonCacheable,
|
|
28
|
+
} from "../context-var.js";
|
|
24
29
|
import { createHandleStore, type HandleStore } from "./handle-store.js";
|
|
25
30
|
import { isHandle } from "../handle.js";
|
|
26
31
|
import { track, type MetricsStore } from "./context.js";
|
|
@@ -30,7 +35,11 @@ import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
|
|
|
30
35
|
import { THEME_COOKIE } from "../theme/constants.js";
|
|
31
36
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
32
37
|
import { NOCACHE_SYMBOL, assertNotInsideCacheExec } from "../cache/taint.js";
|
|
33
|
-
import {
|
|
38
|
+
import { isInsideCacheScope } from "./context.js";
|
|
39
|
+
import {
|
|
40
|
+
createReverseFunction,
|
|
41
|
+
stripInternalParams,
|
|
42
|
+
} from "../router/handler-context.js";
|
|
34
43
|
import { getGlobalRouteMap, isRouteRootScoped } from "../route-map-builder.js";
|
|
35
44
|
import { invariant } from "../errors.js";
|
|
36
45
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
@@ -58,7 +67,7 @@ export interface RequestContext<
|
|
|
58
67
|
originalUrl: URL;
|
|
59
68
|
/** URL pathname */
|
|
60
69
|
pathname: string;
|
|
61
|
-
/** URL search params (
|
|
70
|
+
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
62
71
|
searchParams: URLSearchParams;
|
|
63
72
|
/** Variables set by middleware (same as ctx.var) */
|
|
64
73
|
var: Record<string, any>;
|
|
@@ -69,8 +78,12 @@ export interface RequestContext<
|
|
|
69
78
|
};
|
|
70
79
|
/** Set a variable (shared with middleware and handlers) */
|
|
71
80
|
set: {
|
|
72
|
-
<T>(
|
|
73
|
-
|
|
81
|
+
<T>(
|
|
82
|
+
contextVar: ContextVar<T>,
|
|
83
|
+
value: T,
|
|
84
|
+
options?: { cache?: boolean },
|
|
85
|
+
): void;
|
|
86
|
+
<K extends string>(key: K, value: any, options?: { cache?: boolean }): void;
|
|
74
87
|
};
|
|
75
88
|
/**
|
|
76
89
|
* Route params (populated after route matching)
|
|
@@ -274,6 +287,12 @@ export interface RequestContext<
|
|
|
274
287
|
|
|
275
288
|
/** @internal Request-scoped performance metrics store */
|
|
276
289
|
_metricsStore?: MetricsStore;
|
|
290
|
+
|
|
291
|
+
/** @internal Dev-only: debug channel for React Performance Tracks */
|
|
292
|
+
_debugChannel?: {
|
|
293
|
+
readable: ReadableStream;
|
|
294
|
+
writable: WritableStream;
|
|
295
|
+
};
|
|
277
296
|
}
|
|
278
297
|
|
|
279
298
|
/**
|
|
@@ -303,6 +322,7 @@ export type PublicRequestContext<
|
|
|
303
322
|
| "_reportBackgroundError"
|
|
304
323
|
| "_debugPerformance"
|
|
305
324
|
| "_metricsStore"
|
|
325
|
+
| "_debugChannel"
|
|
306
326
|
| "_setStatus"
|
|
307
327
|
| "res"
|
|
308
328
|
>;
|
|
@@ -503,6 +523,18 @@ export function createRequestContext<TEnv>(
|
|
|
503
523
|
responseCookieCache = null;
|
|
504
524
|
};
|
|
505
525
|
|
|
526
|
+
// Guard: throw if a response-level side effect is called inside a cache() scope.
|
|
527
|
+
// Uses ALS to detect the scope (set during segment resolution).
|
|
528
|
+
function assertNotInsideCacheScopeALS(methodName: string): void {
|
|
529
|
+
if (isInsideCacheScope()) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`ctx.${methodName}() cannot be called inside a cache() boundary. ` +
|
|
532
|
+
`On cache hit the handler is skipped, so this side effect would be lost. ` +
|
|
533
|
+
`Move ctx.${methodName}() to a middleware or layout outside the cache() scope.`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
506
538
|
// Effective cookie read: response stub Set-Cookie wins, then original header.
|
|
507
539
|
// The stub IS the source of truth for same-request mutations.
|
|
508
540
|
const effectiveCookie = (name: string): string | undefined => {
|
|
@@ -555,20 +587,31 @@ export function createRequestContext<TEnv>(
|
|
|
555
587
|
invalidateResponseCookieCache();
|
|
556
588
|
};
|
|
557
589
|
|
|
590
|
+
// Strip internal _rsc* params so userland sees a clean URL.
|
|
591
|
+
const cleanUrl = stripInternalParams(url);
|
|
592
|
+
|
|
558
593
|
// Build the context object first (without use), then add use
|
|
559
594
|
const ctx: RequestContext<TEnv> = {
|
|
560
595
|
env,
|
|
561
596
|
request,
|
|
562
|
-
url,
|
|
597
|
+
url: cleanUrl,
|
|
563
598
|
originalUrl: new URL(request.url),
|
|
564
599
|
pathname: url.pathname,
|
|
565
|
-
searchParams:
|
|
600
|
+
searchParams: cleanUrl.searchParams,
|
|
566
601
|
var: variables,
|
|
567
|
-
get: ((keyOrVar: any) =>
|
|
568
|
-
|
|
569
|
-
|
|
602
|
+
get: ((keyOrVar: any) => {
|
|
603
|
+
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
606
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
607
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return contextGet(variables, keyOrVar);
|
|
611
|
+
}) as RequestContext<TEnv>["get"],
|
|
612
|
+
set: ((keyOrVar: any, value: any, options?: any) => {
|
|
570
613
|
assertNotInsideCacheExec(ctx, "set");
|
|
571
|
-
contextSet(variables, keyOrVar, value);
|
|
614
|
+
contextSet(variables, keyOrVar, value, options);
|
|
572
615
|
}) as RequestContext<TEnv>["set"],
|
|
573
616
|
params: {} as Record<string, string>,
|
|
574
617
|
|
|
@@ -606,6 +649,7 @@ export function createRequestContext<TEnv>(
|
|
|
606
649
|
|
|
607
650
|
setCookie(name: string, value: string, options?: CookieOptions): void {
|
|
608
651
|
assertNotInsideCacheExec(ctx, "setCookie");
|
|
652
|
+
assertNotInsideCacheScopeALS("setCookie");
|
|
609
653
|
stubResponse.headers.append(
|
|
610
654
|
"Set-Cookie",
|
|
611
655
|
serializeCookieValue(name, value, options),
|
|
@@ -618,6 +662,7 @@ export function createRequestContext<TEnv>(
|
|
|
618
662
|
options?: Pick<CookieOptions, "domain" | "path">,
|
|
619
663
|
): void {
|
|
620
664
|
assertNotInsideCacheExec(ctx, "deleteCookie");
|
|
665
|
+
assertNotInsideCacheScopeALS("deleteCookie");
|
|
621
666
|
stubResponse.headers.append(
|
|
622
667
|
"Set-Cookie",
|
|
623
668
|
serializeCookieValue(name, "", { ...options, maxAge: 0 }),
|
|
@@ -627,11 +672,13 @@ export function createRequestContext<TEnv>(
|
|
|
627
672
|
|
|
628
673
|
header(name: string, value: string): void {
|
|
629
674
|
assertNotInsideCacheExec(ctx, "header");
|
|
675
|
+
assertNotInsideCacheScopeALS("header");
|
|
630
676
|
stubResponse.headers.set(name, value);
|
|
631
677
|
},
|
|
632
678
|
|
|
633
679
|
setStatus(status: number): void {
|
|
634
680
|
assertNotInsideCacheExec(ctx, "setStatus");
|
|
681
|
+
assertNotInsideCacheScopeALS("setStatus");
|
|
635
682
|
stubResponse = new Response(null, {
|
|
636
683
|
status,
|
|
637
684
|
headers: stubResponse.headers,
|
|
@@ -670,6 +717,7 @@ export function createRequestContext<TEnv>(
|
|
|
670
717
|
|
|
671
718
|
onResponse(callback: (response: Response) => Response): void {
|
|
672
719
|
assertNotInsideCacheExec(ctx, "onResponse");
|
|
720
|
+
assertNotInsideCacheScopeALS("onResponse");
|
|
673
721
|
this._onResponseCallbacks.push(callback);
|
|
674
722
|
},
|
|
675
723
|
|
|
@@ -900,7 +948,6 @@ export function createUseFunction<TEnv>(
|
|
|
900
948
|
),
|
|
901
949
|
};
|
|
902
950
|
|
|
903
|
-
// Start loader execution with tracking
|
|
904
951
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
|
905
952
|
const promise = Promise.resolve(loaderFn(loaderCtx)).finally(() => {
|
|
906
953
|
doneLoader();
|