@rangojs/router 0.20.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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -33,11 +33,22 @@ import type {
33
33
  SegmentHandleData,
34
34
  } from "../cache/types.js";
35
35
  import type { RouteMatchResult } from "./pattern-matching.js";
36
+ import type { RouterVersions } from "../router-versions.js";
37
+ import type { OnDemandRouteConfig } from "../prerender/on-demand.js";
38
+ import { isPrerenderPersonalizationError } from "../prerender/producer-guard.js";
39
+ import { isDataNotFoundError } from "../errors.js";
36
40
 
37
41
  export interface PrerenderMatchDeps<TEnv = any> {
38
42
  /** Owning router id; scopes bake-time root-scope/search-schema lookups the
39
43
  * same way reqCtx._routerId does at runtime (issue #762). */
40
44
  routerId?: string;
45
+ /**
46
+ * The owning router's cache versions. Set on the requestless on-demand
47
+ * producer's context as `_versions`, so cache stores built inside the render
48
+ * key under the router's pair like a live request does instead of the
49
+ * whole-build fallback (server/build-version-table.ts getCacheVersions).
50
+ */
51
+ versions?: RouterVersions;
41
52
  findMatch: (
42
53
  pathname: string,
43
54
  ) => RouteMatchResult<TEnv> | null | Promise<RouteMatchResult<TEnv> | null>;
@@ -66,6 +77,14 @@ export async function matchForPrerender<TEnv = any>(
66
77
  buildEnv?: TEnv,
67
78
  /** Dev-only: check getParams() for passthrough routes to skip unknown params. */
68
79
  devMode?: boolean,
80
+ /**
81
+ * Requestless on-demand refresh (router.prerender()). Arms the personalization
82
+ * guard: cookies()/headers()/response mutations throw a
83
+ * PrerenderPersonalizationError so the caller maps a personalized render to
84
+ * skipped-personalized instead of baking a shared payload. `buildEnv` here is
85
+ * the live trigger env, not the shared build env.
86
+ */
87
+ onDemand?: boolean,
69
88
  ): Promise<{
70
89
  segments: SerializedSegmentData[];
71
90
  /** RSC-encoded handle map ("" when none) — see handle-snapshot.ts. Encoded in
@@ -79,6 +98,11 @@ export async function matchForPrerender<TEnv = any>(
79
98
  * the sinks store it as-is (no longer merge raw records). */
80
99
  interceptHandles?: string;
81
100
  passthrough?: true;
101
+ /** On-demand only: the render hit `notFound()` and nothing else failed. */
102
+ notFound?: true;
103
+ /** Route-level onDemand config (ttl/tags) read off the route entry; only set
104
+ * when `onDemand` and the route declared an onDemand object literal. */
105
+ onDemandConfig?: OnDemandRouteConfig;
82
106
  /**
83
107
  * The matched route entry's `ppr` path option, surfaced so the build
84
108
  * collection can flag Prerender+ppr routes as build-time shell candidates
@@ -109,383 +133,467 @@ export async function matchForPrerender<TEnv = any>(
109
133
  passthrough: true as const,
110
134
  });
111
135
 
112
- return runWithRouterContext(routerCtx, async () => {
113
- // 2. Load the manifest entry tree
114
- const manifestEntry = await loadManifest(
115
- matched.entry,
116
- matched.routeKey,
117
- pathname,
118
- undefined,
119
- false,
120
- );
121
-
122
- // 3. Build ancestor chain [root, ..., route]
123
- const entries: EntryData[] = [];
124
- for (const entry of traverseBack(manifestEntry)) {
125
- entries.push(entry);
126
- }
127
-
128
- // 3b. Dev-mode passthrough shortcut: if the route is a Passthrough route
129
- // and has getParams(), check if the matched params are in the known list.
130
- // In production, only known params are pre-rendered; unknown params fall
131
- // through to the live handler. Mirror that behavior in dev mode to avoid
132
- // rendering unknown params with build: true.
133
- // Vars collected from getParams() probe — merged into render context below.
134
- let devProbeBuildVars: Record<string, any> | undefined;
135
-
136
- if (devMode && matchedPassthroughRoute) {
137
- const routeEntry = entries.find(
138
- (
139
- e,
140
- ): e is EntryData & {
141
- type: "route";
142
- prerenderDef: { getParams: (ctx: any) => Promise<any[]> | any[] };
143
- } =>
144
- e.type === "route" &&
145
- !!(e as any).isPassthrough &&
146
- !!(e as any).prerenderDef?.getParams,
147
- );
148
- if (routeEntry) {
149
- try {
150
- const probeBuildVars: Record<string, any> = {};
151
- const knownParamsList = await routeEntry.prerenderDef.getParams({
152
- build: true as const,
153
- dev: true,
154
- set: ((keyOrVar: any, value: any) => {
155
- contextSet(probeBuildVars, keyOrVar, value);
156
- }) as any,
157
- reverse: createReverseFunction(deps.mergedRouteMap),
158
- get env() {
159
- if (buildEnv !== undefined) return buildEnv;
160
- throw new Error(
161
- "[rango] ctx.env is not available during dev-mode getParams(). " +
162
- "Configure buildEnv in your rango() plugin options to enable build-time env access.",
163
- );
164
- },
165
- });
166
- // Compare only the keys returned by getParams — ignore mount params
167
- // from include() prefixes that aren't part of the handler's params.
168
- const isKnown = knownParamsList.some((known: Record<string, any>) => {
169
- const knownKeys = Object.keys(known);
170
- return knownKeys.every(
171
- (k) => String(known[k]) === String(matchedParams[k]),
172
- );
173
- });
174
- if (!isKnown) {
175
- return passthroughResult();
176
- }
177
- // Preserve vars set by getParams() for the render context
178
- if (hasContextVars(probeBuildVars)) {
179
- devProbeBuildVars = probeBuildVars;
180
- }
181
- } catch (err: any) {
182
- // Mirror production semantics (prerender-collection.ts):
183
- // Skip errors are intentional — treat as passthrough.
184
- // All other errors propagate so dev surfaces them.
185
- if (err?.name === "Skip") {
186
- return passthroughResult();
187
- }
188
- throw err;
189
- }
190
- }
191
- }
136
+ // Set inside the render; out here for the notFound() outcome at the end.
137
+ let onDemandConfig: OnDemandRouteConfig | undefined;
192
138
 
193
- // 4. Create handle store for collecting handle data
194
- const handleStore = createHandleStore();
195
-
196
- // 5. Create a minimal request context with the handle store
197
- // Shallow-copy getParams vars so each param set is independent.
198
- // In dev mode, merge vars from the getParams() probe if the caller
199
- // didn't provide buildVars (production passes them from expandPrerenderRoutes).
200
- const effectiveBuildVars = buildVars ?? devProbeBuildVars;
201
- const variables: Record<string, any> = effectiveBuildVars
202
- ? { ...effectiveBuildVars }
203
- : {};
204
- const stubRes = new Response(null, { status: 200 });
205
- const minimalRequestContext: RequestContext<TEnv> = {
206
- env: buildEnv ?? ({} as TEnv),
207
- request: new Request("http://prerender" + pathname),
208
- url: new URL("http://prerender" + pathname),
209
- originalUrl: new URL("http://prerender" + pathname),
210
- pathname,
211
- searchParams: new URLSearchParams(),
212
- _variables: variables,
213
- build: true,
214
- // Inert here: the prerender-collect / static-render pass has no live
215
- // PPR-shell decision to gate; dynamic() reaches the shell axis only on a
216
- // live request or a shell capture.
217
- dynamic: () => {},
218
- get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
219
- set: ((keyOrVar: any, value: any) => {
220
- contextSet(variables, keyOrVar, value);
221
- }) as any,
222
- params: matchedParams,
223
- res: stubRes,
224
- cookie: () => undefined,
225
- cookies: () => ({}),
226
- _readCookie: () => undefined,
227
- _readCookies: () => ({}),
228
- setCookie: () => {},
229
- deleteCookie: () => {},
230
- header: () => {},
231
- setStatus: () => {},
232
- _setStatus: () => {},
233
- _rotateStateCookie: () => {},
234
- _setKeepCacheDirective: () => {},
235
- _setFreshReadsCookie: () => {},
236
- use: (() => {
237
- throw new Error("use() not available during pre-rendering");
238
- }) as any,
239
- method: "GET",
240
- _handleStore: handleStore,
241
- _requestTags: new Set<string>(),
242
- waitUntil: () => {},
243
- onResponse: () => {},
244
- _onResponseCallbacks: [],
245
- // No theme at build time: no request cookie.
246
- _readTheme: () => undefined,
247
- setLocationState() {},
248
- _locationState: undefined,
249
- _renderBarrier: Promise.resolve(),
250
- _resolveRenderBarrier: () => {},
251
- _reportedErrors: new WeakSet<object>(),
252
- reverse: createReverseFunction(
253
- deps.mergedRouteMap,
139
+ const rendered = runWithRouterContext(routerCtx, () =>
140
+ routerCtx.getContext().runIsolated(matched.routeKey, async () => {
141
+ // 2. Load the manifest entry tree
142
+ const manifestEntry = await loadManifest(
143
+ matched.entry,
254
144
  matched.routeKey,
255
- matchedParams,
256
- matched.routeKey
257
- ? isRouteRootScoped(matched.routeKey, deps.routerId)
258
- : undefined,
259
- ),
260
- // Scope the bake context's own lookups (createPrerenderContext reads it
261
- // through the request ALS) per router as well.
262
- _routerId: deps.routerId,
263
- };
264
-
265
- return runWithRequestContext(minimalRequestContext, async () => {
266
- // 6. Create prerender context with synthetic URL.
267
- // Prerender handlers get params, pathname, url, searchParams, search,
268
- // reverse, use(handle), and optionally env (when buildEnv is configured).
269
- const buildCtx = createPrerenderContext<TEnv>(
270
- matchedParams,
271
145
  pathname,
272
- deps.mergedRouteMap,
273
- matched.routeKey,
274
- variables,
275
- matchedPassthroughRoute,
276
- buildEnv,
277
- devMode,
146
+ undefined,
147
+ false,
278
148
  );
279
149
 
280
- // 7. Wire use() for handles only (loaders throw)
281
- setupBuildUse(buildCtx);
150
+ // 3. Build ancestor chain [root, ..., route]
151
+ const entries: EntryData[] = [];
152
+ for (const entry of traverseBack(manifestEntry)) {
153
+ entries.push(entry);
154
+ }
282
155
 
283
- // 8. Resolve all segments with skipLoaders
284
- const loaderPromises = new Map<string, Promise<any>>();
285
- const allSegments = await deps.resolveAllSegments(
286
- entries,
287
- matched.routeKey,
288
- matchedParams,
289
- buildCtx,
290
- loaderPromises,
291
- // throwOnError: a render failure (or `throw new Skip()`) must reach the
292
- // build/dev caller, not be baked into a frozen error page (issue #587).
293
- { skipLoaders: true, throwOnError: true },
294
- );
156
+ // On-demand refresh: read the route's onDemand config (ttl/tags) off the
157
+ // retained route entry. `tags` is a function, so it can't ride the trie; the
158
+ // trigger resolves it from here. Only an object literal carries config;
159
+ // `onDemand: true` uses router-level defaults (config stays undefined).
160
+ if (onDemand) {
161
+ const routeEntry = entries.find(
162
+ (e) =>
163
+ e.type === "route" &&
164
+ !!(e as { prerenderDef?: unknown }).prerenderDef,
165
+ ) as
166
+ | { prerenderDef?: { options?: { onDemand?: unknown } } }
167
+ | undefined;
168
+ const od = routeEntry?.prerenderDef?.options?.onDemand;
169
+ if (od && typeof od === "object") {
170
+ onDemandConfig = od as OnDemandRouteConfig;
171
+ }
172
+ }
295
173
 
296
- // 9. Detect passthrough sentinel: handler returned ctx.passthrough().
297
- // When the route declares loading(), the handler result is deferred so the
298
- // component is a thenable resolving to the sentinel — detectPrerenderPassthrough
299
- // resolves thenables before testing (a sync check would miss it and bake a
300
- // corrupt artifact).
301
- if (await detectPrerenderPassthrough(allSegments)) {
302
- return passthroughResult();
174
+ // 3b. Dev-mode passthrough shortcut: if the route is a Passthrough route
175
+ // and has getParams(), check if the matched params are in the known list.
176
+ // In production, only known params are pre-rendered; unknown params fall
177
+ // through to the live handler. Mirror that behavior in dev mode to avoid
178
+ // rendering unknown params with build: true.
179
+ // Vars collected from getParams() probe — merged into render context below.
180
+ let devProbeBuildVars: Record<string, any> | undefined;
181
+
182
+ // On-demand refresh renders exactly the requested param set, even one not
183
+ // returned by getParams() (that is the point of an explicit refresh), so the
184
+ // dev getParams() passthrough probe is skipped for onDemand.
185
+ if (devMode && matchedPassthroughRoute && !onDemand) {
186
+ const routeEntry = entries.find(
187
+ (
188
+ e,
189
+ ): e is EntryData & {
190
+ type: "route";
191
+ prerenderDef: { getParams: (ctx: any) => Promise<any[]> | any[] };
192
+ } =>
193
+ e.type === "route" &&
194
+ !!(e as any).isPassthrough &&
195
+ !!(e as any).prerenderDef?.getParams,
196
+ );
197
+ if (routeEntry) {
198
+ try {
199
+ const probeBuildVars: Record<string, any> = {};
200
+ const knownParamsList = await routeEntry.prerenderDef.getParams({
201
+ build: true as const,
202
+ dev: true,
203
+ set: ((keyOrVar: any, value: any) => {
204
+ contextSet(probeBuildVars, keyOrVar, value);
205
+ }) as any,
206
+ reverse: createReverseFunction(deps.mergedRouteMap),
207
+ get env() {
208
+ if (buildEnv !== undefined) return buildEnv;
209
+ throw new Error(
210
+ "[rango] ctx.env is not available during dev-mode getParams(). " +
211
+ "Configure buildEnv in your rango() plugin options to enable build-time env access.",
212
+ );
213
+ },
214
+ });
215
+ // Compare only the keys returned by getParams — ignore mount params
216
+ // from include() prefixes that aren't part of the handler's params.
217
+ const isKnown = knownParamsList.some(
218
+ (known: Record<string, any>) => {
219
+ const knownKeys = Object.keys(known);
220
+ return knownKeys.every(
221
+ (k) => String(known[k]) === String(matchedParams[k]),
222
+ );
223
+ },
224
+ );
225
+ if (!isKnown) {
226
+ return passthroughResult();
227
+ }
228
+ // Preserve vars set by getParams() for the render context
229
+ if (hasContextVars(probeBuildVars)) {
230
+ devProbeBuildVars = probeBuildVars;
231
+ }
232
+ } catch (err: any) {
233
+ // Mirror production semantics (prerender-collection.ts):
234
+ // Skip errors are intentional — treat as passthrough.
235
+ // All other errors propagate so dev surfaces them.
236
+ if (err?.name === "Skip") {
237
+ return passthroughResult();
238
+ }
239
+ throw err;
240
+ }
241
+ }
303
242
  }
304
243
 
305
- // 10. Filter out any loader segments (belt-and-suspenders)
306
- const nonLoaderSegments = allSegments.filter((s) => s.type !== "loader");
307
-
308
- // 11. Wait for handles to settle
309
- handleStore.seal();
310
- await handleStore.settled;
311
-
312
- // 12. Serialize segments using the cache serializer
313
- const { serializeSegments } = await import("../cache/segment-codec.js");
314
- const { encodeHandles } = await import("../cache/handle-snapshot.js");
315
- // Flight encodes a component that throws (an async server component in
316
- // the tree) as an error row and completes normally. Rethrown below like
317
- // a handler throw, so the caller's policy decides instead of baking the
318
- // error (#914).
319
- const flightErrors: unknown[] = [];
320
- const onFlightError = (error: unknown): void => {
321
- flightErrors.push(error);
244
+ // 4. Create handle store for collecting handle data
245
+ const handleStore = createHandleStore();
246
+
247
+ // 5. Create a minimal request context with the handle store
248
+ // Shallow-copy getParams vars so each param set is independent.
249
+ // In dev mode, merge vars from the getParams() probe if the caller
250
+ // didn't provide buildVars (production passes them from expandPrerenderRoutes).
251
+ const effectiveBuildVars = buildVars ?? devProbeBuildVars;
252
+ const variables: Record<string, any> = effectiveBuildVars
253
+ ? { ...effectiveBuildVars }
254
+ : {};
255
+ const stubRes = new Response(null, { status: 200 });
256
+ const minimalRequestContext: RequestContext<TEnv> = {
257
+ env: buildEnv ?? ({} as TEnv),
258
+ // Requestless refresh: arm the personalization guard so standalone
259
+ // cookies()/headers() (and the cache-directive helpers) throw a
260
+ // PrerenderPersonalizationError, which the producer maps to
261
+ // skipped-personalized. Build/dev prerender leave this unset.
262
+ ...(onDemand ? { _onDemandProducer: true as const } : {}),
263
+ ...(onDemand && deps.versions ? { _versions: deps.versions } : {}),
264
+ request: new Request("http://prerender" + pathname),
265
+ url: new URL("http://prerender" + pathname),
266
+ originalUrl: new URL("http://prerender" + pathname),
267
+ pathname,
268
+ searchParams: new URLSearchParams(),
269
+ _variables: variables,
270
+ build: true,
271
+ // Inert here: the prerender-collect / static-render pass has no live
272
+ // PPR-shell decision to gate; dynamic() reaches the shell axis only on a
273
+ // live request or a shell capture.
274
+ dynamic: () => {},
275
+ get: ((keyOrVar: any) => contextGet(variables, keyOrVar)) as any,
276
+ set: ((keyOrVar: any, value: any) => {
277
+ contextSet(variables, keyOrVar, value);
278
+ }) as any,
279
+ params: matchedParams,
280
+ res: stubRes,
281
+ cookie: () => undefined,
282
+ cookies: () => ({}),
283
+ _readCookie: () => undefined,
284
+ _readCookies: () => ({}),
285
+ setCookie: () => {},
286
+ deleteCookie: () => {},
287
+ header: () => {},
288
+ setStatus: () => {},
289
+ _setStatus: () => {},
290
+ _rotateStateCookie: () => {},
291
+ _setKeepCacheDirective: () => {},
292
+ _setFreshReadsCookie: () => {},
293
+ use: (() => {
294
+ throw new Error("use() not available during pre-rendering");
295
+ }) as any,
296
+ method: "GET",
297
+ _handleStore: handleStore,
298
+ _requestTags: new Set<string>(),
299
+ waitUntil: () => {},
300
+ onResponse: () => {},
301
+ _onResponseCallbacks: [],
302
+ // No theme at build time: no request cookie.
303
+ _readTheme: () => undefined,
304
+ setLocationState() {},
305
+ _locationState: undefined,
306
+ _renderBarrier: Promise.resolve(),
307
+ _resolveRenderBarrier: () => {},
308
+ _reportedErrors: new WeakSet<object>(),
309
+ reverse: createReverseFunction(
310
+ deps.mergedRouteMap,
311
+ matched.routeKey,
312
+ matchedParams,
313
+ matched.routeKey
314
+ ? isRouteRootScoped(matched.routeKey, deps.routerId)
315
+ : undefined,
316
+ ),
317
+ // Scope the bake context's own lookups (createPrerenderContext reads it
318
+ // through the request ALS) per router as well.
319
+ _routerId: deps.routerId,
322
320
  };
323
- const serializedSegments = await serializeSegments(
324
- nonLoaderSegments,
325
- onFlightError,
326
- );
327
321
 
328
- // 13. Collect handle data per segment (skip segments with no handle data).
329
- // Encoded through the Flight codec (not stored raw) so Promise/ReactNode
330
- // handle values survive the JSON-serialized build artifact / dev wire —
331
- // the same fix the runtime cache uses. Encode happens here, in the RSC
332
- // environment where the codec resolves; the node-side sinks only persist.
333
- const handlesRecord: Record<string, SegmentHandleData> = {};
334
- for (const seg of nonLoaderSegments) {
335
- const segHandles = handleStore.getDataForSegment(seg.id);
336
- if (Object.keys(segHandles).length > 0) {
337
- // Resolve deferred values before encoding so the baked artifact holds
338
- // resolved data (prerender = build-time cache).
339
- handlesRecord[seg.id] = await resolveSegmentHandleValues(segHandles);
322
+ return runWithRequestContext(minimalRequestContext, async () => {
323
+ // 6. Create prerender context with synthetic URL.
324
+ // Prerender handlers get params, pathname, url, searchParams, search,
325
+ // reverse, use(handle), and optionally env (when buildEnv is configured).
326
+ const buildCtx = createPrerenderContext<TEnv>(
327
+ matchedParams,
328
+ pathname,
329
+ deps.mergedRouteMap,
330
+ matched.routeKey,
331
+ variables,
332
+ matchedPassthroughRoute,
333
+ buildEnv,
334
+ devMode,
335
+ onDemand,
336
+ );
337
+
338
+ // 7. Wire use() for handles only (loaders throw)
339
+ setupBuildUse(buildCtx);
340
+
341
+ // 8. Resolve all segments with skipLoaders
342
+ const loaderPromises = new Map<string, Promise<any>>();
343
+ const allSegments = await deps.resolveAllSegments(
344
+ entries,
345
+ matched.routeKey,
346
+ matchedParams,
347
+ buildCtx,
348
+ loaderPromises,
349
+ // throwOnError: a render failure (or `throw new Skip()`) must reach the
350
+ // build/dev caller, not be baked into a frozen error page (issue #587).
351
+ { skipLoaders: true, throwOnError: true },
352
+ );
353
+
354
+ // 9. Detect passthrough sentinel: handler returned ctx.passthrough().
355
+ // When the route declares loading(), the handler result is deferred so the
356
+ // component is a thenable resolving to the sentinel — detectPrerenderPassthrough
357
+ // resolves thenables before testing (a sync check would miss it and bake a
358
+ // corrupt artifact).
359
+ if (await detectPrerenderPassthrough(allSegments)) {
360
+ return { ...passthroughResult(), onDemandConfig };
340
361
  }
341
- }
342
- const handles = await encodeHandles(handlesRecord, onFlightError);
343
- // Before intercept resolution, as a handler throw would be.
344
- if (flightErrors.length > 0) throw flightErrors[0];
345
-
346
- // Use the trie-level route key (e.g., "docs", "docs.article")
347
- const routeName = matched.routeKey;
348
-
349
- // Surface the matched route entry's ppr option for the build collection
350
- // (leaf-first: the deepest type:"route" entry in the ancestor chain is
351
- // the matched page route; ancestors are layouts/includes).
352
- let routePpr: boolean | PartialPrerenderProps | undefined;
353
- for (let i = entries.length - 1; i >= 0; i--) {
354
- const e = entries[i]!;
355
- if (e.type === "route") {
356
- routePpr = e.ppr;
357
- break;
362
+
363
+ // 10. Filter out any loader segments (belt-and-suspenders)
364
+ const nonLoaderSegments = allSegments.filter(
365
+ (s) => s.type !== "loader",
366
+ );
367
+
368
+ // 11. Wait for handles to settle
369
+ handleStore.seal();
370
+ await handleStore.settled;
371
+
372
+ // 12. Serialize segments using the cache serializer.
373
+ // Flight encodes a component that throws (an async server component in
374
+ // the tree) as an error row and completes normally. Rethrown below like
375
+ // a handler throw, so the caller's policy decides instead of baking the
376
+ // error (#914). On-demand refresh: a deep async child calling
377
+ // cookies()/headers() throws here, not in resolveAllSegments; a
378
+ // personalization error is thrown in preference to any other so the
379
+ // trigger maps it to skipped-personalized and keeps the old entry.
380
+ const { serializeSegments } = await import("../cache/segment-codec.js");
381
+ const { encodeHandles } = await import("../cache/handle-snapshot.js");
382
+ const flightErrors: unknown[] = [];
383
+ const onFlightError = (error: unknown): void => {
384
+ flightErrors.push(error);
385
+ };
386
+ // The collected error to rethrow. On an on-demand refresh a failure
387
+ // goes ahead of a notFound(): a render that hit a missing item AND a
388
+ // failing data source must keep the page (render-failed), not remove
389
+ // it because the notFound() happened to be reported first.
390
+ const firstFlightError = (): unknown => {
391
+ const failure = onDemand
392
+ ? flightErrors.findIndex((e) => !isDataNotFoundError(e))
393
+ : -1;
394
+ return flightErrors[failure === -1 ? 0 : failure];
395
+ };
396
+ const serializedSegments = await serializeSegments(
397
+ nonLoaderSegments,
398
+ onFlightError,
399
+ );
400
+ const personalized = flightErrors.find((e) =>
401
+ isPrerenderPersonalizationError(e),
402
+ );
403
+ if (onDemand && personalized) {
404
+ throw personalized;
358
405
  }
359
- }
360
406
 
361
- // 14. Resolve intercept segments for this route (if any ancestor defines
362
- // an intercept targeting this route). At build time we skip when()
363
- // evaluation -- we pre-render all intercepts unconditionally and let
364
- // runtime matching decide which to serve.
365
- let interceptSegments: SerializedSegmentData[] | undefined;
366
- let interceptHandles: string | undefined;
367
-
368
- const foundIntercepts: {
369
- intercept: InterceptEntry;
370
- entry: EntryData;
371
- }[] = [];
372
- // The entry, then its orphans (nested ones included): the same traversal
373
- // findInterceptForRoute uses; the build keeps ALL matches (not just the
374
- // innermost) and skips when().
375
- const collect = (source: EntryData): undefined => {
376
- for (const ic of source.intercept) {
377
- if (ic.routeName === matched.routeKey) {
378
- foundIntercepts.push({ intercept: ic, entry: source });
407
+ // 13. Collect handle data per segment (skip segments with no handle data).
408
+ // Encoded through the Flight codec (not stored raw) so Promise/ReactNode
409
+ // handle values survive the JSON-serialized build artifact / dev wire —
410
+ // the same fix the runtime cache uses. Encode happens here, in the RSC
411
+ // environment where the codec resolves; the node-side sinks only persist.
412
+ const handlesRecord: Record<string, SegmentHandleData> = {};
413
+ for (const seg of nonLoaderSegments) {
414
+ const segHandles = handleStore.getDataForSegment(seg.id);
415
+ if (Object.keys(segHandles).length > 0) {
416
+ // Resolve deferred values before encoding so the baked artifact holds
417
+ // resolved data (prerender = build-time cache).
418
+ handlesRecord[seg.id] =
419
+ await resolveSegmentHandleValues(segHandles);
379
420
  }
380
421
  }
381
- return undefined;
382
- };
383
- let current: EntryData | null = manifestEntry;
384
- let prev: EntryData | null = null;
385
- while (current) {
386
- collect(current);
387
- findInOrphans(current, collect, prev);
388
- prev = current;
389
- current = current.parent;
390
- }
391
-
392
- if (foundIntercepts.length > 0) {
393
- const interceptResolvedSegments: typeof nonLoaderSegments = [];
394
-
395
- for (const { intercept, entry: parentEntry } of foundIntercepts) {
396
- // setupBuildUse keys handle pushes by ctx._currentSegmentId. The main
397
- // resolveAllSegments pass left it on the route's segment id, so pin it
398
- // to THIS intercept's slot id before resolving the handler/layout --
399
- // otherwise the intercept's ctx.use() handle pushes land in the wrong
400
- // bucket and getDataForSegment(seg.id) below drops them from the baked
401
- // artifact. Re-pinned per iteration so multiple intercepts targeting
402
- // the same route each get their own id (mirrors fresh.ts parallel-slot
403
- // pinning).
404
- const interceptSegmentId = `${parentEntry.shortCode}.${intercept.slotName}`;
405
- (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
406
- interceptSegmentId;
407
-
408
- // Resolve handler
409
- const handlerRaw =
410
- typeof intercept.handler === "function"
411
- ? intercept.handler(buildCtx)
412
- : intercept.handler;
413
- const handlerResolved =
414
- handlerRaw instanceof Promise ? await handlerRaw : handlerRaw;
415
- if (handlerResolved instanceof Response) {
416
- // Handler returned a redirect/response -- skip this intercept
417
- continue;
422
+ const handles = await encodeHandles(handlesRecord, onFlightError);
423
+ // Before intercept resolution, as a handler throw would be.
424
+ if (flightErrors.length > 0) throw firstFlightError();
425
+
426
+ // Use the trie-level route key (e.g., "docs", "docs.article")
427
+ const routeName = matched.routeKey;
428
+
429
+ // Surface the matched route entry's ppr option for the build collection
430
+ // (leaf-first: the deepest type:"route" entry in the ancestor chain is
431
+ // the matched page route; ancestors are layouts/includes).
432
+ let routePpr: boolean | PartialPrerenderProps | undefined;
433
+ for (let i = entries.length - 1; i >= 0; i--) {
434
+ const e = entries[i]!;
435
+ if (e.type === "route") {
436
+ // A durable segment refresh cannot atomically replace the separate
437
+ // document shell. Keep on-demand routes on axis 1 so a refreshed
438
+ // payload cannot be paired with a stale shell prelude.
439
+ routePpr = e.isOnDemand === true ? undefined : e.ppr;
440
+ break;
418
441
  }
419
- const component: ReactNode = handlerResolved;
420
-
421
- // Resolve layout (if any)
422
- let layoutElement: ReactNode | undefined;
423
- if (intercept.layout) {
424
- if (typeof intercept.layout === "function") {
425
- const layoutResult = await intercept.layout(buildCtx);
426
- if (layoutResult instanceof Response) continue;
427
- layoutElement = layoutResult;
428
- } else {
429
- layoutElement = intercept.layout;
442
+ }
443
+
444
+ // 14. Resolve intercept segments for this route (if any ancestor defines
445
+ // an intercept targeting this route). At build time we skip when()
446
+ // evaluation -- we pre-render all intercepts unconditionally and let
447
+ // runtime matching decide which to serve.
448
+ let interceptSegments: SerializedSegmentData[] | undefined;
449
+ let interceptHandles: string | undefined;
450
+
451
+ const foundIntercepts: {
452
+ intercept: InterceptEntry;
453
+ entry: EntryData;
454
+ }[] = [];
455
+ // The entry, then its orphans (nested ones included): the same traversal
456
+ // findInterceptForRoute uses; the build keeps ALL matches (not just the
457
+ // innermost) and skips when().
458
+ const collect = (source: EntryData): undefined => {
459
+ for (const ic of source.intercept) {
460
+ if (ic.routeName === matched.routeKey) {
461
+ foundIntercepts.push({ intercept: ic, entry: source });
430
462
  }
431
463
  }
432
-
433
- interceptResolvedSegments.push({
434
- id: interceptSegmentId,
435
- namespace: `intercept:${intercept.routeName}`,
436
- type: "parallel" as const,
437
- index: 0,
438
- component,
439
- loading: intercept.loading === false ? null : intercept.loading,
440
- layout: layoutElement,
441
- params: matchedParams,
442
- slot: intercept.slotName,
443
- belongsToRoute: true,
444
- parallelName: `intercept:${intercept.routeName}.${intercept.slotName}`,
445
- });
464
+ return undefined;
465
+ };
466
+ let current: EntryData | null = manifestEntry;
467
+ let prev: EntryData | null = null;
468
+ while (current) {
469
+ collect(current);
470
+ findInOrphans(current, collect, prev);
471
+ prev = current;
472
+ current = current.parent;
446
473
  }
447
474
 
448
- if (interceptResolvedSegments.length > 0) {
449
- // Wait for handles again (intercept handlers may have called use())
450
- await handleStore.settled;
451
- interceptSegments = await serializeSegments(
452
- interceptResolvedSegments,
453
- onFlightError,
454
- );
455
- const interceptHandlesRecord: Record<string, SegmentHandleData> = {};
456
- for (const seg of interceptResolvedSegments) {
457
- const segHandles = handleStore.getDataForSegment(seg.id);
458
- if (Object.keys(segHandles).length > 0) {
459
- interceptHandlesRecord[seg.id] =
460
- await resolveSegmentHandleValues(segHandles);
475
+ // V1 writes only the main variant. Do not execute intercept producers
476
+ // whose output would be discarded (and whose personalization/errors
477
+ // must not block an otherwise valid main refresh).
478
+ if (!onDemand && foundIntercepts.length > 0) {
479
+ const interceptResolvedSegments: typeof nonLoaderSegments = [];
480
+
481
+ for (const { intercept, entry: parentEntry } of foundIntercepts) {
482
+ // setupBuildUse keys handle pushes by ctx._currentSegmentId. The main
483
+ // resolveAllSegments pass left it on the route's segment id, so pin it
484
+ // to THIS intercept's slot id before resolving the handler/layout --
485
+ // otherwise the intercept's ctx.use() handle pushes land in the wrong
486
+ // bucket and getDataForSegment(seg.id) below drops them from the baked
487
+ // artifact. Re-pinned per iteration so multiple intercepts targeting
488
+ // the same route each get their own id (mirrors fresh.ts parallel-slot
489
+ // pinning).
490
+ const interceptSegmentId = `${parentEntry.shortCode}.${intercept.slotName}`;
491
+ (buildCtx as InternalHandlerContext<any, TEnv>)._currentSegmentId =
492
+ interceptSegmentId;
493
+
494
+ // Resolve handler
495
+ const handlerRaw =
496
+ typeof intercept.handler === "function"
497
+ ? intercept.handler(buildCtx)
498
+ : intercept.handler;
499
+ const handlerResolved =
500
+ handlerRaw instanceof Promise ? await handlerRaw : handlerRaw;
501
+ if (handlerResolved instanceof Response) {
502
+ // Handler returned a redirect/response -- skip this intercept
503
+ continue;
504
+ }
505
+ const component: ReactNode = handlerResolved;
506
+
507
+ // Resolve layout (if any)
508
+ let layoutElement: ReactNode | undefined;
509
+ if (intercept.layout) {
510
+ if (typeof intercept.layout === "function") {
511
+ const layoutResult = await intercept.layout(buildCtx);
512
+ if (layoutResult instanceof Response) continue;
513
+ layoutElement = layoutResult;
514
+ } else {
515
+ layoutElement = intercept.layout;
516
+ }
461
517
  }
518
+
519
+ interceptResolvedSegments.push({
520
+ id: interceptSegmentId,
521
+ namespace: `intercept:${intercept.routeName}`,
522
+ type: "parallel" as const,
523
+ index: 0,
524
+ component,
525
+ loading: intercept.loading === false ? null : intercept.loading,
526
+ layout: layoutElement,
527
+ params: matchedParams,
528
+ slot: intercept.slotName,
529
+ belongsToRoute: true,
530
+ parallelName: `intercept:${intercept.routeName}.${intercept.slotName}`,
531
+ });
462
532
  }
463
- // The intercept artifact serves main + intercept segments together, so
464
- // encode the MERGED handle map here (the sinks no longer merge raw
465
- // records — they store this pre-encoded string as-is).
466
- interceptHandles = await encodeHandles(
467
- {
468
- ...handlesRecord,
469
- ...interceptHandlesRecord,
470
- },
471
- onFlightError,
472
- );
473
- }
474
- }
475
533
 
476
- if (flightErrors.length > 0) throw flightErrors[0];
534
+ if (interceptResolvedSegments.length > 0) {
535
+ // Wait for handles again (intercept handlers may have called use())
536
+ await handleStore.settled;
537
+ interceptSegments = await serializeSegments(
538
+ interceptResolvedSegments,
539
+ onFlightError,
540
+ );
541
+ const interceptHandlesRecord: Record<string, SegmentHandleData> =
542
+ {};
543
+ for (const seg of interceptResolvedSegments) {
544
+ const segHandles = handleStore.getDataForSegment(seg.id);
545
+ if (Object.keys(segHandles).length > 0) {
546
+ interceptHandlesRecord[seg.id] =
547
+ await resolveSegmentHandleValues(segHandles);
548
+ }
549
+ }
550
+ // The intercept artifact serves main + intercept segments together, so
551
+ // encode the MERGED handle map here (the sinks no longer merge raw
552
+ // records — they store this pre-encoded string as-is).
553
+ interceptHandles = await encodeHandles(
554
+ {
555
+ ...handlesRecord,
556
+ ...interceptHandlesRecord,
557
+ },
558
+ onFlightError,
559
+ );
560
+ }
561
+ }
477
562
 
478
- return {
479
- segments: serializedSegments,
480
- handles,
481
- routeName,
482
- params: matchedParams,
483
- interceptSegments,
484
- interceptHandles,
485
- ppr: routePpr,
486
- };
487
- });
488
- });
563
+ if (flightErrors.length > 0) throw firstFlightError();
564
+
565
+ return {
566
+ segments: serializedSegments,
567
+ handles,
568
+ routeName,
569
+ params: matchedParams,
570
+ interceptSegments,
571
+ interceptHandles,
572
+ onDemandConfig,
573
+ ppr: routePpr,
574
+ };
575
+ });
576
+ }),
577
+ );
578
+ // A build or dev render keeps the throw (the caller's onError policy).
579
+ if (!onDemand) return rendered;
580
+ // An on-demand refresh reports a notFound() anywhere in the render (a
581
+ // handler's throw, or the only kind of error Flight collected) as an
582
+ // outcome, with the route's onDemand config: the trigger stores the
583
+ // "removed" marker stamped like a page (create-prerender-trigger.ts).
584
+ try {
585
+ return await rendered;
586
+ } catch (error) {
587
+ if (!isDataNotFoundError(error)) throw error;
588
+ return {
589
+ segments: [],
590
+ handles: "",
591
+ routeName: matched.routeKey,
592
+ params: matchedParams,
593
+ notFound: true,
594
+ onDemandConfig,
595
+ };
596
+ }
489
597
  }
490
598
 
491
599
  /**