@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
@@ -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[]>,
@@ -86,7 +86,12 @@ export async function getLoaderLazy(
86
86
 
87
87
  // Same as the lazy branch: a thrown import is a server error, not a
88
88
  // not-found. Let it propagate to the caller for a 500 + onError.
89
- await import(/* @vite-ignore */ `/${filePath}`);
89
+ // Not a template literal: es-module-lexer 3 reports `/${x}` as the
90
+ // specifier "/*", and plugin-rsc's client-reference scan build rewrites
91
+ // every import to `import "<specifier>"`, so the build fails to resolve
92
+ // "/*" (dynamic-import-specifiers.test.ts).
93
+ const modulePath = "/" + filePath;
94
+ await import(/* @vite-ignore */ modulePath);
90
95
 
91
96
  const registered = getFetchableLoader(id);
92
97
  if (registered) {