@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.
- package/dist/bin/rango.js +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
- package/dist/types/cache/handle-snapshot.d.ts +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +6 -4
- package/dist/types/cache/types.d.ts +8 -6
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
- package/dist/types/handles/deferred-resolution.d.ts +3 -2
- package/dist/types/handles/meta.d.ts +5 -0
- package/dist/types/prerender/build-shell-capture.d.ts +1 -2
- package/dist/types/prerender/writable-store.d.ts +2 -3
- package/dist/types/router/dev-reload-handoff.d.ts +26 -0
- package/dist/types/router/router-interfaces.d.ts +0 -5
- package/dist/types/router/router-options.d.ts +0 -20
- package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
- package/dist/types/router-versions.d.ts +8 -6
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -6
- package/dist/types/rsc/shell-capture.d.ts +25 -8
- package/dist/types/rsc/types.d.ts +0 -8
- package/dist/types/server/build-version-table.d.ts +10 -13
- package/dist/types/server/context.d.ts +26 -2
- package/dist/types/server/handle-store.d.ts +17 -6
- package/dist/types/server/request-context.d.ts +4 -3
- package/dist/types/testing/build-versions.d.ts +0 -3
- package/dist/types/testing/internal/seed-vars.d.ts +4 -4
- package/dist/types/testing/serve-shell-request.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +6 -1
- package/dist/types/urls/pattern-types.d.ts +7 -6
- package/dist/vite/index.js +3 -8
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +4 -8
- package/skills/caching/SKILL.md +3 -5
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/hooks/handle-and-actions.md +6 -6
- package/skills/host-router/SKILL.md +1 -1
- package/skills/loader/SKILL.md +10 -9
- package/skills/ppr/SKILL.md +83 -60
- package/skills/prerender/SKILL.md +3 -3
- package/skills/router-setup/SKILL.md +0 -2
- package/skills/testing/render-handler.md +17 -17
- package/skills/testing/serve-shell-request.md +1 -1
- package/skills/testing/server-actions.md +16 -16
- package/skills/vercel/SKILL.md +1 -1
- package/src/cache/cf/cf-cache-store.ts +4 -7
- package/src/cache/cf/cf-cache-types.ts +0 -17
- package/src/cache/handle-snapshot.ts +2 -2
- package/src/cache/shell-snapshot.ts +7 -3
- package/src/cache/types.ts +8 -6
- package/src/cache/vercel/vercel-cache-store.ts +2 -18
- package/src/handles/deferred-resolution.ts +3 -2
- package/src/handles/meta.ts +5 -0
- package/src/prerender/build-shell-capture.ts +1 -2
- package/src/prerender/writable-store.ts +2 -3
- package/src/route-definition/dsl-helpers.ts +14 -14
- package/src/router/dev-reload-handoff.ts +73 -0
- package/src/router/manifest.ts +9 -2
- package/src/router/match-middleware/intercept-resolution.ts +2 -2
- package/src/router/router-interfaces.ts +0 -6
- package/src/router/router-options.ts +0 -21
- package/src/router/segment-resolution/loader-cache.ts +4 -3
- package/src/router/segment-resolution/mask-nested.ts +5 -3
- package/src/router-versions.ts +7 -7
- package/src/router.ts +19 -8
- package/src/rsc/handler.ts +3 -7
- package/src/rsc/rsc-rendering.ts +44 -12
- package/src/rsc/shell-capture-constants.ts +10 -6
- package/src/rsc/shell-capture.ts +163 -88
- package/src/rsc/types.ts +0 -9
- package/src/server/build-version-table.ts +12 -24
- package/src/server/context.ts +64 -2
- package/src/server/handle-store.ts +77 -10
- package/src/server/request-context.ts +4 -3
- package/src/testing/build-versions.ts +0 -3
- package/src/testing/dispatch.ts +1 -3
- package/src/testing/internal/seed-vars.ts +4 -4
- package/src/testing/serve-shell-request.ts +2 -5
- package/src/types/loader-types.ts +6 -1
- package/src/urls/pattern-types.ts +7 -6
- 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` (
|
|
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
|
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
1632
|
-
//
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
*
|
|
409
|
-
* HIT
|
|
410
|
-
*
|
|
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
|
},
|
package/src/cache/types.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
690
|
-
*
|
|
691
|
-
* body): a HIT then still runs the loader
|
|
692
|
-
* (pin-first) so those pushes reach the page.
|
|
693
|
-
*
|
|
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
|
-
* `
|
|
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(
|
|
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:
|
|
120
|
-
*
|
|
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,
|
package/src/handles/meta.ts
CHANGED
|
@@ -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
|
|
26
|
-
*
|
|
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.
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
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(
|
|
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
|
-
//
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
+
}
|
package/src/router/manifest.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
236
|
-
//
|
|
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
|
|
382
|
-
* the record already restored, and keeps the
|
|
383
|
-
* not keep (
|
|
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.
|
|
129
|
-
* record and reaches a HIT only from a run of its
|
|
130
|
-
* keeps it; a push without one is the record's
|
|
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);
|
package/src/router-versions.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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`:
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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)
|