@wular/pnext 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (392) hide show
  1. package/README.md +11 -11
  2. package/bin/pnext +1 -1
  3. package/config/lint/base.js +7 -7
  4. package/config/ts/base.json +2 -4
  5. package/config/ts/react.json +2 -6
  6. package/package.json +23 -2
  7. package/reference/compat.md +1 -1
  8. package/reference/config.md +2 -2
  9. package/reference/css.md +13 -9
  10. package/reference/dev.md +1 -1
  11. package/reference/metadata.md +7 -7
  12. package/reference/navigation.md +35 -28
  13. package/reference/performance.md +70 -70
  14. package/reference/rendering.md +17 -17
  15. package/reference/routing.md +17 -17
  16. package/reference/typegen.md +12 -8
  17. package/src/api/cache.ts +36 -37
  18. package/src/api/client-cache.ts +2 -2
  19. package/src/api/client-navigation.ts +113 -109
  20. package/src/api/dynamic.tsx +58 -55
  21. package/src/api/link.tsx +49 -52
  22. package/src/api/navigation.ts +59 -58
  23. package/src/api/server.ts +152 -144
  24. package/src/api/suspense.ts +4 -4
  25. package/src/cli/adapters/vercel-warm.ts +126 -121
  26. package/src/cli/adapters/vercel.ts +437 -443
  27. package/src/cli/analyze.ts +333 -144
  28. package/src/cli/{named-bin.ts → boot/named-bin.ts} +37 -37
  29. package/src/cli/{boot-trace.ts → boot/trace.ts} +10 -10
  30. package/src/cli/build.ts +976 -978
  31. package/src/cli/create.ts +71 -63
  32. package/src/cli/dev.ts +116 -116
  33. package/src/cli/index.ts +88 -85
  34. package/src/cli/migrate/package-json.ts +73 -71
  35. package/src/cli/migrate/report.ts +32 -33
  36. package/src/cli/migrate/{index.ts → run.ts} +48 -49
  37. package/src/cli/migrate/scan.ts +46 -46
  38. package/src/cli/migrate/spinner.ts +9 -9
  39. package/src/cli/migrate/tsconfig.ts +35 -35
  40. package/src/cli/{server-entry.ts → serve/entry.ts} +57 -57
  41. package/src/cli/{request-pipeline.ts → serve/pipeline.ts} +386 -394
  42. package/src/cli/{serve-ui.ts → serve/ui.ts} +47 -47
  43. package/src/cli/start.ts +65 -66
  44. package/src/{typegen.ts → cli/typegen.ts} +59 -59
  45. package/src/client/build.ts +794 -768
  46. package/src/client/chunk-fold.ts +245 -240
  47. package/src/client/{paths.ts → chunk-name.ts} +6 -6
  48. package/src/client/entry.ts +162 -147
  49. package/src/client/prebuilt.ts +231 -223
  50. package/src/client/profile.ts +29 -29
  51. package/src/client/react-compiler.ts +20 -15
  52. package/src/client/{compat-surface.ts → react-tier.ts} +64 -64
  53. package/src/client/reference-stub.ts +51 -51
  54. package/src/client/reference.ts +19 -19
  55. package/src/{api → client}/router/events.ts +17 -17
  56. package/src/{api → client}/router/history.ts +16 -16
  57. package/src/{api → client}/router/hub.ts +52 -53
  58. package/src/{api/router.ts → client/router/index.ts} +61 -60
  59. package/src/{api → client}/router/policies.ts +24 -24
  60. package/src/{api → client}/router/runtime.ts +1988 -1992
  61. package/src/{api → client}/router/types.ts +93 -98
  62. package/src/compat/actions/client-plugin.ts +42 -43
  63. package/src/compat/actions/client-stub.ts +14 -14
  64. package/src/compat/actions/{action-client.ts → client.ts} +194 -193
  65. package/src/compat/actions/config.ts +63 -64
  66. package/src/compat/actions/detect.ts +68 -68
  67. package/src/compat/actions/discovery.ts +105 -105
  68. package/src/compat/actions/{action-dispatch.ts → dispatch.ts} +277 -274
  69. package/src/compat/actions/early-submit.ts +1 -1
  70. package/src/compat/actions/endpoint.ts +198 -198
  71. package/src/compat/actions/extensions.ts +811 -0
  72. package/src/compat/actions/flight.ts +23 -23
  73. package/src/compat/actions/form-state.ts +34 -34
  74. package/src/compat/actions/hoist.ts +382 -236
  75. package/src/compat/actions/ids.ts +7 -12
  76. package/src/compat/actions/index.ts +8 -8
  77. package/src/compat/actions/instances.ts +46 -46
  78. package/src/compat/actions/origin.ts +47 -48
  79. package/src/compat/actions/protocol.ts +22 -22
  80. package/src/compat/actions/registry.ts +19 -19
  81. package/src/compat/{misc/action-return.ts → actions/return.ts} +60 -57
  82. package/src/compat/actions/rewrite.ts +125 -125
  83. package/src/compat/actions/{action-router.ts → router.ts} +10 -10
  84. package/src/compat/actions/serve.ts +132 -136
  85. package/src/compat/actions/server-tag.ts +4 -4
  86. package/src/compat/actions/{action-shared.ts → shared.ts} +34 -34
  87. package/src/compat/actions/unrecognized-error.ts +4 -4
  88. package/src/compat/{index.ts → aliases.ts} +112 -109
  89. package/src/compat/bundler/bun-externals.ts +18 -18
  90. package/src/compat/bundler/cjs-exports.ts +271 -223
  91. package/src/compat/bundler/config.ts +116 -111
  92. package/src/compat/bundler/externals.ts +12 -12
  93. package/src/compat/bundler/import-meta-url.ts +19 -19
  94. package/src/compat/bundler/modularize-imports.ts +36 -33
  95. package/src/compat/bundler/new-url-asset.ts +22 -24
  96. package/src/compat/bundler/optimize-package-imports.ts +111 -107
  97. package/src/compat/bundler/polyfill.ts +28 -28
  98. package/src/compat/bundler/react-compiler.ts +35 -29
  99. package/src/compat/bundler/react-profiler.tsx +11 -11
  100. package/src/compat/bundler/relay-transform.ts +48 -47
  101. package/src/compat/bundler/require-context.ts +119 -113
  102. package/src/compat/bundler/resolve-extensions.ts +19 -19
  103. package/src/compat/bundler/source-cache.ts +25 -25
  104. package/src/compat/bundler/static-imports.ts +7 -7
  105. package/src/compat/bundler/symlink-imports.ts +46 -46
  106. package/src/compat/bundler/tsconfig-paths.ts +13 -15
  107. package/src/compat/bundler/wasm.ts +58 -60
  108. package/src/compat/bundler/webpack-loaders.ts +254 -241
  109. package/src/compat/bundler/worker.ts +101 -104
  110. package/src/compat/cache/build-flags.ts +29 -29
  111. package/src/compat/cache/build-prerender-errors.ts +40 -44
  112. package/src/compat/cache/custom-handler.ts +60 -53
  113. package/src/compat/cache/fetch-patch.ts +240 -240
  114. package/src/compat/cache/handler.ts +27 -31
  115. package/src/compat/cache/modern-handler.ts +148 -126
  116. package/src/compat/cache/resume-data-cache.ts +47 -45
  117. package/src/compat/cache/revalidate.ts +233 -232
  118. package/src/compat/cache/runtime-error.ts +35 -35
  119. package/src/compat/cache/use-cache-transform.ts +442 -417
  120. package/src/compat/cache/use-cache.ts +650 -614
  121. package/src/compat/cache-control.ts +140 -142
  122. package/src/compat/client/base-path.ts +21 -20
  123. package/src/compat/client/css-order.ts +18 -18
  124. package/src/compat/client/errors/bare-boundary.ts +11 -11
  125. package/src/compat/client/errors/control-flow.ts +23 -23
  126. package/src/compat/client/errors/error-boundary.ts +61 -61
  127. package/src/compat/client/errors/global-error.ts +101 -91
  128. package/src/compat/client/errors/install.ts +71 -72
  129. package/src/compat/client/errors/lazy.ts +23 -23
  130. package/src/compat/client/errors/primitive-throw.ts +47 -45
  131. package/src/compat/client/errors/soft-refresh.ts +4 -4
  132. package/src/compat/client/link-status.ts +33 -33
  133. package/src/compat/client/{nav-compat-runtime.ts → nav-runtime.ts} +20 -20
  134. package/src/compat/client/{nav-compat.ts → nav.ts} +12 -13
  135. package/src/compat/client/navigation-scroll.ts +63 -63
  136. package/src/compat/client/optimistic-routing.ts +93 -88
  137. package/src/compat/client/prefetch-cache.ts +23 -24
  138. package/src/compat/client/route-announcer.ts +35 -35
  139. package/src/compat/client/segment-cache-policy.ts +20 -20
  140. package/src/compat/client/segment-cache.ts +309 -315
  141. package/src/compat/client/segment-prefetch.ts +127 -132
  142. package/src/compat/client/trailing-slash.ts +5 -4
  143. package/src/compat/css/chunking.ts +113 -116
  144. package/src/compat/css/inline-css.ts +21 -21
  145. package/src/compat/css/lightningcss.ts +37 -38
  146. package/src/compat/css/modules.ts +161 -175
  147. package/src/compat/css/nonce.ts +7 -7
  148. package/src/compat/css/sass-plugin.ts +18 -21
  149. package/src/compat/css/sass.ts +150 -152
  150. package/src/compat/css/styled-jsx-runtime.ts +27 -27
  151. package/src/compat/css/styled-jsx.ts +21 -21
  152. package/src/compat/edge-runtime.ts +27 -27
  153. package/src/compat/{adapter → export}/build-complete.ts +72 -75
  154. package/src/compat/export/client.ts +29 -31
  155. package/src/compat/export/{index.ts → emit.ts} +111 -110
  156. package/src/compat/export/standalone.ts +62 -54
  157. package/src/compat/image-optimizer/cache.ts +51 -49
  158. package/src/compat/image-optimizer/detect.ts +52 -52
  159. package/src/compat/image-optimizer/{index.ts → optimize.ts} +189 -194
  160. package/src/compat/image-optimizer/source.ts +77 -80
  161. package/src/compat/lifecycle/after-scope.ts +26 -26
  162. package/src/compat/lifecycle/after.ts +48 -45
  163. package/src/compat/lifecycle/error-funnel.ts +98 -102
  164. package/src/compat/lifecycle/error-serialize.ts +30 -26
  165. package/src/compat/lifecycle/error-ui.ts +67 -32
  166. package/src/compat/lifecycle/instrumentation-client.ts +36 -38
  167. package/src/compat/lifecycle/instrumentation.ts +85 -85
  168. package/src/compat/lifecycle/node-console.ts +11 -11
  169. package/src/compat/lifecycle/testmode.ts +160 -132
  170. package/src/compat/mdx/compile.ts +60 -57
  171. package/src/compat/mdx/plugin.ts +11 -11
  172. package/src/compat/mdx/{next-mdx-stub.ts → stub.ts} +5 -5
  173. package/src/compat/{metadata-route-artifacts.ts → metadata-artifacts.ts} +195 -191
  174. package/src/compat/metadata.ts +75 -81
  175. package/src/compat/next/cache.ts +68 -69
  176. package/src/compat/next/canonical-url.ts +9 -9
  177. package/src/compat/next/client-cache.ts +19 -19
  178. package/src/compat/next/client-navigation.ts +112 -116
  179. package/src/compat/next/client-only.ts +1 -1
  180. package/src/compat/next/client-script.tsx +99 -93
  181. package/src/compat/next/client-server.ts +10 -10
  182. package/src/compat/next/config-loader.ts +176 -173
  183. package/src/compat/next/config.ts +7 -7
  184. package/src/compat/next/constants.cjs +6 -6
  185. package/src/compat/next/constants.ts +6 -6
  186. package/src/compat/next/custom-server.ts +26 -24
  187. package/src/compat/next/dist/client/components/app-router-headers.ts +21 -21
  188. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +3 -4
  189. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -1
  190. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -1
  191. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -1
  192. package/src/compat/next/dynamic.tsx +21 -18
  193. package/src/compat/next/error.tsx +52 -52
  194. package/src/compat/next/font/cache.ts +74 -74
  195. package/src/compat/next/font/google.ts +2 -2
  196. package/src/compat/next/font/index.ts +1 -1
  197. package/src/compat/next/font/local.ts +3 -3
  198. package/src/compat/next/font/runtime-client.ts +17 -15
  199. package/src/compat/next/font/runtime.ts +443 -408
  200. package/src/compat/next/font/shared.ts +120 -108
  201. package/src/compat/next/form.tsx +63 -63
  202. package/src/compat/next/head.tsx +2 -2
  203. package/src/compat/next/headers.ts +103 -95
  204. package/src/compat/next/image/client.tsx +220 -0
  205. package/src/compat/next/image/config.ts +56 -58
  206. package/src/compat/next/image/optimizer.ts +40 -36
  207. package/src/compat/next/image/patterns.ts +37 -40
  208. package/src/compat/next/{image-props.ts → image/props.ts} +208 -202
  209. package/src/compat/next/image/shared.ts +65 -57
  210. package/src/compat/next/image/static-metadata.ts +98 -107
  211. package/src/compat/next/image/validate.ts +79 -88
  212. package/src/compat/next/image.tsx +19 -23
  213. package/src/compat/next/index.ts +1 -1
  214. package/src/compat/next/legacy-image.tsx +59 -60
  215. package/src/compat/next/{link-validation-transform.ts → link-transform.ts} +95 -96
  216. package/src/compat/next/link.tsx +158 -158
  217. package/src/compat/next/navigation.cjs +12 -3
  218. package/src/compat/next/navigation.ts +48 -50
  219. package/src/compat/next/offline.ts +27 -27
  220. package/src/compat/next/og.ts +121 -124
  221. package/src/compat/next/preferred-region.ts +13 -14
  222. package/src/compat/next/redirects.ts +58 -56
  223. package/src/compat/next/resource-hints.ts +73 -76
  224. package/src/compat/next/rewrites.ts +130 -133
  225. package/src/compat/next/root-params.ts +45 -45
  226. package/src/compat/next/{optimistic-route-state.ts → route-state.ts} +52 -52
  227. package/src/compat/next/router.cjs +4 -2
  228. package/src/compat/next/router.ts +58 -61
  229. package/src/compat/next/script.tsx +108 -108
  230. package/src/compat/next/server-only.ts +1 -1
  231. package/src/compat/next/server.ts +19 -19
  232. package/src/compat/next/svgr.ts +18 -17
  233. package/src/compat/next/telemetry.ts +24 -24
  234. package/src/compat/next/{image-usage.ts → usage.ts} +70 -39
  235. package/src/compat/next/user-agent.ts +53 -49
  236. package/src/compat/next/web-vitals.ts +22 -24
  237. package/src/compat/otel/api.ts +41 -41
  238. package/src/compat/otel/client-trace-metadata.ts +25 -27
  239. package/src/compat/otel/fetch-span.ts +29 -29
  240. package/src/compat/otel/tracer.ts +331 -331
  241. package/src/compat/pages/api.ts +456 -0
  242. package/src/compat/pages/client-plugin.ts +36 -36
  243. package/src/compat/pages/router-state.ts +34 -34
  244. package/src/compat/pages/{index.ts → router.ts} +130 -135
  245. package/src/compat/ppr/io.ts +12 -12
  246. package/src/compat/ppr/missing-root-params.ts +34 -38
  247. package/src/compat/ppr/root-params-scan.ts +66 -66
  248. package/src/compat/ppr/root-params-transform.ts +24 -26
  249. package/src/compat/ppr/root-params.ts +30 -30
  250. package/src/compat/ppr/segment-config-incompat.ts +6 -7
  251. package/src/compat/protocol.ts +71 -70
  252. package/src/compat/react/action-state.ts +47 -48
  253. package/src/compat/react/client-lite.ts +15 -15
  254. package/src/compat/react/client.ts +4 -4
  255. package/src/compat/react/compiler-runtime.ts +11 -11
  256. package/src/compat/react/dom-client.ts +44 -44
  257. package/src/compat/react/dom-react-server.ts +10 -16
  258. package/src/compat/react/dom-server.ts +10 -10
  259. package/src/compat/react/dom.ts +52 -52
  260. package/src/compat/react/hooks-extra.ts +34 -35
  261. package/src/compat/react/parity.ts +64 -61
  262. package/src/compat/react/preact.ts +81 -82
  263. package/src/compat/react/react-server.ts +28 -28
  264. package/src/compat/react/router-shim.ts +1 -1
  265. package/src/compat/react/server-component-use.ts +8 -8
  266. package/src/compat/react/server-inserted-html.ts +30 -31
  267. package/src/compat/react/server.ts +53 -55
  268. package/src/compat/react/use.ts +32 -32
  269. package/src/compat/react/view-transition.ts +20 -20
  270. package/src/compat/register/actions.ts +35 -824
  271. package/src/compat/register/boot.ts +41 -41
  272. package/src/compat/register/build-tier.ts +5 -5
  273. package/src/compat/register/build.ts +74 -70
  274. package/src/compat/register/bundler.ts +161 -156
  275. package/src/compat/register/cache.ts +20 -20
  276. package/src/compat/register/client-errors.ts +3 -3
  277. package/src/compat/register/config.ts +6 -6
  278. package/src/compat/register/css-extras.ts +30 -34
  279. package/src/compat/register/edge-runtime.ts +3 -3
  280. package/src/compat/register/errors.ts +10 -12
  281. package/src/compat/register/export.ts +16 -16
  282. package/src/compat/register/font.ts +11 -11
  283. package/src/compat/register/hooks.ts +2 -2
  284. package/src/compat/register/image.ts +40 -40
  285. package/src/compat/register/index.ts +59 -62
  286. package/src/compat/register/instrumentation-client.ts +10 -10
  287. package/src/compat/register/lifecycle.ts +25 -28
  288. package/src/compat/register/mdx.ts +10 -10
  289. package/src/compat/register/middleware.ts +226 -16
  290. package/src/compat/register/otel.ts +80 -88
  291. package/src/compat/register/pages-api.ts +10 -463
  292. package/src/compat/register/ppr.ts +16 -16
  293. package/src/compat/register/protocol.ts +17 -18
  294. package/src/compat/register/proxy.ts +49 -51
  295. package/src/compat/register/render.ts +79 -76
  296. package/src/compat/register/routing.ts +162 -159
  297. package/src/compat/register/segment.ts +24 -1897
  298. package/src/compat/register/static-image.ts +3 -3
  299. package/src/compat/register/{misc.ts → taint.ts} +9 -9
  300. package/src/compat/register/typed-routes.ts +14 -14
  301. package/src/compat/register/{usecache.ts → use-cache.ts} +39 -39
  302. package/src/compat/register/validation.ts +18 -18
  303. package/src/compat/segment/loading-boundary.ts +43 -45
  304. package/src/compat/segment/page-slot.ts +69 -69
  305. package/src/compat/segment/serve.ts +1884 -0
  306. package/src/compat/segment/tree.ts +113 -112
  307. package/src/compat/segment/vary-key.ts +38 -38
  308. package/src/compat/segment/vary-params.ts +138 -142
  309. package/src/compat/static-params.ts +14 -12
  310. package/src/compat/tsconfig-defaults.ts +87 -91
  311. package/src/compat/typecheck/{index.ts → check.ts} +234 -212
  312. package/src/compat/typecheck/worker.ts +15 -12
  313. package/src/compat/typed-routes/{index.ts → generate.ts} +36 -36
  314. package/src/compat/typed-routes/manifest.ts +174 -170
  315. package/src/compat/typed-routes/typegen.ts +127 -110
  316. package/src/compat/validation/errors.ts +16 -19
  317. package/src/compat/validation/prerender-diagnostics.ts +521 -504
  318. package/src/compat/validation/{index.ts → validate.ts} +647 -648
  319. package/src/compat-bootstrap.ts +16 -16
  320. package/src/config.ts +69 -69
  321. package/src/css/build.ts +230 -227
  322. package/src/css/postcss.ts +79 -80
  323. package/src/css/worker.ts +14 -15
  324. package/src/dev/client-actions.ts +10 -10
  325. package/src/dev/client-chunk-store.ts +27 -27
  326. package/src/dev/{client-key-cache.ts → restart/client-key.ts} +76 -76
  327. package/src/dev/{restart-cache.ts → restart/enabled.ts} +1 -1
  328. package/src/dev/{global-css-cache.ts → restart/global-css.ts} +91 -83
  329. package/src/dev/{node-module-bundle-cache.ts → restart/node-modules.ts} +24 -24
  330. package/src/dev/{route-bundle-key-cache.ts → restart/route-bundle-key.ts} +53 -53
  331. package/src/dev/{route-facts-cache.ts → restart/route-facts.ts} +82 -82
  332. package/src/dev/server.ts +804 -820
  333. package/src/env.ts +46 -43
  334. package/src/extensions.ts +491 -478
  335. package/src/index.ts +8 -8
  336. package/src/internal.ts +20 -23
  337. package/src/{islands → render}/boundary-error.ts +3 -3
  338. package/src/render/hooks.ts +71 -71
  339. package/src/render/island-context.ts +14 -14
  340. package/src/render/metadata.ts +310 -310
  341. package/src/{ppr-postpone.ts → render/postpone.ts} +5 -5
  342. package/src/{ppr.ts → render/ppr.ts} +244 -245
  343. package/src/render/renderer.ts +2076 -2082
  344. package/src/render/resource-hints.ts +16 -17
  345. package/src/render/slots.tsx +224 -235
  346. package/src/{islands → render}/static-children.ts +9 -12
  347. package/src/{islands → render}/static-slots.ts +37 -37
  348. package/src/{cache/context.ts → request/cache.ts} +20 -20
  349. package/src/request/context.ts +107 -107
  350. package/src/{dynamic/source.ts → resolve/dynamic.ts} +139 -139
  351. package/src/resolve/engine.ts +90 -77
  352. package/src/resolve/imports.ts +475 -463
  353. package/src/resolve/scan-facts.ts +318 -296
  354. package/src/resolve/source-text.ts +37 -37
  355. package/src/{dynamic → resolve}/tree-shake.ts +132 -128
  356. package/src/routing/forwarded.ts +19 -19
  357. package/src/routing/handler.ts +84 -91
  358. package/src/routing/href.ts +69 -70
  359. package/src/routing/{metadata.ts → metadata-files.ts} +403 -401
  360. package/src/{proxy.ts → routing/proxy.ts} +306 -312
  361. package/src/routing/{request-runtime.ts → request-environment.ts} +10 -10
  362. package/src/routing/routes.ts +827 -815
  363. package/src/routing/slots.ts +164 -160
  364. package/src/runtime/loader.ts +952 -0
  365. package/src/{dev → runtime}/module-cache.ts +309 -287
  366. package/src/{dev → runtime}/module-generations.ts +9 -9
  367. package/src/{dev → runtime}/module-transform.ts +81 -72
  368. package/src/{dev/imports.ts → runtime/modules.ts} +934 -847
  369. package/src/runtime/{server.ts → vendor-build.ts} +848 -1696
  370. package/src/runtime/vendor.ts +425 -404
  371. package/src/styles.d.ts +9 -0
  372. package/src/types.ts +320 -335
  373. package/src/utils/ansi.ts +5 -5
  374. package/src/utils/{source.ts → code.ts} +15 -12
  375. package/src/utils/content-type.ts +3 -3
  376. package/src/utils/decode.ts +2 -2
  377. package/src/utils/dev-profile.ts +13 -13
  378. package/src/utils/error-log.ts +6 -6
  379. package/src/utils/esbuild.ts +18 -18
  380. package/src/utils/fs-cache.ts +13 -13
  381. package/src/utils/fs.ts +57 -49
  382. package/src/utils/html.ts +20 -24
  383. package/src/utils/native-require.ts +8 -8
  384. package/src/utils/serialize.ts +139 -146
  385. package/src/utils/verbose.ts +18 -18
  386. package/src/cli/analyze-print.ts +0 -181
  387. package/src/compat/middleware/manifest.ts +0 -210
  388. package/src/compat/next/image-client.tsx +0 -215
  389. package/src/compat/next/link-usage.ts +0 -29
  390. package/src/css/index.ts +0 -2
  391. package/src/render/index.ts +0 -1
  392. package/src/style-modules.d.ts +0 -9
@@ -1,7 +1,7 @@
1
- import { createHash } from 'node:crypto';
2
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
- import { copyFile, mkdir, readFile, rename, stat } from 'node:fs/promises';
4
- import path from 'node:path';
1
+ import { createHash } from 'node:crypto'
2
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
3
+ import { copyFile, mkdir, readFile, rename, stat } from 'node:fs/promises'
4
+ import path from 'node:path'
5
5
  import {
6
6
  build,
7
7
  type BuildOptions,
@@ -12,23 +12,27 @@ import {
12
12
  type Plugin,
13
13
  type PluginBuild,
14
14
  type ResolveResult,
15
- } from 'esbuild';
16
- import { foldInitialChunks } from './chunk-fold';
17
- import { clientEntryName } from './paths';
18
- import { clientProfile } from './profile';
19
- import { publicEnvDefines, type ResolvedConfig } from '../config';
20
- import { cssModuleClientPlugin } from '../css';
21
- import { withAssetPrefix } from '../css/build';
22
- import { devDynamicSplitEnabled, rewriteDeferredDynamicImports, rewriteLiteralDynamicCalls } from '../dynamic/source';
23
- import { scanFacts } from '../resolve/scan-facts';
24
- import { readSourceText } from '../resolve/source-text';
25
- import { ensureDir, listFiles, readText, writeText } from '../utils/fs';
26
- import { escapeRegex } from '../utils/source';
15
+ } from 'esbuild'
16
+ import { foldInitialChunks } from './chunk-fold'
17
+ import { clientEntryName } from './chunk-name'
18
+ import { clientProfile } from './profile'
19
+ import { publicEnvDefines, type ResolvedConfig } from '../config'
20
+ import { cssModuleClientPlugin } from '../css/build'
21
+ import { withAssetPrefix } from '../css/build'
22
+ import {
23
+ devDynamicSplitEnabled,
24
+ rewriteDeferredDynamicImports,
25
+ rewriteLiteralDynamicCalls,
26
+ } from '../resolve/dynamic'
27
+ import { scanFacts } from '../resolve/scan-facts'
28
+ import { readSourceText } from '../resolve/source-text'
29
+ import { ensureDir, listFiles, readText, writeText } from '../utils/fs'
30
+ import { escapeRegex } from '../utils/code'
27
31
  import {
28
32
  getExternalPackagePolicy,
29
33
  resolveImport,
30
34
  resolveLinkedPackageSpecifier,
31
- } from '../resolve/imports';
35
+ } from '../resolve/imports'
32
36
  import {
33
37
  CLIENT_RUNTIME_MODULE,
34
38
  DYN_SHARED_GLOBAL,
@@ -37,8 +41,8 @@ import {
37
41
  clientRuntimeFacts,
38
42
  clientRuntimeSource,
39
43
  type ClientRuntimeFacts,
40
- } from './entry';
41
- import { clientSuspenseFree } from './compat-surface';
44
+ } from './entry'
45
+ import { clientSuspenseFree } from './react-tier'
42
46
  import {
43
47
  ensurePrebuiltRuntime,
44
48
  prebuiltAssets,
@@ -48,13 +52,13 @@ import {
48
52
  prebuiltSpecifierFilter,
49
53
  settlePrebuiltRuntime,
50
54
  type PrebuiltRuntime,
51
- } from './prebuilt';
52
- import { findLayouts } from '../routing/routes';
53
- import { clientReferenceId, ssrClientReference, type ClientReference } from './reference';
54
- import { shouldReactCompile, transformReactCompiler } from './react-compiler';
55
- import { createVerboseLogger } from '../utils/verbose';
56
- import { dim as dimText } from '../utils/ansi';
57
- import type { RouteManifestEntry } from '../types';
55
+ } from './prebuilt'
56
+ import { findLayouts } from '../routing/routes'
57
+ import { clientReferenceId, ssrClientReference, type ClientReference } from './reference'
58
+ import { shouldReactCompile, transformReactCompiler } from './react-compiler'
59
+ import { createVerboseLogger, formatDuration } from '../utils/verbose'
60
+ import { dim as dimText } from '../utils/ansi'
61
+ import type { RouteManifestEntry } from '../types'
58
62
  import {
59
63
  applyClientSourceAsyncPreTransforms,
60
64
  applyClientSourceTransforms,
@@ -64,7 +68,7 @@ import {
64
68
  getImportAliasExtensions,
65
69
  hasClientSourceAsyncPreTransforms,
66
70
  type CompatModeExtensions,
67
- } from '../extensions';
71
+ } from '../extensions'
68
72
 
69
73
  /**
70
74
  * Write the standalone static chunks a bundler extension supplies (compat: the
@@ -73,44 +77,44 @@ import {
73
77
  * build. No-op for pure-core apps (the seam returns none).
74
78
  */
75
79
  export async function emitStaticClientChunks(config: ResolvedConfig, outDir: string) {
76
- const chunks = getBundlerExtensions().staticClientChunks(config);
77
- if (chunks.length === 0) return;
78
- const chunksDir = path.join(outDir, 'chunks');
79
- await ensureDir(chunksDir);
80
+ const chunks = getBundlerExtensions().staticClientChunks(config)
81
+ if (chunks.length === 0) return
82
+ const chunksDir = path.join(outDir, 'chunks')
83
+ await ensureDir(chunksDir)
80
84
  for (const chunk of chunks) {
81
- await writeText(path.join(chunksDir, chunk.name), chunk.contents);
85
+ await writeText(path.join(chunksDir, chunk.name), chunk.contents)
82
86
  }
83
87
  }
84
88
 
85
89
  interface ClientBuildOptions {
86
- config: ResolvedConfig;
87
- route: RouteManifestEntry;
88
- outDir: string;
89
- dev?: boolean;
90
+ config: ResolvedConfig
91
+ route: RouteManifestEntry
92
+ outDir: string
93
+ dev?: boolean
90
94
  }
91
95
 
92
96
  interface ClientBatchBuildOptions {
93
- config: ResolvedConfig;
94
- routes: RouteManifestEntry[];
95
- outDir: string;
96
- verbose?: boolean;
97
+ config: ResolvedConfig
98
+ routes: RouteManifestEntry[]
99
+ outDir: string
100
+ verbose?: boolean
97
101
  /**
98
102
  * The app defines at least one server action (the build's action manifest). Unset means "unknown" -
99
103
  * dev, where the manifest is discovered per request - and the action runtime is emitted, since only
100
104
  * prod bundles are budgeted.
101
105
  */
102
- hasServerActions?: boolean;
106
+ hasServerActions?: boolean
103
107
  /**
104
108
  * A pipeline already warmed by the caller (the build starts one under the
105
109
  * route-fact scan). Omitted — dev, tests — the stage opens its own.
106
110
  */
107
- pipeline?: ClientSourcePipeline;
111
+ pipeline?: ClientSourcePipeline
108
112
  }
109
113
 
110
- const virtualEntryNamespace = 'pnext-client-entry';
114
+ const virtualEntryNamespace = 'pnext-client-entry'
111
115
 
112
116
  function nextCompatEnabled(config: ResolvedConfig) {
113
- return getCompatModeExtensions().nextEnabled(config);
117
+ return getCompatModeExtensions().nextEnabled(config)
114
118
  }
115
119
 
116
120
  /**
@@ -119,13 +123,13 @@ function nextCompatEnabled(config: ResolvedConfig) {
119
123
  * directories once per route. Memoized for the duration of one build and cleared at each entry
120
124
  * point, so a dev rebuild re-stats after a file is added or removed.
121
125
  */
122
- const conventionCache = new Map<string, unknown>();
126
+ const conventionCache = new Map<string, unknown>()
123
127
 
124
128
  function memoConvention<T>(key: string, compute: () => T): T {
125
- if (conventionCache.has(key)) return conventionCache.get(key) as T;
126
- const value = compute();
127
- conventionCache.set(key, value);
128
- return value;
129
+ if (conventionCache.has(key)) return conventionCache.get(key) as T
130
+ const value = compute()
131
+ conventionCache.set(key, value)
132
+ return value
129
133
  }
130
134
 
131
135
  /**
@@ -136,10 +140,10 @@ function memoConvention<T>(key: string, compute: () => T): T {
136
140
  * drops it.
137
141
  */
138
142
  function appUsesActions(routes: RouteManifestEntry[], hasServerActions?: boolean) {
139
- if (hasServerActions !== false) return true;
143
+ if (hasServerActions !== false) return true
140
144
  return routes.some(route =>
141
145
  route.clientEntryReasons?.some(reason => reason === 'actions' || reason === 'form'),
142
- );
146
+ )
143
147
  }
144
148
 
145
149
  /**
@@ -148,10 +152,10 @@ function appUsesActions(routes: RouteManifestEntry[], hasServerActions?: boolean
148
152
  * actions elsewhere.
149
153
  */
150
154
  function routeUsesActions(route: RouteManifestEntry, appActions: boolean) {
151
- if (!appActions) return false;
155
+ if (!appActions) return false
152
156
  return (
153
157
  route.clientEntryReasons?.some(reason => reason === 'actions' || reason === 'form') === true
154
- );
158
+ )
155
159
  }
156
160
 
157
161
  /**
@@ -160,15 +164,15 @@ function routeUsesActions(route: RouteManifestEntry, appActions: boolean) {
160
164
  * file - has to be in the tree to catch it.
161
165
  */
162
166
  function routeThrowsClientControlFlow(route: RouteManifestEntry): boolean {
163
- return route.clientEntryReasons?.includes('control-flow') === true;
167
+ return route.clientEntryReasons?.includes('control-flow') === true
164
168
  }
165
169
 
166
170
  function routeErrorFile(config: ResolvedConfig, route: RouteManifestEntry): string | undefined {
167
- return routeConventionFile(config, route, 'error');
171
+ return routeConventionFile(config, route, 'error')
168
172
  }
169
173
 
170
174
  function routeNotFoundFile(config: ResolvedConfig, route: RouteManifestEntry): string | undefined {
171
- return routeConventionFile(config, route, 'not-found');
175
+ return routeConventionFile(config, route, 'not-found')
172
176
  }
173
177
 
174
178
  // Nearest segment convention file for a route: walked from the page's own
@@ -178,26 +182,26 @@ function routeConventionFile(
178
182
  route: RouteManifestEntry,
179
183
  name: string,
180
184
  ): string | undefined {
181
- if (!nextCompatEnabled(config)) return undefined;
182
- const appPath = path.resolve(config.appPath);
183
- const start = path.dirname(path.resolve(route.file));
184
- const relative = path.relative(appPath, start);
185
- if (relative.startsWith('..') || path.isAbsolute(relative)) return undefined;
185
+ if (!nextCompatEnabled(config)) return undefined
186
+ const appPath = path.resolve(config.appPath)
187
+ const start = path.dirname(path.resolve(route.file))
188
+ const relative = path.relative(appPath, start)
189
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return undefined
186
190
  // Memoized per directory, not per route: sibling routes walk the same chain,
187
191
  // and a parent's answer is the tail of every child's walk.
188
192
  return memoConvention(`${name}\0${start}`, () => {
189
- let dir = start;
193
+ let dir = start
190
194
  for (;;) {
191
195
  for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
192
- const candidate = path.join(dir, `${name}.${ext}`);
193
- if (existsSync(candidate)) return candidate;
196
+ const candidate = path.join(dir, `${name}.${ext}`)
197
+ if (existsSync(candidate)) return candidate
194
198
  }
195
- if (dir === appPath) return undefined;
196
- const parent = path.dirname(dir);
197
- if (parent === dir) return undefined;
198
- dir = parent;
199
+ if (dir === appPath) return undefined
200
+ const parent = path.dirname(dir)
201
+ if (parent === dir) return undefined
202
+ dir = parent
199
203
  }
200
- });
204
+ })
201
205
  }
202
206
 
203
207
  // The app-root global-error.* convention file (single per app, unlike error.*
@@ -205,15 +209,15 @@ function routeConventionFile(
205
209
  // can mount a user global-error component for client throws that escape every
206
210
  // route error boundary. Absent → the built-in fallback document is used.
207
211
  function globalErrorFile(config: ResolvedConfig): string | undefined {
208
- if (!nextCompatEnabled(config)) return undefined;
209
- const appPath = path.resolve(config.appPath);
212
+ if (!nextCompatEnabled(config)) return undefined
213
+ const appPath = path.resolve(config.appPath)
210
214
  return memoConvention(`global-error\0${appPath}`, () => {
211
215
  for (const ext of ['tsx', 'ts', 'jsx', 'js']) {
212
- const candidate = path.join(appPath, `global-error.${ext}`);
213
- if (existsSync(candidate)) return candidate;
216
+ const candidate = path.join(appPath, `global-error.${ext}`)
217
+ if (existsSync(candidate)) return candidate
214
218
  }
215
- return undefined;
216
- });
219
+ return undefined
220
+ })
217
221
  }
218
222
 
219
223
  /**
@@ -225,10 +229,10 @@ function globalErrorFile(config: ResolvedConfig): string | undefined {
225
229
  * this is true, and the shell path depends on those markers.
226
230
  */
227
231
  function hasClientRootLayout(config: ResolvedConfig, route: RouteManifestEntry): boolean {
228
- if (route.client) return false;
229
- const rootLayout = shellLayoutOrder(config, route)[0];
230
- if (!rootLayout) return false;
231
- return route.clientReferences.some(reference => reference.file === rootLayout);
232
+ if (route.client) return false
233
+ const rootLayout = shellLayoutOrder(config, route)[0]
234
+ if (!rootLayout) return false
235
+ return route.clientReferences.some(reference => reference.file === rootLayout)
232
236
  }
233
237
 
234
238
  /**
@@ -240,7 +244,7 @@ function shellLayoutOrder(config: ResolvedConfig, route: RouteManifestEntry): st
240
244
  // Asked three times per route (shell order, client-root check, runtime facts).
241
245
  return memoConvention(`layouts\0${config.appPath}\0${route.file}`, () =>
242
246
  findLayouts(config.appPath, route.file),
243
- );
247
+ )
244
248
  }
245
249
 
246
250
  /**
@@ -256,24 +260,22 @@ function prebuiltLocation(config: ResolvedConfig, outDir: string, key: string, d
256
260
  dir: prebuiltRuntimeDir(config, key),
257
261
  publicPath: `/__pnext/runtime/${key}`,
258
262
  assetPath: `__pnext/runtime/${key}`,
259
- };
263
+ }
260
264
  }
261
265
  // The SERVED url, not a relative one: chunks sit a directory below the
262
266
  // entries, so no single relative path is correct from both — and esbuild
263
267
  // writes an external path through verbatim, the same string everywhere.
264
- const served = nextCompatEnabled(config)
265
- ? `/_next/static/rt/${key}`
266
- : `/assets/rt/${key}`;
268
+ const served = nextCompatEnabled(config) ? `/_next/static/rt/${key}` : `/assets/rt/${key}`
267
269
  return {
268
270
  dir: path.join(outDir, 'rt', key),
269
271
  publicPath: withAssetPrefix(config, served),
270
272
  assetPath: `assets/rt/${key}`,
271
- };
273
+ }
272
274
  }
273
275
 
274
276
  /** Where the dev server reads the artifact back from. */
275
277
  export function prebuiltRuntimeDir(config: ResolvedConfig, key: string) {
276
- return path.join(config.root, 'node_modules', '.cache', 'pnext', 'client-runtime', key);
278
+ return path.join(config.root, 'node_modules', '.cache', 'pnext', 'client-runtime', key)
277
279
  }
278
280
 
279
281
  /**
@@ -285,7 +287,7 @@ export function prebuiltRuntimeDir(config: ResolvedConfig, key: string) {
285
287
  */
286
288
  function prebuiltEnabled() {
287
289
  // eslint-disable-next-line turbo/no-undeclared-env-vars
288
- return process.env.PNEXT_PREBUILT_RUNTIME === '1';
290
+ return process.env.PNEXT_PREBUILT_RUNTIME === '1'
289
291
  }
290
292
 
291
293
  /**
@@ -296,7 +298,7 @@ function prebuiltEnabled() {
296
298
  * the same framework names.
297
299
  */
298
300
  function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, actions: boolean) {
299
- const nextCompat = nextCompatEnabled(config);
301
+ const nextCompat = nextCompatEnabled(config)
300
302
  const facts = clientRuntimeFacts(
301
303
  [
302
304
  {
@@ -306,7 +308,7 @@ function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, act
306
308
  ],
307
309
  nextCompat,
308
310
  !routeSuspenseFree(config, route),
309
- );
311
+ )
310
312
  return JSON.stringify([
311
313
  nextCompat,
312
314
  actions,
@@ -316,14 +318,14 @@ function surfaceSignature(config: ResolvedConfig, route: RouteManifestEntry, act
316
318
  routeThrowsClientControlFlow(route) === true,
317
319
  Boolean(route.needsRouterEntry),
318
320
  facts,
319
- ]);
321
+ ])
320
322
  }
321
323
 
322
324
  interface PreparedPrebuilt {
323
- runtime: PrebuiltRuntime;
324
- plugin: Plugin;
325
+ runtime: PrebuiltRuntime
326
+ plugin: Plugin
325
327
  /** Fold what the build demanded into the artifact. */
326
- settle(): Promise<void>;
328
+ settle(): Promise<void>
327
329
  }
328
330
 
329
331
  /**
@@ -339,10 +341,10 @@ async function preparePrebuilt(
339
341
  signature = '',
340
342
  reactLite = false,
341
343
  ): Promise<PreparedPrebuilt | undefined> {
342
- if (!prebuiltEnabled()) return undefined;
343
- const buildOptions = baseClientBuildOptions(config, dev);
344
- const key = prebuiltRuntimeKey(config, buildOptions, signature);
345
- const { dir, publicPath, assetPath } = prebuiltLocation(config, outDir, key, dev);
344
+ if (!prebuiltEnabled()) return undefined
345
+ const buildOptions = baseClientBuildOptions(config, dev)
346
+ const key = prebuiltRuntimeKey(config, buildOptions, signature)
347
+ const { dir, publicPath, assetPath } = prebuiltLocation(config, outDir, key, dev)
346
348
  // The artifact build gets the same plugin chain — and therefore the same
347
349
  // aliases, transforms and asset seam — as the route build it stands in for.
348
350
  // Not the prebuilt plugin itself: inside the artifact, framework imports are
@@ -355,51 +357,51 @@ async function preparePrebuilt(
355
357
  assetPath,
356
358
  buildOptions,
357
359
  plugins: () => clientBuildPlugins(config, createClientSourcePipeline(config), [], reactLite),
358
- };
359
- const runtime = await ensurePrebuiltRuntime(options);
360
- const unprobed = new Set<string>();
360
+ }
361
+ const runtime = await ensurePrebuiltRuntime(options)
362
+ const unprobed = new Set<string>()
361
363
  return {
362
364
  runtime,
363
365
  plugin: prebuiltExternalPlugin(runtime, prebuiltSpecifierFilter(config), unprobed, sourceOf),
364
366
  async settle() {
365
- await settlePrebuiltRuntime(runtime, options, outDir, unprobed);
367
+ await settlePrebuiltRuntime(runtime, options, outDir, unprobed)
366
368
  },
367
- };
369
+ }
368
370
  }
369
371
 
370
372
  /** The served URL of a deferred dynamic reference's on-demand chunk (dev split). */
371
373
  export function deferredDynamicChunkHref(reference: Pick<ClientReference, 'id'>) {
372
- return `/__pnext/client-dyn/${reference.id}.js`;
374
+ return `/__pnext/client-dyn/${reference.id}.js`
373
375
  }
374
376
 
375
377
  export interface DeferredDynamicRef {
376
- file: string;
377
- exportName: string;
378
+ file: string
379
+ exportName: string
378
380
  }
379
381
 
380
382
  // Process-level registry the dev chunk endpoint resolves ids through. Entries
381
383
  // come from the pipeline rewrite below and from each out-dir's sidecar (a
382
384
  // restart serving a cached entry never re-ran the rewrite).
383
- const deferredDynamicRefs = new Map<string, DeferredDynamicRef>();
385
+ const deferredDynamicRefs = new Map<string, DeferredDynamicRef>()
384
386
 
385
387
  export function registerDeferredDynamicRef(id: string, ref: DeferredDynamicRef) {
386
- deferredDynamicRefs.set(id, ref);
388
+ deferredDynamicRefs.set(id, ref)
387
389
  }
388
390
 
389
391
  export function deferredDynamicRefById(id: string) {
390
- return deferredDynamicRefs.get(id);
392
+ return deferredDynamicRefs.get(id)
391
393
  }
392
394
 
393
395
  /** Sidecar name persisted next to a dev entry naming its deferred dynamic refs. */
394
- export const DEFERRED_DYNAMIC_SIDECAR = 'dyn-refs.json';
396
+ export const DEFERRED_DYNAMIC_SIDECAR = 'dyn-refs.json'
395
397
 
396
398
  /** Dev-only: deferred dynamic references load from the on-demand chunk endpoint. */
397
399
  function devDeferredDynamicHref(dev: boolean | undefined) {
398
- if (!dev || !devDynamicSplitEnabled()) return undefined;
400
+ if (!dev || !devDynamicSplitEnabled()) return undefined
399
401
  return (reference: ClientReference) =>
400
402
  reference.dynamic && !ssrClientReference(reference)
401
403
  ? deferredDynamicChunkHref(reference)
402
- : undefined;
404
+ : undefined
403
405
  }
404
406
 
405
407
  /** Deferred chunk URLs resolve at runtime, never inside the entry build. */
@@ -410,15 +412,15 @@ function deferredDynamicExternalPlugin(): Plugin {
410
412
  build.onResolve({ filter: /^\/__pnext\/client-dyn\// }, args => ({
411
413
  path: args.path,
412
414
  external: true,
413
- }));
415
+ }))
414
416
  },
415
- };
417
+ }
416
418
  }
417
419
 
418
420
  export async function buildClientEntry({ config, route, outDir, dev }: ClientBuildOptions) {
419
- conventionCache.clear();
420
- await ensureDir(outDir);
421
- const suspense = routeSuspenseFree(config, route) === false;
421
+ conventionCache.clear()
422
+ await ensureDir(outDir)
423
+ const suspense = routeSuspenseFree(config, route) === false
422
424
  const source = clientEntrySource({
423
425
  deferredDynamicHref: devDeferredDynamicHref(dev),
424
426
  pageFile: route.client ? route.file : undefined,
@@ -434,11 +436,11 @@ export async function buildClientEntry({ config, route, outDir, dev }: ClientBui
434
436
  router: Boolean(route.needsRouterEntry),
435
437
  clientRootLayout: hasClientRootLayout(config, route),
436
438
  shellLayoutOrder: shellLayoutOrder(config, route),
437
- });
438
- const entryName = clientEntryName(route);
439
- const outfile = path.join(outDir, `${entryName}.js`);
440
- const pipeline = createClientSourcePipeline(config, dev === true);
441
- pipeline.warmRoutes([route]);
439
+ })
440
+ const entryName = clientEntryName(route)
441
+ const outfile = path.join(outDir, `${entryName}.js`)
442
+ const pipeline = createClientSourcePipeline(config, dev === true)
443
+ pipeline.warmRoutes([route])
442
444
  const runtimeFacts = clientRuntimeFacts(
443
445
  [
444
446
  {
@@ -448,65 +450,76 @@ export async function buildClientEntry({ config, route, outDir, dev }: ClientBui
448
450
  ],
449
451
  nextCompatEnabled(config),
450
452
  suspense,
451
- );
452
- const prebuilt = await clientProfile.timeAsync('prebuilt', () => preparePrebuilt(
453
- config,
454
- outDir,
455
- dev === true,
456
- importer =>
457
- importer === CLIENT_RUNTIME_MODULE
458
- ? clientRuntimeSource(runtimeFacts)
459
- : (pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)),
460
- // One route is its own surface group, so the key is the same one the batch
461
- // build would give this route's group dev and prod derive it identically.
462
- surfaceSignature(config, route, true),
463
- !suspense,
464
- ));
465
-
466
- let metafile: Metafile | undefined;
453
+ )
454
+ const prebuilt = await clientProfile.timeAsync('prebuilt', () =>
455
+ preparePrebuilt(
456
+ config,
457
+ outDir,
458
+ dev === true,
459
+ importer =>
460
+ importer === CLIENT_RUNTIME_MODULE
461
+ ? clientRuntimeSource(runtimeFacts)
462
+ : (pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)),
463
+ // One route is its own surface group, so the key is the same one the batch
464
+ // build would give this route's group — dev and prod derive it identically.
465
+ surfaceSignature(config, route, true),
466
+ !suspense,
467
+ ),
468
+ )
469
+
470
+ let metafile: Metafile | undefined
467
471
  try {
468
472
  const result = await profileClientBuild(route, dev, () =>
469
- clientProfile.timeAsync('esbuild', () => build({
470
- ...baseClientBuildOptions(config, dev),
471
- stdin: {
472
- contents: source,
473
- loader: 'ts',
474
- resolveDir: process.cwd(),
475
- sourcefile: `${route.id}.ts`,
476
- },
477
- outdir: outDir,
478
- entryNames: entryName,
479
- metafile: true,
480
- plugins: clientBuildPlugins(config, pipeline, [
481
- ...(prebuilt ? [prebuilt.plugin] : []),
482
- ...(devDeferredDynamicHref(dev) ? [deferredDynamicExternalPlugin()] : []),
483
- clientRuntimePlugin(runtimeFacts),
484
- ], !suspense),
485
- })),
486
- );
487
- metafile = result.metafile;
473
+ clientProfile.timeAsync('esbuild', () =>
474
+ build({
475
+ ...baseClientBuildOptions(config, dev),
476
+ stdin: {
477
+ contents: source,
478
+ loader: 'ts',
479
+ resolveDir: process.cwd(),
480
+ sourcefile: `${route.id}.ts`,
481
+ },
482
+ outdir: outDir,
483
+ entryNames: entryName,
484
+ metafile: true,
485
+ plugins: clientBuildPlugins(
486
+ config,
487
+ pipeline,
488
+ [
489
+ ...(prebuilt ? [prebuilt.plugin] : []),
490
+ ...(devDeferredDynamicHref(dev) ? [deferredDynamicExternalPlugin()] : []),
491
+ clientRuntimePlugin(runtimeFacts),
492
+ ],
493
+ !suspense,
494
+ ),
495
+ }),
496
+ ),
497
+ )
498
+ metafile = result.metafile
488
499
  } catch (error) {
489
- throw withClientImportTrace(error, config, route, source);
500
+ throw withClientImportTrace(error, config, route, source)
490
501
  }
491
- await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve());
502
+ await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
492
503
 
493
504
  if (metafile) {
494
- metafile = await clientProfile.timeAsync('fold', () => foldInitialChunks({
495
- outDir,
496
- metafile: metafile!,
497
- buildOptions: baseClientBuildOptions(config, dev),
498
- }));
505
+ metafile = await clientProfile.timeAsync('fold', () =>
506
+ foldInitialChunks({
507
+ outDir,
508
+ metafile: metafile!,
509
+ buildOptions: baseClientBuildOptions(config, dev),
510
+ }),
511
+ )
499
512
  }
500
- await clientProfile.timeAsync('write', () => nameSharedClientChunks(outDir, metafile));
513
+ await clientProfile.timeAsync('write', () => nameSharedClientChunks(outDir, metafile))
501
514
  if (dev && devDynamicSplitEnabled() && pipeline.deferredRefs().size > 0) {
502
515
  await writeText(
503
516
  path.join(outDir, DEFERRED_DYNAMIC_SIDECAR),
504
517
  JSON.stringify(Object.fromEntries(pipeline.deferredRefs())),
505
- );
518
+ )
506
519
  }
507
- clientProfile.report(`client entry ${route.id}`);
520
+ clientProfile.report(`client entry ${route.id}`)
508
521
 
509
- return outfile;
522
+ return outfile
510
523
  }
511
524
 
512
525
  /**
@@ -522,71 +535,73 @@ export async function buildClientDynamicChunk({
522
535
  reference,
523
536
  outDir,
524
537
  }: {
525
- config: ResolvedConfig;
526
- route?: RouteManifestEntry;
527
- reference: DeferredDynamicRef & { id: string };
528
- outDir: string;
538
+ config: ResolvedConfig
539
+ route?: RouteManifestEntry
540
+ reference: DeferredDynamicRef & { id: string }
541
+ outDir: string
529
542
  }) {
530
- await ensureDir(outDir);
531
- const pipeline = createClientSourcePipeline(config, true);
543
+ await ensureDir(outDir)
544
+ const pipeline = createClientSourcePipeline(config, true)
532
545
  const prebuilt = await preparePrebuilt(
533
546
  config,
534
547
  outDir,
535
548
  true,
536
549
  importer => pipeline.sourceOf(importer) ?? readTextSyncSafe(importer),
537
550
  route ? surfaceSignature(config, route, true) : '',
538
- );
551
+ )
539
552
  const source = [
540
553
  reference.exportName === 'default'
541
554
  ? `export { default } from ${JSON.stringify(reference.file)};`
542
555
  : '',
543
556
  `export * from ${JSON.stringify(reference.file)};`,
544
- ].join('\n');
545
- const outfile = path.join(outDir, `${reference.id}.js`);
546
- await clientProfile.timeAsync('dynChunk', () => build({
547
- ...baseClientBuildOptions(config, true),
548
- stdin: {
549
- contents: source,
550
- loader: 'ts',
551
- resolveDir: process.cwd(),
552
- sourcefile: `${reference.id}.dyn.ts`,
553
- },
554
- outdir: outDir,
555
- entryNames: reference.id,
556
- plugins: clientBuildPlugins(config, pipeline, [
557
- ...(prebuilt ? [prebuilt.plugin] : []),
558
- dynSharedVendorPlugin(nextCompatEnabled(config)),
559
- deferredDynamicExternalPlugin(),
560
- ]),
561
- }));
562
- await prebuilt?.settle();
563
- clientProfile.report(`client dynamic chunk ${reference.id}`);
564
- return outfile;
557
+ ].join('\n')
558
+ const outfile = path.join(outDir, `${reference.id}.js`)
559
+ await clientProfile.timeAsync('dynChunk', () =>
560
+ build({
561
+ ...baseClientBuildOptions(config, true),
562
+ stdin: {
563
+ contents: source,
564
+ loader: 'ts',
565
+ resolveDir: process.cwd(),
566
+ sourcefile: `${reference.id}.dyn.ts`,
567
+ },
568
+ outdir: outDir,
569
+ entryNames: reference.id,
570
+ plugins: clientBuildPlugins(config, pipeline, [
571
+ ...(prebuilt ? [prebuilt.plugin] : []),
572
+ dynSharedVendorPlugin(nextCompatEnabled(config)),
573
+ deferredDynamicExternalPlugin(),
574
+ ]),
575
+ }),
576
+ )
577
+ await prebuilt?.settle()
578
+ clientProfile.report(`client dynamic chunk ${reference.id}`)
579
+ return outfile
565
580
  }
566
581
 
567
582
  /** Shared-vendor shims: preact resolves to the entry's published namespace. */
568
583
  function dynSharedVendorPlugin(nextCompat: boolean): Plugin {
569
- const namespace = 'pnext-dyn-shared';
570
- const filter = new RegExp(`^(?:${dynSharedSpecifiers(nextCompat).map(escapeRegex).join('|')})$`);
584
+ const namespace = 'pnext-dyn-shared'
585
+ const filter = new RegExp(`^(?:${dynSharedSpecifiers(nextCompat).map(escapeRegex).join('|')})$`)
571
586
  return {
572
587
  name: 'pnext-dyn-shared-vendor',
573
588
  setup(build) {
574
589
  build.onResolve({ filter }, args =>
575
590
  args.namespace === namespace ? undefined : { path: args.path, namespace },
576
- );
591
+ )
577
592
  build.onLoad({ filter: /.*/, namespace }, async args => {
578
593
  const names = Object.keys((await import(args.path)) as Record<string, unknown>).filter(
579
594
  name => /^[A-Za-z_$][\w$]*$/.test(name) && name !== 'default',
580
- );
595
+ )
581
596
  const lines = [
582
597
  `const m = window.${DYN_SHARED_GLOBAL}[${JSON.stringify(args.path)}];`,
583
598
  'export default (m && m.default);',
584
599
  ...names.map(name => `export const ${name} = m.${name};`),
585
- ];
586
- return { contents: lines.join('\n'), loader: 'js' };
587
- });
600
+ ]
601
+ return { contents: lines.join('\n'), loader: 'js' }
602
+ })
588
603
  },
589
- };
604
+ }
590
605
  }
591
606
 
592
607
  /**
@@ -603,84 +618,86 @@ export async function buildClientEntries({
603
618
  hasServerActions,
604
619
  pipeline: warmed,
605
620
  }: ClientBatchBuildOptions) {
606
- if (!warmed) conventionCache.clear();
607
- const actions = appUsesActions(routes, hasServerActions);
608
- const entries = clientProfile.time('entrySources', () => routes
609
- .filter(route => route.client || route.clientReferences.length > 0 || route.needsRouterEntry)
610
- .map(route => ({
611
- route,
612
- entryName: clientEntryName(route),
613
- source: clientEntrySource({
614
- pageFile: route.client ? route.file : undefined,
615
- clientReferences: route.clientReferences,
616
- nextCompat: nextCompatEnabled(config),
617
- suspense: !routeSuspenseFree(config, route),
618
- actions: routeUsesActions(route, actions),
619
- errorFile: routeErrorFile(config, route),
620
- globalErrorFile: globalErrorFile(config),
621
- notFoundFile: routeNotFoundFile(config, route),
622
- controlFlow: routeThrowsClientControlFlow(route),
623
- router: Boolean(route.needsRouterEntry),
624
- clientRootLayout: hasClientRootLayout(config, route),
625
- shellLayoutOrder: shellLayoutOrder(config, route),
626
- }),
627
- })));
628
- if (entries.length === 0) return;
621
+ if (!warmed) conventionCache.clear()
622
+ const actions = appUsesActions(routes, hasServerActions)
623
+ const entries = clientProfile.time('entrySources', () =>
624
+ routes
625
+ .filter(route => route.client || route.clientReferences.length > 0 || route.needsRouterEntry)
626
+ .map(route => ({
627
+ route,
628
+ entryName: clientEntryName(route),
629
+ source: clientEntrySource({
630
+ pageFile: route.client ? route.file : undefined,
631
+ clientReferences: route.clientReferences,
632
+ nextCompat: nextCompatEnabled(config),
633
+ suspense: !routeSuspenseFree(config, route),
634
+ actions: routeUsesActions(route, actions),
635
+ errorFile: routeErrorFile(config, route),
636
+ globalErrorFile: globalErrorFile(config),
637
+ notFoundFile: routeNotFoundFile(config, route),
638
+ controlFlow: routeThrowsClientControlFlow(route),
639
+ router: Boolean(route.needsRouterEntry),
640
+ clientRootLayout: hasClientRootLayout(config, route),
641
+ shellLayoutOrder: shellLayoutOrder(config, route),
642
+ }),
643
+ })),
644
+ )
645
+ if (entries.length === 0) return
629
646
  // Every route's client sources start their transform and React Compiler pass here, in one batch on
630
647
  // oxc's threadpool, while esbuild is still booting and walking the graph. A caller-warmed pipeline
631
648
  // has them in flight already; warming twice is a Map hit, so the same call covers both.
632
- const pipeline = warmed ?? createClientSourcePipeline(config);
633
- pipeline.warmRoutes(entries.map(entry => entry.route));
634
- await ensureDir(outDir);
635
- const groups = surfaceGroups(config, entries, actions);
636
- clientProfile.count('group#', groups.length);
637
- const built: BuiltEntryGroup[] = [];
649
+ const pipeline = warmed ?? createClientSourcePipeline(config)
650
+ pipeline.warmRoutes(entries.map(entry => entry.route))
651
+ await ensureDir(outDir)
652
+ const groups = surfaceGroups(config, entries, actions)
653
+ clientProfile.count('group#', groups.length)
654
+ const built: BuiltEntryGroup[] = []
638
655
  for (const group of groups) {
639
- built.push(await buildEntryGroup({ config, outDir, entries, group, pipeline, verbose }));
656
+ built.push(await buildEntryGroup({ config, outDir, entries, group, pipeline, verbose }))
640
657
  }
641
658
 
642
659
  // Fold and rename once over the finished output, never per group. Chunk names
643
660
  // are content hashes, so two groups emit the same file for the same chunk —
644
661
  // and the fold DELETES the members it merges, so a per-group fold would
645
662
  // unlink a chunk another group's manifest still points at.
646
- let merged: Metafile = { inputs: {}, outputs: {} };
663
+ let merged: Metafile = { inputs: {}, outputs: {} }
647
664
  for (const item of built) {
648
- if (!item.metafile) continue;
649
- Object.assign(merged.inputs, item.metafile.inputs);
650
- Object.assign(merged.outputs, item.metafile.outputs);
665
+ if (!item.metafile) continue
666
+ Object.assign(merged.inputs, item.metafile.inputs)
667
+ Object.assign(merged.outputs, item.metafile.outputs)
651
668
  }
652
669
  merged = await clientProfile.timeAsync('fold', () =>
653
670
  foldInitialChunks({ outDir, metafile: merged, buildOptions: baseClientBuildOptions(config) }),
654
- );
671
+ )
655
672
  const renames = await clientProfile.timeAsync('write', () =>
656
673
  nameSharedClientChunks(outDir, merged),
657
- );
674
+ )
658
675
  clientProfile.time('entryImports', () => {
659
676
  for (const item of built) {
660
677
  if (item.metafile) {
661
- assignClientEntryImports(item.group.entries, merged, outDir, renames, item.prebuilt);
678
+ assignClientEntryImports(item.group.entries, merged, outDir, renames, item.prebuilt)
662
679
  }
663
680
  }
664
- });
665
- clientProfile.report('client stage');
681
+ })
682
+ clientProfile.report('client stage')
666
683
  }
667
684
 
668
685
  interface ClientEntry {
669
- route: RouteManifestEntry;
670
- entryName: string;
671
- source: string;
686
+ route: RouteManifestEntry
687
+ entryName: string
688
+ source: string
672
689
  }
673
690
 
674
691
  interface SurfaceGroup {
675
- signature: string;
676
- facts: ClientRuntimeFacts;
677
- entries: ClientEntry[];
692
+ signature: string
693
+ facts: ClientRuntimeFacts
694
+ entries: ClientEntry[]
678
695
  }
679
696
 
680
697
  interface BuiltEntryGroup {
681
- group: SurfaceGroup;
682
- metafile: Metafile | undefined;
683
- prebuilt: PreparedPrebuilt | undefined;
698
+ group: SurfaceGroup
699
+ metafile: Metafile | undefined
700
+ prebuilt: PreparedPrebuilt | undefined
684
701
  }
685
702
 
686
703
  /**
@@ -704,16 +721,16 @@ function surfaceGroups(
704
721
  })),
705
722
  nextCompatEnabled(config),
706
723
  !members.every(entry => routeSuspenseFree(config, entry.route)),
707
- );
724
+ )
708
725
  // eslint-disable-next-line turbo/no-undeclared-env-vars
709
726
  if (!prebuiltEnabled() || process.env.PNEXT_PREBUILT_GROUPS === '0')
710
- return [{ signature: '', facts: facts(entries), entries }];
711
- const bySignature = new Map<string, ClientEntry[]>();
727
+ return [{ signature: '', facts: facts(entries), entries }]
728
+ const bySignature = new Map<string, ClientEntry[]>()
712
729
  for (const entry of entries) {
713
- const signature = surfaceSignature(config, entry.route, routeUsesActions(entry.route, actions));
714
- const members = bySignature.get(signature) ?? [];
715
- bySignature.set(signature, members);
716
- members.push(entry);
730
+ const signature = surfaceSignature(config, entry.route, routeUsesActions(entry.route, actions))
731
+ const members = bySignature.get(signature) ?? []
732
+ bySignature.set(signature, members)
733
+ members.push(entry)
717
734
  }
718
735
  return [...bySignature].map(([signature, members]) => ({
719
736
  signature,
@@ -722,7 +739,7 @@ function surfaceGroups(
722
739
  // no route in this group has.
723
740
  facts: facts(members),
724
741
  entries: members,
725
- }));
742
+ }))
726
743
  }
727
744
 
728
745
  async function buildEntryGroup({
@@ -733,62 +750,69 @@ async function buildEntryGroup({
733
750
  pipeline,
734
751
  verbose,
735
752
  }: {
736
- config: ResolvedConfig;
737
- outDir: string;
753
+ config: ResolvedConfig
754
+ outDir: string
738
755
  /** Every entry in the app: the batch fallback and `sourceOf` span groups. */
739
- entries: ClientEntry[];
740
- group: SurfaceGroup;
741
- pipeline: ClientSourcePipeline;
742
- verbose?: boolean;
756
+ entries: ClientEntry[]
757
+ group: SurfaceGroup
758
+ pipeline: ClientSourcePipeline
759
+ verbose?: boolean
743
760
  }): Promise<BuiltEntryGroup> {
744
- const reactLite = group.facts.suspense === false;
761
+ const reactLite = group.facts.suspense === false
745
762
  const prebuilt = await clientProfile.timeAsync('prebuilt', () =>
746
763
  preparePrebuilt(
747
764
  config,
748
765
  outDir,
749
766
  false,
750
767
  importer => {
751
- if (importer === CLIENT_RUNTIME_MODULE) return clientRuntimeSource(group.facts);
752
- const entry = entries.find(item => item.route.id === importer);
753
- return entry?.source ?? pipeline.sourceOf(importer) ?? readTextSyncSafe(importer);
768
+ if (importer === CLIENT_RUNTIME_MODULE) return clientRuntimeSource(group.facts)
769
+ const entry = entries.find(item => item.route.id === importer)
770
+ return entry?.source ?? pipeline.sourceOf(importer) ?? readTextSyncSafe(importer)
754
771
  },
755
772
  group.signature,
756
773
  reactLite,
757
774
  ),
758
- );
775
+ )
759
776
 
760
- let metafile: Metafile | undefined;
777
+ let metafile: Metafile | undefined
761
778
  try {
762
- const result = await clientProfile.timeAsync('esbuild', () => build({
763
- ...baseClientBuildOptions(config),
764
- entryPoints: group.entries.map(entry => ({
765
- in: virtualEntryPath(entry.route.id),
766
- out: entry.entryName,
767
- })),
768
- outdir: outDir,
769
- metafile: true,
770
- plugins: clientBuildPlugins(config, pipeline, [
771
- ...(prebuilt ? [prebuilt.plugin] : []),
772
- virtualEntryPlugin(group.entries),
773
- clientRuntimePlugin(group.facts),
774
- ], reactLite),
775
- }));
776
- metafile = result.metafile;
779
+ const result = await clientProfile.timeAsync('esbuild', () =>
780
+ build({
781
+ ...baseClientBuildOptions(config),
782
+ entryPoints: group.entries.map(entry => ({
783
+ in: virtualEntryPath(entry.route.id),
784
+ out: entry.entryName,
785
+ })),
786
+ outdir: outDir,
787
+ metafile: true,
788
+ plugins: clientBuildPlugins(
789
+ config,
790
+ pipeline,
791
+ [
792
+ ...(prebuilt ? [prebuilt.plugin] : []),
793
+ virtualEntryPlugin(group.entries),
794
+ clientRuntimePlugin(group.facts),
795
+ ],
796
+ reactLite,
797
+ ),
798
+ }),
799
+ )
800
+ metafile = result.metafile
777
801
  } catch (error) {
778
802
  // A batched build cannot attribute a "Could not resolve" error to a single route, so fall back to
779
803
  // per-route builds - the offending route then throws with its precise client import trace. Other
780
804
  // diagnostics already carry a clear message from the batch build; re-throw rather than let a
781
805
  // per-route build surface a more confusing downstream error.
782
- if (!hasUnresolvedImportError(error)) throw error;
806
+ if (!hasUnresolvedImportError(error)) throw error
783
807
  for (const { route } of group.entries) {
784
- await buildClientEntry({ config, route, outDir });
808
+ await buildClientEntry({ config, route, outDir })
785
809
  }
786
- throw error;
810
+ throw error
787
811
  }
788
- await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve());
812
+ await clientProfile.timeAsync('prebuiltSettle', () => prebuilt?.settle() ?? Promise.resolve())
789
813
 
790
- if (verbose && metafile) reportClientBundleSizes(group.entries, metafile, outDir);
791
- return { group, metafile, prebuilt };
814
+ if (verbose && metafile) reportClientBundleSizes(group.entries, metafile, outDir)
815
+ return { group, metafile, prebuilt }
792
816
  }
793
817
 
794
818
  /**
@@ -802,11 +826,11 @@ async function buildEntryGroup({
802
826
  * Memoized per build; a group of routes qualifies iff every member does.
803
827
  */
804
828
  function routeSuspenseFree(config: ResolvedConfig, route: RouteManifestEntry): boolean {
805
- if (!nextCompatEnabled(config)) return false;
829
+ if (!nextCompatEnabled(config)) return false
806
830
  return memoConvention(`suspenseFree:${route.id}`, () => {
807
- const seeds: string[] = [];
808
- if (route.client) seeds.push(route.file);
809
- for (const reference of route.clientReferences) seeds.push(reference.file);
831
+ const seeds: string[] = []
832
+ if (route.client) seeds.push(route.file)
833
+ for (const reference of route.clientReferences) seeds.push(reference.file)
810
834
  for (const file of [
811
835
  routeErrorFile(config, route),
812
836
  globalErrorFile(config),
@@ -814,19 +838,19 @@ function routeSuspenseFree(config: ResolvedConfig, route: RouteManifestEntry): b
814
838
  // Shell layouts hydrate client-side only when the root layout is 'use client'.
815
839
  ...(hasClientRootLayout(config, route) ? shellLayoutOrder(config, route) : []),
816
840
  ]) {
817
- if (file) seeds.push(file);
841
+ if (file) seeds.push(file)
818
842
  }
819
- return clientSuspenseFree(seeds);
820
- });
843
+ return clientSuspenseFree(seeds)
844
+ })
821
845
  }
822
846
 
823
847
  function routeClientSources(routes: RouteManifestEntry[]) {
824
- const files = new Set<string>();
848
+ const files = new Set<string>()
825
849
  for (const route of routes) {
826
- if (route.client) files.add(route.file);
827
- for (const reference of route.clientReferences) files.add(reference.file);
850
+ if (route.client) files.add(route.file)
851
+ for (const reference of route.clientReferences) files.add(reference.file)
828
852
  }
829
- return files;
853
+ return files
830
854
  }
831
855
 
832
856
  // Record each entry's static chunk closure on the route so rendered pages can
@@ -839,58 +863,58 @@ function assignClientEntryImports(
839
863
  renames: Map<string, string>,
840
864
  prebuilt: PreparedPrebuilt | undefined,
841
865
  ) {
842
- const outputs = metafile.outputs;
866
+ const outputs = metafile.outputs
843
867
  // One basename index for the whole entry set: the linear scan this replaces
844
868
  // was O(entries × outputs), which on a route-per-page app is the entire
845
869
  // output list walked once per route.
846
- const byBasename = new Map<string, string>();
870
+ const byBasename = new Map<string, string>()
847
871
  for (const item of Object.keys(outputs)) {
848
872
  // First match wins, as the linear scan did.
849
- const basename = path.basename(item);
850
- if (!byBasename.has(basename)) byBasename.set(basename, item);
873
+ const basename = path.basename(item)
874
+ if (!byBasename.has(basename)) byBasename.set(basename, item)
851
875
  }
852
876
  for (const { route, entryName } of entries) {
853
- const entryOutput = byBasename.get(`${entryName}.js`);
854
- if (!entryOutput) continue;
855
- const staticImports = new Set<string>();
856
- const dynamicImports = new Set<string>();
857
- const visited = new Set([`${entryOutput}:static`]);
858
- const queue = [{ output: entryOutput, dynamic: false }];
877
+ const entryOutput = byBasename.get(`${entryName}.js`)
878
+ if (!entryOutput) continue
879
+ const staticImports = new Set<string>()
880
+ const dynamicImports = new Set<string>()
881
+ const visited = new Set([`${entryOutput}:static`])
882
+ const queue = [{ output: entryOutput, dynamic: false }]
859
883
  // eslint-disable-next-line @typescript-eslint/prefer-for-of
860
884
  for (let head = 0; head < queue.length; head += 1) {
861
- const current = queue[head];
862
- if (!current) continue;
885
+ const current = queue[head]
886
+ if (!current) continue
863
887
  for (const imported of outputs[current.output]?.imports ?? []) {
864
- if (imported.kind !== 'import-statement' && imported.kind !== 'dynamic-import') continue;
865
- const dynamic = current.dynamic || imported.kind === 'dynamic-import';
866
- const visitKey = `${imported.path}:${dynamic ? 'dynamic' : 'static'}`;
867
- if (visited.has(visitKey)) continue;
868
- visited.add(visitKey);
888
+ if (imported.kind !== 'import-statement' && imported.kind !== 'dynamic-import') continue
889
+ const dynamic = current.dynamic || imported.kind === 'dynamic-import'
890
+ const visitKey = `${imported.path}:${dynamic ? 'dynamic' : 'static'}`
891
+ if (visited.has(visitKey)) continue
892
+ visited.add(visitKey)
869
893
  // Prebuilt-runtime shims are external: already a url relative to the
870
894
  // assets root, and with no output in THIS metafile to walk into. Their
871
895
  // own chunk closure comes from the artifact's manifest instead, so the
872
896
  // page preloads the whole set rather than discovering chunks one
873
897
  // round trip after the shim lands.
874
898
  if (imported.external) {
875
- if (!prebuilt || imported.path !== prebuiltModuleUrl(prebuilt.runtime)) continue;
876
- const assets = prebuiltAssets(prebuilt.runtime);
877
- for (const asset of assets.static) (dynamic ? dynamicImports : staticImports).add(asset);
878
- for (const asset of assets.dynamic) dynamicImports.add(asset);
879
- continue;
899
+ if (!prebuilt || imported.path !== prebuiltModuleUrl(prebuilt.runtime)) continue
900
+ const assets = prebuiltAssets(prebuilt.runtime)
901
+ for (const asset of assets.static) (dynamic ? dynamicImports : staticImports).add(asset)
902
+ for (const asset of assets.dynamic) dynamicImports.add(asset)
903
+ continue
880
904
  }
881
- queue.push({ output: imported.path, dynamic });
882
- const basename = path.basename(imported.path);
883
- const relative = path.relative(outDir, path.resolve(imported.path));
905
+ queue.push({ output: imported.path, dynamic })
906
+ const basename = path.basename(imported.path)
907
+ const relative = path.relative(outDir, path.resolve(imported.path))
884
908
  const publicRelative = path
885
909
  .join('assets', path.dirname(relative), renames.get(basename) ?? basename)
886
910
  .split(path.sep)
887
- .join('/');
888
- (dynamic ? dynamicImports : staticImports).add(publicRelative);
911
+ .join('/')
912
+ ;(dynamic ? dynamicImports : staticImports).add(publicRelative)
889
913
  }
890
914
  }
891
- if (staticImports.size > 0) route.clientEntryImports = [...staticImports];
892
- for (const asset of staticImports) dynamicImports.delete(asset);
893
- if (dynamicImports.size > 0) route.clientDynamicImports = [...dynamicImports];
915
+ if (staticImports.size > 0) route.clientEntryImports = [...staticImports]
916
+ for (const asset of staticImports) dynamicImports.delete(asset)
917
+ if (dynamicImports.size > 0) route.clientDynamicImports = [...dynamicImports]
894
918
  }
895
919
  }
896
920
 
@@ -905,62 +929,62 @@ function reportClientBundleSizes(
905
929
  metafile: Metafile,
906
930
  outDir: string,
907
931
  ) {
908
- const log = createVerboseLogger(true, 'client');
909
- const outputs = metafile.outputs;
932
+ const log = createVerboseLogger(true, 'client')
933
+ const outputs = metafile.outputs
910
934
  const rows = entries
911
935
  .map(({ route, entryName }) => {
912
- const key = `${path.relative(process.cwd(), path.join(outDir, `${entryName}.js`))}`;
936
+ const key = `${path.relative(process.cwd(), path.join(outDir, `${entryName}.js`))}`
913
937
  const output = outputs[key]
914
938
  ? key
915
- : Object.keys(outputs).find(item => path.basename(item) === `${entryName}.js`);
916
- const bytes = output ? reachableOutputBytes(output, outputs) : 0;
917
- return { route: route.route, bytes };
939
+ : Object.keys(outputs).find(item => path.basename(item) === `${entryName}.js`)
940
+ const bytes = output ? reachableOutputBytes(output, outputs) : 0
941
+ return { route: route.route, bytes }
918
942
  })
919
- .sort((a, b) => b.bytes - a.bytes);
943
+ .sort((a, b) => b.bytes - a.bytes)
920
944
 
921
945
  for (const row of rows) {
922
- log.log(`${row.route} ${dimText(`→ ${formatBytes(row.bytes)}`)}`);
946
+ log.log(`${row.route} ${dimText(`→ ${formatBytes(row.bytes)}`)}`)
923
947
  }
924
948
  }
925
949
 
926
950
  function reachableOutputBytes(entry: string, outputs: Metafile['outputs']) {
927
- const visited = new Set<string>();
928
- const queue = [entry];
929
- let bytes = 0;
951
+ const visited = new Set<string>()
952
+ const queue = [entry]
953
+ let bytes = 0
930
954
 
931
955
  while (queue.length > 0) {
932
- const current = queue.shift();
933
- if (!current || visited.has(current)) continue;
934
- visited.add(current);
935
- const output = outputs[current];
936
- if (!output) continue;
937
- bytes += output.bytes;
956
+ const current = queue.shift()
957
+ if (!current || visited.has(current)) continue
958
+ visited.add(current)
959
+ const output = outputs[current]
960
+ if (!output) continue
961
+ bytes += output.bytes
938
962
  for (const imported of output.imports) {
939
963
  if (imported.kind === 'import-statement' || imported.kind === 'dynamic-import') {
940
- queue.push(imported.path);
964
+ queue.push(imported.path)
941
965
  }
942
966
  }
943
967
  }
944
968
 
945
- return bytes;
969
+ return bytes
946
970
  }
947
971
 
948
972
  function formatBytes(bytes: number) {
949
- if (bytes < 1024) return `${bytes} B`;
950
- const units = ['KB', 'MB'] as const;
951
- let value = bytes / 1024;
952
- let unitIndex = 0;
973
+ if (bytes < 1024) return `${bytes} B`
974
+ const units = ['KB', 'MB'] as const
975
+ let value = bytes / 1024
976
+ let unitIndex = 0
953
977
  while (value >= 1024 && unitIndex < units.length - 1) {
954
- value /= 1024;
955
- unitIndex += 1;
978
+ value /= 1024
979
+ unitIndex += 1
956
980
  }
957
- return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${units[unitIndex]}`;
981
+ return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${units[unitIndex]}`
958
982
  }
959
983
 
960
984
  function baseClientBuildOptions(config: ResolvedConfig, dev = false): BuildOptions {
961
- const extensions = getBundlerExtensions();
962
- const pure = extensions.clientPureFunctions(config);
963
- const inject = extensions.clientInjects();
985
+ const extensions = getBundlerExtensions()
986
+ const pure = extensions.clientPureFunctions(config)
987
+ const inject = extensions.clientInjects()
964
988
  return {
965
989
  ...(inject.length > 0 ? { inject } : {}),
966
990
  chunkNames: 'chunks/[name]-[hash]',
@@ -1003,15 +1027,15 @@ function baseClientBuildOptions(config: ResolvedConfig, dev = false): BuildOptio
1003
1027
  '.otf': 'file',
1004
1028
  '.eot': 'file',
1005
1029
  },
1006
- };
1030
+ }
1007
1031
  }
1008
1032
 
1009
1033
  function deploymentIdDefine(): Record<string, string> {
1010
1034
  // eslint-disable-next-line turbo/no-undeclared-env-vars
1011
- const deploymentId = process.env.NEXT_DEPLOYMENT_ID;
1035
+ const deploymentId = process.env.NEXT_DEPLOYMENT_ID
1012
1036
  return deploymentId === undefined
1013
1037
  ? {}
1014
- : { 'process.env.NEXT_DEPLOYMENT_ID': JSON.stringify(deploymentId) };
1038
+ : { 'process.env.NEXT_DEPLOYMENT_ID': JSON.stringify(deploymentId) }
1015
1039
  }
1016
1040
 
1017
1041
  /**
@@ -1021,28 +1045,29 @@ function deploymentIdDefine(): Record<string, string> {
1021
1045
  * slow" from "we are answering 40k questions". Off unless PNEXT_CLIENT_PROFILE.
1022
1046
  */
1023
1047
  function instrumentedPlugins(plugins: Plugin[]): Plugin[] {
1024
- if (!clientProfile.enabled) return plugins;
1048
+ if (!clientProfile.enabled) return plugins
1025
1049
  return plugins.map(plugin => ({
1026
1050
  name: plugin.name,
1027
1051
  setup(build) {
1028
- const wrap = (kind: 'resolve' | 'load', register: (options: any, cb: any) => void) =>
1052
+ const wrap =
1053
+ (kind: 'resolve' | 'load', register: (options: any, cb: any) => void) =>
1029
1054
  (options: any, callback: any) =>
1030
1055
  register(options, async (args: any) => {
1031
- clientProfile.count(`${plugin.name}:${kind}#`);
1032
- const start = performance.now();
1056
+ clientProfile.count(`${plugin.name}:${kind}#`)
1057
+ const start = performance.now()
1033
1058
  try {
1034
- return await (callback as (a: unknown) => unknown)(args);
1059
+ return await (callback as (a: unknown) => unknown)(args)
1035
1060
  } finally {
1036
- clientProfile.count(`${plugin.name}:${kind}ms`, performance.now() - start);
1061
+ clientProfile.count(`${plugin.name}:${kind}ms`, performance.now() - start)
1037
1062
  }
1038
- });
1063
+ })
1039
1064
  return plugin.setup({
1040
1065
  ...build,
1041
1066
  onResolve: wrap('resolve', build.onResolve.bind(build)),
1042
1067
  onLoad: wrap('load', build.onLoad.bind(build)),
1043
- });
1068
+ })
1044
1069
  },
1045
- }));
1070
+ }))
1046
1071
  }
1047
1072
 
1048
1073
  /**
@@ -1057,49 +1082,52 @@ function instrumentedPlugins(plugins: Plugin[]): Plugin[] {
1057
1082
  */
1058
1083
  export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[] {
1059
1084
  // eslint-disable-next-line turbo/no-undeclared-env-vars
1060
- if (process.env.PNEXT_CLIENT_COALESCE === '0') return plugins;
1085
+ if (process.env.PNEXT_CLIENT_COALESCE === '0') return plugins
1061
1086
  interface Registration {
1062
- plugin: string;
1063
- filter: RegExp;
1064
- namespace?: string;
1065
- callback: (args: OnResolveArgs) => unknown;
1087
+ plugin: string
1088
+ filter: RegExp
1089
+ namespace?: string
1090
+ callback: (args: OnResolveArgs) => unknown
1066
1091
  }
1067
1092
  return [
1068
1093
  {
1069
1094
  name,
1070
1095
  async setup(build) {
1071
- const registered: Registration[] = [];
1096
+ const registered: Registration[] = []
1072
1097
  // Registrations made after setup (rare, and esbuild allows it) keep
1073
1098
  // their own hook: the chain's order is already fixed by then.
1074
- let sealed = false;
1099
+ let sealed = false
1075
1100
  /** The chain, minus one plugin's own hooks — see `resolve` below. */
1076
1101
  const dispatch = async (args: OnResolveArgs, skip?: string) => {
1077
1102
  for (const entry of registered) {
1078
- if (entry.plugin === skip) continue;
1079
- if (entry.namespace !== undefined && entry.namespace !== args.namespace) continue;
1080
- if (!entry.filter.test(args.path)) continue;
1081
- const result = (await entry.callback(args)) as OnResolveResult | null | undefined;
1082
- if (result == null) continue;
1103
+ if (entry.plugin === skip) continue
1104
+ if (entry.namespace !== undefined && entry.namespace !== args.namespace) continue
1105
+ if (!entry.filter.test(args.path)) continue
1106
+ const result = (await entry.callback(args)) as OnResolveResult | null | undefined
1107
+ if (result == null) continue
1083
1108
  for (const message of [...(result.errors ?? []), ...(result.warnings ?? [])]) {
1084
- message.pluginName ??= entry.plugin;
1109
+ message.pluginName ??= entry.plugin
1085
1110
  }
1086
- return result;
1111
+ return result
1087
1112
  }
1088
- return undefined;
1089
- };
1113
+ return undefined
1114
+ }
1090
1115
  for (const plugin of plugins) {
1091
1116
  const proxy: PluginBuild = {
1092
1117
  ...build,
1093
1118
  onResolve(options: OnResolveOptions, callback: (args: OnResolveArgs) => unknown) {
1094
- if (sealed) return build.onResolve(options, callback as never);
1119
+ if (sealed) return build.onResolve(options, callback as never)
1095
1120
  registered.push({
1096
1121
  plugin: plugin.name,
1097
1122
  // Drop /g and /y: a sticky or global regex carries lastIndex
1098
1123
  // between tests, which would make dispatch order-dependent.
1099
- filter: new RegExp(options.filter.source, options.filter.flags.replace(/[gy]/g, '')),
1124
+ filter: new RegExp(
1125
+ options.filter.source,
1126
+ options.filter.flags.replace(/[gy]/g, ''),
1127
+ ),
1100
1128
  namespace: options.namespace,
1101
1129
  callback,
1102
- });
1130
+ })
1103
1131
  },
1104
1132
  // esbuild's own `resolve` re-runs every plugin EXCEPT the caller's (its recursion guard,
1105
1133
  // keyed on plugin name). Sharing one name would make it skip the whole chain and fall
@@ -1122,16 +1150,16 @@ export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[]
1122
1150
  ? resolvedFromHook(specifier, result)
1123
1151
  : build.resolve(specifier, { ...options, pluginName: name }),
1124
1152
  ),
1125
- };
1126
- await plugin.setup(proxy);
1153
+ }
1154
+ await plugin.setup(proxy)
1127
1155
  }
1128
- sealed = true;
1129
- if (registered.length === 0) return;
1130
- const filter = new RegExp(registered.map(entry => `(?:${entry.filter.source})`).join('|'));
1131
- build.onResolve({ filter }, args => dispatch(args));
1156
+ sealed = true
1157
+ if (registered.length === 0) return
1158
+ const filter = new RegExp(registered.map(entry => `(?:${entry.filter.source})`).join('|'))
1159
+ build.onResolve({ filter }, args => dispatch(args))
1132
1160
  },
1133
1161
  },
1134
- ];
1162
+ ]
1135
1163
  }
1136
1164
 
1137
1165
  /**
@@ -1139,7 +1167,7 @@ export function coalesceResolveHooks(plugins: Plugin[], name: string): Plugin[]
1139
1167
  * this specifier, do not bundle it" - esbuild's own rule, verified against the binary.
1140
1168
  */
1141
1169
  function resolvedFromHook(specifier: string, result: OnResolveResult): ResolveResult {
1142
- const resolved = result.path ?? (result.external ? specifier : '');
1170
+ const resolved = result.path ?? (result.external ? specifier : '')
1143
1171
  return {
1144
1172
  errors: (result.errors ?? []) as ResolveResult['errors'],
1145
1173
  warnings: (result.warnings ?? []) as ResolveResult['warnings'],
@@ -1149,7 +1177,7 @@ function resolvedFromHook(specifier: string, result: OnResolveResult): ResolveRe
1149
1177
  namespace: result.namespace ?? (result.path ? 'file' : ''),
1150
1178
  suffix: result.suffix ?? '',
1151
1179
  pluginData: result.pluginData as unknown,
1152
- };
1180
+ }
1153
1181
  }
1154
1182
 
1155
1183
  /**
@@ -1163,7 +1191,7 @@ function resolveHookSettled(result: OnResolveResult) {
1163
1191
  result.external !== undefined ||
1164
1192
  (result.errors?.length ?? 0) > 0 ||
1165
1193
  (result.warnings?.length ?? 0) > 0
1166
- );
1194
+ )
1167
1195
  }
1168
1196
 
1169
1197
  function clientBuildPlugins(
@@ -1172,55 +1200,58 @@ function clientBuildPlugins(
1172
1200
  extra: Plugin[] = [],
1173
1201
  reactLite = false,
1174
1202
  ): Plugin[] {
1175
- return coalesceResolveHooks(instrumentedPlugins([
1176
- serverOnlyClientImportPlugin(),
1177
- // The virtual-module plugins (`extra`: generated entries + the shared route
1178
- // runtime) go first so they claim their own synthetic specifiers before the
1179
- // resolve-chain plugins are asked about them one entry point per route is
1180
- // otherwise offered to every plugin in the chain that only ever declines it.
1181
- ...extra,
1182
- ...getBundlerExtensions().clientEsbuildPlugins(config),
1183
- linkedPackageClientResolvePlugin(config),
1184
- clientStaticAssetPlugin(config),
1185
- pipeline.plugin(),
1186
- importAliasPlugin(config, reactLite),
1187
- cssModuleClientPlugin(),
1188
- ]), 'pnext-client-resolve-chain');
1203
+ return coalesceResolveHooks(
1204
+ instrumentedPlugins([
1205
+ serverOnlyClientImportPlugin(),
1206
+ // The virtual-module plugins (`extra`: generated entries + the shared route
1207
+ // runtime) go first so they claim their own synthetic specifiers before the
1208
+ // resolve-chain plugins are asked about them one entry point per route is
1209
+ // otherwise offered to every plugin in the chain that only ever declines it.
1210
+ ...extra,
1211
+ ...getBundlerExtensions().clientEsbuildPlugins(config),
1212
+ linkedPackageClientResolvePlugin(config),
1213
+ clientStaticAssetPlugin(config),
1214
+ pipeline.plugin(),
1215
+ importAliasPlugin(config, reactLite),
1216
+ cssModuleClientPlugin(),
1217
+ ]),
1218
+ 'pnext-client-resolve-chain',
1219
+ )
1189
1220
  }
1190
1221
 
1191
1222
  function importAliasPlugin(config: ResolvedConfig, reactLite = false): Plugin {
1192
- const aliases = { ...getImportAliasExtensions().aliases(config, 'client') };
1223
+ const aliases = { ...getImportAliasExtensions().aliases(config, 'client') }
1193
1224
  // Suspense-free tier: the app's `react` imports resolve to the compat-free lite shim, so the
1194
1225
  // bundle ships preact core + hooks without preact/compat (see clientSuspenseFree).
1195
1226
  if (reactLite && aliases.react) {
1196
- aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts');
1227
+ aliases.react = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client-lite.ts')
1197
1228
  }
1198
- const specifiers = Object.keys(aliases);
1229
+ const specifiers = Object.keys(aliases)
1199
1230
  return {
1200
1231
  name: 'pnext-import-alias',
1201
1232
  setup(build) {
1202
1233
  build.onResolve({ filter: /^(?:react(?:-dom)?|next)(?:\/|$)/ }, args => {
1203
- if (aliases[args.path]) return undefined;
1204
- const message = getImportAliasExtensions().missingImportError(config, args.path);
1205
- return message ? { errors: [{ text: message }] } : undefined;
1206
- });
1207
- if (specifiers.length === 0) return;
1234
+ if (aliases[args.path]) return undefined
1235
+ const message = getImportAliasExtensions().missingImportError(config, args.path)
1236
+ return message ? { errors: [{ text: message }] } : undefined
1237
+ })
1238
+ if (specifiers.length === 0) return
1208
1239
  build.onResolve(
1209
1240
  { filter: new RegExp(`^(${specifiers.map(escapeRegex).join('|')})$`) },
1210
1241
  async args => {
1211
- const target = aliases[args.path];
1212
- if (!target) return undefined;
1213
- if (path.isAbsolute(target)) return { path: target };
1242
+ const target = aliases[args.path]
1243
+ if (!target) return undefined
1244
+ if (path.isAbsolute(target)) return { path: target }
1214
1245
  return build.resolve(target, {
1215
1246
  kind: args.kind,
1216
1247
  importer: args.importer,
1217
1248
  namespace: args.namespace,
1218
1249
  resolveDir: args.resolveDir,
1219
- });
1250
+ })
1220
1251
  },
1221
- );
1252
+ )
1222
1253
  },
1223
- };
1254
+ }
1224
1255
  }
1225
1256
 
1226
1257
  // preact core + preact/compat + react/react-dom/next are single-instance: they must ALWAYS resolve
@@ -1230,11 +1261,11 @@ function importAliasPlugin(config: ResolvedConfig, reactLite = false): Plugin {
1230
1261
  // `preact` to the linked copy while pnext-internal compat code keeps resolving to the alias -
1231
1262
  // shipping TWO physical preact cores. That breaks single-instance option hooks, context identity and
1232
1263
  // error interception. Never let the linked-package resolver shadow these; the alias owns them.
1233
- const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)/;
1264
+ const frameworkAliasedSpecifier = /^(?:preact|react|react-dom|next)(?:\/|$)/
1234
1265
 
1235
1266
  function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
1236
- const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts');
1237
- const compatReactClient = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts');
1267
+ const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts')
1268
+ const compatReactClient = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts')
1238
1269
  return {
1239
1270
  name: 'pnext-linked-package-client-resolve',
1240
1271
  setup(build) {
@@ -1243,36 +1274,36 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
1243
1274
  // outputs under node_modules paths, e.g. @next/third-parties client
1244
1275
  // components) where no sibling ./preact exists; only a real sibling
1245
1276
  // file (an app's own ./preact.*) opts out.
1246
- if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact };
1277
+ if (path.resolve(args.importer) === compatReactClient) return { path: compatPreact }
1247
1278
  const hasRealSibling = ['.ts', '.tsx', '.js', '.mjs', '.jsx'].some(ext =>
1248
1279
  existsSync(path.join(args.resolveDir, `preact${ext}`)),
1249
- );
1250
- if (hasRealSibling) return undefined;
1251
- return { path: compatPreact };
1252
- });
1280
+ )
1281
+ if (hasRealSibling) return undefined
1282
+ return { path: compatPreact }
1283
+ })
1253
1284
  // One build's worth of memo, keyed on exactly what esbuild hands us: the resolver walks
1254
1285
  // node_modules and probes package targets per call, and a directory of components importing
1255
1286
  // the same few packages asks the same question repeatedly. Per-BUILD, so a linked package's
1256
1287
  // layout changing between builds is still seen - esbuild already assumes it is fixed within one.
1257
- const linked = new Map<string, string | undefined>();
1288
+ const linked = new Map<string, string | undefined>()
1258
1289
  build.onResolve({ filter: /^[^./#][^:]*$/ }, args => {
1259
- if (frameworkAliasedSpecifier.test(args.path)) return undefined;
1260
- const dir = args.resolveDir || config.root;
1261
- const key = `${dir}\0${args.path}`;
1262
- let resolved = linked.get(key);
1290
+ if (frameworkAliasedSpecifier.test(args.path)) return undefined
1291
+ const dir = args.resolveDir || config.root
1292
+ const key = `${dir}\0${args.path}`
1293
+ let resolved = linked.get(key)
1263
1294
  if (resolved === undefined && !linked.has(key)) {
1264
1295
  resolved = resolveLinkedPackageSpecifier(
1265
1296
  config.root,
1266
1297
  path.join(dir, 'pnext-resolve.ts'),
1267
1298
  args.path,
1268
1299
  ['browser', 'style', 'import', 'default'],
1269
- );
1270
- linked.set(key, resolved);
1300
+ )
1301
+ linked.set(key, resolved)
1271
1302
  }
1272
- return resolved ? { path: resolved } : undefined;
1273
- });
1303
+ return resolved ? { path: resolved } : undefined
1304
+ })
1274
1305
  },
1275
- };
1306
+ }
1276
1307
  }
1277
1308
 
1278
1309
  // Mirror of the server build's static-asset seam for the CLIENT bundle. A static image import from a
@@ -1280,7 +1311,7 @@ function linkedPackageClientResolvePlugin(config: ResolvedConfig): Plugin {
1280
1311
  // object - with the asset emitted under `/_next/static/media/...` - rather than a bare `file` loader
1281
1312
  // URL string. Uses core's `staticAssetModule` extension seam so compat (next/image) supplies the
1282
1313
  // descriptor and pure-core apps fall back to the generic URL module.
1283
- const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/;
1314
+ const staticImageAssetFilter = /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/
1284
1315
 
1285
1316
  function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
1286
1317
  return {
@@ -1291,66 +1322,66 @@ function clientStaticAssetPlugin(config: ResolvedConfig): Plugin {
1291
1322
  // `*.svg`) preempts the generic static-image resolver: defer so the
1292
1323
  // compat loader-rule plugin runs the chain instead.
1293
1324
  if (getAssetExtensions().hasLoaderRuleFor(args.path)) {
1294
- return undefined;
1325
+ return undefined
1295
1326
  }
1296
1327
  return {
1297
1328
  path: resolveAssetPath(args.path, args.resolveDir),
1298
1329
  namespace: 'pnext-client-static-asset',
1299
- };
1300
- });
1330
+ }
1331
+ })
1301
1332
  build.onLoad({ filter: /.*/, namespace: 'pnext-client-static-asset' }, async args => ({
1302
1333
  contents: await staticImageModuleSource(config, args.path),
1303
1334
  loader: 'js',
1304
- }));
1335
+ }))
1305
1336
  },
1306
- };
1337
+ }
1307
1338
  }
1308
1339
 
1309
1340
  function resolveAssetPath(specifier: string, resolveDir: string) {
1310
- const { sourcePath, hash } = splitAssetHash(specifier);
1311
- const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath);
1312
- return `${resolved}${hash}`;
1341
+ const { sourcePath, hash } = splitAssetHash(specifier)
1342
+ const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath)
1343
+ return `${resolved}${hash}`
1313
1344
  }
1314
1345
 
1315
1346
  function splitAssetHash(specifier: string) {
1316
- const index = specifier.search(/[?#]/);
1347
+ const index = specifier.search(/[?#]/)
1317
1348
  return index === -1
1318
1349
  ? { sourcePath: specifier, hash: '' }
1319
- : { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) };
1350
+ : { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) }
1320
1351
  }
1321
1352
 
1322
1353
  async function staticImageModuleSource(config: ResolvedConfig, file: string) {
1323
- const { sourcePath } = splitAssetHash(file);
1324
- const bytes = new Uint8Array(await readFile(sourcePath));
1325
- const emitted: string[] = [];
1354
+ const { sourcePath } = splitAssetHash(file)
1355
+ const bytes = new Uint8Array(await readFile(sourcePath))
1356
+ const emitted: string[] = []
1326
1357
  const emit = (relative: string) => {
1327
- emitted.push(relative);
1328
- return `/${relative}`;
1329
- };
1330
- const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit });
1331
- const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit);
1358
+ emitted.push(relative)
1359
+ return `/${relative}`
1360
+ }
1361
+ const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit })
1362
+ const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit)
1332
1363
  for (const relative of emitted) {
1333
- const target = path.join(config.outPath, 'public', ...relative.split('/'));
1334
- await mkdir(path.dirname(target), { recursive: true });
1335
- if (!existsSync(target)) await copyFile(sourcePath, target);
1364
+ const target = path.join(config.outPath, 'public', ...relative.split('/'))
1365
+ await mkdir(path.dirname(target), { recursive: true })
1366
+ if (!existsSync(target)) await copyFile(sourcePath, target)
1336
1367
  }
1337
- return source;
1368
+ return source
1338
1369
  }
1339
1370
 
1340
1371
  // Core's generic static-asset module (no compat override): emit under a hashed
1341
1372
  // `/_next/static/media` URL and export the URL string as default. Mirrors the
1342
- // identically named helper in runtime/server.ts + dev/imports.ts.
1373
+ // identically named helper in runtime/loader.ts + runtime/modules.ts.
1343
1374
  function coreStaticAssetModule(
1344
1375
  sourcePath: string,
1345
1376
  bytes: Uint8Array,
1346
1377
  emit: (relative: string) => string,
1347
1378
  ): string {
1348
- const ext = path.extname(sourcePath).toLowerCase() || '.bin';
1349
- const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8);
1350
- const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-');
1351
- const relative = path.posix.join('_next', 'static', 'media', `${base}.${hash}${ext}`);
1352
- const src = emit(relative);
1353
- return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`;
1379
+ const ext = path.extname(sourcePath).toLowerCase() || '.bin'
1380
+ const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8)
1381
+ const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-')
1382
+ const relative = path.posix.join('_next', 'static', 'media', `${base}.${hash}${ext}`)
1383
+ const src = emit(relative)
1384
+ return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`
1354
1385
  }
1355
1386
 
1356
1387
  /**
@@ -1366,91 +1397,93 @@ function coreStaticAssetModule(
1366
1397
  * loader; scoped to the source roots, so third-party node_modules .js keeps esbuild's default loader.
1367
1398
  */
1368
1399
  function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
1369
- const sourceRewrite = nextCompatEnabled(config);
1370
- const compiler = getCompatModeExtensions().reactCompilerOptions(config);
1371
- const asyncPre = hasClientSourceAsyncPreTransforms();
1400
+ const sourceRewrite = nextCompatEnabled(config)
1401
+ const compiler = getCompatModeExtensions().reactCompilerOptions(config)
1402
+ const asyncPre = hasClientSourceAsyncPreTransforms()
1372
1403
  // Under a source root, and under a root only first-party source plus transpiled packages - said in
1373
1404
  // the FILTER, not the callback: a dependency's files are the bulk of a real graph, so a callback
1374
1405
  // would decline most of them one at a time.
1375
- const roots = clientSourceRoots(config).map(root => escapeRegex(root)).join('|');
1376
- const sep = escapeRegex(path.sep);
1406
+ const roots = clientSourceRoots(config)
1407
+ .map(root => escapeRegex(root))
1408
+ .join('|')
1409
+ const sep = escapeRegex(path.sep)
1377
1410
  const filter = new RegExp(
1378
1411
  `^(?:${roots})(?:${sep}${nonSegmentPattern('node_modules', sep)})*${sep}[^${sep}]*\\.(?:[cm]?[jt]sx?)$`,
1379
- );
1380
- const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined;
1412
+ )
1413
+ const transpiled = sourceRewrite ? transpiledPackageFilter(roots) : undefined
1381
1414
  // One in-flight pipeline per file for the whole build: `warm()` and the
1382
1415
  // onLoad hook share it, so a warmed file is already done (or in flight on
1383
1416
  // oxc's threadpool) by the time esbuild asks for it, and a component shared
1384
1417
  // by twenty routes is prepared once.
1385
- const prepared = new Map<string, Promise<OnLoadOutput>>();
1418
+ const prepared = new Map<string, Promise<OnLoadOutput>>()
1386
1419
  // Deferred dynamic refs THIS pipeline rewrote — persisted as the out-dir sidecar.
1387
- const deferredRefs = new Map<string, DeferredDynamicRef>();
1420
+ const deferredRefs = new Map<string, DeferredDynamicRef>()
1388
1421
  // The text esbuild actually parsed, kept so the prebuilt seam can read a
1389
1422
  // file's framework imports off the POST-transform source — a compat rewrite
1390
1423
  // can add or rename one, and the seam must offer exactly what is there.
1391
- const loaded = new Map<string, string>();
1424
+ const loaded = new Map<string, string>()
1392
1425
 
1393
1426
  function prepare(resolved: string): Promise<OnLoadOutput> {
1394
- const existing = prepared.get(resolved);
1395
- if (existing) return existing;
1396
- const pending = runPipeline(resolved);
1397
- prepared.set(resolved, pending);
1398
- return pending;
1427
+ const existing = prepared.get(resolved)
1428
+ if (existing) return existing
1429
+ const pending = runPipeline(resolved)
1430
+ prepared.set(resolved, pending)
1431
+ return pending
1399
1432
  }
1400
1433
 
1401
1434
  async function runPipeline(resolved: string): Promise<OnLoadOutput> {
1402
- const inNodeModules = resolved.split(path.sep).includes('node_modules');
1435
+ const inNodeModules = resolved.split(path.sep).includes('node_modules')
1403
1436
  // A nested node_modules under a source root is a dependency, not app
1404
1437
  // source: only an explicitly transpiled package opts into the rewrites,
1405
1438
  // and none of them are ever React-Compiled.
1406
- if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined;
1407
- clientProfile.count('loadFiles');
1439
+ if (inNodeModules && (!sourceRewrite || !isTranspiledPackageFile(resolved))) return undefined
1440
+ clientProfile.count('loadFiles')
1408
1441
  // Build-scoped cache: the route-fact walk has already read most of these,
1409
1442
  // so this is a map lookup rather than a second read of the same file.
1410
- const original = await clientProfile.timeAsync('read', () => readSourceText(resolved));
1411
- let contents = original;
1443
+ const original = await clientProfile.timeAsync('read', () => readSourceText(resolved))
1444
+ let contents = original
1412
1445
  // Worker bundling runs first and awaits: it consumes `import.meta.url`,
1413
1446
  // which the sync chain inlines.
1414
1447
  if (asyncPre) {
1415
- contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root);
1448
+ contents = await applyClientSourceAsyncPreTransforms(contents, resolved, config.root)
1416
1449
  }
1417
1450
  contents = clientProfile.time('chain', () =>
1418
1451
  applyClientSourceTransforms(contents, resolved, config.root),
1419
- );
1420
- contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved));
1452
+ )
1453
+ contents = clientProfile.time('dynamic', () => rewriteLiteralDynamicCalls(contents, resolved))
1421
1454
  if (dev && devDynamicSplitEnabled()) {
1422
1455
  contents = rewriteDeferredDynamicImports(
1423
1456
  contents,
1424
1457
  resolved,
1425
1458
  specifier => resolveImport(rootFromFile(resolved), resolved, specifier),
1426
1459
  target => {
1427
- const id = clientReferenceId(target.file, target.exportName);
1428
- registerDeferredDynamicRef(id, target);
1429
- deferredRefs.set(id, target);
1430
- return deferredDynamicChunkHref({ id });
1460
+ const id = clientReferenceId(target.file, target.exportName)
1461
+ registerDeferredDynamicRef(id, target)
1462
+ deferredRefs.set(id, target)
1463
+ return deferredDynamicChunkHref({ id })
1431
1464
  },
1432
- );
1465
+ )
1433
1466
  }
1434
1467
  if (compiler && !inNodeModules && shouldReactCompile(contents, resolved)) {
1435
1468
  contents = await clientProfile.timeAsync('reactCompiler', () =>
1436
1469
  reactCompiled(resolved, contents, compiler),
1437
- );
1470
+ )
1438
1471
  }
1439
1472
  // Unchanged sources go back to esbuild's native loader; only .js/.mjs
1440
1473
  // must be claimed to force the jsx parse.
1441
- loaded.set(resolved, contents);
1442
- if (contents === original && !/\.m?js$/.test(resolved)) return undefined;
1443
- return { contents, loader: clientSourceLoader(resolved) };
1474
+ loaded.set(resolved, contents)
1475
+ if (contents === original && !/\.m?js$/.test(resolved)) return undefined
1476
+ return { contents, loader: clientSourceLoader(resolved) }
1444
1477
  }
1445
1478
 
1446
1479
  return {
1447
1480
  /** The parsed text of a file this build has loaded, if it has. */
1448
1481
  sourceOf(file: string) {
1449
- return loaded.get(file);
1482
+ return loaded.get(file)
1450
1483
  },
1451
1484
  /** Deferred dynamic refs rewritten during this build (dev split). */
1452
1485
  deferredRefs() {
1453
- return deferredRefs;
1486
+ return deferredRefs
1454
1487
  },
1455
1488
  /**
1456
1489
  * Start the pipeline for sources the route table already names, before esbuild runs. Without it
@@ -1459,15 +1492,15 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
1459
1492
  */
1460
1493
  /** `warm` for whole routes - every client source the route table names. */
1461
1494
  warmRoutes(routes: RouteManifestEntry[]) {
1462
- this.warm(routeClientSources(routes));
1495
+ this.warm(routeClientSources(routes))
1463
1496
  },
1464
1497
  warm(files: Iterable<string>) {
1465
1498
  for (const file of files) {
1466
- const resolved = path.resolve(file);
1467
- if (!filter.test(resolved) && !transpiled?.test(resolved)) continue;
1499
+ const resolved = path.resolve(file)
1500
+ if (!filter.test(resolved) && !transpiled?.test(resolved)) continue
1468
1501
  // Errors surface at the onLoad await, where esbuild attributes them to
1469
1502
  // the importing module; nothing must reject on the warm path.
1470
- prepare(resolved).catch(() => undefined);
1503
+ prepare(resolved).catch(() => undefined)
1471
1504
  }
1472
1505
  },
1473
1506
  /**
@@ -1483,24 +1516,24 @@ function createClientSourcePipeline(config: ResolvedConfig, dev = false) {
1483
1516
  // namespace: 'file' — an onLoad without a namespace matches EVERY
1484
1517
  // namespace, which would hijack virtual modules (e.g. the server-action
1485
1518
  // client stub) whose paths end in .js and feed back the original source.
1486
- build.onLoad({ filter, namespace: 'file' }, args => prepare(path.resolve(args.path)));
1519
+ build.onLoad({ filter, namespace: 'file' }, args => prepare(path.resolve(args.path)))
1487
1520
  // Second hook, not a wider first one: `transpilePackages` opts a
1488
1521
  // dependency's own files into the same pipeline, and they are the only
1489
1522
  // node_modules paths that ever produce contents.
1490
1523
  if (transpiled) {
1491
1524
  build.onLoad({ filter: transpiled, namespace: 'file' }, args =>
1492
1525
  prepare(path.resolve(args.path)),
1493
- );
1526
+ )
1494
1527
  }
1495
1528
  },
1496
- };
1529
+ }
1497
1530
  },
1498
- };
1531
+ }
1499
1532
  }
1500
1533
 
1501
- type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined;
1534
+ type OnLoadOutput = { contents: string; loader: 'js' | 'ts' | 'tsx' | 'jsx' } | undefined
1502
1535
 
1503
- export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>;
1536
+ export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
1504
1537
 
1505
1538
  /**
1506
1539
  * Open the client-source pipeline early so the build can feed it routes as the fact scan resolves
@@ -1509,64 +1542,64 @@ export type ClientSourcePipeline = ReturnType<typeof createClientSourcePipeline>
1509
1542
  * otherwise create for itself.
1510
1543
  */
1511
1544
  export function startClientSources(config: ResolvedConfig): ClientSourcePipeline {
1512
- conventionCache.clear();
1513
- return createClientSourcePipeline(config);
1545
+ conventionCache.clear()
1546
+ return createClientSourcePipeline(config)
1514
1547
  }
1515
1548
 
1516
1549
  // The compiled-source cache is keyed on the file and a hash of its post-rewrite
1517
1550
  // input, so a rebuild of an unchanged file skips the oxc pass. It holds the
1518
1551
  // PROMISE, so two concurrent loads of the same source (two routes sharing a
1519
1552
  // component) share one compile instead of racing two.
1520
- const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>();
1553
+ const reactCompiledCache = new Map<string, { hash: bigint; code: Promise<string> }>()
1521
1554
 
1522
1555
  function reactCompiled(
1523
1556
  file: string,
1524
1557
  source: string,
1525
1558
  options: NonNullable<ReturnType<CompatModeExtensions['reactCompilerOptions']>>,
1526
1559
  ) {
1527
- const hash = Bun.hash.wyhash(`${options.target}\0${source}`);
1528
- const cached = reactCompiledCache.get(file);
1529
- if (cached?.hash === hash) return cached.code;
1530
- const code = transformReactCompiler(source, file, options);
1531
- reactCompiledCache.set(file, { hash, code });
1532
- return code;
1560
+ const hash = Bun.hash.wyhash(`${options.target}\0${source}`)
1561
+ const cached = reactCompiledCache.get(file)
1562
+ if (cached?.hash === hash) return cached.code
1563
+ const code = transformReactCompiler(source, file, options)
1564
+ reactCompiledCache.set(file, { hash, code })
1565
+ return code
1533
1566
  }
1534
1567
 
1535
1568
  // config.root can sit outside the workspace root (and vice versa), so both
1536
1569
  // anchor the filter — as does each one's realpath, since esbuild reports
1537
1570
  // resolved paths through the OS realpath.
1538
1571
  function clientSourceRoots(config: ResolvedConfig) {
1539
- const roots = new Set<string>();
1572
+ const roots = new Set<string>()
1540
1573
  for (const root of [config.root, config.workspaceRoot]) {
1541
- const resolved = path.resolve(root);
1542
- roots.add(resolved);
1574
+ const resolved = path.resolve(root)
1575
+ roots.add(resolved)
1543
1576
  try {
1544
- roots.add(realpathSync.native(resolved));
1577
+ roots.add(realpathSync.native(resolved))
1545
1578
  } catch {
1546
1579
  // A root that cannot be realpath'd is already covered by its literal form.
1547
1580
  }
1548
1581
  }
1549
- return [...roots];
1582
+ return [...roots]
1550
1583
  }
1551
1584
 
1552
1585
  // `.cjs` keeps the plain js loader: CommonJS never carries JSX, and parsing a
1553
1586
  // `a < b` comparison as JSX would fail the build.
1554
1587
  function clientSourceLoader(file: string): 'js' | 'ts' | 'tsx' | 'jsx' {
1555
- if (file.endsWith('.tsx')) return 'tsx';
1556
- if (/\.[cm]?ts$/.test(file)) return 'ts';
1557
- return file.endsWith('.cjs') ? 'js' : 'jsx';
1588
+ if (file.endsWith('.tsx')) return 'tsx'
1589
+ if (/\.[cm]?ts$/.test(file)) return 'ts'
1590
+ return file.endsWith('.cjs') ? 'js' : 'jsx'
1558
1591
  }
1559
1592
 
1560
1593
  // A path segment that is NOT `word`, spelled positively: esbuild's Go regexp has
1561
1594
  // no lookahead, so the negation enumerates "differs at some position / ends
1562
1595
  // early / runs longer".
1563
1596
  function nonSegmentPattern(word: string, sep: string) {
1564
- const parts = [`${word}[^${sep}]+`];
1597
+ const parts = [`${word}[^${sep}]+`]
1565
1598
  for (let index = 0; index < word.length; index += 1) {
1566
- const prefix = word.slice(0, index);
1567
- parts.push(prefix, `${prefix}[^${sep}${word[index]}][^${sep}]*`);
1599
+ const prefix = word.slice(0, index)
1600
+ parts.push(prefix, `${prefix}[^${sep}${word[index]}][^${sep}]*`)
1568
1601
  }
1569
- return `(?:${parts.join('|')})`;
1602
+ return `(?:${parts.join('|')})`
1570
1603
  }
1571
1604
 
1572
1605
  /**
@@ -1575,45 +1608,45 @@ function nonSegmentPattern(word: string, sep: string) {
1575
1608
  * path on the hook.
1576
1609
  */
1577
1610
  function transpiledPackageFilter(roots: string): RegExp | undefined {
1578
- const sep = escapeRegex(path.sep);
1579
- const names = getExternalPackagePolicy().transpiled?.();
1580
- if (names?.length === 0) return undefined;
1611
+ const sep = escapeRegex(path.sep)
1612
+ const names = getExternalPackagePolicy().transpiled?.()
1613
+ if (names?.length === 0) return undefined
1581
1614
  const packages = names
1582
1615
  ? `(?:${names.map(name => escapeRegex(name).split('/').join(sep)).join('|')})${sep}`
1583
- : '';
1584
- return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`);
1616
+ : ''
1617
+ return new RegExp(`^(?:${roots}).*${sep}node_modules${sep}${packages}.*\\.(?:[cm]?[jt]sx?)$`)
1585
1618
  }
1586
1619
 
1587
1620
  function isTranspiledPackageFile(file: string) {
1588
- const parts = file.split(path.sep);
1589
- const nodeModules = parts.lastIndexOf('node_modules');
1590
- if (nodeModules === -1) return false;
1591
- const first = parts[nodeModules + 1];
1592
- if (!first) return false;
1593
- const name = first.startsWith('@') ? `${first}/${parts[nodeModules + 2] ?? ''}` : first;
1594
- return getExternalPackagePolicy().transpile(name);
1621
+ const parts = file.split(path.sep)
1622
+ const nodeModules = parts.lastIndexOf('node_modules')
1623
+ if (nodeModules === -1) return false
1624
+ const first = parts[nodeModules + 1]
1625
+ if (!first) return false
1626
+ const name = first.startsWith('@') ? `${first}/${parts[nodeModules + 2] ?? ''}` : first
1627
+ return getExternalPackagePolicy().transpile(name)
1595
1628
  }
1596
1629
 
1597
1630
  function virtualEntryPath(routeId: string) {
1598
- return `${virtualEntryNamespace}:${routeId}`;
1631
+ return `${virtualEntryNamespace}:${routeId}`
1599
1632
  }
1600
1633
 
1601
1634
  function virtualEntryPlugin(entries: { route: RouteManifestEntry; source: string }[]): Plugin {
1602
- const sources = new Map(entries.map(entry => [entry.route.id, entry.source]));
1635
+ const sources = new Map(entries.map(entry => [entry.route.id, entry.source]))
1603
1636
  return {
1604
1637
  name: 'pnext-virtual-client-entry',
1605
1638
  setup(build) {
1606
1639
  build.onResolve({ filter: new RegExp(`^${escapeRegex(virtualEntryNamespace)}:`) }, args => ({
1607
1640
  path: args.path.slice(virtualEntryNamespace.length + 1),
1608
1641
  namespace: virtualEntryNamespace,
1609
- }));
1642
+ }))
1610
1643
  build.onLoad({ filter: /.*/, namespace: virtualEntryNamespace }, args => {
1611
- const contents = sources.get(args.path);
1612
- if (contents === undefined) return undefined;
1613
- return { contents, loader: 'ts', resolveDir: process.cwd() };
1614
- });
1644
+ const contents = sources.get(args.path)
1645
+ if (contents === undefined) return undefined
1646
+ return { contents, loader: 'ts', resolveDir: process.cwd() }
1647
+ })
1615
1648
  },
1616
- };
1649
+ }
1617
1650
  }
1618
1651
 
1619
1652
  // The shared route runtime: one virtual module per build, imported by every
@@ -1626,14 +1659,14 @@ function clientRuntimePlugin(facts: ClientRuntimeFacts): Plugin {
1626
1659
  build.onResolve({ filter: new RegExp(`^${escapeRegex(CLIENT_RUNTIME_MODULE)}$`) }, () => ({
1627
1660
  path: CLIENT_RUNTIME_MODULE,
1628
1661
  namespace: CLIENT_RUNTIME_MODULE,
1629
- }));
1662
+ }))
1630
1663
  build.onLoad({ filter: /.*/, namespace: CLIENT_RUNTIME_MODULE }, () => ({
1631
1664
  contents: clientRuntimeSource(facts),
1632
1665
  loader: 'ts',
1633
1666
  resolveDir: process.cwd(),
1634
- }));
1667
+ }))
1635
1668
  },
1636
- };
1669
+ }
1637
1670
  }
1638
1671
 
1639
1672
  async function profileClientBuild<T>(
@@ -1642,25 +1675,18 @@ async function profileClientBuild<T>(
1642
1675
  task: () => Promise<T>,
1643
1676
  ) {
1644
1677
  // eslint-disable-next-line turbo/no-undeclared-env-vars
1645
- if (!process.env.PNEXT_DEV_PROFILE) return task();
1646
- const start = performance.now();
1678
+ if (!process.env.PNEXT_DEV_PROFILE) return task()
1679
+ const start = performance.now()
1647
1680
  try {
1648
- return await task();
1681
+ return await task()
1649
1682
  } finally {
1650
- const mode = dev ? 'dev' : 'prod';
1683
+ const mode = dev ? 'dev' : 'prod'
1651
1684
  console.log(
1652
1685
  `dev-profile client build ${mode} ${route.route} in ${formatDuration(performance.now() - start)}`,
1653
- );
1686
+ )
1654
1687
  }
1655
1688
  }
1656
1689
 
1657
- function formatDuration(durationMs: number) {
1658
- const ms = Math.max(0, durationMs);
1659
- if (ms < 10) return `${ms.toFixed(1)}ms`;
1660
- if (ms < 1000) return `${Math.round(ms)}ms`;
1661
- return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
1662
- }
1663
-
1664
1690
  function serverOnlyClientImportPlugin(): Plugin {
1665
1691
  return {
1666
1692
  name: 'pnext-server-only-client-import',
@@ -1674,26 +1700,26 @@ function serverOnlyClientImportPlugin(): Plugin {
1674
1700
  },
1675
1701
  ],
1676
1702
  }),
1677
- );
1703
+ )
1678
1704
  },
1679
- };
1705
+ }
1680
1706
  }
1681
1707
 
1682
1708
  interface EsbuildLikeError extends Error {
1683
1709
  errors?: {
1684
- text?: string;
1710
+ text?: string
1685
1711
  location?: {
1686
- file?: string;
1687
- };
1688
- }[];
1712
+ file?: string
1713
+ }
1714
+ }[]
1689
1715
  }
1690
1716
 
1691
1717
  interface TraceParent {
1692
- file: string;
1693
- specifier: string;
1718
+ file: string
1719
+ specifier: string
1694
1720
  }
1695
1721
 
1696
- const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css'];
1722
+ const traceSourceExtensions = ['.tsx', '.ts', '.jsx', '.js', '.mts', '.cts', '.module.css', '.css']
1697
1723
 
1698
1724
  function withClientImportTrace(
1699
1725
  error: unknown,
@@ -1701,16 +1727,16 @@ function withClientImportTrace(
1701
1727
  route: RouteManifestEntry,
1702
1728
  entrySource: string,
1703
1729
  ) {
1704
- const message = error instanceof Error ? error.message : String(error);
1705
- const trace = clientImportTrace(config, route, entrySource, error);
1706
- if (!trace) return error;
1707
- const next = new Error(`${message}\n\n${trace}`);
1730
+ const message = error instanceof Error ? error.message : String(error)
1731
+ const trace = clientImportTrace(config, route, entrySource, error)
1732
+ if (!trace) return error
1733
+ const next = new Error(`${message}\n\n${trace}`)
1708
1734
  if (error instanceof Error) {
1709
- next.name = error.name;
1710
- next.stack = error.stack ? `${error.stack}\n\n${trace}` : next.stack;
1711
- Object.assign(next, error);
1735
+ next.name = error.name
1736
+ next.stack = error.stack ? `${error.stack}\n\n${trace}` : next.stack
1737
+ Object.assign(next, error)
1712
1738
  }
1713
- return next;
1739
+ return next
1714
1740
  }
1715
1741
 
1716
1742
  function clientImportTrace(
@@ -1719,51 +1745,51 @@ function clientImportTrace(
1719
1745
  entrySource: string,
1720
1746
  error: unknown,
1721
1747
  ) {
1722
- const unresolved = unresolvedImportFromError(error);
1723
- if (!unresolved) return undefined;
1748
+ const unresolved = unresolvedImportFromError(error)
1749
+ if (!unresolved) return undefined
1724
1750
 
1725
- const entryFile = `${route.id}.ts`;
1726
- const failedFile = unresolved.file ? resolveTraceFile(unresolved.file) : undefined;
1727
- const parents = new Map<string, TraceParent>();
1728
- const visited = new Set<string>();
1729
- const queue: { file: string; source: string }[] = [{ file: entryFile, source: entrySource }];
1730
- let unresolvedImporter: string | undefined;
1751
+ const entryFile = `${route.id}.ts`
1752
+ const failedFile = unresolved.file ? resolveTraceFile(unresolved.file) : undefined
1753
+ const parents = new Map<string, TraceParent>()
1754
+ const visited = new Set<string>()
1755
+ const queue: { file: string; source: string }[] = [{ file: entryFile, source: entrySource }]
1756
+ let unresolvedImporter: string | undefined
1731
1757
 
1732
1758
  while (queue.length > 0) {
1733
- const current = queue.shift();
1734
- if (!current || visited.has(current.file)) continue;
1735
- visited.add(current.file);
1736
- if (visited.size > 2000) break;
1759
+ const current = queue.shift()
1760
+ if (!current || visited.has(current.file)) continue
1761
+ visited.add(current.file)
1762
+ if (visited.size > 2000) break
1737
1763
 
1738
1764
  for (const specifier of localImports(current.file, current.source)) {
1739
- const resolved = resolveTraceImport(config.root, current.file, specifier);
1765
+ const resolved = resolveTraceImport(config.root, current.file, specifier)
1740
1766
  if (!resolved) {
1741
1767
  if (
1742
1768
  specifier === unresolved.specifier &&
1743
1769
  (!failedFile || sameFile(current.file, failedFile))
1744
1770
  ) {
1745
- unresolvedImporter = current.file;
1746
- queue.length = 0;
1747
- break;
1771
+ unresolvedImporter = current.file
1772
+ queue.length = 0
1773
+ break
1748
1774
  }
1749
- continue;
1775
+ continue
1750
1776
  }
1751
1777
 
1752
- if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier });
1778
+ if (!parents.has(resolved)) parents.set(resolved, { file: current.file, specifier })
1753
1779
  if (failedFile && sameFile(resolved, failedFile)) {
1754
- unresolvedImporter = resolved;
1755
- queue.length = 0;
1756
- break;
1780
+ unresolvedImporter = resolved
1781
+ queue.length = 0
1782
+ break
1757
1783
  }
1758
1784
  if (!visited.has(resolved) && !isAssetLike(resolved)) {
1759
- queue.push({ file: resolved, source: readTextSyncSafe(resolved) });
1785
+ queue.push({ file: resolved, source: readTextSyncSafe(resolved) })
1760
1786
  }
1761
1787
  }
1762
1788
  }
1763
1789
 
1764
- if (!unresolvedImporter) return undefined;
1765
- const trace = tracePath(entryFile, unresolvedImporter, parents);
1766
- if (trace.length === 0) return undefined;
1790
+ if (!unresolvedImporter) return undefined
1791
+ const trace = tracePath(entryFile, unresolvedImporter, parents)
1792
+ if (trace.length === 0) return undefined
1767
1793
 
1768
1794
  return [
1769
1795
  `pnext client import trace for route ${route.route}:`,
@@ -1771,135 +1797,135 @@ function clientImportTrace(
1771
1797
  (file, index) => `${index === 0 ? ' ' : ' -> '}${displayTraceFile(config, file)}`,
1772
1798
  ),
1773
1799
  ` -> ${unresolved.specifier} (unresolved in browser client bundle)`,
1774
- ].join('\n');
1800
+ ].join('\n')
1775
1801
  }
1776
1802
 
1777
1803
  /** True when the build failed because an import could not be resolved. */
1778
1804
  function hasUnresolvedImportError(error: unknown): boolean {
1779
- const errors = (error as EsbuildLikeError).errors;
1805
+ const errors = (error as EsbuildLikeError).errors
1780
1806
  if (Array.isArray(errors)) {
1781
- return errors.some(item => item.text?.startsWith('Could not resolve '));
1807
+ return errors.some(item => item.text?.startsWith('Could not resolve '))
1782
1808
  }
1783
- const message = error instanceof Error ? error.message : String(error);
1784
- return message.includes('Could not resolve ');
1809
+ const message = error instanceof Error ? error.message : String(error)
1810
+ return message.includes('Could not resolve ')
1785
1811
  }
1786
1812
 
1787
1813
  function unresolvedImportFromError(error: unknown) {
1788
- const buildError = error as EsbuildLikeError;
1789
- const diagnostic = buildError.errors?.find(item => item.text?.startsWith('Could not resolve '));
1790
- const message = error instanceof Error ? error.message : String(error);
1791
- const text = diagnostic?.text ?? message;
1792
- const specifier = /Could not resolve "([^"]+)"/.exec(text)?.[1];
1793
- if (!specifier) return undefined;
1814
+ const buildError = error as EsbuildLikeError
1815
+ const diagnostic = buildError.errors?.find(item => item.text?.startsWith('Could not resolve '))
1816
+ const message = error instanceof Error ? error.message : String(error)
1817
+ const text = diagnostic?.text ?? message
1818
+ const specifier = /Could not resolve "([^"]+)"/.exec(text)?.[1]
1819
+ if (!specifier) return undefined
1794
1820
  return {
1795
1821
  specifier,
1796
1822
  file: diagnostic?.location?.file ?? errorFileFromMessage(message),
1797
- };
1823
+ }
1798
1824
  }
1799
1825
 
1800
1826
  function errorFileFromMessage(message: string) {
1801
- const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message);
1802
- return match?.[1];
1827
+ const match = /\n\s+([^:\n]+):\d+:\d+:\s+ERROR:\s+Could not resolve /.exec(message)
1828
+ return match?.[1]
1803
1829
  }
1804
1830
 
1805
1831
  function resolveTraceImport(root: string, fromFile: string, specifier: string) {
1806
- if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier);
1807
- return resolveImport(root, fromFile, specifier);
1832
+ if (path.isAbsolute(specifier)) return resolveTraceCandidate(specifier)
1833
+ return resolveImport(root, fromFile, specifier)
1808
1834
  }
1809
1835
 
1810
1836
  function resolveTraceFile(file: string) {
1811
1837
  return canonicalTraceFile(
1812
1838
  path.isAbsolute(file) ? path.resolve(file) : path.resolve(process.cwd(), file),
1813
- );
1839
+ )
1814
1840
  }
1815
1841
 
1816
1842
  function resolveTraceCandidate(base: string) {
1817
- const ext = path.extname(base);
1843
+ const ext = path.extname(base)
1818
1844
  const candidates = ext
1819
1845
  ? [base]
1820
1846
  : [
1821
1847
  ...traceSourceExtensions.map(extension => `${base}${extension}`),
1822
1848
  ...traceSourceExtensions.map(extension => path.join(base, `index${extension}`)),
1823
1849
  base,
1824
- ];
1825
- const resolved = candidates.find(isTraceFile);
1826
- return resolved ? canonicalTraceFile(resolved) : undefined;
1850
+ ]
1851
+ const resolved = candidates.find(isTraceFile)
1852
+ return resolved ? canonicalTraceFile(resolved) : undefined
1827
1853
  }
1828
1854
 
1829
1855
  function isTraceFile(file: string) {
1830
1856
  try {
1831
- return statSync(file).isFile();
1857
+ return statSync(file).isFile()
1832
1858
  } catch {
1833
- return false;
1859
+ return false
1834
1860
  }
1835
1861
  }
1836
1862
 
1837
1863
  function readTextSyncSafe(file: string) {
1838
1864
  try {
1839
- return existsSync(file) ? readFileSync(file, 'utf8') : '';
1865
+ return existsSync(file) ? readFileSync(file, 'utf8') : ''
1840
1866
  } catch {
1841
- return '';
1867
+ return ''
1842
1868
  }
1843
1869
  }
1844
1870
 
1845
1871
  function tracePath(entryFile: string, targetFile: string, parents: Map<string, TraceParent>) {
1846
- const pathItems = [targetFile];
1847
- let current = targetFile;
1872
+ const pathItems = [targetFile]
1873
+ let current = targetFile
1848
1874
 
1849
1875
  while (current !== entryFile) {
1850
- const parent = parents.get(current);
1851
- if (!parent) return [];
1852
- current = parent.file;
1853
- pathItems.push(current);
1876
+ const parent = parents.get(current)
1877
+ if (!parent) return []
1878
+ current = parent.file
1879
+ pathItems.push(current)
1854
1880
  }
1855
1881
 
1856
- return pathItems.reverse();
1882
+ return pathItems.reverse()
1857
1883
  }
1858
1884
 
1859
1885
  function sameFile(a: string, b: string) {
1860
- return canonicalTraceFile(a) === canonicalTraceFile(b);
1886
+ return canonicalTraceFile(a) === canonicalTraceFile(b)
1861
1887
  }
1862
1888
 
1863
1889
  function displayTraceFile(config: ResolvedConfig, file: string) {
1864
- if (!path.isAbsolute(file)) return `${file} (generated client entry)`;
1865
- const root = canonicalTraceFile(config.root);
1866
- const workspaceRoot = canonicalTraceFile(config.workspaceRoot);
1867
- const resolvedFile = canonicalTraceFile(file);
1868
- const rootRelative = path.relative(root, resolvedFile);
1869
- if (!rootRelative.startsWith('..') && !path.isAbsolute(rootRelative)) return rootRelative;
1870
- const workspaceRelative = path.relative(workspaceRoot, resolvedFile);
1890
+ if (!path.isAbsolute(file)) return `${file} (generated client entry)`
1891
+ const root = canonicalTraceFile(config.root)
1892
+ const workspaceRoot = canonicalTraceFile(config.workspaceRoot)
1893
+ const resolvedFile = canonicalTraceFile(file)
1894
+ const rootRelative = path.relative(root, resolvedFile)
1895
+ if (!rootRelative.startsWith('..') && !path.isAbsolute(rootRelative)) return rootRelative
1896
+ const workspaceRelative = path.relative(workspaceRoot, resolvedFile)
1871
1897
  if (!workspaceRelative.startsWith('..') && !path.isAbsolute(workspaceRelative))
1872
- return workspaceRelative;
1873
- return resolvedFile;
1898
+ return workspaceRelative
1899
+ return resolvedFile
1874
1900
  }
1875
1901
 
1876
1902
  function canonicalTraceFile(file: string) {
1877
1903
  try {
1878
- return realpathSync.native(file);
1904
+ return realpathSync.native(file)
1879
1905
  } catch {
1880
- return path.resolve(file);
1906
+ return path.resolve(file)
1881
1907
  }
1882
1908
  }
1883
1909
 
1884
1910
  /** One walked client source, with the stat that lets a later boot skip reading it. */
1885
1911
  export interface ClientCacheSource {
1886
- file: string;
1887
- mtimeMs: number;
1888
- size: number;
1889
- hash: string;
1912
+ file: string
1913
+ mtimeMs: number
1914
+ size: number
1915
+ hash: string
1890
1916
  }
1891
1917
 
1892
1918
  export interface ClientCacheKeyParts {
1893
- key: string;
1894
- staticHash: string;
1895
- sources: ClientCacheSource[];
1919
+ key: string
1920
+ staticHash: string
1921
+ sources: ClientCacheSource[]
1896
1922
  }
1897
1923
 
1898
1924
  /**
1899
1925
  * The half of the key that is not a walked source: the client build pipeline's own sources, the
1900
1926
  * generated entry, and the route's reference list. A handful of small reads, so it is cheap to
1901
1927
  * recompute on every lookup - which is what lets the expensive source walk be validated from a
1902
- * persisted index instead (see dev/client-key-cache.ts).
1928
+ * persisted index instead (see dev/restart/client-key.ts).
1903
1929
  */
1904
1930
  export async function clientCacheStaticHash(
1905
1931
  route: RouteManifestEntry,
@@ -1908,18 +1934,18 @@ export async function clientCacheStaticHash(
1908
1934
  ) {
1909
1935
  // The key must observe convention files added since the last build, so it
1910
1936
  // never reads a memo an earlier call left behind.
1911
- conventionCache.clear();
1912
- const hash = createHash('sha256');
1937
+ conventionCache.clear()
1938
+ const hash = createHash('sha256')
1913
1939
  for (const file of clientBuildPipelineFiles()) {
1914
- hash.update(file);
1915
- hash.update('\0');
1916
- hash.update(await readText(file));
1917
- hash.update('\0');
1940
+ hash.update(file)
1941
+ hash.update('\0')
1942
+ hash.update(await readText(file))
1943
+ hash.update('\0')
1918
1944
  }
1919
1945
 
1920
1946
  hash.update(
1921
1947
  clientEntrySource({
1922
- // Dev-only callers (client-key-cache): the key must move with the split
1948
+ // Dev-only callers (restart/client-key): the key must move with the split
1923
1949
  // flag or a toggled PNEXT_DYNAMIC_SPLIT would serve the other arm's entry.
1924
1950
  deferredDynamicHref: devDeferredDynamicHref(true),
1925
1951
  pageFile: route.client ? route.file : undefined,
@@ -1933,11 +1959,11 @@ export async function clientCacheStaticHash(
1933
1959
  clientRootLayout: config ? hasClientRootLayout(config, route) : false,
1934
1960
  shellLayoutOrder: config ? shellLayoutOrder(config, route) : [],
1935
1961
  }),
1936
- );
1937
- hash.update('\0');
1938
- hash.update(route.client ? route.file : '');
1939
- hash.update(JSON.stringify(route.clientReferences));
1940
- return hash.digest('hex');
1962
+ )
1963
+ hash.update('\0')
1964
+ hash.update(route.client ? route.file : '')
1965
+ hash.update(JSON.stringify(route.clientReferences))
1966
+ return hash.digest('hex')
1941
1967
  }
1942
1968
 
1943
1969
  /** The out-dir name: the static half plus every walked source's content hash. */
@@ -1945,18 +1971,18 @@ export function clientCacheKeyFrom(
1945
1971
  staticHash: string,
1946
1972
  sources: readonly { file: string; hash: string }[],
1947
1973
  ) {
1948
- const hash = createHash('sha256').update(staticHash).update('\0');
1974
+ const hash = createHash('sha256').update(staticHash).update('\0')
1949
1975
  for (const source of sources) {
1950
- hash.update(source.file);
1951
- hash.update('\0');
1952
- hash.update(source.hash);
1953
- hash.update('\0');
1976
+ hash.update(source.file)
1977
+ hash.update('\0')
1978
+ hash.update(source.hash)
1979
+ hash.update('\0')
1954
1980
  }
1955
- return hash.digest('hex').slice(0, 16);
1981
+ return hash.digest('hex').slice(0, 16)
1956
1982
  }
1957
1983
 
1958
1984
  export function clientSourceHash(source: string) {
1959
- return Bun.hash(source).toString(36);
1985
+ return Bun.hash(source).toString(36)
1960
1986
  }
1961
1987
 
1962
1988
  /**
@@ -1965,13 +1991,13 @@ export function clientSourceHash(source: string) {
1965
1991
  * hash that stat never described.
1966
1992
  */
1967
1993
  export async function readClientCacheSource(file: string): Promise<ClientCacheSource> {
1968
- const stats = await stat(file).catch(() => undefined);
1994
+ const stats = await stat(file).catch(() => undefined)
1969
1995
  return {
1970
1996
  file,
1971
1997
  mtimeMs: stats?.mtimeMs ?? 0,
1972
1998
  size: stats?.size ?? 0,
1973
1999
  hash: clientSourceHash(await readText(file)),
1974
- };
2000
+ }
1975
2001
  }
1976
2002
 
1977
2003
  export async function clientCacheKeyParts(
@@ -1980,11 +2006,11 @@ export async function clientCacheKeyParts(
1980
2006
  config?: ResolvedConfig,
1981
2007
  staticHash?: string,
1982
2008
  ): Promise<ClientCacheKeyParts> {
1983
- const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config));
2009
+ const staticDigest = staticHash ?? (await clientCacheStaticHash(route, nextCompat, config))
1984
2010
  const sources = await Promise.all(
1985
2011
  (await clientSourceFiles(route)).map(file => readClientCacheSource(file)),
1986
- );
1987
- return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources };
2012
+ )
2013
+ return { key: clientCacheKeyFrom(staticDigest, sources), staticHash: staticDigest, sources }
1988
2014
  }
1989
2015
 
1990
2016
  export async function clientCacheKey(
@@ -1992,7 +2018,7 @@ export async function clientCacheKey(
1992
2018
  nextCompat?: boolean,
1993
2019
  config?: ResolvedConfig,
1994
2020
  ) {
1995
- return (await clientCacheKeyParts(route, nextCompat, config)).key;
2021
+ return (await clientCacheKeyParts(route, nextCompat, config)).key
1996
2022
  }
1997
2023
 
1998
2024
  function clientBuildPipelineFiles() {
@@ -2004,61 +2030,61 @@ function clientBuildPipelineFiles() {
2004
2030
  path.join(import.meta.dirname, 'build.ts'),
2005
2031
  path.resolve(import.meta.dirname, '..', 'compat', 'react', 'client.ts'),
2006
2032
  path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts'),
2007
- ].filter(file => existsSync(file));
2033
+ ].filter(file => existsSync(file))
2008
2034
  }
2009
2035
 
2010
2036
  async function clientSourceFiles(route: RouteManifestEntry) {
2011
2037
  const entryFiles = [
2012
2038
  ...(route.client ? [route.file] : []),
2013
2039
  ...route.clientReferences.map(reference => reference.file),
2014
- ];
2015
- const files = new Set<string>();
2040
+ ]
2041
+ const files = new Set<string>()
2016
2042
 
2017
- await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)));
2043
+ await Promise.all(entryFiles.map(file => collectClientSourceFile(file, files)))
2018
2044
 
2019
- return [...files].sort();
2045
+ return [...files].sort()
2020
2046
  }
2021
2047
 
2022
2048
  // The walk fans out: every file joins `files` before its own read starts, so
2023
2049
  // concurrent branches still visit each file once. Serially this was the whole
2024
2050
  // cost of a route's client cache key (~1.8 s on a 300-module route).
2025
2051
  async function collectClientSourceFile(file: string, files: Set<string>) {
2026
- if (files.has(file) || !existsSync(file)) return;
2027
- files.add(file);
2028
- if (isAssetLike(file)) return;
2052
+ if (files.has(file) || !existsSync(file)) return
2053
+ files.add(file)
2054
+ if (isAssetLike(file)) return
2029
2055
 
2030
- const source = rewriteLiteralDynamicCalls(await readText(file), file);
2056
+ const source = rewriteLiteralDynamicCalls(await readText(file), file)
2031
2057
  await Promise.all(
2032
2058
  [...localImports(file, source)].map(specifier => {
2033
- const resolved = resolveImport(rootFromFile(file), file, specifier);
2034
- return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve();
2059
+ const resolved = resolveImport(rootFromFile(file), file, specifier)
2060
+ return resolved ? collectClientSourceFile(resolved, files) : Promise.resolve()
2035
2061
  }),
2036
- );
2062
+ )
2037
2063
  }
2038
2064
 
2039
2065
  // Value imports, re-exports and literal `import()` of one module, off the
2040
2066
  // memoized parse (PERF-REWRITES #21) — type-only imports bind nothing and are
2041
2067
  // already excluded there.
2042
2068
  export function localImports(file: string, source: string) {
2043
- const facts = scanFacts(file, source);
2044
- const imports = new Set<string>();
2045
- for (const edge of facts.imports) imports.add(edge.specifier);
2046
- for (const edge of facts.dynamicImports) imports.add(edge.specifier);
2047
- return imports;
2069
+ const facts = scanFacts(file, source)
2070
+ const imports = new Set<string>()
2071
+ for (const edge of facts.imports) imports.add(edge.specifier)
2072
+ for (const edge of facts.dynamicImports) imports.add(edge.specifier)
2073
+ return imports
2048
2074
  }
2049
2075
 
2050
2076
  function isAssetLike(file: string) {
2051
- return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file);
2077
+ return /\.(css|svg|png|jpe?g|gif|webp|ico|woff2?)$/.test(file)
2052
2078
  }
2053
2079
 
2054
2080
  function rootFromFile(file: string) {
2055
- const parts = file.split(path.sep);
2056
- const srcApp = parts.lastIndexOf('src');
2081
+ const parts = file.split(path.sep)
2082
+ const srcApp = parts.lastIndexOf('src')
2057
2083
  if (srcApp !== -1 && parts[srcApp + 1] === 'app')
2058
- return parts.slice(0, srcApp).join(path.sep) || path.sep;
2059
- const app = parts.lastIndexOf('app');
2060
- if (app !== -1) return parts.slice(0, app).join(path.sep) || path.sep;
2061
- return path.dirname(file);
2084
+ return parts.slice(0, srcApp).join(path.sep) || path.sep
2085
+ const app = parts.lastIndexOf('app')
2086
+ if (app !== -1) return parts.slice(0, app).join(path.sep) || path.sep
2087
+ return path.dirname(file)
2062
2088
  }
2063
2089
 
2064
2090
  // esbuild names every shared chunk "chunk-<hash>", which makes bundle reports
@@ -2068,96 +2094,96 @@ function rootFromFile(file: string) {
2068
2094
  async function nameSharedClientChunks(outDir: string, metafile?: Metafile) {
2069
2095
  // eslint-disable-next-line turbo/no-undeclared-env-vars
2070
2096
  if (metafile && process.env.PNEXT_CLIENT_METAFILE) {
2071
- await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile));
2097
+ await writeText(path.join(outDir, 'metafile.json'), JSON.stringify(metafile))
2072
2098
  }
2073
- const chunksDir = path.join(outDir, 'chunks');
2074
- const files = (await listFiles(chunksDir)).filter(file => file.endsWith('.js'));
2075
- const labels = metafile ? chunkLabelsFromMetafile(metafile) : new Map<string, string>();
2076
- const renames = new Map<string, string>();
2099
+ const chunksDir = path.join(outDir, 'chunks')
2100
+ const files = (await listFiles(chunksDir)).filter(file => file.endsWith('.js'))
2101
+ const labels = metafile ? chunkLabelsFromMetafile(metafile) : new Map<string, string>()
2102
+ const renames = new Map<string, string>()
2077
2103
 
2078
2104
  for (const file of files) {
2079
- const basename = path.basename(file);
2080
- if (!basename.startsWith('chunk-')) continue;
2081
- renames.set(basename, `${labels.get(basename) ?? 'shared'}-${hashSuffix(basename)}.js`);
2105
+ const basename = path.basename(file)
2106
+ if (!basename.startsWith('chunk-')) continue
2107
+ renames.set(basename, `${labels.get(basename) ?? 'shared'}-${hashSuffix(basename)}.js`)
2082
2108
  }
2083
2109
 
2084
- if (renames.size === 0) return renames;
2110
+ if (renames.size === 0) return renames
2085
2111
 
2086
2112
  await Promise.all(
2087
2113
  [...renames].map(async ([from, to]) => {
2088
- const fromPath = path.join(chunksDir, from);
2089
- const toPath = path.join(chunksDir, to);
2090
- if (from === to || !existsSync(fromPath)) return;
2114
+ const fromPath = path.join(chunksDir, from)
2115
+ const toPath = path.join(chunksDir, to)
2116
+ if (from === to || !existsSync(fromPath)) return
2091
2117
  try {
2092
- await rename(fromPath, toPath);
2118
+ await rename(fromPath, toPath)
2093
2119
  } catch (error) {
2094
2120
  // Tolerate a concurrent build having already moved this chunk (ENOENT);
2095
2121
  // the reference rewrite below still points at the renamed file.
2096
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
2122
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
2097
2123
  }
2098
2124
  // Move the external sourcemap alongside its chunk. The `sourceMappingURL`
2099
2125
  // comment in the renamed chunk is rewritten below (replaceChunkNames matches
2100
2126
  // the `chunk-<hash>.js` prefix inside `chunk-<hash>.js.map`), so the map file
2101
2127
  // must adopt the new name to keep the link resolvable.
2102
- const fromMap = `${fromPath}.map`;
2103
- if (!existsSync(fromMap)) return;
2128
+ const fromMap = `${fromPath}.map`
2129
+ if (!existsSync(fromMap)) return
2104
2130
  try {
2105
- await rename(fromMap, `${toPath}.map`);
2131
+ await rename(fromMap, `${toPath}.map`)
2106
2132
  } catch (error) {
2107
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
2133
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
2108
2134
  }
2109
2135
  }),
2110
- );
2136
+ )
2111
2137
 
2112
2138
  // One read+rewrite per output file. An app with a route per page has hundreds
2113
2139
  // of them, so they go out concurrently rather than one await at a time.
2114
- const outputs = (await listFiles(outDir)).filter(item => item.endsWith('.js'));
2140
+ const outputs = (await listFiles(outDir)).filter(item => item.endsWith('.js'))
2115
2141
  await Promise.all(
2116
2142
  outputs.map(async file => {
2117
- const source = await readText(file);
2118
- const next = replaceChunkNames(source, renames);
2119
- if (next !== source) await writeText(file, next);
2143
+ const source = await readText(file)
2144
+ const next = replaceChunkNames(source, renames)
2145
+ if (next !== source) await writeText(file, next)
2120
2146
  }),
2121
- );
2147
+ )
2122
2148
 
2123
- return renames;
2149
+ return renames
2124
2150
  }
2125
2151
 
2126
2152
  function hashSuffix(file: string) {
2127
- return path.basename(file, '.js').split('-').at(-1) ?? 'chunk';
2153
+ return path.basename(file, '.js').split('-').at(-1) ?? 'chunk'
2128
2154
  }
2129
2155
 
2130
2156
  function chunkLabelsFromMetafile(metafile: Metafile) {
2131
- const labels = new Map<string, string>();
2157
+ const labels = new Map<string, string>()
2132
2158
  for (const [outputPath, output] of Object.entries(metafile.outputs)) {
2133
- const basename = path.basename(outputPath);
2134
- if (!basename.startsWith('chunk-') || !basename.endsWith('.js')) continue;
2135
- const weights = new Map<string, number>();
2159
+ const basename = path.basename(outputPath)
2160
+ if (!basename.startsWith('chunk-') || !basename.endsWith('.js')) continue
2161
+ const weights = new Map<string, number>()
2136
2162
  for (const [input, { bytesInOutput }] of Object.entries(output.inputs)) {
2137
- const label = chunkInputLabel(input);
2138
- if (!label) continue;
2139
- weights.set(label, (weights.get(label) ?? 0) + bytesInOutput);
2163
+ const label = chunkInputLabel(input)
2164
+ if (!label) continue
2165
+ weights.set(label, (weights.get(label) ?? 0) + bytesInOutput)
2140
2166
  }
2141
- const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0];
2142
- if (top) labels.set(basename, top[0]);
2167
+ const top = [...weights.entries()].sort((a, b) => b[1] - a[1])[0]
2168
+ if (top) labels.set(basename, top[0])
2143
2169
  }
2144
- return labels;
2170
+ return labels
2145
2171
  }
2146
2172
 
2147
2173
  function chunkInputLabel(input: string) {
2148
- const posix = input.split(path.sep).join('/');
2149
- const packageIndex = posix.lastIndexOf('node_modules/');
2174
+ const posix = input.split(path.sep).join('/')
2175
+ const packageIndex = posix.lastIndexOf('node_modules/')
2150
2176
  if (packageIndex !== -1) {
2151
- const parts = posix.slice(packageIndex + 'node_modules/'.length).split('/');
2152
- const name = parts[0]?.startsWith('@') ? `${parts[0]}-${parts[1] ?? ''}` : parts[0];
2153
- return sanitizeChunkLabel(name ?? '');
2177
+ const parts = posix.slice(packageIndex + 'node_modules/'.length).split('/')
2178
+ const name = parts[0]?.startsWith('@') ? `${parts[0]}-${parts[1] ?? ''}` : parts[0]
2179
+ return sanitizeChunkLabel(name ?? '')
2154
2180
  }
2155
2181
  return sanitizeChunkLabel(
2156
2182
  posix
2157
2183
  .split('/')
2158
2184
  .at(-1)
2159
2185
  ?.replace(/\.[^.]+$/, '') ?? '',
2160
- );
2186
+ )
2161
2187
  }
2162
2188
 
2163
2189
  function sanitizeChunkLabel(label: string) {
@@ -2165,11 +2191,11 @@ function sanitizeChunkLabel(label: string) {
2165
2191
  .replace(/^@/, '')
2166
2192
  .replace(/[^a-zA-Z0-9._-]+/g, '-')
2167
2193
  .replace(/^[-.]+|[-.]+$/g, '')
2168
- .slice(0, 40);
2169
- return clean || undefined;
2194
+ .slice(0, 40)
2195
+ return clean || undefined
2170
2196
  }
2171
2197
 
2172
2198
  function replaceChunkNames(source: string, renames: Map<string, string>) {
2173
- const pattern = new RegExp([...renames.keys()].map(escapeRegex).join('|'), 'g');
2174
- return source.replace(pattern, match => renames.get(match) ?? match);
2199
+ const pattern = new RegExp([...renames.keys()].map(escapeRegex).join('|'), 'g')
2200
+ return source.replace(pattern, match => renames.get(match) ?? match)
2175
2201
  }