This repository has been archived by the owner on May 22, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 35
/
bundler.ts
182 lines (160 loc) · 6.58 KB
/
bundler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import { basename, dirname, extname, resolve, join } from 'path'
import { build, Metafile } from '@netlify/esbuild'
import { tmpName } from 'tmp-promise'
import type { FunctionConfig } from '../../../../config.js'
import { FeatureFlags } from '../../../../feature_flags.js'
import { FunctionBundlingUserError } from '../../../../utils/error.js'
import { getPathWithExtension, safeUnlink } from '../../../../utils/fs.js'
import { getBundlerTarget, getModuleFormat } from './bundler_target.js'
import { getDynamicImportsPlugin } from './plugin_dynamic_imports.js'
import { getNativeModulesPlugin } from './plugin_native_modules.js'
import { getNodeBuiltinPlugin } from './plugin_node_builtin.js'
// Maximum number of log messages that an esbuild instance will produce. This
// limit is important to avoid out-of-memory errors due to too much data being
// sent in the Go<>Node IPC channel.
export const ESBUILD_LOG_LIMIT = 10
// When resolving imports with no extension (e.g. require('./foo')), these are
// the extensions that esbuild will look for, in this order.
const RESOLVE_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.json']
// eslint-disable-next-line max-statements
export const bundleJsFile = async function ({
additionalModulePaths,
basePath,
config,
externalModules = [],
featureFlags,
ignoredModules = [],
name,
srcDir,
srcFile,
}: {
additionalModulePaths?: string[]
basePath?: string
config: FunctionConfig
externalModules: string[]
featureFlags: FeatureFlags
ignoredModules: string[]
name: string
srcDir: string
srcFile: string
}) {
// We use a temporary directory as the destination for esbuild files to avoid
// any naming conflicts with files generated by other functions.
const targetDirectory = await tmpName()
// De-duping external and ignored modules.
const external = [...new Set([...externalModules, ...ignoredModules])]
// To be populated by the native modules plugin with the names, versions and
// paths of any Node modules with native dependencies.
const nativeNodeModules = {}
// To be populated by the dynamic imports plugin with the names of the Node
// modules that include imports with dynamic expressions.
const nodeModulesWithDynamicImports: Set<string> = new Set()
// To be populated by the dynamic imports plugin with any paths (in a glob
// format) to be included in the bundle in order to make a dynamic import
// work at runtime.
const dynamicImportsIncludedPaths: Set<string> = new Set()
// The list of esbuild plugins to enable for this build.
const plugins = [
getNodeBuiltinPlugin(),
getNativeModulesPlugin(nativeNodeModules),
getDynamicImportsPlugin({
basePath,
includedPaths: dynamicImportsIncludedPaths,
moduleNames: nodeModulesWithDynamicImports,
processImports: config.processDynamicNodeImports !== false,
srcDir,
}),
]
// The version of ECMAScript to use as the build target. This will determine
// whether certain features are transpiled down or left untransformed.
const nodeTarget = getBundlerTarget(config.nodeVersion)
// esbuild will format `sources` relative to the sourcemap file, which lives
// in `destFolder`. We use `sourceRoot` to establish that relation. They are
// URLs, not paths, so even on Windows they should use forward slashes.
const sourceRoot = targetDirectory.replace(/\\/g, '/')
// Configuring the output format of esbuild. The `includedFiles` array we get
// here contains additional paths to include with the bundle, like the path
// to a `package.json` with {"type": "module"} in case of an ESM function.
const { includedFiles: includedFilesFromModuleDetection, moduleFormat } = await getModuleFormat(
srcDir,
featureFlags,
config.nodeVersion,
)
try {
const { metafile = { inputs: {}, outputs: {} }, warnings } = await build({
bundle: true,
entryPoints: [srcFile],
external,
format: moduleFormat,
logLevel: 'warning',
logLimit: ESBUILD_LOG_LIMIT,
metafile: true,
nodePaths: additionalModulePaths,
outdir: targetDirectory,
platform: 'node',
plugins,
resolveExtensions: RESOLVE_EXTENSIONS,
sourcemap: Boolean(config.nodeSourcemap),
sourceRoot,
target: [nodeTarget],
})
const bundlePaths = getBundlePaths({
destFolder: targetDirectory,
outputs: metafile.outputs,
srcFile,
})
const inputs = Object.keys(metafile.inputs).map((path) => resolve(path))
const cleanTempFiles = getCleanupFunction([...bundlePaths.keys()])
const additionalPaths = [...dynamicImportsIncludedPaths, ...includedFilesFromModuleDetection]
return {
additionalPaths,
bundlePaths,
cleanTempFiles,
inputs,
moduleFormat,
nativeNodeModules,
nodeModulesWithDynamicImports: [...nodeModulesWithDynamicImports],
warnings,
}
} catch (error) {
throw new FunctionBundlingUserError(error, { functionName: name, runtime: 'js', bundler: 'esbuild' })
}
}
// Takes the `outputs` object produced by esbuild and returns a Map with the
// absolute paths of the generated files as keys, and the paths that those
// files should take in the generated bundle as values. This is compatible
// with the `aliases` format used upstream.
const getBundlePaths = ({
destFolder,
outputs,
srcFile,
}: {
destFolder: string
outputs: Metafile['outputs']
srcFile: string
}) => {
const bundleFilename = `${basename(srcFile, extname(srcFile))}.js`
const mainFileDirectory = dirname(srcFile)
const bundlePaths: Map<string, string> = new Map()
// The paths returned by esbuild are relative to the current directory, which
// is a problem on Windows if the target directory is in a different drive
// letter. To get around that, instead of using `path.resolve`, we compute
// the absolute path by joining `destFolder` with the `basename` of each
// entry of the `outputs` object.
Object.entries(outputs).forEach(([path, output]) => {
const filename = basename(path)
const extension = extname(path)
const absolutePath = join(destFolder, filename)
if (output.entryPoint && basename(output.entryPoint) === basename(srcFile)) {
// Ensuring the main file has a `.js` extension.
const normalizedSrcFile = getPathWithExtension(srcFile, '.js')
bundlePaths.set(absolutePath, normalizedSrcFile)
} else if (extension === '.js' || filename === `${bundleFilename}.map`) {
bundlePaths.set(absolutePath, join(mainFileDirectory, filename))
}
})
return bundlePaths
}
const getCleanupFunction = (paths: string[]) => async () => {
await Promise.all(paths.filter(Boolean).map(safeUnlink))
}