@rangojs/router 0.20.0 → 0.21.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 (178) hide show
  1. package/dist/bin/rango.js +26 -14
  2. package/dist/types/browser/event-controller.d.ts +32 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  5. package/dist/types/browser/rango-state.d.ts +3 -1
  6. package/dist/types/browser/react/context.d.ts +11 -0
  7. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  8. package/dist/types/browser/react/location-state.d.ts +7 -2
  9. package/dist/types/browser/react/subscribe-to-nothing.d.ts +7 -0
  10. package/dist/types/cache/cache-scope.d.ts +4 -5
  11. package/dist/types/cache/cf/cf-cache-store.d.ts +27 -6
  12. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  13. package/dist/types/cache/handle-snapshot.d.ts +36 -50
  14. package/dist/types/cache/index.d.ts +2 -0
  15. package/dist/types/cache/shell-snapshot.d.ts +19 -3
  16. package/dist/types/cache/types.d.ts +21 -10
  17. package/dist/types/cache/vercel/vercel-cache-store.d.ts +28 -16
  18. package/dist/types/client-urls/navigation.d.ts +7 -1
  19. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  20. package/dist/types/handles/deferred-resolution.d.ts +11 -1
  21. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  22. package/dist/types/router/match-middleware/cache-lookup.d.ts +1 -1
  23. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  24. package/dist/types/router/router-interfaces.d.ts +2 -1
  25. package/dist/types/router/router-options.d.ts +17 -7
  26. package/dist/types/router/segment-resolution/loader-cache.d.ts +21 -9
  27. package/dist/types/router-versions.d.ts +39 -0
  28. package/dist/types/rsc/handler-context.d.ts +5 -0
  29. package/dist/types/rsc/shell-build-manifest.d.ts +3 -2
  30. package/dist/types/rsc/types.d.ts +4 -6
  31. package/dist/types/server/build-version-table.d.ts +44 -0
  32. package/dist/types/server/handle-store.d.ts +87 -46
  33. package/dist/types/server/request-context.d.ts +22 -8
  34. package/dist/types/testing/build-versions.d.ts +32 -0
  35. package/dist/types/testing/dom.entry.d.ts +1 -1
  36. package/dist/types/testing/flight.entry.d.ts +1 -1
  37. package/dist/types/testing/index.d.ts +2 -0
  38. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  39. package/dist/types/testing/location-state-key.d.ts +5 -1
  40. package/dist/types/testing/render-route.d.ts +77 -6
  41. package/dist/types/testing/serve-shell-request.d.ts +21 -0
  42. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  43. package/dist/types/types/handler-context.d.ts +8 -5
  44. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  45. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  46. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  47. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  48. package/dist/types/vite/discovery/state.d.ts +14 -0
  49. package/dist/types/vite/encryption-key.d.ts +18 -0
  50. package/dist/types/vite/plugin-types.d.ts +27 -0
  51. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  52. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  53. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  54. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  55. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  56. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  57. package/dist/types/vite/router-discovery.d.ts +6 -0
  58. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  59. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  60. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  61. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  62. package/dist/vite/index.js +1537 -444
  63. package/package.json +1 -2
  64. package/skills/cache-guide/SKILL.md +84 -20
  65. package/skills/caching/SKILL.md +16 -5
  66. package/skills/client-urls/SKILL.md +16 -0
  67. package/skills/cloudflare/SKILL.md +2 -1
  68. package/skills/comparison/references/framework-comparison.md +16 -12
  69. package/skills/hooks/SKILL.md +3 -0
  70. package/skills/hooks/handle-and-actions.md +6 -0
  71. package/skills/hooks/navigation.md +28 -0
  72. package/skills/hooks/state.md +209 -8
  73. package/skills/hooks/urls.md +40 -1
  74. package/skills/host-router/SKILL.md +13 -0
  75. package/skills/intercept/SKILL.md +6 -0
  76. package/skills/loader/SKILL.md +19 -1
  77. package/skills/middleware/SKILL.md +2 -0
  78. package/skills/ppr/SKILL.md +119 -40
  79. package/skills/prerender/SKILL.md +7 -3
  80. package/skills/rango/SKILL.md +8 -7
  81. package/skills/router-setup/SKILL.md +15 -1
  82. package/skills/testing/SKILL.md +30 -26
  83. package/skills/testing/client-components.md +193 -8
  84. package/skills/testing/handles.md +2 -0
  85. package/skills/testing/loader.md +1 -1
  86. package/skills/testing/middleware.md +1 -1
  87. package/skills/testing/render-handler.md +18 -18
  88. package/skills/testing/serve-shell-request.md +63 -14
  89. package/skills/testing/server-actions.md +17 -17
  90. package/skills/testing/server-tree.md +1 -1
  91. package/skills/typesafety/generated-files-and-cli.md +1 -1
  92. package/skills/use-cache/SKILL.md +16 -5
  93. package/skills/vercel/SKILL.md +3 -3
  94. package/src/bin/rango.ts +1 -1
  95. package/src/browser/event-controller.ts +75 -2
  96. package/src/browser/history-state.ts +39 -17
  97. package/src/browser/navigation-bridge.ts +41 -18
  98. package/src/browser/navigation-transaction.ts +15 -10
  99. package/src/browser/partial-update.ts +10 -2
  100. package/src/browser/rango-state.ts +3 -1
  101. package/src/browser/react/Link.tsx +2 -3
  102. package/src/browser/react/NavigationProvider.tsx +25 -1
  103. package/src/browser/react/context.ts +13 -0
  104. package/src/browser/react/location-state-shared.ts +200 -34
  105. package/src/browser/react/location-state.ts +40 -75
  106. package/src/browser/react/subscribe-to-nothing.ts +7 -0
  107. package/src/browser/react/use-handle.ts +20 -1
  108. package/src/browser/rsc-router.tsx +33 -11
  109. package/src/browser/server-action-bridge.ts +17 -1
  110. package/src/cache/cache-scope.ts +18 -8
  111. package/src/cache/cf/cf-cache-store.ts +75 -55
  112. package/src/cache/cf/cf-cache-types.ts +12 -4
  113. package/src/cache/handle-snapshot.ts +69 -87
  114. package/src/cache/index.ts +4 -0
  115. package/src/cache/shell-snapshot.ts +35 -5
  116. package/src/cache/types.ts +21 -10
  117. package/src/cache/vercel/vercel-cache-store.ts +48 -20
  118. package/src/client-urls/client-root.tsx +1 -0
  119. package/src/client-urls/navigation.ts +10 -0
  120. package/src/client-urls/optimistic-location.ts +10 -4
  121. package/src/handles/deferred-resolution.ts +31 -0
  122. package/src/prerender/build-shell-capture.ts +11 -9
  123. package/src/router/manifest.ts +2 -0
  124. package/src/router/match-middleware/cache-lookup.ts +11 -18
  125. package/src/router/navigation-snapshot.ts +25 -6
  126. package/src/router/request-classification.ts +15 -12
  127. package/src/router/router-interfaces.ts +2 -1
  128. package/src/router/router-options.ts +17 -7
  129. package/src/router/segment-resolution/loader-cache.ts +95 -60
  130. package/src/router-versions.ts +60 -0
  131. package/src/rsc/full-payload.ts +6 -2
  132. package/src/rsc/handler-context.ts +5 -0
  133. package/src/rsc/handler.ts +14 -4
  134. package/src/rsc/rsc-rendering.ts +12 -1
  135. package/src/rsc/shell-build-manifest.ts +10 -7
  136. package/src/rsc/shell-capture.ts +36 -26
  137. package/src/rsc/types.ts +4 -6
  138. package/src/server/build-version-table.ts +115 -0
  139. package/src/server/handle-store.ts +344 -127
  140. package/src/server/request-context.ts +62 -11
  141. package/src/ssr/ssr-root.tsx +16 -4
  142. package/src/testing/build-versions.ts +64 -0
  143. package/src/testing/dispatch.ts +6 -0
  144. package/src/testing/dom.entry.ts +1 -0
  145. package/src/testing/flight.entry.ts +1 -0
  146. package/src/testing/index.ts +3 -0
  147. package/src/testing/internal/context.ts +2 -1
  148. package/src/testing/internal/seed-vars.ts +14 -2
  149. package/src/testing/location-state-key.ts +18 -8
  150. package/src/testing/render-handler.ts +2 -1
  151. package/src/testing/render-route.tsx +277 -43
  152. package/src/testing/serve-shell-request.ts +80 -11
  153. package/src/testing/vitest-stubs/version.ts +4 -1
  154. package/src/types/handler-context.ts +8 -5
  155. package/src/vite/discovery/build-versions.ts +1281 -0
  156. package/src/vite/discovery/installed-packages.ts +159 -0
  157. package/src/vite/discovery/prerender-collection.ts +38 -1
  158. package/src/vite/discovery/router-versions-phase.ts +336 -0
  159. package/src/vite/discovery/shell-prerender-phase.ts +28 -38
  160. package/src/vite/discovery/state.ts +19 -0
  161. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  162. package/src/vite/encryption-key.ts +101 -9
  163. package/src/vite/plugin-types.ts +28 -0
  164. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  165. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  166. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  167. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  168. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  169. package/src/vite/plugins/version-plugin.ts +19 -24
  170. package/src/vite/plugins/version.d.ts +17 -2
  171. package/src/vite/plugins/virtual-entries.ts +31 -5
  172. package/src/vite/rango.ts +28 -3
  173. package/src/vite/router-discovery.ts +82 -13
  174. package/src/vite/utils/client-chunks.ts +5 -1
  175. package/src/vite/utils/compare-strings.ts +9 -0
  176. package/src/vite/utils/package-resolution.ts +9 -0
  177. package/src/vite/utils/prerender-utils.ts +4 -1
  178. package/src/vite/utils/shared-utils.ts +1 -1
@@ -17,6 +17,25 @@ export type HandleData = Record<string, Record<string, unknown[]>>;
17
17
  * Reads data directly from the store for each segment in order.
18
18
  */
19
19
  export declare function buildHandleSnapshot(handleStore: HandleStore, segmentOrder: string[]): HandleData;
20
+ /**
21
+ * What a restored record is to one loader's handle pushes on this request
22
+ * (loader-cache.ts loaderPins; docs/design/handle-push-ownership.md). Two
23
+ * answers let the record's copies stand, two make them placeholders, and in
24
+ * each pair one also speaks for the loader's other pushes:
25
+ * - "pin": the request serves the loader's value from the pin stored with
26
+ * the record, and the record lists every settled push of that run. Its
27
+ * copies stand, and any other settled push of the loader is dropped.
28
+ * - "copies": its copies stand and nothing else is known: a dependency of
29
+ * pinned loaders, or a pin written before captures recorded every push.
30
+ * - "hole": the route runs the loader on this request (it registers it and
31
+ * the record does not pin it). Its copies are placeholders and a push made
32
+ * in its body is its own, with or without copies.
33
+ * - "placeholders": its copies are placeholders and nothing else is known: a
34
+ * dependency the route does not register while a pin is missing (the
35
+ * record does not name the loader that ran it). Without copies its push
36
+ * counts for the loader that awaits it.
37
+ */
38
+ export type RecordAuthority = "pin" | "copies" | "hole" | "placeholders";
20
39
  /**
21
40
  * HandleStore tracks pending handler promises and stores handle data.
22
41
  *
@@ -81,69 +100,86 @@ export interface HandleStore {
81
100
  */
82
101
  push(handleName: string, segmentId: string, data: unknown, loaderPush?: boolean, owner?: string): void;
83
102
  /**
84
- * push() a loader-cache HIT's replayed value, recorded from `loaderId`'s
85
- * body (loader-cache.ts replayLoaderHandles). If that loader then runs live
86
- * in this request, its first push removes every value replayed for it and
87
- * takes the first one's position (later live pushes follow it), so the
88
- * handle output stays live without a duplicate. A push counts as that
89
- * loader's when it is made in its body, or in a body it entered that has
90
- * no replayed values of its own (the capture credits a dependency's push
91
- * under a live-lane loader to that loader); a hole's run that ends without
92
- * one drops them (settleLoaderRun).
103
+ * push() a cached unit's copy of a value `loaderId`'s body pushed
104
+ * (handle-snapshot.ts appendHandles: a loader-cache or "use cache" HIT). If
105
+ * that loader then runs live in this request, its first push removes every
106
+ * copy pushed for it (replays and placeholders) and takes the first one's
107
+ * position, and the pushes of that run follow in push order, a dependency's
108
+ * included, so the handle output stays live without a duplicate. A push
109
+ * counts as that loader's when it is made in its body, or in a body it
110
+ * entered that has no copies of its own (the capture credits a dependency's
111
+ * push under a live-lane loader to that loader). A run that ends without a
112
+ * push leaves a replay in place, unless the loader is a hole
113
+ * (settleLoaderRun).
93
114
  */
94
115
  pushReplayed(handleName: string, segmentId: string, data: unknown, loaderId: string): void;
95
116
  /**
96
- * push() a document HIT's copy of `loaderId`'s settled push, restored from
97
- * the shell's doc record (handle-snapshot.ts restoreHandles). The record is
98
- * authoritative for that loader's settled pushes: the prelude was rendered
99
- * from it, and a run of the loader on the HIT reads the store, not the
100
- * capture's values. So from then on this request drops every settled,
101
- * thenable-free push the loader makes (live, or replayed from a cache
102
- * entry, anywhere inside its body), and keeps the ones holding a thenable
103
- * (holdsThenable: what the record could not keep, `runs`), placed among the
104
- * restored copies in the loader's push order. Only bake-lane owners are
105
- * restored this way (withCacheLookup): a live-lane loader is a hole.
117
+ * push() a record's copy of `loaderId`'s push as a placeholder
118
+ * (handle-snapshot.ts restoreHandles): the record does not supply that
119
+ * loader's value on this request, so its own source decides its pushes. A
120
+ * live run replaces it as it does a replay; a run that ends without a push
121
+ * drops it (settleLoaderRun); a cached unit that claims the loader delivers
122
+ * in its place (replacePlaceholders).
123
+ */
124
+ pushPlaceholder(handleName: string, segmentId: string, data: unknown, loaderId: string): void;
125
+ /**
126
+ * push() a record's copy of `loaderId`'s settled push that stands
127
+ * (restoreHandles): the record supplies the loader's value too. From then
128
+ * on this request drops every settled, thenable-free push the loader makes
129
+ * (live, or replayed from a cache entry, anywhere inside its body), and
130
+ * keeps the ones holding a thenable (holdsThenable: what the record could
131
+ * not keep), placed among the restored copies in the loader's push order.
106
132
  */
107
133
  pushRestored(handleName: string, segmentId: string, data: unknown, loaderId: string): void;
108
134
  /**
109
- * The loaders the route runs on the live lane (liveLaneLoaderIds), from a
110
- * PPR shell record's restore (handle-snapshot.ts restoreHandles): the
111
- * holes. A push counts for a restored loader when that loader is the
112
- * replayed one or the innermost enclosing loader body that restored
113
- * copies; the walk out through enclosing bodies stops at a hole's body,
114
- * and a replay of a hole's push counts for none. So a restored loader
115
- * never takes a push made inside a hole it awaited, while a body inside the
116
- * hole that restored copies of its own (a dependency the capture ran under
117
- * a bake-lane loader) still takes its pushes.
135
+ * What the record this request restored is to each loader (RecordAuthority;
136
+ * CacheScope.lookupRouteDetailed sets it on a hit). A "pin" loader counts
137
+ * as restored with or without copies. A hole is a "hole" loader or one with
138
+ * placeholders: the walk out through enclosing loader bodies that finds the
139
+ * loader a push counts for stops at its body, so a pinned loader never
140
+ * takes a push made inside a hole it awaited.
118
141
  */
119
- markLiveLane(loaderIds: ReadonlySet<string>): void;
142
+ setRecordAuthority(authority: (loaderId: string) => RecordAuthority): void;
120
143
  /**
121
144
  * `loaderId`'s run in this request settled (resolved or rejected), its
122
- * pushes made (loader-resolution.ts createLoaderExecutor). When the loader
123
- * is a hole (markLiveLane), values replayed for it that no push of the run
124
- * replaced are dropped: the run's output decides, so a hole that throws or
125
- * skips its push shows none rather than the capture's. Any other loader's
126
- * replayed values stay: a bake-lane loader's record is authoritative for
127
- * its settled pushes, as its pin is for its data. Restored copies are not
128
- * replays and stand.
145
+ * pushes made (loader-resolution.ts createLoaderExecutor). Its placeholders
146
+ * that no push of the run replaced are dropped: the run's output decides,
147
+ * so a loader that throws or skips its push shows none rather than the
148
+ * record's. A hole's replays go with them. Any other loader's replays stay
149
+ * (a dependency another entry's HIT replayed), and so do restored copies.
129
150
  */
130
151
  settleLoaderRun(loaderId: string): void;
131
152
  /**
132
- * A hole's own cache() HIT (loader-cache.ts replayLoaderHandles, once it
133
- * claims the hole): the values replayed for hole `loaderId` before (its
134
- * shell copies, which restoreHandles leaves unclaimed, those of the
135
- * dependencies credited to it included) give way to the pushes the entry
136
- * recorded, none when it recorded none, so they match the entry's data.
137
- * `deliver` makes the pushReplayed calls, which take the removed values'
138
- * place in each array. Any other loader, or a hole with nothing replayed,
139
- * delivers as a plain replay.
153
+ * A cached unit's HIT delivering the pushes it recorded for `loaderIds`,
154
+ * the loaders it claimed (appendHandles). Their placeholders give way to
155
+ * what `deliver` pushes for them, nothing when it pushes nothing, so the
156
+ * page shows the pushes of the run that produced the unit's value. The
157
+ * pushReplayed calls `deliver` makes take the removed values' place in each
158
+ * array, in order. Without placeholders it is a plain replay.
140
159
  */
141
- redeliverReplays(loaderId: string, deliver: () => void): void;
160
+ replacePlaceholders(loaderIds: readonly string[], deliver: () => void): void;
142
161
  /**
143
162
  * Get all collected handle data after all handlers have settled.
144
163
  * Waits for `settled` (handler lane), then returns the data at that point.
145
164
  */
146
165
  getData(): Promise<HandleData>;
166
+ /**
167
+ * Fix the document lane at the store's current state. From then on
168
+ * `stream("settled")` yields this state and nothing newer, and
169
+ * `streamLate()` delivers every change made after it, whether or not it
170
+ * beat the handler barrier. The first call wins. It must run before the
171
+ * document stream is first read: a stream that started on the live state
172
+ * stays on it (reported through `onError` in development).
173
+ *
174
+ * A PPR shell HIT calls it once its record is replayed and before a loader
175
+ * runs (rsc-rendering.ts serveShellHit). The prelude was rendered from the
176
+ * record's handle data (resolvedHandleStream `recordedOnly`), so that is
177
+ * what the client must hydrate with. What the request's loaders then push,
178
+ * replace or drop is theirs, not the shell's: in the document snapshot it
179
+ * rendered elements the prelude never had, or left out ones it has (issue
180
+ * #1035), and it got there or not by a race with the lane's first read.
181
+ */
182
+ freezeDocumentSnapshot(): void;
147
183
  /**
148
184
  * Get an async iterator that yields handle data on each push.
149
185
  * Completes at the chosen settlement barrier: "fullySettled" (default —
@@ -152,6 +188,9 @@ export interface HandleStore {
152
188
  * blocking positions and must not wait on loader bodies).
153
189
  * Each yield contains the full accumulated state (not just the delta).
154
190
  * Safe for concurrent consumers (per-consumer version cursor).
191
+ * The "settled" lane of a frozen store (freezeDocumentSnapshot) yields the
192
+ * frozen state once, at once, and ends. That yield is the store's own
193
+ * frozen object: a consumer must not write to it.
155
194
  */
156
195
  stream(until?: "settled" | "fullySettled"): AsyncGenerator<HandleData, void, unknown>;
157
196
  /**
@@ -160,7 +199,9 @@ export interface HandleStore {
160
199
  * fullySettled. Returns without yielding when the auxiliary lane is empty
161
200
  * by the time `settled` resolves. The client consumes this post-hydration
162
201
  * (rsc-router.tsx) and merges via the same application path as nav-lane
163
- * progressive handle updates.
202
+ * progressive handle updates. On a frozen store (freezeDocumentSnapshot)
203
+ * it starts from the frozen state instead: a change made before the
204
+ * handler barrier is late too.
164
205
  */
165
206
  streamLate(): AsyncGenerator<HandleData, void, unknown>;
166
207
  /**
@@ -20,6 +20,7 @@ import { type ContextVar } from "../context-var.js";
20
20
  import { type HandleStore, type HandleData } from "./handle-store.js";
21
21
  import { type MetricsStore } from "./context.js";
22
22
  import type { SegmentCacheStore } from "../cache/types.js";
23
+ import type { RouterVersions } from "../router-versions.js";
23
24
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
24
25
  import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
25
26
  import type { TransitionWhenRecord } from "../router/transition-when.js";
@@ -283,7 +284,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
283
284
  _routeRecordWindow?: RouteRecordWindow;
284
285
  /**
285
286
  * @internal Loader-family snapshot seed for a shell HIT's tail render:
286
- * segment-key -> the capture's elided container (already Flight-deserialized
287
+ * loader id -> the capture's elided container (already Flight-deserialized
287
288
  * by serveShellHit) plus its capture-computed hole bit. resolveLoaderData
288
289
  * overlays it onto the fresh run's container (recorded paths pinned,
289
290
  * hole-marker paths keep the fresh nested promises) so the payload's baked
@@ -291,7 +292,9 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
291
292
  * and does not run the loader. Two owners: serveShellHit sets it on the
292
293
  * HIT tail's derived context, and matchPartialWithPprReplay sets it on the
293
294
  * request context of a PPR partial replay once its doc record hits, and
294
- * restores the previous value after the match.
295
+ * restores the previous value after the match. Read through servedPins
296
+ * (loader-cache.ts) only: it also decides which of the record's
297
+ * loader-owned handle pushes stand (loaderPins).
295
298
  */
296
299
  _shellLoaderSeed?: Map<string, import("../cache/shell-snapshot.js").ShellLoaderSeedEntry>;
297
300
  /**
@@ -348,9 +351,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
348
351
  * scope replaces any route-derived cache() scope for the tail's match
349
352
  * (resolveShellImplicitCacheScope), and a lookup that does not hit throws
350
353
  * ShellRecordUnavailableError instead of resolving segments
351
- * (withCacheLookup): a HIT never runs a handler. The record's
352
- * loader-owned handle pushes restore as authoritative
353
- * (HandleStore.pushRestored): the prelude rendered them.
354
+ * (withCacheLookup): a HIT never runs a handler.
354
355
  */
355
356
  docTail?: true;
356
357
  /**
@@ -708,6 +709,14 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
708
709
  /** @internal Owning router id; scopes the search-schema/root-scope registry
709
710
  * lookups per router (route-map-builder.ts) */
710
711
  _routerId?: string;
712
+ /**
713
+ * @internal Cache versions of the router serving this request. The cache
714
+ * factory builds a store per request without knowing the router, so a store
715
+ * reads its key versions from here, lazily, the same way CFCacheStore
716
+ * resolves its base URL (getCacheVersions, server/build-version-table.ts).
717
+ * Unset on a context no router created; a store then uses the build version.
718
+ */
719
+ _versions?: RouterVersions;
711
720
  /**
712
721
  * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
713
722
  * to avoid a second resolveRoute call. Cleared on HMR invalidation.
@@ -748,7 +757,7 @@ export interface RouteRecordWindow {
748
757
  * This is the type exported to library consumers. Internal code should
749
758
  * use the full RequestContext interface directly.
750
759
  */
751
- export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_searchParamsFilter" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureUnrecordedLoaderPush" | "_shellCaptureLoaderLanes" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_routeRecordWindow" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_locationState" | "_routeName" | "_prevRouteKey" | "_inActionRevalidation" | "_reportedErrors" | "_renderErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
760
+ export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_searchParamsFilter" | "_shellCaptureRun" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureUnrecordedLoaderPush" | "_shellCaptureLoaderLanes" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_routeRecordWindow" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_locationState" | "_routeName" | "_prevRouteKey" | "_inActionRevalidation" | "_reportedErrors" | "_renderErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_versions" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
752
761
  /**
753
762
  * Marker for a waitUntil-scheduled fn whose task promise must NOT enter
754
763
  * _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
@@ -824,8 +833,13 @@ export interface CreateRequestContextOptions<TEnv> {
824
833
  themeConfig?: ResolvedThemeConfig | null;
825
834
  /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
826
835
  stateCookieName?: string;
827
- /** Build version, used as the prefix of a server-rotated rango state value. */
828
- version?: string;
836
+ /**
837
+ * The versions of the router serving this request (resolveRouterVersions).
838
+ * Stored as `_versions` for the cache stores; `document` is also the prefix
839
+ * of a server-rotated rango state value, which the browser compares with the
840
+ * payload's `metadata.version`.
841
+ */
842
+ versions?: RouterVersions;
829
843
  }
830
844
  /**
831
845
  * Create a full request context with all methods implemented
@@ -0,0 +1,32 @@
1
+ import { type RouterVersions } from "../router-versions.js";
2
+ export type { RouterVersions };
3
+ /**
4
+ * The versions a simulated build ships.
5
+ *
6
+ * - `data` and `document`: the versions of every router the test does not list
7
+ * under `routers` (the build's whole-build pair).
8
+ * - `routers`: versions per router id (`router.id`), as a host-router build
9
+ * computes them.
10
+ */
11
+ export interface BuildVersions extends RouterVersions {
12
+ routers?: Record<string, RouterVersions>;
13
+ }
14
+ /**
15
+ * Install the cache versions of a simulated build, or remove them with no
16
+ * argument (routers serve unversioned again, the default in tests).
17
+ *
18
+ * A router that sets `createRouter({ version })` keeps that value for both
19
+ * versions, exactly as in production.
20
+ *
21
+ * @example A deploy that changes the client only keeps cached data
22
+ * ```ts
23
+ * setBuildVersions({ data: "d1", document: "h1" });
24
+ * await serveShellRequest(router, "/product/1", { cacheStore });
25
+ * setBuildVersions({ data: "d1", document: "h2" });
26
+ * const { shellStatus } = await serveShellRequest(router, "/product/1", {
27
+ * cacheStore,
28
+ * });
29
+ * // shellStatus is "MISS": the stored HTML was keyed by document version h1.
30
+ * ```
31
+ */
32
+ export declare function setBuildVersions(versions?: BuildVersions): void;
@@ -12,4 +12,4 @@
12
12
  * (`happy-dom` or `jsdom`).
13
13
  */
14
14
  export { renderRoute } from "./render-route.js";
15
- export type { RenderRouteSpec, RenderRouteOptions, TestRouterHandle, RenderRouteResult, HandleDataSeed, } from "./render-route.js";
15
+ export type { RenderRouteSpec, RenderRouteOptions, TestRouterHandle, RenderRouteResult, RenderRouteHydrateResult, HandleDataSeed, } from "./render-route.js";
@@ -31,4 +31,4 @@ export type { RenderServerTreeOptions, RenderServerTreeResult, ClientBoundary, B
31
31
  export { renderHandler } from "./render-handler.js";
32
32
  export type { TestableHandler, RenderHandlerOptions, RenderHandlerResult, StateCookieSeed, } from "./render-handler.js";
33
33
  export { serveShellRequest, resetShellTestState, } from "./serve-shell-request.js";
34
- export type { ServeShellRequestOptions, ServeShellRequestResult, } from "./serve-shell-request.js";
34
+ export type { ServeShellRequestOptions, ServeShellRequestResult, ShellRequestHandles, } from "./serve-shell-request.js";
@@ -48,6 +48,8 @@ export type { ExpectedCacheStatus, CacheStatusTarget, CacheSink, } from "./cache
48
48
  export type { TelemetryEvent, TelemetrySink, CacheDecisionEvent, CacheSegmentSignal, CacheSegmentStatus, } from "../router/telemetry.js";
49
49
  export { assertPprReplayStatus, assertShellStatus, parsePprReplayStatus, parseShellStatus, PPR_REPLAY_STATUS_HEADER, shellCacheKey, SHELL_STATUS_HEADER, } from "./shell-status.js";
50
50
  export type { PprReplayBypassReason, PprReplayStatus, ShellCachePartition, ShellStatus, ShellStatusTarget, } from "./shell-status.js";
51
+ export { setBuildVersions } from "./build-versions.js";
52
+ export type { BuildVersions, RouterVersions } from "./build-versions.js";
51
53
  export { collectHandle } from "./collect-handle.js";
52
54
  export { withLocationStateKey } from "./location-state-key.js";
53
55
  export { diffGeneratedRoutes, assertGeneratedRoutesMatch, } from "./generated-routes.js";
@@ -7,6 +7,7 @@
7
7
  * client/browser modules).
8
8
  */
9
9
  import { type ContextVar } from "../../context-var.js";
10
+ import { type RouterVersions } from "../../router-versions.js";
10
11
  export interface StateCookieSeed {
11
12
  /**
12
13
  * Cookie-name prefix, sanitized then composed with `routerId` exactly like
@@ -20,11 +21,15 @@ export interface StateCookieSeed {
20
21
  */
21
22
  routerId?: string;
22
23
  /**
23
- * Build version used as the rotated value's prefix (`{version}:{timestamp}`).
24
- * Defaults to `"0"` (resolved inside createRequestContext).
24
+ * The app's version, as `createRouter({ version })` would set it: the
25
+ * rotated value's prefix (`{version}:{timestamp}`) and the version a cache
26
+ * store keys with. Defaults to `"0"` for the prefix (resolved inside
27
+ * createRequestContext) and to the build versions for the store.
25
28
  */
26
29
  version?: string;
27
30
  }
28
31
  export declare function resolveSeededStateCookieName(seed?: StateCookieSeed): string;
32
+ /** The request context's `versions` for a seeded `stateCookie.version`. */
33
+ export declare function seededVersions(seed?: StateCookieSeed): RouterVersions | undefined;
29
34
  export type VarsInit = Record<string, unknown> | ReadonlyArray<readonly [ContextVar<unknown> | string, unknown]>;
30
35
  export declare function seedVariables(variables: Record<string, unknown>, vars?: VarsInit): Record<string, unknown>;
@@ -6,11 +6,15 @@ import { type LocationStateDefinition } from "../browser/react/location-state-sh
6
6
  * use (`MyState(value)`, `useLocationState(MyState)`, `.read()`).
7
7
  *
8
8
  * - `withLocationStateKey(GridState, "GridState")` sets `__rsc_ls_GridState`
9
- * (a name that already starts with `__rsc_ls_` is used as-is).
9
+ * (a name that already starts with `__rsc_ls_` is used as-is). A definition
10
+ * with `clearOnReload` appends its suffix, so read the stored key back from
11
+ * `GridState.__rsc_ls_key` (`__rsc_ls_GridState~r`).
10
12
  * - Without a name, a key already set (by the plugin or an earlier call) is
11
13
  * kept; otherwise the definition gets a synthetic `__rsc_ls_test_<n>` key,
12
14
  * which then stays on it.
13
15
  *
16
+ * A name may not contain "~": that separator is reserved for the suffix.
17
+ *
14
18
  * Returns the definition.
15
19
  *
16
20
  * @example
@@ -35,11 +35,19 @@
35
35
  * useRouter().replace kind "replace", refresh() kind "revalidate".
36
36
  * Back/forward, action and optimistic clientUrls() swap decisions are
37
37
  * e2e territory.
38
+ * - Location state reaches readers the way it does in production:
39
+ * NavigationProvider holds the committed entry's state next to the
40
+ * payload, so a navigation whose loader the test holds keeps the entry
41
+ * being left on screen with its own state. A `popstate` event is a
42
+ * back/forward onto the entry history.state holds; the tree has one
43
+ * location, so no page is restored with it. A server action's state, a
44
+ * redirect's and an intercept's are e2e territory.
38
45
  * What it DOES cover: client hooks that read NavigationProvider /
39
46
  * OutletContext — useParams, useReverse, useHref, useMount, useNavigation,
40
47
  * useRouter, usePathname, useSearchParams, Outlet/useOutlet nesting and seeded
41
48
  * descendant pending state, useLoader/useFetchLoader (seeded data),
42
- * useLocationState (seeded), and useHandle (seeded).
49
+ * useLocationState (seeded, or written by a navigation), and useHandle
50
+ * (seeded).
43
51
  * Basename-mounted apps: pass the `basename` option so useRouter().basename,
44
52
  * <Link> prefixing, and useMount/useHref resolve against the mount prefix
45
53
  * (without it they resolve at the root "/"). For an include("/shop", ...)
@@ -48,10 +56,10 @@
48
56
  */
49
57
  import { type ComponentType } from "react";
50
58
  import type { RenderResult } from "@testing-library/react";
51
- import type { NavigationStore } from "../browser/types.js";
59
+ import type { NavigateOptions, NavigationStore } from "../browser/types.js";
52
60
  import type { EventController } from "../browser/event-controller.js";
53
61
  import type { LoaderDefinition, TransitionConfig } from "../types.js";
54
- import type { LocationStateDefinition } from "../browser/react/location-state-shared.js";
62
+ import { type LocationStateDefinition } from "../browser/react/location-state-shared.js";
55
63
  import type { Handle } from "../handle.js";
56
64
  import type { ThemeConfig } from "../theme/types.js";
57
65
  import type { PrefetchStrategy } from "../router/prefetch-default.js";
@@ -159,6 +167,8 @@ export interface RenderRouteOptions {
159
167
  * `useLocationState(StateDef)`. Like loaders, a real `createLocationState()`
160
168
  * handle has an empty injected key in a bare test, so pass `[def, value]`
161
169
  * pairs; renderRoute assigns a synthetic key and writes it to `history.state`.
170
+ * With `hydrate: true` a `clearOnReload` definition's seed is removed before
171
+ * hydration, as on a document load.
162
172
  *
163
173
  * @example
164
174
  * renderRoute([{ path: "/", Component: FlashBanner }], {
@@ -192,6 +202,24 @@ export interface RenderRouteOptions {
192
202
  * computes the segment id for you). Merged with `handles`.
193
203
  */
194
204
  handle?: HandleDataSeed;
205
+ /**
206
+ * Handle values a document delivers after hydration, as a loader's push
207
+ * that is not in the document's handle snapshot is (a push made after the
208
+ * handler output settled, or any loader push on a PPR shell HIT). Each
209
+ * entry is `[handle, values]`: the handle's values after the update, which
210
+ * replace its `handles` seed once the root has hydrated (or mounted). With
211
+ * `hydrate: true` the server HTML is rendered from `handles`, and a reader
212
+ * inside a `<Suspense>` boundary hydrates after the update was applied, as
213
+ * a boundary that hydrates after the root does in production.
214
+ *
215
+ * @example
216
+ * const { serverHtml, recoverableErrors } = await renderRoute(routes, {
217
+ * hydrate: true,
218
+ * handles: [[Notes, ["from-handler"]]],
219
+ * lateHandles: [[Notes, ["from-handler", "from-loader"]]],
220
+ * });
221
+ */
222
+ lateHandles?: ReadonlyArray<readonly [Handle<any, any>, unknown[]]>;
195
223
  /**
196
224
  * Route name -> pattern map. Informational for parity with the server test
197
225
  * context; client useReverse takes its map directly as an argument, so this
@@ -253,6 +281,25 @@ export interface RenderRouteOptions {
253
281
  * not override a `"none"` default.
254
282
  */
255
283
  defaultPrefetch?: PrefetchStrategy;
284
+ /**
285
+ * Hydrate instead of mounting fresh: the same element is rendered to HTML
286
+ * (react-dom/server `renderToString`), placed in the container and hydrated.
287
+ * The result gains `serverHtml` and `recoverableErrors`
288
+ * (RenderRouteHydrateResult). `data-hydrated` is set on `<html>` after
289
+ * hydration, as production's root does, and removed on unmount.
290
+ *
291
+ * Limits:
292
+ * - The server pass runs without `window` and `document`, and only those. A
293
+ * hook that reads a global that stays defined there (`history`,
294
+ * `localStorage`, `navigator`) outside a server snapshot renders the same
295
+ * in both passes here and can still mismatch in real SSR.
296
+ * - Content that suspends in the server pass (`lazy`, a pending or untracked
297
+ * `use()` promise) is emitted as its fallback and client-rendered, not
298
+ * hydrated; React reports the boundary in `recoverableErrors`. Outside any
299
+ * `<Suspense>` it rejects.
300
+ * - Needs @testing-library/react >= 16.2.0; throws on older versions.
301
+ */
302
+ hydrate?: boolean;
256
303
  }
257
304
  /**
258
305
  * Imperative handle returned alongside the RTL result.
@@ -282,10 +329,17 @@ export interface TestRouterHandle {
282
329
  * `options.transition: false` is the per-navigation opt-out
283
330
  * (`router.push(url, { transition: false })`, `<Link transition={false}>`):
284
331
  * the commit is urgent and no transition({ when }) predicate is called.
332
+ *
333
+ * `options.state` and `options.replace: true` write the history entry the
334
+ * way `useRouter().push(url, { state })` and `.replace(url, { state })` do,
335
+ * so a test can carry location state on a navigation whose loader it
336
+ * holds: `state` pushes an entry with it, `replace: true` replaces the
337
+ * current entry (with `state`, or with none, which drops the entry's
338
+ * location state as a production replace does). With neither, history is
339
+ * left alone and readers keep the state they have: `navigate(url)` and
340
+ * `navigate(url, { replace: false })` are the same call.
285
341
  */
286
- navigate(url: string, options?: Pick<RenderRouteOptions, "loaders"> & {
287
- transition?: boolean;
288
- }): Promise<void>;
342
+ navigate(url: string, options?: Pick<RenderRouteOptions, "loaders"> & Pick<NavigateOptions, "state" | "replace" | "transition">): Promise<void>;
289
343
  /**
290
344
  * Re-render the current location, as router.refresh() does: a spec's
291
345
  * transition({ when }) decides it with kind "revalidate" (`to` is `from`).
@@ -304,4 +358,21 @@ export interface TestRouterHandle {
304
358
  export type RenderRouteResult = RenderResult & {
305
359
  router: TestRouterHandle;
306
360
  };
361
+ /** Result of renderRoute with `hydrate: true`. */
362
+ export type RenderRouteHydrateResult = RenderRouteResult & {
363
+ /** The HTML the server pass produced, as placed in the container. */
364
+ serverHtml: string;
365
+ /**
366
+ * Messages of the errors React recovered from (`onRecoverableError`): a
367
+ * hydration mismatch that made it re-render on the client, or a boundary the
368
+ * server pass could not finish. Empty means the tree hydrated as rendered.
369
+ * Live: React appends for as long as the root is mounted. An attribute-only
370
+ * mismatch is NOT here — React keeps the server attribute and only logs it
371
+ * with `console.error` in development.
372
+ */
373
+ recoverableErrors: string[];
374
+ };
375
+ export declare function renderRoute(routes: RenderRouteSpec[], options: RenderRouteOptions & {
376
+ hydrate: true;
377
+ }): Promise<RenderRouteHydrateResult>;
307
378
  export declare function renderRoute(routes: RenderRouteSpec[], options?: RenderRouteOptions): Promise<RenderRouteResult>;
@@ -33,6 +33,7 @@
33
33
  */
34
34
  import { type Rango } from "../router/router-interfaces.js";
35
35
  import type { SegmentCacheStore, ShellCacheEntry } from "../cache/types.js";
36
+ import type { HandleData } from "../server/handle-store.js";
36
37
  import { type PprReplayStatus, type ShellStatus } from "./shell-status.js";
37
38
  /** Options for {@link serveShellRequest}. */
38
39
  export interface ServeShellRequestOptions<TEnv = any> {
@@ -60,6 +61,21 @@ export interface ServeShellRequestOptions<TEnv = any> {
60
61
  segments?: readonly string[];
61
62
  };
62
63
  }
64
+ /** A response's handle data, as the browser consumes it. */
65
+ export interface ShellRequestHandles {
66
+ /**
67
+ * What the document hydrates with: `metadata.handles`, read to its end
68
+ * before hydration starts. For a `partial` request, the last state the
69
+ * navigation streamed.
70
+ */
71
+ hydration: HandleData;
72
+ /**
73
+ * The states `metadata.handlesLate` delivered, in order. Each one replaces
74
+ * the client's handle data after hydration. Empty when nothing arrived
75
+ * late, and for a `partial` request.
76
+ */
77
+ late: HandleData[];
78
+ }
63
79
  /** Result of {@link serveShellRequest}. */
64
80
  export interface ServeShellRequestResult {
65
81
  /** The handler's Response. Its body is already read: see `body`. */
@@ -96,6 +112,11 @@ export interface ServeShellRequestResult {
96
112
  * (CFCacheStore, VercelCacheStore) it warms the memo like any other read.
97
113
  */
98
114
  readEntry(): Promise<ShellCacheEntry | null>;
115
+ /**
116
+ * Decode the handle data of `flight` as the browser reads it, deferred
117
+ * values resolved. Undefined when no Flight was rendered.
118
+ */
119
+ readHandles(): Promise<ShellRequestHandles | undefined>;
99
120
  }
100
121
  /**
101
122
  * Reset the per-isolate PPR state requests leave behind, so the next test
@@ -1 +1,2 @@
1
1
  export declare const VERSION = "";
2
+ export declare const ROUTER_VERSIONS: undefined;
@@ -342,11 +342,14 @@ export type InternalHandlerContext<TParams = {}, TEnv = DefaultEnv, TSearch exte
342
342
  */
343
343
  _loaderCacheOriginalUse?: (item: any) => any;
344
344
  /**
345
- * @internal A loader-cache HIT asks before replaying a loader's recorded
346
- * pushes: true when that loader has not run in this request and no other
347
- * replay delivered it; its later run then replaces the replayed values
348
- * (HandleStore.pushReplayed). One copy per loader per request. See
349
- * setupLoaderAccess.
345
+ * @internal A loader-cache or "use cache" HIT asks before replaying a
346
+ * loader's recorded pushes: true when that loader has not run in this
347
+ * request and no other replay delivered it; its later run then replaces
348
+ * the replayed values (HandleStore.pushReplayed). One copy per loader per
349
+ * request. A record's restore does not ask (restoreHandles): its copies
350
+ * are the pinned loader's own, or placeholders the claimed replay
351
+ * replaces. Absent on a stale refresh's ctx, whose pushes are diverted
352
+ * (handle-snapshot.ts appendHandles). See setupLoaderAccess.
350
353
  */
351
354
  _claimLoaderPushes?: (loaderId: string) => boolean;
352
355
  /**