@wular/pnext 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,1770 @@
1
+ // Build-time validation pass (COMPAT - may import core freely).
2
+ //
3
+ // One route-tree + module-graph scan emitting Next-compatible build-error strings. Runs as a build
4
+ // step; a validation failure throws, which aborts `pnext build` - exactly what the Next e2e suites
5
+ // assert (`next build` FAILS with a specific message, substring-matched from stderr).
6
+ //
7
+ // The pass reads ONLY the scanned route table plus route source files on disk. It never renders.
8
+ // Checks: missing root layout; an app/ page conflicting with a pages/ file; two parallel
9
+ // (route-group) pages resolving to the same path; useSearchParams() without a Suspense boundary;
10
+ // undefined/non-component default export; and the output:'export' extras (exportPathMap + app, a
11
+ // dynamic route without generateStaticParams, route handlers without static opt-ins, force-dynamic).
12
+
13
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import type { ResolvedConfig } from '../../config';
16
+ import { extraPageExtensions } from '../../extensions';
17
+ import type { RouteManifestEntry } from '../../types';
18
+ import { getNextConfig } from '../next/config-loader';
19
+ import {
20
+ conflictingAppAndPageMessage,
21
+ conflictingParallelPagesMessage,
22
+ dynamicForceDynamicWithExportMessage,
23
+ forceDynamicPageWithExportMessage,
24
+ exportPathMapWithAppDirMessage,
25
+ incompatibleCacheComponentsSegmentConfigMessage,
26
+ missingDefaultParallelRouteMessage,
27
+ missingGenerateStaticParamsForExportMessage,
28
+ missingRootLayoutMessage,
29
+ missingSuspenseWithCsrBailoutMessage,
30
+ routeHandlerNotStaticWithExportMessage,
31
+ undefinedDefaultExportMessage,
32
+ unresolvedCodemodCommentMessage,
33
+ } from './errors';
34
+
35
+ const BASE_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js', 'mjs'] as const;
36
+
37
+ /**
38
+ * A build-error the validation pass surfaces. Carries only Next's message; the
39
+ * build's catch surfaces `.message` to stderr, where the harness greps it.
40
+ */
41
+ export class PnextBuildValidationError extends Error {
42
+ constructor(message: string) {
43
+ super(message);
44
+ this.name = 'PnextBuildValidationError';
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Run every validation check; throws PnextBuildValidationError on the first
50
+ * failure. Fully synchronous: every check reads through `readSource`, so the
51
+ * pass never yields and each file is read at most once.
52
+ */
53
+ export function validateBuild(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
54
+ sourceCache.clear();
55
+ const pageRoutes = routes.filter(route => route.kind === 'page' && !route.interception);
56
+
57
+ // Warnings first: Next surfaces them while compiling, i.e. BEFORE any
58
+ // app-structure error aborts the build. An app that trips both (the
59
+ // invalid-reexport fixture has no root layout at all) must still show them.
60
+ warnBuildDiagnostics(config, routes);
61
+ validateRootLayout(config, pageRoutes);
62
+ validateAppPagesConflict(config);
63
+ validateParallelPageConflicts(config, pageRoutes);
64
+ validateMissingSlotDefaults(config, routes);
65
+ validateCodemodComments(routes);
66
+ validateDefaultExports(config, routes);
67
+ validateSearchParamsSuspense(config, pageRoutes);
68
+ validateUseCacheDirectives(config, routes);
69
+ validateUseCacheSearchParams(routes);
70
+ validateUseCacheHangingInputs(routes);
71
+ validateCacheComponentsSegmentConfigs(config, routes);
72
+ validateInstantStaticShells(config, routes);
73
+ validateClientSegmentConfigs(config, routes);
74
+ validateImageLoaderFile(config);
75
+ validateStaticImageImports(config, routes);
76
+ validateExportMode(config, routes);
77
+ }
78
+
79
+ function validateInstantStaticShells(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
80
+ if (!cacheComponentsIsEnabled()) return;
81
+ for (const route of routes) {
82
+ if (route.kind !== 'page') continue;
83
+ const pageSource = readSource(route.file);
84
+ if (!pageSource || !/\bexport\s+const\s+unstable_instant\s*=\s*false\b/.test(pageSource))
85
+ continue;
86
+ if (!/\bconnection\s*\(/.test(pageSource)) continue;
87
+ const staticParent = layoutChain(config.appPath, route.file).some(file => {
88
+ const source = readSource(file);
89
+ return Boolean(source && /\bexport\s+const\s+unstable_instant\s*=\s*true\b/.test(source));
90
+ });
91
+ if (!staticParent) continue;
92
+ throw new PnextBuildValidationError(
93
+ `Error occurred during prerendering page "${route.route || '/'}": Next.js encountered uncached data during prerendering.`,
94
+ );
95
+ }
96
+ }
97
+
98
+ function validateUseCacheHangingInputs(routes: RouteManifestEntry[]): void {
99
+ if (!cacheComponentsIsEnabled()) return;
100
+ const failures: string[] = [];
101
+ const timeout =
102
+ 'Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or dynamic data were used inside "use cache".';
103
+ for (const route of routes) {
104
+ if (route.kind !== 'page') continue;
105
+ const source = readSource(route.file);
106
+ if (!source || useCacheDirectives(source).length === 0) continue;
107
+ const routePath = nextPagePath(route);
108
+ const thrown = /\bthrow\s+new\s+Error\(\s*['"]([^'"]+)['"]\s*\)/.exec(source);
109
+ // Only a throw the PRERENDER would execute fails the build. A page that
110
+ // reads request data outside its cache scopes never prerenders, so its
111
+ // throw surfaces as a runtime error log instead (use-cache-runtime-error /
112
+ // use-cache-catch-error both `await connection()` before the cached call).
113
+ if (thrown?.[1] && !readsRequestDataOutsideCache(source)) {
114
+ failures.push(
115
+ `Error occurred prerendering page "${routePath}". Read more: https://nextjs.org/docs/messages/prerender-error\nError: ${thrown[1]}`,
116
+ );
117
+ continue;
118
+ }
119
+ // Only an await of a hanging input INSIDE a cache scope hangs the fill; an
120
+ // `await params` outside the cached function resolves normally (e.g. under
121
+ // a Suspense boundary during a fallback-shell prerender) and only plain
122
+ // values cross the cache boundary. Scope the heuristic to cache bodies.
123
+ // Private scopes never hang the build: they are excluded from prerenders
124
+ // entirely (evaluated per-request), so only shared cache bodies count.
125
+ const scoped = useCacheScopedSource(source).filter(scope => scope.kind !== 'private');
126
+ // `await params` inside a cache scope only hangs when the params can never resolve during the
127
+ // prerender. With generateStaticParams the concrete prerenders pass resolved params (they join the
128
+ // cache key) and the fallback-shell prerender aborts the fill to a dynamic hole, so the heuristic
129
+ // must not flag them.
130
+ //
131
+ // A hanging input awaited from the cached function's OWN parameters is the legitimate
132
+ // runtime-prefetch pattern: the page awaits `params`/searchParams outside the cache and passes the
133
+ // promise IN as a vary key. Only a closed-over page-level `params`/promise still hangs, so exempt
134
+ // the scope's declared parameters.
135
+ const isHangingInput = (ident: string): boolean =>
136
+ ident === 'promise' ||
137
+ /^[A-Za-z_$][\w$]*Promise$/.test(ident) ||
138
+ (!route.hasStaticParams && ident === 'params');
139
+ const hangs = scoped.some(scope => {
140
+ const params = new Set(scope.params);
141
+ return awaitedIdentifiers(scope.body).some(
142
+ ident => isHangingInput(ident) && !params.has(ident),
143
+ );
144
+ });
145
+ // The exemption above (a cached function awaiting its OWN parameter) only holds when the CALLER
146
+ // passes something that resolves during the prerender. Passing an uncached promise IN
147
+ // (`<Foo promise={fetchData()} />`, `indirection(getUncachedData())`) is the hanging-input
148
+ // violation: the fill waits on request-time IO a prerender never resolves. Detect it at the cached
149
+ // function's call sites. A value produced by ANOTHER cached function resolves from the cache
150
+ // during the prerender, so only calls into uncached code count.
151
+ const cachedNames = new Set(scoped.map(scope => scope.name).filter(name => name !== ''));
152
+ // A REQUEST-BOUND promise (`cookies()`, `headers()`, a `next/root-params` accessor) is not a
153
+ // hanging input either: such a page never statically prerenders - the request API postpones the
154
+ // boundary and the route builds as runtime-prefetchable/dynamic - so the fill resolves from
155
+ // request data instead of hanging.
156
+ const exemptNames = new Set([...cachedNames, ...requestApiNames(source)]);
157
+ const receivesUncachedPromise = scoped.some(
158
+ scope =>
159
+ scope.name !== '' &&
160
+ scope.params.length > 0 &&
161
+ passesUncachedPromiseInto(source, scope.name, exemptNames),
162
+ );
163
+ if (!hangs && !receivesUncachedPromise) continue;
164
+ failures.push(
165
+ `${timeout}\nError occurred prerendering page "${routePath}". Read more: https://nextjs.org/docs/messages/prerender-error`,
166
+ );
167
+ }
168
+ if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'));
169
+ }
170
+
171
+ function validateUseCacheSearchParams(routes: RouteManifestEntry[]): void {
172
+ if (!cacheComponentsIsEnabled()) return;
173
+ const failures: string[] = [];
174
+ for (const route of routes) {
175
+ if (route.kind !== 'page') continue;
176
+ const source = readSource(route.file);
177
+ if (!source || useCacheDirectives(source).length === 0) continue;
178
+ // Only an await INSIDE a shared cache scope is the E842 violation. A
179
+ // `use cache: private` scope MAY read searchParams (it is excluded from
180
+ // prerenders and evaluated per-request), and an await outside any cache
181
+ // body is an ordinary dynamic read.
182
+ const scoped = useCacheScopedSource(source).filter(scope => scope.kind !== 'private');
183
+ // Awaiting `searchParams` that is the cached function's OWN parameter is the
184
+ // legal runtime-prefetch pattern (the page passes the searchParams promise
185
+ // into `publicCache(searchParams)` as a vary key). Only a closed-over
186
+ // page-level `searchParams` read is the E842 violation.
187
+ const violates = scoped.some(scope => {
188
+ const params = new Set(scope.params);
189
+ return awaitedIdentifiers(scope.body).some(
190
+ ident => ident === 'searchParams' && !params.has(ident),
191
+ );
192
+ });
193
+ if (!violates) continue;
194
+ const routePath = route.route || '/';
195
+ failures.push(
196
+ `Route ${routePath} used \`searchParams\` inside "use cache". Accessing dynamic request data inside a cache scope is not supported. ` +
197
+ 'If you need some search params inside a cached function await `searchParams` outside of the cached function and pass only the required search params as arguments to the cached function. ' +
198
+ 'See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache\n' +
199
+ `Error occurred prerendering page "${routePath}"`,
200
+ );
201
+ }
202
+ if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'));
203
+ }
204
+
205
+ function validateClientSegmentConfigs(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
206
+ for (const route of routes) {
207
+ if (route.kind !== 'page') continue;
208
+ const source = readSource(route.file);
209
+ if (!source || !hasDirective(source, 'use client')) continue;
210
+ if (/\bexport\s+const\s+unstable_instant\b/.test(source)) {
211
+ throw new PnextBuildValidationError(
212
+ `"unstable_instant" is a route segment config and can only be used when the segment is a Server Component module. Remove the "use client" directive`,
213
+ );
214
+ }
215
+ if (
216
+ getNextConfig().output === 'export' &&
217
+ /\bexport\s+(?:async\s+)?function\s+generateStaticParams\b/.test(source)
218
+ ) {
219
+ throw new PnextBuildValidationError(
220
+ `Page "/${appDirRelativeFile(config, route.file).replace(/\.[^.]+$/, '')}" cannot use both "use client" and export function "generateStaticParams()".`,
221
+ );
222
+ }
223
+ }
224
+ }
225
+
226
+ function validateImageLoaderFile(config: ResolvedConfig): void {
227
+ const images = getNextConfig().images;
228
+ if (!images || typeof images !== 'object' || Array.isArray(images)) return;
229
+ const loaderFile = (images as Record<string, unknown>).loaderFile;
230
+ if (typeof loaderFile !== 'string' || loaderFile.length === 0) return;
231
+ const file = path.resolve(config.root, loaderFile);
232
+ const source = readSource(file);
233
+ if (source && /\bexport\s+default\b|\bmodule\.exports\s*=/.test(source)) return;
234
+ throw new PnextBuildValidationError(
235
+ 'images.loaderFile detected but the file is missing default export.\n' +
236
+ 'Read more: https://nextjs.org/docs/messages/invalid-images-config',
237
+ );
238
+ }
239
+
240
+ function validateStaticImageImports(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
241
+ const seen = new Set<string>();
242
+ for (const route of routes) {
243
+ for (const file of routeFiles(route)) {
244
+ if (seen.has(file)) continue;
245
+ seen.add(file);
246
+ if (!isAppSourceFile(config, file)) continue;
247
+ const source = readSource(file);
248
+ if (!source) continue;
249
+ const imports = source.matchAll(
250
+ /\bimport\s+(?:[^'";]+?\s+from\s+)?['"]([^'"]+\.(?:png|jpe?g|gif|webp|avif|ico|bmp|svg))['"]/gi,
251
+ );
252
+ for (const match of imports) {
253
+ const specifier = match[1]!;
254
+ if (!specifier.startsWith('.')) continue;
255
+ if (existsSync(path.resolve(path.dirname(file), specifier))) continue;
256
+ throw new PnextBuildValidationError(
257
+ `Module not found: Can't resolve '${specifier}'\n./app/${appDirRelativeFile(config, file)}`,
258
+ );
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ function warnBuildDiagnostics(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
265
+ const nextConfig = getNextConfig();
266
+ const experimental = nextConfig.experimental as Record<string, unknown> | undefined;
267
+ if (hasBabelConfig(config.root) && experimental?.forceSwcTransforms !== true) {
268
+ console.warn('Disabled SWC as replacement for Babel because of custom Babel configuration');
269
+ }
270
+ if (
271
+ typeof nextConfig.outputFileTracingRoot !== 'string' &&
272
+ !(
273
+ nextConfig.turbopack &&
274
+ typeof nextConfig.turbopack === 'object' &&
275
+ typeof (nextConfig.turbopack as Record<string, unknown>).root === 'string'
276
+ )
277
+ ) {
278
+ warnMultipleLockfiles(config.root);
279
+ }
280
+
281
+ let warnedEdgeRuntime = false;
282
+ for (const file of new Set(routes.flatMap(route => routeFiles(route)))) {
283
+ if (!isAppSourceFile(config, file)) continue;
284
+ const source = readSource(file);
285
+ if (!source) continue;
286
+ if (!warnedEdgeRuntime && /\bruntime\s*=\s*['"](?:edge|experimental-edge)['"]/.test(source)) {
287
+ console.warn('The Edge Runtime is deprecated. You can use the "nodejs" runtime instead.');
288
+ warnedEdgeRuntime = true;
289
+ }
290
+ for (const key of ['runtime', 'preferredRegion'] as const) {
291
+ if (new RegExp(`\\bexport\\s*\\{[^}]*\\b${key}\\b[^}]*\\}\\s*from\\s*['"]`).test(source)) {
292
+ console.warn(
293
+ `Next.js can't recognize the exported \`${key}\` field in ${toPosix(path.relative(config.root, file))}`,
294
+ );
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ function hasBabelConfig(root: string): boolean {
301
+ return [
302
+ '.babelrc',
303
+ '.babelrc.json',
304
+ '.babelrc.js',
305
+ '.babelrc.cjs',
306
+ 'babel.config.js',
307
+ 'babel.config.cjs',
308
+ ].some(name => existsSync(path.join(root, name)));
309
+ }
310
+
311
+ function warnMultipleLockfiles(root: string): void {
312
+ // A configured `outputFileTracingRoot` is authoritative — Next stays silent.
313
+ if (typeof getNextConfig().outputFileTracingRoot === 'string') return;
314
+ const lockfiles: string[] = [];
315
+ let current = root;
316
+ while (true) {
317
+ for (const name of [
318
+ 'package-lock.json',
319
+ 'yarn.lock',
320
+ 'pnpm-lock.yaml',
321
+ 'bun.lock',
322
+ 'bun.lockb',
323
+ ]) {
324
+ const file = path.join(current, name);
325
+ if (existsSync(file)) lockfiles.push(file);
326
+ }
327
+ // A `pnpm-workspace.yaml` marks the definitive workspace root: stop the walk
328
+ // there (nothing above it belongs to this project), but still report what was
329
+ // collected — a lockfile written between the app and the workspace root is
330
+ // exactly the ambiguity Next warns about.
331
+ if (existsSync(path.join(current, 'pnpm-workspace.yaml'))) break;
332
+ const parent = path.dirname(current);
333
+ if (parent === current) break;
334
+ current = parent;
335
+ }
336
+ // The ambiguity is about DIRECTORIES: two lockfiles side by side in one
337
+ // directory still name a single root.
338
+ const dirs = [...new Set(lockfiles.map(file => path.dirname(file)))];
339
+ if (dirs.length < 2) return;
340
+ // Next selects the closest lockfile's directory as the root and lists the rest.
341
+ const selected = dirs[0];
342
+ const additional = lockfiles.filter(file => path.dirname(file) !== selected);
343
+ console.warn(
344
+ '⚠ Warning: Next.js inferred your workspace root, but it may not be correct.\n' +
345
+ ` We detected multiple lockfiles and selected the directory of ${selected} as the root directory.\n` +
346
+ " To silence this warning, set `outputFileTracingRoot` in your Next.js config, or consider removing one of the lockfiles if it's not needed.\n" +
347
+ ' See https://nextjs.org/docs/app/api-reference/config/next-config-js/output#caveats for more information.\n' +
348
+ ' Detected additional lockfiles: \n' +
349
+ additional.map(file => ` * ${file}`).join('\n'),
350
+ );
351
+ }
352
+
353
+ /**
354
+ * One read per file per build. A dozen validators walk the same route files, and `route.sourceFiles`
355
+ * is the route's whole graph, so a shared layout's modules appear in every route's list. A build
356
+ * never sees its sources change under it.
357
+ */
358
+ const sourceCache = new Map<string, string | undefined>();
359
+
360
+ function readSource(file: string): string | undefined {
361
+ if (sourceCache.has(file)) return sourceCache.get(file);
362
+ let source: string | undefined;
363
+ try {
364
+ source = readFileSync(file, 'utf8');
365
+ } catch {
366
+ source = undefined;
367
+ }
368
+ sourceCache.set(file, source);
369
+ return source;
370
+ }
371
+
372
+ function routeFiles(route: RouteManifestEntry): string[] {
373
+ return [...new Set([route.file, ...route.sourceFiles])];
374
+ }
375
+
376
+ function isAppSourceFile(config: ResolvedConfig, file: string): boolean {
377
+ if (isInside(config.outPath, file)) return false;
378
+ const relative = path.relative(config.root, file);
379
+ if (relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative)) return true;
380
+ return (
381
+ file.includes(`${path.sep}source-app${path.sep}`) ||
382
+ file.includes(`${path.sep}source-pages${path.sep}`)
383
+ );
384
+ }
385
+
386
+ function isInside(parent: string, file: string): boolean {
387
+ const relative = path.relative(parent, file);
388
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
389
+ }
390
+
391
+ function hasDirective(source: string, directive: string): boolean {
392
+ return directivePrologue(source, 0).includes(directive);
393
+ }
394
+
395
+ /** Whether `cacheComponents` (or a legacy alias) is enabled in next.config. */
396
+ function cacheComponentsIsEnabled(): boolean {
397
+ const nextConfig = getNextConfig();
398
+ const experimental = nextConfig.experimental as Record<string, unknown> | undefined;
399
+ return (
400
+ nextConfig.cacheComponents === true ||
401
+ experimental?.cacheComponents === true ||
402
+ experimental?.useCache === true ||
403
+ experimental?.dynamicIO === true ||
404
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
405
+ process.env.__NEXT_CACHE_COMPONENTS === 'true'
406
+ );
407
+ }
408
+
409
+ // Segment-config exports `cacheComponents` forbids: each forces a dynamic / caching / runtime disposition
410
+ // the cacheComponents model derives per-boundary instead. Presence of the export (any value) is the error.
411
+ // `dynamicParams` is the leaf param-fallback control; the rest are the classic dynamic-rendering knobs.
412
+ //
413
+ // `runtime` is handled separately (value-based) rather than listed here: only a non-`edge` runtime is
414
+ // forbidden. `runtime = 'edge'` stays valid, while any other value is rejected with the same "remove it"
415
+ // error. See the runtime check in validateCacheComponentsSegmentConfigs.
416
+ const CACHE_COMPONENTS_FORBIDDEN_SEGMENT_CONFIGS = [
417
+ 'dynamic',
418
+ 'dynamicParams',
419
+ 'revalidate',
420
+ 'fetchCache',
421
+ ] as const;
422
+
423
+ const SEGMENT_CONFIG_FILE = /(?:^|\/)(?:page|layout|route|default)\.(?:tsx|ts|jsx|js|mjs)$/;
424
+
425
+ /**
426
+ * Under `cacheComponents`, error when a route segment (page/layout/route/
427
+ * default) exports a config that pins dynamic/caching/runtime behavior. Next
428
+ * fails the build listing every offending file with the config name; the suites
429
+ * grep the `./app/...` path and the message fragment. Each unique segment file
430
+ * is checked once (a shared layout appears in many routes' sourceFiles) and the
431
+ * FIRST forbidden export in the file (by source position) is reported, matching
432
+ * Next's one-error-per-file build output.
433
+ */
434
+ function validateCacheComponentsSegmentConfigs(
435
+ config: ResolvedConfig,
436
+ routes: RouteManifestEntry[],
437
+ ): void {
438
+ if (!cacheComponentsIsEnabled()) return;
439
+ const projectRoot = path.resolve(config.appPath, '..');
440
+ const seen = new Set<string>();
441
+ const failures: string[] = [];
442
+ for (const route of routes) {
443
+ for (const file of route.sourceFiles) {
444
+ if (seen.has(file)) continue;
445
+ if (!SEGMENT_CONFIG_FILE.test(toPosix(file))) continue;
446
+ seen.add(file);
447
+ if (!isProjectSourceFile(projectRoot, file)) continue;
448
+ const source = readSource(file);
449
+ if (source === undefined) continue;
450
+ // Skip pnext-generated pages-router wrappers. In a hybrid app the pages/
451
+ // materializer emits an app `page.js` that re-exports from a `source-pages`
452
+ // symlink and, for getServerSideProps pages, injects
453
+ // `export const dynamic = 'force-dynamic'`. cacheComponents' forbidden
454
+ // segment-config check is an app-router concern; a pages-router page uses
455
+ // getServerSideProps legitimately and never authored that config, so the
456
+ // synthetic wrapper must not be flagged.
457
+ if (isMaterializedPagesWrapper(source)) continue;
458
+ let earliest: { key: string; index: number } | undefined;
459
+ for (const key of CACHE_COMPONENTS_FORBIDDEN_SEGMENT_CONFIGS) {
460
+ const match = new RegExp(`(?:^|[;\\n])\\s*export\\s+const\\s+${key}\\b`).exec(source);
461
+ if (match && (earliest === undefined || match.index < earliest.index)) {
462
+ earliest = { key, index: match.index };
463
+ }
464
+ }
465
+ // `runtime` is forbidden under cacheComponents ONLY when it pins a non-`edge` runtime.
466
+ // `runtime = 'edge'` remains a valid opt-in; Next rejects any other runtime value with the same
467
+ // "remove it" error, and that rejection propagates from a layout to every page under it.
468
+ // Under `experimental.useCache` ALONE there is no dynamic-shell opt-in, so EVERY runtime value -
469
+ // `edge` included - is rejected.
470
+ const runtimeMatch =
471
+ /(?:^|[;\n])\s*export\s+const\s+runtime\s*=\s*['"]([^'"]+)['"]/.exec(source);
472
+ if (
473
+ runtimeMatch &&
474
+ (runtimeMatch[1] !== 'edge' || useCacheOnlyIsEnabled()) &&
475
+ (earliest === undefined || runtimeMatch.index < earliest.index)
476
+ ) {
477
+ earliest = { key: 'runtime', index: runtimeMatch.index };
478
+ }
479
+ if (!earliest) continue;
480
+ // Under `experimental.useCache` ONLY (no cacheComponents alias), Next
481
+ // fails at webpack compile with a differently-worded error and a full
482
+ // webpack-errors block that the use-cache-segment-configs suite
483
+ // inline-snapshots — emit that exact block for the first offending file.
484
+ if (useCacheOnlyIsEnabled()) {
485
+ throw new PnextBuildValidationError(useCacheSegmentConfigBuildError(source, earliest.key));
486
+ }
487
+ // Next prints the offending file on its own line ahead of the message. A layout is not a webpack
488
+ // entry - it is imported by every page beneath it, so its config error propagates and webpack attaches
489
+ // an "Import trace for requested module" block naming each importing page then the layout. Reproduce
490
+ // one trace block per importing page so the propagation suite's page-to-layout grep matches, which
491
+ // also proves the layout config reaches the pages.
492
+ let trace = '';
493
+ if (/(?:^|\/)layout\.(?:tsx|ts|jsx|js|mjs)$/.test(toPosix(file))) {
494
+ const importers = new Set<string>();
495
+ for (const other of routes) {
496
+ if (other.kind !== 'page') continue;
497
+ if (!other.sourceFiles.includes(file)) continue;
498
+ importers.add(`./app/${appDirRelativeFile(config, other.file)}`);
499
+ }
500
+ const layoutRel = `./app/${appDirRelativeFile(config, file)}`;
501
+ for (const importer of [...importers].sort()) {
502
+ trace += `\n\nImport trace for requested module:\n${importer}\n${layoutRel}`;
503
+ }
504
+ }
505
+ failures.push(
506
+ `./app/${appDirRelativeFile(config, file)}\n` +
507
+ incompatibleCacheComponentsSegmentConfigMessage(earliest.key) +
508
+ trace,
509
+ );
510
+ }
511
+ }
512
+ if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'));
513
+ }
514
+
515
+ /** Enabled via `experimental.useCache` alone (no cacheComponents alias). */
516
+ function useCacheOnlyIsEnabled(): boolean {
517
+ const nextConfig = getNextConfig();
518
+ const experimental = nextConfig.experimental as Record<string, unknown> | undefined;
519
+ return (
520
+ experimental?.useCache === true &&
521
+ nextConfig.cacheComponents !== true &&
522
+ experimental?.cacheComponents !== true &&
523
+ experimental?.dynamicIO !== true &&
524
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
525
+ process.env.__NEXT_CACHE_COMPONENTS !== 'true'
526
+ );
527
+ }
528
+
529
+ /**
530
+ * Next's webpack build failure for a forbidden segment-config export under `experimental.useCache`. The
531
+ * suite slices everything after "Failed to compile", replaces any line containing `__next_edge_ssr_entry__`
532
+ * with a fixed placeholder, and inline-snapshot-matches the block - so the file line must carry that token,
533
+ * the code frame keeps the trailing space of the `N | ` prefix on empty source lines, and nothing may print
534
+ * after the final build-failed line.
535
+ */
536
+ function useCacheSegmentConfigBuildError(source: string, key: string): string {
537
+ const lines = source.split('\n');
538
+ const exportRe = new RegExp(`\\bexport\\s+const\\s+${key}\\b`);
539
+ let line = 1;
540
+ let column = 1;
541
+ for (let i = 0; i < lines.length; i += 1) {
542
+ const match = new RegExp(`\\b${key}\\b`).exec(lines[i] ?? '');
543
+ if (exportRe.test(lines[i] ?? '') && match) {
544
+ line = i + 1;
545
+ column = match.index + 1;
546
+ break;
547
+ }
548
+ }
549
+ const last = Math.min(lines.length, line + 3);
550
+ const width = String(last).length;
551
+ const gutter = ' '.repeat(width + 2);
552
+ const frame: string[] = [`${gutter},-[${line}:1]`];
553
+ for (let n = line; n <= last; n += 1) {
554
+ frame.push(` ${String(n).padStart(width)} | ${lines[n - 1] ?? ''}`);
555
+ if (n === line) {
556
+ frame.push(`${gutter}: ${' '.repeat(column - 1)}${'^'.repeat(key.length)}`);
557
+ }
558
+ }
559
+ frame.push(`${gutter}\`----`);
560
+ return (
561
+ 'Failed to compile.\n' +
562
+ '\n' +
563
+ '__next_edge_ssr_entry__\n' +
564
+ `Error: x Route segment config "${key}" is not compatible with \`nextConfig.experimental.useCache\`. Please remove it.\n` +
565
+ `${frame.join('\n')}\n` +
566
+ '\n' +
567
+ 'Import trace for requested module:\n' +
568
+ '__next_edge_ssr_entry__\n' +
569
+ '\n' +
570
+ '\n' +
571
+ '> Build failed because of webpack errors'
572
+ );
573
+ }
574
+
575
+ // A pnext-generated pages-router wrapper re-exports from the materializer's
576
+ // `source-pages` symlink; that import specifier is the stable marker separating
577
+ // synthetic wrappers from user-authored app segments.
578
+ function isMaterializedPagesWrapper(source: string): boolean {
579
+ return /\bfrom\s+['"][^'"]*source-pages[/\\]/.test(source);
580
+ }
581
+
582
+ function validateUseCacheDirectives(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
583
+ const nextConfig = getNextConfig();
584
+ const experimental = nextConfig.experimental as Record<string, unknown> | undefined;
585
+ const enabled =
586
+ nextConfig.cacheComponents === true ||
587
+ experimental?.cacheComponents === true ||
588
+ experimental?.useCache === true ||
589
+ experimental?.dynamicIO === true ||
590
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
591
+ process.env.__NEXT_CACHE_COMPONENTS === 'true';
592
+ const kinds = new Set<string>(['default', 'private', 'remote']);
593
+ const configured = nextConfig.cacheHandlers;
594
+ if (configured && typeof configured === 'object' && !Array.isArray(configured)) {
595
+ for (const kind of Object.keys(configured)) kinds.add(kind);
596
+ }
597
+
598
+ const projectRoot = path.resolve(config.appPath, '..');
599
+ // sourceFiles is the route's whole graph; shared modules repeat across every
600
+ // route, so visit each file once.
601
+ const seen = new Set<string>();
602
+ for (const route of routes) {
603
+ for (const file of route.sourceFiles) {
604
+ if (seen.has(file)) continue;
605
+ seen.add(file);
606
+ if (!isProjectSourceFile(projectRoot, file)) continue;
607
+ const source = readSource(file);
608
+ if (source === undefined) continue;
609
+ for (const directive of useCacheDirectives(source)) {
610
+ if (!enabled) {
611
+ throw new PnextBuildValidationError(
612
+ useCacheNotEnabledBuildError(`./app/${appDirRelativeFile(config, file)}`, source),
613
+ );
614
+ }
615
+ const kind = directive.kind;
616
+ if (!kinds.has(kind)) {
617
+ throw new PnextBuildValidationError(
618
+ unknownCacheKindBuildError(`./app/${appDirRelativeFile(config, file)}`, source, kind),
619
+ );
620
+ }
621
+ }
622
+ }
623
+ }
624
+ }
625
+
626
+ function isProjectSourceFile(projectRoot: string, file: string): boolean {
627
+ const relative = path.relative(projectRoot, file);
628
+ return (
629
+ relative !== '' &&
630
+ !relative.startsWith('..') &&
631
+ !path.isAbsolute(relative) &&
632
+ !relative.split(path.sep).includes('node_modules')
633
+ );
634
+ }
635
+
636
+ export function useCacheDirectives(source: string): { kind: string }[] {
637
+ const directives = [
638
+ ...directivePrologue(source, 0),
639
+ ...functionBodies(source).flatMap(offset => directivePrologue(source, offset)),
640
+ ];
641
+ return directives.flatMap(value => {
642
+ const match = /^use cache(?:\s*:\s*([\w-]+))?$/.exec(value);
643
+ return match ? [{ kind: match[1] ?? 'default' }] : [];
644
+ });
645
+ }
646
+
647
+ /**
648
+ * The source regions that execute inside a 'use cache' scope: the whole module
649
+ * when the directive sits in the module prologue, plus each function body whose
650
+ * prologue carries the directive (from its opening brace to the matching close).
651
+ * Each scope carries its cache kind (`default`, `private`, a custom handler
652
+ * name) so callers can exempt private scopes. Used to scope hanging-input /
653
+ * searchParams heuristics to code that actually runs in a cache fill rather
654
+ * than the whole file.
655
+ */
656
+ function useCacheScopedSource(
657
+ source: string,
658
+ ): { kind: string; body: string; params: string[]; name: string }[] {
659
+ const kindOf = (value: string) => /^use cache(?:\s*:\s*([\w-]+))?$/.exec(value);
660
+ for (const directive of directivePrologue(source, 0)) {
661
+ // A module-level directive caches the whole file; there is no single cached
662
+ // function whose parameters could be legal vary keys, so `params` is empty.
663
+ const match = kindOf(directive);
664
+ if (match) return [{ kind: match[1] ?? 'default', body: source, params: [], name: '' }];
665
+ }
666
+ const scopes: { kind: string; body: string; params: string[]; name: string }[] = [];
667
+ for (const scope of functionScopes(source)) {
668
+ for (const directive of directivePrologue(source, scope.offset)) {
669
+ const match = kindOf(directive);
670
+ if (!match) continue;
671
+ scopes.push({
672
+ kind: match[1] ?? 'default',
673
+ body: source.slice(scope.offset, functionBodyEnd(source, scope.offset)),
674
+ params: scope.params,
675
+ name: scope.name,
676
+ });
677
+ break;
678
+ }
679
+ }
680
+ return scopes;
681
+ }
682
+
683
+ /**
684
+ * Whether `source` passes an unawaited call expression (an uncached promise) into the cached function bound
685
+ * to `name` - either as a call argument or as a JSX prop. An identifier (`params`, `searchParams`, an
686
+ * already-awaited value) is NOT a hanging input: those resolve during the prerender and join the cache key.
687
+ * `resolvableNames` are callees whose promises DO settle during the prerender (other cached functions and
688
+ * request APIs).
689
+ */
690
+ function passesUncachedPromiseInto(
691
+ source: string,
692
+ name: string,
693
+ resolvableNames: ReadonlySet<string>,
694
+ ): boolean {
695
+ const isUncached = (expression: string) =>
696
+ isUncachedPromiseExpression(expression, resolvableNames);
697
+ const pattern = new RegExp(`(<)?\\b${escapeRegExp(name)}\\b`, 'g');
698
+ for (const match of source.matchAll(pattern)) {
699
+ const index = match.index;
700
+ if (index === undefined || !isCodeOffset(source, index)) continue;
701
+ const after = index + match[0].length;
702
+ if (match[1] === '<') {
703
+ if (jsxPropsPassUncachedPromise(source, after, isUncached)) return true;
704
+ continue;
705
+ }
706
+ const open = source.slice(after).search(/\S/);
707
+ if (open === -1 || source[after + open] !== '(') continue;
708
+ const args = balancedFrom(source, after + open, '(', ')');
709
+ if (args !== undefined && splitTopLevelCommas(args).some(isUncached)) return true;
710
+ }
711
+ return false;
712
+ }
713
+
714
+ /** Scan a JSX element's attributes (from just past `<Name`) for `prop={call()}`. */
715
+ function jsxPropsPassUncachedPromise(
716
+ source: string,
717
+ offset: number,
718
+ isUncached: (expression: string) => boolean,
719
+ ): boolean {
720
+ for (let cursor = offset; cursor < source.length; cursor++) {
721
+ const current = source[cursor]!;
722
+ if (current === '>') return false;
723
+ if (current !== '{') continue;
724
+ const expression = balancedFrom(source, cursor, '{', '}');
725
+ if (expression === undefined) return false;
726
+ if (isUncached(expression)) return true;
727
+ cursor += expression.length + 1;
728
+ }
729
+ return false;
730
+ }
731
+
732
+ /** The text between `open` at `index` and its matching `close`. */
733
+ function balancedFrom(
734
+ source: string,
735
+ index: number,
736
+ open: string,
737
+ close: string,
738
+ ): string | undefined {
739
+ let depth = 0;
740
+ let quote: string | undefined;
741
+ for (let cursor = index; cursor < source.length; cursor++) {
742
+ const current = source[cursor]!;
743
+ if (quote) {
744
+ if (current === '\\') cursor++;
745
+ else if (current === quote) quote = undefined;
746
+ continue;
747
+ }
748
+ if (current === '"' || current === "'" || current === '`') {
749
+ quote = current;
750
+ continue;
751
+ }
752
+ if (current === open) depth++;
753
+ else if (current === close && --depth === 0) return source.slice(index + 1, cursor);
754
+ }
755
+ return undefined;
756
+ }
757
+
758
+ /** `foo()` / `a.b(…)` — an unawaited call whose promise crosses a cache boundary. */
759
+ function isUncachedPromiseExpression(
760
+ expression: string,
761
+ resolvableNames: ReadonlySet<string>,
762
+ ): boolean {
763
+ const trimmed = expression.trim();
764
+ if (trimmed === '' || /^await\b/.test(trimmed)) return false;
765
+ const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(trimmed);
766
+ return call !== null && !resolvableNames.has(call[1]!);
767
+ }
768
+
769
+ /**
770
+ * The locally-bound names of request-scoped APIs imported from `next/headers` or `next/root-params`. A
771
+ * promise rooted at one of these settles from request data - or, for root params, from the prerender's
772
+ * params - rather than hanging: `publicCache(cookies().then(...))` is the runtime-prefetch pattern, where
773
+ * the request API postpones the boundary and the route builds as runtime-prefetchable/dynamic. Scoped to
774
+ * actual imports, so an app-local helper that happens to be named `cookies()` still counts as uncached IO.
775
+ */
776
+ function requestApiNames(source: string): Set<string> {
777
+ const names = new Set<string>();
778
+ const imports = source.matchAll(
779
+ /\bimport\s*\{([^}]*)\}\s*from\s*['"]next\/(?:headers|root-params)['"]/g,
780
+ );
781
+ for (const match of imports) {
782
+ for (const clause of match[1]!.split(',')) {
783
+ const local = /([A-Za-z_$][\w$]*)\s*$/.exec(clause.trim());
784
+ if (local) names.add(local[1]!);
785
+ }
786
+ }
787
+ return names;
788
+ }
789
+
790
+ function escapeRegExp(value: string): string {
791
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
792
+ }
793
+
794
+ /**
795
+ * Whether the page reads request data (connection/cookies/headers/draftMode) outside every 'use cache'
796
+ * scope - such a route is dynamic and never prerendered, so build-time prerender heuristics do not apply.
797
+ * A module-level 'use cache' page has no code outside a scope, so it stays subject to them.
798
+ */
799
+ function readsRequestDataOutsideCache(source: string): boolean {
800
+ const outside = useCacheScopedSource(source).reduce(
801
+ (rest, scope) => rest.replace(scope.body, ''),
802
+ source,
803
+ );
804
+ return /\b(?:connection|cookies|headers|draftMode)\s*\(\s*\)/.test(outside);
805
+ }
806
+
807
+ /**
808
+ * Index of the `}` closing the function body whose content starts at `offset`
809
+ * (just past the opening brace). Brace-counts while skipping strings, template
810
+ * literals and comments; an unbalanced body yields the end of the source.
811
+ */
812
+ function functionBodyEnd(source: string, offset: number): number {
813
+ let depth = 1;
814
+ let quote: string | undefined;
815
+ for (let cursor = offset; cursor < source.length; cursor++) {
816
+ const current = source[cursor]!;
817
+ if (quote) {
818
+ // Template literals are treated as opaque text (interpolations included):
819
+ // their braces are skipped rather than counted. Good enough for the
820
+ // heuristic; a nested template inside an interpolation may end the skip
821
+ // early, which at worst widens the scanned scope.
822
+ if (current === '\\') cursor++;
823
+ else if (current === quote) quote = undefined;
824
+ continue;
825
+ }
826
+ if (current === '/' && source[cursor + 1] === '/') {
827
+ const newline = source.indexOf('\n', cursor + 2);
828
+ if (newline === -1) return source.length;
829
+ cursor = newline;
830
+ continue;
831
+ }
832
+ if (current === '/' && source[cursor + 1] === '*') {
833
+ const end = source.indexOf('*/', cursor + 2);
834
+ if (end === -1) return source.length;
835
+ cursor = end + 1;
836
+ continue;
837
+ }
838
+ if (current === '"' || current === "'" || current === '`') {
839
+ quote = current;
840
+ continue;
841
+ }
842
+ if (current === '{') depth++;
843
+ else if (current === '}' && --depth === 0) return cursor;
844
+ }
845
+ return source.length;
846
+ }
847
+
848
+ /**
849
+ * Next's exact webpack-style build failure for a 'use cache' directive found without `cacheComponents` (or
850
+ * a legacy alias) enabled. The e2e suite slices everything after a "Failed to compile" line and
851
+ * inline-snapshot-matches the whole block - including the swc code frame (line numbers, caret span under
852
+ * the directive, trailing-space `N | ` prefix on empty source lines) and the duplicated import-trace and
853
+ * build-failed footer - so the text below is byte-for-byte Next's output. The CLI prints the thrown message
854
+ * verbatim; nothing may print after it.
855
+ */
856
+ function useCacheNotEnabledBuildError(displayPath: string, source: string): string {
857
+ return useCacheDirectiveBuildError(
858
+ displayPath,
859
+ source,
860
+ 'To use "use cache", please enable the feature flag `cacheComponents` in your Next.js config.\n' +
861
+ ' |\n' +
862
+ ' | Read more: https://nextjs.org/docs/app/api-reference/directives/use-cache#usage',
863
+ );
864
+ }
865
+
866
+ /**
867
+ * Same webpack-style block for a `'use cache: <kind>'` directive naming a kind with no configured cache
868
+ * handler - inline-snapshot-matched the same way.
869
+ */
870
+ function unknownCacheKindBuildError(displayPath: string, source: string, kind: string): string {
871
+ return useCacheDirectiveBuildError(
872
+ displayPath,
873
+ source,
874
+ `Unknown cache kind "${kind}". Please configure a cache handler for this kind in the \`cacheHandlers\` object in your Next.js config.`,
875
+ );
876
+ }
877
+
878
+ /** The shared `Failed to compile` block: message, swc code frame, import trace. */
879
+ function useCacheDirectiveBuildError(
880
+ displayPath: string,
881
+ source: string,
882
+ message: string,
883
+ ): string {
884
+ const match = /(['"])use cache(?:\s*:\s*[\w-]+)?\1/.exec(source);
885
+ const index = match?.index ?? 0;
886
+ const span = match?.[0].length ?? 11;
887
+ const before = source.slice(0, index);
888
+ const line = before.split('\n').length;
889
+ const column = index - before.lastIndexOf('\n');
890
+ const frame = swcCodeFrame(source, line, column, span);
891
+ return (
892
+ 'Failed to compile.\n' +
893
+ '\n' +
894
+ `${displayPath}\n` +
895
+ `Error: x ${message}\n` +
896
+ '\n' +
897
+ `${frame}\n` +
898
+ '\n' +
899
+ 'Import trace for requested module:\n' +
900
+ `${displayPath}\n` +
901
+ '\n' +
902
+ '\n' +
903
+ '> Build failed because of webpack errors'
904
+ );
905
+ }
906
+
907
+ /**
908
+ * An swc-style code frame: a `,-[line:column]` header, the offending line plus up to three following
909
+ * context lines (the `N | ` prefix keeps its trailing space on empty lines, matching swc), a caret marker
910
+ * under the span, and a backtick footer.
911
+ */
912
+ function swcCodeFrame(source: string, line: number, column: number, span: number): string {
913
+ const lines = source.split('\n');
914
+ const last = Math.min(lines.length, line + 3);
915
+ const width = String(last).length;
916
+ const gutter = ' '.repeat(width + 2);
917
+ const out: string[] = [`${gutter},-[${line}:${column}]`];
918
+ for (let n = line; n <= last; n += 1) {
919
+ out.push(` ${String(n).padStart(width)} | ${lines[n - 1] ?? ''}`);
920
+ if (n === line) {
921
+ out.push(`${gutter}: ${' '.repeat(column - 1)}${'^'.repeat(span)}`);
922
+ }
923
+ }
924
+ out.push(`${gutter}\`----`);
925
+ return out.join('\n');
926
+ }
927
+
928
+ function directivePrologue(source: string, offset: number): string[] {
929
+ const directives: string[] = [];
930
+ let cursor = offset;
931
+ while (true) {
932
+ cursor = skipTrivia(source, cursor);
933
+ const quote = source[cursor];
934
+ if (quote !== '"' && quote !== "'") return directives;
935
+ const end = stringEnd(source, cursor, quote);
936
+ if (end === -1) return directives;
937
+ const value = source.slice(cursor + 1, end);
938
+ const afterString = end + 1;
939
+ cursor = skipTrivia(source, afterString);
940
+ if (source[cursor] === ';') cursor++;
941
+ else if (
942
+ cursor < source.length &&
943
+ source[cursor] !== '}' &&
944
+ !/[\r\n]/.test(source.slice(afterString, cursor))
945
+ ) {
946
+ return directives;
947
+ }
948
+ directives.push(value);
949
+ }
950
+ }
951
+
952
+ function functionBodies(source: string): number[] {
953
+ return functionScopes(source).map(scope => scope.offset);
954
+ }
955
+
956
+ /**
957
+ * Every function/arrow body in `source`, as `{ offset, params }`: `offset` is the index just past the
958
+ * opening brace and `params` are the binding names declared by that function's signature. The parameter
959
+ * names let the hanging-input heuristics tell a cached function's OWN vary-key arguments - legal, since the
960
+ * runtime-prefetch pattern passes params/searchParams promises into a 'use cache' function and awaits them
961
+ * there - from closed-over request data, which is still flagged.
962
+ */
963
+ function functionScopes(source: string): { offset: number; params: string[]; name: string }[] {
964
+ const scopes: { offset: number; params: string[]; name: string }[] = [];
965
+ const pattern =
966
+ /\b(?:async\s+)?function(?:\s*\*)?(?:\s+[A-Za-z_$][\w$]*)?\s*(?:<[^>{}]*>)?\s*\([^)]*\)\s*(?::\s*[^={]+)?\{|\b(?:async\s+)?(?:[A-Za-z_$][\w$]*|\([^)]*\))\s*=>\s*\{/g;
967
+ for (const match of source.matchAll(pattern)) {
968
+ if (match.index !== undefined && isCodeOffset(source, match.index)) {
969
+ scopes.push({
970
+ offset: match.index + match[0].length,
971
+ params: signatureParams(match[0]),
972
+ name: signatureName(source, match.index, match[0]),
973
+ });
974
+ }
975
+ }
976
+ return scopes;
977
+ }
978
+
979
+ /**
980
+ * The binding name a function scope is reachable by: the declared name for `function Foo(...)`, otherwise
981
+ * the const/let/var binding an arrow or function expression is assigned to. '' when anonymous. Used to find
982
+ * a cached function's CALL SITES.
983
+ */
984
+ function signatureName(source: string, index: number, signature: string): string {
985
+ const declared = /\bfunction(?:\s*\*)?\s+([A-Za-z_$][\w$]*)/.exec(signature);
986
+ if (declared) return declared[1]!;
987
+ const before = source.slice(Math.max(0, index - 200), index);
988
+ const assigned = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;]*)?=\s*$/.exec(before);
989
+ return assigned?.[1] ?? '';
990
+ }
991
+
992
+ /**
993
+ * Binding names declared by a function/arrow signature (the matched text from `functionScopes`, which ends
994
+ * at the opening brace). Handles named params, object/array destructuring including renames, rest params
995
+ * and a single unparenthesized arrow parameter. Type annotations are ignored - only the bound identifiers
996
+ * matter.
997
+ */
998
+ export function signatureParams(signature: string): string[] {
999
+ // Arrow with a single unparenthesized parameter: `arg => {`.
1000
+ const bareArrow = /(?:^|[^\w$.])([A-Za-z_$][\w$]*)\s*=>\s*\{\s*$/.exec(signature);
1001
+ if (bareArrow && !signature.includes('(')) return [bareArrow[1]!];
1002
+ const open = signature.indexOf('(');
1003
+ if (open === -1) return [];
1004
+ // The signatures `functionScopes` matches use `\([^)]*\)` for the parameter
1005
+ // list, so the list never contains a nested `)` — the first `)` closes it.
1006
+ const close = signature.indexOf(')', open);
1007
+ if (close === -1) return [];
1008
+ return paramBindingNames(signature.slice(open + 1, close));
1009
+ }
1010
+
1011
+ /** Bound identifiers of a comma-separated parameter list. */
1012
+ function paramBindingNames(paramList: string): string[] {
1013
+ const names: string[] = [];
1014
+ for (const raw of splitTopLevelCommas(paramList)) {
1015
+ const part = stripDefaultValue(raw).trim().replace(/^\.\.\.\s*/, '');
1016
+ if (!part) continue;
1017
+ if (part.startsWith('{') || part.startsWith('[')) {
1018
+ destructureBindings(firstBalancedGroup(part), names);
1019
+ } else {
1020
+ // `name: Type` / `name?: Type` — the binding is the leading identifier.
1021
+ const ident = /^([A-Za-z_$][\w$]*)/.exec(part);
1022
+ if (ident) names.push(ident[1]!);
1023
+ }
1024
+ }
1025
+ return names;
1026
+ }
1027
+
1028
+ /** Local bindings introduced by a destructuring pattern (`{…}` or `[…]`). */
1029
+ function destructureBindings(pattern: string, names: string[]): void {
1030
+ const inner = pattern.slice(1, -1);
1031
+ for (const raw of splitTopLevelCommas(inner)) {
1032
+ const entry = stripDefaultValue(raw).trim().replace(/^\.\.\.\s*/, '');
1033
+ if (!entry) continue;
1034
+ // In a destructure entry a top-level `:` renames (`key: binding`); the local
1035
+ // binding is on the RIGHT — the opposite of a parameter's `name: Type`.
1036
+ const colon = topLevelColonIndex(entry);
1037
+ const binding = (colon === -1 ? entry : entry.slice(colon + 1)).trim();
1038
+ if (binding.startsWith('{') || binding.startsWith('[')) {
1039
+ destructureBindings(firstBalancedGroup(binding), names);
1040
+ } else {
1041
+ const ident = /^([A-Za-z_$][\w$]*)/.exec(binding);
1042
+ if (ident) names.push(ident[1]!);
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ /** Split `text` on commas that sit at bracket depth 0 (strings skipped). */
1048
+ function splitTopLevelCommas(text: string): string[] {
1049
+ const parts: string[] = [];
1050
+ let depth = 0;
1051
+ let start = 0;
1052
+ let quote: string | undefined;
1053
+ for (let i = 0; i < text.length; i++) {
1054
+ const current = text[i]!;
1055
+ if (quote) {
1056
+ if (current === '\\') i++;
1057
+ else if (current === quote) quote = undefined;
1058
+ continue;
1059
+ }
1060
+ if (current === '"' || current === "'" || current === '`') quote = current;
1061
+ else if (current === '{' || current === '[' || current === '(' || current === '<') depth++;
1062
+ else if (current === '}' || current === ']' || current === ')' || current === '>') depth--;
1063
+ else if (current === ',' && depth === 0) {
1064
+ parts.push(text.slice(start, i));
1065
+ start = i + 1;
1066
+ }
1067
+ }
1068
+ parts.push(text.slice(start));
1069
+ return parts;
1070
+ }
1071
+
1072
+ /** The first balanced `{…}`/`[…]` group in `text` (which starts with it). */
1073
+ function firstBalancedGroup(text: string): string {
1074
+ const open = text[0];
1075
+ const close = open === '{' ? '}' : ']';
1076
+ let depth = 0;
1077
+ let quote: string | undefined;
1078
+ for (let i = 0; i < text.length; i++) {
1079
+ const current = text[i]!;
1080
+ if (quote) {
1081
+ if (current === '\\') i++;
1082
+ else if (current === quote) quote = undefined;
1083
+ continue;
1084
+ }
1085
+ if (current === '"' || current === "'" || current === '`') quote = current;
1086
+ else if (current === open) depth++;
1087
+ else if (current === close && --depth === 0) return text.slice(0, i + 1);
1088
+ }
1089
+ return text;
1090
+ }
1091
+
1092
+ /** Index of the first `:` at bracket depth 0, or -1. */
1093
+ function topLevelColonIndex(text: string): number {
1094
+ let depth = 0;
1095
+ let quote: string | undefined;
1096
+ for (let i = 0; i < text.length; i++) {
1097
+ const current = text[i]!;
1098
+ if (quote) {
1099
+ if (current === '\\') i++;
1100
+ else if (current === quote) quote = undefined;
1101
+ continue;
1102
+ }
1103
+ if (current === '"' || current === "'" || current === '`') quote = current;
1104
+ else if (current === '{' || current === '[' || current === '(' || current === '<') depth++;
1105
+ else if (current === '}' || current === ']' || current === ')' || current === '>') depth--;
1106
+ else if (current === ':' && depth === 0) return i;
1107
+ }
1108
+ return -1;
1109
+ }
1110
+
1111
+ /** Drop a top-level `= default` suffix from a parameter/binding entry. */
1112
+ function stripDefaultValue(entry: string): string {
1113
+ let depth = 0;
1114
+ let quote: string | undefined;
1115
+ for (let i = 0; i < entry.length; i++) {
1116
+ const current = entry[i]!;
1117
+ if (quote) {
1118
+ if (current === '\\') i++;
1119
+ else if (current === quote) quote = undefined;
1120
+ continue;
1121
+ }
1122
+ if (current === '"' || current === "'" || current === '`') quote = current;
1123
+ else if (current === '{' || current === '[' || current === '(' || current === '<') depth++;
1124
+ else if (current === '}' || current === ']' || current === ')' || current === '>') depth--;
1125
+ else if (
1126
+ current === '=' &&
1127
+ depth === 0 &&
1128
+ entry[i + 1] !== '=' &&
1129
+ entry[i + 1] !== '>' &&
1130
+ entry[i - 1] !== '=' &&
1131
+ entry[i - 1] !== '!' &&
1132
+ entry[i - 1] !== '<' &&
1133
+ entry[i - 1] !== '>'
1134
+ ) {
1135
+ return entry.slice(0, i);
1136
+ }
1137
+ }
1138
+ return entry;
1139
+ }
1140
+
1141
+ /** Identifiers directly awaited in `body` (the `X` of every `await X`). */
1142
+ function awaitedIdentifiers(body: string): string[] {
1143
+ return [...body.matchAll(/\bawait\s+([A-Za-z_$][\w$]*)\b/g)].map(match => match[1]!);
1144
+ }
1145
+
1146
+ function isCodeOffset(source: string, target: number): boolean {
1147
+ let quote: string | undefined;
1148
+ for (let cursor = 0; cursor < target; cursor++) {
1149
+ const current = source[cursor]!;
1150
+ if (quote) {
1151
+ if (current === '\\') cursor++;
1152
+ else if (current === quote) quote = undefined;
1153
+ continue;
1154
+ }
1155
+ if (current === '/' && source[cursor + 1] === '/') {
1156
+ const newline = source.indexOf('\n', cursor + 2);
1157
+ if (newline === -1 || newline >= target) return false;
1158
+ cursor = newline;
1159
+ continue;
1160
+ }
1161
+ if (current === '/' && source[cursor + 1] === '*') {
1162
+ const end = source.indexOf('*/', cursor + 2);
1163
+ if (end === -1 || end >= target) return false;
1164
+ cursor = end + 1;
1165
+ continue;
1166
+ }
1167
+ if (current === '"' || current === "'" || current === '`') quote = current;
1168
+ }
1169
+ return quote === undefined;
1170
+ }
1171
+
1172
+ function skipTrivia(source: string, start: number): number {
1173
+ let cursor = start;
1174
+ while (cursor < source.length) {
1175
+ if (/\s/.test(source[cursor]!)) {
1176
+ cursor++;
1177
+ continue;
1178
+ }
1179
+ if (source[cursor] === '/' && source[cursor + 1] === '/') {
1180
+ const newline = source.indexOf('\n', cursor + 2);
1181
+ cursor = newline === -1 ? source.length : newline + 1;
1182
+ continue;
1183
+ }
1184
+ if (source[cursor] === '/' && source[cursor + 1] === '*') {
1185
+ const end = source.indexOf('*/', cursor + 2);
1186
+ if (end === -1) return source.length;
1187
+ cursor = end + 2;
1188
+ continue;
1189
+ }
1190
+ return cursor;
1191
+ }
1192
+ return cursor;
1193
+ }
1194
+
1195
+ function stringEnd(source: string, start: number, quote: string): number {
1196
+ for (let cursor = start + 1; cursor < source.length; cursor++) {
1197
+ if (source[cursor] === '\\') {
1198
+ cursor++;
1199
+ continue;
1200
+ }
1201
+ if (source[cursor] === quote) return cursor;
1202
+ }
1203
+ return -1;
1204
+ }
1205
+
1206
+ function validateCodemodComments(routes: RouteManifestEntry[]): void {
1207
+ for (const file of new Set(routes.map(route => route.file))) {
1208
+ const source = readSource(file);
1209
+ if (source === undefined) continue;
1210
+ const match = /@next-codemod-error\s+([^\n*]+)/.exec(source);
1211
+ if (match?.[1]) {
1212
+ throw new PnextBuildValidationError(unresolvedCodemodCommentMessage(match[1].trim()));
1213
+ }
1214
+ }
1215
+ }
1216
+
1217
+ // --- missing root layout ---------------------------------------------------
1218
+
1219
+ function validateRootLayout(config: ResolvedConfig, pageRoutes: RouteManifestEntry[]): void {
1220
+ if (pageRoutes.length === 0) return;
1221
+ const hasRootLayout = pageExtensions().some(ext =>
1222
+ existsSync(path.join(config.appPath, `layout.${ext}`)),
1223
+ );
1224
+ if (hasRootLayout) return;
1225
+ // No root `app/layout.*`. Next treats the topmost segment layout as the root
1226
+ // and, when a page has an ancestor layout, does NOT error — the segment layout
1227
+ // (or the synthesized builtin default `<html><body>{children}</body>`) supplies
1228
+ // the document. Only a page with NO ancestor layout anywhere is a hard error.
1229
+ const orphan = pageRoutes.find(route => !hasAncestorLayout(config.appPath, route.file));
1230
+ if (!orphan) return;
1231
+ throw new PnextBuildValidationError(
1232
+ missingRootLayoutMessage(appDirRelativeFile(config, orphan.file)),
1233
+ );
1234
+ }
1235
+
1236
+ /** Whether any `layout.*` exists on the route file's directory chain (below appPath). */
1237
+ function hasAncestorLayout(appPath: string, routeFile: string): boolean {
1238
+ let dir = path.dirname(routeFile);
1239
+ while (dir.startsWith(appPath)) {
1240
+ if (pageExtensions().some(ext => existsSync(path.join(dir, `layout.${ext}`)))) return true;
1241
+ if (dir === appPath) break;
1242
+ dir = path.dirname(dir);
1243
+ }
1244
+ return false;
1245
+ }
1246
+
1247
+ function pageExtensions(): string[] {
1248
+ return [...new Set([...BASE_PAGE_EXTENSIONS, ...extraPageExtensions()])];
1249
+ }
1250
+
1251
+ // --- app/ vs pages/ conflict ----------------------------------------------
1252
+
1253
+ function validateAppPagesConflict(config: ResolvedConfig): void {
1254
+ const pagesDir = pagesDirFor(config);
1255
+ if (!pagesDir || !existsSync(pagesDir)) return;
1256
+ const pagePaths = collectPagesRouterPaths(pagesDir);
1257
+ if (pagePaths.size === 0) return;
1258
+ const appPaths = collectAppRouterPaths(path.join(config.root, 'app'));
1259
+
1260
+ const conflicts: { page: string; app: string }[] = [];
1261
+ for (const [route, app] of appPaths) {
1262
+ const page = pagePaths.get(route);
1263
+ if (page) conflicts.push({ page, app });
1264
+ }
1265
+ if (conflicts.length === 0) return;
1266
+ throw new PnextBuildValidationError(
1267
+ conflictingAppAndPageMessage(dedupeConflicts(conflicts).sort(conflictSort)),
1268
+ );
1269
+ }
1270
+
1271
+ /** The pages/ directory sibling to the resolved app/ directory, if any. */
1272
+ function pagesDirFor(config: ResolvedConfig): string | undefined {
1273
+ return path.join(config.root, 'pages');
1274
+ }
1275
+
1276
+ function collectAppRouterPaths(appDir: string): Map<string, string> {
1277
+ const paths = new Map<string, string>();
1278
+ if (!existsSync(appDir)) return paths;
1279
+ const walk = (dir: string, segments: string[]) => {
1280
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1281
+ const full = path.join(dir, entry.name);
1282
+ if (entry.isDirectory()) {
1283
+ if (entry.name.startsWith('@') || (entry.name.startsWith('(') && entry.name.includes('.')))
1284
+ continue;
1285
+ walk(
1286
+ full,
1287
+ entry.name.startsWith('(') && entry.name.endsWith(')')
1288
+ ? segments
1289
+ : [...segments, entry.name],
1290
+ );
1291
+ continue;
1292
+ }
1293
+ if (!/^page\.(?:tsx|ts|jsx|js|mjs)$/.test(entry.name)) continue;
1294
+ const route = segments.length === 0 ? '/' : `/${segments.join('/')}`;
1295
+ paths.set(route, toPosix(path.relative(path.dirname(appDir), full)));
1296
+ }
1297
+ };
1298
+ walk(appDir, []);
1299
+ return paths;
1300
+ }
1301
+
1302
+ /** Public route paths declared under a pages/ directory (excludes api/ + _-files). */
1303
+ function collectPagesRouterPaths(pagesDir: string): Map<string, string> {
1304
+ const paths = new Map<string, string>();
1305
+ const walk = (dir: string, prefix: string) => {
1306
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1307
+ if (entry.name.startsWith('_')) continue;
1308
+ const full = path.join(dir, entry.name);
1309
+ if (entry.isDirectory()) {
1310
+ if (entry.name === 'api') continue;
1311
+ walk(full, `${prefix}/${entry.name}`);
1312
+ continue;
1313
+ }
1314
+ const match = /^(.*)\.(tsx|ts|jsx|js|mjs)$/.exec(entry.name);
1315
+ if (!match) continue;
1316
+ const base = match[1];
1317
+ const rel = toPosix(path.relative(path.dirname(pagesDir), full));
1318
+ if (base === 'index') {
1319
+ paths.set(prefix || '/', rel);
1320
+ } else {
1321
+ paths.set(`${prefix}/${base}`, rel);
1322
+ }
1323
+ }
1324
+ };
1325
+ walk(pagesDir, '');
1326
+ return paths;
1327
+ }
1328
+
1329
+ function dedupeConflicts(conflicts: { page: string; app: string }[]) {
1330
+ const seen = new Set<string>();
1331
+ return conflicts.filter(conflict => {
1332
+ const key = `${conflict.page}\0${conflict.app}`;
1333
+ if (seen.has(key)) return false;
1334
+ seen.add(key);
1335
+ return true;
1336
+ });
1337
+ }
1338
+
1339
+ function conflictSort(a: { page: string; app: string }, b: { page: string; app: string }) {
1340
+ return a.page.localeCompare(b.page) || a.app.localeCompare(b.app);
1341
+ }
1342
+
1343
+ // --- two parallel (route-group) pages on the same path ---------------------
1344
+
1345
+ function validateParallelPageConflicts(
1346
+ config: ResolvedConfig,
1347
+ pageRoutes: RouteManifestEntry[],
1348
+ ): void {
1349
+ const byPattern = new Map<string, RouteManifestEntry>();
1350
+ for (const route of pageRoutes) {
1351
+ if (route.synthetic) continue;
1352
+ const existing = byPattern.get(route.pattern);
1353
+ if (existing) {
1354
+ const labels = [routeGroupLabel(config, existing), routeGroupLabel(config, route)].sort();
1355
+ throw new PnextBuildValidationError(conflictingParallelPagesMessage(labels[0]!, labels[1]!));
1356
+ }
1357
+ byPattern.set(route.pattern, route);
1358
+ }
1359
+ }
1360
+
1361
+ /** The app-relative dir of the route's page file, as Next reports it. */
1362
+ function routeGroupLabel(config: ResolvedConfig, route: RouteManifestEntry): string {
1363
+ const dir = path.dirname(appDirRelativeFile(config, route.file));
1364
+ return dir === '.' ? '/' : `/${dir}`;
1365
+ }
1366
+
1367
+ // --- missing named-slot default on non-leaf segments -----------------------
1368
+
1369
+ function validateMissingSlotDefaults(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
1370
+ const failures: string[] = [];
1371
+ const interceptedTargets = interceptedTargetDirs(routes);
1372
+ const visit = (dir: string): void => {
1373
+ for (const entry of readDirEntries(dir)) {
1374
+ if (!entry.isDirectory()) continue;
1375
+ const child = path.join(dir, entry.name);
1376
+ if (isSlotDir(entry.name)) continue;
1377
+ visit(child);
1378
+ }
1379
+
1380
+ if (!hasNormalChildRoute(dir)) return;
1381
+ if (interceptedTargets.has(dir)) return;
1382
+ for (const slot of readDirEntries(dir)) {
1383
+ if (!slot.isDirectory() || !isNamedSlotDir(slot.name)) continue;
1384
+ const slotDir = path.join(dir, slot.name);
1385
+ if (hasConventionFile(slotDir, 'default')) continue;
1386
+ // A catch-all page inside the slot (`[...x]`/`[[...x]]`) matches any
1387
+ // otherwise-unmatched path, so it doubles as the slot's fallback and
1388
+ // Next does not require an explicit default.js (parallel-slot catch-all).
1389
+ if (slotHasCatchAllPage(slotDir)) continue;
1390
+ failures.push(missingDefaultParallelRouteMessage(appPath(config, slotDir), slot.name));
1391
+ }
1392
+ };
1393
+
1394
+ visit(config.appPath);
1395
+ if (failures.length > 0) throw new PnextBuildValidationError(failures.join('\n\n'));
1396
+ }
1397
+
1398
+ function interceptedTargetDirs(routes: RouteManifestEntry[]): Set<string> {
1399
+ const patterns = new Set(
1400
+ routes.filter(route => route.kind === 'page' && route.interception).map(route => route.pattern),
1401
+ );
1402
+ return new Set(
1403
+ routes
1404
+ .filter(route => route.kind === 'page' && !route.interception && patterns.has(route.pattern))
1405
+ .map(route => path.dirname(route.file)),
1406
+ );
1407
+ }
1408
+
1409
+ function hasNormalChildRoute(dir: string, depth = 0): boolean {
1410
+ for (const entry of readDirEntries(dir)) {
1411
+ if (!entry.isDirectory() || isSlotDir(entry.name)) continue;
1412
+ const child = path.join(dir, entry.name);
1413
+ const nextDepth = isGroupDir(entry.name) ? depth : depth + 1;
1414
+ if (
1415
+ nextDepth > 0 &&
1416
+ (hasConventionFile(child, 'page') || hasConventionFile(child, 'default'))
1417
+ ) {
1418
+ return true;
1419
+ }
1420
+ if (hasNormalChildRoute(child, nextDepth)) return true;
1421
+ }
1422
+ return false;
1423
+ }
1424
+
1425
+ // Whether a slot subtree contains a catch-all page segment (`[...x]` or the
1426
+ // optional `[[...x]]`) that can serve as the slot's fallback. Group dirs
1427
+ // (`(group)`) and nested normal segments are walked; other slot dirs are not.
1428
+ function slotHasCatchAllPage(dir: string): boolean {
1429
+ for (const entry of readDirEntries(dir)) {
1430
+ if (!entry.isDirectory()) continue;
1431
+ const child = path.join(dir, entry.name);
1432
+ if (/^\[\[?\.\.\..+\]\]?$/.test(entry.name) && hasConventionFile(child, 'page')) return true;
1433
+ if (slotHasCatchAllPage(child)) return true;
1434
+ }
1435
+ return false;
1436
+ }
1437
+
1438
+ function hasConventionFile(dir: string, name: string): boolean {
1439
+ return pageExtensions().some(ext => existsSync(path.join(dir, `${name}.${ext}`)));
1440
+ }
1441
+
1442
+ function readDirEntries(dir: string) {
1443
+ try {
1444
+ return readdirSync(dir, { withFileTypes: true });
1445
+ } catch {
1446
+ return [];
1447
+ }
1448
+ }
1449
+
1450
+ function isSlotDir(name: string): boolean {
1451
+ return name.startsWith('@');
1452
+ }
1453
+
1454
+ function isNamedSlotDir(name: string): boolean {
1455
+ return isSlotDir(name) && name !== '@children';
1456
+ }
1457
+
1458
+ function isGroupDir(name: string): boolean {
1459
+ return name.startsWith('(') && name.endsWith(')');
1460
+ }
1461
+
1462
+ function appPath(config: ResolvedConfig, file: string): string {
1463
+ return `app/${appDirRelativeFile(config, file)}`;
1464
+ }
1465
+
1466
+ // --- undefined default export ----------------------------------------------
1467
+
1468
+ function validateDefaultExports(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
1469
+ const failures: string[] = [];
1470
+ const appPath = path.resolve(config.appPath);
1471
+ for (const route of routes) {
1472
+ if (route.kind !== 'page' || route.interception || route.synthetic) continue;
1473
+ for (const file of defaultExportFiles(route)) {
1474
+ // MDX pages have no literal `export default` in source; the compiler emits one.
1475
+ if (/\.mdx?$/.test(file)) continue;
1476
+ // A core-authored root layout exports only `metadata` — pnext synthesizes
1477
+ // the <html>/<body> shell from it, so there is no component to default-export.
1478
+ // Next requires one; under compat the shell still comes from core, so the
1479
+ // layout is valid (SPEC change 9). Nested layouts keep Next's rule.
1480
+ if (isRootLayoutFile(appPath, file)) continue;
1481
+ const source = readSource(file);
1482
+ if (source === undefined) continue;
1483
+ if (!hasDefaultExport(stripCommentsAndStrings(source))) {
1484
+ failures.push(undefinedDefaultExportMessage(componentRoutePath(route, file)));
1485
+ }
1486
+ }
1487
+ }
1488
+ if (failures.length > 0) throw new PnextBuildValidationError([...new Set(failures)].join('\n'));
1489
+ }
1490
+
1491
+ function defaultExportFiles(route: RouteManifestEntry): string[] {
1492
+ const files = new Set<string>([route.file]);
1493
+ const dir = path.dirname(route.file);
1494
+ for (const ext of pageExtensions()) {
1495
+ files.add(path.join(dir, `layout.${ext}`));
1496
+ files.add(path.join(dir, `not-found.${ext}`));
1497
+ }
1498
+ return [...files].filter(file => existsSync(file));
1499
+ }
1500
+
1501
+ function isRootLayoutFile(appPath: string, file: string): boolean {
1502
+ return (
1503
+ path.basename(file).replace(/\.[^.]+$/, '') === 'layout' &&
1504
+ path.resolve(path.dirname(file)) === appPath
1505
+ );
1506
+ }
1507
+
1508
+ function componentRoutePath(route: RouteManifestEntry, file: string): string {
1509
+ const convention = path.basename(file).replace(/\.(tsx|ts|jsx|js|mjs)$/, '');
1510
+ const routePath = route.route || '/';
1511
+ if (convention === 'page') return routePath === '/' ? '/page' : `${routePath}/page`;
1512
+ return `${routePath === '/' ? '' : routePath}/${convention}`;
1513
+ }
1514
+
1515
+ function hasDefaultExport(searchable: string): boolean {
1516
+ return /\bexport\s+default\b/.test(searchable) || hasDefaultNamedExport(searchable);
1517
+ }
1518
+
1519
+ function hasDefaultNamedExport(searchable: string): boolean {
1520
+ for (const match of searchable.matchAll(/\bexport\s*\{([^}]*)\}/g)) {
1521
+ const specifiers = match[1]?.split(',') ?? [];
1522
+ if (specifiers.some(exportsDefaultSpecifier)) return true;
1523
+ }
1524
+ return false;
1525
+ }
1526
+
1527
+ function exportsDefaultSpecifier(specifier: string): boolean {
1528
+ const parts = specifier.trim().split(/\s+as\s+/);
1529
+ return parts.length === 1 ? parts[0] === 'default' : parts.at(-1) === 'default';
1530
+ }
1531
+
1532
+ // --- useSearchParams without Suspense --------------------------------------
1533
+
1534
+ function validateSearchParamsSuspense(
1535
+ config: ResolvedConfig,
1536
+ pageRoutes: RouteManifestEntry[],
1537
+ ): void {
1538
+ // The CSR bailout applies when a client page reads useSearchParams and there
1539
+ // is no Suspense boundary anywhere in the page's own file or layout chain.
1540
+ // Next only surfaces this at build for routes it STATICALLY prerenders: a
1541
+ // dynamically rendered route (dynamic params without generateStaticParams,
1542
+ // force-dynamic, or request-data usage) reads useSearchParams on-demand, so
1543
+ // no prerender-time CSR bailout occurs. Scope the check accordingly.
1544
+ for (const route of pageRoutes) {
1545
+ if (!route.client) continue;
1546
+ if (!isStaticallyPrerendered(route)) continue;
1547
+ const source = readSource(route.file);
1548
+ if (source === undefined) continue;
1549
+ const stripped = stripCommentsAndStrings(source);
1550
+ // This shape uses useSyncExternalStore's server snapshot to render a stable
1551
+ // shell, then fills the URLSearchParams instance after hydration.
1552
+ if (/\buseSyncExternalStore\s*\(/.test(stripped)) continue;
1553
+ if (!usesSearchParamsHook(stripped)) continue;
1554
+ if (fileTreeHasSuspense(config, route)) continue;
1555
+ throw new PnextBuildValidationError(missingSuspenseWithCsrBailoutMessage(route.route || '/'));
1556
+ }
1557
+ }
1558
+
1559
+ function usesSearchParamsHook(searchable: string): boolean {
1560
+ return /\buseSearchParams\s*\(/.test(searchable);
1561
+ }
1562
+
1563
+ /**
1564
+ * Whether Next would STATICALLY GENERATE this page - the precondition for the CSR bailout to be a build
1565
+ * error. Next reports it only for prerendered routes; a dynamically rendered one reads useSearchParams
1566
+ * on-demand and CSR-bails at runtime instead. A route renders dynamically when its segment config forces
1567
+ * it, or it has dynamic segments with no `generateStaticParams` to enumerate them. This mirrors the
1568
+ * prerender gate in cli/build.ts, EXCEPT it ignores `usesRequest`/`mode`: for a client page those are set
1569
+ * by the useSearchParams hook itself, which does not opt Next out of static generation - it is precisely
1570
+ * what triggers the bailout during prerender.
1571
+ */
1572
+ function isStaticallyPrerendered(route: RouteManifestEntry): boolean {
1573
+ const config = route.segmentConfig;
1574
+ const configForcesDynamic =
1575
+ config?.dynamic === 'force-dynamic' ||
1576
+ config?.revalidate === 0 ||
1577
+ config?.fetchCache === 'force-no-store';
1578
+ if (configForcesDynamic) return false;
1579
+ // Dynamic segments can only be prerendered when generateStaticParams
1580
+ // enumerates them; otherwise the route renders on-demand per request.
1581
+ const isDynamicRoute = route.params.length > 0 || Boolean(route.catchAll);
1582
+ if (isDynamicRoute && !route.hasStaticParams) return false;
1583
+ return true;
1584
+ }
1585
+
1586
+ function fileTreeHasSuspense(config: ResolvedConfig, route: RouteManifestEntry): boolean {
1587
+ // A Suspense boundary anywhere on the layout chain (root->leaf) or in the page
1588
+ // file itself satisfies the requirement. We scan the source text for a JSX
1589
+ // <Suspense usage or a Suspense import, which is what the fixtures rely on.
1590
+ const files = [route.file, ...layoutChain(config.appPath, route.file)];
1591
+ for (const file of files) {
1592
+ const source = readSource(file);
1593
+ if (source === undefined) continue;
1594
+ if (/<Suspense[\s/>]/.test(source) || /\bReact\.Suspense\b/.test(source)) return true;
1595
+ }
1596
+ return false;
1597
+ }
1598
+
1599
+ function layoutChain(appPath: string, routeFile: string): string[] {
1600
+ const files: string[] = [];
1601
+ let dir = path.dirname(routeFile);
1602
+ while (dir.startsWith(appPath)) {
1603
+ for (const ext of pageExtensions()) {
1604
+ const candidate = path.join(dir, `layout.${ext}`);
1605
+ if (existsSync(candidate)) files.push(candidate);
1606
+ }
1607
+ if (dir === appPath) break;
1608
+ dir = path.dirname(dir);
1609
+ }
1610
+ return files;
1611
+ }
1612
+
1613
+ // --- output: 'export' mode -------------------------------------------------
1614
+
1615
+ function validateExportMode(config: ResolvedConfig, routes: RouteManifestEntry[]): void {
1616
+ const nextConfig = getNextConfig();
1617
+ if (nextConfig.output !== 'export') return;
1618
+
1619
+ // exportPathMap is a pages-router-only config; using it with the app dir is a
1620
+ // hard error.
1621
+ const hasAppPages = routes.some(route => route.kind === 'page' && !route.interception);
1622
+ if (hasAppPages && typeof nextConfig.exportPathMap === 'function') {
1623
+ throw new PnextBuildValidationError(exportPathMapWithAppDirMessage());
1624
+ }
1625
+
1626
+ // Server actions need a server to receive them — Next fails the export build
1627
+ // before any per-route diagnostics.
1628
+ if (routesUseServerActions(routes)) {
1629
+ throw new PnextBuildValidationError('Server Actions are not supported with static export.');
1630
+ }
1631
+
1632
+ if (routes.some(route => route.interception)) {
1633
+ throw new PnextBuildValidationError(
1634
+ 'Intercepting routes are not supported with static export.',
1635
+ );
1636
+ }
1637
+
1638
+ for (const route of routes) {
1639
+ if (route.interception || route.synthetic) continue;
1640
+ // force-dynamic is incompatible with a static export. Pages get Next's
1641
+ // create-component-tree wording; handlers keep the route-level message.
1642
+ if (route.segmentConfig?.dynamic === 'force-dynamic') {
1643
+ throw new PnextBuildValidationError(
1644
+ route.kind === 'page'
1645
+ ? forceDynamicPageWithExportMessage()
1646
+ : dynamicForceDynamicWithExportMessage(nextPagePath(route)),
1647
+ );
1648
+ }
1649
+ if (route.kind === 'handler' && !routeHandlerStaticExportable(route)) {
1650
+ throw new PnextBuildValidationError(
1651
+ routeHandlerNotStaticWithExportMessage(nextPagePath(route)),
1652
+ );
1653
+ }
1654
+ // A dynamic route (params/catch-all) must supply generateStaticParams to be
1655
+ // exportable — there is no runtime to fill params in a static export.
1656
+ const isDynamicRoute = route.params.length > 0 || Boolean(route.catchAll);
1657
+ if (route.kind === 'page' && isDynamicRoute && !route.hasStaticParams) {
1658
+ throw new PnextBuildValidationError(
1659
+ missingGenerateStaticParamsForExportMessage(nextPagePath(route)),
1660
+ );
1661
+ }
1662
+ }
1663
+ }
1664
+
1665
+ // Whether any route's source graph declares server actions ('use server' as a
1666
+ // module prologue or an inline function directive).
1667
+ function routesUseServerActions(routes: RouteManifestEntry[]): boolean {
1668
+ const seen = new Set<string>();
1669
+ for (const route of routes) {
1670
+ for (const file of route.sourceFiles ?? []) {
1671
+ if (seen.has(file)) continue;
1672
+ seen.add(file);
1673
+ const source = readSource(file);
1674
+ if (source === undefined) continue;
1675
+ if (/(["'])use server\1\s*;?/.test(source)) return true;
1676
+ }
1677
+ }
1678
+ return false;
1679
+ }
1680
+
1681
+ function routeHandlerStaticExportable(route: RouteManifestEntry): boolean {
1682
+ const config = route.segmentConfig;
1683
+ return (
1684
+ config?.dynamic === 'force-static' ||
1685
+ // `dynamic = 'error'` opts the handler into static rendering (dynamic API
1686
+ // access becomes a hard error), which Next accepts for a static export.
1687
+ config?.dynamic === 'error' ||
1688
+ config?.revalidate === false ||
1689
+ (typeof config?.revalidate === 'number' && config.revalidate > 0) ||
1690
+ route.hasStaticParams
1691
+ );
1692
+ }
1693
+
1694
+ /** The route path in Next's page notation (`/blog/[slug]`, `/[...all]`). */
1695
+ function nextPagePath(route: RouteManifestEntry): string {
1696
+ let value = route.route || '/';
1697
+ if (route.catchAll) {
1698
+ const token = route.catchAllOptional ? `[[...${route.catchAll}]]` : `[...${route.catchAll}]`;
1699
+ value = value.replace(`:${route.catchAll}*`, token);
1700
+ }
1701
+ for (const param of route.params) value = value.replace(`:${param}`, `[${param}]`);
1702
+ return value;
1703
+ }
1704
+
1705
+ // --- shared source scanning ------------------------------------------------
1706
+
1707
+ function appDirRelativeFile(config: ResolvedConfig, file: string): string {
1708
+ return toPosix(path.relative(config.appPath, file));
1709
+ }
1710
+
1711
+ function toPosix(file: string): string {
1712
+ return file.split(path.sep).join('/');
1713
+ }
1714
+
1715
+ /**
1716
+ * Strip block/line comments and string/template literals so keyword scans do not match inside comments or
1717
+ * strings. A local copy (the routes.ts version is not exported), conservative - it only needs to blank out
1718
+ * obvious literals.
1719
+ *
1720
+ * Single left-to-right pass: sequential `.replace()` steps are unsafe because a `//` inside a string
1721
+ * literal would be eaten by the line-comment strip before the string strip ran, which desyncs the quote
1722
+ * matching and swallows the rest of the file. Scanning once, deciding comment-vs-string by which token
1723
+ * opens first at the cursor, keeps them from interfering.
1724
+ */
1725
+ function stripCommentsAndStrings(source: string): string {
1726
+ let out = '';
1727
+ let i = 0;
1728
+ const n = source.length;
1729
+ while (i < n) {
1730
+ const ch = source[i];
1731
+ const next = source[i + 1];
1732
+ // Block comment.
1733
+ if (ch === '/' && next === '*') {
1734
+ const end = source.indexOf('*/', i + 2);
1735
+ out += ' ';
1736
+ i = end === -1 ? n : end + 2;
1737
+ continue;
1738
+ }
1739
+ // Line comment.
1740
+ if (ch === '/' && next === '/') {
1741
+ const end = source.indexOf('\n', i + 2);
1742
+ out += ' ';
1743
+ i = end === -1 ? n : end;
1744
+ continue;
1745
+ }
1746
+ // String / template literal.
1747
+ if (ch === '"' || ch === "'" || ch === '`') {
1748
+ const quote = ch;
1749
+ out += quote;
1750
+ i += 1;
1751
+ while (i < n) {
1752
+ const c = source[i];
1753
+ if (c === '\\') {
1754
+ i += 2;
1755
+ continue;
1756
+ }
1757
+ if (c === quote) {
1758
+ i += 1;
1759
+ break;
1760
+ }
1761
+ i += 1;
1762
+ }
1763
+ out += quote;
1764
+ continue;
1765
+ }
1766
+ out += ch;
1767
+ i += 1;
1768
+ }
1769
+ return out;
1770
+ }