@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
@@ -49,7 +49,7 @@ import { getFetchableLoader } from "./fetchable-loader-store.js";
49
49
  import type { SegmentCacheStore } from "../cache/types.js";
50
50
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
51
51
  import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
52
- import type { TransitionWhenFn } from "../types/segments.js";
52
+ import type { TransitionWhenRecord } from "../router/transition-when.js";
53
53
  import type { ResolvedTracing } from "../router/tracing.js";
54
54
  import type { RenderMode, RenderPhase } from "../router/timeout.js";
55
55
  import {
@@ -65,7 +65,12 @@ import {
65
65
  assertNonCacheableReadAllowed,
66
66
  clearPprHeaderScope,
67
67
  } from "./context.js";
68
- import { assertThemeReadAllowed } from "./cookie-store.js";
68
+ import {
69
+ guardRawCookieRead,
70
+ guardRequestHeaders,
71
+ readGuardedTheme,
72
+ } from "./cookie-store.js";
73
+ import { requestHeaders } from "./request-headers.js";
69
74
  import {
70
75
  createReverseFunction,
71
76
  stripInternalParams,
@@ -76,6 +81,10 @@ import {
76
81
  getSearchSchema,
77
82
  } from "../route-map-builder.js";
78
83
  import { parseSearchParams } from "../search-params.js";
84
+ import {
85
+ executionStart,
86
+ type ExecutionStart,
87
+ } from "../cache/invalidation-order.js";
79
88
  import { invariant } from "../errors.js";
80
89
  import { isAutoGeneratedRouteName } from "../route-name.js";
81
90
 
@@ -146,10 +155,24 @@ export interface RequestContext<
146
155
  /** @internal Request-local PPR opt-out marker set by ctx.dynamic(). */
147
156
  _dynamic?: boolean;
148
157
 
149
- /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
158
+ /**
159
+ * @internal Get a cookie value (effective: request + response mutations).
160
+ * Use cookies().get() instead. Guarded like cookies() (#976).
161
+ */
150
162
  cookie(name: string): string | undefined;
151
- /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
163
+ /**
164
+ * @internal Get all cookies (effective merged view). Use
165
+ * cookies().getAll() instead. Guarded like cookies() (#976).
166
+ */
152
167
  cookies(): Record<string, string>;
168
+ /**
169
+ * @internal `cookie()` without the identity guard: the read behind the
170
+ * cookies() store, which guards at its own call and records per read
171
+ * method. Non-enumerable, like `_readTheme`.
172
+ */
173
+ _readCookie(name: string): string | undefined;
174
+ /** @internal `cookies()` without the identity guard (see `_readCookie`). */
175
+ _readCookies(): Record<string, string>;
153
176
  /** @internal Set a cookie on the response. Use cookies().set() instead. */
154
177
  setCookie(name: string, value: string, options?: CookieOptions): void;
155
178
  /** @internal Delete a cookie. Use cookies().delete() instead. */
@@ -182,19 +205,18 @@ export interface RequestContext<
182
205
  /**
183
206
  * @internal The context createRequestContext() returned. A context derived
184
207
  * from it with Object.create (a PPR HIT tail, a shell capture, a background
185
- * revalidation render) inherits it, so per-request invalidation state
186
- * (tag-invalidation.ts, the stores' masks) is keyed by the request, not by
187
- * whichever derived object called or read (#973).
208
+ * revalidation render) inherits it, so per-request invalidation state (the
209
+ * stores' masks) is keyed by the request, not by whichever derived object
210
+ * called or read (#973).
188
211
  */
189
212
  _requestRoot?: RequestContext<any, any>;
190
213
  /**
191
- * @internal The tags this request invalidated with
192
- * updateTag()/revalidateTag(), each with its position in the invalidation
193
- * order (tag-invalidation.ts currentCacheSeq). Kept on _requestRoot. A
194
- * "use cache" execution that started before one of its own tags was
195
- * invalidated is neither joined nor written (cache-runtime.ts, #973).
214
+ * @internal Where the request started in the isolate's invalidation order
215
+ * (cache/invalidation-order.ts), taken when createRequestContext() ran. The
216
+ * document cache gates its write on it (#977): a middleware ahead of the
217
+ * document cache can read tagged data the document bakes.
196
218
  */
197
- _tagInvalidations?: Map<string, number>;
219
+ _requestStart?: ExecutionStart;
198
220
  /**
199
221
  * @internal The handler has handed the response to the host (rsc/handler.ts
200
222
  * `rango.response`): header and cookie writes after this are lost, as from
@@ -242,16 +264,13 @@ export interface RequestContext<
242
264
  readonly _handleStore: HandleStore;
243
265
 
244
266
  /**
245
- * @internal transition({ when }) predicates for segments matched this request,
246
- * keyed by segment id. Collected during resolution (the function is stripped
247
- * from the serialized segment config), then evaluated post-handler in
248
- * rsc-rendering — outside any cache scope — to drop the transition of any
249
- * segment whose predicate returns false.
267
+ * @internal transition({ when }) client references of the matched entry
268
+ * chain, keyed by segment id. Recorded at match time from the static route
269
+ * definition (router/transition-when.ts) and attached to the payload's
270
+ * segments right before Flight (rsc/attach-transition-when.ts). The server
271
+ * never calls them, and no cache or snapshot stores them.
250
272
  */
251
- _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
252
-
253
- /** @internal PPR transition decisions evaluated before cache lookup/handlers. */
254
- _pprTransitionDecisions?: Map<string, boolean>;
273
+ _transitionWhenRefs?: ReadonlyMap<string, TransitionWhenRecord>;
255
274
 
256
275
  /**
257
276
  * @internal Post-match serve-source truth for the PPR replay reporter.
@@ -279,8 +298,8 @@ export interface RequestContext<
279
298
  * capture task's derived request context (built by shell-capture.ts). This is
280
299
  * the switch every capture-specific behavior reads: loader masking
281
300
  * (loader-mask.ts isShellCaptureActive / fresh.ts emitStreaming) and the
282
- * cookies()/headers() capture guard (cookie-store.ts
283
- * assertNotInsideShellCapture). The foreground render never sets it, so the
301
+ * identity-read capture guard (server/context.ts guardIdentityRead). The
302
+ * foreground render never sets it, so the
284
303
  * served response is byte-identical to axis 1. The capture descriptor itself
285
304
  * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
286
305
  * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
@@ -308,6 +327,16 @@ export interface RequestContext<
308
327
  */
309
328
  _shellCaptureUnrecordedLoaderPush?: true;
310
329
 
330
+ /**
331
+ * @internal The lane of each loader the capture's matched route registers
332
+ * (loader-cache.ts routeLoaderLanes), set by createMatchContextForFull
333
+ * before any loader runs. The capture's push funnel credits a push made
334
+ * under a live-lane loader's body, at any depth, to that loader
335
+ * (shell-capture.ts deriveShellCaptureContext). Own property of the
336
+ * capture's derived context only.
337
+ */
338
+ _shellCaptureLoaderLanes?: ReadonlyMap<string, "live" | "bake">;
339
+
311
340
  /**
312
341
  * @internal Record keys resolved for this request (CacheScope
313
342
  * resolveKeyFrom): by the scope's `key()` (one result) or its store's
@@ -414,7 +443,9 @@ export interface RequestContext<
414
443
  * scope replaces any route-derived cache() scope for the tail's match
415
444
  * (resolveShellImplicitCacheScope), and a lookup that does not hit throws
416
445
  * ShellRecordUnavailableError instead of resolving segments
417
- * (withCacheLookup): a HIT never runs a handler.
446
+ * (withCacheLookup): a HIT never runs a handler. The record's
447
+ * loader-owned handle pushes restore as authoritative
448
+ * (HandleStore.pushRestored): the prelude rendered them.
418
449
  */
419
450
  docTail?: true;
420
451
  /**
@@ -468,8 +499,9 @@ export interface RequestContext<
468
499
 
469
500
  /**
470
501
  * @internal Set by the capture guard (server/context.ts
471
- * tripShellCaptureGuard) when it throws DURING a capture render: the read
472
- * ("cookies()", "headers()", "ctx.get()", "ctx.theme") and the fix the
502
+ * guardIdentityRead) when it throws DURING a capture render: the read
503
+ * ("cookies()", "headers()", `ctx.get() for a non-cacheable variable`,
504
+ * "ctx.theme") and the fix the
473
505
  * refusal warning gives for it. Load-bearing for the
474
506
  * bake lane: a guard throw inside an executing loader is swallowed by
475
507
  * wrapLoaderPromise into per-loader error UI, which would otherwise bake
@@ -611,12 +643,27 @@ export interface RequestContext<
611
643
  /**
612
644
  * @internal The theme cookie (or the default) read with no identity guard.
613
645
  * The guarded reads (`theme` here, the handler and middleware `ctx.theme`)
614
- * call it after assertThemeReadAllowed; the router's own payload read goes
615
- * through payloadInitialTheme (rsc/full-payload.ts). Undefined without
646
+ * call it after guardIdentityRead (readGuardedTheme); the router's own payload read goes
647
+ * through payloadInitialTheme (rsc/full-payload.ts), except the unmatched
648
+ * route's 404 (rsc/handler.ts), which is never stored. Undefined without
616
649
  * theme config.
617
650
  */
618
651
  _readTheme(): Theme | undefined;
619
652
 
653
+ /**
654
+ * @internal Set by the document-cache middleware (cache/document-cache.ts)
655
+ * before it runs a render whose response it may store: the MISS and the
656
+ * stale refresh. payloadInitialTheme reads it (#978).
657
+ */
658
+ _documentCacheRender?: boolean;
659
+
660
+ /**
661
+ * @internal Set by payloadInitialTheme when the document payload carries
662
+ * the visitor's theme and it differs from the no-cookie default. The
663
+ * document cache does not store that response (#978).
664
+ */
665
+ _payloadVisitorTheme?: boolean;
666
+
620
667
  /**
621
668
  * Attach location state entries to the current response.
622
669
  *
@@ -660,20 +707,6 @@ export interface RequestContext<
660
707
  /** @internal Previous route key (from the navigation source), used for revalidation */
661
708
  _prevRouteKey?: string;
662
709
 
663
- /**
664
- * @internal Navigation/action source data the transition({ when }) gate reads
665
- * to build its ShouldRevalidateFn-shaped predicate context. currentUrl/Params
666
- * come from the navigation snapshot (set at match time); action* are stashed
667
- * at the action-bearing gate call sites. All undefined when there is no source
668
- * (initial full load) or no action (plain navigation).
669
- */
670
- _gateCurrentUrl?: URL;
671
- _gateCurrentParams?: Record<string, string>;
672
- _gateActionId?: string;
673
- _gateActionUrl?: URL;
674
- _gateActionResult?: unknown;
675
- _gateFormData?: FormData;
676
-
677
710
  /**
678
711
  * @internal True while the post-action revalidation render is running (set by
679
712
  * revalidateAfterAction). The "use cache" runtime reads this to prefer
@@ -872,11 +905,12 @@ export type PublicRequestContext<
872
905
  RequestContext<TEnv, TParams>,
873
906
  | "cookie"
874
907
  | "cookies"
908
+ | "_readCookie"
909
+ | "_readCookies"
875
910
  | "setCookie"
876
911
  | "deleteCookie"
877
912
  | "_handleStore"
878
- | "_transitionWhen"
879
- | "_pprTransitionDecisions"
913
+ | "_transitionWhenRefs"
880
914
  | "_pprReplayPostMatchReason"
881
915
  | "_cacheStore"
882
916
  | "_searchParamsFilter"
@@ -885,6 +919,7 @@ export type PublicRequestContext<
885
919
  | "_shellLoaderSeed"
886
920
  | "_shellCaptureLoaderRecords"
887
921
  | "_shellCaptureUnrecordedLoaderPush"
922
+ | "_shellCaptureLoaderLanes"
888
923
  | "_resolvedCacheKeys"
889
924
  | "_shellKey"
890
925
  | "_shellForcedMiss"
@@ -901,15 +936,11 @@ export type PublicRequestContext<
901
936
  | "_pendingBackgroundTasks"
902
937
  | "_themeConfig"
903
938
  | "_readTheme"
939
+ | "_documentCacheRender"
940
+ | "_payloadVisitorTheme"
904
941
  | "_locationState"
905
942
  | "_routeName"
906
943
  | "_prevRouteKey"
907
- | "_gateCurrentUrl"
908
- | "_gateCurrentParams"
909
- | "_gateActionId"
910
- | "_gateActionUrl"
911
- | "_gateActionResult"
912
- | "_gateFormData"
913
944
  | "_inActionRevalidation"
914
945
  | "_reportedErrors"
915
946
  | "_renderErrors"
@@ -938,7 +969,7 @@ export type PublicRequestContext<
938
969
  | "_freshReads"
939
970
  | "_setFreshReadsCookie"
940
971
  | "_requestRoot"
941
- | "_tagInvalidations"
972
+ | "_requestStart"
942
973
  | "_responseSent"
943
974
  | "_variables"
944
975
  | "_classifiedRoute"
@@ -1042,17 +1073,10 @@ export function setRequestContextParams(
1042
1073
  */
1043
1074
  export function setRequestContextPrevRouteKey(
1044
1075
  prevRouteKey: string | undefined,
1045
- currentUrl?: URL,
1046
- currentParams?: Record<string, string>,
1047
1076
  ): void {
1048
1077
  const ctx = requestContextStorage.getStore();
1049
1078
  if (!ctx) return;
1050
1079
  if (prevRouteKey !== undefined) ctx._prevRouteKey = prevRouteKey;
1051
- // Source URL/params for the transition({ when }) gate (effectiveFromUrl /
1052
- // effectiveFromMatch.params from the navigation snapshot). Same write point as
1053
- // _prevRouteKey, which doubles as fromRouteName.
1054
- if (currentUrl !== undefined) ctx._gateCurrentUrl = currentUrl;
1055
- if (currentParams !== undefined) ctx._gateCurrentParams = currentParams;
1056
1080
  }
1057
1081
 
1058
1082
  /**
@@ -1134,7 +1158,10 @@ export function createRequestContext<TEnv>(
1134
1158
  stateCookieName,
1135
1159
  version: stateVersion,
1136
1160
  } = options;
1137
- const cookieHeader = request.headers.get("Cookie");
1161
+ // ctx.request.headers is guarded like headers() (#976); every router read
1162
+ // of this request's headers goes through requestHeaders().
1163
+ guardRequestHeaders(request);
1164
+ const cookieHeader = requestHeaders(request).get("Cookie");
1138
1165
  let rangoStateRotated = false;
1139
1166
  const freshCookie = stateCookieName
1140
1167
  ? freshReadsCookieName(stateCookieName)
@@ -1233,6 +1260,26 @@ export function createRequestContext<TEnv>(
1233
1260
  return getParsedCookies()[name];
1234
1261
  };
1235
1262
 
1263
+ // The effective jar: request cookies minus deletions plus same-request writes.
1264
+ const effectiveCookies = (): Record<string, string> => {
1265
+ const parsed = getParsedCookies();
1266
+ const mutations = getResponseCookies();
1267
+ if (mutations.size === 0) return { ...parsed };
1268
+ // Build result without delete (avoids V8 dictionary-mode de-opt)
1269
+ const deleted = new Set<string>();
1270
+ for (const [k, v] of mutations) {
1271
+ if (v === null) deleted.add(k);
1272
+ }
1273
+ const result: Record<string, string> = {};
1274
+ for (const key of Object.keys(parsed)) {
1275
+ if (!deleted.has(key)) result[key] = parsed[key];
1276
+ }
1277
+ for (const [k, v] of mutations) {
1278
+ if (v !== null) result[k] = v;
1279
+ }
1280
+ return result;
1281
+ };
1282
+
1236
1283
  const getTheme = (): Theme | undefined => {
1237
1284
  if (!themeConfig) return undefined;
1238
1285
 
@@ -1312,29 +1359,23 @@ export function createRequestContext<TEnv>(
1312
1359
  );
1313
1360
  },
1314
1361
 
1362
+ // The visitor's cookies, guarded like cookies() (#976): before, a plain
1363
+ // read that a ppr shell, a cache() entry or a "use cache" entry stored
1364
+ // for every visitor.
1315
1365
  cookie(name: string): string | undefined {
1366
+ guardRawCookieRead(ctx, "getRequestContext().cookie()");
1316
1367
  return effectiveCookie(name);
1317
1368
  },
1318
1369
 
1319
1370
  cookies(): Record<string, string> {
1320
- const parsed = getParsedCookies();
1321
- const mutations = getResponseCookies();
1322
- if (mutations.size === 0) return { ...parsed };
1323
- // Build result without delete (avoids V8 dictionary-mode de-opt)
1324
- const deleted = new Set<string>();
1325
- for (const [k, v] of mutations) {
1326
- if (v === null) deleted.add(k);
1327
- }
1328
- const result: Record<string, string> = {};
1329
- for (const key of Object.keys(parsed)) {
1330
- if (!deleted.has(key)) result[key] = parsed[key];
1331
- }
1332
- for (const [k, v] of mutations) {
1333
- if (v !== null) result[k] = v;
1334
- }
1335
- return result;
1371
+ guardRawCookieRead(ctx, "getRequestContext().cookies()");
1372
+ return effectiveCookies();
1336
1373
  },
1337
1374
 
1375
+ // Non-enumerable, defined after this literal.
1376
+ _readCookie: effectiveCookie,
1377
+ _readCookies: effectiveCookies,
1378
+
1338
1379
  setCookie(name: string, value: string, options?: CookieOptions): void {
1339
1380
  assertNotInsideCacheExec(ctx, "setCookie");
1340
1381
  stubResponse.headers.append(
@@ -1383,7 +1424,7 @@ export function createRequestContext<TEnv>(
1383
1424
  // decodeStateValue decodes exactly once) AND is the same parser the client
1384
1425
  // mirror uses, so both seats read the same jar entry.
1385
1426
  const prevRaw =
1386
- (request.headers.get("x-rango-state") || null) ??
1427
+ (requestHeaders(request).get("x-rango-state") || null) ??
1387
1428
  getRawCookieValue(cookieHeader, stateCookieName);
1388
1429
  const value = mintStateValue(stateVersion ?? "0", prevRaw);
1389
1430
  // rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
@@ -1460,7 +1501,6 @@ export function createRequestContext<TEnv>(
1460
1501
  method: request.method,
1461
1502
 
1462
1503
  _handleStore: handleStore,
1463
- _transitionWhen: [],
1464
1504
  _cacheStore: cacheStore,
1465
1505
  _searchParamsFilter: searchParamsFilter,
1466
1506
  _explicitTaggedStores: explicitTaggedStores,
@@ -1560,20 +1600,18 @@ export function createRequestContext<TEnv>(
1560
1600
  // the ambient context, as cookies() does, and falls back to the receiver.
1561
1601
  // _readTheme is the unguarded read: non-enumerable too, so a spread copy of
1562
1602
  // the context (the fetchable-loader ctx, loader-fetch.ts) cannot carry it.
1603
+ // The same holds for the unguarded cookie reads (#976).
1563
1604
  Object.defineProperties(ctx, {
1564
1605
  theme: {
1565
1606
  get(this: RequestContext<TEnv>): Theme | undefined {
1566
- if (!themeConfig) return undefined;
1567
- assertThemeReadAllowed(
1568
- _getRequestContext() ?? this,
1569
- "getRequestContext().theme",
1570
- );
1571
- return getTheme();
1607
+ return readGuardedTheme(this, "getRequestContext().theme");
1572
1608
  },
1573
1609
  enumerable: false,
1574
1610
  configurable: true,
1575
1611
  },
1576
1612
  _readTheme: { enumerable: false },
1613
+ _readCookie: { enumerable: false },
1614
+ _readCookies: { enumerable: false },
1577
1615
  });
1578
1616
 
1579
1617
  (ctx as any)[NOCACHE_SYMBOL] = true;
@@ -1585,6 +1623,7 @@ export function createRequestContext<TEnv>(
1585
1623
  configurable: true,
1586
1624
  writable: true,
1587
1625
  });
1626
+ ctx._requestStart = executionStart();
1588
1627
  return ctx;
1589
1628
  }
1590
1629
 
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The guarded `ctx.request.headers` (#976) and the router's raw reads.
3
+ *
4
+ * The Request the router hands to user code (handler, middleware and loader
5
+ * `ctx.request`, `getRequestContext().request`) carries an own `headers`
6
+ * getter that shadows the prototype's and runs the identity guard
7
+ * (cookie-store.ts guardRequestHeaders). A Proxy around the Request is not an
8
+ * option: `fetch(request)`, `new Request(request)` and `request.clone()` read
9
+ * the platform's internal slots, which a Proxy does not have. The own getter
10
+ * keeps the object a real Request, and those operations never call it.
11
+ */
12
+
13
+ /** The Headers each shadowed request's getter returns. */
14
+ const RAW_HEADERS = new WeakMap<Request, Headers>();
15
+
16
+ const INSPECT = Symbol.for("nodejs.util.inspect.custom");
17
+
18
+ /**
19
+ * Set while the platform or a request facade reads a shadowed request's
20
+ * headers for itself, not for user code (runUnguarded).
21
+ */
22
+ let unguarded = false;
23
+
24
+ function runUnguarded<T>(fn: () => T): T {
25
+ const outer = unguarded;
26
+ unguarded = true;
27
+ try {
28
+ return fn();
29
+ } finally {
30
+ unguarded = outer;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * A request's headers, past the guard. The rule: router code never reads a
36
+ * request's headers through the getter, only through this, so the guard sees
37
+ * user reads alone. Several router reads run inside the scopes the guard
38
+ * refuses (the state cookie rotation at a ppr capture, match's HMR marker,
39
+ * a store's cf-ray inside a "use cache" body, telemetry inside a loader
40
+ * fill); the rest follow the rule so a new call site cannot drift into one.
41
+ */
42
+ export function requestHeaders(request: Request): Headers {
43
+ return RAW_HEADERS.get(request) ?? request.headers;
44
+ }
45
+
46
+ /**
47
+ * Shadow `request.headers` with an own, non-enumerable getter that calls
48
+ * `beforeRead` and returns the request's own Headers (stable per request, so
49
+ * a header set through it stays visible to every reader). Idempotent. Leaves
50
+ * the request as is when `headers` is a non-configurable own property (a
51
+ * runtime that defines it per instance). Three inherited members are
52
+ * shadowed with it:
53
+ *
54
+ * - `clone()` returns a clone shadowed the same way. A plain clone was a
55
+ * Request without the getter, so `ctx.request.clone().headers` read the
56
+ * visitor's headers past the guard. (`new Request(ctx.request)` still
57
+ * returns a plain Request: the constructor cannot be intercepted.)
58
+ * - `_request`, on a lazy srvx NodeRequest (what @vitejs/plugin-rsc's Node
59
+ * dev and preview servers and the vercel-output launcher hand the
60
+ * router), runs unguarded: it builds the platform Request on first use
61
+ * from `this.headers`, for clone() and the body readers. Through the
62
+ * getter, a clone() inside a cache() boundary threw as a user read. It
63
+ * stays lazy, so a request whose body srvx reads from its buffered
64
+ * `rawBody` never builds one.
65
+ * - Node's util.inspect hook runs unguarded: undici's inspect reads
66
+ * `this.headers`, so `console.log(ctx.request)` inside a "use cache" body
67
+ * threw. A log is not a read into a shared entry.
68
+ */
69
+ export function shadowRequestHeaders(
70
+ request: Request,
71
+ beforeRead: () => void,
72
+ ): Request {
73
+ if (RAW_HEADERS.has(request)) return request;
74
+ const own = Object.getOwnPropertyDescriptor(request, "headers");
75
+ if (own && !own.configurable) return request;
76
+ const lazy = inheritedGetter(request, "_request");
77
+ if (lazy) {
78
+ Object.defineProperty(request, "_request", {
79
+ get(this: Request): unknown {
80
+ return runUnguarded(() => lazy.call(this));
81
+ },
82
+ configurable: true,
83
+ });
84
+ }
85
+ const raw = request.headers;
86
+ RAW_HEADERS.set(request, raw);
87
+ Object.defineProperty(request, "headers", {
88
+ get(): Headers {
89
+ if (!unguarded) beforeRead();
90
+ return raw;
91
+ },
92
+ enumerable: false,
93
+ configurable: true,
94
+ });
95
+ const clone = request.clone;
96
+ defineMethod(request, "clone", function (this: Request): Request {
97
+ return shadowRequestHeaders(clone.call(this), beforeRead);
98
+ });
99
+ const inspect = (request as unknown as Record<symbol, unknown>)[INSPECT];
100
+ if (typeof inspect === "function") {
101
+ defineMethod(
102
+ request,
103
+ INSPECT,
104
+ function (this: Request, ...args: unknown[]) {
105
+ return runUnguarded(() => inspect.apply(this, args));
106
+ },
107
+ );
108
+ }
109
+ return request;
110
+ }
111
+
112
+ function defineMethod(
113
+ target: object,
114
+ key: PropertyKey,
115
+ value: (...args: never[]) => unknown,
116
+ ): void {
117
+ Object.defineProperty(target, key, {
118
+ value,
119
+ configurable: true,
120
+ writable: true,
121
+ });
122
+ }
123
+
124
+ /** The getter `key` resolves to on `target`'s prototype chain, if any. */
125
+ function inheritedGetter(
126
+ target: object,
127
+ key: string,
128
+ ): (() => unknown) | undefined {
129
+ for (let o = Object.getPrototypeOf(target); o; o = Object.getPrototypeOf(o)) {
130
+ const found = Object.getOwnPropertyDescriptor(o, key);
131
+ if (found) return found.get;
132
+ }
133
+ return undefined;
134
+ }
package/src/server.ts CHANGED
@@ -48,6 +48,10 @@ export {
48
48
  // Component utilities (used internally for server/client boundary checks)
49
49
  export { isClientComponent, assertClientComponent } from "./component-utils.js";
50
50
 
51
+ // transition({ when }) strict validation: the generated routes-manifest module
52
+ // enables it in the app's RSC realm (see transition-when-ref.ts).
53
+ export { enableTransitionWhenValidation } from "./transition-when-ref.js";
54
+
51
55
  // Client URL projection registry (Vite discovery/bootstrap bridge)
52
56
  export {
53
57
  clearClientUrlProjections,
@@ -627,9 +627,9 @@ export async function dispatch<TEnv = any>(
627
627
  const manifestEntry = preview?.manifestEntry;
628
628
  if (manifestEntry) {
629
629
  // Lazy so the testing barrel's eager graph stays plugin-rsc-free (see the
630
- // import note above): the leaf takes createCacheScope/resolveCacheTags as
631
- // INJECTED deps so it never imports plugin-rsc; we hand it the lazily
632
- // imported pair here, only once a response route actually matched.
630
+ // import note above): the leaf takes createCacheScope as an INJECTED
631
+ // dep so it never imports plugin-rsc; we hand it the lazily imported
632
+ // builder here, only once a response route actually matched.
633
633
  const cacheScopeMod = await import("../cache/cache-scope.js");
634
634
  const { serveResponseRouteWithCache } =
635
635
  await import("../rsc/response-cache-serve.js");
@@ -645,10 +645,7 @@ export async function dispatch<TEnv = any>(
645
645
  responseType: responseType as string,
646
646
  url,
647
647
  executeHandler,
648
- deps: {
649
- createCacheScope: cacheScopeMod.createCacheScope,
650
- resolveCacheTags: cacheScopeMod.resolveCacheTags,
651
- },
648
+ deps: { createCacheScope: cacheScopeMod.createCacheScope },
652
649
  });
653
650
  if (cached !== undefined) return cached;
654
651
  }
@@ -53,6 +53,7 @@ export {
53
53
  SHELL_STATUS_HEADER,
54
54
  type PprReplayBypassReason,
55
55
  type PprReplayStatus,
56
+ type ShellCachePartition,
56
57
  type ShellStatus,
57
58
  type ShellStatusTarget,
58
59
  } from "../shell-status.js";
@@ -49,6 +49,8 @@ export type {
49
49
 
50
50
  export { runTransitionWhen } from "./run-transition-when.js";
51
51
  export type {
52
+ RunTransitionWhenAction,
53
+ RunTransitionWhenLocation,
52
54
  RunTransitionWhenOptions,
53
55
  RunTransitionWhenResult,
54
56
  } from "./run-transition-when.js";
@@ -91,12 +93,15 @@ export {
91
93
  export type {
92
94
  PprReplayBypassReason,
93
95
  PprReplayStatus,
96
+ ShellCachePartition,
94
97
  ShellStatus,
95
98
  ShellStatusTarget,
96
99
  } from "./shell-status.js";
97
100
 
98
101
  export { collectHandle } from "./collect-handle.js";
99
102
 
103
+ export { withLocationStateKey } from "./location-state-key.js";
104
+
100
105
  export {
101
106
  diffGeneratedRoutes,
102
107
  assertGeneratedRoutesMatch,
@@ -0,0 +1,25 @@
1
+ import { resolveActionRefId } from "../../router/is-action.js";
2
+
3
+ /**
4
+ * The id a testing primitive's `action` option names: a single imported
5
+ * server action (its `$id ?? $$id`, as the action boundary derives it) or a
6
+ * raw actionId string. A function without either id throws, naming the
7
+ * primitive.
8
+ */
9
+ export function resolveTestActionId(
10
+ action: ((...args: never[]) => unknown) | string | undefined,
11
+ primitive: string,
12
+ ): string | undefined {
13
+ if (action === undefined) return undefined;
14
+ if (typeof action === "string") return action;
15
+ const id = resolveActionRefId(action);
16
+ if (id === undefined) {
17
+ throw new Error(
18
+ `${primitive}: \`action\` must be a single imported server action ` +
19
+ "(carrying its build-injected id) or an actionId string. The passed " +
20
+ "function has no $id/$$id — outside a built app, pass the id string " +
21
+ 'your predicate should match (e.g. "src/actions/cart.ts#addToCart").',
22
+ );
23
+ }
24
+ return id;
25
+ }
@@ -250,7 +250,7 @@ export function snapshotRunEffects<TEnv>(ctx: RequestContext<TEnv>): {
250
250
  locationState: Record<string, unknown>;
251
251
  } {
252
252
  return {
253
- cookies: { ...ctx.cookies() },
253
+ cookies: ctx._readCookies(),
254
254
  locationState: resolveLocationStateEntries(ctx._locationState ?? []),
255
255
  };
256
256
  }