@rangojs/router 0.25.0 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  3. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  4. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  5. package/dist/types/cache/shell-snapshot.d.ts +6 -4
  6. package/dist/types/cache/types.d.ts +8 -6
  7. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  8. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  9. package/dist/types/handles/meta.d.ts +5 -0
  10. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  11. package/dist/types/prerender/writable-store.d.ts +2 -3
  12. package/dist/types/router/dev-reload-handoff.d.ts +26 -0
  13. package/dist/types/router/router-interfaces.d.ts +0 -5
  14. package/dist/types/router/router-options.d.ts +0 -20
  15. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  16. package/dist/types/router-versions.d.ts +8 -6
  17. package/dist/types/rsc/shell-capture-constants.d.ts +10 -6
  18. package/dist/types/rsc/shell-capture.d.ts +25 -8
  19. package/dist/types/rsc/types.d.ts +0 -8
  20. package/dist/types/server/build-version-table.d.ts +10 -13
  21. package/dist/types/server/context.d.ts +26 -2
  22. package/dist/types/server/handle-store.d.ts +17 -6
  23. package/dist/types/server/request-context.d.ts +4 -3
  24. package/dist/types/testing/build-versions.d.ts +0 -3
  25. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  26. package/dist/types/testing/serve-shell-request.d.ts +1 -1
  27. package/dist/types/types/loader-types.d.ts +6 -1
  28. package/dist/types/urls/pattern-types.d.ts +7 -6
  29. package/dist/vite/index.js +3 -8
  30. package/package.json +1 -1
  31. package/skills/cache-guide/SKILL.md +4 -8
  32. package/skills/caching/SKILL.md +3 -5
  33. package/skills/comparison/references/framework-comparison.md +1 -1
  34. package/skills/hooks/handle-and-actions.md +6 -6
  35. package/skills/host-router/SKILL.md +1 -1
  36. package/skills/loader/SKILL.md +10 -9
  37. package/skills/ppr/SKILL.md +83 -60
  38. package/skills/prerender/SKILL.md +3 -3
  39. package/skills/router-setup/SKILL.md +0 -2
  40. package/skills/testing/render-handler.md +17 -17
  41. package/skills/testing/serve-shell-request.md +1 -1
  42. package/skills/testing/server-actions.md +16 -16
  43. package/skills/vercel/SKILL.md +1 -1
  44. package/src/cache/cf/cf-cache-store.ts +4 -7
  45. package/src/cache/cf/cf-cache-types.ts +0 -17
  46. package/src/cache/handle-snapshot.ts +2 -2
  47. package/src/cache/shell-snapshot.ts +7 -3
  48. package/src/cache/types.ts +8 -6
  49. package/src/cache/vercel/vercel-cache-store.ts +2 -18
  50. package/src/handles/deferred-resolution.ts +3 -2
  51. package/src/handles/meta.ts +5 -0
  52. package/src/prerender/build-shell-capture.ts +1 -2
  53. package/src/prerender/writable-store.ts +2 -3
  54. package/src/route-definition/dsl-helpers.ts +14 -14
  55. package/src/router/dev-reload-handoff.ts +73 -0
  56. package/src/router/manifest.ts +9 -2
  57. package/src/router/match-middleware/intercept-resolution.ts +2 -2
  58. package/src/router/router-interfaces.ts +0 -6
  59. package/src/router/router-options.ts +0 -21
  60. package/src/router/segment-resolution/loader-cache.ts +4 -3
  61. package/src/router/segment-resolution/mask-nested.ts +5 -3
  62. package/src/router-versions.ts +7 -7
  63. package/src/router.ts +19 -8
  64. package/src/rsc/handler.ts +3 -7
  65. package/src/rsc/rsc-rendering.ts +44 -12
  66. package/src/rsc/shell-capture-constants.ts +10 -6
  67. package/src/rsc/shell-capture.ts +163 -88
  68. package/src/rsc/types.ts +0 -9
  69. package/src/server/build-version-table.ts +12 -24
  70. package/src/server/context.ts +64 -2
  71. package/src/server/handle-store.ts +77 -10
  72. package/src/server/request-context.ts +4 -3
  73. package/src/testing/build-versions.ts +0 -3
  74. package/src/testing/dispatch.ts +1 -3
  75. package/src/testing/internal/seed-vars.ts +4 -4
  76. package/src/testing/serve-shell-request.ts +2 -5
  77. package/src/types/loader-types.ts +6 -1
  78. package/src/urls/pattern-types.ts +7 -6
  79. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
@@ -8,22 +8,22 @@
8
8
 
9
9
  ### Options — `CreateTestContextOptions<TEnv>`
10
10
 
11
- | Field | Type | Meaning |
12
- | --------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`). |
14
- | `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`. |
15
- | `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`). |
16
- | `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`. |
17
- | `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list). |
18
- | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state. |
19
- | `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references). |
20
- | `params` | `Record<string, string>` | Route params on `ctx.params`. |
21
- | `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`. |
22
- | `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
23
- | `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
24
- | `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
25
- | `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
26
- | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (the app's version, as `createRouter({ version })` would set it; value is `{version}:{timestamp}`, default `"0"`). |
11
+ | Field | Type | Meaning |
12
+ | --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`). |
14
+ | `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`. |
15
+ | `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`). |
16
+ | `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`. |
17
+ | `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list). |
18
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state. |
19
+ | `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references). |
20
+ | `params` | `Record<string, string>` | Route params on `ctx.params`. |
21
+ | `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`. |
22
+ | `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
23
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
24
+ | `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
25
+ | `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
26
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (a seeded app version: the value is `{version}:{timestamp}`, default `"0"`). |
27
27
 
28
28
  ### Context — `RequestContext<TEnv>` (what your code receives)
29
29
 
@@ -120,7 +120,7 @@ The cache factory receives `(env, ctx)`: on Vercel `env` is `process.env` and `c
120
120
 
121
121
  `router.prerender()` can warm a route into this store before traffic (`/prerender` → "Warm any route before traffic"). The Runtime Cache is per region, so `VercelCacheStore` declares `scope: "regional"`: a warm fills the region the call runs in. That is all traffic when the project's functions run in one region (the default `iad1` for new projects, and every Hobby project); on a multi-region project it warms one region and the others fill on first use. The router does not detect the region count. The memory store used off-platform above is refused in production and allowed under the dev server.
122
122
 
123
- `getCache()` takes no deployment id: `VercelCacheStore` puts a version in front of every key itself (data version for families `s`, `i`; document version for `r`, `h`; none for tag markers), computed per `createRouter()` from its built code. A deploy that does not change a router keeps its entries; a deploy that changes it reads under new keys. `version` on the store (or on `createRouter`) replaces both versions. To clear the cache on every deploy, set `version` to a per-deploy value or keep a deployment-scoped `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`. Two environments built from the same code share entries if they share a cache namespace; give each its own namespace or `version`.
123
+ `getCache()` takes no deployment id: `VercelCacheStore` puts a version in front of every key itself (data version for families `s`, `i`; document version for `r`, `h`; none for tag markers), computed per `createRouter()` from its built code. A deploy that does not change a router keeps its entries; a deploy that changes it reads under new keys. To clear the cache on every deploy, keep a deployment-scoped `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`. Two environments built from the same code share entries if they share a cache namespace; give each its own namespace.
124
124
 
125
125
  ## Host routers (multi-app)
126
126
 
@@ -500,8 +500,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
500
500
  private readonly namespace?: string;
501
501
  private readonly explicitBaseUrl?: string;
502
502
  private readonly waitUntil?: (fn: () => Promise<void>) => void;
503
- /** The `version` option: used for every versioned family when set. */
504
- private readonly explicitVersion?: string;
505
503
  private readonly edgeLookupTimeoutMs: number;
506
504
  private readonly edgeReadTimeoutMs: number;
507
505
  private readonly kvReadTimeoutMs: number;
@@ -554,7 +552,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
554
552
  // the internal host. Only the explicit override can be captured eagerly.
555
553
  this.explicitBaseUrl = options.baseUrl;
556
554
  this.defaults = options.defaults;
557
- this.explicitVersion = options.version;
558
555
  // Coalesce only finite numbers to the override; a non-finite value (NaN from
559
556
  // `Number(env.UNSET)`, or Infinity) would otherwise sail past `?? DEFAULT`
560
557
  // (which only replaces null/undefined) into setTimeout, where NaN/Infinity
@@ -1628,8 +1625,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1628
1625
  // stamp). Carried on doc L1 entries for the herd guard; never serve them.
1629
1626
  headers.delete(CACHE_EXPIRES_AT_HEADER);
1630
1627
  headers.delete(CACHE_REVALIDATING_AT_HEADER);
1631
- // Finding #3 (read side): strip per-client signals a pre-fix or
1632
- // pinned-version L1 entry may carry. See the read-side note in the design doc.
1628
+ // Finding #3 (read side): strip per-client signals a pre-fix L1 entry
1629
+ // may carry. See the read-side note in the design doc.
1633
1630
  stripPerClientSignals(headers);
1634
1631
  return new Response(response.body, {
1635
1632
  status: response.status,
@@ -2952,7 +2949,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2952
2949
  private keyToRequest(key: string, family: KeyVersion): Request {
2953
2950
  const encodedKey = encodeURIComponent(key);
2954
2951
  return new Request(
2955
- `${this.resolveBaseUrl()}${versionKeyPrefix(this.explicitVersion, family)}${encodedKey}`,
2952
+ `${this.resolveBaseUrl()}${versionKeyPrefix(family)}${encodedKey}`,
2956
2953
  { method: "GET" },
2957
2954
  );
2958
2955
  }
@@ -2976,7 +2973,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2976
2973
  * @internal
2977
2974
  */
2978
2975
  private async toKVKey(key: string, family: KeyVersion): Promise<string> {
2979
- const composed = `${versionKeyPrefix(this.explicitVersion, family)}${key}`;
2976
+ const composed = `${versionKeyPrefix(family)}${key}`;
2980
2977
  if (kvKeyByteLength(composed) <= KV_MAX_KEY_BYTES) return composed;
2981
2978
  const prefix = truncateToBytes(composed, KV_KEY_PRESERVED_PREFIX_BYTES);
2982
2979
  return `${prefix}~${await kvKeyDigest(composed)}`;
@@ -322,23 +322,6 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
322
322
  */
323
323
  tagCacheTtl?: number;
324
324
 
325
- /**
326
- * Cache version string override. When set, this exact value prefixes every
327
- * versioned key the store writes (`v/{version}/...`), for cached data and
328
- * stored HTML alike; changing it makes every earlier entry unreachable.
329
- *
330
- * Leave it unset for the default: the store keys with the versions of the
331
- * router serving the request. Segment entries and `"use cache"` items use
332
- * the router's data version; document responses and PPR shells use its
333
- * document version (see `createRouter({ version })`). Those are computed
334
- * from the router's built code, so a deploy that does not change the router
335
- * keeps its entries.
336
- *
337
- * Tag invalidation markers are never versioned: an invalidation applies to
338
- * the entries of every version, including one that comes back in a rollback.
339
- */
340
- version?: string;
341
-
342
325
  /**
343
326
  * Latency budget (ms) for an L1 edge cache (CF Cache API) read. A `match`
344
327
  * slower than this is abandoned and treated as a miss, so a degraded colo
@@ -177,8 +177,8 @@ export type OwnedPushDelivery = (loaderId: string) => RecordAuthority;
177
177
  * says the record supplies that loader's value too ("pin", "copies"), and is
178
178
  * a placeholder (pushPlaceholder) everywhere else: without `owned`, every
179
179
  * owner. Neither claims its loader, so the loader's own cache() HIT still
180
- * delivers (the thenable pushes a restored copy lets through, or its entry's
181
- * pushes in a placeholder's place).
180
+ * delivers (the pushes holding a promise a restored copy lets through, or its
181
+ * entry's pushes in a placeholder's place).
182
182
  */
183
183
  export function restoreHandles(
184
184
  handles: Record<string, SegmentHandleData>,
@@ -405,12 +405,15 @@ const PIN_KEY_PREFIX = /^[^D.]*D\d+\./;
405
405
  * by loader. A key that does not parse pins nothing.
406
406
  *
407
407
  * Deserializations run in parallel; a record that fails to decode is skipped
408
- * (that loader drifts — the pre-snapshot behavior — instead of failing the
409
- * HIT). Returns undefined when the snapshot carries no loader records, without
410
- * touching the Flight codec (kept lazy for cold paths and non-RSC configs).
408
+ * and reported to `onUndecodable`: its loader runs instead, which a document
409
+ * HIT has already committed a prelude against, so serveShellHit retires the
410
+ * entry (dc-t89). Returns undefined when the snapshot carries no loader
411
+ * records, without touching the Flight codec (kept lazy for cold paths and
412
+ * non-RSC configs).
411
413
  */
412
414
  export async function buildShellLoaderSeed(
413
415
  snapshot: ShellSnapshotRecord[],
416
+ onUndecodable?: (key: string) => void,
414
417
  ): Promise<Map<string, ShellLoaderSeedEntry> | undefined> {
415
418
  const loaderRecords: ShellSnapshotRecord[] = [];
416
419
  for (const rec of snapshot) {
@@ -441,6 +444,7 @@ export async function buildShellLoaderSeed(
441
444
  },
442
445
  ];
443
446
  } catch {
447
+ onUndecodable?.(rec.key);
444
448
  return null;
445
449
  }
446
450
  },
@@ -445,7 +445,7 @@ export interface ShellCacheEntry {
445
445
  /** React.version captured at prerender time; the read-time invalidation gate. */
446
446
  reactVersion: string;
447
447
  /**
448
- * Build version captured at prerender time: the RSC handler's `version`,
448
+ * Build version captured at prerender time: the RSC handler's version,
449
449
  * which is the router's DOCUMENT version (router-versions.ts) — a hash of
450
450
  * its server code and the client asset names by default, a stamp bumped on
451
451
  * dev RSC-module edits. The second read-time gate: a persistent shared
@@ -686,11 +686,13 @@ export interface ShellSnapshotLoaderValue {
686
686
  */
687
687
  holes: 0 | 1;
688
688
  /**
689
- * 1 when the capture saw a loader push it could not record (a deferred
690
- * push, one with masked nested promises, or one made outside any loader
691
- * body): a HIT then still runs the loader body in the background
692
- * (pin-first) so those pushes reach the page. 0: a hole-free record is
693
- * served from the pin alone and its body does not run on a HIT.
689
+ * 1 when the capture saw a loader push it could not record (one whose
690
+ * settled value holds masked nested promises, one a live-lane loader made,
691
+ * or one made outside any loader body): a HIT then still runs the loader
692
+ * body in the background (pin-first) so those pushes reach the page. A
693
+ * pushed promise that settles thenable-free is recorded (#1182).
694
+ * 0: a hole-free record is served from the pin alone and its body does not
695
+ * run on a HIT.
694
696
  *
695
697
  * Both bits are always written. A record stored before they existed (v0.17)
696
698
  * reads each missing bit as 1 (buildShellLoaderSeed): its snapshot lacks
@@ -330,7 +330,7 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
330
330
  *
331
331
  * Vercel does not reconcile entries between deployments; the store does it
332
332
  * through its keys, which carry the serving router's cache versions (see
333
- * `version`). So a deploy that changes a router's code stops reading that
333
+ * `getCacheVersions`). So a deploy that changes a router's code stops reading that
334
334
  * router's old entries, and a deploy that does not keeps them. A namespace
335
335
  * that changes per deployment (`getCache({ namespace: VERCEL_DEPLOYMENT_ID })`)
336
336
  * still works and clears the cache on every deploy, which is what you want
@@ -363,19 +363,6 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
363
363
  defaultKey: string,
364
364
  ) => string | Promise<string>;
365
365
 
366
- /**
367
- * Version override, folded into every versioned key as `v/{version}/...`.
368
- * When set, this exact value is used for cached data and stored HTML alike;
369
- * changing it makes everything this store wrote before unreachable.
370
- *
371
- * Leave it unset for the default: the store keys with the versions of the
372
- * router serving the request, as CFCacheStore does. Segments and `"use
373
- * cache"` items use the router's data version; responses and PPR shells use
374
- * its document version (see `createRouter({ version })`). Tag markers are
375
- * never versioned, so an invalidation applies to every version.
376
- */
377
- version?: string;
378
-
379
366
  /**
380
367
  * Max serialized entry size in bytes before a write is skipped. Defaults to
381
368
  * VERCEL_MAX_ITEM_BYTES (2 MB - the platform's hard cap).
@@ -537,8 +524,6 @@ export class VercelCacheStore<
537
524
 
538
525
  private readonly cache: VercelRuntimeCache;
539
526
  private readonly waitUntil?: (promise: Promise<unknown>) => void;
540
- /** The `version` option: used for every versioned family when set. */
541
- private readonly explicitVersion?: string;
542
527
  private readonly maxItemBytes: number;
543
528
  private readonly memo: ResolvedShellMemoOptions;
544
529
  private readonly handleMemos: VercelHandleMemos;
@@ -563,7 +548,6 @@ export class VercelCacheStore<
563
548
  this.waitUntil = options.waitUntil;
564
549
  this.defaults = options.defaults;
565
550
  this.keyGenerator = options.keyGenerator;
566
- this.explicitVersion = options.version;
567
551
  this.maxItemBytes = options.maxItemBytes ?? VERCEL_MAX_ITEM_BYTES;
568
552
  this.maxShellEntryBytes = Math.floor((this.maxItemBytes * 3) / 4);
569
553
  this.memo = resolveShellMemoOptions(options.memo, {
@@ -1516,7 +1500,7 @@ export class VercelCacheStore<
1516
1500
  // cross-family one reads as corrupt. See the class doc for passing a wider
1517
1501
  // hash (sha256) when many keys are live.
1518
1502
  private toStoreKey(key: string, family: CacheFamily): string {
1519
- return `${versionKeyPrefix(this.explicitVersion, FAMILY_VERSION[family])}rg:${family}:${key}`;
1503
+ return `${versionKeyPrefix(FAMILY_VERSION[family])}rg:${family}:${key}`;
1520
1504
  }
1521
1505
 
1522
1506
  private async write(
@@ -116,8 +116,9 @@ function keptValues(resolved: ResolvedValue[]): unknown[] {
116
116
  * so a push the record leaves out must not be in the prelude either: it
117
117
  * rendered an element no HIT's hydration data matched (issue #1035). Which
118
118
  * pushes a record keeps is the capture funnel's decision (shell-capture.ts
119
- * deriveShellCaptureContext: today it leaves out a deferred loader push and
120
- * one holding a masked promise), not this function's.
119
+ * deriveShellCaptureContext: it leaves out a loader push whose settled value
120
+ * holds a masked promise, and one a live-lane loader made), not this
121
+ * function's.
121
122
  */
122
123
  export async function* resolvedHandleStream(
123
124
  handleStore: HandleStore,
@@ -231,6 +231,11 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
231
231
  * (resolve-by-default), so they participate in deduplication and `%s`
232
232
  * title-templating identically to synchronous descriptors. A descriptor that
233
233
  * resolves to `null`/`undefined` (or rejects) is dropped.
234
+ *
235
+ * On a ppr route, a descriptor a handler or an `ssr: false` loader pushes,
236
+ * promise or not, is in the shell's `<head>` on every HIT (#1182). One a
237
+ * loader without `ssr: false` pushes is live: on a HIT it arrives after
238
+ * hydration.
234
239
  */
235
240
  export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<
236
241
  MetaDescriptor,
@@ -106,8 +106,7 @@ export interface BuildShellCaptureOptions {
106
106
  /** Build-time env bindings (rango plugin buildEnv), if configured. */
107
107
  buildEnv?: unknown;
108
108
  /**
109
- * The versions the SHIPPED build serves this router with (the build table,
110
- * or the router's consumer-set version) — NOT the temp server's own
109
+ * The versions the SHIPPED build serves this router with (the build table) — NOT the temp server's own
111
110
  * version-plugin stamp. The serve-side isValidShellHit gate compares
112
111
  * entry.buildVersion against the running worker's ctx.version, which is the
113
112
  * document version; stamping the temp server's would make every build entry
@@ -22,9 +22,8 @@ import { paramsEqual } from "../router/params-util.js";
22
22
  export interface PrerenderKey {
23
23
  routerId: string;
24
24
  /**
25
- * The owning router's cache version (its data version, or
26
- * `createRouter({ version })`). Opaque: never set or read by app code; a
27
- * store keys off it as given.
25
+ * The owning router's cache version (its data version). Opaque: never set
26
+ * or read by app code; a store keys off it as given.
28
27
  */
29
28
  version: string;
30
29
  routeName: string;
@@ -9,6 +9,8 @@ import type {
9
9
  TransitionConfig,
10
10
  } from "../types.js";
11
11
  import {
12
+ applyDocumentModeLoading,
13
+ documentModeLoader,
12
14
  getContext,
13
15
  getNamePrefix,
14
16
  getReverseScope,
@@ -928,17 +930,14 @@ const loader: RouteHelpers<any, any>["loader"] = (
928
930
 
929
931
  const name = `${ctx.namespace}.$${store.getNextIndex("loader")}`;
930
932
 
931
- // Create loader entry with empty revalidate array. awaitBeforeFlush is
932
- // resolved here, at DSL-evaluation time, for the same reason
933
- // loading({ ssr: false }) is (below) — per-isSSR entry caching makes the
934
- // flag request-mode-correct with no isSSR threading; see LoaderEntry.
935
- // `bake` is the lane, the same on both evaluations.
933
+ // Create loader entry with empty revalidate array. `bake` is the lane, the
934
+ // same on both evaluations; a document evaluation adds awaitBeforeFlush at
935
+ // the push (documentModeLoader) — per-isSSR entry caching makes the flag
936
+ // request-mode-correct with no isSSR threading; see LoaderEntry.
936
937
  const loaderEntry: LoaderEntry = {
937
938
  loader: loaderDef,
938
939
  revalidate: [] as ShouldRevalidateFn<any, any>[],
939
940
  ...(optionsGiven?.ssr === false && { bake: true as const }),
940
- ...(optionsGiven?.ssr === false &&
941
- ctx.isSSR && { awaitBeforeFlush: true as const }),
942
941
  ...(optionsGiven?.prefetch === false && { prefetch: false as const }),
943
942
  // The declaring layout's / parallel's / cache()'s scope. A route's (and
944
943
  // an intercept's) loaders have none: they resolve from the route's name,
@@ -978,7 +977,9 @@ const loader: RouteHelpers<any, any>["loader"] = (
978
977
  validateUseItems(result, name, "loader", "use");
979
978
  }
980
979
 
981
- ctx.parent.loader.push(loaderEntry);
980
+ ctx.parent.loader.push(
981
+ ctx.isSSR ? documentModeLoader(loaderEntry) : loaderEntry,
982
+ );
982
983
  return { name, type: "loader" } as LoaderItem;
983
984
  };
984
985
 
@@ -1006,12 +1007,11 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
1006
1007
  const resolved =
1007
1008
  typeof component === "function" ? (component as () => any)() : component;
1008
1009
 
1009
- // If ssr: false and we're in SSR, set loading to false
1010
- if (options?.ssr === false && ctx.isSSR) {
1011
- parent.loading = false;
1012
- } else {
1013
- parent.loading = resolved;
1014
- }
1010
+ // `loadingSsr` records ssr: false on both evaluations; a document drops
1011
+ // the fallback (applyDocumentModeLoading).
1012
+ parent.loading = resolved;
1013
+ parent.loadingSsr = options?.ssr === false ? false : undefined;
1014
+ if (ctx.isSSR) applyDocumentModeLoading(parent);
1015
1015
 
1016
1016
  // The flag means "what this fallback covers": without a fallback to show
1017
1017
  // there is nothing to defer behind. Recorded from `resolved`, not
@@ -0,0 +1,73 @@
1
+ import type { RangoInternal } from "./router-interfaces.js";
2
+
3
+ /**
4
+ * Dev only (#1186). A runner full reload clears its evaluated modules while a
5
+ * request may still be loading them, splitting it across two router copies
6
+ * whose registries disagree. A router of a dropped copy hands its requests to
7
+ * the router the reload re-created. docs/manifests.md, "A request the reload
8
+ * split".
9
+ */
10
+ let runnerReloaded = false;
11
+
12
+ /**
13
+ * Bound on the wait for the re-created router. cloudflare-basic's entry took
14
+ * about 5 s to re-evaluate after the optimizer's `invalidateAll`.
15
+ */
16
+ const DEV_RECREATED_ROUTER_WAIT_MS = 10_000;
17
+ const DEV_RECREATED_ROUTER_POLL_MS = 20;
18
+
19
+ /** The `import.meta.hot` subset this reads. */
20
+ interface RunnerReloadEvents {
21
+ on(event: "vite:beforeFullReload", listener: () => void): void;
22
+ }
23
+
24
+ /**
25
+ * Mark this module copy dropped on the runner's next full reload.
26
+ * @internal
27
+ */
28
+ export function watchRunnerReloads(
29
+ events: RunnerReloadEvents | undefined,
30
+ ): void {
31
+ events?.on("vite:beforeFullReload", () => {
32
+ runnerReloaded = true;
33
+ });
34
+ }
35
+
36
+ if (process.env.NODE_ENV !== "production") {
37
+ watchRunnerReloads(import.meta.hot);
38
+ }
39
+
40
+ /**
41
+ * Whether a runner full reload dropped this module copy.
42
+ * @internal
43
+ */
44
+ export function droppedByRunnerReload(): boolean {
45
+ return runnerReloaded;
46
+ }
47
+
48
+ /**
49
+ * The router the reload re-created for `routerId`, or undefined when none
50
+ * registered within DEV_RECREATED_ROUTER_WAIT_MS. Importing `sourceFile`
51
+ * evaluates it when nothing else is. While the reload's re-import is still
52
+ * evaluating it, the runner returns the unfinished module (it is in this
53
+ * copy's import call stack, `cachedRequest` in vite's module-runner), so this
54
+ * waits for the fresh registry to receive the router.
55
+ * @internal
56
+ */
57
+ export async function routerAfterRunnerReload(
58
+ routerId: string,
59
+ sourceFile: string | undefined,
60
+ ): Promise<RangoInternal<any, any> | undefined> {
61
+ if (!sourceFile) return undefined;
62
+ await import(/* @vite-ignore */ sourceFile);
63
+ const { RouterRegistry } = await import("./router-registry.js");
64
+ const deadline = Date.now() + DEV_RECREATED_ROUTER_WAIT_MS;
65
+ let router = RouterRegistry.get(routerId);
66
+ while (!router && Date.now() < deadline) {
67
+ await new Promise((resolve) =>
68
+ setTimeout(resolve, DEV_RECREATED_ROUTER_POLL_MS),
69
+ );
70
+ router = RouterRegistry.get(routerId);
71
+ }
72
+ return router;
73
+ }
@@ -3,6 +3,7 @@ import { createRouteHelpers } from "../route-definition";
3
3
  import {
4
4
  getContext,
5
5
  runWithPrefixes,
6
+ getDocumentModeEntry,
6
7
  getIsolatedLazyParent,
7
8
  type EntryData,
8
9
  type MetricsStore,
@@ -120,12 +121,18 @@ export async function loadManifest(
120
121
  mountIndex !== undefined ? `#router.M${mountIndex}` : "#router";
121
122
 
122
123
  // For lazy entries, use the captured parent from include() context
123
- // This ensures routes are registered under the correct layout hierarchy
124
+ // This ensures routes are registered under the correct layout hierarchy.
125
+ // The captured chain comes from an evaluation without isSSR: a document
126
+ // request takes its document-mode copy (getDocumentModeEntry, #1179).
124
127
  const lazyContext =
125
128
  entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
129
+ const capturedParent =
130
+ (lazyContext?.parent as EntryData | null | undefined) ?? Store.parent;
126
131
  const parentForContext = lazyContext
127
132
  ? getIsolatedLazyParent(
128
- (lazyContext.parent as EntryData | null) ?? Store.parent,
133
+ isSSR && capturedParent
134
+ ? getDocumentModeEntry(capturedParent)
135
+ : capturedParent,
129
136
  )
130
137
  : Store.parent;
131
138
 
@@ -232,8 +232,8 @@ async function handleCacheHitIntercept<TEnv>(
232
232
  const cachedSlot = segments.filter(isInterceptSegment);
233
233
  // A record stored before 0.25.0 carries the loaders' aggregate on the slot;
234
234
  // the loader segments below replace it. Build versions hash the router's
235
- // code, so only a `version` pinned across the upgrade (createRouter's or
236
- // the store's) still reads one.
235
+ // code, so an upgrade does not read one; a custom store that ignores
236
+ // versions can.
237
237
  for (const slot of cachedSlot) {
238
238
  delete slot.loaderDataPromise;
239
239
  delete slot.loaderIds;
@@ -479,12 +479,6 @@ export interface RangoInternal<
479
479
  */
480
480
  readonly nonce?: NonceProvider<TEnv>;
481
481
 
482
- /**
483
- * The consumer-set version (createRouter({ version })), used for both of the
484
- * router's versions. Undefined when the router uses its build versions.
485
- */
486
- readonly version?: string;
487
-
488
482
  /**
489
483
  * URL patterns reference for build-time manifest generation
490
484
  */
@@ -531,27 +531,6 @@ export interface RangoOptions<TEnv = any> {
531
531
  */
532
532
  nonce?: NonceProvider<TEnv>;
533
533
 
534
- /**
535
- * Version override. When set, this exact value is used for both of the
536
- * router's versions: it keys the router's cached data (segments, `"use
537
- * cache"` values, loader data) and its stored HTML (PPR shells,
538
- * document-cache responses), is sent in payload metadata, and is what the
539
- * browser's `_rsc_v` is compared with (a mismatch reloads the tab).
540
- *
541
- * Leave it unset for the default: rango computes two versions per router at
542
- * build time, a data version from the router's server code and a document
543
- * version that also covers the client assets. The same code builds to the
544
- * same versions, so a deploy that does not change this router keeps its
545
- * cache. In dev both are a stamp bumped on every RSC module edit.
546
- *
547
- * Set it to clear this router's cache on demand (change the value), or to
548
- * tie the cache to your own release id (a per-deploy value clears the cache
549
- * on every deploy).
550
- *
551
- * @default the router's build versions
552
- */
553
- version?: string;
554
-
555
534
  /**
556
535
  * TTL (in seconds) for the client-side in-memory prefetch cache.
557
536
  *
@@ -378,9 +378,10 @@ function getLoaderStore(
378
378
  * push funnel reads the pushReplayed owner, shell-capture.ts), so a replay
379
379
  * of that shell that serves this loader from its pin restores them owned
380
380
  * (HandleStore.pushRestored), on a document HIT and on a navigation alike.
381
- * This replay still runs there: the store drops its settled values, which
382
- * the record already restored, and keeps the thenable ones the record could
383
- * not keep (a deferred push, the reason the loader's pin carries `runs`).
381
+ * This replay still runs there: the store drops its values that settle
382
+ * thenable-free, which the record already restored, and keeps the ones
383
+ * holding a promise the record could not keep (the reason the loader's pin
384
+ * carries `runs`).
384
385
  */
385
386
  async function replayLoaderHandles(
386
387
  encoded: string | undefined,
@@ -125,9 +125,11 @@ function someNestedThenable(
125
125
  /**
126
126
  * True when `value` is a thenable or holds one where the capture's loader
127
127
  * push funnel masks it (maskNestedContainerThenables): through plain data
128
- * objects, arrays and React element props. Such a push stays out of a shell's
129
- * record and reaches a HIT only from a run of its loader there, so a HIT
130
- * keeps it; a push without one is the record's (HandleStore.pushRestored).
128
+ * objects, arrays and React element props. A settled push that holds one
129
+ * stays out of a shell's record and reaches a HIT only from a run of its
130
+ * loader there, so a HIT keeps it; a push without one is the record's
131
+ * (HandleStore.pushRestored). A top-level thenable is judged by the value it
132
+ * settles to, on both sides (#1182).
131
133
  */
132
134
  export function holdsThenable(value: unknown): boolean {
133
135
  return someNestedThenable(value, () => true);
@@ -33,24 +33,24 @@ export type RouterVersionsTable = Readonly<
33
33
  /** Table key of the whole-build versions. Not a legal `$$id` (8 hex chars). */
34
34
  export const DEFAULT_ROUTER_VERSIONS_KEY = "*";
35
35
 
36
- /** One value for both versions: a consumer-set `version`, or the dev stamp. */
36
+ /**
37
+ * One value for both versions: the dev stamp, the version a build-time shell
38
+ * capture is asked for, or a test's seed.
39
+ */
37
40
  export function uniformVersions(version: string): RouterVersions {
38
41
  return { data: version, document: version };
39
42
  }
40
43
 
41
44
  /**
42
- * The versions a router serves with under `table`: a consumer-set `version`
43
- * for both; else the router's own entry; else the whole-build pair, for a
44
- * router the table does not list. `undefined` when there is no table (dev,
45
- * tests) or it has neither entry. One function for the request handler and
45
+ * The versions a router serves with under `table`: the router's own entry,
46
+ * else the whole-build pair, for a router the table does not list.
47
+ * `undefined` when there is no table (dev, tests) or it has neither entry. One function for the request handler and
46
48
  * the build's shell capture, which must stamp what the handler will check.
47
49
  */
48
50
  export function resolveVersionsFrom(
49
51
  table: RouterVersionsTable | undefined,
50
52
  routerId: string | undefined,
51
- override: string | undefined,
52
53
  ): RouterVersions | undefined {
53
- if (override !== undefined) return uniformVersions(override);
54
54
  if (!table) return undefined;
55
55
  const pair =
56
56
  (routerId !== undefined && Object.hasOwn(table, routerId)