aws-sam-webpack-plugin
Version:
Webpack plugin to replace the AWS SAM CLI build command
252 lines (251 loc) • 14.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const yaml_cfn_1 = require("yaml-cfn");
const js_yaml_1 = __importDefault(require("js-yaml"));
class AwsSamPlugin {
constructor(options) {
this.options = Object.assign({ projects: { default: "." }, outFile: "app", vscodeDebug: true }, options);
this.samConfigs = [];
}
// Returns the name of the SAM template file or null if it's not found
findTemplateName(prefix) {
for (const f of AwsSamPlugin.defaultTemplates) {
const template = `${prefix}/${f}`;
if (fs.existsSync(template)) {
return template;
}
}
return null;
}
// Returns a webpack entry object based on the SAM template
entryFor(projectKey, projectPath, projectTemplateName, projectTemplate, outFile) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const entryPoints = {};
const launchConfigs = [];
const samConfigs = [];
const samConfig = js_yaml_1.default.load(projectTemplate, { filename: projectTemplateName, schema: yaml_cfn_1.schema });
const defaultRuntime = (_c = (_b = (_a = samConfig.Globals) === null || _a === void 0 ? void 0 : _a.Function) === null || _b === void 0 ? void 0 : _b.Runtime) !== null && _c !== void 0 ? _c : null;
const defaultHandler = (_f = (_e = (_d = samConfig.Globals) === null || _d === void 0 ? void 0 : _d.Function) === null || _e === void 0 ? void 0 : _e.Handler) !== null && _f !== void 0 ? _f : null;
const defaultCodeUri = (_j = (_h = (_g = samConfig.Globals) === null || _g === void 0 ? void 0 : _g.Function) === null || _h === void 0 ? void 0 : _h.CodeUri) !== null && _j !== void 0 ? _j : null;
// Loop through all of the resources
for (const resourceKey in samConfig.Resources) {
const resource = samConfig.Resources[resourceKey];
const buildRoot = projectPath === "" ? `.aws-sam/build` : `${projectPath}/.aws-sam/build`;
// Correct paths for files that can be uploaded using "aws couldformation package"
if (resource.Type === "AWS::ApiGateway::RestApi" && typeof resource.Properties.BodyS3Location === "string") {
samConfig.Resources[resourceKey].Properties.BodyS3Location = path.relative(buildRoot, resource.Properties.BodyS3Location);
}
if (resource.Type === "AWS::Lambda::Function" && typeof resource.Properties.Code === "string") {
samConfig.Resources[resourceKey].Properties.Code = path.relative(buildRoot, resource.Properties.Code);
}
if (resource.Type === "AWS::AppSync::GraphQLSchema" &&
typeof resource.Properties.DefinitionS3Location === "string" &&
resource.Properties.DefinitionS3Location.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.DefinitionS3Location = path.relative(buildRoot, resource.Properties.DefinitionS3Location);
}
if (resource.Type === "AWS::AppSync::Resolver" &&
typeof resource.Properties.RequestMappingTemplateS3Location === "string" &&
resource.Properties.RequestMappingTemplateS3Location.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.RequestMappingTemplateS3Location = path.relative(buildRoot, resource.Properties.RequestMappingTemplateS3Location);
}
if (resource.Type === "AWS::AppSync::Resolver" &&
typeof resource.Properties.ResponseMappingTemplateS3Location === "string" &&
resource.Properties.ResponseMappingTemplateS3Location.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.ResponseMappingTemplateS3Location = path.relative(buildRoot, resource.Properties.ResponseMappingTemplateS3Location);
}
if (resource.Type === "AWS::Serverless::Api" &&
typeof resource.Properties.DefinitionUri === "string" &&
resource.Properties.DefinitionUri.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.DefinitionUri = path.relative(buildRoot, resource.Properties.DefinitionUri);
}
if (resource.Type === "AWS::Include" &&
typeof resource.Properties.Location === "string" &&
resource.Properties.Location.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.Location = path.relative(buildRoot, resource.Properties.Location);
}
if (resource.Type === "AWS::ElasticBeanstalk::ApplicationVersion" &&
typeof resource.Properties.SourceBundle === "string" &&
resource.Properties.SourceBundle.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.SourceBundle = path.relative(buildRoot, resource.Properties.SourceBundle);
}
if (resource.Type === "AWS::CloudFormation::Stack" &&
typeof resource.Properties.TemplateURL === "string" &&
resource.Properties.TemplateURL.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.TemplateURL = path.relative(buildRoot, resource.Properties.TemplateURL);
}
if (resource.Type === "AWS::Glue::Job" &&
resource.Properties.Command &&
typeof resource.Properties.Command.ScriptLocation === "string" &&
resource.Properties.Command.ScriptLocation.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.Command.ScriptLocation = path.relative(buildRoot, resource.Properties.Command.ScriptLocation);
}
if (resource.Type === "AWS::StepFunctions::StateMachine" &&
typeof resource.Properties.DefinitionS3Location === "string" &&
resource.Properties.DefinitionS3Location.startsWith("s3://") === false) {
samConfig.Resources[resourceKey].Properties.DefinitionS3Location = path.relative(buildRoot, resource.Properties.DefinitionS3Location);
}
// Find all of the functions
if (resource.Type === "AWS::Serverless::Function") {
const properties = resource.Properties;
if (!properties) {
throw new Error(`${resourceKey} is missing Properties`);
}
// Check the runtime is supported
if (!["nodejs18.x", "nodejs20.x", "nodejs22.x"].includes((_k = properties.Runtime) !== null && _k !== void 0 ? _k : defaultRuntime)) {
throw new Error(`${resourceKey} has an unsupport Runtime. Must be nodejs18.x, nodejs20.x or nodejs22.x`);
}
// Continue with a warning if they're using inline code
if (properties.InlineCode) {
console.log(`WARNING: This plugin does not compile inline code. The InlineCode for '${resourceKey}' will be copied 'as is'.`);
continue;
}
// Check we have a valid handler
const handler = (_l = properties.Handler) !== null && _l !== void 0 ? _l : defaultHandler;
if (!handler) {
throw new Error(`${resourceKey} is missing a Handler`);
}
const handlerComponents = handler.split(".");
if (handlerComponents.length !== 2) {
throw new Error(`${resourceKey} Handler must contain exactly one "."`);
}
// Check we have a CodeUri
const codeUri = (_m = properties.CodeUri) !== null && _m !== void 0 ? _m : defaultCodeUri;
if (!codeUri) {
throw new Error(`${resourceKey} is missing a CodeUri`);
}
const basePathPrefix = projectPath === "" ? "." : `./${projectPath}`;
const basePath = `${basePathPrefix}/${codeUri}`;
const fileBase = `${basePath}/${handlerComponents[0]}`;
// Generate the launch config for the VS Code debugger
launchConfigs.push({
name: projectKey === "default" ? resourceKey : `${projectKey}:${resourceKey}`,
type: "node",
request: "attach",
address: "localhost",
port: 5858,
localRoot: `\${workspaceFolder}/${buildRoot}/${resourceKey}`,
remoteRoot: "/var/task",
protocol: "inspector",
stopOnEntry: false,
outFiles: [`\${workspaceFolder}/${buildRoot}/${resourceKey}/**/*.js`],
sourceMaps: true,
skipFiles: ["/var/runtime/**/*.js", "<node_internals>/**/*.js"],
});
// Add the entry point for webpack
const entryPointName = projectKey === "default" ? resourceKey : `${projectKey}#${resourceKey}`;
entryPoints[entryPointName] = fileBase;
samConfig.Resources[resourceKey].Properties.CodeUri = resourceKey;
samConfig.Resources[resourceKey].Properties.Handler = `${outFile}.${handlerComponents[1]}`;
samConfigs.push({
buildRoot,
entryPointName,
outFile: `./${buildRoot}/${resourceKey}/${outFile}.js`,
projectKey,
samConfig,
templateName: projectTemplateName,
});
}
}
return { entryPoints, launchConfigs, samConfigs };
}
entry() {
// Reset the entry points and launch config
let allEntryPoints = {};
this.launchConfig = {
version: "0.2.0",
configurations: [],
};
this.samConfigs = [];
// The name of the out file
const outFile = this.options.outFile;
// Loop through each of the "projects" from the options
for (const projectKey in this.options.projects) {
// The value will be the name of a folder or a template file
const projectFolderOrTemplateName = this.options.projects[projectKey];
// If the projectFolderOrTemplateName isn't a file then we should look for common template file names
const projectTemplateName = fs.statSync(projectFolderOrTemplateName).isFile()
? projectFolderOrTemplateName
: this.findTemplateName(projectFolderOrTemplateName);
// If we still cannot find a project template name then throw an error because something is wrong
if (projectTemplateName === null) {
throw new Error(`Could not find ${AwsSamPlugin.defaultTemplates.join(" or ")} in ${projectFolderOrTemplateName}`);
}
// Retrieve the entry points, VS Code debugger launch configs and SAM config for this entry
const { entryPoints, launchConfigs, samConfigs } = this.entryFor(projectKey, path.relative(".", path.dirname(projectTemplateName)), path.basename(projectTemplateName), fs.readFileSync(projectTemplateName).toString(), outFile);
// Addd them to the entry pointsm launch configs and SAM confis we've already discovered.
allEntryPoints = Object.assign(Object.assign({}, allEntryPoints), entryPoints);
this.launchConfig.configurations = [...this.launchConfig.configurations, ...launchConfigs];
this.samConfigs = [...this.samConfigs, ...samConfigs];
}
// Once we're done return the entry points
return allEntryPoints;
}
filename(chunkData) {
const samConfig = this.samConfigs.find((c) => c.entryPointName === chunkData.chunk.name);
if (!samConfig) {
throw new Error(`Unable to find filename for ${chunkData.chunk.name}`);
}
return samConfig.outFile;
}
apply(compiler) {
compiler.hooks.afterEmit.tap("SamPlugin", (_compilation) => {
if (!(this.samConfigs && this.launchConfig)) {
throw new Error("It looks like AwsSamPlugin.entry() was not called");
}
const yamlUnique = this.samConfigs.reduce((a, e) => {
const { buildRoot, samConfig } = e;
a[buildRoot] = samConfig;
return a;
}, {});
for (const buildRoot in yamlUnique) {
const samConfig = yamlUnique[buildRoot];
fs.writeFileSync(`${buildRoot}/template.yaml`, js_yaml_1.default.dump(samConfig, { indent: 2, quotingType: '"', schema: yaml_cfn_1.schema }));
}
if (this.options.vscodeDebug !== false) {
if (!fs.existsSync(".vscode")) {
fs.mkdirSync(".vscode");
}
fs.writeFileSync(".vscode/launch.json", JSON.stringify(this.launchConfig, null, 2));
}
});
}
}
AwsSamPlugin.defaultTemplates = ["template.yaml", "template.yml"];
module.exports = AwsSamPlugin;