@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
@@ -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
- getCacheExecScope,
19
- type CacheExecScope,
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: URL; // Source URL (where user is coming from)
173
- to: URL; // Destination URL (where user is navigating to)
174
- params: Record<string, string>; // Matched route params
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
- callback,
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
- * Throw if called during the ACTIVE background shell-capture render
900
- * (`_shellCaptureRun` true on the derived request context built by
901
- * shell-capture.ts). The captured shell is shared across every user hitting
902
- * the URL, so a request-scoped read here would bake one user's cookies,
903
- * headers, or per-request variables into markup served to others — same
904
- * hazard as the cache scopes above, at the document tier. DSL segment loaders
905
- * need no exemption: the live lane is masked (never executed) during capture,
906
- * and the bake lane is exactly what this guard exists for.
907
- *
908
- * It covers everything the capture waits for, not only synchronous handler
909
- * code: a promise the handler passes or pushes, an async server component,
910
- * and a loader the handler awaits (`await ctx.use(Loader)`) all run under the
911
- * capture context and all bake. The last one used to be exempt (the
912
- * consumption-lane rule, #672/#674): while a HIT re-ran handlers, a slot
913
- * handler's `ctx.use` of an identity loader rendered per visitor on the HIT.
914
- * Now every HIT replays the capture's copy, so the exemption would bake the
915
- * capturing request's identity into every visitor's page. The cache()
916
- * guards above keep it (a cache() HIT is a separate tier).
917
- *
918
- * Keys off `_shellCaptureRun`, NOT the `_shellCapture` descriptor: the descriptor
919
- * is also present during the FOREGROUND render (it means "a capture is wanted"),
920
- * and the foreground must read cookies/headers normally to serve the real user.
921
- * Only the derived capture context sets `_shellCaptureRun`.
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
- * Applies only to the READ surfaces (cookies(), headers(), a { cache: false }
924
- * variable, and the theme reads through tripShellCaptureGuard) whose values
925
- * become markup. Response directives
926
- * (invalidateClientCache(), keepClientCache()) stay callable: during capture
927
- * they are header effects on a discarded response, and on the live HIT path
928
- * the full pipeline runs so their headers flow to the client normally.
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
- * The throw can be caught by the code that made the read, so the guard also
931
- * flags the capture context; the capture refuses on the flag
932
- * (shell-capture.ts refuseOnCaptureGuard), nothing is stored, and every
933
- * request keeps getting the normal axis-1 render.
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 assertNotInsideShellCapture(
961
+ export function guardIdentityRead(
936
962
  ctx: unknown,
937
- fnName: string,
963
+ surface: string,
964
+ wording: IdentityReadWording,
938
965
  ): void {
939
- if (!tripShellCaptureGuard(ctx, `${fnName}()`, REQUEST_READ_CAPTURE_FIX)) {
940
- return;
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
- const what =
943
- fnName === "cookies"
944
- ? "cookies"
945
- : fnName === "headers"
946
- ? "headers"
947
- : "per-request variables";
948
- throw new Error(
949
- `${fnName}() cannot be called while capturing a shared shell ` +
950
- `(ppr shell capture). The captured shell is served to every user ` +
951
- `of this URL, so request-scoped data read here would leak one user's ` +
952
- `${what} to others. Read it ` +
953
- `inside a loader without ssr: false and consume it with useLoader, e.g. ` +
954
- `createLoader(async () => getUser(cookies().get("session")?.value)). ` +
955
- PPR_LANE_HINT,
956
- );
974
+ refuseInCacheScope(surface, wording);
975
+ if (wording.record !== false) recordLoaderIdentityRead(surface, verb);
957
976
  }
958
977
 
959
978
  /**
960
- * The fix the capture refusal warning (shell-capture.ts refuseOnCaptureGuard)
961
- * gives for a cookies()/headers()/{ cache: false } variable read. Recorded
962
- * with the trip (tripShellCaptureGuard), so each guarded read carries its own
963
- * fix; the theme reads (cookie-store.ts assertThemeReadAllowed) record theirs.
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
- const REQUEST_READ_CAPTURE_FIX: string =
966
- "Read it in a loader without ssr: false and consume it with useLoader under " +
967
- "loading() or an inline <Suspense> (a live hole). A promise the handler passes " +
968
- "or pushes does not help: the capture waits for it.";
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
- * True when `ctx` is the active capture render (assertNotInsideShellCapture
972
- * above). On true the capture context is flagged with the read (`surface`,
973
- * e.g. "cookies()", "ctx.theme") and the fix the refusal warning gives, plus
974
- * the loader body (if any) that made the read, and the caller throws.
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 tripShellCaptureGuard(
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). Callers check isNonCacheable() first
1002
- * so ordinary reads never reach the scope lookups.
1003
- *
1004
- * Throws during a PPR shell capture (`requestCtx` is the ambient request
1005
- * context; assertNotInsideShellCapture): the shell is shared per host+URL.
1006
- *
1007
- * Throws inside a "use cache" body: the key does not include the value, so the
1008
- * first caller's value would be stored and served to later callers (#925). A
1009
- * loader body entered INSIDE the cached function is exempt, the same baked-copy
1010
- * trade isInsideCacheScope() makes for cache(). A cached function called FROM a
1011
- * loader is not: the loader re-runs, the cached body does not. The body scope
1012
- * records the exec scope it was entered in, so the exemption holds only while
1013
- * that is still the innermost one.
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
- // A shell capture bakes what it reads into a page every visitor gets.
1020
- assertNotInsideShellCapture(requestCtx, "ctx.get");
1021
- const execScope = getCacheExecScope();
1022
- const variable = ` for a non-cacheable variable${typeof keyOrVar === "string" ? ` "${keyOrVar}"` : ""}`;
1023
- if (
1024
- execScope !== undefined &&
1025
- loaderBodyScopeALS.getStore()?.execScope !== execScope
1026
- ) {
1027
- throw new Error(
1028
- `ctx.get()${variable} cannot be called inside a "use cache" function. ` +
1029
- `The variable was created with { cache: false } or set with { cache: false }, ` +
1030
- `and the cache key does not include its value, so the first caller's value ` +
1031
- `would be served to later callers. Read it before calling the cached function ` +
1032
- `and pass the value in as an argument so it becomes part of the cache key.`,
1033
- );
1034
- }
1035
- if (isInsideCacheScope()) {
1036
- throw new Error(
1037
- `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
1038
- `The variable was created with { cache: false } or set with { cache: false }, ` +
1039
- `and its value would be stale on cache hit. Move the read outside the cached scope.`,
1040
- );
1041
- }
1042
- recordLoaderIdentityRead(`ctx.get()${variable}`);
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. */
@@ -1094,6 +1136,7 @@ export function recordLoaderIdentityRead(
1094
1136
  surface: string,
1095
1137
  verb: LoaderIdentityReadVerb = "called",
1096
1138
  ): void {
1139
+ if (isInsideIdentityExempt()) return;
1097
1140
  const recorder = (globalThis as Record<symbol, unknown>)[
1098
1141
  IDENTITY_READ_RECORDER_KEY
1099
1142
  ] as ((surface: string, verb: LoaderIdentityReadVerb) => void) | undefined;
@@ -1296,25 +1339,29 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
1296
1339
 
1297
1340
  /**
1298
1341
  * Run `fn` inside a loader BODY scope. Marks loader-function execution for the
1299
- * cache-purity guard only (isInsideCacheScope), WITHOUT affecting
1342
+ * cache() purity guard only (isInsideCacheScope), WITHOUT affecting
1300
1343
  * isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
1301
1344
  * and handler-invoked via ctx.use) so request-scoped reads inside a loader
1302
- * never trip the cache-scope guards — loaders always run fresh.
1345
+ * never trip the cache() guard — a route cache() never stores loader values.
1346
+ * A "use cache" body the loader was entered in still refuses them
1347
+ * (guardIdentityRead).
1303
1348
  */
1304
1349
  export function runInsideLoaderBodyScope<T>(
1305
1350
  fn: () => T,
1306
1351
  loaderId?: string,
1307
1352
  tags?: Set<string>,
1308
1353
  ): T {
1309
- return loaderBodyScopeALS.run(
1310
- {
1311
- active: true,
1312
- loaderId,
1313
- parent: loaderBodyScopeALS.getStore(),
1314
- execScope: getCacheExecScope(),
1315
- tags,
1316
- },
1317
- fn,
1354
+ // A loader a key() starts with ctx.use() runs guarded (runIdentityExempt).
1355
+ return endIdentityExempt(() =>
1356
+ loaderBodyScopeALS.run(
1357
+ {
1358
+ active: true,
1359
+ loaderId,
1360
+ parent: loaderBodyScopeALS.getStore(),
1361
+ tags,
1362
+ },
1363
+ fn,
1364
+ ),
1318
1365
  );
1319
1366
  }
1320
1367
 
@@ -1326,7 +1373,7 @@ export function getLoaderBodyTags(): Set<string> | undefined {
1326
1373
  /**
1327
1374
  * The $$id of the loader whose body is currently executing, or undefined
1328
1375
  * outside any loader body. Used by the shell-capture identity guard
1329
- * (assertNotInsideShellCapture) so its refusal warning can name the loader that read
1376
+ * (guardIdentityRead) so its refusal warning can name the loader that read
1330
1377
  * cookies()/headers() instead of blaming a lane it cannot see — the old
1331
1378
  * hardcoded "bake-lane loader" text misled a live-lane debugging session
1332
1379
  * (issue #672, secondary).
@@ -1335,6 +1382,24 @@ export function getCurrentLoaderBodyId(): string | undefined {
1335
1382
  return loaderBodyScopeALS.getStore()?.loaderId;
1336
1383
  }
1337
1384
 
1385
+ /**
1386
+ * The innermost loader body running here, or around it through the
1387
+ * ctx.use(Loader) chain, whose id `match` accepts (HandleStore.push, the
1388
+ * shell capture's push funnel). A body `stop` accepts, and `match` does not,
1389
+ * ends the walk: nothing around it is returned.
1390
+ */
1391
+ export function findEnclosingLoaderBody(
1392
+ match: (loaderId: string) => boolean,
1393
+ stop?: (loaderId: string) => boolean,
1394
+ ): string | undefined {
1395
+ for (let s = loaderBodyScopeALS.getStore(); s; s = s.parent) {
1396
+ if (s.loaderId === undefined) continue;
1397
+ if (match(s.loaderId)) return s.loaderId;
1398
+ if (stop?.(s.loaderId)) return undefined;
1399
+ }
1400
+ return undefined;
1401
+ }
1402
+
1338
1403
  /**
1339
1404
  * True while `loaderId`'s body, or a loader it awaits via ctx.use at any
1340
1405
  * depth, is executing. The loader-level cache records these pushes