@rangojs/router 0.18.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.
Files changed (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -8,14 +8,20 @@
8
8
  */
9
9
 
10
10
  import type { CookieOptions } from "../router/middleware-types.js";
11
- import { getRequestContext, _getRequestContext } from "./request-context.js";
11
+ import type { Theme } from "../theme/types.js";
12
12
  import {
13
- isInsideCacheScope,
14
- assertNotInsideShellCapture,
15
- tripShellCaptureGuard,
13
+ getRequestContext,
14
+ _getRequestContext,
15
+ type RequestContext,
16
+ } from "./request-context.js";
17
+ import {
18
+ guardIdentityRead,
16
19
  recordLoaderIdentityRead,
20
+ refuseInCacheScope,
21
+ requestReadCaptureFix,
22
+ type IdentityReadWording,
17
23
  } from "./context.js";
18
- import { isInsideCacheExecScope } from "../cache/cache-exec-scope.js";
24
+ import { requestHeaders, shadowRequestHeaders } from "./request-headers.js";
19
25
 
20
26
  /**
21
27
  * A single cookie entry returned by get() and getAll().
@@ -66,8 +72,7 @@ export interface CookieStore {
66
72
  */
67
73
  export function cookies(): CookieStore {
68
74
  const ctx = getRequestContext();
69
- assertNotInsideCacheContext("cookies");
70
- assertNotInsideShellCapture(ctx, "cookies");
75
+ guardIdentityRead(ctx, "cookies()", COOKIES_READ);
71
76
  return createCookieStore(ctx);
72
77
  }
73
78
 
@@ -91,66 +96,98 @@ export interface ReadonlyHeaders {
91
96
  type HeadersIterator<T> = IterableIterator<T>;
92
97
 
93
98
  /**
94
- * Throw if called inside a cache boundary — either a "use cache" function
95
- * (`isInsideCacheExecScope()` — the AsyncLocalStorage scope the cache runtime
96
- * enters around the cached body) or a `cache()` DSL boundary
97
- * (`isInsideCacheScope()` — the render-store flag set while resolving a
98
- * `type: "cache"` route entry).
99
- *
100
- * Reading request-scoped data (cookies, headers) inside a cached scope
101
- * produces per-request values that are NOT reflected in the cache key, so
102
- * they would be frozen into the shared cache entry and served to the wrong
103
- * users. This is the same hazard for both scopes: a `cache()` boundary caches
104
- * everything except loaders (it is the document-level "PPR shell"), so a read
105
- * here is baked into the shell exactly like a `"use cache"` return value is
106
- * baked into its cache entry.
107
- *
108
- * Both checks are scoped so loaders stay exempt — loaders are the dynamic
109
- * "holes" of a cached document and always run fresh. `isInsideCacheScope()`
110
- * returns false inside loaders by construction; `isInsideCacheExecScope()`
111
- * follows the cached body's own async chain, so a loader running in PARALLEL
112
- * with a slow "use cache" fetch on the same request reads cookies() freely.
113
- * (Scar: the previous INSIDE_CACHE_EXEC stamp on the shared RequestContext
114
- * made exactly that parallel read throw for the cached fetch's whole
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.
99
+ * The cache-scope wording of cookies(), headers() and the response
100
+ * directives. Request-scoped data read in a cached scope is not reflected in
101
+ * its key, so it would be frozen into the shared entry and served to the
102
+ * wrong users; a "use cache" scope follows the cached body's own async chain
103
+ * (cache-exec-scope.ts), so a loader running in PARALLEL with a slow cached
104
+ * fetch on the same request reads cookies() freely. (Scar: an
105
+ * INSIDE_CACHE_EXEC stamp on the shared RequestContext made that parallel
106
+ * read throw for the fetch's whole execution window.)
119
107
  */
120
- function assertNotInsideCacheContext(fnName: string): void {
121
- const scope = refusingCacheScope();
122
- if (scope === "use cache") {
123
- throw new Error(
124
- `${fnName}() cannot be called inside a "use cache" function. ` +
125
- `Request-scoped data (cookies, headers) varies per request but is not ` +
126
- `reflected in the cache key, so cached results would be served to the ` +
127
- `wrong users. Extract the value before the cached function and pass it ` +
128
- `as an argument:\n\n` +
129
- ` const locale = cookies().get("locale")?.value ?? "en";\n` +
130
- ` const data = await getCachedData(locale); // locale is now in the cache key`,
131
- );
132
- }
133
- if (scope === "cache()") {
134
- throw new Error(
135
- `${fnName}() cannot be called inside a cache() boundary. ` +
136
- `A cache() scope caches everything except loaders, so request-scoped ` +
137
- `data (cookies, headers) read here would be frozen into the shared ` +
138
- `cached shell and served to other users. Read it inside a loader ` +
139
- `instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
140
- ` loader("user", () => getUser(cookies().get("session")?.value));`,
141
- );
142
- }
108
+ const REQUEST_READ_CACHE_FIX: Pick<
109
+ IdentityReadWording["fix"],
110
+ "useCache" | "cacheScope"
111
+ > = {
112
+ useCache:
113
+ "Request-scoped data (cookies, headers) varies per request but is not " +
114
+ "reflected in the cache key, so cached results would be served to the " +
115
+ "wrong users. Extract the value before the cached function and pass it " +
116
+ "as an argument:\n\n" +
117
+ ' const locale = cookies().get("locale")?.value ?? "en";\n' +
118
+ " const data = await getCachedData(locale); // locale is now in the cache key",
119
+ cacheScope:
120
+ "A cache() scope caches everything except loaders, so request-scoped " +
121
+ "data (cookies, headers) read here would be frozen into the shared " +
122
+ "cached shell and served to other users. Read it inside a loader " +
123
+ "instead — loaders always run fresh on every request, even on a cache hit:\n\n" +
124
+ ' loader("user", () => getUser(cookies().get("session")?.value));',
125
+ };
126
+
127
+ /**
128
+ * cookies() and headers() record the read on the returned view's read
129
+ * methods (createCookieStore, the headers() proxy), so a write alone is not
130
+ * one (#972).
131
+ */
132
+ function requestReadWording(what: string): IdentityReadWording {
133
+ return {
134
+ verb: "called",
135
+ fix: { ...REQUEST_READ_CACHE_FIX, ...requestReadCaptureFix(what) },
136
+ record: false,
137
+ };
138
+ }
139
+
140
+ const COOKIES_READ: IdentityReadWording = requestReadWording("cookies");
141
+ const HEADERS_READ: IdentityReadWording = requestReadWording("headers");
142
+
143
+ /**
144
+ * The raw reads (#976): `ctx.request.headers` and `getRequestContext()
145
+ * .cookie()` / `.cookies()`. Same wording as cookies()/headers(), but each
146
+ * returns the data itself, so the read is recorded at the access.
147
+ */
148
+ const REQUEST_HEADERS_READ: IdentityReadWording = {
149
+ verb: "read",
150
+ fix: HEADERS_READ.fix,
151
+ };
152
+ const RAW_COOKIES_READ: IdentityReadWording = {
153
+ verb: "called",
154
+ fix: COOKIES_READ.fix,
155
+ };
156
+
157
+ /**
158
+ * Run the identity guard for a raw cookie read on `ctx`
159
+ * (request-context.ts `cookie()` / `cookies()`). Before #976 they were plain
160
+ * reads of the Cookie header that no shared scope refused. The guard reads
161
+ * the ambient context, as cookies() does, and falls back to `ctx`.
162
+ */
163
+ export function guardRawCookieRead(
164
+ ctx: RequestContext<any>,
165
+ surface: "getRequestContext().cookie()" | "getRequestContext().cookies()",
166
+ ): void {
167
+ guardIdentityRead(_getRequestContext() ?? ctx, surface, RAW_COOKIES_READ);
168
+ }
169
+
170
+ function guardRequestHeadersRead(): void {
171
+ guardIdentityRead(
172
+ _getRequestContext(),
173
+ "ctx.request.headers",
174
+ REQUEST_HEADERS_READ,
175
+ );
143
176
  }
144
177
 
145
178
  /**
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.
179
+ * Guard `request.headers` like headers() (#976): the request every ctx
180
+ * exposes (request context, handler, middleware and loader `ctx.request`)
181
+ * gets an own getter that runs guardIdentityRead against the ambient request
182
+ * context, so a read inside a ppr capture, a cache() boundary, a "use cache"
183
+ * body or an unkeyed loader cache() fill refuses as cookies() does. A Request
184
+ * argument keys a "use cache" entry by its URL only (cache-runtime.ts), so a
185
+ * header read there baked the first caller's value into the shared entry.
186
+ * The router's own reads go through requestHeaders() (request-headers.ts).
187
+ * Idempotent; returns `request`.
149
188
  */
150
- function refusingCacheScope(): "use cache" | "cache()" | undefined {
151
- if (isInsideCacheExecScope()) return "use cache";
152
- if (isInsideCacheScope()) return "cache()";
153
- return undefined;
189
+ export function guardRequestHeaders(request: Request): Request {
190
+ return shadowRequestHeaders(request, guardRequestHeadersRead);
154
191
  }
155
192
 
156
193
  /** The fix for a theme read that a cache() boundary or a ppr capture refuses. */
@@ -160,57 +197,49 @@ const THEME_READ_FIX =
160
197
  "The <html> theme class needs neither: the theme script sets it before paint. " +
161
198
  "See the /theme skill (node_modules/@rangojs/router/skills/theme/SKILL.md).";
162
199
 
163
- /** The public reads of the visitor's theme cookie that assertThemeReadAllowed guards. */
200
+ /**
201
+ * The theme is the visitor's theme cookie, so a read of it is an identity
202
+ * read like cookies() (guardIdentityRead). Before #971 each read was a plain
203
+ * cookie read, so a ppr shell, a cache() entry or a "use cache" entry stored
204
+ * the first visitor's theme and served it to every later visitor.
205
+ */
206
+ const THEME_READ: IdentityReadWording = {
207
+ verb: "read",
208
+ fix: {
209
+ useCache:
210
+ "The theme comes from the visitor's cookie and is not in the cache " +
211
+ "key, so the first caller's theme would be served to later callers. " +
212
+ "Read it before the cached function and pass it in as an argument.",
213
+ cacheScope:
214
+ "The theme comes from the visitor's cookie, so the first visitor's " +
215
+ "theme would be stored in the shared entry and served to everyone. " +
216
+ THEME_READ_FIX,
217
+ capture:
218
+ "The captured shell is served to every visitor of this URL, so the " +
219
+ `capturing visitor's theme would reach everyone. ${THEME_READ_FIX}`,
220
+ warning: THEME_READ_FIX,
221
+ },
222
+ };
223
+
224
+ /** The public reads of the visitor's theme cookie that readGuardedTheme guards. */
164
225
  export type ThemeReadSurface = "ctx.theme" | "getRequestContext().theme";
165
226
 
166
227
  /**
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.
176
- *
177
- * A read the guards allow is recorded on the current loader execution, as a
178
- * cookies() read method records it (#972, recordLoaderIdentityRead).
179
- *
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()`.
228
+ * `ctx`'s theme through guardIdentityRead: the one body of every public theme
229
+ * read (the handler and middleware `ctx.theme`, handler-context.ts and
230
+ * middleware.ts, and `getRequestContext().theme`, request-context.ts). The
231
+ * guard reads the ambient context, as cookies() does, and falls back to
232
+ * `ctx`; the value is `ctx`'s unguarded `_readTheme()`, which the router's
233
+ * own payload read (payloadInitialTheme, rsc/full-payload.ts) calls directly.
234
+ * Undefined without a theme config.
183
235
  */
184
- export function assertThemeReadAllowed(
185
- ctx: unknown,
236
+ export function readGuardedTheme(
237
+ ctx: RequestContext<any> | undefined,
186
238
  surface: ThemeReadSurface,
187
- ): void {
188
- const scope = refusingCacheScope();
189
- if (scope === "use cache") {
190
- throw new Error(
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}`,
209
- );
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");
239
+ ): Theme | undefined {
240
+ if (!ctx?._themeConfig) return undefined;
241
+ guardIdentityRead(_getRequestContext() ?? ctx, surface, THEME_READ);
242
+ return ctx._readTheme();
214
243
  }
215
244
 
216
245
  const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
@@ -244,9 +273,8 @@ const HEADERS_READ_PROPS = new Set<string | symbol>([
244
273
  */
245
274
  export function headers(): ReadonlyHeaders {
246
275
  const ctx = getRequestContext();
247
- assertNotInsideCacheContext("headers");
248
- assertNotInsideShellCapture(ctx, "headers");
249
- return new Proxy(ctx.request.headers, {
276
+ guardIdentityRead(ctx, "headers()", HEADERS_READ);
277
+ return new Proxy(requestHeaders(ctx.request), {
250
278
  get(target, prop, receiver) {
251
279
  if (HEADERS_READ_PROPS.has(prop)) recordLoaderIdentityRead("headers()");
252
280
  if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
@@ -285,7 +313,7 @@ export function invalidateClientCache(): void {
285
313
  }
286
314
  return;
287
315
  }
288
- assertNotInsideCacheContext("invalidateClientCache");
316
+ refuseInCacheScope("invalidateClientCache()", COOKIES_READ);
289
317
  ctx._rotateStateCookie();
290
318
  }
291
319
 
@@ -310,33 +338,31 @@ export function keepClientCache(): void {
310
338
  }
311
339
  return;
312
340
  }
313
- assertNotInsideCacheContext("keepClientCache");
341
+ refuseInCacheScope("keepClientCache()", COOKIES_READ);
314
342
  ctx._setKeepCacheDirective();
315
343
  }
316
344
 
317
345
  /**
318
- * Create a CookieStore backed by a RequestContext.
319
- * @internal Shared between cookies() shorthand and context methods.
346
+ * Create a CookieStore backed by a RequestContext. Reads go through the
347
+ * context's unguarded `_readCookie()` / `_readCookies()`: cookies() ran the
348
+ * guard at its call and records on each read method here.
320
349
  */
321
- function createCookieStore(ctx: {
322
- cookie(name: string): string | undefined;
323
- cookies(): Record<string, string>;
324
- setCookie(name: string, value: string, options?: CookieOptions): void;
325
- deleteCookie(
326
- name: string,
327
- options?: Pick<CookieOptions, "domain" | "path">,
328
- ): void;
329
- }): CookieStore {
350
+ function createCookieStore(
351
+ ctx: Pick<
352
+ RequestContext<any>,
353
+ "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie"
354
+ >,
355
+ ): CookieStore {
330
356
  return {
331
357
  get(name: string): Cookie | undefined {
332
358
  recordLoaderIdentityRead("cookies()");
333
- const value = ctx.cookie(name);
359
+ const value = ctx._readCookie(name);
334
360
  return value !== undefined ? { name, value } : undefined;
335
361
  },
336
362
 
337
363
  getAll(name?: string): Cookie[] {
338
364
  recordLoaderIdentityRead("cookies()");
339
- const all = ctx.cookies();
365
+ const all = ctx._readCookies();
340
366
  if (name !== undefined) {
341
367
  const value = all[name];
342
368
  return value !== undefined ? [{ name, value }] : [];
@@ -346,7 +372,7 @@ function createCookieStore(ctx: {
346
372
 
347
373
  has(name: string): boolean {
348
374
  recordLoaderIdentityRead("cookies()");
349
- return ctx.cookie(name) !== undefined;
375
+ return ctx._readCookie(name) !== undefined;
350
376
  },
351
377
 
352
378
  set(name: string, value: string, options?: CookieOptions): void {