@wular/pnext 0.1.7 → 0.1.9

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 (172) hide show
  1. package/dist/server/_chunks/{aliases-j47a10ce.js → aliases-fhv4dt06.js} +7 -5
  2. package/dist/server/_chunks/{analyze-arswqe6s.js → analyze-09kx9q6r.js} +17 -13
  3. package/dist/server/_chunks/{boot-af0mv3dy.js → boot-8aj7efqk.js} +13 -11
  4. package/dist/server/_chunks/{build-spy3dxp4.js → build-jda155c9.js} +7 -5
  5. package/dist/server/_chunks/{build-tier-m7yvqz61.js → build-tier-qrkpfsk1.js} +59 -75
  6. package/dist/server/_chunks/{build-t9h0hec3.js → build-wspv5h60.js} +1004 -1191
  7. package/dist/server/_chunks/{compile-53j02j27.js → compile-040r79gj.js} +2 -2
  8. package/dist/server/_chunks/{config-n2z7yxps.js → config-7h84p6q8.js} +6 -4
  9. package/dist/server/_chunks/{config-1yzwexq6.js → config-d4hqyygy.js} +12 -3
  10. package/dist/server/_chunks/{create-meec9hs6.js → create-skj8wzt5.js} +4 -3
  11. package/dist/server/_chunks/{dev-fjmqg6en.js → dev-pennd36n.js} +2 -2
  12. package/dist/server/_chunks/{generate-n9sh6hzt.js → generate-d8vb1d21.js} +5 -4
  13. package/dist/server/_chunks/{index-eeaanp2e.js → index-0qt2vapb.js} +3 -3
  14. package/dist/server/_chunks/{index-91x12ykq.js → index-0r5eqfc7.js} +6 -1
  15. package/dist/server/_chunks/{index-9ych9ma8.js → index-0wm2p3ks.js} +2 -2
  16. package/dist/server/_chunks/{index-357na4a8.js → index-141vcwk6.js} +1 -1
  17. package/dist/server/_chunks/{index-2dv4y4wd.js → index-1y0cesgn.js} +16 -10
  18. package/dist/server/_chunks/{index-ytjvpbm2.js → index-26gwrm07.js} +3 -3
  19. package/dist/server/_chunks/{index-8k7pfejn.js → index-3r6qpe8a.js} +7 -7
  20. package/dist/server/_chunks/{index-rn8dyr1f.js → index-42fef2rv.js} +8 -48
  21. package/dist/server/_chunks/{index-ncnes8c6.js → index-47gx7trn.js} +2 -2
  22. package/dist/server/_chunks/{index-20ppawv7.js → index-4qsce64w.js} +3 -3
  23. package/dist/server/_chunks/{index-1011cscr.js → index-4vf121ws.js} +61 -29
  24. package/dist/server/_chunks/{index-cf4rvxv6.js → index-52k54ykj.js} +1 -1
  25. package/dist/server/_chunks/{index-nyrba2kr.js → index-53fnn598.js} +11 -11
  26. package/dist/server/_chunks/{index-4hgjrqh7.js → index-6tb725m8.js} +2 -2
  27. package/dist/server/_chunks/{index-3xbr4eyv.js → index-912z4mfx.js} +3 -3
  28. package/dist/server/_chunks/{index-sjpckxqn.js → index-9f49w76j.js} +1 -1
  29. package/dist/server/_chunks/{index-jepjvcrf.js → index-aarf1brn.js} +1 -1
  30. package/dist/server/_chunks/{index-9n2914ww.js → index-akkrvjn6.js} +1 -1
  31. package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
  32. package/dist/server/_chunks/{index-sv8m6789.js → index-cqcwwxfp.js} +5 -5
  33. package/dist/server/_chunks/{index-yz54pxrh.js → index-d1jsmhhk.js} +4 -9
  34. package/dist/server/_chunks/{index-vvmmknm1.js → index-da0fphj0.js} +19 -15
  35. package/dist/server/_chunks/{index-sctcgyvv.js → index-drwv61cc.js} +2 -2
  36. package/dist/server/_chunks/{index-z84ab1w0.js → index-e176wk6x.js} +3 -3
  37. package/dist/server/_chunks/{index-9hx2641q.js → index-entm8bmz.js} +1 -1
  38. package/dist/server/_chunks/index-fpghsgb4.js +352 -0
  39. package/dist/server/_chunks/{index-jx7bqvw1.js → index-hr63fvzn.js} +1 -1
  40. package/dist/server/_chunks/{index-eajaspjk.js → index-j0m1jxqa.js} +1 -1
  41. package/dist/server/_chunks/index-jbj7ehef.js +50 -0
  42. package/dist/server/_chunks/{index-egykcqg3.js → index-jexsy9a8.js} +1 -1
  43. package/dist/server/_chunks/{index-fkp000p1.js → index-jjppzw0s.js} +2 -2
  44. package/dist/server/_chunks/index-jt8e1pje.js +12 -0
  45. package/dist/server/_chunks/{index-ggk2j965.js → index-kpdph69z.js} +56 -50
  46. package/dist/server/_chunks/{index-6e4fqv30.js → index-m63czgq5.js} +2 -0
  47. package/dist/server/_chunks/{index-c0g2wf3z.js → index-md5jchzy.js} +1 -1
  48. package/dist/server/_chunks/{index-tvqkq6e6.js → index-n814hcky.js} +1 -1
  49. package/dist/server/_chunks/{index-06nyqntx.js → index-q3r9z8mq.js} +1 -1
  50. package/dist/server/_chunks/{index-4q2gbfwk.js → index-qjrh7w2g.js} +2 -2
  51. package/dist/server/_chunks/{index-3b3at80h.js → index-s61bx2aj.js} +6 -3
  52. package/dist/server/_chunks/{index-ym67zqxp.js → index-tf4j19ge.js} +1 -1
  53. package/dist/server/_chunks/{index-qn4z5s3c.js → index-typcbcsv.js} +3 -3
  54. package/dist/server/_chunks/{index-p77b4kcx.js → index-veycp46e.js} +1 -1
  55. package/dist/server/_chunks/{index-11sfk4hb.js → index-vmr35jjw.js} +1 -1
  56. package/dist/server/_chunks/{index-kq8kpzye.js → index-vqs3rebe.js} +3 -3
  57. package/dist/server/_chunks/{index-danncfhy.js → index-wm36rj4v.js} +1 -1
  58. package/dist/server/_chunks/{index-z4z9zv3b.js → index-x9c55pzy.js} +141 -3
  59. package/dist/server/_chunks/{index-y2jr2n72.js → index-xrsg0041.js} +4 -4
  60. package/dist/server/_chunks/{index-k1g3epw0.js → index-xs3411qh.js} +10 -10
  61. package/dist/server/_chunks/{index-0374j2b9.js → index-y4d02znn.js} +1 -1
  62. package/dist/server/_chunks/{index-wpfhfbs4.js → index-y71a76dn.js} +4 -4
  63. package/dist/server/_chunks/{index-qm6kmhqs.js → index-y7mzvq2q.js} +1 -1
  64. package/dist/server/_chunks/{index-hkdfj58q.js → index-y9bygr95.js} +1 -1
  65. package/dist/server/_chunks/{index-3tf1xjsp.js → index-yghskk8w.js} +11 -11
  66. package/dist/server/_chunks/{index-5gyr4emz.js → index-yzcpr3yw.js} +4 -4
  67. package/dist/server/_chunks/{index-x93hv0sc.js → index-zedjt25v.js} +23 -10
  68. package/dist/server/_chunks/{index-tq0pc7eg.js → index-zfrwq472.js} +3 -3
  69. package/dist/server/_chunks/{loader-7yw0j98z.js → loader-w1e0n5sm.js} +12 -10
  70. package/dist/server/_chunks/{modules-jn8bmbn8.js → modules-730j14cm.js} +14 -12
  71. package/dist/server/_chunks/{pipeline-yy9jhydf.js → pipeline-yjdnwbhm.js} +25 -21
  72. package/dist/server/_chunks/{production-jar9ztm3.js → production-1xe9b7wd.js} +14 -3
  73. package/dist/server/_chunks/{proxy-config-j84dgksn.js → proxy-config-9r9e0jvn.js} +7 -5
  74. package/dist/server/_chunks/{proxy-w1d0wmsn.js → proxy-pnevsbfd.js} +12 -10
  75. package/dist/server/_chunks/{router-k1reahfn.js → router-rzge0vz8.js} +10 -7
  76. package/dist/server/_chunks/{routes-2s277fc6.js → routes-0x8wjvcj.js} +12 -10
  77. package/dist/server/_chunks/{run-pvd8bs1t.js → run-tk90r3ac.js} +5 -4
  78. package/dist/server/_chunks/{server-mt4a994c.js → server-gev78ejc.js} +38 -34
  79. package/dist/server/_chunks/{tsconfig-defaults-1nb3308g.js → tsconfig-defaults-qe9wd6ne.js} +5 -4
  80. package/dist/server/_chunks/{typegen-0qr0459b.js → typegen-0yw8sry8.js} +15 -13
  81. package/dist/server/cli/index.js +12 -15
  82. package/dist/server/cli/start.js +35 -14
  83. package/dist/server/compat/next/cache.js +5 -5
  84. package/dist/server/compat/next/config-headers.js +8 -6
  85. package/dist/server/compat/next/config-loader.js +1 -1
  86. package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +5 -5
  87. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +5 -5
  88. package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +5 -5
  89. package/dist/server/compat/next/dynamic.js +3 -14
  90. package/dist/server/compat/next/font/cache.js +6 -4
  91. package/dist/server/compat/next/font/local.js +7 -5
  92. package/dist/server/compat/next/font/runtime.js +7 -5
  93. package/dist/server/compat/next/headers.js +4 -4
  94. package/dist/server/compat/next/image/config.js +2 -2
  95. package/dist/server/compat/next/image/optimizer.js +3 -3
  96. package/dist/server/compat/next/image/static-metadata.js +4 -4
  97. package/dist/server/compat/next/legacy-request-apis.js +3 -3
  98. package/dist/server/compat/next/link-transform.js +4 -3
  99. package/dist/server/compat/next/navigation.js +5 -4
  100. package/dist/server/compat/next/redirects.js +8 -6
  101. package/dist/server/compat/next/resource-hints.js +2 -2
  102. package/dist/server/compat/next/rewrites.js +7 -5
  103. package/dist/server/compat/next/root-params.js +3 -3
  104. package/dist/server/compat/next/route-state.js +2 -2
  105. package/dist/server/compat/next/script.js +2 -2
  106. package/dist/server/compat/next/server.js +5 -5
  107. package/dist/server/generation.json +1 -1
  108. package/package.json +1 -1
  109. package/reference/compat.md +1 -1
  110. package/reference/config.md +2 -2
  111. package/reference/dev.md +1 -1
  112. package/reference/getting-started.md +1 -1
  113. package/reference/rendering.md +4 -4
  114. package/src/api/dynamic.tsx +84 -4
  115. package/src/cli/adapters/vercel-warm.ts +40 -29
  116. package/src/cli/adapters/vercel.ts +101 -1265
  117. package/src/cli/analyze.ts +3 -2
  118. package/src/cli/build/cache.ts +6 -6
  119. package/src/cli/build.ts +33 -33
  120. package/src/cli/index.ts +4 -6
  121. package/src/cli/serve/pipeline.ts +16 -10
  122. package/src/cli/serve/standalone-worker.ts +8 -0
  123. package/src/cli/serve/standalone.ts +727 -0
  124. package/src/cli/start.ts +29 -8
  125. package/src/client/build.ts +1 -1
  126. package/src/client/reference-stub.ts +2 -34
  127. package/src/compat/actions/extensions.ts +3 -2
  128. package/src/compat/bundler/require-context.ts +2 -1
  129. package/src/compat/bundler/webpack-loaders.ts +3 -2
  130. package/src/compat/bundler/worker.ts +2 -2
  131. package/src/compat/css/inline-css.ts +1 -1
  132. package/src/compat/export/build-complete.ts +1 -1
  133. package/src/compat/export/emit.ts +4 -4
  134. package/src/compat/export/standalone.ts +30 -65
  135. package/src/compat/image-optimizer/optimize.ts +1 -1
  136. package/src/compat/lifecycle/instrumentation-client.ts +4 -3
  137. package/src/compat/metadata-artifacts.ts +1 -1
  138. package/src/compat/next/config-loader.ts +3 -0
  139. package/src/compat/next/dynamic.tsx +10 -23
  140. package/src/compat/next/font/cache.ts +2 -1
  141. package/src/compat/next/font/runtime.ts +1 -1
  142. package/src/compat/register/image.ts +1 -1
  143. package/src/compat/typecheck/check.ts +6 -4
  144. package/src/config.ts +28 -5
  145. package/src/css/assets.ts +1 -1
  146. package/src/css/build.ts +16 -9
  147. package/src/dev/client-chunk-store.ts +2 -1
  148. package/src/dev/restart/client-key.ts +2 -1
  149. package/src/dev/restart/global-css.ts +2 -1
  150. package/src/dev/restart/route-bundle-key.ts +2 -1
  151. package/src/dev/restart/route-facts.ts +2 -1
  152. package/src/dev/server.ts +8 -7
  153. package/src/out-paths.ts +10 -0
  154. package/src/render/renderer.ts +1 -1
  155. package/src/resolve/dynamic.ts +4 -0
  156. package/src/resolve/engine.ts +2 -12
  157. package/src/resolve/scan-facts.ts +185 -1
  158. package/src/routing/metadata-files.ts +1 -1
  159. package/src/runtime/loader.ts +5 -3
  160. package/src/runtime/module-cache.ts +2 -1
  161. package/src/runtime/modules.ts +50 -16
  162. package/src/runtime/prebundle.ts +23 -3
  163. package/src/runtime/production.ts +105 -40
  164. package/src/runtime/release-graph.ts +21 -13
  165. package/src/runtime/vendor-build.ts +45 -23
  166. package/src/types.ts +2 -3
  167. package/src/utils/directive.ts +40 -0
  168. package/src/utils/fs.ts +7 -2
  169. package/src/utils/resource-query.ts +12 -0
  170. package/dist/server/_chunks/index-tqn2qnnn.js +0 -241
  171. package/dist/server/_chunks/index-y36a0ybw.js +0 -57
  172. package/src/cli/serve/entry.ts +0 -52
@@ -1,38 +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, devOutSegment, pathToFileHref } from '../../config'
4
+ import { pathToFileHref } from '../../config'
7
5
  import type { ResolvedConfig } from '../../config'
8
- import { compiledClientReferenceFiles } from '../../client/reference'
9
- import { globalCssSources } from '../../css/build'
10
- import { getImportAliasExtensions } from '../../extensions'
11
6
  import { nextCompatEnabled } from '../../render/hooks'
12
- import { importSpecifiers } from '../../resolve/scan-facts'
13
- import { compiledSpecifiersManifestSuffix } from '../../runtime/modules'
14
- import { resolveImport, workspacePackageRoots } from '../../resolve/imports'
15
7
  import { findProxyFile, proxyRoutePatterns, type ProxyModule } from '../../routing/proxy'
16
8
  import { cacheRoot } from '../boot/named-bin'
17
- import { startWarmChild, type WarmChild } from './vercel-warm'
18
9
  import {
19
10
  immutableAssetPath,
20
11
  immutableAssetPrefixes,
21
12
  immutableCacheControl,
22
13
  } 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'
27
14
  import { escapeRegex } from '../../utils/code'
28
15
  import { listFiles, toPosixPath, writeText } from '../../utils/fs'
29
16
  import { createVerboseLogger, type VerboseLogger } from '../../utils/verbose'
30
17
  import type { BuildManifest, StaticFileMetadata } from '../../types'
31
18
 
32
- // Vercel unpacks a function's `.func` directory here at runtime. Baked
33
- // absolute paths in the build output are rewritten to this root.
34
- const RUNTIME_ROOT = '/var/task'
35
-
36
19
  // The single catch-all server function: the same `pnext start` pipeline,
37
20
  // running on Vercel's Bun runtime (`"bunVersion": "1.x"` in vercel.json).
38
21
  const SERVER_FUNCTION = '_pnext'
@@ -48,182 +31,9 @@ const FUNCTION_PLATFORM = {
48
31
  libc: 'glibc',
49
32
  } as const
50
33
 
51
- // Directories that only ever hold build-tool output, pruned at *any* depth. Depth matters: pruning
52
- // only a package's top level ships every nested one. node_modules and .git are here for the same
53
- // reason - the function resolves through its own traced node_modules, never a nested store.
54
- const prunedDirNames = [
55
- 'node_modules',
56
- '.git',
57
- '.next',
58
- '.pnext',
59
- '.turbo',
60
- '.cache',
61
- '.vercel',
62
- '.tmp',
63
- ]
64
-
65
- // Files nothing reads at request time. Sourcemaps only decorate stack traces,
66
- // and Bun strips types rather than resolving declarations — neither can change
67
- // what the function serves, and together they are a fifth of a typical closure.
68
- // Markdown is deliberately absent: an mdx app imports it as a module.
69
- const droppedFileSuffixes = [
70
- '.map',
71
- '.d.ts',
72
- '.d.mts',
73
- '.d.cts',
74
- '.DS_Store',
75
- // The compile-time specifier sidecars (see traceNodeModulesClosure below):
76
- // build-time metadata the trace consumes, never read at request time.
77
- compiledSpecifiersManifestSuffix,
78
- ]
79
-
80
- /** What the function tree ships, resolved once from config. */
81
- interface PackRules {
82
- prunedDirs: Set<string>
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>
88
- /** Absolute sources exempt from the prune list — the app's own build output. */
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>
92
- }
93
-
94
- // Test seam: the packing suite builds one app both ways and diffs the trees,
95
- // so it needs the unpruned closure on demand. Never off for a real deployment.
96
- let packPruningEnabled = true
97
-
98
- /** @internal Test-only. Returns a restore function. */
99
- export function setPackPruningEnabled(enabled: boolean) {
100
- const previous = packPruningEnabled
101
- packPruningEnabled = enabled
102
- return () => {
103
- packPruningEnabled = previous
104
- }
105
- }
106
-
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'))
116
- if (!packPruningEnabled) {
117
- return {
118
- // The two the function could never resolve through stay pruned even
119
- // here: a nested store or object database is not a packing choice.
120
- prunedDirs: new Set(['node_modules', '.git']),
121
- droppedSuffixes: [],
122
- compiled: new Set(),
123
- keep: new Set(),
124
- keepPaths: new Set([path.resolve(config.outPath)]),
125
- devPaths,
126
- }
127
- }
128
- const keep = new Set(config.adapter?.keep ?? [])
129
- const extra = config.adapter?.exclude ?? []
130
- const isSuffix = (entry: string) => entry.startsWith('.') && !prunedDirNames.includes(entry)
131
- return {
132
- prunedDirs: new Set(
133
- [...prunedDirNames, ...extra.filter(entry => !isSuffix(entry))].filter(
134
- name => !keep.has(name),
135
- ),
136
- ),
137
- droppedSuffixes: [...droppedFileSuffixes, ...extra.filter(isSuffix)].filter(
138
- suffix => !keep.has(suffix),
139
- ),
140
- compiled: new Set(),
141
- keep,
142
- // The app's outDir shares its name with the prune list; it holds the
143
- // compiled module cache the function serves from and must survive. The
144
- // default out root goes with it: a `distDir` app moved its output away, but
145
- // the next.config bundle the function imports at boot still lives there.
146
- keepPaths: new Set([path.resolve(config.outPath), path.resolve(config.root, DEFAULT_OUT_DIR)]),
147
- devPaths,
148
- }
149
- }
150
-
151
- /**
152
- * A keep path and everything under it. Prefix, not equality: the compiled module cache mirrors the
153
- * source layout, so a dependency's artifacts land in `<outDir>/cache/server/<profile>/node_modules/
154
- * ...`. That segment names a source tree, not a package store, and pruning it by name ships the
155
- * importer without the module it imports — the function then 500s on the first request that reaches
156
- * it. Only the app's own build output is exempt; a real `node_modules` beside it still prunes.
157
- */
158
- function withinKeepPath(pack: PackRules, source: string) {
159
- const resolved = path.resolve(source)
160
- for (const keep of pack.keepPaths) {
161
- if (resolved === keep || resolved.startsWith(`${keep}${path.sep}`)) return true
162
- }
163
- return false
164
- }
165
-
166
- /**
167
- * `isLink` is load-bearing, not a detail: the exemption covers real directories the build output
168
- * owns, never a link out of it. The proxy shim links `node_modules` into `.pnext`, and following
169
- * that would pull the whole store into the function.
170
- */
171
- function shipsDir(pack: PackRules, name: string, source: string, isLink = false) {
172
- if (pack.devPaths.has(path.resolve(source))) return false
173
- if (!pack.prunedDirs.has(name)) return true
174
- return !isLink && withinKeepPath(pack, source)
175
- }
176
-
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
226
- }
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/]
227
37
 
228
38
  interface VercelConfig {
229
39
  version: 3
@@ -241,52 +51,29 @@ interface VercelConfig {
241
51
  overrides?: Record<string, { path?: string; contentType?: string }>
242
52
  }
243
53
 
54
+ /** `.vercel/output`: the standalone dir as the server function, its `static/` as the CDN tree. */
244
55
  export async function writeVercelOutput(
245
56
  config: ResolvedConfig,
246
57
  manifest: BuildManifest,
247
- options: { verbose?: boolean; warm?: WarmChild } = {},
58
+ options: { verbose?: boolean } = {},
248
59
  ) {
249
60
  const log = createVerboseLogger(options.verbose ?? false, 'vercel')
250
61
  const outputPath = path.join(config.root, '.vercel', 'output')
62
+ const staticPath = path.join(config.outPath, 'static')
251
63
  const staticFiles = manifest.staticFiles ?? {}
252
- // Warming runs in its own process (see ./vercel-warm) and normally started
253
- // with the build; a caller that skipped that only loses the overlap.
254
- const warm = options.warm ?? startWarmChild(config)
255
64
  await log.step('prepare output directory', () => emptyDir(outputPath))
256
65
 
257
- // The compiled set is already final once the child reaches its handler
258
- // phase, so `onCompiled` resolves this ahead of full completion and
259
- // writeServerFunction starts tracing while the child's handler imports
260
- // still run. `finish` itself always fires `onCompiled` before it resolves,
261
- // so this promise never hangs on it.
262
- let resolveWarmedModules: (modules: string[]) => void
263
- const warmedModulesEarly = new Promise<string[]>(resolve => {
264
- resolveWarmedModules = resolve
265
- })
266
- const warmSettled = warm.finish(log, modules => resolveWarmedModules(modules), {
267
- globalCss: globalCssSources(config),
268
- clientReferences: [...compiledClientReferenceFiles()],
269
- })
270
- // Nothing awaits it until writeServerFunction's copy step; park the
271
- // rejection so it never surfaces as unhandled in between (it never actually
272
- // rejects — warmWithRestarts catches everything itself — but the gap
273
- // between here and that await is otherwise unguarded).
274
- warmSettled.catch(() => undefined)
275
- const warmedModules = await log.step('warm module cache (compiled)', () => warmedModulesEarly)
276
-
277
66
  const functionPath = path.join(outputPath, 'functions', `${SERVER_FUNCTION}.func`)
278
- await writeServerFunction(config, manifest, functionPath, warmedModules, warmSettled, log)
67
+ await writeServerFunction(config.outPath, manifest, functionPath, log)
279
68
 
280
- // After the warm pass, never before: it is what emits next/font bytes under public/, and a fully
281
- // dynamic app renders nothing at build, so copying earlier ships a CDN with no fonts.
282
69
  await log.step('copy static files', () =>
283
70
  copyStaticFiles(
284
- path.join(config.outPath, 'public'),
71
+ staticPath,
285
72
  path.join(outputPath, 'static'),
286
73
  relative => !staticFiles[relative] || canServeStaticOnVercel(staticFiles[relative]),
287
74
  ),
288
75
  )
289
- const overrides = await staticOverrides(path.join(config.outPath, 'public'), staticFiles)
76
+ const overrides = await staticOverrides(staticPath, staticFiles)
290
77
 
291
78
  const routes: NonNullable<VercelConfig['routes']> = [
292
79
  // next-compat documents reference the build output under Next's static path (assetPathname),
@@ -295,7 +82,7 @@ export async function writeVercelOutput(
295
82
  // server function: served, but at function cost with no edge cache.
296
83
  ...compatStaticRewrite(config, outputPath),
297
84
  // Every content-hashed build asset, exactly the set `pnext start` serves immutable.
298
- ...(await immutableAssetRoutes(path.join(config.outPath, 'public'), manifest.publicAssets)),
85
+ ...(await immutableAssetRoutes(staticPath, manifest.publicAssets)),
299
86
  // Proxy-matched paths go to the server function before the CDN filesystem
300
87
  // check — `pnext start` runs the proxy ahead of static files too.
301
88
  ...(await proxyRoutes(config)),
@@ -371,145 +158,30 @@ async function proxyRoutes(config: ResolvedConfig) {
371
158
  }
372
159
 
373
160
  async function writeServerFunction(
374
- config: ResolvedConfig,
161
+ standalonePath: string,
375
162
  manifest: BuildManifest,
376
163
  functionPath: string,
377
- warmedModules: string[],
378
- // Resolves once the warm child fully exits (handlers included). The trace below only needs the
379
- // compiled set `warmedModules` already carries - it reasons from static specifiers, never from
380
- // handler execution - so it starts immediately; only the tree copy further down, which must ship
381
- // whatever handler imports wrote to the build cache, waits on this.
382
- warmSettled: Promise<unknown>,
383
164
  log: VerboseLogger,
384
165
  ) {
385
- const workspaceRoot = config.workspaceRoot
386
- await mkdir(functionPath, { recursive: true })
387
-
388
- const pack = packRules(config)
389
- const framework = await resolveFrameworkPackage(workspaceRoot)
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
394
- const closure = await traceNodeModulesClosure(
395
- config,
396
- manifest,
397
- functionPath,
398
- framework,
399
- [...warmedModules, ...(release ?? [])],
400
- pack,
401
- log,
402
- Boolean(release),
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 }),
403
170
  )
404
-
405
- const replicaPathFor = (file: string) => {
406
- if (isInsideDir(pnextRoot, file)) {
407
- return path.join(functionPath, pnextTargetRel, path.relative(pnextRoot, file))
408
- }
409
- if (isInsideDir(workspaceRoot, file)) {
410
- return path.join(functionPath, path.relative(workspaceRoot, file))
411
- }
412
- return undefined
413
- }
414
-
415
- // Replicate the workspace layout the build ran in: app source (the renderer reads convention files
416
- // and global css imports from it at request time), the build output, and the workspace packages the
417
- // server can actually reach. Unrelated workspace trees never ship.
418
- const appRel = path.relative(workspaceRoot, config.root)
419
- const packageRoots = workspacePackageRoots(workspaceRoot).map(root => path.resolve(root))
420
- const neededRoots = new Set<string>()
421
- for (const file of closure.tracedFiles) {
422
- const root = packageRoots.find(candidate => isInsideDir(candidate, file))
423
- if (root) neededRoots.add(root)
424
- }
425
- for (const relative of closure.workspacePackages) {
426
- neededRoots.add(path.resolve(workspaceRoot, relative))
427
- }
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])
437
- }
438
- neededRoots.delete(path.resolve(config.root))
439
- neededRoots.delete(pnextRoot)
440
-
441
- // The trace above never needed the handler phase, but the copy below ships
442
- // config.outPath verbatim — handler imports can still be writing vendor
443
- // bundles into it. Most of this wait is already gone by the time the trace
444
- // and the roots above finish; whatever remains is genuine handler-phase work.
445
- await log.step('await handler warm-up', () => warmSettled)
446
-
447
- // Every tree lands in its own subdirectory and the copies are io-bound, so
448
- // the whole replica goes out as one concurrent step instead of tree by tree.
449
- const roots = neededRoots.size + 2
450
- await log.step(`copy function tree (${closure.packageCount} packages, ${roots} roots)`, () =>
451
- Promise.all([
452
- closure.copy(),
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
- ),
469
- ...[...neededRoots].map(packageRoot =>
470
- copyPackageTree(
471
- packageRoot,
472
- path.join(functionPath, path.relative(workspaceRoot, packageRoot)),
473
- pack,
474
- ),
475
- ),
476
- ...['package.json', 'tsconfig.json', 'bunfig.toml']
477
- .map(name => path.join(workspaceRoot, name))
478
- .filter(file => existsSync(file))
479
- .map(file => copyFile(file, path.join(functionPath, path.basename(file)))),
480
- ]),
481
- )
482
-
483
- // Baked absolute paths (compiled cache imports, manifest file paths) point
484
- // at the build machine's workspace; rewrite them to the runtime root.
485
- await log.step('rewrite baked paths', () =>
486
- rewriteBakedPaths(path.join(functionPath, appRel, path.relative(config.root, config.outPath)), [
487
- ...(pnextInWorkspace
488
- ? []
489
- : [[pnextRoot, toPosixPath(path.join(RUNTIME_ROOT, pnextTargetRel))] as const]),
490
- [workspaceRoot, RUNTIME_ROOT] as const,
491
- ]),
492
- )
493
-
494
- // Bun's runtime onResolve plugins never see bare specifiers, so the compat aliases the dev pipeline
495
- // applies during compilation cannot be applied at runtime for raw-loaded sources (proxy, handlers).
496
- // Bun does honor tsconfig paths for bare imports, so map the aliased specifiers to their shims in
497
- // every shipped tsconfig.
498
- await log.step('inject compat tsconfig paths', () =>
499
- 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 })))),
500
177
  )
178
+ await log.step('platform natives', () => replaceHostNatives(packages, log))
501
179
 
502
- const startEntry = toPosixPath(
503
- path.join(pnextTargetRel, prebundle ? 'dist/server/cli/start.js' : 'src/cli/start.ts'),
504
- )
505
180
  await writeText(
506
181
  path.join(functionPath, 'index.mjs'),
507
- `import path from 'node:path';
508
- import { createRequestHandler } from ${JSON.stringify(`./${startEntry}`)};
182
+ `import { createRequestHandler } from './server/entry.js';
509
183
 
510
- const handlerPromise = createRequestHandler({
511
- root: path.join(import.meta.dirname, ${JSON.stringify(appRel)}),
512
- });
184
+ const handlerPromise = createRequestHandler({ root: import.meta.dirname });
513
185
 
514
186
  async function handler(request) {
515
187
  return (await handlerPromise)(request);
@@ -549,860 +221,108 @@ export default Object.assign(handler, { fetch: handler });
549
221
  )
550
222
  }
551
223
 
552
- /**
553
- * Grow `roots` with every workspace package transitively depended on by one of `from`. A workspace
554
- * dependency is one whose name maps to a package root in this workspace - the workspace: protocol is
555
- * the usual spelling, but a pinned version resolved through the workspace counts the same.
556
- */
557
- function addWorkspaceDependencies(roots: Set<string>, packageRoots: string[], from: string[]) {
558
- const byName = new Map<string, string>()
559
- for (const root of packageRoots) {
560
- const name = packageManifest(root)?.name
561
- if (name && !byName.has(name)) byName.set(name, root)
562
- }
563
- const queue = [...from]
564
- const seen = new Set(queue)
565
- while (queue.length > 0) {
566
- const manifest = packageManifest(queue.pop()!)
567
- if (!manifest) continue
568
- for (const field of ['dependencies', 'optionalDependencies'] as const) {
569
- for (const name of Object.keys(manifest[field] ?? {})) {
570
- const root = byName.get(name)
571
- if (!root || seen.has(root)) continue
572
- seen.add(root)
573
- roots.add(root)
574
- 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)
575
241
  }
576
242
  }
577
243
  }
244
+ await visit(modules, 0)
245
+ return found
578
246
  }
579
247
 
580
- interface PackageManifest {
248
+ interface PackageJson {
581
249
  name?: string
582
- dependencies?: Record<string, string>
250
+ version?: string
251
+ os?: string[]
252
+ cpu?: string[]
253
+ libc?: string[]
583
254
  optionalDependencies?: Record<string, string>
584
255
  }
585
256
 
586
- const manifests = new Map<string, PackageManifest | undefined>()
587
-
588
- function packageManifest(root: string): PackageManifest | undefined {
589
- const cached = manifests.get(root)
590
- if (cached !== undefined || manifests.has(root)) return cached
591
- let parsed: PackageManifest | undefined
592
- try {
593
- parsed = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8')) as PackageManifest
594
- } catch {
595
- parsed = undefined // not a package, or unreadable
596
- }
597
- manifests.set(root, parsed)
598
- return parsed
599
- }
600
-
601
- // Clones a tree with hardlinks instead of byte copies - the output is packaging-only, so sharing
602
- // inodes is safe as long as nothing writes in place (rewriteBakedPaths unlinks before writing).
603
- // Exclusions apply to direct children only; node_modules and .git are pruned at any depth. Symlinks
604
- // are dereferenced so the replica is self-contained; the visited set breaks symlink cycles.
605
- async function copyTree(
606
- from: string,
607
- to: string,
608
- pack: PackRules,
609
- excludeNames: string[] = [],
610
- visited = new Set<string>([realDir(from) ?? path.resolve(from)]),
611
- ) {
612
- const exclude = new Set(excludeNames)
613
- const clone = directoryCloneEnabled ? await directoryCloner() : null
614
- // Nothing to skip at this level: the whole tree clones in one syscall.
615
- if (exclude.size === 0 && !existsSync(to)) {
616
- await mkdir(path.dirname(to), { recursive: true })
617
- if (clone?.(from, to)) return normalizeClone(to, from, pack, visited)
618
- }
619
- // The build output is still live while the walk runs — typegen rewrites
620
- // `.pnext/types` under it — so a directory can vanish between the parent's
621
- // readdir and this one. It was never part of the shipped closure; skip it.
622
- const entries = readDirEntries(from)
623
- if (!entries) return
624
- await mkdir(to, { recursive: true })
625
- await Promise.all(
626
- entries.map(async entry => {
627
- const source = path.join(from, entry.name)
628
- const target = path.join(to, entry.name)
629
- // Matched by name before type: a symlinked `node_modules` must be pruned
630
- // as the directory it points at, not followed.
631
- if (exclude.has(entry.name) || !shipsDir(pack, entry.name, source, entry.isSymbolicLink()))
632
- return
633
- if (entry.isSymbolicLink()) {
634
- const link = resolveLinkTarget(source)
635
- if (!link) return
636
- if (link.isDirectory) {
637
- if (visited.has(link.path)) return
638
- visited.add(link.path)
639
- return copyTree(link.path, target, pack, [], visited)
640
- }
641
- return shipsFile(pack, entry.name, source) ? linkOrCopyFile(link.path, target) : undefined
642
- }
643
- // One syscall for the whole subtree beats one per file — the excluded
644
- // names are stripped from the copy afterwards.
645
- if (entry.isDirectory() && clone?.(source, target)) {
646
- return normalizeClone(target, source, pack, visited)
647
- }
648
- if (entry.isDirectory()) return copyTree(source, target, pack, [], visited)
649
- if (entry.isFile() && shipsFile(pack, entry.name, source))
650
- return linkOrCopyFile(source, target)
651
- }),
652
- )
653
- }
654
-
655
- /** Directory entries, or undefined if the directory vanished under the walk. */
656
- function readDirEntries(dir: string) {
657
- try {
658
- return readdirSync(dir, { withFileTypes: true })
659
- } catch (error) {
660
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
661
- throw error
662
- }
663
- }
664
-
665
- /**
666
- * Directory-level copy-on-write clone, when the platform has one: macOS `clonefile` copies a whole
667
- * hierarchy in a single call and, like a hardlink, shares the underlying blocks. Resolved once and
668
- * never retried - no cloner just means the hardlink walk below does the work.
669
- */
670
- // Test seam: the equivalence suite builds the same app both ways and diffs the trees.
671
- let directoryCloneEnabled = true
672
-
673
- /** @internal Test-only. Returns a restore function. */
674
- export function setDirectoryCloneEnabled(enabled: boolean) {
675
- const previous = directoryCloneEnabled
676
- directoryCloneEnabled = enabled
677
- return () => {
678
- directoryCloneEnabled = previous
679
- }
680
- }
681
-
682
- let cloner: ((from: string, to: string) => boolean) | null | undefined
683
- async function directoryCloner() {
684
- if (cloner !== undefined) return cloner
685
- cloner = null
686
- if (process.platform === 'darwin') {
687
- try {
688
- const { dlopen } = await import('bun:ffi')
689
- const { symbols } = dlopen('libSystem.B.dylib', {
690
- clonefile: { args: ['cstring', 'cstring', 'i32'], returns: 'i32' },
691
- })
692
- cloner = (from, to) =>
693
- symbols.clonefile(Buffer.from(`${from}\0`), Buffer.from(`${to}\0`), 0) === 0
694
- } catch {
695
- // no clonefile here; the per-file walk stays correct, just slower
696
- }
697
- }
698
- return cloner
699
- }
700
-
701
- /**
702
- * A clone is verbatim, so it still holds what the walk would have skipped:
703
- * drop the pruned entries and dereference inner symlinks, which would otherwise
704
- * point outside the function at runtime. Mirrors the walk's rules exactly, so
705
- * both paths emit the same tree. Reading directory entries costs nothing next
706
- * to the per-file copies this replaces.
707
- */
708
- async function normalizeClone(
709
- dir: string,
710
- // Where `dir` was cloned from. Carried so the keep-path exemption reads the same here as in the
711
- // walk: the clone's own paths are inside the function and would never match a source keep path.
712
- from: string,
713
- pack: PackRules,
714
- visited: Set<string>,
715
- ) {
716
- const stack: [string, string][] = [[dir, from]]
717
- const pending: Promise<unknown>[] = []
718
- while (stack.length > 0) {
719
- const [current, currentFrom] = stack.pop()!
720
- for (const entry of readdirSync(current, { withFileTypes: true })) {
721
- const full = path.join(current, entry.name)
722
- const source = path.join(currentFrom, entry.name)
723
- // Matched by name before type, as the walk does: the pages-compat shim
724
- // links `node_modules` at the app root, and following it would pull the
725
- // whole store into the function.
726
- if (!shipsDir(pack, entry.name, source, entry.isSymbolicLink())) {
727
- rmSync(full, { recursive: true, force: true })
728
- } else if (entry.isSymbolicLink()) {
729
- // From the source: a relative link in the clone would resolve against the function tree.
730
- const link = resolveLinkTarget(source)
731
- rmSync(full, { force: true })
732
- if (!link) continue
733
- if (link.isDirectory) {
734
- if (visited.has(link.path)) continue
735
- visited.add(link.path)
736
- pending.push(copyTree(link.path, full, pack, [], visited))
737
- } else if (shipsFile(pack, entry.name, source)) {
738
- pending.push(linkOrCopyFile(link.path, full))
739
- }
740
- } else if (entry.isDirectory()) {
741
- stack.push([full, source])
742
- } else if (!shipsFile(pack, entry.name, source)) {
743
- rmSync(full, { force: true })
744
- }
745
- }
746
- }
747
- await Promise.all(pending)
748
- }
749
-
750
- interface FrameworkPackage {
751
- root: string
752
- packageName: string
753
- /** Declared runtime dependencies — the only packages it can load eagerly. */
754
- dependencies: string[]
755
- /** Lazily loaded feature deps (og, mdx, sass, image optimization). */
756
- optionalDependencies: string[]
757
- inWorkspace: boolean
758
- /** Where the framework ships inside the function, relative to its root. */
759
- targetRel: string
760
- }
761
-
762
- // The framework itself ships with the function. Inside a workspace it keeps its
763
- // workspace-relative location so baked paths line up; when consumed from
764
- // outside the workspace (e.g. installed from a registry) it lands in the
765
- // function's node_modules under its package name.
766
- async function resolveFrameworkPackage(workspaceRoot: string): Promise<FrameworkPackage> {
767
- const root = path.resolve(import.meta.dirname, '../../..')
768
- const packageJson = await readPackageJson(root)
769
- const packageName = packageJson?.name ?? 'pnext'
770
- const inWorkspace = isInsideDir(workspaceRoot, root) && !root.includes('node_modules')
771
- return {
772
- root,
773
- packageName,
774
- // Never devDependencies: their tooling (typescript, eslint, …) is
775
- // reachable from build-time sources only and must not enter the function.
776
- dependencies: Object.keys(packageJson?.dependencies ?? {}),
777
- optionalDependencies: Object.keys(packageJson?.optionalDependencies ?? {}),
778
- inWorkspace,
779
- targetRel: inWorkspace
780
- ? path.relative(workspaceRoot, root)
781
- : path.join('node_modules', packageName),
782
- }
783
- }
784
-
785
- // Build-time directories a package that publishes its whole tree still keeps.
786
- const packageCopyExcludes = [
787
- 'node_modules',
788
- '.cache',
789
- '.vercel',
790
- '.next',
791
- '.pnext',
792
- '.turbo',
793
- 'test',
794
- 'tests',
795
- '__tests__',
796
- ]
797
-
798
- /**
799
- * Copies a workspace package the way npm would publish it: a `files` list is the package's own
800
- * statement of what it ships, so it beats guessing at build-time directory names. Glob entries fall
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.
803
- */
804
- async function copyPackageTree(from: string, to: string, pack: PackRules, only?: string[]) {
805
- const files = only ?? (await readPackageJson(from))?.files
806
- if (!files?.length || files.some(entry => /[*?[\]{}!]/.test(entry))) {
807
- return copyTree(from, to, pack, packageCopyExcludes)
808
- }
809
- await mkdir(to, { recursive: true })
810
- // npm always publishes these two regardless of `files`; the runtime resolves
811
- // through both (tsconfig for Bun's `paths`/jsx settings). Listed entries need
812
- // no exclude list — `files` already said what ships — which also lets each
813
- // directory clone whole.
814
- await Promise.all(
815
- [...files, 'package.json', 'tsconfig.json'].map(async name => {
816
- const source = path.join(from, name)
817
- const stats = statSync(source, { throwIfNoEntry: false })
818
- if (!stats) return
819
- if (stats.isDirectory()) return copyTree(source, path.join(to, name), pack)
820
- if (!shipsFile(pack, name, source)) return
821
- await mkdir(path.dirname(path.join(to, name)), { recursive: true })
822
- return linkOrCopyFile(source, path.join(to, name))
823
- }),
824
- )
825
- }
826
-
827
257
  async function readPackageJson(dir: string) {
828
- const file = path.join(dir, 'package.json')
829
- if (!existsSync(file)) return undefined
830
- return JSON.parse(await readFile(file, 'utf8')) as {
831
- name?: string
832
- version?: string
833
- files?: string[]
834
- os?: string[]
835
- cpu?: string[]
836
- libc?: string[]
837
- dependencies?: Record<string, string>
838
- optionalDependencies?: Record<string, string>
839
- }
840
- }
841
-
842
- // The closure hardlinks ~13k files: ~2.9s of a ~3.1s build. Test seam so suites
843
- // asserting only config shape can skip it. Never off for a real deployment.
844
- let dependencyClosureEnabled = true
845
-
846
- /** @internal Test-only. Returns a restore function. */
847
- export function setDependencyClosureEnabled(enabled: boolean) {
848
- const previous = dependencyClosureEnabled
849
- dependencyClosureEnabled = enabled
850
- return () => {
851
- dependencyClosureEnabled = previous
852
- }
853
- }
854
-
855
- function skipDependencyClosure() {
856
- return !dependencyClosureEnabled
857
- }
858
-
859
- async function linkOrCopyFile(from: string, to: string) {
860
- try {
861
- await link(from, to)
862
- } catch {
863
- // Same race as the walk above: a source that vanished mid-copy was never
864
- // part of the closure, and nothing else here is worth failing the build for.
865
- await copyFile(from, to).catch(error => {
866
- if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
867
- })
868
- }
869
- }
870
-
871
- // Where a symlink really points, and whether that is a directory. Dangling
872
- // links are ordinary in a real node_modules (removed package, unmet optional
873
- // dep, interrupted install), so an unreadable target is skipped rather than
874
- // failing the build.
875
- function resolveLinkTarget(link: string) {
876
- const resolved = realDir(link) ?? realFile(link)
877
- if (!resolved) return undefined
878
258
  try {
879
- return { path: resolved, isDirectory: statSync(resolved).isDirectory() }
259
+ return JSON.parse(await readFile(path.join(dir, 'package.json'), 'utf8')) as PackageJson
880
260
  } catch {
881
261
  return undefined
882
262
  }
883
263
  }
884
264
 
885
- function realFile(file: string) {
886
- try {
887
- return realpathSync(file)
888
- } catch {
889
- return undefined
890
- }
891
- }
892
-
893
- function isInsideDir(root: string, file: string) {
894
- const relative = path.relative(root, file)
895
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))
896
- }
897
-
898
- /**
899
- * Whether a compat alias resolves to framework source (react, next/*) rather than to a real package.
900
- * Containment, not a `node_modules` substring: an installed framework lives under node_modules
901
- * itself, and a substring test there classifies every next/* shim as a package to ship.
902
- * @internal Exported for tests.
903
- */
904
- export function isFrameworkOwnedAliasTarget(frameworkRoot: string, target: string) {
905
- if (!path.isAbsolute(target) || !isInsideDir(frameworkRoot, target)) return false
906
- // A nested store under the framework root is still a real package.
907
- return !path.relative(frameworkRoot, target).split(path.sep).includes('node_modules')
908
- }
909
-
910
- function frameworkOwnedAliases(config: ResolvedConfig, frameworkRoot: string) {
911
- return Object.entries(getImportAliasExtensions().aliases(config, 'server')).filter(([, target]) =>
912
- isFrameworkOwnedAliasTarget(frameworkRoot, target),
913
- )
914
- }
915
-
916
- async function injectCompatPaths(
917
- config: ResolvedConfig,
918
- functionPath: string,
919
- frameworkRoot: string,
920
- replicaPathFor: (file: string) => string | undefined,
921
- ) {
922
- const aliasTargets = frameworkOwnedAliases(config, frameworkRoot)
923
- .map(([specifier, target]) => [specifier, replicaPathFor(target)] as const)
924
- .filter((entry): entry is readonly [string, string] => Boolean(entry[1]))
925
- if (aliasTargets.length === 0) return
926
-
927
- for (const file of walkTsconfigFiles(functionPath)) {
928
- let parsed: { compilerOptions?: { baseUrl?: string; paths?: Record<string, string[]> } }
929
- try {
930
- const source = stripJsonComments(await readFile(file, 'utf8')).replace(/,\s*([}\]])/g, '$1')
931
- parsed = JSON.parse(source) as typeof parsed
932
- } catch (error) {
933
- console.warn(`vercel adapter: could not update ${file}:`, error)
934
- continue
935
- }
936
- const dir = path.dirname(file)
937
- const compilerOptions = (parsed.compilerOptions ??= {})
938
- const paths = (compilerOptions.paths ??= {})
939
- for (const [specifier, target] of aliasTargets) {
940
- if (paths[specifier]) continue
941
- const relative = toPosixPath(path.relative(dir, target))
942
- paths[specifier] = [relative.startsWith('.') ? relative : `./${relative}`]
943
- }
944
- compilerOptions.baseUrl ??= '.'
945
- // The replica shares inodes with the original tsconfig; never write
946
- // through the hardlink.
947
- await rm(file)
948
- await writeFile(file, `${JSON.stringify(parsed, null, 2)}\n`)
949
- }
950
- }
951
-
952
- // Minimal JSONC support for tsconfig files: strips // and /* */ comments
953
- // outside strings (a "$schema": "https://..." value must survive).
954
- function stripJsonComments(source: string) {
955
- let result = ''
956
- let inString = false
957
- for (let index = 0; index < source.length; index++) {
958
- const char = source[index]!
959
- if (inString) {
960
- result += char
961
- if (char === '\\') {
962
- result += source[++index] ?? ''
963
- } else if (char === '"') {
964
- inString = false
965
- }
966
- continue
967
- }
968
- if (char === '"') {
969
- inString = true
970
- result += char
971
- continue
972
- }
973
- if (char === '/' && source[index + 1] === '/') {
974
- while (index < source.length && source[index] !== '\n') index++
975
- result += '\n'
976
- continue
977
- }
978
- if (char === '/' && source[index + 1] === '*') {
979
- index += 2
980
- while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) index++
981
- index++
982
- continue
983
- }
984
- result += char
985
- }
986
- return result
987
- }
988
-
989
- const bakedPathFileFilter = /\.(?:m?js|cjs|jsx|tsx?|json|html|css)$/
990
-
991
- // The build output is thousands of small files and only a few hundred carry a
992
- // baked path, so the pass is io-latency-bound: run it in bounded-concurrency
993
- // batches instead of one file at a time.
994
- const BAKED_PATH_CONCURRENCY = 64
995
-
996
- async function rewriteBakedPaths(
997
- dir: string,
998
- replacements: readonly (readonly [string, string])[],
999
- ) {
1000
- if (!existsSync(dir)) return
1001
- const files = (await listFiles(dir)).filter(file => bakedPathFileFilter.test(file))
1002
- for (let i = 0; i < files.length; i += BAKED_PATH_CONCURRENCY) {
1003
- await Promise.all(
1004
- files.slice(i, i + BAKED_PATH_CONCURRENCY).map(async file => {
1005
- const source = await readFile(file, 'utf8')
1006
- let rewritten = source
1007
- for (const [from, to] of replacements) {
1008
- if (rewritten.includes(from)) rewritten = rewritten.replaceAll(from, to)
1009
- }
1010
- if (rewritten === source) return
1011
- // The replica is hardlinked to the real build output; unlink before
1012
- // writing so the rewrite never reaches the original through a shared
1013
- // inode.
1014
- await rm(file)
1015
- await writeFile(file, rewritten)
1016
- }),
1017
- )
1018
- }
1019
- }
1020
-
1021
265
  /**
1022
- * An artifact's import specifiers, preferring the sidecar the compile step recorded over re-reading
1023
- * and re-parsing the file. Falls back to the parse whenever the sidecar is missing or unreadable - a
1024
- * stale cache hit from a prior dev build never wrote one - so the traced closure is identical either
1025
- * 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.
1026
269
  */
1027
- async function artifactSpecifiers(file: string): Promise<string[] | null> {
1028
- const sidecar = await readFile(`${file}${compiledSpecifiersManifestSuffix}`, 'utf8').catch(
1029
- () => null,
1030
- )
1031
- if (sidecar !== null) {
1032
- try {
1033
- const parsed: unknown = JSON.parse(sidecar)
1034
- if (Array.isArray(parsed) && parsed.every(entry => typeof entry === 'string')) {
1035
- return parsed
1036
- }
1037
- } catch {
1038
- // malformed sidecar (partial write, foreign tool) — fall through to parse
1039
- }
1040
- }
1041
- const source = await readFile(file, 'utf8').catch(() => null)
1042
- return source === null ? null : importSpecifiers(source, file)
1043
- }
1044
-
1045
- // Ship only the node_modules packages the server can actually reach at request time: bare imports
1046
- // traced from the modules the runtime loads, plus the framework's declared runtime dependencies,
1047
- // expanded through their dependency closures. Nothing reachable only from build-time code enters the
1048
- // function - the full monorepo node_modules does not fit Vercel's size limit. Returns the closure
1049
- // plus the deferred copy so the caller can order the io.
1050
- async function traceNodeModulesClosure(
1051
- config: ResolvedConfig,
1052
- manifest: BuildManifest,
1053
- functionPath: string,
1054
- framework: FrameworkPackage,
1055
- warmedModules: string[],
1056
- pack: PackRules,
1057
- log: VerboseLogger,
1058
- // A release imports only build output, so its closure is traced from that alone.
1059
- release = false,
1060
- ) {
1061
- const workspaceRoot = config.workspaceRoot
1062
- const specifiers = new Set<string>()
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>()
1067
- // Compat-aliased specifiers whose target lives in the framework source
1068
- // (react, next/*) never resolve from node_modules at runtime — shipping
1069
- // them would drag in the full next/react trees. Aliases that point into
1070
- // node_modules (the pinned preact family) still need their packages.
1071
- const frameworkAliasedSpecifiers = new Set(
1072
- frameworkOwnedAliases(config, path.resolve(framework.root)).map(([specifier]) => specifier),
1073
- )
1074
- await log.step('trace runtime imports', async () => {
1075
- // Only code Bun imports raw at request time needs real node_modules: route handlers, the proxy,
1076
- // the config chain, and the compiled modules the warm step just wrote. Pages resolve through that
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.
1079
- const queue: string[] = []
1080
- const seen = tracedFiles
1081
- const enqueue = (file: string | undefined) => {
1082
- if (!file) return
1083
- const resolved = path.resolve(file)
1084
- if (seen.has(resolved) || !existsSync(resolved)) return
1085
- seen.add(resolved)
1086
- queue.push(resolved)
1087
- }
1088
-
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'))
1096
- }
1097
- for (const file of warmedModules) enqueue(file)
1098
-
1099
- const visit = async (file: string) => {
1100
- if (!scriptFilePattern.test(file)) return
1101
- const found = await artifactSpecifiers(file)
1102
- if (found === null) return
1103
- for (const specifier of found) {
1104
- if (frameworkAliasedSpecifiers.has(specifier)) continue
1105
- // Compiled modules import each other — and whatever the compiler left
1106
- // external — by absolute href. Follow the ones inside the build output
1107
- // and ship the packages the rest point into.
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
1111
- if (absolute) {
1112
- const name = packageNameFromPath(absolute)
1113
- if (name) {
1114
- specifiers.add(name)
1115
- hrefPackageDirs.add(packageDirFromPath(absolute, name))
1116
- } else if (isInsideDir(config.outPath, absolute)) enqueue(absolute)
1117
- continue
1118
- }
1119
- if (/^(?:node:|bun$|bun:|data:)/.test(specifier)) continue
1120
- // resolveImport covers relative paths, tsconfig aliases, package imports and workspace
1121
- // packages - workspace source gets traced further. Bare package specifiers are also recorded
1122
- // so the closure ships (or workspace-links) their node_modules entries; workspace packages
1123
- // need the link even when their source is traced.
1124
- const resolved = resolveImport(config.root, file, specifier, config.workspaceRoot)
1125
- if (
1126
- resolved &&
1127
- isInsideDir(workspaceRoot, resolved) &&
1128
- !resolved.includes('node_modules')
1129
- ) {
1130
- enqueue(resolved)
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
- }
1138
- const name = packageNameFromSpecifier(specifier)
1139
- if (name) specifiers.add(name)
1140
- }
1141
- }
1142
-
1143
- // A level's reads are independent, so the walk advances a level at a time, bounded so a wide
1144
- // graph cannot exhaust the fd table.
1145
- const READ_BATCH = 64
1146
- for (let frontier = queue.splice(0); frontier.length > 0; frontier = queue.splice(0)) {
1147
- for (let index = 0; index < frontier.length; index += READ_BATCH) {
1148
- await Promise.all(frontier.slice(index, index + READ_BATCH).map(visit))
1149
- }
1150
- }
1151
- log.log(`traced ${seen.size} runtime modules`)
1152
- })
1153
-
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>()
1158
- const workspaceLinks = new Map<string, string>()
1159
- // Native bindings resolved here are built for the build host; the function
1160
- // needs the ones for its own platform, fetched below.
1161
- const hostNatives = new Map<string, { dir: string; owner?: PackageVersion }>()
1162
- // The framework ships whole, so what it can load at runtime is exactly what it declares as a
1163
- // runtime dependency - resolved from its own root, which is not the app's when it comes from a
1164
- // registry. Its optional deps back compat features that only load when the app uses them, so they
1165
- // ship only where the app declares them too.
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
1170
- const queue: { name: string; from: string; owner?: PackageVersion }[] = [
1171
- ...framework.dependencies
1172
- .filter(name => !runtimeDependencies || runtimeDependencies.has(name))
1173
- .map(name => ({ name, from: framework.root })),
1174
- ...framework.optionalDependencies
1175
- .filter(name => appDependencies.has(name))
1176
- .map(name => ({ name, from: framework.root })),
1177
- ...[...specifiers].map(name => ({ name, from: workspaceRoot })),
1178
- ]
1179
- const seen = new Set<string>()
1180
-
1181
- while (queue.length > 0) {
1182
- const { name, from, owner } = queue.shift()!
1183
- if (seen.has(name)) continue
1184
- seen.add(name)
1185
- // The framework is copied to its own location by the caller; copying it
1186
- // again under its package name collides with that (and with the workspace
1187
- // link it needs when it lives in the workspace).
1188
- if (name === framework.packageName) {
1189
- if (framework.inWorkspace) workspaceLinks.set(name, framework.targetRel)
1190
- continue
1191
- }
1192
- const dir = resolvePackageDir(name, [from, workspaceRoot, config.root])
1193
- if (!dir) continue
1194
-
1195
- // Workspace packages ship at their workspace-relative path; link them so
1196
- // bare imports resolve to the same real path the compiled cache uses.
1197
- const workspaceRelative = path.relative(workspaceRoot, dir)
1198
- if (!workspaceRelative.startsWith('..') && !workspaceRelative.startsWith('node_modules')) {
1199
- workspaceLinks.set(name, workspaceRelative)
1200
- continue
1201
- }
1202
-
1203
- const packageJson = await readPackageJson(dir)
1204
- // `os`/`cpu` are npm's own declaration that a package is platform-bound;
1205
- // one that already fits the function's platform ships as-is.
1206
- const platformBound = Boolean(packageJson?.os ?? packageJson?.cpu)
1207
- if (packageJson && platformBound && !matchesFunctionPlatform(packageJson)) {
1208
- hostNatives.set(name, { dir, owner })
1209
- continue
1210
- }
1211
- packageDirs.set(name, dir)
1212
- if (!packageJson) continue
1213
- // Peer dependencies are deliberately not expanded — they fan out to whole
1214
- // ecosystems (react, next, ...) that are only shipped when something
1215
- // actually imports them.
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
- }
1239
- }
1240
- await expand(dir, packageJson, path.join('node_modules', name))
1241
- }
1242
-
1243
- if (hostNatives.size > 0 && !skipDependencyClosure()) {
1244
- const targets = await log.step(
1245
- `resolve ${FUNCTION_PLATFORM.os}-${FUNCTION_PLATFORM.cpu} natives (${hostNatives.size} packages)`,
1246
- () => platformNativePackages(hostNatives, log),
1247
- )
1248
- 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
+ })
1249
287
  }
1250
-
1251
- const copy = async () => {
1252
- const entries = skipDependencyClosure() ? [] : [...packageDirs]
1253
- const batchSize = 16
1254
- for (let index = 0; index < entries.length; index += batchSize) {
1255
- await Promise.all(
1256
- entries.slice(index, index + batchSize).map(async ([name, dir]) => {
1257
- const target = path.join(functionPath, 'node_modules', name)
1258
- await mkdir(path.dirname(target), { recursive: true })
1259
- await copyTree(dir, target, pack)
1260
- }),
1261
- )
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
- }
1276
- for (const [name, workspaceRelative] of workspaceLinks) {
1277
- const linkPath = path.join(functionPath, 'node_modules', name)
1278
- await mkdir(path.dirname(linkPath), { recursive: true })
1279
- const target = path.relative(
1280
- path.dirname(linkPath),
1281
- path.join(functionPath, workspaceRelative),
1282
- )
1283
- if (!existsSync(linkPath)) await symlink(target, linkPath, 'dir')
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 })
1284
295
  }
1285
296
  }
1286
-
1287
- return {
1288
- tracedFiles,
1289
- workspacePackages: [...workspaceLinks.values()],
1290
- packageCount: packageDirs.size,
1291
- copy,
1292
- }
1293
- }
1294
-
1295
- interface PackageVersion {
1296
- name: string
1297
- version: string
1298
- }
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
297
  }
1315
298
 
1316
299
  /**
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
-
1374
- /**
1375
- * Native bindings for the function's platform, not the build host's. The host install only ever
1376
- * carries its own platform's optional deps, so the right builds have to come from the registry: one
1377
- * cross-platform install per distinct set of owning packages, cached by content in the user cache dir
1378
- * (the build output is wiped every build). Falls back to the host copies when the install is
1379
- * 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.
1380
304
  */
1381
305
  async function platformNativePackages(
1382
- hostNatives: Map<string, { dir: string; owner?: PackageVersion }>,
306
+ hostNatives: Map<string, { dirs: string[]; owner?: { name: string; version: string } }>,
307
+ manifests: Map<string, PackageJson>,
1383
308
  log: VerboseLogger,
1384
309
  ) {
1385
- // Install the package that *owns* the binding: only it lists every
1386
- // platform's build in its optional deps.
1387
310
  const owners = new Map<string, string>()
1388
- for (const [name, { dir, owner }] of hostNatives) {
1389
- 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 ?? '' }
1390
313
  if (spec.version) owners.set(spec.name, spec.version)
1391
314
  }
1392
315
  const specs = [...owners].map(([name, version]) => `${name}@${version}`).sort()
1393
- const fallback = () => new Map([...hostNatives].map(([name, { dir }]) => [name, dir]))
1394
- if (specs.length === 0) return fallback()
1395
-
316
+ if (specs.length === 0) return new Map<string, string>()
1396
317
  try {
1397
318
  const installed = await crossPlatformInstall(specs)
1398
319
  const matches = new Map<string, string>()
1399
320
  for (const [name, dir] of installed) {
1400
- const packageJson = await readPackageJson(dir)
1401
- 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)
1402
323
  }
1403
- if (matches.size === 0) {
324
+ if (matches.size === 0)
1404
325
  throw new Error(`no ${FUNCTION_PLATFORM.os} builds in ${specs.join(' ')}`)
1405
- }
1406
326
  return matches
1407
327
  } catch (error) {
1408
328
  console.warn(
@@ -1411,7 +331,7 @@ async function platformNativePackages(
1411
331
  error,
1412
332
  )
1413
333
  log.log('falling back to host native packages')
1414
- return fallback()
334
+ return new Map<string, string>()
1415
335
  }
1416
336
  }
1417
337
 
@@ -1487,95 +407,6 @@ async function crossPlatformInstall(specs: string[]) {
1487
407
  return installed
1488
408
  }
1489
409
 
1490
- const scriptFilePattern = /\.(?:m?js|cjs|jsx|tsx?)$/
1491
-
1492
- // Directories the shipped tsconfigs can never live in; pruned during the walk —
1493
- // a naive recursive listing would crawl the function's own node_modules.
1494
- const skippedScanDirs = new Set(['node_modules', '.git', '.next', '.pnext', '.turbo', '.vercel'])
1495
-
1496
- function* walkTsconfigFiles(root: string): Generator<string> {
1497
- const stack = [root]
1498
- while (stack.length > 0) {
1499
- const dir = stack.pop()!
1500
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1501
- const full = path.join(dir, entry.name)
1502
- if (entry.isDirectory()) {
1503
- if (!skippedScanDirs.has(entry.name)) stack.push(full)
1504
- } else if (entry.isFile() && entry.name === 'tsconfig.json') {
1505
- yield full
1506
- }
1507
- }
1508
- }
1509
- }
1510
-
1511
- /** Every dependency name an app's own package.json declares. */
1512
- async function declaredDependencies(root: string) {
1513
- const packageJson = (await readPackageJson(root)) as
1514
- Record<string, Record<string, string> | undefined> | undefined
1515
- return new Set(
1516
- ['dependencies', 'optionalDependencies', 'devDependencies'].flatMap(field =>
1517
- Object.keys(packageJson?.[field] ?? {}),
1518
- ),
1519
- )
1520
- }
1521
-
1522
- /** Absolute path behind an `import`, whether written as a path or a file URL. */
1523
- function absolutePathFromSpecifier(specifier: string) {
1524
- if (specifier.startsWith('file://')) return fileURLToPath(specifier)
1525
- return path.isAbsolute(specifier) ? specifier : undefined
1526
- }
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
-
1535
- /** The package an absolute path belongs to, if it points inside node_modules. */
1536
- function packageNameFromPath(file: string) {
1537
- const parts = toPosixPath(file).split('/node_modules/')
1538
- const tail = parts.at(-1)
1539
- if (parts.length < 2 || !tail) return undefined
1540
- const segments = tail.split('/')
1541
- return segments[0]?.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0]
1542
- }
1543
-
1544
- function packageNameFromSpecifier(specifier: string) {
1545
- if (/^[./#]|^file:|^node:|^data:/.test(specifier)) return undefined
1546
- if (specifier === 'bun' || specifier.startsWith('bun:')) return undefined
1547
- if (specifier.includes(':')) return undefined
1548
- // Scoped names need a real scope — `@/env`-style tsconfig aliases are not
1549
- // packages.
1550
- if (specifier.startsWith('@') && !/^@[^/]+\/[^/]+/.test(specifier)) return undefined
1551
- const parts = specifier.split('/')
1552
- const name = specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
1553
- if (!name || builtinModules.includes(name)) return undefined
1554
- return name
1555
- }
1556
-
1557
- function resolvePackageDir(name: string, fromDirs: string[]) {
1558
- for (const from of fromDirs) {
1559
- let dir = path.resolve(from)
1560
- while (true) {
1561
- const candidate = path.join(dir, 'node_modules', name)
1562
- if (existsSync(path.join(candidate, 'package.json'))) return realDir(candidate)
1563
- const parent = path.dirname(dir)
1564
- if (parent === dir) break
1565
- dir = parent
1566
- }
1567
- }
1568
- return undefined
1569
- }
1570
-
1571
- function realDir(dir: string) {
1572
- try {
1573
- return statSync(dir).isDirectory() ? realpathSync(dir) : undefined
1574
- } catch {
1575
- return undefined
1576
- }
1577
- }
1578
-
1579
410
  async function copyStaticFiles(
1580
411
  from: string,
1581
412
  to: string,
@@ -1590,7 +421,9 @@ async function copyStaticFiles(
1590
421
  )
1591
422
  await Promise.all([...directories].map(dir => mkdir(dir, { recursive: true })))
1592
423
  await Promise.all(
1593
- 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
+ ),
1594
427
  )
1595
428
  }
1596
429
 
@@ -1615,6 +448,7 @@ async function staticOverrides(
1615
448
  }
1616
449
 
1617
450
  // `important` lets route headers win over the CDN's own static-file cache-control, as Next's builder does.
451
+ // Vercel anchors every `src` at both ends, so a prefix must match the rest of the path.
1618
452
  async function immutableAssetRoutes(publicDir: string, publicAssets: string[] = []) {
1619
453
  const app = new Set(publicAssets)
1620
454
  const built =
@@ -1624,7 +458,7 @@ async function immutableAssetRoutes(publicDir: string, publicAssets: string[] =
1624
458
  .filter(relative => !app.has(relative) && immutableAssetPath(relative))
1625
459
  : []
1626
460
  const patterns = immutableAssetPatterns(built, publicAssets)
1627
- return routeSources(patterns, source => `^/(?:${source})`).map(src => ({
461
+ return routeSources(patterns, source => `^/(?:${source})$`).map(src => ({
1628
462
  src,
1629
463
  headers: { 'cache-control': immutableCacheControl },
1630
464
  continue: true,
@@ -1639,12 +473,14 @@ async function immutableAssetRoutes(publicDir: string, publicAssets: string[] =
1639
473
  function immutableAssetPatterns(built: string[], publicAssets: string[]) {
1640
474
  const appDirs = new Set(publicAssets.flatMap(ancestorDirs))
1641
475
  const prefixes = [...new Set(immutableAssetPrefixes())]
1642
- const patterns = prefixes.filter(prefix => !appDirs.has(prefix)).map(escapeRegex)
476
+ const patterns = prefixes
477
+ .filter(prefix => !appDirs.has(prefix))
478
+ .map(prefix => `${escapeRegex(prefix)}.+`)
1643
479
  for (const relative of built) {
1644
480
  const prefix = prefixes.find(prefix => relative.startsWith(prefix))
1645
481
  if (!prefix || !appDirs.has(prefix)) continue
1646
482
  const dir = ancestorDirs(relative).find(dir => dir.length > prefix.length && !appDirs.has(dir))
1647
- patterns.push(dir ? escapeRegex(requestPath(dir)) : `${escapeRegex(requestPath(relative))}$`)
483
+ patterns.push(dir ? `${escapeRegex(requestPath(dir))}.+` : escapeRegex(requestPath(relative)))
1648
484
  }
1649
485
  return [...new Set(patterns)]
1650
486
  }