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.
Files changed (53) hide show
  1. package/LICENSE +353 -0
  2. package/LICENSE.md +350 -0
  3. package/README.md +114 -0
  4. package/dist/cdk-booster.d.ts +29 -0
  5. package/dist/cdk-booster.mjs +733 -0
  6. package/dist/cdkFrameworkWorker.mjs +50 -0
  7. package/dist/configuration.d.ts +16 -0
  8. package/dist/configuration.mjs +29 -0
  9. package/dist/constants.d.ts +1 -0
  10. package/dist/constants.mjs +3 -0
  11. package/dist/getConfigFromCliArgs.d.ts +7 -0
  12. package/dist/getConfigFromCliArgs.mjs +23 -0
  13. package/dist/getDirname.d.ts +10 -0
  14. package/dist/getDirname.mjs +19 -0
  15. package/dist/logger.d.ts +51 -0
  16. package/dist/logger.mjs +83 -0
  17. package/dist/types/bundleSettings.d.ts +6 -0
  18. package/dist/types/bundleSettings.mjs +1 -0
  19. package/dist/types/cbConfig.d.ts +8 -0
  20. package/dist/types/cbConfig.mjs +1 -0
  21. package/dist/types/lambdaBundle.d.ts +11 -0
  22. package/dist/types/lambdaBundle.mjs +1 -0
  23. package/dist/utils/findPackageJson.d.ts +6 -0
  24. package/dist/utils/findPackageJson.mjs +33 -0
  25. package/dist/version.d.ts +5 -0
  26. package/dist/version.mjs +25 -0
  27. package/node_modules/chalk/license +9 -0
  28. package/node_modules/chalk/package.json +83 -0
  29. package/node_modules/chalk/readme.md +297 -0
  30. package/node_modules/chalk/source/index.d.ts +325 -0
  31. package/node_modules/chalk/source/index.js +225 -0
  32. package/node_modules/chalk/source/utilities.js +33 -0
  33. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  34. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  35. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  36. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  37. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  38. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  39. package/node_modules/commander/LICENSE +22 -0
  40. package/node_modules/commander/Readme.md +1159 -0
  41. package/node_modules/commander/esm.mjs +16 -0
  42. package/node_modules/commander/index.js +24 -0
  43. package/node_modules/commander/lib/argument.js +149 -0
  44. package/node_modules/commander/lib/command.js +2778 -0
  45. package/node_modules/commander/lib/error.js +39 -0
  46. package/node_modules/commander/lib/help.js +747 -0
  47. package/node_modules/commander/lib/option.js +379 -0
  48. package/node_modules/commander/lib/suggestSimilar.js +101 -0
  49. package/node_modules/commander/package-support.json +16 -0
  50. package/node_modules/commander/package.json +82 -0
  51. package/node_modules/commander/typings/esm.d.mts +3 -0
  52. package/node_modules/commander/typings/index.d.ts +1113 -0
  53. 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
+ });