@wular/pnext 0.0.3 → 0.0.6

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