@wular/pnext 0.1.6 → 0.1.8

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 (337) hide show
  1. package/bin/pnext +9 -2
  2. package/dist/server/_chunks/aliases-pycbtjt1.js +46 -0
  3. package/dist/server/_chunks/analyze-4d9e00g9.js +449 -0
  4. package/dist/server/_chunks/boot-bhemypxz.js +38 -0
  5. package/dist/server/_chunks/build-c991dv65.js +86 -0
  6. package/dist/server/_chunks/build-kh5e3x48.js +3341 -0
  7. package/dist/server/_chunks/build-tier-0a96kmx3.js +2456 -0
  8. package/dist/server/_chunks/compile-040r79gj.js +22 -0
  9. package/dist/server/_chunks/config-0tvz03a6.js +24 -0
  10. package/dist/server/_chunks/config-27rj7830.js +36 -0
  11. package/dist/server/_chunks/create-9rvwshsp.js +135 -0
  12. package/dist/server/_chunks/dev-ex9exynt.js +220 -0
  13. package/dist/server/_chunks/esbuild-yyktfrtt.js +15 -0
  14. package/dist/server/_chunks/generate-myw9hef7.js +22 -0
  15. package/dist/server/_chunks/index-0r5eqfc7.js +279 -0
  16. package/dist/server/_chunks/index-0yjv5v47.js +105 -0
  17. package/dist/server/_chunks/index-11xpbfej.js +17 -0
  18. package/dist/server/_chunks/index-141vcwk6.js +109 -0
  19. package/dist/server/_chunks/index-1pzm1hdy.js +4130 -0
  20. package/dist/server/_chunks/index-29pq0kax.js +590 -0
  21. package/dist/server/_chunks/index-2vz0wzdn.js +59 -0
  22. package/dist/server/_chunks/index-2wfv4kvx.js +182 -0
  23. package/dist/server/_chunks/index-38xpdxwd.js +194 -0
  24. package/dist/server/_chunks/index-3e075kny.js +128 -0
  25. package/dist/server/_chunks/index-3ycf5ckv.js +472 -0
  26. package/dist/server/_chunks/index-4399bvd2.js +18 -0
  27. package/dist/server/_chunks/index-4da76t62.js +297 -0
  28. package/dist/server/_chunks/index-4t92hdxd.js +172 -0
  29. package/dist/server/_chunks/index-4zzebszd.js +10 -0
  30. package/dist/server/_chunks/index-52k54ykj.js +95 -0
  31. package/dist/server/_chunks/index-5hr4062s.js +70 -0
  32. package/dist/server/_chunks/index-68m3mwbd.js +84 -0
  33. package/dist/server/_chunks/index-6b009mt0.js +56 -0
  34. package/dist/server/_chunks/index-6gex3pfa.js +15 -0
  35. package/dist/server/_chunks/index-6kp9r0d9.js +11 -0
  36. package/dist/server/_chunks/index-6q7zvrap.js +134 -0
  37. package/dist/server/_chunks/index-71885zaq.js +284 -0
  38. package/dist/server/_chunks/index-73z4esab.js +38 -0
  39. package/dist/server/_chunks/index-79ngvam8.js +60 -0
  40. package/dist/server/_chunks/index-7wc9n8be.js +25 -0
  41. package/dist/server/_chunks/index-8ct7rqy3.js +198 -0
  42. package/dist/server/_chunks/index-8kcxdfya.js +206 -0
  43. package/dist/server/_chunks/index-9ch50vtq.js +18 -0
  44. package/dist/server/_chunks/index-9f49w76j.js +213 -0
  45. package/dist/server/_chunks/index-9hz3s0m8.js +851 -0
  46. package/dist/server/_chunks/index-akkrvjn6.js +535 -0
  47. package/dist/server/_chunks/index-b6wtkdp4.js +24 -0
  48. package/dist/server/_chunks/index-b7j671dj.js +207 -0
  49. package/dist/server/_chunks/index-bcws5eh2.js +35 -0
  50. package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
  51. package/dist/server/_chunks/index-cjgqs13g.js +65 -0
  52. package/dist/server/_chunks/index-ckkdpht5.js +41 -0
  53. package/dist/server/_chunks/index-cn86z041.js +296 -0
  54. package/dist/server/_chunks/index-cvr4z22m.js +92 -0
  55. package/dist/server/_chunks/index-cyfednwb.js +87 -0
  56. package/dist/server/_chunks/index-d158y67j.js +152 -0
  57. package/dist/server/_chunks/index-d1jsmhhk.js +955 -0
  58. package/dist/server/_chunks/index-dg0f05zs.js +199 -0
  59. package/dist/server/_chunks/index-dgr390q7.js +9438 -0
  60. package/dist/server/_chunks/index-drwv61cc.js +185 -0
  61. package/dist/server/_chunks/index-e3a6yx84.js +18 -0
  62. package/dist/server/_chunks/index-e88tyv35.js +45 -0
  63. package/dist/server/_chunks/index-en7n9f7m.js +22 -0
  64. package/dist/server/_chunks/index-entm8bmz.js +28 -0
  65. package/dist/server/_chunks/index-evhd413j.js +57 -0
  66. package/dist/server/_chunks/index-ezgj0tr1.js +88 -0
  67. package/dist/server/_chunks/index-fjv96re1.js +5295 -0
  68. package/dist/server/_chunks/index-fn1chkkd.js +214 -0
  69. package/dist/server/_chunks/index-fp8r0sqs.js +64 -0
  70. package/dist/server/_chunks/index-gcmj16xy.js +92 -0
  71. package/dist/server/_chunks/index-gkndxkxf.js +736 -0
  72. package/dist/server/_chunks/index-grta8ap3.js +238 -0
  73. package/dist/server/_chunks/index-h2a70tg4.js +7 -0
  74. package/dist/server/_chunks/index-h4xy648y.js +51 -0
  75. package/dist/server/_chunks/index-hdbwtdv2.js +352 -0
  76. package/dist/server/_chunks/index-he6n2bvp.js +1270 -0
  77. package/dist/server/_chunks/index-j0m1jxqa.js +1253 -0
  78. package/dist/server/_chunks/index-j2zrcp1k.js +66 -0
  79. package/dist/server/_chunks/index-j681c48a.js +2530 -0
  80. package/dist/server/_chunks/index-jbj7ehef.js +50 -0
  81. package/dist/server/_chunks/index-jcsjq3d1.js +20 -0
  82. package/dist/server/_chunks/index-jks0x0a1.js +14 -0
  83. package/dist/server/_chunks/index-jn9rbhrx.js +1599 -0
  84. package/dist/server/_chunks/index-jt8e1pje.js +12 -0
  85. package/dist/server/_chunks/index-kdhxpwyq.js +483 -0
  86. package/dist/server/_chunks/index-knp0s912.js +7 -0
  87. package/dist/server/_chunks/index-kqyy1d4f.js +50 -0
  88. package/dist/server/_chunks/index-kresp2n8.js +445 -0
  89. package/dist/server/_chunks/index-m63czgq5.js +590 -0
  90. package/dist/server/_chunks/index-ms9v9tnx.js +31 -0
  91. package/dist/server/_chunks/index-n4hxpng2.js +11 -0
  92. package/dist/server/_chunks/index-n629dy38.js +1071 -0
  93. package/dist/server/_chunks/index-nf3df4jy.js +266 -0
  94. package/dist/server/_chunks/index-nqh3xhdc.js +57 -0
  95. package/dist/server/_chunks/index-pym1cezq.js +39 -0
  96. package/dist/server/_chunks/index-q3r9z8mq.js +213 -0
  97. package/dist/server/_chunks/index-q738kkhj.js +100 -0
  98. package/dist/server/_chunks/index-qcrvt6er.js +112 -0
  99. package/dist/server/_chunks/index-qjj6gddn.js +54 -0
  100. package/dist/server/_chunks/index-qpwsgt3a.js +158 -0
  101. package/dist/server/_chunks/index-r97ptz1q.js +97 -0
  102. package/dist/server/_chunks/index-ras0sdab.js +26 -0
  103. package/dist/server/_chunks/index-rcs322t1.js +19 -0
  104. package/dist/server/_chunks/index-rgj5mx84.js +101 -0
  105. package/dist/server/_chunks/index-rjegyfx4.js +1435 -0
  106. package/dist/server/_chunks/index-rt9qq1va.js +4 -0
  107. package/dist/server/_chunks/index-rthrsed5.js +356 -0
  108. package/dist/server/_chunks/index-sq3kvz3g.js +200 -0
  109. package/dist/server/_chunks/index-stbk3jhn.js +31 -0
  110. package/dist/server/_chunks/index-tf4j19ge.js +130 -0
  111. package/dist/server/_chunks/index-tgcakdxc.js +125 -0
  112. package/dist/server/_chunks/index-tgsfjmep.js +74 -0
  113. package/dist/server/_chunks/index-tq1f96v7.js +51 -0
  114. package/dist/server/_chunks/index-tq3b39a5.js +107 -0
  115. package/dist/server/_chunks/index-typcbcsv.js +819 -0
  116. package/dist/server/_chunks/index-v1ags1na.js +132 -0
  117. package/dist/server/_chunks/index-vmr35jjw.js +170 -0
  118. package/dist/server/_chunks/index-vvq7rqtn.js +329 -0
  119. package/dist/server/_chunks/index-vz0km5qk.js +33 -0
  120. package/dist/server/_chunks/index-w6h8m6vp.js +138 -0
  121. package/dist/server/_chunks/index-web361b8.js +138 -0
  122. package/dist/server/_chunks/index-wm36rj4v.js +311 -0
  123. package/dist/server/_chunks/index-x0y005nh.js +283 -0
  124. package/dist/server/_chunks/index-x2m17v9c.js +6166 -0
  125. package/dist/server/_chunks/index-x3yn4ywm.js +22 -0
  126. package/dist/server/_chunks/index-x9c55pzy.js +871 -0
  127. package/dist/server/_chunks/index-xh1nbk46.js +1299 -0
  128. package/dist/server/_chunks/index-xvj80c3a.js +30 -0
  129. package/dist/server/_chunks/index-xywjg2vc.js +100 -0
  130. package/dist/server/_chunks/index-y4d02znn.js +66 -0
  131. package/dist/server/_chunks/index-y7mzvq2q.js +110 -0
  132. package/dist/server/_chunks/index-ye9hmsvf.js +47 -0
  133. package/dist/server/_chunks/index-yhxkxghe.js +56 -0
  134. package/dist/server/_chunks/index-yrjkqtfj.js +86 -0
  135. package/dist/server/_chunks/index-z88qv16t.js +11 -0
  136. package/dist/server/_chunks/index-zfrwq472.js +154 -0
  137. package/dist/server/_chunks/index-zqpqyg8p.js +666 -0
  138. package/dist/server/_chunks/loader-5en7wpt5.js +80 -0
  139. package/dist/server/_chunks/modules-t92favpj.js +76 -0
  140. package/dist/server/_chunks/pipeline-4t9q43ht.js +836 -0
  141. package/dist/server/_chunks/production-385bw8df.js +55 -0
  142. package/dist/server/_chunks/proxy-config-60gprphe.js +23 -0
  143. package/dist/server/_chunks/proxy-gfhmktbk.js +35 -0
  144. package/dist/server/_chunks/router-xnrva921.js +501 -0
  145. package/dist/server/_chunks/routes-ht868pdc.js +66 -0
  146. package/dist/server/_chunks/run-d40d63ag.js +697 -0
  147. package/dist/server/_chunks/server-2p8fgqq2.js +1839 -0
  148. package/dist/server/_chunks/tsconfig-defaults-bcn34eqb.js +21 -0
  149. package/dist/server/_chunks/typegen-dvtcjtaw.js +37 -0
  150. package/dist/server/api/client-cache.js +14 -0
  151. package/dist/server/cli/index.js +375 -0
  152. package/dist/server/cli/start.js +158 -0
  153. package/dist/server/client/reference.js +16 -0
  154. package/dist/server/compat/next/cache.js +56 -0
  155. package/dist/server/compat/next/canonical-url.js +13 -0
  156. package/dist/server/compat/next/client-cache.js +57 -0
  157. package/dist/server/compat/next/client-navigation.js +232 -0
  158. package/dist/server/compat/next/client-only.js +1 -0
  159. package/dist/server/compat/next/client-script.js +178 -0
  160. package/dist/server/compat/next/client-server.js +34 -0
  161. package/dist/server/compat/next/config-headers.js +34 -0
  162. package/dist/server/compat/next/config-loader.js +25 -0
  163. package/dist/server/compat/next/config.js +10 -0
  164. package/dist/server/compat/next/constants.js +18 -0
  165. package/dist/server/compat/next/custom-server.js +10 -0
  166. package/dist/server/compat/next/dist/client/components/app-router-headers.js +48 -0
  167. package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +27 -0
  168. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +25 -0
  169. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +25 -0
  170. package/dist/server/compat/next/dynamic.js +21 -0
  171. package/dist/server/compat/next/error.js +112 -0
  172. package/dist/server/compat/next/font/cache.js +16 -0
  173. package/dist/server/compat/next/font/google.js +3 -0
  174. package/dist/server/compat/next/font/index.js +1 -0
  175. package/dist/server/compat/next/font/local.js +24 -0
  176. package/dist/server/compat/next/font/runtime-client.js +33 -0
  177. package/dist/server/compat/next/font/runtime.js +43 -0
  178. package/dist/server/compat/next/font/shared.js +22 -0
  179. package/dist/server/compat/next/form.js +129 -0
  180. package/dist/server/compat/next/head.js +10 -0
  181. package/dist/server/compat/next/headers.js +30 -0
  182. package/dist/server/compat/next/image/client.js +11 -0
  183. package/dist/server/compat/next/image/config.js +18 -0
  184. package/dist/server/compat/next/image/optimizer.js +25 -0
  185. package/dist/server/compat/next/image/patterns.js +12 -0
  186. package/dist/server/compat/next/image/props.js +16 -0
  187. package/dist/server/compat/next/image/shared.js +86 -0
  188. package/dist/server/compat/next/image/static-metadata.js +32 -0
  189. package/dist/server/compat/next/image/validate.js +8 -0
  190. package/dist/server/compat/next/image.js +59 -0
  191. package/dist/server/compat/next/index.js +1 -0
  192. package/dist/server/compat/next/legacy-image.js +78 -0
  193. package/dist/server/compat/next/legacy-request-apis.js +24 -0
  194. package/dist/server/compat/next/link-transform.js +15 -0
  195. package/dist/server/compat/next/link.js +14 -0
  196. package/dist/server/compat/next/navigation.js +191 -0
  197. package/dist/server/compat/next/offline.js +45 -0
  198. package/dist/server/compat/next/og.js +8 -0
  199. package/dist/server/compat/next/preferred-region.js +8 -0
  200. package/dist/server/compat/next/redirects.js +32 -0
  201. package/dist/server/compat/next/resource-hints.js +30 -0
  202. package/dist/server/compat/next/rewrites.js +43 -0
  203. package/dist/server/compat/next/root-params.js +121 -0
  204. package/dist/server/compat/next/route-state.js +23 -0
  205. package/dist/server/compat/next/router.js +103 -0
  206. package/dist/server/compat/next/script.js +37 -0
  207. package/dist/server/compat/next/server-only.js +1 -0
  208. package/dist/server/compat/next/server.js +131 -0
  209. package/dist/server/compat/next/svgr.js +14 -0
  210. package/dist/server/compat/next/telemetry.js +10 -0
  211. package/dist/server/compat/next/usage.js +13 -0
  212. package/dist/server/compat/next/user-agent.js +12 -0
  213. package/dist/server/compat/next/web-vitals.js +35 -0
  214. package/dist/server/compat/react/action-state.js +11 -0
  215. package/dist/server/compat/react/client-lite.js +83 -0
  216. package/dist/server/compat/react/client.js +131 -0
  217. package/dist/server/compat/react/compiler-runtime.js +42 -0
  218. package/dist/server/compat/react/dom-client.js +106 -0
  219. package/dist/server/compat/react/dom-react-server.js +37 -0
  220. package/dist/server/compat/react/dom-server.js +40 -0
  221. package/dist/server/compat/react/dom.js +52 -0
  222. package/dist/server/compat/react/hooks-extra.js +12 -0
  223. package/dist/server/compat/react/internals.js +14 -0
  224. package/dist/server/compat/react/parity.js +11 -0
  225. package/dist/server/compat/react/preact.js +113 -0
  226. package/dist/server/compat/react/react-server.js +133 -0
  227. package/dist/server/compat/react/router-shim.js +8 -0
  228. package/dist/server/compat/react/server-component-use.js +14 -0
  229. package/dist/server/compat/react/server-inserted-html.js +14 -0
  230. package/dist/server/compat/react/server.js +194 -0
  231. package/dist/server/compat/react/style-values.js +10 -0
  232. package/dist/server/compat/react/use.js +10 -0
  233. package/dist/server/compat/react/view-transition.js +12 -0
  234. package/dist/server/generation.json +1 -0
  235. package/package.json +4 -1
  236. package/reference/compat.md +1 -1
  237. package/reference/config.md +2 -2
  238. package/reference/dev.md +1 -1
  239. package/reference/getting-started.md +1 -1
  240. package/reference/rendering.md +4 -4
  241. package/src/api/dynamic.tsx +86 -4
  242. package/src/api/server.ts +24 -6
  243. package/src/cli/adapters/vercel-warm.ts +327 -62
  244. package/src/cli/adapters/vercel.ts +164 -1054
  245. package/src/cli/analyze.ts +3 -2
  246. package/src/cli/build/cache.ts +6 -6
  247. package/src/cli/build.ts +78 -45
  248. package/src/cli/index.ts +21 -3
  249. package/src/cli/serve/immutable.ts +31 -0
  250. package/src/cli/serve/pipeline.ts +97 -93
  251. package/src/cli/serve/standalone-worker.ts +8 -0
  252. package/src/cli/serve/standalone.ts +703 -0
  253. package/src/cli/start.ts +31 -8
  254. package/src/client/build.ts +1 -1
  255. package/src/client/chunk-fold.ts +19 -6
  256. package/src/client/react-tier.ts +3 -2
  257. package/src/client/reference-stub.ts +23 -36
  258. package/src/compat/actions/client-plugin.ts +4 -2
  259. package/src/compat/actions/config.ts +6 -5
  260. package/src/compat/actions/extensions.ts +5 -5
  261. package/src/compat/aliases.ts +8 -1
  262. package/src/compat/bundler/config.ts +88 -0
  263. package/src/compat/bundler/optimize-package-imports.ts +86 -26
  264. package/src/compat/bundler/relay-transform.ts +15 -3
  265. package/src/compat/bundler/require-context.ts +2 -1
  266. package/src/compat/bundler/webpack-loaders.ts +3 -2
  267. package/src/compat/bundler/worker.ts +2 -2
  268. package/src/compat/cache-control.ts +16 -8
  269. package/src/compat/css/inline-css.ts +1 -1
  270. package/src/compat/css/modules.ts +18 -5
  271. package/src/compat/export/build-complete.ts +1 -1
  272. package/src/compat/export/emit.ts +4 -4
  273. package/src/compat/export/standalone.ts +30 -65
  274. package/src/compat/image-optimizer/optimize.ts +1 -1
  275. package/src/compat/lifecycle/error-funnel.ts +1 -1
  276. package/src/compat/lifecycle/instrumentation-client.ts +4 -3
  277. package/src/compat/lifecycle/instrumentation.ts +117 -24
  278. package/src/compat/lifecycle/testmode.ts +3 -2
  279. package/src/compat/metadata-artifacts.ts +1 -1
  280. package/src/compat/next/config-loader.ts +140 -13
  281. package/src/compat/next/dynamic.tsx +10 -23
  282. package/src/compat/next/font/cache.ts +2 -1
  283. package/src/compat/next/font/runtime.ts +23 -3
  284. package/src/compat/next/og.ts +3 -3
  285. package/src/compat/next/usage.ts +3 -2
  286. package/src/compat/pages/api.ts +83 -14
  287. package/src/compat/pages/router.ts +30 -14
  288. package/src/compat/register/bundler.ts +3 -1
  289. package/src/compat/register/image.ts +1 -1
  290. package/src/compat/register/middleware.ts +1 -1
  291. package/src/compat/register/otel.ts +5 -9
  292. package/src/compat/register/render.ts +3 -1
  293. package/src/compat/register/use-cache.ts +3 -1
  294. package/src/compat/release.ts +46 -0
  295. package/src/compat/segment/serve.ts +1 -1
  296. package/src/compat/typecheck/check.ts +6 -4
  297. package/src/compat/validation/validate.ts +10 -7
  298. package/src/config.ts +76 -9
  299. package/src/css/assets.ts +213 -0
  300. package/src/css/build.ts +190 -247
  301. package/src/dev/client-chunk-store.ts +2 -1
  302. package/src/dev/restart/client-key.ts +2 -1
  303. package/src/dev/restart/global-css.ts +2 -1
  304. package/src/dev/restart/route-bundle-key.ts +2 -1
  305. package/src/dev/restart/route-facts.ts +2 -1
  306. package/src/dev/server.ts +34 -14
  307. package/src/extensions.ts +4 -0
  308. package/src/out-paths.ts +10 -0
  309. package/src/render/hooks.ts +13 -0
  310. package/src/render/renderer.ts +53 -33
  311. package/src/request/context.ts +9 -0
  312. package/src/resolve/dynamic.ts +7 -1
  313. package/src/resolve/engine.ts +2 -12
  314. package/src/resolve/imports.ts +12 -52
  315. package/src/resolve/package-json.ts +56 -0
  316. package/src/resolve/scan-facts.ts +250 -1
  317. package/src/routing/href.ts +10 -3
  318. package/src/routing/match.ts +287 -0
  319. package/src/routing/metadata-files.ts +20 -6
  320. package/src/routing/proxy-config.ts +137 -0
  321. package/src/routing/proxy.ts +35 -134
  322. package/src/routing/routes.ts +22 -281
  323. package/src/runtime/fingerprint.ts +23 -8
  324. package/src/runtime/load.ts +170 -0
  325. package/src/runtime/loader.ts +218 -44
  326. package/src/runtime/module-cache.ts +15 -1
  327. package/src/runtime/modules.ts +405 -82
  328. package/src/runtime/prebundle.ts +101 -0
  329. package/src/runtime/production.ts +456 -0
  330. package/src/runtime/release-graph.ts +342 -0
  331. package/src/runtime/vendor-build.ts +750 -17
  332. package/src/runtime/vendor.ts +16 -3
  333. package/src/types.ts +4 -3
  334. package/src/utils/directive.ts +40 -0
  335. package/src/utils/fs.ts +95 -4
  336. package/src/utils/resource-query.ts +12 -0
  337. package/src/cli/serve/entry.ts +0 -45
@@ -1,28 +1,21 @@
1
- import { copyFile, link, mkdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'
2
- import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs'
3
- import { builtinModules } from 'node:module'
1
+ import { copyFile, cp, mkdir, readFile, readdir, rename, rm } from 'node:fs/promises'
2
+ import { constants, existsSync, readdirSync } from 'node:fs'
4
3
  import path from 'node:path'
5
- import { fileURLToPath } from 'node:url'
6
- import { DEFAULT_OUT_DIR, pathToFileHref } from '../../config'
4
+ import { pathToFileHref } from '../../config'
7
5
  import type { ResolvedConfig } from '../../config'
8
- import { globalCssSources } from '../../css/build'
9
- import { getImportAliasExtensions } from '../../extensions'
10
6
  import { nextCompatEnabled } from '../../render/hooks'
11
- import { importSpecifiers } from '../../resolve/scan-facts'
12
- import { compiledSpecifiersManifestSuffix } from '../../runtime/modules'
13
- import { resolveImport, workspacePackageRoots } from '../../resolve/imports'
14
7
  import { findProxyFile, proxyRoutePatterns, type ProxyModule } from '../../routing/proxy'
15
8
  import { cacheRoot } from '../boot/named-bin'
16
- import { startWarmChild, type WarmChild } from './vercel-warm'
9
+ import {
10
+ immutableAssetPath,
11
+ immutableAssetPrefixes,
12
+ immutableCacheControl,
13
+ } from '../serve/immutable'
17
14
  import { escapeRegex } from '../../utils/code'
18
15
  import { listFiles, toPosixPath, writeText } from '../../utils/fs'
19
16
  import { createVerboseLogger, type VerboseLogger } from '../../utils/verbose'
20
17
  import type { BuildManifest, StaticFileMetadata } from '../../types'
21
18
 
22
- // Vercel unpacks a function's `.func` directory here at runtime. Baked
23
- // absolute paths in the build output are rewritten to this root.
24
- const RUNTIME_ROOT = '/var/task'
25
-
26
19
  // The single catch-all server function: the same `pnext start` pipeline,
27
20
  // running on Vercel's Bun runtime (`"bunVersion": "1.x"` in vercel.json).
28
21
  const SERVER_FUNCTION = '_pnext'
@@ -38,114 +31,9 @@ const FUNCTION_PLATFORM = {
38
31
  libc: 'glibc',
39
32
  } as const
40
33
 
41
- // Directories that only ever hold build-tool output, pruned at *any* depth. Depth matters: pruning
42
- // only a package's top level ships every nested one. node_modules and .git are here for the same
43
- // reason - the function resolves through its own traced node_modules, never a nested store.
44
- const prunedDirNames = [
45
- 'node_modules',
46
- '.git',
47
- '.next',
48
- '.pnext',
49
- '.turbo',
50
- '.cache',
51
- '.vercel',
52
- '.tmp',
53
- ]
54
-
55
- // Files nothing reads at request time. Sourcemaps only decorate stack traces,
56
- // and Bun strips types rather than resolving declarations — neither can change
57
- // what the function serves, and together they are a fifth of a typical closure.
58
- // Markdown is deliberately absent: an mdx app imports it as a module.
59
- const droppedFileSuffixes = [
60
- '.map',
61
- '.d.ts',
62
- '.d.mts',
63
- '.d.cts',
64
- '.DS_Store',
65
- // The compile-time specifier sidecars (see traceNodeModulesClosure below):
66
- // build-time metadata the trace consumes, never read at request time.
67
- compiledSpecifiersManifestSuffix,
68
- ]
69
-
70
- /** What the function tree ships, resolved once from config. */
71
- interface PackRules {
72
- prunedDirs: Set<string>
73
- droppedSuffixes: string[]
74
- /** Absolute sources exempt from the prune list — the app's own build output. */
75
- keepPaths: Set<string>
76
- }
77
-
78
- // Test seam: the packing suite builds one app both ways and diffs the trees,
79
- // so it needs the unpruned closure on demand. Never off for a real deployment.
80
- let packPruningEnabled = true
81
-
82
- /** @internal Test-only. Returns a restore function. */
83
- export function setPackPruningEnabled(enabled: boolean) {
84
- const previous = packPruningEnabled
85
- packPruningEnabled = enabled
86
- return () => {
87
- packPruningEnabled = previous
88
- }
89
- }
90
-
91
- function packRules(config: ResolvedConfig): PackRules {
92
- if (!packPruningEnabled) {
93
- return {
94
- // The two the function could never resolve through stay pruned even
95
- // here: a nested store or object database is not a packing choice.
96
- prunedDirs: new Set(['node_modules', '.git']),
97
- droppedSuffixes: [],
98
- keepPaths: new Set([path.resolve(config.outPath)]),
99
- }
100
- }
101
- const keep = new Set(config.adapter?.keep ?? [])
102
- const extra = config.adapter?.exclude ?? []
103
- const isSuffix = (entry: string) => entry.startsWith('.') && !prunedDirNames.includes(entry)
104
- return {
105
- prunedDirs: new Set(
106
- [...prunedDirNames, ...extra.filter(entry => !isSuffix(entry))].filter(
107
- name => !keep.has(name),
108
- ),
109
- ),
110
- droppedSuffixes: [...droppedFileSuffixes, ...extra.filter(isSuffix)].filter(
111
- suffix => !keep.has(suffix),
112
- ),
113
- // The app's outDir shares its name with the prune list; it holds the
114
- // compiled module cache the function serves from and must survive. The
115
- // default out root goes with it: a `distDir` app moved its output away, but
116
- // the next.config bundle the function imports at boot still lives there.
117
- keepPaths: new Set([path.resolve(config.outPath), path.resolve(config.root, DEFAULT_OUT_DIR)]),
118
- }
119
- }
120
-
121
- /**
122
- * A keep path and everything under it. Prefix, not equality: the compiled module cache mirrors the
123
- * source layout, so a dependency's artifacts land in `<outDir>/cache/server/<profile>/node_modules/
124
- * ...`. That segment names a source tree, not a package store, and pruning it by name ships the
125
- * importer without the module it imports — the function then 500s on the first request that reaches
126
- * it. Only the app's own build output is exempt; a real `node_modules` beside it still prunes.
127
- */
128
- function withinKeepPath(pack: PackRules, source: string) {
129
- const resolved = path.resolve(source)
130
- for (const keep of pack.keepPaths) {
131
- if (resolved === keep || resolved.startsWith(`${keep}${path.sep}`)) return true
132
- }
133
- return false
134
- }
135
-
136
- /**
137
- * `isLink` is load-bearing, not a detail: the exemption covers real directories the build output
138
- * owns, never a link out of it. The proxy shim links `node_modules` into `.pnext`, and following
139
- * that would pull the whole store into the function.
140
- */
141
- function shipsDir(pack: PackRules, name: string, source: string, isLink = false) {
142
- if (!pack.prunedDirs.has(name)) return true
143
- return !isLink && withinKeepPath(pack, source)
144
- }
145
-
146
- function shipsFile(pack: PackRules, name: string) {
147
- return !pack.droppedSuffixes.some(suffix => name.endsWith(suffix))
148
- }
34
+ // Image optimization's native stack: Vercel optimizes images itself, as Next's builder drops these
35
+ // (next/dist/build/collect-build-traces.js).
36
+ const functionDroppedPackages = [/^sharp$/, /^@img\/sharp-libvips/]
149
37
 
150
38
  interface VercelConfig {
151
39
  version: 3
@@ -156,55 +44,36 @@ interface VercelConfig {
156
44
  methods?: string[]
157
45
  headers?: Record<string, string>
158
46
  continue?: boolean
47
+ important?: boolean
159
48
  }
160
49
  | { handle: 'filesystem' }
161
50
  )[]
162
51
  overrides?: Record<string, { path?: string; contentType?: string }>
163
52
  }
164
53
 
54
+ /** `.vercel/output`: the standalone dir as the server function, its `static/` as the CDN tree. */
165
55
  export async function writeVercelOutput(
166
56
  config: ResolvedConfig,
167
57
  manifest: BuildManifest,
168
- options: { verbose?: boolean; warm?: WarmChild } = {},
58
+ options: { verbose?: boolean } = {},
169
59
  ) {
170
60
  const log = createVerboseLogger(options.verbose ?? false, 'vercel')
171
61
  const outputPath = path.join(config.root, '.vercel', 'output')
62
+ const staticPath = path.join(config.outPath, 'static')
172
63
  const staticFiles = manifest.staticFiles ?? {}
173
- // Warming runs in its own process (see ./vercel-warm) and normally started
174
- // with the build; a caller that skipped that only loses the overlap.
175
- const warm = options.warm ?? startWarmChild(config)
176
64
  await log.step('prepare output directory', () => emptyDir(outputPath))
177
65
 
178
- // The compiled set is already final once the child reaches its handler
179
- // phase, so `onCompiled` resolves this ahead of full completion and
180
- // writeServerFunction starts tracing while the child's handler imports
181
- // still run. `finish` itself always fires `onCompiled` before it resolves,
182
- // so this promise never hangs on it.
183
- let resolveWarmedModules: (modules: string[]) => void
184
- const warmedModulesEarly = new Promise<string[]>(resolve => {
185
- resolveWarmedModules = resolve
186
- })
187
- const warmSettled = warm.finish(log, modules => resolveWarmedModules(modules))
188
- // Nothing awaits it until writeServerFunction's copy step; park the
189
- // rejection so it never surfaces as unhandled in between (it never actually
190
- // rejects — warmWithRestarts catches everything itself — but the gap
191
- // between here and that await is otherwise unguarded).
192
- warmSettled.catch(() => undefined)
193
- const warmedModules = await log.step('warm module cache (compiled)', () => warmedModulesEarly)
194
-
195
66
  const functionPath = path.join(outputPath, 'functions', `${SERVER_FUNCTION}.func`)
196
- await writeServerFunction(config, manifest, functionPath, warmedModules, warmSettled, log)
67
+ await writeServerFunction(config.outPath, manifest, functionPath, log)
197
68
 
198
- // After the warm pass, never before: it is what emits next/font bytes under public/, and a fully
199
- // dynamic app renders nothing at build, so copying earlier ships a CDN with no fonts.
200
69
  await log.step('copy static files', () =>
201
70
  copyStaticFiles(
202
- path.join(config.outPath, 'public'),
71
+ staticPath,
203
72
  path.join(outputPath, 'static'),
204
73
  relative => !staticFiles[relative] || canServeStaticOnVercel(staticFiles[relative]),
205
74
  ),
206
75
  )
207
- const overrides = await staticOverrides(path.join(config.outPath, 'public'), staticFiles)
76
+ const overrides = await staticOverrides(staticPath, staticFiles)
208
77
 
209
78
  const routes: NonNullable<VercelConfig['routes']> = [
210
79
  // next-compat documents reference the build output under Next's static path (assetPathname),
@@ -213,7 +82,7 @@ export async function writeVercelOutput(
213
82
  // server function: served, but at function cost with no edge cache.
214
83
  ...compatStaticRewrite(config, outputPath),
215
84
  // Every content-hashed build asset, exactly the set `pnext start` serves immutable.
216
- await immutableAssetRoute(),
85
+ ...(await immutableAssetRoutes(staticPath, manifest.publicAssets)),
217
86
  // Proxy-matched paths go to the server function before the CDN filesystem
218
87
  // check — `pnext start` runs the proxy ahead of static files too.
219
88
  ...(await proxyRoutes(config)),
@@ -281,141 +150,38 @@ async function proxyRoutes(config: ResolvedConfig) {
281
150
  const proxyFile = findProxyFile(config)
282
151
  if (!proxyFile) return []
283
152
  const proxyModule = (await import(pathToFileHref(proxyFile))) as ProxyModule
284
- return proxyRoutePatterns(proxyModule.config).map(src => ({
153
+ // Percent-encoded paths and repeated slashes go to the function, which decodes or 308s them.
154
+ return [...proxyRoutePatterns(proxyModule.config), '^.*(?:%|//).*$'].map(src => ({
285
155
  src,
286
156
  dest: `/${SERVER_FUNCTION}`,
287
157
  }))
288
158
  }
289
159
 
290
160
  async function writeServerFunction(
291
- config: ResolvedConfig,
161
+ standalonePath: string,
292
162
  manifest: BuildManifest,
293
163
  functionPath: string,
294
- warmedModules: string[],
295
- // Resolves once the warm child fully exits (handlers included). The trace below only needs the
296
- // compiled set `warmedModules` already carries - it reasons from static specifiers, never from
297
- // handler execution - so it starts immediately; only the tree copy further down, which must ship
298
- // whatever handler imports wrote to the build cache, waits on this.
299
- warmSettled: Promise<unknown>,
300
164
  log: VerboseLogger,
301
165
  ) {
302
- const workspaceRoot = config.workspaceRoot
303
- await mkdir(functionPath, { recursive: true })
304
-
305
- const pack = packRules(config)
306
- const framework = await resolveFrameworkPackage(workspaceRoot)
307
- const { root: pnextRoot, inWorkspace: pnextInWorkspace, targetRel: pnextTargetRel } = framework
308
- const closure = await traceNodeModulesClosure(
309
- config,
310
- manifest,
311
- functionPath,
312
- framework,
313
- warmedModules,
314
- pack,
315
- log,
166
+ await mkdir(path.dirname(functionPath), { recursive: true })
167
+ // A copy-on-write clone where the filesystem has one; links stay links.
168
+ await log.step('copy standalone', () =>
169
+ cp(standalonePath, functionPath, { recursive: true, verbatimSymlinks: true }),
316
170
  )
317
-
318
- const replicaPathFor = (file: string) => {
319
- if (isInsideDir(pnextRoot, file)) {
320
- return path.join(functionPath, pnextTargetRel, path.relative(pnextRoot, file))
321
- }
322
- if (isInsideDir(workspaceRoot, file)) {
323
- return path.join(functionPath, path.relative(workspaceRoot, file))
324
- }
325
- return undefined
326
- }
327
-
328
- // Replicate the workspace layout the build ran in: app source (the renderer reads convention files
329
- // and global css imports from it at request time), the build output, and the workspace packages the
330
- // server can actually reach. Unrelated workspace trees never ship.
331
- const appRel = path.relative(workspaceRoot, config.root)
332
- const packageRoots = workspacePackageRoots(workspaceRoot).map(root => path.resolve(root))
333
- const neededRoots = new Set<string>()
334
- for (const file of closure.tracedFiles) {
335
- const root = packageRoots.find(candidate => isInsideDir(candidate, file))
336
- if (root) neededRoots.add(root)
337
- }
338
- for (const relative of closure.workspacePackages) {
339
- neededRoots.add(path.resolve(workspaceRoot, relative))
340
- }
341
- // The renderer re-resolves the root layout's global css imports from source
342
- // at request time; the packages holding those css files must be present.
343
- for (const file of globalCssSources(config)) {
344
- const root = packageRoots.find(candidate => isInsideDir(candidate, file))
345
- if (root) neededRoots.add(root)
346
- }
347
- // Tracing sees what the *server* loaded, which is not everything the shipped source can still ask
348
- // for: the app's own sources travel with the function and a compat compile at request time resolves
349
- // their imports for real. A workspace package one of them names but no traced module reached would
350
- // not be here, so close over the declared workspace dependencies too.
351
- addWorkspaceDependencies(neededRoots, packageRoots, [path.resolve(config.root), ...neededRoots])
352
- neededRoots.delete(path.resolve(config.root))
353
- neededRoots.delete(pnextRoot)
354
-
355
- // The trace above never needed the handler phase, but the copy below ships
356
- // config.outPath verbatim — handler imports can still be writing vendor
357
- // bundles into it. Most of this wait is already gone by the time the trace
358
- // and the roots above finish; whatever remains is genuine handler-phase work.
359
- await log.step('await handler warm-up', () => warmSettled)
360
-
361
- // Every tree lands in its own subdirectory and the copies are io-bound, so
362
- // the whole replica goes out as one concurrent step instead of tree by tree.
363
- const roots = neededRoots.size + 2
364
- await log.step(`copy function tree (${closure.packageCount} packages, ${roots} roots)`, () =>
365
- Promise.all([
366
- closure.copy(),
367
- // `public` is served by the CDN, never from the function; the rest are
368
- // named again because the app root is the one tree whose build output
369
- // ships, so it cannot rely on the depth-wise prune alone.
370
- copyTree(config.root, path.join(functionPath, appRel), pack, [
371
- 'public',
372
- '.vercel',
373
- '.next',
374
- '.turbo',
375
- ]),
376
- copyPackageTree(pnextRoot, path.join(functionPath, pnextTargetRel), pack),
377
- ...[...neededRoots].map(packageRoot =>
378
- copyPackageTree(
379
- packageRoot,
380
- path.join(functionPath, path.relative(workspaceRoot, packageRoot)),
381
- pack,
382
- ),
383
- ),
384
- ...['package.json', 'tsconfig.json', 'bunfig.toml']
385
- .map(name => path.join(workspaceRoot, name))
386
- .filter(file => existsSync(file))
387
- .map(file => copyFile(file, path.join(functionPath, path.basename(file)))),
388
- ]),
389
- )
390
-
391
- // Baked absolute paths (compiled cache imports, manifest file paths) point
392
- // at the build machine's workspace; rewrite them to the runtime root.
393
- await log.step('rewrite baked paths', () =>
394
- rewriteBakedPaths(path.join(functionPath, appRel, path.relative(config.root, config.outPath)), [
395
- ...(pnextInWorkspace
396
- ? []
397
- : [[pnextRoot, toPosixPath(path.join(RUNTIME_ROOT, pnextTargetRel))] as const]),
398
- [workspaceRoot, RUNTIME_ROOT] as const,
399
- ]),
400
- )
401
-
402
- // Bun's runtime onResolve plugins never see bare specifiers, so the compat aliases the dev pipeline
403
- // applies during compilation cannot be applied at runtime for raw-loaded sources (proxy, handlers).
404
- // Bun does honor tsconfig paths for bare imports, so map the aliased specifiers to their shims in
405
- // every shipped tsconfig.
406
- await log.step('inject compat tsconfig paths', () =>
407
- injectCompatPaths(config, functionPath, pnextRoot, replicaPathFor),
171
+ const modules = path.join(functionPath, 'node_modules')
172
+ const packages = await packageDirs(modules)
173
+ await Promise.all(
174
+ [...packages]
175
+ .filter(([name]) => functionDroppedPackages.some(pattern => pattern.test(name)))
176
+ .map(([, dirs]) => Promise.all(dirs.map(dir => rm(dir, { recursive: true, force: true })))),
408
177
  )
178
+ await log.step('platform natives', () => replaceHostNatives(packages, log))
409
179
 
410
- const startEntry = toPosixPath(path.join(pnextTargetRel, 'src/cli/start.ts'))
411
180
  await writeText(
412
181
  path.join(functionPath, 'index.mjs'),
413
- `import path from 'node:path';
414
- import { createRequestHandler } from ${JSON.stringify(`./${startEntry}`)};
182
+ `import { createRequestHandler } from './server/entry.js';
415
183
 
416
- const handlerPromise = createRequestHandler({
417
- root: path.join(import.meta.dirname, ${JSON.stringify(appRel)}),
418
- });
184
+ const handlerPromise = createRequestHandler({ root: import.meta.dirname });
419
185
 
420
186
  async function handler(request) {
421
187
  return (await handlerPromise)(request);
@@ -455,726 +221,108 @@ export default Object.assign(handler, { fetch: handler });
455
221
  )
456
222
  }
457
223
 
458
- /**
459
- * Grow `roots` with every workspace package transitively depended on by one of `from`. A workspace
460
- * dependency is one whose name maps to a package root in this workspace - the workspace: protocol is
461
- * the usual spelling, but a pinned version resolved through the workspace counts the same.
462
- */
463
- function addWorkspaceDependencies(roots: Set<string>, packageRoots: string[], from: string[]) {
464
- const byName = new Map<string, string>()
465
- for (const root of packageRoots) {
466
- const name = packageManifest(root)?.name
467
- if (name && !byName.has(name)) byName.set(name, root)
468
- }
469
- const queue = [...from]
470
- const seen = new Set(queue)
471
- while (queue.length > 0) {
472
- const manifest = packageManifest(queue.pop()!)
473
- if (!manifest) continue
474
- for (const field of ['dependencies', 'optionalDependencies'] as const) {
475
- for (const name of Object.keys(manifest[field] ?? {})) {
476
- const root = byName.get(name)
477
- if (!root || seen.has(root)) continue
478
- seen.add(root)
479
- roots.add(root)
480
- queue.push(root)
224
+ /** Package name -> the shipped directories holding it (a store layout can carry several). */
225
+ async function packageDirs(modules: string) {
226
+ const found = new Map<string, string[]>()
227
+ const visit = async (dir: string, depth: number) => {
228
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => [])
229
+ for (const entry of entries) {
230
+ if (!entry.isDirectory()) continue
231
+ const full = path.join(dir, entry.name)
232
+ if (entry.name === '.bun' || entry.name === '.pnpm') {
233
+ for (const store of await readdir(full, { withFileTypes: true }).catch(() => []))
234
+ if (store.isDirectory()) await visit(path.join(full, store.name, 'node_modules'), 0)
235
+ } else if (entry.name.startsWith('@') && depth === 0) {
236
+ await visit(full, 1)
237
+ } else if (existsSync(path.join(full, 'package.json'))) {
238
+ const name = depth === 1 ? `${path.basename(dir)}/${entry.name}` : entry.name
239
+ found.set(name, [...(found.get(name) ?? []), full])
240
+ await visit(path.join(full, 'node_modules'), 0)
481
241
  }
482
242
  }
483
243
  }
244
+ await visit(modules, 0)
245
+ return found
484
246
  }
485
247
 
486
- interface PackageManifest {
248
+ interface PackageJson {
487
249
  name?: string
488
- dependencies?: Record<string, string>
250
+ version?: string
251
+ os?: string[]
252
+ cpu?: string[]
253
+ libc?: string[]
489
254
  optionalDependencies?: Record<string, string>
490
255
  }
491
256
 
492
- const manifests = new Map<string, PackageManifest | undefined>()
493
-
494
- function packageManifest(root: string): PackageManifest | undefined {
495
- const cached = manifests.get(root)
496
- if (cached !== undefined || manifests.has(root)) return cached
497
- let parsed: PackageManifest | undefined
498
- try {
499
- parsed = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) as PackageManifest
500
- } catch {
501
- parsed = undefined // not a package, or unreadable
502
- }
503
- manifests.set(root, parsed)
504
- return parsed
505
- }
506
-
507
- // Clones a tree with hardlinks instead of byte copies - the output is packaging-only, so sharing
508
- // inodes is safe as long as nothing writes in place (rewriteBakedPaths unlinks before writing).
509
- // Exclusions apply to direct children only; node_modules and .git are pruned at any depth. Symlinks
510
- // are dereferenced so the replica is self-contained; the visited set breaks symlink cycles.
511
- async function copyTree(
512
- from: string,
513
- to: string,
514
- pack: PackRules,
515
- excludeNames: string[] = [],
516
- visited = new Set<string>([realDir(from) ?? path.resolve(from)]),
517
- ) {
518
- const exclude = new Set(excludeNames)
519
- const clone = directoryCloneEnabled ? await directoryCloner() : null
520
- // Nothing to skip at this level: the whole tree clones in one syscall.
521
- if (exclude.size === 0 && !existsSync(to)) {
522
- await mkdir(path.dirname(to), { recursive: true })
523
- if (clone?.(from, to)) return normalizeClone(to, from, pack, visited)
524
- }
525
- // The build output is still live while the walk runs — typegen rewrites
526
- // `.pnext/types` under it — so a directory can vanish between the parent's
527
- // readdir and this one. It was never part of the shipped closure; skip it.
528
- const entries = readDirEntries(from)
529
- if (!entries) return
530
- await mkdir(to, { recursive: true })
531
- await Promise.all(
532
- entries.map(async entry => {
533
- const source = path.join(from, entry.name)
534
- const target = path.join(to, entry.name)
535
- // Matched by name before type: a symlinked `node_modules` must be pruned
536
- // as the directory it points at, not followed.
537
- if (exclude.has(entry.name) || !shipsDir(pack, entry.name, source, entry.isSymbolicLink()))
538
- return
539
- if (entry.isSymbolicLink()) {
540
- const link = resolveLinkTarget(source)
541
- if (!link) return
542
- if (link.isDirectory) {
543
- if (visited.has(link.path)) return
544
- visited.add(link.path)
545
- return copyTree(link.path, target, pack, [], visited)
546
- }
547
- return shipsFile(pack, entry.name) ? linkOrCopyFile(link.path, target) : undefined
548
- }
549
- // One syscall for the whole subtree beats one per file — the excluded
550
- // names are stripped from the copy afterwards.
551
- if (entry.isDirectory() && clone?.(source, target)) {
552
- return normalizeClone(target, source, pack, visited)
553
- }
554
- if (entry.isDirectory()) return copyTree(source, target, pack, [], visited)
555
- if (entry.isFile() && shipsFile(pack, entry.name)) return linkOrCopyFile(source, target)
556
- }),
557
- )
558
- }
559
-
560
- /** Directory entries, or undefined if the directory vanished under the walk. */
561
- function readDirEntries(dir: string) {
562
- try {
563
- return readdirSync(dir, { withFileTypes: true })
564
- } catch (error) {
565
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
566
- throw error
567
- }
568
- }
569
-
570
- /**
571
- * Directory-level copy-on-write clone, when the platform has one: macOS `clonefile` copies a whole
572
- * hierarchy in a single call and, like a hardlink, shares the underlying blocks. Resolved once and
573
- * never retried - no cloner just means the hardlink walk below does the work.
574
- */
575
- // Test seam: the equivalence suite builds the same app both ways and diffs the trees.
576
- let directoryCloneEnabled = true
577
-
578
- /** @internal Test-only. Returns a restore function. */
579
- export function setDirectoryCloneEnabled(enabled: boolean) {
580
- const previous = directoryCloneEnabled
581
- directoryCloneEnabled = enabled
582
- return () => {
583
- directoryCloneEnabled = previous
584
- }
585
- }
586
-
587
- let cloner: ((from: string, to: string) => boolean) | null | undefined
588
- async function directoryCloner() {
589
- if (cloner !== undefined) return cloner
590
- cloner = null
591
- if (process.platform === 'darwin') {
592
- try {
593
- const { dlopen } = await import('bun:ffi')
594
- const { symbols } = dlopen('libSystem.B.dylib', {
595
- clonefile: { args: ['cstring', 'cstring', 'i32'], returns: 'i32' },
596
- })
597
- cloner = (from, to) =>
598
- symbols.clonefile(Buffer.from(`${from}\0`), Buffer.from(`${to}\0`), 0) === 0
599
- } catch {
600
- // no clonefile here; the per-file walk stays correct, just slower
601
- }
602
- }
603
- return cloner
604
- }
605
-
606
- /**
607
- * A clone is verbatim, so it still holds what the walk would have skipped:
608
- * drop the pruned entries and dereference inner symlinks, which would otherwise
609
- * point outside the function at runtime. Mirrors the walk's rules exactly, so
610
- * both paths emit the same tree. Reading directory entries costs nothing next
611
- * to the per-file copies this replaces.
612
- */
613
- async function normalizeClone(
614
- dir: string,
615
- // Where `dir` was cloned from. Carried so the keep-path exemption reads the same here as in the
616
- // walk: the clone's own paths are inside the function and would never match a source keep path.
617
- from: string,
618
- pack: PackRules,
619
- visited: Set<string>,
620
- ) {
621
- const stack: [string, string][] = [[dir, from]]
622
- const pending: Promise<unknown>[] = []
623
- while (stack.length > 0) {
624
- const [current, currentFrom] = stack.pop()!
625
- for (const entry of readdirSync(current, { withFileTypes: true })) {
626
- const full = path.join(current, entry.name)
627
- const source = path.join(currentFrom, entry.name)
628
- // Matched by name before type, as the walk does: the pages-compat shim
629
- // links `node_modules` at the app root, and following it would pull the
630
- // whole store into the function.
631
- if (!shipsDir(pack, entry.name, source, entry.isSymbolicLink())) {
632
- rmSync(full, { recursive: true, force: true })
633
- } else if (entry.isSymbolicLink()) {
634
- const link = resolveLinkTarget(full)
635
- rmSync(full, { force: true })
636
- if (!link) continue
637
- if (link.isDirectory) {
638
- if (visited.has(link.path)) continue
639
- visited.add(link.path)
640
- pending.push(copyTree(link.path, full, pack, [], visited))
641
- } else if (shipsFile(pack, entry.name)) {
642
- pending.push(linkOrCopyFile(link.path, full))
643
- }
644
- } else if (entry.isDirectory()) {
645
- stack.push([full, source])
646
- } else if (!shipsFile(pack, entry.name)) {
647
- rmSync(full, { force: true })
648
- }
649
- }
650
- }
651
- await Promise.all(pending)
652
- }
653
-
654
- interface FrameworkPackage {
655
- root: string
656
- packageName: string
657
- /** Declared runtime dependencies — the only packages it can load eagerly. */
658
- dependencies: string[]
659
- /** Lazily loaded feature deps (og, mdx, sass, image optimization). */
660
- optionalDependencies: string[]
661
- inWorkspace: boolean
662
- /** Where the framework ships inside the function, relative to its root. */
663
- targetRel: string
664
- }
665
-
666
- // The framework itself ships with the function. Inside a workspace it keeps its
667
- // workspace-relative location so baked paths line up; when consumed from
668
- // outside the workspace (e.g. installed from a registry) it lands in the
669
- // function's node_modules under its package name.
670
- async function resolveFrameworkPackage(workspaceRoot: string): Promise<FrameworkPackage> {
671
- const root = path.resolve(import.meta.dirname, '../../..')
672
- const packageJson = await readPackageJson(root)
673
- const packageName = packageJson?.name ?? 'pnext'
674
- const inWorkspace = isInsideDir(workspaceRoot, root) && !root.includes('node_modules')
675
- return {
676
- root,
677
- packageName,
678
- // Never devDependencies: their tooling (typescript, eslint, …) is
679
- // reachable from build-time sources only and must not enter the function.
680
- dependencies: Object.keys(packageJson?.dependencies ?? {}),
681
- optionalDependencies: Object.keys(packageJson?.optionalDependencies ?? {}),
682
- inWorkspace,
683
- targetRel: inWorkspace
684
- ? path.relative(workspaceRoot, root)
685
- : path.join('node_modules', packageName),
686
- }
687
- }
688
-
689
- // Build-time directories a package that publishes its whole tree still keeps.
690
- const packageCopyExcludes = [
691
- 'node_modules',
692
- '.cache',
693
- '.vercel',
694
- '.next',
695
- '.pnext',
696
- '.turbo',
697
- 'test',
698
- 'tests',
699
- '__tests__',
700
- ]
701
-
702
- /**
703
- * Copies a workspace package the way npm would publish it: a `files` list is the package's own
704
- * statement of what it ships, so it beats guessing at build-time directory names. Glob entries fall
705
- * back to the exclude list - matching npm's glob semantics is not worth it for a packaging copy.
706
- */
707
- async function copyPackageTree(from: string, to: string, pack: PackRules) {
708
- const packageJson = await readPackageJson(from)
709
- const files = packageJson?.files
710
- if (!files?.length || files.some(entry => /[*?[\]{}!]/.test(entry))) {
711
- return copyTree(from, to, pack, packageCopyExcludes)
712
- }
713
- await mkdir(to, { recursive: true })
714
- // npm always publishes these two regardless of `files`; the runtime resolves
715
- // through both (tsconfig for Bun's `paths`/jsx settings). Listed entries need
716
- // no exclude list — `files` already said what ships — which also lets each
717
- // directory clone whole.
718
- await Promise.all(
719
- [...files, 'package.json', 'tsconfig.json'].map(async name => {
720
- const source = path.join(from, name)
721
- const stats = statSync(source, { throwIfNoEntry: false })
722
- if (!stats) return
723
- if (stats.isDirectory()) return copyTree(source, path.join(to, name), pack)
724
- if (!shipsFile(pack, name)) return
725
- return linkOrCopyFile(source, path.join(to, name))
726
- }),
727
- )
728
- }
729
-
730
257
  async function readPackageJson(dir: string) {
731
- const file = path.join(dir, 'package.json')
732
- if (!existsSync(file)) return undefined
733
- return JSON.parse(await readFile(file, 'utf8')) as {
734
- name?: string
735
- version?: string
736
- files?: string[]
737
- os?: string[]
738
- cpu?: string[]
739
- libc?: string[]
740
- dependencies?: Record<string, string>
741
- optionalDependencies?: Record<string, string>
742
- }
743
- }
744
-
745
- // The closure hardlinks ~13k files: ~2.9s of a ~3.1s build. Test seam so suites
746
- // asserting only config shape can skip it. Never off for a real deployment.
747
- let dependencyClosureEnabled = true
748
-
749
- /** @internal Test-only. Returns a restore function. */
750
- export function setDependencyClosureEnabled(enabled: boolean) {
751
- const previous = dependencyClosureEnabled
752
- dependencyClosureEnabled = enabled
753
- return () => {
754
- dependencyClosureEnabled = previous
755
- }
756
- }
757
-
758
- function skipDependencyClosure() {
759
- return !dependencyClosureEnabled
760
- }
761
-
762
- async function linkOrCopyFile(from: string, to: string) {
763
258
  try {
764
- await link(from, to)
765
- } catch {
766
- // Same race as the walk above: a source that vanished mid-copy was never
767
- // part of the closure, and nothing else here is worth failing the build for.
768
- await copyFile(from, to).catch(error => {
769
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
770
- })
771
- }
772
- }
773
-
774
- // Where a symlink really points, and whether that is a directory. Dangling
775
- // links are ordinary in a real node_modules (removed package, unmet optional
776
- // dep, interrupted install), so an unreadable target is skipped rather than
777
- // failing the build.
778
- function resolveLinkTarget(link: string) {
779
- const resolved = realDir(link) ?? realFile(link)
780
- if (!resolved) return undefined
781
- try {
782
- return { path: resolved, isDirectory: statSync(resolved).isDirectory() }
259
+ return JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8')) as PackageJson
783
260
  } catch {
784
261
  return undefined
785
262
  }
786
263
  }
787
264
 
788
- function realFile(file: string) {
789
- try {
790
- return realpathSync(file)
791
- } catch {
792
- return undefined
793
- }
794
- }
795
-
796
- function isInsideDir(root: string, file: string) {
797
- const relative = path.relative(root, file)
798
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
799
- }
800
-
801
- /**
802
- * Whether a compat alias resolves to framework source (react, next/*) rather than to a real package.
803
- * Containment, not a `node_modules` substring: an installed framework lives under node_modules
804
- * itself, and a substring test there classifies every next/* shim as a package to ship.
805
- * @internal Exported for tests.
806
- */
807
- export function isFrameworkOwnedAliasTarget(frameworkRoot: string, target: string) {
808
- if (!path.isAbsolute(target) || !isInsideDir(frameworkRoot, target)) return false
809
- // A nested store under the framework root is still a real package.
810
- return !path.relative(frameworkRoot, target).split(path.sep).includes('node_modules')
811
- }
812
-
813
- function frameworkOwnedAliases(config: ResolvedConfig, frameworkRoot: string) {
814
- return Object.entries(getImportAliasExtensions().aliases(config, 'server')).filter(([, target]) =>
815
- isFrameworkOwnedAliasTarget(frameworkRoot, target),
816
- )
817
- }
818
-
819
- async function injectCompatPaths(
820
- config: ResolvedConfig,
821
- functionPath: string,
822
- frameworkRoot: string,
823
- replicaPathFor: (file: string) => string | undefined,
824
- ) {
825
- const aliasTargets = frameworkOwnedAliases(config, frameworkRoot)
826
- .map(([specifier, target]) => [specifier, replicaPathFor(target)] as const)
827
- .filter((entry): entry is readonly [string, string] => Boolean(entry[1]))
828
- if (aliasTargets.length === 0) return
829
-
830
- for (const file of walkTsconfigFiles(functionPath)) {
831
- let parsed: { compilerOptions?: { baseUrl?: string; paths?: Record<string, string[]> } }
832
- try {
833
- const source = stripJsonComments(await readFile(file, 'utf8')).replace(/,\s*([}\]])/g, '$1')
834
- parsed = JSON.parse(source) as typeof parsed
835
- } catch (error) {
836
- console.warn(`vercel adapter: could not update ${file}:`, error)
837
- continue
838
- }
839
- const dir = path.dirname(file)
840
- const compilerOptions = (parsed.compilerOptions ??= {})
841
- const paths = (compilerOptions.paths ??= {})
842
- for (const [specifier, target] of aliasTargets) {
843
- if (paths[specifier]) continue
844
- const relative = toPosixPath(path.relative(dir, target))
845
- paths[specifier] = [relative.startsWith('.') ? relative : `./${relative}`]
846
- }
847
- compilerOptions.baseUrl ??= '.'
848
- // The replica shares inodes with the original tsconfig; never write
849
- // through the hardlink.
850
- await rm(file)
851
- await writeFile(file, `${JSON.stringify(parsed, null, 2)}\n`)
852
- }
853
- }
854
-
855
- // Minimal JSONC support for tsconfig files: strips // and /* */ comments
856
- // outside strings (a "$schema": "https://..." value must survive).
857
- function stripJsonComments(source: string) {
858
- let result = ''
859
- let inString = false
860
- for (let index = 0; index < source.length; index++) {
861
- const char = source[index]!
862
- if (inString) {
863
- result += char
864
- if (char === '\\') {
865
- result += source[++index] ?? ''
866
- } else if (char === '"') {
867
- inString = false
868
- }
869
- continue
870
- }
871
- if (char === '"') {
872
- inString = true
873
- result += char
874
- continue
875
- }
876
- if (char === '/' && source[index + 1] === '/') {
877
- while (index < source.length && source[index] !== '\n') index++
878
- result += '\n'
879
- continue
880
- }
881
- if (char === '/' && source[index + 1] === '*') {
882
- index += 2
883
- while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) index++
884
- index++
885
- continue
886
- }
887
- result += char
888
- }
889
- return result
890
- }
891
-
892
- const bakedPathFileFilter = /\.(?:m?js|cjs|jsx|tsx?|json|html|css)$/
893
-
894
- // The build output is thousands of small files and only a few hundred carry a
895
- // baked path, so the pass is io-latency-bound: run it in bounded-concurrency
896
- // batches instead of one file at a time.
897
- const BAKED_PATH_CONCURRENCY = 64
898
-
899
- async function rewriteBakedPaths(
900
- dir: string,
901
- replacements: readonly (readonly [string, string])[],
902
- ) {
903
- if (!existsSync(dir)) return
904
- const files = (await listFiles(dir)).filter(file => bakedPathFileFilter.test(file))
905
- for (let i = 0; i < files.length; i += BAKED_PATH_CONCURRENCY) {
906
- await Promise.all(
907
- files.slice(i, i + BAKED_PATH_CONCURRENCY).map(async file => {
908
- const source = await readFile(file, 'utf8')
909
- let rewritten = source
910
- for (const [from, to] of replacements) {
911
- if (rewritten.includes(from)) rewritten = rewritten.replaceAll(from, to)
912
- }
913
- if (rewritten === source) return
914
- // The replica is hardlinked to the real build output; unlink before
915
- // writing so the rewrite never reaches the original through a shared
916
- // inode.
917
- await rm(file)
918
- await writeFile(file, rewritten)
919
- }),
920
- )
921
- }
922
- }
923
-
924
265
  /**
925
- * An artifact's import specifiers, preferring the sidecar the compile step recorded over re-reading
926
- * and re-parsing the file. Falls back to the parse whenever the sidecar is missing or unreadable - a
927
- * stale cache hit from a prior dev build never wrote one - so the traced closure is identical either
928
- * way, just cheaper when it is there.
266
+ * Native bindings resolved here are built for the build host; the function needs the ones for its
267
+ * own platform. Each is replaced by its owner's build for the function platform, whole: a binding
268
+ * package is its binary.
929
269
  */
930
- async function artifactSpecifiers(file: string): Promise<string[] | null> {
931
- const sidecar = await readFile(`${file}${compiledSpecifiersManifestSuffix}`, 'utf8').catch(
932
- () => null,
933
- )
934
- if (sidecar !== null) {
935
- try {
936
- const parsed: unknown = JSON.parse(sidecar)
937
- if (Array.isArray(parsed) && parsed.every(entry => typeof entry === 'string')) {
938
- return parsed
939
- }
940
- } catch {
941
- // malformed sidecar (partial write, foreign tool) — fall through to parse
942
- }
943
- }
944
- const source = await readFile(file, 'utf8').catch(() => null)
945
- return source === null ? null : importSpecifiers(source, file)
946
- }
947
-
948
- // Ship only the node_modules packages the server can actually reach at request time: bare imports
949
- // traced from the modules the runtime loads, plus the framework's declared runtime dependencies,
950
- // expanded through their dependency closures. Nothing reachable only from build-time code enters the
951
- // function - the full monorepo node_modules does not fit Vercel's size limit. Returns the closure
952
- // plus the deferred copy so the caller can order the io.
953
- async function traceNodeModulesClosure(
954
- config: ResolvedConfig,
955
- manifest: BuildManifest,
956
- functionPath: string,
957
- framework: FrameworkPackage,
958
- warmedModules: string[],
959
- pack: PackRules,
960
- log: VerboseLogger,
961
- ) {
962
- const workspaceRoot = config.workspaceRoot
963
- const specifiers = new Set<string>()
964
- const tracedFiles = new Set<string>()
965
- // Compat-aliased specifiers whose target lives in the framework source
966
- // (react, next/*) never resolve from node_modules at runtime — shipping
967
- // them would drag in the full next/react trees. Aliases that point into
968
- // node_modules (the pinned preact family) still need their packages.
969
- const frameworkAliasedSpecifiers = new Set(
970
- frameworkOwnedAliases(config, path.resolve(framework.root)).map(([specifier]) => specifier),
971
- )
972
- await log.step('trace runtime imports', async () => {
973
- // Only code Bun imports raw at request time needs real node_modules: route handlers, the proxy,
974
- // the config chain, and the compiled modules the warm step just wrote. Pages resolve through that
975
- // compiled cache, whose externals are vendored, so their dependencies never load raw.
976
- const queue: string[] = []
977
- const seen = tracedFiles
978
- const enqueue = (file: string | undefined) => {
979
- if (!file) return
980
- const resolved = path.resolve(file)
981
- if (seen.has(resolved) || !existsSync(resolved)) return
982
- seen.add(resolved)
983
- queue.push(resolved)
984
- }
985
-
986
- for (const route of manifest.routes) {
987
- if (route.kind === 'handler') enqueue(route.file)
988
- }
989
- enqueue(findProxyFile(config) ?? undefined)
990
- enqueue(path.join(config.root, 'pnext.config.ts'))
991
- enqueue(path.join(config.root, 'next.config.js'))
992
- for (const file of warmedModules) enqueue(file)
993
-
994
- const visit = async (file: string) => {
995
- if (!scriptFilePattern.test(file)) return
996
- const found = await artifactSpecifiers(file)
997
- if (found === null) return
998
- for (const specifier of found) {
999
- if (frameworkAliasedSpecifiers.has(specifier)) continue
1000
- // Compiled modules import each other — and whatever the compiler left
1001
- // external — by absolute href. Follow the ones inside the build output
1002
- // and ship the packages the rest point into.
1003
- const absolute = absolutePathFromSpecifier(specifier)
1004
- if (absolute) {
1005
- const name = packageNameFromPath(absolute)
1006
- if (name) specifiers.add(name)
1007
- else if (isInsideDir(config.outPath, absolute)) enqueue(absolute)
1008
- continue
1009
- }
1010
- if (/^(?:node:|bun$|bun:|data:)/.test(specifier)) continue
1011
- // resolveImport covers relative paths, tsconfig aliases, package imports and workspace
1012
- // packages - workspace source gets traced further. Bare package specifiers are also recorded
1013
- // so the closure ships (or workspace-links) their node_modules entries; workspace packages
1014
- // need the link even when their source is traced.
1015
- const resolved = resolveImport(config.root, file, specifier, config.workspaceRoot)
1016
- if (
1017
- resolved &&
1018
- isInsideDir(workspaceRoot, resolved) &&
1019
- !resolved.includes('node_modules')
1020
- ) {
1021
- enqueue(resolved)
1022
- }
1023
- const name = packageNameFromSpecifier(specifier)
1024
- if (name) specifiers.add(name)
1025
- }
1026
- }
1027
-
1028
- // A level's reads are independent, so the walk advances a level at a time, bounded so a wide
1029
- // graph cannot exhaust the fd table.
1030
- const READ_BATCH = 64
1031
- for (let frontier = queue.splice(0); frontier.length > 0; frontier = queue.splice(0)) {
1032
- for (let index = 0; index < frontier.length; index += READ_BATCH) {
1033
- await Promise.all(frontier.slice(index, index + READ_BATCH).map(visit))
1034
- }
1035
- }
1036
- log.log(`traced ${seen.size} runtime modules`)
1037
- })
1038
-
1039
- const packageDirs = new Map<string, string>()
1040
- const workspaceLinks = new Map<string, string>()
1041
- // Native bindings resolved here are built for the build host; the function
1042
- // needs the ones for its own platform, fetched below.
1043
- const hostNatives = new Map<string, { dir: string; owner?: PackageVersion }>()
1044
- // The framework ships whole, so what it can load at runtime is exactly what it declares as a
1045
- // runtime dependency - resolved from its own root, which is not the app's when it comes from a
1046
- // registry. Its optional deps back compat features that only load when the app uses them, so they
1047
- // ship only where the app declares them too.
1048
- const appDependencies = await declaredDependencies(config.root)
1049
- const queue: { name: string; from: string; owner?: PackageVersion }[] = [
1050
- ...framework.dependencies.map(name => ({ name, from: framework.root })),
1051
- ...framework.optionalDependencies
1052
- .filter(name => appDependencies.has(name))
1053
- .map(name => ({ name, from: framework.root })),
1054
- ...[...specifiers].map(name => ({ name, from: workspaceRoot })),
1055
- ]
1056
- const seen = new Set<string>()
1057
-
1058
- while (queue.length > 0) {
1059
- const { name, from, owner } = queue.shift()!
1060
- if (seen.has(name)) continue
1061
- seen.add(name)
1062
- // The framework is copied to its own location by the caller; copying it
1063
- // again under its package name collides with that (and with the workspace
1064
- // link it needs when it lives in the workspace).
1065
- if (name === framework.packageName) {
1066
- if (framework.inWorkspace) workspaceLinks.set(name, framework.targetRel)
1067
- continue
1068
- }
1069
- const dir = resolvePackageDir(name, [from, workspaceRoot, config.root])
1070
- if (!dir) continue
1071
-
1072
- // Workspace packages ship at their workspace-relative path; link them so
1073
- // bare imports resolve to the same real path the compiled cache uses.
1074
- const workspaceRelative = path.relative(workspaceRoot, dir)
1075
- if (!workspaceRelative.startsWith('..') && !workspaceRelative.startsWith('node_modules')) {
1076
- workspaceLinks.set(name, workspaceRelative)
1077
- continue
1078
- }
1079
-
1080
- const packageJson = await readPackageJson(dir)
1081
- // `os`/`cpu` are npm's own declaration that a package is platform-bound;
1082
- // one that already fits the function's platform ships as-is.
1083
- const platformBound = Boolean(packageJson?.os ?? packageJson?.cpu)
1084
- if (packageJson && platformBound && !matchesFunctionPlatform(packageJson)) {
1085
- hostNatives.set(name, { dir, owner })
1086
- continue
1087
- }
1088
- packageDirs.set(name, dir)
1089
- if (!packageJson) continue
1090
- const self = { name, version: packageJson.version ?? '' }
1091
- // Peer dependencies are deliberately not expanded — they fan out to whole
1092
- // ecosystems (react, next, ...) that are only shipped when something
1093
- // actually imports them.
1094
- for (const dependency of [
1095
- ...Object.keys(packageJson.dependencies ?? {}),
1096
- ...Object.keys(packageJson.optionalDependencies ?? {}),
1097
- ]) {
1098
- queue.push({ name: dependency, from: dir, owner: self })
1099
- }
1100
- }
1101
-
1102
- if (hostNatives.size > 0 && !skipDependencyClosure()) {
1103
- const targets = await log.step(
1104
- `resolve ${FUNCTION_PLATFORM.os}-${FUNCTION_PLATFORM.cpu} natives (${hostNatives.size} packages)`,
1105
- () => platformNativePackages(hostNatives, log),
1106
- )
1107
- for (const [name, dir] of targets) packageDirs.set(name, dir)
270
+ async function replaceHostNatives(packages: Map<string, string[]>, log: VerboseLogger) {
271
+ const manifests = new Map<string, PackageJson>()
272
+ for (const [name, dirs] of packages) {
273
+ const json = await readPackageJson(dirs[0]!)
274
+ if (json) manifests.set(name, json)
275
+ }
276
+ const hostNatives = new Map<
277
+ string,
278
+ { dirs: string[]; owner?: { name: string; version: string } }
279
+ >()
280
+ for (const [name, json] of manifests) {
281
+ if (!(json.os ?? json.cpu) || matchesFunctionPlatform(json)) continue
282
+ const owner = [...manifests].find(([, candidate]) => candidate.optionalDependencies?.[name])
283
+ hostNatives.set(name, {
284
+ dirs: packages.get(name)!,
285
+ owner: owner && { name: owner[0], version: owner[1].version ?? '' },
286
+ })
1108
287
  }
1109
-
1110
- const copy = async () => {
1111
- const entries = skipDependencyClosure() ? [] : [...packageDirs]
1112
- const batchSize = 16
1113
- for (let index = 0; index < entries.length; index += batchSize) {
1114
- await Promise.all(
1115
- entries.slice(index, index + batchSize).map(async ([name, dir]) => {
1116
- const target = path.join(functionPath, 'node_modules', name)
1117
- await mkdir(path.dirname(target), { recursive: true })
1118
- await copyTree(dir, target, pack)
1119
- }),
1120
- )
288
+ if (hostNatives.size === 0) return
289
+ const targets = await platformNativePackages(hostNatives, manifests, log)
290
+ for (const [name, dir] of targets) {
291
+ for (const host of hostNatives.get(name)?.dirs ?? []) {
292
+ const target = path.join(path.dirname(host), name.split('/').at(-1)!)
293
+ await rm(host, { recursive: true, force: true })
294
+ await cp(dir, target, { recursive: true, dereference: true })
1121
295
  }
1122
- for (const [name, workspaceRelative] of workspaceLinks) {
1123
- const linkPath = path.join(functionPath, 'node_modules', name)
1124
- await mkdir(path.dirname(linkPath), { recursive: true })
1125
- const target = path.relative(
1126
- path.dirname(linkPath),
1127
- path.join(functionPath, workspaceRelative),
1128
- )
1129
- if (!existsSync(linkPath)) await symlink(target, linkPath, 'dir')
1130
- }
1131
- }
1132
-
1133
- return {
1134
- tracedFiles,
1135
- workspacePackages: [...workspaceLinks.values()],
1136
- packageCount: packageDirs.size,
1137
- copy,
1138
296
  }
1139
297
  }
1140
298
 
1141
- interface PackageVersion {
1142
- name: string
1143
- version: string
1144
- }
1145
-
1146
299
  /**
1147
- * Native bindings for the function's platform, not the build host's. The host install only ever
1148
- * carries its own platform's optional deps, so the right builds have to come from the registry: one
1149
- * cross-platform install per distinct set of owning packages, cached by content in the user cache dir
1150
- * (the build output is wiped every build). Falls back to the host copies when the install is
1151
- * unavailable (offline CI, private registry), so packaging never hard-fails on a network hiccup.
300
+ * The function platform's builds, from the registry: one cross-platform install per distinct set of
301
+ * owning packages, cached by content in the user cache dir. Falls back to the host copies when the
302
+ * install is unavailable (offline CI, private registry), so packaging never hard-fails on a network
303
+ * hiccup.
1152
304
  */
1153
305
  async function platformNativePackages(
1154
- hostNatives: Map<string, { dir: string; owner?: PackageVersion }>,
306
+ hostNatives: Map<string, { dirs: string[]; owner?: { name: string; version: string } }>,
307
+ manifests: Map<string, PackageJson>,
1155
308
  log: VerboseLogger,
1156
309
  ) {
1157
- // Install the package that *owns* the binding: only it lists every
1158
- // platform's build in its optional deps.
1159
310
  const owners = new Map<string, string>()
1160
- for (const [name, { dir, owner }] of hostNatives) {
1161
- const spec = owner ?? { name, version: (await readPackageJson(dir))?.version ?? '' }
311
+ for (const [name, { owner }] of hostNatives) {
312
+ const spec = owner ?? { name, version: manifests.get(name)?.version ?? '' }
1162
313
  if (spec.version) owners.set(spec.name, spec.version)
1163
314
  }
1164
315
  const specs = [...owners].map(([name, version]) => `${name}@${version}`).sort()
1165
- const fallback = () => new Map([...hostNatives].map(([name, { dir }]) => [name, dir]))
1166
- if (specs.length === 0) return fallback()
1167
-
316
+ if (specs.length === 0) return new Map<string, string>()
1168
317
  try {
1169
318
  const installed = await crossPlatformInstall(specs)
1170
319
  const matches = new Map<string, string>()
1171
320
  for (const [name, dir] of installed) {
1172
- const packageJson = await readPackageJson(dir)
1173
- if (packageJson && matchesFunctionPlatform(packageJson)) matches.set(name, dir)
321
+ const json = await readPackageJson(dir)
322
+ if (json && (json.os ?? json.cpu) && matchesFunctionPlatform(json)) matches.set(name, dir)
1174
323
  }
1175
- if (matches.size === 0) {
324
+ if (matches.size === 0)
1176
325
  throw new Error(`no ${FUNCTION_PLATFORM.os} builds in ${specs.join(' ')}`)
1177
- }
1178
326
  return matches
1179
327
  } catch (error) {
1180
328
  console.warn(
@@ -1183,7 +331,7 @@ async function platformNativePackages(
1183
331
  error,
1184
332
  )
1185
333
  log.log('falling back to host native packages')
1186
- return fallback()
334
+ return new Map<string, string>()
1187
335
  }
1188
336
  }
1189
337
 
@@ -1259,88 +407,6 @@ async function crossPlatformInstall(specs: string[]) {
1259
407
  return installed
1260
408
  }
1261
409
 
1262
- const scriptFilePattern = /\.(?:m?js|cjs|jsx|tsx?)$/
1263
-
1264
- // Directories the shipped tsconfigs can never live in; pruned during the walk —
1265
- // a naive recursive listing would crawl the function's own node_modules.
1266
- const skippedScanDirs = new Set(['node_modules', '.git', '.next', '.pnext', '.turbo', '.vercel'])
1267
-
1268
- function* walkTsconfigFiles(root: string): Generator<string> {
1269
- const stack = [root]
1270
- while (stack.length > 0) {
1271
- const dir = stack.pop()!
1272
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1273
- const full = path.join(dir, entry.name)
1274
- if (entry.isDirectory()) {
1275
- if (!skippedScanDirs.has(entry.name)) stack.push(full)
1276
- } else if (entry.isFile() && entry.name === 'tsconfig.json') {
1277
- yield full
1278
- }
1279
- }
1280
- }
1281
- }
1282
-
1283
- /** Every dependency name an app's own package.json declares. */
1284
- async function declaredDependencies(root: string) {
1285
- const packageJson = (await readPackageJson(root)) as
1286
- Record<string, Record<string, string> | undefined> | undefined
1287
- return new Set(
1288
- ['dependencies', 'optionalDependencies', 'devDependencies'].flatMap(field =>
1289
- Object.keys(packageJson?.[field] ?? {}),
1290
- ),
1291
- )
1292
- }
1293
-
1294
- /** Absolute path behind an `import`, whether written as a path or a file URL. */
1295
- function absolutePathFromSpecifier(specifier: string) {
1296
- if (specifier.startsWith('file://')) return fileURLToPath(specifier)
1297
- return path.isAbsolute(specifier) ? specifier : undefined
1298
- }
1299
-
1300
- /** The package an absolute path belongs to, if it points inside node_modules. */
1301
- function packageNameFromPath(file: string) {
1302
- const parts = toPosixPath(file).split('/node_modules/')
1303
- const tail = parts.at(-1)
1304
- if (parts.length < 2 || !tail) return undefined
1305
- const segments = tail.split('/')
1306
- return segments[0]?.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]
1307
- }
1308
-
1309
- function packageNameFromSpecifier(specifier: string) {
1310
- if (/^[./#]|^file:|^node:|^data:/.test(specifier)) return undefined
1311
- if (specifier === 'bun' || specifier.startsWith('bun:')) return undefined
1312
- if (specifier.includes(':')) return undefined
1313
- // Scoped names need a real scope — `@/env`-style tsconfig aliases are not
1314
- // packages.
1315
- if (specifier.startsWith('@') && !/^@[^/]+\/[^/]+/.test(specifier)) return undefined
1316
- const parts = specifier.split('/')
1317
- const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
1318
- if (!name || builtinModules.includes(name)) return undefined
1319
- return name
1320
- }
1321
-
1322
- function resolvePackageDir(name: string, fromDirs: string[]) {
1323
- for (const from of fromDirs) {
1324
- let dir = path.resolve(from)
1325
- while (true) {
1326
- const candidate = path.join(dir, 'node_modules', name)
1327
- if (existsSync(path.join(candidate, 'package.json'))) return realDir(candidate)
1328
- const parent = path.dirname(dir)
1329
- if (parent === dir) break
1330
- dir = parent
1331
- }
1332
- }
1333
- return undefined
1334
- }
1335
-
1336
- function realDir(dir: string) {
1337
- try {
1338
- return statSync(dir).isDirectory() ? realpathSync(dir) : undefined
1339
- } catch {
1340
- return undefined
1341
- }
1342
- }
1343
-
1344
410
  async function copyStaticFiles(
1345
411
  from: string,
1346
412
  to: string,
@@ -1355,7 +421,9 @@ async function copyStaticFiles(
1355
421
  )
1356
422
  await Promise.all([...directories].map(dir => mkdir(dir, { recursive: true })))
1357
423
  await Promise.all(
1358
- files.map(file => linkOrCopyFile(file, path.join(to, path.relative(from, file)))),
424
+ files.map(file =>
425
+ copyFile(file, path.join(to, path.relative(from, file)), constants.COPYFILE_FICLONE),
426
+ ),
1359
427
  )
1360
428
  }
1361
429
 
@@ -1379,14 +447,50 @@ async function staticOverrides(
1379
447
  return overrides
1380
448
  }
1381
449
 
1382
- async function immutableAssetRoute() {
1383
- const { immutableAssetPrefixes, immutableCacheControl } = await import('../serve/pipeline')
1384
- const prefixes = [...new Set(immutableAssetPrefixes())].map(escapeRegex)
1385
- return {
1386
- src: `^/(?:${prefixes.join('|')})`,
450
+ // `important` lets route headers win over the CDN's own static-file cache-control, as Next's builder does.
451
+ async function immutableAssetRoutes(publicDir: string, publicAssets: string[] = []) {
452
+ const app = new Set(publicAssets)
453
+ const built =
454
+ publicAssets.length > 0
455
+ ? (await listFiles(publicDir))
456
+ .map(file => toPosixPath(path.relative(publicDir, file)))
457
+ .filter(relative => !app.has(relative) && immutableAssetPath(relative))
458
+ : []
459
+ const patterns = immutableAssetPatterns(built, publicAssets)
460
+ return routeSources(patterns, source => `^/(?:${source})`).map(src => ({
461
+ src,
1387
462
  headers: { 'cache-control': immutableCacheControl },
1388
463
  continue: true,
464
+ important: true,
465
+ }))
466
+ }
467
+
468
+ /**
469
+ * Whole immutable prefixes that hold no app public file; under the others, each build file collapsed
470
+ * to its topmost folder below the prefix that holds no app public file, else its exact name.
471
+ */
472
+ function immutableAssetPatterns(built: string[], publicAssets: string[]) {
473
+ const appDirs = new Set(publicAssets.flatMap(ancestorDirs))
474
+ const prefixes = [...new Set(immutableAssetPrefixes())]
475
+ const patterns = prefixes.filter(prefix => !appDirs.has(prefix)).map(escapeRegex)
476
+ for (const relative of built) {
477
+ const prefix = prefixes.find(prefix => relative.startsWith(prefix))
478
+ if (!prefix || !appDirs.has(prefix)) continue
479
+ const dir = ancestorDirs(relative).find(dir => dir.length > prefix.length && !appDirs.has(dir))
480
+ patterns.push(dir ? escapeRegex(requestPath(dir)) : `${escapeRegex(requestPath(relative))}$`)
1389
481
  }
482
+ return [...new Set(patterns)]
483
+ }
484
+
485
+ /** A public-relative path as browsers request it (the WHATWG path percent-encode set). */
486
+ function requestPath(relative: string) {
487
+ return relative.replace(/[\0-\x20"#<>?`{}\x7f-\u{10ffff}]/gu, char => encodeURIComponent(char))
488
+ }
489
+
490
+ /** `a/b/c.js` -> `['a/', 'a/b/']`. */
491
+ function ancestorDirs(relative: string) {
492
+ const parts = relative.split('/').slice(0, -1)
493
+ return parts.map((_, index) => `${parts.slice(0, index + 1).join('/')}/`)
1390
494
  }
1391
495
 
1392
496
  // Headers a CDN route cannot replay; a file that sets one stays on the server function.
@@ -1434,23 +538,29 @@ function staticHeaderRoutes(staticFiles: Record<string, StaticFileMetadata>) {
1434
538
  groups.set(key, group)
1435
539
  group.patterns.push(servedPathPattern(relative))
1436
540
  }
1437
- return [...groups.values()].flatMap(({ headers, patterns }) => {
1438
- const sources: string[] = []
1439
- for (const pattern of patterns) {
1440
- const last = sources.length - 1
1441
- if (last >= 0 && sources[last]!.length + pattern.length < maxRouteSourceLength) {
1442
- sources[last] += `|${pattern}`
1443
- } else {
1444
- sources.push(pattern)
1445
- }
1446
- }
1447
- return sources.map(source => ({
1448
- src: `^/(?:${source})$`,
541
+ return [...groups.values()].flatMap(({ headers, patterns }) =>
542
+ routeSources(patterns, source => `^/(?:${source})$`).map(src => ({
543
+ src,
1449
544
  methods: ['GET', 'HEAD'],
1450
545
  headers,
1451
546
  continue: true,
1452
- }))
1453
- })
547
+ important: true,
548
+ })),
549
+ )
550
+ }
551
+
552
+ /** Joins patterns into as few `wrap`ped route sources as fit `maxRouteSourceLength`. */
553
+ function routeSources(patterns: string[], wrap: (alternation: string) => string) {
554
+ const sources: string[] = []
555
+ for (const pattern of patterns) {
556
+ const last = sources.length - 1
557
+ if (last >= 0 && wrap(`${sources[last]}|${pattern}`).length <= maxRouteSourceLength) {
558
+ sources[last] += `|${pattern}`
559
+ } else {
560
+ sources.push(pattern)
561
+ }
562
+ }
563
+ return sources.map(wrap)
1454
564
  }
1455
565
 
1456
566
  /** A static file's request paths: `a/index.html` also answers `/a` and `/a/`. */