@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
@@ -18,26 +18,32 @@
18
18
 
19
19
  import React from "react";
20
20
  import { isPprEntry, type EntryData } from "../server/context.js";
21
- import { sortedSearchString } from "../cache/cache-key-utils.js";
22
- import type { SearchParamsFilter } from "../cache/search-params-filter.js";
23
21
  import { base64ToBytes } from "../cache/cf/cf-base64.js";
24
- import { getNonceAttribute } from "../theme/theme-script.js";
25
22
  import type {
26
23
  DocumentShellCacheEntry,
27
24
  ShellCacheEntry,
28
25
  ShellDocumentRead,
26
+ ShellEntryHead,
29
27
  ShellReadStats,
30
28
  ShellSnapshotFailure,
31
29
  ShellSnapshotRecord,
32
30
  SegmentCacheStore,
33
31
  } from "../cache/types.js";
34
- import { SHELL_CAPTURE_MAX_WAIT_MS } from "./shell-capture-constants.js";
35
-
36
- /** Debug/status header the browser (and e2e assertions) can read: HIT | MISS. */
37
- export const SHELL_STATUS_HEADER = "x-rango-shell";
32
+ import {
33
+ SHELL_CAPTURE_MAX_WAIT_MS,
34
+ hasWarnedOnce,
35
+ resetShellWarningsForTests,
36
+ warnOnce,
37
+ } from "./shell-capture-constants.js";
38
38
 
39
- /** Partial-navigation status header set only after captured PPR segments are consumed. */
40
- export const PPR_REPLAY_STATUS_HEADER = "x-rango-ppr-replay";
39
+ export {
40
+ PPR_REPLAY_STATUS_HEADER,
41
+ SHELL_STATUS_HEADER,
42
+ buildShellKey,
43
+ navigationShellKey,
44
+ partitionShellKey,
45
+ shellSearchSeed,
46
+ } from "./shell-capture-constants.js";
41
47
 
42
48
  /**
43
49
  * Default shell ttl (seconds) for `ppr: true` and for a PartialPrerenderProps
@@ -120,29 +126,6 @@ export function resolvePprConfig(
120
126
  };
121
127
  }
122
128
 
123
- /**
124
- * Shell cache key: host + pathname + sorted search + a `:shell` namespace suffix
125
- * (so it can never collide with a document-cache key; the store further isolates
126
- * the shell family internally).
127
- *
128
- * The key includes the request HOST: in a multi-tenant host-router deployment
129
- * (one worker, one shared KV/runtime-cache store) a host-less key would serve
130
- * tenant A's captured shell to tenant B's users.
131
- *
132
- * `filter` is the request's compiled `cache.searchParams` config
133
- * (ctx._searchParamsFilter): excluded params collapse onto one shell slot.
134
- * Callers on the serve/capture path MUST pass it -- key drift between capture
135
- * and lookup makes every shell request a permanent miss.
136
- */
137
- export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
138
- return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
139
- }
140
-
141
- export {
142
- navigationShellKey,
143
- partitionShellKey,
144
- } from "./shell-capture-constants.js";
145
-
146
129
  /**
147
130
  * The forced-MISS query marker. A document HIT that cannot finish its tail
148
131
  * reloads the page with it (shellReloadScript); the serve gate renders a
@@ -179,38 +162,24 @@ export function withoutShellMissMarker(request: Request): Request | undefined {
179
162
  * did (a server that ignores the marker) it would return without touching
180
163
  * the page. window.stop() runs only when it reloads: it keeps the half-sent
181
164
  * Flight stream from being closed by DOMContentLoaded, which would throw
182
- * React's "Connection closed" (#412) before the reload lands. `nonce` is the
183
- * request's CSP nonce; a ppr route with an active nonce is axis 1
184
- * (rsc-rendering.ts shellServePlan), so on a HIT it is normally absent.
165
+ * React's "Connection closed" (#412) before the reload lands.
185
166
  */
186
- export function shellReloadScript(nonce?: string): string {
167
+ export function shellReloadScript(): string {
187
168
  return inlineShellScript(
188
169
  "(function(){var u=new URL(location.href);" +
189
170
  `if(u.searchParams.has(${JSON.stringify(SHELL_MISS_PARAM)}))return;` +
190
171
  `u.searchParams.set(${JSON.stringify(SHELL_MISS_PARAM)},"miss");` +
191
172
  "try{window.stop()}catch(e){}location.replace(u.href)})()",
192
- nonce,
193
173
  );
194
174
  }
195
175
 
196
- /** An inline script a shell HIT's tail appends, carrying the CSP nonce if any. */
197
- export function inlineShellScript(body: string, nonce?: string): string {
198
- return `<script${getNonceAttribute(nonce)}>${body}</script>`;
199
- }
200
-
201
176
  /**
202
- * The shell key's search portion (`?`-prefixed sorted search with the
203
- * cache.searchParams filter applied, or "") — ALSO the string the capture and
204
- * resume SSR renders seed their store location with (SSRRenderOptions.search
205
- * / ShellCaptureOptions.search / ShellResumeOptions.search). Search is part
206
- * of shell identity, so static-part `useSearchParams` reads render exactly
207
- * what the key names; deriving seed and key from this one helper is what
208
- * keeps capture, resume, and lookup byte-agreed (drift = replay mismatch or
209
- * permanent MISS).
177
+ * An inline script a shell HIT's tail appends. It carries no CSP nonce: a
178
+ * request with an active per-request nonce never reaches a HIT
179
+ * (rsc-rendering.ts shellServePlan serves it on axis 1).
210
180
  */
211
- export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
212
- const sorted = sortedSearchString(url.searchParams, filter);
213
- return sorted ? `?${sorted}` : "";
181
+ export function inlineShellScript(body: string): string {
182
+ return `<script>${body}</script>`;
214
183
  }
215
184
 
216
185
  /**
@@ -219,12 +188,10 @@ export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
219
188
  * one exact tree, so resuming it under a different React OR a different app
220
189
  * build tree-mismatches inside resume() — after the 200 + prelude committed,
221
190
  * with no recovery. Either mismatch is a miss: the recapture overwrites the
222
- * same key (self-healing) and the entry otherwise ages out via TTL. An entry
223
- * with no buildVersion (stored before the field existed) is a miss for the
224
- * same reason — its build is unknown, so it cannot be proven resumable.
191
+ * same key (self-healing) and the entry otherwise ages out via TTL.
225
192
  */
226
193
  export function isValidShellHit(
227
- entry: ShellCacheEntry,
194
+ entry: Pick<ShellCacheEntry, "reactVersion" | "buildVersion">,
228
195
  buildVersion: string,
229
196
  ): boolean {
230
197
  return (
@@ -233,9 +200,9 @@ export function isValidShellHit(
233
200
  }
234
201
 
235
202
  /** The postponed blob is stored and parses (null is the DATA variant). */
236
- function hasParseablePostponed(
237
- entry: ShellCacheEntry,
238
- ): entry is ShellCacheEntry & { postponed: string | null } {
203
+ function hasParseablePostponed<T extends ShellEntryHead>(
204
+ entry: T,
205
+ ): entry is T & { postponed: string | null } {
239
206
  if (entry.postponed === undefined) return false;
240
207
  try {
241
208
  if (entry.postponed !== null) JSON.parse(entry.postponed);
@@ -266,7 +233,7 @@ export function hasIntactShellPayload(
266
233
 
267
234
  /** A document shell ready to serve: its prelude decoded exactly once. */
268
235
  export interface ShellDocument {
269
- entry: ShellCacheEntry;
236
+ entry: ShellEntryHead;
270
237
  prelude: Uint8Array;
271
238
  /** React's postponed state, parse-checked (null = DATA variant). */
272
239
  postponed: string | null;
@@ -275,10 +242,7 @@ export interface ShellDocument {
275
242
  * after a prelude-first read (SegmentCacheStore.readShellDocument), which
276
243
  * only the tail awaits.
277
244
  */
278
- snapshot:
279
- | ShellSnapshotRecord[]
280
- | undefined
281
- | Promise<ShellSnapshotRecord[] | undefined>;
245
+ snapshot: ShellSnapshotRecord[] | Promise<ShellSnapshotRecord[] | undefined>;
282
246
  /** Why a prelude-first read's snapshot is missing (ShellDocumentRead). */
283
247
  snapshotFailure?: Promise<ShellSnapshotFailure | undefined>;
284
248
  /** The prelude-first read's stats, when perf metrics are on. */
@@ -297,8 +261,16 @@ export interface ShellDocument {
297
261
  * A prelude-first `read` (SegmentCacheStore.readShellDocument) already carries
298
262
  * the raw bytes and a pending snapshot, so only its postponed blob is checked.
299
263
  */
264
+ export function openShellDocument(entry: ShellCacheEntry): ShellDocument | null;
300
265
  export function openShellDocument(
301
- entry: ShellCacheEntry,
266
+ entry: ShellEntryHead,
267
+ read: Pick<
268
+ ShellDocumentRead,
269
+ "prelude" | "snapshot" | "snapshotFailure" | "stats"
270
+ >,
271
+ ): ShellDocument | null;
272
+ export function openShellDocument(
273
+ entry: ShellEntryHead | ShellCacheEntry,
302
274
  read?: Pick<
303
275
  ShellDocumentRead,
304
276
  "prelude" | "snapshot" | "snapshotFailure" | "stats"
@@ -315,18 +287,20 @@ export function openShellDocument(
315
287
  ...(read.stats && { stats: read.stats }),
316
288
  };
317
289
  }
318
- if (!hasIntactShellPayload(entry)) return null;
290
+ // The read-less overload takes a whole entry (getShell, a build shell).
291
+ const whole = entry as ShellCacheEntry;
292
+ if (!hasIntactShellPayload(whole)) return null;
319
293
  let prelude: Uint8Array;
320
294
  try {
321
- prelude = base64ToBytes(entry.prelude);
295
+ prelude = base64ToBytes(whole.prelude);
322
296
  } catch {
323
297
  return null;
324
298
  }
325
299
  return {
326
- entry,
327
- postponed: entry.postponed,
300
+ entry: whole,
301
+ postponed: whole.postponed,
328
302
  prelude,
329
- snapshot: entry.snapshot,
303
+ snapshot: whole.snapshot,
330
304
  };
331
305
  }
332
306
 
@@ -369,7 +343,7 @@ export interface ShellTailTiming {
369
343
  snapshotReadMs?: number;
370
344
  /** Snapshot JSON.parse CPU (reads 0 on a deployed worker: see bytes). */
371
345
  snapshotParseMs?: number;
372
- /** Snapshot records by family, e.g. `segment:1/item:5` (no commas: it rides a Server-Timing desc). */
346
+ /** Snapshot records by family, e.g. `segment:1/loader:2` (no commas: it rides a Server-Timing desc). */
373
347
  snapshotRecords?: string;
374
348
  /** Records the capture pruned from the snapshot, same format (ShellCacheEntry.prunedRecords). */
375
349
  snapshotPruned?: string;
@@ -393,7 +367,7 @@ export interface ShellTailTiming {
393
367
 
394
368
  /**
395
369
  * Compact single-line form for the console log and the Server-Timing mirror's
396
- * `desc`: alphanumerics and `=`, `-`, `:`, `/` only (records=segment:1 pruned=item:5),
370
+ * `desc`: alphanumerics and `=`, `-`, `:`, `/` only (records=segment:1/loader:2 pruned=loader:1),
397
371
  * so no quoted-string escaping is needed. Offsets are from the commit; the
398
372
  * `-cpu` fields are CPU-only durations.
399
373
  */
@@ -472,9 +446,6 @@ export function takeShellTailTimingForServerTiming(
472
446
  return timing;
473
447
  }
474
448
 
475
- /** Keys already warned about a missing shell store family (once per key). */
476
- const warnedMissingStore = new Set<string>();
477
-
478
449
  /**
479
450
  * Warn once per key that a route declared `ppr` but the app-level cache store
480
451
  * does not implement the shell family (getShell/putShell), so the route stays on
@@ -482,10 +453,11 @@ const warnedMissingStore = new Set<string>();
482
453
  * honored deserves a diagnostic.
483
454
  */
484
455
  export function warnShellStoreMissingOnce(key: string): void {
485
- if (warnedMissingStore.has(key)) return;
486
- warnedMissingStore.add(key);
487
- console.warn(
488
- `[rango] Route for "${key}" declares the ppr path option, but the app-level ` +
456
+ warnOnce(
457
+ "store-missing",
458
+ key,
459
+ () =>
460
+ `[rango] Route for "${key}" declares the ppr path option, but the app-level ` +
489
461
  "cache store does not implement the shell family (getShell/putShell), so " +
490
462
  "the route is served on axis 1 without a shell. Use MemorySegmentCacheStore, " +
491
463
  "CFCacheStore, or VercelCacheStore (or add the family to your custom store) " +
@@ -493,9 +465,6 @@ export function warnShellStoreMissingOnce(key: string): void {
493
465
  );
494
466
  }
495
467
 
496
- /** Keys already warned about an active per-request nonce (once per key). */
497
- const warnedNonceActive = new Set<string>();
498
-
499
468
  /**
500
469
  * Warn once per key that a route declared `ppr` but a per-request CSP nonce is
501
470
  * active for the request, so the route stays on axis 1 (a shared shell would
@@ -507,10 +476,11 @@ const warnedNonceActive = new Set<string>();
507
476
  * the missing-store warning above (an undeclared route stays silent).
508
477
  */
509
478
  export function warnPprNonceActiveOnce(key: string): void {
510
- if (warnedNonceActive.has(key)) return;
511
- warnedNonceActive.add(key);
512
- console.warn(
513
- `[rango] Route for "${key}" declares the ppr path option, but a per-request ` +
479
+ warnOnce(
480
+ "nonce-active",
481
+ key,
482
+ () =>
483
+ `[rango] Route for "${key}" declares the ppr path option, but a per-request ` +
514
484
  "CSP nonce is active for this request (from createRouter({ nonce }) or a " +
515
485
  "ctx.set(nonce, …) token write in middleware), so the route is served on " +
516
486
  "axis 1 without a shell. A shell is shared per host+URL; baking one " +
@@ -519,9 +489,6 @@ export function warnPprNonceActiveOnce(key: string): void {
519
489
  );
520
490
  }
521
491
 
522
- /** Routes already warned about a ppr window the route cache() caps. */
523
- const warnedPprWindowCapped = new Set<string>();
524
-
525
492
  /**
526
493
  * Dev only (the caller gates on NODE_ENV): warn once per route that the
527
494
  * route's cache() entry reduces an explicit `ppr.ttl` or `ppr.swr`. `shell`
@@ -537,7 +504,7 @@ export function warnPprWindowCappedOnce(
537
504
  shell: { ttl: number; swr: number },
538
505
  cache: { ttl: number; swr: number },
539
506
  ): void {
540
- if (warnedPprWindowCapped.has(routeName)) return;
507
+ if (hasWarnedOnce("ppr-window-capped", routeName)) return;
541
508
  const reduced: string[] = [];
542
509
  for (const field of ["ttl", "swr"] as const) {
543
510
  const own = ppr[field];
@@ -546,9 +513,11 @@ export function warnPprWindowCappedOnce(
546
513
  }
547
514
  }
548
515
  if (reduced.length === 0) return;
549
- warnedPprWindowCapped.add(routeName);
550
- console.warn(
551
- `[rango] Route "${routeName}": its shell is stored with ttl ${shell.ttl} ` +
516
+ warnOnce(
517
+ "ppr-window-capped",
518
+ routeName,
519
+ () =>
520
+ `[rango] Route "${routeName}": its shell is stored with ttl ${shell.ttl} ` +
552
521
  `and swr ${shell.swr}, below its ${reduced.join(" and ")}. A shell ` +
553
522
  `never outlives the route cache() entry (ttl ${cache.ttl}, swr ` +
554
523
  `${cache.swr}) it was captured from, so a document request and a ` +
@@ -558,9 +527,6 @@ export function warnPprWindowCappedOnce(
558
527
  );
559
528
  }
560
529
 
561
- /** Routes (or paths) already warned that a partition skips their build shell. */
562
- const partitionBuildShellWarned = new Set<string>();
563
-
564
530
  /**
565
531
  * Paths a partitioned request found without a build shell. Build shells are
566
532
  * per path (a param route prerenders some of its paths), so a negative is
@@ -578,7 +544,7 @@ export function partitionBuildShellCheckDone(
578
544
  routeName: string | undefined,
579
545
  ): boolean {
580
546
  return (
581
- partitionBuildShellWarned.has(routeName ?? pathname) ||
547
+ hasWarnedOnce("partition-build-shell", routeName ?? pathname) ||
582
548
  pathsWithoutBuildShell.has(pathname)
583
549
  );
584
550
  }
@@ -606,11 +572,11 @@ export function notePartitionBuildShellCheck(
606
572
  return;
607
573
  }
608
574
  // Concurrent first requests can both probe; one of them warns.
609
- const route = routeName ?? pathname;
610
- if (partitionBuildShellWarned.has(route)) return;
611
- partitionBuildShellWarned.add(route);
612
- console.warn(
613
- `[rango] Route ${routeName ? `"${routeName}" ` : ""}("${pathname}") has a ` +
575
+ warnOnce(
576
+ "partition-build-shell",
577
+ routeName ?? pathname,
578
+ () =>
579
+ `[rango] Route ${routeName ? `"${routeName}" ` : ""}("${pathname}") has a ` +
614
580
  "build-time shell, but its request partition (a cache({ key }) enclosing the route, " +
615
581
  "or the store's keyGenerator) keys its shell per partition, so the build " +
616
582
  "shell is not served and each partition captures its own at runtime. A " +
@@ -619,14 +585,12 @@ export function notePartitionBuildShellCheck(
619
585
  }
620
586
 
621
587
  /**
622
- * @internal Reset the serve path's once-per-key warnings and buffered tail
623
- * timings. Tests only (testing/serve-shell-request.ts resetShellTestState).
588
+ * @internal Reset the serve path's once-per-key warnings (the shell path's
589
+ * one registry), its build-shell probe memo and buffered tail timings. Tests
590
+ * only (testing/serve-shell-request.ts resetShellTestState).
624
591
  */
625
592
  export function resetShellServeStateForTests(): void {
626
- warnedMissingStore.clear();
627
- warnedNonceActive.clear();
628
- partitionBuildShellWarned.clear();
593
+ resetShellWarningsForTests();
629
594
  pathsWithoutBuildShell.clear();
630
- warnedPprWindowCapped.clear();
631
595
  lastTailTimingsForServerTiming.clear();
632
596
  }
@@ -6,6 +6,7 @@
6
6
  * matching, so they can run in parallel with segment resolution.
7
7
  */
8
8
 
9
+ import { requestHeaders } from "../server/request-headers.js";
9
10
  import type { HandlerContext } from "./handler-context.js";
10
11
  import type { SSRModule, SSRRenderOptions } from "./types.js";
11
12
  import type { SSRStreamMode } from "../router/router-options.js";
@@ -183,7 +184,7 @@ export function getSSRSetup<TEnv>(
183
184
  */
184
185
  function acceptsFlightExplicitly(request: Request, url: URL): boolean {
185
186
  if (url.searchParams.has("__html")) return false;
186
- const accept = request.headers.get("accept");
187
+ const accept = requestHeaders(request).get("accept");
187
188
  if (accept === null || !accept.includes(RSC_WIRE_MIME)) return false;
188
189
  return prefersFlightRepresentation(parseAcceptTypes(accept));
189
190
  }
@@ -206,7 +207,7 @@ export function mayNeedSSR(request: Request, url: URL): boolean {
206
207
  if (
207
208
  url.searchParams.has("_rsc_partial") ||
208
209
  url.searchParams.has("_rsc_action") ||
209
- request.headers.has("rsc-action") ||
210
+ requestHeaders(request).has("rsc-action") ||
210
211
  url.searchParams.has("_rsc_loader") ||
211
212
  url.searchParams.has("__rsc")
212
213
  ) {
package/src/rsc/types.ts CHANGED
@@ -34,6 +34,8 @@ export interface RscPayload {
34
34
  resolvedIds?: string[];
35
35
  /** Merged route params from the matched route */
36
36
  params?: Record<string, string>;
37
+ /** Matched route name, when named (transition({ when }) route names). */
38
+ routeName?: string;
37
39
  slots?: Record<string, SlotState>;
38
40
  /**
39
41
  * Intercept TARGET route names reachable from this location as a
@@ -125,6 +125,14 @@ export interface RenderSegmentsOptions {
125
125
  /** Seeded descendant client-route pending state for testing. */
126
126
  outletPending?: boolean;
127
127
 
128
+ /**
129
+ * This commit's transition({ when }) decision was false
130
+ * (browser/transition-when.ts): the navigation is urgent, so no segment
131
+ * animates. Every router <ViewTransition> stays in the tree with each class
132
+ * "none", so the element type never changes; keys never read this (#995).
133
+ */
134
+ transitionGatedOff?: boolean;
135
+
128
136
  /**
129
137
  * Intercept segments to inject into the tree.
130
138
  * These are parallel segments from intercept routes that need to be
@@ -143,13 +151,31 @@ export interface RenderSegmentsOptions {
143
151
  rootLayout?: ComponentType<RootLayoutProps>;
144
152
  }
145
153
 
154
+ /**
155
+ * On a gated-off commit every segment keeps its <ViewTransition> so the
156
+ * element type never changes with a per-navigation `transition({ when })`
157
+ * decision (#995), but animates nothing: "none" for every class, whatever the
158
+ * transition type.
159
+ */
160
+ const GATED_OFF_CLASSES = {
161
+ enter: "none",
162
+ exit: "none",
163
+ update: "none",
164
+ share: "none",
165
+ default: "none",
166
+ } as const;
167
+
146
168
  function createViewTransitionBoundary(
147
169
  transition: NonNullable<ResolvedSegment["transition"]>,
148
170
  children: ReactNode,
149
171
  ): ReactNode {
150
- // `viewTransition` is a router-specific flag (boundary opt-out), not a React
151
- // <ViewTransition> prop — strip it so it never reaches React.
152
- const { viewTransition: _viewTransition, ...vtProps } = transition;
172
+ // `viewTransition` (boundary opt-out) and `when` (the browser-run gate) are
173
+ // router fields, not React <ViewTransition> props — strip them.
174
+ const {
175
+ viewTransition: _viewTransition,
176
+ when: _when,
177
+ ...vtProps
178
+ } = transition;
153
179
  return createElement(ReactViewTransition, {
154
180
  ...vtProps,
155
181
  // The commit after an optimistic clientUrls() presentation repaints the
@@ -306,6 +332,7 @@ export async function renderSegments(
306
332
  forceAwait,
307
333
  outletPending = false,
308
334
  rootLayout: RootLayout,
335
+ transitionGatedOff,
309
336
  } = options || {};
310
337
 
311
338
  const segDebug = INTERNAL_RANGO_DEBUG;
@@ -358,8 +385,11 @@ export async function renderSegments(
358
385
  // same-route navigation reconciles (holds content) instead of remounting. The
359
386
  // value is a static property of the route's position in the tree, so it is the
360
387
  // same on every render of that route (SSR, navigation, action) — the keys
361
- // never drift. Cross-route navigation still remounts: different routes have
362
- // different segment ids regardless of transition scope.
388
+ // never drift. It reads the static `transition` config only, never the
389
+ // per-navigation `when` decision (transitionGatedOff): a server-side `when`
390
+ // used to delete `transition`, which flipped this value and remounted the
391
+ // route twice (#995). Cross-route navigation still remounts: different routes
392
+ // have different segment ids regardless of transition scope.
363
393
  const inTransitionScope = normalizedSegments.some(
364
394
  (s) =>
365
395
  s.transition != null &&
@@ -522,10 +552,13 @@ export async function renderSegments(
522
552
  transition &&
523
553
  transition.viewTransition !== false
524
554
  ) {
555
+ const boundary = transitionGatedOff
556
+ ? { ...transition, ...GATED_OFF_CLASSES }
557
+ : transition;
525
558
  if (node.segment.type === "layout") {
526
- outletContent = wrapDefaultOutletContent(outletContent, transition);
559
+ outletContent = wrapDefaultOutletContent(outletContent, boundary);
527
560
  } else {
528
- nodeContent = createViewTransitionBoundary(transition, nodeContent);
561
+ nodeContent = createViewTransitionBoundary(boundary, nodeContent);
529
562
  }
530
563
  }
531
564