@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
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serveShellRequest — one request through the production PPR serve path.
|
|
3
|
+
*
|
|
4
|
+
* Builds the router's request handler as `router.fetch` does
|
|
5
|
+
* (`createRSCHandler` with the router's `nonce`, `version` and `cache`
|
|
6
|
+
* config) and serves one GET through it, so middleware, the shell serve path
|
|
7
|
+
* and a MISS's background capture all run production code, on real Flight
|
|
8
|
+
* (the vendored react-server-dom serializer). The call settles every
|
|
9
|
+
* background task the request scheduled before it returns, so a MISS's
|
|
10
|
+
* `putShell` has landed.
|
|
11
|
+
*
|
|
12
|
+
* The one stub is the SSR module (the HTML step): `react-dom/server` refuses
|
|
13
|
+
* to load under the react-server condition this project runs with.
|
|
14
|
+
* - `renderHTML` passes the Flight stream through: a document body is its
|
|
15
|
+
* Flight payload.
|
|
16
|
+
* - `captureShellHTML` returns, as the prelude, the Flight text the capture
|
|
17
|
+
* rendered before `quiesce` (plus the post-quiesce task hops the real
|
|
18
|
+
* capture takes). `postponed` is always null. A capture that does not
|
|
19
|
+
* quiesce within what `ppr.captureTimeout` leaves after the handler output
|
|
20
|
+
* settled returns no shell; the real one goes on to its abort and returns
|
|
21
|
+
* whatever prelude rendered by then.
|
|
22
|
+
* - `resumeShellHTML` passes the tail's Flight stream through: a HIT body is
|
|
23
|
+
* the stored prelude followed by the tail's Flight payload.
|
|
24
|
+
* So there is no HTML: the prelude's `<body` sanity gate, SSR render errors
|
|
25
|
+
* (production refuses the capture on one), fizz resume of the holes,
|
|
26
|
+
* bootstrap scripts and nonce injection stay e2e-only. A route whose real
|
|
27
|
+
* capture refuses for a root postpone (a live loader read with no boundary
|
|
28
|
+
* above it) or an SSR render error is captured here.
|
|
29
|
+
*
|
|
30
|
+
* Must run under the `react-server` condition (the rsc Vitest project), with
|
|
31
|
+
* `rangoTestAliases()` resolving `@vitejs/plugin-rsc/rsc/server` to the stub
|
|
32
|
+
* that carries the Flight runtime.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
36
|
+
import { toInternal, type Rango } from "../router/router-interfaces.js";
|
|
37
|
+
import { ensureRouterManifest } from "../route-map-builder.js";
|
|
38
|
+
import type { SegmentCacheStore, ShellCacheEntry } from "../cache/types.js";
|
|
39
|
+
import type { ExecutionContext } from "../types/request-scope.js";
|
|
40
|
+
import type { HandlerCacheConfig, SSRModule } from "../rsc/types.js";
|
|
41
|
+
import type { createRSCHandler } from "../rsc/handler.js";
|
|
42
|
+
import {
|
|
43
|
+
POST_QUIESCE_TASK_HOPS,
|
|
44
|
+
SHELL_CAPTURE_MAX_WAIT_MS,
|
|
45
|
+
} from "../rsc/shell-capture-constants.js";
|
|
46
|
+
import { SEGMENT_FRAGMENT_CAPABILITY_HEADER } from "../segment-fragments.js";
|
|
47
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
48
|
+
import {
|
|
49
|
+
parsePprReplayStatus,
|
|
50
|
+
parseShellStatus,
|
|
51
|
+
shellCacheKey,
|
|
52
|
+
type PprReplayStatus,
|
|
53
|
+
type ShellStatus,
|
|
54
|
+
} from "./shell-status.js";
|
|
55
|
+
|
|
56
|
+
/** Options for {@link serveShellRequest}. */
|
|
57
|
+
export interface ServeShellRequestOptions<TEnv = any> {
|
|
58
|
+
/**
|
|
59
|
+
* The shell and segment store for this request, in place of the router's
|
|
60
|
+
* `createRouter({ cache })` store. The rest of that config (`enabled`,
|
|
61
|
+
* `searchParams`) still applies. Omit it to serve through the router's own
|
|
62
|
+
* store.
|
|
63
|
+
*/
|
|
64
|
+
cacheStore?: SegmentCacheStore;
|
|
65
|
+
/** Environment bindings, as `router.fetch(request, { env })` passes them. */
|
|
66
|
+
env?: TEnv;
|
|
67
|
+
/**
|
|
68
|
+
* Request headers. A document request defaults `accept` to `text/html`.
|
|
69
|
+
*/
|
|
70
|
+
headers?: HeadersInit;
|
|
71
|
+
/**
|
|
72
|
+
* Serve the client navigation the browser sends (`?_rsc_partial`) instead
|
|
73
|
+
* of a document GET: from `from` (default `/`) with `segments` mounted
|
|
74
|
+
* (default none), advertising fragment passthrough. The replay decision is
|
|
75
|
+
* `result.replayStatus`.
|
|
76
|
+
*/
|
|
77
|
+
partial?: true | { from?: string; segments?: readonly string[] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Result of {@link serveShellRequest}. */
|
|
81
|
+
export interface ServeShellRequestResult {
|
|
82
|
+
/** The handler's Response. Its body is already read: see `body`. */
|
|
83
|
+
response: Response;
|
|
84
|
+
/** The response body text. */
|
|
85
|
+
body: string;
|
|
86
|
+
/** `x-rango-shell` (`HIT` | `MISS`), or null when the header is absent. */
|
|
87
|
+
shellStatus: ShellStatus | null;
|
|
88
|
+
/** `x-rango-ppr-replay` on a partial request, or null. */
|
|
89
|
+
replayStatus: PprReplayStatus | null;
|
|
90
|
+
/**
|
|
91
|
+
* The prelude a HIT served: the capture's Flight text at quiesce (the
|
|
92
|
+
* shell as captured). Undefined unless `shellStatus` is `HIT`.
|
|
93
|
+
*/
|
|
94
|
+
prelude: string | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* The Flight payload this request rendered: a HIT's tail, a document's
|
|
97
|
+
* render, or a Flight response. Undefined when no Flight was rendered (a
|
|
98
|
+
* redirect or a middleware response).
|
|
99
|
+
*/
|
|
100
|
+
flight: string | undefined;
|
|
101
|
+
/**
|
|
102
|
+
* The shell key production resolved for this request: for a document the
|
|
103
|
+
* serve path read and captured (a MISS or a HIT), the key it used, request
|
|
104
|
+
* partition included (a route partitioned by `cache({ key })` or the
|
|
105
|
+
* store's `keyGenerator`). Otherwise (a route without `ppr`, a request
|
|
106
|
+
* passed to axis 1 before its key was resolved, a partial request) the
|
|
107
|
+
* document URL's key without a partition, as `shellCacheKey` builds it.
|
|
108
|
+
*/
|
|
109
|
+
key: string;
|
|
110
|
+
/**
|
|
111
|
+
* Read the document shell entry under `key` from the request's store
|
|
112
|
+
* (a passive `getShell`), or null. A read, so on a store with a shell memo
|
|
113
|
+
* (CFCacheStore, VercelCacheStore) it warms the memo like any other read.
|
|
114
|
+
*/
|
|
115
|
+
readEntry(): Promise<ShellCacheEntry | null>;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
type ShellHandler = ReturnType<typeof createRSCHandler>;
|
|
119
|
+
|
|
120
|
+
/** What one call's cache resolution and SSR step saw. */
|
|
121
|
+
interface Recorder {
|
|
122
|
+
cache?: HandlerCacheConfig;
|
|
123
|
+
/** The shell key the serve path resolved (RequestContext._shellKey). */
|
|
124
|
+
shellKey?: string;
|
|
125
|
+
/** A document render reached the HTML step. */
|
|
126
|
+
rendered?: boolean;
|
|
127
|
+
/** The HIT tail's Flight text. */
|
|
128
|
+
tail?: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const recorders = new AsyncLocalStorage<Recorder>();
|
|
132
|
+
|
|
133
|
+
function macrotask(): Promise<void> {
|
|
134
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Record the shell key the serve path resolved: every SSR step of a document
|
|
139
|
+
* the serve path read runs inside its request context (a capture's derived
|
|
140
|
+
* context inherits it).
|
|
141
|
+
*/
|
|
142
|
+
function recordShellKey(recorder: Recorder | undefined): void {
|
|
143
|
+
const shellKey = _getRequestContext()?._shellKey;
|
|
144
|
+
if (recorder && shellKey !== undefined) recorder.shellKey ??= shellKey;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const SSR_STUB: SSRModule = {
|
|
148
|
+
async renderHTML(rscStream) {
|
|
149
|
+
const recorder = recorders.getStore();
|
|
150
|
+
if (recorder) recorder.rendered = true;
|
|
151
|
+
recordShellKey(recorder);
|
|
152
|
+
return rscStream;
|
|
153
|
+
},
|
|
154
|
+
async captureShellHTML(rscStream, options) {
|
|
155
|
+
const reader = rscStream.getReader();
|
|
156
|
+
const decoder = new TextDecoder();
|
|
157
|
+
let frozen = "";
|
|
158
|
+
// The capture gate freezes the stream at quiesce without closing it.
|
|
159
|
+
void (async () => {
|
|
160
|
+
for (;;) {
|
|
161
|
+
const { done, value } = await reader.read();
|
|
162
|
+
if (done) return;
|
|
163
|
+
frozen += decoder.decode(value, { stream: true });
|
|
164
|
+
}
|
|
165
|
+
})().catch(() => {});
|
|
166
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
167
|
+
const deadline = new Promise<false>((resolve) => {
|
|
168
|
+
timer = setTimeout(
|
|
169
|
+
() => resolve(false),
|
|
170
|
+
options.maxWaitMs ?? SHELL_CAPTURE_MAX_WAIT_MS,
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
const quiet = await Promise.race([
|
|
175
|
+
options.quiesce.then(() => true as const),
|
|
176
|
+
deadline,
|
|
177
|
+
]);
|
|
178
|
+
if (!quiet) return null;
|
|
179
|
+
} finally {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
}
|
|
182
|
+
for (let i = 0; i < POST_QUIESCE_TASK_HOPS; i++) await macrotask();
|
|
183
|
+
return { prelude: new TextEncoder().encode(frozen), postponed: null };
|
|
184
|
+
},
|
|
185
|
+
async resumeShellHTML(rscStream) {
|
|
186
|
+
const recorder = recorders.getStore();
|
|
187
|
+
recordShellKey(recorder);
|
|
188
|
+
// The served HIT's tail only, not a background re-render's.
|
|
189
|
+
if (!recorder || recorder.tail !== undefined) return rscStream;
|
|
190
|
+
recorder.tail = "";
|
|
191
|
+
const decoder = new TextDecoder();
|
|
192
|
+
return rscStream.pipeThrough(
|
|
193
|
+
new TransformStream<Uint8Array, Uint8Array>({
|
|
194
|
+
transform(chunk, controller) {
|
|
195
|
+
recorder.tail += decoder.decode(chunk, { stream: true });
|
|
196
|
+
controller.enqueue(chunk);
|
|
197
|
+
},
|
|
198
|
+
flush() {
|
|
199
|
+
recorder.tail += decoder.decode();
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** Handlers per router, keyed by the `cacheStore` override or the router. */
|
|
207
|
+
const handlers = new WeakMap<object, WeakMap<object, ShellHandler>>();
|
|
208
|
+
|
|
209
|
+
async function getHandler(
|
|
210
|
+
router: Rango<any, any>,
|
|
211
|
+
cacheStore: SegmentCacheStore | undefined,
|
|
212
|
+
): Promise<ShellHandler> {
|
|
213
|
+
let byStore = handlers.get(router);
|
|
214
|
+
if (!byStore) handlers.set(router, (byStore = new WeakMap()));
|
|
215
|
+
const cached = byStore.get(cacheStore ?? router);
|
|
216
|
+
if (cached) return cached;
|
|
217
|
+
|
|
218
|
+
const internal = toInternal(router);
|
|
219
|
+
const routerCache = internal.cache;
|
|
220
|
+
// Resolved per request, as the handler resolves the router's own config.
|
|
221
|
+
const cache = (env: unknown, ctx?: ExecutionContext): HandlerCacheConfig => {
|
|
222
|
+
const base =
|
|
223
|
+
typeof routerCache === "function" ? routerCache(env, ctx) : routerCache;
|
|
224
|
+
const config = cacheStore ? { ...base, store: cacheStore } : base!;
|
|
225
|
+
const recorder = recorders.getStore();
|
|
226
|
+
if (recorder) recorder.cache = config;
|
|
227
|
+
return config;
|
|
228
|
+
};
|
|
229
|
+
const { createRSCHandler } = await import("../rsc/handler.js");
|
|
230
|
+
const handler = createRSCHandler({
|
|
231
|
+
router: internal,
|
|
232
|
+
nonce: internal.nonce,
|
|
233
|
+
version: internal.version,
|
|
234
|
+
loadSSRModule: async () => SSR_STUB,
|
|
235
|
+
cache: routerCache || cacheStore ? cache : undefined,
|
|
236
|
+
});
|
|
237
|
+
byStore.set(cacheStore ?? router, handler);
|
|
238
|
+
return handler;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildRequest(url: URL, options: ServeShellRequestOptions): Request {
|
|
242
|
+
const headers = new Headers(options.headers);
|
|
243
|
+
const partial = options.partial;
|
|
244
|
+
if (!partial) {
|
|
245
|
+
if (!headers.has("accept")) headers.set("accept", "text/html");
|
|
246
|
+
return new Request(url, { headers });
|
|
247
|
+
}
|
|
248
|
+
const { from = "/", segments = [] } = partial === true ? {} : partial;
|
|
249
|
+
const target = new URL(url);
|
|
250
|
+
target.searchParams.set("_rsc_partial", "true");
|
|
251
|
+
target.searchParams.set("_rsc_segments", segments.join(","));
|
|
252
|
+
if (!headers.has("X-RSC-Router-Client-Path")) {
|
|
253
|
+
headers.set("X-RSC-Router-Client-Path", new URL(from, url).href);
|
|
254
|
+
}
|
|
255
|
+
if (!headers.has(SEGMENT_FRAGMENT_CAPABILITY_HEADER)) {
|
|
256
|
+
headers.set(SEGMENT_FRAGMENT_CAPABILITY_HEADER, "1");
|
|
257
|
+
}
|
|
258
|
+
return new Request(target, { headers });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Reset the per-isolate PPR state requests leave behind, so the next test
|
|
263
|
+
* starts as a fresh worker would: the capture's stampede guard, its backoff
|
|
264
|
+
* (a refused capture backs its URL off for later tests too), the shell path's
|
|
265
|
+
* once-per-key warnings, the build-shell manifest memo, the "use cache"
|
|
266
|
+
* in-flight leaders and warnings, and CFCacheStore's isolate memos (shells,
|
|
267
|
+
* tag markers, tag hints) and warnings, which every CFCacheStore in the
|
|
268
|
+
* process shares by namespace and URL. Call it in `beforeEach`, never while
|
|
269
|
+
* a request is in flight. VercelCacheStore's memos live on the `cache`
|
|
270
|
+
* handle it is given: a new handle starts empty.
|
|
271
|
+
*/
|
|
272
|
+
export async function resetShellTestState(): Promise<void> {
|
|
273
|
+
const [capture, serve, buildShells, cf, cacheRuntime] = await Promise.all([
|
|
274
|
+
import("../rsc/shell-capture.js"),
|
|
275
|
+
import("../rsc/shell-serve.js"),
|
|
276
|
+
import("../rsc/shell-build-manifest.js"),
|
|
277
|
+
import("../cache/cf/cf-cache-store.js"),
|
|
278
|
+
import("../cache/cache-runtime.js"),
|
|
279
|
+
]);
|
|
280
|
+
capture.resetShellCaptureStateForTests();
|
|
281
|
+
serve.resetShellServeStateForTests();
|
|
282
|
+
buildShells.resetBuildShellManifestForTests();
|
|
283
|
+
cf.resetCFShellMemoForTests();
|
|
284
|
+
cacheRuntime.resetCacheRuntimeForTests();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Settle background tasks, including ones scheduled while settling. */
|
|
288
|
+
async function settle(tasks: Promise<unknown>[]): Promise<void> {
|
|
289
|
+
do {
|
|
290
|
+
while (tasks.length > 0) await Promise.allSettled(tasks.splice(0));
|
|
291
|
+
await macrotask();
|
|
292
|
+
} while (tasks.length > 0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Serve one GET for `url` through the router's production request handler
|
|
297
|
+
* and settle the background tasks it scheduled (a MISS's capture and
|
|
298
|
+
* `putShell` included). The first request for a `ppr` route is a MISS; the
|
|
299
|
+
* next one with the same store is a HIT from the stored shell. The HTML step
|
|
300
|
+
* is stubbed (see the module header), so `prelude` and `flight` are Flight
|
|
301
|
+
* text. The request starts in a later millisecond than the call: a store
|
|
302
|
+
* refuses a capture that starts in the millisecond of a tag invalidation, so
|
|
303
|
+
* an `updateTag()` made just before the call never refuses its recapture.
|
|
304
|
+
* Rejects when the handler throws or the response stream errors (a failing
|
|
305
|
+
* HIT tail), after settling the tasks scheduled so far.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* const store = new MemorySegmentCacheStore();
|
|
309
|
+
* const miss = await serveShellRequest(router, "/product/1", { cacheStore: store });
|
|
310
|
+
* expect(miss.shellStatus).toBe("MISS");
|
|
311
|
+
* const hit = await serveShellRequest(router, "/product/1", { cacheStore: store });
|
|
312
|
+
* expect(hit.shellStatus).toBe("HIT");
|
|
313
|
+
* expect(hit.prelude).toContain("Widget"); // captured shell
|
|
314
|
+
* expect(hit.flight).toContain("in stock"); // live hole, rendered now
|
|
315
|
+
*/
|
|
316
|
+
export async function serveShellRequest<TEnv = any>(
|
|
317
|
+
router: Rango<TEnv, any>,
|
|
318
|
+
url: string | URL,
|
|
319
|
+
options: ServeShellRequestOptions<TEnv> = {},
|
|
320
|
+
): Promise<ServeShellRequestResult> {
|
|
321
|
+
const called = Date.now();
|
|
322
|
+
const target = new URL(url, "http://localhost");
|
|
323
|
+
await ensureRouterManifest(router.id);
|
|
324
|
+
const handler = await getHandler(router, options.cacheStore);
|
|
325
|
+
const request = buildRequest(target, options);
|
|
326
|
+
const tasks: Promise<unknown>[] = [];
|
|
327
|
+
const executionContext: ExecutionContext = {
|
|
328
|
+
waitUntil(promise) {
|
|
329
|
+
tasks.push(
|
|
330
|
+
Promise.resolve(promise).catch((error) =>
|
|
331
|
+
console.error("[waitUntil] Background task failed:", error),
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
passThroughOnException() {},
|
|
336
|
+
};
|
|
337
|
+
const recorder: Recorder = {};
|
|
338
|
+
while (Date.now() === called) await macrotask();
|
|
339
|
+
|
|
340
|
+
const { response, body } = await recorders.run(recorder, async () => {
|
|
341
|
+
try {
|
|
342
|
+
const response = await handler(request, {
|
|
343
|
+
env: options.env,
|
|
344
|
+
ctx: executionContext,
|
|
345
|
+
});
|
|
346
|
+
return { response, body: await response.text() };
|
|
347
|
+
} finally {
|
|
348
|
+
await settle(tasks);
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const config =
|
|
353
|
+
recorder.cache && recorder.cache.enabled !== false
|
|
354
|
+
? recorder.cache
|
|
355
|
+
: undefined;
|
|
356
|
+
const key = recorder.shellKey ?? shellCacheKey(target, config?.searchParams);
|
|
357
|
+
const tail = recorder.tail;
|
|
358
|
+
const isFlight =
|
|
359
|
+
recorder.rendered ||
|
|
360
|
+
response.headers.get("content-type")?.includes("text/x-component");
|
|
361
|
+
return {
|
|
362
|
+
response,
|
|
363
|
+
body,
|
|
364
|
+
shellStatus: parseShellStatus(response),
|
|
365
|
+
replayStatus: parsePprReplayStatus(response),
|
|
366
|
+
prelude:
|
|
367
|
+
tail !== undefined && body.endsWith(tail)
|
|
368
|
+
? body.slice(0, body.length - tail.length)
|
|
369
|
+
: undefined,
|
|
370
|
+
flight: tail ?? (isFlight && body ? body : undefined),
|
|
371
|
+
key,
|
|
372
|
+
async readEntry() {
|
|
373
|
+
// Passive, like the serve path's own reads: a stale entry's SWR
|
|
374
|
+
// revalidation stays unclaimed for the next request.
|
|
375
|
+
const read = await config?.store.getShell?.(key, {
|
|
376
|
+
claimRevalidation: false,
|
|
377
|
+
});
|
|
378
|
+
return read?.entry ?? null;
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
@@ -18,56 +18,42 @@
|
|
|
18
18
|
* Secondary unit signal: `store.getShell(shellCacheKey(url))` after a real
|
|
19
19
|
* capture flush (background `putShell`). There is no Flight flag for shell HIT.
|
|
20
20
|
*
|
|
21
|
-
* 3. **
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* 3. **A real MISS → capture → HIT in unit tests:** `serveShellRequest`
|
|
22
|
+
* (`@rangojs/router/testing/flight`, serve-shell-request.ts) serves one
|
|
23
|
+
* request through the router's production handler and settles its
|
|
24
|
+
* background capture; a later request with the same store is a real HIT or
|
|
25
|
+
* partial replay. Its only stub is the HTML step (the SSR module).
|
|
26
|
+
* **Stays e2e-only:** real HTML (the prelude's `<body` gate, fizz resume of
|
|
27
|
+
* holes), browser resume of holes, live Cloudflare/Vercel runtimes (store
|
|
28
|
+
* test doubles work in unit), and build-time producer B bake. The unit layer
|
|
29
|
+
* must never stub a shell HIT response.
|
|
26
30
|
*
|
|
27
31
|
* Import from `@rangojs/router/testing` (Vitest) or `@rangojs/router/testing/e2e`
|
|
28
32
|
* (Playwright — same pure helpers, no Vite virtuals).
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
|
-
import {
|
|
35
|
+
import { composeCacheKeys } from "../cache/cache-key-utils.js";
|
|
36
|
+
import {
|
|
37
|
+
PPR_REPLAY_BYPASS_REASONS,
|
|
38
|
+
PPR_REPLAY_STATUS_HEADER,
|
|
39
|
+
SHELL_STATUS_HEADER,
|
|
40
|
+
buildShellKey,
|
|
41
|
+
partitionShellKey,
|
|
42
|
+
type PprReplayBypassReason,
|
|
43
|
+
} from "../rsc/shell-capture-constants.js";
|
|
32
44
|
import {
|
|
33
45
|
compileSearchParamsFilter,
|
|
34
46
|
type CacheSearchParams,
|
|
35
47
|
} from "../cache/search-params-filter.js";
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export const PPR_REPLAY_STATUS_HEADER: string = "x-rango-ppr-replay";
|
|
49
|
+
// The production header names and bypass reasons, from the React-free leaf
|
|
50
|
+
// the serve path reads them from.
|
|
51
|
+
export { PPR_REPLAY_STATUS_HEADER, SHELL_STATUS_HEADER };
|
|
52
|
+
export type { PprReplayBypassReason };
|
|
42
53
|
|
|
43
54
|
/** Values the serve path writes on `x-rango-shell`. */
|
|
44
55
|
export type ShellStatus = "HIT" | "MISS";
|
|
45
56
|
|
|
46
|
-
const PPR_REPLAY_BYPASS_REASONS = [
|
|
47
|
-
"method",
|
|
48
|
-
"dynamic",
|
|
49
|
-
"nonce",
|
|
50
|
-
"store-unavailable",
|
|
51
|
-
"passive-read-unsupported",
|
|
52
|
-
"no-navigation-context",
|
|
53
|
-
"prerender-store",
|
|
54
|
-
"intercept",
|
|
55
|
-
"cache-disabled",
|
|
56
|
-
"read-error",
|
|
57
|
-
"no-entry",
|
|
58
|
-
"invalid-version",
|
|
59
|
-
"corrupt-entry",
|
|
60
|
-
"handler-live-holes",
|
|
61
|
-
"transition-when",
|
|
62
|
-
"no-segment-snapshot",
|
|
63
|
-
"snapshot-miss",
|
|
64
|
-
"explicit-cache-hit",
|
|
65
|
-
"stale-build-entry",
|
|
66
|
-
] as const;
|
|
67
|
-
|
|
68
|
-
/** Bounded reasons a PPR partial request can fall open to ordinary matching. */
|
|
69
|
-
export type PprReplayBypassReason = (typeof PPR_REPLAY_BYPASS_REASONS)[number];
|
|
70
|
-
|
|
71
57
|
/** Parsed `x-rango-ppr-replay` value. */
|
|
72
58
|
export type PprReplayStatus =
|
|
73
59
|
| { outcome: "HIT"; freshness: "fresh" | "stale" }
|
|
@@ -77,28 +63,68 @@ export type PprReplayStatus =
|
|
|
77
63
|
export type ShellStatusTarget = Response | { headers: Headers };
|
|
78
64
|
|
|
79
65
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
66
|
+
* What partitions a ppr route's shell, for {@link shellCacheKey}: the
|
|
67
|
+
* request's results of the functions that partition the route's cache()
|
|
68
|
+
* record.
|
|
69
|
+
*/
|
|
70
|
+
export interface ShellCachePartition {
|
|
71
|
+
/** The route's cache() `key()` results, outermost first. */
|
|
72
|
+
keys?: readonly string[];
|
|
73
|
+
/**
|
|
74
|
+
* Store `keyGenerator` results, one per store, outermost first: an
|
|
75
|
+
* enclosing cache() on another store's, then the store's of the route's
|
|
76
|
+
* own cache() when it sets no `key()` (with no route cache(), the app
|
|
77
|
+
* store's). When every one returns the default key they partition
|
|
78
|
+
* nothing: leave them out. Otherwise each keeps its position, and one
|
|
79
|
+
* that returns the default key is `""`.
|
|
80
|
+
*/
|
|
81
|
+
generated?: readonly string[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Shell store key for a document URL: the production `buildShellKey` (host +
|
|
86
|
+
* pathname + sorted search + `:shell`) and `partitionShellKey`, both from the
|
|
87
|
+
* React-free leaf `rsc/shell-capture-constants.ts`, so the testing barrel never
|
|
88
|
+
* imports the shell-serve module (it pulls React).
|
|
84
89
|
*
|
|
85
90
|
* Accepts a `URL` or an absolute/relative request URL string (relative strings
|
|
86
91
|
* resolve against `http://localhost`). Pass the router's `cache.searchParams`
|
|
87
92
|
* config as `searchParams` when the app under test sets one — the production
|
|
88
|
-
* key applies it, so the expected key must too.
|
|
93
|
+
* key applies it, so the expected key must too. When the route's record is
|
|
94
|
+
* partitioned by the request, its shell is too: pass what partitions it as
|
|
95
|
+
* `partition`, composed as production composes it (`composeCacheKeys`,
|
|
96
|
+
* which namespaces each `key()` result):
|
|
97
|
+
* - its `cache({ key })` result, as a string;
|
|
98
|
+
* - nested keyed `cache()` scopes' `key()` results, as an array, outermost
|
|
99
|
+
* first;
|
|
100
|
+
* - store `keyGenerator` results, with or without `key()` results, as
|
|
101
|
+
* `{ keys, generated }` (see {@link ShellCachePartition}).
|
|
89
102
|
*/
|
|
90
103
|
export function shellCacheKey(
|
|
91
104
|
url: URL | string,
|
|
92
105
|
searchParams?: CacheSearchParams,
|
|
106
|
+
partition?: string | readonly string[] | ShellCachePartition,
|
|
93
107
|
): string {
|
|
94
108
|
const resolved =
|
|
95
109
|
typeof url === "string" ? new URL(url, "http://localhost") : url;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
const key = buildShellKey(resolved, compileSearchParamsFilter(searchParams));
|
|
111
|
+
if (partition === undefined) return key;
|
|
112
|
+
const { keys = [], generated = [] }: ShellCachePartition =
|
|
113
|
+
typeof partition === "string"
|
|
114
|
+
? { keys: [partition] }
|
|
115
|
+
: isPartitionList(partition)
|
|
116
|
+
? { keys: partition }
|
|
117
|
+
: partition;
|
|
118
|
+
// Nothing partitions: the key as when `partition` is omitted.
|
|
119
|
+
return keys.length === 0 && generated.length === 0
|
|
120
|
+
? key
|
|
121
|
+
: partitionShellKey(key, composeCacheKeys(keys, generated));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isPartitionList(
|
|
125
|
+
partition: readonly string[] | ShellCachePartition,
|
|
126
|
+
): partition is readonly string[] {
|
|
127
|
+
return Array.isArray(partition);
|
|
102
128
|
}
|
|
103
129
|
|
|
104
130
|
function getHeaders(target: ShellStatusTarget): Headers {
|
|
@@ -133,12 +133,13 @@ export function ThemeProvider({
|
|
|
133
133
|
setSystemTheme(getSystemTheme());
|
|
134
134
|
// Re-sync state from an EXPLICITLY stored theme after mount. initialTheme
|
|
135
135
|
// comes from the payload and can legitimately differ from the visitor's
|
|
136
|
-
// stored theme — on a PPR shell HIT it is deliberately the CAPTURE's
|
|
137
|
-
// (the resume tree must match the
|
|
138
|
-
// ShellCacheEntry.initialTheme). The FOUC script
|
|
139
|
-
// theme to the document pre-paint; this brings
|
|
140
|
-
// useTheme readers) in line with it, and is
|
|
141
|
-
// reads storage (the initializer must not —
|
|
136
|
+
// stored theme — on a PPR shell HIT it is deliberately the CAPTURE's
|
|
137
|
+
// initialTheme, the no-cookie default (the resume tree must match the
|
|
138
|
+
// frozen prelude; see ShellCacheEntry.initialTheme). The FOUC script
|
|
139
|
+
// already applied the stored theme to the document pre-paint; this brings
|
|
140
|
+
// the provider state (toggles, useTheme readers) in line with it, and is
|
|
141
|
+
// the ONLY place the provider reads storage (the initializer must not —
|
|
142
|
+
// see the parity note above).
|
|
142
143
|
// Only an explicit cookie/localStorage value re-syncs — a defaultTheme
|
|
143
144
|
// fallback must not override a server-provided initialTheme when the
|
|
144
145
|
// visitor never chose a theme.
|