@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
@@ -47,6 +47,7 @@ import { type MetricsStore } from "./context.js";
47
47
  import { observePhase, PHASES } from "../router/instrument.js";
48
48
  import { getFetchableLoader } from "./fetchable-loader-store.js";
49
49
  import type { SegmentCacheStore } from "../cache/types.js";
50
+ import type { RouterVersions } from "../router-versions.js";
50
51
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
51
52
  import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
52
53
  import type { TransitionWhenRecord } from "../router/transition-when.js";
@@ -377,7 +378,7 @@ export interface RequestContext<
377
378
 
378
379
  /**
379
380
  * @internal Loader-family snapshot seed for a shell HIT's tail render:
380
- * segment-key -> the capture's elided container (already Flight-deserialized
381
+ * loader id -> the capture's elided container (already Flight-deserialized
381
382
  * by serveShellHit) plus its capture-computed hole bit. resolveLoaderData
382
383
  * overlays it onto the fresh run's container (recorded paths pinned,
383
384
  * hole-marker paths keep the fresh nested promises) so the payload's baked
@@ -385,7 +386,9 @@ export interface RequestContext<
385
386
  * and does not run the loader. Two owners: serveShellHit sets it on the
386
387
  * HIT tail's derived context, and matchPartialWithPprReplay sets it on the
387
388
  * request context of a PPR partial replay once its doc record hits, and
388
- * restores the previous value after the match.
389
+ * restores the previous value after the match. Read through servedPins
390
+ * (loader-cache.ts) only: it also decides which of the record's
391
+ * loader-owned handle pushes stand (loaderPins).
389
392
  */
390
393
  _shellLoaderSeed?: Map<
391
394
  string,
@@ -446,9 +449,7 @@ export interface RequestContext<
446
449
  * scope replaces any route-derived cache() scope for the tail's match
447
450
  * (resolveShellImplicitCacheScope), and a lookup that does not hit throws
448
451
  * ShellRecordUnavailableError instead of resolving segments
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.
452
+ * (withCacheLookup): a HIT never runs a handler.
452
453
  */
453
454
  docTail?: true;
454
455
  /**
@@ -857,6 +858,15 @@ export interface RequestContext<
857
858
  * lookups per router (route-map-builder.ts) */
858
859
  _routerId?: string;
859
860
 
861
+ /**
862
+ * @internal Cache versions of the router serving this request. The cache
863
+ * factory builds a store per request without knowing the router, so a store
864
+ * reads its key versions from here, lazily, the same way CFCacheStore
865
+ * resolves its base URL (getCacheVersions, server/build-version-table.ts).
866
+ * Unset on a context no router created; a store then uses the build version.
867
+ */
868
+ _versions?: RouterVersions;
869
+
860
870
  /**
861
871
  * @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
862
872
  * to avoid a second resolveRoute call. Cleared on HMR invalidation.
@@ -966,6 +976,7 @@ export type PublicRequestContext<
966
976
  | "_tracing"
967
977
  | "_basename"
968
978
  | "_routerId"
979
+ | "_versions"
969
980
  | "_setStatus"
970
981
  | "_rotateStateCookie"
971
982
  | "_setKeepCacheDirective"
@@ -1130,8 +1141,13 @@ export interface CreateRequestContextOptions<TEnv> {
1130
1141
  themeConfig?: ResolvedThemeConfig | null;
1131
1142
  /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
1132
1143
  stateCookieName?: string;
1133
- /** Build version, used as the prefix of a server-rotated rango state value. */
1134
- version?: string;
1144
+ /**
1145
+ * The versions of the router serving this request (resolveRouterVersions).
1146
+ * Stored as `_versions` for the cache stores; `document` is also the prefix
1147
+ * of a server-rotated rango state value, which the browser compares with the
1148
+ * payload's `metadata.version`.
1149
+ */
1150
+ versions?: RouterVersions;
1135
1151
  }
1136
1152
 
1137
1153
  /**
@@ -1159,7 +1175,7 @@ export function createRequestContext<TEnv>(
1159
1175
  build = false,
1160
1176
  themeConfig,
1161
1177
  stateCookieName,
1162
- version: stateVersion,
1178
+ versions,
1163
1179
  } = options;
1164
1180
  // ctx.request.headers is guarded like headers() (#976); every router read
1165
1181
  // of this request's headers goes through requestHeaders().
@@ -1194,6 +1210,12 @@ export function createRequestContext<TEnv>(
1194
1210
  // Set-Cookie drain (rsc/helpers.ts) run outside any latched funnel scope,
1195
1211
  // where assertCachedHeaderWriteAllowed is a no-op.
1196
1212
  let rawStubHeaders = stubResponse.headers;
1213
+ // Node's util.inspect unwraps this Proxy and calls undici's Headers hook
1214
+ // with the proxy as `this`. That reads `#headersList` and throws (#1000).
1215
+ // Inspect does not consult the get trap, so the hook is an own property of
1216
+ // the target and formats `rawStubHeaders`. Read on each call: setStatus
1217
+ // rebinds that let to a new Headers.
1218
+ const NODE_INSPECT = Symbol.for("nodejs.util.inspect.custom");
1197
1219
  const guardedStubHeaders: Headers = new Proxy(new Headers(), {
1198
1220
  get(_target, prop) {
1199
1221
  const raw = rawStubHeaders;
@@ -1208,6 +1230,22 @@ export function createRequestContext<TEnv>(
1208
1230
  return (value as (...a: unknown[]) => unknown).bind(raw);
1209
1231
  },
1210
1232
  });
1233
+ Object.defineProperty(guardedStubHeaders, NODE_INSPECT, {
1234
+ value(...args: unknown[]): unknown {
1235
+ const hook = (rawStubHeaders as unknown as Record<symbol, unknown>)[
1236
+ NODE_INSPECT
1237
+ ];
1238
+ if (typeof hook === "function") return hook.apply(rawStubHeaders, args);
1239
+ // DOM Headers has forEach and not entries. Node always has the hook above.
1240
+ const listed: Record<string, string> = {};
1241
+ rawStubHeaders.forEach((value, key) => {
1242
+ listed[key] = value;
1243
+ });
1244
+ return listed;
1245
+ },
1246
+ configurable: true,
1247
+ writable: true,
1248
+ });
1211
1249
  const shadowStubHeaders = (res: Response): void => {
1212
1250
  Object.defineProperty(res, "headers", {
1213
1251
  value: guardedStubHeaders,
@@ -1429,7 +1467,7 @@ export function createRequestContext<TEnv>(
1429
1467
  const prevRaw =
1430
1468
  (requestHeaders(request).get("x-rango-state") || null) ??
1431
1469
  getRawCookieValue(cookieHeader, stateCookieName);
1432
- const value = mintStateValue(stateVersion ?? "0", prevRaw);
1470
+ const value = mintStateValue(versions?.document ?? "0", prevRaw);
1433
1471
  // rawStubHeaders: guard-exempt internal writer — invalidateClientCache()
1434
1472
  // is documented callable from loaders and during shell capture.
1435
1473
  rawStubHeaders.append(
@@ -1505,6 +1543,7 @@ export function createRequestContext<TEnv>(
1505
1543
 
1506
1544
  _handleStore: handleStore,
1507
1545
  _cacheStore: cacheStore,
1546
+ _versions: versions,
1508
1547
  _searchParamsFilter: searchParamsFilter,
1509
1548
  _explicitTaggedStores: explicitTaggedStores,
1510
1549
  _requestTags: new Set<string>(),
@@ -1515,11 +1554,23 @@ export function createRequestContext<TEnv>(
1515
1554
 
1516
1555
  waitUntil(fn: () => Promise<void>): void {
1517
1556
  if (ctx.build) return;
1518
- // Wrap in Promise.resolve().then(fn) so a SYNCHRONOUS throw in a
1557
+ // A deferred task does not rely on the platform carrying the request's
1558
+ // async context into it. Deployed workerd lost it once ("use cache"
1559
+ // background revalidation, cache-runtime.ts); Node and miniflare never
1560
+ // do, so no local run shows a loss. A cache store builds its key inside
1561
+ // the task and reads the serving router's versions and the request host
1562
+ // from the context: without it the write lands under the whole-build
1563
+ // pair, which no router with its own version reads. So the task
1564
+ // re-enters the context it was scheduled under: the store active then
1565
+ // (a derived context), else this one.
1566
+ const scheduledUnder = requestContextStorage.getStore() ?? ctx;
1567
+ // Wrap in Promise.resolve().then(...) so a SYNCHRONOUS throw in a
1519
1568
  // non-async callback becomes a rejected promise handed to the host's
1520
1569
  // waitUntil (logged as a background failure), instead of escaping into
1521
1570
  // the request flow. Mirrors fireAndForgetWaitUntil's deferral.
1522
- const task = Promise.resolve().then(fn);
1571
+ const task = Promise.resolve().then(() =>
1572
+ requestContextStorage.run(scheduledUnder, fn),
1573
+ );
1523
1574
  // Track the task promise so the PPR shell capture can settle the
1524
1575
  // foreground's deferred cache writes before its own match/render (the
1525
1576
  // ordering edge; see _pendingBackgroundTasks). The capture task itself
@@ -7,7 +7,10 @@ import {
7
7
  } from "../browser/react/filter-segment-order.js";
8
8
  import { ThemeProvider } from "../theme/ThemeProvider.js";
9
9
  import { NonceContext } from "../browser/react/nonce-context.js";
10
- import { NavigationStoreContext } from "../browser/react/context.js";
10
+ import {
11
+ LocationStateContext,
12
+ NavigationStoreContext,
13
+ } from "../browser/react/context.js";
11
14
  import type { NavigationStoreContextValue } from "../browser/react/context.js";
12
15
  import type { HandleData } from "../browser/types.js";
13
16
  import type { ResolvedSegment } from "../types.js";
@@ -119,11 +122,17 @@ function createSsrEventController(opts: {
119
122
  flushRouteState: () => {},
120
123
  setHandleData: () => {},
121
124
  getHandleState: () => handleState,
125
+ freezeHydrationHandleState: () => {},
126
+ // The server render is the state the document is rendered with.
127
+ getHydrationHandleState: () => undefined,
122
128
  setRouteSegmentIds: () => {},
123
129
  setParams: (nextParams) => {
124
130
  params = nextParams;
125
131
  },
126
132
  getParams: () => params,
133
+ commitLocationState: () => {},
134
+ getLocationState: () => undefined,
135
+ takeTreelessLocationState: () => false,
127
136
  setLocation: () => {},
128
137
  startNavigation: () => {
129
138
  throw new Error("Navigation not supported during SSR");
@@ -294,7 +303,7 @@ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
294
303
  };
295
304
 
296
305
  // Build content tree from segments.
297
- // Order must match NavigationProvider: NavigationStoreContext > NonceContext > ThemeProvider > content
306
+ // Order must match NavigationProvider: NavigationStoreContext > LocationStateContext > NonceContext > ThemeProvider > content
298
307
  // Memoize like payload/handles above: renderSegments is async, so
299
308
  // React.use() on a fresh promise suspends and replays SsrRoot, which
300
309
  // would re-run the entire segment-tree build on every initial render.
@@ -324,10 +333,13 @@ export function createSsrRootComponent(opts: SsrRootOptions): React.FC {
324
333
  <NonceContext.Provider value={nonce}>{content}</NonceContext.Provider>
325
334
  );
326
335
 
327
- // Wrap with NavigationStoreContext for useNavigation hook
336
+ // Wrap with NavigationStoreContext for useNavigation hook. The server has
337
+ // no history entry: location state is undefined, as readers hydrate.
328
338
  return (
329
339
  <NavigationStoreContext.Provider value={ssrContextValue!}>
330
- {content}
340
+ <LocationStateContext.Provider value={undefined}>
341
+ {content}
342
+ </LocationStateContext.Provider>
331
343
  </NavigationStoreContext.Provider>
332
344
  );
333
345
  };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Testing primitive for the per-router cache versions.
3
+ *
4
+ * A production build gives every `createRouter()` a data version and a
5
+ * document version (docs/design/per-app-cache-version.md) and ships them as a
6
+ * table in the `@rangojs/router:version` module. A test has no build, so the
7
+ * module is a stub and every router serves unversioned. `setBuildVersions`
8
+ * installs a table in its place; the request handler, `dispatch` and the cache
9
+ * stores then resolve versions through the same code path the built app uses.
10
+ * Calling it again is a deploy: the next request is served by a handler that
11
+ * resolved the new versions.
12
+ */
13
+ import { installRouterVersionsTable } from "../server/build-version-table.js";
14
+ import {
15
+ DEFAULT_ROUTER_VERSIONS_KEY,
16
+ type RouterVersions,
17
+ type RouterVersionsTable,
18
+ } from "../router-versions.js";
19
+
20
+ export type { RouterVersions };
21
+
22
+ /**
23
+ * The versions a simulated build ships.
24
+ *
25
+ * - `data` and `document`: the versions of every router the test does not list
26
+ * under `routers` (the build's whole-build pair).
27
+ * - `routers`: versions per router id (`router.id`), as a host-router build
28
+ * computes them.
29
+ */
30
+ export interface BuildVersions extends RouterVersions {
31
+ routers?: Record<string, RouterVersions>;
32
+ }
33
+
34
+ /**
35
+ * Install the cache versions of a simulated build, or remove them with no
36
+ * argument (routers serve unversioned again, the default in tests).
37
+ *
38
+ * A router that sets `createRouter({ version })` keeps that value for both
39
+ * versions, exactly as in production.
40
+ *
41
+ * @example A deploy that changes the client only keeps cached data
42
+ * ```ts
43
+ * setBuildVersions({ data: "d1", document: "h1" });
44
+ * await serveShellRequest(router, "/product/1", { cacheStore });
45
+ * setBuildVersions({ data: "d1", document: "h2" });
46
+ * const { shellStatus } = await serveShellRequest(router, "/product/1", {
47
+ * cacheStore,
48
+ * });
49
+ * // shellStatus is "MISS": the stored HTML was keyed by document version h1.
50
+ * ```
51
+ */
52
+ export function setBuildVersions(versions?: BuildVersions): void {
53
+ if (!versions) {
54
+ installRouterVersionsTable(undefined);
55
+ return;
56
+ }
57
+ const table: Record<string, readonly [string, string]> = {
58
+ [DEFAULT_ROUTER_VERSIONS_KEY]: [versions.data, versions.document],
59
+ };
60
+ for (const [routerId, pair] of Object.entries(versions.routers ?? {})) {
61
+ table[routerId] = [pair.data, pair.document];
62
+ }
63
+ installRouterVersionsTable(table satisfies RouterVersionsTable);
64
+ }
@@ -106,6 +106,7 @@ import {
106
106
  runWithRequestContext,
107
107
  setRequestContextParams,
108
108
  } from "../server/request-context.js";
109
+ import { resolveRouterVersions } from "../server/build-version-table.js";
109
110
  import { executeMiddleware, matchMiddleware } from "../router/middleware.js";
110
111
  import type {
111
112
  MiddlewareEntry,
@@ -166,6 +167,8 @@ import { applyStreamIdleTimeout } from "../rsc/stream-idle.js";
166
167
  interface DispatchableRouter<TEnv> {
167
168
  id?: string;
168
169
  routerId?: string;
170
+ /** createRouter({ version }), the consumer-set version. */
171
+ version?: string;
169
172
  routeMap: Record<string, unknown>;
170
173
  middleware: MiddlewareEntry<TEnv>[];
171
174
  onError?: OnErrorCallback<TEnv>;
@@ -411,6 +414,9 @@ export async function dispatch<TEnv = any>(
411
414
  cacheStore,
412
415
  searchParamsFilter,
413
416
  cacheProfiles: router.cacheProfiles,
417
+ // The store keys with the router's versions, resolved as the production
418
+ // handler resolves them (rsc/handler.ts).
419
+ versions: resolveRouterVersions(routerId, router.version),
414
420
  });
415
421
  // Wire background error reporting so cache degradation (reportCacheError ->
416
422
  // _reportBackgroundError) reaches the router's onError, mirroring the production
@@ -18,5 +18,6 @@ export type {
18
18
  RenderRouteOptions,
19
19
  TestRouterHandle,
20
20
  RenderRouteResult,
21
+ RenderRouteHydrateResult,
21
22
  HandleDataSeed,
22
23
  } from "./render-route.js";
@@ -63,4 +63,5 @@ export {
63
63
  export type {
64
64
  ServeShellRequestOptions,
65
65
  ServeShellRequestResult,
66
+ ShellRequestHandles,
66
67
  } from "./serve-shell-request.js";
@@ -98,6 +98,9 @@ export type {
98
98
  ShellStatusTarget,
99
99
  } from "./shell-status.js";
100
100
 
101
+ export { setBuildVersions } from "./build-versions.js";
102
+ export type { BuildVersions, RouterVersions } from "./build-versions.js";
103
+
101
104
  export { collectHandle } from "./collect-handle.js";
102
105
 
103
106
  export { withLocationStateKey } from "./location-state-key.js";
@@ -19,6 +19,7 @@ import { normalizeBasename } from "../../router/basename.js";
19
19
  import {
20
20
  seedVariables,
21
21
  resolveSeededStateCookieName,
22
+ seededVersions,
22
23
  type VarsInit,
23
24
  type StateCookieSeed,
24
25
  } from "./seed-vars.js";
@@ -164,7 +165,7 @@ export function createTestRequestContext<TEnv>(
164
165
  cacheStore: opts.cacheStore,
165
166
  cacheProfiles: opts.cacheProfiles,
166
167
  stateCookieName,
167
- version: opts.stateCookie?.version,
168
+ versions: seededVersions(opts.stateCookie),
168
169
  });
169
170
  if (opts.basename !== undefined)
170
171
  ctx._basename = normalizeBasename(opts.basename);
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { contextSet, type ContextVar } from "../../context-var.js";
10
10
  import { resolveStateCookieName } from "../../router/state-cookie-name.js";
11
+ import { uniformVersions, type RouterVersions } from "../../router-versions.js";
11
12
 
12
13
  export interface StateCookieSeed {
13
14
  /**
@@ -22,8 +23,10 @@ export interface StateCookieSeed {
22
23
  */
23
24
  routerId?: string;
24
25
  /**
25
- * Build version used as the rotated value's prefix (`{version}:{timestamp}`).
26
- * Defaults to `"0"` (resolved inside createRequestContext).
26
+ * The app's version, as `createRouter({ version })` would set it: the
27
+ * rotated value's prefix (`{version}:{timestamp}`) and the version a cache
28
+ * store keys with. Defaults to `"0"` for the prefix (resolved inside
29
+ * createRequestContext) and to the build versions for the store.
27
30
  */
28
31
  version?: string;
29
32
  }
@@ -32,6 +35,15 @@ export function resolveSeededStateCookieName(seed?: StateCookieSeed): string {
32
35
  return resolveStateCookieName(seed?.prefix, seed?.routerId ?? "router_0");
33
36
  }
34
37
 
38
+ /** The request context's `versions` for a seeded `stateCookie.version`. */
39
+ export function seededVersions(
40
+ seed?: StateCookieSeed,
41
+ ): RouterVersions | undefined {
42
+ return seed?.version === undefined
43
+ ? undefined
44
+ : uniformVersions(seed.version);
45
+ }
46
+
35
47
  export type VarsInit =
36
48
  | Record<string, unknown>
37
49
  | ReadonlyArray<readonly [ContextVar<unknown> | string, unknown]>;
@@ -1,12 +1,11 @@
1
1
  import {
2
+ isLocationStateKey,
3
+ LOCATION_STATE_KEY_PREFIX,
4
+ LOCATION_STATE_KEY_SUFFIX_SEPARATOR,
2
5
  peekLocationStateKey,
3
6
  type LocationStateDefinition,
4
7
  } from "../browser/react/location-state-shared.js";
5
8
 
6
- // Typed history.state slots must carry this prefix: buildHistoryState spreads
7
- // only `__rsc_ls_*` keys onto history.state (history-state.ts).
8
- const KEY_PREFIX = "__rsc_ls_";
9
-
10
9
  let syntheticKeyCounter = 0;
11
10
 
12
11
  /**
@@ -16,11 +15,15 @@ let syntheticKeyCounter = 0;
16
15
  * use (`MyState(value)`, `useLocationState(MyState)`, `.read()`).
17
16
  *
18
17
  * - `withLocationStateKey(GridState, "GridState")` sets `__rsc_ls_GridState`
19
- * (a name that already starts with `__rsc_ls_` is used as-is).
18
+ * (a name that already starts with `__rsc_ls_` is used as-is). A definition
19
+ * with `clearOnReload` appends its suffix, so read the stored key back from
20
+ * `GridState.__rsc_ls_key` (`__rsc_ls_GridState~r`).
20
21
  * - Without a name, a key already set (by the plugin or an earlier call) is
21
22
  * kept; otherwise the definition gets a synthetic `__rsc_ls_test_<n>` key,
22
23
  * which then stays on it.
23
24
  *
25
+ * A name may not contain "~": that separator is reserved for the suffix.
26
+ *
24
27
  * Returns the definition.
25
28
  *
26
29
  * @example
@@ -35,11 +38,18 @@ export function withLocationStateKey<
35
38
  TDefinition extends LocationStateDefinition<any, any>,
36
39
  >(definition: TDefinition, name?: string): TDefinition {
37
40
  if (name !== undefined) {
38
- definition.__rsc_ls_key = name.startsWith(KEY_PREFIX)
41
+ if (name.includes(LOCATION_STATE_KEY_SUFFIX_SEPARATOR)) {
42
+ throw new Error(
43
+ `withLocationStateKey: "${name}" contains "${LOCATION_STATE_KEY_SUFFIX_SEPARATOR}", ` +
44
+ "which is reserved for the key suffix of the `clearOnReload` option. " +
45
+ "Pass the name without a suffix; the definition appends its own.",
46
+ );
47
+ }
48
+ definition.__rsc_ls_key = isLocationStateKey(name)
39
49
  ? name
40
- : KEY_PREFIX + name;
50
+ : LOCATION_STATE_KEY_PREFIX + name;
41
51
  } else if (!peekLocationStateKey(definition)) {
42
- definition.__rsc_ls_key = `${KEY_PREFIX}test_${syntheticKeyCounter++}`;
52
+ definition.__rsc_ls_key = `${LOCATION_STATE_KEY_PREFIX}test_${syntheticKeyCounter++}`;
43
53
  }
44
54
  return definition;
45
55
  }
@@ -37,6 +37,7 @@ import type { LoaderDefinition } from "../types.js";
37
37
  import {
38
38
  seedVariables,
39
39
  resolveSeededStateCookieName,
40
+ seededVersions,
40
41
  type VarsInit,
41
42
  type StateCookieSeed,
42
43
  } from "./internal/seed-vars.js";
@@ -253,7 +254,7 @@ export async function renderHandler<TEnv = any>(
253
254
  variables: seedVariables({}, opts.vars),
254
255
  build: opts.build,
255
256
  stateCookieName,
256
- version: opts.stateCookie?.version,
257
+ versions: seededVersions(opts.stateCookie),
257
258
  cacheStore: opts.cacheStore,
258
259
  cacheProfiles: opts.cacheProfiles,
259
260
  themeConfig: