UNPKG

aws-sam-webpack-plugin

Version:

Webpack plugin to replace the AWS SAM CLI build command

252 lines (251 loc) 14.8 kB
"use strict"; 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;