@rangojs/router 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. 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
- getCurrentLoaderBodyId,
15
- isInsideHandlerInvokedLoaderBody,
13
+ getRequestContext,
14
+ _getRequestContext,
15
+ type RequestContext,
16
+ } from "./request-context.js";
17
+ import {
18
+ guardIdentityRead,
19
+ recordLoaderIdentityRead,
20
+ refuseInCacheScope,
21
+ requestReadCaptureFix,
22
+ type IdentityReadWording,
16
23
  } from "./context.js";
17
- import { isInsideCacheExecScope } from "../cache/cache-exec-scope.js";
18
- import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.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,120 +96,165 @@ 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.)
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.)
116
107
  */
117
- function assertNotInsideCacheContext(fnName: string): void {
118
- if (isInsideCacheExecScope()) {
119
- throw new Error(
120
- `${fnName}() cannot be called inside a "use cache" function. ` +
121
- `Request-scoped data (cookies, headers) varies per request but is not ` +
122
- `reflected in the cache key, so cached results would be served to the ` +
123
- `wrong users. Extract the value before the cached function and pass it ` +
124
- `as an argument:\n\n` +
125
- ` const locale = cookies().get("locale")?.value ?? "en";\n` +
126
- ` const data = await getCachedData(locale); // locale is now in the cache key`,
127
- );
128
- }
129
- if (isInsideCacheScope()) {
130
- throw new Error(
131
- `${fnName}() cannot be called inside a cache() boundary. ` +
132
- `A cache() scope caches everything except loaders, so request-scoped ` +
133
- `data (cookies, headers) read here would be frozen into the shared ` +
134
- `cached shell and served to other users. Read it inside a loader ` +
135
- `instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
136
- ` loader("user", () => getUser(cookies().get("session")?.value));`,
137
- );
138
- }
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
+ };
139
138
  }
140
139
 
140
+ const COOKIES_READ: IdentityReadWording = requestReadWording("cookies");
141
+ const HEADERS_READ: IdentityReadWording = requestReadWording("headers");
142
+
141
143
  /**
142
- * Throw if called during the ACTIVE background shell-capture render
143
- * (`_shellCaptureRun` true on the derived request context built by
144
- * shell-capture.ts). The captured shell prelude is shared across every user
145
- * hitting the URL, so a request-scoped read here would bake one user's
146
- * cookies/headers into markup served to others — same hazard as the cache
147
- * scopes above, at the document tier. DSL segment loaders need no exemption:
148
- * the live lane is masked (never executed) during capture, and the bake lane
149
- * is exactly what this guard exists for.
150
- *
151
- * HANDLER-INVOKED loader bodies (`await ctx.use(Loader)` from a handler) are
152
- * EXEMPT — the consumption-lane rule: handler consumption yields a BAKED
153
- * shared copy in every artifact tier, and the cache-purity guards above
154
- * already permit identity reads there (cache()/"use cache" bake the same
155
- * reads today). Guarding only the PPR tier made the same code legal under
156
- * cache() but capture-refusing under ppr (issue #672 / #674). The trade is
157
- * documented: an identity read in a handler-consumed loader bakes the CAPTURE
158
- * request's value into the shared shell; client-side consumption (useLoader)
159
- * is the live lane.
160
- *
161
- * Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
162
- * is also present during the FOREGROUND render (it means "a capture is wanted"),
163
- * and the foreground must read cookies/headers normally to serve the real user.
164
- * Only the derived capture context sets `_shellCaptureRun`.
165
- *
166
- * Applies only to the READ surfaces (cookies(), headers()) whose values
167
- * become markup. Response directives (invalidateClientCache(),
168
- * keepClientCache()) stay callable: during capture they are header effects on
169
- * a discarded response, and on the live HIT path the full pipeline runs so their
170
- * headers flow to the client normally.
171
- *
172
- * The throw makes such a route PPR-ineligible by construction: the capture
173
- * render errors, nothing is stored, and every request keeps getting the
174
- * normal axis-1 render.
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.
175
147
  */
176
- function assertNotInsideShellCapture(ctx: unknown, fnName: string): void {
177
- if (
178
- ctx !== null &&
179
- typeof ctx === "object" &&
180
- (ctx as { _shellCaptureRun?: unknown })._shellCaptureRun === true
181
- ) {
182
- if (isInsideHandlerInvokedLoaderBody()) return;
183
- // Flag the capture context BEFORE throwing: inside an executing bake-lane
184
- // loader this throw is swallowed by wrapLoaderPromise into per-loader error
185
- // UI, which would bake silently into the shared shell. The capture checks
186
- // the flag after the render and refuses (shell-capture.ts). Also record
187
- // WHICH loader body (if any) made the read, so the refusal warning can
188
- // name the real source instead of hardcoding a lane — the read may come
189
- // from a bake-lane loader OR from handler/render code (issue #672).
190
- (ctx as { _shellCaptureGuardTripped?: string })._shellCaptureGuardTripped =
191
- fnName;
192
- (
193
- ctx as { _shellCaptureGuardTrippedLoaderId?: string }
194
- )._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
195
- throw new Error(
196
- `${fnName}() cannot be called while capturing a shared shell ` +
197
- `(ppr shell capture). The captured shell is served to every user ` +
198
- `of this URL, so request-scoped data read here would leak one user's ` +
199
- `${fnName === "cookies" ? "cookies" : "headers"} to others. Read it ` +
200
- `inside a loader without ssr: false and consume it with useLoader, e.g. ` +
201
- `createLoader(async () => getUser(cookies().get("session")?.value)). ` +
202
- PPR_LANE_HINT,
203
- );
204
- }
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
+ );
176
+ }
177
+
178
+ /**
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`.
188
+ */
189
+ export function guardRequestHeaders(request: Request): Request {
190
+ return shadowRequestHeaders(request, guardRequestHeadersRead);
191
+ }
192
+
193
+ /** The fix for a theme read that a cache() boundary or a ppr capture refuses. */
194
+ const THEME_READ_FIX =
195
+ "On ppr and cache() routes, read the theme with useTheme() in a client " +
196
+ "component, or in a live loader (no ssr: false) with cookies().get(<storageKey>). " +
197
+ "The <html> theme class needs neither: the theme script sets it before paint. " +
198
+ "See the /theme skill (node_modules/@rangojs/router/skills/theme/SKILL.md).";
199
+
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. */
225
+ export type ThemeReadSurface = "ctx.theme" | "getRequestContext().theme";
226
+
227
+ /**
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.
235
+ */
236
+ export function readGuardedTheme(
237
+ ctx: RequestContext<any> | undefined,
238
+ surface: ThemeReadSurface,
239
+ ): Theme | undefined {
240
+ if (!ctx?._themeConfig) return undefined;
241
+ guardIdentityRead(_getRequestContext() ?? ctx, surface, THEME_READ);
242
+ return ctx._readTheme();
205
243
  }
206
244
 
207
245
  const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
246
+ // Reading one records the read (#972), like the cookies() read methods: a
247
+ // headers() view taken outside a loader and read inside one still counts.
248
+ const HEADERS_READ_PROPS = new Set<string | symbol>([
249
+ "get",
250
+ "has",
251
+ "entries",
252
+ "keys",
253
+ "values",
254
+ "forEach",
255
+ "getSetCookie",
256
+ Symbol.iterator,
257
+ ]);
208
258
 
209
259
  /**
210
260
  * Get the original request headers (read-only).
@@ -223,10 +273,10 @@ const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
223
273
  */
224
274
  export function headers(): ReadonlyHeaders {
225
275
  const ctx = getRequestContext();
226
- assertNotInsideCacheContext("headers");
227
- assertNotInsideShellCapture(ctx, "headers");
228
- return new Proxy(ctx.request.headers, {
276
+ guardIdentityRead(ctx, "headers()", HEADERS_READ);
277
+ return new Proxy(requestHeaders(ctx.request), {
229
278
  get(target, prop, receiver) {
279
+ if (HEADERS_READ_PROPS.has(prop)) recordLoaderIdentityRead("headers()");
230
280
  if (typeof prop === "string" && HEADERS_MUTATION_METHODS.has(prop)) {
231
281
  return () => {
232
282
  throw new Error(
@@ -263,7 +313,7 @@ export function invalidateClientCache(): void {
263
313
  }
264
314
  return;
265
315
  }
266
- assertNotInsideCacheContext("invalidateClientCache");
316
+ refuseInCacheScope("invalidateClientCache()", COOKIES_READ);
267
317
  ctx._rotateStateCookie();
268
318
  }
269
319
 
@@ -288,31 +338,31 @@ export function keepClientCache(): void {
288
338
  }
289
339
  return;
290
340
  }
291
- assertNotInsideCacheContext("keepClientCache");
341
+ refuseInCacheScope("keepClientCache()", COOKIES_READ);
292
342
  ctx._setKeepCacheDirective();
293
343
  }
294
344
 
295
345
  /**
296
- * Create a CookieStore backed by a RequestContext.
297
- * @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.
298
349
  */
299
- function createCookieStore(ctx: {
300
- cookie(name: string): string | undefined;
301
- cookies(): Record<string, string>;
302
- setCookie(name: string, value: string, options?: CookieOptions): void;
303
- deleteCookie(
304
- name: string,
305
- options?: Pick<CookieOptions, "domain" | "path">,
306
- ): void;
307
- }): CookieStore {
350
+ function createCookieStore(
351
+ ctx: Pick<
352
+ RequestContext<any>,
353
+ "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie"
354
+ >,
355
+ ): CookieStore {
308
356
  return {
309
357
  get(name: string): Cookie | undefined {
310
- const value = ctx.cookie(name);
358
+ recordLoaderIdentityRead("cookies()");
359
+ const value = ctx._readCookie(name);
311
360
  return value !== undefined ? { name, value } : undefined;
312
361
  },
313
362
 
314
363
  getAll(name?: string): Cookie[] {
315
- const all = ctx.cookies();
364
+ recordLoaderIdentityRead("cookies()");
365
+ const all = ctx._readCookies();
316
366
  if (name !== undefined) {
317
367
  const value = all[name];
318
368
  return value !== undefined ? [{ name, value }] : [];
@@ -321,7 +371,8 @@ function createCookieStore(ctx: {
321
371
  },
322
372
 
323
373
  has(name: string): boolean {
324
- return ctx.cookie(name) !== undefined;
374
+ recordLoaderIdentityRead("cookies()");
375
+ return ctx._readCookie(name) !== undefined;
325
376
  },
326
377
 
327
378
  set(name: string, value: string, options?: CookieOptions): void {