mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
860 lines
29 KiB
JavaScript
860 lines
29 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-plugin-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
|