cdk-booster 1.0.1
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/LICENSE +353 -0
- package/LICENSE.md +350 -0
- package/README.md +114 -0
- package/dist/cdk-booster.d.ts +29 -0
- package/dist/cdk-booster.mjs +733 -0
- package/dist/cdkFrameworkWorker.mjs +50 -0
- package/dist/configuration.d.ts +16 -0
- package/dist/configuration.mjs +29 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.mjs +3 -0
- package/dist/getConfigFromCliArgs.d.ts +7 -0
- package/dist/getConfigFromCliArgs.mjs +23 -0
- package/dist/getDirname.d.ts +10 -0
- package/dist/getDirname.mjs +19 -0
- package/dist/logger.d.ts +51 -0
- package/dist/logger.mjs +83 -0
- package/dist/types/bundleSettings.d.ts +6 -0
- package/dist/types/bundleSettings.mjs +1 -0
- package/dist/types/cbConfig.d.ts +8 -0
- package/dist/types/cbConfig.mjs +1 -0
- package/dist/types/lambdaBundle.d.ts +11 -0
- package/dist/types/lambdaBundle.mjs +1 -0
- package/dist/utils/findPackageJson.d.ts +6 -0
- package/dist/utils/findPackageJson.mjs +33 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.mjs +25 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/commander/LICENSE +22 -0
- package/node_modules/commander/Readme.md +1159 -0
- package/node_modules/commander/esm.mjs +16 -0
- package/node_modules/commander/index.js +24 -0
- package/node_modules/commander/lib/argument.js +149 -0
- package/node_modules/commander/lib/command.js +2778 -0
- package/node_modules/commander/lib/error.js +39 -0
- package/node_modules/commander/lib/help.js +747 -0
- package/node_modules/commander/lib/option.js +379 -0
- package/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/node_modules/commander/package-support.json +16 -0
- package/node_modules/commander/package.json +82 -0
- package/node_modules/commander/typings/esm.d.mts +3 -0
- package/node_modules/commander/typings/index.d.ts +1113 -0
- package/package.json +100 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ****** support require in for CJS modules ******
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
global.require = require;
|
|
7
|
+
import { getVersion } from './version.mjs';
|
|
8
|
+
import { Configuration } from './configuration.mjs';
|
|
9
|
+
import { Logger } from './logger.mjs';
|
|
10
|
+
import { getModuleDirname, getProjectDirname } from './getDirname.mjs';
|
|
11
|
+
import * as esbuild from 'esbuild';
|
|
12
|
+
import * as fs from 'fs/promises';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import { pathToFileURL } from 'url';
|
|
15
|
+
import { outputFolder } from './constants.mjs';
|
|
16
|
+
import { findPackageJson } from './utils/findPackageJson.mjs';
|
|
17
|
+
import { Worker } from 'node:worker_threads';
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
import crypto from 'node:crypto';
|
|
20
|
+
import { dirname, resolve } from 'path';
|
|
21
|
+
import { existsSync } from 'fs';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import * as os from 'os';
|
|
24
|
+
/**
|
|
25
|
+
* Start the CDK Booster
|
|
26
|
+
*/
|
|
27
|
+
async function run() {
|
|
28
|
+
let copyAgainFunction;
|
|
29
|
+
const version = await getVersion();
|
|
30
|
+
Logger.log(`Welcome to CDK Booster 🚀 version ${version}.`);
|
|
31
|
+
await Configuration.readConfig();
|
|
32
|
+
Logger.setVerbose(Configuration.config.verbose === true);
|
|
33
|
+
Logger.verbose(`Parameters: ${Object.entries(Configuration.config)
|
|
34
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
35
|
+
.join(', ')}`);
|
|
36
|
+
Logger.verbose(`NPM module folder: ${getModuleDirname()}`);
|
|
37
|
+
Logger.verbose(`Project folder: ${getProjectDirname()}`);
|
|
38
|
+
const config = Configuration.config;
|
|
39
|
+
const rootDir = process.cwd();
|
|
40
|
+
Logger.verbose(`Compiling CDK code from ${config.entryFile}`);
|
|
41
|
+
const compileCodeFile = await compileCdk({
|
|
42
|
+
rootDir,
|
|
43
|
+
entryFile: config.entryFile,
|
|
44
|
+
});
|
|
45
|
+
Logger.verbose(` Running the CDK code ${compileCodeFile} to extract Lambda functions.`);
|
|
46
|
+
const secondRun = await isSecondRun();
|
|
47
|
+
Logger.verbose(secondRun ? `This is the second run.` : `This is the first run.`);
|
|
48
|
+
if (!secondRun) {
|
|
49
|
+
// Clean up any existing bundling temp folders before starting
|
|
50
|
+
await deleteBundlingTempFolders();
|
|
51
|
+
const { lambdas, missing } = await runCdkCodeAndReturnLambdas({
|
|
52
|
+
config,
|
|
53
|
+
compileCodeFile,
|
|
54
|
+
});
|
|
55
|
+
Logger.verbose(`Found ${lambdas.length} Lambda functions in the CDK code:`, JSON.stringify(lambdas, null, 2));
|
|
56
|
+
const lambdasEsBuildCommands = lambdas;
|
|
57
|
+
// Prepare bundling temp folders for each Lambda function
|
|
58
|
+
await recreateBundlingTempFolders(lambdasEsBuildCommands);
|
|
59
|
+
// Execute pre-bundling commands
|
|
60
|
+
await executeCommands(lambdasEsBuildCommands, 'commandBeforeBundling');
|
|
61
|
+
const outputs = await bundle(lambdasEsBuildCommands);
|
|
62
|
+
// move files to the output folder
|
|
63
|
+
await copyFilesToOutput(lambdasEsBuildCommands, outputs);
|
|
64
|
+
// Execute post-bundling commands
|
|
65
|
+
await executeCommands(lambdasEsBuildCommands, 'commandAfterBundling');
|
|
66
|
+
if (missing) {
|
|
67
|
+
copyAgainFunction = async () => {
|
|
68
|
+
await recreateBundlingTempFolders(lambdasEsBuildCommands);
|
|
69
|
+
await executeCommands(lambdasEsBuildCommands, 'commandBeforeBundling');
|
|
70
|
+
await copyFilesToOutput(lambdasEsBuildCommands, outputs);
|
|
71
|
+
await executeCommands(lambdasEsBuildCommands, 'commandAfterBundling');
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
Logger.log(`All Lambda functions have been built and copied to the output folder.`);
|
|
75
|
+
}
|
|
76
|
+
Logger.log(`Starting to run regular CDK code.`);
|
|
77
|
+
// Regular import and execution of the compiled CDK code
|
|
78
|
+
await import(pathToFileURL(compileCodeFile).href);
|
|
79
|
+
if (copyAgainFunction) {
|
|
80
|
+
Logger.verbose(`Some resources are missing and need to be looked up. The synth process will run again. Assets will be copied again to avoid re-bundling.`);
|
|
81
|
+
await copyAgainFunction();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Bundle Lambda functions using esbuild
|
|
86
|
+
* @param lambdasEsBuildCommands - Array of Lambda bundle configurations
|
|
87
|
+
* @returns esbuild metafile outputs mapping
|
|
88
|
+
*/
|
|
89
|
+
async function bundle(lambdasEsBuildCommands) {
|
|
90
|
+
const tempFolder = path.resolve(path.join(outputFolder, 'bundle'));
|
|
91
|
+
let outputs = {};
|
|
92
|
+
// Create build combinations grouped by identical build options to optimize bundling
|
|
93
|
+
const allBuildCombinations = createBuildCombinations(lambdasEsBuildCommands);
|
|
94
|
+
// Group by unique build option hashes to bundle functions with identical settings together
|
|
95
|
+
const uniqueBuildHashes = new Set(allBuildCombinations.map((b) => b.buildOptionsHash));
|
|
96
|
+
// Bundle each group of functions with identical build options
|
|
97
|
+
await Promise.all(Array.from(uniqueBuildHashes).map(async (buildHash) => {
|
|
98
|
+
const buildCombinations = allBuildCombinations.filter((b) => b.buildOptionsHash === buildHash);
|
|
99
|
+
const buildOptions = buildCombinations[0].buildOptions;
|
|
100
|
+
const entryPoints = buildCombinations.map((b) => b.entryPoint);
|
|
101
|
+
const normalizedEsbuildArgs = normalizeEsbuildArgs(buildOptions.esbuildArgs);
|
|
102
|
+
const esBuildOpt = {
|
|
103
|
+
entryPoints,
|
|
104
|
+
bundle: true,
|
|
105
|
+
platform: 'node',
|
|
106
|
+
outdir: tempFolder,
|
|
107
|
+
target: buildOptions.target,
|
|
108
|
+
format: buildOptions.format,
|
|
109
|
+
minify: buildOptions.minify,
|
|
110
|
+
sourcemap: buildOptions.sourcemap,
|
|
111
|
+
sourcesContent: buildOptions.sourcesContent,
|
|
112
|
+
external: buildOptions.external,
|
|
113
|
+
loader: buildOptions.loader,
|
|
114
|
+
define: buildOptions.define,
|
|
115
|
+
logLevel: buildOptions.logLevel,
|
|
116
|
+
keepNames: buildOptions.keepNames,
|
|
117
|
+
tsconfig: buildOptions.tsconfig,
|
|
118
|
+
banner: buildOptions.banner,
|
|
119
|
+
footer: buildOptions.footer,
|
|
120
|
+
mainFields: buildOptions.mainFields,
|
|
121
|
+
inject: buildOptions.inject,
|
|
122
|
+
alias: normalizedEsbuildArgs?.alias,
|
|
123
|
+
drop: normalizedEsbuildArgs?.drop,
|
|
124
|
+
pure: normalizedEsbuildArgs?.pure,
|
|
125
|
+
logOverride: normalizedEsbuildArgs?.logOverride,
|
|
126
|
+
// I need this to properly output bundled files
|
|
127
|
+
entryNames: '[dir]/[name]-[hash]/index',
|
|
128
|
+
metafile: true,
|
|
129
|
+
outExtension: { '.js': '.mjs' },
|
|
130
|
+
};
|
|
131
|
+
if (Logger.isVerbose()) {
|
|
132
|
+
Logger.verbose(`Bundling with options:`, JSON.stringify(esBuildOpt, null, 2), `following functions:\n - ${entryPoints.join('\n - ')}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
Logger.log(`Bundling:\n - ${entryPoints.join('\n - ')}`);
|
|
136
|
+
}
|
|
137
|
+
const buildingResults = await esbuild.build(esBuildOpt);
|
|
138
|
+
outputs = {
|
|
139
|
+
...outputs,
|
|
140
|
+
...buildingResults.metafile?.outputs,
|
|
141
|
+
};
|
|
142
|
+
}));
|
|
143
|
+
Logger.log(`All functions have been bundled.`);
|
|
144
|
+
return outputs;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Copy built files from esbuild output to the cdk.out/bundling-temp-* folders
|
|
148
|
+
* @param lambdasEsBuildCommands - Array of Lambda bundle configurations
|
|
149
|
+
* @param outputs - esbuild metafile outputs mapping
|
|
150
|
+
*/
|
|
151
|
+
async function copyFilesToOutput(lambdasEsBuildCommands, outputs) {
|
|
152
|
+
await Promise.all(lambdasEsBuildCommands.map(async (lambdasEsBuildCommand) => {
|
|
153
|
+
let esBuildOutput;
|
|
154
|
+
for (const outputFile in outputs) {
|
|
155
|
+
const output = outputs[outputFile];
|
|
156
|
+
const entryPoint = output.entryPoint
|
|
157
|
+
? path.resolve(output.entryPoint)
|
|
158
|
+
: undefined;
|
|
159
|
+
if (entryPoint &&
|
|
160
|
+
(lambdasEsBuildCommand.entryPoint.endsWith(entryPoint) ||
|
|
161
|
+
lambdasEsBuildCommand.entryPoint.endsWith(output.entryPoint))) {
|
|
162
|
+
esBuildOutput = outputFile;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!esBuildOutput) {
|
|
167
|
+
throw new Error(`No output found for entry point ${lambdasEsBuildCommand.entryPoint}`);
|
|
168
|
+
}
|
|
169
|
+
// const source = path.dirname(
|
|
170
|
+
// path.resolve(path.join(rootFolder, esBuildOutput)),
|
|
171
|
+
// );
|
|
172
|
+
const source = path.dirname(path.resolve(esBuildOutput));
|
|
173
|
+
const entryOutputFilename = lambdasEsBuildCommand.out.replaceAll('-building', '');
|
|
174
|
+
const target = path.dirname(entryOutputFilename);
|
|
175
|
+
Logger.verbose(`Moving files from ${source} to ${target}`);
|
|
176
|
+
// create folder if it doesn't exist
|
|
177
|
+
await copyFolderRecursive(source, target, entryOutputFilename);
|
|
178
|
+
}));
|
|
179
|
+
Logger.log(`All built files have been copied to the output folders.`);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if this is the second run of the CDK Booster
|
|
183
|
+
* @returns True if this is the second run, false otherwise
|
|
184
|
+
*/
|
|
185
|
+
async function isSecondRun() {
|
|
186
|
+
// second run is if there is cdk.out/manifest.json file with node missing
|
|
187
|
+
const manifestPath = path.join(process.cwd(), 'cdk.out', 'manifest.json');
|
|
188
|
+
const manifestExists = await fs
|
|
189
|
+
.access(manifestPath)
|
|
190
|
+
.then(() => true)
|
|
191
|
+
.catch(() => false);
|
|
192
|
+
if (!manifestExists)
|
|
193
|
+
return false;
|
|
194
|
+
const manifestRaw = await fs.readFile(manifestPath, { encoding: 'utf-8' });
|
|
195
|
+
const manifest = JSON.parse(manifestRaw);
|
|
196
|
+
return !!manifest.missing;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Create build combinations grouped by build options hash for efficient bundling
|
|
200
|
+
* @param lambdasEsBuildCommands - Array of Lambda bundle configurations
|
|
201
|
+
* @returns Array of build combinations with hashed build options
|
|
202
|
+
*/
|
|
203
|
+
function createBuildCombinations(lambdasEsBuildCommands) {
|
|
204
|
+
return lambdasEsBuildCommands.map((lambdasEsBuildCommand) => {
|
|
205
|
+
// Create a copy of the command without non-build-related properties
|
|
206
|
+
const copy = {
|
|
207
|
+
...lambdasEsBuildCommand,
|
|
208
|
+
};
|
|
209
|
+
// Remove properties that don't affect the build configuration
|
|
210
|
+
delete copy.outfile;
|
|
211
|
+
delete copy.command;
|
|
212
|
+
delete copy.entryPoint;
|
|
213
|
+
delete copy.out;
|
|
214
|
+
delete copy.commandBeforeBundling;
|
|
215
|
+
delete copy.commandAfterBundling;
|
|
216
|
+
const buildOptions = copy;
|
|
217
|
+
const entryPoint = lambdasEsBuildCommand.entryPoint;
|
|
218
|
+
// Create a hash of build options to group identical configurations
|
|
219
|
+
const buildOptionsHash = crypto
|
|
220
|
+
.createHash('sha256')
|
|
221
|
+
.update(JSON.stringify(buildOptions))
|
|
222
|
+
.digest('hex');
|
|
223
|
+
return {
|
|
224
|
+
buildOptions,
|
|
225
|
+
entryPoint,
|
|
226
|
+
buildOptionsHash,
|
|
227
|
+
};
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Convert esbuildArgs with CLI-style keys into esbuild options object.
|
|
232
|
+
* This normalizes command-line style arguments into the format expected by esbuild.
|
|
233
|
+
* @param esbuildArgs - CLI-style esbuild arguments
|
|
234
|
+
* @returns Normalized esbuild options object
|
|
235
|
+
*/
|
|
236
|
+
export function normalizeEsbuildArgs(esbuildArgs = {}) {
|
|
237
|
+
const out = {};
|
|
238
|
+
for (const [key, value] of Object.entries(esbuildArgs)) {
|
|
239
|
+
const [prefix, name] = key.split(':');
|
|
240
|
+
switch (prefix) {
|
|
241
|
+
case '--alias':
|
|
242
|
+
out.alias ??= {};
|
|
243
|
+
if (name && typeof value === 'string')
|
|
244
|
+
out.alias[name] = value;
|
|
245
|
+
break;
|
|
246
|
+
case '--drop':
|
|
247
|
+
out.drop ??= [];
|
|
248
|
+
if (name)
|
|
249
|
+
out.drop.push(name);
|
|
250
|
+
else if (typeof value === 'string')
|
|
251
|
+
out.drop.push(...value.split(','));
|
|
252
|
+
break;
|
|
253
|
+
case '--pure':
|
|
254
|
+
out.pure ??= [];
|
|
255
|
+
if (name)
|
|
256
|
+
out.pure.push(name);
|
|
257
|
+
else if (typeof value === 'string')
|
|
258
|
+
out.pure.push(...value.split(','));
|
|
259
|
+
break;
|
|
260
|
+
case '--log-override':
|
|
261
|
+
out.logOverride ??= {};
|
|
262
|
+
if (name && typeof value === 'string')
|
|
263
|
+
out.logOverride[name] = value;
|
|
264
|
+
break;
|
|
265
|
+
case '--out-extension':
|
|
266
|
+
out.outExtension ??= {};
|
|
267
|
+
if (name && typeof value === 'string')
|
|
268
|
+
out.outExtension[name] = value;
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Delete all bundling temp folders in the cdk.out folder
|
|
276
|
+
*/
|
|
277
|
+
async function deleteBundlingTempFolders() {
|
|
278
|
+
const rootDir = process.cwd();
|
|
279
|
+
const cdkOutFolder = path.join(rootDir, 'cdk.out');
|
|
280
|
+
try {
|
|
281
|
+
Logger.verbose(`Cleaning bundling temp folders in ${cdkOutFolder}`);
|
|
282
|
+
const bundlingTempFolders = await fs.readdir(cdkOutFolder);
|
|
283
|
+
await Promise.all(bundlingTempFolders.map(async (folder) => {
|
|
284
|
+
const folderPath = path.join(cdkOutFolder, folder);
|
|
285
|
+
if (folder.startsWith('bundling-temp-')) {
|
|
286
|
+
Logger.verbose(`Deleting bundling temp folder: ${folderPath}`);
|
|
287
|
+
await fs.rm(folderPath, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
}));
|
|
290
|
+
Logger.verbose(`Successfully cleaned bundling temp folders`);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
// If cdk.out doesn't exist yet, that's fine - we'll create it later
|
|
294
|
+
if (error.code === 'ENOENT') {
|
|
295
|
+
// cdk.out folder doesn't exist yet, skipping cleanup
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
throw new Error(`Error cleaning bundling temp folders`, { cause: error });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Recreate bundling-temp-*** folders in the cdk.out folder
|
|
304
|
+
* @param lambdasEsBuildCommands
|
|
305
|
+
*/
|
|
306
|
+
async function recreateBundlingTempFolders(lambdasEsBuildCommands) {
|
|
307
|
+
await Promise.all(lambdasEsBuildCommands.map(async (lambdasEsBuildCommand) => {
|
|
308
|
+
const entryOutputFilename = lambdasEsBuildCommand.out.replaceAll('-building', '');
|
|
309
|
+
const target = path.dirname(entryOutputFilename);
|
|
310
|
+
// create folder
|
|
311
|
+
await fs.mkdir(target, { recursive: true });
|
|
312
|
+
Logger.verbose(`Created bundling temp folder: ${target} for ${lambdasEsBuildCommand.entryPoint}`);
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Compile CDK TypeScript/JavaScript code into a single executable file
|
|
317
|
+
* This bundles the CDK code with necessary patches for Lambda function extraction
|
|
318
|
+
* @param options - Compilation options including root directory and entry file
|
|
319
|
+
* @returns Path to the compiled CDK code file
|
|
320
|
+
*/
|
|
321
|
+
async function compileCdk({ rootDir, entryFile, }) {
|
|
322
|
+
const isESM = await isEsm(entryFile);
|
|
323
|
+
// Plugin that:
|
|
324
|
+
// - Fixes __dirname issues in bundled code
|
|
325
|
+
// - Injects code to extract Lambda function configurations from CDK
|
|
326
|
+
const injectCodePlugin = {
|
|
327
|
+
name: 'injectCode',
|
|
328
|
+
setup(build) {
|
|
329
|
+
build.onLoad({ filter: /.*/ }, async (args) => {
|
|
330
|
+
// fix __dirname issues
|
|
331
|
+
const isWindows = /^win/.test(process.platform);
|
|
332
|
+
const esc = (p) => (isWindows ? p.replace(/\\/g, '/') : p);
|
|
333
|
+
const variables = `
|
|
334
|
+
const __fileloc = {
|
|
335
|
+
filename: "${esc(args.path)}",
|
|
336
|
+
dirname: "${esc(path.dirname(args.path))}",
|
|
337
|
+
relativefilename: "${esc(path.relative(rootDir, args.path))}",
|
|
338
|
+
relativedirname: "${esc(path.relative(rootDir, path.dirname(args.path)))}",
|
|
339
|
+
import: { meta: { url: "file://${esc(args.path)}" } }
|
|
340
|
+
};
|
|
341
|
+
`;
|
|
342
|
+
let fileContent = new TextDecoder().decode(await fs.readFile(args.path));
|
|
343
|
+
// remove shebang
|
|
344
|
+
if (fileContent.startsWith('#!')) {
|
|
345
|
+
const firstNewLine = fileContent.indexOf('\n');
|
|
346
|
+
fileContent = fileContent.slice(firstNewLine + 1);
|
|
347
|
+
}
|
|
348
|
+
let contents;
|
|
349
|
+
if (args.path.endsWith('.ts') || args.path.endsWith('.js')) {
|
|
350
|
+
// add the variables at the top of the file, that contains the file location
|
|
351
|
+
contents = `${variables}\n${fileContent}`;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
contents = fileContent;
|
|
355
|
+
}
|
|
356
|
+
// for .mjs files, use js loader
|
|
357
|
+
const fileExtension = args.path.split('.').pop();
|
|
358
|
+
const loader = fileExtension === 'mjs' || fileExtension === 'cjs'
|
|
359
|
+
? 'js'
|
|
360
|
+
: fileExtension;
|
|
361
|
+
// Inject code to extract Lambda function configurations
|
|
362
|
+
if (args.path.includes(path.join('aws-cdk-lib', 'aws-lambda-nodejs', 'lib', 'bundling.'))) {
|
|
363
|
+
contents = contents.replace('return chain([...this.props.commandHooks', 'const command = chain([...this.props.commandHooks');
|
|
364
|
+
const codeToFind = 'afterBundling(options.inputDir,options.outputDir)??[]])';
|
|
365
|
+
if (!contents.includes(codeToFind)) {
|
|
366
|
+
throw new Error(`Can not find code to inject in ${args.path}`);
|
|
367
|
+
}
|
|
368
|
+
// Inject code to get the file path of the Lambda function and CDK hierarchy
|
|
369
|
+
// path to match it with the Lambda function. Store data in the global variable.
|
|
370
|
+
//NOTE: This handles diferent versions of CDK. Newer versions use scope
|
|
371
|
+
// target: this.props.target ?? (typeof scope !== "undefined" ? toTarget(scope,this.props.runtime): toTarget(this.props.runtime)),
|
|
372
|
+
contents = contents.replace(codeToFind, codeToFind +
|
|
373
|
+
`;
|
|
374
|
+
if (process.env.CDK_BOOSTER_INSPECT === 'true') {
|
|
375
|
+
if (!options.outputDir.startsWith('/asset-output')) {
|
|
376
|
+
global.lambdas = global.lambdas ?? [];
|
|
377
|
+
|
|
378
|
+
const out = pathJoin(options.outputDir,outFile);
|
|
379
|
+
|
|
380
|
+
const lambdaInfo = {
|
|
381
|
+
command: command,
|
|
382
|
+
entryPoint: relativeEntryPath,
|
|
383
|
+
out,
|
|
384
|
+
target: this.props.target ?? (typeof scope !== "undefined" ? toTarget(scope,this.props.runtime): toTarget(this.props.runtime)),
|
|
385
|
+
format: this.props.format,
|
|
386
|
+
minify: this.props.minify,
|
|
387
|
+
sourcemap: sourceMapEnabled ? ((this.props.sourceMapMode === 'default' || !this.props.sourceMapMode) ? true : this.props.sourceMapMode) : false,
|
|
388
|
+
sourcesContent,
|
|
389
|
+
external: this.externals,
|
|
390
|
+
loader: this.props.loader,
|
|
391
|
+
define: this.props.define,
|
|
392
|
+
logLevel: this.props.logLevel,
|
|
393
|
+
keepNames: this.props.keepNames,
|
|
394
|
+
tsconfig: this.relativeTsconfigPath ? pathJoin(options.inputDir, this.relativeTsconfigPath): undefined,
|
|
395
|
+
banner: this.props.banner ? { js: this.props.banner } : undefined,
|
|
396
|
+
footer: this.props.footer ? { js: this.props.footer } : undefined,
|
|
397
|
+
mainFields: this.props.mainFields,
|
|
398
|
+
inject: this.props.inject,
|
|
399
|
+
esbuildArgs: this.props.esbuildArgs,
|
|
400
|
+
commandBeforeBundling: chain([...this.props.commandHooks?.beforeBundling(options.inputDir, options.outputDir) ?? [], tscCommand]),
|
|
401
|
+
commandAfterBundling: chain([...(this.props.nodeModules && this.props.commandHooks?.beforeInstall(options.inputDir, options.outputDir)) ?? [], depsCommand, ...this.props.commandHooks?.afterBundling(options.inputDir, options.outputDir) ?? []]),
|
|
402
|
+
environment: this.environment,
|
|
403
|
+
projectRoot: this.projectRoot,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
global.lambdas.push(lambdaInfo);
|
|
407
|
+
|
|
408
|
+
const fs = require('fs');
|
|
409
|
+
const path = require('path');
|
|
410
|
+
const dir = path.dirname(out);
|
|
411
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
412
|
+
fs.writeFileSync(out, '');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return command;
|
|
416
|
+
`);
|
|
417
|
+
const codeToFind3 = 'return(0,util_1().exec)(osPlatform==="win32"?"cmd":"bash",[osPlatform==="win32"?"/c":"-c",localCommand],{env:{...process.env,...environment},stdio:["ignore",process.stderr,"inherit"],cwd,windowsVerbatimArguments:osPlatform==="win32"}),!0';
|
|
418
|
+
contents = contents.replace(codeToFind3, `return (process.env.CDK_BOOSTER_INSPECT === 'true') ? true : (${codeToFind3.replace('return', '')})`);
|
|
419
|
+
Logger.verbose(`Injected code into ${args.path}`);
|
|
420
|
+
}
|
|
421
|
+
else if (args.path.includes(path.join('aws-cdk-lib', 'aws-s3-deployment', 'lib', 'bucket-deployment.'))) {
|
|
422
|
+
const codeToFind = 'super(scope,id),this.requestDestinationArn=!1;';
|
|
423
|
+
if (!contents.includes(codeToFind)) {
|
|
424
|
+
throw new Error(`Can not find code to inject in ${args.path}`);
|
|
425
|
+
}
|
|
426
|
+
// Inject code to prevent deploying the assets
|
|
427
|
+
contents = contents.replace(codeToFind, codeToFind + `return;`);
|
|
428
|
+
Logger.verbose(`Injected code into ${args.path}`);
|
|
429
|
+
}
|
|
430
|
+
else if (args.path.includes(path.join('aws-cdk-lib', 'core', 'lib', 'app.'))) {
|
|
431
|
+
const codeToFind = ',policyValidationBeta1:props.policyValidationBeta1});';
|
|
432
|
+
if (!contents.includes(codeToFind)) {
|
|
433
|
+
throw new Error(`Can not find code to inject in ${args.path}`);
|
|
434
|
+
}
|
|
435
|
+
// make CDK app available
|
|
436
|
+
contents = contents.replace(codeToFind, codeToFind + `global.cdkApp = this;`);
|
|
437
|
+
Logger.verbose(`Injected code into ${args.path}`);
|
|
438
|
+
}
|
|
439
|
+
else if (args.path.includes(path.join('aws-cdk-lib', 'core', 'lib', 'asset-staging.'))) {
|
|
440
|
+
const codeToFind = 'if(fs().existsSync(bundleDir))return;';
|
|
441
|
+
if (!contents.includes(codeToFind)) {
|
|
442
|
+
throw new Error(`Can not find code to inject in ${args.path}`);
|
|
443
|
+
}
|
|
444
|
+
// Inject code to get the file path of the Lambda function and CDK hierarchy
|
|
445
|
+
contents = contents.replace(codeToFind, `
|
|
446
|
+
if (process.env.CDK_BOOSTER_SKIP === 'true') {
|
|
447
|
+
console.log('[🚀 CDK Booster]', "Skipping asset bundling");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if(fs().existsSync(bundleDir)) {
|
|
451
|
+
if (process.env.CDK_BOOSTER_INSPECT !== 'true') {
|
|
452
|
+
console.log('[🚀 CDK Booster]', "😀 Function " + options.relativeEntryPath + " was prebundled");
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (process.env.CDK_BOOSTER_INSPECT !== 'true') {
|
|
457
|
+
console.error('[🚀 CDK Booster]', "🚨 Function " + options.relativeEntryPath + " was not prebundled");
|
|
458
|
+
}
|
|
459
|
+
`);
|
|
460
|
+
Logger.verbose(`Injected code into ${args.path}`);
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
contents,
|
|
464
|
+
loader,
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
const compileCodeFile = path.join(getProjectDirname(), outputFolder, `compiledCdk.${isESM ? 'mjs' : 'cjs'}`);
|
|
470
|
+
try {
|
|
471
|
+
// Build CDK code
|
|
472
|
+
await esbuild.build({
|
|
473
|
+
entryPoints: [entryFile],
|
|
474
|
+
bundle: true,
|
|
475
|
+
platform: 'node',
|
|
476
|
+
keepNames: true,
|
|
477
|
+
outfile: compileCodeFile,
|
|
478
|
+
sourcemap: false,
|
|
479
|
+
plugins: [injectCodePlugin],
|
|
480
|
+
...(isESM
|
|
481
|
+
? {
|
|
482
|
+
format: 'esm',
|
|
483
|
+
target: 'esnext',
|
|
484
|
+
mainFields: ['module', 'main'],
|
|
485
|
+
banner: {
|
|
486
|
+
js: [
|
|
487
|
+
`import { createRequire as topLevelCreateRequire } from 'module';`,
|
|
488
|
+
`global.require = global.require ?? topLevelCreateRequire(import.meta.url);`,
|
|
489
|
+
`import { fileURLToPath as topLevelFileUrlToPath, URL as topLevelURL } from "url"`,
|
|
490
|
+
`global.__dirname = global.__dirname ?? topLevelFileUrlToPath(new topLevelURL(".", import.meta.url))`,
|
|
491
|
+
].join('\n'),
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
: {
|
|
495
|
+
format: 'cjs',
|
|
496
|
+
target: 'node18',
|
|
497
|
+
}),
|
|
498
|
+
define: {
|
|
499
|
+
// replace __dirname,... with the a variable that contains the file location
|
|
500
|
+
__filename: '__fileloc.filename',
|
|
501
|
+
__dirname: '__fileloc.dirname',
|
|
502
|
+
__relativefilename: '__fileloc.relativefilename',
|
|
503
|
+
__relativedirname: '__fileloc.relativedirname',
|
|
504
|
+
'import.meta.url': '__fileloc.import.meta.url',
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
throw new Error(`Error building CDK code: ${error.message}`, {
|
|
510
|
+
cause: error,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
return compileCodeFile;
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Determine if the project uses ES modules based on package.json configuration
|
|
517
|
+
* @param entryFile - Path to the entry file
|
|
518
|
+
* @returns True if the project uses ES modules, false otherwise
|
|
519
|
+
*/
|
|
520
|
+
async function isEsm(entryFile) {
|
|
521
|
+
let isESM = false;
|
|
522
|
+
const packageJsonPath = await findPackageJson(entryFile);
|
|
523
|
+
if (packageJsonPath) {
|
|
524
|
+
try {
|
|
525
|
+
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, { encoding: 'utf-8' }));
|
|
526
|
+
if (packageJson.type === 'module') {
|
|
527
|
+
isESM = true;
|
|
528
|
+
Logger.verbose(`Using ES modules format`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
Logger.error(`Error reading CDK package.json (${packageJsonPath}): ${err.message}`, err);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return isESM;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Execute commands for Lambda bundles before or after bundling
|
|
539
|
+
* @param lambdasBundle - Array of Lambda bundle configurations
|
|
540
|
+
* @param commandPick - Which command to execute ('commandBeforeBundling' or 'commandAfterBundling')
|
|
541
|
+
*/
|
|
542
|
+
async function executeCommands(lambdasBundle, commandPick) {
|
|
543
|
+
// Filter bundles that have the specified command
|
|
544
|
+
const commandsToExecute = lambdasBundle.filter((lambdasEsBuildCommand) => lambdasEsBuildCommand[commandPick]);
|
|
545
|
+
if (commandsToExecute.length === 0) {
|
|
546
|
+
Logger.verbose(`No commands to execute for ${commandPick}, skipping`);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
// Execute all commands in parallel
|
|
550
|
+
const promises = commandsToExecute.map(async (lambdasEsBuildCommand) => {
|
|
551
|
+
let command = lambdasEsBuildCommand[commandPick];
|
|
552
|
+
const environment = lambdasEsBuildCommand.environment;
|
|
553
|
+
const projectRoot = lambdasEsBuildCommand.projectRoot;
|
|
554
|
+
// Remove '-building' suffix from paths in commands
|
|
555
|
+
command = command.replaceAll('-building', '');
|
|
556
|
+
Logger.verbose(`Executing command for ${lambdasEsBuildCommand.entryPoint}: ${command}`);
|
|
557
|
+
const osPlatform = os.platform();
|
|
558
|
+
try {
|
|
559
|
+
const { stdout, stderr } = await spawnAsync(osPlatform === 'win32' ? 'cmd' : 'bash', [osPlatform === 'win32' ? '/c' : '-c', command], {
|
|
560
|
+
env: { ...process.env, ...environment },
|
|
561
|
+
cwd: projectRoot ?? process.cwd(),
|
|
562
|
+
windowsVerbatimArguments: osPlatform === 'win32',
|
|
563
|
+
});
|
|
564
|
+
if (stdout) {
|
|
565
|
+
Logger.log(`Command stdout: ${stdout}`);
|
|
566
|
+
}
|
|
567
|
+
if (stderr) {
|
|
568
|
+
Logger.log(`Command stderr: ${stderr}`);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
throw new Error(`Command execution failed for ${lambdasEsBuildCommand.entryPoint}: ${error.message}`, { cause: error });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
await Promise.all(promises);
|
|
576
|
+
if (promises.length > 0) {
|
|
577
|
+
Logger.log(`All ${commandPick === 'commandBeforeBundling' ? 'before bundling' : 'after bundling'} commands executed successfully`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Async wrapper for spawning child processes
|
|
582
|
+
* @param command - The command to run
|
|
583
|
+
* @param args - The arguments to pass to the command
|
|
584
|
+
* @param options - Options to configure the child process
|
|
585
|
+
* @returns A promise that resolves with the command output or rejects with an error
|
|
586
|
+
*/
|
|
587
|
+
export async function spawnAsync(command, args = [], options = {}) {
|
|
588
|
+
return new Promise((resolve, reject) => {
|
|
589
|
+
const child = spawn(command, args, { ...options, stdio: 'pipe' });
|
|
590
|
+
let stdout = '';
|
|
591
|
+
let stderr = '';
|
|
592
|
+
child.stdout?.on('data', (chunk) => {
|
|
593
|
+
stdout += chunk.toString();
|
|
594
|
+
});
|
|
595
|
+
child.stderr?.on('data', (chunk) => {
|
|
596
|
+
stderr += chunk.toString();
|
|
597
|
+
});
|
|
598
|
+
child.on('error', reject);
|
|
599
|
+
child.on('close', (code) => {
|
|
600
|
+
if (code !== 0) {
|
|
601
|
+
reject(new Error(`Command failed with exit code ${code}: ${command} ${args.join(' ')}\nstderr: ${stderr}\nstdout: ${stdout}`));
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
resolve({
|
|
605
|
+
stdout,
|
|
606
|
+
stderr,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Run CDK code in a node worker thread and extract Lambda function configurations
|
|
614
|
+
* This isolates the CDK execution to prevent interference with the main process
|
|
615
|
+
* @param config - CDK Booster configuration
|
|
616
|
+
* @param compileCodeFile - Path to the compiled CDK code file
|
|
617
|
+
* @returns Array of Lambda function configurations found in the CDK code
|
|
618
|
+
*/
|
|
619
|
+
async function runCdkCodeAndReturnLambdas({ config, compileCodeFile, }) {
|
|
620
|
+
Logger.verbose(`Running CDK code in worker thread to extract Lambda configurations`);
|
|
621
|
+
const workerResults = await new Promise((resolve, reject) => {
|
|
622
|
+
const workerPath = pathToFileURL(path.resolve(path.join(getModuleDirname(), 'cdkFrameworkWorker.mjs'))).href;
|
|
623
|
+
Logger.verbose(`Starting worker thread from: ${workerPath}`);
|
|
624
|
+
const worker = new Worker(new URL(workerPath), {
|
|
625
|
+
workerData: {
|
|
626
|
+
verbose: config.verbose,
|
|
627
|
+
projectDirname: getProjectDirname(),
|
|
628
|
+
moduleDirname: getModuleDirname(),
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
// Handle successful completion
|
|
632
|
+
worker.on('message', async (message) => {
|
|
633
|
+
Logger.verbose(`Worker completed successfully, found ${message.length} Lambda functions`);
|
|
634
|
+
resolve(message);
|
|
635
|
+
await worker.terminate();
|
|
636
|
+
});
|
|
637
|
+
// Handle worker errors
|
|
638
|
+
worker.on('error', (error) => {
|
|
639
|
+
Logger.error(`Worker error: ${error.message}`, error);
|
|
640
|
+
reject(new Error(`Error running CDK code in worker: ${error.message}`, {
|
|
641
|
+
cause: error,
|
|
642
|
+
}));
|
|
643
|
+
});
|
|
644
|
+
// Handle worker exit
|
|
645
|
+
// worker.on('exit', (code) => {
|
|
646
|
+
// if (code !== 0) {
|
|
647
|
+
// const errorMessage = `CDK worker stopped with exit code ${code}`;
|
|
648
|
+
// Logger.error(`${errorMessage}`);
|
|
649
|
+
// reject(new Error(errorMessage));
|
|
650
|
+
// } else {
|
|
651
|
+
// Logger.verbose(`Worker exited successfully`);
|
|
652
|
+
// }
|
|
653
|
+
// });
|
|
654
|
+
// Forward worker stdout to main process
|
|
655
|
+
// worker.stdout.on('data', (data: Buffer) => {
|
|
656
|
+
// Logger.log(data.toString().trim());
|
|
657
|
+
// });
|
|
658
|
+
// Forward worker stderr to main process
|
|
659
|
+
// worker.stderr.on('data', (data: Buffer) => {
|
|
660
|
+
// Logger.verbose(data.toString().trim());
|
|
661
|
+
// });
|
|
662
|
+
// Send the compiled code file path to the worker for execution
|
|
663
|
+
Logger.verbose(`Sending compiled code file to worker: ${compileCodeFile}`);
|
|
664
|
+
worker.postMessage({
|
|
665
|
+
compileOutput: compileCodeFile,
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
Logger.verbose(`Successfully extracted ${workerResults.lambdas.length} Lambda function configurations from CDK code`);
|
|
669
|
+
const lambdas = workerResults.lambdas;
|
|
670
|
+
return { lambdas, missing: workerResults.missing };
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Recursively copies a folder from source to destination,
|
|
674
|
+
* deleting the destination folder first.
|
|
675
|
+
* @param src - The source folder path
|
|
676
|
+
* @param dest - The destination folder path
|
|
677
|
+
* @param entryOutputFilename - The expected output filename pattern for fixing extensions
|
|
678
|
+
*/
|
|
679
|
+
async function copyFolderRecursive(src, dest, entryOutputFilename) {
|
|
680
|
+
if (!existsSync(dest)) {
|
|
681
|
+
await fs.mkdir(dest, { recursive: true });
|
|
682
|
+
}
|
|
683
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
684
|
+
const entryDir = path.dirname(entryOutputFilename);
|
|
685
|
+
const entryBasename = path.basename(entryOutputFilename, path.extname(entryOutputFilename));
|
|
686
|
+
const entryExt = path.extname(entryOutputFilename);
|
|
687
|
+
for (const entry of entries) {
|
|
688
|
+
const srcPath = path.join(src, entry.name);
|
|
689
|
+
let destPath = path.join(dest, entry.name);
|
|
690
|
+
// Fix extension if destPath matches entryOutputFilename pattern but has different extension
|
|
691
|
+
const destBasename = path.basename(destPath, path.extname(destPath));
|
|
692
|
+
if (path.dirname(destPath) === entryDir &&
|
|
693
|
+
destBasename === entryBasename &&
|
|
694
|
+
path.extname(destPath) !== entryExt) {
|
|
695
|
+
const srcExt = path.extname(srcPath);
|
|
696
|
+
const fixedExt = srcExt.endsWith('.map') ? `${entryExt}.map` : entryExt;
|
|
697
|
+
destPath = path.join(entryDir, `${entryBasename}${fixedExt}`);
|
|
698
|
+
Logger.verbose(`Fixing extension from ${srcExt} to ${fixedExt}, destPath: ${destPath}`);
|
|
699
|
+
}
|
|
700
|
+
if (entry.isDirectory()) {
|
|
701
|
+
await copyFolderRecursive(srcPath, destPath, entryOutputFilename);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
await fs.copyFile(srcPath, destPath);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
export function getModuleRoot(moduleName) {
|
|
709
|
+
const require = createRequire(import.meta.url);
|
|
710
|
+
const modulePath = require.resolve(moduleName);
|
|
711
|
+
let dir = dirname(modulePath);
|
|
712
|
+
while (!existsSync(resolve(dir, 'package.json'))) {
|
|
713
|
+
const parent = dirname(dir);
|
|
714
|
+
if (parent === dir)
|
|
715
|
+
break; // Reached filesystem root
|
|
716
|
+
dir = parent;
|
|
717
|
+
}
|
|
718
|
+
return dir;
|
|
719
|
+
}
|
|
720
|
+
export function getProjectRoot() {
|
|
721
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
722
|
+
while (!existsSync(resolve(dir, 'package.json'))) {
|
|
723
|
+
const parent = dirname(dir);
|
|
724
|
+
if (parent === dir)
|
|
725
|
+
break; // Reached filesystem root
|
|
726
|
+
dir = parent;
|
|
727
|
+
}
|
|
728
|
+
return dir;
|
|
729
|
+
}
|
|
730
|
+
run().catch((error) => {
|
|
731
|
+
Logger.error(error);
|
|
732
|
+
process.exit(1);
|
|
733
|
+
});
|