@rangojs/router 0.32.0 → 0.33.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/README.md +3 -2
- package/dist/bin/browser-7KIJ5HSV.js +15 -0
- package/dist/bin/chunk-3B7CKBRO.js +463 -0
- package/dist/bin/chunk-524VACCG.js +71 -0
- package/dist/bin/chunk-LD7UBHT4.js +196 -0
- package/dist/bin/chunk-TO6CRWLN.js +1429 -0
- package/dist/bin/chunk-TTXC2U3G.js +2477 -0
- package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
- package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
- package/dist/bin/rango.js +42 -10173
- package/dist/bin/runtime-EPUDEHO4.js +596 -0
- package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
- package/dist/bin/state-ACD2MGKJ.js +16 -0
- package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
- package/dist/types/bin/rango.d.ts +1 -0
- package/dist/types/browser/action-fence.d.ts +20 -0
- package/dist/types/browser/cookie-name.d.ts +51 -10
- package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
- package/dist/types/browser/navigation-bridge.d.ts +36 -1
- package/dist/types/browser/navigation-client.d.ts +19 -0
- package/dist/types/browser/navigation-store-handle.d.ts +37 -4
- package/dist/types/browser/navigation-store.d.ts +2 -2
- package/dist/types/browser/navigation-transaction.d.ts +0 -1
- package/dist/types/browser/partial-update.d.ts +13 -0
- package/dist/types/browser/prefetch/cache.d.ts +96 -8
- package/dist/types/browser/prefetch/fetch.d.ts +26 -2
- package/dist/types/browser/prefetch/queue.d.ts +3 -2
- package/dist/types/browser/rango-state.d.ts +34 -5
- package/dist/types/browser/react/Link.d.ts +15 -6
- package/dist/types/browser/types.d.ts +25 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/include-resolution.d.ts +9 -1
- package/dist/types/build/route-types/router-processing.d.ts +3 -1
- package/dist/types/cache/cache-key-utils.d.ts +12 -8
- package/dist/types/cache/cache-scope.d.ts +4 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
- package/dist/types/cache/document-cache-policy.d.ts +9 -1
- package/dist/types/cache/memory-segment-store.d.ts +3 -2
- package/dist/types/cache/segment-codec.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +12 -7
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
- package/dist/types/client-urls/types.d.ts +7 -5
- package/dist/types/prefetch-markers.d.ts +9 -0
- package/dist/types/prefetch-source-scope.d.ts +66 -0
- package/dist/types/route-definition/helpers-types.d.ts +8 -5
- package/dist/types/router/intercept-resolution.d.ts +25 -11
- package/dist/types/router/match-context.d.ts +1 -0
- package/dist/types/router/match-handlers.d.ts +1 -1
- package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
- package/dist/types/router/router-context.d.ts +1 -1
- package/dist/types/router/router-options.d.ts +6 -3
- package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
- package/dist/types/router/source-reads.d.ts +53 -0
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/helpers.d.ts +5 -2
- package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
- package/dist/types/rsc/request-stages.d.ts +2 -0
- package/dist/types/rsc/shell-serve.d.ts +6 -1
- package/dist/types/server/context.d.ts +23 -6
- package/dist/types/server/cookie-store.d.ts +15 -1
- package/dist/types/server/document-location-state.d.ts +5 -2
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +43 -7
- package/dist/types/testing/e2e/server.d.ts +16 -1
- package/dist/types/testing/render-route.d.ts +66 -2
- package/dist/types/testing/serve-shell-request.d.ts +6 -3
- package/dist/types/types/handler-context.d.ts +7 -5
- package/dist/types/types/loader-types.d.ts +7 -4
- package/dist/types/types/segments.d.ts +15 -4
- package/dist/types/urls/path-helper-types.d.ts +7 -5
- package/dist/types/vite/temp-rsc-server.d.ts +2 -2
- package/dist/vite/index.js +449 -189
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +74 -0
- package/skills/app-structure/e-commerce.md +10 -9
- package/skills/app-structure/large-app.md +15 -2
- package/skills/caching/SKILL.md +10 -5
- package/skills/client-urls/SKILL.md +18 -11
- package/skills/comparison/references/framework-comparison.md +13 -8
- package/skills/document-cache/SKILL.md +8 -0
- package/skills/hooks/navigation.md +8 -8
- package/skills/hooks/state.md +24 -8
- package/skills/intercept/SKILL.md +33 -12
- package/skills/layout/SKILL.md +11 -0
- package/skills/links/SKILL.md +101 -4
- package/skills/loader/SKILL.md +49 -22
- package/skills/migrate-nextjs/SKILL.md +8 -2
- package/skills/migrate-react-router/data-and-actions.md +5 -0
- package/skills/ppr/SKILL.md +26 -5
- package/skills/prerender/SKILL.md +4 -1
- package/skills/route/SKILL.md +6 -6
- package/skills/router-setup/SKILL.md +2 -1
- package/skills/server-actions/SKILL.md +41 -8
- package/skills/testing/client-components.md +29 -13
- package/skills/testing/serve-shell-request.md +7 -7
- package/skills/testing/server-actions.md +8 -0
- package/skills/ux-architecture/SKILL.md +67 -18
- package/src/bin/rango.ts +39 -13
- package/src/browser/action-fence.ts +72 -0
- package/src/browser/cookie-name.ts +82 -14
- package/src/browser/invalidate-client-cache.ts +9 -7
- package/src/browser/navigation-bridge.ts +164 -32
- package/src/browser/navigation-client.ts +95 -20
- package/src/browser/navigation-store-handle.ts +67 -5
- package/src/browser/navigation-store.ts +86 -17
- package/src/browser/navigation-transaction.ts +0 -6
- package/src/browser/partial-update.ts +31 -12
- package/src/browser/prefetch/cache.ts +188 -11
- package/src/browser/prefetch/fetch.ts +211 -51
- package/src/browser/prefetch/queue.ts +3 -2
- package/src/browser/rango-state.ts +50 -8
- package/src/browser/react/Link.tsx +20 -9
- package/src/browser/react/use-navigation.ts +1 -1
- package/src/browser/rsc-router.tsx +8 -1
- package/src/browser/server-action-bridge.ts +3 -3
- package/src/browser/types.ts +25 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/include-resolution.ts +40 -3
- package/src/build/route-types/router-processing.ts +170 -25
- package/src/build/route-types/scan-filter.ts +1 -0
- package/src/cache/cache-key-utils.ts +12 -8
- package/src/cache/cache-runtime.ts +5 -1
- package/src/cache/cache-scope.ts +28 -7
- package/src/cache/cf/cf-cache-types.ts +9 -5
- package/src/cache/document-cache-policy.ts +27 -0
- package/src/cache/document-cache.ts +5 -0
- package/src/cache/memory-segment-store.ts +3 -2
- package/src/cache/segment-codec.ts +2 -2
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +12 -7
- package/src/cache/vercel/vercel-cache-store.ts +2 -1
- package/src/client-urls/types.ts +7 -5
- package/src/prefetch-markers.ts +10 -0
- package/src/prefetch-source-scope.ts +137 -0
- package/src/route-definition/helpers-types.ts +8 -5
- package/src/router/intercept-resolution.ts +94 -50
- package/src/router/loader-resolution.ts +8 -6
- package/src/router/match-api.ts +33 -29
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +2 -1
- package/src/router/match-middleware/cache-lookup.ts +21 -13
- package/src/router/match-result.ts +39 -87
- package/src/router/navigation-snapshot.ts +18 -3
- package/src/router/prefetch-cache-ttl.ts +15 -3
- package/src/router/revalidation.ts +53 -23
- package/src/router/router-context.ts +2 -1
- package/src/router/router-options.ts +6 -3
- package/src/router/segment-resolution/loader-cache.ts +21 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
- package/src/router/segment-resolution/revalidation.ts +40 -12
- package/src/router/source-reads.ts +122 -0
- package/src/router/types.ts +2 -1
- package/src/router.ts +11 -12
- package/src/rsc/helpers.ts +8 -3
- package/src/rsc/prefetch-render-error.ts +7 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/request-stages.ts +59 -0
- package/src/rsc/rsc-rendering.ts +49 -33
- package/src/rsc/server-action.ts +2 -1
- package/src/rsc/shell-capture.ts +4 -5
- package/src/rsc/shell-serve.ts +12 -1
- package/src/segment-system.tsx +1 -1
- package/src/server/context.ts +37 -6
- package/src/server/cookie-store.ts +22 -1
- package/src/server/document-location-state.ts +23 -2
- package/src/server/handle-store.ts +2 -0
- package/src/server/request-context.ts +113 -28
- package/src/testing/dispatch.ts +3 -0
- package/src/testing/e2e/fixture.ts +4 -12
- package/src/testing/e2e/server.ts +91 -4
- package/src/testing/render-route.tsx +269 -4
- package/src/testing/serve-shell-request.ts +6 -3
- package/src/types/handler-context.ts +7 -5
- package/src/types/loader-types.ts +7 -4
- package/src/types/segments.ts +15 -4
- package/src/urls/path-helper-types.ts +7 -5
- package/src/vite/temp-rsc-server.ts +2 -2
|
@@ -32,27 +32,41 @@ import {
|
|
|
32
32
|
type EntryData,
|
|
33
33
|
type InterceptEntry,
|
|
34
34
|
type LoaderEntry,
|
|
35
|
+
segmentPinOf,
|
|
35
36
|
} from "../../server/context.js";
|
|
36
37
|
import { requestHeaders } from "../../server/request-headers.js";
|
|
37
38
|
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
38
39
|
import type { CacheScope } from "../../cache/cache-scope.js";
|
|
39
|
-
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
40
|
+
import { PREFETCH_HEADER, PREFETCH_INTENT } from "../../prefetch-markers.js";
|
|
40
41
|
import { WIRE } from "../../wire-params.js";
|
|
41
42
|
import { isInterceptSegment } from "../../browser/intercept-utils.js";
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* What a request says it is, read once when its context is created
|
|
45
46
|
* (RequestContext._requestKind). A fill is marked on the raw URL and wins
|
|
46
|
-
* over the prefetch header.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* over the prefetch header. `"prefetch"` is a speculative prefetch (viewport,
|
|
48
|
+
* render) and `"intent"` a hover or an explicit `prefetch()` call
|
|
49
|
+
* (PREFETCH_INTENT): both are prefetches, and only the first defers. Whether
|
|
50
|
+
* a prefetch defers anything is decided per match, by partialDeferralMode: a
|
|
51
|
+
* shell capture inherits the context of the request that scheduled it and is
|
|
52
|
+
* never a prefetch.
|
|
49
53
|
*/
|
|
50
54
|
export function requestKind(
|
|
51
55
|
request: Request,
|
|
52
56
|
rawUrl: URL,
|
|
53
|
-
):
|
|
57
|
+
): RequestKind | undefined {
|
|
54
58
|
if (rawUrl.searchParams.has(WIRE.fill)) return "fill";
|
|
55
|
-
|
|
59
|
+
const prefetch = requestHeaders(request).get(PREFETCH_HEADER);
|
|
60
|
+
if (prefetch === null) return undefined;
|
|
61
|
+
return prefetch === PREFETCH_INTENT ? "intent" : "prefetch";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** requestKind's answer for a marked request. */
|
|
65
|
+
export type RequestKind = "prefetch" | "intent" | "fill";
|
|
66
|
+
|
|
67
|
+
/** A prefetch of either kind: speculative or intent. */
|
|
68
|
+
export function isPrefetchKind(kind: RequestKind | undefined): boolean {
|
|
69
|
+
return kind === "prefetch" || kind === "intent";
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
/** Static, per matched chain. */
|
|
@@ -173,7 +187,6 @@ const EMPTY_SCOPE: DeferralScope = {
|
|
|
173
187
|
};
|
|
174
188
|
|
|
175
189
|
const scopes = new WeakMap<EntryData, DeferralScope>();
|
|
176
|
-
const warnedBakeLoaders = new Set<string>();
|
|
177
190
|
|
|
178
191
|
/**
|
|
179
192
|
* The deferral scope of a matched chain, memoized on its leaf entry (entries
|
|
@@ -192,9 +205,6 @@ export function resolveDeferralScope(
|
|
|
192
205
|
const loaderIds = new Set<string>();
|
|
193
206
|
const units = new Map<EntryData, DeferralUnit>();
|
|
194
207
|
const chain = new Set(entries);
|
|
195
|
-
// ssr: false on a ppr route is the bake lane (loader-cache.ts
|
|
196
|
-
// resolveLoaderData): shell material, served from the record.
|
|
197
|
-
const pprRoute = isPprEntry(leaf);
|
|
198
208
|
// A clientUrls() group route renders loading() itself and its handler is
|
|
199
209
|
// ClientUrlsRoot (segment-system.tsx keys it by group): a unit would hand
|
|
200
210
|
// React a promise in place of the optimistic destination on screen. So
|
|
@@ -214,20 +224,8 @@ export function resolveDeferralScope(
|
|
|
214
224
|
for (const loaderEntry of entry.loader ?? []) {
|
|
215
225
|
const own = loaderEntry.prefetch === false;
|
|
216
226
|
if (!own && behind.length === 0) continue;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
process.env.NODE_ENV !== "production" &&
|
|
220
|
-
loaderEntry.prefetch === false &&
|
|
221
|
-
!warnedBakeLoaders.has(loaderEntry.loader.$$id)
|
|
222
|
-
) {
|
|
223
|
-
warnedBakeLoaders.add(loaderEntry.loader.$$id);
|
|
224
|
-
console.warn(
|
|
225
|
-
`[rango] loader "${loaderEntry.loader.$$id}" is registered with ssr: false and prefetch: false on a ppr route ("${leaf.id}"). ` +
|
|
226
|
-
`Under ppr an ssr: false loader is baked into the shell and served from it, so prefetch: false is ignored there. Drop one of the two options.`,
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
227
|
+
// An ssr: false loader on a ppr route is in scope too: whether this
|
|
228
|
+
// request serves it from a shell pin is per request (defersLoader).
|
|
231
229
|
loaders.set(loaderEntry, own ? true : behind);
|
|
232
230
|
loaderIds.add(loaderEntry.loader.$$id);
|
|
233
231
|
}
|
|
@@ -303,7 +301,9 @@ export function resolveInterceptDeferralScope(
|
|
|
303
301
|
|
|
304
302
|
/**
|
|
305
303
|
* Which half of the feature a partial request is, if either (`fill` and
|
|
306
|
-
* `prefetch` are requestKind's answer).
|
|
304
|
+
* `prefetch` are requestKind's answer). `prefetch` is a speculative prefetch
|
|
305
|
+
* only: an intent prefetch defers nothing, like the navigation it starts
|
|
306
|
+
* early (R16). Both halves are GETs: a request of
|
|
307
307
|
* another method that carries either marker is a normal request. A prefetch
|
|
308
308
|
* defers only on a plain GET navigation: an action re-renders what it
|
|
309
309
|
* changed, and a shell capture is never a prefetch whatever the request that
|
|
@@ -496,7 +496,7 @@ export function startDeferredLoader(
|
|
|
496
496
|
/**
|
|
497
497
|
* The segment a run of `loaderId` files its handle pushes under: the one
|
|
498
498
|
* that registers it when it is registered (registerDeferredLoader), else the
|
|
499
|
-
* segment
|
|
499
|
+
* segment whose code starts it (segmentPinOf).
|
|
500
500
|
*/
|
|
501
501
|
export function loaderOwner(
|
|
502
502
|
ctx: HandlerContext<any, any>,
|
|
@@ -504,8 +504,7 @@ export function loaderOwner(
|
|
|
504
504
|
): string | undefined {
|
|
505
505
|
const internal = ctx as InternalHandlerContext<any, any>;
|
|
506
506
|
return (
|
|
507
|
-
internal._deferredLoaders?.get(loaderId)?.owner ??
|
|
508
|
-
internal._currentSegmentId
|
|
507
|
+
internal._deferredLoaders?.get(loaderId)?.owner ?? segmentPinOf(internal)
|
|
509
508
|
);
|
|
510
509
|
}
|
|
511
510
|
|
|
@@ -514,18 +513,26 @@ export function loaderOwner(
|
|
|
514
513
|
* does not hold that segment, and the loader is flagged itself or sits behind
|
|
515
514
|
* the fallback of a flagged entry the client does not hold either. Under a
|
|
516
515
|
* flagged layout the client holds, a new route's loaders run.
|
|
516
|
+
*
|
|
517
|
+
* `servesPin` (loader-cache.ts servesLoaderPin) answers whether this request
|
|
518
|
+
* serves the loader from a pin of a document-captured shell. Such a loader
|
|
519
|
+
* is not deferred: its body does not decide the value, and the pin is what
|
|
520
|
+
* the document shows. On every other request it would run, so it is
|
|
521
|
+
* deferred as on a route without ppr. Asked when loaders resolve, after a
|
|
522
|
+
* replay HIT has armed the pins (rsc-rendering.ts matchPartialWithPprReplay).
|
|
517
523
|
*/
|
|
518
524
|
export function defersLoader(
|
|
519
525
|
ctx: HandlerContext<any, any>,
|
|
520
526
|
loaderEntry: LoaderEntry,
|
|
521
527
|
segmentId: string,
|
|
528
|
+
servesPin: (loaderEntry: LoaderEntry) => boolean,
|
|
522
529
|
): boolean {
|
|
523
530
|
const plan = getPrefetchDeferral(ctx);
|
|
524
531
|
if (plan?.mode !== "prefetch" || plan.held.has(segmentId)) return false;
|
|
525
532
|
const by = plan.scope.loaders.get(loaderEntry);
|
|
526
|
-
|
|
527
|
-
by === true || (by !== undefined && by.some((id) => !plan.held.has(id)))
|
|
528
|
-
);
|
|
533
|
+
const deferrable =
|
|
534
|
+
by === true || (by !== undefined && by.some((id) => !plan.held.has(id)));
|
|
535
|
+
return deferrable && !servesPin(loaderEntry);
|
|
529
536
|
}
|
|
530
537
|
|
|
531
538
|
/** A unit whose handler output is not stored: a prefetch can skip it. */
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
pushRevalidationTraceEntry,
|
|
41
41
|
isTraceActive,
|
|
42
42
|
} from "../logging.js";
|
|
43
|
-
import { resolveLoaderData } from "./loader-cache.js";
|
|
43
|
+
import { resolveLoaderData, servesLoaderPin } from "./loader-cache.js";
|
|
44
44
|
import {
|
|
45
45
|
entryLoadingMasksLoaders,
|
|
46
46
|
isShellCaptureActive,
|
|
@@ -65,6 +65,7 @@ import {
|
|
|
65
65
|
noteThrower,
|
|
66
66
|
} from "./helpers.js";
|
|
67
67
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
68
|
+
import { isRenderableLoading } from "../../types/boundaries.js";
|
|
68
69
|
import { isLayoutLoaderCopy } from "../../client-urls/layout-loader-copies.js";
|
|
69
70
|
import { getRouterContext } from "../router-context.js";
|
|
70
71
|
import { observeEvent, observeSegmentHandler } from "../instrument.js";
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
track,
|
|
75
76
|
RangoContext,
|
|
76
77
|
runInsideLoaderScope,
|
|
78
|
+
runAsSegment,
|
|
77
79
|
latchCachedHeaderScope,
|
|
78
80
|
latchPprHeaderScopeForEntries,
|
|
79
81
|
} from "../../server/context.js";
|
|
@@ -267,6 +269,28 @@ export function ownerOf(
|
|
|
267
269
|
};
|
|
268
270
|
}
|
|
269
271
|
|
|
272
|
+
/**
|
|
273
|
+
* A route's loader copies inherited into a child layout with parallel slots
|
|
274
|
+
* are redundant beside the route's own loader segments: true when the route
|
|
275
|
+
* renders no loading() fallback on any request, so nothing hides the route's
|
|
276
|
+
* copy from useLoader() in the slots. A redundant copy is never resolved, on
|
|
277
|
+
* any request: the walkers check this before they inherit.
|
|
278
|
+
*
|
|
279
|
+
* Decided from the entry, never from the route segment's `loading`, which
|
|
280
|
+
* differs between requests: `loading(fallback, { ssr: false })` is `null` on
|
|
281
|
+
* a document (applyDocumentModeLoading) and the fallback on partials, and the
|
|
282
|
+
* walkers emit `loading(false)` as `null` too. A copy sent on one request and
|
|
283
|
+
* not on the next gives the layout a loader entry it did not have, so
|
|
284
|
+
* renderSegments wraps it in StreamedLoaderErrorBoundary and React remounts
|
|
285
|
+
* it with its Outlet (docs/tree-structure.md). Resolving a redundant copy and
|
|
286
|
+
* dropping it from the payload later is not enough: when the route's own
|
|
287
|
+
* loader is held (its revalidate() declines), the copy is new to the client,
|
|
288
|
+
* so it re-runs and is sent without the route's copy beside it.
|
|
289
|
+
*/
|
|
290
|
+
export function inheritedCopyRedundant(route: EntryData): boolean {
|
|
291
|
+
return !isRenderableLoading(route.loading) && route.loadingSsr !== false;
|
|
292
|
+
}
|
|
293
|
+
|
|
270
294
|
/**
|
|
271
295
|
* resolveOwnerLoaders has something to resolve for `entry`'s loaders. The
|
|
272
296
|
* walker checks before the call: most entries own none, and the async call
|
|
@@ -526,10 +550,14 @@ export async function resolveOwnerLoaders<TEnv>(
|
|
|
526
550
|
// Only a loader on a segment the client lacks (defersLoader). It
|
|
527
551
|
// would have run. Registered, not started: it runs, and reads its
|
|
528
552
|
// cache(), only if a reader starts it (startDeferredLoader).
|
|
529
|
-
if (defersLoader(ctx, loaderEntry, segmentId)) {
|
|
553
|
+
if (defersLoader(ctx, loaderEntry, segmentId, servesLoaderPin)) {
|
|
530
554
|
registerDeferredLoader(ctx, loader.$$id, shortCode, () =>
|
|
531
555
|
kickoff(loaderEntry, segmentId),
|
|
532
556
|
);
|
|
557
|
+
// A ppr shell record this request restored keeps an ssr: false
|
|
558
|
+
// loader's pushes as copies that wait for its run (loaderPins "hole").
|
|
559
|
+
// No run is made here: drop them, the fill sends the run's pushes.
|
|
560
|
+
_getRequestContext()?._handleStore?.settleLoaderRun(loader.$$id);
|
|
533
561
|
const deferred: ResolvedSegment = {
|
|
534
562
|
id: segmentId,
|
|
535
563
|
namespace: owner.namespace,
|
|
@@ -656,7 +684,8 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
656
684
|
entry.type === "route" &&
|
|
657
685
|
entry.loader &&
|
|
658
686
|
entry.loader.length > 0 &&
|
|
659
|
-
Object.keys(layoutEntry.parallel).length > 0
|
|
687
|
+
Object.keys(layoutEntry.parallel).length > 0 &&
|
|
688
|
+
!inheritedCopyRedundant(entry)
|
|
660
689
|
) {
|
|
661
690
|
const inherited = await resolveOwnerLoaders(
|
|
662
691
|
pass,
|
|
@@ -666,7 +695,6 @@ export async function resolveLoadersOnly<TEnv>(
|
|
|
666
695
|
for (const seg of inherited.segments) {
|
|
667
696
|
if (!seenIds.has(seg.id)) {
|
|
668
697
|
seenIds.add(seg.id);
|
|
669
|
-
seg._inherited = true;
|
|
670
698
|
allLoaderSegments.push(seg);
|
|
671
699
|
}
|
|
672
700
|
}
|
|
@@ -805,7 +833,7 @@ async function resolveParallelSlotComponent<TEnv>(
|
|
|
805
833
|
// A throw before the slot streams reaches the owning entry's boundary.
|
|
806
834
|
try {
|
|
807
835
|
if (hasLoadingFallback) {
|
|
808
|
-
const result = run
|
|
836
|
+
const result = runAsSegment(parallelId, run);
|
|
809
837
|
if (result instanceof Promise) {
|
|
810
838
|
warnOnStreamedResponse(result, parallelId);
|
|
811
839
|
result.finally(doneHandler).catch(() => {});
|
|
@@ -1174,7 +1202,9 @@ async function resolveRouteComponent<TEnv>(
|
|
|
1174
1202
|
}
|
|
1175
1203
|
if (!actionContext) {
|
|
1176
1204
|
const result = handleHandlerResult(
|
|
1177
|
-
|
|
1205
|
+
runAsSegment(entry.shortCode, () =>
|
|
1206
|
+
observeSegmentHandler(entry.shortCode, entry.id, handler, context),
|
|
1207
|
+
),
|
|
1178
1208
|
);
|
|
1179
1209
|
if (result instanceof Promise) {
|
|
1180
1210
|
warnOnStreamedResponse(result, entry.id);
|
|
@@ -1475,17 +1505,14 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
1475
1505
|
parentRouteEntry &&
|
|
1476
1506
|
parentRouteEntry.loader &&
|
|
1477
1507
|
parentRouteEntry.loader.length > 0 &&
|
|
1478
|
-
Object.keys(orphan.parallel).length > 0
|
|
1508
|
+
Object.keys(orphan.parallel).length > 0 &&
|
|
1509
|
+
!inheritedCopyRedundant(parentRouteEntry)
|
|
1479
1510
|
) {
|
|
1480
1511
|
const inheritedResult = await resolveOwnerLoaders(
|
|
1481
1512
|
pass,
|
|
1482
1513
|
ownerOf(parentRouteEntry, orphan.shortCode),
|
|
1483
1514
|
belongsToRoute,
|
|
1484
1515
|
);
|
|
1485
|
-
// Tag as inherited so buildMatchResult can deduplicate when safe
|
|
1486
|
-
for (const s of inheritedResult.segments) {
|
|
1487
|
-
s._inherited = true;
|
|
1488
|
-
}
|
|
1489
1516
|
segments.push(...inheritedResult.segments);
|
|
1490
1517
|
matchedIds.push(...inheritedResult.matchedIds);
|
|
1491
1518
|
}
|
|
@@ -1505,7 +1532,8 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
1505
1532
|
handlerRan = true;
|
|
1506
1533
|
// Its handler's pushes are its own. Pinned only while it runs: a route
|
|
1507
1534
|
// under loading() streams its handler, whose pushes after an await
|
|
1508
|
-
// read the pin after this returns
|
|
1535
|
+
// read the pin after this returns where a runtime lost its
|
|
1536
|
+
// runAsSegment scope (segmentPinOf).
|
|
1509
1537
|
const internal = context as InternalHandlerContext<any, TEnv>;
|
|
1510
1538
|
const pinned = internal._currentSegmentId;
|
|
1511
1539
|
internal._currentSegmentId = orphan.shortCode;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording what revalidate() rules read of the source page, so a prefetch
|
|
3
|
+
* answer a rule kept is shared between the pages with equal values of what
|
|
4
|
+
* was read (prefetch-source-scope.ts).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createSourceReads,
|
|
9
|
+
formatSourceLabel,
|
|
10
|
+
mergeSourceReads,
|
|
11
|
+
PARAMS_SCOPE_PREFIX,
|
|
12
|
+
shapeOfReads,
|
|
13
|
+
type SourcePage,
|
|
14
|
+
type SourceReads,
|
|
15
|
+
} from "../prefetch-source-scope.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The source group of a match whose diff a source read decided: the reads
|
|
19
|
+
* every kept decision made, unioned, and the source page they read.
|
|
20
|
+
*/
|
|
21
|
+
export interface SourceHeld extends SourcePage {
|
|
22
|
+
reads: SourceReads;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A copy of `params` whose reads land in `reads`: an enumerable getter per
|
|
27
|
+
* key, so a spread, Object.entries, JSON.stringify or structuredClone reads
|
|
28
|
+
* every key. Not a Proxy: structuredClone refuses one. A read of an absent
|
|
29
|
+
* key is not recorded and needs no record: the key set is part of the group
|
|
30
|
+
* (`reads.keys`), so a page in the group lacks that key too.
|
|
31
|
+
*/
|
|
32
|
+
export function recordingParams(
|
|
33
|
+
params: Readonly<Record<string, string>>,
|
|
34
|
+
reads: SourceReads,
|
|
35
|
+
): Record<string, string> {
|
|
36
|
+
reads.keys = true;
|
|
37
|
+
const view: Record<string, string> = {};
|
|
38
|
+
for (const name of Object.keys(params)) {
|
|
39
|
+
Object.defineProperty(view, name, {
|
|
40
|
+
enumerable: true,
|
|
41
|
+
configurable: true,
|
|
42
|
+
get() {
|
|
43
|
+
reads.params.add(name);
|
|
44
|
+
return params[name];
|
|
45
|
+
},
|
|
46
|
+
// A rule may write to the object, as to the plain one it replaces:
|
|
47
|
+
// what it wrote is its own value, not the source's.
|
|
48
|
+
set(value: string) {
|
|
49
|
+
Object.defineProperty(view, name, {
|
|
50
|
+
value,
|
|
51
|
+
writable: true,
|
|
52
|
+
enumerable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return view;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add `reads` to the source group of the match `context` decides for. All
|
|
63
|
+
* of one match's decisions read the same source page; a caller that reads
|
|
64
|
+
* no route name passes none.
|
|
65
|
+
*/
|
|
66
|
+
export function markSourceHeld(
|
|
67
|
+
context: { _sourceHeld?: SourceHeld },
|
|
68
|
+
reads: SourceReads,
|
|
69
|
+
page: SourcePage,
|
|
70
|
+
): void {
|
|
71
|
+
const held = (context._sourceHeld ??= {
|
|
72
|
+
reads: createSourceReads(),
|
|
73
|
+
params: page.params,
|
|
74
|
+
routeName: undefined,
|
|
75
|
+
});
|
|
76
|
+
if (page.routeName !== undefined) held.routeName = page.routeName;
|
|
77
|
+
mergeSourceReads(held.reads, reads);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The `x-rsc-prefetch-scope` of a partial response, or undefined when every
|
|
82
|
+
* source page may share it.
|
|
83
|
+
*
|
|
84
|
+
* - `source`: the exact source page's. A route an intercept targets renders
|
|
85
|
+
* the modal or the full page depending on the source, so a full page
|
|
86
|
+
* prefetched where the intercept does not apply would otherwise serve a
|
|
87
|
+
* later click where it does (#1007); a response with deferred units
|
|
88
|
+
* answers one page's prefetch (`sourceOnly`); a kept rule that read
|
|
89
|
+
* `currentUrl` can depend on anything in it; and a source-held answer that
|
|
90
|
+
* sends nothing (`sentNothing`) commits treeless on the page that adopts
|
|
91
|
+
* it, which keeps its whole tree without checking it holds the ids the
|
|
92
|
+
* answer kept (partial-update.ts): only the source is known to hold them.
|
|
93
|
+
* - `params; <label>`: a revalidate() rule read the source's params or route
|
|
94
|
+
* name and kept a segment. A page that adopted the answer would keep its
|
|
95
|
+
* own copy of that segment (#1331), which is right only where the rule
|
|
96
|
+
* decides the same: on pages with equal values of what it read. The label
|
|
97
|
+
* names them (prefetch-source-scope.ts); the browser stores the answer
|
|
98
|
+
* under it. A page that lacks a kept id refetches (the commit's
|
|
99
|
+
* missing-segment check), as for a shared answer.
|
|
100
|
+
*/
|
|
101
|
+
export function prefetchScopeOf(
|
|
102
|
+
sourceOnly: boolean,
|
|
103
|
+
held: SourceHeld | undefined,
|
|
104
|
+
sentNothing: boolean,
|
|
105
|
+
): string | undefined {
|
|
106
|
+
if (sourceOnly) return "source";
|
|
107
|
+
if (!held) return undefined;
|
|
108
|
+
if (held.reads.url || sentNothing) return "source";
|
|
109
|
+
return (
|
|
110
|
+
PARAMS_SCOPE_PREFIX + formatSourceLabel(shapeOfReads(held.reads), held)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Every param of the source read: its params must equal the page's. */
|
|
115
|
+
export function readsOfAllParams(
|
|
116
|
+
params: Readonly<Record<string, string>>,
|
|
117
|
+
): SourceReads {
|
|
118
|
+
const reads = createSourceReads();
|
|
119
|
+
reads.keys = true;
|
|
120
|
+
for (const name of Object.keys(params)) reads.params.add(name);
|
|
121
|
+
return reads;
|
|
122
|
+
}
|
package/src/router/types.ts
CHANGED
|
@@ -91,10 +91,11 @@ export interface MatchApiDeps<TEnv = any> {
|
|
|
91
91
|
findMatch: (pathname: string, ms?: any) => any;
|
|
92
92
|
getMetricsStore: () => any;
|
|
93
93
|
findInterceptForRoute: (
|
|
94
|
-
|
|
94
|
+
routeKeys: string | readonly string[],
|
|
95
95
|
parentEntry: EntryData | null,
|
|
96
96
|
selectorContext: InterceptSelectorContext | null,
|
|
97
97
|
isAction: boolean,
|
|
98
|
+
held?: ReadonlySet<string>,
|
|
98
99
|
) => { intercept: InterceptEntry; entry: EntryData } | null;
|
|
99
100
|
callOnError: SegmentResolutionDeps<TEnv>["callOnError"];
|
|
100
101
|
findNearestErrorBoundary: SegmentResolutionDeps<TEnv>["findNearestErrorBoundary"];
|
package/src/router.ts
CHANGED
|
@@ -654,12 +654,21 @@ export function createRouter<TEnv = any>(
|
|
|
654
654
|
viewTransitionDefault: viewTransitionOption,
|
|
655
655
|
};
|
|
656
656
|
|
|
657
|
+
// Create segment resolution wrappers bound to segmentDeps
|
|
658
|
+
const {
|
|
659
|
+
resolveLoadersOnly,
|
|
660
|
+
buildEntryRevalidateMap,
|
|
661
|
+
resolveAllSegments,
|
|
662
|
+
findInterceptForRoute,
|
|
663
|
+
resolveInterceptEntry,
|
|
664
|
+
resolveInterceptLoadersOnly,
|
|
665
|
+
} = createSegmentWrappers<TEnv>(segmentDeps);
|
|
666
|
+
|
|
657
667
|
// Match API dependencies
|
|
658
668
|
const matchApiDeps: MatchApiDeps<TEnv> = {
|
|
659
669
|
findMatch: (pathname: string, ms?: any) => findMatch(pathname, ms),
|
|
660
670
|
getMetricsStore,
|
|
661
|
-
findInterceptForRoute
|
|
662
|
-
findInterceptForRoute(routeKey, parentEntry, selectorContext, isAction),
|
|
671
|
+
findInterceptForRoute,
|
|
663
672
|
callOnError,
|
|
664
673
|
findNearestErrorBoundary,
|
|
665
674
|
// Use per-router manifest when available, otherwise the static named map
|
|
@@ -667,16 +676,6 @@ export function createRouter<TEnv = any>(
|
|
|
667
676
|
getRouteMap: () => getRouterManifest(routerId) ?? mergedRouteMap,
|
|
668
677
|
};
|
|
669
678
|
|
|
670
|
-
// Create segment resolution wrappers bound to segmentDeps
|
|
671
|
-
const {
|
|
672
|
-
resolveLoadersOnly,
|
|
673
|
-
buildEntryRevalidateMap,
|
|
674
|
-
resolveAllSegments,
|
|
675
|
-
findInterceptForRoute,
|
|
676
|
-
resolveInterceptEntry,
|
|
677
|
-
resolveInterceptLoadersOnly,
|
|
678
|
-
} = createSegmentWrappers<TEnv>(segmentDeps);
|
|
679
|
-
|
|
680
679
|
// Lazy evaluation deps — captures closure state for extracted evaluateLazyEntry
|
|
681
680
|
const lazyEvalDeps: LazyEvalDeps<TEnv> = {
|
|
682
681
|
routesEntries,
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
setRequestContextParams,
|
|
11
11
|
} from "../server/request-context.js";
|
|
12
12
|
import type { RequestContext } from "../server/request-context.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
resolveLocationStateEntries,
|
|
15
|
+
type LocationStateEntry,
|
|
16
|
+
} from "../browser/react/location-state-shared.js";
|
|
14
17
|
import {
|
|
15
18
|
isRedirectResponse,
|
|
16
19
|
isWebSocketUpgradeResponse,
|
|
@@ -343,13 +346,15 @@ export function interceptRedirectForPartial(
|
|
|
343
346
|
* is populated. A `document` payload (a page load's or a no-JS form post's
|
|
344
347
|
* bootstrap payload) that carries state marks the request
|
|
345
348
|
* `_documentLocationState`: it is the visitor's, so no shared cache keeps it
|
|
346
|
-
* (server/document-location-state.ts).
|
|
349
|
+
* (server/document-location-state.ts). `locationState` defaults to the
|
|
350
|
+
* current request context's; a shell HIT's tail passes the state set before
|
|
351
|
+
* its render (rsc/rsc-rendering.ts serveShellHit).
|
|
347
352
|
*/
|
|
348
353
|
export function attachLocationStateIfPresent(
|
|
349
354
|
payload: RscPayload,
|
|
350
355
|
document = false,
|
|
356
|
+
locationState: LocationStateEntry[] | undefined = getLocationState(),
|
|
351
357
|
): void {
|
|
352
|
-
const locationState = getLocationState();
|
|
353
358
|
if (locationState && locationState.length > 0) {
|
|
354
359
|
payload.metadata!.locationState =
|
|
355
360
|
resolveLocationStateEntries(locationState);
|
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
* So the body ends cleanly with one more row, PREFETCH_FAILED_ROW, which the
|
|
12
12
|
* Flight decoder skips and teeWithCompletion (browser/response-adapter.ts)
|
|
13
13
|
* reads as a stream that did not end cleanly: the client evicts the prefetch
|
|
14
|
-
* and never respawns it.
|
|
15
|
-
*
|
|
14
|
+
* and never respawns it. The router puts no Cache-Control of its own on a
|
|
15
|
+
* prefetch with a live part, the only kind whose render can report an error
|
|
16
|
+
* here, so the browser HTTP cache keeps none of it unless the route sets one;
|
|
17
|
+
* a prefetch with nothing live in it carries `private, max-age`
|
|
18
|
+
* (request-stages.ts applyStoredPrefetchCacheControl); a prefetch
|
|
19
|
+
* scoped to its source page or carrying deferred units is sent
|
|
20
|
+
* `private, no-cache` (rsc-rendering.ts NOT_REUSED).
|
|
16
21
|
*
|
|
17
22
|
* The errors are the render's `_renderErrors`: a Flight error row, a streamed
|
|
18
23
|
* handler recovered into its boundary, a loader that threw (redirect() and
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
setRequestContextParams,
|
|
14
14
|
} from "../server/request-context.js";
|
|
15
15
|
import { printServerError } from "../server/print-server-error.js";
|
|
16
|
+
import { runActionBody } from "../server/cookie-store.js";
|
|
16
17
|
import { createSsrHtmlStage } from "./ssr-setup.js";
|
|
17
18
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
18
19
|
import { executeMiddleware } from "../router/middleware.js";
|
|
@@ -187,7 +188,7 @@ async function runFormAction<TEnv>(
|
|
|
187
188
|
// Meter the no-JS form action as the action phase, same as the JS path.
|
|
188
189
|
actionResult = await observePhase(
|
|
189
190
|
PHASES.action(useActionStateId ?? "useActionState"),
|
|
190
|
-
() => boundAction(),
|
|
191
|
+
() => runActionBody(() => boundAction()),
|
|
191
192
|
);
|
|
192
193
|
} catch (error) {
|
|
193
194
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
@@ -216,7 +217,7 @@ async function runFormAction<TEnv>(
|
|
|
216
217
|
try {
|
|
217
218
|
const loadedAction = await ctx.loadServerAction(directActionId);
|
|
218
219
|
actionResult = await observePhase(PHASES.action(directActionId), () =>
|
|
219
|
-
loadedAction.apply(null, args),
|
|
220
|
+
runActionBody(() => loadedAction.apply(null, args)),
|
|
220
221
|
);
|
|
221
222
|
} catch (error) {
|
|
222
223
|
// Handle thrown redirect (e.g., throw redirect('/path'))
|
|
@@ -84,6 +84,9 @@ import {
|
|
|
84
84
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
85
85
|
import type { CacheErrorCategory } from "../cache/cache-error.js";
|
|
86
86
|
import { WIRE } from "../wire-params.js";
|
|
87
|
+
import { isPrefetchKind } from "../router/segment-resolution/prefetch-deferral.js";
|
|
88
|
+
import { storedPrefetchCacheControl } from "../router/prefetch-cache-ttl.js";
|
|
89
|
+
import { carriesClientState } from "../cache/document-cache-policy.js";
|
|
87
90
|
import {
|
|
88
91
|
carryLocationStateAcrossRedirect,
|
|
89
92
|
consumeDocumentLocationState,
|
|
@@ -103,6 +106,8 @@ export interface StageRouter<TEnv = unknown> {
|
|
|
103
106
|
readonly onTimeout?: OnTimeoutCallback<TEnv>;
|
|
104
107
|
/** Names the cookie location state crosses a document redirect in. */
|
|
105
108
|
readonly resolvedStateCookieName: string;
|
|
109
|
+
/** The in-memory prefetch TTL in ms; also a stored prefetch's max-age. */
|
|
110
|
+
readonly prefetchCacheTTL: number;
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
/** Report an error through the router's onError. */
|
|
@@ -682,6 +687,57 @@ export interface FinalizeRequestOptions<TEnv> {
|
|
|
682
687
|
beforeGuard?: (response: Response) => void;
|
|
683
688
|
}
|
|
684
689
|
|
|
690
|
+
/**
|
|
691
|
+
* A prefetch with nothing live in it (`_servedFromStorage`: a document cache
|
|
692
|
+
* HIT, or a partial whose every sent segment came from storage and ran no
|
|
693
|
+
* loader) leaves with the router's prefetch Cache-Control, so the browser
|
|
694
|
+
* HTTP cache can answer a later prefetch or the click with it.
|
|
695
|
+
* Nothing in such an answer can fail after its 200 left; any other prefetch
|
|
696
|
+
* carries none (#1170). Set here, after every middleware: the document cache
|
|
697
|
+
* reads the route's own header when it stores the response (s-maxage), the
|
|
698
|
+
* browser gets this one, in place of any other the route set.
|
|
699
|
+
*
|
|
700
|
+
* Left as it is: a partial sent NOT_REUSED (scoped to its source page, or
|
|
701
|
+
* carrying deferred units), a header that forbids storing it (`no-store`,
|
|
702
|
+
* `no-cache`, a zero `max-age`), a redirect a middleware returned after the render (`redirect`,
|
|
703
|
+
* converted to a Flight redirect above), and a response carrying one
|
|
704
|
+
* client's state (a per-client signal header, location state), which a
|
|
705
|
+
* stored copy would replay to later requests.
|
|
706
|
+
*/
|
|
707
|
+
function applyStoredPrefetchCacheControl(
|
|
708
|
+
response: Response,
|
|
709
|
+
reqCtx: RequestContext<any>,
|
|
710
|
+
url: URL,
|
|
711
|
+
router: StageRouter<any>,
|
|
712
|
+
redirect: boolean,
|
|
713
|
+
): void {
|
|
714
|
+
if (
|
|
715
|
+
redirect ||
|
|
716
|
+
!reqCtx._servedFromStorage ||
|
|
717
|
+
!isPrefetchKind(reqCtx._requestKind) ||
|
|
718
|
+
!url.searchParams.has(WIRE.partial) ||
|
|
719
|
+
reqCtx._payloadSourceScoped ||
|
|
720
|
+
reqCtx._payloadDeferred ||
|
|
721
|
+
response.status !== 200 ||
|
|
722
|
+
!response.headers.get("content-type")?.startsWith("text/x-component") ||
|
|
723
|
+
FORBIDS_STORE.test(response.headers.get("cache-control") ?? "") ||
|
|
724
|
+
carriesClientState(response, reqCtx)
|
|
725
|
+
) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const cacheControl = storedPrefetchCacheControl(router.prefetchCacheTTL);
|
|
729
|
+
if (cacheControl) trySetHeader(response, "cache-control", cacheControl);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* A Cache-Control directive that forbids reusing the response unvalidated:
|
|
734
|
+
* `no-store`, `no-cache`, or a zero `max-age` (`s-maxage` is the shared
|
|
735
|
+
* caches' and does not match). The route asked for a fresh answer each time;
|
|
736
|
+
* the router's `max-age` would override it.
|
|
737
|
+
*/
|
|
738
|
+
const FORBIDS_STORE =
|
|
739
|
+
/(^|[\s,;])(no-store|no-cache|max-age=["']?0+["']?)(?=$|[\s,;=])/i;
|
|
740
|
+
|
|
685
741
|
/**
|
|
686
742
|
* The response the global chain returned, made ready to leave: a partial or
|
|
687
743
|
* action request's redirect converted, onResponse callbacks drained, the
|
|
@@ -694,6 +750,7 @@ export function finalizeRequestResponse<TEnv>(
|
|
|
694
750
|
const { ctx, reqCtx, request, url, env } = opts;
|
|
695
751
|
const router = ctx.router;
|
|
696
752
|
let response: Response;
|
|
753
|
+
let redirect = false;
|
|
697
754
|
if (opts.hasMiddleware) {
|
|
698
755
|
if (
|
|
699
756
|
url.searchParams.has(WIRE.partial) ||
|
|
@@ -704,6 +761,7 @@ export function finalizeRequestResponse<TEnv>(
|
|
|
704
761
|
ctx.createRedirectFlightResponse,
|
|
705
762
|
{ requestOrigin: url.origin, basename: router.basename },
|
|
706
763
|
);
|
|
764
|
+
redirect = intercepted !== null;
|
|
707
765
|
response = intercepted ?? finalizeResponse(downstream);
|
|
708
766
|
} else {
|
|
709
767
|
response = finalizeResponse(downstream);
|
|
@@ -715,6 +773,7 @@ export function finalizeRequestResponse<TEnv>(
|
|
|
715
773
|
if (opts.rewrittenTo !== undefined) {
|
|
716
774
|
trySetHeader(response, REWRITE_HEADER, opts.rewrittenTo);
|
|
717
775
|
}
|
|
776
|
+
applyStoredPrefetchCacheControl(response, reqCtx, url, router, redirect);
|
|
718
777
|
opts.beforeGuard?.(response);
|
|
719
778
|
|
|
720
779
|
// Single open-redirect chokepoint: every response (PE, full-page,
|