@rangojs/router 0.23.0 → 0.24.0
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 +4 -3
- package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
- package/dist/types/browser/partial-update.d.ts +6 -4
- package/dist/types/browser/react/NavigationProvider.d.ts +11 -7
- package/dist/types/browser/react/context.d.ts +3 -2
- package/dist/types/browser/types.d.ts +5 -1
- package/dist/types/build/route-types/scan-filter.d.ts +7 -1
- package/dist/types/cache/document-cache-policy.d.ts +24 -0
- package/dist/types/cache/document-cache.d.ts +2 -10
- package/dist/types/context-var.d.ts +12 -0
- package/dist/types/errors.d.ts +10 -0
- package/dist/types/handles/pushed-by.d.ts +8 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.rsc.d.ts +2 -0
- package/dist/types/response-utils.d.ts +7 -0
- package/dist/types/reverse.d.ts +8 -8
- package/dist/types/route-definition/rewrite.d.ts +56 -0
- package/dist/types/route-map-builder.d.ts +7 -2
- package/dist/types/router/basename.d.ts +7 -0
- package/dist/types/router/handler-context.d.ts +1 -5
- package/dist/types/router/internal-params.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +15 -0
- package/dist/types/router/revalidation.d.ts +1 -2
- package/dist/types/router/route-trie-builder.d.ts +10 -0
- package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +36 -13
- package/dist/types/rsc/full-payload.d.ts +11 -0
- package/dist/types/rsc/handler.d.ts +33 -0
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/loader-fetch.d.ts +16 -0
- package/dist/types/rsc/rewrite-passes.d.ts +74 -0
- package/dist/types/rsc/ssr-setup.d.ts +3 -1
- package/dist/types/server/request-context.d.ts +39 -2
- package/dist/types/server/rewrite-trail.d.ts +49 -0
- package/dist/types/server/slot-accessors.d.ts +29 -0
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +8 -0
- package/dist/types/testing/run-middleware.d.ts +20 -0
- package/dist/types/testing/serve-shell-request.d.ts +2 -1
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/request-scope.d.ts +7 -0
- package/dist/types/vite/discovery/state.d.ts +12 -9
- package/dist/vite/index.js +164 -103
- package/package.json +1 -1
- package/skills/caching/SKILL.md +12 -0
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +84 -0
- package/skills/hooks/SKILL.md +11 -6
- package/skills/hooks/data.md +3 -1
- package/skills/loader/SKILL.md +7 -1
- package/skills/middleware/SKILL.md +183 -2
- package/skills/ppr/SKILL.md +9 -0
- package/skills/rango/SKILL.md +1 -1
- package/skills/server-actions/SKILL.md +27 -0
- package/skills/testing/client-components.md +1 -0
- package/skills/testing/middleware.md +31 -10
- package/skills/testing/response-routes.md +1 -0
- package/skills/testing/serve-shell-request.md +19 -1
- package/skills/use-cache/SKILL.md +4 -2
- package/src/__augment-tests__/augmented.check.ts +20 -0
- package/src/browser/event-controller.ts +3 -0
- package/src/browser/merge-segment-loaders.ts +31 -18
- package/src/browser/navigation-bridge.ts +19 -3
- package/src/browser/partial-update.ts +27 -12
- package/src/browser/react/Link.tsx +7 -9
- package/src/browser/react/NavigationProvider.tsx +46 -26
- package/src/browser/react/context.ts +3 -2
- package/src/browser/react/use-router.ts +2 -4
- package/src/browser/server-action-bridge.ts +6 -0
- package/src/browser/types.ts +5 -1
- package/src/build/route-types/scan-filter.ts +11 -4
- package/src/cache/document-cache-policy.ts +131 -0
- package/src/cache/document-cache.ts +12 -119
- package/src/context-var.ts +32 -2
- package/src/errors.ts +19 -0
- package/src/handles/pushed-by.ts +26 -0
- package/src/index.rsc.ts +8 -0
- package/src/index.ts +7 -0
- package/src/response-utils.ts +19 -0
- package/src/reverse.ts +17 -14
- package/src/route-definition/redirect.ts +2 -15
- package/src/route-definition/rewrite.ts +146 -0
- package/src/route-map-builder.ts +14 -11
- package/src/router/basename.ts +20 -0
- package/src/router/handler-context.ts +3 -13
- package/src/router/intercept-resolution.ts +29 -12
- package/src/router/internal-params.ts +16 -0
- package/src/router/loader-resolution.ts +2 -0
- package/src/router/match-middleware/background-revalidation.ts +11 -9
- package/src/router/match-middleware/cache-lookup.ts +20 -15
- package/src/router/match-middleware/cache-store.ts +9 -11
- package/src/router/match-middleware/intercept-resolution.ts +4 -2
- package/src/router/middleware-types.ts +16 -0
- package/src/router/middleware.ts +127 -49
- package/src/router/revalidation.ts +1 -1
- package/src/router/route-trie-builder.ts +28 -1
- package/src/router/segment-resolution/loader-snapshot.ts +18 -4
- package/src/router/segment-resolution/mask-nested.ts +8 -4
- package/src/router/segment-resolution/prefetch-deferral.ts +57 -17
- package/src/router/segment-resolution/revalidation.ts +57 -33
- package/src/rsc/full-payload.ts +37 -5
- package/src/rsc/handler.ts +82 -55
- package/src/rsc/helpers.ts +24 -0
- package/src/rsc/loader-fetch.ts +79 -24
- package/src/rsc/response-cache-serve.ts +18 -2
- package/src/rsc/response-route-handler.ts +4 -7
- package/src/rsc/rewrite-passes.ts +203 -0
- package/src/rsc/rsc-rendering.ts +4 -2
- package/src/rsc/ssr-setup.ts +11 -5
- package/src/server/cookie-parse.ts +36 -12
- package/src/server/cookie-store.ts +7 -3
- package/src/server/handle-store.ts +48 -17
- package/src/server/request-context.ts +154 -63
- package/src/server/request-headers.ts +4 -14
- package/src/server/rewrite-trail.ts +190 -0
- package/src/server/slot-accessors.ts +57 -0
- package/src/ssr/index.tsx +118 -16
- package/src/testing/dispatch.ts +138 -67
- package/src/testing/index.ts +1 -0
- package/src/testing/render-route.tsx +16 -3
- package/src/testing/run-loader.ts +2 -0
- package/src/testing/run-middleware.ts +56 -11
- package/src/testing/serve-shell-request.ts +15 -2
- package/src/thenable-status.ts +11 -0
- package/src/types/request-scope.ts +8 -0
- package/src/vite/discovery/state.ts +21 -13
- package/src/vite/plugins/expose-internal-ids.ts +2 -3
- package/src/vite/router-discovery.ts +29 -27
|
@@ -99,11 +99,8 @@ export interface PrefetchDeferral {
|
|
|
99
99
|
* not stored and not held. Empty outside a prefetch.
|
|
100
100
|
*/
|
|
101
101
|
readonly skipped: ReadonlySet<string>;
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
* has no content for it, a route cache() record included.
|
|
105
|
-
*/
|
|
106
|
-
readonly skipsChain: boolean;
|
|
102
|
+
/** How this request may use the route's cache() record (recordUse). */
|
|
103
|
+
readonly record: RecordUse;
|
|
107
104
|
/** Segment id of the handler unit this prefetch skipped. */
|
|
108
105
|
deferredUnit?: string;
|
|
109
106
|
/**
|
|
@@ -125,6 +122,31 @@ export interface PrefetchDeferral {
|
|
|
125
122
|
deferredLoaders?: Map<string, DeferredLoader>;
|
|
126
123
|
}
|
|
127
124
|
|
|
125
|
+
/**
|
|
126
|
+
* How a request may use the route's cache() record, decided once per match
|
|
127
|
+
* (planPrefetchDeferral) for the cache middleware.
|
|
128
|
+
*/
|
|
129
|
+
export interface RecordUse {
|
|
130
|
+
/**
|
|
131
|
+
* withCacheLookup reads it. False when this prefetch skips a chain unit
|
|
132
|
+
* (PrefetchDeferral.skipped): the unit takes everything below it, so the
|
|
133
|
+
* read would replay the record's handle pushes for segments the response
|
|
134
|
+
* does not carry. Decided from what the client holds, never per tree: a
|
|
135
|
+
* prefetch that holds the flagged layout reads and writes the record like
|
|
136
|
+
* any other request.
|
|
137
|
+
*/
|
|
138
|
+
readonly read: boolean;
|
|
139
|
+
/** withCacheStore writes it. False where `read` is: the record would be incomplete. */
|
|
140
|
+
readonly write: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* A background re-render of the whole route may write it: the stale-hit
|
|
143
|
+
* refresh (withBackgroundRevalidation) and the proactive write a partial
|
|
144
|
+
* response needs (withCacheStore). False for a fill: the re-render runs
|
|
145
|
+
* the handlers of segments the client holds, which a fill does not run.
|
|
146
|
+
*/
|
|
147
|
+
readonly refresh: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
/** A loader this prefetch deferred: PrefetchDeferral.deferredLoaders. */
|
|
129
151
|
export interface DeferredLoader {
|
|
130
152
|
/**
|
|
@@ -329,7 +351,12 @@ export function planPrefetchDeferral(
|
|
|
329
351
|
}
|
|
330
352
|
}
|
|
331
353
|
}
|
|
332
|
-
|
|
354
|
+
const record: RecordUse = {
|
|
355
|
+
read: !skipsChain,
|
|
356
|
+
write: !skipsChain,
|
|
357
|
+
refresh: mode !== "fill",
|
|
358
|
+
};
|
|
359
|
+
return { scope, mode, storedFrom, held, skipped, record };
|
|
333
360
|
}
|
|
334
361
|
|
|
335
362
|
export function getPrefetchDeferral(
|
|
@@ -344,6 +371,30 @@ export function isFillRequest(ctx: HandlerContext<any, any>): boolean {
|
|
|
344
371
|
return getPrefetchDeferral(ctx)?.mode === "fill";
|
|
345
372
|
}
|
|
346
373
|
|
|
374
|
+
/**
|
|
375
|
+
* A fill's whole revalidation rule (R5): send a segment the client does not
|
|
376
|
+
* hold, keep one it holds, and consult nothing else (no revalidate()
|
|
377
|
+
* predicate, no params or search comparison, no trace entry). Undefined on
|
|
378
|
+
* any other request, where revalidation decides. `held` is the set the
|
|
379
|
+
* deciding site resolves with.
|
|
380
|
+
*/
|
|
381
|
+
export function fillSends(
|
|
382
|
+
ctx: HandlerContext<any, any>,
|
|
383
|
+
segmentId: string,
|
|
384
|
+
held: ReadonlySet<string>,
|
|
385
|
+
): boolean | undefined {
|
|
386
|
+
return isFillRequest(ctx) ? !held.has(segmentId) : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const RECORD_OPEN: RecordUse = { read: true, write: true, refresh: true };
|
|
390
|
+
|
|
391
|
+
/** PrefetchDeferral.record, or all of it open when the match has no plan. */
|
|
392
|
+
export function recordUse(
|
|
393
|
+
ctx: HandlerContext<any, any> | undefined,
|
|
394
|
+
): RecordUse {
|
|
395
|
+
return getPrefetchDeferral(ctx)?.record ?? RECORD_OPEN;
|
|
396
|
+
}
|
|
397
|
+
|
|
347
398
|
/**
|
|
348
399
|
* Register a loader this prefetch deferred, with the segment that registers
|
|
349
400
|
* it and how to start it there (PrefetchDeferral.deferredLoaders). The first
|
|
@@ -481,17 +532,6 @@ export function firstUnitCandidate(
|
|
|
481
532
|
return undefined;
|
|
482
533
|
}
|
|
483
534
|
|
|
484
|
-
/**
|
|
485
|
-
* This prefetch skips a layout above the route cache's boundary, and with it
|
|
486
|
-
* everything the record below holds: withCacheLookup and withCacheStore leave
|
|
487
|
-
* the record alone for it. Decided per request, from what the client holds:
|
|
488
|
-
* a prefetch of the same tree that holds the layout reads and writes the
|
|
489
|
-
* record like any other request.
|
|
490
|
-
*/
|
|
491
|
-
export function defersAboveRecord(ctx: HandlerContext<any, any>): boolean {
|
|
492
|
-
return getPrefetchDeferral(ctx)?.skipsChain === true;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
535
|
/**
|
|
496
536
|
* Record the handler unit this prefetch skipped. A `chain` entry takes what
|
|
497
537
|
* the walk reaches after it (PrefetchDeferral.below); a slot is skipped
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
import type { ReactNode } from "react";
|
|
10
10
|
import { invariant } from "../../errors";
|
|
11
11
|
import { revalidate } from "../loader-resolution.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
evaluateRevalidation,
|
|
14
|
+
type EvaluateRevalidationOptions,
|
|
15
|
+
} from "../revalidation.js";
|
|
13
16
|
import {
|
|
14
17
|
getParallelEntries,
|
|
15
18
|
getParallelSlotEntries,
|
|
@@ -38,8 +41,8 @@ import { entryLoadingMasksLoaders } from "./loader-mask.js";
|
|
|
38
41
|
import {
|
|
39
42
|
defersLoader,
|
|
40
43
|
deferUnit,
|
|
44
|
+
fillSends,
|
|
41
45
|
getPrefetchDeferral,
|
|
42
|
-
isFillRequest,
|
|
43
46
|
registerDeferredLoader,
|
|
44
47
|
} from "./prefetch-deferral.js";
|
|
45
48
|
import {
|
|
@@ -110,6 +113,28 @@ function emitRevalidationDecision(
|
|
|
110
113
|
});
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Whether a segment the client holds (`held`, the set resolution works on)
|
|
118
|
+
* renders again. A fill keeps it (prefetch-deferral.ts fillSends): no
|
|
119
|
+
* revalidate() predicate runs and nothing is traced or observed. Any other
|
|
120
|
+
* request asks the segment's revalidate() chain, and `onEvaluated` sees that
|
|
121
|
+
* answer.
|
|
122
|
+
*/
|
|
123
|
+
async function revalidateHeld<TEnv>({
|
|
124
|
+
held,
|
|
125
|
+
onEvaluated,
|
|
126
|
+
...options
|
|
127
|
+
}: EvaluateRevalidationOptions<TEnv> & {
|
|
128
|
+
held: ReadonlySet<string>;
|
|
129
|
+
onEvaluated?: (shouldRevalidate: boolean) => void;
|
|
130
|
+
}): Promise<boolean> {
|
|
131
|
+
const fill = fillSends(options.context, options.segment.id, held);
|
|
132
|
+
if (fill !== undefined) return fill;
|
|
133
|
+
const shouldRevalidate = await evaluateRevalidation(options);
|
|
134
|
+
onEvaluated?.(shouldRevalidate);
|
|
135
|
+
return shouldRevalidate;
|
|
136
|
+
}
|
|
137
|
+
|
|
113
138
|
// ---------------------------------------------------------------------------
|
|
114
139
|
// prefetch: false (prefetch-deferral.ts)
|
|
115
140
|
// ---------------------------------------------------------------------------
|
|
@@ -194,7 +219,6 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
194
219
|
}));
|
|
195
220
|
|
|
196
221
|
const matchedIds = loaderMeta.map((m) => m.segmentId);
|
|
197
|
-
const fill = isFillRequest(ctx);
|
|
198
222
|
|
|
199
223
|
const revalidationChecks = await Promise.all(
|
|
200
224
|
loaderMeta.map(
|
|
@@ -221,8 +245,6 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
221
245
|
}
|
|
222
246
|
return true;
|
|
223
247
|
}
|
|
224
|
-
// Fill: a held segment is skipped outright, no predicate runs.
|
|
225
|
-
if (fill) return false;
|
|
226
248
|
|
|
227
249
|
const dummySegment: ResolvedSegment = {
|
|
228
250
|
id: segmentId,
|
|
@@ -235,7 +257,7 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
235
257
|
belongsToRoute,
|
|
236
258
|
};
|
|
237
259
|
|
|
238
|
-
return await
|
|
260
|
+
return await revalidateHeld({
|
|
239
261
|
segment: dummySegment,
|
|
240
262
|
prevParams,
|
|
241
263
|
getPrevSegment: null,
|
|
@@ -251,6 +273,7 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
251
273
|
actionContext,
|
|
252
274
|
stale,
|
|
253
275
|
traceSource: "loader",
|
|
276
|
+
held: clientSegmentIds,
|
|
254
277
|
});
|
|
255
278
|
},
|
|
256
279
|
async () => true,
|
|
@@ -704,10 +727,12 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
|
|
|
704
727
|
// runs: a prefetch sends what the slot declares instead of running its
|
|
705
728
|
// handler.
|
|
706
729
|
let deferred = deferUnit(context, parallelId, "below");
|
|
730
|
+
// A fill sends exactly the slots the client lacks, ahead of every rule
|
|
731
|
+
// below (fillSends).
|
|
732
|
+
const fill = fillSends(context, parallelId, clientSegmentIds);
|
|
707
733
|
let shouldResolve: boolean;
|
|
708
|
-
if (
|
|
709
|
-
|
|
710
|
-
shouldResolve = !clientSegmentIds.has(parallelId);
|
|
734
|
+
if (fill !== undefined) {
|
|
735
|
+
shouldResolve = fill;
|
|
711
736
|
} else if (deferred) {
|
|
712
737
|
shouldResolve = true;
|
|
713
738
|
} else if (isFullRefetch) {
|
|
@@ -880,8 +905,6 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
880
905
|
}
|
|
881
906
|
return true;
|
|
882
907
|
}
|
|
883
|
-
// Fill: a held segment is skipped outright, no predicate runs.
|
|
884
|
-
if (isFillRequest(context)) return false;
|
|
885
908
|
|
|
886
909
|
const dummySegment: ResolvedSegment = {
|
|
887
910
|
id: entry.shortCode,
|
|
@@ -900,7 +923,7 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
900
923
|
...(entry.mountPath ? { mountPath: entry.mountPath } : {}),
|
|
901
924
|
};
|
|
902
925
|
|
|
903
|
-
|
|
926
|
+
return revalidateHeld({
|
|
904
927
|
segment: dummySegment,
|
|
905
928
|
prevParams,
|
|
906
929
|
getPrevSegment: null,
|
|
@@ -917,18 +940,20 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
|
|
|
917
940
|
stale,
|
|
918
941
|
traceSource:
|
|
919
942
|
entry.type === "route" ? "route-handler" : "layout-handler",
|
|
943
|
+
held: clientSegmentIds,
|
|
944
|
+
onEvaluated: (shouldRevalidate) => {
|
|
945
|
+
emitRevalidationDecision(
|
|
946
|
+
entry.shortCode,
|
|
947
|
+
context.pathname,
|
|
948
|
+
routeKey,
|
|
949
|
+
shouldRevalidate,
|
|
950
|
+
);
|
|
951
|
+
debugLog("segment.revalidate", "entry revalidation decision", {
|
|
952
|
+
segmentId: entry.shortCode,
|
|
953
|
+
shouldRevalidate,
|
|
954
|
+
});
|
|
955
|
+
},
|
|
920
956
|
});
|
|
921
|
-
emitRevalidationDecision(
|
|
922
|
-
entry.shortCode,
|
|
923
|
-
context.pathname,
|
|
924
|
-
routeKey,
|
|
925
|
-
shouldRevalidate,
|
|
926
|
-
);
|
|
927
|
-
debugLog("segment.revalidate", "entry revalidation decision", {
|
|
928
|
-
segmentId: entry.shortCode,
|
|
929
|
-
shouldRevalidate,
|
|
930
|
-
});
|
|
931
|
-
return shouldRevalidate;
|
|
932
957
|
},
|
|
933
958
|
async () => {
|
|
934
959
|
// Only an entry the client lacks (defersUnit). Its handler would
|
|
@@ -1308,8 +1333,6 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1308
1333
|
}
|
|
1309
1334
|
return true;
|
|
1310
1335
|
}
|
|
1311
|
-
// Fill: a held segment is skipped outright, no predicate runs.
|
|
1312
|
-
if (isFillRequest(context)) return false;
|
|
1313
1336
|
|
|
1314
1337
|
const dummySegment: ResolvedSegment = {
|
|
1315
1338
|
id: orphan.shortCode,
|
|
@@ -1323,7 +1346,7 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1323
1346
|
...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
|
|
1324
1347
|
};
|
|
1325
1348
|
|
|
1326
|
-
|
|
1349
|
+
return revalidateHeld({
|
|
1327
1350
|
segment: dummySegment,
|
|
1328
1351
|
prevParams,
|
|
1329
1352
|
getPrevSegment: null,
|
|
@@ -1339,14 +1362,15 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1339
1362
|
actionContext,
|
|
1340
1363
|
stale,
|
|
1341
1364
|
traceSource: "orphan-layout",
|
|
1365
|
+
held: clientSegmentIds,
|
|
1366
|
+
onEvaluated: (shouldRevalidate) =>
|
|
1367
|
+
emitRevalidationDecision(
|
|
1368
|
+
orphan.shortCode,
|
|
1369
|
+
context.pathname,
|
|
1370
|
+
routeKey,
|
|
1371
|
+
shouldRevalidate,
|
|
1372
|
+
),
|
|
1342
1373
|
});
|
|
1343
|
-
emitRevalidationDecision(
|
|
1344
|
-
orphan.shortCode,
|
|
1345
|
-
context.pathname,
|
|
1346
|
-
routeKey,
|
|
1347
|
-
shouldRevalidate,
|
|
1348
|
-
);
|
|
1349
|
-
return shouldRevalidate;
|
|
1350
1374
|
},
|
|
1351
1375
|
async () => {
|
|
1352
1376
|
// Below a unit a prefetch skipped: the orphan goes with it.
|
package/src/rsc/full-payload.ts
CHANGED
|
@@ -14,7 +14,11 @@ import type { HandlerContext } from "./handler-context.js";
|
|
|
14
14
|
import type { RequestContext } from "../server/request-context.js";
|
|
15
15
|
import type { HandleStore } from "../server/handle-store.js";
|
|
16
16
|
import type { Theme } from "../theme/types.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
rendersForSharedAudience,
|
|
19
|
+
rewriteTrail,
|
|
20
|
+
visitorUrl,
|
|
21
|
+
} from "../server/rewrite-trail.js";
|
|
18
22
|
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
19
23
|
import { publicRouteName } from "../route-name.js";
|
|
20
24
|
import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
@@ -38,7 +42,7 @@ export function buildFullPayload(
|
|
|
38
42
|
): RscPayload {
|
|
39
43
|
return {
|
|
40
44
|
metadata: {
|
|
41
|
-
pathname: url.pathname,
|
|
45
|
+
pathname: renderedLocation(reqCtx, url).pathname,
|
|
42
46
|
routerId: ctx.router.id,
|
|
43
47
|
basename: ctx.router.basename,
|
|
44
48
|
segments: attachTransitionWhen(m.segments, reqCtx),
|
|
@@ -84,6 +88,36 @@ export function buildFullPayload(
|
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The URL a render shows as the page's location (payload `pathname`, the SSR
|
|
93
|
+
* search and origin seed). For a request middleware rewrote, the visitor's
|
|
94
|
+
* URL, so the server markup and the hydrating client (which reads the address
|
|
95
|
+
* bar) agree on `usePathname()` and `useSearchParams()`. A render other
|
|
96
|
+
* visitors may be served shows `url`, the target, as a direct visit does: a
|
|
97
|
+
* shell capture, and a render the document cache stores (its key is the
|
|
98
|
+
* target). A render that shows the visitor's URL marks
|
|
99
|
+
* `_payloadVisitorLocation`, and the document cache does not store it.
|
|
100
|
+
*/
|
|
101
|
+
export function renderedLocation(
|
|
102
|
+
reqCtx:
|
|
103
|
+
| Pick<
|
|
104
|
+
RequestContext<any>,
|
|
105
|
+
| "_variables"
|
|
106
|
+
| "_shellCaptureRun"
|
|
107
|
+
| "_documentCacheRender"
|
|
108
|
+
| "_payloadVisitorLocation"
|
|
109
|
+
| "res"
|
|
110
|
+
>
|
|
111
|
+
| undefined,
|
|
112
|
+
url: URL,
|
|
113
|
+
): URL {
|
|
114
|
+
if (!reqCtx || !rewriteTrail(reqCtx) || rendersForSharedAudience(reqCtx)) {
|
|
115
|
+
return url;
|
|
116
|
+
}
|
|
117
|
+
reqCtx._payloadVisitorLocation = true;
|
|
118
|
+
return visitorUrl(reqCtx, url);
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
/**
|
|
88
122
|
* The theme a document payload carries as `metadata.initialTheme` (the
|
|
89
123
|
* ThemeProvider's first state), read with no identity guard: it is the
|
|
@@ -116,9 +150,7 @@ export function payloadInitialTheme(
|
|
|
116
150
|
): Theme | undefined {
|
|
117
151
|
const config = reqCtx._themeConfig;
|
|
118
152
|
if (!config) return undefined;
|
|
119
|
-
if (
|
|
120
|
-
return config.defaultTheme;
|
|
121
|
-
}
|
|
153
|
+
if (rendersForSharedAudience(reqCtx)) return config.defaultTheme;
|
|
122
154
|
const theme = reqCtx._readTheme();
|
|
123
155
|
if (theme !== config.defaultTheme) reqCtx._payloadVisitorTheme = true;
|
|
124
156
|
return theme;
|
package/src/rsc/handler.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
createRequestContext,
|
|
18
18
|
captureRequestEntryContext,
|
|
19
19
|
} from "../server/request-context.js";
|
|
20
|
+
import { REWRITE_HEADER, runRewritePasses } from "./rewrite-passes.js";
|
|
21
|
+
import { renderedLocation } from "./full-payload.js";
|
|
20
22
|
import * as rscDeps from "@vitejs/plugin-rsc/rsc/server";
|
|
21
23
|
import type {
|
|
22
24
|
RscPayload,
|
|
@@ -25,6 +27,7 @@ import type {
|
|
|
25
27
|
SSRModule,
|
|
26
28
|
} from "./types.js";
|
|
27
29
|
import {
|
|
30
|
+
createReloadResponse,
|
|
28
31
|
createResponseWithMergedHeaders,
|
|
29
32
|
finalizeResponse,
|
|
30
33
|
interceptRedirectForPartial,
|
|
@@ -41,6 +44,7 @@ import { resolvedHandleStream } from "../handles/deferred-resolution.js";
|
|
|
41
44
|
import {
|
|
42
45
|
isWebSocketUpgradeResponse,
|
|
43
46
|
appendVaryAccept,
|
|
47
|
+
trySetHeader,
|
|
44
48
|
} from "../response-utils.js";
|
|
45
49
|
import {
|
|
46
50
|
handleResponseRoute,
|
|
@@ -57,10 +61,7 @@ import {
|
|
|
57
61
|
invokeOnError,
|
|
58
62
|
resolveDefaultNotFound,
|
|
59
63
|
} from "../router/error-handling.js";
|
|
60
|
-
import {
|
|
61
|
-
createReverseFunction,
|
|
62
|
-
stripInternalParams,
|
|
63
|
-
} from "../router/handler-context.js";
|
|
64
|
+
import { createReverseFunction } from "../router/handler-context.js";
|
|
64
65
|
import { contextSet } from "../context-var.js";
|
|
65
66
|
import {
|
|
66
67
|
hasCachedManifest,
|
|
@@ -82,7 +83,7 @@ import {
|
|
|
82
83
|
revalidateAfterAction,
|
|
83
84
|
type ActionContinuation,
|
|
84
85
|
} from "./server-action.js";
|
|
85
|
-
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
86
|
+
import { handleLoaderFetch, renderLoaderError } from "./loader-fetch.js";
|
|
86
87
|
import { applyStreamIdleTimeout } from "./stream-idle.js";
|
|
87
88
|
import {
|
|
88
89
|
checkRequestOrigin,
|
|
@@ -113,6 +114,7 @@ import {
|
|
|
113
114
|
mayNeedSSR,
|
|
114
115
|
isRscRequest,
|
|
115
116
|
SSR_SETUP_VAR,
|
|
117
|
+
type SSRSetup,
|
|
116
118
|
} from "./ssr-setup.js";
|
|
117
119
|
import {
|
|
118
120
|
classifyRequest,
|
|
@@ -155,21 +157,6 @@ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
|
155
157
|
* ```
|
|
156
158
|
*/
|
|
157
159
|
|
|
158
|
-
/**
|
|
159
|
-
* Response that tells the client to do a full document navigation. Shared by
|
|
160
|
-
* the terminal reload plans (version-mismatch and app-switch): an empty 200
|
|
161
|
-
* carrying X-RSC-Reload, which the client turns into window.location.href.
|
|
162
|
-
*/
|
|
163
|
-
function createReloadResponse(reloadUrl: string) {
|
|
164
|
-
return createResponseWithMergedHeaders(null, {
|
|
165
|
-
status: 200,
|
|
166
|
-
headers: {
|
|
167
|
-
"X-RSC-Reload": reloadUrl,
|
|
168
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
160
|
export function createRSCHandler<
|
|
174
161
|
TEnv = unknown,
|
|
175
162
|
TRoutes extends Record<string, string> = Record<string, string>,
|
|
@@ -703,24 +690,70 @@ export function createRSCHandler<
|
|
|
703
690
|
// latter is reserved for the low-cardinality template (OTel convention).
|
|
704
691
|
span.setAttribute("url.path", url.pathname);
|
|
705
692
|
|
|
706
|
-
//
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const downstream =
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
693
|
+
// One pass per URL: the global middleware chain matched on the pass's
|
|
694
|
+
// path, wrapping the core handler. A `throw rewrite()` from global or
|
|
695
|
+
// route middleware unwinds the pass, and the next one runs the same
|
|
696
|
+
// request as the target from the top, as a direct visit would
|
|
697
|
+
// (rsc/rewrite-passes.ts). The response is finalized below, inside the
|
|
698
|
+
// response span.
|
|
699
|
+
let passIndex = 0;
|
|
700
|
+
let hasMiddleware = false;
|
|
701
|
+
const { response: downstream, rewrittenTo } = await runRewritePasses({
|
|
702
|
+
reqCtx: requestContext,
|
|
703
|
+
request,
|
|
704
|
+
url,
|
|
705
|
+
routeMap: getRequiredRouteMap,
|
|
706
|
+
basename: router.basename,
|
|
707
|
+
runPass: (passRequest, passUrl) => {
|
|
708
|
+
const passMiddleware =
|
|
709
|
+
passIndex++ === 0
|
|
710
|
+
? matchedMiddleware
|
|
711
|
+
: matchMiddleware(
|
|
712
|
+
requestContext.url.pathname,
|
|
713
|
+
router.middleware,
|
|
714
|
+
);
|
|
715
|
+
hasMiddleware = passMiddleware.length > 0;
|
|
716
|
+
const coreHandler = (): Promise<Response> =>
|
|
717
|
+
coreRequestHandler(passRequest, env, passUrl, variables, nonce);
|
|
718
|
+
return hasMiddleware
|
|
719
|
+
? executeMiddleware(
|
|
720
|
+
passMiddleware,
|
|
721
|
+
passRequest,
|
|
722
|
+
env,
|
|
723
|
+
variables,
|
|
724
|
+
coreHandler,
|
|
725
|
+
createReverseFunction(getRequiredRouteMap()),
|
|
726
|
+
)
|
|
727
|
+
: coreHandler();
|
|
728
|
+
},
|
|
729
|
+
onHop: (pass) => {
|
|
730
|
+
span.setAttribute("rango.rewrite.target", pass.target.pathname);
|
|
731
|
+
span.setAttribute("rango.rewrite.hops", pass.hops);
|
|
732
|
+
},
|
|
733
|
+
report: (error, at) => {
|
|
734
|
+
callOnError(error, "routing", {
|
|
735
|
+
request: at.request,
|
|
736
|
+
url: at.url,
|
|
718
737
|
env,
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
:
|
|
738
|
+
handledByBoundary: false,
|
|
739
|
+
});
|
|
740
|
+
},
|
|
741
|
+
// A loader fetch answers with the error `load()` rejects with.
|
|
742
|
+
refuse: (error) =>
|
|
743
|
+
url.searchParams.has("_rsc_loader")
|
|
744
|
+
? renderLoaderError(
|
|
745
|
+
handlerCtx,
|
|
746
|
+
{ message: (error as Error).message, name: "Error" },
|
|
747
|
+
500,
|
|
748
|
+
{
|
|
749
|
+
request,
|
|
750
|
+
url,
|
|
751
|
+
env,
|
|
752
|
+
loaderId: url.searchParams.get("_rsc_loader") ?? "",
|
|
753
|
+
},
|
|
754
|
+
)
|
|
755
|
+
: undefined,
|
|
756
|
+
});
|
|
724
757
|
|
|
725
758
|
// Final response construction + host handoff, wrapped in rango.response
|
|
726
759
|
// — the explicit stream-handoff marker. The callback is synchronous, so
|
|
@@ -787,14 +820,9 @@ export function createRSCHandler<
|
|
|
787
820
|
}
|
|
788
821
|
|
|
789
822
|
const fullTiming = timingParts.join(", ");
|
|
790
|
-
if (fullTiming
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
} catch {
|
|
794
|
-
// Immutable headers (e.g. a passed-through platform Response) — drop
|
|
795
|
-
// the timing header, never the response. Instrumentation must not
|
|
796
|
-
// 500 a request.
|
|
797
|
-
}
|
|
823
|
+
if (fullTiming) trySetHeader(response, "Server-Timing", fullTiming);
|
|
824
|
+
if (rewrittenTo !== undefined) {
|
|
825
|
+
trySetHeader(response, REWRITE_HEADER, rewrittenTo);
|
|
798
826
|
}
|
|
799
827
|
|
|
800
828
|
// Single open-redirect chokepoint: every response (PE, full-page,
|
|
@@ -956,18 +984,19 @@ export function createRSCHandler<
|
|
|
956
984
|
// which returns { redirect: url }. Fall through to full-render.
|
|
957
985
|
}
|
|
958
986
|
|
|
987
|
+
// A reload points at the visitor's URL, never a rewrite target's.
|
|
959
988
|
if (plan.mode === "version-mismatch") {
|
|
960
989
|
console.log(
|
|
961
990
|
`[RSC] Version mismatch: client=${url.searchParams.get("_rsc_v")}, server=${version}. Forcing reload.`,
|
|
962
991
|
);
|
|
963
|
-
return createReloadResponse(plan.reloadUrl);
|
|
992
|
+
return createReloadResponse(getRequestContext(), plan.reloadUrl);
|
|
964
993
|
}
|
|
965
994
|
|
|
966
995
|
if (plan.mode === "app-switch") {
|
|
967
996
|
// Cross-app SPA navigation crossed a host-router app boundary. Force a
|
|
968
997
|
// real document navigation so the target app's document is re-established
|
|
969
998
|
// (stylesheets, theme, warmup, prefetch-TTL). See request-classification.
|
|
970
|
-
return createReloadResponse(plan.reloadUrl);
|
|
999
|
+
return createReloadResponse(getRequestContext(), plan.reloadUrl);
|
|
971
1000
|
}
|
|
972
1001
|
|
|
973
1002
|
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
@@ -1137,6 +1166,9 @@ export function createRSCHandler<
|
|
|
1137
1166
|
(mayNeedSSR(request, url) &&
|
|
1138
1167
|
!isRscRequest(request, url, plan.mode === "partial-render"));
|
|
1139
1168
|
if (plan.mode !== "loader" && willRenderHtml) {
|
|
1169
|
+
// A rewritten pass keeps the SSR module the earlier pass started
|
|
1170
|
+
// loading; the stream mode is resolved for the request it renders.
|
|
1171
|
+
const earlier = variables[SSR_SETUP_VAR] as Promise<SSRSetup> | undefined;
|
|
1140
1172
|
const ssrSetup = startSSRSetup(
|
|
1141
1173
|
handlerCtx,
|
|
1142
1174
|
request,
|
|
@@ -1145,6 +1177,7 @@ export function createRSCHandler<
|
|
|
1145
1177
|
router.debugPerformance
|
|
1146
1178
|
? () => getRequestContext()._metricsStore
|
|
1147
1179
|
: undefined,
|
|
1180
|
+
earlier?.then(([ssrModule]) => ssrModule),
|
|
1148
1181
|
);
|
|
1149
1182
|
variables[SSR_SETUP_VAR] = ssrSetup;
|
|
1150
1183
|
|
|
@@ -1348,13 +1381,7 @@ export function createRSCHandler<
|
|
|
1348
1381
|
`[RSC] Route handler at ${url.pathname} returned a Response during client-side navigation. ` +
|
|
1349
1382
|
`Falling back to hard navigation. Use data-external on the <Link> to avoid the extra round-trip.`,
|
|
1350
1383
|
);
|
|
1351
|
-
return
|
|
1352
|
-
status: 200,
|
|
1353
|
-
headers: {
|
|
1354
|
-
"X-RSC-Reload": stripInternalParams(url).toString(),
|
|
1355
|
-
"content-type": "text/x-component;charset=utf-8",
|
|
1356
|
-
},
|
|
1357
|
-
});
|
|
1384
|
+
return createReloadResponse(getRequestContext(), url);
|
|
1358
1385
|
}
|
|
1359
1386
|
|
|
1360
1387
|
if (treatAsPartial) {
|
|
@@ -1396,7 +1423,7 @@ export function createRSCHandler<
|
|
|
1396
1423
|
|
|
1397
1424
|
const payload: RscPayload = {
|
|
1398
1425
|
metadata: {
|
|
1399
|
-
pathname: url.pathname,
|
|
1426
|
+
pathname: renderedLocation(getRequestContext(), url).pathname,
|
|
1400
1427
|
routerId: router.id,
|
|
1401
1428
|
basename: router.basename,
|
|
1402
1429
|
segments: [notFoundSegment],
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
25
25
|
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
26
26
|
import type { RscPayload } from "./types.js";
|
|
27
|
+
import { rewriteTrail, visitorUrl } from "../server/rewrite-trail.js";
|
|
27
28
|
import type { HandlerContext } from "./handler-context.js";
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -157,6 +158,29 @@ export function createResponseWithMergedHeaders(
|
|
|
157
158
|
return drainOnResponseCallbacks(ctx, response);
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* An empty 200 carrying X-RSC-Reload: the client turns it into a document
|
|
163
|
+
* navigation (window.location.href). It points at the visitor's URL: the
|
|
164
|
+
* first URL of a rewritten request (server/rewrite-trail.ts visitorUrl),
|
|
165
|
+
* else `url` with the internal `_rsc*` params stripped, or `url` as given
|
|
166
|
+
* when it is a string the caller resolved (an action's referer).
|
|
167
|
+
*/
|
|
168
|
+
export function createReloadResponse(
|
|
169
|
+
reqCtx: RequestContext<any> | undefined,
|
|
170
|
+
url: URL | string,
|
|
171
|
+
): Response {
|
|
172
|
+
const target =
|
|
173
|
+
rewriteTrail(reqCtx)?.[0] ??
|
|
174
|
+
(typeof url === "string" ? url : visitorUrl(undefined, url).href);
|
|
175
|
+
return createResponseWithMergedHeaders(null, {
|
|
176
|
+
status: 200,
|
|
177
|
+
headers: {
|
|
178
|
+
"X-RSC-Reload": target,
|
|
179
|
+
"content-type": "text/x-component;charset=utf-8",
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
160
184
|
/**
|
|
161
185
|
* Rebuild a Response returned or thrown by a response-route handler so request
|
|
162
186
|
* effects merge consistently in production and the public dispatch primitive.
|