@rangojs/router 0.24.4 → 0.25.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 +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -21,7 +21,11 @@ import {
|
|
|
21
21
|
rebaseRequestContext,
|
|
22
22
|
type RequestContext,
|
|
23
23
|
} from "../server/request-context.js";
|
|
24
|
-
import {
|
|
24
|
+
import { bodylessRequest } from "../server/request-headers.js";
|
|
25
|
+
import {
|
|
26
|
+
recordRewriteHop,
|
|
27
|
+
recordRewriteShell,
|
|
28
|
+
} from "../server/rewrite-trail.js";
|
|
25
29
|
|
|
26
30
|
/** Response header naming the route path a rewritten request rendered. */
|
|
27
31
|
export const REWRITE_HEADER = "x-rango-rewrite";
|
|
@@ -45,11 +49,9 @@ export function resolveRewriteTarget(
|
|
|
45
49
|
const path =
|
|
46
50
|
"path" in target
|
|
47
51
|
? prefixBasename(target.path, basename)
|
|
48
|
-
:
|
|
49
|
-
// missing ones instead of resolving to its raw pattern.
|
|
50
|
-
createReverseFunction(routeMap)(
|
|
52
|
+
: createReverseFunction(routeMap)(
|
|
51
53
|
target.name,
|
|
52
|
-
target.params
|
|
54
|
+
target.params,
|
|
53
55
|
target.search,
|
|
54
56
|
);
|
|
55
57
|
const resolved = stripInternalParams(new URL(path, base.origin));
|
|
@@ -94,11 +96,38 @@ export interface RewritePass {
|
|
|
94
96
|
hops: number;
|
|
95
97
|
}
|
|
96
98
|
|
|
99
|
+
let lateLockReported = false;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `.disallowMutations()` thrown after the action ran cannot refuse it. Dev
|
|
103
|
+
* warns on every throw; production logs one error per isolate, so a lock
|
|
104
|
+
* that never refused anything is not silent there.
|
|
105
|
+
*/
|
|
106
|
+
function reportLateLock(rewrite: Rewrite): void {
|
|
107
|
+
const message = `[rango] ${describeRewriteTarget(rewrite.target)}.disallowMutations() came after the server action or form post ran, so it cannot refuse it: route middleware, and global middleware on a later rewritten pass, run only after the action. The render moves to the target. To refuse mutations, throw it from global middleware (router.use()) matched on the URL the form posts to.`;
|
|
108
|
+
if (process.env.NODE_ENV !== "production") {
|
|
109
|
+
console.warn(message);
|
|
110
|
+
} else if (!lateLockReported) {
|
|
111
|
+
lateLockReported = true;
|
|
112
|
+
console.error(message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
97
116
|
/**
|
|
98
117
|
* Move the request context to `rewrite`'s target for the next pass: check
|
|
99
118
|
* that this request can be rewritten, resolve the target, count the hop,
|
|
100
|
-
* then rebase the context. Throws the site error for a
|
|
101
|
-
*
|
|
119
|
+
* then rebase the context. Throws the site error for a loader fetch, a
|
|
120
|
+
* refused mutation or one whose body middleware read before its action ran,
|
|
121
|
+
* a resolution error, and RewriteLoopError.
|
|
122
|
+
*
|
|
123
|
+
* A mutation moves too (#1145). Before its action ran (a rewrite from global
|
|
124
|
+
* middleware) it moves with its body, and the target's pass runs the action
|
|
125
|
+
* after the target's global middleware, as a direct post there would. After
|
|
126
|
+
* it ran (route middleware wraps only the re-render), the target's pass
|
|
127
|
+
* re-renders from `reqCtx._actionRan`. `disallowMutations()` refuses the
|
|
128
|
+
* first, so a lock never runs a mutation; the second is already committed,
|
|
129
|
+
* and refusing it reports an error for a write that happened, so the caller
|
|
130
|
+
* retries it.
|
|
102
131
|
*/
|
|
103
132
|
export function applyRewrite(
|
|
104
133
|
rewrite: Rewrite,
|
|
@@ -113,12 +142,30 @@ export function applyRewrite(
|
|
|
113
142
|
throw rewriteNotSupported(rewrite, "loader-fetch");
|
|
114
143
|
}
|
|
115
144
|
if (passRequest.method !== "GET" && passRequest.method !== "HEAD") {
|
|
116
|
-
|
|
145
|
+
if (!reqCtx._actionRan) {
|
|
146
|
+
if (rewrite.mutationsDisallowed) {
|
|
147
|
+
throw rewriteNotSupported(rewrite, "mutation");
|
|
148
|
+
}
|
|
149
|
+
// The target's pass hands the body to the action: one middleware read
|
|
150
|
+
// would run it with no arguments.
|
|
151
|
+
if (passRequest.bodyUsed) throw rewriteNotSupported(rewrite, "body-read");
|
|
152
|
+
} else if (rewrite.mutationsDisallowed) {
|
|
153
|
+
reportLateLock(rewrite);
|
|
154
|
+
}
|
|
117
155
|
}
|
|
118
156
|
const target = resolveRewriteTarget(rewrite, routeMap, reqCtx.url, basename);
|
|
119
157
|
const hops = recordRewriteHop(reqCtx, reqCtx.url, target);
|
|
158
|
+
recordRewriteShell(reqCtx, rewrite.shellAudience);
|
|
120
159
|
const url = rewriteTransportUrl(target, reqCtx.originalUrl);
|
|
121
|
-
|
|
160
|
+
// A mutation keeps its body: one whose action has not run yet, and a no-JS
|
|
161
|
+
// form post, since the PE handler reads a clone, so a form-field check
|
|
162
|
+
// reads it on every pass. A JS action that ran consumed it
|
|
163
|
+
// (`new Request(url, used)` throws): the same request without it or its
|
|
164
|
+
// content-type, so a body check gated on the content-type passes over this
|
|
165
|
+
// pass, which never runs the action.
|
|
166
|
+
const request = passRequest.bodyUsed
|
|
167
|
+
? bodylessRequest(url, passRequest, passRequest.method)
|
|
168
|
+
: new Request(url, passRequest);
|
|
122
169
|
rebaseRequestContext(reqCtx, request, url);
|
|
123
170
|
return { request, url, target, hops };
|
|
124
171
|
}
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
renderRscFlightStage,
|
|
41
41
|
renderRscResponse,
|
|
42
42
|
} from "./render-pipeline.js";
|
|
43
|
+
import { markPrefetchOnRenderError } from "./prefetch-render-error.js";
|
|
43
44
|
import {
|
|
44
45
|
createRoutineTrace,
|
|
45
46
|
handoff,
|
|
@@ -51,7 +52,7 @@ import {
|
|
|
51
52
|
import type { HandlerContext } from "./handler-context.js";
|
|
52
53
|
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
53
54
|
import { publicRouteName } from "../route-name.js";
|
|
54
|
-
import { buildFullPayload,
|
|
55
|
+
import { buildFullPayload, payloadLocation } from "./full-payload.js";
|
|
55
56
|
import {
|
|
56
57
|
capShellWindow,
|
|
57
58
|
lastStoredCaptureSeq,
|
|
@@ -94,6 +95,8 @@ import {
|
|
|
94
95
|
type DevShellLookup,
|
|
95
96
|
} from "./shell-build-manifest.js";
|
|
96
97
|
import { contextGet } from "../context-var.js";
|
|
98
|
+
import { rewriteShellAudience, rewriteTrail } from "../server/rewrite-trail.js";
|
|
99
|
+
import { visitorShellKey } from "./shell-capture-constants.js";
|
|
97
100
|
import { PREFETCH_HEADER } from "../prefetch-markers.js";
|
|
98
101
|
import {
|
|
99
102
|
isWarmReplace,
|
|
@@ -155,10 +158,52 @@ function describePprReplayStatus(status: PprReplayStatus): string {
|
|
|
155
158
|
: `bypass:${status.reason}`;
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
/**
|
|
162
|
+
* The shell key before the route's own partition, or undefined: a request
|
|
163
|
+
* middleware rewrote reads and captures no shell unless its rewrite chose
|
|
164
|
+
* `.shell()`, so a URL's shared shell is read and written only by requests
|
|
165
|
+
* on that URL (#1146).
|
|
166
|
+
*/
|
|
167
|
+
interface RequestShellBase {
|
|
168
|
+
baseKey: string;
|
|
169
|
+
/** shellSearchSeed of the URL the key names. */
|
|
170
|
+
searchSeed: string;
|
|
171
|
+
/** A visitor-keyed shell's location (ShellCacheEntry.pathname). */
|
|
172
|
+
pathname?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function requestShellBase(
|
|
176
|
+
ctx: HandlerContext<any>,
|
|
177
|
+
reqCtx: RequestContext<any>,
|
|
178
|
+
url: URL,
|
|
179
|
+
): RequestShellBase | undefined {
|
|
180
|
+
const filter = reqCtx._searchParamsFilter;
|
|
181
|
+
const trail = rewriteTrail(reqCtx);
|
|
182
|
+
const shell = rewriteShellAudience(reqCtx);
|
|
183
|
+
if (trail && shell === "visitor") {
|
|
184
|
+
const visitor = new URL(trail[0]!);
|
|
185
|
+
return {
|
|
186
|
+
baseKey: visitorShellKey(
|
|
187
|
+
ctx.router.id,
|
|
188
|
+
visitor,
|
|
189
|
+
new URL(trail[trail.length - 1]!),
|
|
190
|
+
filter,
|
|
191
|
+
),
|
|
192
|
+
searchSeed: shellSearchSeed(visitor, filter),
|
|
193
|
+
pathname: visitor.pathname,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (trail && shell !== "target") return undefined;
|
|
197
|
+
return {
|
|
198
|
+
baseKey: buildShellKey(ctx.router.id, url, filter),
|
|
199
|
+
searchSeed: shellSearchSeed(url, filter),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
158
203
|
function createShellCaptureDescriptor(
|
|
159
204
|
ctx: HandlerContext<any>,
|
|
160
205
|
key: string,
|
|
161
|
-
|
|
206
|
+
base: RequestShellBase,
|
|
162
207
|
originSeed: string,
|
|
163
208
|
pprConfig: ResolvedPprConfig,
|
|
164
209
|
store: SegmentCacheStore<any>,
|
|
@@ -166,7 +211,8 @@ function createShellCaptureDescriptor(
|
|
|
166
211
|
): ShellCaptureDescriptor {
|
|
167
212
|
return {
|
|
168
213
|
key,
|
|
169
|
-
searchSeed,
|
|
214
|
+
searchSeed: base.searchSeed,
|
|
215
|
+
pathname: base.pathname,
|
|
170
216
|
// Derived from the same request URL as the key (the callers compute
|
|
171
217
|
// both next to each other, so they cannot drift): seeding the origin
|
|
172
218
|
// keeps origin-dependent static markup (Link's data-external)
|
|
@@ -499,13 +545,15 @@ function* shellServePlan<TEnv>(
|
|
|
499
545
|
// no shell read, no capture, so it can never degrade again. The handler
|
|
500
546
|
// stripped the marker from the request and left this flag.
|
|
501
547
|
if (reqCtx._shellForcedMiss) return { kind: "pass" };
|
|
548
|
+
const base = requestShellBase(ctx, reqCtx, url);
|
|
549
|
+
if (!base) return { kind: "pass" };
|
|
502
550
|
|
|
503
551
|
// A per-request CSP nonce pins the route to axis 1: a shared shell would
|
|
504
552
|
// freeze the capture request's nonce and CSP would reject it for every
|
|
505
553
|
// other visitor. Every step below, a HIT included, therefore runs without
|
|
506
554
|
// one.
|
|
507
555
|
const store = reqCtx._cacheStore;
|
|
508
|
-
const baseKey =
|
|
556
|
+
const { baseKey } = base;
|
|
509
557
|
if (activeRequestNonce(nonce, reqCtx) !== undefined) {
|
|
510
558
|
// Declared intent that cannot be honored deserves a diagnostic (unlike an
|
|
511
559
|
// undeclared route, which is silent): a ppr route gated off by an active
|
|
@@ -596,7 +644,7 @@ function* shellServePlan<TEnv>(
|
|
|
596
644
|
const descriptor = createShellCaptureDescriptor(
|
|
597
645
|
ctx,
|
|
598
646
|
key,
|
|
599
|
-
|
|
647
|
+
base,
|
|
600
648
|
url.origin,
|
|
601
649
|
pprConfig,
|
|
602
650
|
store,
|
|
@@ -626,10 +674,13 @@ function* shellServePlan<TEnv>(
|
|
|
626
674
|
// A HIT replays the handler layer from the entry's doc record (`docKey`),
|
|
627
675
|
// or, for a Prerender route, from the prerender store. An entry that has
|
|
628
676
|
// neither is a MISS: serving it would run handlers after the commit.
|
|
677
|
+
// The entry must have rendered this request's location: the browser
|
|
678
|
+
// hydrates against the address bar.
|
|
629
679
|
if (
|
|
630
680
|
cached &&
|
|
631
681
|
!cached.entry.navigationOnly &&
|
|
632
682
|
(cached.entry.docKey !== undefined || route?.matched?.pr === true) &&
|
|
683
|
+
cached.entry.pathname === base.pathname &&
|
|
633
684
|
isValidShellHit(cached.entry, ctx.version)
|
|
634
685
|
) {
|
|
635
686
|
const document = openShellDocumentMetered(reqCtx, cached);
|
|
@@ -713,7 +764,9 @@ function* shellServePlan<TEnv>(
|
|
|
713
764
|
// compile-time constant).
|
|
714
765
|
const devShellLookup = resolveDevShellLookup(reqCtx, pprConfig);
|
|
715
766
|
let buildHit: Awaited<ReturnType<typeof lookupBuildShell>> = null;
|
|
716
|
-
if (
|
|
767
|
+
if (base.pathname !== undefined) {
|
|
768
|
+
// A build shell is the target's: never served under a visitor key.
|
|
769
|
+
} else if (key !== baseKey) {
|
|
717
770
|
// Probed once per path without a build shell, and not again once the
|
|
718
771
|
// route warned. A search-bearing URL never reads one: nothing to probe.
|
|
719
772
|
const routeName = reqCtx._classifiedRoute?.routeKey;
|
|
@@ -845,7 +898,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
845
898
|
kind: "payload",
|
|
846
899
|
payload: {
|
|
847
900
|
metadata: {
|
|
848
|
-
|
|
901
|
+
...payloadLocation(reqCtx, url),
|
|
849
902
|
// routerId is serialized on every payload (including within-session
|
|
850
903
|
// ones) so the frontend can read the current app/router identity. It
|
|
851
904
|
// always equals the current app's id: a cross-app navigation is
|
|
@@ -1209,16 +1262,6 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1209
1262
|
if (sourceScoped) {
|
|
1210
1263
|
rscHeaders["x-rsc-prefetch-scope"] = "source";
|
|
1211
1264
|
}
|
|
1212
|
-
// Enable browser HTTP caching for prefetch responses only.
|
|
1213
|
-
// Requires a prefetch request (X-Rango-Prefetch, sent by Link prefetch
|
|
1214
|
-
// fetch), a source-agnostic response (see sourceScoped above), and a
|
|
1215
|
-
// configured cache-control value (false disables caching).
|
|
1216
|
-
if (reqCtx._requestKind === "prefetch" && isPartial && !sourceScoped) {
|
|
1217
|
-
const cc = ctx.router.prefetchCacheControl;
|
|
1218
|
-
if (cc) {
|
|
1219
|
-
rscHeaders["cache-control"] = cc;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
1265
|
// prefetch: false (docs/design/prefetch-false.md, R7). A response that
|
|
1223
1266
|
// carries deferred units answers one page's prefetch, and a fill is the
|
|
1224
1267
|
// second half of one adoption: neither is reused by a shared cache or the
|
|
@@ -1248,7 +1291,7 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1248
1291
|
reqCtx._activeRoutine &&
|
|
1249
1292
|
createRenderStageTraceBridge(reqCtx._activeRoutine),
|
|
1250
1293
|
};
|
|
1251
|
-
|
|
1294
|
+
const rendered = renderRscResponse(
|
|
1252
1295
|
{
|
|
1253
1296
|
ctx,
|
|
1254
1297
|
request,
|
|
@@ -1272,6 +1315,11 @@ function renderPreparedRscResponse<TEnv>(
|
|
|
1272
1315
|
}),
|
|
1273
1316
|
},
|
|
1274
1317
|
);
|
|
1318
|
+
// A prefetch whose render reported an error is not kept (#1170).
|
|
1319
|
+
if (reqCtx._requestKind !== "prefetch" || !isPartial) return rendered;
|
|
1320
|
+
return rendered.then((response) =>
|
|
1321
|
+
markPrefetchOnRenderError(response, reqCtx._renderErrors ?? []),
|
|
1322
|
+
);
|
|
1275
1323
|
}
|
|
1276
1324
|
|
|
1277
1325
|
/**
|
|
@@ -1286,6 +1334,8 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
1286
1334
|
const { ctx, request, env, url, reqCtx } = input;
|
|
1287
1335
|
const pprConfig = resolvePprConfig(reqCtx._classifiedRoute?.manifestEntry)!;
|
|
1288
1336
|
const store = reqCtx._cacheStore!;
|
|
1337
|
+
const base = requestShellBase(ctx, reqCtx, url);
|
|
1338
|
+
if (!base) return;
|
|
1289
1339
|
scheduleShellCapture(
|
|
1290
1340
|
ctx,
|
|
1291
1341
|
request,
|
|
@@ -1305,7 +1355,7 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
1305
1355
|
createShellCaptureDescriptor(
|
|
1306
1356
|
ctx,
|
|
1307
1357
|
captureKey,
|
|
1308
|
-
|
|
1358
|
+
base,
|
|
1309
1359
|
url.origin,
|
|
1310
1360
|
pprConfig,
|
|
1311
1361
|
store,
|
|
@@ -1464,10 +1514,13 @@ async function matchPartialWithPprReplay<TEnv>(
|
|
|
1464
1514
|
return runMatch({ outcome: "BYPASS", reason: "cache-disabled" });
|
|
1465
1515
|
}
|
|
1466
1516
|
|
|
1517
|
+
const base = requestShellBase(ctx, reqCtx, url);
|
|
1518
|
+
if (!base) return runMatch({ outcome: "BYPASS", reason: "rewritten" });
|
|
1519
|
+
|
|
1467
1520
|
// The visitor's own partition (resolveShellPartition): replay never
|
|
1468
1521
|
// crosses into another partition's shell.
|
|
1469
1522
|
const key = await resolveRequestShellKey(
|
|
1470
|
-
|
|
1523
|
+
base.baseKey,
|
|
1471
1524
|
routeSnapshot,
|
|
1472
1525
|
store,
|
|
1473
1526
|
reqCtx,
|
|
@@ -1806,6 +1859,9 @@ function serveShellHit(
|
|
|
1806
1859
|
descriptor: ShellCaptureDescriptor,
|
|
1807
1860
|
): Response {
|
|
1808
1861
|
const { entry, prelude: preludeBytes } = document;
|
|
1862
|
+
// A visitor-keyed shell shows the visitor's URL: the document cache keys
|
|
1863
|
+
// this response by the target.
|
|
1864
|
+
if (entry.pathname !== undefined) reqCtx._payloadVisitorLocation = true;
|
|
1809
1865
|
const metricsStore = reqCtx._metricsStore;
|
|
1810
1866
|
// Per-stage tail timing for the `ppr:tail` Server-Timing mirror and the
|
|
1811
1867
|
// perf console line: always in dev, in production only when this request
|
|
@@ -1867,8 +1923,8 @@ function serveShellHit(
|
|
|
1867
1923
|
payload.metadata.initialTheme = entry.initialTheme as
|
|
1868
1924
|
| import("../theme/types.js").Theme
|
|
1869
1925
|
| undefined;
|
|
1870
|
-
// Same for the location: the
|
|
1871
|
-
payload.metadata.pathname = url.pathname;
|
|
1926
|
+
// Same for the location: the key's URL, or a visitor-keyed shell's.
|
|
1927
|
+
payload.metadata.pathname = entry.pathname ?? url.pathname;
|
|
1872
1928
|
}
|
|
1873
1929
|
// Full Flight render per request: hydration needs the whole payload (there
|
|
1874
1930
|
// is no Flight-side resume — a React limitation, not ours).
|
|
@@ -1911,7 +1967,7 @@ function serveShellHit(
|
|
|
1911
1967
|
nonce: undefined,
|
|
1912
1968
|
// The shell key's own search — identical to the capture seed for
|
|
1913
1969
|
// this key, so the resume tree matches the captured tree.
|
|
1914
|
-
search:
|
|
1970
|
+
search: descriptor.searchSeed,
|
|
1915
1971
|
// The HIT request's origin — same host as the capture's (key-scoped).
|
|
1916
1972
|
origin: url.origin,
|
|
1917
1973
|
// The document cache reads the list before storing this composite.
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { observePhase, PHASES } from "../router/instrument.js";
|
|
|
24
24
|
import { resolveActionRefId } from "../router/is-action.js";
|
|
25
25
|
import type { TraceSpan } from "../router/tracing.js";
|
|
26
26
|
import { attachTransitionWhen } from "./attach-transition-when.js";
|
|
27
|
+
import { payloadLocation } from "./full-payload.js";
|
|
27
28
|
import { publicRouteName } from "../route-name.js";
|
|
28
29
|
import type { RscPayload } from "./types.js";
|
|
29
30
|
import {
|
|
@@ -323,6 +324,28 @@ export async function executeServerAction<TEnv>(
|
|
|
323
324
|
};
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The continuation for a rewrite pass after the action ran (#1145): a failed
|
|
329
|
+
* action's error boundary is re-matched on the target, since the one pass 1
|
|
330
|
+
* matched (or missed) is the rewriting route's.
|
|
331
|
+
*/
|
|
332
|
+
export async function continuationForRewritePass<TEnv>(
|
|
333
|
+
ctx: HandlerContext<TEnv>,
|
|
334
|
+
request: Request,
|
|
335
|
+
env: TEnv,
|
|
336
|
+
continuation: ActionContinuation,
|
|
337
|
+
): Promise<ActionContinuation> {
|
|
338
|
+
if (continuation.returnValue.ok) return continuation;
|
|
339
|
+
const errorBoundary =
|
|
340
|
+
(await ctx.router.matchError(
|
|
341
|
+
request,
|
|
342
|
+
{ env },
|
|
343
|
+
continuation.actionContext.actionResult,
|
|
344
|
+
"route",
|
|
345
|
+
)) ?? undefined;
|
|
346
|
+
return { ...continuation, errorBoundary };
|
|
347
|
+
}
|
|
348
|
+
|
|
326
349
|
/**
|
|
327
350
|
* Phase 2: Revalidate after action.
|
|
328
351
|
*
|
|
@@ -513,7 +536,7 @@ function renderActionBoundaryResponse<TEnv>(
|
|
|
513
536
|
|
|
514
537
|
const errorPayload: RscPayload = {
|
|
515
538
|
metadata: {
|
|
516
|
-
|
|
539
|
+
...payloadLocation(reqCtx, url),
|
|
517
540
|
// routerId exposed for the frontend (current app identity); see
|
|
518
541
|
// rsc-rendering.ts partial branch.
|
|
519
542
|
routerId: ctx.router.id,
|
|
@@ -576,7 +599,7 @@ function renderRevalidationResponse<TEnv>(
|
|
|
576
599
|
|
|
577
600
|
const payload: RscPayload = {
|
|
578
601
|
metadata: {
|
|
579
|
-
|
|
602
|
+
...payloadLocation(reqCtx, url),
|
|
580
603
|
// routerId exposed for the frontend (current app identity); see
|
|
581
604
|
// rsc-rendering.ts partial branch.
|
|
582
605
|
routerId: ctx.router.id,
|
|
@@ -67,6 +67,8 @@ export const PPR_REPLAY_BYPASS_REASONS = [
|
|
|
67
67
|
"no-segment-snapshot",
|
|
68
68
|
"snapshot-miss",
|
|
69
69
|
"explicit-cache-hit",
|
|
70
|
+
// A rewritten request whose rewrite chose no `.shell()`.
|
|
71
|
+
"rewritten",
|
|
70
72
|
] as const;
|
|
71
73
|
|
|
72
74
|
/** One of {@link PPR_REPLAY_BYPASS_REASONS}. */
|
|
@@ -126,6 +128,26 @@ export function partitionShellKey(key: string, partition: string): string {
|
|
|
126
128
|
return `${key}|${encodeURIComponent(partition)}`;
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The shell key of a `rewrite(...).shell("visitor")` request: the visitor's
|
|
133
|
+
* URL, partitioned by `rw:` + the target's path and search (both searches
|
|
134
|
+
* filtered and sorted, shellSearchSeed). The visitor's URL keeps two public
|
|
135
|
+
* URLs of one target apart; the target keeps a conditional rewrite (an A/B
|
|
136
|
+
* arm, a lock) apart from the URL's own shell. The route's own partition
|
|
137
|
+
* composes after it.
|
|
138
|
+
*/
|
|
139
|
+
export function visitorShellKey(
|
|
140
|
+
routerId: string,
|
|
141
|
+
visitor: URL,
|
|
142
|
+
target: URL,
|
|
143
|
+
filter?: SearchParamsFilter,
|
|
144
|
+
): string {
|
|
145
|
+
return partitionShellKey(
|
|
146
|
+
buildShellKey(routerId, visitor, filter),
|
|
147
|
+
`rw:${target.pathname}${shellSearchSeed(target, filter)}`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
129
151
|
/**
|
|
130
152
|
* The navigation-only entry beside a document shell key (a cold partial
|
|
131
153
|
* request's capture, rsc-rendering.ts). A suffix after the (encoded)
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -1063,6 +1063,13 @@ export interface ShellCaptureDescriptor {
|
|
|
1063
1063
|
debugSink?: (event: ShellCaptureDebugEvent) => void;
|
|
1064
1064
|
/** Store the snapshot for navigation replay, never the captured HTML prelude. */
|
|
1065
1065
|
navigationOnly?: true;
|
|
1066
|
+
/**
|
|
1067
|
+
* Set for a shell keyed by the visitor's URL (`rewrite(...).shell("visitor")`,
|
|
1068
|
+
* visitorShellKey): the capture renders the visitor's location
|
|
1069
|
+
* (`RequestContext._shellCaptureForVisitor`) and stores it as
|
|
1070
|
+
* ShellCacheEntry.pathname.
|
|
1071
|
+
*/
|
|
1072
|
+
pathname?: string;
|
|
1066
1073
|
/**
|
|
1067
1074
|
* lastStoredCaptureSeq(key) when a document request read the store and
|
|
1068
1075
|
* missed. The request schedules its capture only after rendering the MISS,
|
|
@@ -1878,6 +1885,9 @@ async function attemptCapture(
|
|
|
1878
1885
|
descriptor.navigationOnly ? { request, url } : undefined,
|
|
1879
1886
|
);
|
|
1880
1887
|
const { derivedCtx, freshHandleStore } = derivation;
|
|
1888
|
+
if (descriptor.pathname !== undefined) {
|
|
1889
|
+
derivedCtx._shellCaptureForVisitor = true;
|
|
1890
|
+
}
|
|
1881
1891
|
// The capture generation starts before matching or any snapshot read
|
|
1882
1892
|
// (ShellCacheEntry.createdAt): a tag invalidated after it wins.
|
|
1883
1893
|
const captureStartedAt = Date.now();
|
|
@@ -2647,6 +2657,7 @@ async function captureAndStoreShell(
|
|
|
2647
2657
|
// default, #971). The serve tail replays it so the resume tree
|
|
2648
2658
|
// matches the frozen prelude — see ShellCacheEntry.initialTheme.
|
|
2649
2659
|
initialTheme: payloadInitialTheme(reqCtx),
|
|
2660
|
+
pathname: capture.pathname,
|
|
2650
2661
|
snapshot,
|
|
2651
2662
|
prunedRecords: snapshot.length > 0 ? prunedRecords : undefined,
|
|
2652
2663
|
// The canonical doc segment record's key, published by the doc
|
package/src/rsc/types.ts
CHANGED
|
@@ -18,6 +18,11 @@ import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
|
18
18
|
export interface RscPayload {
|
|
19
19
|
metadata?: {
|
|
20
20
|
pathname: string;
|
|
21
|
+
/**
|
|
22
|
+
* Path and search of the URL the route rendered for, when middleware
|
|
23
|
+
* rewrote the request there (full-payload.ts payloadLocation).
|
|
24
|
+
*/
|
|
25
|
+
rewriteTarget?: string;
|
|
21
26
|
segments: ResolvedSegment[];
|
|
22
27
|
/** Router instance ID. When this changes between navigations, the client
|
|
23
28
|
* discards cached segments and does a full tree replacement (app switch). */
|