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