@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.
- package/dist/server/_chunks/{aliases-j47a10ce.js → aliases-fhv4dt06.js} +7 -5
- package/dist/server/_chunks/{analyze-arswqe6s.js → analyze-09kx9q6r.js} +17 -13
- package/dist/server/_chunks/{boot-af0mv3dy.js → boot-8aj7efqk.js} +13 -11
- package/dist/server/_chunks/{build-spy3dxp4.js → build-jda155c9.js} +7 -5
- package/dist/server/_chunks/{build-tier-m7yvqz61.js → build-tier-qrkpfsk1.js} +59 -75
- package/dist/server/_chunks/{build-t9h0hec3.js → build-wspv5h60.js} +1004 -1191
- package/dist/server/_chunks/{compile-53j02j27.js → compile-040r79gj.js} +2 -2
- package/dist/server/_chunks/{config-n2z7yxps.js → config-7h84p6q8.js} +6 -4
- package/dist/server/_chunks/{config-1yzwexq6.js → config-d4hqyygy.js} +12 -3
- package/dist/server/_chunks/{create-meec9hs6.js → create-skj8wzt5.js} +4 -3
- package/dist/server/_chunks/{dev-fjmqg6en.js → dev-pennd36n.js} +2 -2
- package/dist/server/_chunks/{generate-n9sh6hzt.js → generate-d8vb1d21.js} +5 -4
- package/dist/server/_chunks/{index-eeaanp2e.js → index-0qt2vapb.js} +3 -3
- package/dist/server/_chunks/{index-91x12ykq.js → index-0r5eqfc7.js} +6 -1
- package/dist/server/_chunks/{index-9ych9ma8.js → index-0wm2p3ks.js} +2 -2
- package/dist/server/_chunks/{index-357na4a8.js → index-141vcwk6.js} +1 -1
- package/dist/server/_chunks/{index-2dv4y4wd.js → index-1y0cesgn.js} +16 -10
- package/dist/server/_chunks/{index-ytjvpbm2.js → index-26gwrm07.js} +3 -3
- package/dist/server/_chunks/{index-8k7pfejn.js → index-3r6qpe8a.js} +7 -7
- package/dist/server/_chunks/{index-rn8dyr1f.js → index-42fef2rv.js} +8 -48
- package/dist/server/_chunks/{index-ncnes8c6.js → index-47gx7trn.js} +2 -2
- package/dist/server/_chunks/{index-20ppawv7.js → index-4qsce64w.js} +3 -3
- package/dist/server/_chunks/{index-1011cscr.js → index-4vf121ws.js} +61 -29
- package/dist/server/_chunks/{index-cf4rvxv6.js → index-52k54ykj.js} +1 -1
- package/dist/server/_chunks/{index-nyrba2kr.js → index-53fnn598.js} +11 -11
- package/dist/server/_chunks/{index-4hgjrqh7.js → index-6tb725m8.js} +2 -2
- package/dist/server/_chunks/{index-3xbr4eyv.js → index-912z4mfx.js} +3 -3
- package/dist/server/_chunks/{index-sjpckxqn.js → index-9f49w76j.js} +1 -1
- package/dist/server/_chunks/{index-jepjvcrf.js → index-aarf1brn.js} +1 -1
- package/dist/server/_chunks/{index-9n2914ww.js → index-akkrvjn6.js} +1 -1
- package/dist/server/_chunks/index-bzd2ef7t.js +10 -0
- package/dist/server/_chunks/{index-sv8m6789.js → index-cqcwwxfp.js} +5 -5
- package/dist/server/_chunks/{index-yz54pxrh.js → index-d1jsmhhk.js} +4 -9
- package/dist/server/_chunks/{index-vvmmknm1.js → index-da0fphj0.js} +19 -15
- package/dist/server/_chunks/{index-sctcgyvv.js → index-drwv61cc.js} +2 -2
- package/dist/server/_chunks/{index-z84ab1w0.js → index-e176wk6x.js} +3 -3
- package/dist/server/_chunks/{index-9hx2641q.js → index-entm8bmz.js} +1 -1
- package/dist/server/_chunks/index-fpghsgb4.js +352 -0
- package/dist/server/_chunks/{index-jx7bqvw1.js → index-hr63fvzn.js} +1 -1
- package/dist/server/_chunks/{index-eajaspjk.js → index-j0m1jxqa.js} +1 -1
- package/dist/server/_chunks/index-jbj7ehef.js +50 -0
- package/dist/server/_chunks/{index-egykcqg3.js → index-jexsy9a8.js} +1 -1
- package/dist/server/_chunks/{index-fkp000p1.js → index-jjppzw0s.js} +2 -2
- package/dist/server/_chunks/index-jt8e1pje.js +12 -0
- package/dist/server/_chunks/{index-ggk2j965.js → index-kpdph69z.js} +56 -50
- package/dist/server/_chunks/{index-6e4fqv30.js → index-m63czgq5.js} +2 -0
- package/dist/server/_chunks/{index-c0g2wf3z.js → index-md5jchzy.js} +1 -1
- package/dist/server/_chunks/{index-tvqkq6e6.js → index-n814hcky.js} +1 -1
- package/dist/server/_chunks/{index-06nyqntx.js → index-q3r9z8mq.js} +1 -1
- package/dist/server/_chunks/{index-4q2gbfwk.js → index-qjrh7w2g.js} +2 -2
- package/dist/server/_chunks/{index-3b3at80h.js → index-s61bx2aj.js} +6 -3
- package/dist/server/_chunks/{index-ym67zqxp.js → index-tf4j19ge.js} +1 -1
- package/dist/server/_chunks/{index-qn4z5s3c.js → index-typcbcsv.js} +3 -3
- package/dist/server/_chunks/{index-p77b4kcx.js → index-veycp46e.js} +1 -1
- package/dist/server/_chunks/{index-11sfk4hb.js → index-vmr35jjw.js} +1 -1
- package/dist/server/_chunks/{index-kq8kpzye.js → index-vqs3rebe.js} +3 -3
- package/dist/server/_chunks/{index-danncfhy.js → index-wm36rj4v.js} +1 -1
- package/dist/server/_chunks/{index-z4z9zv3b.js → index-x9c55pzy.js} +141 -3
- package/dist/server/_chunks/{index-y2jr2n72.js → index-xrsg0041.js} +4 -4
- package/dist/server/_chunks/{index-k1g3epw0.js → index-xs3411qh.js} +10 -10
- package/dist/server/_chunks/{index-0374j2b9.js → index-y4d02znn.js} +1 -1
- package/dist/server/_chunks/{index-wpfhfbs4.js → index-y71a76dn.js} +4 -4
- package/dist/server/_chunks/{index-qm6kmhqs.js → index-y7mzvq2q.js} +1 -1
- package/dist/server/_chunks/{index-hkdfj58q.js → index-y9bygr95.js} +1 -1
- package/dist/server/_chunks/{index-3tf1xjsp.js → index-yghskk8w.js} +11 -11
- package/dist/server/_chunks/{index-5gyr4emz.js → index-yzcpr3yw.js} +4 -4
- package/dist/server/_chunks/{index-x93hv0sc.js → index-zedjt25v.js} +23 -10
- package/dist/server/_chunks/{index-tq0pc7eg.js → index-zfrwq472.js} +3 -3
- package/dist/server/_chunks/{loader-7yw0j98z.js → loader-w1e0n5sm.js} +12 -10
- package/dist/server/_chunks/{modules-jn8bmbn8.js → modules-730j14cm.js} +14 -12
- package/dist/server/_chunks/{pipeline-yy9jhydf.js → pipeline-yjdnwbhm.js} +25 -21
- package/dist/server/_chunks/{production-jar9ztm3.js → production-1xe9b7wd.js} +14 -3
- package/dist/server/_chunks/{proxy-config-j84dgksn.js → proxy-config-9r9e0jvn.js} +7 -5
- package/dist/server/_chunks/{proxy-w1d0wmsn.js → proxy-pnevsbfd.js} +12 -10
- package/dist/server/_chunks/{router-k1reahfn.js → router-rzge0vz8.js} +10 -7
- package/dist/server/_chunks/{routes-2s277fc6.js → routes-0x8wjvcj.js} +12 -10
- package/dist/server/_chunks/{run-pvd8bs1t.js → run-tk90r3ac.js} +5 -4
- package/dist/server/_chunks/{server-mt4a994c.js → server-gev78ejc.js} +38 -34
- package/dist/server/_chunks/{tsconfig-defaults-1nb3308g.js → tsconfig-defaults-qe9wd6ne.js} +5 -4
- package/dist/server/_chunks/{typegen-0qr0459b.js → typegen-0yw8sry8.js} +15 -13
- package/dist/server/cli/index.js +12 -15
- package/dist/server/cli/start.js +35 -14
- package/dist/server/compat/next/cache.js +5 -5
- package/dist/server/compat/next/config-headers.js +8 -6
- package/dist/server/compat/next/config-loader.js +1 -1
- package/dist/server/compat/next/dist/server/web/spec-extension/revalidate.js +5 -5
- package/dist/server/compat/next/dist/server/web/spec-extension/unstable-cache.js +5 -5
- package/dist/server/compat/next/dist/server/web/spec-extension/unstable-no-store.js +5 -5
- package/dist/server/compat/next/dynamic.js +3 -14
- package/dist/server/compat/next/font/cache.js +6 -4
- package/dist/server/compat/next/font/local.js +7 -5
- package/dist/server/compat/next/font/runtime.js +7 -5
- package/dist/server/compat/next/headers.js +4 -4
- package/dist/server/compat/next/image/config.js +2 -2
- package/dist/server/compat/next/image/optimizer.js +3 -3
- package/dist/server/compat/next/image/static-metadata.js +4 -4
- package/dist/server/compat/next/legacy-request-apis.js +3 -3
- package/dist/server/compat/next/link-transform.js +4 -3
- package/dist/server/compat/next/navigation.js +5 -4
- package/dist/server/compat/next/redirects.js +8 -6
- package/dist/server/compat/next/resource-hints.js +2 -2
- package/dist/server/compat/next/rewrites.js +7 -5
- package/dist/server/compat/next/root-params.js +3 -3
- package/dist/server/compat/next/route-state.js +2 -2
- package/dist/server/compat/next/script.js +2 -2
- package/dist/server/compat/next/server.js +5 -5
- package/dist/server/generation.json +1 -1
- package/package.json +1 -1
- package/reference/compat.md +1 -1
- package/reference/config.md +2 -2
- package/reference/dev.md +1 -1
- package/reference/getting-started.md +1 -1
- package/reference/rendering.md +4 -4
- package/src/api/dynamic.tsx +84 -4
- package/src/cli/adapters/vercel-warm.ts +40 -29
- package/src/cli/adapters/vercel.ts +101 -1265
- package/src/cli/analyze.ts +3 -2
- package/src/cli/build/cache.ts +6 -6
- package/src/cli/build.ts +33 -33
- package/src/cli/index.ts +4 -6
- package/src/cli/serve/pipeline.ts +16 -10
- package/src/cli/serve/standalone-worker.ts +8 -0
- package/src/cli/serve/standalone.ts +727 -0
- package/src/cli/start.ts +29 -8
- package/src/client/build.ts +1 -1
- package/src/client/reference-stub.ts +2 -34
- package/src/compat/actions/extensions.ts +3 -2
- package/src/compat/bundler/require-context.ts +2 -1
- package/src/compat/bundler/webpack-loaders.ts +3 -2
- package/src/compat/bundler/worker.ts +2 -2
- package/src/compat/css/inline-css.ts +1 -1
- package/src/compat/export/build-complete.ts +1 -1
- package/src/compat/export/emit.ts +4 -4
- package/src/compat/export/standalone.ts +30 -65
- package/src/compat/image-optimizer/optimize.ts +1 -1
- package/src/compat/lifecycle/instrumentation-client.ts +4 -3
- package/src/compat/metadata-artifacts.ts +1 -1
- package/src/compat/next/config-loader.ts +3 -0
- package/src/compat/next/dynamic.tsx +10 -23
- package/src/compat/next/font/cache.ts +2 -1
- package/src/compat/next/font/runtime.ts +1 -1
- package/src/compat/register/image.ts +1 -1
- package/src/compat/typecheck/check.ts +6 -4
- package/src/config.ts +28 -5
- package/src/css/assets.ts +1 -1
- package/src/css/build.ts +16 -9
- package/src/dev/client-chunk-store.ts +2 -1
- package/src/dev/restart/client-key.ts +2 -1
- package/src/dev/restart/global-css.ts +2 -1
- package/src/dev/restart/route-bundle-key.ts +2 -1
- package/src/dev/restart/route-facts.ts +2 -1
- package/src/dev/server.ts +8 -7
- package/src/out-paths.ts +10 -0
- package/src/render/renderer.ts +1 -1
- package/src/resolve/dynamic.ts +4 -0
- package/src/resolve/engine.ts +2 -12
- package/src/resolve/scan-facts.ts +185 -1
- package/src/routing/metadata-files.ts +1 -1
- package/src/runtime/loader.ts +5 -3
- package/src/runtime/module-cache.ts +2 -1
- package/src/runtime/modules.ts +50 -16
- package/src/runtime/prebundle.ts +23 -3
- package/src/runtime/production.ts +105 -40
- package/src/runtime/release-graph.ts +21 -13
- package/src/runtime/vendor-build.ts +45 -23
- package/src/types.ts +2 -3
- package/src/utils/directive.ts +40 -0
- package/src/utils/fs.ts +7 -2
- package/src/utils/resource-query.ts +12 -0
- package/dist/server/_chunks/index-tqn2qnnn.js +0 -241
- package/dist/server/_chunks/index-y36a0ybw.js +0 -57
- package/src/cli/serve/entry.ts +0 -52
|
@@ -1,38 +1,21 @@
|
|
|
1
|
-
import { copyFile,
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
248
|
+
interface PackageJson {
|
|
581
249
|
name?: string
|
|
582
|
-
|
|
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
|
|
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
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
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
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
await
|
|
1256
|
-
|
|
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
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
|
|
1320
|
-
|
|
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, {
|
|
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, {
|
|
1389
|
-
const spec = owner ?? { name, 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
|
-
|
|
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
|
|
1401
|
-
if (
|
|
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
|
|
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 =>
|
|
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})
|
|
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
|
|
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)) :
|
|
483
|
+
patterns.push(dir ? `${escapeRegex(requestPath(dir))}.+` : escapeRegex(requestPath(relative)))
|
|
1648
484
|
}
|
|
1649
485
|
return [...new Set(patterns)]
|
|
1650
486
|
}
|