@wular/pnext 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (371) hide show
  1. package/README.md +153 -0
  2. package/bin/pnext +67 -0
  3. package/config/lint/base.js +48 -0
  4. package/config/ts/base.json +26 -0
  5. package/config/ts/react.json +13 -0
  6. package/package.json +70 -0
  7. package/reference/compat.md +63 -0
  8. package/reference/config.md +120 -0
  9. package/reference/css.md +58 -0
  10. package/reference/dev.md +69 -0
  11. package/reference/env.md +40 -0
  12. package/reference/metadata.md +86 -0
  13. package/reference/navigation.md +149 -0
  14. package/reference/overview.md +35 -0
  15. package/reference/performance.md +97 -0
  16. package/reference/rendering.md +127 -0
  17. package/reference/routing.md +167 -0
  18. package/reference/typegen.md +64 -0
  19. package/src/api/cache.ts +80 -0
  20. package/src/api/client-cache.ts +9 -0
  21. package/src/api/client-navigation.ts +279 -0
  22. package/src/api/dynamic.tsx +102 -0
  23. package/src/api/link.tsx +119 -0
  24. package/src/api/navigation.ts +198 -0
  25. package/src/api/router/events.ts +53 -0
  26. package/src/api/router/history.ts +70 -0
  27. package/src/api/router/hub.ts +194 -0
  28. package/src/api/router/policies.ts +107 -0
  29. package/src/api/router/runtime.ts +5238 -0
  30. package/src/api/router/types.ts +299 -0
  31. package/src/api/router.ts +167 -0
  32. package/src/api/server.ts +323 -0
  33. package/src/api/suspense.ts +16 -0
  34. package/src/cache/context.ts +61 -0
  35. package/src/cli/adapters/vercel-warm.ts +375 -0
  36. package/src/cli/adapters/vercel.ts +1310 -0
  37. package/src/cli/analyze-print.ts +181 -0
  38. package/src/cli/analyze.ts +328 -0
  39. package/src/cli/boot-trace.ts +29 -0
  40. package/src/cli/build.ts +3114 -0
  41. package/src/cli/dev.ts +276 -0
  42. package/src/cli/index.ts +196 -0
  43. package/src/cli/named-bin.ts +119 -0
  44. package/src/cli/serve-ui.ts +160 -0
  45. package/src/cli/start.ts +1425 -0
  46. package/src/client/build.ts +2136 -0
  47. package/src/client/chunk-fold.ts +526 -0
  48. package/src/client/entry.ts +1525 -0
  49. package/src/client/paths.ts +22 -0
  50. package/src/client/prebuilt.ts +621 -0
  51. package/src/client/profile.ts +75 -0
  52. package/src/client/react-compiler.ts +94 -0
  53. package/src/client/reference-stub.ts +145 -0
  54. package/src/client/reference.ts +47 -0
  55. package/src/compat/actions/action-client.ts +531 -0
  56. package/src/compat/actions/action-dispatch.ts +676 -0
  57. package/src/compat/actions/action-router.ts +40 -0
  58. package/src/compat/actions/action-shared.ts +95 -0
  59. package/src/compat/actions/client-plugin.ts +135 -0
  60. package/src/compat/actions/client-stub.ts +65 -0
  61. package/src/compat/actions/config.ts +164 -0
  62. package/src/compat/actions/detect.ts +208 -0
  63. package/src/compat/actions/discovery.ts +244 -0
  64. package/src/compat/actions/early-submit.ts +40 -0
  65. package/src/compat/actions/endpoint.ts +602 -0
  66. package/src/compat/actions/flight.ts +52 -0
  67. package/src/compat/actions/form-state.ts +73 -0
  68. package/src/compat/actions/hoist.ts +485 -0
  69. package/src/compat/actions/ids.ts +39 -0
  70. package/src/compat/actions/index.ts +41 -0
  71. package/src/compat/actions/instances.ts +144 -0
  72. package/src/compat/actions/origin.ts +109 -0
  73. package/src/compat/actions/protocol.ts +125 -0
  74. package/src/compat/actions/registry.ts +74 -0
  75. package/src/compat/actions/rewrite.ts +282 -0
  76. package/src/compat/actions/serve.ts +414 -0
  77. package/src/compat/actions/server-tag.ts +21 -0
  78. package/src/compat/actions/unrecognized-error.ts +30 -0
  79. package/src/compat/adapter/build-complete.ts +257 -0
  80. package/src/compat/bundler/bun-externals.ts +53 -0
  81. package/src/compat/bundler/cjs-exports.ts +542 -0
  82. package/src/compat/bundler/config.ts +363 -0
  83. package/src/compat/bundler/externals.ts +34 -0
  84. package/src/compat/bundler/import-meta-url.ts +60 -0
  85. package/src/compat/bundler/modularize-imports.ts +119 -0
  86. package/src/compat/bundler/new-url-asset.ts +87 -0
  87. package/src/compat/bundler/optimize-package-imports.ts +273 -0
  88. package/src/compat/bundler/polyfill.ts +88 -0
  89. package/src/compat/bundler/react-compiler.ts +61 -0
  90. package/src/compat/bundler/react-profiler.tsx +25 -0
  91. package/src/compat/bundler/relay-transform.ts +116 -0
  92. package/src/compat/bundler/require-context.ts +281 -0
  93. package/src/compat/bundler/resolve-extensions.ts +75 -0
  94. package/src/compat/bundler/source-cache.ts +61 -0
  95. package/src/compat/bundler/static-imports.ts +25 -0
  96. package/src/compat/bundler/symlink-imports.ts +119 -0
  97. package/src/compat/bundler/tsconfig-paths.ts +50 -0
  98. package/src/compat/bundler/wasm.ts +153 -0
  99. package/src/compat/bundler/webpack-loaders.ts +685 -0
  100. package/src/compat/bundler/worker.ts +278 -0
  101. package/src/compat/cache/build-flags.ts +81 -0
  102. package/src/compat/cache/build-prerender-errors.ts +163 -0
  103. package/src/compat/cache/custom-handler.ts +159 -0
  104. package/src/compat/cache/fetch-patch.ts +745 -0
  105. package/src/compat/cache/handler.ts +100 -0
  106. package/src/compat/cache/modern-handler.ts +275 -0
  107. package/src/compat/cache/resume-data-cache.ts +143 -0
  108. package/src/compat/cache/revalidate.ts +759 -0
  109. package/src/compat/cache/runtime-error.ts +124 -0
  110. package/src/compat/cache/use-cache-transform.ts +961 -0
  111. package/src/compat/cache/use-cache.ts +1695 -0
  112. package/src/compat/cache-control.ts +269 -0
  113. package/src/compat/client/base-path.ts +64 -0
  114. package/src/compat/client/css-order.ts +36 -0
  115. package/src/compat/client/errors/control-flow.ts +92 -0
  116. package/src/compat/client/errors/error-boundary.ts +222 -0
  117. package/src/compat/client/errors/global-error.ts +238 -0
  118. package/src/compat/client/errors/install.ts +217 -0
  119. package/src/compat/client/errors/lazy.ts +53 -0
  120. package/src/compat/client/errors/primitive-throw.ts +126 -0
  121. package/src/compat/client/errors/soft-refresh.ts +14 -0
  122. package/src/compat/client/link-status.ts +86 -0
  123. package/src/compat/client/nav-compat-runtime.ts +57 -0
  124. package/src/compat/client/nav-compat.ts +42 -0
  125. package/src/compat/client/navigation-scroll.ts +154 -0
  126. package/src/compat/client/optimistic-routing.ts +206 -0
  127. package/src/compat/client/prefetch-cache.ts +111 -0
  128. package/src/compat/client/route-announcer.ts +72 -0
  129. package/src/compat/client/segment-cache-policy.ts +159 -0
  130. package/src/compat/client/segment-cache.ts +1077 -0
  131. package/src/compat/client/segment-prefetch.ts +375 -0
  132. package/src/compat/client/trailing-slash.ts +24 -0
  133. package/src/compat/css/chunking.ts +254 -0
  134. package/src/compat/css/inline-css.ts +73 -0
  135. package/src/compat/css/lightningcss.ts +90 -0
  136. package/src/compat/css/modules.ts +373 -0
  137. package/src/compat/css/nonce.ts +30 -0
  138. package/src/compat/css/sass-plugin.ts +65 -0
  139. package/src/compat/css/sass.ts +392 -0
  140. package/src/compat/css/styled-jsx-runtime.ts +80 -0
  141. package/src/compat/css/styled-jsx.ts +49 -0
  142. package/src/compat/edge-runtime.ts +71 -0
  143. package/src/compat/export/client.ts +112 -0
  144. package/src/compat/export/index.ts +272 -0
  145. package/src/compat/export/standalone.ts +207 -0
  146. package/src/compat/image-optimizer/cache.ts +119 -0
  147. package/src/compat/image-optimizer/detect.ts +143 -0
  148. package/src/compat/image-optimizer/index.ts +601 -0
  149. package/src/compat/image-optimizer/source.ts +243 -0
  150. package/src/compat/index.ts +458 -0
  151. package/src/compat/lifecycle/after-scope.ts +86 -0
  152. package/src/compat/lifecycle/after.ts +173 -0
  153. package/src/compat/lifecycle/error-funnel.ts +306 -0
  154. package/src/compat/lifecycle/error-serialize.ts +87 -0
  155. package/src/compat/lifecycle/error-ui.ts +167 -0
  156. package/src/compat/lifecycle/instrumentation-client.ts +138 -0
  157. package/src/compat/lifecycle/instrumentation.ts +277 -0
  158. package/src/compat/lifecycle/node-console.ts +19 -0
  159. package/src/compat/lifecycle/testmode.ts +263 -0
  160. package/src/compat/mdx/compile.ts +219 -0
  161. package/src/compat/mdx/next-mdx-stub.ts +46 -0
  162. package/src/compat/mdx/plugin.ts +37 -0
  163. package/src/compat/metadata-route-artifacts.ts +458 -0
  164. package/src/compat/metadata.ts +295 -0
  165. package/src/compat/middleware/manifest.ts +210 -0
  166. package/src/compat/misc/action-return.ts +173 -0
  167. package/src/compat/next/cache.ts +211 -0
  168. package/src/compat/next/canonical-url.ts +35 -0
  169. package/src/compat/next/client-cache.ts +57 -0
  170. package/src/compat/next/client-navigation.ts +313 -0
  171. package/src/compat/next/client-only.ts +3 -0
  172. package/src/compat/next/client-script.tsx +215 -0
  173. package/src/compat/next/client-server.ts +39 -0
  174. package/src/compat/next/config-loader.ts +569 -0
  175. package/src/compat/next/config.ts +29 -0
  176. package/src/compat/next/constants.cjs +6 -0
  177. package/src/compat/next/constants.ts +6 -0
  178. package/src/compat/next/custom-server.ts +236 -0
  179. package/src/compat/next/dist/client/components/app-router-headers.ts +32 -0
  180. package/src/compat/next/dist/server/app-render/work-unit-async-storage.external.cjs +38 -0
  181. package/src/compat/next/dist/server/web/spec-extension/revalidate.ts +1 -0
  182. package/src/compat/next/dist/server/web/spec-extension/unstable-cache.ts +1 -0
  183. package/src/compat/next/dist/server/web/spec-extension/unstable-no-store.ts +1 -0
  184. package/src/compat/next/dynamic.tsx +46 -0
  185. package/src/compat/next/error.tsx +148 -0
  186. package/src/compat/next/font/cache.ts +171 -0
  187. package/src/compat/next/font/google.ts +2 -0
  188. package/src/compat/next/font/index.ts +8 -0
  189. package/src/compat/next/font/local.ts +5 -0
  190. package/src/compat/next/font/runtime-client.ts +71 -0
  191. package/src/compat/next/font/runtime.ts +974 -0
  192. package/src/compat/next/font/shared.ts +281 -0
  193. package/src/compat/next/form.tsx +156 -0
  194. package/src/compat/next/head.tsx +10 -0
  195. package/src/compat/next/headers.ts +247 -0
  196. package/src/compat/next/image/config.ts +196 -0
  197. package/src/compat/next/image/optimizer.ts +96 -0
  198. package/src/compat/next/image/patterns.ts +103 -0
  199. package/src/compat/next/image/shared.ts +141 -0
  200. package/src/compat/next/image/static-metadata.ts +283 -0
  201. package/src/compat/next/image/validate.ts +269 -0
  202. package/src/compat/next/image-client.tsx +215 -0
  203. package/src/compat/next/image-props.ts +575 -0
  204. package/src/compat/next/image-usage.ts +102 -0
  205. package/src/compat/next/image.tsx +56 -0
  206. package/src/compat/next/index.ts +1 -0
  207. package/src/compat/next/legacy-image.tsx +97 -0
  208. package/src/compat/next/link-usage.ts +29 -0
  209. package/src/compat/next/link-validation-transform.ts +200 -0
  210. package/src/compat/next/link.tsx +466 -0
  211. package/src/compat/next/navigation.cjs +21 -0
  212. package/src/compat/next/navigation.ts +188 -0
  213. package/src/compat/next/offline.ts +51 -0
  214. package/src/compat/next/og.ts +324 -0
  215. package/src/compat/next/optimistic-route-state.ts +188 -0
  216. package/src/compat/next/preferred-region.ts +39 -0
  217. package/src/compat/next/redirects.ts +131 -0
  218. package/src/compat/next/resource-hints.ts +136 -0
  219. package/src/compat/next/rewrites.ts +350 -0
  220. package/src/compat/next/root-params.ts +142 -0
  221. package/src/compat/next/router.cjs +49 -0
  222. package/src/compat/next/router.ts +143 -0
  223. package/src/compat/next/script.tsx +355 -0
  224. package/src/compat/next/server-only.ts +3 -0
  225. package/src/compat/next/server.ts +28 -0
  226. package/src/compat/next/svgr.ts +58 -0
  227. package/src/compat/next/telemetry.ts +77 -0
  228. package/src/compat/next/user-agent.ts +100 -0
  229. package/src/compat/next/web-vitals.ts +56 -0
  230. package/src/compat/otel/api.ts +95 -0
  231. package/src/compat/otel/client-trace-metadata.ts +71 -0
  232. package/src/compat/otel/fetch-span.ts +77 -0
  233. package/src/compat/otel/tracer.ts +944 -0
  234. package/src/compat/pages/client-plugin.ts +108 -0
  235. package/src/compat/pages/index.ts +527 -0
  236. package/src/compat/pages/router-state.ts +94 -0
  237. package/src/compat/ppr/io.ts +38 -0
  238. package/src/compat/ppr/missing-root-params.ts +105 -0
  239. package/src/compat/ppr/root-params-scan.ts +164 -0
  240. package/src/compat/ppr/root-params-transform.ts +75 -0
  241. package/src/compat/ppr/root-params.ts +129 -0
  242. package/src/compat/ppr/segment-config-incompat.ts +34 -0
  243. package/src/compat/protocol.ts +202 -0
  244. package/src/compat/react/client.ts +59 -0
  245. package/src/compat/react/compiler-runtime.ts +60 -0
  246. package/src/compat/react/dom-client.ts +115 -0
  247. package/src/compat/react/dom-react-server.ts +20 -0
  248. package/src/compat/react/dom-server.ts +40 -0
  249. package/src/compat/react/dom.ts +154 -0
  250. package/src/compat/react/preact.ts +522 -0
  251. package/src/compat/react/react-server.ts +84 -0
  252. package/src/compat/react/router-shim.ts +26 -0
  253. package/src/compat/react/server-component-use.ts +48 -0
  254. package/src/compat/react/server-inserted-html.ts +87 -0
  255. package/src/compat/react/server.ts +156 -0
  256. package/src/compat/react/view-transition.ts +60 -0
  257. package/src/compat/register/actions.ts +875 -0
  258. package/src/compat/register/boot.ts +141 -0
  259. package/src/compat/register/build-tier.ts +11 -0
  260. package/src/compat/register/build.ts +182 -0
  261. package/src/compat/register/bundler.ts +587 -0
  262. package/src/compat/register/cache.ts +85 -0
  263. package/src/compat/register/client-errors.ts +18 -0
  264. package/src/compat/register/config.ts +17 -0
  265. package/src/compat/register/css-extras.ts +120 -0
  266. package/src/compat/register/edge-runtime.ts +6 -0
  267. package/src/compat/register/errors.ts +46 -0
  268. package/src/compat/register/export.ts +23 -0
  269. package/src/compat/register/font.ts +36 -0
  270. package/src/compat/register/hooks.ts +34 -0
  271. package/src/compat/register/image.ts +133 -0
  272. package/src/compat/register/index.ts +111 -0
  273. package/src/compat/register/instrumentation-client.ts +35 -0
  274. package/src/compat/register/lifecycle.ts +86 -0
  275. package/src/compat/register/mdx.ts +48 -0
  276. package/src/compat/register/middleware.ts +36 -0
  277. package/src/compat/register/misc.ts +44 -0
  278. package/src/compat/register/otel.ts +288 -0
  279. package/src/compat/register/pages-api.ts +473 -0
  280. package/src/compat/register/ppr.ts +56 -0
  281. package/src/compat/register/protocol.ts +57 -0
  282. package/src/compat/register/proxy.ts +127 -0
  283. package/src/compat/register/render.ts +268 -0
  284. package/src/compat/register/routing.ts +410 -0
  285. package/src/compat/register/segment.ts +1903 -0
  286. package/src/compat/register/static-image.ts +21 -0
  287. package/src/compat/register/typed-routes.ts +35 -0
  288. package/src/compat/register/usecache.ts +131 -0
  289. package/src/compat/register/validation.ts +56 -0
  290. package/src/compat/segment/loading-boundary.ts +113 -0
  291. package/src/compat/segment/page-slot.ts +200 -0
  292. package/src/compat/segment/tree.ts +481 -0
  293. package/src/compat/segment/vary-key.ts +102 -0
  294. package/src/compat/segment/vary-params.ts +551 -0
  295. package/src/compat/static-params.ts +33 -0
  296. package/src/compat/tsconfig-defaults.ts +301 -0
  297. package/src/compat/typecheck/index.ts +1481 -0
  298. package/src/compat/typecheck/worker.ts +26 -0
  299. package/src/compat/typed-routes/index.ts +92 -0
  300. package/src/compat/typed-routes/manifest.ts +356 -0
  301. package/src/compat/typed-routes/typegen.ts +566 -0
  302. package/src/compat/validation/errors.ts +159 -0
  303. package/src/compat/validation/index.ts +1770 -0
  304. package/src/compat/validation/prerender-diagnostics.ts +1508 -0
  305. package/src/compat-bootstrap.ts +67 -0
  306. package/src/config.ts +218 -0
  307. package/src/css/build.ts +697 -0
  308. package/src/css/index.ts +2 -0
  309. package/src/css/postcss.ts +236 -0
  310. package/src/css/worker.ts +34 -0
  311. package/src/dev/client-actions.ts +35 -0
  312. package/src/dev/client-chunk-store.ts +92 -0
  313. package/src/dev/client-key-cache.ts +178 -0
  314. package/src/dev/global-css-cache.ts +212 -0
  315. package/src/dev/imports.ts +2430 -0
  316. package/src/dev/module-cache.ts +721 -0
  317. package/src/dev/module-generations.ts +38 -0
  318. package/src/dev/module-transform.ts +188 -0
  319. package/src/dev/node-module-bundle-cache.ts +63 -0
  320. package/src/dev/restart-cache.ts +10 -0
  321. package/src/dev/route-bundle-key-cache.ts +154 -0
  322. package/src/dev/route-facts-cache.ts +223 -0
  323. package/src/dev/server.ts +1710 -0
  324. package/src/dynamic/source.ts +307 -0
  325. package/src/dynamic/tree-shake.ts +262 -0
  326. package/src/env.ts +92 -0
  327. package/src/extensions.ts +1898 -0
  328. package/src/index.ts +34 -0
  329. package/src/internal.ts +43 -0
  330. package/src/islands/boundary-error.ts +8 -0
  331. package/src/islands/static-children.ts +37 -0
  332. package/src/islands/static-slots.ts +106 -0
  333. package/src/ppr-postpone.ts +24 -0
  334. package/src/ppr.ts +784 -0
  335. package/src/proxy.ts +752 -0
  336. package/src/render/hooks.ts +384 -0
  337. package/src/render/index.ts +1 -0
  338. package/src/render/island-context.ts +47 -0
  339. package/src/render/metadata.ts +857 -0
  340. package/src/render/renderer.ts +7391 -0
  341. package/src/render/resource-hints.ts +44 -0
  342. package/src/render/slots.tsx +679 -0
  343. package/src/request/context.ts +396 -0
  344. package/src/resolve/engine.ts +219 -0
  345. package/src/resolve/imports.ts +1104 -0
  346. package/src/resolve/scan-facts.ts +474 -0
  347. package/src/resolve/source-text.ts +86 -0
  348. package/src/routing/forwarded.ts +41 -0
  349. package/src/routing/handler.ts +271 -0
  350. package/src/routing/href.ts +203 -0
  351. package/src/routing/metadata.ts +1018 -0
  352. package/src/routing/request-runtime.ts +43 -0
  353. package/src/routing/routes.ts +2560 -0
  354. package/src/routing/slots.ts +432 -0
  355. package/src/runtime/server.ts +3453 -0
  356. package/src/runtime/vendor.ts +1160 -0
  357. package/src/style-modules.d.ts +9 -0
  358. package/src/typegen.ts +151 -0
  359. package/src/types.ts +725 -0
  360. package/src/utils/ansi.ts +9 -0
  361. package/src/utils/content-type.ts +31 -0
  362. package/src/utils/decode.ts +7 -0
  363. package/src/utils/dev-profile.ts +31 -0
  364. package/src/utils/error-log.ts +29 -0
  365. package/src/utils/fs-cache.ts +31 -0
  366. package/src/utils/fs.ts +119 -0
  367. package/src/utils/html.ts +46 -0
  368. package/src/utils/serialize.ts +378 -0
  369. package/src/utils/source.ts +35 -0
  370. package/src/utils/verbose.ts +39 -0
  371. package/tsconfig.json +10 -0
@@ -0,0 +1,3453 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { builtinModules, createRequire } from 'node:module';
3
+ import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
4
+ import { copyFile, mkdir, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { build, transform, type Loader, type Metafile, type OnLoadResult, type OnResolveResult, type Plugin } from 'esbuild';
8
+ import {
9
+ applyBundledSourceTransforms,
10
+ applyServerSourcePreTransforms,
11
+ applyServerSourceTransforms,
12
+ extraLoadableExtensions,
13
+ getAssetExtensions,
14
+ getBundlerExtensions,
15
+ getCssExtensions,
16
+ getImportAliasExtensions,
17
+ onLoadableExtensionsChanged,
18
+ serverDefineOptions,
19
+ sourceNeedsServerTransforms,
20
+ type ServerEsbuildPluginOptions,
21
+ } from '../extensions';
22
+ import {
23
+ frameworkRuntimeAliasEntries,
24
+ pathToFileHref,
25
+ pnextAliases,
26
+ type CompatAliasTarget,
27
+ type ResolvedConfig,
28
+ } from '../config';
29
+ import {
30
+ rewriteDynamicCallTargets,
31
+ rewriteLiteralDynamicCalls,
32
+ sourceHasDynamicImport,
33
+ } from '../dynamic/source';
34
+ import { cacheRoot, devArtifactUsable, noteDevArtifactWritten } from '../dev/module-cache';
35
+ import { readNodeModuleBundle, writeNodeModuleBundle } from '../dev/node-module-bundle-cache';
36
+ import { setBasePathPrefix, setTrailingSlashUrls } from '../routing/href';
37
+ import {
38
+ getExternalPackagePolicy,
39
+ isEsmModuleEntry,
40
+ isEsmModuleFile,
41
+ isPackageSubpathUnexported,
42
+ resolveExternalLoadTarget,
43
+ resolveImport,
44
+ resolveLinkedPackageSpecifier,
45
+ resolveNestedPackageFromImporter,
46
+ resolveVendorPackageSpecifier,
47
+ clearVendorPackageResolutions,
48
+ clearProvidedEntryResolutions,
49
+ provideEntryResolution,
50
+ workspacePackageRoots,
51
+ } from '../resolve/imports';
52
+ import { clearNodeModulesResolutionCache } from '../resolve/engine';
53
+ import { cachedExistsSync, clearNodeModulesFsCache } from '../utils/fs-cache';
54
+ import {
55
+ clientReferenceExportNames,
56
+ clientReferenceModuleSource,
57
+ hasUseClientDirective,
58
+ } from '../client/reference-stub';
59
+ import { reactCompatEnabled } from '../render/hooks';
60
+ import { escapeRegex } from '../utils/source';
61
+ import { writeFileAtomic } from '../utils/fs';
62
+ import {
63
+ addCommonJsNamedExports,
64
+ canonicalVendorCode,
65
+ noteVendorContentId,
66
+ vendorContentId,
67
+ cjsEntryMayHaveNamedExports,
68
+ cjsNamedExportFacade,
69
+ heavyProfRow,
70
+ clearVendorPipeline,
71
+ copyBrowserReadyEsmDist,
72
+ dropVendorBundle,
73
+ dropVendorGroup,
74
+ nextVendorTraceSeq,
75
+ outsideVendorSlot,
76
+ trackPreplanBuild,
77
+ rewriteEmittedRefs,
78
+ vendorBundle,
79
+ vendorBundleMemHit,
80
+ vendorTraceEnabled,
81
+ vendorTraceRow,
82
+ verifyVendorArtifact,
83
+ type VendorBuildPlan,
84
+ type VendorGroupMember,
85
+ type VendorGroupPlan,
86
+ } from './vendor';
87
+
88
+ const require = createRequire(import.meta.url);
89
+
90
+ // B4/B5: extra esbuild resolve conditions layered onto the server (RSC) vendor
91
+ // bundle. Core always applies `react-server` (the server graph IS the RSC
92
+ // layer). Compat can add `next-js` (only when cacheComponents is on) via the
93
+ // setter. Kept as a module-level seam so core carries no static edge into
94
+ // compat's config reader.
95
+ export type ServerBundleTarget =
96
+ | CompatAliasTarget
97
+ | 'edge'
98
+ | 'pages-api'
99
+ | 'pages-api-edge'
100
+ | 'pages'
101
+ | 'pages-edge';
102
+
103
+ let extraServerBundleConditions: (target: ServerBundleTarget) => string[] = () => [];
104
+
105
+ /** Install compat-driven extra vendor-bundle conditions (B5 `next-js`). */
106
+ export function setServerBundleConditions(
107
+ factory: ((target: ServerBundleTarget) => string[]) | undefined,
108
+ ): void {
109
+ extraServerBundleConditions = factory ?? (() => []);
110
+ }
111
+
112
+ export function serverBundleTargetForRuntime(runtime: string | undefined): ServerBundleTarget {
113
+ return runtime === 'edge' || runtime === 'experimental-edge' ? 'edge' : 'server';
114
+ }
115
+
116
+ export function pagesApiBundleTargetForRuntime(runtime: string | undefined): ServerBundleTarget {
117
+ return runtime === 'edge' || runtime === 'experimental-edge' ? 'pages-api-edge' : 'pages-api';
118
+ }
119
+
120
+ /**
121
+ * Pages-router PAGE SSR layer: Next compiles it in the server bundle with `node` (or
122
+ * `edge-light`/`browser` for edge) export conditions but WITHOUT `react-server` - a pages page is
123
+ * not part of the RSC layer, so `library/react`-style react-server splits must resolve to `default`.
124
+ */
125
+ export function pagesBundleTargetForRuntime(runtime: string | undefined): ServerBundleTarget {
126
+ return runtime === 'edge' || runtime === 'experimental-edge' ? 'pages-edge' : 'pages';
127
+ }
128
+
129
+ function serverBundleConditions(target: ServerBundleTarget): string[] {
130
+ // Preserve the original client-target conditions (module/import) to avoid
131
+ // regressing the browser SSR vendor bundle; only the RSC/server layer gains
132
+ // `react-server`. Compat-added conditions (next-js) prepend for either target.
133
+ const base =
134
+ target === 'client'
135
+ ? ['module', 'import']
136
+ : target === 'edge'
137
+ ? ['react-server', 'edge-light', 'browser', 'module', 'import']
138
+ : target === 'pages-api-edge'
139
+ ? ['edge-light', 'browser', 'module', 'import']
140
+ : target === 'pages-api'
141
+ ? ['node', 'require', 'import']
142
+ : target === 'pages-edge'
143
+ ? ['edge-light', 'browser', 'module', 'import']
144
+ : target === 'pages'
145
+ ? ['node', 'module', 'import']
146
+ : ['react-server', 'node', 'module', 'import'];
147
+ return [...new Set([...extraServerBundleConditions(target), ...base])];
148
+ }
149
+
150
+ export function serverBundleRequireConditions(target: ServerBundleTarget): string[] {
151
+ const base =
152
+ target === 'client'
153
+ ? ['module', 'require']
154
+ : target === 'edge'
155
+ ? ['react-server', 'edge-light', 'browser', 'module', 'require']
156
+ : target === 'pages-api-edge'
157
+ ? ['edge-light', 'browser', 'module', 'require']
158
+ : target === 'pages-api'
159
+ ? ['node', 'require']
160
+ : target === 'pages-edge'
161
+ ? ['edge-light', 'browser', 'module', 'require']
162
+ : target === 'pages'
163
+ ? ['node', 'module', 'require']
164
+ : ['react-server', 'node', 'module', 'require'];
165
+ return [...new Set([...extraServerBundleConditions(target), ...base])];
166
+ }
167
+
168
+ const runtimeConfigs = new Map<string, RuntimeConfig>();
169
+ const aliasCache = new WeakMap<ResolvedConfig, Map<string, string>>();
170
+ const packageRootCache = new WeakMap<ResolvedConfig, string[]>();
171
+ const registeredSignatures = new WeakMap<ResolvedConfig, Set<string>>();
172
+ const rootPathCache = new Map<string, string[]>();
173
+ const runtimeConfigLookupCache = new Map<string, RuntimeConfig | null>();
174
+ const firstAliasCache = new Map<string, string | null>();
175
+ const transformCache = new Map<string, TransformCacheEntry>();
176
+ const nodeModuleTransformCache = new Map<string, TransformCacheEntry>();
177
+ // Freshness of generated `.pnext-require.cjs` sidecars (see inlineModuleScopeRequires).
178
+ const requireSidecarCache = new Map<string, { mtimeMs: number; size: number }>();
179
+ const registeredLoadRoots = new Set<string>();
180
+ let resolveRegistered = false;
181
+ let nodeModulesLoadRegistered = false;
182
+ // A live Bun.plugin forces every later import through the plugin pipeline (and
183
+ // pnext's own src through transformSource), so importing the compat graph with
184
+ // the plugins already up costs ~5x. build/start load compat before they ever
185
+ // register a runtime; dev arms the plugins on the first request instead.
186
+ let pluginsDeferred = false;
187
+ const deferredPluginRoots = new Set<string>();
188
+
189
+ const packageJsxLoaders = {
190
+ '.js': 'jsx',
191
+ '.mjs': 'jsx',
192
+ '.cjs': 'jsx',
193
+ } satisfies Record<string, Loader>;
194
+
195
+ interface RuntimeConfig {
196
+ root: string;
197
+ roots: Set<string>;
198
+ aliases: Map<string, string>;
199
+ /** Full resolved config, so compile sites can invoke config-aware extensions. */
200
+ resolved: ResolvedConfig;
201
+ missingImportError: (specifier: string) => string | undefined;
202
+ }
203
+
204
+ interface TransformCacheEntry {
205
+ mtimeMs: number;
206
+ size: number;
207
+ code: string;
208
+ }
209
+
210
+ export function registerServerRuntime(config: ResolvedConfig, sourceFiles: string[] = []) {
211
+ // Server-rendered Links emit canonical (slashed) hrefs under trailingSlash.
212
+ setTrailingSlashUrls(Boolean(config.trailingSlash));
213
+ // File-convention metadata asset hrefs (og-image, manifest) carry the
214
+ // basePath prefix; core render reads it through the href seam.
215
+ setBasePathPrefix(typeof config.basePath === 'string' ? config.basePath : '');
216
+ if (typeof Bun === 'undefined') return;
217
+ const sourceRoots = [...new Set(sourceFiles.map(file => sourceRootForFile(config, file)))].sort();
218
+ const signature = sourceRoots.join('\0');
219
+ let registered = registeredSignatures.get(config);
220
+ if (!registered) {
221
+ registered = new Set();
222
+ registeredSignatures.set(config, registered);
223
+ }
224
+ if (registered.has(signature)) return;
225
+ registered.add(signature);
226
+
227
+ const aliases = aliasesForConfig(config);
228
+ const roots = [
229
+ ...rootPaths(config.appPath),
230
+ ...rootPaths(path.join(config.outPath, 'cache', 'server')),
231
+ // The pnext src root, so the framework's own source transforms too.
232
+ ...rootPaths(path.join(import.meta.dirname, '..')),
233
+ ...sourceRoots.flatMap(rootPaths),
234
+ ];
235
+ const key = rootPaths(config.appPath).join('\0');
236
+ const missingImportError = (specifier: string) =>
237
+ getImportAliasExtensions().missingImportError(config, specifier);
238
+ const existing = runtimeConfigs.get(key);
239
+ if (existing) {
240
+ for (const root of roots) existing.roots.add(root);
241
+ for (const [specifier, target] of aliases) existing.aliases.set(specifier, target);
242
+ existing.resolved = config;
243
+ existing.missingImportError = missingImportError;
244
+ } else {
245
+ runtimeConfigs.set(key, {
246
+ root: config.root,
247
+ roots: new Set(roots),
248
+ aliases: new Map(aliases),
249
+ resolved: config,
250
+ missingImportError,
251
+ });
252
+ }
253
+ runtimeConfigLookupCache.clear();
254
+ firstAliasCache.clear();
255
+ if (pluginsDeferred) {
256
+ for (const root of roots) deferredPluginRoots.add(root);
257
+ return;
258
+ }
259
+ registerResolvePlugin();
260
+ registerNodeModulesLoadPlugin();
261
+ for (const root of roots) registerLoadPlugin(root);
262
+ }
263
+
264
+ /**
265
+ * Hold the Bun module plugins back until installServerRuntimePlugins() runs, so
266
+ * whatever the process still has to import of its own code loads natively.
267
+ */
268
+ export function deferServerRuntimePlugins(): void {
269
+ pluginsDeferred = true;
270
+ }
271
+
272
+ /** Arm the plugins deferServerRuntimePlugins() held back. Idempotent, cheap. */
273
+ export function installServerRuntimePlugins(): void {
274
+ if (!pluginsDeferred) return;
275
+ pluginsDeferred = false;
276
+ registerResolvePlugin();
277
+ registerNodeModulesLoadPlugin();
278
+ for (const root of deferredPluginRoots) registerLoadPlugin(root);
279
+ deferredPluginRoots.clear();
280
+ }
281
+
282
+ export function clearServerRuntimeCaches() {
283
+ clearVendorPipeline();
284
+ clearVendorNativeCaches();
285
+ vendorEntryResolutions.clear();
286
+ clearVendorPackageResolutions();
287
+ vendorLayerRecords.clear();
288
+ vendorSharedEntries.clear();
289
+ vendorSharedLoaded.clear();
290
+ vendorReuseVerdicts.clear();
291
+ vendorReuseLoaded.clear();
292
+ vendorReuseProbes.clear();
293
+ vendorLayerProbeMemo.clear();
294
+ // The marker memo tracks files under `.pnext`; a wipe takes them with it.
295
+ esmDistPackages.clear();
296
+ clearProvidedEntryResolutions();
297
+ clearNodeModulesResolutionCache();
298
+ clearNodeModulesFsCache();
299
+ }
300
+
301
+ function resolveRuntimeTarget(target: string) {
302
+ return path.isAbsolute(target) ? target : require.resolve(target);
303
+ }
304
+
305
+ function aliasesForConfig(config: ResolvedConfig) {
306
+ let aliases = aliasCache.get(config);
307
+ if (!aliases) {
308
+ aliases = new Map<string, string>();
309
+ for (const [specifier, target] of Object.entries(coreAliases(config, 'server'))) {
310
+ aliases.set(specifier, resolveRuntimeTarget(target));
311
+ }
312
+ aliasCache.set(config, aliases);
313
+ }
314
+ return aliases;
315
+ }
316
+
317
+ function coreAliases(config: ResolvedConfig, target: CompatAliasTarget): Record<string, string> {
318
+ return {
319
+ ...frameworkRuntimeAliasEntries(),
320
+ ...pnextAliases(target),
321
+ ...getImportAliasExtensions().aliases(config, target),
322
+ };
323
+ }
324
+
325
+ const aliasSpecifierFilter =
326
+ /^(?:@wular\/pnext\/cache|preact(?:\/|$)|react(?:-dom)?(?:\/|$)|react-compiler-runtime$|next(?:\/|$)|server-only$|client-only$|@vercel\/og$)/;
327
+
328
+ function registerResolvePlugin() {
329
+ if (resolveRegistered) return;
330
+ resolveRegistered = true;
331
+ Bun.plugin({
332
+ name: 'pnext-server-runtime-resolve',
333
+ setup(plugin) {
334
+ plugin.onResolve({ filter: aliasSpecifierFilter }, args => {
335
+ const runtime = runtimeConfigForFile(args.importer);
336
+ const resolved = runtime?.aliases.get(args.path) ?? firstAliasForSpecifier(args.path);
337
+ const target = resolved ? serverRequireAlias(args.path, resolved, args.kind) : undefined;
338
+ if (target) return { path: target };
339
+ const message = (runtime ?? firstRuntimeConfig())?.missingImportError(args.path);
340
+ if (message) throw new Error(message);
341
+ return undefined;
342
+ });
343
+ plugin.onResolve({ filter: /^[^./].*/ }, ({ path: specifier, importer }) => {
344
+ const runtime = runtimeConfigForFile(importer);
345
+ const target = runtime && resolveExternalLoadTarget({
346
+ root: runtime.root,
347
+ fromFile: importer,
348
+ specifier,
349
+ target: 'server',
350
+ });
351
+ if (!target) return undefined;
352
+ return { path: target };
353
+ });
354
+ },
355
+ });
356
+ }
357
+
358
+ function registerLoadPlugin(root: string) {
359
+ if (registeredLoadRoots.has(root)) return;
360
+ registeredLoadRoots.add(root);
361
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
362
+ if (process.env.PNEXT_DEBUG_LOADPLUGIN) {
363
+ console.error(`[loadplugin] register root=${root} extras=${currentLoadExtras().join(',')}`);
364
+ }
365
+ loadPluginExtras.set(root, currentLoadExtras());
366
+ Bun.plugin({
367
+ name: `pnext-server-runtime-load-${hashRoot(root)}`,
368
+ setup(plugin) {
369
+ plugin.onLoad({ filter: rootFilter(root) }, async ({ path: file }) => ({
370
+ contents: await transformSource(file),
371
+ loader: 'js',
372
+ }));
373
+ },
374
+ });
375
+ }
376
+
377
+ // The load plugin's filter regex latches the page extensions known at registration time. A root registered
378
+ // BEFORE compat adds `mdx`/`md` (boot order shifts under load) would let those modules fall through to Bun's
379
+ // file loader, whose default export is the file path and renders as a bogus JSX tag - so a late registration
380
+ // extends every latched root with an extras-only plugin.
381
+ const loadPluginExtras = new Map<string, string[]>();
382
+
383
+ function currentLoadExtras(): string[] {
384
+ return extraLoadableExtensions().filter(ext => /^[a-z0-9]+$/i.test(ext));
385
+ }
386
+
387
+ onLoadableExtensionsChanged(() => {
388
+ for (const [root, seen] of loadPluginExtras) {
389
+ const fresh = currentLoadExtras().filter(ext => !seen.includes(ext));
390
+ if (fresh.length === 0) continue;
391
+ loadPluginExtras.set(root, [...seen, ...fresh]);
392
+ const exts = fresh.map(escapeRegex).join('|');
393
+ const filter = new RegExp(
394
+ `^${escapeRegex(root)}(?!.*\\/node_modules\\/)(?:/.*)?\\.(?:${exts})$`,
395
+ );
396
+ Bun.plugin({
397
+ name: `pnext-server-runtime-load-extras-${hashRoot(root)}-${fresh.join('-')}`,
398
+ setup(plugin) {
399
+ plugin.onLoad({ filter }, async ({ path: file }) => ({
400
+ contents: await transformSource(file),
401
+ loader: 'js',
402
+ }));
403
+ },
404
+ });
405
+ }
406
+ });
407
+
408
+ function registerNodeModulesLoadPlugin() {
409
+ if (nodeModulesLoadRegistered) return;
410
+ nodeModulesLoadRegistered = true;
411
+ Bun.plugin({
412
+ name: 'pnext-server-node-modules-load',
413
+ setup(plugin) {
414
+ plugin.onLoad(
415
+ {
416
+ filter:
417
+ /\/node_modules\/.*(?:\.mjs|\.esm\.js|\/(?:es|esm|dist\/esm|build\/modern)\/.*\.js)$/,
418
+ },
419
+ async ({ path: file }) => {
420
+ const code = await transformNodeModuleSource(file);
421
+ return { contents: code, loader: 'js' };
422
+ },
423
+ );
424
+ },
425
+ });
426
+ }
427
+
428
+ function runtimeConfigForFile(file: string | undefined) {
429
+ if (!file) return undefined;
430
+ const key = path.resolve(file);
431
+ if (runtimeConfigLookupCache.has(key)) {
432
+ return runtimeConfigLookupCache.get(key) ?? undefined;
433
+ }
434
+ for (const config of runtimeConfigs.values()) {
435
+ for (const root of config.roots) {
436
+ if (isInside(root, file)) {
437
+ runtimeConfigLookupCache.set(key, config);
438
+ return config;
439
+ }
440
+ }
441
+ }
442
+ runtimeConfigLookupCache.set(key, null);
443
+ return undefined;
444
+ }
445
+
446
+ function sourceRootForFile(config: ResolvedConfig, file: string) {
447
+ const dir = path.dirname(file);
448
+ if (isInside(config.appPath, dir)) return config.appPath;
449
+ return packageRootsForConfig(config).find(root => isInside(root, dir)) ?? dir;
450
+ }
451
+
452
+ function packageRootsForConfig(config: ResolvedConfig) {
453
+ let roots = packageRootCache.get(config);
454
+ if (!roots) {
455
+ roots = workspacePackageRoots(config.workspaceRoot);
456
+ packageRootCache.set(config, roots);
457
+ }
458
+ return roots;
459
+ }
460
+
461
+ function firstRuntimeConfig() {
462
+ for (const config of runtimeConfigs.values()) return config;
463
+ return undefined;
464
+ }
465
+
466
+ function firstAliasForSpecifier(specifier: string) {
467
+ if (firstAliasCache.has(specifier)) return firstAliasCache.get(specifier) ?? undefined;
468
+ for (const config of runtimeConfigs.values()) {
469
+ const target = config.aliases.get(specifier);
470
+ if (target) {
471
+ firstAliasCache.set(specifier, target);
472
+ return target;
473
+ }
474
+ }
475
+ firstAliasCache.set(specifier, null);
476
+ return undefined;
477
+ }
478
+
479
+ async function transformSource(file: string) {
480
+ const fileStat = await stat(file);
481
+ const cached = transformCache.get(file);
482
+ if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
483
+ return cached.code;
484
+ }
485
+ // The in-memory cache is empty on every restart, so a restarted server
486
+ // re-transforms the whole materialized module set on the request path.
487
+ const diskCache = transformCacheFile(file, fileStat);
488
+ if (diskCache) {
489
+ const code = await readFile(diskCache, 'utf8').catch(() => undefined);
490
+ if (code !== undefined) {
491
+ transformCache.set(file, { mtimeMs: fileStat.mtimeMs, size: fileStat.size, code });
492
+ return code;
493
+ }
494
+ }
495
+
496
+ const runtime = runtimeConfigForFile(file);
497
+ let raw = await readFile(file, 'utf8');
498
+ // Raw markdown compiles through the MDX pipeline first; materialized
499
+ // cache/server copies were already compiled in place (dev/imports) and would
500
+ // be corrupted by a second pass.
501
+ if (/\.mdx?$/.test(file) && !file.includes(`${path.sep}cache${path.sep}server${path.sep}`)) {
502
+ const { compileMdx } = await import('../compat/mdx/compile');
503
+ raw = (await compileMdx(raw, file, runtime?.root ?? path.dirname(file))).code;
504
+ }
505
+ const source = await inlineModuleScopeRequires(
506
+ rewriteServerSource(raw, file, {
507
+ root: runtime?.root,
508
+ }),
509
+ file,
510
+ runtime?.root,
511
+ );
512
+ const result = await transform(source, {
513
+ loader: esbuildLoader(file),
514
+ jsx: 'automatic',
515
+ jsxImportSource: 'preact',
516
+ sourcefile: file,
517
+ target: 'es2022',
518
+ ...serverDefineOptions(),
519
+ });
520
+ transformCache.set(file, {
521
+ mtimeMs: fileStat.mtimeMs,
522
+ size: fileStat.size,
523
+ code: result.code,
524
+ });
525
+ if (diskCache) {
526
+ await mkdir(path.dirname(diskCache), { recursive: true }).catch(() => undefined);
527
+ void writeFileAtomic(diskCache, result.code).catch(() => undefined);
528
+ }
529
+ return result.code;
530
+ }
531
+
532
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
533
+ const transformDiskCacheDisabled = process.env.PNEXT_TRANSFORM_DISK_CACHE === '0';
534
+ const materializedSegment = `${path.sep}cache${path.sep}server${path.sep}`;
535
+
536
+ /**
537
+ * Where a transform of `file` is remembered across restarts. Only for the
538
+ * materialized `cache/server` modules: they are hash-named build output under
539
+ * our own cache root, so the key (path + mtime + size + the define/extension
540
+ * inputs of the transform) is complete, and the whole tree is discarded when
541
+ * a build or a config change rewrites it.
542
+ */
543
+ function transformCacheFile(file: string, fileStat: { mtimeMs: number; size: number }) {
544
+ if (transformDiskCacheDisabled) return undefined;
545
+ const at = file.lastIndexOf(materializedSegment);
546
+ if (at === -1) return undefined;
547
+ const cacheRoot = file.slice(0, at + materializedSegment.length);
548
+ const key = `${file}\0${fileStat.mtimeMs}\0${fileStat.size}\0${currentLoadExtras().join(',')}\0${JSON.stringify(serverDefineOptions())}`;
549
+ return path.join(cacheRoot, 'transform', `${hashBundleSpecifier(key)}.js`);
550
+ }
551
+
552
+ // A synchronous `require('./x')` of an app source module fails at runtime with `require() async
553
+ // module ... is unsupported`: the load plugin's onLoad hook makes every app `[jt]sx?` module an
554
+ // ASYNC Bun module, and Bun cannot `require` one. Bun does not route relative `require-call`s
555
+ // through onResolve plugins (only bare specifiers reach them), so the alias trick cannot intercept
556
+ // them at resolve time. Instead each such require is rewritten to a sibling
557
+ // `<x>.pnext-require.cjs` compiled to CommonJS: a real `.cjs` on disk is deliberately excluded by
558
+ // rootFilter, so Bun loads it natively and synchronously.
559
+ //
560
+ // Only requires that resolve to a rootFilter-matched app module are rewritten (a
561
+ // `.cjs`/`.json`/node_modules require already loads synchronously). The sidecar is a distinct module
562
+ // instance from the module's ESM copy - acceptable here, since the require path was an outright
563
+ // crash before and such modules are typically leaf value modules.
564
+ //
565
+ // Both the source form and the BUILT form are matched: the server bundle keeps such a require
566
+ // unbundled and emits esbuild's `__require("file:///abs/path/...")` shim call, which loads the very
567
+ // same async app module at runtime.
568
+ const REQUIRE_CALL = /\b(?:__)?require\(\s*(['"])((?:\.\.?\/|\/|file:\/\/)[^'"]+)\1\s*\)/g;
569
+
570
+ async function inlineModuleScopeRequires(
571
+ source: string,
572
+ file: string,
573
+ root: string | undefined,
574
+ ): Promise<string> {
575
+ if (!root || !source.includes('require(')) return source;
576
+ const filter = rootFilter(root);
577
+ const sidecars = new Map<string, string>();
578
+ const pending: Promise<void>[] = [];
579
+ const seen = new Set<string>();
580
+ for (const match of source.matchAll(REQUIRE_CALL)) {
581
+ const specifier = match[2]!;
582
+ if (seen.has(specifier)) continue;
583
+ seen.add(specifier);
584
+ const target = requireCallTarget(root, file, specifier);
585
+ if (!target || !filter.test(target)) continue;
586
+ pending.push(
587
+ requireCjsSidecar(target).then(sidecar => {
588
+ if (sidecar) sidecars.set(specifier, sidecar);
589
+ }),
590
+ );
591
+ }
592
+ if (pending.length === 0) return source;
593
+ await Promise.all(pending);
594
+ if (sidecars.size === 0) return source;
595
+ return source.replace(REQUIRE_CALL, (whole, quote: string, specifier: string) => {
596
+ const sidecar = sidecars.get(specifier);
597
+ return sidecar ? `require(${quote}${sidecar}${quote})` : whole;
598
+ });
599
+ }
600
+
601
+ /** The module a rewritable `require(...)` call resolves to, if any. */
602
+ function requireCallTarget(root: string, file: string, specifier: string): string | undefined {
603
+ if (specifier.startsWith('file://')) {
604
+ try {
605
+ return resolveImport(root, file, fileURLToPath(specifier));
606
+ } catch {
607
+ return undefined;
608
+ }
609
+ }
610
+ return resolveImport(root, file, specifier);
611
+ }
612
+
613
+ // Compile an app module to a sibling `.pnext-require.cjs` (CommonJS), reusing a
614
+ // mtime/size-cached copy. Returns the sidecar path, or undefined on any failure
615
+ // (the original require is then left as-is). The module runs through the same
616
+ // server source pipeline as a normal load so the CJS copy behaves like the ESM
617
+ // one; nested module-scope requires inside it are NOT recursively inlined.
618
+ async function requireCjsSidecar(target: string): Promise<string | undefined> {
619
+ try {
620
+ const sidecar = requireCjsSidecarPath(target);
621
+ const fileStat = await stat(target);
622
+ const cached = requireSidecarCache.get(target);
623
+ if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return sidecar;
624
+ const runtime = runtimeConfigForFile(target);
625
+ const source = rewriteServerSource(await readFile(target, 'utf8'), target, {
626
+ root: runtime?.root,
627
+ });
628
+ const result = await transform(source, {
629
+ loader: esbuildLoader(target),
630
+ jsx: 'automatic',
631
+ jsxImportSource: 'preact',
632
+ sourcefile: target,
633
+ target: 'es2022',
634
+ format: 'cjs',
635
+ ...serverDefineOptions(),
636
+ });
637
+ await writeFile(sidecar, result.code);
638
+ requireSidecarCache.set(target, { mtimeMs: fileStat.mtimeMs, size: fileStat.size });
639
+ return sidecar;
640
+ } catch {
641
+ return undefined;
642
+ }
643
+ }
644
+
645
+ function requireCjsSidecarPath(target: string): string {
646
+ return `${target.replace(/\.[^./\\]+$/, '')}.pnext-require.cjs`;
647
+ }
648
+
649
+ async function transformNodeModuleSource(file: string) {
650
+ const fileStat = await stat(file);
651
+ const cached = nodeModuleTransformCache.get(file);
652
+ if (cached?.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
653
+ return cached.code;
654
+ }
655
+ // The in-memory cache is empty on every restart, so a restarted server
656
+ // re-BUNDLES each of these packages on the request path.
657
+ const record = nodeModuleBundleRecordFile(file, fileStat);
658
+ const reused = record ? await readNodeModuleBundle(record) : undefined;
659
+ if (reused !== undefined) {
660
+ nodeModuleTransformCache.set(file, { mtimeMs: fileStat.mtimeMs, size: fileStat.size, code: reused });
661
+ return reused;
662
+ }
663
+
664
+ const { code, inputs } = await bundleNodeModuleSource(file);
665
+ nodeModuleTransformCache.set(file, {
666
+ mtimeMs: fileStat.mtimeMs,
667
+ size: fileStat.size,
668
+ code,
669
+ });
670
+ if (record) void writeNodeModuleBundle(record, inputs, code);
671
+ return code;
672
+ }
673
+
674
+ /**
675
+ * Where a bundle of `file` is remembered across restarts. The entry's own stat is in the name, but
676
+ * the build inlines the package's relative graph too, so the record carries every esbuild input and
677
+ * is only reused while all of them still stat the same. A record that cannot be validated is a miss.
678
+ */
679
+ function nodeModuleBundleRecordFile(file: string, fileStat: { mtimeMs: number; size: number }) {
680
+ if (transformDiskCacheDisabled) return undefined;
681
+ const outPath = (runtimeConfigForFile(file) ?? firstRuntimeConfig())?.resolved.outPath;
682
+ if (!outPath) return undefined;
683
+ const key = `${file}\0${fileStat.mtimeMs}\0${fileStat.size}\0${currentLoadExtras().join(',')}\0${JSON.stringify(serverDefineOptions())}`;
684
+ return path.join(cacheRoot(outPath), 'node-module-bundle', `${hashBundleSpecifier(key)}.json`);
685
+ }
686
+
687
+ async function bundleNodeModuleSource(file: string) {
688
+ const resolved = runtimeConfigForFile(file)?.resolved;
689
+ const extensionPlugins = resolved ? getBundlerExtensions().serverEsbuildPlugins(resolved) : [];
690
+ const result = await build({
691
+ // The marker entry only resolves when the extension chain is present.
692
+ entryPoints: [
693
+ (resolved
694
+ ? getBundlerExtensions().serverBundleEntry(file, path.dirname(file), file)
695
+ : undefined) ?? file,
696
+ ],
697
+ bundle: true,
698
+ write: false,
699
+ format: 'esm',
700
+ platform: 'neutral',
701
+ target: 'es2022',
702
+ loader: packageJsxLoaders,
703
+ jsx: 'automatic',
704
+ jsxImportSource: 'preact',
705
+ packages: 'external',
706
+ logLevel: 'silent',
707
+ metafile: true,
708
+ ...serverDefineOptions(),
709
+ plugins: [
710
+ serverAssetPlugin(),
711
+ ...extensionPlugins,
712
+ runtimeAliasBuildPlugin(undefined, 'server', { bundleRequireAliases: true }),
713
+ ],
714
+ });
715
+ const output = result.outputFiles[0];
716
+ if (!output) throw new Error(`Failed to bundle ${file}`);
717
+ // Packages can ship 'use cache' functions too (Next allows it); everything
718
+ // else in the source chain was already settled by the bundle above (imports
719
+ // resolved by the plugins, constants inlined by esbuild `define`).
720
+ return {
721
+ code: applyBundledSourceTransforms(output.text, file),
722
+ inputs: Object.keys(result.metafile.inputs).map(input => path.resolve(input)),
723
+ };
724
+ }
725
+
726
+ /**
727
+ * The one vendor-compile entry point. Every demand - a module compile's import scan, an esbuild
728
+ * resolve callback, a transitive dependency of another vendor bundle - arrives here and is scheduled
729
+ * by the pipeline in `./vendor`. `nested` marks demands raised from inside a running vendor build;
730
+ * see `withVendorSlot` for why they must not queue.
731
+ */
732
+ export async function externalServerPackageHref(
733
+ config: ResolvedConfig,
734
+ specifier: string,
735
+ target: CompatAliasTarget,
736
+ resolveDir = config.root,
737
+ conditionTarget: ServerBundleTarget = target,
738
+ nested = false,
739
+ ) {
740
+ let plan = vendorBuildPlan(config, specifier, target, resolveDir, conditionTarget);
741
+ const aliased = await crossLayerReusePlan(config, specifier, resolveDir, target, conditionTarget, plan);
742
+ if (aliased) plan = aliased;
743
+ if (vendorTraceEnabled()) {
744
+ vendorTraceRow({
745
+ kind: 'request',
746
+ specifier,
747
+ resolveDir,
748
+ target,
749
+ conditionTarget,
750
+ memHit: vendorBundleMemHit(plan.key),
751
+ atMs: performance.now(),
752
+ });
753
+ }
754
+ let file = await vendorBundle(plan, nested);
755
+ // A `pnext build` wiping the output directory under a running server
756
+ // deletes vendor bundles; re-write instead of handing out a dead path.
757
+ if (!cachedExistsSync(file) || !(await compiledModuleUsable(file))) {
758
+ dropVendorBundle(plan.key, file);
759
+ if (plan.group) dropVendorGroup(plan.group.key);
760
+ file = await vendorBundle(plan, nested);
761
+ }
762
+ return pathToFileHref(file);
763
+ }
764
+
765
+ /**
766
+ * Artifact identity: what the bundle is compiled FROM, never who asked. Keying on the importer's
767
+ * directory (what `require.resolve` fell back to when a package publishes no `require` condition)
768
+ * split one package into one build per importing directory.
769
+ */
770
+ function vendorArtifactKey(
771
+ config: ResolvedConfig,
772
+ target: CompatAliasTarget,
773
+ conditionTarget: ServerBundleTarget,
774
+ source: string,
775
+ ) {
776
+ return `${config.outPath}\0${target}\0${conditionTarget}\0${source}`;
777
+ }
778
+
779
+ function vendorArtifactFile(config: ResolvedConfig, key: string) {
780
+ return path.join(config.outPath, 'cache', 'server', 'vendor', `${hashBundleSpecifier(key)}.mjs`);
781
+ }
782
+
783
+ /**
784
+ * The file this specifier names - the artifact's identity. Resolved with `import` on top of the
785
+ * layer's conditions because that is what esbuild adds for an ESM entry point: without it a package
786
+ * publishing only an `import` condition resolves to nothing, and the key falls back to the
787
+ * importer's directory - the duplicate-build shape this path exists to remove.
788
+ */
789
+ function resolveVendorEntry(
790
+ config: ResolvedConfig,
791
+ specifier: string,
792
+ resolveDir: string,
793
+ conditionTarget: ServerBundleTarget,
794
+ ) {
795
+ if (path.isAbsolute(specifier)) return path.resolve(specifier);
796
+ // Every demand builds a plan before dedup can see it, so this resolve runs
797
+ // once per DEMAND, not once per artifact — and the same specifier arrives
798
+ // from hundreds of importers. The answer is pure in its inputs.
799
+ const key = `${config.root}\0${conditionTarget}\0${resolveDir}\0${specifier}`;
800
+ if (vendorEntryResolutions.has(key)) return vendorEntryResolutions.get(key);
801
+ const entry = resolveVendorPackageSpecifier(
802
+ config.root,
803
+ path.join(resolveDir, 'pnext-resolve.ts'),
804
+ specifier,
805
+ [...serverBundleConditions(conditionTarget), 'import'],
806
+ );
807
+ vendorEntryResolutions.set(key, entry);
808
+ return entry;
809
+ }
810
+
811
+ const vendorEntryResolutions = new Map<string, string | undefined>();
812
+
813
+ function vendorBuildPlan(
814
+ config: ResolvedConfig,
815
+ specifier: string,
816
+ target: CompatAliasTarget,
817
+ resolveDir: string,
818
+ conditionTarget: ServerBundleTarget,
819
+ ): VendorBuildPlan {
820
+ const entry = resolveVendorEntry(config, specifier, resolveDir, conditionTarget);
821
+ // An unresolvable specifier has no artifact identity to key on; keep the old
822
+ // importer-scoped key so the demand still compiles (and still dedups per dir).
823
+ const key = vendorArtifactKey(
824
+ config,
825
+ target,
826
+ conditionTarget,
827
+ entry ?? `${path.resolve(resolveDir)}\0${specifier}`,
828
+ );
829
+ const file = vendorArtifactFile(config, key);
830
+ return {
831
+ key,
832
+ file,
833
+ prepare: () =>
834
+ prepareVendorArtifact(config, specifier, resolveDir, conditionTarget, file, target, entry),
835
+ buildOne: nested =>
836
+ writeExternalServerPackageBundle(
837
+ config,
838
+ specifier,
839
+ target,
840
+ resolveDir,
841
+ file,
842
+ conditionTarget,
843
+ nested,
844
+ ),
845
+ group: entry
846
+ ? vendorGroupPlan(config, { specifier, entry, file }, target, resolveDir, conditionTarget)
847
+ : undefined,
848
+ };
849
+ }
850
+
851
+ /**
852
+ * A vendor artifact this process just wrote is trusted for the rest of the run.
853
+ * Without the note, a cache folder that started without a marker (every cold
854
+ * boot) distrusts what was just written, and `externalServerPackageHref`'s
855
+ * liveness check rebuilds every bundle a second time.
856
+ */
857
+ async function writeVendorArtifact(file: string, code: string) {
858
+ const id = await publishVendorContent(file, code);
859
+ if (id === undefined) {
860
+ await writeFileAtomic(file, code);
861
+ noteDevArtifactWritten(file);
862
+ }
863
+ return id;
864
+ }
865
+
866
+ // --------------------------------------------------------------------------
867
+ // content-addressed artifacts and the cross-layer verdict
868
+ // --------------------------------------------------------------------------
869
+
870
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
871
+ const vendorContentDisabled = () => process.env.PNEXT_VENDOR_CONTENT === '0';
872
+
873
+ function vendorContentFile(vendorDir: string, id: string) {
874
+ return path.join(vendorDir, `${id}.content.mjs`);
875
+ }
876
+
877
+ /** Entries only: a chunk reaches its entry by relative path, so it stays put. */
878
+ async function publishVendorContent(file: string, code: string) {
879
+ const vendorDir = path.dirname(file);
880
+ if (vendorContentDisabled() || path.basename(vendorDir) !== 'vendor') return undefined;
881
+ const id = hashBundleSpecifier(canonicalVendorCode(code));
882
+ const content = vendorContentFile(vendorDir, id);
883
+ if (!cachedExistsSync(content)) await writeFileAtomic(content, code);
884
+ noteDevArtifactWritten(content);
885
+ await linkVendorArtifact(file, content);
886
+ noteVendorContentId(file, id);
887
+ noteDevArtifactWritten(file);
888
+ return id;
889
+ }
890
+
891
+ /** The layer-keyed path becomes an alias to the shared bytes. */
892
+ async function linkVendorArtifact(file: string, content: string) {
893
+ await rm(file, { force: true }).catch(() => undefined);
894
+ try {
895
+ await symlink(path.basename(content), file);
896
+ } catch {
897
+ await copyFile(content, file);
898
+ }
899
+ }
900
+
901
+ /** What one layer's build of an entry saw, for the cross-layer comparison. */
902
+ interface VendorLayerRecord {
903
+ inputs: string;
904
+ externals: string;
905
+ blocked: boolean;
906
+ id: string;
907
+ }
908
+
909
+ // Keyed by output path exactly as `vendorArtifactKey` is: a verdict holds for
910
+ // ONE app's conditions, aliases and defines, and several run in one process.
911
+ const vendorLayerRecords = new Map<string, Map<string, VendorLayerRecord>>();
912
+ /** Entries proven layer-independent -> the content id both layers resolve to. */
913
+ const vendorSharedEntries = new Map<string, Map<string, string>>();
914
+ const vendorSharedLoaded = new Set<string>();
915
+
916
+ function vendorSharedFor(config: ResolvedConfig) {
917
+ let shared = vendorSharedEntries.get(config.outPath);
918
+ if (!shared) {
919
+ shared = new Map();
920
+ vendorSharedEntries.set(config.outPath, shared);
921
+ }
922
+ return shared;
923
+ }
924
+
925
+ /** Only these two layers were ever the duplicated half. */
926
+ function vendorLayerKey(target: CompatAliasTarget, conditionTarget: ServerBundleTarget) {
927
+ if (target === 'server' && conditionTarget === 'server') return 'server';
928
+ if (target === 'client' && conditionTarget === 'client') return 'client';
929
+ return undefined;
930
+ }
931
+
932
+ function vendorSharedFile(config: ResolvedConfig) {
933
+ return path.join(config.outPath, 'cache', 'server', 'vendor', 'layer-shared.json');
934
+ }
935
+
936
+ /** Verdicts from earlier runs, so reuse lands on the first demand of this one. */
937
+ function loadVendorSharedEntries(config: ResolvedConfig) {
938
+ if (vendorSharedLoaded.has(config.outPath)) return;
939
+ vendorSharedLoaded.add(config.outPath);
940
+ try {
941
+ const stored = JSON.parse(readFileSync(vendorSharedFile(config), 'utf8')) as Record<string, string>;
942
+ const shared = vendorSharedFor(config);
943
+ for (const [entry, id] of Object.entries(stored)) shared.set(entry, id);
944
+ } catch {
945
+ // No verdicts yet; they are written as builds settle.
946
+ }
947
+ }
948
+
949
+ function persistVendorSharedEntries(config: ResolvedConfig) {
950
+ void writeFileAtomic(
951
+ vendorSharedFile(config),
952
+ JSON.stringify(Object.fromEntries(vendorSharedFor(config))),
953
+ ).catch(() => undefined);
954
+ }
955
+
956
+ /** Offline verdict: externals compare as content ids, so this is a fixpoint. */
957
+ function recordVendorLayerBuild(
958
+ config: ResolvedConfig,
959
+ entry: string | undefined,
960
+ target: CompatAliasTarget,
961
+ conditionTarget: ServerBundleTarget,
962
+ metafile: Metafile | undefined,
963
+ code: string,
964
+ id: string | undefined,
965
+ ) {
966
+ const layer = entry && id && metafile && vendorLayerKey(target, conditionTarget);
967
+ if (!layer || !entry || !id || !metafile) return;
968
+ loadVendorSharedEntries(config);
969
+ const externals = new Set<string>();
970
+ let blocked = /['"]use (?:client|server|cache)['"]/.test(code);
971
+ for (const meta of Object.values(metafile.inputs)) {
972
+ for (const reference of meta.imports) {
973
+ if (!reference.external) continue;
974
+ const target_ = reference.path.startsWith('file:')
975
+ ? fileURLToPath(reference.path)
976
+ : undefined;
977
+ // A shim outside the vendor tree is this layer's own alias target: the
978
+ // artifact is layer-specific whatever else matches.
979
+ if (target_ && path.basename(path.dirname(target_)) !== 'vendor') blocked = true;
980
+ externals.add(target_ ? (vendorContentId(target_) ?? target_) : reference.path);
981
+ }
982
+ }
983
+ const record: VendorLayerRecord = {
984
+ inputs: Object.keys(metafile.inputs).sort().join('\n'),
985
+ externals: [...externals].sort().join('\n'),
986
+ blocked,
987
+ id,
988
+ };
989
+ const recordKey = `${config.outPath}\0${entry}`;
990
+ let byLayer = vendorLayerRecords.get(recordKey);
991
+ if (!byLayer) {
992
+ byLayer = new Map();
993
+ vendorLayerRecords.set(recordKey, byLayer);
994
+ }
995
+ byLayer.set(layer, record);
996
+ const server = byLayer.get('server');
997
+ const client = byLayer.get('client');
998
+ if (!server || !client || server.blocked || client.blocked) return;
999
+ if (server.inputs !== client.inputs || server.externals !== client.externals) return;
1000
+ vendorSharedFor(config).set(entry, server.id);
1001
+ persistVendorSharedEntries(config);
1002
+ }
1003
+
1004
+ /** A proven-shared entry's content file, or undefined to compile as usual. */
1005
+ function reusableVendorContent(
1006
+ config: ResolvedConfig,
1007
+ entry: string | undefined,
1008
+ target: CompatAliasTarget,
1009
+ conditionTarget: ServerBundleTarget,
1010
+ ) {
1011
+ if (vendorContentDisabled() || !entry || !vendorLayerKey(target, conditionTarget)) return undefined;
1012
+ loadVendorSharedEntries(config);
1013
+ const id = vendorSharedFor(config).get(entry);
1014
+ if (!id) return undefined;
1015
+ const content = vendorContentFile(path.join(config.outPath, 'cache', 'server', 'vendor'), id);
1016
+ return existsSync(content) ? content : undefined;
1017
+ }
1018
+
1019
+ // --------------------------------------------------------------------------
1020
+ // pre-build cross-layer reuse verdict (PNEXT_VENDOR_REUSE=0 to disable)
1021
+ // --------------------------------------------------------------------------
1022
+
1023
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1024
+ const vendorReuseEnabled = () => process.env.PNEXT_VENDOR_REUSE !== '0';
1025
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1026
+ const vendorReuseExtEnabled = () => process.env.PNEXT_VENDOR_REUSE_EXT !== '0';
1027
+
1028
+ /** Alias targets that differ by layer (compat/index.ts); prefix-matches subpaths. */
1029
+ const VENDOR_LAYER_ALIASES = new Set([
1030
+ 'react',
1031
+ 'react-dom',
1032
+ 'next/cache',
1033
+ 'next/navigation',
1034
+ 'next/script',
1035
+ 'next/server',
1036
+ 'server-only',
1037
+ 'client-only',
1038
+ ]);
1039
+
1040
+ function isVendorLayerAlias(external: string) {
1041
+ return (
1042
+ VENDOR_LAYER_ALIASES.has(external) ||
1043
+ VENDOR_LAYER_ALIASES.has(external.split('/').slice(0, 2).join('/'))
1044
+ );
1045
+ }
1046
+
1047
+ interface VendorReuseVerdict {
1048
+ stat: string;
1049
+ conds: string;
1050
+ reusable: boolean;
1051
+ reason?: string;
1052
+ }
1053
+
1054
+ const vendorReuseVerdicts = new Map<string, Map<string, VendorReuseVerdict>>();
1055
+ const vendorReuseLoaded = new Set<string>();
1056
+ const vendorReuseProbes = new Map<string, Promise<boolean>>();
1057
+
1058
+ function vendorReuseFile(config: ResolvedConfig) {
1059
+ return path.join(config.outPath, 'cache', 'server', 'vendor', 'reuse-verdicts.json');
1060
+ }
1061
+
1062
+ function vendorReuseFor(config: ResolvedConfig) {
1063
+ let verdicts = vendorReuseVerdicts.get(config.outPath);
1064
+ if (!verdicts) {
1065
+ verdicts = new Map();
1066
+ vendorReuseVerdicts.set(config.outPath, verdicts);
1067
+ }
1068
+ return verdicts;
1069
+ }
1070
+
1071
+ /** Verdicts persist per checkout: probes are paid once, restarts read the store. */
1072
+ function loadVendorReuseVerdicts(config: ResolvedConfig) {
1073
+ if (vendorReuseLoaded.has(config.outPath)) return;
1074
+ vendorReuseLoaded.add(config.outPath);
1075
+ try {
1076
+ const stored = JSON.parse(readFileSync(vendorReuseFile(config), 'utf8')) as Record<
1077
+ string,
1078
+ VendorReuseVerdict
1079
+ >;
1080
+ const verdicts = vendorReuseFor(config);
1081
+ for (const [key, verdict] of Object.entries(stored)) verdicts.set(key, verdict);
1082
+ } catch {
1083
+ // No store yet; verdicts are written as probes settle.
1084
+ }
1085
+ }
1086
+
1087
+ function persistVendorReuseVerdicts(config: ResolvedConfig) {
1088
+ void writeFileAtomic(
1089
+ vendorReuseFile(config),
1090
+ JSON.stringify(Object.fromEntries(vendorReuseFor(config))),
1091
+ ).catch(() => undefined);
1092
+ }
1093
+
1094
+ /** Probe results shared across verdict pairs — node_modules bytes are immutable per process. */
1095
+ const vendorLayerProbeMemo = new Map<string, Promise<VendorLayerProbe>>();
1096
+
1097
+ type VendorLayerProbe = Awaited<ReturnType<typeof probeVendorLayer>>;
1098
+
1099
+ function probeVendorLayerMemo(config: ResolvedConfig, entry: string, conditions: string[]) {
1100
+ if (!vendorReuseExtEnabled()) return probeVendorLayer(config, entry, conditions);
1101
+ const key = `${config.outPath}\0${entry}\0${conditions.join(',')}`;
1102
+ let probe = vendorLayerProbeMemo.get(key);
1103
+ if (!probe) {
1104
+ probe = probeVendorLayer(config, entry, conditions);
1105
+ probe.catch(() => vendorLayerProbeMemo.delete(key));
1106
+ vendorLayerProbeMemo.set(key, probe);
1107
+ }
1108
+ return probe;
1109
+ }
1110
+
1111
+ /** Metafile-only probe of one layer's view of an entry (never writes, no slots). */
1112
+ async function probeVendorLayer(config: ResolvedConfig, entry: string, conditions: string[]) {
1113
+ const result = await build({
1114
+ entryPoints: [entry],
1115
+ absWorkingDir: config.root,
1116
+ bundle: true,
1117
+ write: false,
1118
+ metafile: true,
1119
+ format: 'esm',
1120
+ platform: 'neutral',
1121
+ target: 'es2022',
1122
+ conditions,
1123
+ mainFields: ['module', 'main'],
1124
+ loader: { '.js': 'jsx', '.mjs': 'jsx', '.cjs': 'jsx', '.css': 'empty' },
1125
+ jsx: 'automatic',
1126
+ jsxImportSource: 'preact',
1127
+ logLevel: 'silent',
1128
+ packages: 'external',
1129
+ });
1130
+ const externals = new Set<string>();
1131
+ for (const meta of Object.values(result.metafile.inputs)) {
1132
+ for (const reference of meta.imports) if (reference.external) externals.add(reference.path);
1133
+ }
1134
+ return {
1135
+ inputs: Object.keys(result.metafile.inputs).sort().join('\n'),
1136
+ externals: [...externals].sort(),
1137
+ directive: /['"]use (?:client|server|cache)['"]/.test(
1138
+ result.outputFiles.map(file => file.text).join(''),
1139
+ ),
1140
+ };
1141
+ }
1142
+
1143
+ async function computeVendorReuseVerdict(
1144
+ config: ResolvedConfig,
1145
+ serverEntry: string,
1146
+ clientEntry: string,
1147
+ otherTarget: ServerBundleTarget = 'client',
1148
+ ): Promise<{ reusable: boolean; reason?: string }> {
1149
+ const tp = performance.now();
1150
+ const done = (v: { reusable: boolean; reason?: string }) => {
1151
+ heavyProfRow({ k: 'reuse-probe', entry: serverEntry, other: otherTarget, ms: performance.now() - tp, ...v });
1152
+ return v;
1153
+ };
1154
+ // A finished server build already proved itself layer-specific: skip both probes.
1155
+ if (vendorReuseExtEnabled()) {
1156
+ const recorded = vendorLayerRecords.get(`${config.outPath}\0${serverEntry}`)?.get('server');
1157
+ if (recorded?.blocked) return done({ reusable: false, reason: 'recorded-blocked' });
1158
+ }
1159
+ const [server, client] = await Promise.all([
1160
+ probeVendorLayerMemo(config, serverEntry, serverBundleConditions('server')),
1161
+ probeVendorLayerMemo(config, clientEntry, serverBundleConditions(otherTarget)),
1162
+ ]);
1163
+ if (server.inputs !== client.inputs) return done({ reusable: false, reason: 'inputs-differ' });
1164
+ if (server.externals.join('\n') !== client.externals.join('\n')) {
1165
+ return done({ reusable: false, reason: 'externals-differ' });
1166
+ }
1167
+ const aliasHit = server.externals.filter(isVendorLayerAlias);
1168
+ if (aliasHit.length > 0) return done({ reusable: false, reason: `layer-alias: ${aliasHit.join(',')}` });
1169
+ if (server.directive || client.directive) return done({ reusable: false, reason: 'directive' });
1170
+ return done({ reusable: true });
1171
+ }
1172
+
1173
+ /** Whether the client demand may resolve to the server-layer artifact. */
1174
+ function crossLayerReuseVerdict(
1175
+ config: ResolvedConfig,
1176
+ serverEntry: string,
1177
+ clientEntry: string,
1178
+ otherTarget: ServerBundleTarget = 'client',
1179
+ ): Promise<boolean> {
1180
+ loadVendorReuseVerdicts(config);
1181
+ const key =
1182
+ otherTarget === 'client'
1183
+ ? `${serverEntry}\0${clientEntry}`
1184
+ : `${otherTarget}:${serverEntry}\0${clientEntry}`;
1185
+ const stats = [statSync(serverEntry, { throwIfNoEntry: false }), statSync(clientEntry, { throwIfNoEntry: false })];
1186
+ if (stats.some(entryStat => !entryStat)) return Promise.resolve(false);
1187
+ const stat_ = stats.map(entryStat => `${entryStat!.mtimeMs}:${entryStat!.size}`).join('|');
1188
+ const conds = JSON.stringify([serverBundleConditions('server'), serverBundleConditions(otherTarget)]);
1189
+ const stored = vendorReuseFor(config).get(key);
1190
+ if (stored?.stat === stat_ && stored.conds === conds) {
1191
+ return Promise.resolve(stored.reusable);
1192
+ }
1193
+ const probeKey = `${config.outPath}\0${key}`;
1194
+ let probe = vendorReuseProbes.get(probeKey);
1195
+ if (!probe) {
1196
+ probe = computeVendorReuseVerdict(config, serverEntry, clientEntry, otherTarget)
1197
+ .catch(error => ({
1198
+ reusable: false,
1199
+ reason: `probe-failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 160)}`,
1200
+ }))
1201
+ .then(verdict => {
1202
+ vendorReuseFor(config).set(key, { stat: stat_, conds, ...verdict });
1203
+ persistVendorReuseVerdicts(config);
1204
+ return verdict.reusable;
1205
+ })
1206
+ .finally(() => vendorReuseProbes.delete(probeKey));
1207
+ vendorReuseProbes.set(probeKey, probe);
1208
+ }
1209
+ return probe;
1210
+ }
1211
+
1212
+ /**
1213
+ * Alias a client-layer vendor demand onto the server-layer plan when the two builds are proven
1214
+ * byte-equivalent. The swap happens BEFORE enqueue, so the demand lands on the server build's
1215
+ * `pendingBundles` slot through ordinary dedup - no new cross-layer wait edges enter the scheduler.
1216
+ * A server build that is neither on disk nor pending is never waited for.
1217
+ */
1218
+ async function crossLayerReusePlan(
1219
+ config: ResolvedConfig,
1220
+ specifier: string,
1221
+ resolveDir: string,
1222
+ target: CompatAliasTarget,
1223
+ conditionTarget: ServerBundleTarget,
1224
+ clientPlan: VendorBuildPlan,
1225
+ ): Promise<VendorBuildPlan | undefined> {
1226
+ if (!vendorReuseEnabled() || vendorContentDisabled()) return undefined;
1227
+ // An edge demand proven condition-independent lands on the server plan —
1228
+ // one build serves both, and the server artifact keeps client reuse alive.
1229
+ if (target === 'server' && conditionTarget === 'edge' && vendorReuseExtEnabled()) {
1230
+ if (vendorBundleMemHit(clientPlan.key) || existsSync(clientPlan.file)) return undefined;
1231
+ const edgeEntry = resolveVendorEntry(config, specifier, resolveDir, 'edge');
1232
+ const serverEntry = resolveVendorEntry(config, specifier, resolveDir, 'server');
1233
+ if (!edgeEntry || !serverEntry || edgeEntry !== serverEntry) return undefined;
1234
+ const reusable = await crossLayerReuseVerdict(config, serverEntry, edgeEntry, 'edge').catch(() => false);
1235
+ return reusable ? vendorBuildPlan(config, specifier, 'server', resolveDir, 'server') : undefined;
1236
+ }
1237
+ if (vendorLayerKey(target, conditionTarget) !== 'client') return undefined;
1238
+ // An in-flight or on-disk client artifact already settles cheaper than a probe.
1239
+ if (vendorBundleMemHit(clientPlan.key) || existsSync(clientPlan.file)) return undefined;
1240
+ const clientEntry = resolveVendorEntry(config, specifier, resolveDir, conditionTarget);
1241
+ const serverEntry = resolveVendorEntry(config, specifier, resolveDir, 'server');
1242
+ if (!clientEntry || !serverEntry) return undefined;
1243
+ const serverPlan = vendorBuildPlan(config, specifier, 'server', resolveDir, 'server');
1244
+ if (!vendorBundleMemHit(serverPlan.key) && !existsSync(serverPlan.file)) return undefined;
1245
+ const reusable = await crossLayerReuseVerdict(config, serverEntry, clientEntry).catch(() => false);
1246
+ return reusable ? serverPlan : undefined;
1247
+ }
1248
+
1249
+ /** Cache hit and no-compile fast paths, in front of every build. */
1250
+ async function prepareVendorArtifact(
1251
+ config: ResolvedConfig,
1252
+ specifier: string,
1253
+ resolveDir: string,
1254
+ conditionTarget: ServerBundleTarget,
1255
+ file: string,
1256
+ target: CompatAliasTarget,
1257
+ entry: string | undefined,
1258
+ ) {
1259
+ // Vendor bundles are keyed by resolved package path (version-specific under
1260
+ // pnpm), so an existing file is already current. Reuse it across dev restarts
1261
+ // instead of re-bundling the whole dependency graph.
1262
+ if (existsSync(file) && (await compiledModuleUsable(file))) return file;
1263
+ await mkdir(path.dirname(file), { recursive: true });
1264
+ // An earlier build proved this entry layer-independent: the other layer's
1265
+ // content IS this layer's artifact, so the demand settles without compiling.
1266
+ const shared = reusableVendorContent(config, entry, target, conditionTarget);
1267
+ if (shared && (await compiledModuleUsable(shared))) {
1268
+ await linkVendorArtifact(file, shared);
1269
+ noteDevArtifactWritten(file);
1270
+ return shared;
1271
+ }
1272
+ // A package that already ships a browser-ready ESM dist is copied, not compiled - the bundler would
1273
+ // only reproduce it. Sits in FRONT of the pass, never replaces it: anything the predicate rejects
1274
+ // falls through unchanged, and the copy is resolved exactly as the bundler would have. The copies
1275
+ // load natively now (rootFilter skips the vendor tree), so an app that configures `compiler.define`
1276
+ // must take the bundling path instead - that is where the defines are applied.
1277
+ if (vendorLoadsNatively() && serverDefineOptions().define) return undefined;
1278
+ const distDir = `${file.replace(/\.mjs$/, '')}.dist`;
1279
+ const distEntry = await copyBrowserReadyEsmDist(
1280
+ resolveVendorPackageSpecifier(
1281
+ config.root,
1282
+ path.join(resolveDir, 'pnext-resolve.ts'),
1283
+ specifier,
1284
+ serverBundleConditions(conditionTarget),
1285
+ ) ?? '',
1286
+ distDir,
1287
+ );
1288
+ if (distEntry) {
1289
+ // Native loading means the `.js` copies need their own type marker: an app
1290
+ // without `"type": "module"` would otherwise have Bun read them as
1291
+ // CommonJS and choke on their `export` syntax.
1292
+ await markEsmDistPackage(distDir);
1293
+ noteDevArtifactWritten(distEntry);
1294
+ }
1295
+ return distEntry;
1296
+ }
1297
+
1298
+ /**
1299
+ * Bisect switch for the vendor-tree load-plugin exclusion. Deliberately NOT `PNEXT_VENDOR_NATIVE` -
1300
+ * that name already gates the plugin-free bundler, and sharing it would arm that opt-in path by
1301
+ * accident whenever this one was bisected.
1302
+ */
1303
+ function vendorLoadsNatively() {
1304
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1305
+ return process.env.PNEXT_VENDOR_DIST_NATIVE !== '0';
1306
+ }
1307
+
1308
+ // --------------------------------------------------------------------------
1309
+ // pre-planned vendor policy (PNEXT_VENDOR_PREPLAN=1 opt-in, server layer only)
1310
+ // --------------------------------------------------------------------------
1311
+
1312
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1313
+ const vendorPreplanEnabled = () => process.env.PNEXT_VENDOR_PREPLAN === '1';
1314
+
1315
+ interface VendorPreplanVerdict {
1316
+ stat: string;
1317
+ conds: string;
1318
+ safe: boolean;
1319
+ reason?: string;
1320
+ }
1321
+
1322
+ const vendorPreplanVerdicts = new Map<string, Map<string, VendorPreplanVerdict>>();
1323
+ const vendorPreplanLoaded = new Set<string>();
1324
+ const vendorPreplanProbes = new Map<string, Promise<VendorPreplanVerdict>>();
1325
+
1326
+ const vendorPreplanStats = {
1327
+ preplanRefs: 0,
1328
+ fallbackRefs: 0,
1329
+ enqueued: 0,
1330
+ composedBuilds: 0,
1331
+ packagesRouted: 0,
1332
+ packagesRejected: 0,
1333
+ reasons: [] as string[],
1334
+ };
1335
+
1336
+ export function vendorPreplanReport() {
1337
+ return { ...vendorPreplanStats, reasons: vendorPreplanStats.reasons.slice(0, 60) };
1338
+ }
1339
+
1340
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1341
+ const vendorPreplanDebug = () => process.env.PNEXT_VENDOR_PREPLAN_DEBUG === '1';
1342
+ let preplanReportFlush: ReturnType<typeof setTimeout> | undefined;
1343
+ function schedulePreplanReport() {
1344
+ if (!vendorPreplanDebug()) return;
1345
+ if (preplanReportFlush) clearTimeout(preplanReportFlush);
1346
+ preplanReportFlush = setTimeout(() => {
1347
+ console.error(`pnext vendor preplan ${JSON.stringify(vendorPreplanReport())}`);
1348
+ console.error(`pnext vendor native ${JSON.stringify(vendorNativeReport())}`);
1349
+ }, 3000);
1350
+ preplanReportFlush.unref?.();
1351
+ }
1352
+
1353
+ function vendorPreplanFile(config: ResolvedConfig) {
1354
+ return path.join(config.outPath, 'cache', 'server', 'vendor', 'preplan-verdicts.json');
1355
+ }
1356
+
1357
+ function vendorPreplanFor(config: ResolvedConfig) {
1358
+ let verdicts = vendorPreplanVerdicts.get(config.outPath);
1359
+ if (!verdicts) {
1360
+ verdicts = new Map();
1361
+ vendorPreplanVerdicts.set(config.outPath, verdicts);
1362
+ }
1363
+ return verdicts;
1364
+ }
1365
+
1366
+ /** Probe verdicts persist per checkout, reuse-verdict style. */
1367
+ function loadVendorPreplanVerdicts(config: ResolvedConfig) {
1368
+ if (vendorPreplanLoaded.has(config.outPath)) return;
1369
+ vendorPreplanLoaded.add(config.outPath);
1370
+ try {
1371
+ const stored = JSON.parse(readFileSync(vendorPreplanFile(config), 'utf8')) as Record<
1372
+ string,
1373
+ VendorPreplanVerdict
1374
+ >;
1375
+ const verdicts = vendorPreplanFor(config);
1376
+ for (const [key, verdict] of Object.entries(stored)) verdicts.set(key, verdict);
1377
+ } catch {
1378
+ // No store yet; verdicts are written as probes settle.
1379
+ }
1380
+ }
1381
+
1382
+ function persistVendorPreplanVerdicts(config: ResolvedConfig) {
1383
+ void writeFileAtomic(
1384
+ vendorPreplanFile(config),
1385
+ JSON.stringify(Object.fromEntries(vendorPreplanFor(config))),
1386
+ ).catch(() => undefined);
1387
+ }
1388
+
1389
+ /** Does this entry's graph change shape under `react-server` (or carry directives)? */
1390
+ function vendorPreplanEntryVerdict(
1391
+ config: ResolvedConfig,
1392
+ entry: string,
1393
+ conditions: string[],
1394
+ ): Promise<VendorPreplanVerdict> | VendorPreplanVerdict {
1395
+ loadVendorPreplanVerdicts(config);
1396
+ const entryStat = statSync(entry, { throwIfNoEntry: false });
1397
+ if (!entryStat) return { stat: '', conds: '', safe: false, reason: 'entry missing' };
1398
+ const stat_ = `${entryStat.mtimeMs}:${entryStat.size}`;
1399
+ const conds = JSON.stringify(conditions);
1400
+ const stored = vendorPreplanFor(config).get(entry);
1401
+ if (stored?.stat === stat_ && stored.conds === conds) return stored;
1402
+ const probeKey = `${config.outPath}\0${entry}\0${conds}`;
1403
+ let probe = vendorPreplanProbes.get(probeKey);
1404
+ if (!probe) {
1405
+ probe = computeVendorPreplanVerdict(config, entry, conditions, stat_, conds)
1406
+ .catch(error => ({
1407
+ stat: stat_,
1408
+ conds,
1409
+ safe: false,
1410
+ reason: `probe-failed: ${(error instanceof Error ? error.message : String(error)).slice(0, 160)}`,
1411
+ }))
1412
+ .then(verdict => {
1413
+ vendorPreplanFor(config).set(entry, verdict);
1414
+ persistVendorPreplanVerdicts(config);
1415
+ return verdict;
1416
+ })
1417
+ .finally(() => vendorPreplanProbes.delete(probeKey));
1418
+ vendorPreplanProbes.set(probeKey, probe);
1419
+ }
1420
+ return probe;
1421
+ }
1422
+
1423
+ async function computeVendorPreplanVerdict(
1424
+ config: ResolvedConfig,
1425
+ entry: string,
1426
+ conditions: string[],
1427
+ stat_: string,
1428
+ conds: string,
1429
+ ): Promise<VendorPreplanVerdict> {
1430
+ const [rsc, plain] = await Promise.all([
1431
+ probeVendorLayer(config, entry, conditions),
1432
+ probeVendorLayer(config, entry, conditions.filter(condition => condition !== 'react-server')),
1433
+ ]);
1434
+ if (rsc.inputs !== plain.inputs) {
1435
+ return { stat: stat_, conds, safe: false, reason: 'react-server inputs differ' };
1436
+ }
1437
+ if (rsc.externals.join('\n') !== plain.externals.join('\n')) {
1438
+ return { stat: stat_, conds, safe: false, reason: 'react-server externals differ' };
1439
+ }
1440
+ if (rsc.directive || plain.directive) {
1441
+ return { stat: stat_, conds, safe: false, reason: 'directive' };
1442
+ }
1443
+ return { stat: stat_, conds, safe: true };
1444
+ }
1445
+
1446
+ /**
1447
+ * May THIS package's build emit pre-planned refs? Any rejection routes the whole build to the awaited
1448
+ * plugin decisions - always available, always correct. ESM-only keeps the exec surface off the
1449
+ * fire-and-forget path: a CJS parent's artifact is imported (facade exec) the moment its build
1450
+ * returns, before any drain could cover its children.
1451
+ */
1452
+ async function vendorPreplanPackageSafe(
1453
+ config: ResolvedConfig,
1454
+ packageName: string | undefined,
1455
+ entry: string | undefined,
1456
+ target: CompatAliasTarget,
1457
+ conditionTarget: ServerBundleTarget,
1458
+ ): Promise<boolean> {
1459
+ if (!vendorPreplanEnabled() || target !== 'server' || conditionTarget !== 'server') return false;
1460
+ const reject = (reason: string) => {
1461
+ vendorPreplanStats.packagesRejected += 1;
1462
+ vendorPreplanStats.reasons.push(`${packageName ?? '?'}: ${reason}`);
1463
+ schedulePreplanReport();
1464
+ return false;
1465
+ };
1466
+ if (!packageName || !entry || !path.isAbsolute(entry)) return reject('unresolved entry');
1467
+ if (/\.[cm]?tsx?$/.test(entry)) return reject('ts entry');
1468
+ if (!isEsmModuleEntry(entry)) return reject('cjs entry');
1469
+ if (getExternalPackagePolicy().transpile(packageName)) return reject('transpilePackages');
1470
+ const verdict = await vendorPreplanEntryVerdict(config, entry, serverBundleConditions('server'));
1471
+ if (!verdict.safe) return reject(verdict.reason ?? 'react-server divergence');
1472
+ vendorPreplanStats.packagesRouted += 1;
1473
+ schedulePreplanReport();
1474
+ return true;
1475
+ }
1476
+
1477
+ /** Mirror of `externalServerPackageHref`'s liveness recheck, minus the await(er). */
1478
+ function enqueuePreplanBuild(plan: VendorBuildPlan) {
1479
+ vendorPreplanStats.enqueued += 1;
1480
+ trackPreplanBuild(
1481
+ (async () => {
1482
+ let file = await vendorBundle(plan, false);
1483
+ if (!cachedExistsSync(file) || !(await compiledModuleUsable(file))) {
1484
+ dropVendorBundle(plan.key, file);
1485
+ if (plan.group) dropVendorGroup(plan.group.key);
1486
+ file = await vendorBundle(plan, false);
1487
+ }
1488
+ })(),
1489
+ );
1490
+ }
1491
+
1492
+ /**
1493
+ * The deterministic-href emit: classify the dep once from pure inputs, hand
1494
+ * back its artifact path pre-build, and enqueue the build without awaiting it.
1495
+ * `undefined` routes the reference to the awaited plugin decision unchanged.
1496
+ * Every emitted href is exactly what `externalServerPackageHref` would have
1497
+ * returned for this demand, so flag-on artifacts stay byte-identical.
1498
+ */
1499
+ async function preplanDependencyDecision(
1500
+ config: ResolvedConfig,
1501
+ target: CompatAliasTarget,
1502
+ conditionTarget: ServerBundleTarget,
1503
+ specifier: string,
1504
+ resolveDir: string,
1505
+ ): Promise<OnResolveResult | undefined> {
1506
+ const depName = packageNameFromSpecifier(specifier);
1507
+ if (!depName || !isPackageSpecifier(specifier)) return undefined;
1508
+ if (builtinModules.includes(depName) || isBunBuiltin(depName)) return undefined;
1509
+ if (getExternalPackagePolicy().external(depName)) return undefined;
1510
+ if (getExternalPackagePolicy().transpile(depName)) return undefined;
1511
+ const entry = resolveVendorEntry(config, specifier, resolveDir, conditionTarget);
1512
+ if (!entry || /\.[cm]?tsx?$/.test(entry) || !isEsmModuleEntry(entry)) return undefined;
1513
+ const plan = vendorBuildPlan(config, specifier, target, resolveDir, conditionTarget);
1514
+ if (cachedExistsSync(plan.file)) {
1515
+ if (!(await compiledModuleUsable(plan.file))) return undefined;
1516
+ } else {
1517
+ // Both prepare fast paths settle on a DIFFERENT file than `plan.file`, so
1518
+ // their demands keep the awaited path (which is nearly free for them).
1519
+ if (reusableVendorContent(config, entry, target, conditionTarget)) return undefined;
1520
+ if (!(vendorLoadsNatively() && serverDefineOptions().define)) {
1521
+ const distEntry = await copyBrowserReadyEsmDist(
1522
+ resolveVendorPackageSpecifier(
1523
+ config.root,
1524
+ path.join(resolveDir, 'pnext-resolve.ts'),
1525
+ specifier,
1526
+ serverBundleConditions(conditionTarget),
1527
+ ) ?? '',
1528
+ `${plan.file.replace(/\.mjs$/, '')}.dist`,
1529
+ );
1530
+ if (distEntry) return undefined;
1531
+ }
1532
+ }
1533
+ enqueuePreplanBuild(plan);
1534
+ return { path: pathToFileHref(plan.file), external: true };
1535
+ }
1536
+
1537
+ const esmDistPackages = new Set<string>();
1538
+
1539
+ async function markEsmDistPackage(distDir: string) {
1540
+ if (esmDistPackages.has(distDir)) return;
1541
+ esmDistPackages.add(distDir);
1542
+ const marker = path.join(distDir, 'package.json');
1543
+ if (existsSync(marker)) return;
1544
+ await writeFile(marker, '{"type":"module"}').catch(() => undefined);
1545
+ }
1546
+
1547
+ /**
1548
+ * Can this compiled artifact be imported as-is? Two O(1) checks - the file itself, and the cache
1549
+ * folder's marker - replace the whole-graph walk that read back every compiled module and checked
1550
+ * each of its `cache/server/` edges. Both guard the same hazard: a `pnext build` wiping `.pnext`
1551
+ * under a running server can strand a dangling import, and Bun caches a failed load for the life of
1552
+ * the process.
1553
+ */
1554
+ // eslint-disable-next-line @typescript-eslint/require-await
1555
+ export async function compiledModuleUsable(file: string): Promise<boolean> {
1556
+ return devArtifactUsable(file);
1557
+ }
1558
+
1559
+ /**
1560
+ * The group this subpath belongs to. Same-package imports are inlined (externalizing would lose a
1561
+ * CommonJS entry's named exports), so a build per subpath re-parses the package's whole graph per
1562
+ * subpath; the subpaths demanded together become entries of one `splitting: true` build instead.
1563
+ *
1564
+ * Restricted to ESM entries: an entry esbuild has to convert from CommonJS goes through
1565
+ * `addCommonJsNamedExports`, whose export recovery reads the shape of a single-file bundle and does
1566
+ * not survive being split into chunks.
1567
+ */
1568
+ function vendorGroupPlan(
1569
+ config: ResolvedConfig,
1570
+ member: VendorGroupMember,
1571
+ target: CompatAliasTarget,
1572
+ resolveDir: string,
1573
+ conditionTarget: ServerBundleTarget,
1574
+ ): VendorGroupPlan | undefined {
1575
+ const packageName = packageNameFromSpecifier(member.specifier);
1576
+ const packageRoot = packageName && vendorPackageRoot(member.entry, packageName);
1577
+ if (!packageName || !packageRoot || !isEsmModuleEntry(member.entry)) return undefined;
1578
+ return {
1579
+ key: `${config.outPath}\0${target}\0${conditionTarget}\0${packageRoot}`,
1580
+ member,
1581
+ build: (members, external, nested) =>
1582
+ buildVendorGroup(
1583
+ config,
1584
+ packageName,
1585
+ members,
1586
+ external,
1587
+ target,
1588
+ resolveDir,
1589
+ conditionTarget,
1590
+ nested,
1591
+ ),
1592
+ };
1593
+ }
1594
+
1595
+ /**
1596
+ * The installed package directory an entry belongs to — the grouping identity,
1597
+ * so a nested (non-hoisted) copy never shares a group with the hoisted one.
1598
+ */
1599
+ function vendorPackageRoot(entry: string, packageName: string) {
1600
+ const marker = `${path.sep}node_modules${path.sep}`;
1601
+ const index = entry.lastIndexOf(marker);
1602
+ if (index < 0) return undefined;
1603
+ return path.join(entry.slice(0, index + marker.length), packageName);
1604
+ }
1605
+
1606
+ /**
1607
+ * One esbuild pass for the subpaths demanded together, with the shared graph split into chunks so it
1608
+ * is parsed once instead of once per subpath. Siblings already on disk are externalized rather than
1609
+ * re-parsed, which keeps artifacts handed out earlier valid across a growth round.
1610
+ */
1611
+ async function buildVendorGroup(
1612
+ config: ResolvedConfig,
1613
+ packageName: string,
1614
+ members: readonly VendorGroupMember[],
1615
+ external: readonly VendorGroupMember[],
1616
+ target: CompatAliasTarget,
1617
+ resolveDir: string,
1618
+ conditionTarget: ServerBundleTarget,
1619
+ nested: boolean,
1620
+ ) {
1621
+ const built = members.filter(member => !existsSync(member.file));
1622
+ if (built.length === 0) return;
1623
+ // Only a sibling whose artifact is ON DISK can be externalized — one still
1624
+ // compiling (a solo build this round did not wait for) must be inlined, or
1625
+ // this bundle would import a file that is not there yet.
1626
+ const siblings = new Map(
1627
+ [...external, ...members]
1628
+ .filter(member => !built.includes(member) && existsSync(member.file))
1629
+ .map(member => [member.specifier, pathToFileHref(member.file)] as const),
1630
+ );
1631
+ const outdir = path.dirname(built[0]!.file);
1632
+ const context = { nestedExternalFiles: [] as string[] };
1633
+ const entryFiles = new Set(built.map(member => member.file));
1634
+ // The plugin-chain fallback builds from bare specifiers; hand the interop
1635
+ // plugin each member's already-resolved entry.
1636
+ for (const member of built) provideEntryResolution(member.specifier, resolveDir, member.entry);
1637
+ const entryPoints = built.map(member => ({
1638
+ in:
1639
+ getBundlerExtensions().serverBundleEntry(member.specifier, resolveDir, member.entry) ??
1640
+ member.specifier,
1641
+ out: path.basename(member.file, '.mjs'),
1642
+ }));
1643
+ const trace: VendorBuildTrace | undefined = vendorTraceEnabled()
1644
+ ? { plugins: [], entry: built[0]!.entry }
1645
+ : undefined;
1646
+ const traceSeq = trace ? nextVendorTraceSeq() : 0;
1647
+ const traceStart = performance.now();
1648
+ const traceRow = (outputs: { text: string }[] | undefined, error?: unknown) => {
1649
+ if (!trace) return;
1650
+ traceVendorBuild({
1651
+ seq: traceSeq,
1652
+ specifier: packageName,
1653
+ resolveDir,
1654
+ target,
1655
+ conditionTarget,
1656
+ conditions: serverBundleConditions(conditionTarget),
1657
+ startMs: traceStart,
1658
+ outBytes: (outputs ?? []).reduce((sum, output) => sum + output.text.length, 0),
1659
+ trace,
1660
+ ok: !error,
1661
+ ...(error
1662
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
1663
+ ? { error: (error instanceof Error ? error.message : String(error)).slice(0, 300) }
1664
+ : {}),
1665
+ });
1666
+ };
1667
+ const preplan =
1668
+ vendorPreplanEnabled() &&
1669
+ (
1670
+ await Promise.all(
1671
+ built.map(member =>
1672
+ vendorPreplanPackageSafe(config, packageName, member.entry, target, conditionTarget),
1673
+ ),
1674
+ )
1675
+ ).every(Boolean);
1676
+ let outputFiles;
1677
+ try {
1678
+ outputFiles = await profileRuntimeStep(
1679
+ `vendor bundle ${packageName} [${target}/${conditionTarget}] (${built.length} entries)`,
1680
+ async () =>
1681
+ (await buildVendorNative({
1682
+ preplan,
1683
+ config,
1684
+ target,
1685
+ conditionTarget,
1686
+ resolveDir,
1687
+ bailKey: `${packageName}\0${target}\0${conditionTarget}\0${outdir}`,
1688
+ packageName,
1689
+ // Resolved entries, not the bare specifiers the plugin build uses:
1690
+ // `packages: 'external'` refuses to bundle a bare entry point.
1691
+ entryPoints: built.map(member => ({
1692
+ in: member.entry,
1693
+ out: path.basename(member.file, '.mjs'),
1694
+ })),
1695
+ outdir,
1696
+ splitting: true,
1697
+ siblings,
1698
+ ownEntries: new Map(built.map(member => [member.entry, member.file])),
1699
+ isEntry: output => entryFiles.has(output),
1700
+ nested,
1701
+ context,
1702
+ trace,
1703
+ })) ??
1704
+ (await buildVendorGroupWithPlugins(
1705
+ config,
1706
+ packageName,
1707
+ siblings,
1708
+ target,
1709
+ resolveDir,
1710
+ conditionTarget,
1711
+ outdir,
1712
+ entryPoints,
1713
+ context,
1714
+ trace,
1715
+ preplan,
1716
+ )),
1717
+ );
1718
+ } catch (error) {
1719
+ traceRow(undefined, error);
1720
+ throw error;
1721
+ }
1722
+ traceRow(outputFiles);
1723
+ await mkdir(path.join(outdir, 'chunks'), { recursive: true });
1724
+ // Chunks first: an entry must never be readable before what it imports is.
1725
+ for (const output of [...outputFiles].sort(
1726
+ (a, b) => Number(entryFiles.has(a.path)) - Number(entryFiles.has(b.path)),
1727
+ )) {
1728
+ const bundled = entryFiles.has(output.path)
1729
+ ? addCommonJsNamedExports(output.text)
1730
+ : output.text;
1731
+ // Packages can ship 'use cache' functions too (Next allows it); client
1732
+ // bundles keep the source untouched.
1733
+ const code =
1734
+ target === 'server' ? applyBundledSourceTransforms(bundled, output.path) : bundled;
1735
+ await writeVendorArtifact(output.path, code);
1736
+ }
1737
+ await emitNestedExternalTraceCopies(built[0]!.file, context.nestedExternalFiles);
1738
+ }
1739
+
1740
+ // vendor plugin-callback attribution (PNEXT_VENDOR_PROFILE=1, dormant otherwise). Every
1741
+ // onResolve/onLoad is an IPC round trip esbuild's bundler blocks on, against the one JS thread that
1742
+ // answers them; counts separate "our callbacks are slow" from "we answer many questions".
1743
+
1744
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1745
+ const vendorProfileEnabled = process.env.PNEXT_VENDOR_PROFILE === '1';
1746
+ const vendorProfileTotals = new Map<string, number>();
1747
+
1748
+ function vendorProfileCount(key: string, by = 1) {
1749
+ vendorProfileTotals.set(key, (vendorProfileTotals.get(key) ?? 0) + by);
1750
+ }
1751
+
1752
+ export function vendorProfileReport() {
1753
+ return Object.fromEntries(
1754
+ [...vendorProfileTotals].sort((a, b) => b[1] - a[1]).map(([k, v]) => [k, Math.round(v)]),
1755
+ );
1756
+ }
1757
+
1758
+ // Printed once the callbacks go quiet, so one page's whole plugin chain lands
1759
+ // in a single line instead of interleaving with the build it is measuring.
1760
+ let vendorProfileFlush: ReturnType<typeof setTimeout> | undefined;
1761
+ function scheduleVendorProfileReport() {
1762
+ if (vendorProfileFlush) clearTimeout(vendorProfileFlush);
1763
+ vendorProfileFlush = setTimeout(() => {
1764
+ console.log(`pnext vendor profile ${JSON.stringify(vendorProfileReport())}`);
1765
+ }, 3000);
1766
+ vendorProfileFlush.unref?.();
1767
+ }
1768
+
1769
+ function profiledVendorPlugins(plugins: Plugin[]): Plugin[] {
1770
+ if (!vendorProfileEnabled) return plugins;
1771
+ return plugins.map(plugin => ({
1772
+ name: plugin.name,
1773
+ setup(build) {
1774
+ const wrap =
1775
+ (kind: 'resolve' | 'load', register: (options: any, cb: any) => void) =>
1776
+ (options: any, callback: any) =>
1777
+ register(options, async (args: any) => {
1778
+ vendorProfileCount(`${plugin.name}:${kind}#`);
1779
+ const start = performance.now();
1780
+ try {
1781
+ return await (callback as (a: unknown) => unknown)(args);
1782
+ } finally {
1783
+ vendorProfileCount(`${plugin.name}:${kind}ms`, performance.now() - start);
1784
+ scheduleVendorProfileReport();
1785
+ }
1786
+ });
1787
+ return plugin.setup({
1788
+ ...build,
1789
+ onResolve: wrap('resolve', build.onResolve.bind(build)),
1790
+ onLoad: wrap('load', build.onLoad.bind(build)),
1791
+ });
1792
+ },
1793
+ }));
1794
+ }
1795
+
1796
+ /** The plugin chain, for whatever the plugin-free build could not express. */
1797
+ async function buildVendorGroupWithPlugins(
1798
+ config: ResolvedConfig,
1799
+ packageName: string,
1800
+ siblings: ReadonlyMap<string, string>,
1801
+ target: CompatAliasTarget,
1802
+ resolveDir: string,
1803
+ conditionTarget: ServerBundleTarget,
1804
+ outdir: string,
1805
+ entryPoints: { in: string; out: string }[],
1806
+ context: { nestedExternalFiles: string[] },
1807
+ trace?: VendorBuildTrace,
1808
+ preplan = false,
1809
+ ) {
1810
+ const pluginStart = performance.now();
1811
+ const groupPlugins = profiledVendorPlugins([
1812
+ serverAssetPlugin(config),
1813
+ ...getBundlerExtensions().serverEsbuildPlugins(
1814
+ config,
1815
+ vendorPluginOptions(config, target, conditionTarget),
1816
+ ),
1817
+ runtimeAliasBuildPlugin(config, target, {
1818
+ bundleRequireAliases: true,
1819
+ reactServerLayer: target === 'server',
1820
+ }),
1821
+ vendorGroupSiblingPlugin(siblings),
1822
+ externalPackageDependencyPlugin(config, packageName, target, conditionTarget, context, preplan),
1823
+ externalBuiltinBuildPlugin(),
1824
+ ]);
1825
+ if (trace) trace.plugins = groupPlugins.map(plugin => plugin.name);
1826
+ const result = await build({
1827
+ entryPoints,
1828
+ absWorkingDir: resolveDir,
1829
+ bundle: true,
1830
+ splitting: true,
1831
+ write: false,
1832
+ outdir,
1833
+ outExtension: { '.js': '.mjs' },
1834
+ chunkNames: 'chunks/[name]-[hash]',
1835
+ format: 'esm',
1836
+ platform: 'neutral',
1837
+ target: 'es2022',
1838
+ conditions: serverBundleConditions(conditionTarget),
1839
+ mainFields: ['module', 'main'],
1840
+ loader: packageJsxLoaders,
1841
+ jsx: 'automatic',
1842
+ jsxImportSource: 'preact',
1843
+ logLevel: 'silent',
1844
+ // Trace-only: the analyzer's per-build input set comes from the metafile.
1845
+ ...(trace ? { metafile: true as const } : {}),
1846
+ ...serverDefineOptions(),
1847
+ plugins: groupPlugins,
1848
+ });
1849
+ vendorNativeStats.pluginBuilds += 1;
1850
+ vendorNativeStats.pluginBuildMs += performance.now() - pluginStart;
1851
+ if (trace) trace.metafile = result.metafile;
1852
+ return result.outputFiles.map(output => ({ path: output.path, text: output.text }));
1853
+ }
1854
+
1855
+ /** Siblings already compiled stay out of the graph, as plain externals. */
1856
+ function vendorGroupSiblingPlugin(siblings: ReadonlyMap<string, string>): Plugin {
1857
+ return {
1858
+ name: 'pnext-vendor-group-siblings',
1859
+ setup(build) {
1860
+ if (siblings.size === 0) return;
1861
+ build.onResolve({ filter: /^[^./].*/ }, args => {
1862
+ if (args.kind === 'entry-point') return undefined;
1863
+ const sibling = siblings.get(args.path);
1864
+ return sibling ? { path: sibling, external: true } : undefined;
1865
+ });
1866
+ },
1867
+ };
1868
+ }
1869
+
1870
+ // per-file client boundaries inside a package.
1871
+ //
1872
+ // A package ships its client half as ordinary files carrying a `'use client'` directive next to a
1873
+ // directive-less index that re-exports them. From the react-server graph such a file is a client
1874
+ // REFERENCE - never code the RSC pass runs - and the module pass already stubs the ones it resolves
1875
+ // itself. A vendor bundle reaches them mid-graph instead, where no resolve of ours ever sees the
1876
+ // specifier, so the directive was lost inside the bundle: the RSC pass then executed a client
1877
+ // component and the app got a SECOND copy of every module the client profile also bundles - two
1878
+ // `createContext` calls, so a provider rendered from the client graph is invisible to a consumer
1879
+ // rendered from this one.
1880
+ //
1881
+ // The scan is off wherever it cannot matter: client-target bundles (the SSR pass must run the real
1882
+ // component) and non-react apps. On the ones that remain it costs a read per input file, which
1883
+ // esbuild was going to make anyway. `PNEXT_VENDOR_CLIENT_BOUNDARY=0` removes the plugin, to bisect.
1884
+
1885
+ // No longer a plugin of its own: the scan is composed into the server chain's
1886
+ // single claiming onLoad (ServerEsbuildPluginOptions.vendorClientBoundary), so
1887
+ // it adds zero plugin-callback round trips. Reads are memoized per config —
1888
+ // the same node_modules bytes used to be re-read and re-decoded per build.
1889
+ const vendorClientBoundaryLoads = new WeakMap<
1890
+ ResolvedConfig,
1891
+ (file: string) => Promise<OnLoadResult | undefined>
1892
+ >();
1893
+
1894
+ function vendorClientBoundaryLoad(config: ResolvedConfig) {
1895
+ let load = vendorClientBoundaryLoads.get(config);
1896
+ if (load) return load;
1897
+ const memo = new Map<string, Promise<OnLoadResult | undefined>>();
1898
+ const scan = async (file: string): Promise<OnLoadResult | undefined> => {
1899
+ const source = await readFile(file, 'utf8').catch(() => undefined);
1900
+ if (source === undefined || !hasUseClientDirective(source)) return undefined;
1901
+ return {
1902
+ contents: clientReferenceModuleSource(
1903
+ file,
1904
+ await clientReferenceExportNames(config, file, source),
1905
+ { inlineSymbol: true },
1906
+ ),
1907
+ loader: 'js',
1908
+ resolveDir: path.dirname(file),
1909
+ };
1910
+ };
1911
+ // Only node_modules answers are cached: immutable for the process life, the
1912
+ // same assumption the vendor artifact cache makes. First-party files re-read.
1913
+ load = file => {
1914
+ if (!file.includes(`${path.sep}node_modules${path.sep}`)) return scan(file);
1915
+ let pending = memo.get(file);
1916
+ if (!pending) {
1917
+ pending = scan(file);
1918
+ memo.set(file, pending);
1919
+ }
1920
+ return pending;
1921
+ };
1922
+ vendorClientBoundaryLoads.set(config, load);
1923
+ return load;
1924
+ }
1925
+
1926
+ /**
1927
+ * Whether the react-server vendor pass scans this build's inputs at all. `target === 'server'` alone
1928
+ * is not the react-server graph: the PAGES router compiles under it too, and there `'use client'` is
1929
+ * inert - Next never runs an RSC pass for a pages route, so the file must be EXECUTED, not stubbed.
1930
+ * Stubbing it there turns a plain exported value into a truthy reference object, which crashes SSR.
1931
+ */
1932
+ function vendorPluginOptions(
1933
+ config: ResolvedConfig,
1934
+ target: CompatAliasTarget,
1935
+ conditionTarget: ServerBundleTarget,
1936
+ ): ServerEsbuildPluginOptions {
1937
+ const boundary =
1938
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1939
+ process.env.PNEXT_VENDOR_CLIENT_BOUNDARY !== '0' &&
1940
+ target === 'server' &&
1941
+ !isPagesBundleTarget(conditionTarget) &&
1942
+ reactCompatEnabled(config)
1943
+ ? vendorClientBoundaryLoad(config)
1944
+ : undefined;
1945
+ return { vendorClientBoundary: boundary, realPathEntries: true };
1946
+ }
1947
+
1948
+ function isPagesBundleTarget(target: ServerBundleTarget) {
1949
+ return target === 'pages' || target === 'pages-api' || target === 'pages-api-edge';
1950
+ }
1951
+
1952
+ // plugin-free vendor builds.
1953
+ //
1954
+ // The four core plugins above resolve by callback, and a callback is a JS bridge hop the bundler
1955
+ // waits on. Each behaviour leaves the callback in one of two ways - an esbuild-native option where
1956
+ // it is expressible, or a post-pass over the emitted chunks, where the metafile hands us the same
1957
+ // (importer, specifier, kind) triples the resolve callback used to see:
1958
+ //
1959
+ // builtins -> `packages: 'external'` covers them, nothing to do
1960
+ // asset stubs -> native `loader: empty`; an extension the map does not cover has no
1961
+ // loader, so the build fails and falls back - the safe direction
1962
+ // framework aliases -> external by default, post-pass repoints each one at its alias target
1963
+ // (require-kind separately, against the react-server layer map)
1964
+ // external packages -> external by default, post-pass runs the policy: nested-version pinning,
1965
+ // transitive vendor recursion, CommonJS facades
1966
+ //
1967
+ // What must be BUNDLED rather than externalized - a package's own subpaths, a default-only
1968
+ // CommonJS dependency, a cross-package asset - is named in `alias`, which overrides
1969
+ // `packages: 'external'` for exactly those specifiers and nothing else. That set is a property of
1970
+ // the package, so it is discovered once (`vendorPackageShape`) and every later build starts with it.
1971
+ // Anything still unexpressible bails to the plugin build; bailing is always available and always
1972
+ // correct, so this path never has to be complete - it has to be RIGHT, which is what the re-bundle
1973
+ // gate proves.
1974
+ //
1975
+ // Off by default: this pipeline is not spending its time in plugin callbacks, and two next-compat
1976
+ // behaviours still diverge - see `vendorNativeEnabled`.
1977
+
1978
+ /**
1979
+ * OFF by default: it is measurably slower AND not yet equivalent. `app-external` loses four
1980
+ * assertions with it on - `transpilePackages`, the react-server export condition, CJS tree-shaking
1981
+ * and an async external module all 500 - which the re-bundle gate cannot catch, because those
1982
+ * artifacts are structurally sound and semantically wrong.
1983
+ */
1984
+ function vendorNativeEnabled() {
1985
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1986
+ return process.env.PNEXT_VENDOR_NATIVE === '1';
1987
+ }
1988
+
1989
+ /**
1990
+ * Artifacts whose plugin-free build did not work out. One bail is enough: the
1991
+ * cause is a property of the package (a require-kind alias to a CommonJS shim,
1992
+ * an asset needing real contents), not of the demand, so a retry pays for the
1993
+ * discarded build again and lands in the same place.
1994
+ */
1995
+ const vendorNativeBailed = new Set<string>();
1996
+
1997
+ /** How the vendor builds of this process split between the two paths. */
1998
+ const vendorNativeStats = {
1999
+ attempts: 0,
2000
+ native: 0,
2001
+ bailed: 0,
2002
+ rounds: 0,
2003
+ buildMs: 0,
2004
+ pluginBuilds: 0,
2005
+ pluginBuildMs: 0,
2006
+ planMs: 0,
2007
+ verifyMs: 0,
2008
+ reasons: [] as string[],
2009
+ };
2010
+
2011
+ export function vendorNativeReport() {
2012
+ return { ...vendorNativeStats, reasons: vendorNativeStats.reasons.slice(0, 40) };
2013
+ }
2014
+
2015
+ /**
2016
+ * What the asset plugin stubs, minus the extensions whose stub has CONTENT
2017
+ * (images, CSS modules). Those have no loader here on purpose: esbuild fails
2018
+ * the build, and failing is how they reach the plugin path.
2019
+ */
2020
+ const vendorEmptyLoaders = {
2021
+ '.css': 'empty',
2022
+ '.scss': 'empty',
2023
+ '.sass': 'empty',
2024
+ '.less': 'empty',
2025
+ } satisfies Record<string, Loader>;
2026
+
2027
+ /** Discovery is a fixpoint; past this a package is not worth another try. */
2028
+ const MAX_VENDOR_NATIVE_ROUNDS = 2;
2029
+
2030
+ interface VendorNativeRequest {
2031
+ config: ResolvedConfig;
2032
+ target: CompatAliasTarget;
2033
+ conditionTarget: ServerBundleTarget;
2034
+ resolveDir: string;
2035
+ /** Cache key for the bail set — the artifact this build is producing. */
2036
+ bailKey: string;
2037
+ /** The package the entries belong to; its own subpaths are inlined. */
2038
+ packageName: string | undefined;
2039
+ entryPoints: { in: string; out: string }[] | string[];
2040
+ outdir?: string;
2041
+ splitting: boolean;
2042
+ /** Demanded siblings already on disk (group builds only). */
2043
+ siblings?: ReadonlyMap<string, string>;
2044
+ /** Entries THIS build emits, by resolved entry file: artifacts-to-be. */
2045
+ ownEntries?: ReadonlyMap<string, string>;
2046
+ /** Raised from inside another build: it may never wait on anything shared. */
2047
+ nested: boolean;
2048
+ /** Preplan-safe package: native build with pure ref decisions, no verify. */
2049
+ preplan?: boolean;
2050
+ /** Entry outputs, by emitted path; everything else is a shared chunk. */
2051
+ isEntry: (outputPath: string) => boolean;
2052
+ /** Where a solo build's single output lands, for the verification gate. */
2053
+ soloFile?: string;
2054
+ context: { nestedExternalFiles: string[] };
2055
+ /** Trace-only: filled with the producing pass's plugins/metafile. */
2056
+ trace?: VendorBuildTrace;
2057
+ }
2058
+
2059
+ /** A repointed reference: the specifier to emit, and the file it must exist at. */
2060
+ interface VendorRefTarget {
2061
+ specifier: string;
2062
+ file: string;
2063
+ }
2064
+
2065
+ /**
2066
+ * Run the plugin-free build, or return undefined to say "use the plugins". Every return path other than the
2067
+ * verified one is a bail, so a caller can treat undefined as "nothing happened" - no artifact is written here.
2068
+ */
2069
+ async function buildVendorNative(request: VendorNativeRequest) {
2070
+ vendorNativeStats.attempts += 1;
2071
+ if (
2072
+ (!vendorNativeEnabled() && !request.preplan) ||
2073
+ vendorNativeBailed.has(request.bailKey)
2074
+ ) {
2075
+ return undefined;
2076
+ }
2077
+ // A bail is remembered so the next demand does not pay for the same
2078
+ // discarded build — unless the cause was the fixpoint running out of rounds,
2079
+ // which the package shape this build just recorded has already moved on from.
2080
+ const bail = (why: string, permanent = true) => {
2081
+ if (permanent) vendorNativeBailed.add(request.bailKey);
2082
+ vendorNativeStats.bailed += 1;
2083
+ vendorNativeStats.reasons.push(`${request.packageName ?? '?'}: ${why}`);
2084
+ return undefined;
2085
+ };
2086
+
2087
+ const conditions = serverBundleConditions(request.conditionTarget);
2088
+ // What a package has to keep INSIDE its bundle is a property of the package,
2089
+ // not of one subpath's build, so it is discovered once and reused by every
2090
+ // later build of it — the fixpoint the §4e prototype ran app-wide, amortized
2091
+ // per package here.
2092
+ const shape = vendorPackageShape(request);
2093
+ // Only the FIRST build of a package discovers its shape; the rest wait for that answer instead of racing to
2094
+ // the same rounds. Off the slot, because a waiter that keeps its worker is exactly the inversion this is
2095
+ // here to fix. A nested build never waits - its own parent is holding a slot for it, and a cycle through two
2096
+ // packages would otherwise have nowhere to go.
2097
+ if (!request.nested && shape.discovery) {
2098
+ await outsideVendorSlot(() => shape.discovery!);
2099
+ }
2100
+ const pioneer = !shape.discovery;
2101
+ let settleDiscovery: (() => void) | undefined;
2102
+ if (pioneer) shape.discovery = new Promise<void>(resolve => (settleDiscovery = resolve));
2103
+ try {
2104
+ return await runVendorNativeRounds(request, shape, conditions, bail);
2105
+ } finally {
2106
+ if (pioneer) {
2107
+ shape.discovery = undefined;
2108
+ settleDiscovery?.();
2109
+ }
2110
+ }
2111
+ }
2112
+
2113
+ async function runVendorNativeRounds(
2114
+ request: VendorNativeRequest,
2115
+ shape: VendorPackageShape,
2116
+ conditions: string[],
2117
+ bail: (why: string, permanent?: boolean) => undefined,
2118
+ ) {
2119
+ const { config, resolveDir } = request;
2120
+
2121
+ for (let round = 1; round <= MAX_VENDOR_NATIVE_ROUNDS; round += 1) {
2122
+ let result;
2123
+ vendorNativeStats.rounds += 1;
2124
+ const buildStart = performance.now();
2125
+ const nativePlugins = getBundlerExtensions().serverEsbuildPlugins(
2126
+ config,
2127
+ vendorPluginOptions(config, request.target, request.conditionTarget),
2128
+ );
2129
+ if (request.trace) request.trace.plugins = nativePlugins.map(plugin => plugin.name);
2130
+ try {
2131
+ result = await build({
2132
+ entryPoints: (request.entryPoints as (string | { in: string; out: string })[]).map(
2133
+ entry =>
2134
+ typeof entry === 'string'
2135
+ ? (getBundlerExtensions().serverBundleEntry(entry, resolveDir, entry) ?? entry)
2136
+ : {
2137
+ ...entry,
2138
+ in:
2139
+ getBundlerExtensions().serverBundleEntry(entry.in, resolveDir, entry.in) ??
2140
+ entry.in,
2141
+ },
2142
+ ) as string[],
2143
+ absWorkingDir: resolveDir,
2144
+ bundle: true,
2145
+ ...(request.splitting ? { splitting: true, chunkNames: 'chunks/[name]-[hash]' } : {}),
2146
+ write: false,
2147
+ ...(request.outdir ? { outdir: request.outdir, outExtension: { '.js': '.mjs' } } : {}),
2148
+ format: 'esm',
2149
+ platform: 'neutral',
2150
+ target: 'es2022',
2151
+ conditions,
2152
+ mainFields: ['module', 'main'],
2153
+ loader: { ...packageJsxLoaders, ...vendorEmptyLoaders },
2154
+ jsx: 'automatic',
2155
+ jsxImportSource: 'preact',
2156
+ logLevel: 'silent',
2157
+ metafile: true,
2158
+ ...serverDefineOptions(),
2159
+ // Every bare specifier out — the cheapest possible graph — except the
2160
+ // handful `alias` names, which is how a same-package subpath or a
2161
+ // default-only CommonJS dependency stays INSIDE without giving up
2162
+ // externalization for everything else.
2163
+ packages: 'external',
2164
+ ...(shape.inline.size > 0 ? { alias: Object.fromEntries(shape.inline) } : {}),
2165
+ plugins: nativePlugins,
2166
+ });
2167
+ } catch (error) {
2168
+ vendorNativeStats.buildMs += performance.now() - buildStart;
2169
+ return bail((error instanceof Error ? error.message : String(error)).slice(0, 200));
2170
+ }
2171
+ vendorNativeStats.buildMs += performance.now() - buildStart;
2172
+
2173
+ const inputs = Object.keys(result.metafile.inputs);
2174
+ // An asset whose stub carries content, or one a configured loader rule
2175
+ // claims, is not what `loader: empty` produced. Hand it to the plugins.
2176
+ for (const input of inputs) {
2177
+ const file = path.resolve(resolveDir, input);
2178
+ if (!serverIgnoredAssetFilter.test(file)) continue;
2179
+ if (getAssetExtensions().hasLoaderRuleFor(file)) return bail(`loader rule for ${input}`);
2180
+ if (getCssExtensions().loadCssModuleForClient(file) !== undefined) {
2181
+ return bail(`css module ${input}`);
2182
+ }
2183
+ }
2184
+ // Preplan class is ESM-only across the GRAPH, not just the entry.
2185
+ if (request.preplan) {
2186
+ for (const input of inputs) {
2187
+ const file = path.resolve(resolveDir, input);
2188
+ if (serverIgnoredAssetFilter.test(file) || !cachedExistsSync(file)) continue;
2189
+ if (/\.[cm]?tsx?$/.test(file)) return bail(`ts input ${input}`);
2190
+ if (file.endsWith('.cjs') ||(/\.jsx?$/.test(file) && !isEsmModuleEntry(file))) {
2191
+ return bail(`cjs input ${input}`);
2192
+ }
2193
+ }
2194
+ }
2195
+
2196
+ // Off the slot: everything below is discovery, and the bundler that needed
2197
+ // the worker has already handed its output over. Nothing else in the
2198
+ // pipeline can do this — the plugin chain's discovery runs inside a resolve
2199
+ // callback, with the bundler still open behind it.
2200
+ const planStart = performance.now();
2201
+ const plan = await outsideVendorSlot(() =>
2202
+ planVendorNativeRefs(request, result.metafile, conditions),
2203
+ );
2204
+ vendorNativeStats.planMs += performance.now() - planStart;
2205
+ if (!plan) return bail('unrepresentable reference');
2206
+ if (plan.inline.size > 0) {
2207
+ // Record before deciding: even a bail teaches the package's shape, so the
2208
+ // next build of it starts where this one gave up.
2209
+ let grew = false;
2210
+ for (const [specifier, file] of plan.inline) {
2211
+ if (shape.inline.has(specifier)) continue;
2212
+ shape.inline.set(specifier, file);
2213
+ grew = true;
2214
+ }
2215
+ if (!grew) return bail('inline set stopped converging');
2216
+ if (round === MAX_VENDOR_NATIVE_ROUNDS) return bail('inlining unsettled', false);
2217
+ continue;
2218
+ }
2219
+
2220
+ const outputs = result.outputFiles.map(output => ({
2221
+ path: request.soloFile ?? output.path,
2222
+ text: rewriteEmittedRefs(output.text, ref =>
2223
+ (ref.require ? plan.requires : plan.imports).get(ref.specifier)?.specifier,
2224
+ ),
2225
+ }));
2226
+ const repointed = new Map(
2227
+ [...plan.imports.values(), ...plan.requires.values()].map(t => [t.specifier, t.file]),
2228
+ );
2229
+ // Preplan mode: the drain barrier + byte-eq gates stand in for the
2230
+ // re-bundle verification loop — its cost is one of the two this composes out.
2231
+ if (!request.preplan) {
2232
+ const verifyStart = performance.now();
2233
+ const chunks = new Map(
2234
+ outputs.filter(output => !request.isEntry(output.path)).map(o => [o.path, o.text]),
2235
+ );
2236
+ for (const output of outputs) {
2237
+ if (!request.isEntry(output.path)) continue;
2238
+ const failure = await verifyVendorArtifact(output.path, output.text, repointed, chunks);
2239
+ if (failure) {
2240
+ vendorNativeStats.verifyMs += performance.now() - verifyStart;
2241
+ return bail(`verification: ${failure}`);
2242
+ }
2243
+ }
2244
+ vendorNativeStats.verifyMs += performance.now() - verifyStart;
2245
+ } else {
2246
+ vendorPreplanStats.composedBuilds += 1;
2247
+ schedulePreplanReport();
2248
+ }
2249
+ vendorNativeStats.native += 1;
2250
+ if (request.trace) request.trace.metafile = result.metafile;
2251
+ return outputs;
2252
+ }
2253
+ return undefined;
2254
+ }
2255
+
2256
+ /**
2257
+ * What one package externalizes and what it keeps inside - discovered once and then reused, so the
2258
+ * rounds a package costs are paid by its FIRST build and no other. Seeded from the manifest: a
2259
+ * declared dependency is external before any build proves it, which keeps a package's own subpaths
2260
+ * (the ones `packages: 'external'` cannot tell apart from dependencies) bundled from the start.
2261
+ */
2262
+ interface VendorPackageShape {
2263
+ /** Specifier -> file it must be bundled from, as an esbuild `alias` entry. */
2264
+ inline: Map<string, string>;
2265
+ /** Set while one build is discovering it; every other build awaits this. */
2266
+ discovery?: Promise<void>;
2267
+ }
2268
+
2269
+ const vendorPackageShapes = new Map<string, VendorPackageShape>();
2270
+
2271
+ function vendorPackageShape(request: VendorNativeRequest): VendorPackageShape {
2272
+ const entry = (request.entryPoints as { in?: string }[])[0];
2273
+ const first = typeof entry === 'string' ? entry : entry?.in;
2274
+ // A workspace package resolves to its real source path, which has no
2275
+ // `node_modules` segment to key on — walk up to its manifest instead.
2276
+ const root =
2277
+ (first && request.packageName && vendorPackageRoot(first, request.packageName)) ??
2278
+ (first && nearestManifestDir(path.dirname(first)));
2279
+ if (!root) return { inline: new Map() };
2280
+ const key = `${root}\0${request.target}\0${request.conditionTarget}`;
2281
+ let shape = vendorPackageShapes.get(key);
2282
+ if (!shape) {
2283
+ shape = { inline: new Map() };
2284
+ vendorPackageShapes.set(key, shape);
2285
+ }
2286
+ return shape;
2287
+ }
2288
+
2289
+ function nearestManifestDir(from: string) {
2290
+ let dir = from;
2291
+ while (true) {
2292
+ if (existsSync(path.join(dir, 'package.json'))) return dir;
2293
+ const parent = path.dirname(dir);
2294
+ if (parent === dir) return undefined;
2295
+ dir = parent;
2296
+ }
2297
+ }
2298
+
2299
+ /**
2300
+ * Decide what each external reference in the emitted code means - the work the resolve callbacks used
2301
+ * to do, off the bundler's critical path. Undefined when a reference cannot be expressed as a rewrite
2302
+ * (the caller then bails), or the set of specifiers that must be pulled back INSIDE the bundle.
2303
+ */
2304
+ async function planVendorNativeRefs(
2305
+ request: VendorNativeRequest,
2306
+ metafile: Metafile,
2307
+ conditions: string[],
2308
+ ) {
2309
+ const imports = new Map<string, VendorRefTarget>();
2310
+ const requires = new Map<string, VendorRefTarget>();
2311
+ const inline = new Map<string, string>();
2312
+ const settled = new Set<string>();
2313
+
2314
+ for (const [input, meta] of Object.entries(metafile.inputs)) {
2315
+ // Decisions are a property of the DIRECTORY an import sits in, never of the
2316
+ // file — so a package's several hundred modules all asking for `react`
2317
+ // decide once, and the answer is reused across builds.
2318
+ const importerFile = path.resolve(request.resolveDir, input);
2319
+ const importerDir = path.dirname(importerFile);
2320
+ for (const reference of meta.imports) {
2321
+ if (!reference.external) continue;
2322
+ const specifier = reference.path;
2323
+ const isRequire = reference.kind === 'require-call';
2324
+ const key = `${isRequire ? 'r' : 'i'}\0${specifier}\0${importerDir}`;
2325
+ if (settled.has(key)) continue;
2326
+ settled.add(key);
2327
+
2328
+ // Same package. A subpath THIS build already emits is repointed at that artifact; a sibling on disk at
2329
+ // its own. Everything else becomes its own vendor demand rather than being inlined here - inlining is
2330
+ // what makes a package's shared graph get re-parsed once per subpath, the cost this whole pipeline
2331
+ // exists to remove. Request-scoped, so it sits in front of the shared decision cache.
2332
+ if (packageNameFromSpecifier(specifier) === request.packageName) {
2333
+ const sibling = request.siblings?.get(specifier);
2334
+ if (sibling) {
2335
+ if (!record(imports, specifier, { specifier: sibling, file: fileURLToPath(sibling) })) {
2336
+ return undefined;
2337
+ }
2338
+ continue;
2339
+ }
2340
+ const own = ownEntryArtifact(request, specifier, importerDir, conditions);
2341
+ if (own) {
2342
+ if (!record(imports, specifier, own)) return undefined;
2343
+ continue;
2344
+ }
2345
+ // A subpath nothing demanded: it comes back inside, as the plugin build
2346
+ // would have had it. Demanding it instead can close a cycle back onto
2347
+ // the artifact being built right now, which no scheduler can settle.
2348
+ const resolved = resolveVendorPackageSpecifier(
2349
+ request.config.root,
2350
+ path.join(importerDir, 'pnext-resolve.ts'),
2351
+ specifier,
2352
+ conditions,
2353
+ );
2354
+ if (!resolved) return undefined;
2355
+ inline.set(specifier, resolved);
2356
+ continue;
2357
+ }
2358
+
2359
+ const decision = await (request.preplan
2360
+ ? preplanVendorRef(request, specifier, importerFile, isRequire, conditions)
2361
+ : decideVendorRef(request, specifier, importerFile, isRequire, conditions));
2362
+ if (!decision) return undefined;
2363
+ if (decision.kind === 'keep') continue;
2364
+ if (decision.kind === 'inline') {
2365
+ inline.set(specifier, decision.file);
2366
+ continue;
2367
+ }
2368
+ if (decision.nested) request.context.nestedExternalFiles.push(decision.nested);
2369
+ if (!record(isRequire ? requires : imports, specifier, decision.target)) return undefined;
2370
+ }
2371
+ }
2372
+ return { imports, requires, inline };
2373
+ }
2374
+
2375
+ /**
2376
+ * The artifact THIS build is already emitting for a same-package specifier, if it is one of its entries. It
2377
+ * is not on disk yet - it is written the moment this build returns - so it is repointed without an existence
2378
+ * check.
2379
+ */
2380
+ function ownEntryArtifact(
2381
+ request: VendorNativeRequest,
2382
+ specifier: string,
2383
+ importerDir: string,
2384
+ conditions: string[],
2385
+ ): VendorRefTarget | undefined {
2386
+ if (!request.ownEntries?.size) return undefined;
2387
+ const entry = resolveVendorPackageSpecifier(
2388
+ request.config.root,
2389
+ path.join(importerDir, 'pnext-resolve.ts'),
2390
+ specifier,
2391
+ conditions,
2392
+ );
2393
+ const artifact = entry && request.ownEntries.get(entry);
2394
+ return artifact ? { specifier: pathToFileHref(artifact), file: artifact } : undefined;
2395
+ }
2396
+
2397
+ type VendorRefDecision =
2398
+ | { kind: 'keep' }
2399
+ /** Must be bundled: `file` is what an alias entry points the specifier at. */
2400
+ | { kind: 'inline'; file: string }
2401
+ | { kind: 'repoint'; target: VendorRefTarget; nested?: string };
2402
+
2403
+ /**
2404
+ * Decisions are pure in (layer, specifier, importing directory), and the same
2405
+ * handful of packages are imported from everywhere, so one answer serves every
2406
+ * build that meets the reference again. Without this the post-pass re-runs the
2407
+ * resolver work the plugin chain at least only did once per bundle.
2408
+ */
2409
+ const vendorRefDecisions = new Map<string, Promise<VendorRefDecision | undefined>>();
2410
+
2411
+ /** Everything the plugin-free path learned about packages, forgotten. */
2412
+ function clearVendorNativeCaches() {
2413
+ vendorRefDecisions.clear();
2414
+ vendorNativeBailed.clear();
2415
+ vendorPackageShapes.clear();
2416
+ }
2417
+
2418
+ /**
2419
+ * Preplan replacement for `decideVendorRef`: the same classification from pure inputs only - no awaited child
2420
+ * builds, no facade exec. Anything it cannot settle purely returns undefined, bailing the whole package to
2421
+ * the plugin path. Cached beside the plugin-era decisions under a distinct key prefix.
2422
+ */
2423
+ function preplanVendorRef(
2424
+ request: VendorNativeRequest,
2425
+ specifier: string,
2426
+ importerFile: string,
2427
+ isRequire: boolean,
2428
+ conditions: string[],
2429
+ ) {
2430
+ const importerDir = path.dirname(importerFile);
2431
+ const key = `p\0${request.target}\0${request.conditionTarget}\0${isRequire ? 'r' : 'i'}\0${specifier}\0${importerDir}`;
2432
+ let decision = vendorRefDecisions.get(key);
2433
+ if (!decision) {
2434
+ decision = preplanVendorRefUncached(request, specifier, importerFile, isRequire, conditions);
2435
+ vendorRefDecisions.set(key, decision);
2436
+ }
2437
+ return decision;
2438
+ }
2439
+
2440
+ async function preplanVendorRefUncached(
2441
+ request: VendorNativeRequest,
2442
+ specifier: string,
2443
+ importerFile: string,
2444
+ isRequire: boolean,
2445
+ conditions: string[],
2446
+ ): Promise<VendorRefDecision | undefined> {
2447
+ const { config, target } = request;
2448
+ const importerDir = path.dirname(importerFile);
2449
+ const keep = { kind: 'keep' } as const;
2450
+ const depName = packageNameFromSpecifier(specifier);
2451
+ if (depName && (builtinModules.includes(depName) || isBunBuiltin(depName))) return keep;
2452
+ // Require-of-external is the async-interop shape that 500ed; not this class.
2453
+ if (isRequire) return undefined;
2454
+ if (aliasSpecifierFilter.test(specifier)) {
2455
+ const resolved = coreAliases(config, target)[specifier] ?? firstAliasForSpecifier(specifier);
2456
+ if (!resolved) return undefined;
2457
+ if (!path.isAbsolute(resolved)) return keep;
2458
+ return { kind: 'repoint', target: { specifier: pathToFileHref(resolved), file: resolved } };
2459
+ }
2460
+ if (serverIgnoredAssetFilter.test(specifier)) {
2461
+ const asset = resolveVendorPackageSpecifier(
2462
+ config.root,
2463
+ path.join(importerDir, 'pnext-resolve.ts'),
2464
+ specifier,
2465
+ conditions,
2466
+ );
2467
+ return asset ? { kind: 'inline', file: asset } : undefined;
2468
+ }
2469
+ if (!depName || !isPackageSpecifier(specifier)) return undefined;
2470
+ if (getExternalPackagePolicy().external(depName)) {
2471
+ const fromImporter = resolveNestedPackageFromImporter(importerFile, specifier, conditions);
2472
+ const fromRoot = resolveVendorPackageSpecifier(
2473
+ config.root,
2474
+ path.join(config.root, 'pnext-resolve.ts'),
2475
+ specifier,
2476
+ conditions,
2477
+ );
2478
+ if (!fromImporter || fromImporter === fromRoot) return keep;
2479
+ return {
2480
+ kind: 'repoint',
2481
+ target: { specifier: fromImporter, file: fromImporter },
2482
+ nested: fromImporter,
2483
+ };
2484
+ }
2485
+ const planned = await preplanDependencyDecision(
2486
+ config,
2487
+ target,
2488
+ request.conditionTarget,
2489
+ specifier,
2490
+ importerDir,
2491
+ );
2492
+ if (!planned?.path) return undefined;
2493
+ return {
2494
+ kind: 'repoint',
2495
+ target: { specifier: planned.path, file: fileURLToPath(planned.path) },
2496
+ };
2497
+ }
2498
+
2499
+ function decideVendorRef(
2500
+ request: VendorNativeRequest,
2501
+ specifier: string,
2502
+ importerFile: string,
2503
+ isRequire: boolean,
2504
+ conditions: string[],
2505
+ ) {
2506
+ const importerDir = path.dirname(importerFile);
2507
+ const key = `${request.target}\0${request.conditionTarget}\0${isRequire ? 'r' : 'i'}\0${specifier}\0${importerDir}`;
2508
+ let decision = vendorRefDecisions.get(key);
2509
+ if (!decision) {
2510
+ decision = decideVendorRefUncached(request, specifier, importerFile, isRequire, conditions);
2511
+ vendorRefDecisions.set(key, decision);
2512
+ }
2513
+ return decision;
2514
+ }
2515
+
2516
+ async function decideVendorRefUncached(
2517
+ request: VendorNativeRequest,
2518
+ specifier: string,
2519
+ importerFile: string,
2520
+ isRequire: boolean,
2521
+ conditions: string[],
2522
+ ): Promise<VendorRefDecision | undefined> {
2523
+ const { config, target, conditionTarget } = request;
2524
+ const importerDir = path.dirname(importerFile);
2525
+ const keep = { kind: 'keep' } as const;
2526
+ const depName = packageNameFromSpecifier(specifier);
2527
+ if (depName && (builtinModules.includes(depName) || isBunBuiltin(depName))) return keep;
2528
+
2529
+ // Framework aliases. A require-call reads the react-server layer — a package
2530
+ // probing `'useState' in require('react')` must see false — so the two kinds
2531
+ // of reference to one specifier are repointed separately.
2532
+ if (aliasSpecifierFilter.test(specifier)) {
2533
+ const resolved = coreAliases(config, target)[specifier] ?? firstAliasForSpecifier(specifier);
2534
+ if (!resolved) return undefined;
2535
+ if (isRequire) {
2536
+ // A CommonJS shim swap (`next/navigation` -> navigation.cjs) is the one
2537
+ // alias the plugin BUNDLES; leave that build to the plugin.
2538
+ if (serverRequireAlias(specifier, resolved, 'require-call') !== resolved) return undefined;
2539
+ const layerAliases =
2540
+ target === 'server' ? getImportAliasExtensions().reactServerLayerAliases(config) : undefined;
2541
+ const layer = layerAliases?.[specifier] ?? resolved;
2542
+ const file = path.isAbsolute(layer) ? layer : require.resolve(layer);
2543
+ return { kind: 'repoint', target: { specifier: file, file } };
2544
+ }
2545
+ if (!path.isAbsolute(resolved)) return keep;
2546
+ return { kind: 'repoint', target: { specifier: pathToFileHref(resolved), file: resolved } };
2547
+ }
2548
+
2549
+ // An asset the bundler externalized (a cross-package CSS import) has to come
2550
+ // back inside, where `loader: empty` stubs it.
2551
+ if (serverIgnoredAssetFilter.test(specifier)) {
2552
+ const asset = resolveVendorPackageSpecifier(
2553
+ config.root,
2554
+ path.join(importerDir, 'pnext-resolve.ts'),
2555
+ specifier,
2556
+ conditions,
2557
+ );
2558
+ return asset ? { kind: 'inline', file: asset } : undefined;
2559
+ }
2560
+ if (!depName || !isPackageSpecifier(specifier)) return undefined;
2561
+
2562
+ // B7: a serverExternalPackage stays external, pinned to the importing
2563
+ // package's own copy when that differs from the hoisted root version.
2564
+ if (getExternalPackagePolicy().external(depName)) {
2565
+ const fromImporter = resolveNestedPackageFromImporter(importerFile, specifier, conditions);
2566
+ const fromRoot = resolveVendorPackageSpecifier(
2567
+ config.root,
2568
+ path.join(config.root, 'pnext-resolve.ts'),
2569
+ specifier,
2570
+ conditions,
2571
+ );
2572
+ if (!fromImporter || fromImporter === fromRoot) return keep;
2573
+ return {
2574
+ kind: 'repoint',
2575
+ target: { specifier: fromImporter, file: fromImporter },
2576
+ nested: fromImporter,
2577
+ };
2578
+ }
2579
+
2580
+ const resolvedEntry = resolveVendorPackageSpecifier(
2581
+ config.root,
2582
+ path.join(importerDir, 'pnext-resolve.ts'),
2583
+ specifier,
2584
+ conditions,
2585
+ );
2586
+ // A CommonJS dependency loses its named exports when externalized, so it is
2587
+ // inlined unless a verified facade can republish them.
2588
+ const commonJs = Boolean(resolvedEntry) && !isEsmModuleEntry(resolvedEntry!);
2589
+ if (commonJs && !(await cjsEntryMayHaveNamedExports(resolvedEntry!))) {
2590
+ return { kind: 'inline', file: resolvedEntry! };
2591
+ }
2592
+ let vendored: string;
2593
+ const tv = performance.now();
2594
+ try {
2595
+ vendored = await externalServerPackageHref(
2596
+ config,
2597
+ specifier,
2598
+ target,
2599
+ importerDir,
2600
+ conditionTarget,
2601
+ // Raised from inside a running build: must not queue behind it.
2602
+ true,
2603
+ );
2604
+ } catch (error) {
2605
+ if (isResolveFailure(error)) return keep;
2606
+ return undefined;
2607
+ }
2608
+ if (vendorTraceEnabled()) {
2609
+ vendorTraceRow({
2610
+ kind: 'edge',
2611
+ from: request.packageName ?? '?',
2612
+ fromTarget: target,
2613
+ fromCondition: conditionTarget,
2614
+ to: specifier,
2615
+ toResolveDir: importerDir,
2616
+ });
2617
+ }
2618
+ if (commonJs) {
2619
+ heavyProfRow({ k: 'dep-build-wait', site: 'ref', spec: specifier, ms: performance.now() - tv });
2620
+ const tf = performance.now();
2621
+ const facade = await cjsNamedExportFacade(fileURLToPath(vendored), vendored);
2622
+ heavyProfRow({ k: 'facade-wait', site: 'ref', spec: specifier, ms: performance.now() - tf });
2623
+ if (!facade) return { kind: 'inline', file: resolvedEntry! };
2624
+ return {
2625
+ kind: 'repoint',
2626
+ target: { specifier: pathToFileHref(facade), file: facade },
2627
+ };
2628
+ }
2629
+ return {
2630
+ kind: 'repoint',
2631
+ target: { specifier: vendored, file: fileURLToPath(vendored) },
2632
+ };
2633
+ }
2634
+
2635
+ /**
2636
+ * Record one specifier's target. A chunk mixes importers, so a specifier that two importers resolve
2637
+ * DIFFERENTLY (a pinned nested version for one of them) cannot be rewritten in place - that is a bail, not a
2638
+ * choice.
2639
+ */
2640
+ function record(
2641
+ targets: Map<string, VendorRefTarget>,
2642
+ specifier: string,
2643
+ target: VendorRefTarget,
2644
+ ) {
2645
+ const existing = targets.get(specifier);
2646
+ if (existing) return existing.specifier === target.specifier;
2647
+ targets.set(specifier, target);
2648
+ return true;
2649
+ }
2650
+
2651
+ async function writeExternalServerPackageBundle(
2652
+ config: ResolvedConfig,
2653
+ specifier: string,
2654
+ target: CompatAliasTarget,
2655
+ resolveDir: string,
2656
+ file: string,
2657
+ conditionTarget: ServerBundleTarget,
2658
+ nested: boolean,
2659
+ ) {
2660
+ const context = { nestedExternalFiles: [] as string[] };
2661
+ // The layer is part of the label: the same specifier legitimately compiles
2662
+ // once per target, and a profile that cannot tell those apart reads as
2663
+ // duplication that is not there.
2664
+ const bundled = await profileRuntimeStep(`vendor bundle ${specifier} [${target}/${conditionTarget}]`, () =>
2665
+ bundleExternalPackage(
2666
+ config,
2667
+ specifier,
2668
+ target,
2669
+ resolveDir,
2670
+ conditionTarget,
2671
+ context,
2672
+ file,
2673
+ nested,
2674
+ ),
2675
+ );
2676
+ // Packages can ship 'use cache' functions too (Next allows it); client
2677
+ // bundles keep the source untouched.
2678
+ const code =
2679
+ target === 'server' ? applyBundledSourceTransforms(bundled.code, specifier) : bundled.code;
2680
+ const id = await writeVendorArtifact(file, code);
2681
+ recordVendorLayerBuild(
2682
+ config,
2683
+ resolveVendorEntry(config, specifier, resolveDir, conditionTarget),
2684
+ target,
2685
+ conditionTarget,
2686
+ bundled.metafile,
2687
+ code,
2688
+ id,
2689
+ );
2690
+ // When this vendor bundle pins a transitive external to a nested version distinct from the hoisted root
2691
+ // copy, Node/webpack file tracing surfaces that dependency's source as a `.js` artifact in the server
2692
+ // output. Emit a `.js` trace copy of each such source so build tooling that scans `**/*.js` - never `.mjs` -
2693
+ // observes the bundled transitive too. These are trace artifacts only; nothing imports them.
2694
+ await emitNestedExternalTraceCopies(file, context.nestedExternalFiles);
2695
+ return file;
2696
+ }
2697
+
2698
+ /** Trace-only side channel: what the esbuild pass that produced the code did. */
2699
+ interface VendorBuildTrace {
2700
+ plugins: string[];
2701
+ entry?: string;
2702
+ metafile?: Metafile;
2703
+ }
2704
+
2705
+ /** Emit one JSONL build row from a finished (or failed) vendor esbuild pass. */
2706
+ function traceVendorBuild(row: {
2707
+ seq: number;
2708
+ specifier: string;
2709
+ resolveDir: string;
2710
+ target: string;
2711
+ conditionTarget: ServerBundleTarget;
2712
+ conditions: string[];
2713
+ startMs: number;
2714
+ outBytes: number;
2715
+ trace: VendorBuildTrace;
2716
+ ok: boolean;
2717
+ error?: string;
2718
+ }) {
2719
+ const inputs = row.trace.metafile?.inputs ?? {};
2720
+ const inputFiles = Object.keys(inputs);
2721
+ const endMs = performance.now();
2722
+ vendorTraceRow({
2723
+ seq: row.seq,
2724
+ specifier: row.specifier,
2725
+ resolveDir: row.resolveDir,
2726
+ target: row.target,
2727
+ conditionTarget: row.conditionTarget,
2728
+ entry: row.trace.entry,
2729
+ conditions: row.conditions,
2730
+ startMs: row.startMs,
2731
+ endMs,
2732
+ ms: endMs - row.startMs,
2733
+ inputCount: inputFiles.length,
2734
+ inputBytes: inputFiles.reduce((sum, input) => sum + (inputs[input]?.bytes ?? 0), 0),
2735
+ inputFiles,
2736
+ outBytes: row.outBytes,
2737
+ plugins: row.trace.plugins,
2738
+ ok: row.ok,
2739
+ ...(row.error ? { error: row.error } : {}),
2740
+ });
2741
+ }
2742
+
2743
+ async function bundleExternalPackage(
2744
+ config: ResolvedConfig,
2745
+ specifier: string,
2746
+ target: CompatAliasTarget,
2747
+ resolveDir: string,
2748
+ conditionTarget: ServerBundleTarget,
2749
+ context?: { nestedExternalFiles: string[] },
2750
+ /** The artifact this bundle becomes — the plugin-free path's identity. */
2751
+ file?: string,
2752
+ nested = false,
2753
+ ) {
2754
+ if (!vendorTraceEnabled()) {
2755
+ return bundleExternalPackageImpl(
2756
+ config,
2757
+ specifier,
2758
+ target,
2759
+ resolveDir,
2760
+ conditionTarget,
2761
+ context,
2762
+ file,
2763
+ nested,
2764
+ );
2765
+ }
2766
+ const trace: VendorBuildTrace = { plugins: [] };
2767
+ const seq = nextVendorTraceSeq();
2768
+ const startMs = performance.now();
2769
+ const common = {
2770
+ seq,
2771
+ specifier,
2772
+ resolveDir,
2773
+ target,
2774
+ conditionTarget,
2775
+ conditions: serverBundleConditions(conditionTarget),
2776
+ startMs,
2777
+ trace,
2778
+ };
2779
+ try {
2780
+ const result = await bundleExternalPackageImpl(
2781
+ config,
2782
+ specifier,
2783
+ target,
2784
+ resolveDir,
2785
+ conditionTarget,
2786
+ context,
2787
+ file,
2788
+ nested,
2789
+ trace,
2790
+ );
2791
+ traceVendorBuild({ ...common, outBytes: result.code.length, ok: true });
2792
+ return result;
2793
+ } catch (error) {
2794
+ traceVendorBuild({
2795
+ ...common,
2796
+ outBytes: 0,
2797
+ ok: false,
2798
+ error: (error instanceof Error ? error.message : String(error)).slice(0, 300),
2799
+ });
2800
+ throw error;
2801
+ }
2802
+ }
2803
+
2804
+ async function bundleExternalPackageImpl(
2805
+ config: ResolvedConfig,
2806
+ specifier: string,
2807
+ target: CompatAliasTarget,
2808
+ resolveDir: string,
2809
+ conditionTarget: ServerBundleTarget,
2810
+ context?: { nestedExternalFiles: string[] },
2811
+ /** The artifact this bundle becomes — the plugin-free path's identity. */
2812
+ file?: string,
2813
+ nested = false,
2814
+ trace?: VendorBuildTrace,
2815
+ ) {
2816
+ const packageName = packageNameFromSpecifier(specifier);
2817
+ const resolvedEntry = resolveVendorPackageSpecifier(
2818
+ config.root,
2819
+ path.join(resolveDir, 'pnext-resolve.ts'),
2820
+ specifier,
2821
+ serverBundleConditions(conditionTarget),
2822
+ );
2823
+ const requiredEntry =
2824
+ resolvedEntry &&
2825
+ !isEsmModuleFile(resolvedEntry) &&
2826
+ getExternalPackagePolicy().esmExternals() === true
2827
+ ? resolveVendorPackageSpecifier(
2828
+ config.root,
2829
+ path.join(resolveDir, 'pnext-resolve.ts'),
2830
+ specifier,
2831
+ serverBundleRequireConditions(conditionTarget),
2832
+ )
2833
+ : undefined;
2834
+ if (
2835
+ packageName &&
2836
+ resolvedEntry?.match(/\.[cm]?tsx?$/) &&
2837
+ !getExternalPackagePolicy().transpile(packageName)
2838
+ ) {
2839
+ throw new Error(
2840
+ `${path.relative(config.root, resolvedEntry)}\nModule parse failed: Unexpected token`,
2841
+ );
2842
+ }
2843
+ const entry =
2844
+ requiredEntry ??
2845
+ resolveLinkedPackageSpecifier(
2846
+ config.root,
2847
+ path.join(resolveDir, 'pnext-resolve.ts'),
2848
+ specifier,
2849
+ serverBundleConditions(conditionTarget),
2850
+ ) ??
2851
+ specifier;
2852
+ // `packages: 'external'` refuses to bundle a BARE entry point, so the
2853
+ // plugin-free path needs the resolved file — which is the same file esbuild
2854
+ // would have resolved the specifier to.
2855
+ const nativeEntry = path.isAbsolute(entry) ? entry : resolvedEntry;
2856
+ if (trace) trace.entry = nativeEntry ?? resolvedEntry;
2857
+ const preplan =
2858
+ vendorPreplanEnabled() &&
2859
+ (await vendorPreplanPackageSafe(
2860
+ config,
2861
+ packageName,
2862
+ nativeEntry ?? resolvedEntry,
2863
+ target,
2864
+ conditionTarget,
2865
+ ));
2866
+ const native =
2867
+ file && nativeEntry
2868
+ ? await buildVendorNative({
2869
+ preplan,
2870
+ config,
2871
+ target,
2872
+ conditionTarget,
2873
+ resolveDir,
2874
+ bailKey: file,
2875
+ packageName,
2876
+ entryPoints: [nativeEntry],
2877
+ splitting: false,
2878
+ ownEntries: new Map([[nativeEntry, file]]),
2879
+ isEntry: () => true,
2880
+ soloFile: file,
2881
+ nested,
2882
+ context: context ?? { nestedExternalFiles: [] },
2883
+ trace,
2884
+ })
2885
+ : undefined;
2886
+ if (native?.[0]) return { code: addCommonJsNamedExports(native[0].text), metafile: undefined };
2887
+
2888
+ let bundled: string;
2889
+ let metafile: Metafile | undefined;
2890
+ const pluginStart = performance.now();
2891
+ const vendorPlugins = profiledVendorPlugins([
2892
+ serverAssetPlugin(config),
2893
+ ...getBundlerExtensions().serverEsbuildPlugins(
2894
+ config,
2895
+ vendorPluginOptions(config, target, conditionTarget),
2896
+ ),
2897
+ runtimeAliasBuildPlugin(config, target, {
2898
+ bundleRequireAliases: true,
2899
+ // The server vendor bundle IS the react-server layer (client-target
2900
+ // bundles are the SSR pass and keep the full-hooks shim).
2901
+ reactServerLayer: target === 'server',
2902
+ }),
2903
+ externalPackageDependencyPlugin(config, specifier, target, conditionTarget, context, preplan),
2904
+ externalBuiltinBuildPlugin(),
2905
+ ]);
2906
+ if (trace) trace.plugins = vendorPlugins.map(plugin => plugin.name);
2907
+ // Bare entry: hand the interop plugin the resolution already computed above.
2908
+ if (!path.isAbsolute(entry) && resolvedEntry) {
2909
+ provideEntryResolution(entry, resolveDir, resolvedEntry);
2910
+ }
2911
+ try {
2912
+ const result = await build({
2913
+ entryPoints: [
2914
+ getBundlerExtensions().serverBundleEntry(
2915
+ entry,
2916
+ resolveDir,
2917
+ path.isAbsolute(entry) ? entry : resolvedEntry,
2918
+ ) ?? entry,
2919
+ ],
2920
+ absWorkingDir: resolveDir,
2921
+ bundle: true,
2922
+ write: false,
2923
+ format: 'esm',
2924
+ platform: 'neutral',
2925
+ target: 'es2022',
2926
+ conditions: serverBundleConditions(conditionTarget),
2927
+ mainFields: ['module', 'main'],
2928
+ loader: packageJsxLoaders,
2929
+ jsx: 'automatic',
2930
+ jsxImportSource: 'preact',
2931
+ logLevel: 'silent',
2932
+ // The verdict is computed offline from this, after the build settles.
2933
+ metafile: true,
2934
+ ...serverDefineOptions(),
2935
+ plugins: vendorPlugins,
2936
+ });
2937
+ const output = result.outputFiles[0];
2938
+ if (!output) throw new Error(`Failed to bundle ${specifier}`);
2939
+ vendorNativeStats.pluginBuilds += 1;
2940
+ vendorNativeStats.pluginBuildMs += performance.now() - pluginStart;
2941
+ bundled = output.text;
2942
+ metafile = result.metafile;
2943
+ if (trace) trace.metafile = result.metafile;
2944
+ } catch (error) {
2945
+ // A subpath the package deliberately withholds from this layer
2946
+ // (`"node": null`) is not a broken dependency: browser-only code reaches it
2947
+ // behind a `typeof window` guard that never runs on the server, and Next's
2948
+ // server compilers drop that branch before their resolver ever sees it.
2949
+ // Vending a module that throws Node's own error keeps the guarded import
2950
+ // out of the build while still failing loudly if anything evaluates it.
2951
+ if (
2952
+ !isPackageSubpathUnexported(
2953
+ config.root,
2954
+ path.join(resolveDir, 'pnext-resolve.ts'),
2955
+ specifier,
2956
+ serverBundleConditions(conditionTarget),
2957
+ )
2958
+ ) {
2959
+ throw error;
2960
+ }
2961
+ return { code: unexportedSubpathModule(specifier), metafile: undefined };
2962
+ }
2963
+ return { code: addCommonJsNamedExports(bundled), metafile };
2964
+ }
2965
+
2966
+ function unexportedSubpathModule(specifier: string) {
2967
+ const packageName = packageNameFromSpecifier(specifier);
2968
+ const subpath = packageName && specifier !== packageName ? `.${specifier.slice(packageName.length)}` : '.';
2969
+ const message = `Package subpath '${subpath}' is not defined by "exports" in ${packageName ?? specifier}`;
2970
+ return `throw Object.assign(new Error(${JSON.stringify(message)}), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });\n`;
2971
+ }
2972
+
2973
+ function runtimeAliasBuildPlugin(
2974
+ config?: ResolvedConfig,
2975
+ target: CompatAliasTarget = 'server',
2976
+ options: { bundleRequireAliases?: boolean; reactServerLayer?: boolean } = {},
2977
+ ): Plugin {
2978
+ const aliases = config ? coreAliases(config, target) : undefined;
2979
+ // A node_modules package bundled into the RSC (react-server) layer that requires 'react' must observe the
2980
+ // react-server subset - no client-hook dispatcher - exactly like the framework's own react-server modules,
2981
+ // so a package's `'useState' in require('react')` probe reads false on the server. ESM named imports stay
2982
+ // external and resolve at runtime against the full-hooks server shim (kept for pages/api back-compat), so
2983
+ // this narrow require-call override never breaks them.
2984
+ const reactServerLayerAliases =
2985
+ config && options.reactServerLayer
2986
+ ? getImportAliasExtensions().reactServerLayerAliases(config)
2987
+ : undefined;
2988
+ return {
2989
+ name: 'pnext-runtime-build-alias',
2990
+ setup(build) {
2991
+ build.onResolve({ filter: /^\.\/preact$/ }, args => {
2992
+ // `./preact` is the compat react shim's private import. It also appears in MIRRORED copies of the
2993
+ // shim (build-cache outputs under node_modules paths) where no sibling preact file exists - those
2994
+ // must resolve to the single compat instance too. Only a REAL sibling ./preact.* opts out.
2995
+ const hasRealSibling = ['.ts', '.tsx', '.js', '.mjs', '.jsx'].some(ext =>
2996
+ existsSync(path.join(args.resolveDir, `preact${ext}`)),
2997
+ );
2998
+ const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts');
2999
+ if (hasRealSibling && path.resolve(args.resolveDir, 'preact.ts') !== compatPreact)
3000
+ return undefined;
3001
+ return { path: compatPreact };
3002
+ });
3003
+ build.onResolve({ filter: /^\.\.\/client\/errors\/primitive-throw$/ }, args => {
3004
+ const compatPreact = path.resolve(import.meta.dirname, '..', 'compat', 'react', 'preact.ts');
3005
+ if (path.resolve(args.importer) !== compatPreact) return undefined;
3006
+ return { path: path.resolve(import.meta.dirname, '..', 'compat', 'client', 'errors', 'primitive-throw.ts') };
3007
+ });
3008
+ build.onResolve({ filter: aliasSpecifierFilter }, args => {
3009
+ const resolved = aliases?.[args.path] ?? firstAliasForSpecifier(args.path);
3010
+ if (!resolved) return undefined;
3011
+ const requireTarget = serverRequireAlias(args.path, resolved, args.kind);
3012
+ if (requireTarget !== resolved) return { path: requireTarget };
3013
+ if (options.bundleRequireAliases && args.kind === 'require-call') {
3014
+ const layerTarget = serverRequireAlias(
3015
+ args.path,
3016
+ reactServerLayerAliases?.[args.path] ?? resolved,
3017
+ args.kind,
3018
+ );
3019
+ return { path: path.isAbsolute(layerTarget) ? layerTarget : require.resolve(layerTarget) };
3020
+ }
3021
+ return {
3022
+ path: path.isAbsolute(resolved) ? pathToFileHref(resolved) : resolved,
3023
+ external: true,
3024
+ };
3025
+ });
3026
+ },
3027
+ };
3028
+ }
3029
+
3030
+ function serverRequireAlias(specifier: string, target: string, kind: string | undefined): string {
3031
+ if (kind !== 'require-call') return target;
3032
+ if (specifier === 'next/navigation' && target.endsWith(`${path.sep}navigation.ts`)) {
3033
+ return path.join(path.dirname(target), 'navigation.cjs');
3034
+ }
3035
+ if (specifier === 'next/router' && target.endsWith(`${path.sep}router.ts`)) {
3036
+ return path.join(path.dirname(target), 'router.cjs');
3037
+ }
3038
+ return target;
3039
+ }
3040
+
3041
+ function externalPackageDependencyPlugin(
3042
+ config: ResolvedConfig,
3043
+ entrySpecifier: string,
3044
+ target: CompatAliasTarget,
3045
+ conditionTarget: ServerBundleTarget,
3046
+ context?: { nestedExternalFiles: string[] },
3047
+ preplan = false,
3048
+ ): Plugin {
3049
+ const entryPackageName = packageNameFromSpecifier(entrySpecifier);
3050
+ return {
3051
+ name: 'pnext-external-package-dependencies',
3052
+ setup(build) {
3053
+ build.onResolve({ filter: /^[^./].*/ }, async args => {
3054
+ if (!isPackageSpecifier(args.path)) return undefined;
3055
+ if (packageNameFromSpecifier(args.path) === entryPackageName) return undefined;
3056
+ if (preplan && args.kind !== 'require-call') {
3057
+ const planned = await preplanDependencyDecision(
3058
+ config,
3059
+ target,
3060
+ conditionTarget,
3061
+ args.path,
3062
+ args.resolveDir,
3063
+ );
3064
+ if (planned) {
3065
+ vendorPreplanStats.preplanRefs += 1;
3066
+ return planned;
3067
+ }
3068
+ vendorPreplanStats.fallbackRefs += 1;
3069
+ }
3070
+ // Off the slot for the whole decision: it ends in a nested vendor
3071
+ // build, and holding a worker across that is what caps real
3072
+ // parallelism at dependency depth (`PNEXT_VENDOR_YIELD=0` pins the old
3073
+ // behaviour for bisecting). The resolver work in front of it is cheap
3074
+ // and off-slot too, exactly as the plugin-free path's post-pass runs it.
3075
+ const decision = await yieldVendorSlotWhile(() =>
3076
+ externalDependencyDecision(config, target, conditionTarget, args.path, args.resolveDir, args.importer),
3077
+ );
3078
+ if (decision?.vendored && vendorTraceEnabled()) {
3079
+ vendorTraceRow({
3080
+ kind: 'edge',
3081
+ from: entrySpecifier,
3082
+ fromTarget: target,
3083
+ fromCondition: conditionTarget,
3084
+ to: args.path,
3085
+ toResolveDir: args.resolveDir,
3086
+ });
3087
+ }
3088
+ if (decision?.nested && context) context.nestedExternalFiles.push(decision.nested);
3089
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
3090
+ if (decision?.error) throw decision.error;
3091
+ return decision?.result;
3092
+ });
3093
+ },
3094
+ };
3095
+ }
3096
+
3097
+ /** What one external reference resolves to, plus the effects the caller replays. */
3098
+ interface ExternalDependencyDecision {
3099
+ result: OnResolveResult | undefined;
3100
+ /** A transitive dep pinned to a nested copy, for the trace-copy pass. */
3101
+ nested?: string;
3102
+ /** A build failure, re-thrown per caller rather than cached as a value. */
3103
+ error?: unknown;
3104
+ /** Trace-only: the dep was deferred to a vendor bundle of its own. */
3105
+ vendored?: boolean;
3106
+ }
3107
+
3108
+ /**
3109
+ * NOT memoized on (layer, specifier, importing directory), though it looks pure in them: the answer
3110
+ * also depends on what is on disk when it is asked - `externalServerPackageHref` re-checks its
3111
+ * artifact's liveness per demand, and the facade gate re-executes a bundle that may have been
3112
+ * replaced. Caching it lost `app-external` outright, so the seam stays as the plugin chain had it.
3113
+ */
3114
+ async function externalDependencyDecision(
3115
+ config: ResolvedConfig,
3116
+ target: CompatAliasTarget,
3117
+ conditionTarget: ServerBundleTarget,
3118
+ specifier: string,
3119
+ resolveDir: string,
3120
+ importer: string | undefined,
3121
+ ): Promise<ExternalDependencyDecision> {
3122
+ // B7: a serverExternalPackage transitive dep stays external (Node resolves it
3123
+ // from node_modules at runtime) instead of being bundled.
3124
+ const depName = packageNameFromSpecifier(specifier);
3125
+ if (depName && getExternalPackagePolicy().external(depName)) {
3126
+ // Transitive-version correctness: the runtime resolves a bare external from the VENDOR bundle's own
3127
+ // location, which walks up to the hoisted root copy - wrong when the importing package pulls a distinct
3128
+ // nested version (pnpm virtual store, npm nesting). Resolve the dep from the IMPORTING package's real
3129
+ // path here, at build time, and when that is a copy distinct from the hoisted root version, pin the
3130
+ // external to that exact resolved file so the correct version loads at runtime. A hoisted dep stays a
3131
+ // portable bare external. Bundling the nested copy inline instead regresses to a resolve ERROR for the
3132
+ // bundle's other bare externals, so keep it external and only pin the path.
3133
+ const conditions = serverBundleConditions(conditionTarget);
3134
+ const fromImporter = importer
3135
+ ? resolveNestedPackageFromImporter(importer, specifier, conditions)
3136
+ : undefined;
3137
+ const fromRoot = resolveVendorPackageSpecifier(
3138
+ config.root,
3139
+ path.join(config.root, 'pnext-resolve.ts'),
3140
+ specifier,
3141
+ conditions,
3142
+ );
3143
+ if (fromImporter && fromImporter !== fromRoot) {
3144
+ return { result: { path: fromImporter, external: true }, nested: fromImporter };
3145
+ }
3146
+ return { result: { path: specifier, external: true } };
3147
+ }
3148
+ const resolvedEntry = resolveVendorPackageSpecifier(
3149
+ config.root,
3150
+ path.join(resolveDir, 'pnext-resolve.ts'),
3151
+ specifier,
3152
+ serverBundleConditions(conditionTarget),
3153
+ );
3154
+ // A CommonJS dependency loses its named exports when externalized (esbuild emits only `default` for a CJS
3155
+ // entry), so it is inlined here instead. `isEsmModuleEntry` first checks CONTENT, not just extension and
3156
+ // `package.json#type` - most "CJS" entries are plain-.js ESM and inline for no reason. What is left gets a
3157
+ // verified named-export facade when it publishes names, and is inlined only when it does not.
3158
+ const commonJs = Boolean(resolvedEntry) && !isEsmModuleEntry(resolvedEntry!);
3159
+ if (commonJs && !(await cjsEntryMayHaveNamedExports(resolvedEntry!))) return { result: undefined };
3160
+ let resolvedPackage: string;
3161
+ const tv = performance.now();
3162
+ try {
3163
+ resolvedPackage = await externalServerPackageHref(
3164
+ config,
3165
+ specifier,
3166
+ target,
3167
+ resolveDir,
3168
+ conditionTarget,
3169
+ // Raised from inside a running build: must not queue behind it.
3170
+ true,
3171
+ );
3172
+ } catch (error) {
3173
+ if (isResolveFailure(error)) return { result: { path: specifier, external: true } };
3174
+ return { result: undefined, error };
3175
+ }
3176
+ if (commonJs) {
3177
+ heavyProfRow({ k: 'dep-build-wait', site: 'plugin', spec: specifier, ms: performance.now() - tv });
3178
+ const tf = performance.now();
3179
+ const facade = await cjsNamedExportFacade(fileURLToPath(resolvedPackage), resolvedPackage);
3180
+ heavyProfRow({ k: 'facade-wait', site: 'plugin', spec: specifier, ms: performance.now() - tf });
3181
+ if (!facade) return { result: undefined };
3182
+ return { result: { path: pathToFileHref(facade), external: true }, vendored: true };
3183
+ }
3184
+ return { result: { path: resolvedPackage, external: true }, vendored: true };
3185
+ }
3186
+
3187
+ /** `PNEXT_VENDOR_YIELD=0` keeps the slot across a nested dependency's build. */
3188
+ function yieldVendorSlotWhile<T>(task: () => Promise<T>) {
3189
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
3190
+ return process.env.PNEXT_VENDOR_YIELD === '0' ? task() : outsideVendorSlot(task);
3191
+ }
3192
+
3193
+ function isResolveFailure(error: unknown) {
3194
+ const details = error instanceof Error ? `${error.message}\n${error.stack ?? ''}` : String(error);
3195
+ return details.includes('Could not resolve');
3196
+ }
3197
+
3198
+ async function profileRuntimeStep<T>(label: string, task: () => Promise<T>) {
3199
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
3200
+ if (!process.env.PNEXT_DEV_PROFILE) return task();
3201
+ const start = performance.now();
3202
+ try {
3203
+ return await task();
3204
+ } finally {
3205
+ console.log(`dev-profile compat ${label} in ${formatDuration(performance.now() - start)}`);
3206
+ }
3207
+ }
3208
+
3209
+ function formatDuration(durationMs: number) {
3210
+ const ms = Math.max(0, durationMs);
3211
+ if (ms < 10) return `${ms.toFixed(1)}ms`;
3212
+ if (ms < 1000) return `${Math.round(ms)}ms`;
3213
+ return `${(ms / 1000).toFixed(ms < 10000 ? 2 : 1)}s`;
3214
+ }
3215
+
3216
+ function externalBuiltinBuildPlugin(): Plugin {
3217
+ return {
3218
+ name: 'pnext-external-builtins',
3219
+ setup(build) {
3220
+ build.onResolve({ filter: /^[^./].*/ }, args => {
3221
+ const packageName = packageNameFromSpecifier(args.path);
3222
+ if (!packageName || (!builtinModules.includes(packageName) && !isBunBuiltin(packageName))) {
3223
+ return undefined;
3224
+ }
3225
+ return { path: args.path, external: true };
3226
+ });
3227
+ },
3228
+ };
3229
+ }
3230
+
3231
+ function isBunBuiltin(specifier: string) {
3232
+ return specifier === 'bun' || specifier.startsWith('bun:');
3233
+ }
3234
+
3235
+ function serverAssetPlugin(config?: ResolvedConfig): Plugin {
3236
+ return {
3237
+ name: 'pnext-empty-server-assets',
3238
+ setup(build) {
3239
+ build.onResolve({ filter: serverIgnoredAssetFilter }, args => {
3240
+ const resolved = resolveAssetPath(args.path, args.resolveDir);
3241
+ // A configured `turbopack.rules` loader chain for this extension (e.g.
3242
+ // `*.svg`) preempts the generic asset pipeline: defer so the compat
3243
+ // loader-rule plugin's onLoad runs the chain against the real file.
3244
+ if (getAssetExtensions().hasLoaderRuleFor(resolved)) {
3245
+ return undefined;
3246
+ }
3247
+ return { path: resolved, namespace: 'pnext-empty-server-asset' };
3248
+ });
3249
+ build.onLoad({ filter: /.*/, namespace: 'pnext-empty-server-asset' }, async args => ({
3250
+ contents: getCssExtensions().loadCssModuleForClient(args.path) ?? (isCssFile(args.path)
3251
+ ? ''
3252
+ : isStaticImageFile(args.path)
3253
+ ? await staticImageModuleSource(config, args.path)
3254
+ : 'export default "";'),
3255
+ loader: 'js',
3256
+ }));
3257
+ },
3258
+ };
3259
+ }
3260
+
3261
+ /**
3262
+ * Whether `rewriteServerSource` can possibly change `source`. One regex scan
3263
+ * over the union of the core dynamic tokens and every registered transform's
3264
+ * own sniff tokens (see `withSniff`); most modules trigger nothing and skip the
3265
+ * whole chain.
3266
+ */
3267
+ export function needsServerSourceRewrite(source: string) {
3268
+ return sourceHasDynamicImport(source) || sourceNeedsServerTransforms(source);
3269
+ }
3270
+
3271
+ export function rewriteServerSource(
3272
+ source: string,
3273
+ file: string,
3274
+ options: { nextFonts?: boolean; root?: string } = {},
3275
+ ) {
3276
+ const root = options.root ?? path.dirname(file);
3277
+ const rewritten = sourceHasDynamicImport(source)
3278
+ ? rewriteDynamicCallTargets(
3279
+ rewriteLiteralDynamicCalls(source, file),
3280
+ specifier => resolveImport(root, file, specifier),
3281
+ file,
3282
+ )
3283
+ : source;
3284
+ // Compat pre-transforms (next/font/root-params) run before generic server
3285
+ // transforms (use-cache/action tags). Pure-core apps register no transforms.
3286
+ if (options.nextFonts === false) return rewritten;
3287
+ if (!sourceNeedsServerTransforms(rewritten)) return rewritten;
3288
+ return applyServerSourceTransforms(
3289
+ applyServerSourcePreTransforms(rewritten, file, options.root),
3290
+ file,
3291
+ options.root,
3292
+ );
3293
+ }
3294
+
3295
+ export function runtimeImportTarget(specifier: string, format: 'esm' | 'cjs' = 'esm') {
3296
+ if (isRelativeOrAbsoluteSpecifier(specifier)) return specifier;
3297
+ const target = firstAliasForSpecifier(specifier);
3298
+ if (!target) return specifier;
3299
+ return format === 'esm' ? pathToFileHref(target) : target;
3300
+ }
3301
+
3302
+ export function runtimeServerImportTarget(specifier: string) {
3303
+ return runtimeImportTarget(specifier, 'esm');
3304
+ }
3305
+
3306
+ function isRelativeOrAbsoluteSpecifier(specifier: string) {
3307
+ return specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:');
3308
+ }
3309
+
3310
+ function isPackageSpecifier(specifier: string) {
3311
+ if (isRelativeOrAbsoluteSpecifier(specifier)) return false;
3312
+ if (specifier.startsWith('#')) return false;
3313
+ if (specifier.startsWith('node:')) return false;
3314
+ if (specifier.includes(':')) return false;
3315
+ const packageName = packageNameFromSpecifier(specifier);
3316
+ return Boolean(packageName && !builtinModules.includes(packageName));
3317
+ }
3318
+
3319
+ function isInside(root: string, file: string) {
3320
+ const relative = path.relative(root, file);
3321
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
3322
+ }
3323
+
3324
+ function rootPaths(root: string) {
3325
+ const key = path.resolve(root);
3326
+ const cached = rootPathCache.get(key);
3327
+ if (cached) return cached;
3328
+ const paths = new Set([path.resolve(root)]);
3329
+ try {
3330
+ paths.add(realpathSync.native(root));
3331
+ } catch {
3332
+ // The dev cache root is registered before it exists.
3333
+ }
3334
+ const resolved = [...paths];
3335
+ rootPathCache.set(key, resolved);
3336
+ return resolved;
3337
+ }
3338
+
3339
+ function rootFilter(root: string) {
3340
+ // Base source extensions plus any extra loadable extensions registered by
3341
+ // compat (e.g. mdx/md via pageExtensions). Without these, `.mdx` modules skip
3342
+ // the transform load hook and resolve as raw assets (default export = path).
3343
+ const extras = currentLoadExtras().map(escapeRegex);
3344
+ const exts = ['[jt]sx?', ...extras].join('|');
3345
+ // Exclude `next.config.{js,cjs,mjs,ts}`: it is loaded for its exported value (often CommonJS
3346
+ // `module.exports = ...`), not as part of the app's server module graph. Routing it through the
3347
+ // ES-module source transform drops the CJS exports, silently disabling next.config
3348
+ // rewrites/redirects.
3349
+ // Exclude the vendor tree under the cache root: the `*.dist/` copies copyBrowserReadyEsmDist
3350
+ // publishes are verbatim third-party ESM the copy predicate has already proved needs no server
3351
+ // rewrite, but they are `.js`, so each was read and re-transformed on every boot. Bun loads them
3352
+ // natively instead. Two clauses because the vendor tree is reachable from two registered roots.
3353
+ const vendorGuard = vendorLoadsNatively()
3354
+ ? `${root.endsWith(`${path.sep}cache${path.sep}server`) ? '(?!\\/vendor\\/)' : ''}(?!.*\\/cache\\/server\\/vendor\\/)`
3355
+ : '';
3356
+ return new RegExp(
3357
+ `^${escapeRegex(root)}${vendorGuard}(?!.*\\/node_modules\\/)(?!\\/next\\.config\\.(?:js|cjs|mjs|ts)$)(?:/.*)?\\.(?:${exts})$`,
3358
+ );
3359
+ }
3360
+
3361
+ function esbuildLoader(file: string) {
3362
+ if (file.endsWith('.tsx')) return 'tsx';
3363
+ if (file.endsWith('.ts')) return 'ts';
3364
+ // Workspace .js/.jsx may contain JSX (Next allows it); jsx is a js superset.
3365
+ return 'jsx';
3366
+ }
3367
+
3368
+ function hashRoot(value: string) {
3369
+ let hash = 5381;
3370
+ for (const char of value) hash = ((hash << 5) + hash) ^ char.charCodeAt(0);
3371
+ return (hash >>> 0).toString(36);
3372
+ }
3373
+
3374
+ function hashBundleSpecifier(value: string) {
3375
+ return createHash('sha256').update(value).digest('hex').slice(0, 16);
3376
+ }
3377
+
3378
+ // Trace copies of nested transitive externals (a version distinct from the
3379
+ // hoisted root copy) written alongside the vendor bundle as `.js`, so file
3380
+ // tracing / server-bundle scans observe the bundled dependency the way
3381
+ // webpack's bundled transitive would appear. Trace-only: never imported.
3382
+ async function emitNestedExternalTraceCopies(vendorFile: string, sources: readonly string[]) {
3383
+ const dir = path.dirname(vendorFile);
3384
+ for (const source of new Set(sources)) {
3385
+ try {
3386
+ const content = await readFile(source, 'utf8');
3387
+ const out = path.join(dir, `${hashBundleSpecifier(source)}.trace.js`);
3388
+ if (!existsSync(out)) await writeFileAtomic(out, content);
3389
+ } catch {
3390
+ // A source that cannot be read is simply not traced.
3391
+ }
3392
+ }
3393
+ }
3394
+
3395
+
3396
+ const serverIgnoredAssetFilter =
3397
+ /\.(?:css|scss|sass|woff2?|ttf|otf|eot|png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/;
3398
+
3399
+ function isCssFile(file: string) {
3400
+ return file.endsWith('.css');
3401
+ }
3402
+
3403
+ function isStaticImageFile(file: string) {
3404
+ return /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/.test(file);
3405
+ }
3406
+
3407
+ function resolveAssetPath(specifier: string, resolveDir: string) {
3408
+ const [sourcePath = '', hash = ''] = specifier.split('#', 2);
3409
+ const resolved = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath);
3410
+ return hash ? `${resolved}#${hash}` : resolved;
3411
+ }
3412
+
3413
+ async function staticImageModuleSource(config: ResolvedConfig | undefined, file: string) {
3414
+ const [sourcePath = ''] = file.split('#', 1);
3415
+ const bytes = new Uint8Array(await readFile(sourcePath));
3416
+ const emitted: { relative: string; bytes: Uint8Array }[] = [];
3417
+ const emit = (relative: string, data: Uint8Array) => {
3418
+ emitted.push({ relative, bytes: data });
3419
+ return `/${relative}`;
3420
+ };
3421
+ const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit });
3422
+ const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit);
3423
+ if (config) {
3424
+ for (const asset of emitted) {
3425
+ const target = path.join(config.outPath, 'public', ...asset.relative.split('/'));
3426
+ await mkdir(path.dirname(target), { recursive: true });
3427
+ if (!existsSync(target)) await Bun.write(target, asset.bytes);
3428
+ }
3429
+ }
3430
+ return source;
3431
+ }
3432
+
3433
+ // Core's generic static-asset module: emit the file under a hashed media URL
3434
+ // and export it as the default (a plain URL string) when no compat override is
3435
+ // registered. next/image compat replaces this with a full StaticImageData shape.
3436
+ function coreStaticAssetModule(
3437
+ sourcePath: string,
3438
+ bytes: Uint8Array,
3439
+ emit: (relative: string, bytes: Uint8Array) => string,
3440
+ ): string {
3441
+ const ext = path.extname(sourcePath).toLowerCase() || '.bin';
3442
+ const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8);
3443
+ const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-');
3444
+ const relative = getAssetExtensions().staticAssetRelativePath({ sourcePath, hash, base, ext });
3445
+ const src = emit(relative, bytes);
3446
+ return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`;
3447
+ }
3448
+
3449
+ function packageNameFromSpecifier(specifier: string) {
3450
+ const normalized = specifier.startsWith('node:') ? specifier.slice(5) : specifier;
3451
+ const parts = normalized.split('/');
3452
+ return normalized.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
3453
+ }