@rangojs/router 0.16.0 → 0.18.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- 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 +185 -44
- 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 +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +75 -57
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +26 -6
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- 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 +990 -348
- 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 +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
package/src/server/context.ts
CHANGED
|
@@ -12,6 +12,12 @@ import type {
|
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { invariant, DslContextError } from "../errors";
|
|
14
14
|
import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
15
|
+
import type { ContextVar } from "../context-var.js";
|
|
16
|
+
import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.js";
|
|
17
|
+
import {
|
|
18
|
+
getCacheExecScope,
|
|
19
|
+
type CacheExecScope,
|
|
20
|
+
} from "../cache/cache-exec-scope.js";
|
|
15
21
|
|
|
16
22
|
// ============================================================================
|
|
17
23
|
// Performance Metrics Types
|
|
@@ -65,6 +71,17 @@ export type EntryPropCommon = {
|
|
|
65
71
|
id: string;
|
|
66
72
|
shortCode: string; // Short identifier for network efficiency (e.g., "L0", "P1", "R2")
|
|
67
73
|
parent: EntryData | null;
|
|
74
|
+
/**
|
|
75
|
+
* Orphan siblings only (attachOrphanSibling in
|
|
76
|
+
* route-definition/dsl-helpers.ts): the entry whose `layout[]` holds this
|
|
77
|
+
* one. `parent` stays null on an orphan (matchError's matched-id stack in
|
|
78
|
+
* router/match-api.ts starts at the boundary-holding entry, possibly an
|
|
79
|
+
* orphan, and must stop there), so the boundary walkers in
|
|
80
|
+
* router/error-handling.ts follow this instead. A lookup that starts at an
|
|
81
|
+
* orphan (its loaders, an intercept it declares) then reaches the owner's
|
|
82
|
+
* boundaries and the owner's ancestors.
|
|
83
|
+
*/
|
|
84
|
+
orphanOwner?: EntryData;
|
|
68
85
|
/** Cache configuration for this entry (set by cache() DSL) */
|
|
69
86
|
cache?: EntryCacheConfig;
|
|
70
87
|
/** URL prefix from include() scope, used for MountContext on client */
|
|
@@ -118,6 +135,15 @@ export type LoaderEntry = {
|
|
|
118
135
|
* appears on navigation-lane entries.
|
|
119
136
|
*/
|
|
120
137
|
awaitBeforeFlush?: true;
|
|
138
|
+
/**
|
|
139
|
+
* loader(Def, { ssr: false }) on every DSL evaluation, document and
|
|
140
|
+
* navigation: the loader's PPR bake lane (loader-cache.ts
|
|
141
|
+
* resolveLoaderData). The bake/seed key rides on it, so a client
|
|
142
|
+
* navigation's shell replay pins exactly the loaders a document HIT pins,
|
|
143
|
+
* whatever the entry's loading(). awaitBeforeFlush cannot carry that: a
|
|
144
|
+
* navigation evaluation never has it.
|
|
145
|
+
*/
|
|
146
|
+
bake?: true;
|
|
121
147
|
};
|
|
122
148
|
|
|
123
149
|
/**
|
|
@@ -187,9 +213,6 @@ export type InterceptEntry = {
|
|
|
187
213
|
routeName: string; // e.g., "card"
|
|
188
214
|
handler: ReactNode | Handler<any, any, any>;
|
|
189
215
|
middleware: MiddlewareFn<any, any>[];
|
|
190
|
-
revalidate: ShouldRevalidateFn<any, any>[];
|
|
191
|
-
errorBoundary: (ReactNode | ErrorBoundaryHandler)[];
|
|
192
|
-
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
193
216
|
loader: LoaderEntry[];
|
|
194
217
|
loading?: ReactNode | false;
|
|
195
218
|
transition?: TransitionConfig;
|
|
@@ -833,27 +856,30 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
|
833
856
|
// Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
|
|
834
857
|
// of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
|
|
835
858
|
// invoked via ctx.use). Consulted by isInsideCacheScope() to exempt
|
|
836
|
-
// request-scoped reads, by getCurrentLoaderBodyId() for guard-warning
|
|
837
|
-
// attribution
|
|
838
|
-
//
|
|
839
|
-
//
|
|
840
|
-
|
|
841
|
-
// unchanged.
|
|
842
|
-
const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
|
|
843
|
-
const loaderBodyScopeALS: AsyncLocalStorage<{
|
|
844
|
-
active: true;
|
|
845
|
-
loaderId?: string;
|
|
846
|
-
handlerInvoked?: boolean;
|
|
847
|
-
}> = ((globalThis as any)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{
|
|
859
|
+
// request-scoped reads, and by getCurrentLoaderBodyId() for guard-warning
|
|
860
|
+
// attribution. It deliberately does NOT affect isInsideLoaderScope(), so
|
|
861
|
+
// rendered()/barrier/deadlock gating (which must distinguish DSL from
|
|
862
|
+
// handler-invoked loaders) is unchanged.
|
|
863
|
+
interface LoaderBodyScope {
|
|
848
864
|
active: true;
|
|
849
865
|
loaderId?: string;
|
|
850
|
-
|
|
851
|
-
|
|
866
|
+
/** The body scope this one was entered from (a ctx.use(Loader) chain). */
|
|
867
|
+
parent?: LoaderBodyScope;
|
|
868
|
+
/** The "use cache" scope the body was entered in (assertNonCacheableReadAllowed). */
|
|
869
|
+
execScope?: CacheExecScope;
|
|
870
|
+
/** The tags this execution records (#964, cache-tag.ts "Recorded-tag sets"). */
|
|
871
|
+
tags?: Set<string>;
|
|
872
|
+
}
|
|
873
|
+
const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
|
|
874
|
+
const loaderBodyScopeALS: AsyncLocalStorage<LoaderBodyScope> = ((
|
|
875
|
+
globalThis as any
|
|
876
|
+
)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<LoaderBodyScope>());
|
|
852
877
|
|
|
853
878
|
/**
|
|
854
879
|
* Check if the current execution is inside a cache() DSL boundary.
|
|
855
|
-
* Returns false inside loader execution
|
|
856
|
-
*
|
|
880
|
+
* Returns false inside loader execution: a route cache() does not store
|
|
881
|
+
* loader values, so non-cacheable reads are safe. A loader bound with its own
|
|
882
|
+
* cache() stores its value; recordLoaderIdentityRead guards that.
|
|
857
883
|
*/
|
|
858
884
|
export function isInsideCacheScope(): boolean {
|
|
859
885
|
if (RangoContext.getStore()?.insideCacheScope !== true) return false;
|
|
@@ -869,6 +895,234 @@ export function isInsideCacheScope(): boolean {
|
|
|
869
895
|
return true;
|
|
870
896
|
}
|
|
871
897
|
|
|
898
|
+
/**
|
|
899
|
+
* Throw if called during the ACTIVE background shell-capture render
|
|
900
|
+
* (`_shellCaptureRun` true on the derived request context built by
|
|
901
|
+
* shell-capture.ts). The captured shell is shared across every user hitting
|
|
902
|
+
* the URL, so a request-scoped read here would bake one user's cookies,
|
|
903
|
+
* headers, or per-request variables into markup served to others — same
|
|
904
|
+
* hazard as the cache scopes above, at the document tier. DSL segment loaders
|
|
905
|
+
* need no exemption: the live lane is masked (never executed) during capture,
|
|
906
|
+
* and the bake lane is exactly what this guard exists for.
|
|
907
|
+
*
|
|
908
|
+
* It covers everything the capture waits for, not only synchronous handler
|
|
909
|
+
* code: a promise the handler passes or pushes, an async server component,
|
|
910
|
+
* and a loader the handler awaits (`await ctx.use(Loader)`) all run under the
|
|
911
|
+
* capture context and all bake. The last one used to be exempt (the
|
|
912
|
+
* consumption-lane rule, #672/#674): while a HIT re-ran handlers, a slot
|
|
913
|
+
* handler's `ctx.use` of an identity loader rendered per visitor on the HIT.
|
|
914
|
+
* Now every HIT replays the capture's copy, so the exemption would bake the
|
|
915
|
+
* capturing request's identity into every visitor's page. The cache()
|
|
916
|
+
* guards above keep it (a cache() HIT is a separate tier).
|
|
917
|
+
*
|
|
918
|
+
* Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
|
|
919
|
+
* is also present during the FOREGROUND render (it means "a capture is wanted"),
|
|
920
|
+
* and the foreground must read cookies/headers normally to serve the real user.
|
|
921
|
+
* Only the derived capture context sets `_shellCaptureRun`.
|
|
922
|
+
*
|
|
923
|
+
* Applies only to the READ surfaces (cookies(), headers(), a { cache: false }
|
|
924
|
+
* variable, and the theme reads through tripShellCaptureGuard) whose values
|
|
925
|
+
* become markup. Response directives
|
|
926
|
+
* (invalidateClientCache(), keepClientCache()) stay callable: during capture
|
|
927
|
+
* they are header effects on a discarded response, and on the live HIT path
|
|
928
|
+
* the full pipeline runs so their headers flow to the client normally.
|
|
929
|
+
*
|
|
930
|
+
* The throw can be caught by the code that made the read, so the guard also
|
|
931
|
+
* flags the capture context; the capture refuses on the flag
|
|
932
|
+
* (shell-capture.ts refuseOnCaptureGuard), nothing is stored, and every
|
|
933
|
+
* request keeps getting the normal axis-1 render.
|
|
934
|
+
*/
|
|
935
|
+
export function assertNotInsideShellCapture(
|
|
936
|
+
ctx: unknown,
|
|
937
|
+
fnName: string,
|
|
938
|
+
): void {
|
|
939
|
+
if (!tripShellCaptureGuard(ctx, `${fnName}()`, REQUEST_READ_CAPTURE_FIX)) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const what =
|
|
943
|
+
fnName === "cookies"
|
|
944
|
+
? "cookies"
|
|
945
|
+
: fnName === "headers"
|
|
946
|
+
? "headers"
|
|
947
|
+
: "per-request variables";
|
|
948
|
+
throw new Error(
|
|
949
|
+
`${fnName}() cannot be called while capturing a shared shell ` +
|
|
950
|
+
`(ppr shell capture). The captured shell is served to every user ` +
|
|
951
|
+
`of this URL, so request-scoped data read here would leak one user's ` +
|
|
952
|
+
`${what} to others. Read it ` +
|
|
953
|
+
`inside a loader without ssr: false and consume it with useLoader, e.g. ` +
|
|
954
|
+
`createLoader(async () => getUser(cookies().get("session")?.value)). ` +
|
|
955
|
+
PPR_LANE_HINT,
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* The fix the capture refusal warning (shell-capture.ts refuseOnCaptureGuard)
|
|
961
|
+
* gives for a cookies()/headers()/{ cache: false } variable read. Recorded
|
|
962
|
+
* with the trip (tripShellCaptureGuard), so each guarded read carries its own
|
|
963
|
+
* fix; the theme reads (cookie-store.ts assertThemeReadAllowed) record theirs.
|
|
964
|
+
*/
|
|
965
|
+
const REQUEST_READ_CAPTURE_FIX: string =
|
|
966
|
+
"Read it in a loader without ssr: false and consume it with useLoader under " +
|
|
967
|
+
"loading() or an inline <Suspense> (a live hole). A promise the handler passes " +
|
|
968
|
+
"or pushes does not help: the capture waits for it.";
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* True when `ctx` is the active capture render (assertNotInsideShellCapture
|
|
972
|
+
* above). On true the capture context is flagged with the read (`surface`,
|
|
973
|
+
* e.g. "cookies()", "ctx.theme") and the fix the refusal warning gives, plus
|
|
974
|
+
* the loader body (if any) that made the read, and the caller throws.
|
|
975
|
+
*/
|
|
976
|
+
export function tripShellCaptureGuard(
|
|
977
|
+
ctx: unknown,
|
|
978
|
+
surface: string,
|
|
979
|
+
fix: string,
|
|
980
|
+
): boolean {
|
|
981
|
+
if (
|
|
982
|
+
ctx === null ||
|
|
983
|
+
typeof ctx !== "object" ||
|
|
984
|
+
(ctx as { _shellCaptureRun?: unknown })._shellCaptureRun !== true
|
|
985
|
+
) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
// Record WHICH loader body (if any) made the read, so the refusal warning
|
|
989
|
+
// can name the real source instead of hardcoding a lane (issue #672).
|
|
990
|
+
const flagged = ctx as {
|
|
991
|
+
_shellCaptureGuardTripped?: { surface: string; fix: string };
|
|
992
|
+
_shellCaptureGuardTrippedLoaderId?: string;
|
|
993
|
+
};
|
|
994
|
+
flagged._shellCaptureGuardTripped = { surface, fix };
|
|
995
|
+
flagged._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
|
|
996
|
+
return true;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Read guard for a non-cacheable variable (`createVar({ cache: false })` or a
|
|
1001
|
+
* `ctx.set(..., { cache: false })` write). Callers check isNonCacheable() first
|
|
1002
|
+
* so ordinary reads never reach the scope lookups.
|
|
1003
|
+
*
|
|
1004
|
+
* Throws during a PPR shell capture (`requestCtx` is the ambient request
|
|
1005
|
+
* context; assertNotInsideShellCapture): the shell is shared per host+URL.
|
|
1006
|
+
*
|
|
1007
|
+
* Throws inside a "use cache" body: the key does not include the value, so the
|
|
1008
|
+
* first caller's value would be stored and served to later callers (#925). A
|
|
1009
|
+
* loader body entered INSIDE the cached function is exempt, the same baked-copy
|
|
1010
|
+
* trade isInsideCacheScope() makes for cache(). A cached function called FROM a
|
|
1011
|
+
* loader is not: the loader re-runs, the cached body does not. The body scope
|
|
1012
|
+
* records the exec scope it was entered in, so the exemption holds only while
|
|
1013
|
+
* that is still the innermost one.
|
|
1014
|
+
*/
|
|
1015
|
+
export function assertNonCacheableReadAllowed(
|
|
1016
|
+
keyOrVar: string | ContextVar<unknown>,
|
|
1017
|
+
requestCtx?: unknown,
|
|
1018
|
+
): void {
|
|
1019
|
+
// A shell capture bakes what it reads into a page every visitor gets.
|
|
1020
|
+
assertNotInsideShellCapture(requestCtx, "ctx.get");
|
|
1021
|
+
const execScope = getCacheExecScope();
|
|
1022
|
+
const variable = ` for a non-cacheable variable${typeof keyOrVar === "string" ? ` "${keyOrVar}"` : ""}`;
|
|
1023
|
+
if (
|
|
1024
|
+
execScope !== undefined &&
|
|
1025
|
+
loaderBodyScopeALS.getStore()?.execScope !== execScope
|
|
1026
|
+
) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`ctx.get()${variable} cannot be called inside a "use cache" function. ` +
|
|
1029
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
1030
|
+
`and the cache key does not include its value, so the first caller's value ` +
|
|
1031
|
+
`would be served to later callers. Read it before calling the cached function ` +
|
|
1032
|
+
`and pass the value in as an argument so it becomes part of the cache key.`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
if (isInsideCacheScope()) {
|
|
1036
|
+
throw new Error(
|
|
1037
|
+
`ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
|
|
1038
|
+
`The variable was created with { cache: false } or set with { cache: false }, ` +
|
|
1039
|
+
`and its value would be stale on cache hit. Move the read outside the cached scope.`,
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
recordLoaderIdentityRead(`ctx.get()${variable}`);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** How the identity error words a read: a function call or a property read. */
|
|
1046
|
+
export type LoaderIdentityReadVerb = "called" | "read";
|
|
1047
|
+
|
|
1048
|
+
/** A request-identity read a loader execution made (recordLoaderIdentityRead). */
|
|
1049
|
+
export interface LoaderIdentityRead {
|
|
1050
|
+
/**
|
|
1051
|
+
* The read as the error names it: "cookies()", "headers()",
|
|
1052
|
+
* `ctx.get() for a non-cacheable variable "x"`, "ctx.theme" or
|
|
1053
|
+
* "getRequestContext().theme".
|
|
1054
|
+
*/
|
|
1055
|
+
surface: string;
|
|
1056
|
+
/** "called" for a function, "read" for a property (the theme getters). */
|
|
1057
|
+
verb: LoaderIdentityReadVerb;
|
|
1058
|
+
/** The loader body that made the read. */
|
|
1059
|
+
bodyId: string | undefined;
|
|
1060
|
+
/**
|
|
1061
|
+
* The cached loader whose value carries a read another loader made
|
|
1062
|
+
* (loader-cache.ts identity mark): its readers read it through `via`.
|
|
1063
|
+
*/
|
|
1064
|
+
via?: string;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Where cache-tag.ts installs its recorder at module init (same key there):
|
|
1069
|
+
* it owns the per-execution recorded sets, and importing it here would be a
|
|
1070
|
+
* cycle.
|
|
1071
|
+
*/
|
|
1072
|
+
const IDENTITY_READ_RECORDER_KEY = Symbol.for(
|
|
1073
|
+
"rangojs-router:identity-read-recorder",
|
|
1074
|
+
);
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Record a request-identity read (cookies(), headers(), a non-cacheable
|
|
1078
|
+
* ctx.get()) on the current loader execution (#972).
|
|
1079
|
+
*
|
|
1080
|
+
* isInsideCacheScope() exempts loader bodies because a route cache() never
|
|
1081
|
+
* stores their values. A loader bound with its own cache() does: with no key()
|
|
1082
|
+
* and no store keyGenerator its entry is keyed by loader, host, path and
|
|
1083
|
+
* params only, and an enclosing route cache() key does not partition it
|
|
1084
|
+
* (#974). A cookies() read there stored the first visitor's session and served
|
|
1085
|
+
* it to everyone for the TTL. The read is recorded on the execution, not
|
|
1086
|
+
* thrown at the call: a reader that starts the loader before its binding does
|
|
1087
|
+
* runs it outside any fill, and the binding's MISS then reuses that run. The
|
|
1088
|
+
* fill checks what its execution recorded, through the same links as its tags
|
|
1089
|
+
* (cache-tag.ts recordedIdentityRead), so both orders fail the same way.
|
|
1090
|
+
* Response directives do not record: a key cannot make a skipped body's side
|
|
1091
|
+
* effect reach a HIT.
|
|
1092
|
+
*/
|
|
1093
|
+
export function recordLoaderIdentityRead(
|
|
1094
|
+
surface: string,
|
|
1095
|
+
verb: LoaderIdentityReadVerb = "called",
|
|
1096
|
+
): void {
|
|
1097
|
+
const recorder = (globalThis as Record<symbol, unknown>)[
|
|
1098
|
+
IDENTITY_READ_RECORDER_KEY
|
|
1099
|
+
] as ((surface: string, verb: LoaderIdentityReadVerb) => void) | undefined;
|
|
1100
|
+
recorder?.(surface, verb);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
/** The error a loader cache() fill with no declared key fails with (#972). */
|
|
1104
|
+
export function loaderCacheIdentityError(
|
|
1105
|
+
read: LoaderIdentityRead,
|
|
1106
|
+
cachedLoaderId: string,
|
|
1107
|
+
): Error {
|
|
1108
|
+
const filling = `while filling its own cache() entry with no key()`;
|
|
1109
|
+
const where =
|
|
1110
|
+
read.via !== undefined && read.via !== cachedLoaderId
|
|
1111
|
+
? `inside loader "${read.bodyId}", which loader "${cachedLoaderId}" reads through another loader ("${read.via}") ${filling}`
|
|
1112
|
+
: read.bodyId !== undefined && read.bodyId !== cachedLoaderId
|
|
1113
|
+
? `inside loader "${read.bodyId}", which loader "${cachedLoaderId}" reads ${filling}`
|
|
1114
|
+
: `inside loader "${cachedLoaderId}", whose own cache() has no key()`;
|
|
1115
|
+
return new Error(
|
|
1116
|
+
`${read.surface} cannot be ${read.verb} ${where}. ` +
|
|
1117
|
+
`The entry is keyed by loader, host, path and params only, so it is ` +
|
|
1118
|
+
`shared across users: request-scoped data (cookies, headers, ` +
|
|
1119
|
+
`non-cacheable variables) read here would be stored from one request ` +
|
|
1120
|
+
`and served to everyone. Add a key that includes the value, or drop ` +
|
|
1121
|
+
`the loader's cache():\n\n` +
|
|
1122
|
+
` cache({ ttl: 60, key: (ctx) => \`session:\${cookies().get("session")?.value}\` })`,
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
872
1126
|
/**
|
|
873
1127
|
* Check if the current execution is inside a DSL loader scope
|
|
874
1128
|
* (wrapped by runInsideLoaderScope). Used by rendered() barrier
|
|
@@ -897,7 +1151,7 @@ export function latchCachedHeaderScope(
|
|
|
897
1151
|
/** True inside ANY loader execution — DSL loader scope or a loader body
|
|
898
1152
|
* (however invoked). The "loaders always re-run fresh" exemptions key off
|
|
899
1153
|
* this. */
|
|
900
|
-
function isInsideAnyLoaderScope(): boolean {
|
|
1154
|
+
export function isInsideAnyLoaderScope(): boolean {
|
|
901
1155
|
return (
|
|
902
1156
|
loaderScopeALS.getStore()?.active === true ||
|
|
903
1157
|
loaderBodyScopeALS.getStore()?.active === true
|
|
@@ -1050,15 +1304,29 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
|
1050
1304
|
export function runInsideLoaderBodyScope<T>(
|
|
1051
1305
|
fn: () => T,
|
|
1052
1306
|
loaderId?: string,
|
|
1053
|
-
|
|
1307
|
+
tags?: Set<string>,
|
|
1054
1308
|
): T {
|
|
1055
|
-
return loaderBodyScopeALS.run(
|
|
1309
|
+
return loaderBodyScopeALS.run(
|
|
1310
|
+
{
|
|
1311
|
+
active: true,
|
|
1312
|
+
loaderId,
|
|
1313
|
+
parent: loaderBodyScopeALS.getStore(),
|
|
1314
|
+
execScope: getCacheExecScope(),
|
|
1315
|
+
tags,
|
|
1316
|
+
},
|
|
1317
|
+
fn,
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/** The innermost loader body's execution tag set (runInsideLoaderBodyScope). */
|
|
1322
|
+
export function getLoaderBodyTags(): Set<string> | undefined {
|
|
1323
|
+
return loaderBodyScopeALS.getStore()?.tags;
|
|
1056
1324
|
}
|
|
1057
1325
|
|
|
1058
1326
|
/**
|
|
1059
1327
|
* The $$id of the loader whose body is currently executing, or undefined
|
|
1060
1328
|
* outside any loader body. Used by the shell-capture identity guard
|
|
1061
|
-
* (
|
|
1329
|
+
* (assertNotInsideShellCapture) so its refusal warning can name the loader that read
|
|
1062
1330
|
* cookies()/headers() instead of blaming a lane it cannot see — the old
|
|
1063
1331
|
* hardcoded "bake-lane loader" text misled a live-lane debugging session
|
|
1064
1332
|
* (issue #672, secondary).
|
|
@@ -1068,16 +1336,16 @@ export function getCurrentLoaderBodyId(): string | undefined {
|
|
|
1068
1336
|
}
|
|
1069
1337
|
|
|
1070
1338
|
/**
|
|
1071
|
-
* True while a
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
* identity guard (cookie-store.ts) permits cookies()/headers() here, exactly
|
|
1076
|
-
* like the cache-purity guards do. DSL segment loaders (live lane masked at
|
|
1077
|
-
* capture, bake lane guarded) never set the flag.
|
|
1339
|
+
* True while `loaderId`'s body, or a loader it awaits via ctx.use at any
|
|
1340
|
+
* depth, is executing. The loader-level cache records these pushes
|
|
1341
|
+
* (loader-cache.ts): a HIT skips the cached body, so a dep that no other
|
|
1342
|
+
* reader runs in that request only reaches the page through the replay.
|
|
1078
1343
|
*/
|
|
1079
|
-
export function
|
|
1080
|
-
|
|
1344
|
+
export function isInsideLoaderBody(loaderId: string): boolean {
|
|
1345
|
+
for (let s = loaderBodyScopeALS.getStore(); s; s = s.parent) {
|
|
1346
|
+
if (s.loaderId === loaderId) return true;
|
|
1347
|
+
}
|
|
1348
|
+
return false;
|
|
1081
1349
|
}
|
|
1082
1350
|
|
|
1083
1351
|
// Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
|
|
@@ -11,8 +11,9 @@ import type { CookieOptions } from "../router/middleware-types.js";
|
|
|
11
11
|
import { getRequestContext, _getRequestContext } from "./request-context.js";
|
|
12
12
|
import {
|
|
13
13
|
isInsideCacheScope,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
assertNotInsideShellCapture,
|
|
15
|
+
tripShellCaptureGuard,
|
|
16
|
+
recordLoaderIdentityRead,
|
|
16
17
|
} from "./context.js";
|
|
17
18
|
import { isInsideCacheExecScope } from "../cache/cache-exec-scope.js";
|
|
18
19
|
|
|
@@ -112,9 +113,13 @@ type HeadersIterator<T> = IterableIterator<T>;
|
|
|
112
113
|
* (Scar: the previous INSIDE_CACHE_EXEC stamp on the shared RequestContext
|
|
113
114
|
* made exactly that parallel read throw for the cached fetch's whole
|
|
114
115
|
* execution window — see cache-exec-scope.ts.)
|
|
116
|
+
*
|
|
117
|
+
* The loader cache() identity reads (#972) are recorded by the read methods
|
|
118
|
+
* instead (createCookieStore, the headers() view), so a write alone is not one.
|
|
115
119
|
*/
|
|
116
120
|
function assertNotInsideCacheContext(fnName: string): void {
|
|
117
|
-
|
|
121
|
+
const scope = refusingCacheScope();
|
|
122
|
+
if (scope === "use cache") {
|
|
118
123
|
throw new Error(
|
|
119
124
|
`${fnName}() cannot be called inside a "use cache" function. ` +
|
|
120
125
|
`Request-scoped data (cookies, headers) varies per request but is not ` +
|
|
@@ -125,7 +130,7 @@ function assertNotInsideCacheContext(fnName: string): void {
|
|
|
125
130
|
` const data = await getCachedData(locale); // locale is now in the cache key`,
|
|
126
131
|
);
|
|
127
132
|
}
|
|
128
|
-
if (
|
|
133
|
+
if (scope === "cache()") {
|
|
129
134
|
throw new Error(
|
|
130
135
|
`${fnName}() cannot be called inside a cache() boundary. ` +
|
|
131
136
|
`A cache() scope caches everything except loaders, so request-scoped ` +
|
|
@@ -138,72 +143,89 @@ function assertNotInsideCacheContext(fnName: string): void {
|
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
*
|
|
146
|
+
* The cache scope that refuses a request-scoped read here, or undefined. One
|
|
147
|
+
* owner of the predicates and their order (assertNotInsideCacheContext above),
|
|
148
|
+
* so cookies()/headers() and the theme reads refuse in the same places.
|
|
149
|
+
*/
|
|
150
|
+
function refusingCacheScope(): "use cache" | "cache()" | undefined {
|
|
151
|
+
if (isInsideCacheExecScope()) return "use cache";
|
|
152
|
+
if (isInsideCacheScope()) return "cache()";
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The fix for a theme read that a cache() boundary or a ppr capture refuses. */
|
|
157
|
+
const THEME_READ_FIX =
|
|
158
|
+
"On ppr and cache() routes, read the theme with useTheme() in a client " +
|
|
159
|
+
"component, or in a live loader (no ssr: false) with cookies().get(<storageKey>). " +
|
|
160
|
+
"The <html> theme class needs neither: the theme script sets it before paint. " +
|
|
161
|
+
"See the /theme skill (node_modules/@rangojs/router/skills/theme/SKILL.md).";
|
|
162
|
+
|
|
163
|
+
/** The public reads of the visitor's theme cookie that assertThemeReadAllowed guards. */
|
|
164
|
+
export type ThemeReadSurface = "ctx.theme" | "getRequestContext().theme";
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Guard for a read of the visitor's theme: the handler and middleware
|
|
168
|
+
* `ctx.theme` (handler-context.ts, middleware.ts) and `getRequestContext().theme`
|
|
169
|
+
* (request-context.ts). The theme is the visitor's theme cookie, so it is an
|
|
170
|
+
* identity read like cookies(): it refuses in the same scopes, in the same
|
|
171
|
+
* order (refusingCacheScope, then tripShellCaptureGuard in context.ts), with
|
|
172
|
+
* the same exemption (a loader body under cache(); at capture there is none).
|
|
173
|
+
* Before #971 each read was a plain cookie read, so a ppr shell, a cache()
|
|
174
|
+
* entry or a "use cache" entry stored the first visitor's theme and served it
|
|
175
|
+
* to every later visitor.
|
|
164
176
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
* keepClientCache()) stay callable: during capture they are header effects on
|
|
168
|
-
* a discarded response, and on the live HIT path the full pipeline runs so their
|
|
169
|
-
* headers flow to the client normally.
|
|
177
|
+
* A read the guards allow is recorded on the current loader execution, as a
|
|
178
|
+
* cookies() read method records it (#972, recordLoaderIdentityRead).
|
|
170
179
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
180
|
+
* `ctx` is the request context at read time, as cookies() reads it. The
|
|
181
|
+
* router's own payload read (payloadInitialTheme, rsc/full-payload.ts) goes
|
|
182
|
+
* through the unguarded `_readTheme()`.
|
|
174
183
|
*/
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
) {
|
|
181
|
-
if (isInsideHandlerInvokedLoaderBody()) return;
|
|
182
|
-
// Flag the capture context BEFORE throwing: inside an executing bake-lane
|
|
183
|
-
// loader this throw is swallowed by wrapLoaderPromise into per-loader error
|
|
184
|
-
// UI, which would bake silently into the shared shell. The capture checks
|
|
185
|
-
// the flag after the render and refuses (shell-capture.ts). Also record
|
|
186
|
-
// WHICH loader body (if any) made the read, so the refusal warning can
|
|
187
|
-
// name the real source instead of hardcoding a lane — the read may come
|
|
188
|
-
// from a bake-lane loader OR from handler/render code (issue #672).
|
|
189
|
-
(ctx as { _shellCaptureGuardTripped?: string })._shellCaptureGuardTripped =
|
|
190
|
-
fnName;
|
|
191
|
-
(
|
|
192
|
-
ctx as { _shellCaptureGuardTrippedLoaderId?: string }
|
|
193
|
-
)._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
|
|
184
|
+
export function assertThemeReadAllowed(
|
|
185
|
+
ctx: unknown,
|
|
186
|
+
surface: ThemeReadSurface,
|
|
187
|
+
): void {
|
|
188
|
+
const scope = refusingCacheScope();
|
|
189
|
+
if (scope === "use cache") {
|
|
194
190
|
throw new Error(
|
|
195
|
-
`${
|
|
196
|
-
`
|
|
197
|
-
`
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
`${surface} cannot be read inside a "use cache" function. The theme ` +
|
|
192
|
+
`comes from the visitor's cookie and is not in the cache key, so the ` +
|
|
193
|
+
`first caller's theme would be served to later callers. Read it ` +
|
|
194
|
+
`before the cached function and pass it in as an argument.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (scope === "cache()") {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${surface} cannot be read inside a cache() boundary. The theme comes ` +
|
|
200
|
+
`from the visitor's cookie, so the first visitor's theme would be ` +
|
|
201
|
+
`stored in the shared entry and served to everyone. ${THEME_READ_FIX}`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (tripShellCaptureGuard(ctx, surface, THEME_READ_FIX)) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`${surface} cannot be read while capturing a shared shell (ppr shell ` +
|
|
207
|
+
`capture). The captured shell is served to every visitor of this URL, ` +
|
|
208
|
+
`so the capturing visitor's theme would reach everyone. ${THEME_READ_FIX}`,
|
|
202
209
|
);
|
|
203
210
|
}
|
|
211
|
+
// An identity read like a cookies() read method: a loader cache() fill with
|
|
212
|
+
// no key() refuses an execution that made one (#972).
|
|
213
|
+
recordLoaderIdentityRead(surface, "read");
|
|
204
214
|
}
|
|
205
215
|
|
|
206
216
|
const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
|
|
217
|
+
// Reading one records the read (#972), like the cookies() read methods: a
|
|
218
|
+
// headers() view taken outside a loader and read inside one still counts.
|
|
219
|
+
const HEADERS_READ_PROPS = new Set<string | symbol>([
|
|
220
|
+
"get",
|
|
221
|
+
"has",
|
|
222
|
+
"entries",
|
|
223
|
+
"keys",
|
|
224
|
+
"values",
|
|
225
|
+
"forEach",
|
|
226
|
+
"getSetCookie",
|
|
227
|
+
Symbol.iterator,
|
|
228
|
+
]);
|
|
207
229
|
|
|
208
230
|
/**
|
|
209
231
|
* Get the original request headers (read-only).
|
|
@@ -226,6 +248,7 @@ export function headers(): ReadonlyHeaders {
|
|
|
226
248
|
assertNotInsideShellCapture(ctx, "headers");
|
|
227
249
|
return new Proxy(ctx.request.headers, {
|
|
228
250
|
get(target, prop, receiver) {
|
|
251
|
+
if (HEADERS_READ_PROPS.has(prop)) recordLoaderIdentityRead("headers()");
|
|
229
252
|
if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
|
|
230
253
|
return () => {
|
|
231
254
|
throw new Error(
|
|
@@ -306,11 +329,13 @@ function createCookieStore(ctx: {
|
|
|
306
329
|
}): CookieStore {
|
|
307
330
|
return {
|
|
308
331
|
get(name: string): Cookie | undefined {
|
|
332
|
+
recordLoaderIdentityRead("cookies()");
|
|
309
333
|
const value = ctx.cookie(name);
|
|
310
334
|
return value !== undefined ? { name, value } : undefined;
|
|
311
335
|
},
|
|
312
336
|
|
|
313
337
|
getAll(name?: string): Cookie[] {
|
|
338
|
+
recordLoaderIdentityRead("cookies()");
|
|
314
339
|
const all = ctx.cookies();
|
|
315
340
|
if (name !== undefined) {
|
|
316
341
|
const value = all[name];
|
|
@@ -320,6 +345,7 @@ function createCookieStore(ctx: {
|
|
|
320
345
|
},
|
|
321
346
|
|
|
322
347
|
has(name: string): boolean {
|
|
348
|
+
recordLoaderIdentityRead("cookies()");
|
|
323
349
|
return ctx.cookie(name) !== undefined;
|
|
324
350
|
},
|
|
325
351
|
|