@rangojs/router 0.23.0 → 0.24.1
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/client-urls/types.d.ts +8 -0
- 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 +86 -1
- 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/client-urls/client-root.tsx +3 -3
- package/src/client-urls/client-urls.ts +8 -1
- package/src/client-urls/types.ts +8 -0
- 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
|
@@ -262,13 +262,23 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
262
262
|
|
|
263
263
|
latchPprHeaderScopeForInterceptTarget(interceptEntry);
|
|
264
264
|
|
|
265
|
+
// The intercept segment's id: the tag scope its handler and layout record in
|
|
266
|
+
// (#957, cache-tag.ts runInSegmentTagScope) and the owner of its handler's
|
|
267
|
+
// and loaders' handle pushes, pinned before the loader kickoffs as
|
|
268
|
+
// fresh.ts/revalidation.ts pin theirs (createLoaderExecutor captures it at
|
|
269
|
+
// kickoff; a handler's ctx.use(Handle) reads it when called).
|
|
270
|
+
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
271
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
272
|
+
slotSegmentId;
|
|
273
|
+
const hasLoaders = interceptEntry.loader.length > 0;
|
|
274
|
+
|
|
265
275
|
const loaderPromises: Promise<any>[] = [];
|
|
266
276
|
const loaderIds: string[] = [];
|
|
267
277
|
|
|
268
278
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
269
279
|
const loaderEntry = interceptEntry.loader[i];
|
|
270
280
|
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
271
|
-
const segmentId = `${
|
|
281
|
+
const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
|
|
272
282
|
|
|
273
283
|
if (revalidationContext) {
|
|
274
284
|
const {
|
|
@@ -282,8 +292,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
282
292
|
stale,
|
|
283
293
|
} = revalidationContext;
|
|
284
294
|
|
|
285
|
-
|
|
286
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
295
|
+
if (clientSegmentIds.has(slotSegmentId)) {
|
|
287
296
|
const dummySegment: ResolvedSegment = {
|
|
288
297
|
id: segmentId,
|
|
289
298
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -345,9 +354,6 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
345
354
|
);
|
|
346
355
|
}
|
|
347
356
|
|
|
348
|
-
// The intercept segment's id, and the tag scope its handler and layout
|
|
349
|
-
// record in (#957, cache-tag.ts runInSegmentTagScope).
|
|
350
|
-
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
351
357
|
let handlerResult: ReactNode;
|
|
352
358
|
try {
|
|
353
359
|
const handler = interceptEntry.handler;
|
|
@@ -399,7 +405,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
399
405
|
const streamedComponent = (promise: Promise<unknown>): ReactNode =>
|
|
400
406
|
trackStreamedHandler(deps, promise, {
|
|
401
407
|
boundaryEntry: parentEntry,
|
|
402
|
-
segmentId:
|
|
408
|
+
segmentId: slotSegmentId,
|
|
403
409
|
segmentType: "parallel",
|
|
404
410
|
context,
|
|
405
411
|
routeKey: interceptEntry.routeName,
|
|
@@ -442,6 +448,13 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
442
448
|
}
|
|
443
449
|
}
|
|
444
450
|
|
|
451
|
+
// The aggregate lists the loaders that ran; empty, not absent, when the
|
|
452
|
+
// client holds the slot and all skipped. Absent replaces the client's
|
|
453
|
+
// aggregate, empty merges into it (merge-segment-loaders.ts).
|
|
454
|
+
if (hasLoaders && loaderPromises.length === 0) {
|
|
455
|
+
loaderDataPromise = [];
|
|
456
|
+
}
|
|
457
|
+
|
|
445
458
|
const interceptSegment = {
|
|
446
459
|
id: slotSegmentId,
|
|
447
460
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -455,7 +468,7 @@ export async function resolveInterceptEntry<TEnv>(
|
|
|
455
468
|
belongsToRoute,
|
|
456
469
|
parallelName: `intercept:${interceptEntry.routeName}.${interceptEntry.slotName}`,
|
|
457
470
|
loaderDataPromise,
|
|
458
|
-
loaderIds:
|
|
471
|
+
loaderIds: hasLoaders ? loaderIds : undefined,
|
|
459
472
|
};
|
|
460
473
|
segments.push(interceptSegment);
|
|
461
474
|
|
|
@@ -498,6 +511,10 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
498
511
|
|
|
499
512
|
latchPprHeaderScopeForInterceptTarget(interceptEntry);
|
|
500
513
|
|
|
514
|
+
// The loaders' handle push owner, as in resolveInterceptEntry.
|
|
515
|
+
const slotSegmentId = `${parentEntry.shortCode}.${interceptEntry.slotName}`;
|
|
516
|
+
(context as InternalHandlerContext<any, TEnv>)._currentSegmentId =
|
|
517
|
+
slotSegmentId;
|
|
501
518
|
const loaderPromises: Promise<any>[] = [];
|
|
502
519
|
const loaderIds: string[] = [];
|
|
503
520
|
|
|
@@ -515,10 +532,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
515
532
|
for (let i = 0; i < interceptEntry.loader.length; i++) {
|
|
516
533
|
const loaderEntry = interceptEntry.loader[i];
|
|
517
534
|
const { loader, revalidate: loaderRevalidateFns } = loaderEntry;
|
|
518
|
-
const segmentId = `${
|
|
535
|
+
const segmentId = `${slotSegmentId}D${i}.${loader.$$id}`;
|
|
519
536
|
|
|
520
|
-
|
|
521
|
-
if (clientSegmentIds.has(interceptSegmentId)) {
|
|
537
|
+
if (clientSegmentIds.has(slotSegmentId)) {
|
|
522
538
|
const dummySegment: ResolvedSegment = {
|
|
523
539
|
id: segmentId,
|
|
524
540
|
namespace: `intercept:${interceptEntry.routeName}`,
|
|
@@ -581,8 +597,9 @@ export async function resolveInterceptLoadersOnly<TEnv>(
|
|
|
581
597
|
);
|
|
582
598
|
}
|
|
583
599
|
|
|
600
|
+
// Every loader held: the empty aggregate (see resolveInterceptEntry).
|
|
584
601
|
if (loaderPromises.length === 0) {
|
|
585
|
-
return
|
|
602
|
+
return { loaderDataPromise: [], loaderIds };
|
|
586
603
|
}
|
|
587
604
|
|
|
588
605
|
// Match fresh-path semantics: only defer (no await) when loading is truthy.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip internal _rsc* query params from a URL.
|
|
3
|
+
* Returns a new URL with only user-facing params.
|
|
4
|
+
*
|
|
5
|
+
* A leaf, so request-scoped modules (server/rewrite-trail.ts) can strip
|
|
6
|
+
* without importing the handler context's graph.
|
|
7
|
+
*/
|
|
8
|
+
export function stripInternalParams(url: URL): URL {
|
|
9
|
+
const clean = new URL(url);
|
|
10
|
+
for (const key of [...clean.searchParams.keys()]) {
|
|
11
|
+
if (key.startsWith("_rsc")) {
|
|
12
|
+
clean.searchParams.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return clean;
|
|
16
|
+
}
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
} from "../server/handle-store.js";
|
|
56
56
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
57
57
|
import { _getRequestContext } from "../server/request-context.js";
|
|
58
|
+
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
58
59
|
import {
|
|
59
60
|
firstUnitCandidate,
|
|
60
61
|
loaderOwner,
|
|
@@ -777,6 +778,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
777
778
|
return renderedPromise;
|
|
778
779
|
},
|
|
779
780
|
};
|
|
781
|
+
defineRewrittenFrom(loaderCtx, reqCtxRef ?? _getRequestContext());
|
|
780
782
|
(loaderCtx as any)[NOCACHE_SYMBOL] = true;
|
|
781
783
|
|
|
782
784
|
// Meter this loader once via observePhase (loader:<id> perf metric +
|
|
@@ -109,11 +109,12 @@ import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
|
109
109
|
import { getContext } from "../../server/context.js";
|
|
110
110
|
import { executionStart } from "../../cache/tag-invalidation.js";
|
|
111
111
|
import {
|
|
112
|
+
bindRes,
|
|
112
113
|
createRequestContext,
|
|
113
114
|
runWithRequestContext,
|
|
114
115
|
type RequestContext,
|
|
115
116
|
} from "../../server/request-context.js";
|
|
116
|
-
import {
|
|
117
|
+
import { recordUse } from "../segment-resolution/prefetch-deferral.js";
|
|
117
118
|
|
|
118
119
|
/**
|
|
119
120
|
* Re-render a cached route in the background and write it with
|
|
@@ -166,9 +167,7 @@ export async function rerenderAndCacheRoute<TEnv>(
|
|
|
166
167
|
themeConfig: requestCtx._themeConfig,
|
|
167
168
|
});
|
|
168
169
|
const renderCtx: RequestContext<TEnv> = Object.assign(
|
|
169
|
-
Object.create(requestCtx,
|
|
170
|
-
res: Object.getOwnPropertyDescriptor(sink, "res")!,
|
|
171
|
-
}),
|
|
170
|
+
Object.create(requestCtx),
|
|
172
171
|
{
|
|
173
172
|
_handleStore: handleStore,
|
|
174
173
|
_metricsStore: undefined,
|
|
@@ -185,6 +184,8 @@ export async function rerenderAndCacheRoute<TEnv>(
|
|
|
185
184
|
_setKeepCacheDirective: sink._setKeepCacheDirective,
|
|
186
185
|
},
|
|
187
186
|
);
|
|
187
|
+
// ctx.res reads the sink's stub, not the live request's.
|
|
188
|
+
bindRes(renderCtx, () => sink.res);
|
|
188
189
|
const store = Object.assign(Object.create(ctx.Store), { metrics: undefined });
|
|
189
190
|
const runInStore = <T>(fn: () => T): T =>
|
|
190
191
|
getContext().runWithStore(
|
|
@@ -282,15 +283,16 @@ export function withBackgroundRevalidation<TEnv>(
|
|
|
282
283
|
// Only trigger background revalidation if:
|
|
283
284
|
// 1. Cache was hit and stale
|
|
284
285
|
// 2. Cache scope exists
|
|
285
|
-
// 3. The request
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
286
|
+
// 3. The request may refresh the record (RecordUse.refresh): a fill may
|
|
287
|
+
// not (prefetch: false). It runs no handler of a segment the client
|
|
288
|
+
// holds, and the re-render runs every handler of the route. The record
|
|
289
|
+
// stays stale until a request that reads it arrives: the prefetch
|
|
290
|
+
// before the fill already was one.
|
|
289
291
|
if (
|
|
290
292
|
!state.shouldRevalidate ||
|
|
291
293
|
!state.cacheHit ||
|
|
292
294
|
!ctx.cacheScope ||
|
|
293
|
-
|
|
295
|
+
!recordUse(ctx.handlerContext).refresh
|
|
294
296
|
) {
|
|
295
297
|
return;
|
|
296
298
|
}
|
|
@@ -100,8 +100,8 @@ import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
|
|
|
100
100
|
import { treeHasStreaming } from "./segment-resolution.js";
|
|
101
101
|
import { loaderPins } from "../segment-resolution/loader-cache.js";
|
|
102
102
|
import {
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
fillSends,
|
|
104
|
+
recordUse,
|
|
105
105
|
} from "../segment-resolution/prefetch-deferral.js";
|
|
106
106
|
import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
|
|
107
107
|
import {
|
|
@@ -346,20 +346,23 @@ async function* yieldFromStore<TEnv>(
|
|
|
346
346
|
const paramsChanged =
|
|
347
347
|
!ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
|
|
348
348
|
const resendRouteSegments = replaceable === true && ctx.isAction;
|
|
349
|
-
// A fill (prefetch-deferral.ts) keeps every segment the client holds.
|
|
350
|
-
const fill = isFillRequest(ctx.handlerContext);
|
|
351
349
|
for (const segment of segments) {
|
|
352
350
|
if (
|
|
353
351
|
!ctx.isFullMatch &&
|
|
354
352
|
ctx.clientSegmentSet.has(segment.id) &&
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
(segment.type === "route" || segment.belongsToRoute === true)
|
|
360
|
-
)))
|
|
353
|
+
// A stored intercept segment is re-sent, as on the runtime cache path:
|
|
354
|
+
// its loaders run (handleCacheHitIntercept), and a kept (null) one would
|
|
355
|
+
// be left out of the response with its loaders' new data.
|
|
356
|
+
!segment.namespace?.startsWith("intercept:")
|
|
361
357
|
) {
|
|
362
|
-
|
|
358
|
+
// A fill keeps it (fillSends); otherwise it is sent again when the
|
|
359
|
+
// params changed or an action re-sends the route's own segments.
|
|
360
|
+
const sends =
|
|
361
|
+
fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ??
|
|
362
|
+
(paramsChanged ||
|
|
363
|
+
(resendRouteSegments &&
|
|
364
|
+
(segment.type === "route" || segment.belongsToRoute === true)));
|
|
365
|
+
if (!sends) keepClientSegment(segment);
|
|
363
366
|
}
|
|
364
367
|
yield segment;
|
|
365
368
|
}
|
|
@@ -739,7 +742,7 @@ export function withCacheLookup<TEnv>(
|
|
|
739
742
|
if (
|
|
740
743
|
ctx.isAction ||
|
|
741
744
|
!ctx.cacheScope?.enabled ||
|
|
742
|
-
|
|
745
|
+
!recordUse(ctx.handlerContext).read
|
|
743
746
|
) {
|
|
744
747
|
yield* source;
|
|
745
748
|
if (ms) {
|
|
@@ -912,7 +915,6 @@ export function withCacheLookup<TEnv>(
|
|
|
912
915
|
|
|
913
916
|
yield* liveSegments;
|
|
914
917
|
|
|
915
|
-
const fill = isFillRequest(ctx.handlerContext);
|
|
916
918
|
for (const segment of cacheResult.segments) {
|
|
917
919
|
if (!ctx.clientSegmentSet.has(segment.id)) {
|
|
918
920
|
if (isTraceActive()) {
|
|
@@ -935,8 +937,11 @@ export function withCacheLookup<TEnv>(
|
|
|
935
937
|
continue;
|
|
936
938
|
}
|
|
937
939
|
|
|
938
|
-
// A fill
|
|
939
|
-
if (
|
|
940
|
+
// A fill keeps a held segment outright (fillSends).
|
|
941
|
+
if (
|
|
942
|
+
fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ===
|
|
943
|
+
false
|
|
944
|
+
) {
|
|
940
945
|
keepClientSegment(segment);
|
|
941
946
|
yield segment;
|
|
942
947
|
continue;
|
|
@@ -114,10 +114,7 @@ import { debugLog, debugWarn, getOrCreateRequestId } from "../logging.js";
|
|
|
114
114
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
115
115
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
116
116
|
import { rerenderAndCacheRoute } from "./background-revalidation.js";
|
|
117
|
-
import {
|
|
118
|
-
defersAboveRecord,
|
|
119
|
-
isFillRequest,
|
|
120
|
-
} from "../segment-resolution/prefetch-deferral.js";
|
|
117
|
+
import { recordUse } from "../segment-resolution/prefetch-deferral.js";
|
|
121
118
|
|
|
122
119
|
/**
|
|
123
120
|
* Creates cache store middleware
|
|
@@ -161,7 +158,7 @@ export function withCacheStore<TEnv>(
|
|
|
161
158
|
ctx.isAction ||
|
|
162
159
|
state.cacheHit ||
|
|
163
160
|
ctx.request.method !== "GET" ||
|
|
164
|
-
|
|
161
|
+
!recordUse(ctx.handlerContext).write
|
|
165
162
|
) {
|
|
166
163
|
if (ms) {
|
|
167
164
|
ms.metrics.push({
|
|
@@ -200,13 +197,14 @@ export function withCacheStore<TEnv>(
|
|
|
200
197
|
cacheScope.covers(s.id, s.namespace),
|
|
201
198
|
);
|
|
202
199
|
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
200
|
+
// The client holds part of what the record covers, so writing it takes
|
|
201
|
+
// the proactive re-render below, which a request that may not refresh
|
|
202
|
+
// the record (RecordUse.refresh) does not start. That is a fill: it
|
|
203
|
+
// renders what the client is missing and nothing else, and writes like
|
|
204
|
+
// any other request only when that is everything the record covers. The
|
|
205
|
+
// stale-hit refresh skips it for the same reason
|
|
208
206
|
// (withBackgroundRevalidation).
|
|
209
|
-
if (hasNullComponents &&
|
|
207
|
+
if (hasNullComponents && !recordUse(ctx.handlerContext).refresh) return;
|
|
210
208
|
|
|
211
209
|
const requestCtx = getRequestContext();
|
|
212
210
|
if (!requestCtx) return;
|
|
@@ -260,7 +260,9 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
260
260
|
interceptMainSegment.loaderIds = freshLoaderResult.loaderIds;
|
|
261
261
|
debugLog(
|
|
262
262
|
"matchPartial.intercept",
|
|
263
|
-
|
|
263
|
+
freshLoaderResult.loaderIds.length > 0
|
|
264
|
+
? "cache hit with fresh intercept loaders"
|
|
265
|
+
: "cache hit, every intercept loader held",
|
|
264
266
|
{
|
|
265
267
|
routeName: ctx.localRouteName,
|
|
266
268
|
slotName,
|
|
@@ -270,7 +272,7 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
270
272
|
} else {
|
|
271
273
|
debugLog(
|
|
272
274
|
"matchPartial.intercept",
|
|
273
|
-
"cache hit
|
|
275
|
+
"cache hit, intercept has no loaders",
|
|
274
276
|
{
|
|
275
277
|
routeName: ctx.localRouteName,
|
|
276
278
|
slotName,
|
|
@@ -8,6 +8,7 @@ import type { GlobalReverseFunction } from "../reverse.js";
|
|
|
8
8
|
import type { Theme } from "../theme/types.js";
|
|
9
9
|
import type { LocationStateEntry } from "../browser/react/location-state-shared.js";
|
|
10
10
|
import type { RequestScope } from "../types/request-scope.js";
|
|
11
|
+
import type { Rewrite } from "../route-definition/rewrite.js";
|
|
11
12
|
|
|
12
13
|
type GetVariableFn = {
|
|
13
14
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
@@ -81,6 +82,21 @@ export interface MiddlewareContext<
|
|
|
81
82
|
|
|
82
83
|
/** Global route names only; see {@link GlobalReverseFunction}. */
|
|
83
84
|
reverse: GlobalReverseFunction<DefaultReverseRouteMap>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Whether `value` is a `rewrite()` thrown downstream. A middleware that
|
|
88
|
+
* wraps `next()` in try/catch rethrows it, so the request re-dispatches:
|
|
89
|
+
*
|
|
90
|
+
* ```ts
|
|
91
|
+
* try {
|
|
92
|
+
* return await next();
|
|
93
|
+
* } catch (error) {
|
|
94
|
+
* if (ctx.isRewrite(error)) throw error;
|
|
95
|
+
* return new Response("Error", { status: 500 });
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
isRewrite(value: unknown): value is Rewrite;
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
export type MiddlewareFn<
|
package/src/router/middleware.ts
CHANGED
|
@@ -32,7 +32,14 @@ import {
|
|
|
32
32
|
import { getCacheExecScope } from "../cache/cache-exec-scope.js";
|
|
33
33
|
import { assertNonCacheableReadAllowed } from "../server/context.js";
|
|
34
34
|
import { readGuardedTheme } from "../server/cookie-store.js";
|
|
35
|
+
import { defineSlot, slotAccessor } from "../server/slot-accessors.js";
|
|
35
36
|
import type { Theme } from "../theme/types.js";
|
|
37
|
+
import {
|
|
38
|
+
isRewrite,
|
|
39
|
+
rewriteNotSupported,
|
|
40
|
+
type Rewrite,
|
|
41
|
+
} from "../route-definition/rewrite.js";
|
|
42
|
+
import { defineRewrittenFrom } from "../server/rewrite-trail.js";
|
|
36
43
|
|
|
37
44
|
// Re-export types consumed through this module's path.
|
|
38
45
|
export type {
|
|
@@ -43,19 +50,54 @@ export type {
|
|
|
43
50
|
} from "./middleware-types.js";
|
|
44
51
|
|
|
45
52
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* request-context getters: a spread of the ctx (inside a "use cache"
|
|
50
|
-
* function, say) does not read it. One shared descriptor, so every ctx keeps
|
|
51
|
-
* the same property shape.
|
|
53
|
+
* The middleware ctx's accessors: shared descriptors (slot-accessors.ts).
|
|
54
|
+
* routeName, _routeName, theme and setTheme read only the request context;
|
|
55
|
+
* headers calls the per-ctx closure in HEADERS_SLOT.
|
|
52
56
|
*/
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
const HEADERS_SLOT: unique symbol = Symbol("rango.middlewareContext.headers");
|
|
58
|
+
|
|
59
|
+
const MIDDLEWARE_ACCESSORS: Record<
|
|
60
|
+
"routeName" | "_routeName" | "headers" | "setTheme" | "theme",
|
|
61
|
+
PropertyDescriptor
|
|
62
|
+
> = {
|
|
63
|
+
// Re-derived from the request context on each access, so global middleware
|
|
64
|
+
// sees the matched route name after await next().
|
|
65
|
+
routeName: {
|
|
66
|
+
get(): string | undefined {
|
|
67
|
+
const raw = _getRequestContext()?._routeName;
|
|
68
|
+
return raw && !isAutoGeneratedRouteName(raw) ? raw : undefined;
|
|
69
|
+
},
|
|
70
|
+
enumerable: true,
|
|
71
|
+
configurable: true,
|
|
72
|
+
},
|
|
73
|
+
// The matched route key, auto-generated names included (the handler ctx's
|
|
74
|
+
// _routeName); undefined before routing.
|
|
75
|
+
_routeName: {
|
|
76
|
+
get(): string | undefined {
|
|
77
|
+
return _getRequestContext()?._routeName;
|
|
78
|
+
},
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
},
|
|
82
|
+
headers: slotAccessor(HEADERS_SLOT, true),
|
|
83
|
+
setTheme: {
|
|
84
|
+
get(): MiddlewareContext<unknown>["setTheme"] {
|
|
85
|
+
return _getRequestContext()?.setTheme;
|
|
86
|
+
},
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
},
|
|
90
|
+
// The visitor's theme cookie, guarded like the handler ctx.theme and
|
|
91
|
+
// cookies() (#971, #946), so a read inside a "use cache" function the
|
|
92
|
+
// middleware calls throws. Non-enumerable like the handler and
|
|
93
|
+
// request-context getters: a spread of the ctx does not read it.
|
|
94
|
+
theme: {
|
|
95
|
+
get(): Theme | undefined {
|
|
96
|
+
return readGuardedTheme(_getRequestContext(), "ctx.theme");
|
|
97
|
+
},
|
|
98
|
+
enumerable: false,
|
|
99
|
+
configurable: true,
|
|
56
100
|
},
|
|
57
|
-
enumerable: false,
|
|
58
|
-
configurable: true,
|
|
59
101
|
};
|
|
60
102
|
|
|
61
103
|
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
@@ -252,15 +294,20 @@ export function createMiddlewareContext<TEnv>(
|
|
|
252
294
|
|
|
253
295
|
// reqCtx captured once above: the request-scoped platform fields
|
|
254
296
|
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
255
|
-
// so snapshotting beats re-reading ALS on every access. The
|
|
256
|
-
//
|
|
257
|
-
//
|
|
297
|
+
// so snapshotting beats re-reading ALS on every access. The accessors
|
|
298
|
+
// (routeName, _routeName, theme, setTheme: MIDDLEWARE_ACCESSORS) read the
|
|
299
|
+
// request context on each access because those can change during
|
|
300
|
+
// `await next()`.
|
|
258
301
|
//
|
|
259
302
|
// Request-scoped like the handler ctx (issue #940): the NOCACHE_SYMBOL brand
|
|
260
303
|
// below makes a "use cache" function taking it key by host, _routeName,
|
|
261
304
|
// pathname, params and search; set/header/headers throw inside that
|
|
262
305
|
// function, and non-cacheable get reads run the shared guard.
|
|
263
|
-
|
|
306
|
+
// Data members only: the accessors are MIDDLEWARE_ACCESSORS.
|
|
307
|
+
const fields: Omit<
|
|
308
|
+
MiddlewareContext<TEnv>,
|
|
309
|
+
"routeName" | "headers" | "setTheme"
|
|
310
|
+
> = {
|
|
264
311
|
request,
|
|
265
312
|
url,
|
|
266
313
|
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
@@ -269,39 +316,12 @@ export function createMiddlewareContext<TEnv>(
|
|
|
269
316
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
270
317
|
params,
|
|
271
318
|
build: reqCtx?.build ?? false,
|
|
319
|
+
isRewrite,
|
|
272
320
|
dynamic(): void {
|
|
273
321
|
reqCtx?.dynamic();
|
|
274
322
|
},
|
|
275
323
|
executionContext: reqCtx?.executionContext,
|
|
276
324
|
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
277
|
-
// Getter: re-derives from request context on each access so that global
|
|
278
|
-
// middleware sees the matched route name after await next().
|
|
279
|
-
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
280
|
-
const reqCtx = _getRequestContext();
|
|
281
|
-
const raw = reqCtx?._routeName;
|
|
282
|
-
return (
|
|
283
|
-
raw && !isAutoGeneratedRouteName(raw) ? raw : undefined
|
|
284
|
-
) as MiddlewareContext<TEnv>["routeName"];
|
|
285
|
-
},
|
|
286
|
-
|
|
287
|
-
// The matched route key, auto-generated names included (the handler
|
|
288
|
-
// ctx's _routeName); undefined before routing.
|
|
289
|
-
get _routeName(): string | undefined {
|
|
290
|
-
return _getRequestContext()?._routeName;
|
|
291
|
-
},
|
|
292
|
-
|
|
293
|
-
// Guarded only inside a "use cache" body: the stamp covers a miss that
|
|
294
|
-
// took this ctx as an argument, the exec scope a stale refresh (which does
|
|
295
|
-
// not stamp its args, cache-runtime.ts). A write there is lost on a hit.
|
|
296
|
-
get headers(): Headers {
|
|
297
|
-
const headers = getResponse().headers;
|
|
298
|
-
return (INSIDE_CACHE_EXEC as symbol) in ctx ||
|
|
299
|
-
getCacheExecScope() !== undefined
|
|
300
|
-
? guardHeaderWrites(headers, () =>
|
|
301
|
-
assertNotInsideCacheExec(ctx, "headers"),
|
|
302
|
-
)
|
|
303
|
-
: headers;
|
|
304
|
-
},
|
|
305
325
|
|
|
306
326
|
get: ((keyOrVar: any) => {
|
|
307
327
|
// "use cache" only: intercept middleware runs inside a cache()
|
|
@@ -338,11 +358,6 @@ export function createMiddlewareContext<TEnv>(
|
|
|
338
358
|
responseHolder.response.headers.set(name, value);
|
|
339
359
|
},
|
|
340
360
|
|
|
341
|
-
// theme: MIDDLEWARE_THEME_DESCRIPTOR, defined after this literal.
|
|
342
|
-
get setTheme(): MiddlewareContext<TEnv>["setTheme"] {
|
|
343
|
-
return _getRequestContext()?.setTheme;
|
|
344
|
-
},
|
|
345
|
-
|
|
346
361
|
setLocationState(entries) {
|
|
347
362
|
const reqCtx = _getRequestContext();
|
|
348
363
|
if (!reqCtx) {
|
|
@@ -376,11 +391,46 @@ export function createMiddlewareContext<TEnv>(
|
|
|
376
391
|
}
|
|
377
392
|
},
|
|
378
393
|
};
|
|
379
|
-
|
|
394
|
+
const ctx = fields as MiddlewareContext<TEnv>;
|
|
395
|
+
// Guarded only inside a "use cache" body: the stamp covers a miss that
|
|
396
|
+
// took this ctx as an argument, the exec scope a stale refresh (which does
|
|
397
|
+
// not stamp its args, cache-runtime.ts). A write there is lost on a hit.
|
|
398
|
+
defineSlot(ctx, HEADERS_SLOT, (): Headers => {
|
|
399
|
+
const headers = getResponse().headers;
|
|
400
|
+
return (INSIDE_CACHE_EXEC as symbol) in ctx ||
|
|
401
|
+
getCacheExecScope() !== undefined
|
|
402
|
+
? guardHeaderWrites(headers, () =>
|
|
403
|
+
assertNotInsideCacheExec(ctx, "headers"),
|
|
404
|
+
)
|
|
405
|
+
: headers;
|
|
406
|
+
});
|
|
407
|
+
Object.defineProperty(ctx, "routeName", MIDDLEWARE_ACCESSORS.routeName);
|
|
408
|
+
Object.defineProperty(ctx, "_routeName", MIDDLEWARE_ACCESSORS._routeName);
|
|
409
|
+
Object.defineProperty(ctx, "headers", MIDDLEWARE_ACCESSORS.headers);
|
|
410
|
+
Object.defineProperty(ctx, "setTheme", MIDDLEWARE_ACCESSORS.setTheme);
|
|
411
|
+
Object.defineProperty(ctx, "theme", MIDDLEWARE_ACCESSORS.theme);
|
|
412
|
+
defineRewrittenFrom(ctx, reqCtx, "middleware");
|
|
380
413
|
(ctx as any)[NOCACHE_SYMBOL] = true;
|
|
381
414
|
return ctx;
|
|
382
415
|
}
|
|
383
416
|
|
|
417
|
+
/** A middleware threw a Rewrite after its next() started the downstream. */
|
|
418
|
+
function rewriteAfterNextError(handler: Function, rewrite: Rewrite): Error {
|
|
419
|
+
return new Error(
|
|
420
|
+
`Middleware "${handler.name || "(anonymous)"}" threw rewrite() after ` +
|
|
421
|
+
`calling next(). Throw rewrite() before await next().`,
|
|
422
|
+
{ cause: rewrite },
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** A middleware returned a Rewrite: only a thrown one re-dispatches. */
|
|
427
|
+
function returnedRewriteError(handler: Function): Error {
|
|
428
|
+
return new Error(
|
|
429
|
+
`Middleware "${handler.name || "(anonymous)"}" returned a rewrite(). ` +
|
|
430
|
+
`Throw it instead: \`throw rewrite(...)\`, don't return it.`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
384
434
|
/**
|
|
385
435
|
* Match middleware entries against a pathname
|
|
386
436
|
* Returns entries that match, with extracted params
|
|
@@ -584,6 +634,12 @@ export async function executeMiddleware<TEnv>(
|
|
|
584
634
|
// downstream chain and overwrite responseHolder.response.
|
|
585
635
|
let nextPromise: Promise<Response> | null = null;
|
|
586
636
|
let nextResolvedAt: number | undefined;
|
|
637
|
+
// Whether next() rejected with a Rewrite: only then did nothing render
|
|
638
|
+
// (a downstream middleware rewrote before its own next()). Any other
|
|
639
|
+
// rejection can come after the route rendered (a route middleware that
|
|
640
|
+
// throws after its next(), the render's own error), so a rewrite thrown
|
|
641
|
+
// after it would re-dispatch onto the used handle store.
|
|
642
|
+
let nextRejectedWithRewrite = false;
|
|
587
643
|
const wrappedNext = (): Promise<Response> => {
|
|
588
644
|
if (nextPromise) {
|
|
589
645
|
throw new Error(
|
|
@@ -599,6 +655,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
599
655
|
},
|
|
600
656
|
(err) => {
|
|
601
657
|
nextResolvedAt = performance.now();
|
|
658
|
+
nextRejectedWithRewrite = isRewrite(err);
|
|
602
659
|
throw err;
|
|
603
660
|
},
|
|
604
661
|
);
|
|
@@ -612,6 +669,11 @@ export async function executeMiddleware<TEnv>(
|
|
|
612
669
|
// middleware-over-middleware and the core handler underneath. Pass-through
|
|
613
670
|
// when neither surface is active.
|
|
614
671
|
let result: Response | void;
|
|
672
|
+
// A thrown rewrite() is control flow like a thrown Response: held past
|
|
673
|
+
// the span (which settles as success) and rethrown below, so it unwinds
|
|
674
|
+
// to the dispatcher (rsc/handler.ts) with no further code of this
|
|
675
|
+
// middleware run.
|
|
676
|
+
let rewrite: Rewrite | undefined;
|
|
615
677
|
try {
|
|
616
678
|
result = await observePhase(PHASES.middleware(metricLabel), async () => {
|
|
617
679
|
try {
|
|
@@ -627,6 +689,10 @@ export async function executeMiddleware<TEnv>(
|
|
|
627
689
|
// this convention (segment-resolution/helpers.ts keeps result handling
|
|
628
690
|
// outside the span). Real errors propagate past the span.
|
|
629
691
|
if (error instanceof Response) return error;
|
|
692
|
+
if (isRewrite(error)) {
|
|
693
|
+
rewrite = error;
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
630
696
|
throw error;
|
|
631
697
|
}
|
|
632
698
|
});
|
|
@@ -635,6 +701,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
635
701
|
// error paths (idempotent guard in finishMiddleware).
|
|
636
702
|
finishMiddleware();
|
|
637
703
|
}
|
|
704
|
+
// A rewrite after this middleware's next() started the downstream chain
|
|
705
|
+
// (resolved, still running, or failed) would leave that render running
|
|
706
|
+
// against the context the next pass moves, or re-dispatch onto what it
|
|
707
|
+
// already pushed, and drop the response it built. Only a rewrite after
|
|
708
|
+
// next() rejected with a Rewrite (nothing rendered) unwinds.
|
|
709
|
+
if (rewrite && nextPromise && !nextRejectedWithRewrite) {
|
|
710
|
+
throw rewriteAfterNextError(entry.handler, rewrite);
|
|
711
|
+
}
|
|
712
|
+
if (rewrite) throw rewrite;
|
|
713
|
+
if (isRewrite(result)) throw returnedRewriteError(entry.handler);
|
|
638
714
|
|
|
639
715
|
// Record post-next() processing time when middleware did work after
|
|
640
716
|
// the downstream chain resolved (e.g. adding headers, logging).
|
|
@@ -805,10 +881,12 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
805
881
|
// runner settles rango.middleware as success, not STATUS_ERROR (same
|
|
806
882
|
// reasoning as executeMiddleware's main chain). Real errors propagate.
|
|
807
883
|
if (error instanceof Response) return error;
|
|
884
|
+
if (isRewrite(error)) throw rewriteNotSupported(error, "intercept");
|
|
808
885
|
throw error;
|
|
809
886
|
}
|
|
810
887
|
},
|
|
811
888
|
);
|
|
889
|
+
if (isRewrite(result)) throw returnedRewriteError(middleware);
|
|
812
890
|
|
|
813
891
|
if (result instanceof Response) {
|
|
814
892
|
earlyResponse = result;
|
|
@@ -20,7 +20,7 @@ import { makeIsAction } from "./is-action.js";
|
|
|
20
20
|
/**
|
|
21
21
|
* Options for revalidation evaluation
|
|
22
22
|
*/
|
|
23
|
-
interface EvaluateRevalidationOptions<TEnv> {
|
|
23
|
+
export interface EvaluateRevalidationOptions<TEnv> {
|
|
24
24
|
/** Current segment to evaluate */
|
|
25
25
|
segment: ResolvedSegment;
|
|
26
26
|
/** Previous route params (from route match, not segment) */
|