@rangojs/router 0.18.0 → 0.19.1
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/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +2 -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 +37 -0
- package/dist/types/cache/cache-key-utils.d.ts +44 -13
- package/dist/types/cache/cache-policy.d.ts +1 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +117 -35
- package/dist/types/cache/cache-tag.d.ts +48 -5
- package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
- package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +40 -8
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-snapshot.d.ts +88 -141
- package/dist/types/cache/tag-invalidation.d.ts +38 -14
- package/dist/types/cache/types.d.ts +68 -61
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
- 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/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/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/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
- package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
- 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 +14 -7
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
- package/dist/types/rsc/shell-capture.d.ts +78 -81
- package/dist/types/rsc/shell-serve.d.ts +19 -48
- package/dist/types/rsc/types.d.ts +2 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +126 -61
- package/dist/types/server/cookie-store.d.ts +30 -18
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +66 -45
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/e2e/index.d.ts +1 -1
- 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-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +7 -6
- package/dist/types/testing/shell-status.d.ts +34 -22
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- 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 +3 -4
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/vite/index.js +962 -97
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +76 -49
- package/skills/caching/SKILL.md +167 -69
- package/skills/client-urls/SKILL.md +1 -1
- package/skills/comparison/references/framework-comparison.md +8 -5
- package/skills/document-cache/SKILL.md +24 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +98 -6
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +52 -21
- package/skills/middleware/SKILL.md +18 -0
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/observability/SKILL.md +6 -5
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +129 -77
- package/skills/prerender/SKILL.md +3 -3
- package/skills/rango/SKILL.md +25 -6
- package/skills/route/SKILL.md +2 -0
- package/skills/testing/SKILL.md +27 -25
- package/skills/testing/cache-prerender.md +9 -9
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +3 -2
- 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 +14 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +8 -4
- package/skills/typesafety/env-and-bindings.md +8 -0
- package/skills/use-cache/SKILL.md +67 -19
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +52 -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 +4 -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 +54 -1
- package/src/cache/cache-key-utils.ts +56 -16
- package/src/cache/cache-policy.ts +9 -4
- package/src/cache/cache-runtime.ts +202 -144
- package/src/cache/cache-scope.ts +304 -112
- package/src/cache/cache-tag.ts +209 -13
- package/src/cache/cf/cf-cache-store.ts +152 -76
- package/src/cache/cf/cf-shell-frame.ts +2 -2
- package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +52 -9
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-snapshot.ts +130 -348
- package/src/cache/tag-invalidation.ts +57 -51
- package/src/cache/types.ts +71 -67
- package/src/cache/vercel/vercel-cache-store.ts +61 -58
- 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 +155 -146
- package/src/route-definition/dsl-helpers.ts +2 -0
- 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 +10 -7
- package/src/router/intercept-resolution.ts +11 -1
- package/src/router/loader-resolution.ts +42 -5
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +62 -34
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +9 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +35 -19
- package/src/router/match-middleware/cache-store.ts +15 -9
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware.ts +2 -5
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +4 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +0 -4
- package/src/router/segment-resolution/loader-cache.ts +152 -26
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +15 -78
- package/src/router/segment-resolution/mask-nested.ts +47 -22
- package/src/router/segment-resolution/revalidation.ts +0 -3
- 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 +30 -10
- package/src/rsc/handler.ts +12 -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 +7 -30
- package/src/rsc/response-cache-serve.ts +68 -80
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +206 -252
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +22 -20
- package/src/rsc/shell-capture-constants.ts +136 -20
- package/src/rsc/shell-capture.ts +352 -589
- package/src/rsc/shell-serve.ts +73 -109
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +2 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +263 -124
- package/src/server/cookie-store.ts +153 -127
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +134 -87
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -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 +5 -1
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +10 -7
- package/src/testing/shell-status.ts +60 -56
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- 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 +3 -4
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +0 -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 +75 -5
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
package/src/server/context.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServerRouteLocation } from "../types/segments.js";
|
|
1
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import type { ReactNode } from "react";
|
|
3
4
|
import type {
|
|
@@ -15,8 +16,9 @@ import type { DefaultRouteName } from "../types/global-namespace.js";
|
|
|
15
16
|
import type { ContextVar } from "../context-var.js";
|
|
16
17
|
import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.js";
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
endIdentityExempt,
|
|
20
|
+
isInsideCacheExecScope,
|
|
21
|
+
isInsideIdentityExempt,
|
|
20
22
|
} from "../cache/cache-exec-scope.js";
|
|
21
23
|
|
|
22
24
|
// ============================================================================
|
|
@@ -163,20 +165,22 @@ export type InterceptSegmentsState = {
|
|
|
163
165
|
* Context passed to intercept selector functions (when())
|
|
164
166
|
* Contains navigation context to determine if interception should occur.
|
|
165
167
|
*
|
|
168
|
+
* `from` / `to` have the shape transition({ when }) sees, without `state`:
|
|
169
|
+
* history state never reaches the server.
|
|
170
|
+
*
|
|
166
171
|
* Note: when() is evaluated during route matching, BEFORE middleware runs.
|
|
167
172
|
* So ctx.get()/ctx.use() are not available, but env (platform bindings) is.
|
|
168
173
|
*
|
|
169
174
|
* @internal This type is an implementation detail and may change without notice.
|
|
170
175
|
*/
|
|
171
176
|
export type InterceptSelectorContext<TEnv = any> = {
|
|
172
|
-
from:
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
/** Where the navigation comes from: the intercept source while one is open. */
|
|
178
|
+
from: ServerRouteLocation;
|
|
179
|
+
/** The navigation target. */
|
|
180
|
+
to: ServerRouteLocation;
|
|
175
181
|
request: Request; // The HTTP request object
|
|
176
182
|
env: TEnv; // Platform bindings (Cloudflare env, etc.)
|
|
177
183
|
segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
|
|
178
|
-
fromRouteName?: DefaultRouteName; // Named route being navigated away from (undefined for unnamed routes)
|
|
179
|
-
toRouteName?: DefaultRouteName; // Named route being navigated to (undefined for unnamed routes)
|
|
180
184
|
};
|
|
181
185
|
|
|
182
186
|
/**
|
|
@@ -546,7 +550,8 @@ export const getContext = (): {
|
|
|
546
550
|
// the header guard's middleware exemption depends on the latch dying
|
|
547
551
|
// with the funnel scope (see assertCachedHeaderWriteAllowed).
|
|
548
552
|
},
|
|
549
|
-
|
|
553
|
+
// A funnel (its cache() scope) never runs exempt (runIdentityExempt).
|
|
554
|
+
() => endIdentityExempt(callback),
|
|
550
555
|
);
|
|
551
556
|
},
|
|
552
557
|
run: <T>(
|
|
@@ -865,8 +870,6 @@ interface LoaderBodyScope {
|
|
|
865
870
|
loaderId?: string;
|
|
866
871
|
/** The body scope this one was entered from (a ctx.use(Loader) chain). */
|
|
867
872
|
parent?: LoaderBodyScope;
|
|
868
|
-
/** The "use cache" scope the body was entered in (assertNonCacheableReadAllowed). */
|
|
869
|
-
execScope?: CacheExecScope;
|
|
870
873
|
/** The tags this execution records (#964, cache-tag.ts "Recorded-tag sets"). */
|
|
871
874
|
tags?: Set<string>;
|
|
872
875
|
}
|
|
@@ -896,84 +899,133 @@ export function isInsideCacheScope(): boolean {
|
|
|
896
899
|
}
|
|
897
900
|
|
|
898
901
|
/**
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
* the
|
|
903
|
-
*
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
902
|
+
* What a refused identity read says (guardIdentityRead): its `verb`, and per
|
|
903
|
+
* refusing scope the text after "<surface> cannot be <verb> ...": why the
|
|
904
|
+
* value must not reach that scope, and what to do instead. `fix.warning` is
|
|
905
|
+
* the fix the capture's refusal warning gives (shell-capture.ts
|
|
906
|
+
* refuseOnCaptureGuard).
|
|
907
|
+
*/
|
|
908
|
+
export interface IdentityReadWording {
|
|
909
|
+
/** "called" for a function (cookies(), ctx.get()), "read" for a property (the theme getters). */
|
|
910
|
+
verb: LoaderIdentityReadVerb;
|
|
911
|
+
fix: {
|
|
912
|
+
useCache: string;
|
|
913
|
+
cacheScope: string;
|
|
914
|
+
capture: string;
|
|
915
|
+
warning: string;
|
|
916
|
+
};
|
|
917
|
+
/**
|
|
918
|
+
* False when the read is recorded later, by the returned view's read
|
|
919
|
+
* methods (cookies(), headers()): a view taken outside a loader and read
|
|
920
|
+
* inside one still counts, and a cookie write alone is not a read.
|
|
921
|
+
*/
|
|
922
|
+
record?: false;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* The one guard every request-identity read goes through: cookies(),
|
|
927
|
+
* headers(), the theme reads (cookie-store.ts readGuardedTheme), a
|
|
928
|
+
* non-cacheable ctx.get() (assertNonCacheableReadAllowed), and the raw reads
|
|
929
|
+
* `ctx.request.headers` and `getRequestContext().cookie()` / `.cookies()`
|
|
930
|
+
* (cookie-store.ts guardRequestHeaders, guardRawCookieRead; #976). Each surface keeps
|
|
931
|
+
* its own wording; the ladder and its exemptions are shared, so the surfaces
|
|
932
|
+
* refuse in exactly the same places:
|
|
922
933
|
*
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
* they
|
|
928
|
-
*
|
|
934
|
+
* 0. A cache's own key(), store keyGenerator, condition() and tags(), and
|
|
935
|
+
* onError, read freely and record nothing (runIdentityExempt,
|
|
936
|
+
* cache/cache-exec-scope.ts): the value picks or labels the entry, or is
|
|
937
|
+
* only observed; it is never rendered. A cached body, loader body or
|
|
938
|
+
* funnel they start is guarded again.
|
|
939
|
+
* 1. A PPR shell capture (`ctx` is the capture's derived context,
|
|
940
|
+
* `_shellCaptureRun`) trips first: the capture context is flagged (so a
|
|
941
|
+
* caught throw still refuses the capture) and the read throws. The shell is
|
|
942
|
+
* shared per host+URL, and every HIT replays what the capture read. There
|
|
943
|
+
* is no loader-body exemption here: a bake-lane loader and a loader a
|
|
944
|
+
* handler awaits both bake.
|
|
945
|
+
* 2. A "use cache" body throws: the key does not include the value, so the
|
|
946
|
+
* first caller's would be stored and served to later callers. That holds
|
|
947
|
+
* for a loader body entered inside the cached function too (`await
|
|
948
|
+
* ctx.use(Loader)` there): its value is part of what the function returns.
|
|
949
|
+
* Before, a non-cacheable ctx.get() there was exempt while cookies() threw,
|
|
950
|
+
* and the entry stored the first request's value.
|
|
951
|
+
* 3. A cache() boundary throws, except inside a loader body
|
|
952
|
+
* (isInsideCacheScope): a route cache() never stores loader values.
|
|
953
|
+
* 4. The read is allowed, and recorded on the current loader execution for a
|
|
954
|
+
* loader cache() fill without key() (#972, recordLoaderIdentityRead)
|
|
955
|
+
* unless `wording.record` defers it to the returned view.
|
|
929
956
|
*
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
* request keeps getting the normal axis-1 render.
|
|
957
|
+
* Outside these scopes every read is allowed, a live loader's included. The
|
|
958
|
+
* response directives (invalidateClientCache(), keepClientCache()) record
|
|
959
|
+
* nothing and are not captured reads: they take refuseInCacheScope alone.
|
|
934
960
|
*/
|
|
935
|
-
export function
|
|
961
|
+
export function guardIdentityRead(
|
|
936
962
|
ctx: unknown,
|
|
937
|
-
|
|
963
|
+
surface: string,
|
|
964
|
+
wording: IdentityReadWording,
|
|
938
965
|
): void {
|
|
939
|
-
if (
|
|
940
|
-
|
|
966
|
+
if (isInsideIdentityExempt()) return;
|
|
967
|
+
const { verb, fix } = wording;
|
|
968
|
+
if (tripShellCaptureGuard(ctx, surface, fix.warning)) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
`${surface} cannot be ${verb} while capturing a shared shell ` +
|
|
971
|
+
`(ppr shell capture). ${fix.capture}`,
|
|
972
|
+
);
|
|
941
973
|
}
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
);
|
|
974
|
+
refuseInCacheScope(surface, wording);
|
|
975
|
+
if (wording.record !== false) recordLoaderIdentityRead(surface, verb);
|
|
957
976
|
}
|
|
958
977
|
|
|
959
978
|
/**
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
* with the trip (tripShellCaptureGuard), so each guarded read carries its own
|
|
963
|
-
* fix; the theme reads (cookie-store.ts assertThemeReadAllowed) record theirs.
|
|
979
|
+
* Steps 2 and 3 of guardIdentityRead: throw when a "use cache" body or a
|
|
980
|
+
* cache() boundary would store what `surface` produces.
|
|
964
981
|
*/
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
982
|
+
export function refuseInCacheScope(
|
|
983
|
+
surface: string,
|
|
984
|
+
{ verb, fix }: IdentityReadWording,
|
|
985
|
+
): void {
|
|
986
|
+
if (isInsideCacheExecScope()) {
|
|
987
|
+
throw new Error(
|
|
988
|
+
`${surface} cannot be ${verb} inside a "use cache" function. ${fix.useCache}`,
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (isInsideCacheScope()) {
|
|
992
|
+
throw new Error(
|
|
993
|
+
`${surface} cannot be ${verb} inside a cache() boundary. ${fix.cacheScope}`,
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
969
997
|
|
|
970
998
|
/**
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
* the loader body (if any) that made the read, and the caller throws.
|
|
999
|
+
* The capture refusal for a request-scoped read (cookies(), headers(), a
|
|
1000
|
+
* { cache: false } variable): the message after the surface, and the fix the
|
|
1001
|
+
* refusal warning gives. `what` names the data that would leak.
|
|
975
1002
|
*/
|
|
976
|
-
export function
|
|
1003
|
+
export function requestReadCaptureFix(
|
|
1004
|
+
what: string,
|
|
1005
|
+
): Pick<IdentityReadWording["fix"], "capture" | "warning"> {
|
|
1006
|
+
return {
|
|
1007
|
+
capture:
|
|
1008
|
+
`The captured shell is served to every user of this URL, so ` +
|
|
1009
|
+
`request-scoped data read here would leak one user's ${what} to ` +
|
|
1010
|
+
`others. Read it inside a loader without ssr: false and consume it ` +
|
|
1011
|
+
`with useLoader, e.g. createLoader(async () => ` +
|
|
1012
|
+
`getUser(cookies().get("session")?.value)). ${PPR_LANE_HINT}`,
|
|
1013
|
+
warning:
|
|
1014
|
+
"Read it in a loader without ssr: false and consume it with useLoader " +
|
|
1015
|
+
"under loading() or an inline <Suspense> (a live hole). A promise the " +
|
|
1016
|
+
"handler passes or pushes does not help: the capture waits for it.",
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* True when `ctx` is the active capture render: the derived request context
|
|
1022
|
+
* shell-capture.ts builds (`_shellCaptureRun`), which only the capture sets,
|
|
1023
|
+
* so the foreground render reads identity normally to serve the real user.
|
|
1024
|
+
* On true the capture context is flagged with the read (`surface`, e.g.
|
|
1025
|
+
* "cookies()", "ctx.theme"), the fix the refusal warning gives, and the loader
|
|
1026
|
+
* body (if any) that made the read, and the caller throws.
|
|
1027
|
+
*/
|
|
1028
|
+
function tripShellCaptureGuard(
|
|
977
1029
|
ctx: unknown,
|
|
978
1030
|
surface: string,
|
|
979
1031
|
fix: string,
|
|
@@ -996,50 +1048,40 @@ export function tripShellCaptureGuard(
|
|
|
996
1048
|
return true;
|
|
997
1049
|
}
|
|
998
1050
|
|
|
1051
|
+
const NON_CACHEABLE_READ: IdentityReadWording = {
|
|
1052
|
+
verb: "called",
|
|
1053
|
+
fix: {
|
|
1054
|
+
useCache:
|
|
1055
|
+
"The variable was created with { cache: false } or set with " +
|
|
1056
|
+
"{ cache: false }, and the cache key does not include its value, so " +
|
|
1057
|
+
"the first caller's value would be served to later callers. Read it " +
|
|
1058
|
+
"before calling the cached function and pass the value in as an " +
|
|
1059
|
+
"argument so it becomes part of the cache key.",
|
|
1060
|
+
cacheScope:
|
|
1061
|
+
"The variable was created with { cache: false } or set with " +
|
|
1062
|
+
"{ cache: false }, and its value would be stale on cache hit. Move the " +
|
|
1063
|
+
"read outside the cached scope.",
|
|
1064
|
+
...requestReadCaptureFix("per-request variables"),
|
|
1065
|
+
},
|
|
1066
|
+
};
|
|
1067
|
+
|
|
999
1068
|
/**
|
|
1000
1069
|
* Read guard for a non-cacheable variable (`createVar({ cache: false })` or a
|
|
1001
|
-
* `ctx.set(..., { cache: false })` write)
|
|
1002
|
-
* so ordinary reads never reach the
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
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.
|
|
1070
|
+
* `ctx.set(..., { cache: false })` write), through guardIdentityRead.
|
|
1071
|
+
* Callers check isNonCacheable() first so ordinary reads never reach the
|
|
1072
|
+
* scope lookups. `requestCtx` is the ambient request context, whose capture
|
|
1073
|
+
* flag the guard reads.
|
|
1014
1074
|
*/
|
|
1015
1075
|
export function assertNonCacheableReadAllowed(
|
|
1016
1076
|
keyOrVar: string | ContextVar<unknown>,
|
|
1017
1077
|
requestCtx?: unknown,
|
|
1018
1078
|
): void {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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}`);
|
|
1079
|
+
const name = typeof keyOrVar === "string" ? ` "${keyOrVar}"` : "";
|
|
1080
|
+
guardIdentityRead(
|
|
1081
|
+
requestCtx,
|
|
1082
|
+
`ctx.get() for a non-cacheable variable${name}`,
|
|
1083
|
+
NON_CACHEABLE_READ,
|
|
1084
|
+
);
|
|
1043
1085
|
}
|
|
1044
1086
|
|
|
1045
1087
|
/** How the identity error words a read: a function call or a property read. */
|
|
@@ -1089,17 +1131,91 @@ const IDENTITY_READ_RECORDER_KEY = Symbol.for(
|
|
|
1089
1131
|
* (cache-tag.ts recordedIdentityRead), so both orders fail the same way.
|
|
1090
1132
|
* Response directives do not record: a key cannot make a skipped body's side
|
|
1091
1133
|
* effect reach a HIT.
|
|
1134
|
+
*
|
|
1135
|
+
* The read also lands on the execution's LoaderRunIdentity (#1011), which
|
|
1136
|
+
* every execution has, from either loader runner (runInsideLoaderRun).
|
|
1092
1137
|
*/
|
|
1093
1138
|
export function recordLoaderIdentityRead(
|
|
1094
1139
|
surface: string,
|
|
1095
1140
|
verb: LoaderIdentityReadVerb = "called",
|
|
1096
1141
|
): void {
|
|
1142
|
+
if (isInsideIdentityExempt()) return;
|
|
1143
|
+
const run = loaderRunALS.getStore();
|
|
1144
|
+
if (run && run.read === undefined) {
|
|
1145
|
+
run.read = { surface, verb, bodyId: run.loaderId };
|
|
1146
|
+
}
|
|
1097
1147
|
const recorder = (globalThis as Record<symbol, unknown>)[
|
|
1098
1148
|
IDENTITY_READ_RECORDER_KEY
|
|
1099
1149
|
] as ((surface: string, verb: LoaderIdentityReadVerb) => void) | undefined;
|
|
1100
1150
|
recorder?.(surface, verb);
|
|
1101
1151
|
}
|
|
1102
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* One loader execution's request-identity record (#1011): the first identity
|
|
1155
|
+
* read its body made, and the executions whose values it read with
|
|
1156
|
+
* ctx.use(Loader). Every execution has one; the recorded-tag sets that carry
|
|
1157
|
+
* the same reads for a loader cache() fill (cache-tag.ts) exist only in a
|
|
1158
|
+
* request that binds one.
|
|
1159
|
+
*
|
|
1160
|
+
* A "use cache" function that reads a loader's value through the request
|
|
1161
|
+
* memo (a handler, the route's loader() binding or a parent layout's started
|
|
1162
|
+
* it first) does not run the body, so guardIdentityRead never sees the read.
|
|
1163
|
+
* Scar: the entry, keyed without the cookie, stored the first visitor's value
|
|
1164
|
+
* and served it to the next. cache-tag.ts readStartedLoaderValue refuses such
|
|
1165
|
+
* a value through this record.
|
|
1166
|
+
*/
|
|
1167
|
+
export interface LoaderRunIdentity {
|
|
1168
|
+
readonly loaderId: string;
|
|
1169
|
+
read?: LoaderIdentityRead;
|
|
1170
|
+
reads?: Set<LoaderRunIdentity>;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Its own scope, not a LoaderBodyScope field: the request-context runner
|
|
1174
|
+
// (request-context.ts createUseFunction) records reads too, but does not
|
|
1175
|
+
// enter a loader body scope, whose cache() exemption it never had.
|
|
1176
|
+
const loaderRunALS: AsyncLocalStorage<LoaderRunIdentity> = ((globalThis as any)[
|
|
1177
|
+
Symbol.for("rangojs-router:loader-run-identity")
|
|
1178
|
+
] ??= new AsyncLocalStorage<LoaderRunIdentity>());
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Run a loader body with `run` as its identity record: both loader runners
|
|
1182
|
+
* (loader-resolution.ts createLoaderExecutor, request-context.ts
|
|
1183
|
+
* createUseFunction) enter it around the loader function.
|
|
1184
|
+
*/
|
|
1185
|
+
export function runInsideLoaderRun<T>(run: LoaderRunIdentity, fn: () => T): T {
|
|
1186
|
+
return loaderRunALS.run(run, fn);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** The innermost loader execution's identity record (runInsideLoaderRun). */
|
|
1190
|
+
export function getLoaderRunIdentity(): LoaderRunIdentity | undefined {
|
|
1191
|
+
return loaderRunALS.getStore();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* The error a "use cache" function's read of a loader value fails with when
|
|
1196
|
+
* the loader's execution, or one whose value it read, made `read` (#1011).
|
|
1197
|
+
* Worded like guardIdentityRead's "use cache" refusal, naming the loaders.
|
|
1198
|
+
*/
|
|
1199
|
+
export function useCacheLoaderIdentityError(
|
|
1200
|
+
read: LoaderIdentityRead,
|
|
1201
|
+
loaderId: string,
|
|
1202
|
+
): Error {
|
|
1203
|
+
const bodyId = read.bodyId ?? loaderId;
|
|
1204
|
+
const source =
|
|
1205
|
+
bodyId === loaderId
|
|
1206
|
+
? `Loader "${loaderId}" ${read.verb} it, and the cached function reads that loader's value`
|
|
1207
|
+
: `Loader "${bodyId}" ${read.verb} it, and the cached function reads loader "${loaderId}", whose value is built from it`;
|
|
1208
|
+
return new Error(
|
|
1209
|
+
`${read.surface} cannot be ${read.verb} inside a "use cache" function. ` +
|
|
1210
|
+
`${source}. The loader ran outside the function (a handler or a ` +
|
|
1211
|
+
`loader() binding started it first), but its value becomes part of ` +
|
|
1212
|
+
`what the function returns, and the cache key does not include it, so ` +
|
|
1213
|
+
`the first caller's value would be stored and served to later callers. ` +
|
|
1214
|
+
`Read the loader before calling the cached function and pass the value ` +
|
|
1215
|
+
`in as an argument so it becomes part of the cache key.`,
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1103
1219
|
/** The error a loader cache() fill with no declared key fails with (#972). */
|
|
1104
1220
|
export function loaderCacheIdentityError(
|
|
1105
1221
|
read: LoaderIdentityRead,
|
|
@@ -1296,25 +1412,30 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
|
1296
1412
|
|
|
1297
1413
|
/**
|
|
1298
1414
|
* Run `fn` inside a loader BODY scope. Marks loader-function execution for the
|
|
1299
|
-
* cache
|
|
1415
|
+
* cache() purity guard only (isInsideCacheScope), WITHOUT affecting
|
|
1300
1416
|
* isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
|
|
1301
1417
|
* and handler-invoked via ctx.use) so request-scoped reads inside a loader
|
|
1302
|
-
* never trip the cache
|
|
1418
|
+
* never trip the cache() guard — a route cache() never stores loader values.
|
|
1419
|
+
* A "use cache" body the loader was entered in still refuses them
|
|
1420
|
+
* (guardIdentityRead); one that reads the value later refuses what the run
|
|
1421
|
+
* recorded (runInsideLoaderRun, cache-tag.ts readStartedLoaderValue).
|
|
1303
1422
|
*/
|
|
1304
1423
|
export function runInsideLoaderBodyScope<T>(
|
|
1305
1424
|
fn: () => T,
|
|
1306
1425
|
loaderId?: string,
|
|
1307
1426
|
tags?: Set<string>,
|
|
1308
1427
|
): T {
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1428
|
+
// A loader a key() starts with ctx.use() runs guarded (runIdentityExempt).
|
|
1429
|
+
return endIdentityExempt(() =>
|
|
1430
|
+
loaderBodyScopeALS.run(
|
|
1431
|
+
{
|
|
1432
|
+
active: true,
|
|
1433
|
+
loaderId,
|
|
1434
|
+
parent: loaderBodyScopeALS.getStore(),
|
|
1435
|
+
tags,
|
|
1436
|
+
},
|
|
1437
|
+
fn,
|
|
1438
|
+
),
|
|
1318
1439
|
);
|
|
1319
1440
|
}
|
|
1320
1441
|
|
|
@@ -1326,7 +1447,7 @@ export function getLoaderBodyTags(): Set<string> | undefined {
|
|
|
1326
1447
|
/**
|
|
1327
1448
|
* The $$id of the loader whose body is currently executing, or undefined
|
|
1328
1449
|
* outside any loader body. Used by the shell-capture identity guard
|
|
1329
|
-
* (
|
|
1450
|
+
* (guardIdentityRead) so its refusal warning can name the loader that read
|
|
1330
1451
|
* cookies()/headers() instead of blaming a lane it cannot see — the old
|
|
1331
1452
|
* hardcoded "bake-lane loader" text misled a live-lane debugging session
|
|
1332
1453
|
* (issue #672, secondary).
|
|
@@ -1335,6 +1456,24 @@ export function getCurrentLoaderBodyId(): string | undefined {
|
|
|
1335
1456
|
return loaderBodyScopeALS.getStore()?.loaderId;
|
|
1336
1457
|
}
|
|
1337
1458
|
|
|
1459
|
+
/**
|
|
1460
|
+
* The innermost loader body running here, or around it through the
|
|
1461
|
+
* ctx.use(Loader) chain, whose id `match` accepts (HandleStore.push, the
|
|
1462
|
+
* shell capture's push funnel). A body `stop` accepts, and `match` does not,
|
|
1463
|
+
* ends the walk: nothing around it is returned.
|
|
1464
|
+
*/
|
|
1465
|
+
export function findEnclosingLoaderBody(
|
|
1466
|
+
match: (loaderId: string) => boolean,
|
|
1467
|
+
stop?: (loaderId: string) => boolean,
|
|
1468
|
+
): string | undefined {
|
|
1469
|
+
for (let s = loaderBodyScopeALS.getStore(); s; s = s.parent) {
|
|
1470
|
+
if (s.loaderId === undefined) continue;
|
|
1471
|
+
if (match(s.loaderId)) return s.loaderId;
|
|
1472
|
+
if (stop?.(s.loaderId)) return undefined;
|
|
1473
|
+
}
|
|
1474
|
+
return undefined;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1338
1477
|
/**
|
|
1339
1478
|
* True while `loaderId`'s body, or a loader it awaits via ctx.use at any
|
|
1340
1479
|
* depth, is executing. The loader-level cache records these pushes
|