@rangojs/router 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +3 -2
  2. package/dist/bin/browser-7KIJ5HSV.js +15 -0
  3. package/dist/bin/chunk-3B7CKBRO.js +463 -0
  4. package/dist/bin/chunk-524VACCG.js +71 -0
  5. package/dist/bin/chunk-LD7UBHT4.js +196 -0
  6. package/dist/bin/chunk-TO6CRWLN.js +1429 -0
  7. package/dist/bin/chunk-TTXC2U3G.js +2477 -0
  8. package/dist/bin/client-urls-projection-DQLWPDAT.js +28 -0
  9. package/dist/bin/forward-user-plugins-URBZKJP4.js +62 -0
  10. package/dist/bin/rango.js +42 -10173
  11. package/dist/bin/runtime-EPUDEHO4.js +596 -0
  12. package/dist/bin/runtime-discovery-CZ2QHT4Q.js +352 -0
  13. package/dist/bin/state-ACD2MGKJ.js +16 -0
  14. package/dist/bin/temp-rsc-server-QRR6HD6T.js +3190 -0
  15. package/dist/types/bin/rango.d.ts +1 -0
  16. package/dist/types/browser/action-fence.d.ts +20 -0
  17. package/dist/types/browser/cookie-name.d.ts +51 -10
  18. package/dist/types/browser/invalidate-client-cache.d.ts +8 -6
  19. package/dist/types/browser/navigation-bridge.d.ts +36 -1
  20. package/dist/types/browser/navigation-client.d.ts +19 -0
  21. package/dist/types/browser/navigation-store-handle.d.ts +37 -4
  22. package/dist/types/browser/navigation-store.d.ts +2 -2
  23. package/dist/types/browser/navigation-transaction.d.ts +0 -1
  24. package/dist/types/browser/partial-update.d.ts +13 -0
  25. package/dist/types/browser/prefetch/cache.d.ts +96 -8
  26. package/dist/types/browser/prefetch/fetch.d.ts +26 -2
  27. package/dist/types/browser/prefetch/queue.d.ts +3 -2
  28. package/dist/types/browser/rango-state.d.ts +34 -5
  29. package/dist/types/browser/react/Link.d.ts +15 -6
  30. package/dist/types/browser/types.d.ts +25 -0
  31. package/dist/types/build/generate-route-types.d.ts +1 -1
  32. package/dist/types/build/route-types/include-resolution.d.ts +9 -1
  33. package/dist/types/build/route-types/router-processing.d.ts +3 -1
  34. package/dist/types/cache/cache-key-utils.d.ts +12 -8
  35. package/dist/types/cache/cache-scope.d.ts +4 -2
  36. package/dist/types/cache/cf/cf-cache-types.d.ts +9 -5
  37. package/dist/types/cache/document-cache-policy.d.ts +9 -1
  38. package/dist/types/cache/memory-segment-store.d.ts +3 -2
  39. package/dist/types/cache/segment-codec.d.ts +2 -2
  40. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  41. package/dist/types/cache/types.d.ts +12 -7
  42. package/dist/types/cache/vercel/vercel-cache-store.d.ts +2 -1
  43. package/dist/types/client-urls/types.d.ts +7 -5
  44. package/dist/types/prefetch-markers.d.ts +9 -0
  45. package/dist/types/prefetch-source-scope.d.ts +66 -0
  46. package/dist/types/route-definition/helpers-types.d.ts +8 -5
  47. package/dist/types/router/intercept-resolution.d.ts +25 -11
  48. package/dist/types/router/match-context.d.ts +1 -0
  49. package/dist/types/router/match-handlers.d.ts +1 -1
  50. package/dist/types/router/prefetch-cache-ttl.d.ts +11 -3
  51. package/dist/types/router/router-context.d.ts +1 -1
  52. package/dist/types/router/router-options.d.ts +6 -3
  53. package/dist/types/router/segment-resolution/loader-cache.d.ts +8 -0
  54. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +23 -7
  55. package/dist/types/router/segment-resolution/revalidation.d.ts +19 -0
  56. package/dist/types/router/source-reads.d.ts +53 -0
  57. package/dist/types/router/types.d.ts +1 -1
  58. package/dist/types/rsc/helpers.d.ts +5 -2
  59. package/dist/types/rsc/prefetch-render-error.d.ts +7 -2
  60. package/dist/types/rsc/request-stages.d.ts +2 -0
  61. package/dist/types/rsc/shell-serve.d.ts +6 -1
  62. package/dist/types/server/context.d.ts +23 -6
  63. package/dist/types/server/cookie-store.d.ts +15 -1
  64. package/dist/types/server/document-location-state.d.ts +5 -2
  65. package/dist/types/server/handle-store.d.ts +2 -0
  66. package/dist/types/server/request-context.d.ts +43 -7
  67. package/dist/types/testing/e2e/server.d.ts +16 -1
  68. package/dist/types/testing/render-route.d.ts +66 -2
  69. package/dist/types/testing/serve-shell-request.d.ts +6 -3
  70. package/dist/types/types/handler-context.d.ts +7 -5
  71. package/dist/types/types/loader-types.d.ts +7 -4
  72. package/dist/types/types/segments.d.ts +15 -4
  73. package/dist/types/urls/path-helper-types.d.ts +7 -5
  74. package/dist/types/vite/temp-rsc-server.d.ts +2 -2
  75. package/dist/vite/index.js +449 -189
  76. package/package.json +2 -2
  77. package/skills/app-structure/SKILL.md +74 -0
  78. package/skills/app-structure/e-commerce.md +10 -9
  79. package/skills/app-structure/large-app.md +15 -2
  80. package/skills/caching/SKILL.md +10 -5
  81. package/skills/client-urls/SKILL.md +18 -11
  82. package/skills/comparison/references/framework-comparison.md +13 -8
  83. package/skills/document-cache/SKILL.md +8 -0
  84. package/skills/hooks/navigation.md +8 -8
  85. package/skills/hooks/state.md +24 -8
  86. package/skills/intercept/SKILL.md +33 -12
  87. package/skills/layout/SKILL.md +11 -0
  88. package/skills/links/SKILL.md +101 -4
  89. package/skills/loader/SKILL.md +49 -22
  90. package/skills/migrate-nextjs/SKILL.md +8 -2
  91. package/skills/migrate-react-router/data-and-actions.md +5 -0
  92. package/skills/ppr/SKILL.md +26 -5
  93. package/skills/prerender/SKILL.md +4 -1
  94. package/skills/route/SKILL.md +6 -6
  95. package/skills/router-setup/SKILL.md +2 -1
  96. package/skills/server-actions/SKILL.md +41 -8
  97. package/skills/testing/client-components.md +29 -13
  98. package/skills/testing/serve-shell-request.md +7 -7
  99. package/skills/testing/server-actions.md +8 -0
  100. package/skills/ux-architecture/SKILL.md +67 -18
  101. package/src/bin/rango.ts +39 -13
  102. package/src/browser/action-fence.ts +72 -0
  103. package/src/browser/cookie-name.ts +82 -14
  104. package/src/browser/invalidate-client-cache.ts +9 -7
  105. package/src/browser/navigation-bridge.ts +164 -32
  106. package/src/browser/navigation-client.ts +95 -20
  107. package/src/browser/navigation-store-handle.ts +67 -5
  108. package/src/browser/navigation-store.ts +86 -17
  109. package/src/browser/navigation-transaction.ts +0 -6
  110. package/src/browser/partial-update.ts +31 -12
  111. package/src/browser/prefetch/cache.ts +188 -11
  112. package/src/browser/prefetch/fetch.ts +211 -51
  113. package/src/browser/prefetch/queue.ts +3 -2
  114. package/src/browser/rango-state.ts +50 -8
  115. package/src/browser/react/Link.tsx +20 -9
  116. package/src/browser/react/use-navigation.ts +1 -1
  117. package/src/browser/rsc-router.tsx +8 -1
  118. package/src/browser/server-action-bridge.ts +3 -3
  119. package/src/browser/types.ts +25 -0
  120. package/src/build/generate-route-types.ts +1 -0
  121. package/src/build/route-types/include-resolution.ts +40 -3
  122. package/src/build/route-types/router-processing.ts +170 -25
  123. package/src/build/route-types/scan-filter.ts +1 -0
  124. package/src/cache/cache-key-utils.ts +12 -8
  125. package/src/cache/cache-runtime.ts +5 -1
  126. package/src/cache/cache-scope.ts +28 -7
  127. package/src/cache/cf/cf-cache-types.ts +9 -5
  128. package/src/cache/document-cache-policy.ts +27 -0
  129. package/src/cache/document-cache.ts +5 -0
  130. package/src/cache/memory-segment-store.ts +3 -2
  131. package/src/cache/segment-codec.ts +2 -2
  132. package/src/cache/shell-snapshot.ts +1 -1
  133. package/src/cache/types.ts +12 -7
  134. package/src/cache/vercel/vercel-cache-store.ts +2 -1
  135. package/src/client-urls/types.ts +7 -5
  136. package/src/prefetch-markers.ts +10 -0
  137. package/src/prefetch-source-scope.ts +137 -0
  138. package/src/route-definition/helpers-types.ts +8 -5
  139. package/src/router/intercept-resolution.ts +94 -50
  140. package/src/router/loader-resolution.ts +8 -6
  141. package/src/router/match-api.ts +33 -29
  142. package/src/router/match-context.ts +5 -0
  143. package/src/router/match-handlers.ts +2 -1
  144. package/src/router/match-middleware/cache-lookup.ts +21 -13
  145. package/src/router/match-result.ts +39 -87
  146. package/src/router/navigation-snapshot.ts +18 -3
  147. package/src/router/prefetch-cache-ttl.ts +15 -3
  148. package/src/router/revalidation.ts +53 -23
  149. package/src/router/router-context.ts +2 -1
  150. package/src/router/router-options.ts +6 -3
  151. package/src/router/segment-resolution/loader-cache.ts +21 -3
  152. package/src/router/segment-resolution/prefetch-deferral.ts +38 -31
  153. package/src/router/segment-resolution/revalidation.ts +40 -12
  154. package/src/router/source-reads.ts +122 -0
  155. package/src/router/types.ts +2 -1
  156. package/src/router.ts +11 -12
  157. package/src/rsc/helpers.ts +8 -3
  158. package/src/rsc/prefetch-render-error.ts +7 -2
  159. package/src/rsc/progressive-enhancement.ts +3 -2
  160. package/src/rsc/request-stages.ts +59 -0
  161. package/src/rsc/rsc-rendering.ts +49 -33
  162. package/src/rsc/server-action.ts +2 -1
  163. package/src/rsc/shell-capture.ts +4 -5
  164. package/src/rsc/shell-serve.ts +12 -1
  165. package/src/segment-system.tsx +1 -1
  166. package/src/server/context.ts +37 -6
  167. package/src/server/cookie-store.ts +22 -1
  168. package/src/server/document-location-state.ts +23 -2
  169. package/src/server/handle-store.ts +2 -0
  170. package/src/server/request-context.ts +113 -28
  171. package/src/testing/dispatch.ts +3 -0
  172. package/src/testing/e2e/fixture.ts +4 -12
  173. package/src/testing/e2e/server.ts +91 -4
  174. package/src/testing/render-route.tsx +269 -4
  175. package/src/testing/serve-shell-request.ts +6 -3
  176. package/src/types/handler-context.ts +7 -5
  177. package/src/types/loader-types.ts +7 -4
  178. package/src/types/segments.ts +15 -4
  179. package/src/urls/path-helper-types.ts +7 -5
  180. package/src/vite/temp-rsc-server.ts +2 -2
@@ -71,12 +71,21 @@ export interface LinkProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorEle
71
71
  * server cannot see.
72
72
  *
73
73
  * The server scopes a prefetch to its source automatically
74
- * (`x-rsc-prefetch-scope: source`) when an intercept targets the route,
75
- * whether or not it applies from this page (the modal and the full page
76
- * are both source-specific), and when a `revalidate()` of the target
77
- * read `currentParams`, `currentUrl` or `fromRouteName` and kept the
78
- * segment: that diff is the source page's (#1331). Every other response
79
- * stays shared.
74
+ * (`x-rsc-prefetch-scope: source`) when an intercept targets the route or
75
+ * the source page, whether or not it applies (the modal and the full page
76
+ * are both source-specific), when the answer defers a `prefetch: false`
77
+ * unit the source does not hold, when a `revalidate()` of the target read
78
+ * `currentUrl` and kept the segment, and when a kept answer sends no
79
+ * segment. A `revalidate()` that read `currentParams` or `fromRouteName`
80
+ * and kept the segment, or a prerendered target that keeps segments on
81
+ * equal params (`storedEntrySends`), shares the answer between the pages
82
+ * with equal values of what was read (`x-rsc-prefetch-scope: params`,
83
+ * #1331). Every other response stays shared. Use the key
84
+ * for what the server does not track: `context` in a `revalidate()`, or
85
+ * a `Referer` read.
86
+ *
87
+ * A source-scoped entry is keyed by the source page's full URL, query
88
+ * and hash included, so every page prefetches the link again.
80
89
  *
81
90
  * @example
82
91
  * ```tsx
@@ -600,7 +600,15 @@ export interface NavigationStore {
600
600
  */
601
601
  getPageHistoryKeys(historyKey: string): string[];
602
602
  markHistoryCacheStale(): void;
603
+ /**
604
+ * Mark history entries and prefetches stale without a broadcast: a cookie
605
+ * read saw another write (rango-state.ts setRangoStateWriteObserver).
606
+ */
607
+ markCachesStaleAfterWrite(): void;
608
+ /** A server action without a directive: mark stale, soft broadcast. */
603
609
  markCacheAsStaleAndBroadcast(): void;
610
+ /** The client seat of invalidateClientCache(): flush, rotate, broadcast. */
611
+ invalidateCacheAndBroadcast(): void;
604
612
  clearHistoryCache(): void;
605
613
  setCrossTabRefreshCallback(callback: () => void): void;
606
614
  getInterceptSourceUrl(): string | null;
@@ -637,6 +645,16 @@ export interface FetchPartialOptions {
637
645
  * to a prefetch cache or the HTTP cache, and it cancels no prefetch.
638
646
  */
639
647
  fill?: boolean;
648
+ /**
649
+ * Whether a prefetch a server action marked stale (prefetch/cache.ts
650
+ * isPrefetchStale) may answer this navigation. Only a click sets it: it
651
+ * revalidates its page once it settled (navigation-bridge.ts navigate,
652
+ * partial-update.ts onStalePrefetch). For any other caller (Back/Forward on
653
+ * a history-cache miss, leaving an intercept) a stale stored entry is a
654
+ * miss, and a stale in-flight one is dropped and fetched again, so no path
655
+ * that never revalidates commits pre-write data.
656
+ */
657
+ acceptStalePrefetch?: boolean;
640
658
  }
641
659
  /**
642
660
  * Result of a partial fetch including stream completion tracking
@@ -654,6 +672,13 @@ export interface FetchPartialResult {
654
672
  * fallbacks stream as usual.
655
673
  */
656
674
  fullyPrefetched?: boolean;
675
+ /**
676
+ * True when the payload is a prefetch a server action marked stale after it
677
+ * was fetched (prefetch/cache.ts isPrefetchStale). The click commits it as
678
+ * any prefetch; the navigation then revalidates its page once it settled,
679
+ * as Back onto a stale history entry does (navigation-bridge.ts navigate).
680
+ */
681
+ stalePrefetch?: boolean;
657
682
  }
658
683
  /**
659
684
  * Navigation client for fetching RSC payloads
@@ -4,5 +4,5 @@ export { generatePerModuleTypesSource, generateRouteTypesSource, } from "./route
4
4
  export { DEFAULT_EXCLUDE_PATTERNS, type ScanFilter, createScanFilter, findTsFiles, isInSkippedScanDir, } from "./route-types/scan-filter.js";
5
5
  export { writePerModuleRouteTypes, writePerModuleRouteTypesForFile, } from "./route-types/per-module-writer.js";
6
6
  export { type UnresolvableReason, type UnresolvableInclude, extractIncludesWithDiagnostics, } from "./route-types/include-resolution.js";
7
- export { extractUrlsFromRouter, type UrlsExtractionResult, buildCombinedRouteMapForRouterFile, detectUnresolvableIncludes, detectUnresolvableIncludesForUrlsFile, findNestedRouterConflict, formatNestedRouterConflictError, findRouterFiles, findHostRouterFiles, writeCombinedRouteTypes, genFileTsPath, resolveSearchSchemas, } from "./route-types/router-processing.js";
7
+ export { extractUrlsFromRouter, type UrlsExtractionResult, buildCombinedRouteMapForRouterFile, detectUnresolvableIncludes, detectUnresolvableIncludesForUrlsFile, findNestedRouterConflict, formatNestedRouterConflictError, findRouterFiles, findHostRouterFiles, isRoutableSourceFile, writeCombinedRouteTypes, genFileTsPath, resolveSearchSchemas, } from "./route-types/router-processing.js";
8
8
  export { findUrlsVariableNames } from "./route-types/per-module-writer.js";
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- export type UnresolvableReason = "factory-call" | "dynamic-expression" | "unresolvable-import" | "file-not-found" | "unresolvable-basename";
2
+ export type UnresolvableReason = "factory-call" | "dynamic-expression" | "unresolvable-import" | "file-not-found" | "unresolvable-basename" | "unresolvable-routes";
3
3
  export interface UnresolvableInclude {
4
4
  pathPrefix: string;
5
5
  namePrefix: string | null;
@@ -25,6 +25,14 @@ export interface ScanMemo {
25
25
  blockSourceFiles: Map<string, ts.SourceFile>;
26
26
  }
27
27
  export declare function createScanMemo(): ScanMemo;
28
+ export declare function isUrlsDefinitionCall(node: ts.CallExpression, aliases?: ReadonlySet<string>): boolean;
29
+ /**
30
+ * Local names that `import { urls as u, clientUrls as cu } from
31
+ * "@rangojs/router"` (or `/client`) binds to urls()/clientUrls(), so a renamed
32
+ * import is read like the plain name. Other modules' exports are not guessed
33
+ * at: a `urls` from elsewhere is read only under its own name.
34
+ */
35
+ export declare function collectUrlsImportAliases(sourceFile: ts.SourceFile): ReadonlySet<string>;
28
36
  /**
29
37
  * Extract include() calls with diagnostics for unresolvable ones.
30
38
  * Returns both resolved includes (identifier second args) and unresolvable
@@ -1,5 +1,6 @@
1
1
  import { type ScanFilter } from "./scan-filter.js";
2
2
  import { type UnresolvableInclude } from "./include-resolution.js";
3
+ export declare function isRoutableSourceFile(name: string): boolean;
3
4
  export declare function findNestedRouterConflict(routerFiles: string[]): {
4
5
  ancestor: string;
5
6
  nested: string;
@@ -22,11 +23,12 @@ export type UrlsExtractionResult = {
22
23
  };
23
24
  /**
24
25
  * Extract the url patterns from a router file using AST.
25
- * Detects four patterns:
26
+ * Detects these patterns, in either mount (`.routes(x)` or `urls: x`):
26
27
  * 1. createRouter(...).routes(variableName)
27
28
  * 2. createRouter({ urls: variableName, ... })
28
29
  * 3. createRouter(...).routes(({ path, ... }) => [...])
29
30
  * 4. createRouter({ urls: ({ path, ... }) => [...], ... })
31
+ * 5. createRouter(...).routes(urls(({ path, ... }) => [...])), and clientUrls()
30
32
  * Returns either a variable name or an inline code block.
31
33
  */
32
34
  export declare function extractUrlsFromRouter(code: string): UrlsExtractionResult | null;
@@ -97,9 +97,10 @@ export declare const KEY_PART_PREFIX = "key:";
97
97
  * (CacheScope.resolveKeyFrom, CacheScope.resolvePartition): the `key()`
98
98
  * results of the scope's cache() chain, outermost first, then its default
99
99
  * parts: the keyGenerator results of enclosing scopes on another store
100
- * (#974) and, for a route record key, the scope's own part: without a
100
+ * (#974) and, for a route record key, the scope's own parts: without a
101
101
  * `key()` of its own, its default key (or its store's keyGenerator result,
102
- * #970); with one, the default key's request kind (`doc`, `partial` or
102
+ * #970, then the request kind when that result does not start with it,
103
+ * #1298); with one, the default key's request kind (`doc`, `partial` or
103
104
  * `intercept:<intercept>`, routeRecordKind in cache-scope.ts).
104
105
  *
105
106
  * Key scheme invariant (#975). A `key()` result is often request input,
@@ -119,7 +120,9 @@ export declare const KEY_PART_PREFIX = "key:";
119
120
  * - one `key()` part is the key; two or more parts are joined by `|`,
120
121
  * `key()` parts first;
121
122
  * - a lone default part stays raw: with no `key()` on the chain the record
122
- * key is the default key or the store's keyGenerator result, unchanged.
123
+ * key is the default key or the store's keyGenerator result, unchanged
124
+ * when that result starts with its request kind, else that result and
125
+ * the kind composed (`items|doc`).
123
126
  *
124
127
  * Why nothing collides: the router's default keys (`doc:`, `partial:`,
125
128
  * `intercept:`, `response:<type>:`) never start with `key:` and hold a `:`.
@@ -128,11 +131,12 @@ export declare const KEY_PART_PREFIX = "key:";
128
131
  * Among composed keys, splitting on `|` recovers each part, a part's kind
129
132
  * (a `key:` prefix, or no `:` at all) and its value. A request kind part is
130
133
  * a default key's prefix without the URL after it, so it never equals a
131
- * default key part; a keyGenerator part can equal it (`doc`) only when the
132
- * keyGenerator returns a constant, which already gives every URL one record.
133
- * Pinned by the collision probe in
134
- * src/cache/__tests__/cache-scope-chain.test.ts. A keyGenerator result is
135
- * the store's own namespace, as before.
134
+ * default key part. A raw keyGenerator record key starts with its kind and
135
+ * holds a `:`, so it never equals a composed one, and two kinds' prefixes
136
+ * differ; a result without its kind is composed with it, never prefixed,
137
+ * since `doc:` + `x` would be the raw result `doc:x`. Pinned by the
138
+ * collision probes in src/cache/__tests__/cache-scope-chain.test.ts. A
139
+ * keyGenerator result is the store's own namespace, as before.
136
140
  *
137
141
  * Shared with the testing helper `shellCacheKey`, so a test builds the
138
142
  * production partition.
@@ -200,8 +200,10 @@ export declare class CacheScope {
200
200
  * replaces the URL part, and its store's keyGenerator does not run. It
201
201
  * appends `kind` instead (routeRecordKind): the `key()` decides which
202
202
  * requests of one kind share a record, never that a document, a
203
- * navigation and an intercept navigation do. With no partition part it is
204
- * resolveDefaultKey alone. With a `key()` on the chain but no request
203
+ * navigation and an intercept navigation do. A keyGenerator result that
204
+ * does not start with its kind gets `kind` after it too (ownParts). With
205
+ * no partition part it is resolveDefaultKey alone, or that result and
206
+ * `kind` composed. With a `key()` on the chain but no request
205
207
  * context it rejects (keyWithoutContextError). `partitionedPrefix`
206
208
  * prefixes a key built from partition parts only (the response family's
207
209
  * `response:`, which passes no `kind`: a response route answers every
@@ -369,19 +369,23 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
369
369
  /**
370
370
  * Custom key generator applied to all cache operations.
371
371
  * Receives the full RequestContext (including env) and the default-generated key.
372
- * Return value becomes the final cache key (unless route overrides with `key` option).
372
+ * The return value is the cache key (unless route overrides with `key`
373
+ * option), except that a route record keeps its request kind: a result that
374
+ * does not start with `defaultKey`'s prefix (`doc:`, `partial:`,
375
+ * `intercept:<intercept>:`) gets the kind appended (see
376
+ * SegmentCacheStore.keyGenerator).
373
377
  *
374
378
  * Reserved prefixes: tag-invalidation markers live in the SAME KV namespace as
375
379
  * data, keyed `__tag__/<tag>` (and `__tagmarker__/<tag>` for the L1 cache). A
376
380
  * returned key must NOT begin with `__tag__/` or `__tagmarker__/`, or it can
377
381
  * collide with a tag marker and corrupt invalidation. The documented
378
- * prepend-style generators below are safe.
382
+ * generators below start with `defaultKey` and are safe.
379
383
  *
380
384
  * @example Using headers for user segmentation
381
385
  * ```typescript
382
386
  * keyGenerator: (ctx, defaultKey) => {
383
387
  * const segment = ctx.request.headers.get('x-user-segment') || 'default';
384
- * return `${segment}:${defaultKey}`;
388
+ * return `${defaultKey}|${segment}`;
385
389
  * }
386
390
  * ```
387
391
  *
@@ -389,7 +393,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
389
393
  * ```typescript
390
394
  * keyGenerator: (ctx, defaultKey) => {
391
395
  * const region = ctx.env.REGION || 'us';
392
- * return `${region}:${defaultKey}`;
396
+ * return `${defaultKey}|${region}`;
393
397
  * }
394
398
  * ```
395
399
  *
@@ -397,7 +401,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
397
401
  * ```typescript
398
402
  * keyGenerator: (ctx, defaultKey) => {
399
403
  * const locale = cookies().get('locale')?.value || 'en';
400
- * return `${locale}:${defaultKey}`;
404
+ * return `${defaultKey}|${locale}`;
401
405
  * }
402
406
  * ```
403
407
  */
@@ -12,7 +12,15 @@ export interface CacheDirectives {
12
12
  /**
13
13
  * Check if response should be cached based on Cache-Control headers
14
14
  */
15
- export declare function shouldCacheResponse(response: Response, requestCtx?: Pick<RequestContext, "_payloadVisitorTheme" | "_payloadDeferred" | "_payloadVisitorLocation" | "_documentLocationState">): CacheDirectives | null;
15
+ export declare function shouldCacheResponse(response: Response, requestCtx?: Pick<RequestContext, "_payloadVisitorTheme" | "_payloadDeferred" | "_payloadSourceScoped" | "_payloadVisitorLocation" | "_documentLocationState">): CacheDirectives | null;
16
+ /**
17
+ * Whether a response carries one client's state, which a stored copy would
18
+ * replay to that client's later requests: a per-client signal header (a state
19
+ * cookie rotation, `x-rango-keep-cache`) or location state. The stored
20
+ * prefetch Cache-Control (rsc/request-stages.ts) is not sent on
21
+ * one.
22
+ */
23
+ export declare function carriesClientState(response: Response, requestCtx: Pick<RequestContext, "_locationState" | "_documentLocationState">): boolean;
16
24
  /**
17
25
  * Whether this cache will store the response the current render produces, as
18
26
  * far as the render can tell: the middleware runs it (a MISS or a stale
@@ -46,13 +46,14 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
46
46
  defaults?: CacheDefaults;
47
47
  /**
48
48
  * Custom key generator applied to all cache operations.
49
- * Receives the full RequestContext and the default-generated key.
49
+ * Receives the full RequestContext and the default-generated key. A route
50
+ * record keeps its request kind (see SegmentCacheStore.keyGenerator).
50
51
  *
51
52
  * @example
52
53
  * ```typescript
53
54
  * keyGenerator: (ctx, defaultKey) => {
54
55
  * const locale = cookies().get('locale')?.value || 'en';
55
- * return `${locale}:${defaultKey}`;
56
+ * return `${defaultKey}|${locale}`;
56
57
  * }
57
58
  * ```
58
59
  */
@@ -88,8 +88,8 @@ export declare function serializeSegments(segments: ResolvedSegment[], onError?:
88
88
  * cached with the route record).
89
89
  *
90
90
  * The loading "null" sentinel decodes here (not on the consumer): loading:null
91
- * must survive as null, and shipping the raw sentinel would change the
92
- * deduplicateLoaderSegments loading-presence check.
91
+ * must survive as null, and shipping the raw sentinel would hand renderSegments
92
+ * a truthy `loading`, the fallback tree shape (docs/tree-structure.md).
93
93
  */
94
94
  export declare function fragmentSegments(data: SerializedSegmentData[]): Promise<ResolvedSegment[]>;
95
95
  /**
@@ -201,7 +201,7 @@ export interface ShellLoaderSeedEntry {
201
201
  * Deserializations run in parallel; a record that fails to decode is skipped
202
202
  * and reported to `onUndecodable`: its loader runs instead, which a document
203
203
  * HIT has already committed a prelude against, so serveShellHit retires the
204
- * entry (dc-t89). Returns undefined when the snapshot carries no loader
204
+ * entry. Returns undefined when the snapshot carries no loader
205
205
  * records, without touching the Flight codec (kept lazy for cold paths and
206
206
  * non-RSC configs).
207
207
  */
@@ -83,16 +83,21 @@ export interface SegmentCacheStore<TEnv = unknown> {
83
83
  /**
84
84
  * Custom key generator applied to all cache operations using this store.
85
85
  * Receives the full RequestContext and the default-generated key.
86
- * Return value becomes the final cache key (unless route overrides with `key` option).
86
+ * The return value is the cache key (unless route overrides with `key`
87
+ * option), with a route record's request kind appended when it does not
88
+ * start with that kind (below).
87
89
  *
88
90
  * Resolution priority:
89
91
  * 1. Route-level `key` function (replaces the URL part; the request kind stays)
90
92
  * 2. Store-level `keyGenerator` (modifies default key)
91
93
  * 3. Default key generation (prefix:pathname:params)
92
94
  *
93
- * Keep `defaultKey` in the result: its prefix names the request kind (a
94
- * document, a navigation or an intercept navigation), whose records hold
95
- * different segments. A result without it gives every kind one record.
95
+ * A route record keeps its request kind (a document, a navigation or an
96
+ * intercept navigation), whose records hold different segments: the kind
97
+ * is `defaultKey`'s prefix (`doc:`, `partial:`, `intercept:<intercept>:`),
98
+ * and a result that does not start with it gets the kind appended as a
99
+ * key part of its own. Build on `defaultKey` (`${defaultKey}|${segment}`)
100
+ * and the result is the record key byte for byte.
96
101
  *
97
102
  * Return `defaultKey` unchanged to leave a request unpartitioned. An empty
98
103
  * string is not a key: where the result partitions a nested cache() on
@@ -104,7 +109,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
104
109
  * ```typescript
105
110
  * keyGenerator: (ctx, defaultKey) => {
106
111
  * const segment = ctx.request.headers.get('x-user-segment') || 'default';
107
- * return `${segment}:${defaultKey}`;
112
+ * return `${defaultKey}|${segment}`;
108
113
  * }
109
114
  * ```
110
115
  *
@@ -112,7 +117,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
112
117
  * ```typescript
113
118
  * keyGenerator: (ctx, defaultKey) => {
114
119
  * const region = ctx.env.REGION || 'us';
115
- * return `${region}:${defaultKey}`;
120
+ * return `${defaultKey}|${region}`;
116
121
  * }
117
122
  * ```
118
123
  *
@@ -120,7 +125,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
120
125
  * ```typescript
121
126
  * keyGenerator: (ctx, defaultKey) => {
122
127
  * const locale = cookies().get('locale')?.value || 'en';
123
- * return `${locale}:${defaultKey}`;
128
+ * return `${defaultKey}|${locale}`;
124
129
  * }
125
130
  * ```
126
131
  */
@@ -125,7 +125,8 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
125
125
  defaults?: CacheDefaults;
126
126
  /**
127
127
  * Custom key generator applied to all cache operations (region/locale/user
128
- * segmentation). Receives the request context and the default key.
128
+ * segmentation). Receives the request context and the default key. A route
129
+ * record keeps its request kind (see SegmentCacheStore.keyGenerator).
129
130
  */
130
131
  keyGenerator?: (ctx: RequestContext<TEnv>, defaultKey: string) => string | Promise<string>;
131
132
  /**
@@ -48,8 +48,9 @@ export interface ClientUrlLoaderRecord {
48
48
  */
49
49
  readonly ssr?: false;
50
50
  /**
51
- * loader(Def, { prefetch: false }): a prefetch of the route, or of an
52
- * intercept's target from its source, defers this loader and the click
51
+ * loader(Def, { prefetch: false }): a viewport or render prefetch of the
52
+ * route, or of an intercept's target from its source, defers this loader
53
+ * (a hover prefetch runs it) and the click
53
54
  * that adopts it runs it in its fill request (see {@link LoaderOptions}).
54
55
  * Projected onto the materialized stub.
55
56
  */
@@ -118,12 +119,13 @@ export interface ClientUrlHelpers {
118
119
  * holes).
119
120
  *
120
121
  * Pass `{ prefetch: false }` to keep this loader out of the route's
121
- * prefetches (see {@link LoaderOptions}): an in-group click still shows
122
+ * viewport and render prefetches; a hover prefetch runs it (see
123
+ * {@link LoaderOptions}). An in-group click still shows
122
124
  * the destination at once, commits the adopted prefetch with the other
123
125
  * loaders' data, and runs only the flagged loaders in its one fill
124
126
  * request. A `layout()` loader with the flag is flagged on every route
125
- * below it. Inside intercept() it defers the loader in a prefetch of the
126
- * intercept from its source, as in urls().
127
+ * below it. Inside intercept() it defers the loader in a viewport or
128
+ * render prefetch of the intercept from its source, as in urls().
127
129
  */
128
130
  readonly loader: <TData>(definition: LoaderDefinition<TData>, optionsOrUse?: Pick<LoaderOptions, "ssr" | "prefetch"> | ClientUrlUse, use?: ClientUrlUse) => ClientUrlItem;
129
131
  readonly loading: (component: ReactNode) => ClientUrlItem;
@@ -7,6 +7,15 @@
7
7
  */
8
8
  /** Request header of a `<Link>` prefetch (browser/prefetch/fetch.ts). */
9
9
  export declare const PREFETCH_HEADER = "X-Rango-Prefetch";
10
+ /**
11
+ * PREFETCH_HEADER's value on an intent prefetch: a hover or an explicit
12
+ * `prefetch()` call (browser/prefetch/fetch.ts prefetchDirect). The server
13
+ * defers nothing in it: `prefetch: false` loaders and the work behind a
14
+ * flagged `loading()` run, as in the navigation it starts early. A
15
+ * speculative prefetch (viewport, render) sends `1`, and any value other than
16
+ * this one reads as speculative.
17
+ */
18
+ export declare const PREFETCH_INTENT = "intent";
10
19
  /**
11
20
  * The last row of a prefetch whose render reported an error
12
21
  * (rsc/prefetch-render-error.ts): a Flight hint with a code React does not
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The source group of a prefetch answer a revalidate() rule kept from the
3
+ * page being left (#1331). Shared by the server, which reports
4
+ * the group in `x-rsc-prefetch-scope: params; <label>` (rsc/rsc-rendering.ts),
5
+ * and the browser, which stores the answer under that label and rebuilds it
6
+ * from the page on screen to look it up (browser/prefetch/cache.ts).
7
+ * Dependency free, like prefetch-markers.ts: the browser must not import the
8
+ * server modules that record the reads (router/source-reads.ts).
9
+ *
10
+ * Why a label is enough: a rule's decision depends on the source page only
11
+ * through what it read. Two pages with equal values of everything the kept
12
+ * rules read run every rule the same way, so one answer serves both.
13
+ */
14
+ /** What the revalidate() rules that kept a segment read of the source page. */
15
+ export interface SourceReads {
16
+ /**
17
+ * `currentParams` was read. Its key set is then part of the group: a rule
18
+ * can test a key's presence (`in`, `Object.keys`) without reading a value.
19
+ */
20
+ keys: boolean;
21
+ /**
22
+ * Each `currentParams` value read; a spread or JSON.stringify reads all.
23
+ * Non-empty only with `keys`: a value is read through `currentParams`.
24
+ */
25
+ params: Set<string>;
26
+ /** `fromRouteName` was read. */
27
+ route: boolean;
28
+ /** `currentUrl` was read: the answer is the exact page's. */
29
+ url: boolean;
30
+ }
31
+ /** The page a label describes: the source on the server, the screen in the browser. */
32
+ export interface SourcePage {
33
+ params: Readonly<Record<string, string | undefined>>;
34
+ /** The public route name (route-name.ts publicRouteName). */
35
+ routeName: string | undefined;
36
+ }
37
+ /** What a label reads, without the values: one per kind of rule set. */
38
+ export interface SourceShape {
39
+ keys: boolean;
40
+ route: boolean;
41
+ /** Sorted. */
42
+ names: readonly string[];
43
+ }
44
+ /** `x-rsc-prefetch-scope` value prefix of a params-grouped answer. */
45
+ export declare const PARAMS_SCOPE_PREFIX = "params; ";
46
+ export declare function createSourceReads(): SourceReads;
47
+ export declare function hasSourceReads(reads: SourceReads): boolean;
48
+ /** Union `from` into `into`. */
49
+ export declare function mergeSourceReads(into: SourceReads, from: SourceReads): void;
50
+ export declare function shapeOfReads(reads: SourceReads): SourceShape;
51
+ /**
52
+ * The label of `page` under `shape`: `&`-joined, `$k=<keys>` first, then
53
+ * `$r[=<route>]`, then `<name>[=<value>]` per read param, sorted. A missing
54
+ * value has no `=`. Names and values are URI-encoded, so a `$` entry never
55
+ * collides with a param (encodeURIComponent encodes `$`).
56
+ */
57
+ export declare function formatSourceLabel(shape: SourceShape, page: SourcePage): string;
58
+ /**
59
+ * The label of an `x-rsc-prefetch-scope` value, or undefined when the value
60
+ * is not a params scope.
61
+ */
62
+ export declare function paramsScopeLabel(header: string | null | undefined): string | undefined;
63
+ /** The shape of a label formatSourceLabel wrote. */
64
+ export declare function parseSourceShape(label: string): SourceShape;
65
+ /** A stable id of a shape, for indexing. */
66
+ export declare function sourceShapeId(shape: SourceShape): string;
@@ -231,9 +231,10 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
231
231
  * loader(RecommendationsLoader), // still streams behind loading()
232
232
  * ```
233
233
  *
234
- * Pass `{ prefetch: false }` to keep the loader out of prefetches: a
235
- * prefetch skips it and the click that adopts the prefetch fetches it in
236
- * one follow-up request (see {@link LoaderOptions}). Only on a segment the
234
+ * Pass `{ prefetch: false }` to keep the loader out of speculative
235
+ * prefetches (viewport, render): such a prefetch skips it and the click
236
+ * that adopts it fetches it in one follow-up request; a hover prefetch runs
237
+ * it (see {@link LoaderOptions}). Only on a segment the
237
238
  * client does not have yet: a loader the client already holds runs in the
238
239
  * prefetch when it revalidates.
239
240
  *
@@ -255,14 +256,16 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
255
256
  * // Skip loading on SSR, only show on client navigation
256
257
  * loading(<Skeleton />, { ssr: false })
257
258
  *
258
- * // Keep everything behind this fallback out of prefetches
259
+ * // Keep everything behind this fallback out of viewport and render
260
+ * // prefetches (a hover prefetch runs it)
259
261
  * loading(<Skeleton />, { prefetch: false })
260
262
  * ```
261
263
  * @param component - The loading UI to show during navigation
262
264
  * @param options - Configuration options
263
265
  * @param options.ssr - If false, skip showing loading on document requests (SSR)
264
266
  * @param options.prefetch - If false, the flag applies when this entry's
265
- * segment is new to the client. A prefetch then skips the whole unit: the
267
+ * segment is new to the client. A viewport or render prefetch then skips
268
+ * the whole unit (a hover prefetch runs it): the
266
269
  * handler, its loaders, its orphan layouts and parallel slots and, for a
267
270
  * layout, every deeper entry; the click that adopts the prefetch shows
268
271
  * this fallback and fetches them in one follow-up request, which reads
@@ -7,20 +7,15 @@
7
7
  import type { EntryData, InterceptEntry, InterceptSelectorContext } from "../server/context";
8
8
  import type { ResolvedSegment } from "../types";
9
9
  import type { ResolutionPass } from "./segment-resolution/resolution-pass.js";
10
+ import { type InterceptResult } from "./router-context.js";
10
11
  /**
11
12
  * Check if an intercept's when conditions are satisfied.
12
13
  * All when() functions must return true for the intercept to activate.
13
14
  * If no when() conditions are defined, the intercept always activates.
14
15
  * A selector that throws yields the conservative default, no intercept (the
15
16
  * full page renders), and is logged; it never fails the request.
16
- *
17
- * During action revalidation, when() is NOT evaluated.
18
- */
19
- export declare function evaluateInterceptWhen(intercept: InterceptEntry, selectorContext: InterceptSelectorContext | null, isAction: boolean): boolean;
20
- /**
21
- * Find an intercept for the target route by walking up the entry chain.
22
- * Returns the first (innermost) matching intercept along with the entry that defines it.
23
17
  */
18
+ export declare function evaluateInterceptWhen(intercept: InterceptEntry, selectorContext: InterceptSelectorContext | null): boolean;
24
19
  /**
25
20
  * Collect every intercept TARGET route name reachable from an origin entry —
26
21
  * the same chain walk findInterceptForRoute() performs when this location is
@@ -33,10 +28,29 @@ export declare function evaluateInterceptWhen(intercept: InterceptEntry, selecto
33
28
  * loses its optimistic loading, never the reverse.
34
29
  */
35
30
  export declare function collectInterceptTargetNames(fromEntry: EntryData | null): string[];
36
- export declare function findInterceptForRoute(targetRouteKey: string, fromEntry: EntryData | null, selectorContext?: InterceptSelectorContext | null, isAction?: boolean): {
37
- intercept: InterceptEntry;
38
- entry: EntryData;
39
- } | null;
31
+ /**
32
+ * The intercept for the target route, with the entry that declares it.
33
+ * `targetRouteKeys` are the names it may be declared for, in precedence
34
+ * order: the route's own name, then its local name inside include()
35
+ * (match-api.ts). A navigation, or an action with no `held`, takes the first
36
+ * along the chain from `fromEntry` up under the first name that has one: on a
37
+ * navigation, the first whose when() passes; on an action, the first, when()
38
+ * not evaluated.
39
+ *
40
+ * An action given `held` (the request's segment ids) re-renders the intercept
41
+ * on screen: one whose slot id the client holds, null when it holds none. Its
42
+ * candidates are collected under every name in one walk, the route's own
43
+ * name's first: a held id under the own name (a parallel() default of the
44
+ * slot's name) must not win before the local name is looked at. One held:
45
+ * that one, when() not evaluated, so an open intercept stays open when its
46
+ * when() would now reject. Several held (intercepts on one entry that fill one
47
+ * slot share its id, as does a parallel() slot of that name): the ones whose
48
+ * slot has no parallel() default on their entry, if any (a default holds its
49
+ * slot id whether or not an intercept is open there). One left: that one,
50
+ * when() not evaluated. Else the first whose when() passes against the
51
+ * action's intercept source, else the first.
52
+ */
53
+ export declare function findInterceptForRoute(targetRouteKeys: string | readonly string[], fromEntry: EntryData | null, selectorContext?: InterceptSelectorContext | null, isAction?: boolean, held?: ReadonlySet<string>): InterceptResult | null;
40
54
  /** The segment id of `intercept`'s slot on `parentEntry`. */
41
55
  export declare function interceptSlotId(parentEntry: EntryData, intercept: InterceptEntry): string;
42
56
  /**
@@ -177,6 +177,7 @@ export interface MatchPipelineState {
177
177
  cachedSegments?: ResolvedSegment[];
178
178
  cachedMatchedIds?: string[];
179
179
  shouldRevalidate?: boolean;
180
+ storedStale?: boolean;
180
181
  cacheSource?: "runtime" | "prerender";
181
182
  segments: ResolvedSegment[];
182
183
  matchedIds: string[];
@@ -11,7 +11,7 @@ export interface MatchHandlerDeps<TEnv = any> {
11
11
  matchApiDeps: MatchApiDeps<TEnv>;
12
12
  defaultErrorBoundary: ReactNode | ErrorBoundaryHandler | undefined;
13
13
  findMatch: (pathname: string, ms?: any) => any;
14
- findInterceptForRoute: (routeKey: string, parentEntry: EntryData | null, selectorContext: InterceptSelectorContext | null, isAction: boolean) => {
14
+ findInterceptForRoute: (routeKeys: string | readonly string[], parentEntry: EntryData | null, selectorContext: InterceptSelectorContext | null, isAction: boolean, held?: ReadonlySet<string>) => {
15
15
  intercept: InterceptEntry;
16
16
  entry: EntryData;
17
17
  } | null;
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Resolve the prefetch cache TTL once, at router init, into the values the
3
3
  * rest of the router consumes: seconds, and milliseconds for the client-side
4
- * in-memory prefetch cache (shipped in payload metadata). Prefetch responses
5
- * carry no Cache-Control: the browser HTTP cache would keep a prefetch whose
6
- * render failed after its 200 went out (#1170).
4
+ * in-memory prefetch cache (shipped in payload metadata). A prefetch with a
5
+ * live part carries no Cache-Control: the browser HTTP cache would keep a
6
+ * prefetch whose render failed after its 200 went out (#1170). One answered
7
+ * from storage carries storedPrefetchCacheControl.
7
8
  *
8
9
  * `false` disables prefetch caching (seconds 0). A non-finite input
9
10
  * (NaN/Infinity, e.g. from an env-derived number that failed to parse) is
@@ -22,3 +23,10 @@ export interface ResolvedPrefetchCacheTTL {
22
23
  ms: number;
23
24
  }
24
25
  export declare function resolvePrefetchCacheTTL(rawTTL: number | false | undefined): ResolvedPrefetchCacheTTL;
26
+ /**
27
+ * The Cache-Control of a prefetch answer with nothing live in it
28
+ * (`_servedFromStorage`): the browser HTTP cache keeps it as long
29
+ * as the in-memory prefetch cache would. Undefined when prefetch caching is
30
+ * off.
31
+ */
32
+ export declare function storedPrefetchCacheControl(ttlMs: number): string | undefined;
@@ -33,7 +33,7 @@ export interface RouterContext<TEnv = any> {
33
33
  };
34
34
  getMetricsStore: () => MetricsStore | undefined;
35
35
  createCacheScope: (cacheConfig: any, parent: CacheScope | null) => CacheScope | null;
36
- findInterceptForRoute: (routeKey: string, parentEntry: EntryData | null, selectorContext: InterceptSelectorContext, isAction: boolean) => InterceptResult | null;
36
+ findInterceptForRoute: (routeKeys: string | readonly string[], parentEntry: EntryData | null, selectorContext: InterceptSelectorContext, isAction: boolean, held?: ReadonlySet<string>) => InterceptResult | null;
37
37
  /** The segment walker; a full match passes freshPassInput (FRESH). */
38
38
  resolveAllSegments: (pass: ResolutionPassInput<TEnv>, entries: EntryData[], walk: RevalidatingWalkOptions) => Promise<{
39
39
  segments: ResolvedSegment[];