-
-
Notifications
You must be signed in to change notification settings - Fork 136
/
bundle.ts
162 lines (128 loc) · 5.15 KB
/
bundle.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
import assert from 'assert';
import type { BuildOptions } from 'esbuild';
import fs from 'fs-extra';
import pMap from 'p-map';
import path from 'path';
import { uniq } from 'ramda';
import type EsbuildServerlessPlugin from './index';
import { asArray, assertIsString, isESM, isString } from './helper';
import type { EsbuildOptions, FileBuildResult, FunctionBuildResult, BuildContext } from './types';
import { trimExtension } from './utils';
const getStringArray = (input: unknown): string[] => asArray(input).filter(isString);
export async function bundle(this: EsbuildServerlessPlugin): Promise<void> {
assert(this.buildOptions, 'buildOptions is not defined');
this.prepare();
this.log.verbose(`Compiling to ${this.buildOptions?.target} bundle with esbuild...`);
const exclude = getStringArray(this.buildOptions?.exclude);
// esbuild v0.7.0 introduced config options validation, so I have to delete plugin specific options from esbuild config.
const esbuildOptions: EsbuildOptions = [
'concurrency',
'zipConcurrency',
'exclude',
'nativeZip',
'packager',
'packagePath',
'watch',
'keepOutputDirectory',
'packagerOptions',
'installExtraArgs',
'outputFileExtension',
'outputBuildFolder',
'outputWorkFolder',
'nodeExternals',
'skipBuild',
'skipRebuild',
'skipBuildExcludeFns',
'stripEntryResolveExtensions',
'disposeContext',
].reduce<Record<string, any>>((options, optionName) => {
const { [optionName]: _, ...rest } = options;
return rest;
}, this.buildOptions);
const config: Omit<BuildOptions, 'watch'> = {
...esbuildOptions,
external: [...getStringArray(this.buildOptions?.external), ...(exclude.includes('*') ? [] : exclude)],
plugins: this.plugins,
};
const { buildOptions, buildDirPath } = this;
assert(buildOptions, 'buildOptions is not defined');
assertIsString(buildDirPath, 'buildDirPath is not a string');
if (isESM(buildOptions) && buildOptions.outputFileExtension === '.cjs') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Serverless typings (as of v3.0.2) are incorrect
throw new this.serverless.classes.Error(
'ERROR: format "esm" or platform "neutral" should not output a file with extension ".cjs".'
);
}
if (!isESM(buildOptions) && buildOptions.outputFileExtension === '.mjs') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Serverless typings (as of v3.0.2) are incorrect
throw new this.serverless.classes.Error('ERROR: Non esm builds should not output a file with extension ".mjs".');
}
if (buildOptions.outputFileExtension !== '.js') {
config.outExtension = { '.js': buildOptions.outputFileExtension };
}
/** Build the files */
const bundleMapper = async (entry: string): Promise<FileBuildResult> => {
const bundlePath = entry.slice(0, entry.lastIndexOf('.')) + buildOptions.outputFileExtension;
// check cache
if (this.buildCache) {
const { result, context } = this.buildCache[entry] ?? {};
if (result?.rebuild) {
await result.rebuild();
return { bundlePath, entry, result };
}
if (context?.rebuild) {
const rebuild = await context.rebuild();
return { bundlePath, entry, context, result: rebuild };
}
}
const options = {
...config,
entryPoints: [entry],
outdir: path.join(buildDirPath, path.dirname(entry)),
};
const pkg = await import('esbuild');
type ContextFn = (opts: typeof options) => Promise<BuildContext>;
type WithContext = typeof pkg & { context?: ContextFn };
const context = await (pkg as WithContext).context?.(options);
let result;
if (!buildOptions.skipRebuild) {
result = await context?.rebuild();
if (!result) {
result = await pkg.build(options);
}
} else {
result = await pkg.build(options);
}
if (config.metafile) {
fs.writeFileSync(
path.join(buildDirPath, `${trimExtension(entry)}-meta.json`),
JSON.stringify(result.metafile, null, 2)
);
}
return { bundlePath, entry, result, context };
};
// Files can contain multiple handlers for multiple functions, we want to get only the unique ones
const uniqueFiles: string[] = uniq(this.functionEntries.map(({ entry }) => entry));
this.log.verbose(`Compiling with concurrency: ${buildOptions.concurrency}`);
const fileBuildResults = await pMap(uniqueFiles, bundleMapper, {
concurrency: buildOptions.concurrency,
});
// Create a cache with entry as key
this.buildCache = fileBuildResults.reduce<Record<string, FileBuildResult>>((acc, fileBuildResult) => {
acc[fileBuildResult.entry] = fileBuildResult;
return acc;
}, {});
// Map function entries back to bundles
this.buildResults = this.functionEntries
.map(({ entry, func, functionAlias }) => {
const { bundlePath } = this.buildCache[entry] ?? {};
if (typeof bundlePath !== 'string' || func === null) {
return;
}
return { bundlePath, func, functionAlias };
})
.filter((result): result is FunctionBuildResult => typeof result === 'object');
this.log.verbose('Compiling completed.');
}