@wular/pnext 0.0.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 (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,1903 @@
1
+ // Segment-prefetch responder registration (COMPAT - may import core freely). Registers:
2
+ // - a request interceptor that answers `/_tree` segment-prefetch requests with the minimal
3
+ // RootTreePrefetch payload, Content-Type text/x-component, x-nextjs-stale-time and
4
+ // cache-control private,no-store; the router Vary is merged by the protocol finalizer.
5
+ // - a response finalizer that stamps x-nextjs-stale-time on ordinary prefetch responses that did
6
+ // not already carry one from `use cache`, so the client segment cache learns the reuse window.
7
+ //
8
+ // The interceptor short-circuits BEFORE route matching / static lookup: a segment prefetch never
9
+ // renders a page, it only announces the route tree + staleness. It runs first among compat
10
+ // interceptors so an action POST never mistakes a `/_tree` GET.
11
+ //
12
+ // `_rsc` cache-buster handling: an RSC/prefetch request carries a `?_rsc=<hash>` param the CDN keys
13
+ // on. The interceptor accepts any value and never treats it as an app search param.
14
+ //
15
+ // No-op when compat is off. The `/_tree` sentinel only fires when the client segment-cache runtime
16
+ // sends it; the HTML-swap navigation path is untouched.
17
+
18
+ import type { ResolvedConfig } from '../../config';
19
+ import type { RouteManifestEntry } from '../../types';
20
+ import { nextCompatEnabled } from '../../compat';
21
+ import {
22
+ getRenderExtensions,
23
+ onExtensionHostReset,
24
+ getRequestExtensions,
25
+ registerRequestInterceptors,
26
+ registerResponseFinalizers,
27
+ type RequestInterceptor,
28
+ type ResponseFinalizer,
29
+ type ResponseFinalizerContext,
30
+ withRouteRuntime,
31
+ } from '../../extensions';
32
+ import { getRequestRuntime } from '../../routing/request-runtime';
33
+ import { takeCacheLifeStash } from '../cache/use-cache';
34
+ import { matchInterception, parseNavState, selectRouteForRequest } from '../../routing/routes';
35
+ import {
36
+ currentPprShellHtml,
37
+ pprShellPath,
38
+ renderPageResponse,
39
+ renderPartialShell,
40
+ renderRuntimePrefetchDocument,
41
+ schedulePprShellUpgrade,
42
+ withRequestRouteParams,
43
+ LATE_METADATA_MARKER,
44
+ } from '../../render/renderer';
45
+ import {
46
+ NEXT_ROUTER_PREFETCH_HEADER,
47
+ NEXT_ROUTER_SEGMENT_PREFETCH_HEADER,
48
+ RSC_HEADER,
49
+ RSC_CONTENT_TYPE_HEADER,
50
+ } from '../next/dist/client/components/app-router-headers';
51
+ import {
52
+ bodySegmentFile,
53
+ buildRootTreePrefetch,
54
+ DEFAULT_DYNAMIC_STALE_TIME_SECONDS,
55
+ DEFAULT_STATIC_STALE_TIME_SECONDS,
56
+ segmentMetaFile,
57
+ segmentBodyResponse,
58
+ ROUTE_SEGMENT_PATH,
59
+ TREE_SEGMENT_PATH,
60
+ treeSegmentFile,
61
+ treePrefetchResponse,
62
+ type SegmentAppShell,
63
+ type SegmentMeta,
64
+ type SegmentVaryInfo,
65
+ } from '../segment/tree';
66
+ import {
67
+ responseVaryParamsFromWire,
68
+ varyNamesFor,
69
+ withVaryParamsTracking,
70
+ type ResponseVaryParams,
71
+ } from '../segment/vary-params';
72
+ import { pageHasInPageSuspense } from '../segment/loading-boundary';
73
+ import { pageSlotFrame, stripPageSlotContent } from '../segment/page-slot';
74
+ import { withoutInlineCss } from '../css/inline-css';
75
+ import { getNextConfig } from '../next/config-loader';
76
+ import { CACHE_BUSTING_REDIRECT_HEADER } from '../protocol';
77
+ import { registerRevalidationInvalidator } from '../cache/revalidate';
78
+ import { staticSiblingNames } from '../next/optimistic-route-state';
79
+ import {
80
+ loadCompatRewrites,
81
+ resolveCompatRewrite,
82
+ type CompatRewrite,
83
+ } from '../next/rewrites';
84
+ import { existsSync, readFileSync, statSync } from 'node:fs';
85
+ import { join } from 'node:path';
86
+ import { writeFile } from 'node:fs/promises';
87
+
88
+ /** experimental.staleTimes.{static,dynamic} from next.config (seconds). */
89
+ function configuredStaleTimes(): { static?: number; dynamic?: number } {
90
+ const experimental = getNextConfig().experimental;
91
+ if (!experimental || typeof experimental !== 'object') return {};
92
+ const staleTimes = (experimental as { staleTimes?: unknown }).staleTimes;
93
+ if (!staleTimes || typeof staleTimes !== 'object') return {};
94
+ const value = staleTimes as { static?: unknown; dynamic?: unknown };
95
+ return {
96
+ ...(typeof value.static === 'number' ? { static: value.static } : {}),
97
+ ...(typeof value.dynamic === 'number' ? { dynamic: value.dynamic } : {}),
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Partial Prefetching: a FULL (`<Link prefetch={true}>`) prefetch is downgraded to a partial/PPR
103
+ * prefetch - the static shell only, never the dynamic data - when the app opts in globally
104
+ * (`partialPrefetching`) or the target route opts in per segment (`export const prefetch =
105
+ * 'partial'`, resolved through the route's whole segment chain so a nested leaf's opt-in reaches
106
+ * the route the scheduler asks about). `'unstable_eager'` is the opposite policy and never downgrades.
107
+ */
108
+ function routePartialPrefetch(route: RouteManifestEntry): boolean {
109
+ // ...EXCEPT under App Shells, where `prefetch = 'partial'` means "rely on the shared shell for the
110
+ // DEFAULT prefetch". An explicit `prefetch={true}` link opts back into per-link prefetching there, so
111
+ // its full prefetch must carry this param's own content.
112
+ if (route.segmentConfig?.prefetch === 'partial') return !appShellsEnabled();
113
+ return (getNextConfig() as { partialPrefetching?: unknown }).partialPrefetching === true;
114
+ }
115
+
116
+ /** `experimental.appShells` from next.config. */
117
+ function appShellsEnabled(): boolean {
118
+ const experimental = getNextConfig().experimental;
119
+ if (!experimental || typeof experimental !== 'object') return false;
120
+ return (experimental as { appShells?: unknown }).appShells === true;
121
+ }
122
+
123
+ /**
124
+ * App Shells: the body prefetch of a runtime-prefetch route serves the SHARED (param-independent)
125
+ * shell, so the render must leave `params` HANGING - the params-dependent subtree stays behind its
126
+ * <Suspense> fallback and the client caches the response at the fallback vary path, reusable for
127
+ * every param. Request data (cookies/headers) still resolves: it is not URL-derived.
128
+ *
129
+ * Strictly opt-in. WITHOUT `appShells` a runtime prefetch is per-URL and its params MUST resolve.
130
+ * An `unstable_eager` segment is excluded even under App Shells: eager routes keep their per-link
131
+ * Speculative prefetch, which carries that param's own content.
132
+ */
133
+ function appShellFallbackParams(route: RouteManifestEntry): boolean {
134
+ if (!appShellsEnabled()) return false;
135
+ return route.segmentConfig?.prefetch !== 'unstable_eager';
136
+ }
137
+
138
+ function validateRscRequestHeaders(): boolean {
139
+ const experimental = getNextConfig().experimental;
140
+ if (!experimental || typeof experimental !== 'object') return false;
141
+ return (experimental as { validateRSCRequestHeaders?: unknown }).validateRSCRequestHeaders === true;
142
+ }
143
+
144
+ function deploymentId(): string {
145
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
146
+ return process.env.NEXT_DEPLOYMENT_ID || process.env.NEXT_PUBLIC_BUILD_ID || 'pnext';
147
+ }
148
+
149
+ /** Effective staleTime (seconds) for a route of the given disposition. */
150
+ function staleTimeSecondsFor(isStatic: boolean): number {
151
+ const configured = configuredStaleTimes();
152
+ if (isStatic) return configured.static ?? DEFAULT_STATIC_STALE_TIME_SECONDS;
153
+ return configured.dynamic ?? DEFAULT_DYNAMIC_STALE_TIME_SECONDS;
154
+ }
155
+
156
+ /** The segment-prefetch header value, or null when this isn't a segment prefetch. */
157
+ function segmentPrefetchTarget(request: Request): string | null {
158
+ if (request.headers.get(RSC_HEADER) !== '1') return null;
159
+ return request.headers.get(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER);
160
+ }
161
+
162
+ let segmentRevalidationVersion = 0;
163
+ const segmentArtifactVersions = new Map<string, number>();
164
+
165
+ // NOTE: there is deliberately NO server-side "the client's `_rsc` predates the last revalidation,
166
+ // bounce it to a fresh CDN key" redirect. Next never issues one: `_rsc` is derived from the
167
+ // request's negotiation headers, and freshness after a revalidation comes from the server
168
+ // re-rendering the stale artifact, not from rotating the URL. A version bounce here would leak
169
+ // server state ACROSS browser sessions, so every page load after a revalidation replays a hash the
170
+ // server has already seen at an older version and its first prefetch answers 302 instead of the
171
+ // payload - which router-act cannot intercept.
172
+
173
+ /**
174
+ * A cache-busting redirect to a per-variant `_rsc` URL, byte-for-byte Next's wire form (307 + a
175
+ * PATH-RELATIVE Location):
176
+ *
177
+ * - 307, not 302. A prefetch that lands on a 302 is invisible to the e2e router-act helper, which
178
+ * only follows 307/308.
179
+ * - Relative Location. An absolute URL is built from the URL the ORIGIN server sees, which behind a
180
+ * proxy/CDN is its internal port - following it would bypass the CDN the client is talking to.
181
+ *
182
+ * It is CACHEABLE (no `cache-control: no-store`): the Location already folds the request's
183
+ * negotiation headers into its `_rsc` token, so a Vary-ignoring CDN that replays the cached redirect
184
+ * still sends each variant to its own URL. Marking it no-store instead broke a reference CDN, which
185
+ * drops the redirect status+Location for non-cacheable responses.
186
+ */
187
+ function cacheBustingRedirect(location: string): Response {
188
+ const target = new URL(location);
189
+ return new Response(null, {
190
+ status: 307,
191
+ headers: {
192
+ location: `${target.pathname}${target.search}`,
193
+ // Protocol marker: this 307 is a CDN key rotation, not an app redirect()
194
+ // — the finalizer must not fold it into Next's 200+Location RSC form
195
+ // (see normalizeRscRedirectStatus, which strips this header).
196
+ [CACHE_BUSTING_REDIRECT_HEADER]: '1',
197
+ },
198
+ });
199
+ }
200
+
201
+ const segmentPrefetchInterceptor: RequestInterceptor = async (request, ctx) => {
202
+ const config = ctx.config;
203
+ if (!nextCompatEnabled(config)) return undefined;
204
+ const segment = segmentPrefetchTarget(request);
205
+ const isRscRequest = request.headers.get(RSC_HEADER) === '1';
206
+ if (segment === null && !isRscRequest) return undefined;
207
+ const method = request.method.toUpperCase();
208
+ if (method !== 'GET' && method !== 'HEAD') return undefined;
209
+ if (segment === null && request.headers.has('x-pnext-soft-nav')) return undefined;
210
+
211
+ const runtime = getRequestRuntime();
212
+ if (!runtime) return undefined;
213
+
214
+ const url = new URL(request.url);
215
+ if (
216
+ segment !== null &&
217
+ validateRscRequestHeaders() &&
218
+ !url.searchParams.has('_rsc')
219
+ ) {
220
+ // Redirect a header-only (no `_rsc`) prefetch to its per-variant cache-
221
+ // busting URL. The Location folds this request's negotiation headers into
222
+ // the `_rsc` token so distinct variants resolve to distinct URLs.
223
+ return cacheBustingRedirect(withRscQuery(url, request.headers));
224
+ }
225
+ // next.config `rewrites` must apply on the RSC/segment serving path too. This
226
+ // interceptor short-circuits BEFORE the register-actions rewrite interceptor
227
+ // runs, so config rewrites (and their x-nextjs-rewritten-path/query headers)
228
+ // would never fire on RSC requests without resolving them here. When a rewrite
229
+ // matches we serve the destination route and stamp the rewritten headers
230
+ // directly (interceptor responses bypass the protocol finalizer). When it does
231
+ // NOT match we fall through unchanged, and the register-actions interceptor
232
+ // handles the full-render path as before.
233
+ const rewrite = await resolveSegmentRewrite(config, request, url);
234
+ const matchPathname = rewrite?.pathname ?? url.pathname;
235
+ const rewriteHeaders = rewrite?.headers;
236
+
237
+ // The `_rsc` cache-buster is a CDN key, not an app param: it never reaches
238
+ // route matching.
239
+ const selection = selectRouteForRequest(runtime.routes, matchPathname, undefined);
240
+
241
+ // A pnext-router prefetch carries the origin's nav state. When an
242
+ // interception entry targets this URL from that origin, the segment/RSC
243
+ // fast path must NOT serve: it renders without nav context, so the cached
244
+ // document would lack the intercepted slot content and the navigation that
245
+ // reuses it would commit the un-intercepted page (modal suites regressed
246
+ // exactly this way). Fall through to the host-render path instead.
247
+ const prefetchNav = parseNavState(request);
248
+ if (
249
+ prefetchNav &&
250
+ ((prefetchNav.children &&
251
+ matchInterception(runtime.routes, matchPathname, prefetchNav.children)) ||
252
+ (selection?.route.kind === 'page' &&
253
+ (selection.route.slotDirs?.length || selection.route.synthetic)))
254
+ ) {
255
+ return undefined;
256
+ }
257
+ const isStatic = selection?.route.kind === 'page' && selection.route.mode === 'static';
258
+
259
+ if (segment === TREE_SEGMENT_PATH && selection?.route.kind === 'page' && selection.route.ppr) {
260
+ schedulePprShellUpgrade({ config, route: selection.route, url: new URL(request.url) });
261
+ }
262
+
263
+ if (segment === null) {
264
+ if (selection?.route.kind !== 'page') return undefined;
265
+ const meta = readSegmentMeta(config.outPath, selection.route.id);
266
+ // A PREFETCH (`next-router-prefetch: 1`) is answered with the cheap route-metadata payload - it only
267
+ // has to teach the client the route's identity and staleness window. A plain `rsc: 1` fetch is the
268
+ // full-route flight request Next answers with the route's rendered content, so render the page for
269
+ // those the same way the PPR branch does.
270
+ const isPrefetchRequest = request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1';
271
+ if (selection.route.ppr || !isPrefetchRequest) {
272
+ const response = await getRenderExtensions().collectRenderMeta(
273
+ () =>
274
+ // A flight body carries stylesheet REFERENCES, never inlined CSS
275
+ // (`experimental.inlineCss` is a document-only optimization) — see
276
+ // compat/css/inline-css.ts.
277
+ withoutInlineCss(() =>
278
+ withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
279
+ renderPageResponse({
280
+ config,
281
+ route: selection.route,
282
+ params: selection.params,
283
+ url,
284
+ request,
285
+ }),
286
+ ),
287
+ ),
288
+ {
289
+ fetchCache: selection.route.segmentConfig?.fetchCache,
290
+ route: matchPathname,
291
+ },
292
+ );
293
+ // pnext's full-route RSC body is the rendered document (HTML-swap
294
+ // protocol), but a flight response must never LOOK like a document: the
295
+ // root-param fallback suites assert the body of an `RSC: 1` fetch does
296
+ // not contain `<!DOCTYPE html>` (while resume-data-cache asserts it DOES
297
+ // carry the page's content). Strip the doctype prefix and append the
298
+ // route's flight segment marker (Next's payload names the leaf segment
299
+ // `__PAGE__`, which the inline-css suite sanity-checks for); this path
300
+ // never serves the pnext client router (x-pnext-soft-nav bails out above).
301
+ const rscBody = stripDocumentDoctype(
302
+ response.value,
303
+ flightSegmentMarker(selection.route.id, isStatic),
304
+ );
305
+ rscBody.headers.set('content-type', RSC_CONTENT_TYPE_HEADER);
306
+ rscBody.headers.set('x-nextjs-stale-time', String(effectiveStaleTime(meta, isStatic)));
307
+ rscBody.headers.set('x-nextjs-deployment-id', deploymentId());
308
+ rscBody.headers.set('cache-control', 'private, no-store');
309
+ return withRewriteHeaders(rscBody, rewriteHeaders);
310
+ }
311
+ return withRewriteHeaders(
312
+ fullRouteRscResponse(
313
+ selection.route.id,
314
+ isStatic,
315
+ effectiveStaleTime(meta, isStatic),
316
+ routeStaticChildren(runtime.routes, selection.route),
317
+ ),
318
+ rewriteHeaders,
319
+ );
320
+ }
321
+
322
+ // Beyond `/_tree`: serve a framed segment payload. A segment request must
323
+ // never fall through to the document/static-file path, which would pair
324
+ // text/x-component headers with a full HTML document.
325
+ if (segment !== TREE_SEGMENT_PATH) {
326
+ if (selection?.route.kind !== 'page') {
327
+ return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
328
+ }
329
+ const routeId = selection.route.id;
330
+ const file = bodySegmentFile(config.outPath, routeId);
331
+ const meta = readSegmentMeta(config.outPath, routeId);
332
+ // `/_layout`: w9-segment-split. The client holds a cached PAGE frame for
333
+ // this URL (its vary set does not contain the param that changed) but its
334
+ // LAYOUT frame missed, so it asks for the layout alone. Render the route and
335
+ // cut the page's markup out of the document: the response carries the layout
336
+ // chain only, keyed on the LAYOUT vary set, and the client splices its
337
+ // cached page frame back in (compat/segment/page-slot.ts).
338
+ if (segment === LAYOUT_SEGMENT_PATH) {
339
+ const rendered = await renderRuntimeSegment({
340
+ config,
341
+ selection,
342
+ pathname: matchPathname,
343
+ url,
344
+ request,
345
+ // NOT truncated at the loading boundary. A truncated render serves the route's baked fallback
346
+ // shell, whose params HANG - the layout never reads them, so the response would publish an
347
+ // EMPTY vary set and the client would share one layout frame across every param value. The
348
+ // page's markup is cut out of the finished document instead, which costs a full render but
349
+ // keeps the layout's vary set honest.
350
+ nav: prefetchNav,
351
+ });
352
+ if (!rendered) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
353
+ return withRewriteHeaders(
354
+ segmentBodyResponse(
355
+ stripPageSlotContent(rendered.body),
356
+ effectiveStaleTime(meta, true),
357
+ // A layout frame is never the whole page: the navigation still needs
358
+ // the page frame (from cache, or from its own dynamic request).
359
+ true,
360
+ isStatic,
361
+ deploymentId(),
362
+ segment,
363
+ // An EMPTY tracked set on a route that HAS params means the render read none of them -
364
+ // either true (a param-independent layout) or an artifact of the document coming back from
365
+ // a prerender cache. The two are indistinguishable here and the failure modes are not
366
+ // symmetric: over-varying costs a cache hit, under-varying serves one param's layout for
367
+ // another. So publish NO vary set and let the client key the frame on its exact URL.
368
+ varyTrusted(rendered.vary, selection, 'layout')
369
+ ? segmentVaryInfo(rendered.vary, 'layout', selection)
370
+ : undefined,
371
+ ),
372
+ rewriteHeaders,
373
+ );
374
+ }
375
+ // `/_page`: the inverse frame of `/_layout`. The client already renders this route's layout chain
376
+ // - it is navigating WITHIN it, or holds a valid cached layout frame - so it asks for the page
377
+ // slot alone. The response carries the slot markers plus the page's own markup and NOTHING above
378
+ // them. Rendered like `/_layout`: NOT truncated at the loading boundary, so the tracked vary set
379
+ // is the page's honest param access.
380
+ if (segment === PAGE_SEGMENT_PATH) {
381
+ const rendered = await renderRuntimeSegment({
382
+ config,
383
+ selection,
384
+ pathname: matchPathname,
385
+ url,
386
+ request,
387
+ nav: prefetchNav,
388
+ });
389
+ if (!rendered) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
390
+ const frame = pageSlotFrame(rendered.body);
391
+ // No page slot (a document whose renderer dropped the markers, a global
392
+ // error page): the frame cannot be proven to line up, so answer a MISS and
393
+ // let the client hard-fall-back to the whole document.
394
+ if (frame === null) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
395
+ return withRewriteHeaders(
396
+ segmentBodyResponse(
397
+ frame,
398
+ // Rendered for the NAVIGATION: a dynamic route's frame carries
399
+ // request data, so it reuses for the dynamic window only.
400
+ effectiveStaleTime(meta, isStatic),
401
+ // A page frame is never the whole document: whatever consumes it still
402
+ // owns the layout chain around it.
403
+ true,
404
+ isStatic,
405
+ deploymentId(),
406
+ segment,
407
+ varyTrusted(rendered.vary, selection, 'page')
408
+ ? segmentVaryInfo(rendered.vary, 'page', selection)
409
+ : undefined,
410
+ ),
411
+ rewriteHeaders,
412
+ );
413
+ }
414
+ // App Shells, second request: the client asks for the route's SHARED shell explicitly. Unlike the
415
+ // runtime-prefetch branch below - which only fires for `allow-runtime`/`unstable_instant` routes -
416
+ // this serves a FULLY STATIC route's param-independent shell too: `params` hang, so the
417
+ // params-dependent subtree stays behind its fallback and the response's empty vary set files it at
418
+ // the fallback vary path. Strictly opt-in: without `experimental.appShells` the marker is ignored.
419
+ if (
420
+ (segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
421
+ appShellsEnabled() &&
422
+ request.headers.get(APP_SHELL_PREFETCH_HEADER) === '1'
423
+ ) {
424
+ const shell = await renderInstantPrefetchShell({
425
+ config,
426
+ selection,
427
+ pathname: matchPathname,
428
+ url,
429
+ request,
430
+ fallbackParams: true,
431
+ });
432
+ if (!shell) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
433
+ return withRewriteHeaders(
434
+ segmentBodyResponse(
435
+ shell.html,
436
+ effectiveStaleTime(meta, true),
437
+ true,
438
+ // A shared shell is a pure function of the ROUTE (its params hung),
439
+ // so the client may serve it for any URL of that route — which its
440
+ // reuse policy allows only for static payloads.
441
+ isStatic || (selection.route.hasStaticParams === true && selection.route.usesRequest !== true),
442
+ deploymentId(),
443
+ segment,
444
+ segmentVaryInfo(shell.vary, 'body', selection),
445
+ ),
446
+ rewriteHeaders,
447
+ );
448
+ }
449
+ // A FULL prefetch of a Partial-Prefetching route is served exactly like a
450
+ // default (partial) one — static shell, dynamic boundaries left as holes —
451
+ // and marked so the requesting client still treats it as a shell (the
452
+ // navigation fetches the dynamic continuation).
453
+ const partialPrefetchDowngrade =
454
+ request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) !== '1' &&
455
+ routePartialPrefetch(selection.route);
456
+ const partialPrefetch =
457
+ request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1' || partialPrefetchDowngrade;
458
+ const runtimePrefetch = routeUsesRuntimePrefetch(selection.route);
459
+ // Partial Prefetching under App Shells: a route declaring `export const prefetch = 'partial'`
460
+ // relies on its SHARED app shell, so the per-link Speculative prefetch must not additionally fetch
461
+ // that param's own content. Answering the per-link default prefetch with the shared shell (params
462
+ // hang, empty vary set) collapses two requests into the one that carries the shell, and the empty
463
+ // vary set files the entry at the fallback vary path so revealing another param needs nothing from
464
+ // the server.
465
+ //
466
+ // Strictly scoped: `experimental.appShells` plus the segment's OWN `prefetch = 'partial'` opt-in.
467
+ // The global `partialPrefetching` config and `unstable_eager` are deliberately excluded.
468
+ if (
469
+ (segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
470
+ request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1' &&
471
+ appShellsEnabled() &&
472
+ selection.route.segmentConfig?.prefetch === 'partial'
473
+ ) {
474
+ const shell = await renderInstantPrefetchShell({
475
+ config,
476
+ selection,
477
+ pathname: matchPathname,
478
+ url,
479
+ request,
480
+ fallbackParams: true,
481
+ });
482
+ if (shell) {
483
+ return withRewriteHeaders(
484
+ segmentBodyResponse(
485
+ shell.html,
486
+ effectiveStaleTime(meta, true),
487
+ // A shell, not the whole page: the navigation still fetches the
488
+ // param-specific continuation behind the boundary.
489
+ true,
490
+ // A pure function of the ROUTE (its params hung), so the client may
491
+ // paint it for any URL of that route — which its reuse policy allows
492
+ // only for payloads marked static.
493
+ true,
494
+ deploymentId(),
495
+ segment,
496
+ segmentVaryInfo(shell.vary, 'body', selection),
497
+ ),
498
+ rewriteHeaders,
499
+ );
500
+ }
501
+ }
502
+ // A route that opts into runtime prefetching (`unstable_instant` or `export const prefetch =
503
+ // 'allow-runtime'`) renders its BODY prefetch as a RUNTIME-PREFETCH prerender: the response
504
+ // samples request data while connection()-gated content is omitted, and nothing from this
505
+ // request-sampled render is persisted. The client caches it as a shell, so the navigation still
506
+ // fetches the dynamic continuation.
507
+ //
508
+ // The selection is the ROUTE's opt-in, NOT the prefetch header: the client ALWAYS sends
509
+ // `next-router-prefetch: 1`, so gating on the header would never let the shell render. It stays
510
+ // scoped to the body segment and yields to an explicit partial-prefetch (static) downgrade.
511
+ if (
512
+ // The client sends '/_index' as the body-segment key (router.ts
513
+ // SEGMENT_PREFETCH_HEADER); ROUTE_SEGMENT_PATH ('/') covers direct/legacy
514
+ // callers. Both mean the whole-route body.
515
+ (segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
516
+ !partialPrefetchDowngrade &&
517
+ routeRuntimePrefetchRender(selection.route)
518
+ ) {
519
+ const fallbackParams = appShellFallbackParams(selection.route);
520
+ const instant = await renderInstantPrefetchShell({
521
+ config,
522
+ selection,
523
+ pathname: matchPathname,
524
+ url,
525
+ request,
526
+ fallbackParams,
527
+ });
528
+ if (instant) {
529
+ // A `pprMetadata` route's <title> is DYNAMIC (generateMetadata reads a param) while this shared
530
+ // shell body legitimately varies on nothing: leaving the title inside it would under-vary the body
531
+ // on its own head and serve one slug's title for another. Outline it here too - the client
532
+ // re-fetches `/_head` per URL - exactly as the baked/full prefetch path below does.
533
+ let instantHtml = instant.html;
534
+ let instantHeadOutlined = false;
535
+ if (selection.route.pprMetadata === true) {
536
+ const outlined = outlineDocumentTitle(instantHtml);
537
+ if (outlined) {
538
+ instantHtml = outlined;
539
+ instantHeadOutlined = true;
540
+ }
541
+ }
542
+ // This response owns its window (computed just below from what the
543
+ // sampled render really resolved). Drop the request-level `use cache`
544
+ // aggregate so the header finalizer does not overwrite it with the
545
+ // route's longer public-cache window.
546
+ takeCacheLifeStash();
547
+ const response = segmentBodyResponse(
548
+ instantHtml,
549
+ // A request-sampled payload reuses for the SHORTEST window it sampled,
550
+ // never the route's baked (public-cache) one: the private caches whose
551
+ // values ride in this response go stale first.
552
+ instant.staleSeconds !== undefined
553
+ ? Math.min(instant.staleSeconds, effectiveStaleTime(meta, true))
554
+ : effectiveStaleTime(meta, true),
555
+ true,
556
+ isStatic,
557
+ deploymentId(),
558
+ segment,
559
+ varyTrusted(
560
+ instant.vary,
561
+ selection,
562
+ 'body',
563
+ fallbackParams,
564
+ // This render always happened LIVE, so an explicit `allow-runtime`
565
+ // route's empty set is the truth: nothing was read before the
566
+ // render postponed, and every param shares one loading shell.
567
+ routeExplicitRuntimePrefetch(selection.route),
568
+ )
569
+ ? segmentVaryInfo(instant.vary, 'body', selection)
570
+ : undefined,
571
+ );
572
+ // Dedicated marker for the client: ONLY a runtime-prefetch (instant)
573
+ // shell downgrades a full prefetch to shell semantics. The generic
574
+ // postponed flag also rides ordinary PPR full renders, which stream
575
+ // their resumed content and MUST keep committing without a refetch
576
+ // (app-client-cache full-prefetch contract).
577
+ response.headers.set('x-pnext-runtime-prefetch', '1');
578
+ if (instantHeadOutlined) {
579
+ response.headers.set(
580
+ 'x-pnext-head-outlined',
581
+ headFetchedFirst(selection) ? 'first' : '1',
582
+ );
583
+ }
584
+ // A COMPLETE runtime prefetch (the render never postponed - the page reads only cookies/params and
585
+ // does no uncached IO) lets the client commit the following navigation network-free. The response
586
+ // itself stays `postponed` on the wire: it is request-sampled, so it must never become
587
+ // CDN-cacheable nor commit through the whole-document caches.
588
+ if (!instant.postponed) response.headers.set('x-pnext-runtime-complete', '1');
589
+ return withRewriteHeaders(response, rewriteHeaders);
590
+ }
591
+ }
592
+ let body =
593
+ partialPrefetch && (meta?.postponed !== true || !runtimePrefetch)
594
+ ? await readBakedSegment({ config, selection, pathname: matchPathname, file })
595
+ : undefined;
596
+ const baked = body !== undefined;
597
+ // A default (partial) prefetch of a dynamic page with no baked segment must NOT fall through to a
598
+ // live render: Next answers such prefetches from static data only, and a hanging dynamic render
599
+ // would pin the prefetch connection open forever. Routes that opt into runtime prefetching keep
600
+ // the live render. Exception: a pure params-derived route (generateStaticParams, no request data)
601
+ // IS static content and its live render cannot hang, so it keeps the full render - it can lack a
602
+ // baked segment when its fallback shell failed to bake.
603
+ const paramsOnlyStatic =
604
+ (selection.route.hasStaticParams === true && selection.route.usesRequest !== true) ||
605
+ clientStaticPrefetchable(selection.route);
606
+ // A route with a LOADING boundary renders a truncated loading shell on a default prefetch (Next's
607
+ // "show layout eagerly with loading one level down") - the render stops at the boundary, so it never
608
+ // hangs. Only a dynamic route with NO loading/Suspense boundary would pin the connection open, so the
609
+ // miss short-circuit applies just to those.
610
+ const hasLoadingShell = routeHasLoadingBoundary(selection.route);
611
+ // A route the build already prerendered for THIS param set (generateStaticParams)
612
+ // has a known-good static shell: the prefetch renders that shell truncated at
613
+ // its Suspense boundary (dynamic content stays a hole), so it cannot hang even
614
+ // though the route reads request data (connection()) inside the boundary.
615
+ const paramPrerendered = routeParamPrerendered(selection.route, selection.params);
616
+ // A params-derived route normally serves its complete content on a default prefetch - but only
617
+ // when the build actually knows that content. When THIS param set was never enumerated by
618
+ // generateStaticParams and the route has a `loading` boundary, its page is dynamic for this URL
619
+ // and the default prefetch stops at the loading shell. Truncating there also keeps the published
620
+ // vary set to what the shell really read, so every itemId of a category shares one entry.
621
+ const truncateAtLoading = routeHasLoadingFile(selection.route) && !paramPrerendered;
622
+ // A fully STATIC route with no baked segment (no cacheComponents build)
623
+ // also live-renders: its content is static data Next serves complete on a
624
+ // default prefetch, and the render cannot hang.
625
+ if (
626
+ partialPrefetch &&
627
+ !runtimePrefetch &&
628
+ body === undefined &&
629
+ !paramsOnlyStatic &&
630
+ !paramPrerendered &&
631
+ !isStatic &&
632
+ !hasLoadingShell
633
+ ) {
634
+ return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
635
+ }
636
+ // Segment-M2 deliverable 4: exactly the routes the miss short-circuit above
637
+ // used to swallow, now kept alive by an IN-PAGE <Suspense> boundary. Next
638
+ // answers their default prefetch with the route's SHARED fallback shell:
639
+ // params hang, so the params-dependent child stays behind its Suspense
640
+ // fallback and the (empty) vary set files ONE entry the client reuses for
641
+ // every param value. A concrete-params render would record the child's param
642
+ // access instead and key an identical fallback per URL. Same conditions as
643
+ // the short-circuit, so no route that already served content changes shape;
644
+ // a shell that fails to render falls through to the live truncated render.
645
+ if (
646
+ (segment === ROUTE_SEGMENT_PATH || segment === '/_index') &&
647
+ partialPrefetch &&
648
+ !runtimePrefetch &&
649
+ body === undefined &&
650
+ !paramsOnlyStatic &&
651
+ !paramPrerendered &&
652
+ !isStatic &&
653
+ !routeHasLoadingFile(selection.route)
654
+ ) {
655
+ const shell = await renderInstantPrefetchShell({
656
+ config,
657
+ selection,
658
+ pathname: matchPathname,
659
+ url,
660
+ request,
661
+ fallbackParams: true,
662
+ });
663
+ if (shell) {
664
+ return withRewriteHeaders(
665
+ segmentBodyResponse(
666
+ shell.html,
667
+ effectiveStaleTime(meta, true),
668
+ // A shell, not the whole page: the navigation still fetches the
669
+ // dynamic continuation behind the boundary.
670
+ true,
671
+ // A pure function of the ROUTE (its params hung), so the client may
672
+ // paint it for any URL of that route — which its reuse policy allows
673
+ // only for payloads marked static.
674
+ true,
675
+ deploymentId(),
676
+ segment,
677
+ segmentVaryInfo(shell.vary, 'body', selection),
678
+ ),
679
+ rewriteHeaders,
680
+ );
681
+ }
682
+ }
683
+ // `/_head`: the outlined dynamic head (metadata) of a full prefetch. The
684
+ // client fetches it as a follow-up to an /_index response marked
685
+ // x-pnext-head-outlined, so the title arrives in its own response AFTER
686
+ // the page content (Next fetches metadata separately from segment data,
687
+ // and the suites assert that response order).
688
+ if (segment === HEAD_SEGMENT_PATH) {
689
+ const rendered = await renderRuntimeSegment({
690
+ config,
691
+ selection,
692
+ pathname: matchPathname,
693
+ url,
694
+ request,
695
+ nav: prefetchNav,
696
+ });
697
+ const title = rendered ? extractDocumentTitle(rendered.body) : null;
698
+ if (title === null) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
699
+ return withRewriteHeaders(
700
+ segmentBodyResponse(
701
+ title,
702
+ effectiveStaleTime(meta, false),
703
+ false,
704
+ false,
705
+ deploymentId(),
706
+ segment,
707
+ // The head segment publishes the HEAD vary set — generateMetadata's
708
+ // own param access, tracked separately from the page body's.
709
+ segmentVaryInfo(rendered!.vary, 'head', selection),
710
+ ),
711
+ rewriteHeaders,
712
+ );
713
+ }
714
+ let postponed: boolean;
715
+ // Segment-M2: the vary set of the live render, when one happened. A BAKED
716
+ // segment was produced at build time outside any tracking scope, so it stays
717
+ // undefined and the client keys that entry on the exact URL.
718
+ let bodyVary: ResponseVaryParams | undefined;
719
+ // The set came off the BAKED `route.segment.meta`, i.e. the BUILD already applied its own trust rule
720
+ // before persisting it - and only the build has the evidence (was the render tracked? did its params
721
+ // hang?). Re-testing it below with the REQUEST-render rule would discard a fallback shell's honest
722
+ // empty set. Presence in the meta IS the trust.
723
+ let bakedVaryFromMeta = false;
724
+ // True when this response is a render TRUNCATED at the route's `loading`
725
+ // boundary (see `truncateAtLoading`): the shell above the boundary, with the
726
+ // dynamic page left as a hole.
727
+ let truncatedShell = false;
728
+ /** The live render's own `use cache` window, when one rendered. */
729
+ let renderStaleSeconds: number | undefined;
730
+ if (body === undefined) {
731
+ // A route that only IMPLIED runtime prefetching (it reads searchParams, so a baked segment must
732
+ // not alias across search values) still follows the default-prefetch contract: the render is
733
+ // truncated at its loading/Suspense shell, never a full dynamic render. Only an explicit
734
+ // `allow-runtime` serves runtime data on a partial prefetch, and a pure params-derived route
735
+ // serves its complete static content.
736
+ truncatedShell =
737
+ partialPrefetch && !routeExplicitRuntimePrefetch(selection.route) && truncateAtLoading;
738
+ const rendered = await renderRuntimeSegment({
739
+ config,
740
+ selection,
741
+ pathname: matchPathname,
742
+ url,
743
+ request,
744
+ keepPrefetchHeader:
745
+ partialPrefetch &&
746
+ !routeExplicitRuntimePrefetch(selection.route) &&
747
+ (!paramsOnlyStatic || truncateAtLoading),
748
+ nav: prefetchNav,
749
+ });
750
+ if (!rendered) return withRewriteHeaders(segmentMissResponse(), rewriteHeaders);
751
+ body = rendered.body;
752
+ bodyVary = rendered.vary;
753
+ renderStaleSeconds = rendered.staleSeconds;
754
+ // A truncated (loading-shell) prefetch is often answered from the route's baked PPR SUB-SHELL, so
755
+ // the request tracked no param access even though the bytes are a known function of the sub-shell's
756
+ // own params. Publish those instead of "unknown" - otherwise every itemId of one category re-fetches
757
+ // a byte-identical shell.
758
+ if (
759
+ truncateAtLoading &&
760
+ bodyVary.params.length === 0 &&
761
+ !bodyVary.search &&
762
+ Object.keys(selection.params).length > 0
763
+ ) {
764
+ bodyVary = subShellVary(selection.route, selection.params) ?? bodyVary;
765
+ }
766
+ // The same artifact, one level up: a render answered from this URL's own
767
+ // BUILD prerender executes no user code, so it tracks nothing even though
768
+ // the build render recorded exactly what each segment read. Publish the
769
+ // persisted sets (`route.prerenderVary`) rather than "unknown".
770
+ if (bodyVary.params.length === 0 && !bodyVary.search) {
771
+ bodyVary = prerenderVary(selection.route, matchPathname) ?? bodyVary;
772
+ }
773
+ postponed = rendered.postponed || (partialPrefetch && meta?.postponed === true);
774
+ // unstable_dynamicOnHover resume: the client already holds this route's
775
+ // static shell (x-pnext-resume-shell) and a full hover prefetch must not
776
+ // re-send static content it has cached. Strip the document down to its
777
+ // streamed dynamic chunks; the client merges them into its shell.
778
+ if (!partialPrefetch && request.headers.get('x-pnext-resume-shell') === '1') {
779
+ const resume = resumeOnlyDocument(body);
780
+ if (resume) {
781
+ const response = segmentBodyResponse(
782
+ resume,
783
+ effectiveStaleTime(meta, false),
784
+ false,
785
+ false,
786
+ deploymentId(),
787
+ segment,
788
+ varyTrusted(rendered.vary, selection, 'body')
789
+ ? segmentVaryInfo(rendered.vary, 'body', selection)
790
+ : undefined,
791
+ );
792
+ response.headers.set('x-pnext-resume-only', '1');
793
+ return withRewriteHeaders(response, rewriteHeaders);
794
+ }
795
+ }
796
+ } else {
797
+ postponed = meta?.postponed === true;
798
+ // Segment-M2: a BAKED body carries the vary set its BUILD render tracked
799
+ // (persisted into `route.segment.meta`). Without it every prerendered
800
+ // route keys on its exact URL and re-fetches the shared shell for every
801
+ // param value. Absent (an older/untracked artifact) stays "unknown".
802
+ bodyVary = meta?.vary ? responseVaryParamsFromWire(meta) : undefined;
803
+ bakedVaryFromMeta = bodyVary !== undefined;
804
+ }
805
+ // A FULL prefetch of a route with DYNAMIC metadata: outline the <title> out of this response -
806
+ // the client fetches it via `/_head` as a separate, LATER response and merges it back before
807
+ // caching (Next serves the head separately, and the suites assert that order). A PARTIAL prefetch
808
+ // outlines too: its body is just as shared across params. A baked route-level shell was rendered
809
+ // with the params hanging and carries NO <title> at all, but the head is dynamic all the same, so
810
+ // mark it outlined and let the client fetch `/_head`.
811
+ let headOutlined = false;
812
+ if (selection.route.pprMetadata === true) {
813
+ const outlined = outlineDocumentTitle(body);
814
+ if (outlined) {
815
+ body = outlined;
816
+ headOutlined = true;
817
+ } else if (!/<title>/i.test(body)) {
818
+ headOutlined = true;
819
+ }
820
+ }
821
+ const completePrerender = baked && !postponed;
822
+ // A postponed (shell-only) prefetch response carries only static data, so its shell reuses for the
823
+ // STATIC window regardless of the route's mode. Likewise a DEFAULT prefetch of a route that never
824
+ // reads request data: its content is a pure function of the URL, so even a complete live render reuses
825
+ // for the static window.
826
+ const routeStaleTime = completePrerender
827
+ ? meta && meta.staleTime > 0
828
+ ? meta.staleTime
829
+ : staleTimeSecondsFor(true)
830
+ : effectiveStaleTime(
831
+ meta,
832
+ postponed ||
833
+ isStatic ||
834
+ (partialPrefetch && (!selection.route.usesRequest || paramsOnlyStatic)),
835
+ );
836
+ // A RUNTIME-PREFETCH answer (`prefetch = 'allow-runtime'` downgrading a full
837
+ // prefetch) carries request-sampled data, so it goes stale with the SHORTEST
838
+ // cache the render resolved — typically a `use cache: private` window — not
839
+ // with the route's baked public-cache window.
840
+ const staleTime =
841
+ partialPrefetchDowngrade && renderStaleSeconds !== undefined
842
+ ? Math.min(renderStaleSeconds, routeStaleTime)
843
+ : routeStaleTime;
844
+ const bodyResponse = segmentBodyResponse(
845
+ body,
846
+ staleTime,
847
+ postponed,
848
+ // A pure params-derived live render is a prerender: its bytes are a
849
+ // function of the URL alone. Marking it lets the client alias it as
850
+ // reusable full data (no speculative re-fetch of the same URL).
851
+ //
852
+ // A TRUNCATED loading shell of such a route qualifies too: everything it
853
+ // contains sits above the boundary and reads nothing but params, so the
854
+ // client may share it across every URL its vary set covers instead of
855
+ // re-fetching a byte-identical shell per param value.
856
+ baked || isStatic || ((!postponed || truncatedShell) && paramsOnlyStatic),
857
+ deploymentId(),
858
+ segment,
859
+ bodyVary &&
860
+ (bakedVaryFromMeta ||
861
+ varyTrusted(
862
+ bodyVary,
863
+ selection,
864
+ 'body',
865
+ false,
866
+ // Live render only (`!baked`): a baked body tracked nothing, so its empty set is the
867
+ // artifact this guard exists for. `allow-runtime` ONLY, not every "explicit"
868
+ // runtime-prefetch route: an `unstable_eager` route RESOLVES its params in this render, so
869
+ // an empty set there is the untracked artifact, not the truth - trusting it published one
870
+ // shared entry for the whole route and elided every later param's prefetch.
871
+ selection.route.segmentConfig?.prefetch === 'allow-runtime' && !baked,
872
+ ))
873
+ ? segmentVaryInfo(bodyVary, 'body', selection)
874
+ : undefined,
875
+ // App Shells: a COMPLETE per-URL prerender carries the route's shared
876
+ // shell alongside it, so a navigation to another param of the route has
877
+ // an instant shell to paint without a second request.
878
+ completePrerender || (!postponed && paramsOnlyStatic)
879
+ ? routeAppShell(config, selection)
880
+ : undefined,
881
+ );
882
+ if (headOutlined) {
883
+ bodyResponse.headers.set(
884
+ 'x-pnext-head-outlined',
885
+ headFetchedFirst(selection) ? 'first' : '1',
886
+ );
887
+ }
888
+ // The client asked for a FULL prefetch; this answer is a shell. Same marker
889
+ // the runtime-prefetch (unstable_instant) downgrade uses — the generic
890
+ // postponed flags never downgrade a full prefetch (ordinary PPR full
891
+ // renders carry them while streaming complete resumed content).
892
+ if (partialPrefetchDowngrade) bodyResponse.headers.set('x-pnext-runtime-prefetch', '1');
893
+ return withRewriteHeaders(bodyResponse, rewriteHeaders);
894
+ }
895
+
896
+ // `/_tree`: the RootTreePrefetch. Unmatched/non-page targets get an empty
897
+ // (dynamic) tree so the client falls back to a full nav fetch (never 404s).
898
+ const routeId = selection?.route.id;
899
+ const bakedTree = routeId ? readSegmentTree(config.outPath, routeId) : undefined;
900
+ if (routeId && bakedTree) {
901
+ // The route tree's SHAPE is static in Next: it reuses for the static window
902
+ // regardless of whether the route itself renders dynamically. markUrlStaticFresh
903
+ // (router.ts) only records a URL's static freshness when x-nextjs-stale-time > 0,
904
+ // so a dynamic route's tree (dynamic window = 0) would never warm the elision
905
+ // map and every re-revealed link would refetch the tree (breaking the
906
+ // encoded-slash-params back-navigation contract). Stamp the STATIC window on the
907
+ // tree even for dynamic routes; the segment body branch keeps its own windows.
908
+ const staleTime = isStatic
909
+ ? effectiveStaleTime(readSegmentMeta(config.outPath, routeId), true)
910
+ : staleTimeSecondsFor(true);
911
+ return withRewriteHeaders(
912
+ new Response(
913
+ treeWithStaleTime(bakedTree, staleTime, url.searchParams.has('_rsc')),
914
+ {
915
+ status: 200,
916
+ headers: {
917
+ 'content-type': RSC_CONTENT_TYPE_HEADER,
918
+ 'x-nextjs-postponed': '2',
919
+ 'x-nextjs-stale-time': String(staleTime),
920
+ ...(isStatic ? { 'x-nextjs-prerender': '1' } : {}),
921
+ 'x-nextjs-deployment-id': deploymentId(),
922
+ // A static route's baked tree is CDN-cacheable like Next's
923
+ // prerendered payloads; the per-variant `_rsc` cache-buster keys
924
+ // it correctly on Vary-ignoring CDNs.
925
+ 'cache-control': isStatic
926
+ ? 's-maxage=31536000, stale-while-revalidate'
927
+ : 'private, no-store',
928
+ },
929
+ },
930
+ ),
931
+ rewriteHeaders,
932
+ );
933
+ }
934
+ const payload = buildRootTreePrefetch({
935
+ pathname: matchPathname,
936
+ isStatic,
937
+ staleTimeSeconds: staleTimeSecondsFor(isStatic),
938
+ routeId: selection?.route.id,
939
+ runtimePrefetch:
940
+ (selection?.route.segmentConfig as { prefetch?: unknown } | undefined)?.prefetch ===
941
+ 'allow-runtime',
942
+ postponed: Boolean(selection?.route.pprHoles?.length),
943
+ // Segment-M2: route identity + this URL's params so the client can key
944
+ // shared segment entries before it has fetched a body for this URL.
945
+ ...(selection?.route.kind === 'page'
946
+ ? { routePattern: selection.route.route, params: wireParams(selection.params) }
947
+ : {}),
948
+ ...(selection?.route.pprMetadata === true
949
+ ? { headOutlined: true, ...(headFetchedFirst(selection) ? { headFirst: true } : {}) }
950
+ : {}),
951
+ });
952
+ return withRewriteHeaders(
953
+ withDeploymentId(
954
+ treePrefetchResponse(payload, { format: url.searchParams.has('_rsc') ? 'flight' : 'json' }),
955
+ ),
956
+ rewriteHeaders,
957
+ );
958
+ };
959
+
960
+ /**
961
+ * The vary payload stamped onto a segment response - the segment's own vary names plus the route identity
962
+ * and concrete params the client needs to build the shared cache key for OTHER URLs of the same route.
963
+ */
964
+ function segmentVaryInfo(
965
+ vary: ResponseVaryParams,
966
+ kind: 'body' | 'head' | 'layout' | 'page',
967
+ selection: NonNullable<ReturnType<typeof selectRouteForRequest>>,
968
+ ): SegmentVaryInfo {
969
+ return {
970
+ vary: varyNamesFor(vary, kind),
971
+ // w9-segment-split: a BODY response carries the layout chain and the page in
972
+ // one document, so its own `vary` stays their union (it is only byte-correct
973
+ // for a URL matching both). The split sets ride alongside it so the client
974
+ // can file the two frames separately and re-fetch just the one that varied.
975
+ // Only when the render actually TRACKED something: a document answered from
976
+ // a prerender cache reads no params, and an empty set would file the page
977
+ // frame as shareable across every param value (see varyTrusted).
978
+ ...(kind === 'body' && (vary.params.length > 0 || vary.search)
979
+ ? {
980
+ layoutVary: varyNamesFor(vary, 'layout'),
981
+ pageVary: varyNamesFor(vary, 'page'),
982
+ }
983
+ : {}),
984
+ route: selection.route.route,
985
+ params: wireParams(selection.params),
986
+ };
987
+ }
988
+
989
+ /**
990
+ * True when a segment's tracked vary set may be published as-is: the render recorded at least one
991
+ * access, or the route has no params for it to record. An untrusted set is published as UNKNOWN,
992
+ * which keys the entry on its exact URL instead of sharing it across param values.
993
+ *
994
+ * An EMPTY tracked set on a route that HAS params is only ever the truth for a `fallbackParams`
995
+ * render, where the params genuinely hung. For a CONCRETE-param render the same empty set is more
996
+ * often an artifact, and the failure modes are not symmetric: over-varying costs a cache hit,
997
+ * under-varying serves one param's content for another.
998
+ *
999
+ * `runtimePrefetch` is the second escape hatch: an EXPLICIT `allow-runtime` render that happened
1000
+ * LIVE tracked its param access for real, so an empty set there means the params were never read
1001
+ * before the render postponed. Never pass it for a BAKED body, produced outside any tracking scope.
1002
+ */
1003
+ function varyTrusted(
1004
+ vary: ResponseVaryParams,
1005
+ selection: NonNullable<ReturnType<typeof selectRouteForRequest>>,
1006
+ kind: 'body' | 'layout' | 'page',
1007
+ fallbackParams = false,
1008
+ runtimePrefetch = false,
1009
+ ): boolean {
1010
+ if (kind === 'layout') {
1011
+ if (vary.layout.length > 0 || vary.layoutSearch) return true;
1012
+ } else if (kind === 'page') {
1013
+ // The mirror of the layout arm: a PAGE frame is trustworthy on its OWN tracked access. An empty page
1014
+ // set is common and honest - a page that reads no params while its layout reads them all - but on a
1015
+ // params-bearing route it is indistinguishable from an untracked render, so it falls through to the
1016
+ // same "route has no params" test the body arm ends on.
1017
+ if (vary.page.length > 0 || vary.pageSearch) return true;
1018
+ } else {
1019
+ if (vary.params.length > 0 || vary.search) return true;
1020
+ if (fallbackParams) return true;
1021
+ if (runtimePrefetch) return true;
1022
+ }
1023
+ return Object.keys(selection.params).length === 0;
1024
+ }
1025
+
1026
+ /** Route params in the JSON-safe shape the client's vary keying reads. */
1027
+ function wireParams(
1028
+ params: Record<string, unknown>,
1029
+ ): Record<string, string | string[]> {
1030
+ const wire: Record<string, string | string[]> = {};
1031
+ for (const [key, value] of Object.entries(params)) {
1032
+ if (Array.isArray(value)) wire[key] = (value as unknown[]).map(paramText);
1033
+ else if (value !== undefined && value !== null) wire[key] = paramText(value);
1034
+ }
1035
+ return wire;
1036
+ }
1037
+
1038
+ /** Route param values are strings (or string arrays); coerce defensively. */
1039
+ function paramText(value: unknown): string {
1040
+ return typeof value === 'string' ? value : JSON.stringify(value) ?? '';
1041
+ }
1042
+
1043
+ /**
1044
+ * True when the route's segment chain declares a `loading` convention (or its
1045
+ * page/layout source wraps content in <Suspense>). A default prefetch of such a
1046
+ * route renders its loading shell (truncated at the boundary) instead of a
1047
+ * hanging dynamic render, so the miss short-circuit must not fire for it.
1048
+ */
1049
+ function routeHasLoadingBoundary(route: RouteManifestEntry): boolean {
1050
+ if (routeHasLoadingFile(route)) return true;
1051
+ // No `loading` file, but the PAGE itself wraps its params-dependent subtree in <Suspense>. That boundary
1052
+ // truncates the prefetch render exactly like a loading convention does, so the route serves the shared
1053
+ // fallback shell instead of a segment miss. Page file only - a layout's <Suspense> says nothing about
1054
+ // where the page's own render stops.
1055
+ return pageHasInPageSuspense(route.file);
1056
+ }
1057
+
1058
+ /**
1059
+ * A `loading` convention file in the route's OWN app segment chain marks a guaranteed truncation point -
1060
+ * the render stops at it, never hanging. (sourceFiles also lists framework modules; anchor on the app
1061
+ * directory.)
1062
+ */
1063
+ function routeHasLoadingFile(route: RouteManifestEntry): boolean {
1064
+ const appMarker = route.file.lastIndexOf('/app/');
1065
+ const appDir = appMarker === -1 ? undefined : route.file.slice(0, appMarker + '/app/'.length);
1066
+ if (appDir === undefined) return false;
1067
+ return (route.sourceFiles ?? [route.file]).some(
1068
+ file => file.startsWith(appDir) && /[\\/]loading\.[jt]sx?$/.test(file),
1069
+ );
1070
+ }
1071
+
1072
+ /** True when the build prerendered this route for the given param set. */
1073
+ function routeParamPrerendered(
1074
+ route: RouteManifestEntry,
1075
+ params: Record<string, unknown>,
1076
+ ): boolean {
1077
+ const sets = route.prerenderedParams;
1078
+ if (!sets?.length) return false;
1079
+ return sets.some(set => {
1080
+ const keys = Object.keys(set);
1081
+ return (
1082
+ keys.length === Object.keys(params).length &&
1083
+ keys.every(key => String(set[key]) === String(params[key]))
1084
+ );
1085
+ });
1086
+ }
1087
+
1088
+ /**
1089
+ * The vary set of a prefetch answered from a PPR SUB-SHELL - the build baked that shell for a param
1090
+ * SUBSET, so a request-time render tracks nothing while its bytes are a pure function of exactly
1091
+ * those params. The layout chain consumed them; the page contributed only its `loading` fallback, so
1092
+ * the PAGE frame stays shareable across every value of the remaining params.
1093
+ */
1094
+ /**
1095
+ * The route's shared APP SHELL - the build's params-hanging render. Carried alongside a per-URL
1096
+ * prerender so the client can file it at the route's fallback vary path. Undefined for a param-free
1097
+ * route, or when the build baked no such shell.
1098
+ */
1099
+ function routeAppShell(
1100
+ config: ResolvedConfig,
1101
+ selection: NonNullable<ReturnType<typeof selectRouteForRequest>>,
1102
+ ): SegmentAppShell | undefined {
1103
+ // Strictly opt-in, like the client's second (param-stripped) shell prefetch:
1104
+ // without the flag the extra bytes would ride on every prerender response.
1105
+ if (!appShellsEnabled()) return undefined;
1106
+ if (!selection.route.route.includes(':')) return undefined;
1107
+ const file = join(config.outPath, 'ppr', `${selection.route.id}.html`);
1108
+ if (!existsSync(file)) return undefined;
1109
+ try {
1110
+ return { html: readFileSync(file, 'utf8'), route: selection.route.route };
1111
+ } catch {
1112
+ return undefined;
1113
+ }
1114
+ }
1115
+
1116
+ /** The vary sets the BUILD render of this exact prerendered URL tracked. */
1117
+ function prerenderVary(
1118
+ route: RouteManifestEntry,
1119
+ pathname: string,
1120
+ ): ResponseVaryParams | undefined {
1121
+ const wire = route.prerenderVary?.[pathname || '/'];
1122
+ return wire ? responseVaryParamsFromWire(wire) : undefined;
1123
+ }
1124
+
1125
+ function subShellVary(
1126
+ route: RouteManifestEntry,
1127
+ params: Record<string, unknown>,
1128
+ ): ResponseVaryParams | undefined {
1129
+ const shell = route.pprSubShells?.find(candidate =>
1130
+ Object.entries(candidate.concreteParams).every(
1131
+ ([name, value]) => String(value) === String(params[name]),
1132
+ ),
1133
+ );
1134
+ const names = Object.keys(shell?.concreteParams ?? {}).sort();
1135
+ if (!names.length) return undefined;
1136
+ return responseVaryParamsFromWire({ vary: names, layoutVary: names, pageVary: [] });
1137
+ }
1138
+
1139
+ async function readBakedSegment({
1140
+ config,
1141
+ selection,
1142
+ pathname,
1143
+ file,
1144
+ }: {
1145
+ config: ResolvedConfig;
1146
+ selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
1147
+ pathname: string;
1148
+ file: string;
1149
+ }): Promise<string | undefined> {
1150
+ if (!existsSync(file)) return undefined;
1151
+ await regenerateStaleSegment({ config, selection, pathname, file });
1152
+ try {
1153
+ // A baked FALLBACK-shell segment was rendered with every param hanging, so
1154
+ // its `__PNEXT_ROUTE__` carries `params: {}`. Stamp this request's params
1155
+ // in, exactly as the document path does — the client paints this body as
1156
+ // the loading shell and reads its route state for useParams()/prediction.
1157
+ return withRequestRouteParams(readFileSync(file, 'utf8'), selection.params);
1158
+ } catch {
1159
+ return undefined;
1160
+ }
1161
+ }
1162
+
1163
+ /** True when the route EXPLICITLY opts into runtime prefetching via segment config. */
1164
+ function routeExplicitRuntimePrefetch(route: RouteManifestEntry): boolean {
1165
+ const prefetch = (route.segmentConfig as { prefetch?: unknown } | undefined)?.prefetch;
1166
+ return prefetch === 'allow-runtime' || prefetch === 'unstable_eager';
1167
+ }
1168
+
1169
+ /**
1170
+ * True when the route opts into rendering its body prefetch as a runtime-
1171
+ * prefetch prerender: `unstable_instant = true` (page or layout chain) or
1172
+ * `export const prefetch = 'allow-runtime'`. A leaf-most `unstable_instant =
1173
+ * false` opts back out even when a layout (or `allow-runtime`) opts in.
1174
+ */
1175
+ function routeRuntimePrefetchRender(route: RouteManifestEntry): boolean {
1176
+ const config = route.segmentConfig;
1177
+ if (config?.unstableInstant === false) return false;
1178
+ return config?.unstableInstant === true || config?.prefetch === 'allow-runtime';
1179
+ }
1180
+
1181
+ function routeUsesRuntimePrefetch(route: RouteManifestEntry): boolean {
1182
+ if (routeExplicitRuntimePrefetch(route)) return true;
1183
+ if (clientStaticPrefetchable(route)) return false;
1184
+ try {
1185
+ return /\b(?:searchParams|useSearchParams)\b/.test(readFileSync(route.file, 'utf8'));
1186
+ } catch {
1187
+ return false;
1188
+ }
1189
+ }
1190
+
1191
+ /**
1192
+ * A CLIENT page's params/searchParams access resolves client-side from the live URL, so its server
1193
+ * render is a pure function of the exact URL and is servable complete on a default prefetch. Only
1194
+ * true when no server segment in the chain reads real request data.
1195
+ */
1196
+ function clientStaticPrefetchable(route: RouteManifestEntry): boolean {
1197
+ if (route.client !== true) return false;
1198
+ // Only the route's own app segments matter (sourceFiles also lists framework
1199
+ // modules, whose text mentions request APIs in comments/implementations).
1200
+ const appDir = route.file.slice(0, route.file.lastIndexOf('/app/') + '/app/'.length);
1201
+ for (const file of route.sourceFiles ?? []) {
1202
+ if (file === route.file) continue;
1203
+ if (!appDir || !file.startsWith(appDir)) continue;
1204
+ try {
1205
+ if (/\b(?:cookies|headers|connection|draftMode)\s*\(/.test(readFileSync(file, 'utf8'))) {
1206
+ return false;
1207
+ }
1208
+ } catch {
1209
+ return false;
1210
+ }
1211
+ }
1212
+ return true;
1213
+ }
1214
+
1215
+ /**
1216
+ * Render the runtime-prefetch (unstable_instant) shell for a full segment prefetch: request data sampled,
1217
+ * connection() content omitted. Best-effort - null falls back to the ordinary full render.
1218
+ */
1219
+ async function renderInstantPrefetchShell({
1220
+ config,
1221
+ selection,
1222
+ pathname,
1223
+ url,
1224
+ request,
1225
+ fallbackParams = false,
1226
+ }: {
1227
+ config: ResolvedConfig;
1228
+ selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
1229
+ pathname: string;
1230
+ url: URL;
1231
+ request: Request;
1232
+ /** App Shells: render the SHARED shell (params hang, request data resolves). */
1233
+ fallbackParams?: boolean;
1234
+ }): Promise<{
1235
+ html: string;
1236
+ postponed: boolean;
1237
+ vary: ResponseVaryParams;
1238
+ /** Shortest `use cache` stale window this render actually sampled. */
1239
+ staleSeconds?: number;
1240
+ } | null> {
1241
+ if (selection.route.kind !== 'page') return null;
1242
+ const renderUrl = new URL(url);
1243
+ renderUrl.pathname = pathname;
1244
+ renderUrl.searchParams.delete('_rsc');
1245
+ const headers = new Headers(request.headers);
1246
+ headers.delete(NEXT_ROUTER_PREFETCH_HEADER);
1247
+ headers.delete(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER);
1248
+ // Segment-M2 deliverable 1: the render runs inside a vary-params tracking
1249
+ // scope so the response can publish which params it actually read.
1250
+ let staleSeconds: number | undefined;
1251
+ const tracked = await withVaryParamsTracking(() =>
1252
+ getRenderExtensions().collectRenderMeta(
1253
+ async () => {
1254
+ const value = await withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
1255
+ renderRuntimePrefetchDocument({
1256
+ config,
1257
+ route: selection.route,
1258
+ params: selection.params,
1259
+ url: renderUrl,
1260
+ request: new Request(renderUrl, { headers }),
1261
+ ...(fallbackParams ? { fallbackParams: true } : {}),
1262
+ }),
1263
+ );
1264
+ // Read inside the collection scope: the shortest window any cache this
1265
+ // render SAMPLED reported (a `use cache: private` one included), which
1266
+ // is how long the request-sampled payload may be reused.
1267
+ staleSeconds = getRenderExtensions().currentCacheStaleSeconds();
1268
+ return value;
1269
+ },
1270
+ {
1271
+ fetchCache: selection.route.segmentConfig?.fetchCache,
1272
+ route: pathname,
1273
+ },
1274
+ ),
1275
+ );
1276
+ const rendered = tracked.value.value;
1277
+ return rendered
1278
+ ? {
1279
+ ...rendered,
1280
+ vary: tracked.vary,
1281
+ ...(staleSeconds !== undefined ? { staleSeconds } : {}),
1282
+ }
1283
+ : null;
1284
+ }
1285
+
1286
+ async function renderRuntimeSegment({
1287
+ config,
1288
+ selection,
1289
+ pathname,
1290
+ url,
1291
+ request,
1292
+ keepPrefetchHeader = false,
1293
+ nav,
1294
+ }: {
1295
+ config: ResolvedConfig;
1296
+ selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
1297
+ pathname: string;
1298
+ url: URL;
1299
+ request: Request;
1300
+ /** Keep next-router-prefetch so the render truncates at its static shell. */
1301
+ keepPrefetchHeader?: boolean;
1302
+ /**
1303
+ * The client's echoed nav state. Threading it makes the render apply the
1304
+ * same shared-layout skip as a navigation render (Next dedupes shared
1305
+ * layouts on prefetches via next-url too), so a prefetch never re-executes
1306
+ * a layout the origin page already renders.
1307
+ */
1308
+ nav?: ReturnType<typeof parseNavState>;
1309
+ }): Promise<
1310
+ | {
1311
+ body: string;
1312
+ postponed: boolean;
1313
+ vary: ResponseVaryParams;
1314
+ /** Shortest `use cache` stale window this render actually resolved. */
1315
+ staleSeconds?: number;
1316
+ }
1317
+ | undefined
1318
+ > {
1319
+ if (selection.route.kind !== 'page') return undefined;
1320
+ const renderUrl = new URL(url);
1321
+ renderUrl.pathname = pathname;
1322
+ renderUrl.searchParams.delete('_rsc');
1323
+ const headers = new Headers(request.headers);
1324
+ if (!keepPrefetchHeader) headers.delete(NEXT_ROUTER_PREFETCH_HEADER);
1325
+ headers.delete(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER);
1326
+ // The segment header is stripped so the render takes the ordinary document
1327
+ // path, but the renderer still has to know this body is a PREFETCH payload
1328
+ // and not a navigation: a prefetch that retains a shared layout must render
1329
+ // the delta (skip marker) rather than resume the prebuilt shell, which
1330
+ // carries the origin page's layout markup back to a client that has it.
1331
+ headers.set(SEGMENT_RENDER_HEADER, '1');
1332
+ const renderRequest = new Request(renderUrl, { headers });
1333
+ // Segment-M2 deliverable 1: track param access for the whole render, INCLUDING
1334
+ // the body stream — the tracking scope closes only once the body is read.
1335
+ const tracked = await withVaryParamsTracking(async () => {
1336
+ const rendered = await getRenderExtensions().collectRenderMeta(
1337
+ () =>
1338
+ withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
1339
+ renderPageResponse({
1340
+ config,
1341
+ route: selection.route,
1342
+ params: selection.params,
1343
+ url: renderUrl,
1344
+ request: renderRequest,
1345
+ ...(nav ? { nav: { soft: true, state: nav, childrenPath: pathname } } : {}),
1346
+ }),
1347
+ ),
1348
+ {
1349
+ fetchCache: selection.route.segmentConfig?.fetchCache,
1350
+ route: pathname,
1351
+ },
1352
+ );
1353
+ if (!rendered.value.ok) return undefined;
1354
+ // The render's OWN window (the `use cache` finalizer stamped the shortest
1355
+ // life it actually resolved, private caches included) — authoritative for a
1356
+ // request-sampled payload, whose baked route meta only knows the public
1357
+ // caches' longer window.
1358
+ const header = Number(rendered.value.headers.get('x-nextjs-stale-time'));
1359
+ return {
1360
+ body: await rendered.value.text(),
1361
+ postponed: rendered.value.headers.get('x-nextjs-postponed') === '1',
1362
+ ...(Number.isFinite(header) && header > 0 ? { staleSeconds: header } : {}),
1363
+ };
1364
+ });
1365
+ return tracked.value ? { ...tracked.value, vary: tracked.vary } : undefined;
1366
+ }
1367
+
1368
+ /** Segment request key for the outlined dynamic head (metadata) of a route. */
1369
+ const HEAD_SEGMENT_PATH = '/_head';
1370
+
1371
+ /**
1372
+ * The segment request key for a route's LAYOUT frame - the document with the page's markup cut out.
1373
+ * Requested when the client's page frame for a URL is a hit but its layout frame is not.
1374
+ */
1375
+ const LAYOUT_SEGMENT_PATH = '/_layout';
1376
+
1377
+ /**
1378
+ * The segment request key for a route's PAGE frame - the page slot alone, with the layout chain cut away.
1379
+ * The inverse of `/_layout`, requested by a navigation that already owns the destination's layout, or
1380
+ * holds a valid cached layout frame for it.
1381
+ */
1382
+ const PAGE_SEGMENT_PATH = '/_page';
1383
+
1384
+ /**
1385
+ * App Shells: the client's marker for the SECOND, param-stripped prefetch that
1386
+ * primes a route's shared shell (compat/client/segment-prefetch.ts). Only
1387
+ * meaningful under `experimental.appShells`.
1388
+ */
1389
+ const APP_SHELL_PREFETCH_HEADER = 'x-pnext-app-shell';
1390
+
1391
+ /**
1392
+ * Marks the render behind a segment-prefetch response, after the segment header
1393
+ * itself has been stripped. The renderer keys its shared-layout delta on this
1394
+ * (see fullSegmentPrefetchSkipsSharedLayout).
1395
+ */
1396
+ const SEGMENT_RENDER_HEADER = 'x-pnext-segment-render';
1397
+
1398
+ const DOCUMENT_TITLE_PATTERN = /<title(?:\s[^>]*)?>[\s\S]*?<\/title>/;
1399
+
1400
+ /** The document's `<title>…</title>` markup, or null when it has none. */
1401
+ function extractDocumentTitle(html: string): string | null {
1402
+ return DOCUMENT_TITLE_PATTERN.exec(html)?.[0] ?? null;
1403
+ }
1404
+
1405
+ /**
1406
+ * True when this route's outlined head must be fetched BEFORE its body. A route with DYNAMIC PARAMS
1407
+ * shares one body across every param value while its metadata varies per URL, so the head is its own
1408
+ * leading segment and is answered first. A PARAMLESS route has nothing to share - its head is
1409
+ * outlined out of the one body response and arrives after it.
1410
+ */
1411
+ function headFetchedFirst(selection: { route: RouteManifestEntry }): boolean {
1412
+ return selection.route.pprMetadata === true && /[:*]/.test(selection.route.route);
1413
+ }
1414
+
1415
+ /** The document with its `<title>` removed, or null when it has none. */
1416
+ function outlineDocumentTitle(html: string): string | null {
1417
+ const match = DOCUMENT_TITLE_PATTERN.exec(html);
1418
+ if (!match) return null;
1419
+ return html.slice(0, match.index) + html.slice(match.index + match[0].length);
1420
+ }
1421
+
1422
+ /**
1423
+ * Cut a streamed document down to its dynamic continuation (the hidden stream chunk divs plus everything
1424
+ * after them), wrapped as a minimal parseable document. Null when the document has no streamed chunks -
1425
+ * nothing dynamic to resume, so the caller serves the full document.
1426
+ */
1427
+ function resumeOnlyDocument(html: string): string | null {
1428
+ const cut = html.indexOf('<div hidden data-pnext-stream');
1429
+ if (cut === -1) return null;
1430
+ const end = html.lastIndexOf('</body>');
1431
+ const chunks = end > cut ? html.slice(cut, end) : html.slice(cut);
1432
+ return `<!DOCTYPE html><html><body>${chunks}</body></html>`;
1433
+ }
1434
+
1435
+ function segmentMissResponse(): Response {
1436
+ return new Response(null, {
1437
+ status: 204,
1438
+ headers: {
1439
+ 'content-type': RSC_CONTENT_TYPE_HEADER,
1440
+ 'x-nextjs-postponed': '2',
1441
+ 'cache-control': 'private, no-store',
1442
+ },
1443
+ });
1444
+ }
1445
+
1446
+ /** x-nextjs-rewritten-path/query values for a matched config rewrite. */
1447
+ interface RewriteHeaderInfo {
1448
+ path?: string;
1449
+ query?: string;
1450
+ }
1451
+
1452
+ /** Modification time of `file`, or undefined when it does not exist. */
1453
+ function fileMtimeMs(file: string): number | undefined {
1454
+ try {
1455
+ return statSync(file).mtimeMs;
1456
+ } catch {
1457
+ return undefined;
1458
+ }
1459
+ }
1460
+
1461
+ async function regenerateStaleSegment({
1462
+ config,
1463
+ selection,
1464
+ pathname,
1465
+ file,
1466
+ }: {
1467
+ config: ResolvedConfig;
1468
+ selection: NonNullable<ReturnType<typeof selectRouteForRequest>>;
1469
+ pathname: string;
1470
+ file: string;
1471
+ }): Promise<void> {
1472
+ let mtimeMs: number;
1473
+ try {
1474
+ mtimeMs = statSync(file).mtimeMs;
1475
+ } catch {
1476
+ return;
1477
+ }
1478
+ const servedVersion = segmentArtifactVersions.get(file) ?? 0;
1479
+ const url = new URL(`http://pnext.local${pathname}`);
1480
+ // A PPR route's shell is the SAME render this segment body is cut from, so a
1481
+ // shell that was rewritten after this artifact makes the artifact stale even
1482
+ // when no revalidation is outstanding: without it the document serves the new
1483
+ // regeneration's data while the prefetch keeps replaying the old one's.
1484
+ const shellMtimeMs =
1485
+ selection.route.ppr === true
1486
+ ? fileMtimeMs(pprShellPath(config.outPath, selection.route.id))
1487
+ : undefined;
1488
+ const stale =
1489
+ servedVersion < segmentRevalidationVersion ||
1490
+ getRequestExtensions().staticStaleness(pathname, mtimeMs, []) ||
1491
+ (shellMtimeMs !== undefined && shellMtimeMs > mtimeMs);
1492
+ if (!stale) return;
1493
+
1494
+ // Adopt the shell this route currently serves (regenerating it only when it
1495
+ // is itself stale, through the renderer's single-flight) rather than running
1496
+ // a second, independent prerender whose cache reads would resolve to their
1497
+ // own values.
1498
+ if (shellMtimeMs !== undefined) {
1499
+ const shell = await currentPprShellHtml({
1500
+ config,
1501
+ route: selection.route,
1502
+ params: selection.params,
1503
+ url,
1504
+ });
1505
+ if (shell !== null) {
1506
+ await writeFile(file, shell);
1507
+ segmentArtifactVersions.set(file, segmentRevalidationVersion);
1508
+ return;
1509
+ }
1510
+ }
1511
+
1512
+ const rendered = await getRenderExtensions().collectRenderMeta(
1513
+ () =>
1514
+ withRouteRuntime(selection.route.segmentConfig?.runtime, () =>
1515
+ renderPartialShell({
1516
+ config,
1517
+ route: selection.route,
1518
+ params: selection.params,
1519
+ url,
1520
+ runtimeRegen: true,
1521
+ }),
1522
+ ),
1523
+ {
1524
+ fetchCache: selection.route.segmentConfig?.fetchCache,
1525
+ refreshFetches: true,
1526
+ blockingStaleFetches: true,
1527
+ route: pathname,
1528
+ prerender: true,
1529
+ },
1530
+ );
1531
+ if (!rendered.value) return;
1532
+ await writeFile(file, rendered.value.shell);
1533
+ segmentArtifactVersions.set(file, segmentRevalidationVersion);
1534
+ }
1535
+
1536
+ const segmentRewriteCache = new Map<string, Promise<CompatRewrite[]>>();
1537
+
1538
+ function loadSegmentRewrites(config: ResolvedConfig): Promise<CompatRewrite[]> {
1539
+ let cached = segmentRewriteCache.get(config.root);
1540
+ if (!cached) {
1541
+ cached = loadCompatRewrites(config.root);
1542
+ segmentRewriteCache.set(config.root, cached);
1543
+ }
1544
+ return cached;
1545
+ }
1546
+
1547
+ function parseCookieHeader(header: string | null): Record<string, string> {
1548
+ if (!header) return {};
1549
+ const cookies: Record<string, string> = {};
1550
+ for (const part of header.split(';')) {
1551
+ const index = part.indexOf('=');
1552
+ if (index === -1) continue;
1553
+ const key = part.slice(0, index).trim();
1554
+ if (key) cookies[key] = decodeURIComponent(part.slice(index + 1).trim());
1555
+ }
1556
+ return cookies;
1557
+ }
1558
+
1559
+ /**
1560
+ * Resolve next.config `rewrites` for an RSC/segment request. Returns the
1561
+ * destination pathname (used for route matching) plus the rewritten-path/query
1562
+ * header values Next emits on RSC responses. The `_rsc` cache-buster is a CDN
1563
+ * key, not an app param, so it is excluded from the query matched/appended by
1564
+ * the rewrite (otherwise it would leak into x-nextjs-rewritten-query).
1565
+ */
1566
+ async function resolveSegmentRewrite(
1567
+ config: ResolvedConfig,
1568
+ request: Request,
1569
+ url: URL,
1570
+ ): Promise<{ pathname: string; headers: RewriteHeaderInfo } | undefined> {
1571
+ const rewrites = await loadSegmentRewrites(config);
1572
+ if (rewrites.length === 0) return undefined;
1573
+ const query = new URLSearchParams(url.searchParams);
1574
+ query.delete('_rsc');
1575
+ const result = resolveCompatRewrite(rewrites, url.pathname, {
1576
+ host: request.headers.get('host') ?? '',
1577
+ headers: request.headers,
1578
+ cookies: parseCookieHeader(request.headers.get('cookie')),
1579
+ query,
1580
+ });
1581
+ if (!result) return undefined;
1582
+ const resolvedQuery = result.search.toString();
1583
+ return {
1584
+ pathname: result.pathname,
1585
+ headers: {
1586
+ // Path header only when the pathname actually changed; query header only
1587
+ // when the destination carries a query (mirrors recordRewrite in the
1588
+ // full-render path).
1589
+ ...(result.pathname !== url.pathname ? { path: result.pathname } : {}),
1590
+ ...(resolvedQuery ? { query: resolvedQuery } : {}),
1591
+ },
1592
+ };
1593
+ }
1594
+
1595
+ /**
1596
+ * Stamp x-nextjs-rewritten-path/query on a segment/RSC response. Interceptor
1597
+ * responses short-circuit before the protocol finalizer runs, so the compat
1598
+ * rewrite headers must be set here directly. No-op when no rewrite matched.
1599
+ */
1600
+ function withRewriteHeaders(response: Response, info: RewriteHeaderInfo | undefined): Response {
1601
+ // Interceptor responses bypass the normal response-finalizer pipeline, so
1602
+ // they must merge the router's CDN vary contract themselves.
1603
+ mergeRouterVary(response.headers);
1604
+ if (info?.path !== undefined) response.headers.set('x-nextjs-rewritten-path', info.path);
1605
+ if (info?.query !== undefined) response.headers.set('x-nextjs-rewritten-query', info.query);
1606
+ return response;
1607
+ }
1608
+
1609
+ const ROUTER_VARY = [
1610
+ 'rsc',
1611
+ 'next-router-state-tree',
1612
+ 'next-router-prefetch',
1613
+ 'next-router-segment-prefetch',
1614
+ ];
1615
+
1616
+ function mergeRouterVary(headers: Headers): void {
1617
+ const existing = headers.get('vary');
1618
+ const values = existing ? existing.split(',').map(value => value.trim()).filter(Boolean) : [];
1619
+ const present = new Set(values.map(value => value.toLowerCase()));
1620
+ for (const value of ROUTER_VARY) {
1621
+ if (!present.has(value)) {
1622
+ present.add(value);
1623
+ values.push(value);
1624
+ }
1625
+ }
1626
+ headers.set('vary', values.join(', '));
1627
+ }
1628
+
1629
+ // Stamp x-nextjs-stale-time on router responses that a `use cache` route did
1630
+ // not already stamp, so both prefetches and navigation-seeded entries learn
1631
+ // their reuse window from the route mode.
1632
+ const APP_ROUTE_KINDS = new Set<ResponseFinalizerContext['routeKind']>([
1633
+ 'html',
1634
+ 'data',
1635
+ 'route-handler',
1636
+ ]);
1637
+
1638
+ const prefetchStaleTimeFinalizer: ResponseFinalizer = ctx => {
1639
+ if (!APP_ROUTE_KINDS.has(ctx.routeKind)) return;
1640
+ if (ctx.request.headers.get(RSC_HEADER) !== '1') return;
1641
+ if (!ctx.request.headers.has('x-pnext-soft-nav')) return;
1642
+ if (ctx.headers.has('x-nextjs-stale-time')) return;
1643
+ const prefetchRequest = ctx.request.headers.get(NEXT_ROUTER_PREFETCH_HEADER) === '1';
1644
+ const postponedResponse = ctx.headers.get('x-nextjs-postponed') === '1';
1645
+ // routeMode: a static/isr prerender reuses for the static window, a dynamic render for the dynamic
1646
+ // window, an unknown mode conservatively for the dynamic one. A shell-only (postponed) PREFETCH
1647
+ // response carries only static data, so its shell reuses for the static window regardless.
1648
+ //
1649
+ // A URL whose params the BUILD PRERENDERED is static even though `routeModeOf`
1650
+ // reports the route `dynamic` (it ignores `prerenderedParams`, and an
1651
+ // interception/parallel-slot render is request-time by nature). Corrected here
1652
+ // rather than in `routeModeOf`: routeMode also drives cache-control and the
1653
+ // ISR promotion, which must keep seeing the route-level mode.
1654
+ const runtime = getRequestRuntime();
1655
+ const selection = runtime
1656
+ ? selectRouteForRequest(runtime.routes, ctx.request.url.pathname, undefined)
1657
+ : null;
1658
+ const isStatic =
1659
+ ctx.routeMode === 'static' ||
1660
+ ctx.routeMode === 'isr' ||
1661
+ (prefetchRequest && postponedResponse) ||
1662
+ (selection !== null && routeParamPrerendered(selection.route, selection.params ?? {}));
1663
+ // A NAVIGATION response of a postponed (PPR) route streams the resumed
1664
+ // dynamic data: it is a dynamic document and must reuse for the DYNAMIC
1665
+ // window (staleTimes.dynamic gates how long the client re-commits it), even
1666
+ // though the route's built segment meta carries the shell's static window.
1667
+ if (!isStatic && postponedResponse) {
1668
+ ctx.headers.set('x-nextjs-stale-time', String(staleTimeSecondsFor(false)));
1669
+ return;
1670
+ }
1671
+ // A statically-served prerender never re-runs its `use cache` scopes, so the
1672
+ // use-cache finalizer can't stamp the route's own cacheLife window here. The
1673
+ // build persisted it into the route's segment meta — prefer that over the
1674
+ // mode default so a page's cacheLife({stale}) drives client expiry.
1675
+ if (runtime && selection?.route.kind === 'page') {
1676
+ const meta = readSegmentMeta(runtime.config.outPath, selection.route.id);
1677
+ ctx.headers.set('x-nextjs-stale-time', String(effectiveStaleTime(meta, isStatic)));
1678
+ return;
1679
+ }
1680
+ ctx.headers.set('x-nextjs-stale-time', String(staleTimeSecondsFor(isStatic)));
1681
+ };
1682
+
1683
+ // Wire-compat content type: Next answers router fetches (rsc: 1) with
1684
+ // `text/x-component`, and suites assert exactly that on soft-nav responses.
1685
+ // Only the pnext HTML-swap router's own fetches are flipped (x-pnext-soft-nav
1686
+ // marks them); plain document loads keep text/html.
1687
+ const rscContentTypeFinalizer: ResponseFinalizer = ctx => {
1688
+ if (ctx.routeKind !== 'html') return;
1689
+ if (ctx.request.headers.get(RSC_HEADER) !== '1') return;
1690
+ if (!ctx.request.headers.has('x-pnext-soft-nav')) return;
1691
+ ctx.headers.set('content-type', RSC_CONTENT_TYPE_HEADER);
1692
+ };
1693
+
1694
+ const rscDeploymentFinalizer: ResponseFinalizer = ctx => {
1695
+ if (!APP_ROUTE_KINDS.has(ctx.routeKind)) return;
1696
+ if (ctx.request.headers.get(RSC_HEADER) !== '1') return;
1697
+ if (!ctx.headers.has('x-nextjs-deployment-id')) {
1698
+ ctx.headers.set('x-nextjs-deployment-id', deploymentId());
1699
+ }
1700
+ };
1701
+
1702
+ function readSegmentMeta(outPath: string, routeId: string): SegmentMeta | undefined {
1703
+ const file = segmentMetaFile(outPath, routeId);
1704
+ if (!existsSync(file)) return undefined;
1705
+ try {
1706
+ return JSON.parse(readFileSync(file, 'utf8')) as SegmentMeta;
1707
+ } catch {
1708
+ return undefined;
1709
+ }
1710
+ }
1711
+
1712
+ function effectiveStaleTime(meta: SegmentMeta | undefined, isStatic: boolean): number {
1713
+ const configured = staleTimeSecondsFor(isStatic);
1714
+ if (!meta) return configured;
1715
+ const builtDefault = isStatic ? DEFAULT_STATIC_STALE_TIME_SECONDS : DEFAULT_DYNAMIC_STALE_TIME_SECONDS;
1716
+ if (meta.staleTime === builtDefault) return configured;
1717
+ // A STATIC-data response (a postponed/truncated shell) of a route whose meta was built with the DYNAMIC
1718
+ // default must reuse for the static window - the meta's 0 describes the route's dynamic resume, not its
1719
+ // static shell. Pinning the shell always-stale made every re-revealed link refetch the route tree.
1720
+ if (isStatic && meta.staleTime === DEFAULT_DYNAMIC_STALE_TIME_SECONDS) return configured;
1721
+ return meta.staleTime;
1722
+ }
1723
+
1724
+ function readSegmentTree(outPath: string, routeId: string): string | undefined {
1725
+ const file = treeSegmentFile(outPath, routeId);
1726
+ if (!existsSync(file)) return undefined;
1727
+ try {
1728
+ return readFileSync(file, 'utf8');
1729
+ } catch {
1730
+ return undefined;
1731
+ }
1732
+ }
1733
+
1734
+ function treeWithStaleTime(tree: string, staleTime: number, flight: boolean): string {
1735
+ const json = tree.startsWith('0:') ? tree.slice(2) : tree;
1736
+ try {
1737
+ const payload = JSON.parse(json) as { staleTime?: unknown };
1738
+ const body = JSON.stringify(
1739
+ payload.staleTime === staleTime ? payload : { ...payload, staleTime },
1740
+ );
1741
+ return flight ? `0:${body}` : body;
1742
+ } catch {
1743
+ return tree;
1744
+ }
1745
+ }
1746
+
1747
+ function cacheBustingToken(url: URL, headers?: Headers): string {
1748
+ let hash = 5381;
1749
+ // Like Next's setCacheBustingSearchParam, the token derives from the router
1750
+ // negotiation HEADERS too: the `/_tree` and `/_index` variants of one URL
1751
+ // must never share a token, or a Vary-ignoring CDN would replay one for the
1752
+ // other after a redirect strips the client's own cache-buster.
1753
+ const variant = headers
1754
+ ? `|${headers.get(NEXT_ROUTER_PREFETCH_HEADER) ?? ''}|${headers.get(NEXT_ROUTER_SEGMENT_PREFETCH_HEADER) ?? ''}|${headers.get('next-url') ?? ''}`
1755
+ : '';
1756
+ const input = `${url.pathname}?${url.searchParams.toString()}${variant}`;
1757
+ for (let index = 0; index < input.length; index++) {
1758
+ hash = ((hash << 5) + hash) ^ input.charCodeAt(index);
1759
+ }
1760
+ return (hash >>> 0).toString(36);
1761
+ }
1762
+
1763
+ function withRscQuery(url: URL, headers?: Headers): string {
1764
+ const next = new URL(url);
1765
+ next.searchParams.delete('_rsc');
1766
+ next.searchParams.set('_rsc', cacheBustingToken(next, headers));
1767
+ return next.href;
1768
+ }
1769
+
1770
+ // The public URL pattern of a route in the client's bracket form, split into
1771
+ // segments — the same shape the optimistic-routing trie keys on.
1772
+ function routePublicPattern(route: Pick<RouteManifestEntry, 'route'>): string[] {
1773
+ return route.route
1774
+ .replace(/:([a-zA-Z0-9_]+)\*/g, '[...$1]')
1775
+ .replace(/:([a-zA-Z0-9_]+)/g, '[$1]')
1776
+ .split('/')
1777
+ .filter(Boolean);
1778
+ }
1779
+
1780
+ /**
1781
+ * Static siblings of the route's deepest dynamic segment, for the RSC payload.
1782
+ * Next's flight data carries the sibling names of a dynamic trie level so the
1783
+ * client never predicts over a static route; the static-siblings suite asserts
1784
+ * the names appear in the server response.
1785
+ */
1786
+ function routeStaticChildren(
1787
+ routes: readonly RouteManifestEntry[],
1788
+ route: RouteManifestEntry,
1789
+ ): string[] {
1790
+ return staticSiblingNames(routes, routePublicPattern(route));
1791
+ }
1792
+
1793
+ /**
1794
+ * The trailing flight marker appended to a full-route RSC body: the leaf
1795
+ * segment name Next's flight payload carries (`__PAGE__`) plus the route
1796
+ * identity, in the same shape `fullRouteRscResponse` sends for a prefetch. An
1797
+ * HTML comment so it stays inert for anything that parses the body as markup.
1798
+ */
1799
+ function flightSegmentMarker(routeId: string, isStatic: boolean): string {
1800
+ return `\n<!--pnext-flight:${JSON.stringify({ route: routeId, isStatic, segment: '__PAGE__' })}-->`;
1801
+ }
1802
+
1803
+ /**
1804
+ * Remove a leading `<!DOCTYPE html>` from a streamed document body (first
1805
+ * chunk(s) only; the rest of the stream passes through untouched) and append
1806
+ * `tail` once the stream ends. Used on the full-route RSC path so an `RSC: 1`
1807
+ * response is never mistakable for a document while keeping the rendered
1808
+ * content byte-identical otherwise.
1809
+ */
1810
+ function stripDocumentDoctype(response: Response, tail = ''): Response {
1811
+ if (!response.body) return response;
1812
+ const prefix = '<!DOCTYPE html>';
1813
+ const decoder = new TextDecoder();
1814
+ const encoder = new TextEncoder();
1815
+ let buffered = '';
1816
+ let resolved = false;
1817
+ let tailEmitted = false;
1818
+ const emit = (controller: TransformStreamDefaultController<Uint8Array>, text: string) => {
1819
+ if (text) controller.enqueue(encoder.encode(text));
1820
+ };
1821
+ const transformed = response.body.pipeThrough(
1822
+ new TransformStream<Uint8Array, Uint8Array>({
1823
+ transform(chunk, controller) {
1824
+ if (resolved) {
1825
+ // The late-metadata section is not document bytes: the tail belongs
1826
+ // to the DOCUMENT, so it goes out ahead of the marker.
1827
+ const text = decoder.decode(chunk, { stream: true });
1828
+ const late = text.indexOf(LATE_METADATA_MARKER);
1829
+ if (late !== -1 && !tailEmitted) {
1830
+ tailEmitted = true;
1831
+ emit(controller, text.slice(0, late));
1832
+ emit(controller, tail);
1833
+ emit(controller, text.slice(late));
1834
+ return;
1835
+ }
1836
+ emit(controller, text);
1837
+ return;
1838
+ }
1839
+ buffered += decoder.decode(chunk, { stream: true });
1840
+ if (buffered.length < prefix.length && prefix.startsWith(buffered)) return;
1841
+ resolved = true;
1842
+ emit(controller, buffered.startsWith(prefix) ? buffered.slice(prefix.length) : buffered);
1843
+ },
1844
+ flush(controller) {
1845
+ if (!resolved) emit(controller, buffered);
1846
+ if (!tailEmitted) emit(controller, tail);
1847
+ },
1848
+ }),
1849
+ );
1850
+ return new Response(transformed, {
1851
+ status: response.status,
1852
+ statusText: response.statusText,
1853
+ headers: response.headers,
1854
+ });
1855
+ }
1856
+
1857
+ function fullRouteRscResponse(
1858
+ routeId: string,
1859
+ isStatic: boolean,
1860
+ staleTime: number,
1861
+ staticChildren: string[] = [],
1862
+ ): Response {
1863
+ const payload = {
1864
+ route: routeId,
1865
+ isStatic,
1866
+ segment: '__PAGE__',
1867
+ ...(staticChildren.length > 0 ? { staticChildren } : {}),
1868
+ };
1869
+ return new Response(`0:${JSON.stringify(payload)}`, {
1870
+ status: 200,
1871
+ headers: {
1872
+ 'content-type': RSC_CONTENT_TYPE_HEADER,
1873
+ 'x-nextjs-stale-time': String(staleTime),
1874
+ 'x-nextjs-deployment-id': deploymentId(),
1875
+ 'cache-control': 'private, no-store',
1876
+ },
1877
+ });
1878
+ }
1879
+
1880
+ function withDeploymentId(response: Response): Response {
1881
+ response.headers.set('x-nextjs-deployment-id', deploymentId());
1882
+ return response;
1883
+ }
1884
+
1885
+ let registered = false;
1886
+ // Host-scoped guard: a fresh host must be registered into again.
1887
+ onExtensionHostReset(() => (registered = false));
1888
+
1889
+ export function registerSegmentExtensions(config: ResolvedConfig): void {
1890
+ if (registered) return;
1891
+ if (!nextCompatEnabled(config)) return;
1892
+ registered = true;
1893
+ registerRevalidationInvalidator(() => {
1894
+ segmentRevalidationVersion += 1;
1895
+ });
1896
+ // Register the responder FIRST so `/_tree` is handled before action dispatch.
1897
+ registerRequestInterceptors(segmentPrefetchInterceptor);
1898
+ registerResponseFinalizers(
1899
+ prefetchStaleTimeFinalizer,
1900
+ rscContentTypeFinalizer,
1901
+ rscDeploymentFinalizer,
1902
+ );
1903
+ }