@rangojs/router 0.21.0 → 0.22.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 (243) 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 +179 -496
  88. package/package.json +12 -2
  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/request-context.ts +87 -1
  215. package/src/ssr/ssr-root.tsx +20 -13
  216. package/src/suspense-audit-react.tsx +98 -0
  217. package/src/suspense-audit.ts +1030 -0
  218. package/src/testing/action-form.ts +66 -0
  219. package/src/testing/flight-runtime.d.ts +27 -0
  220. package/src/testing/flight.entry.ts +3 -0
  221. package/src/testing/index.ts +19 -0
  222. package/src/testing/internal/server-modules.ts +32 -0
  223. package/src/testing/render-route.tsx +25 -12
  224. package/src/testing/serve-shell-request.ts +190 -34
  225. package/src/testing/shell-status.ts +17 -6
  226. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  227. package/src/thenable-status.ts +10 -0
  228. package/src/types/handler-context.ts +7 -0
  229. package/src/types/route-entry.ts +6 -0
  230. package/src/urls/path-helper.ts +69 -9
  231. package/src/use-loader.tsx +57 -30
  232. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  233. package/src/vite/discovery/discover-routers.ts +15 -0
  234. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  235. package/src/vite/discovery/state.ts +17 -1
  236. package/src/vite/inject-client-debug.ts +42 -20
  237. package/src/vite/plugins/virtual-entries.ts +8 -1
  238. package/src/vite/router-discovery.ts +14 -0
  239. package/src/vite/utils/bundle-analysis.ts +39 -7
  240. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  241. package/dist/types/segment-content-promise.d.ts +0 -13
  242. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  243. package/src/segment-content-promise.ts +0 -67
@@ -5,7 +5,7 @@ import {
5
5
  type ComponentType,
6
6
  isValidElement,
7
7
  } from "react";
8
- import { OutletProvider } from "./outlet-provider.js";
8
+ import { AuditedOutletProvider, OutletProvider } from "./outlet-provider.js";
9
9
  import { withOptimisticCommitNone } from "./browser/optimistic-commit.js";
10
10
  import { MountContextProvider } from "./browser/react/mount-context.js";
11
11
  import type { ResolvedSegment, RootLayoutProps } from "./types.js";
@@ -13,17 +13,25 @@ import { decodeLoaderResults } from "./decode-loader-results.js";
13
13
  import { LoaderRedirect } from "./loader-redirect.js";
14
14
  import { invariant } from "./errors.js";
15
15
  import {
16
+ AuditedLoaderBoundary,
17
+ AuditedRouteContent,
16
18
  RouteContentWrapper,
17
19
  LoaderBoundary,
18
20
  StreamedLoaderErrorBoundary,
19
21
  } from "./route-content-wrapper.js";
20
22
  import { RootErrorBoundary } from "./root-error-boundary.js";
21
23
  import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
22
- import { getMemoizedContentPromise } from "./segment-content-promise.js";
24
+ import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
25
+ import { getBoundaryContent } from "./segment-boundary-content.js";
23
26
  import {
24
27
  buildLoaderPromise,
25
28
  getMemoizedLoaderPromise,
26
29
  } from "./segment-loader-promise.js";
30
+ import {
31
+ auditSegmentElement,
32
+ finishTreeAudit,
33
+ startTreeAudit,
34
+ } from "./suspense-audit.js";
27
35
 
28
36
  /**
29
37
  * Debug log for the segment tree build, gated on the baked flag. Runs on BOTH
@@ -212,13 +220,25 @@ function wrapDefaultOutletContent(
212
220
  } as any);
213
221
  }
214
222
 
215
- if (content.type === OutletProvider && props.segment?.type === "layout") {
223
+ if (
224
+ content.type ===
225
+ (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
226
+ ? AuditedOutletProvider
227
+ : OutletProvider) &&
228
+ props.segment?.type === "layout"
229
+ ) {
216
230
  return React.cloneElement(content, {
217
231
  content: wrapDefaultOutletContent(props.content, transition),
218
232
  } as any);
219
233
  }
220
234
 
221
- if (content.type === LoaderBoundary && props.segment?.type === "layout") {
235
+ if (
236
+ content.type ===
237
+ (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
238
+ ? AuditedLoaderBoundary
239
+ : LoaderBoundary) &&
240
+ props.segment?.type === "layout"
241
+ ) {
222
242
  return React.cloneElement(content, {
223
243
  outletContent: wrapDefaultOutletContent(props.outletContent, transition),
224
244
  } as any);
@@ -376,6 +396,10 @@ export async function renderSegments(
376
396
  }
377
397
  // Separate segments by type, passing intercept segments for explicit injection
378
398
  const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
399
+ // Dev only: the wrapper chain per segment, compared with the previous
400
+ // tree's (suspense-audit.ts, I5).
401
+ const treeAudit =
402
+ process.env.NODE_ENV !== "production" ? startTreeAudit() : undefined;
379
403
 
380
404
  // A route is "in a transition scope" when its own segment OR any layout in
381
405
  // its matched chain declares transition(). Both transition() forms land here:
@@ -473,34 +497,33 @@ export async function renderSegments(
473
497
 
474
498
  let nodeContent: ReactNode = null;
475
499
  if (!clientGroup && isRenderableLoading(loading)) {
476
- // forceAwait (popstate, stale-revalidation, fully-prefetched nav) renders a
477
- // loading() route with the route content ALREADY resolved, so its
478
- // RouteContentWrapper Suspender does not suspend for a microtask and flash
479
- // the loading() fallback on a NORMAL (non-transition) commit. The router
480
- // data is known-ready on these paths, so awaiting the content here is free.
481
- // The wrapper tree is unchanged (RouteContentWrapper is still created with
482
- // the same key/fallback) — only the `content` prop is a resolved node
483
- // instead of a pending promise, which Suspender renders synchronously. This
484
- // mirrors the forceAwait loaderData unwrap above; a CLIENT component that
485
- // suspends on mount inside the content still reveals a fallback (it is not
486
- // pre-resolved).
487
- const contentPromise = getMemoizedContentPromise(resolvedComponent);
488
- let loadingContent: Promise<ReactNode> | ReactNode = contentPromise;
500
+ // The wrapper tree is the same on every path; only `content` differs. A
501
+ // settled component is handed over as the node (getBoundaryContent), a
502
+ // pending one as its promise, so only pending work shows the loading()
503
+ // fallback. forceAwait also awaits a pending component here. A CLIENT
504
+ // component that suspends on mount inside the content still reveals a
505
+ // fallback.
506
+ let loadingContent: ReactNode = getBoundaryContent(resolvedComponent);
489
507
  if (forceAwait) {
490
508
  const contentAwaitStart = segDebug ? performance.now() : 0;
491
- loadingContent = await contentPromise;
509
+ loadingContent = await loadingContent;
492
510
  if (segDebug) {
493
511
  segDebugLog(`segment ${id}: content awaited (forceAwait)`, {
494
512
  ms: Math.round(performance.now() - contentAwaitStart),
495
513
  });
496
514
  }
497
515
  }
498
- nodeContent = createElement(RouteContentWrapper, {
499
- key: `suspense-loading-${id}`,
500
- content: loadingContent,
501
- fallback: loading,
502
- segmentId: id,
503
- });
516
+ nodeContent = createElement(
517
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
518
+ ? AuditedRouteContent
519
+ : RouteContentWrapper,
520
+ {
521
+ key: `suspense-loading-${id}`,
522
+ content: loadingContent,
523
+ fallback: loading,
524
+ segmentId: id,
525
+ },
526
+ );
504
527
  } else {
505
528
  // [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. A segment in the no-loading()
506
529
  // branch whose component decodes as a Promise/lazy gets registered into
@@ -593,7 +616,10 @@ export async function renderSegments(
593
616
  let boundaryAwaitedLoaderIds: string[] | undefined;
594
617
  if (forceAwait || isAction) {
595
618
  const awaitStart = segDebug ? performance.now() : 0;
596
- boundaryLoaderData = await loaderDataPromise;
619
+ boundaryLoaderData =
620
+ loaderDataPromise instanceof Promise
621
+ ? await loaderDataPromise
622
+ : loaderDataPromise;
597
623
  if (segDebug) {
598
624
  segDebugLog(`segment ${id}: loaders awaited (forceAwait/action)`, {
599
625
  loaderIds,
@@ -621,28 +647,38 @@ export async function renderSegments(
621
647
  );
622
648
  }
623
649
  }
624
- content = createElement(LoaderBoundary, {
625
- key: `loader-boundary-${key}`,
626
- loaderDataPromise: boundaryLoaderData,
627
- loaderIds,
628
- loaderStreams: boundaryLoaderStreams,
629
- awaitedLoaderIds: boundaryAwaitedLoaderIds,
630
- fallback: loading,
631
- outletKey: key,
632
- outletContent,
633
- segment: node.segment,
634
- parallel: node.parallel,
635
- children: nodeContent,
636
- });
650
+ content = createElement(
651
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
652
+ ? AuditedLoaderBoundary
653
+ : LoaderBoundary,
654
+ {
655
+ key: `loader-boundary-${key}`,
656
+ loaderDataPromise: boundaryLoaderData,
657
+ loaderIds,
658
+ loaderStreams: boundaryLoaderStreams,
659
+ awaitedLoaderIds: boundaryAwaitedLoaderIds,
660
+ fallback: loading,
661
+ outletKey: key,
662
+ outletContent,
663
+ segment: node.segment,
664
+ parallel: node.parallel,
665
+ children: nodeContent,
666
+ },
667
+ );
637
668
  } else if (loaderEntries.length === 0) {
638
- content = createElement(OutletProvider, {
639
- key,
640
- content: outletContent,
641
- segment: node.segment,
642
- parallel: node.parallel,
643
- pending: outletPending,
644
- children: nodeContent,
645
- });
669
+ content = createElement(
670
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
671
+ ? AuditedOutletProvider
672
+ : OutletProvider,
673
+ {
674
+ key,
675
+ content: outletContent,
676
+ segment: node.segment,
677
+ parallel: node.parallel,
678
+ pending: outletPending,
679
+ children: nodeContent,
680
+ },
681
+ );
646
682
  } else {
647
683
  const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
648
684
  const parallelOwnedLoaders = loaderEntries.filter(
@@ -776,17 +812,23 @@ export async function renderSegments(
776
812
  }
777
813
  }
778
814
 
779
- content = createElement(OutletProvider, {
780
- key,
781
- content: outletContent,
782
- segment: node.segment,
783
- parallel: node.parallel,
784
- loaderData: Object.keys(loaderData).length > 0 ? loaderData : undefined,
785
- loaderStreams,
786
- awaitedLoaderIds,
787
- pending: outletPending,
788
- children: errorFallback ?? nodeContent,
789
- });
815
+ content = createElement(
816
+ process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT
817
+ ? AuditedOutletProvider
818
+ : OutletProvider,
819
+ {
820
+ key,
821
+ content: outletContent,
822
+ segment: node.segment,
823
+ parallel: node.parallel,
824
+ loaderData:
825
+ Object.keys(loaderData).length > 0 ? loaderData : undefined,
826
+ loaderStreams,
827
+ awaitedLoaderIds,
828
+ pending: outletPending,
829
+ children: errorFallback ?? nodeContent,
830
+ },
831
+ );
790
832
  }
791
833
 
792
834
  // Wrap with MountContextProvider for include() scoped components.
@@ -800,6 +842,22 @@ export async function renderSegments(
800
842
  });
801
843
  }
802
844
 
845
+ if (process.env.NODE_ENV !== "production") {
846
+ auditSegmentElement(
847
+ treeAudit,
848
+ id,
849
+ node.segment.type,
850
+ key,
851
+ content,
852
+ node.segment.type === "layout" &&
853
+ !!ReactViewTransition &&
854
+ !!transition &&
855
+ transition.viewTransition !== false,
856
+ node.parallel.map((p) => p.id),
857
+ [node.segment, ...node.parallel, ...node.loaders],
858
+ );
859
+ }
860
+
803
861
  if (segDebug) {
804
862
  segDebugLog(`segment ${id} built`, {
805
863
  type: node.segment.type,
@@ -849,6 +907,8 @@ export async function renderSegments(
849
907
  });
850
908
  }
851
909
 
910
+ if (process.env.NODE_ENV !== "production") finishTreeAudit(treeAudit, result);
911
+
852
912
  return result;
853
913
  }
854
914
 
@@ -39,6 +39,21 @@ export function resolveRouterVersions(
39
39
  );
40
40
  }
41
41
 
42
+ /**
43
+ * The version an on-demand prerender overlay key carries: the router's data
44
+ * version, because the overlay stores segment data, as the segment cache does
45
+ * (a client-only deploy keeps both). The write side (`router.prerender`,
46
+ * router.ts) and the read side (rsc/handler.ts) both call this with
47
+ * `createRouter({ version })`, the only override the trigger can see, so they
48
+ * cannot disagree; a `createRSCHandler({ version })` does not move it.
49
+ */
50
+ export function resolvePrerenderVersion(
51
+ routerId: string | undefined,
52
+ routerVersion: string | undefined,
53
+ ): string {
54
+ return resolveRouterVersions(routerId, routerVersion).data;
55
+ }
56
+
42
57
  /** The table gives some router a pair of its own. */
43
58
  function hasRouterVersions(): boolean {
44
59
  if (!table) return false;
@@ -15,6 +15,10 @@ import { invariant, DslContextError } from "../errors";
15
15
  import type { DefaultRouteName } from "../types/global-namespace.js";
16
16
  import type { ContextVar } from "../context-var.js";
17
17
  import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.js";
18
+ import {
19
+ noteWarmIdentityRead,
20
+ type WarmContext,
21
+ } from "../prerender/warm-request.js";
18
22
  import {
19
23
  endIdentityExempt,
20
24
  isInsideCacheExecScope,
@@ -261,8 +265,13 @@ export type EntryData =
261
265
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
262
266
  prerenderDef?: {
263
267
  getParams?: (ctx: any) => Promise<any[]> | any[];
264
- options?: { concurrency?: number };
268
+ options?: {
269
+ concurrency?: number;
270
+ onDemand?: import("../prerender/on-demand.js").OnDemandOption;
271
+ };
265
272
  };
273
+ /** Set when the route opted into on-demand prerender (Prerender(..., { onDemand })) */
274
+ isOnDemand?: true;
266
275
  /** Set when route is wrapped with Passthrough() — has a separate live handler */
267
276
  isPassthrough?: true;
268
277
  /** Live handler for runtime fallback (only set on Passthrough routes) */
@@ -428,6 +437,7 @@ export const getContext = (): {
428
437
  getStore: () => HelperContext;
429
438
  getParent: () => EntryData | null;
430
439
  getOrCreateStore: (forRoute?: string) => HelperContext;
440
+ runIsolated: <T>(forRoute: string, callback: () => T) => T;
431
441
  getNextIndex: (
432
442
  type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
433
443
  ) => string;
@@ -447,26 +457,29 @@ export const getContext = (): {
447
457
  ) => T;
448
458
  } => {
449
459
  const context = RangoContext;
460
+ const createStore = (forRoute?: string): HelperContext => ({
461
+ manifest: new Map<string, EntryData>(),
462
+ namespace: "",
463
+ parent: null,
464
+ forRoute,
465
+ counters: {},
466
+ patterns: new Map<string, string>(),
467
+ patternsByPrefix: new Map<string, Map<string, string>>(),
468
+ trailingSlash: new Map<string, "never" | "always" | "ignore">(),
469
+ searchSchemas: new Map<string, Record<string, string>>(),
470
+ });
450
471
 
451
472
  return {
452
473
  context,
453
474
  getOrCreateStore: (forRoute?: string): HelperContext => {
454
475
  let store = RangoContext.getStore();
455
476
  if (!store) {
456
- store = {
457
- manifest: new Map<string, EntryData>(),
458
- namespace: "",
459
- parent: null,
460
- forRoute,
461
- counters: {},
462
- patterns: new Map<string, string>(),
463
- patternsByPrefix: new Map<string, Map<string, string>>(),
464
- trailingSlash: new Map<string, "never" | "always" | "ignore">(),
465
- searchSchemas: new Map<string, Record<string, string>>(),
466
- } satisfies HelperContext;
477
+ store = createStore(forRoute);
467
478
  }
468
479
  return store;
469
480
  },
481
+ runIsolated: <T>(forRoute: string, callback: () => T): T =>
482
+ context.run(createStore(forRoute), callback),
470
483
  getStore: (): HelperContext => {
471
484
  const store = context.getStore();
472
485
  if (!store) {
@@ -959,19 +972,20 @@ export interface IdentityReadWording {
959
972
  * nothing and are not captured reads: they take refuseInCacheScope alone.
960
973
  */
961
974
  export function guardIdentityRead(
962
- ctx: unknown,
975
+ ctx: WarmContext | null | undefined,
963
976
  surface: string,
964
977
  wording: IdentityReadWording,
965
978
  ): void {
966
979
  if (isInsideIdentityExempt()) return;
967
980
  const { verb, fix } = wording;
968
981
  if (tripShellCaptureGuard(ctx, surface, fix.warning)) {
982
+ noteWarmIdentityRead(ctx, surface);
969
983
  throw new Error(
970
984
  `${surface} cannot be ${verb} while capturing a shared shell ` +
971
985
  `(ppr shell capture). ${fix.capture}`,
972
986
  );
973
987
  }
974
- refuseInCacheScope(surface, wording);
988
+ refuseInCacheScope(surface, wording, ctx);
975
989
  if (wording.record !== false) recordLoaderIdentityRead(surface, verb);
976
990
  }
977
991
 
@@ -982,13 +996,16 @@ export function guardIdentityRead(
982
996
  export function refuseInCacheScope(
983
997
  surface: string,
984
998
  { verb, fix }: IdentityReadWording,
999
+ ctx?: WarmContext | null,
985
1000
  ): void {
986
1001
  if (isInsideCacheExecScope()) {
1002
+ noteWarmIdentityRead(ctx, surface);
987
1003
  throw new Error(
988
1004
  `${surface} cannot be ${verb} inside a "use cache" function. ${fix.useCache}`,
989
1005
  );
990
1006
  }
991
1007
  if (isInsideCacheScope()) {
1008
+ noteWarmIdentityRead(ctx, surface);
992
1009
  throw new Error(
993
1010
  `${surface} cannot be ${verb} inside a cache() boundary. ${fix.cacheScope}`,
994
1011
  );
@@ -1074,7 +1091,7 @@ const NON_CACHEABLE_READ: IdentityReadWording = {
1074
1091
  */
1075
1092
  export function assertNonCacheableReadAllowed(
1076
1093
  keyOrVar: string | ContextVar<unknown>,
1077
- requestCtx?: unknown,
1094
+ requestCtx?: WarmContext | null,
1078
1095
  ): void {
1079
1096
  const name = typeof keyOrVar === "string" ? ` "${keyOrVar}"` : "";
1080
1097
  guardIdentityRead(
@@ -1276,7 +1293,10 @@ export function isInsideAnyLoaderScope(): boolean {
1276
1293
 
1277
1294
  /**
1278
1295
  * The one ppr opt-in predicate: a page route entry that DECLARED `ppr`
1279
- * (`false` and undefined mean plain axis 1). Shared by the serve path
1296
+ * (`false`, undefined, and on-demand routes mean plain axis 1). A writable
1297
+ * prerender refresh cannot atomically replace the separate document shell, so
1298
+ * combining those lanes would pair a fresh tail with a stale prelude. Shared by
1299
+ * the serve path
1280
1300
  * (rsc/shell-serve.ts resolvePprConfig) and the header-write latch below so
1281
1301
  * the two layers can never drift on what counts as a ppr route.
1282
1302
  */
@@ -1285,7 +1305,10 @@ export function isPprEntry(entry: EntryData): entry is EntryData & {
1285
1305
  ppr: true | import("../urls/pattern-types.js").PartialPrerenderProps;
1286
1306
  } {
1287
1307
  return (
1288
- entry.type === "route" && entry.ppr !== undefined && entry.ppr !== false
1308
+ entry.type === "route" &&
1309
+ entry.isOnDemand !== true &&
1310
+ entry.ppr !== undefined &&
1311
+ entry.ppr !== false
1289
1312
  );
1290
1313
  }
1291
1314
 
@@ -21,6 +21,7 @@ import {
21
21
  requestReadCaptureFix,
22
22
  type IdentityReadWording,
23
23
  } from "./context.js";
24
+ import { assertNotInsidePrerenderProducer } from "../prerender/producer-guard.js";
24
25
  import { requestHeaders, shadowRequestHeaders } from "./request-headers.js";
25
26
 
26
27
  /**
@@ -72,6 +73,7 @@ export interface CookieStore {
72
73
  */
73
74
  export function cookies(): CookieStore {
74
75
  const ctx = getRequestContext();
76
+ assertNotInsidePrerenderProducer(ctx, "cookies()");
75
77
  guardIdentityRead(ctx, "cookies()", COOKIES_READ);
76
78
  return createCookieStore(ctx);
77
79
  }
@@ -273,6 +275,7 @@ const HEADERS_READ_PROPS = new Set<string | symbol>([
273
275
  */
274
276
  export function headers(): ReadonlyHeaders {
275
277
  const ctx = getRequestContext();
278
+ assertNotInsidePrerenderProducer(ctx, "headers()");
276
279
  guardIdentityRead(ctx, "headers()", HEADERS_READ);
277
280
  return new Proxy(requestHeaders(ctx.request), {
278
281
  get(target, prop, receiver) {
@@ -313,6 +316,7 @@ export function invalidateClientCache(): void {
313
316
  }
314
317
  return;
315
318
  }
319
+ assertNotInsidePrerenderProducer(ctx, "invalidateClientCache()");
316
320
  refuseInCacheScope("invalidateClientCache()", COOKIES_READ);
317
321
  ctx._rotateStateCookie();
318
322
  }
@@ -338,6 +342,7 @@ export function keepClientCache(): void {
338
342
  }
339
343
  return;
340
344
  }
345
+ assertNotInsidePrerenderProducer(ctx, "keepClientCache()");
341
346
  refuseInCacheScope("keepClientCache()", COOKIES_READ);
342
347
  ctx._setKeepCacheDirective();
343
348
  }
@@ -340,6 +340,9 @@ export interface HandleStore {
340
340
  segmentId: string,
341
341
  ): Record<string, (string | null)[]> | undefined;
342
342
 
343
+ /** The segment ids for which getRecordOwners returns an entry. */
344
+ getOwnedSegmentIds(): string[];
345
+
343
346
  /**
344
347
  * Replay cached handle data back into the store (for cache hits).
345
348
  * Used to restore handle data when serving cached segments.
@@ -1087,6 +1090,16 @@ export function createHandleStore(): HandleStore {
1087
1090
  return result;
1088
1091
  },
1089
1092
 
1093
+ getOwnedSegmentIds(): string[] {
1094
+ const ids = new Set<string>();
1095
+ for (const handleName in data) {
1096
+ for (const id in data[handleName]) {
1097
+ if (!ids.has(id) && this.getRecordOwners(id)) ids.add(id);
1098
+ }
1099
+ }
1100
+ return [...ids];
1101
+ },
1102
+
1090
1103
  replaySegmentData(
1091
1104
  segmentId: string,
1092
1105
  segmentHandles: Record<string, unknown[]>,
@@ -46,7 +46,10 @@ import { withDefer } from "../defer.js";
46
46
  import { type MetricsStore } from "./context.js";
47
47
  import { observePhase, PHASES } from "../router/instrument.js";
48
48
  import { getFetchableLoader } from "./fetchable-loader-store.js";
49
- import type { SegmentCacheStore } from "../cache/types.js";
49
+ import type {
50
+ SegmentCacheStore,
51
+ ShellSnapshotHandlesValue,
52
+ } from "../cache/types.js";
50
53
  import type { RouterVersions } from "../router-versions.js";
51
54
  import type { Theme, ResolvedThemeConfig } from "../theme/types.js";
52
55
  import type { ExecutionContext, RequestScope } from "../types/request-scope.js";
@@ -289,6 +292,30 @@ export interface RequestContext<
289
292
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
290
293
  _cacheStore?: SegmentCacheStore;
291
294
 
295
+ /**
296
+ * @internal Env-scoped writable prerender store (the durable overlay) plus the
297
+ * deployment identity for build-scoped keys, resolved per request from the
298
+ * `prerender` router option. Read by the serve-path overlay lookup before the
299
+ * bundled build manifest. Absent when no prerender store is configured.
300
+ */
301
+ // ResolvedPrerender<any> (not <TEnv>): onRevalidate's contravariant `env` would
302
+ // otherwise make RequestContext invariant in TEnv, breaking existing
303
+ // RequestContext<{}> -> RequestContext<unknown> assignments.
304
+ _prerender?: import("../prerender/on-demand.js").ResolvedPrerender<any>;
305
+
306
+ /**
307
+ * @internal Set only on the requestless on-demand producer's context. When
308
+ * present, the standalone request-scoped READS — `cookies()`, `headers()`,
309
+ * `invalidateClientCache()`, `keepClientCache()` — throw a
310
+ * PrerenderPersonalizationError so a personalized render is skipped rather than
311
+ * baked into a shared payload. Response MUTATIONS on the producer context
312
+ * (`ctx.setStatus`/`header`/`setCookie`) stay silent no-op stubs, matching
313
+ * build-time prerender (the design's "throw or skip" contract permits skip):
314
+ * they write to a discarded synthetic response, so there is nothing to leak.
315
+ * Never set on the live request path.
316
+ */
317
+ _onDemandProducer?: true;
318
+
292
319
  /**
293
320
  * @internal Compiled `cache.searchParams` filter for default cache-key
294
321
  * generation (undefined = "all", the byte-stable unfiltered format). Set
@@ -365,6 +392,22 @@ export interface RequestContext<
365
392
  */
366
393
  _shellForcedMiss?: true;
367
394
 
395
+ /**
396
+ * @internal This request is a router.prerender() warm
397
+ * (prerender/warm-request.ts). In `replace` mode every runtime cache read
398
+ * misses (isWarmReplace); the layers report into the record. A capture's
399
+ * derived context inherits it through the prototype.
400
+ */
401
+ _prerenderWarm?: import("../prerender/warm-request.js").PrerenderWarmRecord;
402
+
403
+ /**
404
+ * @internal Runs a callback in the async context this request entered the
405
+ * handler with (captureRequestEntryContext). router.prerender() dispatches
406
+ * a warm through it when called from inside a request. Absent where the
407
+ * runtime has no AsyncLocalStorage.snapshot.
408
+ */
409
+ _runAtRequestEntry?: <T>(fn: () => T) => T;
410
+
368
411
  /**
369
412
  * @internal The freshness window of the route cache() records this
370
413
  * request read or wrote (cache-scope.ts noteRouteRecordWindow), as ms
@@ -459,6 +502,8 @@ export interface RequestContext<
459
502
  * host) and miss the record.
460
503
  */
461
504
  fixedDocKey?: string;
505
+ /** @internal The snapshot's `handles` record (ShellSnapshotHandlesValue). */
506
+ prerenderHandles?: ShellSnapshotHandlesValue;
462
507
  /**
463
508
  * @internal Set ONLY by matchPartialWithPprReplay on the navigation-replay
464
509
  * serve path. Its presence arms the explicit-scope composition in
@@ -808,6 +853,18 @@ export interface RequestContext<
808
853
  */
809
854
  _renderErrors?: unknown[];
810
855
 
856
+ /**
857
+ * @internal Rejections of streamed (loading()) handlers that were recovered
858
+ * into a declared errorBoundary()/notFoundBoundary() fallback, by segment id.
859
+ * The recovered promise resolves, so Flight reports nothing; cacheRoute
860
+ * (cache-scope.ts) refuses a write only when one of ITS segments is listed,
861
+ * the granularity a Flight error row has. The same errors are also pushed to
862
+ * `_renderErrors` for the document cache and PPR shell capture. A render
863
+ * under a derived context (shell capture, SWR re-render) owns its own map.
864
+ * Allocated on the first recovery.
865
+ */
866
+ _recoveredHandlerErrors?: Map<string, unknown>;
867
+
811
868
  /**
812
869
  * @internal Report a non-fatal background error through the router's
813
870
  * onError callback. Wired by the RSC handler / router during request
@@ -926,6 +983,8 @@ export type PublicRequestContext<
926
983
  | "_transitionWhenRefs"
927
984
  | "_pprReplayPostMatchReason"
928
985
  | "_cacheStore"
986
+ | "_prerender"
987
+ | "_onDemandProducer"
929
988
  | "_searchParamsFilter"
930
989
  | "_shellCaptureRun"
931
990
  | "_shellImplicitCache"
@@ -936,6 +995,8 @@ export type PublicRequestContext<
936
995
  | "_resolvedCacheKeys"
937
996
  | "_shellKey"
938
997
  | "_shellForcedMiss"
998
+ | "_prerenderWarm"
999
+ | "_runAtRequestEntry"
939
1000
  | "_routeRecordWindow"
940
1001
  | "_shellFragmentPayload"
941
1002
  | "_shellCaptureGuardTripped"
@@ -957,6 +1018,7 @@ export type PublicRequestContext<
957
1018
  | "_inActionRevalidation"
958
1019
  | "_reportedErrors"
959
1020
  | "_renderErrors"
1021
+ | "_recoveredHandlerErrors"
960
1022
  | "_renderBarrier"
961
1023
  | "_resolveRenderBarrier"
962
1024
  | "_renderBarrierSegmentOrder"
@@ -1007,6 +1069,22 @@ export const UNTRACKED_BACKGROUND_TASK: unique symbol = Symbol.for(
1007
1069
  // AsyncLocalStorage instance for request context
1008
1070
  const requestContextStorage = new AsyncLocalStorage<RequestContext<any>>();
1009
1071
 
1072
+ /**
1073
+ * Capture the current async context (RequestContext._runAtRequestEntry), taken
1074
+ * on handler entry before any router scope, so a nested dispatch (a
1075
+ * router.prerender() warm called from a handler, loader or action) does not
1076
+ * inherit the caller's route-definition, loader and cache-scope state. Per
1077
+ * request: workerd refuses a snapshot outside the request that created it.
1078
+ * See docs/design/prerender-every-route.md.
1079
+ */
1080
+ export function captureRequestEntryContext():
1081
+ | (<T>(fn: () => T) => T)
1082
+ | undefined {
1083
+ return typeof AsyncLocalStorage.snapshot === "function"
1084
+ ? AsyncLocalStorage.snapshot()
1085
+ : undefined;
1086
+ }
1087
+
1010
1088
  /**
1011
1089
  * Run a function within a request context
1012
1090
  * Used by the RSC handler to provide context to server actions
@@ -1118,6 +1196,12 @@ export interface CreateRequestContextOptions<TEnv> {
1118
1196
  initialResponse?: Response;
1119
1197
  /** Optional cache store for segment caching (used by CacheScope) */
1120
1198
  cacheStore?: SegmentCacheStore;
1199
+ /**
1200
+ * Optional env-scoped writable prerender store + deployment identity (the
1201
+ * durable overlay), resolved per request by the RSC handler from the
1202
+ * `prerender` router option.
1203
+ */
1204
+ prerender?: import("../prerender/on-demand.js").ResolvedPrerender<any>;
1121
1205
  /**
1122
1206
  * Compiled `cache.searchParams` filter from the resolved handler cache
1123
1207
  * config (handler.ts). Stored as _searchParamsFilter.
@@ -1168,6 +1252,7 @@ export function createRequestContext<TEnv>(
1168
1252
  variables,
1169
1253
  initialResponse,
1170
1254
  cacheStore,
1255
+ prerender,
1171
1256
  searchParamsFilter,
1172
1257
  explicitTaggedStores,
1173
1258
  cacheProfiles,
@@ -1543,6 +1628,7 @@ export function createRequestContext<TEnv>(
1543
1628
 
1544
1629
  _handleStore: handleStore,
1545
1630
  _cacheStore: cacheStore,
1631
+ _prerender: prerender,
1546
1632
  _versions: versions,
1547
1633
  _searchParamsFilter: searchParamsFilter,
1548
1634
  _explicitTaggedStores: explicitTaggedStores,