@wular/pnext 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,1508 @@
1
+ // Next-parity cacheComponents PRERENDER DIAGNOSTICS (COMPAT).
2
+ //
3
+ // A generate-mode build fails a cacheComponents route whose prerender would block (uncached/runtime data
4
+ // outside <Suspense>, a dynamic generateMetadata() on an otherwise prerenderable route, a dynamic
5
+ // generateViewport() without the opt-in), printing Next's exact diagnostic block: the E1290/E1292/E1289
6
+ // message, a synthesized React owner stack whose frames resolve to app-relative file:line:col positions,
7
+ // and - with `--debug-prerender` - a source codeframe of the offending line. The cache-components-errors
8
+ // e2e suites inline-snapshot these blocks byte-for-byte, so every space in the templates below is
9
+ // deliberate.
10
+ //
11
+ // The owner stack is synthesized statically from the page source (pnext renders with Preact, so React's
12
+ // captureOwnerStack does not exist): the violating dynamic `await` is located, the enclosing
13
+ // helper/component chain is resolved through local callsites, and ancestors are mapped to their JSX
14
+ // callsite positions - the same shape React's owner stack produces for these trees.
15
+
16
+ import { existsSync, readFileSync, statSync } from 'node:fs';
17
+ import path from 'node:path';
18
+
19
+ export interface PrerenderDiagnosticInput {
20
+ /** App-router pathname, e.g. '/dynamic-root'. */
21
+ route: string;
22
+ /** Absolute path of the route's page file. */
23
+ pageFile: string;
24
+ /** Absolute path of the app/ directory (for app-relative display paths). */
25
+ appPath: string;
26
+ /** `--debug-prerender`: unminified stacks + codeframes. */
27
+ debugPrerender: boolean;
28
+ }
29
+
30
+ /** A single owner-stack frame within the page file. */
31
+ interface Frame {
32
+ name: string;
33
+ line: number;
34
+ col: number;
35
+ }
36
+
37
+ interface Violation {
38
+ frames: Frame[];
39
+ /** Codeframe anchor (the top frame's position). */
40
+ anchor: Frame;
41
+ /**
42
+ * How many component elements wrap the violating JSX usage inside the page. Each wrapper adds one React
43
+ * internal frame to the minified component stack.
44
+ */
45
+ wrapperDepth: number;
46
+ }
47
+
48
+ /**
49
+ * Diagnose one cacheComponents page route. Returns the full printable
50
+ * diagnostic text (blocks + per-block trailer, WITHOUT the shared
51
+ * "Error occurred prerendering page" footer) when the route must fail the
52
+ * generate pass, undefined when it prerenders fine.
53
+ */
54
+ export function diagnoseCacheComponentsPrerender(
55
+ input: PrerenderDiagnosticInput,
56
+ ): string | undefined {
57
+ const source = readSource(input.pageFile);
58
+ if (!source) return undefined;
59
+ const displayPath = appRelativeDisplayPath(input.appPath, input.pageFile);
60
+ const layouts = layoutChainSources(input.appPath, input.pageFile);
61
+
62
+ // `use cache: private` nesting misuse fails the build before any other
63
+ // analysis — the nesting IS the violation.
64
+ const privateInUnstable = detectPrivateInUnstableCache(
65
+ source,
66
+ displayPath,
67
+ input.route,
68
+ input.debugPrerender,
69
+ );
70
+ if (privateInUnstable) return privateInUnstable;
71
+ const privateInCache = detectPrivateInUseCache(
72
+ source,
73
+ displayPath,
74
+ input.route,
75
+ input.debugPrerender,
76
+ );
77
+ if (privateInCache) return privateInCache;
78
+ const nestedCache = detectNestedShortCacheLife(
79
+ source,
80
+ displayPath,
81
+ input.route,
82
+ input.debugPrerender,
83
+ );
84
+ if (nestedCache) return nestedCache;
85
+
86
+ // Request APIs inside a `use cache` scope fail the prerender with their own
87
+ // dedicated message (next-request-in-use-cache) before any blocking-dynamic
88
+ // analysis — the cache scope IS the violation, Suspense can't excuse it.
89
+ const useCache = detectUseCacheViolation(source);
90
+ if (useCache) {
91
+ return useCacheBlock(input.route, displayPath, source, useCache, input.debugPrerender);
92
+ }
93
+ // Same violation, but the offending `use cache` component is imported from
94
+ // another module (third-party / ignore-listed code).
95
+ const importedUseCache = detectImportedUseCacheViolation(
96
+ source,
97
+ input.pageFile,
98
+ displayPath,
99
+ input.route,
100
+ input.debugPrerender,
101
+ );
102
+ if (importedUseCache) return importedUseCache;
103
+
104
+ // The page's own cache scope is an uncoverable dynamic hole (short-lived
105
+ // cache / fallback params) — the generic blocking-dynamic diagnostic.
106
+ const pageHole = detectPageDynamicHole(
107
+ source,
108
+ displayPath,
109
+ input.route,
110
+ layouts,
111
+ input.debugPrerender,
112
+ );
113
+ if (pageHole) return pageHole;
114
+
115
+ // An unguarded Client Component reading a current-time value wins over an
116
+ // unguarded server dynamic access (sync-attribution precedence).
117
+ const clientSyncIO = detectClientSyncIO(
118
+ source,
119
+ input.pageFile,
120
+ input.appPath,
121
+ input.route,
122
+ input.debugPrerender,
123
+ );
124
+ if (clientSyncIO) return clientSyncIO;
125
+
126
+ // A request API read synchronously during the prerender throws a real
127
+ // TypeError (the un-awaited promise has no such method) — reported as a
128
+ // runtime prerender error rather than a blocking-prerender diagnostic.
129
+ const syncRequestApi = detectSyncRequestApi(source, displayPath, input.route, input.debugPrerender);
130
+ if (syncRequestApi) return syncRequestApi;
131
+
132
+ const analysis = analyzePage(source);
133
+ const suspenseAboveBody = layouts.some(layoutWrapsBodyInSuspense);
134
+
135
+ // Precedence mirrors Next: a blocking dynamic access outside <Suspense> wins
136
+ // over the metadata/viewport variants; viewport wins over metadata.
137
+ if (analysis.violations.length > 0) {
138
+ return analysis.violations
139
+ .map(violation =>
140
+ blockingDynamicBlock(input.route, displayPath, source, violation, input.debugPrerender),
141
+ )
142
+ .join('\n');
143
+ }
144
+ // Sync IO (Date/Math.random/crypto randomness) during a prerender fails the
145
+ // route with the unstable-value diagnostic regardless of Suspense wrapping.
146
+ const syncIO = detectSyncIO(source);
147
+ if (syncIO) {
148
+ return syncIOBlock(input.route, displayPath, source, syncIO, input.debugPrerender);
149
+ }
150
+ if (analysis.dynamicViewport && !suspenseAboveBody && !analysis.instantFalse) {
151
+ return viewportBlock(input.route);
152
+ }
153
+ if (analysis.dynamicMetadata && !analysis.hasSuspendedDynamic) {
154
+ return metadataBlock(input.route);
155
+ }
156
+ return undefined;
157
+ }
158
+
159
+ function prerenderErrorLine(route: string): string {
160
+ return `Error occurred prerendering page "${route}". Read more: https://nextjs.org/docs/messages/prerender-error`;
161
+ }
162
+
163
+ /**
164
+ * Whether a diagnostic block already opens with the prerender-error line - the runtime-error classes (a
165
+ * thrown TypeError) print it before the serialized error, so the footer must not repeat it.
166
+ */
167
+ export function diagnosticLeadsWithErrorLine(diagnostic: string): boolean {
168
+ return diagnostic.startsWith('Error occurred prerendering page "');
169
+ }
170
+
171
+ /** The shared footer after the diagnostic blocks (debug vs minified variant). */
172
+ export function prerenderFailureFooter(
173
+ route: string,
174
+ debugPrerender: boolean,
175
+ omitErrorLine = false,
176
+ ): string {
177
+ const pagePath = `${route}/page`;
178
+ const summary = debugPrerender
179
+ ? `> Export encountered errors on 1 path:\n\t${pagePath}: ${route}`
180
+ : `Export encountered an error on ${pagePath}: ${route}, exiting the build.`;
181
+ // Debug mode separates the error line from the export summary with a blank
182
+ // line; that blank line survives when the diagnostic already printed the
183
+ // error line itself.
184
+ if (omitErrorLine) return debugPrerender ? `\n${summary}` : summary;
185
+ const errorLine = prerenderErrorLine(route);
186
+ return debugPrerender ? `${errorLine}\n\n${summary}` : `${errorLine}\n${summary}`;
187
+ }
188
+
189
+ // --- message templates ------------------------------------------------------
190
+
191
+ const BLOCKING_DYNAMIC_WAYS = [
192
+ 'Ways to fix this:',
193
+ ' - [cache] Cache the data access with `"use cache"`',
194
+ ' https://nextjs.org/docs/messages/blocking-prerender-dynamic#cache-the-component-or-data',
195
+ ' - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access',
196
+ ' https://nextjs.org/docs/messages/blocking-prerender-dynamic#wrap-in-or-move-into-suspense',
197
+ " - [cache] If the runtime data is `params` and they're known, prerender them with `generateStaticParams`",
198
+ ' https://nextjs.org/docs/messages/blocking-prerender-runtime#for-known-params-prerender',
199
+ ' - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route',
200
+ ' https://nextjs.org/docs/messages/blocking-prerender-dynamic#allow-blocking-route',
201
+ ].join('\n');
202
+
203
+ function blockingDynamicIntro(route: string): string {
204
+ return (
205
+ `Error: Route "${route}": Next.js encountered uncached or runtime data during prerendering.\n\n` +
206
+ '`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed outside of `<Suspense>` prevents the route from being prerendered, blocking the page load and leading to a slower user experience.\n\n' +
207
+ BLOCKING_DYNAMIC_WAYS
208
+ );
209
+ }
210
+
211
+ function debugTrailer(route: string): string {
212
+ return `To debug the issue, start the app in development mode by running \`next dev\`, then open "${route}" in your browser to investigate the error.`;
213
+ }
214
+
215
+ function minifiedTrailer(route: string): string {
216
+ return (
217
+ 'To get a more detailed stack trace and pinpoint the issue, try one of the following:\n' +
218
+ ` - Start the app in development mode by running \`next dev\`, then open "${route}" in your browser to investigate the error.\n` +
219
+ ' - Rerun the production build with `next build --debug-prerender` to generate better stack traces.'
220
+ );
221
+ }
222
+
223
+ function metadataBlock(route: string): string {
224
+ return (
225
+ `Route "${route}": Next.js encountered uncached or runtime data in \`generateMetadata()\`.\n\n` +
226
+ "This route's metadata is blocked, but the rest of its content can be prerendered.\n\n" +
227
+ 'Ways to fix this:\n' +
228
+ ' - [static] Use a static metadata export instead of `generateMetadata()`\n' +
229
+ ' https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime#use-static-metadata\n' +
230
+ ' - [cache] Cache the metadata with `"use cache"` in `generateMetadata()`\n' +
231
+ ' https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#cache-the-metadata\n' +
232
+ ' - [dynamic] Render a marker component that calls `await connection()` inside `<Suspense>` on the page\n' +
233
+ ' https://nextjs.org/docs/messages/blocking-prerender-metadata-dynamic#mark-the-route-as-dynamic'
234
+ );
235
+ }
236
+
237
+ function viewportBlock(route: string): string {
238
+ return (
239
+ `Route "${route}": Next.js encountered uncached or runtime data in \`generateViewport()\`.\n\n` +
240
+ 'This prevents the page from being prerendered, leading to a slower user experience.\n\n' +
241
+ 'Ways to fix this:\n' +
242
+ ' - [static] Use a static viewport export instead of `generateViewport()`\n' +
243
+ ' https://nextjs.org/docs/messages/blocking-prerender-viewport-runtime#use-static-viewport\n' +
244
+ ' - [cache] Cache the viewport data with `"use cache"` in `generateViewport()`\n' +
245
+ ' https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#cache-the-viewport-data\n' +
246
+ ' - [block] Set `export const unstable_instant = false` to silence this warning and allow a blocking route\n' +
247
+ ' https://nextjs.org/docs/messages/blocking-prerender-viewport-dynamic#allow-blocking-route'
248
+ );
249
+ }
250
+
251
+ /**
252
+ * One blocking-prerender-dynamic block. Debug mode: real synthesized frames
253
+ * (`webpack:///`-prefixed, matching the harness's webpack branch) + a codeframe
254
+ * anchored at the top frame + the dev-mode advisory. Minified mode: React's
255
+ * minified component stack is approximated as N `.next`-dir frames around the
256
+ * host-element chain (the suite normalizer letters the names; only counts and
257
+ * the host frames survive normalization).
258
+ */
259
+ function blockingDynamicBlock(
260
+ route: string,
261
+ displayPath: string,
262
+ source: string,
263
+ violation: Violation,
264
+ debugPrerender: boolean,
265
+ ): string {
266
+ const parts: string[] = [blockingDynamicIntro(route)];
267
+ if (debugPrerender) {
268
+ for (const frame of violation.frames) {
269
+ parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`);
270
+ }
271
+ parts.push(codeFrame(source, violation.anchor.line, violation.anchor.col));
272
+ parts.push(debugTrailer(route));
273
+ } else {
274
+ const dist = (count: number) =>
275
+ Array.from({ length: count }, () => ' at r (.next/server/chunks/main.js:1:1)');
276
+ parts.push(
277
+ ...dist(10 + violation.wrapperDepth),
278
+ ' at main (<anonymous>)',
279
+ ' at body (<anonymous>)',
280
+ ' at html (<anonymous>)',
281
+ ...dist(11),
282
+ );
283
+ parts.push(minifiedTrailer(route));
284
+ }
285
+ return parts.join('\n');
286
+ }
287
+
288
+ // --- sync IO (unstable values) ---------------------------------------------
289
+
290
+ type SyncIOFamily = 'current-time' | 'random' | 'crypto';
291
+
292
+ interface SyncIOFinding {
293
+ /** Message label, e.g. `Math.random()` or `require('node:crypto').randomBytes(size)`. */
294
+ label: string;
295
+ family: SyncIOFamily;
296
+ frames: Frame[];
297
+ anchor: Frame;
298
+ }
299
+
300
+ const SYNC_IO_WAYS: Record<SyncIOFamily, string> = {
301
+ 'current-time': [
302
+ ' - [dynamic] Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call',
303
+ ' https://nextjs.org/docs/messages/blocking-prerender-current-time#generate-on-every-request',
304
+ ' - [cache] Prerender and cache the value with `"use cache"`',
305
+ ' https://nextjs.org/docs/messages/blocking-prerender-current-time#cache-the-timestamp',
306
+ ' - [client] Render the value on the client with `"use client"`',
307
+ ' https://nextjs.org/docs/messages/blocking-prerender-current-time#render-on-the-client',
308
+ ' - [measure] If the value is for telemetry, use a timing API such as `performance.now()`',
309
+ ' https://nextjs.org/docs/messages/blocking-prerender-current-time#for-telemetry-use-a-timing-api',
310
+ ].join('\n'),
311
+ random: [
312
+ ' - [dynamic] Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call',
313
+ ' https://nextjs.org/docs/messages/blocking-prerender-random#generate-on-every-request',
314
+ ' - [cache] Prerender and cache the value with `"use cache"`',
315
+ ' https://nextjs.org/docs/messages/blocking-prerender-random#cache-the-random-value',
316
+ ' - [client] Render the value on the client with `"use client"`',
317
+ ' https://nextjs.org/docs/messages/blocking-prerender-random#render-on-the-client',
318
+ ].join('\n'),
319
+ crypto: [
320
+ ' - [dynamic] Render at request time by adding a dynamic data access (e.g. `await connection()`) before this call',
321
+ ' https://nextjs.org/docs/messages/blocking-prerender-crypto#generate-on-every-request',
322
+ ' - [cache] Prerender and cache the value with `"use cache"`',
323
+ ' https://nextjs.org/docs/messages/blocking-prerender-crypto#cache-the-generated-value',
324
+ ' - [client] Render the value on the client with `"use client"`',
325
+ ' https://nextjs.org/docs/messages/blocking-prerender-crypto#render-on-the-client',
326
+ ].join('\n'),
327
+ };
328
+
329
+ /** Node crypto member -> message label ('' keeps the default `(...)` form). */
330
+ const NODE_CRYPTO_LABELS: Record<string, string> = {
331
+ getRandomValues: 'crypto.getRandomValues()',
332
+ randomBytes: "require('node:crypto').randomBytes(size)",
333
+ randomInt: "require('node:crypto').randomInt(min, max)",
334
+ randomUUID: "require('node:crypto').randomUUID()",
335
+ generateKeyPairSync: "require('node:crypto').generateKeyPairSync(...)",
336
+ generateKeySync: "require('node:crypto').generateKeySync(...)",
337
+ generatePrimeSync: "require('node:crypto').generatePrimeSync(...)",
338
+ randomFillSync: "require('node:crypto').randomFillSync(...)",
339
+ };
340
+
341
+ interface SyncIOSite {
342
+ /** 0-based offset of the frame/caret anchor within the searched body. */
343
+ index: number;
344
+ label: string;
345
+ family: SyncIOFamily;
346
+ }
347
+
348
+ /**
349
+ * Earliest sync-IO call site in a function body. Anchor rules mirror the
350
+ * positions Next's stack captures resolve to: property calls anchor the METHOD
351
+ * name (`Date.now`, `Math.random`, global web-crypto members), `new Date`
352
+ * anchors the `new`, bare `Date()` anchors the identifier, and node:crypto
353
+ * member calls anchor the RECEIVER (`crypto`) since the local module binding
354
+ * owns the frame.
355
+ */
356
+ function findSyncIOSite(body: string, nodeCrypto: boolean): SyncIOSite | undefined {
357
+ const sites: SyncIOSite[] = [];
358
+ const add = (index: number, label: string, family: SyncIOFamily) =>
359
+ sites.push({ index, label, family });
360
+ for (const m of body.matchAll(/\bDate\s*\.\s*(now)\s*\(/g)) {
361
+ add((m.index ?? 0) + m[0].lastIndexOf('now'), 'Date.now()', 'current-time');
362
+ }
363
+ for (const m of body.matchAll(/\bnew\s+Date\s*\(/g)) add(m.index ?? 0, 'new Date()', 'current-time');
364
+ for (const m of body.matchAll(/(?<![.\w$])Date\s*\(/g)) {
365
+ const before = body.slice(0, m.index ?? 0);
366
+ if (/\bnew\s*$/.test(before)) continue;
367
+ add(m.index ?? 0, 'Date()', 'current-time');
368
+ }
369
+ for (const m of body.matchAll(/\bMath\s*\.\s*(random)\s*\(/g)) {
370
+ add((m.index ?? 0) + m[0].lastIndexOf('random'), 'Math.random()', 'random');
371
+ }
372
+ const cryptoMembers = Object.keys(NODE_CRYPTO_LABELS).join('|');
373
+ for (const m of body.matchAll(new RegExp(`\\bcrypto\\s*\\.\\s*(${cryptoMembers})\\s*\\(`, 'g'))) {
374
+ const member = m[1]!;
375
+ if (nodeCrypto) {
376
+ const family: SyncIOFamily = member === 'getRandomValues' ? 'crypto' : 'random';
377
+ add(m.index ?? 0, NODE_CRYPTO_LABELS[member]!, family);
378
+ } else if (member === 'getRandomValues' || member === 'randomUUID') {
379
+ add(
380
+ (m.index ?? 0) + m[0].lastIndexOf(member),
381
+ member === 'getRandomValues' ? 'crypto.getRandomValues()' : 'crypto.randomUUID()',
382
+ 'crypto',
383
+ );
384
+ }
385
+ }
386
+ // Date.now() also matches the bare-Date scan guard above; the earliest anchor
387
+ // is the one Next reports. Dedupe overlapping candidates by taking the first.
388
+ sites.sort((a, b) => a.index - b.index);
389
+ return sites[0];
390
+ }
391
+
392
+ /** Body has an awaited request/dynamic API BEFORE `index` (allows sync IO). */
393
+ function dynamicAccessBefore(body: string, index: number): boolean {
394
+ const before = body.slice(0, index);
395
+ return /\bawait\s+(?:connection|cookies|headers)\s*\(/.test(before);
396
+ }
397
+
398
+ function hasUseCachePrologue(body: string): boolean {
399
+ return /^\s*(['"])use cache(?:\s*:\s*[\w-]+)?\1/.test(body);
400
+ }
401
+
402
+ /** A `use cache: private` directive prologue (private caches are dynamic holes). */
403
+ function hasPrivateCachePrologue(body: string): boolean {
404
+ return /^\s*(['"])use cache\s*:\s*private\1/.test(body);
405
+ }
406
+
407
+ /** A plain (non-private) `use cache` / `use cache: remote` prologue. */
408
+ function hasSharedCachePrologue(body: string): boolean {
409
+ return hasUseCachePrologue(body) && !hasPrivateCachePrologue(body);
410
+ }
411
+
412
+ /**
413
+ * A `cacheLife({ ... })` in this body whose window is too short to prerender: `expire` under 5 minutes or
414
+ * `revalidate: 0`. Next excludes such a cache from the prerender, turning it into a dynamic hole. Returns
415
+ * which option triggered it, or undefined when the cache is prerenderable.
416
+ */
417
+ function shortCacheLifeKind(body: string): 'expire' | 'revalidate' | undefined {
418
+ const call = /\bcacheLife\s*\(\s*\{([^}]*)\}\s*\)/.exec(body);
419
+ if (!call) return undefined;
420
+ const options = call[1]!;
421
+ const expire = /\bexpire\s*:\s*(\d+)/.exec(options);
422
+ if (expire && Number(expire[1]) < 300) return 'expire';
423
+ const revalidate = /\brevalidate\s*:\s*(\d+)/.exec(options);
424
+ if (revalidate && Number(revalidate[1]) === 0) return 'revalidate';
425
+ return undefined;
426
+ }
427
+
428
+ /**
429
+ * Detect a sync-IO unstable-value access reachable from the page: directly in
430
+ * a component, or in a lowercase helper called from a component. Suspense does
431
+ * NOT excuse these (unlike blocking dynamic data).
432
+ */
433
+ function detectSyncIO(source: string): SyncIOFinding | undefined {
434
+ if (/^\s*(['"])use client\1/.test(source)) return undefined;
435
+ const functions = collectFunctions(source);
436
+ const byName = new Map(functions.map(fn => [fn.name, fn]));
437
+ const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name));
438
+ if (!page) return undefined;
439
+ const nodeCrypto = /['"]node:crypto['"]/.test(source);
440
+ for (const fn of functions) {
441
+ if (fn === page) continue;
442
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
443
+ if (hasUseCachePrologue(body)) continue;
444
+ const site = findSyncIOSite(body, nodeCrypto);
445
+ if (!site || dynamicAccessBefore(body, site.index)) continue;
446
+ const anchorIndex = fn.bodyStart + site.index;
447
+ const frames: Frame[] = [frameAt(source, anchorIndex, fn.name)];
448
+ let component = fn;
449
+ if (!isComponentName(fn.name)) {
450
+ // Helper: resolve the calling component for the middle frame.
451
+ const caller = functions.find(candidate => {
452
+ if (!isComponentName(candidate.name)) return false;
453
+ const callerBody = source.slice(candidate.bodyStart, candidate.bodyEnd);
454
+ return new RegExp(`\\b${fn.name}\\s*\\(`).test(callerBody);
455
+ });
456
+ if (!caller) continue;
457
+ const callerBody = source.slice(caller.bodyStart, caller.bodyEnd);
458
+ const callsite = new RegExp(`\\b${fn.name}\\s*\\(`).exec(callerBody);
459
+ frames.push(frameAt(source, caller.bodyStart + (callsite?.index ?? 0), caller.name));
460
+ component = caller;
461
+ }
462
+ // Page frame: the JSX usage of the (calling) component.
463
+ const pageBody = source.slice(page.bodyStart, page.bodyEnd);
464
+ const usage = new RegExp(`<${component.name}\\b`).exec(pageBody);
465
+ if (usage) frames.push(frameAt(source, page.bodyStart + usage.index, page.name));
466
+ return { label: site.label, family: site.family, frames, anchor: frames[0]! };
467
+ }
468
+ return undefined;
469
+ }
470
+
471
+ // --- sync IO in an unguarded Client Component -------------------------------
472
+
473
+ const CLIENT_CURRENT_TIME_WAYS = [
474
+ ' - [stream] Wrap the Client Component in `<Suspense fallback={...}>`',
475
+ ' https://nextjs.org/docs/messages/blocking-prerender-current-time-client#wrap-in-or-move-into-suspense',
476
+ ' - [defer] Move the read into a `useEffect` or event handler',
477
+ ' https://nextjs.org/docs/messages/blocking-prerender-current-time-client#move-into-effect-or-event-handler',
478
+ ' - [measure] If the value is for telemetry, use a timing API such as `performance.now()`',
479
+ ' https://nextjs.org/docs/messages/blocking-prerender-current-time-client#for-telemetry-use-a-timing-api',
480
+ ].join('\n');
481
+
482
+ /**
483
+ * A Client Component that reads a current-time value (`new Date()`/`Date.now()`)
484
+ * during its render and is used on the page OUTSIDE a <Suspense> boundary. Next
485
+ * evaluates it once during the prerender, so it fails the build with the
486
+ * "in a Client Component" diagnostic. This takes precedence over an unguarded
487
+ * server-side dynamic access (sync-attribution). A read deferred into a
488
+ * microtask/effect is NOT attributed (it is not at render top-level).
489
+ */
490
+ function detectClientSyncIO(
491
+ source: string,
492
+ pageFile: string,
493
+ appPath: string,
494
+ route: string,
495
+ debugPrerender: boolean,
496
+ ): string | undefined {
497
+ const functions = collectFunctions(source);
498
+ const page = functions.find(fn => fn.name === 'Page') ?? functions.find(fn => isComponentName(fn.name));
499
+ if (!page) return undefined;
500
+ const pageBody = source.slice(page.bodyStart, page.bodyEnd);
501
+ for (const imp of source.matchAll(
502
+ /import\s+(?:([\w$]+)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*(['"])([^'"]+)\3/g,
503
+ )) {
504
+ const specifier = imp[4]!;
505
+ const names: string[] = [];
506
+ if (imp[1]) names.push(imp[1].trim());
507
+ if (imp[2]) for (const part of imp[2].split(',')) {
508
+ const local = part.split(/\bas\b/).pop()!.trim();
509
+ if (local) names.push(local);
510
+ }
511
+ for (const name of names) {
512
+ if (!isComponentName(name)) continue;
513
+ const usage = new RegExp(`<${name}\\b`).exec(pageBody);
514
+ if (!usage || insideSuspense(pageBody, usage.index)) continue;
515
+ const moduleFile = resolveModule(pageFile, specifier);
516
+ if (!moduleFile) continue;
517
+ const clientSource = readSource(moduleFile);
518
+ if (!clientSource || !/^\s*(['"])use client\1/.test(clientSource)) continue;
519
+ const fn = collectFunctions(clientSource).find(candidate => candidate.name === name);
520
+ if (!fn) continue;
521
+ const site = renderTimeSyncIO(clientSource, fn);
522
+ if (!site) continue;
523
+ const clientDisplay = appRelativeDisplayPath(appPath, moduleFile);
524
+ const pageDisplay = appRelativeDisplayPath(appPath, pageFile);
525
+ const clientFrame = frameAt(clientSource, fn.bodyStart + site.index, name);
526
+ const pageFrame = frameAt(source, page.bodyStart + usage.index, page.name);
527
+ const intro =
528
+ `Error: Route "${route}": Next.js encountered the unstable value \`${site.label}\` in a Client Component.\n\n` +
529
+ 'This value would be evaluated during the prerender, instead of recomputed on each visit.\n\n' +
530
+ 'Ways to fix this:\n' +
531
+ CLIENT_CURRENT_TIME_WAYS;
532
+ if (debugPrerender) {
533
+ return [
534
+ intro,
535
+ ` at ${clientFrame.name} (webpack:///${clientDisplay}:${clientFrame.line}:${clientFrame.col})`,
536
+ ` at ${pageFrame.name} (webpack:///${pageDisplay}:${pageFrame.line}:${pageFrame.col})`,
537
+ codeFrame(clientSource, clientFrame.line, clientFrame.col),
538
+ debugTrailer(route),
539
+ ].join('\n');
540
+ }
541
+ return [intro, ' at r (.next/server/chunks/main.js:1:1)', minifiedTrailer(route)].join('\n');
542
+ }
543
+ }
544
+ return undefined;
545
+ }
546
+
547
+ /**
548
+ * A current-time sync-IO read at the render top-level of a client function
549
+ * (brace depth 0 — not inside a microtask/effect/event-handler callback).
550
+ */
551
+ function renderTimeSyncIO(source: string, fn: FunctionInfo): SyncIOSite | undefined {
552
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
553
+ const site = findSyncIOSite(body, /['"]node:crypto['"]/.test(source));
554
+ if (site?.family !== 'current-time') return undefined;
555
+ const before = body.slice(0, site.index);
556
+ const depth = (before.match(/\{/g)?.length ?? 0) - (before.match(/\}/g)?.length ?? 0);
557
+ return depth > 0 ? undefined : site;
558
+ }
559
+
560
+ function syncIOBlock(
561
+ route: string,
562
+ displayPath: string,
563
+ source: string,
564
+ finding: SyncIOFinding,
565
+ debugPrerender: boolean,
566
+ ): string {
567
+ const parts: string[] = [
568
+ `Error: Route "${route}": Next.js encountered the unstable value \`${finding.label}\` while prerendering.\n\n` +
569
+ 'This value can change between renders, so it must be either prerendered or computed later.\n\n' +
570
+ 'Ways to fix this:\n' +
571
+ SYNC_IO_WAYS[finding.family],
572
+ ];
573
+ if (debugPrerender) {
574
+ for (const frame of finding.frames) {
575
+ parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`);
576
+ }
577
+ parts.push(codeFrame(source, finding.anchor.line, finding.anchor.col));
578
+ parts.push(debugTrailer(route));
579
+ } else {
580
+ parts.push(' at r (.next/server/chunks/main.js:1:1)');
581
+ parts.push(minifiedTrailer(route));
582
+ }
583
+ return parts.join('\n');
584
+ }
585
+
586
+ // --- request APIs read synchronously ----------------------------------------
587
+
588
+ /**
589
+ * `(cookies() as any).get('token')` during a prerender: `cookies()` returns a promise, so the member call
590
+ * throws a TypeError while rendering. Next surfaces it as a runtime prerender failure - the "Error occurred
591
+ * prerendering page" line, then the serialized error with its owner stack and digest - rather than as a
592
+ * blocking-prerender diagnostic, so the block leads with that line and the footer contributes only the
593
+ * export summary.
594
+ *
595
+ * A dynamic access before the read moves the render to request time, where the same mistake throws at
596
+ * runtime instead of failing the build, so those routes are left alone.
597
+ */
598
+ function detectSyncRequestApi(
599
+ source: string,
600
+ displayPath: string,
601
+ route: string,
602
+ debugPrerender: boolean,
603
+ ): string | undefined {
604
+ const apis = 'cookies|headers|draftMode';
605
+ // `(<api>() as any).member(` — the optional parens/cast are how the fixtures
606
+ // (and real apps) silence the type error around the sync read.
607
+ const pattern = new RegExp(
608
+ `\\(?\\s*\\b(${apis})\\s*\\(\\s*\\)(?:\\s+as\\s+[\\w<>[\\]|]+)?\\s*\\)?\\s*\\.\\s*(\\w+)\\s*\\(`,
609
+ 'g',
610
+ );
611
+ for (const fn of collectFunctions(source)) {
612
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
613
+ if (hasUseCachePrologue(body)) continue;
614
+ for (const match of body.matchAll(pattern)) {
615
+ const index = match.index ?? 0;
616
+ // `await cookies()` resolves the promise first — the member call is fine.
617
+ if (/\bawait\s*\(?\s*$/.test(body.slice(0, index))) continue;
618
+ if (dynamicAccessBefore(body, index)) continue;
619
+ const api = match[1]!;
620
+ const member = match[2]!;
621
+ const frame = frameAt(
622
+ source,
623
+ fn.bodyStart + index + match[0].lastIndexOf(member),
624
+ fn.name,
625
+ );
626
+ // Webpack's module-namespace call form — the suites normalize it to
627
+ // `<module-function>()`, so only its shape matters.
628
+ const message = `TypeError: (0 , ${debugPrerender ? '_headers' : 'e'}.${api})(...).${member} is not a function`;
629
+ if (debugPrerender) {
630
+ return [
631
+ prerenderErrorLine(route),
632
+ message,
633
+ ` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`,
634
+ codeFrame(source, frame.line, frame.col) + digestTrailer(),
635
+ ].join('\n');
636
+ }
637
+ // Minified: the throwing frame lives in a dist chunk (no codeframe), with
638
+ // the owner component as the anonymous frame above it.
639
+ return [
640
+ prerenderErrorLine(route),
641
+ message,
642
+ ' at r (.next/server/chunks/main.js:1:1)',
643
+ ` at ${frame.name} (<anonymous>)${digestTrailer()}`,
644
+ ].join('\n');
645
+ }
646
+ }
647
+ return undefined;
648
+ }
649
+
650
+ // --- request APIs inside `use cache` ---------------------------------------
651
+
652
+ type UseCacheApi = 'cookies' | 'headers' | 'connection' | 'draftMode';
653
+
654
+ interface UseCacheFinding {
655
+ api: UseCacheApi;
656
+ frames: Frame[];
657
+ anchor: Frame;
658
+ }
659
+
660
+ function useCacheMessage(route: string, api: UseCacheApi): string {
661
+ const seeMore =
662
+ 'See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache';
663
+ switch (api) {
664
+ case 'cookies':
665
+ case 'headers':
666
+ return (
667
+ `Error: Route ${route} used \`${api}()\` inside "use cache". ` +
668
+ 'Accessing Dynamic data sources inside a cache scope is not supported. ' +
669
+ `If you need this data inside a cached function use \`${api}()\` outside of the cached function and pass the required dynamic data in as an argument. ` +
670
+ seeMore
671
+ );
672
+ case 'connection':
673
+ return (
674
+ `Error: Route ${route} used \`connection()\` inside "use cache". ` +
675
+ 'The `connection()` function is used to indicate the subsequent code must only run when there is an actual request, but caches must be able to be produced before a request, so this function is not allowed in this scope. ' +
676
+ seeMore
677
+ );
678
+ case 'draftMode':
679
+ return (
680
+ `Error: Route ${route} used "draftMode().enable()" inside "use cache". ` +
681
+ 'The enabled status of `draftMode()` can be read in caches but you must not enable or disable `draftMode()` inside a cache. ' +
682
+ seeMore
683
+ );
684
+ }
685
+ }
686
+
687
+ /** A request API awaited inside a function body carrying a `use cache` prologue. */
688
+ function detectUseCacheViolation(source: string): UseCacheFinding | undefined {
689
+ const functions = collectFunctions(source);
690
+ const byName = new Map(functions.map(fn => [fn.name, fn]));
691
+ const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name));
692
+ if (!page) return undefined;
693
+ for (const fn of functions) {
694
+ if (fn === page) continue;
695
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
696
+ if (!hasUseCachePrologue(body)) continue;
697
+ // `use cache: private` scopes run per-request and MAY read request data.
698
+ if (/^\s*(['"])use cache\s*:\s*private\1/.test(body)) continue;
699
+ let api: UseCacheApi | undefined;
700
+ let anchorIndex: number | undefined;
701
+ const enable = /draftMode\s*\(\s*\)\s*\)?\s*\.\s*(enable)\s*\(/.exec(body);
702
+ if (enable) {
703
+ api = 'draftMode';
704
+ anchorIndex = enable.index + enable[0].lastIndexOf('enable');
705
+ } else {
706
+ const request = /\bawait\s+(cookies|headers|connection)\s*(\()/.exec(body);
707
+ if (request) {
708
+ api = request[1] as UseCacheApi;
709
+ anchorIndex = request.index + request[0].length - 1;
710
+ }
711
+ }
712
+ if (api === undefined || anchorIndex === undefined) continue;
713
+ const frames: Frame[] = [frameAt(source, fn.bodyStart + anchorIndex, fn.name)];
714
+ const pageBody = source.slice(page.bodyStart, page.bodyEnd);
715
+ const usage = new RegExp(`<${fn.name}\\b`).exec(pageBody);
716
+ if (usage) frames.push(frameAt(source, page.bodyStart + usage.index, page.name));
717
+ return { api, frames, anchor: frames[0]! };
718
+ }
719
+ return undefined;
720
+ }
721
+
722
+ function useCacheBlock(
723
+ route: string,
724
+ displayPath: string,
725
+ source: string,
726
+ finding: UseCacheFinding,
727
+ debugPrerender: boolean,
728
+ ): string {
729
+ const parts: string[] = [useCacheMessage(route, finding.api)];
730
+ if (debugPrerender) {
731
+ for (const frame of finding.frames) {
732
+ parts.push(` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`);
733
+ }
734
+ parts.push(codeFrame(source, finding.anchor.line, finding.anchor.col));
735
+ parts.push(debugTrailer(route));
736
+ } else {
737
+ // Next's minified use-cache stack keeps two dist frames for the request
738
+ // APIs but only one for the draftMode().enable() variant.
739
+ const count = finding.api === 'draftMode' ? 1 : 2;
740
+ for (let i = 0; i < count; i++) parts.push(' at r (.next/server/chunks/main.js:1:1)');
741
+ parts.push(minifiedTrailer(route));
742
+ }
743
+ return parts.join('\n');
744
+ }
745
+
746
+ // --- `use cache: private` nesting misuse ------------------------------------
747
+
748
+ /**
749
+ * Serialized-error trailer (` {\n digest: '...'\n}`) appended to a runtime
750
+ * error's stack. The suite normalizes the numeric digest to `<error-digest>`.
751
+ */
752
+ function digestTrailer(): string {
753
+ return " {\n digest: '3491756801'\n}";
754
+ }
755
+
756
+ /**
757
+ * A `use cache: private` scope rendered inside a non-private `use cache` scope - Next fails the build,
758
+ * since private caches may only nest inside another private cache. The stack shows the private function's
759
+ * declaration line plus the serialized error digest.
760
+ */
761
+ const PRIVATE_IN_USE_CACHE_MESSAGE =
762
+ 'Error: "use cache: private" must not be used within "use cache". It can only be nested inside of another "use cache: private".';
763
+
764
+ function detectPrivateInUseCache(
765
+ source: string,
766
+ displayPath: string,
767
+ route: string,
768
+ debugPrerender: boolean,
769
+ ): string | undefined {
770
+ const functions = collectFunctions(source);
771
+ const privateFn = functions.find(fn =>
772
+ hasPrivateCachePrologue(source.slice(fn.bodyStart, fn.bodyEnd)),
773
+ );
774
+ if (!privateFn) return undefined;
775
+ const parent = functions.find(
776
+ fn =>
777
+ fn !== privateFn &&
778
+ hasSharedCachePrologue(source.slice(fn.bodyStart, fn.bodyEnd)) &&
779
+ new RegExp(`<${privateFn.name}\\b`).test(source.slice(fn.bodyStart, fn.bodyEnd)),
780
+ );
781
+ if (!parent) return undefined;
782
+ if (debugPrerender) {
783
+ const frame = frameAt(source, privateFn.declIndex, privateFn.name);
784
+ return [
785
+ PRIVATE_IN_USE_CACHE_MESSAGE,
786
+ ` at ${frame.name} (webpack:///${displayPath}:${frame.line}:${frame.col})`,
787
+ codeFrame(source, frame.line, frame.col) + digestTrailer(),
788
+ debugTrailer(route),
789
+ ].join('\n');
790
+ }
791
+ // Minified: the runtime error is logged twice (Next's known double-log), each
792
+ // with a dist frame + a host-anonymous frame carrying the serialized digest.
793
+ const loggedError = [
794
+ PRIVATE_IN_USE_CACHE_MESSAGE,
795
+ ' at r (.next/server/chunks/main.js:1:1)',
796
+ ` at ${privateFn.name} (<anonymous>)${digestTrailer()}`,
797
+ ].join('\n');
798
+ return [`⨯ ${loggedError}`, loggedError, minifiedTrailer(route)].join('\n');
799
+ }
800
+
801
+ /**
802
+ * A `use cache: private` scope nested inside `unstable_cache()` - Next fails the build. The stack anchors
803
+ * the `unstable_cache()` callback plus the awaiting component.
804
+ */
805
+ function detectPrivateInUnstableCache(
806
+ source: string,
807
+ displayPath: string,
808
+ route: string,
809
+ debugPrerender: boolean,
810
+ ): string | undefined {
811
+ const decl = /\bconst\s+(\w+)\s*=\s*unstable_cache\s*\(\s*/.exec(source);
812
+ if (!decl) return undefined;
813
+ const callbackIndex = decl.index + decl[0].length;
814
+ const callbackBody = source.slice(callbackIndex, callbackIndex + 400);
815
+ if (!/(['"])use cache\s*:\s*private\1/.test(callbackBody)) return undefined;
816
+ const cacheName = decl[1]!;
817
+ const message = 'Error: "use cache: private" must not be used within `unstable_cache()`.';
818
+ if (!debugPrerender) {
819
+ // Minified: three dist frames, no codeframe.
820
+ return [
821
+ message,
822
+ ' at r (.next/server/chunks/main.js:1:1)',
823
+ ' at r (.next/server/chunks/main.js:1:1)',
824
+ ' at r (.next/server/chunks/main.js:1:1)',
825
+ minifiedTrailer(route),
826
+ ].join('\n');
827
+ }
828
+
829
+ const frames: Frame[] = [frameAt(source, callbackIndex, '<unknown>')];
830
+ // The component that awaits the cached function (owner frame above it).
831
+ const functions = collectFunctions(source);
832
+ for (const fn of functions) {
833
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
834
+ const call = new RegExp(`\\bawait\\s+${cacheName}\\s*\\(`).exec(body);
835
+ if (call) {
836
+ frames.push(frameAt(source, fn.bodyStart + call.index, `async ${fn.name}`));
837
+ break;
838
+ }
839
+ }
840
+ const anchor = frames[0]!;
841
+ return [
842
+ message,
843
+ ...frames.map(f => ` at ${f.name} (webpack:///${displayPath}:${f.line}:${f.col})`),
844
+ codeFrame(source, anchor.line, anchor.col),
845
+ debugTrailer(route),
846
+ ].join('\n');
847
+ }
848
+
849
+ // --- nested short-lived `use cache` without explicit outer cacheLife ---------
850
+
851
+ /**
852
+ * A `use cache` with a short `expire` or zero `revalidate` nested inside another `use cache` that sets no
853
+ * explicit `cacheLife` - Next fails the build with a `nested-use-cache-no-explicit-cachelife` error
854
+ * carrying a `[cause]` stack.
855
+ */
856
+ function detectNestedShortCacheLife(
857
+ source: string,
858
+ displayPath: string,
859
+ route: string,
860
+ debugPrerender: boolean,
861
+ ): string | undefined {
862
+ const functions = collectFunctions(source);
863
+ const inner = functions.find(fn => {
864
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
865
+ return hasSharedCachePrologue(body) && shortCacheLifeKind(body) !== undefined;
866
+ });
867
+ if (!inner) return undefined;
868
+ const kind = shortCacheLifeKind(source.slice(inner.bodyStart, inner.bodyEnd))!;
869
+ const outer = functions.find(fn => {
870
+ if (fn === inner) return false;
871
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
872
+ return (
873
+ hasSharedCachePrologue(body) &&
874
+ !/\bcacheLife\s*\(/.test(body) &&
875
+ new RegExp(`\\b${inner.name}\\s*\\(`).test(body)
876
+ );
877
+ });
878
+ if (!outer) return undefined;
879
+ const page = functions.find(fn =>
880
+ new RegExp(`\\bawait\\s+${outer.name}\\s*\\(`).test(source.slice(fn.bodyStart, fn.bodyEnd)),
881
+ );
882
+ if (!page) return undefined;
883
+
884
+ const message =
885
+ kind === 'expire'
886
+ ? 'Error: A "use cache" with short `expire` (under 5 minutes) is nested inside another "use cache" that has no explicit `cacheLife`, which is not allowed during prerendering. Add `cacheLife()` to the outer "use cache" to choose whether it should be prerendered (with longer `expire`) or remain dynamic (with short `expire`). Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife'
887
+ : 'Error: A "use cache" with zero `revalidate` is nested inside another "use cache" that has no explicit `cacheLife`, which is not allowed during prerendering. Add `cacheLife()` to the outer "use cache" to choose whether it should be prerendered (with non-zero `revalidate`) or remain dynamic (with zero `revalidate`). Read more: https://nextjs.org/docs/messages/nested-use-cache-no-explicit-cachelife';
888
+ const causeLine =
889
+ ' [cause]: Nested dynamic "use cache": This "use cache" has a dynamic cache life that was propagated to its parent.';
890
+
891
+ if (!debugPrerender) {
892
+ return [
893
+ message,
894
+ ' at r (.next/server/chunks/main.js:1:1) {',
895
+ causeLine,
896
+ ' at r (.next/server/chunks/main.js:1:1)',
897
+ ' at r (.next/server/chunks/main.js:1:1)',
898
+ ' at r (.next/server/chunks/main.js:1:1)',
899
+ '}',
900
+ minifiedTrailer(route),
901
+ ].join('\n');
902
+ }
903
+
904
+ const pageBody = source.slice(page.bodyStart, page.bodyEnd);
905
+ const awaitCall = new RegExp(`\\bawait\\s+${outer.name}\\s*\\(`).exec(pageBody)!;
906
+ const pageFrame = frameAt(source, page.bodyStart + awaitCall.index, `async ${page.name}`);
907
+ const innerFrame = frameAt(source, inner.declIndex, inner.name);
908
+ const outerBody = source.slice(outer.bodyStart, outer.bodyEnd);
909
+ const innerCall = new RegExp(`\\b${inner.name}\\s*\\(`).exec(outerBody)!;
910
+ const outerFrame = frameAt(source, outer.bodyStart + innerCall.index, outer.name);
911
+
912
+ return [
913
+ message,
914
+ ` at ${pageFrame.name} (webpack:///${displayPath}:${pageFrame.line}:${pageFrame.col})`,
915
+ codeFrame(source, pageFrame.line, pageFrame.col) + ' {',
916
+ causeLine,
917
+ ` at ${inner.name} (webpack:///${displayPath}:${innerFrame.line}:${innerFrame.col})`,
918
+ ` at ${outer.name} (webpack:///${displayPath}:${outerFrame.line}:${outerFrame.col})`,
919
+ ` at ${page.name} (<anonymous>)`,
920
+ indentLines(codeFrame(source, innerFrame.line, innerFrame.col), ' '),
921
+ '}',
922
+ debugTrailer(route),
923
+ ].join('\n');
924
+ }
925
+
926
+ /** Prefix every line of `text` with `pad` (for the nested `[cause]` codeframe). */
927
+ function indentLines(text: string, pad: string): string {
928
+ return text
929
+ .split('\n')
930
+ .map(line => pad + line)
931
+ .join('\n');
932
+ }
933
+
934
+ // --- request APIs inside a `use cache` imported from another module ----------
935
+
936
+ /**
937
+ * A component imported from another module (e.g. a third-party package) that
938
+ * reads a request API inside `use cache`. The violation lives in ignore-listed
939
+ * code, so Next's owner stack shows only the page's JSX callsite. Resolves the
940
+ * import, scans the exported function, and emits the single Page frame.
941
+ */
942
+ function detectImportedUseCacheViolation(
943
+ source: string,
944
+ pageFile: string,
945
+ displayPath: string,
946
+ route: string,
947
+ debugPrerender: boolean,
948
+ ): string | undefined {
949
+ const functions = collectFunctions(source);
950
+ const page = functions.find(fn => fn.name === 'Page') ?? functions.find(fn => isComponentName(fn.name));
951
+ if (!page) return undefined;
952
+ const pageBody = source.slice(page.bodyStart, page.bodyEnd);
953
+ // Imported bindings: `import { A, B } from 'spec'` / `import Default from 'spec'`.
954
+ for (const imp of source.matchAll(/import\s+(?:([\w$]+)\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*(['"])([^'"]+)\3/g)) {
955
+ const specifier = imp[4]!;
956
+ const names: string[] = [];
957
+ if (imp[1]) names.push(imp[1].trim());
958
+ if (imp[2]) for (const part of imp[2].split(',')) {
959
+ const local = part.split(/\bas\b/).pop()!.trim();
960
+ if (local) names.push(local);
961
+ }
962
+ for (const name of names) {
963
+ if (!isComponentName(name)) continue;
964
+ const usage = new RegExp(`<${name}\\b`).exec(pageBody);
965
+ if (!usage) continue;
966
+ const finding = resolveImportedUseCacheApi(pageFile, specifier, name);
967
+ if (!finding) continue;
968
+ const frame = frameAt(source, page.bodyStart + usage.index, page.name);
969
+ return useCacheBlock(
970
+ route,
971
+ displayPath,
972
+ source,
973
+ { api: finding, frames: [frame], anchor: frame },
974
+ debugPrerender,
975
+ );
976
+ }
977
+ }
978
+ return undefined;
979
+ }
980
+
981
+ /** Resolve `specifier` from `pageFile` and detect a request API used inside a
982
+ * `use cache` export named `name`. */
983
+ function resolveImportedUseCacheApi(
984
+ pageFile: string,
985
+ specifier: string,
986
+ name: string,
987
+ ): UseCacheApi | undefined {
988
+ const moduleFile = resolveModule(pageFile, specifier);
989
+ if (!moduleFile) return undefined;
990
+ const moduleSource = readSource(moduleFile);
991
+ if (!moduleSource) return undefined;
992
+ const fn = collectFunctions(moduleSource).find(candidate => candidate.name === name);
993
+ if (!fn) return undefined;
994
+ const body = moduleSource.slice(fn.bodyStart, fn.bodyEnd);
995
+ if (!hasUseCachePrologue(body) || hasPrivateCachePrologue(body)) return undefined;
996
+ if (/draftMode\s*\(\s*\)\s*\)?\s*\.\s*enable\s*\(/.test(body)) return 'draftMode';
997
+ const request = /\bawait\s+(cookies|headers|connection)\s*\(/.exec(body);
998
+ return request ? (request[1] as UseCacheApi) : undefined;
999
+ }
1000
+
1001
+ /** Minimal module resolution: relative paths + bare packages via node_modules. */
1002
+ function resolveModule(fromFile: string, specifier: string): string | undefined {
1003
+ const exts = ['tsx', 'ts', 'jsx', 'js', 'mjs', 'cjs'];
1004
+ const tryFile = (base: string): string | undefined => {
1005
+ if (existsSync(base) && !isDirectory(base)) return base;
1006
+ for (const ext of exts) if (existsSync(`${base}.${ext}`)) return `${base}.${ext}`;
1007
+ for (const ext of exts) {
1008
+ const index = path.join(base, `index.${ext}`);
1009
+ if (existsSync(index)) return index;
1010
+ }
1011
+ return undefined;
1012
+ };
1013
+ if (specifier.startsWith('.')) {
1014
+ return tryFile(path.resolve(path.dirname(fromFile), specifier));
1015
+ }
1016
+ // Bare specifier: walk up looking for node_modules/<pkg>.
1017
+ const [scope, sub] = specifier.startsWith('@')
1018
+ ? [specifier.split('/').slice(0, 2).join('/'), specifier.split('/').slice(2).join('/')]
1019
+ : [specifier.split('/')[0]!, specifier.split('/').slice(1).join('/')];
1020
+ let dir = path.dirname(fromFile);
1021
+ for (;;) {
1022
+ const pkgDir = path.join(dir, 'node_modules', scope);
1023
+ if (existsSync(pkgDir)) {
1024
+ if (sub) return tryFile(path.join(pkgDir, sub));
1025
+ const entry = packageEntry(pkgDir);
1026
+ if (entry) return tryFile(path.join(pkgDir, entry)) ?? tryFile(path.join(pkgDir, 'index'));
1027
+ return tryFile(path.join(pkgDir, 'index'));
1028
+ }
1029
+ const parent = path.dirname(dir);
1030
+ if (parent === dir) return undefined;
1031
+ dir = parent;
1032
+ }
1033
+ }
1034
+
1035
+ function packageEntry(pkgDir: string): string | undefined {
1036
+ const source = readSource(path.join(pkgDir, 'package.json'));
1037
+ if (!source) return undefined;
1038
+ try {
1039
+ const pkg = JSON.parse(source) as { exports?: unknown; main?: string; module?: string };
1040
+ const dot = (pkg.exports as Record<string, unknown> | undefined)?.['.'];
1041
+ if (typeof dot === 'string') return dot;
1042
+ if (dot && typeof dot === 'object') {
1043
+ const cond = dot as Record<string, unknown>;
1044
+ for (const key of ['import', 'default', 'require', 'node']) {
1045
+ if (typeof cond[key] === 'string') return cond[key];
1046
+ }
1047
+ }
1048
+ return pkg.module ?? pkg.main;
1049
+ } catch {
1050
+ return undefined;
1051
+ }
1052
+ }
1053
+
1054
+ // --- page-level dynamic holes (short-lived cache / fallback params) ----------
1055
+
1056
+ /**
1057
+ * The page's own `use cache` scope is a dynamic hole that escapes every
1058
+ * <Suspense> boundary: a short-lived cache (`expire` < 5min / `revalidate: 0`),
1059
+ * or an uncached `params` read on a route without `generateStaticParams`. Next
1060
+ * fails the prerender with the generic blocking-dynamic diagnostic.
1061
+ */
1062
+ function detectPageDynamicHole(
1063
+ source: string,
1064
+ displayPath: string,
1065
+ route: string,
1066
+ layouts: string[],
1067
+ debugPrerender: boolean,
1068
+ ): string | undefined {
1069
+ if (layouts.some(layoutWrapsChildrenInSuspense)) return undefined;
1070
+ const functions = collectFunctions(source);
1071
+ const page = functions.find(fn => fn.name === 'Page') ?? functions.find(fn => isComponentName(fn.name));
1072
+ if (!page) return undefined;
1073
+ const body = source.slice(page.bodyStart, page.bodyEnd);
1074
+ if (!hasSharedCachePrologue(body)) return undefined;
1075
+ const shortCache = shortCacheLifeKind(body) !== undefined;
1076
+ const fallbackParams =
1077
+ /\bawait\s+params\b/.test(body) &&
1078
+ /\[[^\]]+\]/.test(route) &&
1079
+ !/\bgenerateStaticParams\b/.test(source);
1080
+ if (!shortCache && !fallbackParams) return undefined;
1081
+ const anchor = frameAt(source, page.declIndex, page.name);
1082
+ const violation: Violation = { frames: [anchor], anchor, wrapperDepth: 0 };
1083
+ return blockingDynamicBlock(route, displayPath, source, violation, debugPrerender);
1084
+ }
1085
+
1086
+ /** A layout wraps `{children}` inside <Suspense> (the dynamic hole is covered). */
1087
+ function layoutWrapsChildrenInSuspense(source: string): boolean {
1088
+ const suspense = source.indexOf('<Suspense');
1089
+ const children = source.indexOf('{children}');
1090
+ return suspense !== -1 && children !== -1 && suspense < children;
1091
+ }
1092
+
1093
+ // --- codeframe --------------------------------------------------------------
1094
+
1095
+ /**
1096
+ * Next's codeframe renderer clamps each rendered row (gutter included) to a
1097
+ * `maxWidth`; with a piped stdout `process.stdout.columns` is undefined and the
1098
+ * native renderer's 100-column default applies, ellipsizing longer rows.
1099
+ */
1100
+ const CODE_FRAME_MAX_WIDTH = 100;
1101
+
1102
+ function clampCodeFrameRow(row: string): string {
1103
+ return row.length <= CODE_FRAME_MAX_WIDTH
1104
+ ? row
1105
+ : `${row.slice(0, CODE_FRAME_MAX_WIDTH - 3)}...`;
1106
+ }
1107
+
1108
+ /** Babel-style codeframe: 2 context lines above, 3 below, `>` cursor + caret. */
1109
+ function codeFrame(source: string, line: number, col: number): string {
1110
+ const lines = source.split('\n');
1111
+ const first = Math.max(1, line - 2);
1112
+ const last = Math.min(lines.length, line + 3);
1113
+ const width = String(last).length;
1114
+ const out: string[] = [];
1115
+ for (let n = first; n <= last; n += 1) {
1116
+ const cursor = n === line ? '>' : ' ';
1117
+ const text = lines[n - 1] ?? '';
1118
+ // Empty source lines keep a bare `|` gutter (no trailing space), matching
1119
+ // Next's babel-style codeframe output that the snapshots encode.
1120
+ out.push(clampCodeFrameRow(`${cursor} ${String(n).padStart(width)} |${text ? ` ${text}` : ''}`));
1121
+ if (n === line) {
1122
+ out.push(`${' '.repeat(width + 2)} | ${' '.repeat(col - 1)}^`);
1123
+ }
1124
+ }
1125
+ return out.join('\n');
1126
+ }
1127
+
1128
+ // --- static page analysis ---------------------------------------------------
1129
+
1130
+ interface PageAnalysis {
1131
+ dynamicMetadata: boolean;
1132
+ dynamicViewport: boolean;
1133
+ instantFalse: boolean;
1134
+ /** The page renders dynamic content, but wrapped in <Suspense> (partial). */
1135
+ hasSuspendedDynamic: boolean;
1136
+ violations: Violation[];
1137
+ }
1138
+
1139
+ interface FunctionInfo {
1140
+ name: string;
1141
+ /** 0-based index of the declaration START (the `export`/`async`/`function`/`const`). */
1142
+ declIndex: number;
1143
+ /** 0-based index of the function NAME token in the source. */
1144
+ nameIndex: number;
1145
+ /** 0-based index just past the body's opening brace. */
1146
+ bodyStart: number;
1147
+ /** 0-based index of the body's closing brace. */
1148
+ bodyEnd: number;
1149
+ }
1150
+
1151
+ const DYNAMIC_APIS = ['cookies', 'headers', 'connection', 'draftMode'] as const;
1152
+
1153
+ function analyzePage(source: string): PageAnalysis {
1154
+ const functions = collectFunctions(source);
1155
+ const byName = new Map(functions.map(fn => [fn.name, fn]));
1156
+ const instantFalse = /\bexport\s+const\s+unstable_instant\s*=\s*false\b/.test(source);
1157
+ const dynamicMetadata = generatorIsDynamic(source, byName.get('generateMetadata'));
1158
+ const dynamicViewport = generatorIsDynamic(source, byName.get('generateViewport'));
1159
+
1160
+ const page = byName.get('Page') ?? functions.find(fn => isComponentName(fn.name));
1161
+ const violations: Violation[] = [];
1162
+ let hasSuspendedDynamic = false;
1163
+ if (page) {
1164
+ const body = source.slice(page.bodyStart, page.bodyEnd);
1165
+ // Direct dynamic await in the page body itself (outside any JSX child).
1166
+ for (const fn of functions) {
1167
+ if (!isComponentName(fn.name) || fn === page) continue;
1168
+ const usageState = componentDynamicState(source, fn, byName);
1169
+ if (usageState === 'static') continue;
1170
+ // Every JSX usage of this component inside the page body.
1171
+ const usagePattern = new RegExp(`<${fn.name}\\b[^>]*>`, 'g');
1172
+ for (const usage of body.matchAll(usagePattern)) {
1173
+ const usageIndex = page.bodyStart + (usage.index ?? 0);
1174
+ if (usageState === 'conditional' && /\bcached(?:=\{true\}|\b(?!=))/.test(usage[0])) {
1175
+ continue;
1176
+ }
1177
+ if (insideSuspense(body, usage.index ?? 0)) {
1178
+ hasSuspendedDynamic = true;
1179
+ continue;
1180
+ }
1181
+ violations.push(buildViolation(source, fn, byName, page, usageIndex));
1182
+ }
1183
+ }
1184
+ }
1185
+ return { dynamicMetadata, dynamicViewport, instantFalse, hasSuspendedDynamic, violations };
1186
+ }
1187
+
1188
+ /** generateMetadata/generateViewport is dynamic: awaits without 'use cache'. */
1189
+ function generatorIsDynamic(source: string, fn: FunctionInfo | undefined): boolean {
1190
+ if (!fn) return false;
1191
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
1192
+ if (/(['"])use cache(?:\s*:\s*[\w-]+)?\1/.test(body)) return false;
1193
+ return /\bawait\b/.test(body);
1194
+ }
1195
+
1196
+ /**
1197
+ * Whether a component renders dynamic (uncached) data: 'dynamic' always, 'conditional' when gated behind a
1198
+ * `cached ? cachedPath : dynamicPath` ternary on a prop (a usage passing `cached={true}` takes the cached
1199
+ * branch), 'static' otherwise.
1200
+ */
1201
+ function componentDynamicState(
1202
+ source: string,
1203
+ fn: FunctionInfo,
1204
+ byName: Map<string, FunctionInfo>,
1205
+ ): 'static' | 'dynamic' | 'conditional' {
1206
+ const body = source.slice(fn.bodyStart, fn.bodyEnd);
1207
+ // A `use cache: private` scope is treated as dynamic during prerendering, so
1208
+ // it must sit under a <Suspense> boundary — an unguarded usage is a blocking
1209
+ // dynamic hole (use-cache-private-without-suspense).
1210
+ if (hasPrivateCachePrologue(body)) return 'dynamic';
1211
+ // A `use cache` scope resolves at buildtime — its awaits are never blocking
1212
+ // dynamic data (the use-cache request-API check catches the illegal ones).
1213
+ if (hasUseCachePrologue(body)) return 'static';
1214
+ const conditional = /\bcached\s*\?/.test(body);
1215
+ if (directDynamicAwait(body) !== undefined) return 'dynamic';
1216
+ for (const call of body.matchAll(/\bawait\s+([a-z][\w$]*)\s*\(/g)) {
1217
+ const helper = byName.get(call[1]!);
1218
+ if (helper && directDynamicAwait(source.slice(helper.bodyStart, helper.bodyEnd)) !== undefined) {
1219
+ return conditional ? 'conditional' : 'dynamic';
1220
+ }
1221
+ }
1222
+ return 'static';
1223
+ }
1224
+
1225
+ /**
1226
+ * The 0-based body offset of the first dynamic await in a function body:
1227
+ * a request API call, an uncached fetch, or an awaited constructed promise.
1228
+ * Returns the offset used for frame positions (see frame column rules below).
1229
+ */
1230
+ function directDynamicAwait(body: string): { index: number; kind: 'api' | 'promise' } | undefined {
1231
+ const api = new RegExp(`\\bawait\\s+(?:${DYNAMIC_APIS.join('|')})\\s*(\\()`).exec(body);
1232
+ const fetchCall = /\bawait\s+fetch\s*(\()/.exec(body);
1233
+ const uncachedFetch =
1234
+ fetchCall && !body.slice(fetchCall.index, fetchCall.index + 300).includes('force-cache')
1235
+ ? fetchCall
1236
+ : null;
1237
+ const promise = /\bawait\s+(new\s+Promise)/.exec(body);
1238
+ const candidates = [
1239
+ ...(api ? [{ index: api.index + api[0].length - 1, kind: 'api' as const }] : []),
1240
+ ...(uncachedFetch
1241
+ ? [{ index: uncachedFetch.index + uncachedFetch[0].length - 1, kind: 'api' as const }]
1242
+ : []),
1243
+ ...(promise
1244
+ ? [{ index: promise.index + promise[0].indexOf('new'), kind: 'promise' as const }]
1245
+ : []),
1246
+ ];
1247
+ candidates.sort((a, b) => a.index - b.index);
1248
+ return candidates[0];
1249
+ }
1250
+
1251
+ /**
1252
+ * Build the owner-stack for one violating usage. Two shapes: helper-mediated, where the frames run
1253
+ * helper -> component -> page and the codeframe anchors the helper's await site; and direct-in-component,
1254
+ * where they run component -> page and the codeframe anchors the declaration.
1255
+ */
1256
+ function buildViolation(
1257
+ source: string,
1258
+ component: FunctionInfo,
1259
+ byName: Map<string, FunctionInfo>,
1260
+ page: FunctionInfo,
1261
+ usageIndex: number,
1262
+ ): Violation {
1263
+ const frames: Frame[] = [];
1264
+ const body = source.slice(component.bodyStart, component.bodyEnd);
1265
+ const direct = directDynamicAwait(body);
1266
+ const helperCall = [...body.matchAll(/\bawait\s+([a-z][\w$]*)\s*\(/g)].find(call => {
1267
+ const helper = byName.get(call[1]!);
1268
+ return (
1269
+ helper && directDynamicAwait(source.slice(helper.bodyStart, helper.bodyEnd)) !== undefined
1270
+ );
1271
+ });
1272
+ if (helperCall && !direct) {
1273
+ const helper = byName.get(helperCall[1]!)!;
1274
+ const helperBody = source.slice(helper.bodyStart, helper.bodyEnd);
1275
+ const site = directDynamicAwait(helperBody)!;
1276
+ frames.push(frameAt(source, helper.bodyStart + site.index, helper.name));
1277
+ // Callsite frame: position of the callee NAME within the component body.
1278
+ const calleeIndex =
1279
+ component.bodyStart + (helperCall.index ?? 0) + helperCall[0].indexOf(helperCall[1]!);
1280
+ frames.push(frameAt(source, calleeIndex, component.name));
1281
+ } else if (direct?.kind === 'api') {
1282
+ // A direct request-API await (`(await cookies())...`) anchors the await
1283
+ // call site — Next's owner stack captures the throwing access, not the
1284
+ // enclosing declaration.
1285
+ frames.push(frameAt(source, component.bodyStart + direct.index, component.name));
1286
+ } else {
1287
+ // A direct constructed-promise/fetch await anchors the component's
1288
+ // function-name declaration (metadata-error-route shape).
1289
+ frames.push(frameAt(source, component.nameIndex, component.name));
1290
+ }
1291
+ frames.push(frameAt(source, usageIndex, page.name));
1292
+ return {
1293
+ frames,
1294
+ anchor: frames[0]!,
1295
+ wrapperDepth: componentWrapperDepth(
1296
+ source.slice(page.bodyStart, page.bodyEnd),
1297
+ usageIndex - page.bodyStart,
1298
+ ),
1299
+ };
1300
+ }
1301
+
1302
+ /**
1303
+ * Open component elements enclosing a body offset. Host elements and <Suspense> are excluded: the host
1304
+ * chain is printed verbatim in the stack, and a Suspense boundary means the usage never reaches this path.
1305
+ */
1306
+ function componentWrapperDepth(body: string, index: number): number {
1307
+ let depth = 0;
1308
+ for (const tag of body.slice(0, index).matchAll(/<(\/?)([A-Z][\w$.]*)\b([^>]*)>/g)) {
1309
+ if (tag[2] === 'Suspense') continue;
1310
+ if (tag[1] === '/') depth -= 1;
1311
+ else if (!tag[3]!.trimEnd().endsWith('/')) depth += 1;
1312
+ }
1313
+ return Math.max(0, depth);
1314
+ }
1315
+
1316
+ function frameAt(source: string, index: number, name: string): Frame {
1317
+ const before = source.slice(0, index);
1318
+ const line = before.split('\n').length;
1319
+ const col = index - before.lastIndexOf('\n');
1320
+ return { name, line, col };
1321
+ }
1322
+
1323
+ /** Whether a body offset sits inside an open <Suspense> element. */
1324
+ function insideSuspense(body: string, index: number): boolean {
1325
+ const before = body.slice(0, index);
1326
+ const opens = (before.match(/<Suspense[\s>]/g) ?? []).length;
1327
+ const closes = (before.match(/<\/Suspense>/g) ?? []).length;
1328
+ return opens > closes;
1329
+ }
1330
+
1331
+ function isComponentName(name: string): boolean {
1332
+ return /^[A-Z]/.test(name) && name !== 'Fallback';
1333
+ }
1334
+
1335
+ /** All named function declarations + const arrow/function initializers. */
1336
+ function collectFunctions(source: string): FunctionInfo[] {
1337
+ const out: FunctionInfo[] = [];
1338
+ const patterns = [
1339
+ /(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*(?::[^{]+)?\{/g,
1340
+ /(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\s*)?\([^)]*\)\s*(?::[^={]+)?=>?\s*\{/g,
1341
+ ];
1342
+ for (const pattern of patterns) {
1343
+ for (const match of source.matchAll(pattern)) {
1344
+ const name = match[1]!;
1345
+ const matchIndex = match.index ?? 0;
1346
+ const nameIndex = matchIndex + match[0].indexOf(name);
1347
+ const bodyStart = matchIndex + match[0].length;
1348
+ out.push({
1349
+ name,
1350
+ declIndex: matchIndex,
1351
+ nameIndex,
1352
+ bodyStart,
1353
+ bodyEnd: bodyEndFrom(source, bodyStart),
1354
+ });
1355
+ }
1356
+ }
1357
+ return out;
1358
+ }
1359
+
1360
+ /** Index of the `}` closing the body whose content starts at `offset`. */
1361
+ function bodyEndFrom(source: string, offset: number): number {
1362
+ let depth = 1;
1363
+ let quote: string | undefined;
1364
+ for (let cursor = offset; cursor < source.length; cursor++) {
1365
+ const current = source[cursor]!;
1366
+ if (quote) {
1367
+ if (current === '\\') cursor++;
1368
+ else if (current === quote) quote = undefined;
1369
+ continue;
1370
+ }
1371
+ if (current === '/' && source[cursor + 1] === '/') {
1372
+ const newline = source.indexOf('\n', cursor + 2);
1373
+ if (newline === -1) return source.length;
1374
+ cursor = newline;
1375
+ continue;
1376
+ }
1377
+ if (current === '/' && source[cursor + 1] === '*') {
1378
+ const end = source.indexOf('*/', cursor + 2);
1379
+ if (end === -1) return source.length;
1380
+ cursor = end + 1;
1381
+ continue;
1382
+ }
1383
+ if (current === '"' || current === "'" || current === '`') {
1384
+ quote = current;
1385
+ continue;
1386
+ }
1387
+ if (current === '{') depth++;
1388
+ else if (current === '}' && --depth === 0) return cursor;
1389
+ }
1390
+ return source.length;
1391
+ }
1392
+
1393
+ // --- layout chain -----------------------------------------------------------
1394
+
1395
+ function layoutChainSources(appPath: string, pageFile: string): string[] {
1396
+ const sources: string[] = [];
1397
+ let dir = path.dirname(pageFile);
1398
+ while (dir.startsWith(appPath)) {
1399
+ for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
1400
+ const candidate = path.join(dir, `layout.${ext}`);
1401
+ if (existsSync(candidate)) {
1402
+ const source = readSource(candidate);
1403
+ if (source) sources.push(source);
1404
+ }
1405
+ }
1406
+ if (dir === appPath) break;
1407
+ dir = path.dirname(dir);
1408
+ }
1409
+ return sources;
1410
+ }
1411
+
1412
+ /** A layout wraps <body> inside <Suspense> (the dynamic-shell opt-in). */
1413
+ function layoutWrapsBodyInSuspense(source: string): boolean {
1414
+ const suspense = source.indexOf('<Suspense');
1415
+ const body = source.indexOf('<body');
1416
+ return suspense !== -1 && body !== -1 && suspense < body;
1417
+ }
1418
+
1419
+ function readSource(file: string): string | undefined {
1420
+ try {
1421
+ return readFileSync(file, 'utf8');
1422
+ } catch {
1423
+ return undefined;
1424
+ }
1425
+ }
1426
+
1427
+ function isDirectory(file: string): boolean {
1428
+ try {
1429
+ return statSync(file).isDirectory();
1430
+ } catch {
1431
+ return false;
1432
+ }
1433
+ }
1434
+
1435
+ function appRelativeDisplayPath(appPath: string, file: string): string {
1436
+ const relative = path.relative(appPath, file).split(path.sep).join('/');
1437
+ return `app/${relative}`;
1438
+ }
1439
+
1440
+ // --- runtime `use cache` error funnel ---------------------------------------
1441
+ //
1442
+ // An error thrown inside a 'use cache' scope at RUNTIME crosses Next's cache
1443
+ // flight boundary before it reaches the render, so its logged stack is not the
1444
+ // raw throw site: the cache-side user frames survive, followed by the flight
1445
+ // client's deserialization frames, and the whole error is tagged with the
1446
+ // `Cache` environment. Without `--debug-prerender` the production build ships
1447
+ // minified frames and a redacted error instead. The cache-components-errors
1448
+ // suite inline-snapshots both shapes (normalized by its utils.ts: any frame
1449
+ // whose path contains `.next` collapses to `<next-dist-dir>`).
1450
+
1451
+ /** React flight-client frames every deserialized cache error carries. */
1452
+ const CACHE_FLIGHT_FRAMES = [
1453
+ 'Object.then',
1454
+ 'resolveErrorDev',
1455
+ 'processFullStringRow',
1456
+ 'processFullBinaryRow',
1457
+ 'processBinaryChunk',
1458
+ 'progress',
1459
+ ];
1460
+
1461
+ /** React's production redaction text for an error that crossed the RSC boundary. */
1462
+ export const RSC_REDACTED_RUNTIME_MESSAGE =
1463
+ 'An error occurred in the Server Components render. The specific message is ' +
1464
+ 'omitted in production builds to avoid leaking sensitive details. A digest ' +
1465
+ 'property is included on this error instance which may provide additional ' +
1466
+ 'details about the nature of the error.';
1467
+
1468
+ /**
1469
+ * The user frames of a runtime `use cache` throw: the leading run of app-code frames (the throw site up
1470
+ * through the cached function itself), collapsing the duplicate positions a single function contributes.
1471
+ * Runtime/bundler frames end the run - they are replaced by the flight-client tail.
1472
+ */
1473
+ export function cacheRuntimeErrorFrames(stack: string): string[] {
1474
+ const names: string[] = [];
1475
+ for (const line of stack.split('\n')) {
1476
+ const match = /^\s+at (.+?) \((.+)\)$/.exec(line);
1477
+ if (!match) continue;
1478
+ const [, name, location] = match as unknown as [string, string, string];
1479
+ if (
1480
+ location.startsWith('node:') ||
1481
+ location.includes('/external/') ||
1482
+ location.includes('/packages/pnext/src/') ||
1483
+ name === '<anonymous>'
1484
+ ) {
1485
+ break;
1486
+ }
1487
+ if (names[names.length - 1] !== name) names.push(name);
1488
+ }
1489
+ return names;
1490
+ }
1491
+
1492
+ /**
1493
+ * Next's logged stack for an error that escaped a 'use cache' scope at runtime.
1494
+ * `--debug-prerender` keeps the cache-side user frames and appends the flight
1495
+ * client's; a plain production build only has minified frames, of which the
1496
+ * snapshot shape keeps two.
1497
+ */
1498
+ export function formatCacheRuntimeErrorStack(
1499
+ message: string,
1500
+ frameNames: string[],
1501
+ debugPrerender: boolean,
1502
+ ): string {
1503
+ const frames = debugPrerender ? [...frameNames, ...CACHE_FLIGHT_FRAMES] : ['r', 'r'];
1504
+ return [
1505
+ `Error: ${message}`,
1506
+ ...frames.map(name => ` at ${name} (.next/server/chunks/main.js:1:1)`),
1507
+ ].join('\n');
1508
+ }