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} 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