@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,2430 @@
1
+ import { builtinModules } from 'node:module';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { existsSync, readFileSync, type Dirent } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { copyFile, mkdir, readdir, symlink, writeFile } from 'node:fs/promises';
7
+ import { build, type OnResolveResult, type Plugin } from 'esbuild';
8
+ import { drainPreplanBuilds, vendorTraceEnabled, vendorTraceRow } from '../runtime/vendor';
9
+ import {
10
+ clientReferenceExportNames,
11
+ clientReferenceModuleSource,
12
+ hasUseClientDirective,
13
+ } from '../client/reference-stub';
14
+ import { getClientActionBundler } from './client-actions';
15
+ import { deferredDynamicImportSpecifiers, devDynamicSplitEnabled } from '../dynamic/source';
16
+ import { noteModuleGeneration, noteModuleImported } from './module-generations';
17
+ import {
18
+ externalServerPackageHref,
19
+ rewriteServerSource,
20
+ runtimeServerImportTarget,
21
+ serverBundleTargetForRuntime,
22
+ serverBundleRequireConditions,
23
+ type ServerBundleTarget,
24
+ } from '../runtime/server';
25
+ import { writeFileAtomic } from '../utils/fs';
26
+ import { createHash } from 'node:crypto';
27
+ import {
28
+ extraLoadableExtensions,
29
+ getAssetExtensions,
30
+ getBundlerExtensions,
31
+ getCssExtensions,
32
+ getImportAliasExtensions,
33
+ serverDefineOptions,
34
+ } from '../extensions';
35
+ import {
36
+ frameworkRuntimeAliasEntries,
37
+ pathToFileHref,
38
+ pnextAliases,
39
+ type CompatAliasTarget,
40
+ type ResolvedConfig,
41
+ } from '../config';
42
+ import { readText, toPosixPath } from '../utils/fs';
43
+ import {
44
+ type ExternalLoadTarget,
45
+ externalPackageImportTarget,
46
+ getExternalPackagePolicy,
47
+ isCommonJsModuleSource,
48
+ packageNameOfSpecifier,
49
+ resolveImport,
50
+ resolveExternalLoadTarget,
51
+ resolvePackageSpecifier,
52
+ } from '../resolve/imports';
53
+ import { importSpecifiers, rewriteSpecifierLiterals } from '../resolve/scan-facts';
54
+ import {
55
+ cacheRoot,
56
+ devArtifactUsable,
57
+ devHeadTrimEnabled,
58
+ devModuleCache,
59
+ devSourceIdentity,
60
+ noteDevArtifactWritten,
61
+ type DevModuleCache,
62
+ } from './module-cache';
63
+ import { cachedRouteBundlePath, saveRouteBundlePath } from './route-bundle-key-cache';
64
+ import {
65
+ outputSpecifiers,
66
+ spliceSource,
67
+ transformBailReason,
68
+ transformServerModule,
69
+ type SpecifierKind,
70
+ } from './module-transform';
71
+ import { findShakeableDynamicImports } from '../dynamic/tree-shake';
72
+ import type { RouteManifestEntry } from '../types';
73
+ import { uniqueIdentifier } from '../utils/source';
74
+
75
+ type AliasMap = Record<string, string>;
76
+
77
+ function nextCompatEnabled(config: ResolvedConfig) {
78
+ return Boolean(config.compat?.next);
79
+ }
80
+
81
+ function reactCompatEnabled(config: ResolvedConfig) {
82
+ return Boolean(config.compat?.next || config.compat?.react || config.compat?.reactCompiler);
83
+ }
84
+
85
+ function coreAliases(config: ResolvedConfig, target: CompatAliasTarget): AliasMap {
86
+ return {
87
+ ...frameworkRuntimeAliasEntries(),
88
+ ...pnextAliases(target),
89
+ ...getImportAliasExtensions().aliases(config, target),
90
+ };
91
+ }
92
+
93
+ // Cap concurrent esbuild builds: the recursive module graph can otherwise fire
94
+ // hundreds of builds at once and saturate the host.
95
+ const BUILD_CONCURRENCY = Math.max(2, (os.availableParallelism?.() ?? os.cpus().length) - 1);
96
+ let buildActive = 0;
97
+ const buildQueue: (() => void)[] = [];
98
+ const moduleBuilds = new Map<string, Promise<string>>();
99
+ const routeBundleBuilds = new Map<string, Promise<string>>();
100
+
101
+ // PNEXT_DEV_MODULE_STATS=1: attribute every per-module compile to its profile,
102
+ // so cross-profile duplicate work is countable rather than inferred. Counts are
103
+ // load-insensitive, which is what makes them usable on a contended box.
104
+ interface ModuleStat { profile: string; ms: number; esbuild: boolean }
105
+ // `${profile}:${file}` of modules that fell off the oxc fast path onto esbuild.
106
+ const esbuildFallbacks = new Set<string>();
107
+ const moduleStats: Map<string, ModuleStat[]> | undefined =
108
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
109
+ process.env.PNEXT_DEV_MODULE_STATS === '1' ? new Map() : undefined;
110
+ let moduleStatsFlush: Timer | undefined;
111
+
112
+ function noteModuleCompile(file: string, profile: string, ms: number, esbuild: boolean) {
113
+ if (!moduleStats) return;
114
+ const entries = moduleStats.get(file) ?? [];
115
+ entries.push({ profile, ms, esbuild });
116
+ moduleStats.set(file, entries);
117
+ // Debounced: stages settle after the response, so a fixed point in the
118
+ // request would miss the preloads this is meant to attribute.
119
+ clearTimeout(moduleStatsFlush);
120
+ moduleStatsFlush = setTimeout(reportModuleStats, 500);
121
+ moduleStatsFlush.unref?.();
122
+ }
123
+
124
+ function reportModuleStats() {
125
+ if (!moduleStats) return;
126
+ const byProfile = new Map<string, { files: number; ms: number; esbuild: number }>();
127
+ let total = 0;
128
+ let duplicated = 0;
129
+ let duplicateCompiles = 0;
130
+ let duplicateMs = 0;
131
+ for (const entries of moduleStats.values()) {
132
+ total += entries.length;
133
+ const profiles = new Set(entries.map(entry => entry.profile));
134
+ if (profiles.size > 1) {
135
+ duplicated += 1;
136
+ duplicateCompiles += entries.length - 1;
137
+ duplicateMs += entries.slice(1).reduce((sum, entry) => sum + entry.ms, 0);
138
+ }
139
+ for (const entry of entries) {
140
+ const bucket = byProfile.get(entry.profile) ?? { files: 0, ms: 0, esbuild: 0 };
141
+ bucket.files += 1;
142
+ bucket.ms += entry.ms;
143
+ bucket.esbuild += entry.esbuild ? 1 : 0;
144
+ byProfile.set(entry.profile, bucket);
145
+ }
146
+ }
147
+ const rows = [...byProfile.entries()]
148
+ .sort((a, b) => b[1].ms - a[1].ms)
149
+ .map(([profile, b]) =>
150
+ ` ${profile}: ${b.files} modules, ${b.ms.toFixed(0)} ms nested-wall, ${b.esbuild} via esbuild`);
151
+ console.error(
152
+ [
153
+ `dev-module-stats ${moduleStats.size} distinct files, ${total} compiles`,
154
+ ...rows,
155
+ ` cross-profile: ${duplicated} files compiled in >1 profile, ` +
156
+ `${duplicateCompiles} redundant compiles, ${duplicateMs.toFixed(0)} ms nested-wall`,
157
+ ].join('\n'),
158
+ );
159
+ // PNEXT_DEV_MODULE_STATS_OUT=<path>: also dump the per-file list, so a floor
160
+ // bench can replay the exact module set a route compiles in each profile.
161
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
162
+ const out = process.env.PNEXT_DEV_MODULE_STATS_OUT;
163
+ if (!out) return;
164
+ const files = [...moduleStats.entries()].flatMap(([file, entries]) =>
165
+ entries.map(entry => ({ file, profile: entry.profile, ms: entry.ms, esbuild: entry.esbuild })));
166
+ void writeFile(out, JSON.stringify(files, null, 0)).catch(() => undefined);
167
+ }
168
+ const routeModuleLoaders = new Map<string, Promise<DevRouteModuleLoaders>>();
169
+
170
+ // A module that throws while evaluating stays failed in Bun's registry for the process lifetime, and
171
+ // re-importing it surfaces downstream symptoms (a missing bundle entry, TDZ on an export) instead of
172
+ // the original error. Record the first failure per compiled href and re-throw THAT on every later
173
+ // request. Compiled hrefs are content-addressed, so a save that fixes the module yields a new href
174
+ // and the entry is never consulted again.
175
+ const moduleEvalErrors = new Map<string, unknown>();
176
+
177
+ /** Import a compiled dev module, re-throwing its first evaluation error on later imports. */
178
+ export async function importDevModule<T>(href: string): Promise<T> {
179
+ if (moduleEvalErrors.has(href)) throw moduleEvalErrors.get(href);
180
+ // Pre-planned vendor builds are enqueued without an awaiter; nothing
181
+ // evaluates until they all settle (no-op when the pipeline is empty).
182
+ await drainPreplanBuilds();
183
+ try {
184
+ const module = (await import(href)) as T;
185
+ noteModuleImported(href);
186
+ return module;
187
+ } catch (error) {
188
+ moduleEvalErrors.set(href, error);
189
+ throw error;
190
+ }
191
+ }
192
+ const extensionlessDynamicImportExtensions = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts']);
193
+ const externalLoadNamespaces = {
194
+ server: 'pnext-route-external-server',
195
+ client: 'pnext-route-external-client',
196
+ module: 'pnext-module-external',
197
+ } as const;
198
+
199
+ const moduleCaches = new WeakMap<ResolvedConfig, DevModuleCache>();
200
+
201
+ /**
202
+ * The content-addressing cache behind every compiled path (dev/module-cache.ts).
203
+ * Artifacts are named by a hash of their whole source graph, so this is also the
204
+ * thing that decides what a save invalidates.
205
+ */
206
+ export function devModuleGraph(config: ResolvedConfig): DevModuleCache {
207
+ const existing = moduleCaches.get(config);
208
+ if (existing) return existing;
209
+ const cache = devModuleCache(config, {
210
+ compileKey: devCompileKey(config),
211
+ edges: (file, source) => graphEdges(config, file, source),
212
+ });
213
+ moduleCaches.set(config, cache);
214
+ return cache;
215
+ }
216
+
217
+ /**
218
+ * Pay the compile pipeline's FIXED first-use costs - esbuild's service child, the oxc native
219
+ * bindings, the cache marker and persisted graph index - without compiling anything. Nothing here is
220
+ * route-specific, so a fresh checkout still compiles zero routes. Every step is idempotent and shares
221
+ * the memo the request path uses, so racing a real first request costs nothing.
222
+ *
223
+ * Call only after `bootstrapCompat`: the graph's compile key hashes the alias extensions compat
224
+ * registers, and it is memoized from the first read.
225
+ */
226
+ export async function warmDevModulePipeline(config: ResolvedConfig) {
227
+ const source = 'import "./pnext-warm-import";\nexport const warm: number = 1;\n';
228
+ const file = path.join(config.root, 'pnext-warm.ts');
229
+ await Promise.allSettled([
230
+ // esbuild spawns its Go service child and handshakes on the first build.
231
+ build({ stdin: { contents: '0', loader: 'ts', resolveDir: config.root }, write: false, logLevel: 'silent' }),
232
+ Promise.resolve().then(() => {
233
+ importSpecifiers(source, file); // oxc-parser
234
+ transformServerModule(source, file, serverDefineOptions().define); // oxc-transform
235
+ resolveLocalImport(config, file, './pnext-warm-import'); // oxc-resolver factory
236
+ devModuleGraph(config); // cache marker + graph.json + framework fingerprint
237
+ }),
238
+ ]);
239
+ }
240
+
241
+ /** Compile inputs that are not sources: a change to any of them renames everything. */
242
+ function devCompileKey(config: ResolvedConfig) {
243
+ return JSON.stringify({
244
+ // Relative, never absolute: the key names every artifact, and a deployed
245
+ // build runs the same workspace from a different root (see module-cache).
246
+ root: toPosixPath(path.relative(config.workspaceRoot, config.root)),
247
+ compat: config.compat ?? null,
248
+ nextConfig: nextConfigFingerprint(config),
249
+ define: serverDefineOptions().define ?? null,
250
+ aliases: [...devAliasKeys(config)].sort(),
251
+ });
252
+ }
253
+
254
+ // next.config knobs (modularizeImports, resolve aliases, compiler options) shape compiled output, and only
255
+ // some of them reach the alias/define maps above - so the config's own bytes are part of the key. Content,
256
+ // never mtime: a rewritten-but-identical config must not rename every artifact. Files the config imports
257
+ // are not covered.
258
+ const NEXT_CONFIG_BASENAMES = [
259
+ 'next.config.ts',
260
+ 'next.config.mts',
261
+ 'next.config.cts',
262
+ 'next.config.js',
263
+ 'next.config.mjs',
264
+ 'next.config.cjs',
265
+ ];
266
+
267
+ function nextConfigFingerprint(config: ResolvedConfig) {
268
+ if (!config.compat?.next) return '';
269
+ for (const name of NEXT_CONFIG_BASENAMES) {
270
+ const candidate = path.join(config.root, name);
271
+ if (!existsSync(candidate)) continue;
272
+ try {
273
+ return `${name}:${Bun.hash(readFileSync(candidate)).toString(36)}`;
274
+ } catch {
275
+ return name;
276
+ }
277
+ }
278
+ return '';
279
+ }
280
+
281
+ const aliasKeys = new WeakMap<ResolvedConfig, Set<string>>();
282
+
283
+ /** Specifiers every profile rewrites at build time — never source edges. */
284
+ function devAliasKeys(config: ResolvedConfig) {
285
+ const existing = aliasKeys.get(config);
286
+ if (existing) return existing;
287
+ const keys = new Set([
288
+ ...Object.keys(coreAliases(config, 'server')),
289
+ ...Object.keys(getImportAliasExtensions().reactServerLayerAliases(config)),
290
+ ...Object.keys(clientSsrAliases(config)),
291
+ ]);
292
+ aliasKeys.set(config, keys);
293
+ return keys;
294
+ }
295
+
296
+ /**
297
+ * Source edges the graph hash follows. A superset of the compiled graph is safe - it only widens what
298
+ * a save invalidates - but a subset would leave a dependent holding a name that no longer describes
299
+ * its content, so assets and client components are edges here even where the compile walk skips them.
300
+ */
301
+ function graphEdges(config: ResolvedConfig, file: string, source: string) {
302
+ const aliases = devAliasKeys(config);
303
+ const imports: string[] = [];
304
+ const packages: string[] = [];
305
+ for (const specifier of importSpecifiers(source, file)) {
306
+ const { sourcePath } = splitHash(specifier);
307
+ if (aliases.has(specifier) || aliases.has(sourcePath)) continue;
308
+ const resolved = resolveLocalImport(config, file, sourcePath.replace(/\?.*$/, ''));
309
+ const isLocalSource = sourcePath.startsWith('.') || path.isAbsolute(sourcePath);
310
+ // Compiled output is never a source of the graph that produced it.
311
+ if (resolved && isInside(config.outPath, resolved)) continue;
312
+ if (resolved && (isLocalSource || isInside(config.workspaceRoot, resolved))) {
313
+ imports.push(path.resolve(resolved));
314
+ continue;
315
+ }
316
+ // Registry package demand: recorded here so a route's vendor bundles can be
317
+ // started as their own stage instead of blocking the module pass at first
318
+ // use.
319
+ if (!isLocalSource && isPackageSpecifier(sourcePath)) packages.push(sourcePath);
320
+ }
321
+ return { imports, packages, client: hasUseClientDirective(source) };
322
+ }
323
+
324
+ function withBuildSlot<T>(task: () => Promise<T>): Promise<T> {
325
+ return new Promise<T>((resolve, reject) => {
326
+ const run = () => {
327
+ buildActive += 1;
328
+ task().then(resolve, reject).finally(() => {
329
+ buildActive -= 1;
330
+ buildQueue.shift()?.();
331
+ });
332
+ };
333
+ if (buildActive < BUILD_CONCURRENCY) run();
334
+ else buildQueue.push(run);
335
+ });
336
+ }
337
+
338
+ export interface DevServerModuleOptions {
339
+ conditionTarget?: ServerBundleTarget;
340
+ externalLoadTarget?: ExternalLoadTarget;
341
+ /**
342
+ * True for modules compiled under the true `react-server` layer (App RSC,
343
+ * route handlers, proxy/middleware): layers stricter React alias overrides
344
+ * with no client hook exports on top of the base server aliases.
345
+ * Pages/api and other server modules keep the base aliases (full-hooks
346
+ * `react`, kept for backward compatibility).
347
+ */
348
+ reactServerLayer?: boolean;
349
+ }
350
+
351
+ export async function devServerModuleHref(
352
+ config: ResolvedConfig,
353
+ file: string,
354
+ // Compiled paths are content-addressed; the caller's dev import version no
355
+ // longer names them (it still keys compat's own caches).
356
+ _version?: string,
357
+ moduleOptions: DevServerModuleOptions = {},
358
+ ) {
359
+ const conditionTarget = pagesLayerConditionTarget(
360
+ config,
361
+ file,
362
+ moduleOptions.conditionTarget ?? 'server',
363
+ );
364
+ const reactServerLayer =
365
+ moduleOptions.reactServerLayer ??
366
+ (conditionTarget === 'server' || conditionTarget === 'edge');
367
+ const release = devModuleGraph(config).hold();
368
+ const compiled = await writeDevModule(config, file, new Set(), {
369
+ aliases: {
370
+ ...coreAliases(config, 'server'),
371
+ ...(reactServerLayer
372
+ ? getImportAliasExtensions().reactServerLayerAliases(config)
373
+ : {}),
374
+ },
375
+ profile: reactCompatEnabled(config) ? 'compat' : 'server',
376
+ conditionTarget,
377
+ reactServerLayer,
378
+ externalLoadTarget:
379
+ moduleOptions.externalLoadTarget ??
380
+ externalLoadTargetForConditionTarget(conditionTarget),
381
+ stubClientImports: reactCompatEnabled(config),
382
+ rewriteExternalServerImports: true,
383
+ bundleExternalPackages: reactCompatEnabled(config),
384
+ }).finally(release);
385
+ return pathToFileHref(compiled);
386
+ }
387
+
388
+ export async function devClientModuleHref(
389
+ config: ResolvedConfig,
390
+ file: string,
391
+ _version?: string,
392
+ serverTarget?: ServerBundleTarget,
393
+ ) {
394
+ const conditionTarget = clientLayerConditionTarget(config, file, serverTarget ?? 'server');
395
+ const throughPackage = await packageClientModuleHref(config, file, conditionTarget);
396
+ if (throughPackage) return throughPackage;
397
+ const release = devModuleGraph(config).hold();
398
+ const compiled = await writeDevModule(
399
+ config,
400
+ file,
401
+ new Set(),
402
+ clientLayerOptions(config, conditionTarget),
403
+ ).finally(release);
404
+ return pathToFileHref(compiled);
405
+ }
406
+
407
+ /**
408
+ * The client-layer module a `'use client'` file inside node_modules SSRs from: its package's own
409
+ * client vendor bundle, the same artifact the app's client code reaches that package through.
410
+ *
411
+ * Compiling the file a SECOND time as a standalone module would hand the SSR pass its own copy of
412
+ * everything the file shares with the rest of the package - a `createContext` call, a registry, a
413
+ * singleton - and a provider rendered from one copy is invisible to a consumer rendered from the
414
+ * other. The browser build is unaffected: it compiles the whole client graph at once.
415
+ *
416
+ * Undefined means "compile it as a module", which is the answer whenever the package cannot be shown
417
+ * to publish the file's exports.
418
+ */
419
+ async function packageClientModuleHref(
420
+ config: ResolvedConfig,
421
+ file: string,
422
+ conditionTarget: ServerBundleTarget,
423
+ ) {
424
+ if (!reactCompatEnabled(config)) return undefined;
425
+ const packageDir = nodeModulesPackageDir(file);
426
+ if (!packageDir) return undefined;
427
+ const specifier = await packageSpecifierPublishing(config, packageDir, file);
428
+ if (!specifier) return undefined;
429
+ return externalServerPackageHref(
430
+ config,
431
+ specifier,
432
+ 'client',
433
+ path.dirname(file),
434
+ conditionTarget,
435
+ ).catch(() => undefined);
436
+ }
437
+
438
+ /**
439
+ * The specifier an importer would use to get `file`'s exports out of its
440
+ * package: the `exports` subpath that resolves to it, or the package itself
441
+ * when its entry re-exports every name the file has.
442
+ */
443
+ async function packageSpecifierPublishing(
444
+ config: ResolvedConfig,
445
+ packageDir: string,
446
+ file: string,
447
+ ) {
448
+ const packageName = packageNameOfDirectory(packageDir);
449
+ if (!packageName) return undefined;
450
+ for (const subpath of packageExportSubpaths(packageDir)) {
451
+ const specifier = subpath === '.' ? packageName : `${packageName}/${subpath.slice(2)}`;
452
+ if (resolvePackageFile(config, file, specifier) === file) return specifier;
453
+ }
454
+ const entry = resolvePackageFile(config, file, packageName);
455
+ if (!entry || entry === file) return entry ? packageName : undefined;
456
+ const published = new Set(await clientReferenceExportNames(config, entry));
457
+ const names = await clientReferenceExportNames(config, file);
458
+ return names.length > 0 && names.every(name => published.has(name)) ? packageName : undefined;
459
+ }
460
+
461
+ /** The client layer's own conditions, so the file matches the bundle's entry. */
462
+ function resolvePackageFile(config: ResolvedConfig, fromFile: string, specifier: string) {
463
+ return resolvePackageSpecifier(config.root, fromFile, specifier, ['module', 'import']);
464
+ }
465
+
466
+ /** Every `exports` key of a package manifest; `{}` and sugar forms included. */
467
+ function packageExportSubpaths(packageDir: string) {
468
+ try {
469
+ const manifest = JSON.parse(readFileSync(path.join(packageDir, 'package.json'), 'utf8')) as {
470
+ exports?: unknown;
471
+ };
472
+ if (typeof manifest.exports !== 'object' || manifest.exports === null) return ['.'];
473
+ const keys = Object.keys(manifest.exports).filter(key => key.startsWith('.'));
474
+ // A conditions-only map (`{ import: ... }`) has no subpath keys at all.
475
+ return keys.length > 0 ? keys.filter(key => !key.includes('*')) : ['.'];
476
+ } catch {
477
+ return ['.'];
478
+ }
479
+ }
480
+
481
+ /** `…/node_modules/@scope/name` -> `@scope/name`. */
482
+ function packageNameOfDirectory(packageDir: string) {
483
+ const parts = packageDir.split(path.sep);
484
+ const index = parts.lastIndexOf('node_modules');
485
+ if (index === -1) return undefined;
486
+ return parts.slice(index + 1).join('/');
487
+ }
488
+
489
+ /** `…/node_modules/@scope/name/build/x.js` -> `…/node_modules/@scope/name`. */
490
+ function nodeModulesPackageDir(file: string) {
491
+ const parts = file.split(path.sep);
492
+ const index = parts.lastIndexOf('node_modules');
493
+ if (index === -1) return undefined;
494
+ const end = index + (parts[index + 1]?.startsWith('@') ? 3 : 2);
495
+ return end <= parts.length ? parts.slice(0, end).join(path.sep) : undefined;
496
+ }
497
+
498
+ export interface DevRouteModuleLoaders {
499
+ moduleLoader: (file: string) => Promise<Record<string, unknown>>;
500
+ clientModuleLoader: (file: string) => Promise<Record<string, unknown>>;
501
+ }
502
+
503
+ interface DevModuleOptions {
504
+ aliases: AliasMap;
505
+ profile: 'server' | 'compat' | 'client';
506
+ conditionTarget: ServerBundleTarget;
507
+ reactServerLayer?: boolean;
508
+ externalLoadTarget: ExternalLoadTarget;
509
+ stubClientImports: boolean;
510
+ rewriteExternalServerImports: boolean;
511
+ bundleExternalPackages: boolean;
512
+ }
513
+
514
+ /**
515
+ * Start this route's vendor bundles as an independent stage. The graph walk
516
+ * that names the artifacts already knows every package the route reaches, so
517
+ * the builds run alongside the module pass rather than serially in front of the
518
+ * first module that imports one. Every build is deduped by the vendor cache, so
519
+ * the module pass coalesces onto these instead of starting its own.
520
+ */
521
+ function startRouteVendorStage(config: ResolvedConfig, route: RouteManifestEntry, files: string[]) {
522
+ // Kill switch for bisecting a suspected stage-overlap difference.
523
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
524
+ if (process.env.PNEXT_DISABLE_VENDOR_STAGE) return;
525
+ if (!reactCompatEnabled(config)) return;
526
+ const conditionTarget = serverBundleTargetForRuntime(route.segmentConfig?.runtime);
527
+ const options: DevModuleOptions = {
528
+ aliases: coreAliases(config, 'server'),
529
+ profile: 'compat',
530
+ conditionTarget,
531
+ externalLoadTarget: externalLoadTargetForConditionTarget(conditionTarget),
532
+ stubClientImports: true,
533
+ rewriteExternalServerImports: true,
534
+ bundleExternalPackages: true,
535
+ };
536
+ // Only the SERVER vendor layer is seeded here: the client-reference walk already fans the
537
+ // client-SSR demand out, so seeding that layer too only competes for the JS thread.
538
+ //
539
+ // Layered seeding: a separate walk pruned at 'use client' boundaries, so client-only packages get
540
+ // no server-target builds. On-demand fallback covers anything needed sooner.
541
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
542
+ const layered = process.env.PNEXT_VENDOR_SEED_LAYERED !== '0';
543
+ const seedT0 = performance.now();
544
+ void (async () => {
545
+ const graph = devModuleGraph(config);
546
+ const demand = new Set<string>();
547
+ const dropped = new Set<string>();
548
+ // The layer of a source is part of the record the naming walk just scanned,
549
+ // so the seed reads it instead of re-reading the whole route graph.
550
+ const prune = (file: string) =>
551
+ (devHeadTrimEnabled() ? graph.isClientSource(file) : fileHasUseClientDirective(file)).catch(
552
+ () => false,
553
+ );
554
+ for (const file of files) {
555
+ const full = await graph.packageDemand(file);
556
+ const serverReachable = layered ? new Set(await graph.packageDemand(file, prune)) : undefined;
557
+ for (const specifier of full) {
558
+ if (!shouldBundleExternalPackage(specifier, config, file, options)) continue;
559
+ if (serverReachable && !serverReachable.has(specifier)) dropped.add(specifier);
560
+ else demand.add(specifier);
561
+ }
562
+ }
563
+ for (const specifier of demand) dropped.delete(specifier);
564
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
565
+ if (process.env.PNEXT_DEV_PROFILE) {
566
+ console.log(
567
+ `dev-import vendor seed walk ${route.route} in ${formatDuration(performance.now() - seedT0)} (kept ${demand.size}, dropped ${dropped.size})`,
568
+ );
569
+ }
570
+ if (vendorTraceEnabled()) {
571
+ vendorTraceRow({
572
+ kind: 'seed',
573
+ route: route.route,
574
+ layered,
575
+ kept: [...demand].sort(),
576
+ dropped: [...dropped].sort(),
577
+ atMs: performance.now(),
578
+ });
579
+ }
580
+ const warm = (specifiers: Iterable<string>) =>
581
+ Promise.all(
582
+ [...specifiers].map(specifier =>
583
+ externalServerPackageHref(config, specifier, 'server', config.root, conditionTarget).catch(
584
+ () => undefined,
585
+ ),
586
+ ),
587
+ );
588
+ await warm(demand);
589
+ })().catch(() => undefined);
590
+ }
591
+
592
+ export async function devRouteModuleLoaders(
593
+ config: ResolvedConfig,
594
+ route: RouteManifestEntry,
595
+ layoutFiles: string[],
596
+ _version?: string,
597
+ ): Promise<DevRouteModuleLoaders> {
598
+ // The bundle's own content-addressed path is the cache key: a save that
599
+ // changes nothing this route imports keeps it, one that does gives a new
600
+ // path — no version bump, no wholesale eviction.
601
+ const release = devModuleGraph(config).hold();
602
+ const key = await profileDevImport(`route bundle key ${route.route}`, () =>
603
+ devRouteBundlePath(config, route, layoutFiles),
604
+ ).catch(error => {
605
+ release();
606
+ throw error;
607
+ });
608
+ noteModuleGeneration(`bundle:${route.route}`, key);
609
+ const existing = routeModuleLoaders.get(key);
610
+ if (existing) {
611
+ release();
612
+ return existing;
613
+ }
614
+ // A bundle already on disk was built against vendor artifacts the previous process left next to it, so
615
+ // seeding the stage would walk the whole graph again to demand what is already there - and it would do it
616
+ // on the JS thread the restart's first response is waiting on. Anything genuinely missing is still built
617
+ // on demand by the resolve path.
618
+ if (!devArtifactUsable(key)) {
619
+ startRouteVendorStage(config, route, uniqueFiles([route.file, ...layoutFiles]));
620
+ }
621
+
622
+ const next = createDevRouteModuleLoaders(config, route, layoutFiles, key)
623
+ .catch(error => {
624
+ routeModuleLoaders.delete(key);
625
+ throw error;
626
+ })
627
+ .finally(release);
628
+ routeModuleLoaders.set(key, next);
629
+ return next;
630
+ }
631
+
632
+ async function createDevRouteModuleLoaders(
633
+ config: ResolvedConfig,
634
+ route: RouteManifestEntry,
635
+ layoutFiles: string[],
636
+ outFile: string,
637
+ ): Promise<DevRouteModuleLoaders> {
638
+ const bundleFile = await writeDevRouteBundle(config, route, layoutFiles, outFile);
639
+ const bundle = await profileDevImport(`import route ${route.route}`, () =>
640
+ importDevModule<DevRouteBundle>(pathToFileHref(bundleFile)));
641
+ const moduleLoader = async (file: string) => {
642
+ const module = bundle.modules[file];
643
+ if (module) return module;
644
+ return importDevModule<Record<string, unknown>>(
645
+ await devServerModuleHref(config, file, undefined, {
646
+ conditionTarget: serverBundleTargetForRuntime(route.segmentConfig?.runtime),
647
+ }),
648
+ );
649
+ };
650
+ const clientModuleLoader = async (file: string) => {
651
+ const module = bundle.clientModules[file];
652
+ if (module) return module;
653
+ return importDevModule<Record<string, unknown>>(
654
+ await devClientModuleHref(
655
+ config,
656
+ file,
657
+ undefined,
658
+ serverBundleTargetForRuntime(route.segmentConfig?.runtime),
659
+ ),
660
+ );
661
+ };
662
+ return { moduleLoader, clientModuleLoader };
663
+ }
664
+
665
+ interface DevRouteBundle {
666
+ modules: Record<string, Record<string, unknown>>;
667
+ clientModules: Record<string, Record<string, unknown>>;
668
+ }
669
+
670
+ async function writeDevRouteBundle(
671
+ config: ResolvedConfig,
672
+ route: RouteManifestEntry,
673
+ layoutFiles: string[],
674
+ outFile: string,
675
+ ) {
676
+ if (devArtifactUsable(outFile)) return outFile;
677
+
678
+ const existing = routeBundleBuilds.get(outFile);
679
+ if (existing) return existing;
680
+
681
+ const next = profileDevImport(`build route ${route.route}`, async () => {
682
+ await mkdir(path.dirname(outFile), { recursive: true });
683
+ const serverFiles = uniqueFiles([
684
+ ...(route.client ? [] : [route.file]),
685
+ ...layoutFiles,
686
+ ]);
687
+ const result = await withBuildSlot(() => build({
688
+ stdin: {
689
+ contents: routeBundleEntrySource(serverFiles, []),
690
+ loader: 'ts',
691
+ resolveDir: config.root,
692
+ sourcefile: `${route.id}.route-bundle.ts`,
693
+ },
694
+ bundle: true,
695
+ write: false,
696
+ format: 'esm',
697
+ platform: 'neutral',
698
+ target: 'es2022',
699
+ // Workspace .js/.mjs app modules may contain JSX (packages stay
700
+ // external, so node_modules never reach these loaders).
701
+ loader: { '.js': 'jsx', '.mjs': 'jsx' },
702
+ jsx: 'automatic',
703
+ jsxImportSource: 'preact',
704
+ packages: 'external',
705
+ logLevel: 'silent',
706
+ ...serverDefineOptions(),
707
+ plugins: [
708
+ serverAssetPlugin(config),
709
+ // Ahead of the extension plugins: they resolve without a namespace, so a tsconfig-`paths`
710
+ // import from a route-bundle module would land in esbuild's default namespace and take its
711
+ // whole subtree with it - out of this build's hooks, resolving bare specifiers like
712
+ // `next/dynamic` against real node_modules instead of the compat aliases.
713
+ devRouteBundlePlugin(config, route),
714
+ ...getBundlerExtensions().serverEsbuildPlugins(config),
715
+ ],
716
+ }));
717
+ const output = result.outputFiles[0];
718
+ if (!output) throw new Error(`Failed to build dev route bundle for ${route.route}`);
719
+ await writeCompiledFile(outFile, output.text);
720
+ return outFile;
721
+ }).catch(error => {
722
+ routeBundleBuilds.delete(outFile);
723
+ throw error;
724
+ });
725
+
726
+ routeBundleBuilds.set(outFile, next);
727
+ return next;
728
+ }
729
+
730
+ function routeBundleEntrySource(serverFiles: string[], clientFiles: string[]) {
731
+ const serverImports = serverFiles.map((file, index) =>
732
+ `import * as server${index} from ${JSON.stringify(`pnext-server:${file}`)};`);
733
+ const clientImports = clientFiles.map((file, index) =>
734
+ `import * as client${index} from ${JSON.stringify(`pnext-client:${file}`)};`);
735
+ const serverEntries = serverFiles.map((file, index) =>
736
+ `${JSON.stringify(file)}: server${index},`);
737
+ const clientEntries = clientFiles.map((file, index) =>
738
+ `${JSON.stringify(file)}: client${index},`);
739
+
740
+ return [
741
+ ...serverImports,
742
+ ...clientImports,
743
+ `export const modules = {${serverEntries.join('')}};`,
744
+ `export const clientModules = {${clientEntries.join('')}};`,
745
+ ].join('\n');
746
+ }
747
+
748
+ function devRouteBundlePlugin(config: ResolvedConfig, route: RouteManifestEntry): Plugin {
749
+ const conditionTarget = serverBundleTargetForRuntime(route.segmentConfig?.runtime);
750
+ const serverOptions: DevModuleOptions = {
751
+ aliases: {
752
+ ...coreAliases(config, 'server'),
753
+ ...getImportAliasExtensions().reactServerLayerAliases(config),
754
+ },
755
+ profile: reactCompatEnabled(config) ? 'compat' : 'server',
756
+ conditionTarget,
757
+ reactServerLayer: true,
758
+ externalLoadTarget: externalLoadTargetForConditionTarget(conditionTarget),
759
+ stubClientImports: reactCompatEnabled(config),
760
+ rewriteExternalServerImports: true,
761
+ bundleExternalPackages: reactCompatEnabled(config),
762
+ };
763
+ const clientOptions: DevModuleOptions = {
764
+ aliases: clientSsrAliases(config),
765
+ profile: 'client',
766
+ conditionTarget: 'client',
767
+ externalLoadTarget: 'client-ssr',
768
+ stubClientImports: false,
769
+ rewriteExternalServerImports: false,
770
+ bundleExternalPackages: reactCompatEnabled(config),
771
+ };
772
+
773
+ return {
774
+ name: 'pnext-dev-route-bundle',
775
+ async setup(build) {
776
+ // Ensure the compat client-action module set is discovered for the active
777
+ // dev version before any route-client module loads (dev startup/reload used
778
+ // to run this inline). No-op for pure-core / non-next apps.
779
+ if (nextCompatEnabled(config)) await getClientActionBundler()?.ensureArmed();
780
+ build.onResolve({ filter: /^file:\/\// }, args => ({ path: args.path, external: true }));
781
+ build.onResolve({ filter: /^pnext-server:/ }, args => ({
782
+ path: args.path.slice('pnext-server:'.length),
783
+ namespace: 'pnext-route-server',
784
+ }));
785
+ build.onResolve({ filter: /^pnext-client:/ }, args => ({
786
+ path: args.path.slice('pnext-client:'.length),
787
+ namespace: 'pnext-route-client',
788
+ }));
789
+
790
+ build.onResolve({ filter: /.*/, namespace: 'pnext-route-server' }, args =>
791
+ resolveRouteBundleSpecifier(config, args.path, args.importer, 'pnext-route-server', serverOptions),
792
+ );
793
+ build.onResolve({ filter: /.*/, namespace: 'pnext-route-client' }, args =>
794
+ resolveRouteBundleSpecifier(config, args.path, args.importer, 'pnext-route-client', clientOptions),
795
+ );
796
+ registerExternalLoadHandlers(build, config, serverOptions, externalLoadNamespaces.server);
797
+ registerExternalLoadHandlers(build, config, clientOptions, externalLoadNamespaces.client);
798
+
799
+ build.onLoad({ filter: /.*/, namespace: 'pnext-route-server' }, async args => ({
800
+ contents: rewriteServerSource(await readText(args.path), args.path, {
801
+ nextFonts: nextCompatEnabled(config),
802
+ root: config.root,
803
+ }),
804
+ loader: esbuildLoader(args.path),
805
+ }));
806
+ build.onLoad({ filter: /.*/, namespace: 'pnext-route-client' }, async args => {
807
+ // A client-side import of a 'use server' module becomes the RPC stub.
808
+ const clientActions = nextCompatEnabled(config) ? getClientActionBundler() : undefined;
809
+ if (clientActions?.isClientActionModule(args.path)) {
810
+ return {
811
+ contents: await clientActions.stubSource(args.path),
812
+ loader: 'js',
813
+ };
814
+ }
815
+ return {
816
+ contents: rewriteServerSource(await readText(args.path), args.path, {
817
+ nextFonts: nextCompatEnabled(config),
818
+ root: config.root,
819
+ }),
820
+ loader: esbuildLoader(args.path),
821
+ };
822
+ });
823
+ build.onLoad({ filter: /.*/, namespace: 'pnext-client-reference' }, async args => ({
824
+ contents: clientReferenceModuleSource(args.path, await clientReferenceExportNames(config, args.path)),
825
+ loader: 'js',
826
+ }));
827
+ },
828
+ };
829
+ }
830
+
831
+ /**
832
+ * Pages-router source modules are never react-server modules: they may import client hooks at top
833
+ * level while only their data functions (`getStaticProps`/`getServerSideProps`) run on the server -
834
+ * the component half renders through the 'use client' facade. Under the react-server layer alias
835
+ * (hook-free react entry) evaluating such a module throws at import binding, so resolve
836
+ * react/react-dom for those importers to the base full-hooks server shims instead.
837
+ */
838
+ function pagesCompatSourceAlias(
839
+ config: ResolvedConfig,
840
+ specifier: string,
841
+ importer: string | undefined,
842
+ options: DevModuleOptions,
843
+ ): string | undefined {
844
+ if (!options.reactServerLayer) return undefined;
845
+ if (specifier !== 'react' && specifier !== 'react-dom') return undefined;
846
+ if (!importer) return undefined;
847
+ const posix = importer.replace(/\\/g, '/');
848
+ if (!posix.includes('/pnext-pages-compat/') || !posix.includes('/source-pages/'))
849
+ return undefined;
850
+ return coreAliases(config, 'server')[specifier];
851
+ }
852
+
853
+ async function resolveRouteBundleSpecifier(
854
+ config: ResolvedConfig,
855
+ specifier: string,
856
+ importer: string,
857
+ namespace: 'pnext-route-server' | 'pnext-route-client',
858
+ options: DevModuleOptions,
859
+ ): Promise<OnResolveResult | undefined> {
860
+ const compatAlias =
861
+ pagesCompatSourceAlias(config, specifier, importer, options) ?? options.aliases[specifier];
862
+ if (compatAlias) {
863
+ return {
864
+ path: path.isAbsolute(compatAlias) ? pathToFileHref(compatAlias) : compatAlias,
865
+ external: true,
866
+ };
867
+ }
868
+
869
+ // A `turbopack.rules` loader chain owns this specifier (see the same check in
870
+ // resolveDevModuleSpecifier): import its materialized output.
871
+ const pendingRuleModule = getAssetExtensions().loaderRuleModule(specifier, importer);
872
+ const ruleModule = pendingRuleModule === undefined ? undefined : await pendingRuleModule;
873
+ if (ruleModule) return { path: pathToFileHref(ruleModule), external: true };
874
+
875
+ if (isServerIgnoredAssetSpecifier(specifier)) return undefined;
876
+
877
+ const { sourcePath, hash } = splitHash(specifier);
878
+ const resolved = resolveLocalImport(config, importer, sourcePath);
879
+ // `resolveExtensions` can land an extensionless specifier on an asset
880
+ // (`import img from './image'` -> image.png), which the specifier-keyed check
881
+ // above and serverAssetPlugin's own filter both miss. Hand it to the asset
882
+ // namespace, or the loader below parses the image bytes as source.
883
+ if (
884
+ resolved &&
885
+ isServerIgnoredAssetSpecifier(resolved) &&
886
+ !getAssetExtensions().hasLoaderRuleFor(resolved)
887
+ ) {
888
+ return { path: resolved, namespace: serverAssetNamespace };
889
+ }
890
+ if (resolved && isInside(config.workspaceRoot, resolved)) {
891
+ if (
892
+ options.stubClientImports &&
893
+ !isCssFile(resolved) &&
894
+ await fileHasUseClientDirective(resolved)
895
+ ) {
896
+ return { path: resolved, namespace: 'pnext-client-reference' };
897
+ }
898
+ return { path: resolved, namespace };
899
+ }
900
+ // An absolute path that lands outside the workspace is framework runtime a rewrite injected by path
901
+ // rather than by alias - pnext's own source is not part of the app's module graph, so it loads
902
+ // externally exactly like the alias branch above. Without this the bundle cannot resolve it at all:
903
+ // esbuild does not fall back to the filesystem for an import raised from a plugin namespace, so the
904
+ // whole route bundle fails and the route silently drops to per-module loading.
905
+ if (resolved && path.isAbsolute(sourcePath)) {
906
+ return { path: `${pathToFileHref(resolved)}${hash}`, external: true };
907
+ }
908
+
909
+ if (options.bundleExternalPackages && shouldBundleExternalPackage(sourcePath, config, importer, options)) {
910
+ return {
911
+ path: await externalServerPackageHref(
912
+ config,
913
+ sourcePath,
914
+ options.profile === 'client' ? 'client' : 'server',
915
+ path.dirname(importer),
916
+ options.conditionTarget,
917
+ ),
918
+ external: true,
919
+ };
920
+ }
921
+
922
+ if (isPackageSpecifier(sourcePath) || isBuiltinSpecifier(sourcePath)) {
923
+ const resolvedExternalLoad = resolveExternalLoadTarget({
924
+ root: config.root,
925
+ fromFile: importer,
926
+ specifier: sourcePath,
927
+ target: options.externalLoadTarget,
928
+ });
929
+ if (resolvedExternalLoad && await externalLoadNeedsFacade(resolvedExternalLoad)) {
930
+ return {
931
+ path: resolvedExternalLoad,
932
+ namespace: options.profile === 'client'
933
+ ? externalLoadNamespaces.client
934
+ : externalLoadNamespaces.server,
935
+ };
936
+ }
937
+ const externalTarget =
938
+ resolvedExternalLoad ?? externalPackageImportTarget(config.root, importer, sourcePath);
939
+ return {
940
+ path: externalTarget
941
+ ? `${pathToFileHref(externalTarget)}${hash}`
942
+ : options.rewriteExternalServerImports
943
+ ? runtimeServerImportTarget(specifier)
944
+ : specifier,
945
+ external: true,
946
+ };
947
+ }
948
+
949
+ return undefined;
950
+ }
951
+
952
+ // Sidecar of each artifact's raw import specifiers so the vercel trace reads
953
+ // them back instead of re-parsing artifacts. Off outside vercel prod builds.
954
+ let emitCompiledSpecifiersManifest = false;
955
+
956
+ /** @internal Test-only. Returns a restore function. */
957
+ export function setEmitCompiledSpecifiersManifest(enabled: boolean) {
958
+ const previous = emitCompiledSpecifiersManifest;
959
+ emitCompiledSpecifiersManifest = enabled;
960
+ return () => {
961
+ emitCompiledSpecifiersManifest = previous;
962
+ };
963
+ }
964
+
965
+ /** Sidecar suffix for a compiled artifact's recorded specifiers. */
966
+ export const compiledSpecifiersManifestSuffix = '.pnext-specifiers.json';
967
+
968
+ const compiledScriptFilePattern = /\.(?:m?js|cjs|jsx|tsx?)$/;
969
+
970
+ async function writeCompiledFile(file: string, contents: string) {
971
+ await writeFileAtomic(file, contents);
972
+ noteDevArtifactWritten(file);
973
+ if (emitCompiledSpecifiersManifest && compiledScriptFilePattern.test(file)) {
974
+ await writeFileAtomic(
975
+ `${file}${compiledSpecifiersManifestSuffix}`,
976
+ JSON.stringify(importSpecifiers(contents, file)),
977
+ );
978
+ }
979
+ }
980
+
981
+ /** Where `file` compiles to under `profile`, named by its current source graph. */
982
+ async function devModulePathFor(config: ResolvedConfig, file: string, profile: string) {
983
+ return devServerPath(config, file, profile, await devModuleGraph(config).graphHash(file));
984
+ }
985
+
986
+ function devModulePath(config: ResolvedConfig, file: string, options: DevModuleOptions) {
987
+ return devModulePathFor(config, file, devModuleProfile(options));
988
+ }
989
+
990
+ async function writeDevModule(
991
+ config: ResolvedConfig,
992
+ file: string,
993
+ visited: Set<string>,
994
+ options: DevModuleOptions,
995
+ ) {
996
+ const profile = devModuleProfile(options);
997
+ const outFile = await devModulePath(config, file, options);
998
+ const visitedKey = `${profile}:${file}`;
999
+ noteModuleGeneration(visitedKey, outFile);
1000
+ if (visited.has(visitedKey)) return outFile;
1001
+
1002
+ // The name carries the hash of this module's whole source graph, so an existing artifact is by
1003
+ // construction current. An unusable one (the cache folder was wiped from outside) falls through to
1004
+ // recompile, which overwrites it in place - never delete first, since Bun caches a failed resolution for
1005
+ // the life of the process.
1006
+ if (devArtifactUsable(outFile)) return outFile;
1007
+
1008
+ const existing = moduleBuilds.get(outFile);
1009
+ if (existing) return existing;
1010
+
1011
+ visited.add(visitedKey);
1012
+ const started = moduleStats ? performance.now() : 0;
1013
+ const next = writeDevModuleUncached(config, file, visited, options, outFile)
1014
+ .then(result => {
1015
+ if (moduleStats) {
1016
+ noteModuleCompile(file, profile, performance.now() - started, esbuildFallbacks.delete(visitedKey));
1017
+ }
1018
+ return result;
1019
+ })
1020
+ .catch(error => {
1021
+ moduleBuilds.delete(outFile);
1022
+ throw error;
1023
+ })
1024
+ .finally(() => {
1025
+ moduleBuilds.delete(outFile);
1026
+ });
1027
+ moduleBuilds.set(outFile, next);
1028
+ return next;
1029
+ }
1030
+
1031
+ async function writeDevModuleUncached(
1032
+ config: ResolvedConfig,
1033
+ file: string,
1034
+ visited: Set<string>,
1035
+ options: DevModuleOptions,
1036
+ outFile: string,
1037
+ ) {
1038
+ await mkdir(path.dirname(outFile), { recursive: true });
1039
+ if (isCssFile(file)) {
1040
+ await copyFile(file, outFile);
1041
+ return outFile;
1042
+ }
1043
+
1044
+ // A client-profile compile of a 'use server' module emits the RPC stub, so
1045
+ // the browser bundle POSTs to the endpoint instead of shipping server code.
1046
+ if (options.profile === 'client' && nextCompatEnabled(config)) {
1047
+ const clientActions = getClientActionBundler();
1048
+ if (clientActions) {
1049
+ await clientActions.ensureArmed();
1050
+ if (clientActions.isClientActionModule(file)) {
1051
+ await writeCompiledFile(outFile, await clientActions.stubSource(file));
1052
+ return outFile;
1053
+ }
1054
+ }
1055
+ }
1056
+
1057
+ let source = await readText(file);
1058
+ if (/\.mdx?$/.test(file) && nextCompatEnabled(config)) {
1059
+ // Markdown modules must be compiled to JS before import analysis and the
1060
+ // stdin-based bundle pass (which bypasses file-resolved onLoad plugins).
1061
+ const { compileMdx } = await import('../compat/mdx/compile');
1062
+ source = (await compileMdx(source, file, config.root)).code;
1063
+ }
1064
+ const transformedSource = rewriteServerSource(source, file, {
1065
+ nextFonts: nextCompatEnabled(config),
1066
+ root: config.root,
1067
+ });
1068
+ const effectiveOptions =
1069
+ reactCompatEnabled(config) && options.profile !== 'client' && hasUseClientDirective(transformedSource)
1070
+ ? clientLayerOptions(config, clientLayerConditionTarget(config, file, options.conditionTarget))
1071
+ : options;
1072
+ // The asset-context link walk is pure I/O against directories nothing below
1073
+ // reads, so it overlaps the shaking/scan/child-compile work and is only
1074
+ // awaited before the write.
1075
+ const assetContext = linkAssetContext(config, file, devModuleProfile(effectiveOptions));
1076
+ // Rewrite destructured dynamic imports (`const { used } = await import('x')`)
1077
+ // to point at a tree-shaken facade so the target's unused exports are dropped
1078
+ // (compat: mirrors webpack's dynamic-import export usage analysis). Done
1079
+ // before the import scans below so the plain (all-exports) target is never
1080
+ // compiled when it's only reached through shakeable dynamic imports.
1081
+ // dynamic(ssr:false) split points: the SSR layer never runs those import()s,
1082
+ // so their subtrees (and the vendor demand they fan out) compile on browser
1083
+ // demand instead of on the cold critical path.
1084
+ const deferredDynamic =
1085
+ effectiveOptions.profile === 'client' && devDynamicSplitEnabled()
1086
+ ? deferredDynamicTargetFiles(transformedSource, config, file)
1087
+ : undefined;
1088
+ const shakenSource = await applyDynamicImportTreeShaking(
1089
+ transformedSource,
1090
+ config,
1091
+ file,
1092
+ visited,
1093
+ effectiveOptions,
1094
+ deferredDynamic,
1095
+ );
1096
+ const clientReferences = reactCompatEnabled(config) && effectiveOptions.stubClientImports && !hasUseClientDirective(shakenSource)
1097
+ ? await writeClientReferenceModules(shakenSource, config, file, effectiveOptions.aliases)
1098
+ : { specifiers: new Map<string, string>(), files: new Set<string>() };
1099
+ // Compiling this module only needs its imports' *paths*, so it runs while the
1100
+ // targets themselves compile. The write still waits for them: a compiled
1101
+ // module must never become loadable before its edges exist on disk.
1102
+ const compiled = buildDevModuleSource(
1103
+ shakenSource,
1104
+ config,
1105
+ file,
1106
+ clientReferences.specifiers,
1107
+ effectiveOptions,
1108
+ );
1109
+ // The real rejection is reported below; this only keeps it from surfacing as
1110
+ // an unhandled rejection while the imports compile.
1111
+ compiled.catch(() => undefined);
1112
+ await Promise.all([
1113
+ assetContext,
1114
+ ...localImportTargets(shakenSource, config, file, effectiveOptions.aliases)
1115
+ // Relative and absolute imports always name first-party source next to its importer (compat's
1116
+ // own out-of-workspace runtime files import each other this way), so compile it regardless of
1117
+ // workspaceRoot. A bare specifier that happens to resolve locally only compiles when it is
1118
+ // inside the workspace; outside that it is meant to stay an untouched external package.
1119
+ .filter(
1120
+ ({ target, isLocalSource }) =>
1121
+ (isLocalSource || isInside(config.workspaceRoot, target)) &&
1122
+ !clientReferences.files.has(path.resolve(target)) &&
1123
+ !deferredDynamic?.has(path.resolve(target)),
1124
+ )
1125
+ .map(({ target }) => writeDevModule(config, target, visited, effectiveOptions)),
1126
+ ]);
1127
+
1128
+ await writeCompiledFile(outFile, await compiled);
1129
+ return outFile;
1130
+ }
1131
+
1132
+ const shakeEntrySpecifier = 'pnext-shake-entry';
1133
+ const shakeEntryNamespace = 'pnext-shake-entry';
1134
+
1135
+ // Rewrite each shakeable destructured dynamic import in `source` so its
1136
+ // specifier points at a tree-shaken facade module (see dynamic/tree-shake.ts).
1137
+ // Only local workspace modules are shaken; 'use client' targets keep their
1138
+ // client-reference stubbing path. Returns the source unchanged when nothing
1139
+ // qualifies.
1140
+ async function applyDynamicImportTreeShaking(
1141
+ source: string,
1142
+ config: ResolvedConfig,
1143
+ file: string,
1144
+ visited: Set<string>,
1145
+ options: DevModuleOptions,
1146
+ deferredDynamic?: Set<string>,
1147
+ ): Promise<string> {
1148
+ if (!nextCompatEnabled(config)) return source;
1149
+ const candidates = findShakeableDynamicImports(source);
1150
+ if (candidates.length === 0) return source;
1151
+
1152
+ const edits: { start: number; end: number; value: string }[] = [];
1153
+ for (const candidate of candidates) {
1154
+ const { sourcePath } = splitHash(candidate.specifier);
1155
+ if (isServerIgnoredAssetSpecifier(sourcePath) && !isCssFile(sourcePath)) continue;
1156
+ const resolved = resolveLocalImport(config, file, sourcePath);
1157
+ if (!resolved || !isInside(config.workspaceRoot, resolved) || isCssFile(resolved)) continue;
1158
+ // A deferred dynamic() target must not compile eagerly through the facade.
1159
+ if (deferredDynamic?.has(path.resolve(resolved))) continue;
1160
+ if (options.stubClientImports && (await fileHasUseClientDirective(resolved))) continue;
1161
+ const facade = await writeShakenDynamicModule(
1162
+ config,
1163
+ resolved,
1164
+ candidate.usedExports,
1165
+ visited,
1166
+ options,
1167
+ );
1168
+ edits.push({
1169
+ start: candidate.literalStart,
1170
+ end: candidate.literalEnd,
1171
+ value: JSON.stringify(pathToFileHref(facade)),
1172
+ });
1173
+ }
1174
+
1175
+ if (edits.length === 0) return source;
1176
+ let next = source;
1177
+ for (const edit of edits.sort((a, b) => b.start - a.start)) {
1178
+ next = `${next.slice(0, edit.start)}${edit.value}${next.slice(edit.end)}`;
1179
+ }
1180
+ return next;
1181
+ }
1182
+
1183
+ function shakeExportsKey(usedExports: string[]): string {
1184
+ const sorted = [...new Set(usedExports)].sort();
1185
+ if (sorted.length === 0) return 'none';
1186
+ return createHash('sha256').update(sorted.join(',')).digest('hex').slice(0, 8);
1187
+ }
1188
+
1189
+ async function shakenModulePath(
1190
+ config: ResolvedConfig,
1191
+ targetFile: string,
1192
+ profile: string,
1193
+ key: string,
1194
+ ): Promise<string> {
1195
+ const base = await devModulePathFor(config, targetFile, profile);
1196
+ const ext = path.extname(base) || '.js';
1197
+ return `${base.slice(0, base.length - ext.length)}.pnext-shake-${key}${ext}`;
1198
+ }
1199
+
1200
+ // Facade entry that re-exports only the destructured names from the target
1201
+ // (loaded inline via the shake-entry namespace), so esbuild tree-shakes the
1202
+ // rest. An empty set keeps side effects only.
1203
+ function shakenEntrySource(usedExports: string[]): string {
1204
+ const names = [...new Set(usedExports)];
1205
+ if (names.length === 0) return `import ${JSON.stringify(shakeEntrySpecifier)};\n`;
1206
+ const named = names.filter(name => name !== 'default');
1207
+ const lines: string[] = [];
1208
+ if (named.length > 0) {
1209
+ lines.push(`export { ${named.join(', ')} } from ${JSON.stringify(shakeEntrySpecifier)};`);
1210
+ }
1211
+ if (names.includes('default')) {
1212
+ lines.push(`export { default } from ${JSON.stringify(shakeEntrySpecifier)};`);
1213
+ }
1214
+ return `${lines.join('\n')}\n`;
1215
+ }
1216
+
1217
+ async function writeShakenDynamicModule(
1218
+ config: ResolvedConfig,
1219
+ targetFile: string,
1220
+ usedExports: string[],
1221
+ visited: Set<string>,
1222
+ options: DevModuleOptions,
1223
+ ): Promise<string> {
1224
+ const profile = devModuleProfile(options);
1225
+ const outFile = await shakenModulePath(config, targetFile, profile, shakeExportsKey(usedExports));
1226
+ if (devArtifactUsable(outFile)) return outFile;
1227
+
1228
+ const key = `shake\0${outFile}`;
1229
+ const existing = moduleBuilds.get(key);
1230
+ if (existing) return existing;
1231
+
1232
+ const next = writeShakenDynamicModuleUncached(config, targetFile, usedExports, visited, options, outFile)
1233
+ .catch(error => {
1234
+ moduleBuilds.delete(key);
1235
+ throw error;
1236
+ })
1237
+ .finally(() => {
1238
+ moduleBuilds.delete(key);
1239
+ });
1240
+ moduleBuilds.set(key, next);
1241
+ return next;
1242
+ }
1243
+
1244
+ async function writeShakenDynamicModuleUncached(
1245
+ config: ResolvedConfig,
1246
+ targetFile: string,
1247
+ usedExports: string[],
1248
+ visited: Set<string>,
1249
+ options: DevModuleOptions,
1250
+ outFile: string,
1251
+ ): Promise<string> {
1252
+ await mkdir(path.dirname(outFile), { recursive: true });
1253
+ const targetSource = rewriteServerSource(await readText(targetFile), targetFile, {
1254
+ nextFonts: nextCompatEnabled(config),
1255
+ root: config.root,
1256
+ });
1257
+ // The target is inlined into the facade, but its own transitive imports stay
1258
+ // external file:// modules — compile them the same way the normal graph walk
1259
+ // would so those hrefs resolve on disk.
1260
+ await Promise.all(
1261
+ localImportTargets(targetSource, config, targetFile, options.aliases)
1262
+ .filter(({ target, isLocalSource }) => isLocalSource || isInside(config.workspaceRoot, target))
1263
+ .map(({ target }) => writeDevModule(config, target, visited, options)),
1264
+ );
1265
+ const externalBundles = await externalBundleSpecifiers(targetSource, config, targetFile, options);
1266
+ const result = await withBuildSlot(() =>
1267
+ build({
1268
+ stdin: {
1269
+ contents: shakenEntrySource(usedExports),
1270
+ loader: 'js',
1271
+ resolveDir: path.dirname(targetFile),
1272
+ sourcefile: `${targetFile}.pnext-shake`,
1273
+ },
1274
+ bundle: true,
1275
+ write: false,
1276
+ format: 'esm',
1277
+ platform: 'neutral',
1278
+ target: 'es2022',
1279
+ jsx: 'automatic',
1280
+ jsxImportSource: 'preact',
1281
+ packages: 'external',
1282
+ logLevel: 'silent',
1283
+ ...serverDefineOptions(),
1284
+ plugins: [
1285
+ shakeEntryPlugin(targetFile, targetSource),
1286
+ serverAssetPlugin(config),
1287
+ ...getBundlerExtensions().serverEsbuildPlugins(config),
1288
+ devModuleResolvePlugin(config, targetFile, new Map(), externalBundles, options),
1289
+ ],
1290
+ }),
1291
+ );
1292
+ const output = result.outputFiles[0];
1293
+ if (!output) throw new Error(`Failed to tree-shake ${targetFile}`);
1294
+ await writeCompiledFile(outFile, addExtensionlessDynamicImportGlobAliases(output.text));
1295
+ return outFile;
1296
+ }
1297
+
1298
+ // Resolve the facade's re-export entry to the target's already-transformed
1299
+ // source, loaded inline (non-external) so esbuild bundles and tree-shakes it.
1300
+ function shakeEntryPlugin(targetFile: string, targetSource: string): Plugin {
1301
+ return {
1302
+ name: 'pnext-shake-entry',
1303
+ setup(build) {
1304
+ build.onResolve({ filter: new RegExp(`^${shakeEntrySpecifier}$`) }, () => ({
1305
+ path: targetFile,
1306
+ namespace: shakeEntryNamespace,
1307
+ }));
1308
+ build.onLoad({ filter: /.*/, namespace: shakeEntryNamespace }, () => ({
1309
+ contents: targetSource,
1310
+ loader: esbuildLoader(targetFile),
1311
+ resolveDir: path.dirname(targetFile),
1312
+ }));
1313
+ },
1314
+ };
1315
+ }
1316
+
1317
+ function externalLoadTargetForConditionTarget(target: ServerBundleTarget): ExternalLoadTarget {
1318
+ return target === 'edge' ? 'edge' : 'server';
1319
+ }
1320
+
1321
+ function clientLayerOptions(
1322
+ config: ResolvedConfig,
1323
+ conditionTarget: ServerBundleTarget = 'client',
1324
+ ): DevModuleOptions {
1325
+ return {
1326
+ aliases: clientSsrAliases(config),
1327
+ profile: 'client',
1328
+ conditionTarget,
1329
+ externalLoadTarget: 'client-ssr',
1330
+ stubClientImports: false,
1331
+ rewriteExternalServerImports: false,
1332
+ bundleExternalPackages: reactCompatEnabled(config),
1333
+ };
1334
+ }
1335
+
1336
+ /**
1337
+ * The SSR condition target for a `use client` module. App-router client components keep the
1338
+ * browser-flavored `client` conditions. Pages-router PAGE sources instead SSR with Next's pages
1339
+ * server-bundle conditions - `node` (or `edge-light`/`browser` under an edge runtime), never
1340
+ * `react-server` - so packages with per-condition `exports` resolve like Next.
1341
+ */
1342
+ /**
1343
+ * SERVER-layer condition target for a pages-router source file. A pages page is not part of the RSC
1344
+ * layer - Next compiles it under `node`/`edge-light` conditions WITHOUT `react-server` - but its
1345
+ * callers only know the route's generic server target, so the demotion happens here. Without it a
1346
+ * dual-published dependency is vendored from its `react-server` entry, which may export no `default`
1347
+ * and 500 on every render of the page.
1348
+ */
1349
+ function pagesLayerConditionTarget(
1350
+ config: ResolvedConfig,
1351
+ file: string,
1352
+ incoming: ServerBundleTarget,
1353
+ ): ServerBundleTarget {
1354
+ if (incoming === 'server') return isPagesRouterSourceFile(config, file) ? 'pages' : 'server';
1355
+ if (incoming === 'edge') return isPagesRouterSourceFile(config, file) ? 'pages-edge' : 'edge';
1356
+ return incoming;
1357
+ }
1358
+
1359
+ function clientLayerConditionTarget(
1360
+ config: ResolvedConfig,
1361
+ file: string,
1362
+ incoming: ServerBundleTarget,
1363
+ ): ServerBundleTarget {
1364
+ if (!isPagesRouterSourceFile(config, file)) return 'client';
1365
+ return incoming === 'edge' || incoming === 'pages-edge' ? 'pages-edge' : 'pages';
1366
+ }
1367
+
1368
+ function isPagesRouterSourceFile(config: ResolvedConfig, file: string): boolean {
1369
+ const posix = file.split(path.sep).join('/');
1370
+ if (posix.includes('/source-pages/')) return true;
1371
+ // Materialized hybrid wrappers live under pnext-pages-compat/<hash>/app/ for
1372
+ // BOTH routers; only pages wrappers re-export from source-pages/, so sniff
1373
+ // the (tiny, generated) wrapper body to tell them apart.
1374
+ if (posix.includes('pnext-pages-compat/')) {
1375
+ try {
1376
+ return readFileSync(file, 'utf8').includes('source-pages/');
1377
+ } catch {
1378
+ return false;
1379
+ }
1380
+ }
1381
+ return isInside(path.join(config.root, 'pages'), file);
1382
+ }
1383
+
1384
+ async function buildDevModuleSource(
1385
+ source: string,
1386
+ config: ResolvedConfig,
1387
+ file: string,
1388
+ clientReferenceSpecifiers: Map<string, string>,
1389
+ options: DevModuleOptions,
1390
+ ) {
1391
+ const externalBundles = await externalBundleSpecifiers(source, config, file, options);
1392
+ const transformed = await transformDevModuleSource(
1393
+ source,
1394
+ config,
1395
+ file,
1396
+ clientReferenceSpecifiers,
1397
+ externalBundles,
1398
+ options,
1399
+ );
1400
+ if (transformed !== undefined) return transformed;
1401
+ if (moduleStats) esbuildFallbacks.add(`${devModuleProfile(options)}:${file}`);
1402
+ const result = await withBuildSlot(() => build({
1403
+ stdin: {
1404
+ contents: source,
1405
+ loader: esbuildLoader(file),
1406
+ resolveDir: path.dirname(file),
1407
+ sourcefile: file,
1408
+ },
1409
+ bundle: true,
1410
+ write: false,
1411
+ format: 'esm',
1412
+ platform: 'neutral',
1413
+ target: 'es2022',
1414
+ // Workspace .js/.mjs app modules may contain JSX (packages stay external,
1415
+ // so node_modules never reach these loaders).
1416
+ loader: { '.js': 'jsx', '.mjs': 'jsx' },
1417
+ jsx: 'automatic',
1418
+ jsxImportSource: 'preact',
1419
+ packages: 'external',
1420
+ logLevel: 'silent',
1421
+ ...serverDefineOptions(),
1422
+ plugins: [
1423
+ serverAssetPlugin(config),
1424
+ ...getBundlerExtensions().serverEsbuildPlugins(config),
1425
+ serverClientReferencePlugin(config, options),
1426
+ devModuleResolvePlugin(config, file, clientReferenceSpecifiers, externalBundles, options),
1427
+ ],
1428
+ }));
1429
+ const output = result.outputFiles[0];
1430
+ if (!output) throw new Error(`Failed to build ${file}`);
1431
+ const compiled = addExtensionlessDynamicImportGlobAliases(output.text);
1432
+ return options.profile === 'client' && isCommonJsModuleSource(source, file)
1433
+ ? unwrapCommonJsDefaultExport(compiled)
1434
+ : compiled;
1435
+ }
1436
+
1437
+ /**
1438
+ * The batched-pipeline fast path: oxc-transform plus our own specifier rewriting, for the great
1439
+ * majority of server modules that are plain ESM. Returns undefined when this module needs esbuild's
1440
+ * bundler - either its source has an esbuild-only shape, or one of its specifiers only resolves by
1441
+ * inlining something (asset loader rules, `#subpath` facades, require-aliases).
1442
+ */
1443
+ async function transformDevModuleSource(
1444
+ source: string,
1445
+ config: ResolvedConfig,
1446
+ file: string,
1447
+ clientReferenceSpecifiers: Map<string, string>,
1448
+ externalBundles: Map<string, string>,
1449
+ options: DevModuleOptions,
1450
+ ): Promise<string | undefined> {
1451
+ // Kill switch: puts every module back on the esbuild build below, for
1452
+ // bisecting a suspected transform-path difference.
1453
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
1454
+ if (process.env.PNEXT_DISABLE_BATCHED_TRANSFORM) return undefined;
1455
+ if (transformBailReason(source, file)) return undefined;
1456
+ const transformed = transformServerModule(source, file, serverDefineOptions().define);
1457
+ if ('bail' in transformed) return undefined;
1458
+
1459
+ const { code } = transformed;
1460
+ const edits: { start: number; end: number; value: string }[] = [];
1461
+ for (const found of outputSpecifiers(code)) {
1462
+ const target = await devModuleSpecifierTarget(
1463
+ found.value,
1464
+ found.kind,
1465
+ config,
1466
+ file,
1467
+ clientReferenceSpecifiers,
1468
+ externalBundles,
1469
+ options,
1470
+ );
1471
+ if (target === undefined) return undefined;
1472
+ if (target !== found.value) edits.push({ start: found.start, end: found.end, value: JSON.stringify(target) });
1473
+ }
1474
+ return spliceSource(code, edits);
1475
+ }
1476
+
1477
+ /**
1478
+ * One specifier's rewrite target, or undefined when only the bundler can serve
1479
+ * it. Assets take the same stub content the bundler inlines, written out as a
1480
+ * real module instead.
1481
+ */
1482
+ async function devModuleSpecifierTarget(
1483
+ specifier: string,
1484
+ kind: SpecifierKind,
1485
+ config: ResolvedConfig,
1486
+ file: string,
1487
+ clientReferenceSpecifiers: Map<string, string>,
1488
+ externalBundles: Map<string, string>,
1489
+ options: DevModuleOptions,
1490
+ ): Promise<string | undefined> {
1491
+ // Same claim `serverAssetPlugin` makes, and in the same order: it is the first
1492
+ // onResolve in the build below.
1493
+ if (isServerIgnoredAssetSpecifier(specifier)) {
1494
+ const resolved = resolveAssetPath(specifier, path.dirname(file));
1495
+ // A configured loader-rule chain (turbopack.rules) preempts the generic
1496
+ // asset stub — fall through to the rule-aware resolver below.
1497
+ if (!getAssetExtensions().hasLoaderRuleFor(resolved)) {
1498
+ return pathToFileHref(await assetStubModule(config, resolved, devModuleProfile(options)));
1499
+ }
1500
+ }
1501
+ const resolved = await resolveDevModuleSpecifier(
1502
+ specifier,
1503
+ config,
1504
+ file,
1505
+ clientReferenceSpecifiers,
1506
+ externalBundles,
1507
+ options,
1508
+ kind,
1509
+ file,
1510
+ );
1511
+ // A non-external result names a namespace the bundler loads inline
1512
+ // (`pnext-module-external` facades) or a bare require-alias path.
1513
+ return resolved.external && typeof resolved.path === 'string' ? resolved.path : undefined;
1514
+ }
1515
+
1516
+ const assetStubs = new Map<string, Promise<string>>();
1517
+
1518
+ /** The empty/static-asset/css-module stub the bundler used to inline, as a file. */
1519
+ async function assetStubModule(config: ResolvedConfig, resolved: string, profile: string) {
1520
+ const base = splitHash(resolved).sourcePath.replace(/\?.*$/, '');
1521
+ const outFile = `${await devModulePathFor(config, base, profile)}.pnext-asset.js`;
1522
+ const existing = assetStubs.get(outFile);
1523
+ if (existing) return existing;
1524
+ const next = (async () => {
1525
+ const contents =
1526
+ getCssExtensions().loadCssModuleForClient(resolved) ??
1527
+ (isCssFile(base)
1528
+ ? ''
1529
+ : isStaticImageFile(base)
1530
+ ? await staticImageModuleSource(config, resolved)
1531
+ : 'export default "";');
1532
+ await mkdir(path.dirname(outFile), { recursive: true });
1533
+ await writeCompiledFile(outFile, contents);
1534
+ return outFile;
1535
+ })().catch(error => {
1536
+ assetStubs.delete(outFile);
1537
+ throw error;
1538
+ });
1539
+ assetStubs.set(outFile, next);
1540
+ return next;
1541
+ }
1542
+
1543
+ function unwrapCommonJsDefaultExport(code: string) {
1544
+ const match = /(^|\n)export default ([^;\n]+);/.exec(code);
1545
+ if (!match?.[2]) return code;
1546
+ const moduleName = uniqueIdentifier(code, '__pnext_cjs_module');
1547
+ const defaultName = uniqueIdentifier(code, '__pnext_cjs_default', moduleName);
1548
+ return code.replace(
1549
+ match[0],
1550
+ `${match[1]}var ${moduleName} = ${match[2]};\nvar ${defaultName} = ${moduleName} != null && "default" in Object(${moduleName}) ? ${moduleName}.default : ${moduleName};\nexport { ${defaultName} as default };`,
1551
+ );
1552
+ }
1553
+
1554
+ function devModuleResolvePlugin(
1555
+ config: ResolvedConfig,
1556
+ file: string,
1557
+ clientReferenceSpecifiers: Map<string, string>,
1558
+ externalBundles: Map<string, string>,
1559
+ options: DevModuleOptions,
1560
+ ): Plugin {
1561
+ return {
1562
+ name: 'pnext-dev-module-resolve',
1563
+ setup(build) {
1564
+ registerExternalLoadHandlers(build, config, options, externalLoadNamespaces.module);
1565
+ build.onResolve({ filter: /.*/ }, args =>
1566
+ resolveDevModuleSpecifier(
1567
+ args.path,
1568
+ config,
1569
+ file,
1570
+ clientReferenceSpecifiers,
1571
+ externalBundles,
1572
+ options,
1573
+ args.kind,
1574
+ args.importer,
1575
+ ),
1576
+ );
1577
+ },
1578
+ };
1579
+ }
1580
+
1581
+ function serverClientReferencePlugin(config: ResolvedConfig, options: DevModuleOptions): Plugin {
1582
+ return {
1583
+ name: 'pnext-server-client-references',
1584
+ setup(build) {
1585
+ build.onLoad({ filter: /\.[cm]?[jt]sx?$/, namespace: 'file' }, async args => {
1586
+ if (!options.stubClientImports) return undefined;
1587
+ const source = await readText(args.path);
1588
+ if (!hasUseClientDirective(source)) return undefined;
1589
+ return {
1590
+ contents: clientReferenceModuleSource(
1591
+ args.path,
1592
+ await clientReferenceExportNames(config, args.path),
1593
+ ),
1594
+ loader: 'js',
1595
+ resolveDir: path.dirname(args.path),
1596
+ };
1597
+ });
1598
+ },
1599
+ };
1600
+ }
1601
+
1602
+ const serverAssetNamespace = 'pnext-empty-server-asset';
1603
+
1604
+ function serverAssetPlugin(config: ResolvedConfig): Plugin {
1605
+ return {
1606
+ name: 'pnext-empty-server-assets',
1607
+ setup(build) {
1608
+ build.onResolve({ filter: serverIgnoredAssetFilter }, args => {
1609
+ const resolved = resolveAssetPath(args.path, args.resolveDir);
1610
+ // A configured `turbopack.rules` loader chain for this extension (e.g.
1611
+ // `*.svg`) preempts the generic asset pipeline: defer so the compat
1612
+ // loader-rule plugin's onLoad runs the chain against the real file.
1613
+ // This is the plugin that builds app-page SERVER bundles, so without
1614
+ // this escape hatch a configured `*.svg` rule never runs here.
1615
+ if (getAssetExtensions().hasLoaderRuleFor(resolved)) return undefined;
1616
+ return { path: resolved, namespace: serverAssetNamespace };
1617
+ });
1618
+ build.onLoad({ filter: /.*/, namespace: serverAssetNamespace }, async args => {
1619
+ // Registry-provided CSS modules (e.g. *.module.scss) export their class
1620
+ // map on the server instead of the empty-asset stub.
1621
+ const cssModule = getCssExtensions().loadCssModuleForClient(args.path);
1622
+ return {
1623
+ contents: cssModule ?? (isCssFile(args.path)
1624
+ ? ''
1625
+ : isStaticImageFile(args.path)
1626
+ ? await staticImageModuleSource(config, args.path)
1627
+ : 'export default "";'),
1628
+ loader: 'js',
1629
+ };
1630
+ });
1631
+ },
1632
+ };
1633
+ }
1634
+
1635
+ async function resolveDevModuleSpecifier(
1636
+ specifier: string,
1637
+ config: ResolvedConfig,
1638
+ file: string,
1639
+ clientReferenceSpecifiers: Map<string, string>,
1640
+ externalBundles: Map<string, string>,
1641
+ options: DevModuleOptions,
1642
+ kind: string,
1643
+ importer: string,
1644
+ ): Promise<OnResolveResult> {
1645
+ // An already-resolved `file://` href is a compiled module on disk; pass it straight through as
1646
+ // external instead of re-resolving it. But a file:// href naming a RAW 'use client' source
1647
+ // (rewriteStaticCompatImports resolves compat aliases textually) still needs the same
1648
+ // client-reference stubbing as a bare specifier - importing the raw file yields an untagged third
1649
+ // module instance the render walk never recognizes as a client boundary.
1650
+ if (specifier.startsWith('file://')) {
1651
+ const { sourcePath: hrefPath, hash: hrefHash } = splitHash(specifier);
1652
+ const target = fileURLToPath(hrefPath);
1653
+ if (
1654
+ options.stubClientImports &&
1655
+ !isCssFile(target) &&
1656
+ !isInside(config.outPath, target) &&
1657
+ existsSync(target) &&
1658
+ (await fileHasUseClientDirective(target))
1659
+ ) {
1660
+ const stub = await clientReferencePath(config, target);
1661
+ await writeClientReferenceModule(stub, target, await clientReferenceExportNames(config, target));
1662
+ return { path: `${pathToFileHref(stub)}${hrefHash}`, external: true };
1663
+ }
1664
+ return { path: specifier, external: true };
1665
+ }
1666
+
1667
+ // A `turbopack.rules` loader chain owns this specifier (`?query` and all):
1668
+ // import the module its output was materialized to. The seam answers a bare
1669
+ // `undefined` when nothing is configured, so the common path stays sync.
1670
+ const pendingRuleModule = getAssetExtensions().loaderRuleModule(
1671
+ specifier,
1672
+ importer && !importer.startsWith('<') ? importer : file,
1673
+ );
1674
+ const ruleModule = pendingRuleModule === undefined ? undefined : await pendingRuleModule;
1675
+ if (ruleModule) return { path: pathToFileHref(ruleModule), external: true };
1676
+
1677
+ const { sourcePath, hash } = splitHash(specifier);
1678
+
1679
+ const compatAlias =
1680
+ pagesCompatSourceAlias(config, specifier, importer, options) ?? options.aliases[specifier];
1681
+ if (compatAlias) {
1682
+ if (kind === 'require-call' && path.isAbsolute(compatAlias)) {
1683
+ return { path: serverRequireAlias(specifier, compatAlias, options) };
1684
+ }
1685
+ // A compat alias pointing at a 'use client' file needs the SAME stub-and-match treatment as any
1686
+ // other local client component: the render tree walk recognizes a client boundary by a
1687
+ // `[clientReferenceSymbol]` tag on the component object it actually rendered, matched by id
1688
+ // against `markClientReferences`' own load of the file. Returning the raw file directly imports a
1689
+ // THIRD, untagged instance, so the component never gets its hydration island.
1690
+ if (
1691
+ path.isAbsolute(compatAlias) &&
1692
+ options.stubClientImports &&
1693
+ !isCssFile(compatAlias) &&
1694
+ (await fileHasUseClientDirective(compatAlias))
1695
+ ) {
1696
+ const stub = await clientReferencePath(config, compatAlias);
1697
+ await writeClientReferenceModule(stub, compatAlias, await clientReferenceExportNames(config, compatAlias));
1698
+ return { path: pathToFileHref(stub), external: true };
1699
+ }
1700
+ return {
1701
+ path: path.isAbsolute(compatAlias) ? pathToFileHref(compatAlias) : compatAlias,
1702
+ external: true,
1703
+ };
1704
+ }
1705
+
1706
+ const externalBundle = externalBundles.get(sourcePath);
1707
+ if (externalBundle && kind === 'require-call') {
1708
+ const required = resolvePackageSpecifier(
1709
+ config.root,
1710
+ file,
1711
+ sourcePath,
1712
+ serverBundleRequireConditions(options.conditionTarget),
1713
+ );
1714
+ if (required) return { path: `${required}${hash}`, external: true };
1715
+ }
1716
+ if (externalBundle) return { path: `${externalBundle}${hash}`, external: true };
1717
+
1718
+ const clientReferenceStubPath = clientReferenceSpecifiers.get(sourcePath);
1719
+ if (clientReferenceStubPath) return { path: `${pathToFileHref(clientReferenceStubPath)}${hash}`, external: true };
1720
+
1721
+ if (
1722
+ sourcePath === './preact' &&
1723
+ path.resolve(importer) === new URL('../compat/react/client.ts', import.meta.url).pathname
1724
+ ) {
1725
+ return { path: pathToFileHref(new URL('../compat/react/preact.ts', import.meta.url).pathname), external: true };
1726
+ }
1727
+
1728
+ if (
1729
+ sourcePath === '../client/errors/primitive-throw' &&
1730
+ path.resolve(importer) === new URL('../compat/react/preact.ts', import.meta.url).pathname
1731
+ ) {
1732
+ return {
1733
+ path: pathToFileHref(new URL('../compat/client/errors/primitive-throw.ts', import.meta.url).pathname),
1734
+ external: true,
1735
+ };
1736
+ }
1737
+
1738
+ const resolved = resolveImport(
1739
+ config.root,
1740
+ importer && !importer.startsWith('<') ? importer : file,
1741
+ sourcePath,
1742
+ );
1743
+ // A relative specifier that resolved names first-party source next to its importer, so compile it
1744
+ // through the normal pipeline even outside workspaceRoot (compat's own runtime files live inside
1745
+ // pnext's package, not the app's workspace). A bare specifier resolving outside workspaceRoot is a
1746
+ // package meant to stay external, untouched.
1747
+ const isLocalSourceSpecifier = sourcePath.startsWith('.') || path.isAbsolute(sourcePath);
1748
+ if (!resolved || (!isLocalSourceSpecifier && !isInside(config.workspaceRoot, resolved))) {
1749
+ const resolvedExternalLoad = resolveExternalLoadTarget({
1750
+ root: config.root,
1751
+ fromFile: file,
1752
+ specifier: sourcePath,
1753
+ target: options.externalLoadTarget,
1754
+ });
1755
+ if (resolvedExternalLoad && await externalLoadNeedsFacade(resolvedExternalLoad)) {
1756
+ return {
1757
+ path: resolvedExternalLoad,
1758
+ namespace: externalLoadNamespaces.module,
1759
+ };
1760
+ }
1761
+ const externalTarget =
1762
+ resolvedExternalLoad ?? externalPackageImportTarget(config.root, file, sourcePath);
1763
+ return {
1764
+ path: externalTarget
1765
+ ? `${pathToFileHref(externalTarget)}${hash}`
1766
+ : options.rewriteExternalServerImports
1767
+ ? runtimeServerImportTarget(specifier)
1768
+ : specifier,
1769
+ external: true,
1770
+ };
1771
+ }
1772
+ // A configured `resolveExtensions` order can make an EXTENSIONLESS specifier
1773
+ // land on an asset (`import img from './image'` -> image.png). The asset
1774
+ // claims above (and serverAssetPlugin's) key on the SPECIFIER, so they miss
1775
+ // those; re-check the resolved path or the compile pass hands a .png to
1776
+ // esbuild. A loader rule for the extension still wins, as everywhere else.
1777
+ if (
1778
+ isServerIgnoredAssetSpecifier(resolved) &&
1779
+ !isServerIgnoredAssetSpecifier(sourcePath) &&
1780
+ // Css keeps its copy-through compile, exactly as the specifier-keyed path does.
1781
+ !isCssFile(resolved) &&
1782
+ !getAssetExtensions().hasLoaderRuleFor(resolved)
1783
+ ) {
1784
+ const stub = await assetStubModule(config, resolved, devModuleProfile(options));
1785
+ return { path: pathToFileHref(stub), external: true };
1786
+ }
1787
+ if (options.stubClientImports && !isCssFile(resolved) && await fileHasUseClientDirective(resolved)) {
1788
+ const stub = await clientReferencePath(config, resolved);
1789
+ await writeClientReferenceModule(stub, resolved, await clientReferenceExportNames(config, resolved));
1790
+ return { path: `${pathToFileHref(stub)}${hash}`, external: true };
1791
+ }
1792
+ // The compiled-path rewrite below is only safe for imports the WRITE pass walks:
1793
+ // writeDevModuleUncached compiles the entry source's own local import targets, nothing deeper. A
1794
+ // first-party compat runtime file pulled in by an alias that esbuild BUNDLES contributes its own
1795
+ // relative imports through this hook, and nothing ever writes those into the profile cache - the
1796
+ // import would resolve to a file that does not exist. Point such a transitive import at its own
1797
+ // source instead; Bun compiles the `.ts` on import.
1798
+ const importerFile = importer && !importer.startsWith('<') ? path.resolve(importer) : '';
1799
+ const bundledCompatImporter =
1800
+ isLocalSourceSpecifier &&
1801
+ importerFile !== '' &&
1802
+ importerFile !== path.resolve(file) &&
1803
+ !isInside(config.workspaceRoot, importerFile) &&
1804
+ !isInside(config.workspaceRoot, resolved);
1805
+ // The `importerFile !== file` term above keeps this to imports the write pass misses: the entry's
1806
+ // OWN local imports are already walked by writeDevModuleUncached. Everything deeper - a relative
1807
+ // sibling of a compat file esbuild inlined into the entry - is nobody's job but this one, whether
1808
+ // the entry is a workspace module or a node_modules-nested 'use client' component. Gating on an
1809
+ // in-workspace entry left those nested client references importing a file that was never written.
1810
+ // Recompiling is not a risk: writeDevModule returns an already-written output as-is, so island
1811
+ // identity across a soft navigation is preserved.
1812
+ if (bundledCompatImporter) {
1813
+ // Compile it here, keeping the same compiled-path rewrite so the module stays a single instance -
1814
+ // pointing at the raw source instead would load a SECOND copy of the compat runtime graph and split its
1815
+ // module state. writeDevModule dedupes in-flight builds per artifact path, so a module reached from
1816
+ // several bundled importers is compiled once.
1817
+ await writeDevModule(config, resolved, new Set(), options);
1818
+ }
1819
+ return {
1820
+ path: `${pathToFileHref(await devModulePath(config, resolved, options))}${hash}`,
1821
+ external: true,
1822
+ };
1823
+ }
1824
+
1825
+ function serverRequireAlias(specifier: string, alias: string, options: DevModuleOptions) {
1826
+ if (options.conditionTarget === 'client') return alias;
1827
+ if (specifier === 'next/navigation' && alias.endsWith(`${path.sep}navigation.ts`)) {
1828
+ return path.join(path.dirname(alias), 'navigation.cjs');
1829
+ }
1830
+ if (specifier === 'next/router' && alias.endsWith(`${path.sep}router.ts`)) {
1831
+ return path.join(path.dirname(alias), 'router.cjs');
1832
+ }
1833
+ return alias;
1834
+ }
1835
+
1836
+ function registerExternalLoadHandlers(
1837
+ build: import('esbuild').PluginBuild,
1838
+ config: ResolvedConfig,
1839
+ options: DevModuleOptions,
1840
+ namespace: string,
1841
+ ) {
1842
+ build.onResolve({ filter: /.*/, namespace }, args => {
1843
+ const { sourcePath } = splitHash(args.path);
1844
+ const importer = args.importer.startsWith(`${namespace}:`)
1845
+ ? args.importer.slice(namespace.length + 1)
1846
+ : args.importer;
1847
+ const compatAlias = options.aliases[sourcePath];
1848
+ if (compatAlias) {
1849
+ return {
1850
+ path: path.isAbsolute(compatAlias) ? pathToFileHref(compatAlias) : compatAlias,
1851
+ external: true,
1852
+ };
1853
+ }
1854
+ const target = resolveExternalLoadTarget({
1855
+ root: config.root,
1856
+ fromFile: importer,
1857
+ specifier: sourcePath,
1858
+ target: options.externalLoadTarget,
1859
+ });
1860
+ if (target) return { path: target, namespace };
1861
+ if (sourcePath.startsWith('.') || path.isAbsolute(sourcePath)) {
1862
+ return { path: path.resolve(path.dirname(importer), sourcePath), namespace };
1863
+ }
1864
+ return { path: args.path, external: true };
1865
+ });
1866
+ build.onLoad({ filter: /.*/, namespace }, async args => ({
1867
+ contents: rewriteExternalLoadSpecifiers(await readText(args.path), args.path, config, options),
1868
+ loader: esbuildLoader(args.path),
1869
+ resolveDir: path.dirname(args.path),
1870
+ }));
1871
+ }
1872
+
1873
+ function rewriteExternalLoadSpecifiers(
1874
+ source: string,
1875
+ file: string,
1876
+ config: ResolvedConfig,
1877
+ options: DevModuleOptions,
1878
+ ) {
1879
+ // Folded onto the module record (PERF-REWRITES #28); side-effect imports keep
1880
+ // resolving through the host, as under the `from`-anchored regex.
1881
+ return rewriteSpecifierLiterals(source, file, (specifier, kind) => {
1882
+ if (kind === 'side-effect' || !specifier.startsWith('#')) return undefined;
1883
+ const target = resolveExternalLoadTarget({
1884
+ root: config.root,
1885
+ fromFile: file,
1886
+ specifier,
1887
+ target: options.externalLoadTarget,
1888
+ });
1889
+ return target ? JSON.stringify(target) : undefined;
1890
+ });
1891
+ }
1892
+
1893
+ async function externalLoadNeedsFacade(file: string) {
1894
+ const source = await readText(file).catch(() => '');
1895
+ return /\bfrom\s*['"]#|\b(?:import|require)\s*\(\s*['"]#/.test(source);
1896
+ }
1897
+
1898
+ function addExtensionlessDynamicImportGlobAliases(source: string) {
1899
+ if (!source.includes('__glob({')) return source;
1900
+ const lines = source.split('\n');
1901
+ const next: string[] = [];
1902
+ let inGlob = false;
1903
+ const keys = new Set<string>();
1904
+
1905
+ for (const line of lines) {
1906
+ if (line.includes('__glob({')) {
1907
+ inGlob = true;
1908
+ keys.clear();
1909
+ }
1910
+ if (inGlob) {
1911
+ const key = globKey(line);
1912
+ if (key) keys.add(key);
1913
+ }
1914
+ next.push(line);
1915
+ if (inGlob) {
1916
+ const aliases = globAliases(line, keys);
1917
+ if (aliases.length > 0) {
1918
+ if (!line.endsWith(',')) next[next.length - 1] = `${line},`;
1919
+ const value = line.replace(/^(\s*)"[^"]+":\s*/, '');
1920
+ const normalizedValue = value.endsWith(',') ? value.slice(0, -1) : value;
1921
+ for (const alias of aliases) {
1922
+ keys.add(alias);
1923
+ next.push(`${line.match(/^\s*/)?.[0] ?? ''}${JSON.stringify(alias)}: ${normalizedValue},`);
1924
+ }
1925
+ }
1926
+ }
1927
+ if (inGlob && line.trim() === '});') {
1928
+ const previous = next[next.length - 2];
1929
+ if (previous?.endsWith(',')) next[next.length - 2] = previous.slice(0, -1);
1930
+ inGlob = false;
1931
+ }
1932
+ }
1933
+
1934
+ return next.join('\n');
1935
+ }
1936
+
1937
+ function globKey(line: string) {
1938
+ return /^\s*"([^"]+)":\s*/.exec(line)?.[1];
1939
+ }
1940
+
1941
+ function globAliases(line: string, existing: Set<string>) {
1942
+ const key = globKey(line);
1943
+ if (!key) return [];
1944
+ const extension = path.extname(key);
1945
+ if (!extensionlessDynamicImportExtensions.has(extension)) return [];
1946
+ const aliases = [key.slice(0, -extension.length)];
1947
+ const indexSuffix = `/index${extension}`;
1948
+ if (key.endsWith(indexSuffix)) aliases.push(key.slice(0, -indexSuffix.length));
1949
+ return aliases.filter(alias => alias && !existing.has(alias));
1950
+ }
1951
+
1952
+ async function externalBundleSpecifiers(
1953
+ source: string,
1954
+ config: ResolvedConfig,
1955
+ file: string,
1956
+ options: DevModuleOptions,
1957
+ ) {
1958
+ const bundles = new Map<string, string>();
1959
+ if (!options.bundleExternalPackages) return bundles;
1960
+
1961
+ const candidates = [...new Set(importSpecifiers(source, file).map(specifier => splitHash(specifier).sourcePath))]
1962
+ .filter(specifier => !isServerIgnoredAssetSpecifier(specifier))
1963
+ .filter(specifier => shouldBundleExternalPackage(specifier, config, file, options));
1964
+ await Promise.all(
1965
+ candidates.map(async specifier => {
1966
+ bundles.set(
1967
+ specifier,
1968
+ await externalServerPackageHref(
1969
+ config,
1970
+ specifier,
1971
+ options.profile === 'client' ? 'client' : 'server',
1972
+ path.dirname(file),
1973
+ options.conditionTarget,
1974
+ ),
1975
+ );
1976
+ }),
1977
+ );
1978
+ return bundles;
1979
+ }
1980
+
1981
+ function shouldBundleExternalPackage(
1982
+ specifier: string,
1983
+ config: ResolvedConfig,
1984
+ file: string,
1985
+ options: DevModuleOptions,
1986
+ ) {
1987
+ if (options.aliases[specifier]) return false;
1988
+ if (specifier === 'server-only') return false;
1989
+ if (!isPackageSpecifier(specifier)) return false;
1990
+ const resolved = resolveImport(config.root, file, specifier);
1991
+ const inWorkspace = resolved !== undefined && isInside(config.workspaceRoot, resolved);
1992
+ // transpilePackages force-bundle; serverExternalPackages force-external (never bundled). A
1993
+ // transpiled package that resolves INSIDE the workspace is first-party source, not a registry
1994
+ // copy: it belongs to the batched module pipeline, which compiles it per file instead of
1995
+ // re-bundling its whole graph once per demanded subpath. Named imports survive that boundary
1996
+ // because CommonJS deps are now externalized or faceted rather than inlined. "Vendor" is registry
1997
+ // node_modules only.
1998
+ const packageName = packageNameOfSpecifier(specifier);
1999
+ if (packageName) {
2000
+ const policy = getExternalPackagePolicy();
2001
+ if (policy.transpile(packageName)) return !inWorkspace;
2002
+ if (policy.external(packageName)) return false;
2003
+ }
2004
+ return !inWorkspace;
2005
+ }
2006
+
2007
+ function clientSsrAliases(config: ResolvedConfig) {
2008
+ return {
2009
+ ...coreAliases(config, 'client'),
2010
+ ...getImportAliasExtensions().clientSsrAliases(config),
2011
+ };
2012
+ }
2013
+
2014
+ /** Absolute files of this module's dynamic(ssr:false) targets — the dev split points. */
2015
+ function deferredDynamicTargetFiles(source: string, config: ResolvedConfig, file: string) {
2016
+ const specifiers = deferredDynamicImportSpecifiers(source, file);
2017
+ if (specifiers.size === 0) return undefined;
2018
+ const files = new Set<string>();
2019
+ for (const specifier of specifiers) {
2020
+ const resolved = resolveLocalImport(config, file, specifier);
2021
+ if (resolved) files.add(path.resolve(resolved));
2022
+ }
2023
+ return files.size > 0 ? files : undefined;
2024
+ }
2025
+
2026
+ function localImportTargets(source: string, config: ResolvedConfig, file: string, aliases: AliasMap) {
2027
+ const targets: { target: string; isLocalSource: boolean }[] = [];
2028
+ for (const specifier of importSpecifiers(source, file)) {
2029
+ // Compat-aliased specifiers (next/server etc.) are rewritten at build
2030
+ // time; never resolve them locally — inside a workspace that ships a real
2031
+ // `next` package they would drag its whole module graph into the recursion.
2032
+ const { sourcePath } = splitHash(specifier);
2033
+ if (aliases[specifier] || aliases[sourcePath]) continue;
2034
+ // Non-css ignored assets (scss, fonts, images) are stubbed at build time;
2035
+ // compiling them as modules would fail the import scan. Css keeps its
2036
+ // copy-through so compiled css module paths stay importable.
2037
+ if (isServerIgnoredAssetSpecifier(sourcePath) && !isCssFile(sourcePath)) continue;
2038
+ const resolved = resolveImport(config.root, file, sourcePath);
2039
+ // `resolveExtensions` can land an extensionless specifier on an asset
2040
+ // (`import img from './image'` -> image.png), which the specifier check
2041
+ // above misses — compiling the image bytes as a module fails the build.
2042
+ if (resolved && isServerIgnoredAssetSpecifier(resolved) && !isCssFile(resolved)) continue;
2043
+ // A loader-rule source is not a module either: its chain output is
2044
+ // materialized by the resolvers, so compiling the raw file would only fail
2045
+ // the parse (`*.txt`, `*.test-file.ts`, …).
2046
+ if (resolved && getAssetExtensions().hasLoaderRuleFor(resolved)) continue;
2047
+ // Our own emitted artifacts (a tree-shake facade the source above was just rewritten to point at, a
2048
+ // vendor bundle) are already compiled modules Bun imports directly. Feeding one back through the
2049
+ // pipeline compiles it a SECOND time, splitting module identity and pulling artifact names into the
2050
+ // source graph, where every boot's fresh names look like edits.
2051
+ if (resolved && !isInside(config.outPath, resolved)) {
2052
+ targets.push({ target: resolved, isLocalSource: sourcePath.startsWith('.') || path.isAbsolute(sourcePath) });
2053
+ }
2054
+ }
2055
+
2056
+ return targets;
2057
+ }
2058
+
2059
+ async function writeClientReferenceModules(
2060
+ source: string,
2061
+ config: ResolvedConfig,
2062
+ file: string,
2063
+ aliases: AliasMap,
2064
+ ) {
2065
+ const references = new Map<string, { file: string; exports: Set<string> }>();
2066
+
2067
+ for (const specifier of importSpecifiers(source, file)) {
2068
+ const { sourcePath } = splitHash(specifier);
2069
+ if (aliases[specifier] || aliases[sourcePath]) continue;
2070
+ const resolved = resolveImport(config.root, file, sourcePath);
2071
+ if (!resolved || !isInside(config.workspaceRoot, resolved) || isCssFile(resolved)) continue;
2072
+ if (!(await fileHasUseClientDirective(resolved))) continue;
2073
+
2074
+ const entry = references.get(sourcePath) ?? { file: resolved, exports: new Set<string>() };
2075
+ for (const exportName of await clientReferenceExportNames(config, resolved)) {
2076
+ entry.exports.add(exportName);
2077
+ }
2078
+ references.set(sourcePath, entry);
2079
+ }
2080
+
2081
+ const specifiers = new Map<string, string>();
2082
+ const files = new Set<string>();
2083
+ await Promise.all(
2084
+ [...references.entries()].map(async ([specifier, reference]) => {
2085
+ const stub = await clientReferencePath(config, reference.file);
2086
+ await writeClientReferenceModule(stub, reference.file, [...reference.exports].sort());
2087
+ specifiers.set(specifier, stub);
2088
+ files.add(path.resolve(reference.file));
2089
+ }),
2090
+ );
2091
+
2092
+ return { specifiers, files };
2093
+ }
2094
+
2095
+ async function writeClientReferenceModule(file: string, sourceFile: string, exportNames: string[]) {
2096
+ await mkdir(path.dirname(file), { recursive: true });
2097
+ await writeCompiledFile(file, clientReferenceModuleSource(sourceFile, exportNames));
2098
+ }
2099
+
2100
+ async function fileHasUseClientDirective(file: string) {
2101
+ return hasUseClientDirective(await readText(file));
2102
+ }
2103
+
2104
+ function splitHash(specifier: string) {
2105
+ // A leading '#' is a subpath-import/tsconfig-paths specifier (e.g. '#/lib/x'),
2106
+ // not a fragment separator.
2107
+ const index = specifier.startsWith('#') ? specifier.indexOf('#', 1) : specifier.indexOf('#');
2108
+ return index === -1
2109
+ ? { sourcePath: specifier, hash: '' }
2110
+ : { sourcePath: specifier.slice(0, index), hash: specifier.slice(index) };
2111
+ }
2112
+
2113
+ function isInside(root: string, file: string) {
2114
+ const relative = path.relative(root, file);
2115
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
2116
+ }
2117
+
2118
+ function devModuleProfile(options: DevModuleOptions) {
2119
+ if (options.profile === 'client') {
2120
+ // Pages-router SSR layers ('pages'/'pages-edge') resolve package exports
2121
+ // with different conditions than the app client layer — keep their
2122
+ // compiled modules (and baked-in vendor hrefs) in separate cache dirs.
2123
+ return options.conditionTarget === 'client'
2124
+ ? options.profile
2125
+ : `client-${options.conditionTarget}`;
2126
+ }
2127
+ const externalTarget =
2128
+ options.externalLoadTarget === externalLoadTargetForConditionTarget(options.conditionTarget)
2129
+ ? ''
2130
+ : `-external-${options.externalLoadTarget}`;
2131
+ return `${options.profile}-${options.conditionTarget}${externalTarget}${options.reactServerLayer ? '-react-server' : ''}`;
2132
+ }
2133
+
2134
+ // Compiled artifacts are named `<source name>.<graph hash>.<ext>` under a per-profile directory that
2135
+ // mirrors the source tree. The hash covers the module's whole source graph, so an edit renames the edited
2136
+ // file and its dependents and nothing else - a stale artifact is simply never asked for.
2137
+ function devServerPath(config: ResolvedConfig, file: string, profile: string, hash: string) {
2138
+ const key = devServerCacheKey(config, file);
2139
+ const ext = path.extname(key) || '.js';
2140
+ // A compiled `.json` module is JS (esbuild's json loader wraps it in exports),
2141
+ // so it must not keep the `.json` name — the runtime would parse the artifact
2142
+ // as JSON. Keeping the source name in front of `.js` keeps it collision-free.
2143
+ const outExt = ext === '.json' ? '.json.js' : ext;
2144
+ return path.join(
2145
+ cacheRoot(config.outPath),
2146
+ profile === 'server' ? 'modules' : `modules-${profile}`,
2147
+ `${key.slice(0, key.length - ext.length)}.${hash}${outExt}`,
2148
+ );
2149
+ }
2150
+
2151
+ /** The staged directory mirroring `dir` for `profile` (asset links live there). */
2152
+ function devServerDir(config: ResolvedConfig, dir: string, profile: string) {
2153
+ return path.dirname(devServerPath(config, path.join(dir, '__pnext_context__.js'), profile, 'dir'));
2154
+ }
2155
+
2156
+ // A module compiled standalone (not merely as a transitive import of an
2157
+ // in-workspace file) can live OUTSIDE config.workspaceRoot: a compat runtime
2158
+ // file (e.g. `next/form` -> src/compat/next/form.tsx, shipped inside pnext's
2159
+ // own package) reached directly as a route's client-reference entry, not
2160
+ // through any in-workspace importer. `path.relative(workspaceRoot, file)` for
2161
+ // such a file is riddled with `..` segments walking up past unrelated
2162
+ // ancestors; joining that under outPath silently produces a bogus path (and
2163
+ // the file's own untouched relative imports then resolve from the wrong
2164
+ // location once written there). Key those out-of-workspace files by a stable
2165
+ // hash of their absolute path instead of a relative path.
2166
+ function devServerCacheKey(config: ResolvedConfig, file: string) {
2167
+ if (isInside(config.workspaceRoot, file)) return path.relative(config.workspaceRoot, file);
2168
+ // Hash the portable identity, not the absolute path: a deployed build runs
2169
+ // these same files from a different root and must find the same artifact.
2170
+ const hash = createHash('sha256')
2171
+ .update(devSourceIdentity(file, config.workspaceRoot))
2172
+ .digest('hex')
2173
+ .slice(0, 16);
2174
+ return path.join('external', `${hash}${path.extname(file) || '.js'}`);
2175
+ }
2176
+
2177
+ // The route bundle inlines the route file and its layouts, so its name carries
2178
+ // every source graph it bundles.
2179
+ async function devRouteBundlePath(
2180
+ config: ResolvedConfig,
2181
+ route: RouteManifestEntry,
2182
+ layoutFiles: string[],
2183
+ ) {
2184
+ const graph = devModuleGraph(config);
2185
+ const entries = uniqueFiles([route.file, ...layoutFiles]);
2186
+ const cached = await cachedRouteBundlePath(config, route.id, graph.graphKey, entries);
2187
+ if (cached) return cached;
2188
+ const hashes = await Promise.all(entries.map(file => graph.graphHash(file)));
2189
+ const hash = createHash('sha256').update(hashes.join('\0')).digest('hex').slice(0, 16);
2190
+ const bundle = path.join(cacheRoot(config.outPath), 'routes', `${route.id}.${hash}.mjs`);
2191
+ saveRouteBundlePath(
2192
+ config,
2193
+ route.id,
2194
+ graph.graphKey,
2195
+ entries,
2196
+ bundle,
2197
+ await graph.graphSources(entries),
2198
+ );
2199
+ return bundle;
2200
+ }
2201
+
2202
+ async function clientReferencePath(config: ResolvedConfig, file: string) {
2203
+ return `${await devModulePathFor(config, file, 'compat')}.client-reference.ts`;
2204
+ }
2205
+
2206
+ function isCssFile(file: string) {
2207
+ return file.endsWith('.css');
2208
+ }
2209
+
2210
+ function isStaticImageFile(file: string) {
2211
+ return /\.(?:png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/.test(file);
2212
+ }
2213
+
2214
+ const serverIgnoredAssetFilter =
2215
+ /\.(?:css|scss|sass|woff2?|ttf|otf|eot|png|jpe?g|gif|webp|avif|svg|ico|bmp)(?:$|[?#])/;
2216
+
2217
+ function isServerIgnoredAssetSpecifier(specifier: string) {
2218
+ return serverIgnoredAssetFilter.test(specifier);
2219
+ }
2220
+
2221
+ function esbuildLoader(file: string) {
2222
+ if (file.endsWith('.tsx')) return 'tsx';
2223
+ if (file.endsWith('.ts')) return 'ts';
2224
+ // `import data from './x.json'` is an ordinary module edge, so the walk
2225
+ // reaches the data file itself; parsing it as JS fails on the first key.
2226
+ if (file.endsWith('.json')) return 'json';
2227
+ // App-convention .js/.jsx/.mjs modules may contain JSX (jsxImportSource is
2228
+ // preact via the build config), so parse them with the jsx loader. .tsx/.ts
2229
+ // keep their dedicated loaders above for the fast path.
2230
+ return 'jsx';
2231
+ }
2232
+
2233
+ function isPackageSpecifier(specifier: string) {
2234
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:')) return false;
2235
+ if (specifier.startsWith('#')) return false;
2236
+ if (specifier.startsWith('node:')) return false;
2237
+ if (specifier.includes(':')) return false;
2238
+ const packageName = packageNameFromSpecifier(specifier);
2239
+ return Boolean(packageName && !builtinModules.includes(packageName));
2240
+ }
2241
+
2242
+ function isBuiltinSpecifier(specifier: string) {
2243
+ if (specifier.startsWith('node:')) return true;
2244
+ const packageName = packageNameFromSpecifier(specifier);
2245
+ return Boolean(packageName && builtinModules.includes(packageName));
2246
+ }
2247
+
2248
+ function packageNameFromSpecifier(specifier: string) {
2249
+ const parts = specifier.split('/');
2250
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
2251
+ }
2252
+
2253
+ function resolveLocalImport(config: ResolvedConfig, fromFile: string, specifier: string) {
2254
+ if (path.isAbsolute(specifier)) return existsSync(specifier) ? specifier : undefined;
2255
+ return resolveImport(config.root, fromFile, specifier);
2256
+ }
2257
+
2258
+ async function linkAssetContext(config: ResolvedConfig, file: string, profile: string) {
2259
+ if (!isInside(config.root, file)) return;
2260
+ const sourceDirs = ancestorDirs(config.root, path.dirname(file));
2261
+ await Promise.all(sourceDirs.map(sourceDir => linkDirectoryAssetContext(config, sourceDir, profile)));
2262
+ }
2263
+
2264
+ function ancestorDirs(root: string, dir: string) {
2265
+ const dirs = [root];
2266
+ const relative = path.relative(root, dir);
2267
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return dirs;
2268
+ let current = root;
2269
+ for (const segment of relative.split(path.sep).filter(Boolean)) {
2270
+ current = path.join(current, segment);
2271
+ dirs.push(current);
2272
+ }
2273
+ return dirs;
2274
+ }
2275
+
2276
+ async function linkDirectoryAssetContext(
2277
+ config: ResolvedConfig,
2278
+ sourceDir: string,
2279
+ profile: string,
2280
+ ) {
2281
+ let entries: Dirent<string>[];
2282
+ try {
2283
+ entries = await readdir(sourceDir, { withFileTypes: true, encoding: 'utf8' });
2284
+ } catch {
2285
+ return;
2286
+ }
2287
+ const targetDir = devServerDir(config, sourceDir, profile);
2288
+ await Promise.all(
2289
+ entries.map(async entry => {
2290
+ if (!assetContextEntry(config, sourceDir, entry.name, entry.isDirectory())) return;
2291
+ const source = path.join(sourceDir, entry.name);
2292
+ const target = path.join(targetDir, entry.name);
2293
+ // Root-level sibling asset directories are mirrored, not symlinked wholesale: a `dir` symlink
2294
+ // would make every path beneath it - including code modules that must be compiled through the
2295
+ // alias transform - resolve straight back to raw source, shadowing the staged compile.
2296
+ // Mirroring links only non-code leaf files.
2297
+ if (entry.isDirectory()) {
2298
+ await mirrorAssetDirectory(source, target);
2299
+ return;
2300
+ }
2301
+ if (existsSync(target)) return;
2302
+ await mkdir(path.dirname(target), { recursive: true });
2303
+ try {
2304
+ await symlink(source, target, 'file');
2305
+ } catch {
2306
+ if (!existsSync(target)) await copyFile(source, target);
2307
+ }
2308
+ }),
2309
+ );
2310
+ }
2311
+
2312
+ // Recursively stage an asset directory: link non-code leaf files and recurse
2313
+ // into subdirectories, but never link code files — those compile through the
2314
+ // module graph into their own staged output and must not be shadowed by a raw
2315
+ // symlink.
2316
+ async function mirrorAssetDirectory(sourceDir: string, targetDir: string) {
2317
+ let entries: Dirent<string>[];
2318
+ try {
2319
+ entries = await readdir(sourceDir, { withFileTypes: true, encoding: 'utf8' });
2320
+ } catch {
2321
+ return;
2322
+ }
2323
+ await Promise.all(
2324
+ entries.map(async entry => {
2325
+ if (entry.name.startsWith('.')) return;
2326
+ const source = path.join(sourceDir, entry.name);
2327
+ const target = path.join(targetDir, entry.name);
2328
+ if (entry.isDirectory()) {
2329
+ await mirrorAssetDirectory(source, target);
2330
+ return;
2331
+ }
2332
+ if (isCodeFile(entry.name)) return;
2333
+ if (existsSync(target)) return;
2334
+ await mkdir(path.dirname(target), { recursive: true });
2335
+ try {
2336
+ await symlink(source, target, 'file');
2337
+ } catch {
2338
+ if (!existsSync(target)) await copyFile(source, target);
2339
+ }
2340
+ }),
2341
+ );
2342
+ }
2343
+
2344
+ function assetContextEntry(
2345
+ config: ResolvedConfig,
2346
+ sourceDir: string,
2347
+ name: string,
2348
+ directory: boolean,
2349
+ ) {
2350
+ if (name.startsWith('.')) return false;
2351
+ if (!directory) return !isCodeFile(name);
2352
+ if (sourceDir !== config.root) return false;
2353
+ return !new Set([
2354
+ 'app',
2355
+ 'src',
2356
+ 'pages',
2357
+ 'public',
2358
+ 'node_modules',
2359
+ path.basename(config.outPath),
2360
+ ]).has(name);
2361
+ }
2362
+
2363
+ function isCodeFile(file: string) {
2364
+ if (/\.(?:tsx?|jsx?|mjs|cjs)$/.test(file)) return true;
2365
+ // Extra loadable source extensions registered by compat (e.g. mdx/md) compile
2366
+ // through the module graph, so they must not be symlinked raw as asset
2367
+ // context — otherwise the raw markdown shadows the compiled staged module.
2368
+ const dot = file.lastIndexOf('.');
2369
+ if (dot < 0) return false;
2370
+ return extraLoadableExtensions().includes(file.slice(dot + 1));
2371
+ }
2372
+
2373
+ function resolveAssetPath(specifier: string, resolveDir: string) {
2374
+ const { sourcePath, hash } = splitHash(specifier);
2375
+ return `${path.isAbsolute(sourcePath) ? sourcePath : path.resolve(resolveDir, sourcePath)}${hash}`;
2376
+ }
2377
+
2378
+ async function staticImageModuleSource(config: ResolvedConfig, file: string) {
2379
+ const { sourcePath } = splitHash(file);
2380
+ const bytes = new Uint8Array(await Bun.file(sourcePath).arrayBuffer());
2381
+ const emitted: string[] = [];
2382
+ const emit = (relative: string) => {
2383
+ emitted.push(relative);
2384
+ return `/${relative}`;
2385
+ };
2386
+ const compat = await getAssetExtensions().staticAssetModule({ sourcePath, bytes, emit });
2387
+ const source = compat ?? coreStaticAssetModule(sourcePath, bytes, emit);
2388
+ for (const relative of emitted) {
2389
+ const target = path.join(config.outPath, 'public', ...relative.split('/'));
2390
+ await mkdir(path.dirname(target), { recursive: true });
2391
+ if (!existsSync(target)) await copyFile(sourcePath, target);
2392
+ }
2393
+ return source;
2394
+ }
2395
+
2396
+ // Core's generic static-asset module (no compat override): emit under a hashed
2397
+ // media URL and export the URL string as default.
2398
+ function coreStaticAssetModule(
2399
+ sourcePath: string,
2400
+ bytes: Uint8Array,
2401
+ emit: (relative: string, bytes: Uint8Array) => string,
2402
+ ): string {
2403
+ const ext = path.extname(sourcePath).toLowerCase() || '.bin';
2404
+ const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 8);
2405
+ const base = path.basename(sourcePath, path.extname(sourcePath)).replace(/[^A-Za-z0-9_-]+/g, '-');
2406
+ const relative = getAssetExtensions().staticAssetRelativePath({ sourcePath, hash, base, ext });
2407
+ const src = emit(relative, bytes);
2408
+ return `const src = ${JSON.stringify(src)};\nexport default src;\nexport { src };\n`;
2409
+ }
2410
+
2411
+ function uniqueFiles(files: string[]) {
2412
+ return [...new Set(files.map(file => path.resolve(file)).filter(file => existsSync(file)))];
2413
+ }
2414
+
2415
+ async function profileDevImport<T>(label: string, task: () => Promise<T>) {
2416
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
2417
+ if (!process.env.PNEXT_DEV_PROFILE) return task();
2418
+ const start = performance.now();
2419
+ try {
2420
+ return await task();
2421
+ } finally {
2422
+ console.log(`dev-import ${label} in ${formatDuration(performance.now() - start)}`);
2423
+ }
2424
+ }
2425
+
2426
+ function formatDuration(durationMs: number) {
2427
+ if (durationMs < 10) return `${durationMs.toFixed(1)}ms`;
2428
+ if (durationMs < 1000) return `${Math.round(durationMs)}ms`;
2429
+ return `${(durationMs / 1000).toFixed(durationMs < 10000 ? 2 : 1)}s`;
2430
+ }