@wular/pnext 0.0.4 → 0.0.7

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