@wular/pnext 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/bin/pnext +9 -2
  2. package/dist/server/_chunks/aliases-j47a10ce.js +44 -0
  3. package/dist/server/_chunks/analyze-arswqe6s.js +445 -0
  4. package/dist/server/_chunks/boot-af0mv3dy.js +36 -0
  5. package/dist/server/_chunks/build-spy3dxp4.js +84 -0
  6. package/dist/server/_chunks/build-t9h0hec3.js +3550 -0
  7. package/dist/server/_chunks/build-tier-m7yvqz61.js +2472 -0
  8. package/dist/server/_chunks/compile-53j02j27.js +22 -0
  9. package/dist/server/_chunks/config-1yzwexq6.js +27 -0
  10. package/dist/server/_chunks/config-n2z7yxps.js +22 -0
  11. package/dist/server/_chunks/create-meec9hs6.js +134 -0
  12. package/dist/server/_chunks/dev-fjmqg6en.js +220 -0
  13. package/dist/server/_chunks/esbuild-yyktfrtt.js +15 -0
  14. package/dist/server/_chunks/generate-n9sh6hzt.js +21 -0
  15. package/dist/server/_chunks/index-0374j2b9.js +66 -0
  16. package/dist/server/_chunks/index-06nyqntx.js +213 -0
  17. package/dist/server/_chunks/index-1011cscr.js +5263 -0
  18. package/dist/server/_chunks/index-11sfk4hb.js +170 -0
  19. package/dist/server/_chunks/index-20ppawv7.js +297 -0
  20. package/dist/server/_chunks/index-2dv4y4wd.js +1065 -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-357na4a8.js +109 -0
  24. package/dist/server/_chunks/index-3b3at80h.js +109 -0
  25. package/dist/server/_chunks/index-3tf1xjsp.js +6166 -0
  26. package/dist/server/_chunks/index-3xbr4eyv.js +105 -0
  27. package/dist/server/_chunks/index-4399bvd2.js +18 -0
  28. package/dist/server/_chunks/index-4hgjrqh7.js +851 -0
  29. package/dist/server/_chunks/index-4q2gbfwk.js +2530 -0
  30. package/dist/server/_chunks/index-4t92hdxd.js +172 -0
  31. package/dist/server/_chunks/index-4zzebszd.js +10 -0
  32. package/dist/server/_chunks/index-5gyr4emz.js +107 -0
  33. package/dist/server/_chunks/index-5hr4062s.js +70 -0
  34. package/dist/server/_chunks/index-68m3mwbd.js +84 -0
  35. package/dist/server/_chunks/index-6b009mt0.js +56 -0
  36. package/dist/server/_chunks/index-6e4fqv30.js +588 -0
  37. package/dist/server/_chunks/index-6gex3pfa.js +15 -0
  38. package/dist/server/_chunks/index-6kp9r0d9.js +11 -0
  39. package/dist/server/_chunks/index-6q7zvrap.js +134 -0
  40. package/dist/server/_chunks/index-73z4esab.js +38 -0
  41. package/dist/server/_chunks/index-79ngvam8.js +60 -0
  42. package/dist/server/_chunks/index-7wc9n8be.js +25 -0
  43. package/dist/server/_chunks/index-8k7pfejn.js +1599 -0
  44. package/dist/server/_chunks/index-8kcxdfya.js +206 -0
  45. package/dist/server/_chunks/index-91x12ykq.js +274 -0
  46. package/dist/server/_chunks/index-9ch50vtq.js +18 -0
  47. package/dist/server/_chunks/index-9hx2641q.js +28 -0
  48. package/dist/server/_chunks/index-9n2914ww.js +535 -0
  49. package/dist/server/_chunks/index-9ych9ma8.js +207 -0
  50. package/dist/server/_chunks/index-b6wtkdp4.js +24 -0
  51. package/dist/server/_chunks/index-bcws5eh2.js +35 -0
  52. package/dist/server/_chunks/index-c0g2wf3z.js +92 -0
  53. package/dist/server/_chunks/index-cf4rvxv6.js +95 -0
  54. package/dist/server/_chunks/index-cjgqs13g.js +65 -0
  55. package/dist/server/_chunks/index-ckkdpht5.js +41 -0
  56. package/dist/server/_chunks/index-cyfednwb.js +87 -0
  57. package/dist/server/_chunks/index-danncfhy.js +311 -0
  58. package/dist/server/_chunks/index-dg0f05zs.js +199 -0
  59. package/dist/server/_chunks/index-e3a6yx84.js +18 -0
  60. package/dist/server/_chunks/index-e88tyv35.js +45 -0
  61. package/dist/server/_chunks/index-eajaspjk.js +1253 -0
  62. package/dist/server/_chunks/index-eeaanp2e.js +483 -0
  63. package/dist/server/_chunks/index-egykcqg3.js +101 -0
  64. package/dist/server/_chunks/index-en7n9f7m.js +22 -0
  65. package/dist/server/_chunks/index-ezgj0tr1.js +88 -0
  66. package/dist/server/_chunks/index-fkp000p1.js +590 -0
  67. package/dist/server/_chunks/index-fn1chkkd.js +214 -0
  68. package/dist/server/_chunks/index-fp8r0sqs.js +64 -0
  69. package/dist/server/_chunks/index-ggk2j965.js +9432 -0
  70. package/dist/server/_chunks/index-grta8ap3.js +238 -0
  71. package/dist/server/_chunks/index-h2a70tg4.js +7 -0
  72. package/dist/server/_chunks/index-h4xy648y.js +51 -0
  73. package/dist/server/_chunks/index-hkdfj58q.js +92 -0
  74. package/dist/server/_chunks/index-j2zrcp1k.js +66 -0
  75. package/dist/server/_chunks/index-jcsjq3d1.js +20 -0
  76. package/dist/server/_chunks/index-jepjvcrf.js +198 -0
  77. package/dist/server/_chunks/index-jks0x0a1.js +14 -0
  78. package/dist/server/_chunks/index-jx7bqvw1.js +57 -0
  79. package/dist/server/_chunks/index-k1g3epw0.js +1299 -0
  80. package/dist/server/_chunks/index-knp0s912.js +7 -0
  81. package/dist/server/_chunks/index-kq8kpzye.js +472 -0
  82. package/dist/server/_chunks/index-kqyy1d4f.js +50 -0
  83. package/dist/server/_chunks/index-kresp2n8.js +445 -0
  84. package/dist/server/_chunks/index-ms9v9tnx.js +31 -0
  85. package/dist/server/_chunks/index-n4hxpng2.js +11 -0
  86. package/dist/server/_chunks/index-ncnes8c6.js +17 -0
  87. package/dist/server/_chunks/index-nqh3xhdc.js +57 -0
  88. package/dist/server/_chunks/index-nyrba2kr.js +4130 -0
  89. package/dist/server/_chunks/index-p77b4kcx.js +296 -0
  90. package/dist/server/_chunks/index-pym1cezq.js +39 -0
  91. package/dist/server/_chunks/index-q738kkhj.js +100 -0
  92. package/dist/server/_chunks/index-qjj6gddn.js +54 -0
  93. package/dist/server/_chunks/index-qm6kmhqs.js +110 -0
  94. package/dist/server/_chunks/index-qn4z5s3c.js +819 -0
  95. package/dist/server/_chunks/index-qpwsgt3a.js +158 -0
  96. package/dist/server/_chunks/index-r97ptz1q.js +97 -0
  97. package/dist/server/_chunks/index-ras0sdab.js +26 -0
  98. package/dist/server/_chunks/index-rcs322t1.js +19 -0
  99. package/dist/server/_chunks/index-rn8dyr1f.js +126 -0
  100. package/dist/server/_chunks/index-rt9qq1va.js +4 -0
  101. package/dist/server/_chunks/index-rthrsed5.js +356 -0
  102. package/dist/server/_chunks/index-sctcgyvv.js +185 -0
  103. package/dist/server/_chunks/index-sjpckxqn.js +213 -0
  104. package/dist/server/_chunks/index-sq3kvz3g.js +200 -0
  105. package/dist/server/_chunks/index-stbk3jhn.js +31 -0
  106. package/dist/server/_chunks/index-sv8m6789.js +266 -0
  107. package/dist/server/_chunks/index-tgcakdxc.js +125 -0
  108. package/dist/server/_chunks/index-tgsfjmep.js +74 -0
  109. package/dist/server/_chunks/index-tq0pc7eg.js +154 -0
  110. package/dist/server/_chunks/index-tq1f96v7.js +51 -0
  111. package/dist/server/_chunks/index-tqn2qnnn.js +241 -0
  112. package/dist/server/_chunks/index-tvqkq6e6.js +128 -0
  113. package/dist/server/_chunks/index-v1ags1na.js +132 -0
  114. package/dist/server/_chunks/index-vvmmknm1.js +1431 -0
  115. package/dist/server/_chunks/index-vvq7rqtn.js +329 -0
  116. package/dist/server/_chunks/index-vz0km5qk.js +33 -0
  117. package/dist/server/_chunks/index-w6h8m6vp.js +138 -0
  118. package/dist/server/_chunks/index-web361b8.js +138 -0
  119. package/dist/server/_chunks/index-wpfhfbs4.js +194 -0
  120. package/dist/server/_chunks/index-x0y005nh.js +283 -0
  121. package/dist/server/_chunks/index-x3yn4ywm.js +22 -0
  122. package/dist/server/_chunks/index-x93hv0sc.js +271 -0
  123. package/dist/server/_chunks/index-xvj80c3a.js +30 -0
  124. package/dist/server/_chunks/index-xywjg2vc.js +100 -0
  125. package/dist/server/_chunks/index-y2jr2n72.js +152 -0
  126. package/dist/server/_chunks/index-y36a0ybw.js +57 -0
  127. package/dist/server/_chunks/index-ye9hmsvf.js +47 -0
  128. package/dist/server/_chunks/index-yhxkxghe.js +56 -0
  129. package/dist/server/_chunks/index-ym67zqxp.js +130 -0
  130. package/dist/server/_chunks/index-ytjvpbm2.js +736 -0
  131. package/dist/server/_chunks/index-yz54pxrh.js +960 -0
  132. package/dist/server/_chunks/index-z4z9zv3b.js +733 -0
  133. package/dist/server/_chunks/index-z84ab1w0.js +1270 -0
  134. package/dist/server/_chunks/index-z88qv16t.js +11 -0
  135. package/dist/server/_chunks/index-zqpqyg8p.js +666 -0
  136. package/dist/server/_chunks/loader-7yw0j98z.js +78 -0
  137. package/dist/server/_chunks/modules-jn8bmbn8.js +74 -0
  138. package/dist/server/_chunks/pipeline-yy9jhydf.js +832 -0
  139. package/dist/server/_chunks/production-jar9ztm3.js +44 -0
  140. package/dist/server/_chunks/proxy-config-j84dgksn.js +21 -0
  141. package/dist/server/_chunks/proxy-w1d0wmsn.js +33 -0
  142. package/dist/server/_chunks/router-k1reahfn.js +498 -0
  143. package/dist/server/_chunks/routes-2s277fc6.js +64 -0
  144. package/dist/server/_chunks/run-pvd8bs1t.js +696 -0
  145. package/dist/server/_chunks/server-mt4a994c.js +1835 -0
  146. package/dist/server/_chunks/tsconfig-defaults-1nb3308g.js +20 -0
  147. package/dist/server/_chunks/typegen-0qr0459b.js +35 -0
  148. package/dist/server/api/client-cache.js +14 -0
  149. package/dist/server/cli/index.js +378 -0
  150. package/dist/server/cli/start.js +137 -0
  151. package/dist/server/client/reference.js +16 -0
  152. package/dist/server/compat/next/cache.js +56 -0
  153. package/dist/server/compat/next/canonical-url.js +13 -0
  154. package/dist/server/compat/next/client-cache.js +57 -0
  155. package/dist/server/compat/next/client-navigation.js +232 -0
  156. package/dist/server/compat/next/client-only.js +1 -0
  157. package/dist/server/compat/next/client-script.js +178 -0
  158. package/dist/server/compat/next/client-server.js +34 -0
  159. package/dist/server/compat/next/config-headers.js +32 -0
  160. package/dist/server/compat/next/config-loader.js +25 -0
  161. package/dist/server/compat/next/config.js +10 -0
  162. package/dist/server/compat/next/constants.js +18 -0
  163. package/dist/server/compat/next/custom-server.js +10 -0
  164. package/dist/server/compat/next/dist/client/components/app-router-headers.js +48 -0
  165. package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +27 -0
  166. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +25 -0
  167. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +25 -0
  168. package/dist/server/compat/next/dynamic.js +32 -0
  169. package/dist/server/compat/next/error.js +112 -0
  170. package/dist/server/compat/next/font/cache.js +14 -0
  171. package/dist/server/compat/next/font/google.js +3 -0
  172. package/dist/server/compat/next/font/index.js +1 -0
  173. package/dist/server/compat/next/font/local.js +22 -0
  174. package/dist/server/compat/next/font/runtime-client.js +33 -0
  175. package/dist/server/compat/next/font/runtime.js +41 -0
  176. package/dist/server/compat/next/font/shared.js +22 -0
  177. package/dist/server/compat/next/form.js +129 -0
  178. package/dist/server/compat/next/head.js +10 -0
  179. package/dist/server/compat/next/headers.js +30 -0
  180. package/dist/server/compat/next/image/client.js +11 -0
  181. package/dist/server/compat/next/image/config.js +18 -0
  182. package/dist/server/compat/next/image/optimizer.js +25 -0
  183. package/dist/server/compat/next/image/patterns.js +12 -0
  184. package/dist/server/compat/next/image/props.js +16 -0
  185. package/dist/server/compat/next/image/shared.js +86 -0
  186. package/dist/server/compat/next/image/static-metadata.js +32 -0
  187. package/dist/server/compat/next/image/validate.js +8 -0
  188. package/dist/server/compat/next/image.js +59 -0
  189. package/dist/server/compat/next/index.js +1 -0
  190. package/dist/server/compat/next/legacy-image.js +78 -0
  191. package/dist/server/compat/next/legacy-request-apis.js +24 -0
  192. package/dist/server/compat/next/link-transform.js +14 -0
  193. package/dist/server/compat/next/link.js +14 -0
  194. package/dist/server/compat/next/navigation.js +190 -0
  195. package/dist/server/compat/next/offline.js +45 -0
  196. package/dist/server/compat/next/og.js +8 -0
  197. package/dist/server/compat/next/preferred-region.js +8 -0
  198. package/dist/server/compat/next/redirects.js +30 -0
  199. package/dist/server/compat/next/resource-hints.js +30 -0
  200. package/dist/server/compat/next/rewrites.js +41 -0
  201. package/dist/server/compat/next/root-params.js +121 -0
  202. package/dist/server/compat/next/route-state.js +23 -0
  203. package/dist/server/compat/next/router.js +103 -0
  204. package/dist/server/compat/next/script.js +37 -0
  205. package/dist/server/compat/next/server-only.js +1 -0
  206. package/dist/server/compat/next/server.js +131 -0
  207. package/dist/server/compat/next/svgr.js +14 -0
  208. package/dist/server/compat/next/telemetry.js +10 -0
  209. package/dist/server/compat/next/usage.js +13 -0
  210. package/dist/server/compat/next/user-agent.js +12 -0
  211. package/dist/server/compat/next/web-vitals.js +35 -0
  212. package/dist/server/compat/react/action-state.js +11 -0
  213. package/dist/server/compat/react/client-lite.js +83 -0
  214. package/dist/server/compat/react/client.js +131 -0
  215. package/dist/server/compat/react/compiler-runtime.js +42 -0
  216. package/dist/server/compat/react/dom-client.js +106 -0
  217. package/dist/server/compat/react/dom-react-server.js +37 -0
  218. package/dist/server/compat/react/dom-server.js +40 -0
  219. package/dist/server/compat/react/dom.js +52 -0
  220. package/dist/server/compat/react/hooks-extra.js +12 -0
  221. package/dist/server/compat/react/internals.js +14 -0
  222. package/dist/server/compat/react/parity.js +11 -0
  223. package/dist/server/compat/react/preact.js +113 -0
  224. package/dist/server/compat/react/react-server.js +133 -0
  225. package/dist/server/compat/react/router-shim.js +8 -0
  226. package/dist/server/compat/react/server-component-use.js +14 -0
  227. package/dist/server/compat/react/server-inserted-html.js +14 -0
  228. package/dist/server/compat/react/server.js +194 -0
  229. package/dist/server/compat/react/style-values.js +10 -0
  230. package/dist/server/compat/react/use.js +10 -0
  231. package/dist/server/compat/react/view-transition.js +12 -0
  232. package/dist/server/generation.json +1 -0
  233. package/package.json +4 -1
  234. package/src/api/dynamic.tsx +2 -0
  235. package/src/api/server.ts +24 -6
  236. package/src/cli/adapters/vercel-warm.ts +315 -61
  237. package/src/cli/adapters/vercel.ts +348 -71
  238. package/src/cli/build.ts +58 -33
  239. package/src/cli/index.ts +23 -10
  240. package/src/cli/serve/entry.ts +21 -159
  241. package/src/cli/serve/immutable.ts +31 -0
  242. package/src/cli/serve/pipeline.ts +82 -84
  243. package/src/cli/start.ts +3 -1
  244. package/src/client/chunk-fold.ts +19 -6
  245. package/src/client/react-tier.ts +3 -2
  246. package/src/client/reference-stub.ts +21 -2
  247. package/src/compat/actions/client-plugin.ts +4 -2
  248. package/src/compat/actions/config.ts +6 -5
  249. package/src/compat/actions/extensions.ts +2 -3
  250. package/src/compat/aliases.ts +8 -1
  251. package/src/compat/bundler/config.ts +88 -0
  252. package/src/compat/bundler/optimize-package-imports.ts +86 -26
  253. package/src/compat/bundler/relay-transform.ts +15 -3
  254. package/src/compat/cache-control.ts +16 -8
  255. package/src/compat/css/modules.ts +18 -5
  256. package/src/compat/lifecycle/error-funnel.ts +1 -1
  257. package/src/compat/lifecycle/instrumentation.ts +117 -24
  258. package/src/compat/lifecycle/testmode.ts +3 -2
  259. package/src/compat/next/config-loader.ts +137 -13
  260. package/src/compat/next/font/runtime.ts +22 -2
  261. package/src/compat/next/og.ts +3 -3
  262. package/src/compat/next/usage.ts +3 -2
  263. package/src/compat/pages/api.ts +83 -14
  264. package/src/compat/pages/router.ts +30 -14
  265. package/src/compat/register/bundler.ts +3 -1
  266. package/src/compat/register/middleware.ts +1 -1
  267. package/src/compat/register/otel.ts +5 -9
  268. package/src/compat/register/render.ts +3 -1
  269. package/src/compat/register/use-cache.ts +3 -1
  270. package/src/compat/release.ts +46 -0
  271. package/src/compat/segment/serve.ts +1 -1
  272. package/src/compat/validation/validate.ts +10 -7
  273. package/src/config.ts +51 -7
  274. package/src/css/assets.ts +213 -0
  275. package/src/css/build.ts +174 -238
  276. package/src/dev/server.ts +27 -8
  277. package/src/extensions.ts +4 -0
  278. package/src/render/hooks.ts +13 -0
  279. package/src/render/renderer.ts +52 -32
  280. package/src/request/context.ts +9 -0
  281. package/src/resolve/dynamic.ts +3 -1
  282. package/src/resolve/imports.ts +12 -52
  283. package/src/resolve/package-json.ts +56 -0
  284. package/src/resolve/scan-facts.ts +65 -0
  285. package/src/routing/href.ts +10 -3
  286. package/src/routing/match.ts +287 -0
  287. package/src/routing/metadata-files.ts +19 -5
  288. package/src/routing/proxy-config.ts +137 -0
  289. package/src/routing/proxy.ts +35 -134
  290. package/src/routing/routes.ts +22 -281
  291. package/src/runtime/fingerprint.ts +23 -8
  292. package/src/runtime/load.ts +170 -0
  293. package/src/runtime/loader.ts +215 -43
  294. package/src/runtime/module-cache.ts +13 -0
  295. package/src/runtime/modules.ts +370 -81
  296. package/src/runtime/prebundle.ts +81 -0
  297. package/src/runtime/production.ts +391 -0
  298. package/src/runtime/release-graph.ts +334 -0
  299. package/src/runtime/vendor-build.ts +724 -13
  300. package/src/runtime/vendor.ts +16 -3
  301. package/src/types.ts +2 -0
  302. package/src/utils/fs.ts +89 -3
@@ -3,8 +3,9 @@ import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync }
3
3
  import { builtinModules } from 'node:module'
4
4
  import path from 'node:path'
5
5
  import { fileURLToPath } from 'node:url'
6
- import { DEFAULT_OUT_DIR, pathToFileHref } from '../../config'
6
+ import { DEFAULT_OUT_DIR, devOutSegment, pathToFileHref } from '../../config'
7
7
  import type { ResolvedConfig } from '../../config'
8
+ import { compiledClientReferenceFiles } from '../../client/reference'
8
9
  import { globalCssSources } from '../../css/build'
9
10
  import { getImportAliasExtensions } from '../../extensions'
10
11
  import { nextCompatEnabled } from '../../render/hooks'
@@ -14,6 +15,15 @@ import { resolveImport, workspacePackageRoots } from '../../resolve/imports'
14
15
  import { findProxyFile, proxyRoutePatterns, type ProxyModule } from '../../routing/proxy'
15
16
  import { cacheRoot } from '../boot/named-bin'
16
17
  import { startWarmChild, type WarmChild } from './vercel-warm'
18
+ import {
19
+ immutableAssetPath,
20
+ immutableAssetPrefixes,
21
+ immutableCacheControl,
22
+ } from '../serve/immutable'
23
+ import { frameworkFingerprint } from '../../runtime/fingerprint'
24
+ import { readPrebundleStamp, runtimeEntryDirs, runtimeEntryFiles } from '../../runtime/prebundle'
25
+ import { buildIndexFile, readBuildIndex, releaseSourceFile } from '../../runtime/production'
26
+ import { compiledSourceGraph, flushDevModuleCaches } from '../../runtime/module-cache'
17
27
  import { escapeRegex } from '../../utils/code'
18
28
  import { listFiles, toPosixPath, writeText } from '../../utils/fs'
19
29
  import { createVerboseLogger, type VerboseLogger } from '../../utils/verbose'
@@ -71,8 +81,14 @@ const droppedFileSuffixes = [
71
81
  interface PackRules {
72
82
  prunedDirs: Set<string>
73
83
  droppedSuffixes: string[]
84
+ /** Absolute app sources the release compiled: dropped unless `adapter.keep` names them. */
85
+ compiled: Set<string>
86
+ /** `adapter.keep` entries: names and `.suffix`es that always ship. */
87
+ keep: Set<string>
74
88
  /** Absolute sources exempt from the prune list — the app's own build output. */
75
89
  keepPaths: Set<string>
90
+ /** Absolute sources never shipped: the dev server's `<outRoot>/dev`, which a build leaves in place. */
91
+ devPaths: Set<string>
76
92
  }
77
93
 
78
94
  // Test seam: the packing suite builds one app both ways and diffs the trees,
@@ -89,13 +105,24 @@ export function setPackPruningEnabled(enabled: boolean) {
89
105
  }
90
106
 
91
107
  function packRules(config: ResolvedConfig): PackRules {
108
+ // An out dir inside a dev path (`outDir: '.pnext/dev'`) is the production output and must ship.
109
+ const devPaths = new Set(
110
+ [config.outRootPath, path.resolve(config.root, DEFAULT_OUT_DIR)]
111
+ .map(root => path.resolve(root, devOutSegment))
112
+ .filter(dev => !isInsideDir(dev, config.outPath)),
113
+ )
114
+ // Materialized pages sources: the release serves their compiled artifacts.
115
+ devPaths.add(path.join(config.outRootPath, 'pnext-pages-compat'))
92
116
  if (!packPruningEnabled) {
93
117
  return {
94
118
  // The two the function could never resolve through stay pruned even
95
119
  // here: a nested store or object database is not a packing choice.
96
120
  prunedDirs: new Set(['node_modules', '.git']),
97
121
  droppedSuffixes: [],
122
+ compiled: new Set(),
123
+ keep: new Set(),
98
124
  keepPaths: new Set([path.resolve(config.outPath)]),
125
+ devPaths,
99
126
  }
100
127
  }
101
128
  const keep = new Set(config.adapter?.keep ?? [])
@@ -110,11 +137,14 @@ function packRules(config: ResolvedConfig): PackRules {
110
137
  droppedSuffixes: [...droppedFileSuffixes, ...extra.filter(isSuffix)].filter(
111
138
  suffix => !keep.has(suffix),
112
139
  ),
140
+ compiled: new Set(),
141
+ keep,
113
142
  // The app's outDir shares its name with the prune list; it holds the
114
143
  // compiled module cache the function serves from and must survive. The
115
144
  // default out root goes with it: a `distDir` app moved its output away, but
116
145
  // the next.config bundle the function imports at boot still lives there.
117
146
  keepPaths: new Set([path.resolve(config.outPath), path.resolve(config.root, DEFAULT_OUT_DIR)]),
147
+ devPaths,
118
148
  }
119
149
  }
120
150
 
@@ -139,12 +169,60 @@ function withinKeepPath(pack: PackRules, source: string) {
139
169
  * that would pull the whole store into the function.
140
170
  */
141
171
  function shipsDir(pack: PackRules, name: string, source: string, isLink = false) {
172
+ if (pack.devPaths.has(path.resolve(source))) return false
142
173
  if (!pack.prunedDirs.has(name)) return true
143
174
  return !isLink && withinKeepPath(pack, source)
144
175
  }
145
176
 
146
- function shipsFile(pack: PackRules, name: string) {
147
- return !pack.droppedSuffixes.some(suffix => name.endsWith(suffix))
177
+ function shipsFile(pack: PackRules, name: string, source: string) {
178
+ if (pack.droppedSuffixes.some(suffix => name.endsWith(suffix))) return false
179
+ if (!pack.compiled.has(path.resolve(source)) || withinKeepPath(pack, source)) return true
180
+ return [...pack.keep].some(
181
+ entry => name === entry || (entry.startsWith('.') && name.endsWith(entry)),
182
+ )
183
+ }
184
+
185
+ /**
186
+ * App sources a release compiled into its output: the closure of `build-index.json`'s modules over the
187
+ * compile graph, plus the proxy, instrumentation and config it froze. Anything else may be read at
188
+ * request time and ships.
189
+ */
190
+ function releaseCompiledSources(config: ResolvedConfig) {
191
+ flushDevModuleCaches()
192
+ const index = readBuildIndex(config.outPath)
193
+ const graph = compiledSourceGraph(config.outPath, config.workspaceRoot)
194
+ const queue = Object.keys(index.modules).map(key =>
195
+ releaseSourceFile(
196
+ key.slice(key.indexOf(':', key.indexOf(':') + 1) + 1),
197
+ config.root,
198
+ config.workspaceRoot,
199
+ ),
200
+ )
201
+ if (index.proxy) queue.push(path.resolve(config.root, index.proxy.file))
202
+ const script = /\.(?:[cm]?[jt]s|[jt]sx)$/
203
+ const frozen = (dir: string, pattern: RegExp) =>
204
+ (readDirEntries(dir) ?? [])
205
+ .filter(entry => pattern.test(entry.name) && script.test(entry.name))
206
+ .forEach(entry => queue.push(path.join(dir, entry.name)))
207
+ frozen(config.root, /^(?:pnext|next)\.config\./)
208
+ if (index.instrumentation) {
209
+ for (const dir of [config.root, path.join(config.root, 'src')])
210
+ frozen(dir, /^instrumentation\./)
211
+ }
212
+ const compiled = new Set<string>()
213
+ for (const file of queue) {
214
+ if (compiled.has(file)) continue
215
+ compiled.add(file)
216
+ let real = file
217
+ try {
218
+ real = realpathSync(file)
219
+ } catch {
220
+ continue
221
+ }
222
+ compiled.add(real)
223
+ queue.push(...(graph.get(file) ?? []), ...(graph.get(real) ?? []))
224
+ }
225
+ return compiled
148
226
  }
149
227
 
150
228
  interface VercelConfig {
@@ -156,6 +234,7 @@ interface VercelConfig {
156
234
  methods?: string[]
157
235
  headers?: Record<string, string>
158
236
  continue?: boolean
237
+ important?: boolean
159
238
  }
160
239
  | { handle: 'filesystem' }
161
240
  )[]
@@ -184,7 +263,10 @@ export async function writeVercelOutput(
184
263
  const warmedModulesEarly = new Promise<string[]>(resolve => {
185
264
  resolveWarmedModules = resolve
186
265
  })
187
- const warmSettled = warm.finish(log, modules => resolveWarmedModules(modules))
266
+ const warmSettled = warm.finish(log, modules => resolveWarmedModules(modules), {
267
+ globalCss: globalCssSources(config),
268
+ clientReferences: [...compiledClientReferenceFiles()],
269
+ })
188
270
  // Nothing awaits it until writeServerFunction's copy step; park the
189
271
  // rejection so it never surfaces as unhandled in between (it never actually
190
272
  // rejects — warmWithRestarts catches everything itself — but the gap
@@ -213,7 +295,7 @@ export async function writeVercelOutput(
213
295
  // server function: served, but at function cost with no edge cache.
214
296
  ...compatStaticRewrite(config, outputPath),
215
297
  // Every content-hashed build asset, exactly the set `pnext start` serves immutable.
216
- await immutableAssetRoute(),
298
+ ...(await immutableAssetRoutes(path.join(config.outPath, 'public'), manifest.publicAssets)),
217
299
  // Proxy-matched paths go to the server function before the CDN filesystem
218
300
  // check — `pnext start` runs the proxy ahead of static files too.
219
301
  ...(await proxyRoutes(config)),
@@ -281,7 +363,8 @@ async function proxyRoutes(config: ResolvedConfig) {
281
363
  const proxyFile = findProxyFile(config)
282
364
  if (!proxyFile) return []
283
365
  const proxyModule = (await import(pathToFileHref(proxyFile))) as ProxyModule
284
- return proxyRoutePatterns(proxyModule.config).map(src => ({
366
+ // Percent-encoded paths and repeated slashes go to the function, which decodes or 308s them.
367
+ return [...proxyRoutePatterns(proxyModule.config), '^.*(?:%|//).*$'].map(src => ({
285
368
  src,
286
369
  dest: `/${SERVER_FUNCTION}`,
287
370
  }))
@@ -305,14 +388,18 @@ async function writeServerFunction(
305
388
  const pack = packRules(config)
306
389
  const framework = await resolveFrameworkPackage(workspaceRoot)
307
390
  const { root: pnextRoot, inWorkspace: pnextInWorkspace, targetRel: pnextTargetRel } = framework
391
+ const release = releaseEntries(config, manifest)
392
+ // A release served by the framework's prebundle needs none of its other source.
393
+ const prebundle = release ? readPrebundleStamp(pnextRoot, frameworkFingerprint()) : undefined
308
394
  const closure = await traceNodeModulesClosure(
309
395
  config,
310
396
  manifest,
311
397
  functionPath,
312
398
  framework,
313
- warmedModules,
399
+ [...warmedModules, ...(release ?? [])],
314
400
  pack,
315
401
  log,
402
+ Boolean(release),
316
403
  )
317
404
 
318
405
  const replicaPathFor = (file: string) => {
@@ -338,17 +425,16 @@ async function writeServerFunction(
338
425
  for (const relative of closure.workspacePackages) {
339
426
  neededRoots.add(path.resolve(workspaceRoot, relative))
340
427
  }
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)
428
+ // Without a release the server compiles app source at request time: the renderer re-resolves the
429
+ // root layout's global css imports, and a compat compile resolves the shipped sources' imports for
430
+ // real, so the packages behind them and the declared workspace dependencies must be present.
431
+ if (!release) {
432
+ for (const file of globalCssSources(config)) {
433
+ const root = packageRoots.find(candidate => isInsideDir(candidate, file))
434
+ if (root) neededRoots.add(root)
435
+ }
436
+ addWorkspaceDependencies(neededRoots, packageRoots, [path.resolve(config.root), ...neededRoots])
346
437
  }
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
438
  neededRoots.delete(path.resolve(config.root))
353
439
  neededRoots.delete(pnextRoot)
354
440
 
@@ -364,16 +450,22 @@ async function writeServerFunction(
364
450
  await log.step(`copy function tree (${closure.packageCount} packages, ${roots} roots)`, () =>
365
451
  Promise.all([
366
452
  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),
453
+ // A release ships the app tree minus what it compiled: request code may read files beside
454
+ // its source. `public` is served by the CDN, never from the function; the rest are named
455
+ // again because the app root is the one tree whose build output ships, so it
456
+ // cannot rely on the depth-wise prune alone.
457
+ copyTree(
458
+ config.root,
459
+ path.join(functionPath, appRel),
460
+ release ? { ...pack, compiled: releaseCompiledSources(config) } : pack,
461
+ ['public', '.vercel', '.next', '.turbo'],
462
+ ),
463
+ copyPackageTree(
464
+ pnextRoot,
465
+ path.join(functionPath, pnextTargetRel),
466
+ pack,
467
+ prebundle && ['config', 'dist', ...prebundle.source],
468
+ ),
377
469
  ...[...neededRoots].map(packageRoot =>
378
470
  copyPackageTree(
379
471
  packageRoot,
@@ -407,7 +499,9 @@ async function writeServerFunction(
407
499
  injectCompatPaths(config, functionPath, pnextRoot, replicaPathFor),
408
500
  )
409
501
 
410
- const startEntry = toPosixPath(path.join(pnextTargetRel, 'src/cli/start.ts'))
502
+ const startEntry = toPosixPath(
503
+ path.join(pnextTargetRel, prebundle ? 'dist/server/cli/start.js' : 'src/cli/start.ts'),
504
+ )
411
505
  await writeText(
412
506
  path.join(functionPath, 'index.mjs'),
413
507
  `import path from 'node:path';
@@ -544,7 +638,7 @@ async function copyTree(
544
638
  visited.add(link.path)
545
639
  return copyTree(link.path, target, pack, [], visited)
546
640
  }
547
- return shipsFile(pack, entry.name) ? linkOrCopyFile(link.path, target) : undefined
641
+ return shipsFile(pack, entry.name, source) ? linkOrCopyFile(link.path, target) : undefined
548
642
  }
549
643
  // One syscall for the whole subtree beats one per file — the excluded
550
644
  // names are stripped from the copy afterwards.
@@ -552,7 +646,8 @@ async function copyTree(
552
646
  return normalizeClone(target, source, pack, visited)
553
647
  }
554
648
  if (entry.isDirectory()) return copyTree(source, target, pack, [], visited)
555
- if (entry.isFile() && shipsFile(pack, entry.name)) return linkOrCopyFile(source, target)
649
+ if (entry.isFile() && shipsFile(pack, entry.name, source))
650
+ return linkOrCopyFile(source, target)
556
651
  }),
557
652
  )
558
653
  }
@@ -631,19 +726,20 @@ async function normalizeClone(
631
726
  if (!shipsDir(pack, entry.name, source, entry.isSymbolicLink())) {
632
727
  rmSync(full, { recursive: true, force: true })
633
728
  } else if (entry.isSymbolicLink()) {
634
- const link = resolveLinkTarget(full)
729
+ // From the source: a relative link in the clone would resolve against the function tree.
730
+ const link = resolveLinkTarget(source)
635
731
  rmSync(full, { force: true })
636
732
  if (!link) continue
637
733
  if (link.isDirectory) {
638
734
  if (visited.has(link.path)) continue
639
735
  visited.add(link.path)
640
736
  pending.push(copyTree(link.path, full, pack, [], visited))
641
- } else if (shipsFile(pack, entry.name)) {
737
+ } else if (shipsFile(pack, entry.name, source)) {
642
738
  pending.push(linkOrCopyFile(link.path, full))
643
739
  }
644
740
  } else if (entry.isDirectory()) {
645
741
  stack.push([full, source])
646
- } else if (!shipsFile(pack, entry.name)) {
742
+ } else if (!shipsFile(pack, entry.name, source)) {
647
743
  rmSync(full, { force: true })
648
744
  }
649
745
  }
@@ -703,10 +799,10 @@ const packageCopyExcludes = [
703
799
  * Copies a workspace package the way npm would publish it: a `files` list is the package's own
704
800
  * statement of what it ships, so it beats guessing at build-time directory names. Glob entries fall
705
801
  * back to the exclude list - matching npm's glob semantics is not worth it for a packaging copy.
802
+ * `only` names a narrower list.
706
803
  */
707
- async function copyPackageTree(from: string, to: string, pack: PackRules) {
708
- const packageJson = await readPackageJson(from)
709
- const files = packageJson?.files
804
+ async function copyPackageTree(from: string, to: string, pack: PackRules, only?: string[]) {
805
+ const files = only ?? (await readPackageJson(from))?.files
710
806
  if (!files?.length || files.some(entry => /[*?[\]{}!]/.test(entry))) {
711
807
  return copyTree(from, to, pack, packageCopyExcludes)
712
808
  }
@@ -721,7 +817,8 @@ async function copyPackageTree(from: string, to: string, pack: PackRules) {
721
817
  const stats = statSync(source, { throwIfNoEntry: false })
722
818
  if (!stats) return
723
819
  if (stats.isDirectory()) return copyTree(source, path.join(to, name), pack)
724
- if (!shipsFile(pack, name)) return
820
+ if (!shipsFile(pack, name, source)) return
821
+ await mkdir(path.dirname(path.join(to, name)), { recursive: true })
725
822
  return linkOrCopyFile(source, path.join(to, name))
726
823
  }),
727
824
  )
@@ -958,10 +1055,15 @@ async function traceNodeModulesClosure(
958
1055
  warmedModules: string[],
959
1056
  pack: PackRules,
960
1057
  log: VerboseLogger,
1058
+ // A release imports only build output, so its closure is traced from that alone.
1059
+ release = false,
961
1060
  ) {
962
1061
  const workspaceRoot = config.workspaceRoot
963
1062
  const specifiers = new Set<string>()
964
1063
  const tracedFiles = new Set<string>()
1064
+ // Package directories compiled modules import by absolute href (natively loaded server
1065
+ // dependencies, externals). One outside the top-level node_modules ships where it is.
1066
+ const hrefPackageDirs = new Set<string>()
965
1067
  // Compat-aliased specifiers whose target lives in the framework source
966
1068
  // (react, next/*) never resolve from node_modules at runtime — shipping
967
1069
  // them would drag in the full next/react trees. Aliases that point into
@@ -972,7 +1074,8 @@ async function traceNodeModulesClosure(
972
1074
  await log.step('trace runtime imports', async () => {
973
1075
  // Only code Bun imports raw at request time needs real node_modules: route handlers, the proxy,
974
1076
  // 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.
1077
+ // compiled cache; what it does not vendor (server dependencies Bun loads natively, externals) it
1078
+ // imports by absolute href, and those packages ship with their dependency closure below.
976
1079
  const queue: string[] = []
977
1080
  const seen = tracedFiles
978
1081
  const enqueue = (file: string | undefined) => {
@@ -983,12 +1086,14 @@ async function traceNodeModulesClosure(
983
1086
  queue.push(resolved)
984
1087
  }
985
1088
 
986
- for (const route of manifest.routes) {
987
- if (route.kind === 'handler') enqueue(route.file)
1089
+ if (!release) {
1090
+ for (const route of manifest.routes) {
1091
+ if (route.kind === 'handler') enqueue(route.file)
1092
+ }
1093
+ enqueue(findProxyFile(config) ?? undefined)
1094
+ enqueue(path.join(config.root, 'pnext.config.ts'))
1095
+ enqueue(path.join(config.root, 'next.config.js'))
988
1096
  }
989
- enqueue(findProxyFile(config) ?? undefined)
990
- enqueue(path.join(config.root, 'pnext.config.ts'))
991
- enqueue(path.join(config.root, 'next.config.js'))
992
1097
  for (const file of warmedModules) enqueue(file)
993
1098
 
994
1099
  const visit = async (file: string) => {
@@ -1001,10 +1106,14 @@ async function traceNodeModulesClosure(
1001
1106
  // external — by absolute href. Follow the ones inside the build output
1002
1107
  // and ship the packages the rest point into.
1003
1108
  const absolute = absolutePathFromSpecifier(specifier)
1109
+ // The framework ships whole at its own location, where baked paths are rewritten to.
1110
+ if (absolute && isInsideDir(framework.root, absolute)) continue
1004
1111
  if (absolute) {
1005
1112
  const name = packageNameFromPath(absolute)
1006
- if (name) specifiers.add(name)
1007
- else if (isInsideDir(config.outPath, absolute)) enqueue(absolute)
1113
+ if (name) {
1114
+ specifiers.add(name)
1115
+ hrefPackageDirs.add(packageDirFromPath(absolute, name))
1116
+ } else if (isInsideDir(config.outPath, absolute)) enqueue(absolute)
1008
1117
  continue
1009
1118
  }
1010
1119
  if (/^(?:node:|bun$|bun:|data:)/.test(specifier)) continue
@@ -1020,6 +1129,12 @@ async function traceNodeModulesClosure(
1020
1129
  ) {
1021
1130
  enqueue(resolved)
1022
1131
  }
1132
+ // A release reaches an installed package it cannot name bare by a relative path.
1133
+ const installed = specifier.startsWith('.') && resolved && packageNameFromPath(resolved)
1134
+ if (installed) {
1135
+ specifiers.add(installed)
1136
+ hrefPackageDirs.add(packageDirFromPath(resolved, installed))
1137
+ }
1023
1138
  const name = packageNameFromSpecifier(specifier)
1024
1139
  if (name) specifiers.add(name)
1025
1140
  }
@@ -1037,6 +1152,9 @@ async function traceNodeModulesClosure(
1037
1152
  })
1038
1153
 
1039
1154
  const packageDirs = new Map<string, string>()
1155
+ // A dependency version nested under the package that needs it (a version conflict): it ships
1156
+ // under that package's copy, where Bun resolves it from at runtime, not over the hoisted one.
1157
+ const nestedCopies = new Map<string, string>()
1040
1158
  const workspaceLinks = new Map<string, string>()
1041
1159
  // Native bindings resolved here are built for the build host; the function
1042
1160
  // needs the ones for its own platform, fetched below.
@@ -1046,8 +1164,13 @@ async function traceNodeModulesClosure(
1046
1164
  // registry. Its optional deps back compat features that only load when the app uses them, so they
1047
1165
  // ship only where the app declares them too.
1048
1166
  const appDependencies = await declaredDependencies(config.root)
1167
+ // A release never compiles, so only the dependencies its runtime entries import ship - not the
1168
+ // compilers the framework also depends on for build and dev.
1169
+ const runtimeDependencies = release ? await frameworkRuntimePackages(framework.root) : undefined
1049
1170
  const queue: { name: string; from: string; owner?: PackageVersion }[] = [
1050
- ...framework.dependencies.map(name => ({ name, from: framework.root })),
1171
+ ...framework.dependencies
1172
+ .filter(name => !runtimeDependencies || runtimeDependencies.has(name))
1173
+ .map(name => ({ name, from: framework.root })),
1051
1174
  ...framework.optionalDependencies
1052
1175
  .filter(name => appDependencies.has(name))
1053
1176
  .map(name => ({ name, from: framework.root })),
@@ -1087,16 +1210,34 @@ async function traceNodeModulesClosure(
1087
1210
  }
1088
1211
  packageDirs.set(name, dir)
1089
1212
  if (!packageJson) continue
1090
- const self = { name, version: packageJson.version ?? '' }
1091
1213
  // Peer dependencies are deliberately not expanded — they fan out to whole
1092
1214
  // ecosystems (react, next, ...) that are only shipped when something
1093
1215
  // 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 })
1216
+ const expand = async (
1217
+ packageDir: string,
1218
+ packageJson: Awaited<ReturnType<typeof readPackageJson>>,
1219
+ target: string,
1220
+ ) => {
1221
+ const self = { name: packageJson?.name ?? name, version: packageJson?.version ?? '' }
1222
+ for (const dependency of [
1223
+ ...Object.keys(packageJson?.dependencies ?? {}),
1224
+ ...Object.keys(packageJson?.optionalDependencies ?? {}),
1225
+ ]) {
1226
+ const nested = path.join(packageDir, 'node_modules', dependency)
1227
+ const nestedJson = existsSync(path.join(nested, 'package.json'))
1228
+ ? await readPackageJson(nested)
1229
+ : undefined
1230
+ if (!nestedJson || nestedJson.os || nestedJson.cpu) {
1231
+ queue.push({ name: dependency, from: packageDir, owner: self })
1232
+ continue
1233
+ }
1234
+ const nestedTarget = path.join(target, 'node_modules', dependency)
1235
+ if (nestedCopies.has(nestedTarget)) continue
1236
+ nestedCopies.set(nestedTarget, nested)
1237
+ await expand(nested, nestedJson, nestedTarget)
1238
+ }
1099
1239
  }
1240
+ await expand(dir, packageJson, path.join('node_modules', name))
1100
1241
  }
1101
1242
 
1102
1243
  if (hostNatives.size > 0 && !skipDependencyClosure()) {
@@ -1119,6 +1260,19 @@ async function traceNodeModulesClosure(
1119
1260
  }),
1120
1261
  )
1121
1262
  }
1263
+ for (const [target, dir] of skipDependencyClosure() ? [] : nestedCopies) {
1264
+ await mkdir(path.dirname(path.join(functionPath, target)), { recursive: true })
1265
+ await copyTree(dir, path.join(functionPath, target), pack)
1266
+ }
1267
+ // Baked paths map the workspace root onto the function root, so an href into a nested or
1268
+ // app-level node_modules needs that exact directory there.
1269
+ for (const dir of skipDependencyClosure() ? [] : hrefPackageDirs) {
1270
+ const relative = path.relative(workspaceRoot, dir)
1271
+ const target = path.join(functionPath, relative)
1272
+ if (relative.startsWith('..') || existsSync(target)) continue
1273
+ await mkdir(path.dirname(target), { recursive: true })
1274
+ await copyTree(dir, target, pack)
1275
+ }
1122
1276
  for (const [name, workspaceRelative] of workspaceLinks) {
1123
1277
  const linkPath = path.join(functionPath, 'node_modules', name)
1124
1278
  await mkdir(path.dirname(linkPath), { recursive: true })
@@ -1143,6 +1297,80 @@ interface PackageVersion {
1143
1297
  version: string
1144
1298
  }
1145
1299
 
1300
+ /** Every compiled entry `<out>/server/build-index.json` names, when the build emitted one. */
1301
+ function releaseEntries(config: ResolvedConfig, manifest: BuildManifest): string[] | undefined {
1302
+ if (!existsSync(buildIndexFile(config.outPath))) return undefined
1303
+ const index = readBuildIndex(config.outPath)
1304
+ return [
1305
+ ...Object.values(index.modules),
1306
+ index.nextConfig,
1307
+ index.instrumentation?.file,
1308
+ index.instrumentation?.edge,
1309
+ ...Object.values(index.compat?.cacheHandlers ?? {}),
1310
+ manifest.proxyModule,
1311
+ ]
1312
+ .filter(artifact => artifact !== undefined)
1313
+ .map(artifact => path.resolve(config.outPath, artifact))
1314
+ }
1315
+
1316
+ /**
1317
+ * Packages the framework's production entries import, statically or dynamically. Lazy `require`s
1318
+ * are the compiler facades (esbuild, oxc), which only build and dev reach.
1319
+ */
1320
+ async function frameworkRuntimePackages(frameworkRoot: string) {
1321
+ const src = path.join(frameworkRoot, 'src')
1322
+ const scanners = {
1323
+ ts: new Bun.Transpiler({ loader: 'ts' }),
1324
+ tsx: new Bun.Transpiler({ loader: 'tsx' }),
1325
+ }
1326
+ const queue = [
1327
+ ...runtimeEntryFiles.map(file => path.join(src, file)),
1328
+ ...runtimeEntryDirs.flatMap(dir =>
1329
+ listSourceFiles(path.join(src, dir)).filter(file => /(?<!\.d)\.tsx?$/.test(file)),
1330
+ ),
1331
+ ]
1332
+ const seen = new Set(queue)
1333
+ const packages = new Set<string>()
1334
+ for (const file of queue) {
1335
+ const scanner = file.endsWith('.tsx') || file.endsWith('.jsx') ? scanners.tsx : scanners.ts
1336
+ for (const edge of scanner.scanImports(await readFile(file, 'utf8'))) {
1337
+ if (edge.kind !== 'import-statement' && edge.kind !== 'dynamic-import') continue
1338
+ if (!edge.path.startsWith('.')) {
1339
+ const name = packageNameFromSpecifier(edge.path)
1340
+ if (name) packages.add(name)
1341
+ continue
1342
+ }
1343
+ const target = resolveFrom(edge.path, path.dirname(file))
1344
+ if (
1345
+ target &&
1346
+ !seen.has(target) &&
1347
+ isInsideDir(src, target) &&
1348
+ scriptFilePattern.test(target)
1349
+ ) {
1350
+ seen.add(target)
1351
+ queue.push(target)
1352
+ }
1353
+ }
1354
+ }
1355
+ return packages
1356
+ }
1357
+
1358
+ function resolveFrom(specifier: string, dir: string) {
1359
+ try {
1360
+ return Bun.resolveSync(specifier, dir)
1361
+ } catch {
1362
+ return undefined
1363
+ }
1364
+ }
1365
+
1366
+ function listSourceFiles(dir: string): string[] {
1367
+ return readdirSync(dir, { withFileTypes: true }).flatMap(entry =>
1368
+ entry.isDirectory()
1369
+ ? listSourceFiles(path.join(dir, entry.name))
1370
+ : [path.join(dir, entry.name)],
1371
+ )
1372
+ }
1373
+
1146
1374
  /**
1147
1375
  * Native bindings for the function's platform, not the build host's. The host install only ever
1148
1376
  * carries its own platform's optional deps, so the right builds have to come from the registry: one
@@ -1297,6 +1525,13 @@ function absolutePathFromSpecifier(specifier: string) {
1297
1525
  return path.isAbsolute(specifier) ? specifier : undefined
1298
1526
  }
1299
1527
 
1528
+ /** The `<...>/node_modules/<name>` directory holding `file`. */
1529
+ function packageDirFromPath(file: string, name: string) {
1530
+ const posix = toPosixPath(file)
1531
+ const marker = `/node_modules/${name}/`
1532
+ return posix.slice(0, posix.lastIndexOf(marker) + marker.length - 1)
1533
+ }
1534
+
1300
1535
  /** The package an absolute path belongs to, if it points inside node_modules. */
1301
1536
  function packageNameFromPath(file: string) {
1302
1537
  const parts = toPosixPath(file).split('/node_modules/')
@@ -1379,14 +1614,50 @@ async function staticOverrides(
1379
1614
  return overrides
1380
1615
  }
1381
1616
 
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('|')})`,
1617
+ // `important` lets route headers win over the CDN's own static-file cache-control, as Next's builder does.
1618
+ async function immutableAssetRoutes(publicDir: string, publicAssets: string[] = []) {
1619
+ const app = new Set(publicAssets)
1620
+ const built =
1621
+ publicAssets.length > 0
1622
+ ? (await listFiles(publicDir))
1623
+ .map(file => toPosixPath(path.relative(publicDir, file)))
1624
+ .filter(relative => !app.has(relative) && immutableAssetPath(relative))
1625
+ : []
1626
+ const patterns = immutableAssetPatterns(built, publicAssets)
1627
+ return routeSources(patterns, source => `^/(?:${source})`).map(src => ({
1628
+ src,
1387
1629
  headers: { 'cache-control': immutableCacheControl },
1388
1630
  continue: true,
1631
+ important: true,
1632
+ }))
1633
+ }
1634
+
1635
+ /**
1636
+ * Whole immutable prefixes that hold no app public file; under the others, each build file collapsed
1637
+ * to its topmost folder below the prefix that holds no app public file, else its exact name.
1638
+ */
1639
+ function immutableAssetPatterns(built: string[], publicAssets: string[]) {
1640
+ const appDirs = new Set(publicAssets.flatMap(ancestorDirs))
1641
+ const prefixes = [...new Set(immutableAssetPrefixes())]
1642
+ const patterns = prefixes.filter(prefix => !appDirs.has(prefix)).map(escapeRegex)
1643
+ for (const relative of built) {
1644
+ const prefix = prefixes.find(prefix => relative.startsWith(prefix))
1645
+ if (!prefix || !appDirs.has(prefix)) continue
1646
+ const dir = ancestorDirs(relative).find(dir => dir.length > prefix.length && !appDirs.has(dir))
1647
+ patterns.push(dir ? escapeRegex(requestPath(dir)) : `${escapeRegex(requestPath(relative))}$`)
1389
1648
  }
1649
+ return [...new Set(patterns)]
1650
+ }
1651
+
1652
+ /** A public-relative path as browsers request it (the WHATWG path percent-encode set). */
1653
+ function requestPath(relative: string) {
1654
+ return relative.replace(/[\0-\x20"#<>?`{}\x7f-\u{10ffff}]/gu, char => encodeURIComponent(char))
1655
+ }
1656
+
1657
+ /** `a/b/c.js` -> `['a/', 'a/b/']`. */
1658
+ function ancestorDirs(relative: string) {
1659
+ const parts = relative.split('/').slice(0, -1)
1660
+ return parts.map((_, index) => `${parts.slice(0, index + 1).join('/')}/`)
1390
1661
  }
1391
1662
 
1392
1663
  // Headers a CDN route cannot replay; a file that sets one stays on the server function.
@@ -1434,23 +1705,29 @@ function staticHeaderRoutes(staticFiles: Record<string, StaticFileMetadata>) {
1434
1705
  groups.set(key, group)
1435
1706
  group.patterns.push(servedPathPattern(relative))
1436
1707
  }
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})$`,
1708
+ return [...groups.values()].flatMap(({ headers, patterns }) =>
1709
+ routeSources(patterns, source => `^/(?:${source})$`).map(src => ({
1710
+ src,
1449
1711
  methods: ['GET', 'HEAD'],
1450
1712
  headers,
1451
1713
  continue: true,
1452
- }))
1453
- })
1714
+ important: true,
1715
+ })),
1716
+ )
1717
+ }
1718
+
1719
+ /** Joins patterns into as few `wrap`ped route sources as fit `maxRouteSourceLength`. */
1720
+ function routeSources(patterns: string[], wrap: (alternation: string) => string) {
1721
+ const sources: string[] = []
1722
+ for (const pattern of patterns) {
1723
+ const last = sources.length - 1
1724
+ if (last >= 0 && wrap(`${sources[last]}|${pattern}`).length <= maxRouteSourceLength) {
1725
+ sources[last] += `|${pattern}`
1726
+ } else {
1727
+ sources.push(pattern)
1728
+ }
1729
+ }
1730
+ return sources.map(wrap)
1454
1731
  }
1455
1732
 
1456
1733
  /** A static file's request paths: `a/index.html` also answers `/a` and `/a/`. */