@rangojs/router 0.17.0 → 0.19.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/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +9 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +1094 -376
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +47 -3
- package/src/rsc/handler.ts +24 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
package/src/rsc/shell-serve.ts
CHANGED
|
@@ -18,20 +18,32 @@
|
|
|
18
18
|
|
|
19
19
|
import React from "react";
|
|
20
20
|
import { isPprEntry, type EntryData } from "../server/context.js";
|
|
21
|
-
import {
|
|
22
|
-
import type { SearchParamsFilter } from "../cache/search-params-filter.js";
|
|
21
|
+
import { base64ToBytes } from "../cache/cf/cf-base64.js";
|
|
23
22
|
import type {
|
|
24
23
|
DocumentShellCacheEntry,
|
|
25
24
|
ShellCacheEntry,
|
|
25
|
+
ShellDocumentRead,
|
|
26
|
+
ShellEntryHead,
|
|
27
|
+
ShellReadStats,
|
|
28
|
+
ShellSnapshotFailure,
|
|
29
|
+
ShellSnapshotRecord,
|
|
26
30
|
SegmentCacheStore,
|
|
27
31
|
} from "../cache/types.js";
|
|
28
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
SHELL_CAPTURE_MAX_WAIT_MS,
|
|
34
|
+
hasWarnedOnce,
|
|
35
|
+
resetShellWarningsForTests,
|
|
36
|
+
warnOnce,
|
|
37
|
+
} from "./shell-capture-constants.js";
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
export {
|
|
40
|
+
PPR_REPLAY_STATUS_HEADER,
|
|
41
|
+
SHELL_STATUS_HEADER,
|
|
42
|
+
buildShellKey,
|
|
43
|
+
navigationShellKey,
|
|
44
|
+
partitionShellKey,
|
|
45
|
+
shellSearchSeed,
|
|
46
|
+
} from "./shell-capture-constants.js";
|
|
35
47
|
|
|
36
48
|
/**
|
|
37
49
|
* Default shell ttl (seconds) for `ppr: true` and for a PartialPrerenderProps
|
|
@@ -115,36 +127,59 @@ export function resolvePprConfig(
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
+
* The forced-MISS query marker. A document HIT that cannot finish its tail
|
|
131
|
+
* reloads the page with it (shellReloadScript); the serve gate renders a
|
|
132
|
+
* request carrying it on axis 1, with no shell read and no capture, so the
|
|
133
|
+
* reload can never be a HIT that degrades again. The handler drops it from
|
|
134
|
+
* the request on entry (withoutShellMissMarker) and keeps only the flag
|
|
135
|
+
* (RequestContext._shellForcedMiss), so nothing else sees it.
|
|
136
|
+
*/
|
|
137
|
+
export const SHELL_MISS_PARAM: string = "_rsc_shell";
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The request without the forced-MISS marker, or undefined when it carries
|
|
141
|
+
* none. The handler calls it before anything reads the request: left in, the
|
|
142
|
+
* marker reached `ctx.request.url`, `originalUrl`, middleware, and the SSR
|
|
143
|
+
* search seed (so `useSearchParams` rendered it), and a `"use cache"` key
|
|
144
|
+
* built from the request URL was never read again. GET and HEAD only: the
|
|
145
|
+
* reload is a navigation, and a request with a body cannot be re-created
|
|
146
|
+
* without re-streaming it. The substring test keeps the URL parse off every
|
|
147
|
+
* other request.
|
|
148
|
+
*/
|
|
149
|
+
export function withoutShellMissMarker(request: Request): Request | undefined {
|
|
150
|
+
if (!request.url.includes(SHELL_MISS_PARAM)) return undefined;
|
|
151
|
+
if (request.method !== "GET" && request.method !== "HEAD") return undefined;
|
|
152
|
+
const url = new URL(request.url);
|
|
153
|
+
if (!url.searchParams.has(SHELL_MISS_PARAM)) return undefined;
|
|
154
|
+
url.searchParams.delete(SHELL_MISS_PARAM);
|
|
155
|
+
return new Request(url, request);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The inline script a degraded document HIT ends with: reload once into a
|
|
160
|
+
* forced MISS. The marker in the URL is the loop bound — a request carrying
|
|
161
|
+
* it is never a HIT, so this script never runs on the reload, and if it ever
|
|
162
|
+
* did (a server that ignores the marker) it would return without touching
|
|
163
|
+
* the page. window.stop() runs only when it reloads: it keeps the half-sent
|
|
164
|
+
* Flight stream from being closed by DOMContentLoaded, which would throw
|
|
165
|
+
* React's "Connection closed" (#412) before the reload lands.
|
|
130
166
|
*/
|
|
131
|
-
export function
|
|
132
|
-
return
|
|
167
|
+
export function shellReloadScript(): string {
|
|
168
|
+
return inlineShellScript(
|
|
169
|
+
"(function(){var u=new URL(location.href);" +
|
|
170
|
+
`if(u.searchParams.has(${JSON.stringify(SHELL_MISS_PARAM)}))return;` +
|
|
171
|
+
`u.searchParams.set(${JSON.stringify(SHELL_MISS_PARAM)},"miss");` +
|
|
172
|
+
"try{window.stop()}catch(e){}location.replace(u.href)})()",
|
|
173
|
+
);
|
|
133
174
|
}
|
|
134
175
|
|
|
135
176
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* / ShellCaptureOptions.search / ShellResumeOptions.search). Search is part
|
|
140
|
-
* of shell identity, so static-part `useSearchParams` reads render exactly
|
|
141
|
-
* what the key names; deriving seed and key from this one helper is what
|
|
142
|
-
* keeps capture, resume, and lookup byte-agreed (drift = replay mismatch or
|
|
143
|
-
* permanent MISS).
|
|
177
|
+
* An inline script a shell HIT's tail appends. It carries no CSP nonce: a
|
|
178
|
+
* request with an active per-request nonce never reaches a HIT
|
|
179
|
+
* (rsc-rendering.ts shellServePlan serves it on axis 1).
|
|
144
180
|
*/
|
|
145
|
-
export function
|
|
146
|
-
|
|
147
|
-
return sorted ? `?${sorted}` : "";
|
|
181
|
+
export function inlineShellScript(body: string): string {
|
|
182
|
+
return `<script>${body}</script>`;
|
|
148
183
|
}
|
|
149
184
|
|
|
150
185
|
/**
|
|
@@ -153,12 +188,10 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
|
|
|
153
188
|
* one exact tree, so resuming it under a different React OR a different app
|
|
154
189
|
* build tree-mismatches inside resume() — after the 200 + prelude committed,
|
|
155
190
|
* with no recovery. Either mismatch is a miss: the recapture overwrites the
|
|
156
|
-
* same key (self-healing) and the entry otherwise ages out via TTL.
|
|
157
|
-
* with no buildVersion (stored before the field existed) is a miss for the
|
|
158
|
-
* same reason — its build is unknown, so it cannot be proven resumable.
|
|
191
|
+
* same key (self-healing) and the entry otherwise ages out via TTL.
|
|
159
192
|
*/
|
|
160
193
|
export function isValidShellHit(
|
|
161
|
-
entry: ShellCacheEntry,
|
|
194
|
+
entry: Pick<ShellCacheEntry, "reactVersion" | "buildVersion">,
|
|
162
195
|
buildVersion: string,
|
|
163
196
|
): boolean {
|
|
164
197
|
return (
|
|
@@ -166,17 +199,25 @@ export function isValidShellHit(
|
|
|
166
199
|
);
|
|
167
200
|
}
|
|
168
201
|
|
|
202
|
+
/** The postponed blob is stored and parses (null is the DATA variant). */
|
|
203
|
+
function hasParseablePostponed<T extends ShellEntryHead>(
|
|
204
|
+
entry: T,
|
|
205
|
+
): entry is T & { postponed: string | null } {
|
|
206
|
+
if (entry.postponed === undefined) return false;
|
|
207
|
+
try {
|
|
208
|
+
if (entry.postponed !== null) JSON.parse(entry.postponed);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
169
215
|
/**
|
|
170
|
-
* DOCUMENT-half
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* page that never hydrates, re-served on every request until the entry ages
|
|
176
|
-
* out (no eviction path exists; failure schedules no recapture by itself).
|
|
177
|
-
* Checking here turns a corrupt entry (store-layer fault) into a plain MISS
|
|
178
|
-
* the recapture overwrites. Cost: one duplicate decode/parse per HIT, sub-ms
|
|
179
|
-
* against a prelude flush that dominates the path.
|
|
216
|
+
* DOCUMENT-half structural gate and type narrowing, without decoding the
|
|
217
|
+
* prelude: the prelude is a string and the postponed blob parses. Partial
|
|
218
|
+
* replay (which never serves the prelude) and the build-manifest read-through
|
|
219
|
+
* use it as-is; the document HIT path runs it inside
|
|
220
|
+
* {@link openShellDocument}, whose single decode is the prelude's check.
|
|
180
221
|
*
|
|
181
222
|
* navigationOnly entries store no document half (prelude/postponed absent) and
|
|
182
223
|
* therefore never pass. The partial-replay path skips this gate for them
|
|
@@ -187,25 +228,92 @@ export function isValidShellHit(
|
|
|
187
228
|
export function hasIntactShellPayload(
|
|
188
229
|
entry: ShellCacheEntry,
|
|
189
230
|
): entry is DocumentShellCacheEntry {
|
|
190
|
-
|
|
191
|
-
|
|
231
|
+
return typeof entry.prelude === "string" && hasParseablePostponed(entry);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A document shell ready to serve: its prelude decoded exactly once. */
|
|
235
|
+
export interface ShellDocument {
|
|
236
|
+
entry: ShellEntryHead;
|
|
237
|
+
prelude: Uint8Array;
|
|
238
|
+
/** React's postponed state, parse-checked (null = DATA variant). */
|
|
239
|
+
postponed: string | null;
|
|
240
|
+
/**
|
|
241
|
+
* The capture snapshot: on the entry, or still arriving on its own promise
|
|
242
|
+
* after a prelude-first read (SegmentCacheStore.readShellDocument), which
|
|
243
|
+
* only the tail awaits.
|
|
244
|
+
*/
|
|
245
|
+
snapshot: ShellSnapshotRecord[] | Promise<ShellSnapshotRecord[] | undefined>;
|
|
246
|
+
/** Why a prelude-first read's snapshot is missing (ShellDocumentRead). */
|
|
247
|
+
snapshotFailure?: Promise<ShellSnapshotFailure | undefined>;
|
|
248
|
+
/** The prelude-first read's stats, when perf metrics are on. */
|
|
249
|
+
stats?: ShellReadStats;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Pre-commit integrity gate for a document HIT, and its only prelude decode.
|
|
254
|
+
* A stored entry whose prelude is not decodable base64 or whose postponed blob
|
|
255
|
+
* is not parseable JSON would otherwise throw AFTER the 200 + full static
|
|
256
|
+
* prelude flushed (`resumeShellHTML` parses in the tail) — the client gets a
|
|
257
|
+
* visually complete page that never hydrates, re-served on every request
|
|
258
|
+
* until the entry ages out. Returning null here turns a corrupt entry
|
|
259
|
+
* (store-layer fault) into a plain MISS the recapture overwrites. The decoded
|
|
260
|
+
* bytes are what serveShellHit enqueues, so the check costs no second decode.
|
|
261
|
+
* A prelude-first `read` (SegmentCacheStore.readShellDocument) already carries
|
|
262
|
+
* the raw bytes and a pending snapshot, so only its postponed blob is checked.
|
|
263
|
+
*/
|
|
264
|
+
export function openShellDocument(entry: ShellCacheEntry): ShellDocument | null;
|
|
265
|
+
export function openShellDocument(
|
|
266
|
+
entry: ShellEntryHead,
|
|
267
|
+
read: Pick<
|
|
268
|
+
ShellDocumentRead,
|
|
269
|
+
"prelude" | "snapshot" | "snapshotFailure" | "stats"
|
|
270
|
+
>,
|
|
271
|
+
): ShellDocument | null;
|
|
272
|
+
export function openShellDocument(
|
|
273
|
+
entry: ShellEntryHead | ShellCacheEntry,
|
|
274
|
+
read?: Pick<
|
|
275
|
+
ShellDocumentRead,
|
|
276
|
+
"prelude" | "snapshot" | "snapshotFailure" | "stats"
|
|
277
|
+
>,
|
|
278
|
+
): ShellDocument | null {
|
|
279
|
+
if (read) {
|
|
280
|
+
if (!hasParseablePostponed(entry)) return null;
|
|
281
|
+
return {
|
|
282
|
+
entry,
|
|
283
|
+
postponed: entry.postponed,
|
|
284
|
+
prelude: read.prelude,
|
|
285
|
+
snapshot: read.snapshot,
|
|
286
|
+
...(read.snapshotFailure && { snapshotFailure: read.snapshotFailure }),
|
|
287
|
+
...(read.stats && { stats: read.stats }),
|
|
288
|
+
};
|
|
192
289
|
}
|
|
290
|
+
// The read-less overload takes a whole entry (getShell, a build shell).
|
|
291
|
+
const whole = entry as ShellCacheEntry;
|
|
292
|
+
if (!hasIntactShellPayload(whole)) return null;
|
|
293
|
+
let prelude: Uint8Array;
|
|
193
294
|
try {
|
|
194
|
-
base64ToBytes(
|
|
195
|
-
if (entry.postponed !== null) JSON.parse(entry.postponed);
|
|
196
|
-
return true;
|
|
295
|
+
prelude = base64ToBytes(whole.prelude);
|
|
197
296
|
} catch {
|
|
198
|
-
return
|
|
297
|
+
return null;
|
|
199
298
|
}
|
|
299
|
+
return {
|
|
300
|
+
entry: whole,
|
|
301
|
+
postponed: whole.postponed,
|
|
302
|
+
prelude,
|
|
303
|
+
snapshot: whole.snapshot,
|
|
304
|
+
};
|
|
200
305
|
}
|
|
201
306
|
|
|
202
|
-
/**
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Prelude enqueue granularity. A streaming compressor in front of the worker
|
|
309
|
+
* emits its first byte only after compressing the whole write it was handed:
|
|
310
|
+
* one 629 KB enqueue delayed the first compressed byte by the full prelude's
|
|
311
|
+
* compression time (brotli-6 5.1 ms, gzip-6 1.1 ms in Node zlib with a flush
|
|
312
|
+
* per write; workerd's CompressionStream emits nothing until the whole write is
|
|
313
|
+
* compressed), while 32 KB chunks emit within 0.1 ms and cost slightly more
|
|
314
|
+
* in total compression (0.55-0.70 ms) than one write (issue #941).
|
|
315
|
+
*/
|
|
316
|
+
export const SHELL_PRELUDE_CHUNK_BYTES: number = 32 * 1024;
|
|
209
317
|
|
|
210
318
|
/** True when the store implements the shell entry family. */
|
|
211
319
|
export function hasShellFamily(
|
|
@@ -227,8 +335,22 @@ export function hasShellFamily(
|
|
|
227
335
|
export interface ShellTailTiming {
|
|
228
336
|
key: string;
|
|
229
337
|
outcome: "complete" | "redirect" | "error";
|
|
338
|
+
/** The capture snapshot available to the tail (read and parsed). */
|
|
339
|
+
snapshotMs?: number;
|
|
340
|
+
/** Snapshot bytes read after the commit (prelude-first stores). */
|
|
341
|
+
snapshotBytes?: number;
|
|
342
|
+
/** Snapshot bytes read after the commit, excluding the parse. */
|
|
343
|
+
snapshotReadMs?: number;
|
|
344
|
+
/** Snapshot JSON.parse CPU (reads 0 on a deployed worker: see bytes). */
|
|
345
|
+
snapshotParseMs?: number;
|
|
346
|
+
/** Snapshot records by family, e.g. `segment:1/loader:2` (no commas: it rides a Server-Timing desc). */
|
|
347
|
+
snapshotRecords?: string;
|
|
348
|
+
/** Records the capture pruned from the snapshot, same format (ShellCacheEntry.prunedRecords). */
|
|
349
|
+
snapshotPruned?: string;
|
|
230
350
|
/** Loader-family seed decode (only when the entry carried a snapshot). */
|
|
231
351
|
seedMs?: number;
|
|
352
|
+
/** The seed decode's own duration: Flight deserialization, CPU only. */
|
|
353
|
+
seedCpuMs?: number;
|
|
232
354
|
/** Tail router.match() settled. */
|
|
233
355
|
matchMs?: number;
|
|
234
356
|
/** Tail stream (resume output) handed to the response stream. */
|
|
@@ -244,13 +366,35 @@ export interface ShellTailTiming {
|
|
|
244
366
|
}
|
|
245
367
|
|
|
246
368
|
/**
|
|
247
|
-
* Compact single-line form for the console log and the
|
|
248
|
-
*
|
|
249
|
-
* escaping needed.
|
|
369
|
+
* Compact single-line form for the console log and the Server-Timing mirror's
|
|
370
|
+
* `desc`: alphanumerics and `=`, `-`, `:`, `/` only (records=segment:1/loader:2 pruned=loader:1),
|
|
371
|
+
* so no quoted-string escaping is needed. Offsets are from the commit; the
|
|
372
|
+
* `-cpu` fields are CPU-only durations.
|
|
250
373
|
*/
|
|
251
374
|
export function describeShellTailTiming(timing: ShellTailTiming): string {
|
|
252
375
|
const parts: string[] = [timing.outcome];
|
|
376
|
+
if (timing.snapshotMs !== undefined) {
|
|
377
|
+
parts.push(`snapshot=${timing.snapshotMs}ms`);
|
|
378
|
+
}
|
|
379
|
+
if (timing.snapshotReadMs !== undefined) {
|
|
380
|
+
parts.push(`snapshot-read=${timing.snapshotReadMs}ms`);
|
|
381
|
+
}
|
|
382
|
+
if (timing.snapshotBytes !== undefined) {
|
|
383
|
+
parts.push(`snapshot-bytes=${timing.snapshotBytes}b`);
|
|
384
|
+
}
|
|
385
|
+
if (timing.snapshotParseMs !== undefined) {
|
|
386
|
+
parts.push(`snapshot-parse-cpu=${timing.snapshotParseMs}ms`);
|
|
387
|
+
}
|
|
388
|
+
if (timing.snapshotRecords !== undefined) {
|
|
389
|
+
parts.push(`records=${timing.snapshotRecords}`);
|
|
390
|
+
}
|
|
391
|
+
if (timing.snapshotPruned !== undefined) {
|
|
392
|
+
parts.push(`pruned=${timing.snapshotPruned}`);
|
|
393
|
+
}
|
|
253
394
|
if (timing.seedMs !== undefined) parts.push(`seed=${timing.seedMs}ms`);
|
|
395
|
+
if (timing.seedCpuMs !== undefined) {
|
|
396
|
+
parts.push(`seed-cpu=${timing.seedCpuMs}ms`);
|
|
397
|
+
}
|
|
254
398
|
if (timing.matchMs !== undefined) parts.push(`match=${timing.matchMs}ms`);
|
|
255
399
|
if (timing.handoverMs !== undefined) {
|
|
256
400
|
parts.push(`handover=${timing.handoverMs}ms`);
|
|
@@ -269,19 +413,19 @@ export function describeShellTailTiming(timing: ShellTailTiming): string {
|
|
|
269
413
|
}
|
|
270
414
|
|
|
271
415
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
416
|
+
* Last-tail-per-key buffer backing the `ppr:tail` Server-Timing mirror: a
|
|
417
|
+
* HIT's tail finishes after its own headers are long gone, so its per-stage
|
|
418
|
+
* numbers ride the NEXT ppr GET for the key when the metrics surface is
|
|
419
|
+
* active (debugPerformance). Same shape and FIFO cap as the capture mirror
|
|
420
|
+
* (shell-capture.ts lastCaptureEventsForTiming). serveShellHit collects and
|
|
421
|
+
* publishes a timing only in dev or when the HIT itself collected metrics, so
|
|
422
|
+
* a production isolate without debugPerformance never grows the map.
|
|
278
423
|
*/
|
|
279
424
|
const lastTailTimingsForServerTiming = new Map<string, ShellTailTiming>();
|
|
280
425
|
const MAX_TAIL_TIMING_KEYS = 100;
|
|
281
426
|
|
|
282
|
-
/** Buffer one terminal tail timing for the
|
|
427
|
+
/** Buffer one terminal tail timing for the Server-Timing mirror. */
|
|
283
428
|
export function publishShellTailTiming(timing: ShellTailTiming): void {
|
|
284
|
-
if (process.env.NODE_ENV === "production") return;
|
|
285
429
|
lastTailTimingsForServerTiming.delete(timing.key);
|
|
286
430
|
if (lastTailTimingsForServerTiming.size >= MAX_TAIL_TIMING_KEYS) {
|
|
287
431
|
const oldest = lastTailTimingsForServerTiming.keys().next().value;
|
|
@@ -302,9 +446,6 @@ export function takeShellTailTimingForServerTiming(
|
|
|
302
446
|
return timing;
|
|
303
447
|
}
|
|
304
448
|
|
|
305
|
-
/** Keys already warned about a missing shell store family (once per key). */
|
|
306
|
-
const warnedMissingStore = new Set<string>();
|
|
307
|
-
|
|
308
449
|
/**
|
|
309
450
|
* Warn once per key that a route declared `ppr` but the app-level cache store
|
|
310
451
|
* does not implement the shell family (getShell/putShell), so the route stays on
|
|
@@ -312,10 +453,11 @@ const warnedMissingStore = new Set<string>();
|
|
|
312
453
|
* honored deserves a diagnostic.
|
|
313
454
|
*/
|
|
314
455
|
export function warnShellStoreMissingOnce(key: string): void {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
456
|
+
warnOnce(
|
|
457
|
+
"store-missing",
|
|
458
|
+
key,
|
|
459
|
+
() =>
|
|
460
|
+
`[rango] Route for "${key}" declares the ppr path option, but the app-level ` +
|
|
319
461
|
"cache store does not implement the shell family (getShell/putShell), so " +
|
|
320
462
|
"the route is served on axis 1 without a shell. Use MemorySegmentCacheStore, " +
|
|
321
463
|
"CFCacheStore, or VercelCacheStore (or add the family to your custom store) " +
|
|
@@ -323,9 +465,6 @@ export function warnShellStoreMissingOnce(key: string): void {
|
|
|
323
465
|
);
|
|
324
466
|
}
|
|
325
467
|
|
|
326
|
-
/** Keys already warned about an active per-request nonce (once per key). */
|
|
327
|
-
const warnedNonceActive = new Set<string>();
|
|
328
|
-
|
|
329
468
|
/**
|
|
330
469
|
* Warn once per key that a route declared `ppr` but a per-request CSP nonce is
|
|
331
470
|
* active for the request, so the route stays on axis 1 (a shared shell would
|
|
@@ -337,10 +476,11 @@ const warnedNonceActive = new Set<string>();
|
|
|
337
476
|
* the missing-store warning above (an undeclared route stays silent).
|
|
338
477
|
*/
|
|
339
478
|
export function warnPprNonceActiveOnce(key: string): void {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
479
|
+
warnOnce(
|
|
480
|
+
"nonce-active",
|
|
481
|
+
key,
|
|
482
|
+
() =>
|
|
483
|
+
`[rango] Route for "${key}" declares the ppr path option, but a per-request ` +
|
|
344
484
|
"CSP nonce is active for this request (from createRouter({ nonce }) or a " +
|
|
345
485
|
"ctx.set(nonce, …) token write in middleware), so the route is served on " +
|
|
346
486
|
"axis 1 without a shell. A shell is shared per host+URL; baking one " +
|
|
@@ -348,3 +488,109 @@ export function warnPprNonceActiveOnce(key: string): void {
|
|
|
348
488
|
"ppr option on this route, or stop setting a per-request nonce for it.",
|
|
349
489
|
);
|
|
350
490
|
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Dev only (the caller gates on NODE_ENV): warn once per route that the
|
|
494
|
+
* route's cache() entry reduces an explicit `ppr.ttl` or `ppr.swr`. `shell`
|
|
495
|
+
* is what the capture stores for a record written just now
|
|
496
|
+
* (shell-capture.ts capShellWindow), so the message states the values the
|
|
497
|
+
* shell actually gets. A ppr value the cap leaves whole (or raises: the
|
|
498
|
+
* cap can move a record's stale time into the shell's swr) is silent, and
|
|
499
|
+
* so is a ppr config that sets no ttl/swr.
|
|
500
|
+
*/
|
|
501
|
+
export function warnPprWindowCappedOnce(
|
|
502
|
+
routeName: string,
|
|
503
|
+
ppr: { ttl?: number; swr?: number },
|
|
504
|
+
shell: { ttl: number; swr: number },
|
|
505
|
+
cache: { ttl: number; swr: number },
|
|
506
|
+
): void {
|
|
507
|
+
if (hasWarnedOnce("ppr-window-capped", routeName)) return;
|
|
508
|
+
const reduced: string[] = [];
|
|
509
|
+
for (const field of ["ttl", "swr"] as const) {
|
|
510
|
+
const own = ppr[field];
|
|
511
|
+
if (own !== undefined && shell[field] < own) {
|
|
512
|
+
reduced.push(`ppr.${field} ${own}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (reduced.length === 0) return;
|
|
516
|
+
warnOnce(
|
|
517
|
+
"ppr-window-capped",
|
|
518
|
+
routeName,
|
|
519
|
+
() =>
|
|
520
|
+
`[rango] Route "${routeName}": its shell is stored with ttl ${shell.ttl} ` +
|
|
521
|
+
`and swr ${shell.swr}, below its ${reduced.join(" and ")}. A shell ` +
|
|
522
|
+
`never outlives the route cache() entry (ttl ${cache.ttl}, swr ` +
|
|
523
|
+
`${cache.swr}) it was captured from, so a document request and a ` +
|
|
524
|
+
"client navigation show the same handler output. Keep ppr.ttl within " +
|
|
525
|
+
"the cache() ttl, and ppr.ttl + ppr.swr within the cache() ttl + swr, " +
|
|
526
|
+
"to silence this.",
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Paths a partitioned request found without a build shell. Build shells are
|
|
532
|
+
* per path (a param route prerenders some of its paths), so a negative is
|
|
533
|
+
* kept per path, not per route; cleared when full, so it stays bounded.
|
|
534
|
+
*/
|
|
535
|
+
const pathsWithoutBuildShell = new Set<string>();
|
|
536
|
+
const PATHS_WITHOUT_BUILD_SHELL_MAX = 1_000;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Whether a partitioned request needs no build-shell probe: its route was
|
|
540
|
+
* already warned, or its path was already found without one.
|
|
541
|
+
*/
|
|
542
|
+
export function partitionBuildShellCheckDone(
|
|
543
|
+
pathname: string,
|
|
544
|
+
routeName: string | undefined,
|
|
545
|
+
): boolean {
|
|
546
|
+
return (
|
|
547
|
+
hasWarnedOnce("partition-build-shell", routeName ?? pathname) ||
|
|
548
|
+
pathsWithoutBuildShell.has(pathname)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Record a partitioned request's build-shell probe, and warn once per route
|
|
554
|
+
* when it found one: the route has a build-time shell its requests cannot
|
|
555
|
+
* read. The route's request partition (a `cache({ key })` enclosing it, at
|
|
556
|
+
* any depth (#970), or the store's keyGenerator) keys the shell per
|
|
557
|
+
* partition, and the build captured only
|
|
558
|
+
* the default one, so each partition captures at runtime. Same
|
|
559
|
+
* declared-intent-cannot-be-honored doctrine as the warnings above; a path
|
|
560
|
+
* with no build shell stays silent.
|
|
561
|
+
*/
|
|
562
|
+
export function notePartitionBuildShellCheck(
|
|
563
|
+
pathname: string,
|
|
564
|
+
routeName: string | undefined,
|
|
565
|
+
found: boolean,
|
|
566
|
+
): void {
|
|
567
|
+
if (!found) {
|
|
568
|
+
if (pathsWithoutBuildShell.size >= PATHS_WITHOUT_BUILD_SHELL_MAX) {
|
|
569
|
+
pathsWithoutBuildShell.clear();
|
|
570
|
+
}
|
|
571
|
+
pathsWithoutBuildShell.add(pathname);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// Concurrent first requests can both probe; one of them warns.
|
|
575
|
+
warnOnce(
|
|
576
|
+
"partition-build-shell",
|
|
577
|
+
routeName ?? pathname,
|
|
578
|
+
() =>
|
|
579
|
+
`[rango] Route ${routeName ? `"${routeName}" ` : ""}("${pathname}") has a ` +
|
|
580
|
+
"build-time shell, but its request partition (a cache({ key }) enclosing the route, " +
|
|
581
|
+
"or the store's keyGenerator) keys its shell per partition, so the build " +
|
|
582
|
+
"shell is not served and each partition captures its own at runtime. A " +
|
|
583
|
+
"keyGenerator that returns the default key unchanged keeps the build shell.",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* @internal Reset the serve path's once-per-key warnings (the shell path's
|
|
589
|
+
* one registry), its build-shell probe memo and buffered tail timings. Tests
|
|
590
|
+
* only (testing/serve-shell-request.ts resetShellTestState).
|
|
591
|
+
*/
|
|
592
|
+
export function resetShellServeStateForTests(): void {
|
|
593
|
+
resetShellWarningsForTests();
|
|
594
|
+
pathsWithoutBuildShell.clear();
|
|
595
|
+
lastTailTimingsForServerTiming.clear();
|
|
596
|
+
}
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* matching, so they can run in parallel with segment resolution.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { requestHeaders } from "../server/request-headers.js";
|
|
9
10
|
import type { HandlerContext } from "./handler-context.js";
|
|
10
11
|
import type { SSRModule, SSRRenderOptions } from "./types.js";
|
|
11
12
|
import type { SSRStreamMode } from "../router/router-options.js";
|
|
@@ -183,7 +184,7 @@ export function getSSRSetup<TEnv>(
|
|
|
183
184
|
*/
|
|
184
185
|
function acceptsFlightExplicitly(request: Request, url: URL): boolean {
|
|
185
186
|
if (url.searchParams.has("__html")) return false;
|
|
186
|
-
const accept = request.
|
|
187
|
+
const accept = requestHeaders(request).get("accept");
|
|
187
188
|
if (accept === null || !accept.includes(RSC_WIRE_MIME)) return false;
|
|
188
189
|
return prefersFlightRepresentation(parseAcceptTypes(accept));
|
|
189
190
|
}
|
|
@@ -206,7 +207,7 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
|
206
207
|
if (
|
|
207
208
|
url.searchParams.has("_rsc_partial") ||
|
|
208
209
|
url.searchParams.has("_rsc_action") ||
|
|
209
|
-
request.
|
|
210
|
+
requestHeaders(request).has("rsc-action") ||
|
|
210
211
|
url.searchParams.has("_rsc_loader") ||
|
|
211
212
|
url.searchParams.has("__rsc")
|
|
212
213
|
) {
|
package/src/rsc/types.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface RscPayload {
|
|
|
34
34
|
resolvedIds?: string[];
|
|
35
35
|
/** Merged route params from the matched route */
|
|
36
36
|
params?: Record<string, string>;
|
|
37
|
+
/** Matched route name, when named (transition({ when }) route names). */
|
|
38
|
+
routeName?: string;
|
|
37
39
|
slots?: Record<string, SlotState>;
|
|
38
40
|
/**
|
|
39
41
|
* Intercept TARGET route names reachable from this location as a
|
|
@@ -228,6 +230,13 @@ export interface SSRModule {
|
|
|
228
230
|
origin?: string;
|
|
229
231
|
/** Each component error the prerender reports (never the capture's own abort). */
|
|
230
232
|
onError?: (error: unknown) => void;
|
|
233
|
+
/**
|
|
234
|
+
* React's errorInfo for each task still pending at the capture's abort
|
|
235
|
+
* (its componentStack is computed when read).
|
|
236
|
+
*/
|
|
237
|
+
onAbortedTask?: (
|
|
238
|
+
errorInfo: { componentStack?: string } | undefined,
|
|
239
|
+
) => void;
|
|
231
240
|
},
|
|
232
241
|
) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
|
|
233
242
|
|
package/src/segment-system.tsx
CHANGED
|
@@ -125,6 +125,14 @@ export interface RenderSegmentsOptions {
|
|
|
125
125
|
/** Seeded descendant client-route pending state for testing. */
|
|
126
126
|
outletPending?: boolean;
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* This commit's transition({ when }) decision was false
|
|
130
|
+
* (browser/transition-when.ts): the navigation is urgent, so no segment
|
|
131
|
+
* animates. Every router <ViewTransition> stays in the tree with each class
|
|
132
|
+
* "none", so the element type never changes; keys never read this (#995).
|
|
133
|
+
*/
|
|
134
|
+
transitionGatedOff?: boolean;
|
|
135
|
+
|
|
128
136
|
/**
|
|
129
137
|
* Intercept segments to inject into the tree.
|
|
130
138
|
* These are parallel segments from intercept routes that need to be
|
|
@@ -143,13 +151,31 @@ export interface RenderSegmentsOptions {
|
|
|
143
151
|
rootLayout?: ComponentType<RootLayoutProps>;
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
/**
|
|
155
|
+
* On a gated-off commit every segment keeps its <ViewTransition> so the
|
|
156
|
+
* element type never changes with a per-navigation `transition({ when })`
|
|
157
|
+
* decision (#995), but animates nothing: "none" for every class, whatever the
|
|
158
|
+
* transition type.
|
|
159
|
+
*/
|
|
160
|
+
const GATED_OFF_CLASSES = {
|
|
161
|
+
enter: "none",
|
|
162
|
+
exit: "none",
|
|
163
|
+
update: "none",
|
|
164
|
+
share: "none",
|
|
165
|
+
default: "none",
|
|
166
|
+
} as const;
|
|
167
|
+
|
|
146
168
|
function createViewTransitionBoundary(
|
|
147
169
|
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
148
170
|
children: ReactNode,
|
|
149
171
|
): ReactNode {
|
|
150
|
-
// `viewTransition`
|
|
151
|
-
// <ViewTransition>
|
|
152
|
-
const {
|
|
172
|
+
// `viewTransition` (boundary opt-out) and `when` (the browser-run gate) are
|
|
173
|
+
// router fields, not React <ViewTransition> props — strip them.
|
|
174
|
+
const {
|
|
175
|
+
viewTransition: _viewTransition,
|
|
176
|
+
when: _when,
|
|
177
|
+
...vtProps
|
|
178
|
+
} = transition;
|
|
153
179
|
return createElement(ReactViewTransition, {
|
|
154
180
|
...vtProps,
|
|
155
181
|
// The commit after an optimistic clientUrls() presentation repaints the
|
|
@@ -306,6 +332,7 @@ export async function renderSegments(
|
|
|
306
332
|
forceAwait,
|
|
307
333
|
outletPending = false,
|
|
308
334
|
rootLayout: RootLayout,
|
|
335
|
+
transitionGatedOff,
|
|
309
336
|
} = options || {};
|
|
310
337
|
|
|
311
338
|
const segDebug = INTERNAL_RANGO_DEBUG;
|
|
@@ -358,8 +385,11 @@ export async function renderSegments(
|
|
|
358
385
|
// same-route navigation reconciles (holds content) instead of remounting. The
|
|
359
386
|
// value is a static property of the route's position in the tree, so it is the
|
|
360
387
|
// same on every render of that route (SSR, navigation, action) — the keys
|
|
361
|
-
// never drift.
|
|
362
|
-
//
|
|
388
|
+
// never drift. It reads the static `transition` config only, never the
|
|
389
|
+
// per-navigation `when` decision (transitionGatedOff): a server-side `when`
|
|
390
|
+
// used to delete `transition`, which flipped this value and remounted the
|
|
391
|
+
// route twice (#995). Cross-route navigation still remounts: different routes
|
|
392
|
+
// have different segment ids regardless of transition scope.
|
|
363
393
|
const inTransitionScope = normalizedSegments.some(
|
|
364
394
|
(s) =>
|
|
365
395
|
s.transition != null &&
|
|
@@ -522,10 +552,13 @@ export async function renderSegments(
|
|
|
522
552
|
transition &&
|
|
523
553
|
transition.viewTransition !== false
|
|
524
554
|
) {
|
|
555
|
+
const boundary = transitionGatedOff
|
|
556
|
+
? { ...transition, ...GATED_OFF_CLASSES }
|
|
557
|
+
: transition;
|
|
525
558
|
if (node.segment.type === "layout") {
|
|
526
|
-
outletContent = wrapDefaultOutletContent(outletContent,
|
|
559
|
+
outletContent = wrapDefaultOutletContent(outletContent, boundary);
|
|
527
560
|
} else {
|
|
528
|
-
nodeContent = createViewTransitionBoundary(
|
|
561
|
+
nodeContent = createViewTransitionBoundary(boundary, nodeContent);
|
|
529
562
|
}
|
|
530
563
|
}
|
|
531
564
|
|