lambda-live-debugger
Version:
Debug Lambda functions locally like it is running in the cloud
211 lines (210 loc) • 8.51 kB
JavaScript
import * as fs from 'fs/promises';
import * as path from 'path';
import { constants } from 'fs';
import toml from 'toml';
import * as yaml from 'yaml';
import { findPackageJson } from '../utils/findPackageJson.mjs';
import { CloudFormation } from '../cloudFormation.mjs';
import { Logger } from '../logger.mjs';
/**
* Support for AWS SAM framework
*/
export class SamFramework {
/**
* Framework name
*/
get name() {
return 'sam';
}
/**
* Can this class handle the current project
* @returns
*/
async canHandle(config) {
const { samConfigFile, samTemplateFile } = this.getConfigFiles(config);
try {
await fs.access(samConfigFile, constants.F_OK);
}
catch {
Logger.verbose(`[SAM] This is not a SAM framework project. ${samConfigFile} not found.`);
return false;
}
try {
await fs.access(samTemplateFile, constants.F_OK);
}
catch {
Logger.verbose(`[SAM] This is not a SAM framework project. ${samTemplateFile} not found.`);
return false;
}
return true;
}
/**
* Get configuration files
* @param config Configuration
* @returns Configuration files
*/
getConfigFiles(config) {
const samConfigFile = config.samConfigFile ?? 'samconfig.toml';
const samTemplateFile = config.samTemplateFile ?? 'template.yaml';
return {
samConfigFile: path.resolve(samConfigFile),
samTemplateFile: path.resolve(samTemplateFile),
};
}
/**
* Get Lambda functions
* @param config Configuration
* @returns Lambda functions
*/
async getLambdas(config) {
const awsConfiguration = {
region: config.region,
profile: config.profile,
role: config.role,
};
const { samConfigFile, samTemplateFile } = this.getConfigFiles(config);
const environment = config.configEnv ?? 'default';
const samConfigContent = await fs.readFile(samConfigFile, 'utf-8');
let samConfig;
// is toml extension
if (samConfigFile.endsWith('.toml')) {
samConfig = toml.parse(samConfigContent);
}
else {
samConfig = yaml.parse(samConfigContent);
}
let stackName;
if (config.samStackName) {
stackName = config.samStackName;
}
else {
stackName = samConfig[environment]?.global?.parameters?.stack_name;
}
if (!stackName) {
throw new Error(`Stack name not found in ${samConfigFile}`);
}
const lambdas = await this.parseLambdasFromTemplate(samTemplateFile, stackName);
const lambdasDiscovered = [];
Logger.verbose(`[SAM] Found Lambdas`, JSON.stringify(lambdas, null, 2));
const lambdasInStack = await CloudFormation.getLambdasInStack(stackName, awsConfiguration);
Logger.verbose(`[SAM] Found Lambdas in stack ${stackName}:`, JSON.stringify(lambdasInStack, null, 2));
// get tags for each Lambda
for (const func of lambdas) {
const handlerFull = path.join(func.codeUri ?? '', func.handler);
const handlerParts = handlerFull.split('.');
const handler = handlerParts[1];
const functionName = lambdasInStack.find((lambda) => lambda.logicalId === func.name &&
lambda.stackLogicalId === func.stackLogicalId)?.lambdaName;
if (!functionName) {
throw new Error(`Function name not found for function: ${func.name}`);
}
let esBuildOptions = undefined;
let codePath;
if (func.buildMethod?.toLowerCase() === 'esbuild') {
if (func.entryPoints && func.entryPoints.length > 0) {
codePath = path.join(func.codeUri ?? '', func.entryPoints[0]);
}
esBuildOptions = {
external: func.external,
minify: func.minify,
format: func.format,
target: func.target,
};
}
if (!codePath) {
const fileWithExtension = handlerParts[0];
const possibleCodePathsTs = `${fileWithExtension}.ts`;
const possibleCodePathsJs = `${fileWithExtension}.js`;
const possibleCodePaths = [
possibleCodePathsTs,
possibleCodePathsJs,
`${fileWithExtension}.cjs`,
`${fileWithExtension}.mjs`,
];
for (const cp of possibleCodePaths) {
try {
await fs.access(cp, constants.F_OK);
codePath = cp;
break;
}
catch {
// ignore, file not found
}
}
if (!codePath) {
codePath = possibleCodePathsJs;
Logger.warn(`[Function ${functionName}] Can not find code path for handler: ${handlerFull}. Using fallback: ${codePath}`);
}
}
const packageJsonPath = await findPackageJson(codePath);
Logger.verbose(`[SAM] package.json path: ${packageJsonPath}`);
const lambdaResource = {
functionName,
codePath,
handler,
packageJsonPath,
esBuildOptions,
metadata: {
framework: 'sam',
stackName,
},
};
lambdasDiscovered.push(lambdaResource);
}
return lambdasDiscovered;
}
/**
* Recursively parse templates to find all Lambda functions, including nested stacks
* @param templatePath The path to the CloudFormation/SAM template file
* @param stackName The name of the stack this template belongs to (for nested stacks)
*/
async parseLambdasFromTemplate(templatePath, stackName) {
const resolvedTemplatePath = path.resolve(templatePath);
const templateDir = path.dirname(resolvedTemplatePath);
let template;
try {
const templateContent = await fs.readFile(resolvedTemplatePath, 'utf-8');
template = yaml.parse(templateContent);
}
catch (err) {
Logger.warn(`[SAM] Could not read or parse template at ${templatePath}: ${err.message}`);
return [];
}
if (!template.Resources) {
return [];
}
const lambdas = [];
for (const resourceName in template.Resources) {
const resource = template.Resources[resourceName];
// Check if it's a Lambda function
if (resource.Type === 'AWS::Serverless::Function') {
lambdas.push({
templatePath,
name: resourceName,
codeUri: resource.Properties?.CodeUri,
handler: resource.Properties?.Handler,
buildMethod: resource.Metadata?.BuildMethod,
entryPoints: resource.Metadata?.BuildProperties?.EntryPoints,
external: resource.Metadata?.BuildProperties?.External,
minify: resource.Metadata?.BuildProperties?.Minify,
format: resource.Metadata?.BuildProperties?.Format,
target: resource.Metadata?.BuildProperties?.Target,
stackLogicalId: stackName,
});
}
// Check if it's a nested stack
else if (resource.Type === 'AWS::Serverless::Application' ||
resource.Type === 'AWS::CloudFormation::Stack') {
const nestedTemplateLocation = resource.Properties?.Location ?? resource.Properties?.TemplateURL;
if (nestedTemplateLocation) {
const nestedTemplatePath = path.resolve(templateDir, nestedTemplateLocation);
const nestedLambdas = await this.parseLambdasFromTemplate(nestedTemplatePath, resourceName);
lambdas.push(...nestedLambdas);
}
}
}
Logger.verbose(JSON.stringify(lambdas, null, 2));
return lambdas;
}
}
export const samFramework = new SamFramework();