serverless/lib/classes/plugin-manager.js

798 lines
28 KiB
JavaScript

import path from 'path';
import _ from 'lodash';
import utils from '@serverlessinc/sf-core/src/utils.js';
import ServerlessError from '../serverless-error.js';
import renderCommandHelp from '../cli/render-help/command.js';
import tokenizeException from '../utils/tokenize-exception.js';
import { fileURLToPath, pathToFileURL } from 'url';
// Load Plugins
import pluginPackage from '../plugins/package/package.js';
import pluginDeploy from '../plugins/deploy.js';
import pluginInvoke from '../plugins/invoke.js';
import pluginInfo from '../plugins/info.js';
import pluginDev from '../plugins/dev.js';
import pluginLogs from '../plugins/logs.js';
import pluginMetrics from '../plugins/metrics.js';
import pluginPrint from '../plugins/print.js';
import pluginRemove from '../plugins/remove.js';
import pluginRollback from '../plugins/rollback.js';
import pluginPlugin from '../plugins/plugin/plugin.js';
import pluginList from '../plugins/plugin/list.js';
import pluginSearch from '../plugins/plugin/search.js';
import pluginAwsProvider from '../plugins/aws/provider.js';
import pluginAwsCommon from '../plugins/aws/common/index.js';
import pluginAwsPackage from '../plugins/aws/package/index.js';
import pluginAwsDeploy from '../plugins/aws/deploy/index.js';
import pluginAwsInvoke from '../plugins/aws/invoke.js';
import pluginAwsDev from '../plugins/aws/dev/index.js';
import pluginAwsInfo from '../plugins/aws/info/index.js';
import pluginAwsLogs from '../plugins/aws/logs.js';
import pluginAwsMetrics from '../plugins/aws/metrics.js';
import pluginAwsRemove from '../plugins/aws/remove/index.js';
import pluginAwsRollback from '../plugins/aws/rollback.js';
import pluginAwsRollbackFunction from '../plugins/aws/rollback-function.js';
import pluginAwsPackageCompileLayers from '../plugins/aws/package/compile/layers.js';
import pluginAwsPackageCompileFunctions from '../plugins/aws/package/compile/functions.js';
import pluginAwsPackageCompileEventsSchedule from '../plugins/aws/package/compile/events/schedule.js';
import pluginAwsPackageCompileEventsS3 from '../plugins/aws/package/compile/events/s3/index.js';
import pluginAwsPackageCompileEventsApiGateway from '../plugins/aws/package/compile/events/api-gateway/index.js';
import pluginAwsPackageCompileEventsWebsockets from '../plugins/aws/package/compile/events/websockets/index.js';
import pluginAwsPackageCompileEventsSns from '../plugins/aws/package/compile/events/sns.js';
import pluginAwsPackageCompileEventsStream from '../plugins/aws/package/compile/events/stream.js';
import pluginAwsPackageCompileEventsKafka from '../plugins/aws/package/compile/events/kafka.js';
import pluginAwsPackageCompileEventsActivemq from '../plugins/aws/package/compile/events/activemq.js';
import pluginAwsPackageCompileEventsRabbitmq from '../plugins/aws/package/compile/events/rabbitmq.js';
import pluginAwsPackageCompileEventsMsk from '../plugins/aws/package/compile/events/msk/index.js';
import pluginAwsPackageCompileEventsAlb from '../plugins/aws/package/compile/events/alb/index.js';
import pluginAwsPackageCompileEventsAlexaSkill from '../plugins/aws/package/compile/events/alexa-skill.js';
import pluginAwsPackageCompileEventsAlexaSmartHome from '../plugins/aws/package/compile/events/alexa-smart-home.js';
import pluginAwsPackageCompileEventsIot from '../plugins/aws/package/compile/events/iot.js';
import pluginAwsPackageCompileEventsIotFleetProvisioning from '../plugins/aws/package/compile/events/iot-fleet-provisioning.js';
import pluginAwsPackageCompileEventsCloudWatchEvent from '../plugins/aws/package/compile/events/cloud-watch-event.js';
import pluginAwsPackageCompileEventsCloudWatchLog from '../plugins/aws/package/compile/events/cloud-watch-log.js';
import pluginAwsPackageCompileEventsCognitoUserPool from '../plugins/aws/package/compile/events/cognito-user-pool.js';
import pluginAwsPackageCompileEventsEventBridge from '../plugins/aws/package/compile/events/event-bridge/index.js';
import pluginAwsPackageCompileEventsSqs from '../plugins/aws/package/compile/events/sqs.js';
import pluginAwsPackageCompileEventsCloudFront from '../plugins/aws/package/compile/events/cloud-front.js';
import pluginAwsPackageCompileEventsHttpApi from '../plugins/aws/package/compile/events/http-api.js';
import pluginAwsDeployFunction from '../plugins/aws/deploy-function.js';
import pluginAwsDeployList from '../plugins/aws/deploy-list.js';
import pluginAwsInvokeLocal from '../plugins/aws/invoke-local/index.js';
import pluginEsbuild from '../plugins/esbuild/index.js';
import { createRequire } from 'module';
const { log, getPluginWriters, readFile } = utils;
const internalPlugins = [
pluginPackage,
pluginDeploy,
pluginInvoke,
pluginInfo,
pluginDev,
pluginLogs,
pluginMetrics,
pluginPrint,
pluginRemove,
pluginRollback,
pluginPlugin,
pluginList,
pluginSearch,
pluginAwsProvider,
pluginAwsCommon,
pluginAwsPackage,
pluginAwsDeploy,
pluginAwsInvoke,
pluginAwsDev,
pluginAwsInfo,
pluginAwsLogs,
pluginAwsMetrics,
pluginAwsRemove,
pluginAwsRollback,
pluginAwsRollbackFunction,
pluginAwsPackageCompileLayers,
pluginAwsPackageCompileFunctions,
pluginAwsPackageCompileEventsSchedule,
pluginAwsPackageCompileEventsS3,
pluginAwsPackageCompileEventsApiGateway,
pluginAwsPackageCompileEventsWebsockets,
pluginAwsPackageCompileEventsSns,
pluginAwsPackageCompileEventsStream,
pluginAwsPackageCompileEventsKafka,
pluginAwsPackageCompileEventsActivemq,
pluginAwsPackageCompileEventsRabbitmq,
pluginAwsPackageCompileEventsMsk,
pluginAwsPackageCompileEventsAlb,
pluginAwsPackageCompileEventsAlexaSkill,
pluginAwsPackageCompileEventsAlexaSmartHome,
pluginAwsPackageCompileEventsIot,
pluginAwsPackageCompileEventsIotFleetProvisioning,
pluginAwsPackageCompileEventsCloudWatchEvent,
pluginAwsPackageCompileEventsCloudWatchLog,
pluginAwsPackageCompileEventsCognitoUserPool,
pluginAwsPackageCompileEventsEventBridge,
pluginAwsPackageCompileEventsSqs,
pluginAwsPackageCompileEventsCloudFront,
pluginAwsPackageCompileEventsHttpApi,
pluginAwsDeployFunction,
pluginAwsDeployList,
pluginAwsInvokeLocal,
pluginEsbuild,
];
let hooksIdCounter = 0;
let nestTracker = 0;
const typescriptPlugins = ['serverless-esbuild', 'serverless-typescript', 'serverless-webpack', 'serverless-bundle'];
const mergeCommands = (target, source) => {
if (!target) return source;
for (const key of Object.keys(source)) {
if (target[key] == null) {
target[key] = source[key];
continue;
}
switch (key) {
case 'options':
for (const [name, value] of Object.entries(source.options)) {
if (!target.options[name]) target.options[name] = value;
}
break;
case 'commands':
for (const [name, value] of Object.entries(source.commands)) {
target.commands[name] = mergeCommands(target.commands[name], value);
}
break;
case 'lifecycleEvents':
if (source[key].length) target[key] = source[key];
break;
default:
}
}
return target;
};
/**
* @private
* Error type to terminate the currently running hook chain successfully without
* executing the rest of the current command's lifecycle chain.
*/
class TerminateHookChain extends Error {
constructor(commands) {
const commandChain = commands.join(':');
const message = `Terminating ${commandChain}`;
super(message);
this.message = message;
this.name = 'TerminateHookChain';
}
}
let isRegisteringExternalPlugins = false;
class PluginManager {
constructor(serverless) {
this.serverless = serverless;
this.cliOptions = {};
this.cliCommands = [];
this.plugins = [];
this.externalPlugins = new Set();
this.commands = {};
this.aliases = {};
this.hooks = {};
this.deprecatedEvents = {};
}
setCliOptions(options) {
this.cliOptions = options;
}
setCliCommands(commands) {
this.cliCommands = commands;
}
addPlugin(Plugin) {
const pluginName = Plugin._serverlessExternalPluginName || Plugin.name;
if (
typescriptPlugins.includes(pluginName) &&
this.serverless.service.build?.esbuild !== false
) {
const errorMessage = `Serverless now includes ESBuild and supports Typescript out-of-the-box. But this conflicts with the plugin '${pluginName}'.\nYou can either remove this plugin and try Serverless's ESBuild support builtin, or you can set 'build.esbuild' to false in your 'serverless.yml'.\nFor more information go to, https://slss.io/buildoptions`;
throw new ServerlessError(errorMessage, 'PLUGIN_TYPESCRIPT_CONFLICT');
}
const pluginUtils = {};
Object.assign(
pluginUtils,
getPluginWriters(Plugin._serverlessExternalPluginName || Plugin.name)
);
const pluginInstance = new Plugin(this.serverless, this.cliOptions, pluginUtils);
if (isRegisteringExternalPlugins) {
this.externalPlugins.add(pluginInstance);
}
let pluginProvider = null;
// check if plugin is provider agnostic
if (pluginInstance.provider) {
if (typeof pluginInstance.provider === 'string') {
pluginProvider = pluginInstance.provider;
} else if (_.isObject(pluginInstance.provider)) {
pluginProvider = pluginInstance.provider.constructor.getProviderName();
}
}
// ignore plugins that specify a different provider than the current one
if (pluginProvider && pluginProvider !== this.serverless.service.provider.name) {
return null;
}
// don't load plugins twice
if (this.plugins.some((plugin) => plugin instanceof Plugin)) {
throw new ServerlessError(
'Encountered duplicate plugin definition. Please remove duplicate plugins from your configuration.',
'DUPLICATE_PLUGIN_DEFINITION'
);
}
this.loadCommands(pluginInstance);
this.loadHooks(pluginInstance);
this.plugins.push(pluginInstance);
return pluginInstance;
}
async loadAllPlugins(servicePlugins) {
// Load Internal Plugins
isRegisteringExternalPlugins = false;
internalPlugins.filter(Boolean).forEach((Plugin) => this.addPlugin(Plugin));
// Load External Plugins
isRegisteringExternalPlugins = true;
const resolvedServicePlugins = await this.resolveServicePlugins(servicePlugins);
const reorderedServicePlugins = this.sortServicePlugins(resolvedServicePlugins);
reorderedServicePlugins.filter(Boolean).forEach((Plugin) => this.addPlugin(Plugin));
isRegisteringExternalPlugins = true;
return this.asyncPluginInit();
}
/**
* Return a promise to require a Service plugin, whether it's local in a relative path,
* local in the service directory, or a node_module
* @param {*} serviceDir
* @param {*} pluginListedName
* @param {*} legacyLocalPluginsPath
* @returns
*/
async requireServicePlugin(serviceDir, pluginListedName, legacyLocalPluginsPath) {
const logger = log.get('sls:plugins:load');
const require = async (dir, module) => {
try {
const require = createRequire(path.resolve(dir, 'require-resolver'));
return require.resolve(module);
} catch (error) {
logger.debug(`Failed to resolve path for module ${module} from ${dir} due to '${error}'`);
return null
}
};
/**
* Check the Service directory for the plugin.
* This will check in the node_modules of the service directory
* and in the service directory itself.
*/
const localPluginPath = await require(serviceDir, pluginListedName);
if (localPluginPath) {
return await import(pathToFileURL(localPluginPath));
}
/**
* Search in the Framework's node_modules
*/
const externalPluginPath = await require(fileURLToPath(import.meta.url), pluginListedName);
if (!externalPluginPath) {
throw new ServerlessError(
`Serverless plugin "${pluginListedName}" not found.`,
'PLUGIN_NOT_FOUND'
);
}
return await import(pathToFileURL(externalPluginPath));
}
async resolveServicePlugins(servicePlugs) {
const pluginsObject = this.parsePluginsObject(servicePlugs);
const serviceDir = this.serverless.serviceDir;
const pluginNames = pluginsObject.modules;
const plugins = [];
for (const name of pluginNames) {
let Plugin;
try {
Plugin = await this.requireServicePlugin(serviceDir, name, pluginsObject.localPath);
Plugin = Plugin.default || Plugin;
} catch (error) {
if (error.code !== 'PLUGIN_NOT_FOUND') throw error;
throw new ServerlessError(
[
`Serverless plugin "${name}" not found.`,
' Make sure it\'s installed and listed in the "plugins" section',
' of your serverless config file.',
' Use the --debug flag to learn more.'
].join(''),
'PLUGIN_NOT_FOUND'
);
}
if (!Plugin) {
throw new ServerlessError(
`Serverless plugin "${name}", didn't export Plugin constructor.`,
'MISSING_PLUGIN_NAME'
);
}
Object.defineProperty(Plugin, '_serverlessExternalPluginName', {
value: name,
configurable: true,
writable: true,
});
plugins.push(Plugin);
}
return plugins;
}
/**
* Sort service plugins by prioritzing plugins with the "build" tag
*
* @param {Array} ServicePlugins
* @returns {Array} reordered plugins
*/
sortServicePlugins(ServicePlugins) {
// List of build plugins that are in the service
const prioritized = ServicePlugins.filter((P) => P?.tags?.includes('build'));
// List of the rest of the plugins that are in the service
const rest = ServicePlugins.filter((P) => !P?.tags?.includes('build'));
// Sort by prioritizing build plugins first, then the rest of the plugins
return [...prioritized, ...rest];
}
parsePluginsObject(servicePlugs) {
let localPath =
this.serverless &&
this.serverless.serviceDir &&
path.join(this.serverless.serviceDir, '.serverless_plugins');
let modules = [];
if (Array.isArray(servicePlugs)) {
modules = servicePlugs;
} else if (servicePlugs) {
localPath =
servicePlugs.localPath && typeof servicePlugs.localPath === 'string'
? servicePlugs.localPath
: localPath;
if (Array.isArray(servicePlugs.modules)) {
modules = servicePlugs.modules;
}
}
return { modules, localPath };
}
createCommandAlias(alias, command) {
// Deny self overrides
if (command.startsWith(alias)) {
throw new ServerlessError(
`Command "${alias}" cannot be overriden by an alias`,
'INVALID_COMMAND_ALIAS'
);
}
const splitAlias = alias.split(':');
const aliasTarget = splitAlias.reduce((__, aliasPath) => {
const currentAlias = __;
if (!currentAlias[aliasPath]) {
currentAlias[aliasPath] = {};
}
return currentAlias[aliasPath];
}, this.aliases);
// Check if the alias is already defined
if (aliasTarget.command) {
throw new ServerlessError(
`Alias "${alias}" is already defined for command ${aliasTarget.command}`,
'COMMAND_ALIAS_ALREADY_DEFINED'
);
}
// Check if the alias would overwrite an exiting command
if (
splitAlias.reduce((__, aliasPath) => {
if (!__ || !__.commands || !__.commands[aliasPath]) {
return null;
}
return __.commands[aliasPath];
}, this)
) {
throw new ServerlessError(
`Command "${alias}" cannot be overriden by an alias`,
'INVALID_COMMAND_ALIAS'
);
}
aliasTarget.command = command;
}
loadCommand(pluginName, details, key, isEntryPoint) {
const commandIsEntryPoint = details.type === 'entrypoint' || isEntryPoint;
log.get('sls:lifecycle:command:register').debug(key);
// Check if there is already an alias for the same path as the command
const aliasCommand = this.getAliasCommandTarget(key.split(':'));
if (aliasCommand) {
throw new ServerlessError(
`Command "${key}" cannot override an existing alias`,
'INVALID_COMMAND_OVERRIDE_EXISTING_ALIAS'
);
}
// Load the command
const commands = _.mapValues(details.commands, (subDetails, subKey) =>
this.loadCommand(pluginName, subDetails, `${key}:${subKey}`, commandIsEntryPoint)
);
// Handle command aliases
(details.aliases || []).forEach((alias) => {
log.get('sls:lifecycle:command:register').debug(` -> @${alias}`);
this.createCommandAlias(alias, key);
});
return Object.assign({}, details, { key, pluginName, commands });
}
loadCommands(pluginInstance) {
const pluginName = pluginInstance.constructor.name;
if (pluginInstance.commands) {
Object.entries(pluginInstance.commands).forEach(([key, details]) => {
const command = this.loadCommand(pluginName, details, key);
if (!command.lifecycleEvents) command.lifecycleEvents = [];
this.commands[key] = mergeCommands(
this.commands[key],
_.merge({}, command, {
isExternal: isRegisteringExternalPlugins,
})
);
});
}
}
loadHooks(pluginInstance) {
const pluginName = pluginInstance.constructor.name;
if (pluginInstance.hooks) {
Object.entries(pluginInstance.hooks).forEach(([event, hook]) => {
let target = event;
const baseEvent = event.replace(/^(?:after:|before:)/, '');
if (this.deprecatedEvents[baseEvent]) {
const redirectedEvent = this.deprecatedEvents[baseEvent];
log.info(
`Plugin "${pluginName}" uses deprecated hook "${event}". Use "${redirectedEvent}" hook instead.`
);
if (redirectedEvent) {
target = event.replace(baseEvent, redirectedEvent);
}
}
this.hooks[target] = this.hooks[target] || [];
this.hooks[target].push({
pluginName,
hook,
});
});
}
}
/**
* Retrieves all public commands and aliases from the plugin manager. This method
* iterates through the commands and stops at entrypoints to include only public
* commands throughout the hierarchy. It then iterates through the existing aliases
* and adds them as commands. The result is an object that maps command names to
* their respective command objects, with each command object omitting its own
* 'commands' property to prevent circular references.
*
* @returns {Object} An object mapping command names to command objects.
*/
getCommands() {
const result = {};
// Iterate through the commands and stop at entrypoints to include only public
// command throughout the hierarchy.
const stack = [{ commands: this.commands, target: result }];
while (stack.length) {
const currentCommands = stack.pop();
const commands = currentCommands.commands;
const target = currentCommands.target;
if (commands) {
Object.entries(commands).forEach(([name, command]) => {
if (command.type !== 'entrypoint') {
target[name] = _.omit(command, 'commands');
if (
Object.values(command.commands).some(
(childCommand) => childCommand.type !== 'entrypoint'
)
) {
target[name].commands = {};
stack.push({ commands: command.commands, target: target[name].commands });
}
}
});
}
}
// Iterate through the existing aliases and add them as commands
_.remove(stack);
stack.push({ aliases: this.aliases, target: result });
while (stack.length) {
const currentAlias = stack.pop();
const aliases = currentAlias.aliases;
const target = currentAlias.target;
if (aliases) {
Object.entries(aliases).forEach(([name, alias]) => {
if (name === 'command') {
return;
}
if (alias.command) {
const commandPath = alias.command.split(':').join('.commands.');
target[name] = _.get(this.commands, commandPath);
} else {
target[name] = target[name] || {};
target[name].commands = target[name].commands || {};
}
stack.push({ aliases: alias, target: target[name].commands });
});
}
}
return result;
}
getAliasCommandTarget(aliasArray) {
// Check if the command references an alias
const aliasCommand = aliasArray.reduce((__, commandPath) => {
if (!__ || !__[commandPath]) {
return null;
}
return __[commandPath];
}, this.aliases);
return _.get(aliasCommand, 'command');
}
/**
* Retrieve the command specified by a command list. The method can be configured
* to include entrypoint commands (which are invisible to the CLI and can only
* be used by plugins).
* @param commandsArray {Array<String>} Commands
* @param allowEntryPoints {undefined|boolean} Allow entrypoint commands to be returned
* @returns {Object} Command
*/
getCommand(commandsArray, allowEntryPoints) {
// Check if the command references an alias
const aliasCommandTarget = this.getAliasCommandTarget(commandsArray);
const commandOrAlias = aliasCommandTarget ? aliasCommandTarget.split(':') : commandsArray;
return commandOrAlias.reduce(
(current, name, index) => {
const commandExists = name in current.commands;
const isNotContainer = commandExists && current.commands[name].type !== 'container';
const isNotEntrypoint = commandExists && current.commands[name].type !== 'entrypoint';
const remainingIterationsLeft = index < commandOrAlias.length - 1;
if (
commandExists &&
(isNotContainer || remainingIterationsLeft) &&
(isNotEntrypoint || allowEntryPoints)
) {
return current.commands[name];
}
if (!isNotContainer && isNotEntrypoint) return current.commands[name];
// Invalid command, can happen only when Framework is used programmatically,
// as otherwise command is validated in main script
const err = new ServerlessError(
`Unrecognized command "${commandsArray.join(' ')}".`,
'UNRECOGNIZED COMMAND'
);
err.stack = undefined;
throw err;
},
{ commands: this.commands }
);
}
getPlugins() {
return this.plugins;
}
getLifecycleEventsData(command) {
const lifecycleEventsData = [];
let hooksLength = 0;
for (const lifecycleEventSubName of command.lifecycleEvents || []) {
const lifecycleEventName = `${command.key}:${lifecycleEventSubName}`;
const hooksData = {
before: this.hooks[`before:${lifecycleEventName}`] || [],
at: this.hooks[lifecycleEventName] || [],
after: this.hooks[`after:${lifecycleEventName}`] || [],
};
hooksLength += hooksData.before.length + hooksData.at.length + hooksData.after.length;
lifecycleEventsData.push({
command,
lifecycleEventSubName,
lifecycleEventName,
hooksData,
});
}
return { lifecycleEventsData, hooksLength };
}
async runHooks(hookName, hooks) {
const debugLog = log.get('sls:lifecycle:command:invoke:hook');
const hookId = ++hooksIdCounter;
for (const { hook } of hooks) {
debugLog.debug(`[%d] ${' '.repeat(nestTracker++)}< %s`, hookId, hookName);
try {
await hook();
} finally {
debugLog.debug(`[%d] ${' '.repeat(--nestTracker)}> %s`, hookId, hookName);
}
}
}
async invoke(commandsArray, allowEntryPoints) {
const command = this.getCommand(commandsArray, allowEntryPoints);
if (command.type === 'container') {
renderCommandHelp(commandsArray.join(' '));
return;
}
this.convertShortcutsIntoOptions(command);
this.validateServerlessConfigDependency(command);
this.assignDefaultOptions(command);
const { lifecycleEventsData, hooksLength } = this.getLifecycleEventsData(command);
log
.get('sls:lifecycle:command:invoke')
.debug(
`Invoke ${commandsArray.join(':')}${
!hooksLength ? ' (noop due to no registered hooks)' : ''
}`
);
try {
for (const {
lifecycleEventName,
hooksData: { before, at, after },
} of lifecycleEventsData) {
await this.runHooks(`before:${lifecycleEventName}`, before);
await this.runHooks(lifecycleEventName, at);
await this.runHooks(`after:${lifecycleEventName}`, after);
}
} catch (error) {
if (error instanceof TerminateHookChain) {
log.debug(`Terminate ${commandsArray.join(':')}`);
return;
}
throw error;
}
}
/**
* Invokes the given command and starts the command's lifecycle.
* This method can be called by plugins directly to spawn a separate sub lifecycle.
*/
async spawn(commandsArray, options) {
let commands = commandsArray;
if (typeof commandsArray === 'string') {
commands = commandsArray.split(':');
}
await this.invoke(commands, true);
if (_.get(options, 'terminateLifecycleAfterExecution', false)) {
throw new TerminateHookChain(commands);
}
}
/**
* Executes the provided commands array. This method initializes hooks, invokes
* the commands, and handles any exceptions that occur during command execution.
* If an exception occurs, it triggers the error hooks and rethrows the exception.
* After successful execution or error handling, it triggers the finalize hooks.
* If an exception occurs during finalize, it rethrows the exception.
*
* @async
* @param {Array} commandsArray - The array of commands to execute.
* @throws {Error} If there's an error in command execution or during the
* execution of error or finalize hooks.
*/
async run(commandsArray) {
this.commandRunStartTime = Date.now();
if (this.serverless.processedInput.commands[0] !== 'plugin') {
// first initialize hooks
for (const { hook } of this.hooks.initialize || []) await hook();
}
let deferredBackendNotificationRequest;
try {
await this.invoke(commandsArray);
} catch (commandException) {
try {
for (const { hook } of this.hooks.error || []) await hook(commandException);
} catch (errorHookException) {
const errorHookExceptionMeta = tokenizeException(errorHookException);
log.warning(
`The "error" hook crashed with:\n${
errorHookExceptionMeta.stack || errorHookExceptionMeta.message
}`
);
} finally {
await deferredBackendNotificationRequest;
throw commandException; // eslint-disable-line no-unsafe-finally
}
}
try {
for (const { hook } of this.hooks.finalize || []) await hook();
} catch (finalizeHookException) {
await deferredBackendNotificationRequest;
throw finalizeHookException;
}
}
/**
* Check if the command is valid. Internally this function will only find
* CLI accessible commands (command.type !== 'entrypoint')
*/
validateCommand(commandsArray) {
this.getCommand(commandsArray);
}
/**
* If the command has no use when operated in a working directory with no serverless
* configuration file, throw an error
*/
validateServerlessConfigDependency(command) {
if (command.configDependent || command.serviceDependencyMode === 'required') {
if (!this.serverless.configurationInput) {
const msg = [
'This command can only be run in a Serverless service directory. ',
"Make sure to reference a valid config file in the current working directory if you're using a custom config file",
].join('');
throw new ServerlessError(msg, 'INVALID_COMMAND_MISSING_SERVICE_DIRECTORY');
}
}
}
convertShortcutsIntoOptions(command) {
if (command.options) {
Object.entries(command.options).forEach(([optionKey, optionObject]) => {
if (optionObject.shortcut && Object.keys(this.cliOptions).includes(optionObject.shortcut)) {
Object.keys(this.cliOptions).forEach((option) => {
if (option === optionObject.shortcut) {
this.cliOptions[optionKey] = this.cliOptions[option];
}
});
}
});
}
}
assignDefaultOptions(command) {
if (command.options) {
Object.entries(command.options).forEach(([key, value]) => {
if (value.default != null && (!this.cliOptions[key] || this.cliOptions[key] === true)) {
this.cliOptions[key] = value.default;
}
});
}
}
async asyncPluginInit() {
return Promise.all(this.plugins.map((plugin) => plugin.asyncInit && plugin.asyncInit()));
}
}
export default PluginManager;