UNPKG

serverless-jetpack

Version:

A faster JavaScript packager for Serverless applications.

1,017 lines (873 loc) 35.3 kB
"use strict"; const path = require("path"); const pLimit = require("p-limit"); const { Worker } = require("jest-worker"); const { globAndZip } = require("./util/bundle"); const globby = require("globby"); const PLUGIN_NAME = require("./package.json").name; const SLS_TMP_DIR = ".serverless"; // Timer and formatter. // eslint-disable-next-line no-magic-numbers const toSecs = (time) => (time / 1000).toFixed(2); // Simple, stable union. const union = (arr1, arr2) => { const set1 = new Set(arr1); return arr1.concat(arr2.filter((o) => !set1.has(o))); }; const dedent = (str, num) => str .split("\n") .map((l) => l.substring(num)) .join("\n"); const uniq = (val, i, arr) => val !== arr[i - 1]; // Concatenate two arrays and produce sorted, unique values. const smartConcat = (arr1, arr2) => [] // Aggregate in service-level includes first .concat(arr1 || [], arr2 || []) // Make unique. .sort() .filter(uniq); // Merge two objects of form `{ key: [] }`. const smartMerge = (obj1 = {}, obj2 = {}) => // Get all unique missing package keys. smartConcat(Object.keys(obj1), Object.keys(obj2)) // Smart merge unique missing values .reduce((obj, key) => { // Aggregate service and function unique missing values. obj[key] = smartConcat(obj1[key], obj2[key]); return obj; }, {}); const toPosix = (file) => !file ? file : file.replace(/\\/g, "/"); /** * Package Serverless applications manually. * * ## How it works * * Essentially, the plugin "tricks" Serverless into thinking that an artifact * is specified, which skips all normal Serverless packaging, and then does * the packaging manually. * * Functionally, this looks something like: * 1. Looks for any services and/or functions that the Serverless framework * would publish with its built-in logic, * 2. Disables this by setting `service|function.package.artifact` fields in * the runtime Serverless configuration object, and; * 3. Invoking scripts to create an artifact that matches the newly-set * `.artifact` paths in configuration * * ## Application * * This plugin only is invoked to create custom packages if default Serverless * packaging would apply. Notably, this means this plugin is a noop for * scenarios including: * - `service.package.artifact` * - `function.package.artifact` * - `function.package.disable` * * The plugin does apply and follows the logic of configurations that include: * - `service.package.individually` * - `function.package.individually` */ class Jetpack { constructor(serverless, options) { this.serverless = serverless; this.options = options; this.commands = { jetpack: { usage: "Alternate Serverless packager", commands: { "package": { usage: "Packages a Serverless service or function", lifecycleEvents: [ "package" ], options: { "function": { usage: "Function name. Packages a single function (see 'deploy function')", shortcut: "f", type: "string" }, report: { usage: "Generate full bundle report", shortcut: "r", type: "boolean" } } } } } }; // The `ServerlessEnterprisePlugin` awkwardly wreaks havoc with alternative // packaging. // // `serverless` special cases it and ensures it's _always_ the last plugin. // This means that our plugin order ends up looking like: // // - ... // - 'Package', // - ... // - 'Jetpack', // - 'ServerlessEnterprisePlugin' // // Unfortunately, the plugin hooks end up like this: // // - `Jetpack:before:package:createDeploymentArtifacts`: // Bundles all files, creating artifact to avoid `Package` doing it. // - `ServerlessEnterprisePlugin:before:package:createDeploymentArtifacts`: // Creates the `s_<NAME>.js` wrapper files. // - `Package:package:createDeploymentArtifacts`: // Creates any artifacts that don't already exist. // // This means that Jetpack can't easily get both the SFE wrappers and still // control packaging: // // - If Jetpack stays with `before:package:createDeploymentArtifacts` // it misses the `s_<NAME>.js` wrapper files. // - _But_, if Jetpack hooks to `package:createDeploymentArtifacts` then // `Package` will actually perform real packaging and it's too late. // // To get around this awkward situation, we create a hooks object that is // delayed until the `initialize` lifecycle, then patched in last. This // ensures that Jetpack's hooks run absolutely last for these events. This // is still a bit hacky, but not nearly as invasive as some of the other // approaches we considered. H/T to `@medikoo` for the strategy: // https://github.com/FormidableLabs/serverless-jetpack/pull/68#issuecomment-556987101 const delayedHooks = { "before:package:createDeploymentArtifacts": this.package.bind(this), "before:package:function:package": this.package.bind(this) }; this.hooks = { // Use delayed hooks to guarantee we are **last** to run so other things // like the Serverless Enterprise plugin run before us. initialize: () => { const { hooks } = serverless.pluginManager; Object.keys(delayedHooks).forEach((event) => { hooks[event] = (hooks[event] || []).concat({ pluginName: this.constructor.name, hook: delayedHooks[event] }); }); }, "jetpack:package:package": this.package.bind(this) }; } _log(msg, opts) { const { cli } = this.serverless; cli.log(`[${PLUGIN_NAME}] ${msg}`, null, opts); } _logDebug(msg) { if (process.env.SLS_DEBUG) { this._log(msg); } } _logWarning(msg) { const { cli } = this.serverless; cli.log(`[${PLUGIN_NAME}] WARNING: ${msg}`, null, { color: "red" }); } // Root options. get _serviceOptions() { if (this.__options) { return this.__options; } const { service } = this.serverless; const defaults = { base: ".", roots: null, preInclude: [], trace: false, concurrency: 1, collapsed: {} }; const custom = (service.custom || {}).jetpack; this.__options = Object.assign({}, defaults, custom, this.options); this.__options.collapsed = { bail: !!this.__options.collapsed.bail }; return this.__options; } // Function, layer overrides, etc. // eslint-disable-next-line complexity _extraOptions({ functionObject, layerObject }) { if (!functionObject && !layerObject) { return this._serviceOptions; } const opts = Object.assign({}, this._serviceOptions); const fnObj = functionObject || {}; const fnOpts = fnObj.jetpack || {}; const layerOpts = (layerObject || {}).jetpack || {}; if (fnOpts.roots || layerOpts.roots) { opts.roots = (opts.roots || []) .concat(fnOpts.roots || []) .concat(layerOpts.roots || []); } if (fnOpts.collapsed || layerOpts.collapsed) { // Assume only one of function / layer provided. const collapsedOpts = { ...fnOpts.collapsed, ...layerOpts.collapsed }; opts.collapsed = { bail: typeof collapsedOpts.bail === "boolean" ? collapsedOpts.bail : opts.collapsed.bail }; } opts.preInclude = opts.preInclude.concat(fnOpts.preInclude || []); opts.trace = fnOpts.trace; return opts; } // Helper for layer patterns we'll need for all packages get _layerExcludes() { if (this.__layerExcludes) { return this.__layerExcludes; } const { service } = this.serverless; this.__layerExcludes = service.getAllLayers() .map((layer) => service.getLayer(layer)) .filter((layerObj) => layerObj.path) .map((layerObj) => `${layerObj.path}/**`); return this.__layerExcludes; } // Helper for trace configurations. _traceConfig({ functionObject, functionObjects } = {}) { // Mode: trace functions via service package const serviceTrace = this._serviceOptions.trace; const serviceEnabled = typeof serviceTrace === "object" || serviceTrace === true; const serviceFnIncludes = (functionObjects || []) // Get array of arrays of function trace includes for service-level packaging only. .map((obj) => (this._extraOptions({ functionObject: obj }).trace || {}).include || []) // Flatten to a single array .reduce((m, a) => m.concat(a), []); const serviceObj = { ignores: [], allowMissing: {}, include: [], collapsed: {}, dynamic: {}, ...typeof serviceTrace === "object" ? serviceTrace : {} }; // Aggregate with all service-packaged functions. serviceObj.include = smartConcat(serviceObj.include, serviceFnIncludes); serviceObj.dynamic = { bail: false, resolutions: {}, ...serviceObj.dynamic }; // Mode: trace individual function. const functionTrace = functionObject && this._extraOptions({ functionObject }).trace; const functionObj = { ignores: [], allowMissing: {}, include: [], collapsed: {}, dynamic: {}, ...typeof functionTrace === "object" ? functionTrace : {} }; functionObj.include = smartConcat(serviceObj.include, functionObj.include); functionObj.ignores = smartConcat(serviceObj.ignores, functionObj.ignores); functionObj.allowMissing = smartMerge(serviceObj.allowMissing, functionObj.allowMissing); functionObj.dynamic = { ...functionObj.dynamic, bail: typeof functionObj.dynamic.bail !== "undefined" ? functionObj.dynamic.bail : serviceObj.dynamic.bail, resolutions: smartMerge(serviceObj.dynamic.resolutions, functionObj.dynamic.resolutions) }; // Convert **relative** paths in allowMissing, resolutions to absolute paths. // Anything starting with at dot (`.`) is considered an application path // and converted. Remaining relative paths are packages. const cwd = this.serverless.config.servicePath || "."; [ serviceObj.allowMissing, functionObj.allowMissing, serviceObj.dynamic.resolutions, functionObj.dynamic.resolutions ].forEach((res) => Object.entries(res) .filter(([key]) => key.startsWith(".")) .forEach(([key, val]) => { // Replace key and mutate falsey values to an empty array. delete res[key]; res[path.resolve(cwd, key)] = val || []; })); return { service: { enabled: serviceEnabled, obj: serviceObj }, "function": { enabled: typeof functionTrace === "object" || functionTrace === true || serviceEnabled && functionTrace !== false, obj: functionObj } }; } // eslint-disable-next-line max-statements async _traceOptions({ functionObject, functionObjects } = {}) { // Detect if in tracing mode const traceConfig = this._traceConfig({ functionObject, functionObjects }); // Filter to only the function objects we should trace. let fnObjs = []; let traceObj; if (functionObjects && traceConfig.service.enabled) { // Service-level trace + service package. fnObjs = functionObjects; traceObj = traceConfig.service.obj; } else if (functionObject && traceConfig.function.enabled) { // `individually` function package with service or individual. fnObjs = [functionObject]; traceObj = traceConfig.function.obj; } // Extract handler functions to trace and short-circuit if none. const handlers = fnObjs.map((obj) => obj.handler); const unit = functionObjects ? "service" : `function: ${functionObject.name}`; this._logDebug( `Found ${handlers.length} handlers to trace for ${unit}: ${JSON.stringify(handlers)}` ); // Short-circuit if there's nothing to trace. if (!handlers.length) { return {}; } // Create the full list of globs to trace. const cwd = this.serverless.config.servicePath || "."; const traceInclude = await Promise // Find all the handler files in preference order. .all(handlers.map(async (handler) => { // We extract handler file name pretty analogously to how serverless does it. let pattern = handler.replace(/\.[^\.]+$/, ""); // Add pattern glob if not already present. if (!(/\.(js|mjs)$/).test(pattern)) { pattern += ".{js,mjs}"; } // Find (potentially multiple) matches const matched = await globby(pattern, { cwd }); if (!matched.length) { throw new Error(`Could not find file for handler: ${handler} with pattern: ${pattern}`); } // Choose JS first, else first matched entry. const matchedJsFile = matched.filter((file) => (/\.js$/).test(file))[0]; return matchedJsFile || matched[0]; })) // Add in the user-configured extra includes. .then((matchedJsFiles) => [].concat(matchedJsFiles, traceObj.include)); return { traceInclude, traceParams: { // These are **only** parameters to `traceFiles()`. ignores: traceObj.ignores, allowMissing: traceObj.allowMissing, extraImports: traceObj.dynamic.resolutions }, dynamic: traceObj.dynamic }; } _traceMissesReport(misses) { return Object.entries(misses) .map(([depPath, missList]) => missList ? missList.map(({ src, loc: { start: { line, column } } }) => `- ${depPath} [${line}:${column}]: ${src}` ) : [`- ${depPath}`] ) .reduce((arr, missList) => arr.concat(missList), []); } _traceMissesPkgsReport(pkgMisses) { return Object.values(pkgMisses) .map((misses) => this._traceMissesReport(misses)) .reduce((arr, missList) => arr.concat(missList), []); } // Handle tracing misses. // 1. Log / error for trace misses. // 2. Generate data for resolutions and remaining misses. // eslint-disable-next-line max-statements _handleTraceMisses({ bundleName, misses, resolutions, bail }) { const cwd = this.serverless.config.servicePath || "."; // Full, normalized paths for all resolutions for matching. const resSrcs = new Set(Object.keys(resolutions) .filter((f) => path.isAbsolute(f)) .map((f) => toPosix(f)) ); const resPkgs = new Set(Object.keys(resolutions) .filter((f) => !path.isAbsolute(f)) .map((f) => toPosix(f)) ); // Create a copy of misses and then iterate and mutate to remove the // entries that were resolved. const { srcs, pkgs } = JSON.parse(JSON.stringify(misses)); const resolved = { srcs: {}, pkgs: {} }; // Misses shape: `{ relPath: MISSES_OBJ }` Object.keys(srcs).forEach((relPath) => { const fullPath = toPosix(path.resolve(cwd, relPath)); // Remove matches. if (resSrcs.has(fullPath)) { resolved.srcs[relPath] = ""; // Just report file path delete srcs[relPath]; } }); // Misses shape: `{ pkg: { relpath: MISSES_OBJ } }` Object.entries(pkgs).forEach(([pkg, pkgSrcs]) => { Object.keys(pkgSrcs).forEach((relPath) => { // Convert `misses.pkgs` entries to `resolutions` entries. // // `resolutions` are in abstract paths (`PKG/path/to/file.js`), whereas // `misses.pkgs` entries are in relative paths // (`../PKG/node_modules/path/to/file.js`). We need to make them // matchable. // // We can do an abbreviated "last package fragment" because we already // have confirmed we have legitimate node_modules packages. const parts = toPosix(path.normalize(relPath)).split("/"); const lastModsIdx = parts.lastIndexOf("node_modules"); const pkgPath = parts.slice(lastModsIdx + 1).join("/"); // Remove matches. if (resPkgs.has(pkgPath)) { resolved.pkgs[pkg] = resolved.pkgs[pkg] || {}; resolved.pkgs[pkg][relPath] = ""; // Just report file path delete pkgs[pkg][relPath]; if (!Object.keys(pkgs[pkg]).length) { delete pkgs[pkg]; } } }); }); const srcsLen = Object.keys(srcs).length; const pkgsLen = Object.keys(pkgs).length; if (srcsLen) { // Full report if bailing. const srcsReport = bail ? `\n${this._traceMissesReport(srcs)}` : JSON.stringify(Object.keys(srcs)); this._logWarning( `Found ${srcsLen} source files with tracing misses in ${bundleName}! ` + "Please see logs and read: https://npm.im/serverless-jetpack#handling-dynamic-import-misses" ); this._log(`${bundleName} source file tracing misses: ${srcsReport}`, { color: "gray" }); } if (pkgsLen) { // Full report if bailing. const pkgReport = bail ? `\n${this._traceMissesPkgsReport(pkgs)}` : JSON.stringify(Object.keys(pkgs)); this._logWarning( `Found ${pkgsLen} dependency packages with tracing misses in ${bundleName}! ` + "Please see logs and read: https://npm.im/serverless-jetpack#handling-dynamic-import-misses" ); this._log(`${bundleName} dependency package tracing misses: ${pkgReport}`, { color: "gray" }); } if ((srcsLen || pkgsLen) && bail) { throw new Error( "Bailing on tracing dynamic import misses. " + `Source Files: ${srcsLen}, Dependencies: ${pkgsLen}. ` + "Please see logs and read: https://npm.im/serverless-jetpack#handling-dynamic-import-misses" ); } return { missed: { srcs, pkgs }, resolved }; } _collapsedReport(summary) { const pkgsSummary = (packages) => packages ? `Packages: ${packages.length}, ` : ""; const pkgsReport = (packages) => packages ? `: [${ Object.values(packages).map((obj) => `${obj.path}@${obj.version}`).join(", ") }]` : ""; return Object.entries(summary) .map(([group, { packages, numUniquePaths, numTotalFiles }]) => `- ${group} (${pkgsSummary(packages)}` + `Files: ${numUniquePaths} unique, ${numTotalFiles} total)${pkgsReport(packages)}` ); } // Handle collapsed duplicates. _handleCollapsed({ collapsed, bundleName, bail }) { const srcsLen = Object.keys(collapsed.srcs).length; const pkgsLen = Object.keys(collapsed.pkgs).length; // Nothing collapsed. Yay! if (!srcsLen && !pkgsLen) { return; } if (srcsLen) { const srcsReport = this._collapsedReport(collapsed.srcs).join("\n"); this._logWarning( `Found ${srcsLen} collapsed source files in ${bundleName}! ` + "Please fix, with hints at: " + "https://npm.im/serverless-jetpack#packaging-files-outside-cwd" ); this._log(`${bundleName} collapsed source files:\n${srcsReport}`, { color: "gray" }); } if (pkgsLen) { const pkgReport = this._collapsedReport(collapsed.pkgs).join("\n"); this._logWarning( `Found ${pkgsLen} collapsed dependencies in ${bundleName}! ` + "Please fix, with hints at: " + "https://npm.im/serverless-jetpack#packaging-files-outside-cwd" ); this._log(`${bundleName} collapsed dependencies:\n${pkgReport}`, { color: "gray" }); } if (bail) { throw new Error( "Bailing on collapsed files. " + `Source Files: ${srcsLen}, Dependencies: ${pkgsLen}. ` + "Please see logs and read: https://npm.im/serverless-jetpack#packaging-files-outside-cwd" ); } } _report({ results }) { const INDENT = 6; const JOIN_STR = `${"\n"}${" ".repeat(INDENT)}`; /* eslint-disable max-len*/ const bundles = results .map(({ mode, bundlePath, roots, patterns, files, trace, collapsed }) => ` ## ${path.basename(bundlePath)} - Path: ${bundlePath} - Mode: ${mode} - Roots: ${roots ? "" : "(None)"} ${(roots || []).map((p) => ` - '${p}'`).join(JOIN_STR)} ### Tracing: Configuration \`\`\`yml # Ignores (\`${trace.ignores.length}\`): ${trace.ignores.map((p) => `- '${p}'`).join(JOIN_STR)} # Allowed Missing (\`${Object.keys(trace.allowMissing).length}\`): ${Object.keys(trace.allowMissing).map((k) => `- '${k}': ${JSON.stringify(trace.allowMissing[k])}`).join(JOIN_STR)} \`\`\` ### Patterns: Include \`\`\`yml # Automatically added - '**' # Jetpack (\`${patterns.preInclude.length}\`): \`custom.jetpack.preInclude\` + \`function.{NAME}.jetpack.preInclude\` ${patterns.preInclude.map((p) => `- '${p}'`).join(JOIN_STR)} # Jetpack (\`${patterns.depInclude.length}\`): dependency filtering mode additions ${patterns.depInclude.map((p) => `- '${p}'`).join(JOIN_STR)} # Jetpack (\`${patterns.traceInclude.length}\`): trace mode additions ${patterns.traceInclude.map((p) => `- '${p}'`).join(JOIN_STR)} # Serverless (\`${patterns.include.length}\`): \`package.include\` + \`function.{NAME}.package.include\` + internal extras ${patterns.include.map((p) => `- '${p}'`).join(JOIN_STR)} \`\`\` ### Patterns: Exclude \`\`\`yml # Serverless (\`${patterns.exclude.length}\`): \`package.exclude\` + \`function.{NAME}.exclude\` + internal extras ${patterns.exclude.map((p) => `- '${p}'`).join(JOIN_STR)} \`\`\` ### Files (\`${files.included.length}\`): Included ${files.included.sort().map((p) => `- ${p}`).join(JOIN_STR)} ### Files (\`${files.excluded.length}\`): Excluded ${files.excluded.sort().map((p) => `- ${p}`).join(JOIN_STR)} ### Tracing Dynamic Misses (\`${Object.keys(trace.missed.srcs).length}\` files): Sources ${this._traceMissesReport(trace.missed.srcs).join(JOIN_STR)} ### Tracing Dynamic Resolved (\`${Object.keys(trace.resolved.srcs).length}\` files): Sources ${this._traceMissesReport(trace.resolved.srcs).join(JOIN_STR)} ### Tracing Dynamic Misses (\`${Object.keys(trace.missed.pkgs).length}\` packages): Dependencies ${this._traceMissesPkgsReport(trace.missed.pkgs).join(JOIN_STR)} ### Tracing Dynamic Resolved (\`${Object.keys(trace.resolved.pkgs).length}\` packages): Dependencies ${this._traceMissesPkgsReport(trace.resolved.pkgs).join(JOIN_STR)} ### Collapsed (\`${Object.keys(collapsed.srcs).length}\`): Sources ${this._collapsedReport(collapsed.srcs).join(JOIN_STR)} ### Collapsed (\`${Object.keys(collapsed.pkgs).length}\`): Dependencies ${this._collapsedReport(collapsed.pkgs).join(JOIN_STR)} `) .join("\n"); /* eslint-enable max-len*/ this._log(dedent(` # Jetpack Bundle Report ${bundles} `, INDENT)); } // eslint-disable-next-line complexity,max-statements filePatterns({ functionObject, layerObject }) { const { service, pluginManager } = this.serverless; const servicePackage = service.package; const serviceInclude = servicePackage.include || []; const serviceExclude = servicePackage.exclude || []; const functionPackage = (functionObject || {}).package || {}; const functionInclude = functionPackage.include || []; const functionExclude = functionPackage.exclude || []; const layerPackage = (layerObject || {}).package || {}; const layerInclude = layerPackage.include || []; const layerExclude = layerPackage.exclude || []; // Combined, unique patterns, in stable sorted order (remove _later_ instances). // This is `_.union` in serverless built-in. let include = serviceInclude; if (functionInclude) { include = union(include, functionInclude); } if (layerInclude) { include = union(include, layerInclude); } // Packaging logic. // // We essentially recreate what `serverless` does: // - Default excludes // - Exclude serverless config files // - Exclude plugin local paths // - Exclude all layers // - Apply service package excludes // - Apply function package excludes // - Apply layer package excludes // // https://serverless.com/framework/docs/providers/aws/guide/packaging#exclude--include // > At first it will apply the globs defined in exclude. After that it'll // > add all the globs from include. This way you can always re-include // > previously excluded files and directories. // // Start with serverless excludes, plus a few refinements. const slsDefaultExcludePatterns = [ ".git/**", ".gitignore", ".DS_Store", ".serverless/**", ".serverless_plugins/**", // Additional things no-one wants. "npm-debug.log*", "yarn-error.log*" ]; // Get plugin local path. const pluginsLocalPath = pluginManager.parsePluginsObject(service.plugins).localPath; // Unify in similar order to serverless. const exclude = [ slsDefaultExcludePatterns, pluginsLocalPath ? [pluginsLocalPath] : null, serviceExclude, this._layerExcludes, functionExclude, layerExclude ] .filter((arr) => !!arr && arr.length) .reduce((memo, arr) => union(memo, arr), []); return { include, exclude }; } async globAndZip({ bundleName, functionObject, layerObject, traceInclude, traceParams, worker, report }) { const { config } = this.serverless; const servicePath = config.servicePath || "."; const layerPath = (layerObject || {}).path; const cwd = layerPath ? path.relative(servicePath, layerPath) : servicePath; const { base, roots, preInclude } = this._extraOptions({ functionObject, layerObject }); const { include, exclude } = this.filePatterns({ functionObject, layerObject }); const buildFn = worker ? worker.globAndZip : globAndZip; const results = await buildFn({ cwd, servicePath, base, roots, bundleName, traceParams, preInclude, traceInclude, include, exclude, report }); const { numFiles, bundlePath } = results; this._logDebug( `Zipped ${numFiles} sources from ${cwd} to artifact location: ${bundlePath}` ); return results; } async packageFunction({ functionName, functionObject, worker, report }) { const opts = this._extraOptions({ functionObject }); // Mimic built-in serverless naming. // **Note**: We _do_ append ".serverless" in path skipping serverless' // internal copying logic. const bundleName = path.join(SLS_TMP_DIR, `${functionName}.zip`); // Get traces. const { traceInclude, traceParams, dynamic } = await this._traceOptions({ functionObject }); const mode = traceInclude ? "trace" : "dependency"; // Package. this._logDebug(`Start packaging function: ${bundleName} in mode: ${mode}`); const results = await this.globAndZip({ bundleName, functionObject, traceInclude, traceParams, worker, report }); const { buildTime, collapsed, trace } = results; if (mode === "trace") { const { missed, resolved } = this._handleTraceMisses({ bundleName, misses: trace.misses, resolutions: dynamic.resolutions, bail: dynamic.bail }); // Add in results Object.assign(trace, { missed, resolved }); } this._handleCollapsed({ collapsed, bundleName, bail: opts.collapsed.bail }); // Mutate serverless configuration to use our artifacts. functionObject.package = functionObject.package || {}; functionObject.package.artifact = bundleName; this._log(`Packaged function (${mode} mode): ${bundleName} (${toSecs(buildTime)}s)`); return { packageType: "function", ...results }; } // eslint-disable-next-line max-statements async packageService({ functionObjects, worker, report }) { const { service } = this.serverless; const serviceName = service.service; const servicePackage = service.package; const opts = this._serviceOptions; // Mimic built-in serverless naming. const bundleName = path.join(SLS_TMP_DIR, `${serviceName}.zip`); // Get traces. const { traceInclude, traceParams, dynamic } = await this._traceOptions({ functionObjects }); const mode = traceInclude ? "trace" : "dependency"; // Package. this._logDebug(`Start packaging service: ${bundleName} in mode: ${mode}`); const results = await this.globAndZip({ bundleName, traceInclude, traceParams, worker, report }); const { buildTime, collapsed, trace } = results; if (mode === "trace") { const { missed, resolved } = this._handleTraceMisses({ bundleName, misses: trace.misses, resolutions: dynamic.resolutions, bail: dynamic.bail }); // Add in results Object.assign(trace, { missed, resolved }); } this._handleCollapsed({ collapsed, bundleName, bail: opts.collapsed.bail }); // Mutate serverless configuration to use our artifacts. servicePackage.artifact = bundleName; this._log(`Packaged service (${mode} mode): ${bundleName} (${toSecs(buildTime)}s)`); return { packageType: "service", ...results }; } async packageLayer({ layerName, layerObject, worker, report }) { const opts = this._extraOptions({ layerObject }); const bundleName = path.join(SLS_TMP_DIR, `${layerName}.zip`); // Package. (Not traced) this._logDebug(`Start packaging layer: ${bundleName}`); const results = await this.globAndZip({ bundleName, layerObject, worker, report }); const { buildTime, collapsed } = results; this._handleCollapsed({ collapsed, bundleName, bail: opts.collapsed.bail }); // Mutate serverless configuration to use our artifacts. layerObject.package = layerObject.package || {}; layerObject.package.artifact = bundleName; this._log(`Packaged layer: ${bundleName} (${toSecs(buildTime)}s)`); return { packageType: "layer", ...results }; } // eslint-disable-next-line max-statements,complexity async package() { const { service } = this.serverless; const servicePackage = service.package; const serviceIsNode = (service.provider.runtime || "").startsWith("node"); const { concurrency } = this._serviceOptions; const options = this.options || {}; const report = !!options.report; let tasks = []; let worker; // ------------------------------------------------------------------------ // Lambdas // ------------------------------------------------------------------------ // Check if we have a single function limitation from `deploy -f name`. const singleFunctionName = options.function; if (singleFunctionName) { this._logDebug(`Packaging only for function: ${singleFunctionName}`); } // Functions. const fnsPkgs = service.getAllFunctions() // Limit to single function if provided. .filter((functionName) => !singleFunctionName || singleFunctionName === functionName) // Convert to more useful format. .map((functionName) => ({ functionName, functionObject: service.getFunction(functionName) })) .map((obj) => ({ ...obj, functionPackage: obj.functionObject.package || {}, runtime: obj.functionObject.runtime, isNode: (obj.functionObject.runtime || "").startsWith("node") })) .map((obj) => ({ ...obj, disable: !!obj.functionPackage.disable, individually: !!obj.functionPackage.individually, artifact: obj.functionPackage.artifact })); // Get list of individual functions to package. const individualPkgs = fnsPkgs.filter((obj) => servicePackage.individually || obj.individually); const fnsPkgsToPackage = individualPkgs.filter((obj) => // Enabled !(obj.disable || obj.artifact) // Function runtime is node or unspecified + service-level node. && (obj.isNode || !obj.runtime && serviceIsNode) ); const numFns = fnsPkgsToPackage.length; tasks = tasks.concat(fnsPkgsToPackage.map((obj) => () => this.packageFunction({ ...obj, worker, report }) )); if (numFns < individualPkgs.length) { this._log(`Skipping individual packaging for ${individualPkgs.length - numFns} functions`); } // We recreate the logic from `packager#packageService` for deciding whether // to package the service or not. const serviceFnsToPkg = !servicePackage.individually && !servicePackage.artifact // Service must be Node.js && serviceIsNode // Don't package service if we specify a single function **and** have a match && (!singleFunctionName || !numFns) // Otherwise, have some functions left that need to use the service package. && fnsPkgs.filter((obj) => !(obj.disable || obj.individually || obj.artifact)); const shouldPackageService = !!serviceFnsToPkg.length; // Package entire service if applicable. if (shouldPackageService) { tasks.push(() => this.packageService({ functionObjects: serviceFnsToPkg.map((o) => o.functionObject), worker, report })); } else if (!numFns) { // Detect if we did nothing... this._logDebug("No matching service or functions to package."); } // ------------------------------------------------------------------------ // Layers // ------------------------------------------------------------------------ const layersPkgs = service.getAllLayers() // Convert to more useful format. .map((layerName) => ({ layerName, layerObject: service.getLayer(layerName) })) .map((obj) => ({ ...obj, layerPackage: obj.layerObject.package || {} })) .map((obj) => ({ ...obj, disable: !!obj.layerObject.disable, artifact: obj.layerObject.artifact })); // Package layers if not in `-f NAME` single-function mode. let numLayers = 0; if (!singleFunctionName) { // Get list of layers to package. const layersPkgsToPackage = layersPkgs.filter((obj) => !(obj.disable || obj.artifact)); numLayers = layersPkgsToPackage.length; tasks = tasks.concat(layersPkgsToPackage.map((obj) => () => this.packageLayer({ ...obj, worker, report }) )); } // Run all packaging work. this._log( `Packaging ${numFns} functions, ${shouldPackageService ? 1 : 0} services, and ` + `${numLayers} layers with concurrency ${concurrency}` ); let results; if (concurrency > 1) { // Run concurrently. worker = new Worker(require.resolve("./util/bundle"), { numWorkers: concurrency }); results = await Promise.all(tasks.map((fn) => fn())) .catch((err) => { worker.end(); throw err; }); worker.end(); } else { // Run serially in-band. const limit = pLimit(1); results = await Promise.all(tasks.map(limit)); } // Report. if (report) { this._report({ results }); } return results; } } module.exports = Jetpack;