@rangojs/router 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +37 -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 +48 -5
  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 +126 -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 +962 -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 +67 -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 +54 -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 +202 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +209 -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 +42 -5
  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 +152 -26
  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 +263 -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 +134 -87
  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 {
@@ -64,8 +64,16 @@ import {
64
64
  assertCachedHeaderWriteAllowed,
65
65
  assertNonCacheableReadAllowed,
66
66
  clearPprHeaderScope,
67
+ runInsideLoaderRun,
68
+ type LoaderRunIdentity,
67
69
  } from "./context.js";
68
- import { assertThemeReadAllowed } from "./cookie-store.js";
70
+ import { readStartedLoaderValue, trackLoaderRun } from "../cache/cache-tag.js";
71
+ import {
72
+ guardRawCookieRead,
73
+ guardRequestHeaders,
74
+ readGuardedTheme,
75
+ } from "./cookie-store.js";
76
+ import { requestHeaders } from "./request-headers.js";
69
77
  import {
70
78
  createReverseFunction,
71
79
  stripInternalParams,
@@ -76,6 +84,10 @@ import {
76
84
  getSearchSchema,
77
85
  } from "../route-map-builder.js";
78
86
  import { parseSearchParams } from "../search-params.js";
87
+ import {
88
+ executionStart,
89
+ type ExecutionStart,
90
+ } from "../cache/invalidation-order.js";
79
91
  import { invariant } from "../errors.js";
80
92
  import { isAutoGeneratedRouteName } from "../route-name.js";
81
93
 
@@ -146,10 +158,24 @@ export interface RequestContext<
146
158
  /** @internal Request-local PPR opt-out marker set by ctx.dynamic(). */
147
159
  _dynamic?: boolean;
148
160
 
149
- /** @internal Get a cookie value (effective: request + response mutations). Use cookies().get() instead. */
161
+ /**
162
+ * @internal Get a cookie value (effective: request + response mutations).
163
+ * Use cookies().get() instead. Guarded like cookies() (#976).
164
+ */
150
165
  cookie(name: string): string | undefined;
151
- /** @internal Get all cookies (effective merged view). Use cookies().getAll() instead. */
166
+ /**
167
+ * @internal Get all cookies (effective merged view). Use
168
+ * cookies().getAll() instead. Guarded like cookies() (#976).
169
+ */
152
170
  cookies(): Record<string, string>;
171
+ /**
172
+ * @internal `cookie()` without the identity guard: the read behind the
173
+ * cookies() store, which guards at its own call and records per read
174
+ * method. Non-enumerable, like `_readTheme`.
175
+ */
176
+ _readCookie(name: string): string | undefined;
177
+ /** @internal `cookies()` without the identity guard (see `_readCookie`). */
178
+ _readCookies(): Record<string, string>;
153
179
  /** @internal Set a cookie on the response. Use cookies().set() instead. */
154
180
  setCookie(name: string, value: string, options?: CookieOptions): void;
155
181
  /** @internal Delete a cookie. Use cookies().delete() instead. */
@@ -182,19 +208,18 @@ export interface RequestContext<
182
208
  /**
183
209
  * @internal The context createRequestContext() returned. A context derived
184
210
  * 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).
211
+ * revalidation render) inherits it, so per-request invalidation state (the
212
+ * stores' masks) is keyed by the request, not by whichever derived object
213
+ * called or read (#973).
188
214
  */
189
215
  _requestRoot?: RequestContext<any, any>;
190
216
  /**
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).
217
+ * @internal Where the request started in the isolate's invalidation order
218
+ * (cache/invalidation-order.ts), taken when createRequestContext() ran. The
219
+ * document cache gates its write on it (#977): a middleware ahead of the
220
+ * document cache can read tagged data the document bakes.
196
221
  */
197
- _tagInvalidations?: Map<string, number>;
222
+ _requestStart?: ExecutionStart;
198
223
  /**
199
224
  * @internal The handler has handed the response to the host (rsc/handler.ts
200
225
  * `rango.response`): header and cookie writes after this are lost, as from
@@ -242,16 +267,13 @@ export interface RequestContext<
242
267
  readonly _handleStore: HandleStore;
243
268
 
244
269
  /**
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.
270
+ * @internal transition({ when }) client references of the matched entry
271
+ * chain, keyed by segment id. Recorded at match time from the static route
272
+ * definition (router/transition-when.ts) and attached to the payload's
273
+ * segments right before Flight (rsc/attach-transition-when.ts). The server
274
+ * never calls them, and no cache or snapshot stores them.
250
275
  */
251
- _transitionWhen?: Array<{ id: string; when: TransitionWhenFn }>;
252
-
253
- /** @internal PPR transition decisions evaluated before cache lookup/handlers. */
254
- _pprTransitionDecisions?: Map<string, boolean>;
276
+ _transitionWhenRefs?: ReadonlyMap<string, TransitionWhenRecord>;
255
277
 
256
278
  /**
257
279
  * @internal Post-match serve-source truth for the PPR replay reporter.
@@ -279,8 +301,8 @@ export interface RequestContext<
279
301
  * capture task's derived request context (built by shell-capture.ts). This is
280
302
  * the switch every capture-specific behavior reads: loader masking
281
303
  * (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
304
+ * identity-read capture guard (server/context.ts guardIdentityRead). The
305
+ * foreground render never sets it, so the
284
306
  * served response is byte-identical to axis 1. The capture descriptor itself
285
307
  * (key/ttl/swr/tags/store) is NOT threaded through the request context — the
286
308
  * integrated PPR serve path (rsc/shell-serve.ts + rsc-rendering.ts) builds it
@@ -308,6 +330,16 @@ export interface RequestContext<
308
330
  */
309
331
  _shellCaptureUnrecordedLoaderPush?: true;
310
332
 
333
+ /**
334
+ * @internal The lane of each loader the capture's matched route registers
335
+ * (loader-cache.ts routeLoaderLanes), set by createMatchContextForFull
336
+ * before any loader runs. The capture's push funnel credits a push made
337
+ * under a live-lane loader's body, at any depth, to that loader
338
+ * (shell-capture.ts deriveShellCaptureContext). Own property of the
339
+ * capture's derived context only.
340
+ */
341
+ _shellCaptureLoaderLanes?: ReadonlyMap<string, "live" | "bake">;
342
+
311
343
  /**
312
344
  * @internal Record keys resolved for this request (CacheScope
313
345
  * resolveKeyFrom): by the scope's `key()` (one result) or its store's
@@ -414,7 +446,9 @@ export interface RequestContext<
414
446
  * scope replaces any route-derived cache() scope for the tail's match
415
447
  * (resolveShellImplicitCacheScope), and a lookup that does not hit throws
416
448
  * ShellRecordUnavailableError instead of resolving segments
417
- * (withCacheLookup): a HIT never runs a handler.
449
+ * (withCacheLookup): a HIT never runs a handler. The record's
450
+ * loader-owned handle pushes restore as authoritative
451
+ * (HandleStore.pushRestored): the prelude rendered them.
418
452
  */
419
453
  docTail?: true;
420
454
  /**
@@ -468,8 +502,9 @@ export interface RequestContext<
468
502
 
469
503
  /**
470
504
  * @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
505
+ * guardIdentityRead) when it throws DURING a capture render: the read
506
+ * ("cookies()", "headers()", `ctx.get() for a non-cacheable variable`,
507
+ * "ctx.theme") and the fix the
473
508
  * refusal warning gives for it. Load-bearing for the
474
509
  * bake lane: a guard throw inside an executing loader is swallowed by
475
510
  * wrapLoaderPromise into per-loader error UI, which would otherwise bake
@@ -611,12 +646,27 @@ export interface RequestContext<
611
646
  /**
612
647
  * @internal The theme cookie (or the default) read with no identity guard.
613
648
  * 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
649
+ * call it after guardIdentityRead (readGuardedTheme); the router's own payload read goes
650
+ * through payloadInitialTheme (rsc/full-payload.ts), except the unmatched
651
+ * route's 404 (rsc/handler.ts), which is never stored. Undefined without
616
652
  * theme config.
617
653
  */
618
654
  _readTheme(): Theme | undefined;
619
655
 
656
+ /**
657
+ * @internal Set by the document-cache middleware (cache/document-cache.ts)
658
+ * before it runs a render whose response it may store: the MISS and the
659
+ * stale refresh. payloadInitialTheme reads it (#978).
660
+ */
661
+ _documentCacheRender?: boolean;
662
+
663
+ /**
664
+ * @internal Set by payloadInitialTheme when the document payload carries
665
+ * the visitor's theme and it differs from the no-cookie default. The
666
+ * document cache does not store that response (#978).
667
+ */
668
+ _payloadVisitorTheme?: boolean;
669
+
620
670
  /**
621
671
  * Attach location state entries to the current response.
622
672
  *
@@ -660,20 +710,6 @@ export interface RequestContext<
660
710
  /** @internal Previous route key (from the navigation source), used for revalidation */
661
711
  _prevRouteKey?: string;
662
712
 
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
713
  /**
678
714
  * @internal True while the post-action revalidation render is running (set by
679
715
  * revalidateAfterAction). The "use cache" runtime reads this to prefer
@@ -872,11 +908,12 @@ export type PublicRequestContext<
872
908
  RequestContext<TEnv, TParams>,
873
909
  | "cookie"
874
910
  | "cookies"
911
+ | "_readCookie"
912
+ | "_readCookies"
875
913
  | "setCookie"
876
914
  | "deleteCookie"
877
915
  | "_handleStore"
878
- | "_transitionWhen"
879
- | "_pprTransitionDecisions"
916
+ | "_transitionWhenRefs"
880
917
  | "_pprReplayPostMatchReason"
881
918
  | "_cacheStore"
882
919
  | "_searchParamsFilter"
@@ -885,6 +922,7 @@ export type PublicRequestContext<
885
922
  | "_shellLoaderSeed"
886
923
  | "_shellCaptureLoaderRecords"
887
924
  | "_shellCaptureUnrecordedLoaderPush"
925
+ | "_shellCaptureLoaderLanes"
888
926
  | "_resolvedCacheKeys"
889
927
  | "_shellKey"
890
928
  | "_shellForcedMiss"
@@ -901,15 +939,11 @@ export type PublicRequestContext<
901
939
  | "_pendingBackgroundTasks"
902
940
  | "_themeConfig"
903
941
  | "_readTheme"
942
+ | "_documentCacheRender"
943
+ | "_payloadVisitorTheme"
904
944
  | "_locationState"
905
945
  | "_routeName"
906
946
  | "_prevRouteKey"
907
- | "_gateCurrentUrl"
908
- | "_gateCurrentParams"
909
- | "_gateActionId"
910
- | "_gateActionUrl"
911
- | "_gateActionResult"
912
- | "_gateFormData"
913
947
  | "_inActionRevalidation"
914
948
  | "_reportedErrors"
915
949
  | "_renderErrors"
@@ -938,7 +972,7 @@ export type PublicRequestContext<
938
972
  | "_freshReads"
939
973
  | "_setFreshReadsCookie"
940
974
  | "_requestRoot"
941
- | "_tagInvalidations"
975
+ | "_requestStart"
942
976
  | "_responseSent"
943
977
  | "_variables"
944
978
  | "_classifiedRoute"
@@ -1042,17 +1076,10 @@ export function setRequestContextParams(
1042
1076
  */
1043
1077
  export function setRequestContextPrevRouteKey(
1044
1078
  prevRouteKey: string | undefined,
1045
- currentUrl?: URL,
1046
- currentParams?: Record<string, string>,
1047
1079
  ): void {
1048
1080
  const ctx = requestContextStorage.getStore();
1049
1081
  if (!ctx) return;
1050
1082
  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
1083
  }
1057
1084
 
1058
1085
  /**
@@ -1134,7 +1161,10 @@ export function createRequestContext<TEnv>(
1134
1161
  stateCookieName,
1135
1162
  version: stateVersion,
1136
1163
  } = options;
1137
- const cookieHeader = request.headers.get("Cookie");
1164
+ // ctx.request.headers is guarded like headers() (#976); every router read
1165
+ // of this request's headers goes through requestHeaders().
1166
+ guardRequestHeaders(request);
1167
+ const cookieHeader = requestHeaders(request).get("Cookie");
1138
1168
  let rangoStateRotated = false;
1139
1169
  const freshCookie = stateCookieName
1140
1170
  ? freshReadsCookieName(stateCookieName)
@@ -1233,6 +1263,26 @@ export function createRequestContext<TEnv>(
1233
1263
  return getParsedCookies()[name];
1234
1264
  };
1235
1265
 
1266
+ // The effective jar: request cookies minus deletions plus same-request writes.
1267
+ const effectiveCookies = (): Record<string, string> => {
1268
+ const parsed = getParsedCookies();
1269
+ const mutations = getResponseCookies();
1270
+ if (mutations.size === 0) return { ...parsed };
1271
+ // Build result without delete (avoids V8 dictionary-mode de-opt)
1272
+ const deleted = new Set<string>();
1273
+ for (const [k, v] of mutations) {
1274
+ if (v === null) deleted.add(k);
1275
+ }
1276
+ const result: Record<string, string> = {};
1277
+ for (const key of Object.keys(parsed)) {
1278
+ if (!deleted.has(key)) result[key] = parsed[key];
1279
+ }
1280
+ for (const [k, v] of mutations) {
1281
+ if (v !== null) result[k] = v;
1282
+ }
1283
+ return result;
1284
+ };
1285
+
1236
1286
  const getTheme = (): Theme | undefined => {
1237
1287
  if (!themeConfig) return undefined;
1238
1288
 
@@ -1312,29 +1362,23 @@ export function createRequestContext<TEnv>(
1312
1362
  );
1313
1363
  },
1314
1364
 
1365
+ // The visitor's cookies, guarded like cookies() (#976): before, a plain
1366
+ // read that a ppr shell, a cache() entry or a "use cache" entry stored
1367
+ // for every visitor.
1315
1368
  cookie(name: string): string | undefined {
1369
+ guardRawCookieRead(ctx, "getRequestContext().cookie()");
1316
1370
  return effectiveCookie(name);
1317
1371
  },
1318
1372
 
1319
1373
  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;
1374
+ guardRawCookieRead(ctx, "getRequestContext().cookies()");
1375
+ return effectiveCookies();
1336
1376
  },
1337
1377
 
1378
+ // Non-enumerable, defined after this literal.
1379
+ _readCookie: effectiveCookie,
1380
+ _readCookies: effectiveCookies,
1381
+
1338
1382
  setCookie(name: string, value: string, options?: CookieOptions): void {
1339
1383
  assertNotInsideCacheExec(ctx, "setCookie");
1340
1384
  stubResponse.headers.append(
@@ -1383,7 +1427,7 @@ export function createRequestContext<TEnv>(
1383
1427
  // decodeStateValue decodes exactly once) AND is the same parser the client
1384
1428
  // mirror uses, so both seats read the same jar entry.
1385
1429
  const prevRaw =
1386
- (request.headers.get("x-rango-state") || null) ??
1430
+ (requestHeaders(request).get("x-rango-state") || null) ??
1387
1431
  getRawCookieValue(cookieHeader, stateCookieName);
1388
1432
  const value = mintStateValue(stateVersion ?? "0", prevRaw);
1389
1433
  // rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
@@ -1460,7 +1504,6 @@ export function createRequestContext<TEnv>(
1460
1504
  method: request.method,
1461
1505
 
1462
1506
  _handleStore: handleStore,
1463
- _transitionWhen: [],
1464
1507
  _cacheStore: cacheStore,
1465
1508
  _searchParamsFilter: searchParamsFilter,
1466
1509
  _explicitTaggedStores: explicitTaggedStores,
@@ -1560,20 +1603,18 @@ export function createRequestContext<TEnv>(
1560
1603
  // the ambient context, as cookies() does, and falls back to the receiver.
1561
1604
  // _readTheme is the unguarded read: non-enumerable too, so a spread copy of
1562
1605
  // the context (the fetchable-loader ctx, loader-fetch.ts) cannot carry it.
1606
+ // The same holds for the unguarded cookie reads (#976).
1563
1607
  Object.defineProperties(ctx, {
1564
1608
  theme: {
1565
1609
  get(this: RequestContext<TEnv>): Theme | undefined {
1566
- if (!themeConfig) return undefined;
1567
- assertThemeReadAllowed(
1568
- _getRequestContext() ?? this,
1569
- "getRequestContext().theme",
1570
- );
1571
- return getTheme();
1610
+ return readGuardedTheme(this, "getRequestContext().theme");
1572
1611
  },
1573
1612
  enumerable: false,
1574
1613
  configurable: true,
1575
1614
  },
1576
1615
  _readTheme: { enumerable: false },
1616
+ _readCookie: { enumerable: false },
1617
+ _readCookies: { enumerable: false },
1577
1618
  });
1578
1619
 
1579
1620
  (ctx as any)[NOCACHE_SYMBOL] = true;
@@ -1585,6 +1626,7 @@ export function createRequestContext<TEnv>(
1585
1626
  configurable: true,
1586
1627
  writable: true,
1587
1628
  });
1629
+ ctx._requestStart = executionStart();
1588
1630
  return ctx;
1589
1631
  }
1590
1632
 
@@ -1820,8 +1862,11 @@ export function createUseFunction<TEnv>(
1820
1862
 
1821
1863
  const loader = item as LoaderDefinition<any, any>;
1822
1864
 
1823
- if (loaderPromises.has(loader.$$id)) {
1824
- return loaderPromises.get(loader.$$id);
1865
+ const memo = loaderPromises.get(loader.$$id);
1866
+ if (memo) {
1867
+ // Same refusal as the handler ctx.use memo hit (#1011): a "use cache"
1868
+ // body reading a value whose run read request identity.
1869
+ return readStartedLoaderValue(memo, loader.$$id);
1825
1870
  }
1826
1871
 
1827
1872
  let loaderFn = loader.fn;
@@ -1908,9 +1953,11 @@ export function createUseFunction<TEnv>(
1908
1953
  // (observePhase), so a loader resolved via this base request-context ctx.use
1909
1954
  // co-emits the "loader:<id>" perf metric AND the "rango.loader" span — no
1910
1955
  // drift between the two ctx.use implementations.
1956
+ const run: LoaderRunIdentity = { loaderId: loader.$$id };
1911
1957
  const promise = observePhase(PHASES.loader(loader.$$id), () =>
1912
- Promise.resolve(loaderFn(loaderCtx)),
1958
+ Promise.resolve(runInsideLoaderRun(run, () => loaderFn(loaderCtx))),
1913
1959
  );
1960
+ trackLoaderRun(promise, run);
1914
1961
 
1915
1962
  loaderPromises.set(loader.$$id, promise);
1916
1963
 
@@ -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
  }