@rangojs/router 0.0.0-experimental.139 → 0.0.0-experimental.140

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 (45) hide show
  1. package/dist/bin/rango.js +27 -2
  2. package/dist/vite/index.js +147 -30
  3. package/package.json +1 -1
  4. package/skills/breadcrumbs/SKILL.md +1 -1
  5. package/skills/cache-guide/SKILL.md +1 -0
  6. package/skills/caching/SKILL.md +1 -1
  7. package/skills/migrate-nextjs/SKILL.md +15 -0
  8. package/skills/migrate-react-router/SKILL.md +15 -2
  9. package/skills/ppr/SKILL.md +426 -0
  10. package/skills/rango/SKILL.md +28 -25
  11. package/skills/route/SKILL.md +43 -0
  12. package/src/build/route-trie.ts +35 -7
  13. package/src/cache/cf/cf-cache-store.ts +155 -0
  14. package/src/cache/index.ts +6 -0
  15. package/src/cache/memory-segment-store.ts +57 -1
  16. package/src/cache/shell-cache.ts +386 -0
  17. package/src/cache/types.ts +58 -0
  18. package/src/cache/vercel/vercel-cache-store.ts +159 -5
  19. package/src/index.rsc.ts +5 -0
  20. package/src/index.ts +17 -0
  21. package/src/router/middleware.ts +14 -5
  22. package/src/router/parse-pattern.ts +115 -0
  23. package/src/router/pattern-matching.ts +53 -64
  24. package/src/router/segment-resolution/fresh.ts +12 -1
  25. package/src/router/segment-resolution/loader-cache.ts +14 -0
  26. package/src/router/segment-resolution/loader-mask.ts +44 -0
  27. package/src/router/substitute-pattern-params.ts +54 -35
  28. package/src/router/trie-matching.ts +19 -11
  29. package/src/router/url-params.ts +13 -0
  30. package/src/rsc/full-payload.ts +70 -0
  31. package/src/rsc/rsc-rendering.ts +105 -51
  32. package/src/rsc/shell-capture.ts +439 -0
  33. package/src/rsc/types.ts +26 -0
  34. package/src/server/cookie-store.ts +45 -0
  35. package/src/server/live.ts +130 -0
  36. package/src/server/request-context.ts +49 -0
  37. package/src/ssr/index.tsx +377 -180
  38. package/src/ssr/ssr-root.tsx +228 -0
  39. package/src/testing/render-route.tsx +7 -9
  40. package/src/types/route-config.ts +19 -7
  41. package/src/urls/type-extraction.ts +43 -18
  42. package/src/vite/discovery/discovery-errors.ts +61 -0
  43. package/src/vite/plugins/virtual-entries.ts +27 -2
  44. package/src/vite/router-discovery.ts +69 -15
  45. package/src/vite/utils/prerender-utils.ts +17 -4
@@ -34,6 +34,7 @@ import type {
34
34
  CacheGetResult,
35
35
  CacheItemResult,
36
36
  CacheItemOptions,
37
+ ShellCacheEntry,
37
38
  } from "../types.js";
38
39
  import {
39
40
  _getRequestContext,
@@ -189,6 +190,29 @@ interface KVItemEnvelope {
189
190
  ta?: number;
190
191
  }
191
192
 
193
+ /**
194
+ * KV envelope for PPR shell cache entries.
195
+ * @internal
196
+ */
197
+ interface KVShellEnvelope {
198
+ /** base64-encoded prelude bytes */
199
+ p: string;
200
+ /** postponed state JSON, or null (DATA variant — no holes) */
201
+ po: string | null;
202
+ /** React.version captured at prerender time */
203
+ rv: string;
204
+ /** createdAt (ms epoch) */
205
+ c: number;
206
+ /** When entry becomes stale (ms epoch) */
207
+ s: number;
208
+ /** When entry hard-expires (ms epoch) */
209
+ e: number;
210
+ /** Cache tags (for distributed tag invalidation) */
211
+ t?: string[];
212
+ /** Timestamp when tags were attached (ms epoch) */
213
+ ta?: number;
214
+ }
215
+
192
216
  /**
193
217
  * KV envelope for document cache entries.
194
218
  * @internal
@@ -1574,6 +1598,137 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1574
1598
  }
1575
1599
  }
1576
1600
 
1601
+ // ============================================================================
1602
+ // Shell Cache Methods (PPR shell resume) — KV-only in v1
1603
+ // ============================================================================
1604
+ //
1605
+ // Unlike the segment/item/document tiers, the shell family has NO Cache-API L1
1606
+ // tier: the prelude bytes + postponed blob are large and version-coupled, and a
1607
+ // per-colo L1 for them is a deliberate follow-up (see the PPR shell-resume
1608
+ // design doc). Shell entries live only in KV (the global tier), so the family
1609
+ // requires a configured KV namespace; without one, getShell/putShell no-op and
1610
+ // the shell-cache middleware fails open to a full HTML render. Tag invalidation
1611
+ // still applies: shell entries carry tags/taggedAt and are checked against the
1612
+ // same KV markers isGloballyInvalidated() reads for every other tier.
1613
+
1614
+ /**
1615
+ * Get a cached PPR shell entry by key from KV (no L1). Applies the KV read
1616
+ * budget, corrupt-entry eviction, hard-expiry, and tag invalidation exactly
1617
+ * like kvGetItem, minus the L1 promote. SWR is a plain staleness flag — KV has
1618
+ * no REVALIDATING herd guard, so the shell-cache middleware's module-level
1619
+ * in-flight set is the recapture stampede guard.
1620
+ */
1621
+ async getShell(
1622
+ key: string,
1623
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
1624
+ if (!this.kv) return null;
1625
+ try {
1626
+ const kvKey = this.toKVKey(`shell:${key}`);
1627
+ const { value: envelope, timedOut } =
1628
+ await this.kvGetOrEvict<KVShellEnvelope>(
1629
+ kvKey,
1630
+ (e) =>
1631
+ typeof e.p === "string" &&
1632
+ (e.po === null || typeof e.po === "string") &&
1633
+ typeof e.rv === "string" &&
1634
+ typeof e.e === "number" &&
1635
+ typeof e.s === "number",
1636
+ "getShell",
1637
+ );
1638
+ // A timeout, a missing key, or an already-evicted corrupt entry is a miss.
1639
+ if (timedOut || !envelope) return null;
1640
+
1641
+ const now = Date.now();
1642
+ if (now > envelope.e) return null;
1643
+
1644
+ if (await this.isGloballyInvalidated(envelope.t, envelope.ta)) {
1645
+ return null;
1646
+ }
1647
+
1648
+ const shouldRevalidate = envelope.s > 0 && now > envelope.s;
1649
+ return {
1650
+ entry: {
1651
+ prelude: envelope.p,
1652
+ postponed: envelope.po,
1653
+ reactVersion: envelope.rv,
1654
+ createdAt: envelope.c,
1655
+ },
1656
+ shouldRevalidate,
1657
+ };
1658
+ } catch (error) {
1659
+ reportCacheError(error, "cache-read", "[CFCacheStore] getShell");
1660
+ return null;
1661
+ }
1662
+ }
1663
+
1664
+ /**
1665
+ * Store a PPR shell entry in KV with TTL and optional SWR window. Non-blocking
1666
+ * (waitUntil) like the other KV writes. The tags/taggedAt ride in the envelope
1667
+ * so isGloballyInvalidated() can invalidate the shell via the shared KV markers.
1668
+ */
1669
+ async putShell(
1670
+ key: string,
1671
+ entry: ShellCacheEntry,
1672
+ ttlSeconds?: number,
1673
+ swrSeconds?: number,
1674
+ tags?: string[],
1675
+ ): Promise<void> {
1676
+ // KV-only tier: needs a KV namespace and waitUntil (writes are non-blocking).
1677
+ if (!this.kv || !this.waitUntil) return;
1678
+ try {
1679
+ const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
1680
+ const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
1681
+ const totalTtl = ttl + swrWindow;
1682
+ // KV requires expirationTtl >= 60s; skip a shorter-lived shell rather than
1683
+ // letting kv.put reject inside waitUntil (mirrors setItem/kvSetSegment).
1684
+ if (totalTtl < 60) return;
1685
+
1686
+ const staleAt = Date.now() + ttl * 1000;
1687
+ const taggedAt =
1688
+ Array.isArray(tags) && tags.length > 0 ? Date.now() : undefined;
1689
+
1690
+ const kvKey = this.toKVKey(`shell:${key}`);
1691
+ // A key over the KV limit makes kv.put reject deep inside waitUntil; report
1692
+ // and skip the doomed write (mirrors kvSetSegment).
1693
+ const kvKeyBytes = kvKeyByteLength(kvKey);
1694
+ if (kvKeyBytes > KV_MAX_KEY_BYTES) {
1695
+ reportCacheError(
1696
+ new Error(
1697
+ `shell cache key produces a ${kvKeyBytes}-byte KV key, over the ` +
1698
+ `${KV_MAX_KEY_BYTES}-byte limit; the shell was not persisted.`,
1699
+ ),
1700
+ "cache-write",
1701
+ "[CFCacheStore] putShell",
1702
+ );
1703
+ return;
1704
+ }
1705
+
1706
+ this.waitUntil(() =>
1707
+ reportingAsync(
1708
+ () => {
1709
+ const envelope: KVShellEnvelope = {
1710
+ p: entry.prelude,
1711
+ po: entry.postponed,
1712
+ rv: entry.reactVersion,
1713
+ c: entry.createdAt,
1714
+ s: staleAt,
1715
+ e: staleAt + swrWindow * 1000,
1716
+ t: tags,
1717
+ ta: taggedAt,
1718
+ };
1719
+ return this.kv!.put(kvKey, JSON.stringify(envelope), {
1720
+ expirationTtl: totalTtl,
1721
+ });
1722
+ },
1723
+ "cache-write",
1724
+ "[CFCacheStore] putShell",
1725
+ ),
1726
+ );
1727
+ } catch (error) {
1728
+ reportCacheError(error, "cache-write", "[CFCacheStore] putShell");
1729
+ }
1730
+ }
1731
+
1577
1732
  // ============================================================================
1578
1733
  // Key Helpers
1579
1734
  // ============================================================================
@@ -4,6 +4,7 @@ export type {
4
4
  CacheGetResult,
5
5
  CacheItemResult,
6
6
  CacheItemOptions,
7
+ ShellCacheEntry,
7
8
  SerializedSegmentData,
8
9
  SegmentHandleData,
9
10
  } from "./types.js";
@@ -43,4 +44,9 @@ export {
43
44
  type DocumentCacheOptions,
44
45
  } from "./document-cache.js";
45
46
 
47
+ export {
48
+ createShellCacheMiddleware,
49
+ type ShellCacheOptions,
50
+ } from "./shell-cache.js";
51
+
46
52
  export type { CacheErrorCategory } from "./cache-error.js";
@@ -12,6 +12,7 @@ import type {
12
12
  CacheGetResult,
13
13
  CacheItemResult,
14
14
  CacheItemOptions,
15
+ ShellCacheEntry,
15
16
  } from "./types.js";
16
17
  import type { RequestContext } from "../server/request-context.js";
17
18
  import { isPerClientSignalHeader } from "../browser/cookie-name.js";
@@ -26,6 +27,7 @@ import { reportCacheError } from "./cache-error.js";
26
27
  const CACHE_REGISTRY_KEY = "__rsc_router_segment_cache_registry__";
27
28
  const RESPONSE_CACHE_REGISTRY_KEY = "__rsc_router_response_cache_registry__";
28
29
  const ITEM_CACHE_REGISTRY_KEY = "__rsc_router_item_cache_registry__";
30
+ const SHELL_CACHE_REGISTRY_KEY = "__rsc_router_shell_cache_registry__";
29
31
  const TAG_INDEX_REGISTRY_KEY = "__rsc_router_tag_index_registry__";
30
32
  const KEY_TAGS_REGISTRY_KEY = "__rsc_router_key_tags_registry__";
31
33
 
@@ -65,6 +67,13 @@ interface CachedItemEntry {
65
67
  tags?: string[];
66
68
  }
67
69
 
70
+ interface CachedShellEntry {
71
+ entry: ShellCacheEntry;
72
+ expiresAt: number;
73
+ staleAt: number;
74
+ tags?: string[];
75
+ }
76
+
68
77
  /**
69
78
  * Options for MemorySegmentCacheStore
70
79
  */
@@ -157,7 +166,8 @@ export class MemorySegmentCacheStore<
157
166
  private cache: Map<string, CachedEntryData>;
158
167
  private responseCache: Map<string, CachedResponseEntry>;
159
168
  private itemCache: Map<string, CachedItemEntry>;
160
- /** tag -> set of prefixed cache keys (seg:key, res:key, item:key) */
169
+ private shellCache: Map<string, CachedShellEntry>;
170
+ /** tag -> set of prefixed cache keys (seg:key, res:key, item:key, shell:key) */
161
171
  private tagIndex: Map<string, Set<string>>;
162
172
  /** prefixed cache key -> set of tags (reverse index for O(tags) unregister) */
163
173
  private keyTags: Map<string, Set<string>>;
@@ -181,6 +191,10 @@ export class MemorySegmentCacheStore<
181
191
  ITEM_CACHE_REGISTRY_KEY,
182
192
  options.name,
183
193
  );
194
+ this.shellCache = getNamedMap<CachedShellEntry>(
195
+ SHELL_CACHE_REGISTRY_KEY,
196
+ options.name,
197
+ );
184
198
  this.tagIndex = getNamedMap<Set<string>>(
185
199
  TAG_INDEX_REGISTRY_KEY,
186
200
  options.name,
@@ -193,6 +207,7 @@ export class MemorySegmentCacheStore<
193
207
  this.cache = new Map<string, CachedEntryData>();
194
208
  this.responseCache = new Map<string, CachedResponseEntry>();
195
209
  this.itemCache = new Map<string, CachedItemEntry>();
210
+ this.shellCache = new Map<string, CachedShellEntry>();
196
211
  this.tagIndex = new Map<string, Set<string>>();
197
212
  this.keyTags = new Map<string, Set<string>>();
198
213
  }
@@ -245,6 +260,7 @@ export class MemorySegmentCacheStore<
245
260
  this.cache.clear();
246
261
  this.responseCache.clear();
247
262
  this.itemCache.clear();
263
+ this.shellCache.clear();
248
264
  this.tagIndex.clear();
249
265
  this.keyTags.clear();
250
266
  }
@@ -353,6 +369,43 @@ export class MemorySegmentCacheStore<
353
369
  }
354
370
  }
355
371
 
372
+ async getShell(
373
+ key: string,
374
+ ): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
375
+ const cached = this.shellCache.get(key);
376
+ if (!cached) return null;
377
+
378
+ const now = Date.now();
379
+ if (now > cached.expiresAt) {
380
+ this.unregisterTags(`shell:${key}`);
381
+ this.shellCache.delete(key);
382
+ return null;
383
+ }
384
+
385
+ // SWR mirrors the item family: stale within the swr window still serves, and
386
+ // signals shouldRevalidate so the middleware schedules a background recapture.
387
+ const shouldRevalidate = cached.staleAt > 0 && now > cached.staleAt;
388
+ return { entry: cached.entry, shouldRevalidate };
389
+ }
390
+
391
+ async putShell(
392
+ key: string,
393
+ entry: ShellCacheEntry,
394
+ ttlSeconds?: number,
395
+ swrSeconds?: number,
396
+ tags?: string[],
397
+ ): Promise<void> {
398
+ const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
399
+ const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
400
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
401
+ const prefixedKey = `shell:${key}`;
402
+ this.unregisterTags(prefixedKey);
403
+ this.shellCache.set(key, { entry, expiresAt, staleAt, tags });
404
+ if (tags && tags.length > 0) {
405
+ this.registerTags(tags, prefixedKey);
406
+ }
407
+ }
408
+
356
409
  async invalidateTags(tags: string[]): Promise<void> {
357
410
  for (const tag of tags) {
358
411
  const keys = this.tagIndex.get(tag);
@@ -371,6 +424,8 @@ export class MemorySegmentCacheStore<
371
424
  this.responseCache.delete(rawKey);
372
425
  } else if (prefix === "item") {
373
426
  this.itemCache.delete(rawKey);
427
+ } else if (prefix === "shell") {
428
+ this.shellCache.delete(rawKey);
374
429
  }
375
430
 
376
431
  this.unregisterTags(prefixedKey);
@@ -421,6 +476,7 @@ export class MemorySegmentCacheStore<
421
476
  delete (globalThis as any)[CACHE_REGISTRY_KEY];
422
477
  delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
423
478
  delete (globalThis as any)[ITEM_CACHE_REGISTRY_KEY];
479
+ delete (globalThis as any)[SHELL_CACHE_REGISTRY_KEY];
424
480
  delete (globalThis as any)[TAG_INDEX_REGISTRY_KEY];
425
481
  delete (globalThis as any)[KEY_TAGS_REGISTRY_KEY];
426
482
  }