@rangojs/router 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/bin/rango.js +13 -1
  2. package/dist/types/browser/event-controller.d.ts +30 -13
  3. package/dist/types/browser/navigation-store.d.ts +9 -0
  4. package/dist/types/browser/partial-update.d.ts +2 -1
  5. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  6. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  7. package/dist/types/browser/react/context.d.ts +14 -0
  8. package/dist/types/browser/rsc-router.d.ts +13 -2
  9. package/dist/types/browser/types.d.ts +10 -0
  10. package/dist/types/build/generate-manifest.d.ts +2 -0
  11. package/dist/types/cache/background-task.d.ts +22 -0
  12. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  13. package/dist/types/cache/cache-policy.d.ts +17 -0
  14. package/dist/types/cache/cache-scope.d.ts +1 -1
  15. package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
  16. package/dist/types/cache/document-cache.d.ts +8 -5
  17. package/dist/types/cache/handle-snapshot.d.ts +23 -0
  18. package/dist/types/cache/index.d.ts +1 -1
  19. package/dist/types/cache/invalidation-order.d.ts +14 -0
  20. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  21. package/dist/types/cache/shell-snapshot.d.ts +10 -1
  22. package/dist/types/cache/store-scope.d.ts +22 -0
  23. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  24. package/dist/types/cache/types.d.ts +63 -14
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
  26. package/dist/types/errors.d.ts +6 -0
  27. package/dist/types/handles/deferred-resolution.d.ts +7 -0
  28. package/dist/types/internal-suspense-audit.d.ts +1 -0
  29. package/dist/types/loader-store.d.ts +4 -8
  30. package/dist/types/outlet-provider.d.ts +2 -1
  31. package/dist/types/prerender/cloudflare.d.ts +49 -0
  32. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  33. package/dist/types/prerender/index.d.ts +12 -0
  34. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  35. package/dist/types/prerender/on-demand.d.ts +272 -0
  36. package/dist/types/prerender/producer-guard.d.ts +33 -0
  37. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  38. package/dist/types/prerender/warm-request.d.ts +65 -0
  39. package/dist/types/prerender/warm.d.ts +65 -0
  40. package/dist/types/prerender/writable-store.d.ts +170 -0
  41. package/dist/types/prerender.d.ts +27 -0
  42. package/dist/types/reverse.d.ts +1 -1
  43. package/dist/types/route-content-wrapper.d.ts +3 -1
  44. package/dist/types/router/error-handling.d.ts +2 -0
  45. package/dist/types/router/handler-context.d.ts +1 -1
  46. package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
  47. package/dist/types/router/pattern-matching.d.ts +2 -0
  48. package/dist/types/router/prefetch-default.d.ts +4 -4
  49. package/dist/types/router/prerender-match.d.ts +23 -1
  50. package/dist/types/router/route-trie-builder.d.ts +8 -1
  51. package/dist/types/router/router-context.d.ts +1 -0
  52. package/dist/types/router/router-interfaces.d.ts +30 -0
  53. package/dist/types/router/router-options.d.ts +24 -0
  54. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
  56. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  57. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  58. package/dist/types/router/trie-matching.d.ts +2 -0
  59. package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
  60. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  61. package/dist/types/rsc/shell-capture.d.ts +14 -1
  62. package/dist/types/rsc/shell-serve.d.ts +2 -2
  63. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  64. package/dist/types/rsc/types.d.ts +8 -0
  65. package/dist/types/segment-boundary-content.d.ts +17 -0
  66. package/dist/types/segment-loader-promise.d.ts +2 -6
  67. package/dist/types/server/build-version-table.d.ts +9 -0
  68. package/dist/types/server/context.d.ts +12 -4
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +63 -2
  71. package/dist/types/suspense-audit-react.d.ts +26 -0
  72. package/dist/types/suspense-audit.d.ts +157 -0
  73. package/dist/types/testing/action-form.d.ts +34 -0
  74. package/dist/types/testing/flight.entry.d.ts +2 -0
  75. package/dist/types/testing/index.d.ts +4 -0
  76. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  77. package/dist/types/testing/serve-shell-request.d.ts +34 -4
  78. package/dist/types/testing/shell-status.d.ts +14 -6
  79. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  80. package/dist/types/thenable-status.d.ts +6 -0
  81. package/dist/types/types/handler-context.d.ts +7 -0
  82. package/dist/types/types/route-entry.d.ts +5 -0
  83. package/dist/types/urls/path-helper.d.ts +18 -0
  84. package/dist/types/vite/discovery/state.d.ts +11 -0
  85. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  86. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  87. package/dist/vite/index.js +180 -497
  88. package/package.json +13 -3
  89. package/skills/cache-guide/SKILL.md +18 -17
  90. package/skills/caching/SKILL.md +40 -4
  91. package/skills/catalog.json +1 -1
  92. package/skills/cloudflare/SKILL.md +131 -0
  93. package/skills/comparison/references/framework-comparison.md +1 -1
  94. package/skills/deployment-caching/SKILL.md +33 -12
  95. package/skills/document-cache/SKILL.md +16 -14
  96. package/skills/hooks/SKILL.md +22 -0
  97. package/skills/hooks/handle-and-actions.md +3 -4
  98. package/skills/hooks/navigation.md +10 -0
  99. package/skills/hooks/state.md +5 -4
  100. package/skills/host-router/SKILL.md +18 -0
  101. package/skills/loader/SKILL.md +6 -4
  102. package/skills/middleware/SKILL.md +1 -1
  103. package/skills/migrate-nextjs/SKILL.md +82 -9
  104. package/skills/ppr/SKILL.md +31 -8
  105. package/skills/prerender/SKILL.md +645 -8
  106. package/skills/rango/SKILL.md +1 -0
  107. package/skills/response-routes/SKILL.md +5 -5
  108. package/skills/route/SKILL.md +29 -0
  109. package/skills/testing/SKILL.md +2 -1
  110. package/skills/testing/cache-prerender.md +18 -17
  111. package/skills/testing/serve-shell-request.md +104 -8
  112. package/skills/use-cache/SKILL.md +9 -5
  113. package/skills/vercel/SKILL.md +2 -0
  114. package/skills/view-transitions/SKILL.md +9 -4
  115. package/src/browser/event-controller.ts +69 -25
  116. package/src/browser/link-interceptor.ts +2 -2
  117. package/src/browser/navigation-bridge.ts +75 -35
  118. package/src/browser/navigation-store.ts +26 -0
  119. package/src/browser/navigation-transaction.ts +9 -2
  120. package/src/browser/network-error-handler.ts +2 -0
  121. package/src/browser/partial-update.ts +48 -12
  122. package/src/browser/prefetch/default-strategy.ts +22 -42
  123. package/src/browser/react/Link.tsx +37 -42
  124. package/src/browser/react/NavigationProvider.tsx +63 -3
  125. package/src/browser/react/context.ts +17 -0
  126. package/src/browser/react/location-state.ts +5 -19
  127. package/src/browser/react/use-action.ts +4 -1
  128. package/src/browser/react/use-handle.ts +4 -20
  129. package/src/browser/react/use-link-status.ts +4 -3
  130. package/src/browser/react/use-navigation.ts +4 -1
  131. package/src/browser/react/use-params.ts +4 -1
  132. package/src/browser/react/use-pathname.ts +4 -1
  133. package/src/browser/react/use-search-params.ts +5 -3
  134. package/src/browser/react/use-segments.ts +36 -54
  135. package/src/browser/rsc-router.tsx +79 -39
  136. package/src/browser/segment-reconciler.ts +3 -3
  137. package/src/browser/server-action-bridge.ts +26 -6
  138. package/src/browser/types.ts +11 -0
  139. package/src/build/generate-manifest.ts +17 -0
  140. package/src/build/merge-full-manifests.ts +5 -0
  141. package/src/build/route-trie.ts +1 -0
  142. package/src/cache/background-task.ts +61 -0
  143. package/src/cache/cache-key-utils.ts +91 -12
  144. package/src/cache/cache-policy.ts +11 -2
  145. package/src/cache/cache-runtime.ts +23 -23
  146. package/src/cache/cache-scope.ts +38 -36
  147. package/src/cache/cf/cf-cache-store.ts +16 -11
  148. package/src/cache/document-cache.ts +35 -14
  149. package/src/cache/handle-snapshot.ts +50 -0
  150. package/src/cache/index.ts +1 -0
  151. package/src/cache/invalidation-order.ts +53 -0
  152. package/src/cache/memory-segment-store.ts +52 -1
  153. package/src/cache/shell-snapshot.ts +17 -0
  154. package/src/cache/store-scope.ts +36 -0
  155. package/src/cache/tag-invalidation.ts +7 -5
  156. package/src/cache/types.ts +69 -14
  157. package/src/cache/vercel/vercel-cache-store.ts +20 -6
  158. package/src/client.tsx +40 -3
  159. package/src/errors.ts +13 -6
  160. package/src/handles/deferred-resolution.ts +7 -2
  161. package/src/internal-suspense-audit.ts +12 -0
  162. package/src/loader-store.ts +4 -9
  163. package/src/outlet-provider.tsx +25 -1
  164. package/src/prerender/cloudflare.ts +130 -0
  165. package/src/prerender/create-prerender-trigger.ts +780 -0
  166. package/src/prerender/index.ts +39 -0
  167. package/src/prerender/memory-prerender-store.ts +115 -0
  168. package/src/prerender/on-demand.ts +339 -0
  169. package/src/prerender/producer-guard.ts +66 -0
  170. package/src/prerender/shell-manifest-key.ts +12 -17
  171. package/src/prerender/warm-request.ts +154 -0
  172. package/src/prerender/warm.ts +193 -0
  173. package/src/prerender/writable-store.ts +278 -0
  174. package/src/prerender.ts +29 -0
  175. package/src/reverse.ts +1 -1
  176. package/src/route-content-wrapper.tsx +107 -13
  177. package/src/router/error-handling.ts +12 -12
  178. package/src/router/find-match.ts +1 -0
  179. package/src/router/handler-context.ts +2 -0
  180. package/src/router/intercept-resolution.ts +15 -8
  181. package/src/router/loader-resolution.ts +2 -10
  182. package/src/router/match-middleware/background-revalidation.ts +2 -0
  183. package/src/router/match-middleware/cache-lookup.ts +230 -38
  184. package/src/router/pattern-matching.ts +14 -0
  185. package/src/router/prefetch-default.ts +4 -4
  186. package/src/router/prerender-match.ts +460 -352
  187. package/src/router/route-trie-builder.ts +9 -0
  188. package/src/router/router-context.ts +1 -0
  189. package/src/router/router-interfaces.ts +33 -0
  190. package/src/router/router-options.ts +30 -0
  191. package/src/router/segment-resolution/fresh.ts +20 -19
  192. package/src/router/segment-resolution/loader-cache.ts +15 -17
  193. package/src/router/segment-resolution/revalidation.ts +34 -24
  194. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  195. package/src/router/trie-matching.ts +3 -0
  196. package/src/router.ts +107 -1
  197. package/src/rsc/handler.ts +51 -4
  198. package/src/rsc/progressive-enhancement.ts +5 -1
  199. package/src/rsc/response-cache-serve.ts +10 -4
  200. package/src/rsc/rsc-rendering.ts +53 -11
  201. package/src/rsc/shell-build-manifest.ts +10 -8
  202. package/src/rsc/shell-capture-constants.ts +39 -7
  203. package/src/rsc/shell-capture.ts +108 -19
  204. package/src/rsc/shell-serve.ts +12 -6
  205. package/src/rsc/ssr-module-loader.ts +26 -0
  206. package/src/rsc/types.ts +8 -0
  207. package/src/segment-boundary-content.ts +25 -0
  208. package/src/segment-loader-promise.ts +28 -38
  209. package/src/segment-system.tsx +117 -57
  210. package/src/server/build-version-table.ts +15 -0
  211. package/src/server/context.ts +40 -17
  212. package/src/server/cookie-store.ts +5 -0
  213. package/src/server/handle-store.ts +13 -0
  214. package/src/server/loader-registry.ts +6 -1
  215. package/src/server/request-context.ts +87 -1
  216. package/src/ssr/ssr-root.tsx +20 -13
  217. package/src/suspense-audit-react.tsx +98 -0
  218. package/src/suspense-audit.ts +1030 -0
  219. package/src/testing/action-form.ts +66 -0
  220. package/src/testing/flight-runtime.d.ts +27 -0
  221. package/src/testing/flight.entry.ts +3 -0
  222. package/src/testing/index.ts +19 -0
  223. package/src/testing/internal/server-modules.ts +32 -0
  224. package/src/testing/render-route.tsx +25 -12
  225. package/src/testing/serve-shell-request.ts +190 -34
  226. package/src/testing/shell-status.ts +17 -6
  227. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  228. package/src/thenable-status.ts +10 -0
  229. package/src/types/handler-context.ts +7 -0
  230. package/src/types/route-entry.ts +6 -0
  231. package/src/urls/path-helper.ts +69 -9
  232. package/src/use-loader.tsx +57 -30
  233. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  234. package/src/vite/discovery/discover-routers.ts +15 -0
  235. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  236. package/src/vite/discovery/state.ts +17 -1
  237. package/src/vite/inject-client-debug.ts +42 -20
  238. package/src/vite/plugins/virtual-entries.ts +8 -1
  239. package/src/vite/router-discovery.ts +14 -0
  240. package/src/vite/utils/bundle-analysis.ts +39 -7
  241. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  242. package/dist/types/segment-content-promise.d.ts +0 -13
  243. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  244. package/src/segment-content-promise.ts +0 -67
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Public on-demand prerender surface (`@rangojs/router/prerender`).
3
+ *
4
+ * Store contracts, config/result/target types, and the in-memory store (usable
5
+ * as a durable overlay in single-process node/Vercel functions, and as a test
6
+ * fake). The Cloudflare KV adapter lives in `@rangojs/router/prerender/cloudflare`.
7
+ */
8
+
9
+ export { PrerenderError } from "./create-prerender-trigger.js";
10
+
11
+ export {
12
+ createMemoryPrerenderStore,
13
+ type MemoryPrerenderStore,
14
+ type MemoryPrerenderStoreOptions,
15
+ } from "./memory-prerender-store.js";
16
+
17
+ export {
18
+ serializePrerenderKey,
19
+ type PrerenderKey,
20
+ type PrerenderStoredEntry,
21
+ type WritablePrerenderStore,
22
+ } from "./writable-store.js";
23
+
24
+ export type {
25
+ OnDemandOption,
26
+ OnDemandRouteConfig,
27
+ PrerenderConfig,
28
+ PrerenderFn,
29
+ PrerenderResult,
30
+ PrerenderWarmCaches,
31
+ PrerenderManyOptions,
32
+ PrerenderRunner,
33
+ PrerenderRunOptions,
34
+ PrerenderRuntime,
35
+ PrerenderTarget,
36
+ PrerenderTargetObject,
37
+ } from "./on-demand.js";
38
+
39
+ export type { PrerenderEntry } from "./store.js";
@@ -0,0 +1,115 @@
1
+ /**
2
+ * In-memory writable prerender store.
3
+ *
4
+ * Backs the zero-config dev overlay and the `@rangojs/router/testing` fake.
5
+ * A plain Map — no per-isolate null memoization (a later set() must be visible
6
+ * after an earlier miss), no React/RSC deps.
7
+ */
8
+
9
+ import {
10
+ lowerStoredEntryStaleAt,
11
+ serializePrerenderKey,
12
+ type PrerenderKey,
13
+ type PrerenderStoredEntry,
14
+ type WritablePrerenderStore,
15
+ } from "./writable-store.js";
16
+
17
+ export interface MemoryPrerenderStore extends WritablePrerenderStore {
18
+ delete(key: PrerenderKey): Promise<void>;
19
+ markStale(routerId: string, tags: string[]): Promise<void>;
20
+ /** Synchronous read by structured key (tests). */
21
+ peek(key: PrerenderKey): PrerenderStoredEntry | null;
22
+ /** All stored [serializedKey, entry] pairs (tests). */
23
+ entries(): [string, PrerenderStoredEntry][];
24
+ /** Drop every entry. */
25
+ clear(): void;
26
+ readonly size: number;
27
+ }
28
+
29
+ export interface MemoryPrerenderStoreOptions {
30
+ /** Injectable clock for `markStale` in deterministic tests. Defaults to Date.now. */
31
+ now?: () => number;
32
+ }
33
+
34
+ /**
35
+ * Create an in-memory {@link WritablePrerenderStore} with test-inspection
36
+ * helpers. Invalidation is mark-stale (sets `staleAt`), never delete — matching
37
+ * the durable v1 contract so the entry keeps serving until refreshed.
38
+ */
39
+ export function createMemoryPrerenderStore(
40
+ options: MemoryPrerenderStoreOptions = {},
41
+ ): MemoryPrerenderStore {
42
+ const now = options.now ?? (() => Date.now());
43
+ // The owning router per entry: the serialized key cannot be split back.
44
+ const map = new Map<
45
+ string,
46
+ { routerId: string; stored: PrerenderStoredEntry }
47
+ >();
48
+
49
+ // routerId -> tag -> when markStale() last named it. A held entry is marked
50
+ // in place; this covers a write that was rendered before the mark and set
51
+ // after it.
52
+ const marks = new Map<string, Map<string, number>>();
53
+
54
+ return {
55
+ async get(key: PrerenderKey): Promise<PrerenderStoredEntry | null> {
56
+ return map.get(serializePrerenderKey(key))?.stored ?? null;
57
+ },
58
+
59
+ async set(key: PrerenderKey, stored: PrerenderStoredEntry): Promise<void> {
60
+ // Strictly after storedAt, unlike the KV read (cloudflare.ts, `>=`): in
61
+ // one process a mark and a render start can share a millisecond with the
62
+ // mark first, which `>=` would read as invalidating that render. KV's
63
+ // marker write is I/O, so a same-millisecond marker there is ambiguous.
64
+ // Marks, never refuses: a stale entry keeps serving.
65
+ const routerMarks = marks.get(key.routerId);
66
+ if (routerMarks) {
67
+ for (const tag of stored.meta.tags) {
68
+ const at = routerMarks.get(tag);
69
+ if (at != null && at > stored.meta.storedAt) {
70
+ lowerStoredEntryStaleAt(stored, at);
71
+ }
72
+ }
73
+ }
74
+ map.set(serializePrerenderKey(key), { routerId: key.routerId, stored });
75
+ },
76
+
77
+ async delete(key: PrerenderKey): Promise<void> {
78
+ map.delete(serializePrerenderKey(key));
79
+ },
80
+
81
+ async markStale(routerId: string, tags: string[]): Promise<void> {
82
+ if (tags.length === 0) return;
83
+ const tagSet = new Set(tags);
84
+ const at = now();
85
+ let routerMarks = marks.get(routerId);
86
+ if (!routerMarks) marks.set(routerId, (routerMarks = new Map()));
87
+ for (const tag of tags) routerMarks.set(tag, at);
88
+ for (const entry of map.values()) {
89
+ if (
90
+ entry.routerId === routerId &&
91
+ entry.stored.meta.tags.some((t) => tagSet.has(t))
92
+ ) {
93
+ lowerStoredEntryStaleAt(entry.stored, at);
94
+ }
95
+ }
96
+ },
97
+
98
+ peek(key: PrerenderKey): PrerenderStoredEntry | null {
99
+ return map.get(serializePrerenderKey(key))?.stored ?? null;
100
+ },
101
+
102
+ entries(): [string, PrerenderStoredEntry][] {
103
+ return [...map].map(([k, v]) => [k, v.stored]);
104
+ },
105
+
106
+ clear(): void {
107
+ map.clear();
108
+ marks.clear();
109
+ },
110
+
111
+ get size(): number {
112
+ return map.size;
113
+ },
114
+ };
115
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Public type surface for on-demand (ISR-style) prerender: the router
3
+ * `prerender` config option, the `router.prerender({ env, ctx })` runner, its typed
4
+ * target, and the inspectable result union.
5
+ *
6
+ * Types only — safe to import from router options, the public interfaces, the
7
+ * testing barrel, and platform adapters without pulling RSC deps.
8
+ */
9
+
10
+ import type { ExecutionContext } from "../types/request-scope.js";
11
+ import type { IsEmptyObject, ParamsFor } from "../reverse.js";
12
+ import type { WritablePrerenderStore } from "./writable-store.js";
13
+ import type { ShellCaptureRefusal } from "../rsc/shell-capture-constants.js";
14
+
15
+ declare const prerenderTargetBrand: unique symbol;
16
+
17
+ /**
18
+ * The target `onRevalidate` receives: a plain `{ route, params }` that goes
19
+ * straight into a queue message. The type-only brand (never present at
20
+ * runtime) lets the runner (`router.prerender({ env })(target)` / `.many()`) accepts it with no cast on a
21
+ * router typed with named routes, while a hand-written `{ route: "typo" }`
22
+ * stays a type error there. After a JSON round trip, assert it back:
23
+ * `JSON.parse(raw) as PrerenderTargetObject`.
24
+ */
25
+ export interface PrerenderTargetObject {
26
+ route: string;
27
+ params: Record<string, string>;
28
+ readonly [prerenderTargetBrand]: true;
29
+ }
30
+
31
+ /**
32
+ * Route map used to type the object target. When the fluent `TRoutes` phantom is
33
+ * empty (the common `createRouter().routes(...)` case leaves it `{}`), fall back
34
+ * to the generated global route map — the same fallback `Prerender<"name">` and
35
+ * `reverse` use — so `router.prerender({ route, params })` stays typed without an
36
+ * explicit route-type parameter.
37
+ */
38
+ type PrerenderRouteMap<TRoutes> = keyof TRoutes extends never
39
+ ? keyof Rango.GeneratedRouteMap extends never
40
+ ? {}
41
+ : Rango.GeneratedRouteMap
42
+ : TRoutes;
43
+
44
+ /**
45
+ * Typed object target, derived from the router's route map the same way
46
+ * `reverse` is. No-param routes may omit `params`.
47
+ */
48
+ type PrerenderRouteTarget<TRoutes> = {
49
+ [TName in keyof PrerenderRouteMap<TRoutes> & string]: IsEmptyObject<
50
+ ParamsFor<PrerenderRouteMap<TRoutes>, TName>
51
+ > extends true
52
+ ? { route: TName; params?: Record<string, never> }
53
+ : { route: TName; params: ParamsFor<PrerenderRouteMap<TRoutes>, TName> };
54
+ }[keyof PrerenderRouteMap<TRoutes> & string];
55
+
56
+ /**
57
+ * A prerender target: a typed named-route object, a path-like string/URL
58
+ * (`router.reverse()` output composes for free), or the `onRevalidate` target.
59
+ */
60
+ export type PrerenderTarget<TRoutes = {}> =
61
+ | string
62
+ | URL
63
+ | PrerenderRouteTarget<TRoutes>
64
+ | PrerenderTargetObject;
65
+
66
+ /** Env + platform capabilities bound once by `router.prerender({ env, ctx })`. */
67
+ export interface PrerenderRuntime<TEnv = any> {
68
+ /** The live env: the producer's `ctx.env` (not the build's `buildEnv`). */
69
+ env: TEnv;
70
+ /** Cloudflare `ExecutionContext`; absent on node/Vercel. */
71
+ ctx?: ExecutionContext;
72
+ /**
73
+ * The origin a warm requests a path or `{ route, params }` target on, for
74
+ * example `"https://shop.example"`. Cache keys carry the host, so a warm
75
+ * must request the host visitors use. A full-URL target uses its own
76
+ * origin; without this option the runner uses the origin of the request it
77
+ * is called from (an action, a route handler, `onRevalidate`), and with
78
+ * neither a warm returns `skipped-no-origin`. An on-demand route's
79
+ * requestless render never needs it.
80
+ */
81
+ origin?: string;
82
+ }
83
+
84
+ /** Per-call options of the bound runner. */
85
+ export interface PrerenderRunOptions {
86
+ /** Throw on failure instead of returning an `{ ok: false }` result. */
87
+ throwOnError?: boolean;
88
+ /**
89
+ * Only refresh what is cold or stale (cron-sweep opt-in), and the only
90
+ * path that can return `already-fresh`. On an on-demand route the stored
91
+ * entry is rendered only when stale. On any other route the warm reads its
92
+ * caches normally instead of forcing misses, so a fresh entry is left
93
+ * alone, a stale one refreshes and a missing one is written.
94
+ */
95
+ onlyIfStale?: boolean;
96
+ }
97
+
98
+ /** Per-call options of the bound runner's `.many()`. */
99
+ export interface PrerenderManyOptions extends PrerenderRunOptions {
100
+ /** Max concurrent renders in a batch (default 1; any invalid value is 1). */
101
+ concurrency?: number;
102
+ }
103
+
104
+ /**
105
+ * What a warm request wrote, by cache. On a result of the warm path, and on
106
+ * an on-demand result whose route was also warmed.
107
+ */
108
+ export interface PrerenderWarmCaches {
109
+ /**
110
+ * Store writes that landed, by store family: `record` (route `cache()`),
111
+ * `item` (`"use cache"` and loader `cache()`), `response` (the document
112
+ * cache and response routes), `shell` (the `ppr` shell).
113
+ */
114
+ writes: { record: number; item: number; response: number; shell: number };
115
+ /**
116
+ * The `ppr` shell; absent on a route without `ppr`. `fresh` is
117
+ * `onlyIfStale` finding a servable shell; `not-eligible` is a request the
118
+ * shell path passed on (an active nonce, a store without shells, the
119
+ * route's own `cache()` opt-out, a buffered response).
120
+ */
121
+ shell?:
122
+ | "stored"
123
+ | "fresh"
124
+ | "refused"
125
+ | "no-shell"
126
+ | "not-eligible"
127
+ | "skipped-capacity"
128
+ | "skipped-queue-timeout"
129
+ | "error";
130
+ /** Why the capture refused to store the shell. */
131
+ refusal?: ShellCaptureRefusal;
132
+ /** The document cache, when `createDocumentCacheMiddleware` ran. */
133
+ document?: "stored" | "not-cacheable";
134
+ }
135
+
136
+ /**
137
+ * Inspectable result, one per target. The trigger does not force callers
138
+ * into try/catch; `throwOnError: true` opts into throwing for admin/CI
139
+ * endpoints. `path` says what ran: `on-demand` is the requestless render of a
140
+ * `Prerender(..., { onDemand })` route into the prerender store, `warm` is a
141
+ * request through the router's handler that rewrites the route's runtime
142
+ * caches.
143
+ */
144
+ export type PrerenderResult =
145
+ | {
146
+ ok: true;
147
+ path: "on-demand";
148
+ /**
149
+ * `already-fresh` only occurs with `onlyIfStale: true`.
150
+ *
151
+ * `removed`: a "removed" marker is stored in place of the page, and
152
+ * requests no longer get the page or its build-time entry. It is
153
+ * `ok: true`, so count it in sweep results. Written by
154
+ * `prerender.remove()` (permanent until a refresh without
155
+ * `onlyIfStale`; `tags` empty, no `ttl`), or by a refresh in which a
156
+ * `notFound()` was the only thing that went wrong, anywhere in the
157
+ * render: the route handler, a layout or parallel slot handler, any
158
+ * server component. That marker has the route's `tags` and `ttl` and is
159
+ * rechecked like a page. `onlyIfStale` finding a fresh marker reports
160
+ * `removed` too. When the data source may be failing, throw anything
161
+ * else: that is `render-failed` and keeps the page.
162
+ */
163
+ status: "rendered" | "already-fresh" | "removed";
164
+ target: string;
165
+ routeName: string;
166
+ /** Opaque, for debugging/logs only — not a stable format. */
167
+ key: string;
168
+ tags: string[];
169
+ /** Absent = never stale. */
170
+ ttl?: number;
171
+ /**
172
+ * The warm request that followed the store write. Absent when the app
173
+ * store is not shared, no origin resolved, or nothing was rendered.
174
+ */
175
+ caches?: PrerenderWarmCaches;
176
+ }
177
+ | {
178
+ ok: true;
179
+ path: "warm";
180
+ /** `already-fresh` only occurs with `onlyIfStale: true`. */
181
+ status: "warmed" | "already-fresh";
182
+ /** The absolute URL that was requested. */
183
+ target: string;
184
+ routeName: string;
185
+ /** The warm response's HTTP status. */
186
+ responseStatus: number;
187
+ caches: PrerenderWarmCaches;
188
+ }
189
+ | {
190
+ ok: false;
191
+ /** Absent when no route matched: every other result is past the match. */
192
+ path?: "on-demand" | "warm";
193
+ status:
194
+ | "no-match"
195
+ // No prerender store (on-demand), or no `createRouter({ cache })` (warm).
196
+ | "no-store"
197
+ | "skipped-personalized"
198
+ | "skipped-unsupported-target"
199
+ // The route (a Passthrough + onDemand route) returned ctx.passthrough()
200
+ // for this param set: no shared payload to persist. The "removed"
201
+ // marker is stored instead, so the live handler serves it and not a
202
+ // page stored or baked earlier.
203
+ | "skipped-passthrough"
204
+ // prerender.remove() on a route that is not on-demand: it has no page
205
+ // in the prerender store. (A refresh of such a route is a warm.)
206
+ | "skipped-not-on-demand"
207
+ // The app store's entries are not readable beyond the place the call
208
+ // runs (SegmentCacheStore.scope), so a warm would serve no one else.
209
+ | "skipped-store-not-shared"
210
+ // A path or { route, params } target with no origin to request it on.
211
+ | "skipped-no-origin"
212
+ // A ppr route whose shell was not stored (caches.shell says why).
213
+ | "shell-not-stored"
214
+ // The request rendered and no cache wrote anything.
215
+ | "skipped-uncached"
216
+ | "render-failed"
217
+ | "store-failed";
218
+ target: string;
219
+ routeName?: string;
220
+ /** The warm response's HTTP status, when a warm request ran. */
221
+ responseStatus?: number;
222
+ caches?: PrerenderWarmCaches;
223
+ error?: unknown;
224
+ };
225
+
226
+ /**
227
+ * Per-route on-demand opt-in, carried inside `PrerenderOptions.onDemand`. Any
228
+ * truthy spelling works (literal, spread, imported const): producer retention
229
+ * is driven by the evaluated route manifest, not a textual scan of the call.
230
+ */
231
+ export interface OnDemandRouteConfig {
232
+ /** Soft TTL (seconds) for entries refreshed for this route. Overrides the router `ttl`. */
233
+ ttl?: number;
234
+ /**
235
+ * Tags stamped on the stored entry, addressable via
236
+ * the runner's `markStale()`. A separate namespace from `cacheTag()` /
237
+ * `updateTag()` / `revalidateTag()`, which never reach the prerender store.
238
+ */
239
+ tags?: string[] | ((target: { params: Record<string, string> }) => string[]);
240
+ }
241
+
242
+ export type OnDemandOption = boolean | OnDemandRouteConfig;
243
+
244
+ /**
245
+ * Env-resolved prerender store configuration. Same union shape as the `cache`
246
+ * option: a plain object or a factory resolved per request/per call, never at
247
+ * `createRouter()` time.
248
+ */
249
+ export interface PrerenderConfig<TEnv = any> {
250
+ store: WritablePrerenderStore;
251
+ /**
252
+ * Default soft TTL (seconds) for a route whose `onDemand` sets none. Absent
253
+ * = never stale. Soft: a stale entry keeps serving; staleness only decides
254
+ * whether `onRevalidate` is scheduled. Entries never expire.
255
+ */
256
+ ttl?: number;
257
+ /**
258
+ * Stale-while-revalidate: its presence is the opt-in. Scheduled through
259
+ * `waitUntil` on a stale overlay hit (the stale entry still serves) with
260
+ * the JSON-serializable target and the live env. Runs at most once per
261
+ * stale key per isolate while one is in flight (one running longer than
262
+ * 15 s counts as finished, so a hung call cannot block the key), so
263
+ * `(target, env, ctx) => router.prerender({ env, ctx })(target, { onlyIfStale: true })`
264
+ * is safe in a single process; across isolates, point it at a queue, which
265
+ * owns dedup. Pass `onlyIfStale` in the call that runs later: a job queued
266
+ * for a page that `prerender.remove()` then removed finds the marker and
267
+ * renders nothing, where a plain refresh would bring the page back.
268
+ */
269
+ onRevalidate?: (
270
+ target: PrerenderTargetObject,
271
+ env: TEnv,
272
+ /** The stale request's execution context; absent where none exists. */
273
+ ctx?: ExecutionContext,
274
+ ) => void | Promise<void>;
275
+ }
276
+
277
+ /**
278
+ * @internal Per-request resolution of the prerender config plus the key
279
+ * version. Threaded onto the request context by the RSC handler and read by
280
+ * the serve-path overlay lookup.
281
+ */
282
+ export interface ResolvedPrerender<TEnv = any> {
283
+ /** The resolved config; the store is `config.store`. */
284
+ config: PrerenderConfig<TEnv>;
285
+ routerId: string;
286
+ /** The router's cache version for overlay keys (resolvePrerenderVersion). */
287
+ version: string;
288
+ }
289
+
290
+ /**
291
+ * The runner `router.prerender({ env, ctx })` returns: callable per target,
292
+ * plus `.many` / `.remove` / `.markStale`. Typing rides the phantom `TRoutes`
293
+ * accumulator, like `reverse`.
294
+ */
295
+ export interface PrerenderRunner<TRoutes = {}> {
296
+ (
297
+ target: PrerenderTarget<TRoutes>,
298
+ options?: PrerenderRunOptions,
299
+ ): Promise<PrerenderResult>;
300
+ many(
301
+ targets: ReadonlyArray<PrerenderTarget<TRoutes>>,
302
+ options?: PrerenderManyOptions,
303
+ ): Promise<PrerenderResult[]>;
304
+ /**
305
+ * Remove an on-demand route's page without rendering: a "removed" marker is
306
+ * stored in place of it. Requests then get a 404 (a `Passthrough` route: its
307
+ * live handler), never the build-time entry. Permanent: no `ttl`, no
308
+ * `onRevalidate`, left by `onlyIfStale`, until a `prerender(target)`
309
+ * without `onlyIfStale` renders the page again. It is a write like a
310
+ * refresh's, and the last write wins. `{ ok: true, status: "removed" }`; a
311
+ * route that is not on-demand is `skipped-not-on-demand`. `.many` is the
312
+ * batch form.
313
+ */
314
+ remove: {
315
+ (
316
+ target: PrerenderTarget<TRoutes>,
317
+ options?: Pick<PrerenderRunOptions, "throwOnError">,
318
+ ): Promise<PrerenderResult>;
319
+ many(
320
+ targets: ReadonlyArray<PrerenderTarget<TRoutes>>,
321
+ options?: Omit<PrerenderManyOptions, "onlyIfStale">,
322
+ ): Promise<PrerenderResult[]>;
323
+ };
324
+ /**
325
+ * Mark this router's stored entries carrying one of `tags` stale. Marking only: the
326
+ * entries keep serving, and a stale hit schedules `onRevalidate` when one is
327
+ * configured. Nothing is deleted or re-rendered here.
328
+ */
329
+ markStale(tags: string[]): Promise<void>;
330
+ }
331
+
332
+ /**
333
+ * The `router.prerender` function: binds the runtime (`env`, `ctx`) and returns
334
+ * the {@link PrerenderRunner}. Binding does no work; the config factory is
335
+ * resolved per call.
336
+ */
337
+ export type PrerenderFn<TEnv = any, TRoutes = {}> = (
338
+ runtime: PrerenderRuntime<TEnv>,
339
+ ) => PrerenderRunner<TRoutes>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Requestless-prerender guard.
3
+ *
4
+ * The on-demand producer renders a route with only route params + env — never
5
+ * the triggering request's cookies, headers, geo, or session (see the design's
6
+ * Producer Semantics). Personalized reads during a producer render would bake
7
+ * one visitor's data into a payload shared with everyone for that route.
8
+ *
9
+ * The producer marks its own request context with `_onDemandProducer: true`. The
10
+ * standalone request-scoped helpers a handler reaches for — `cookies()`,
11
+ * `headers()`, `invalidateClientCache()`, `keepClientCache()` — call
12
+ * {@link assertNotInsidePrerenderProducer}, which throws
13
+ * {@link PrerenderPersonalizationError}; the producer catches it and maps to a
14
+ * `skipped-personalized` result, leaving any existing entry untouched. (The
15
+ * producer context's own `ctx.cookie`/`header`/etc. remain the pre-existing
16
+ * build-time no-op stubs over empty synthetic state — the guard covers the
17
+ * standalone helpers, which are the reads that would otherwise see real request
18
+ * data.)
19
+ */
20
+
21
+ /** Thrown when a producer render touches request-specific state. */
22
+ export class PrerenderPersonalizationError extends Error {
23
+ /** Cross-realm-safe brand (instanceof can fail across bundle/realm boundaries). */
24
+ readonly isPrerenderPersonalizationError = true as const;
25
+
26
+ constructor(api: string) {
27
+ super(
28
+ `${api} cannot be used during on-demand prerender. The prerender producer ` +
29
+ `is requestless: it renders with route params and env only, and its output ` +
30
+ `is a shared payload keyed by route + params. Reading request-scoped state ` +
31
+ `(cookies, headers) or mutating the response here would leak one visitor's ` +
32
+ `data to every later visitor. This route was skipped as personalized.`,
33
+ );
34
+ this.name = "PrerenderPersonalizationError";
35
+ }
36
+ }
37
+
38
+ export function isPrerenderPersonalizationError(
39
+ value: unknown,
40
+ ): value is PrerenderPersonalizationError {
41
+ return (
42
+ value != null &&
43
+ typeof value === "object" &&
44
+ (value as PrerenderPersonalizationError).isPrerenderPersonalizationError ===
45
+ true
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Throw {@link PrerenderPersonalizationError} when `ctx` is a requestless
51
+ * on-demand producer context. A no-op for the live request path and the
52
+ * build/dev prerender paths (which do not set `_onDemandProducer`), so existing
53
+ * callers are unaffected.
54
+ */
55
+ export function assertNotInsidePrerenderProducer(
56
+ ctx: unknown,
57
+ api: string,
58
+ ): void {
59
+ if (
60
+ ctx != null &&
61
+ typeof ctx === "object" &&
62
+ (ctx as { _onDemandProducer?: boolean })._onDemandProducer === true
63
+ ) {
64
+ throw new PrerenderPersonalizationError(api);
65
+ }
66
+ }
@@ -1,20 +1,15 @@
1
1
  /**
2
- * Build shell manifest key, shared by the producer (the build's shell
3
- * prerender phase, vite/discovery/shell-prerender-phase.ts) and the consumer
4
- * (the runtime read-through, rsc/shell-build-manifest.ts) so the format
5
- * cannot drift. Dependency-free: the producer runs node-side in the plugin,
6
- * the consumer in the RSC runtime.
7
- *
8
- * PATHNAME-ONLY by design. Host-free because the build knows no request
9
- * host. Router-free because the router id ($$id) is a hash of
10
- * filePath:lineNumber of the TRANSFORMED source, which differs between the
11
- * discovery temp server's dev-style transform chain and the main build's —
12
- * a temp-realm router id can never be looked up by the shipped worker. The
13
- * producer instead detects pathname collisions across routers at build time
14
- * and declines both entries (loudly), keeping the key unambiguous. This is
15
- * a manifest namespace, never a store keyspace: the runtime shell key
16
- * (host + pathname + search + ":shell") stays untouched.
2
+ * Build-time shell manifest key, `{routerId}@{pathname}`: the ONE derivation
3
+ * shared by the producer (vite/discovery/shell-prerender-phase.ts) and the
4
+ * consumer (rsc/shell-build-manifest.ts) so the two sides cannot drift. The
5
+ * router part follows the router rule (cache-key-utils.ts). Host-free (the
6
+ * build knows no request host); a manifest namespace, never a store keyspace.
17
7
  */
18
- export function buildShellManifestKey(pathname: string): string {
19
- return pathname;
8
+ import { routerKeyPrefix } from "../cache/cache-key-utils.js";
9
+
10
+ export function buildShellManifestKey(
11
+ routerId: string,
12
+ pathname: string,
13
+ ): string {
14
+ return `${routerKeyPrefix(routerId)}${pathname}`;
20
15
  }