lambda-live-debugger
Version:
Debug Lambda functions locally like it is running in the cloud
187 lines (186 loc) • 7.87 kB
JavaScript
import * as path from 'path';
import * as fs from 'fs/promises';
import * as esbuild from 'esbuild';
import { Configuration } from './configuration.mjs';
import { Logger } from './logger.mjs';
import { getProjectDirname } from './getDirname.mjs';
import { outputFolder } from './constants.mjs';
import { combineArray } from './utils/combineArray.mjs';
import { combineObject } from './utils/combineObject.mjs';
import { combineObjectStrings } from './utils/combineObjectStrings.mjs';
import { removeUndefinedProperties } from './utils/removeUndefinedProperties.mjs';
const buildCache = {};
/**
* Get the build for the function
* @param functionId
* @returns
*/
async function getBuild(functionId) {
try {
let newBuild = false;
const func = await Configuration.getLambda(functionId);
// if handler is a JavaScript file and not force bundle, just return the file
if ((func.codePath.endsWith('.js') ||
func.codePath.endsWith('.mjs') ||
func.codePath.endsWith('.cjs')) &&
!func.forceBundle) {
return func.codePath;
}
// uses promise to avoid multiple parallel builds for the same function
let buildAssets;
if (buildCache[functionId] && buildCache[functionId].current) {
// use existing build
buildAssets = buildCache[functionId];
Logger.verbose(`[Function ${functionId}] Using existing build`);
}
else {
Logger.verbose(`[Function ${functionId}] No existing build found, building...`);
newBuild = true;
const newBuildAssets = build({
functionId,
function: func,
oldCtx: buildCache[functionId]
? await buildCache[functionId].ctx
: undefined,
});
buildAssets = {
result: newBuildAssets.then((b) => b.result),
ctx: newBuildAssets.then((b) => b.ctx),
current: true,
};
buildCache[functionId] = buildAssets;
}
const result = await buildAssets.result;
if (newBuild) {
Logger.verbose(`[Function ${functionId}] Build complete`);
}
const artifactFile = Object.keys(result.metafile.outputs).find((key) => key.endsWith('.js'));
if (!artifactFile) {
throw new Error(`Artifact file not found for function ${functionId}`);
}
return path.join(getProjectDirname(), artifactFile);
}
catch (error) {
throw new Error(`Error building function ${functionId}: ${error.message}`, {
cause: error,
});
}
}
/**
* Build the function
* @param input
* @returns
*/
async function build(input) {
const targetFolder = path.join(getProjectDirname(), `${outputFolder}/artifacts`, input.functionId);
await fs.rm(targetFolder, { recursive: true, force: true });
await fs.mkdir(targetFolder, { recursive: true });
const esbuildOptions = removeUndefinedProperties(input.function.esBuildOptions);
const handlerCodePath = input.function.codePath;
// get module type from package.json
const packageJsonPath = input.function.packageJsonPath;
let isESMFromPackageJson = false;
if (packageJsonPath) {
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, { encoding: 'utf-8' }));
isESMFromPackageJson = packageJson.type === 'module';
}
const isESMFromBundling = esbuildOptions?.format === 'esm' ? true : undefined;
let isESM;
if (isESMFromPackageJson !== undefined &&
isESMFromBundling !== undefined &&
isESMFromPackageJson !== isESMFromBundling) {
Logger.warn(`WARNING! Mismatch module type between package.json and bundling options for ${handlerCodePath}. Package.json: ${isESMFromPackageJson ? 'ESM' : 'CJS'}, bundling options: ${isESMFromBundling ? 'ESM' : 'CJS'}. Using ${isESMFromBundling ? 'ESM' : 'CJS'} from bunding otions.`);
isESM = isESMFromBundling;
}
else if (isESMFromPackageJson !== undefined) {
isESM = isESMFromPackageJson;
}
else if (isESMFromBundling !== undefined) {
isESM = isESMFromBundling;
}
else {
isESM = false;
}
let ctx = input.oldCtx;
Logger.verbose(`[Function ${input.functionId}] Module type: ${isESM ? 'ESM' : 'CJS'}`);
if (!ctx) {
const optionsDefault = {
entryPoints: [handlerCodePath],
platform: 'node',
keepNames: true,
bundle: true,
logLevel: 'silent',
metafile: true,
...(isESM
? {
format: 'esm',
target: 'esnext',
mainFields: ['module', 'main'],
banner: {
js: [
`import { createRequire as topLevelCreateRequire } from 'module';`,
`global.require = global.require ?? topLevelCreateRequire(import.meta.url);`,
`import { fileURLToPath as topLevelFileUrlToPath, URL as topLevelURL } from "url"`,
`global.__dirname = global.__dirname ?? topLevelFileUrlToPath(new topLevelURL(".", import.meta.url))`,
].join('\n'),
},
}
: {
format: 'cjs',
target: 'node14',
}),
outdir: targetFolder,
sourcemap: 'linked',
};
const options = {
...optionsDefault,
...esbuildOptions,
external: combineArray(optionsDefault.external, esbuildOptions?.external),
alias: combineObject(optionsDefault.alias, esbuildOptions?.alias),
loader: combineObject(optionsDefault.loader, esbuildOptions?.loader),
resolveExtensions: combineArray(optionsDefault.resolveExtensions, esbuildOptions?.resolveExtensions),
mainFields: combineArray(optionsDefault.mainFields, esbuildOptions?.mainFields),
conditions: combineArray(optionsDefault.conditions, esbuildOptions?.conditions),
outExtension: combineObject(optionsDefault.outExtension, esbuildOptions?.outExtension),
banner: combineObjectStrings(optionsDefault.banner, esbuildOptions?.banner),
footer: combineObjectStrings(optionsDefault.footer, esbuildOptions?.footer),
plugins: combineArray(optionsDefault.plugins, esbuildOptions?.plugins),
nodePaths: combineArray(optionsDefault.nodePaths, esbuildOptions?.nodePaths),
};
// remove all undefined values just to make it cleaner
removeUndefinedProperties(options);
if (Configuration.config.verbose) {
Logger.verbose(`[Function ${input.functionId}] Building ${handlerCodePath} with options:`, JSON.stringify(options, null, 2));
}
else {
Logger.log(`[Function ${input.functionId}] Building ${handlerCodePath}`);
}
ctx = await esbuild.context(options);
}
const result = await ctx.rebuild();
if (input.function.packageJsonPath) {
const from = path.resolve(input.function.packageJsonPath);
Logger.verbose(`[Function ${input.functionId}] package.json: ${from}`);
const to = path.resolve(path.join(targetFolder, 'package.json'));
await fs.copyFile(from, to);
}
else {
Logger.verbose(`[Function ${input.functionId}] No package.json found`);
}
return {
ctx: ctx,
result: result,
};
}
/**
* Mark all builds as old
*/
function markAllBuildAsOld() {
for (const key in buildCache) {
buildCache[key].current = false;
}
}
export const NodeEsBuild = {
getBuild,
markAllBuildAsOld,
};