mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
639 lines
21 KiB
JavaScript
639 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
const path = require('path');
|
|
const config = require('../utils/config');
|
|
const cjsResolve = require('ncjsm/resolve/sync');
|
|
const BbPromise = require('bluebird');
|
|
const _ = require('lodash');
|
|
const crypto = require('crypto');
|
|
const isModuleNotFoundError = require('ncjsm/is-module-not-found-error');
|
|
const writeFile = require('../utils/fs/writeFile');
|
|
const getCacheFilePath = require('../utils/getCacheFilePath');
|
|
const serverlessConfigFileUtils = require('../utils/getServerlessConfigFile');
|
|
const getCommandSuggestion = require('../utils/getCommandSuggestion');
|
|
|
|
const requireServicePlugin = (servicePath, pluginPath, localPluginsPath) => {
|
|
if (localPluginsPath && !pluginPath.startsWith('./')) {
|
|
// TODO (BREAKING): Consider removing support for localPluginsPath with next major
|
|
const absoluteLocalPluginPath = path.resolve(localPluginsPath, pluginPath);
|
|
|
|
try {
|
|
return require(absoluteLocalPluginPath);
|
|
} catch (error) {
|
|
if (!isModuleNotFoundError(error, absoluteLocalPluginPath)) throw error;
|
|
}
|
|
}
|
|
return require(cjsResolve(servicePath, pluginPath).realPath);
|
|
};
|
|
|
|
/**
|
|
* @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 = _.join(commands, ':');
|
|
const message = `Terminating ${commandChain}`;
|
|
|
|
super(message);
|
|
this.message = message;
|
|
this.name = 'TerminateHookChain';
|
|
}
|
|
}
|
|
|
|
class PluginManager {
|
|
constructor(serverless) {
|
|
this.serverless = serverless;
|
|
this.serverlessConfigFile = null;
|
|
|
|
this.cliOptions = {};
|
|
this.cliCommands = [];
|
|
|
|
this.plugins = [];
|
|
this.commands = {};
|
|
this.aliases = {};
|
|
this.hooks = {};
|
|
this.deprecatedEvents = {};
|
|
}
|
|
|
|
loadConfigFile() {
|
|
return serverlessConfigFileUtils
|
|
.getServerlessConfigFile(this.serverless)
|
|
.then(serverlessConfigFile => {
|
|
this.serverlessConfigFile = serverlessConfigFile;
|
|
return;
|
|
});
|
|
}
|
|
|
|
setCliOptions(options) {
|
|
this.cliOptions = options;
|
|
}
|
|
|
|
setCliCommands(commands) {
|
|
this.cliCommands = commands;
|
|
}
|
|
|
|
addPlugin(Plugin) {
|
|
const pluginInstance = new Plugin(this.serverless, this.cliOptions);
|
|
|
|
let pluginProvider = null;
|
|
// check if plugin is provider agnostic
|
|
if (pluginInstance.provider) {
|
|
if (_.isString(pluginInstance.provider)) {
|
|
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;
|
|
}
|
|
|
|
// don't load plugins twice
|
|
if (this.plugins.some(plugin => plugin instanceof Plugin)) {
|
|
this.serverless.cli.log(`WARNING: duplicate plugin ${Plugin.name} was not loaded\n`);
|
|
return;
|
|
}
|
|
|
|
this.loadCommands(pluginInstance);
|
|
this.loadHooks(pluginInstance);
|
|
this.loadVariableResolvers(pluginInstance);
|
|
|
|
this.plugins.push(pluginInstance);
|
|
}
|
|
|
|
loadAllPlugins(servicePlugins) {
|
|
[
|
|
...require('../plugins'),
|
|
...this.resolveServicePlugins(servicePlugins),
|
|
this.resolveEnterprisePlugin(),
|
|
]
|
|
.filter(Boolean)
|
|
.forEach(Plugin => this.addPlugin(Plugin));
|
|
return this.asyncPluginInit();
|
|
}
|
|
|
|
resolveServicePlugins(servicePlugs) {
|
|
const pluginsObject = this.parsePluginsObject(servicePlugs);
|
|
const servicePath = this.serverless.config.servicePath;
|
|
return pluginsObject.modules
|
|
.filter(name => name !== '@serverless/enterprise-plugin')
|
|
.map(name => {
|
|
let Plugin;
|
|
try {
|
|
Plugin = requireServicePlugin(servicePath, name, pluginsObject.localPath);
|
|
} catch (error) {
|
|
if (!isModuleNotFoundError(error, name)) throw error;
|
|
|
|
// Plugin not installed
|
|
if (this.cliOptions.help || this.cliCommands[0] === 'plugin') {
|
|
// User may intend to install plugins just listed in serverless config
|
|
// Therefore skip on MODULE_NOT_FOUND case
|
|
return null;
|
|
}
|
|
|
|
throw new this.serverless.classes.Error(
|
|
[
|
|
`Serverless plugin "${name}" not found.`,
|
|
' Make sure it\'s installed and listed in the "plugins" section',
|
|
' of your serverless config file.',
|
|
].join('')
|
|
);
|
|
}
|
|
if (!Plugin) {
|
|
throw new this.serverless.classes.Error(
|
|
`Serverless plugin "${name}", didn't export Plugin constructor.`
|
|
);
|
|
}
|
|
return Plugin;
|
|
});
|
|
}
|
|
|
|
resolveEnterprisePlugin() {
|
|
if (config.getGlobalConfig().enterpriseDisabled) return null;
|
|
return require('@serverless/enterprise-plugin');
|
|
}
|
|
|
|
parsePluginsObject(servicePlugs) {
|
|
let localPath =
|
|
this.serverless &&
|
|
this.serverless.config &&
|
|
this.serverless.config.servicePath &&
|
|
path.join(this.serverless.config.servicePath, '.serverless_plugins');
|
|
let modules = [];
|
|
|
|
if (_.isArray(servicePlugs)) {
|
|
modules = servicePlugs;
|
|
} else if (servicePlugs) {
|
|
localPath =
|
|
servicePlugs.localPath && _.isString(servicePlugs.localPath)
|
|
? servicePlugs.localPath
|
|
: localPath;
|
|
if (_.isArray(servicePlugs.modules)) {
|
|
modules = servicePlugs.modules;
|
|
}
|
|
}
|
|
|
|
return { modules, localPath };
|
|
}
|
|
|
|
createCommandAlias(alias, command) {
|
|
// Deny self overrides
|
|
if (_.startsWith(command, alias)) {
|
|
throw new this.serverless.classes.Error(`Command "${alias}" cannot be overriden by an alias`);
|
|
}
|
|
|
|
const splitAlias = _.split(alias, ':');
|
|
const aliasTarget = _.reduce(
|
|
splitAlias,
|
|
(__, 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 this.serverless.classes.Error(
|
|
`Alias "${alias}" is already defined for command ${aliasTarget.command}`
|
|
);
|
|
}
|
|
// Check if the alias would overwrite an exiting command
|
|
if (
|
|
_.reduce(
|
|
splitAlias,
|
|
(__, aliasPath) => {
|
|
if (!__ || !__.commands || !__.commands[aliasPath]) {
|
|
return null;
|
|
}
|
|
return __.commands[aliasPath];
|
|
},
|
|
this
|
|
)
|
|
) {
|
|
throw new this.serverless.classes.Error(`Command "${alias}" cannot be overriden by an alias`);
|
|
}
|
|
aliasTarget.command = command;
|
|
}
|
|
|
|
loadCommand(pluginName, details, key, isEntryPoint) {
|
|
const commandIsEntryPoint = details.type === 'entrypoint' || isEntryPoint;
|
|
if (process.env.SLS_DEBUG && !commandIsEntryPoint) {
|
|
this.serverless.cli.log(`Load command ${key}`);
|
|
}
|
|
// Check if there is already an alias for the same path as the command
|
|
const aliasCommand = this.getAliasCommandTarget(_.split(key, ':'));
|
|
if (aliasCommand) {
|
|
throw new this.serverless.classes.Error(`Command "${key}" cannot override an existing alias`);
|
|
}
|
|
// Load the command
|
|
const commands = _.mapValues(details.commands, (subDetails, subKey) =>
|
|
this.loadCommand(pluginName, subDetails, `${key}:${subKey}`, commandIsEntryPoint)
|
|
);
|
|
// Handle command aliases
|
|
_.forEach(details.aliases, alias => {
|
|
if (process.env.SLS_DEBUG) {
|
|
this.serverless.cli.log(` -> @${alias}`);
|
|
}
|
|
this.createCommandAlias(alias, key);
|
|
});
|
|
return _.assign({}, details, { key, pluginName, commands });
|
|
}
|
|
|
|
loadCommands(pluginInstance) {
|
|
const pluginName = pluginInstance.constructor.name;
|
|
_.forEach(pluginInstance.commands, (details, key) => {
|
|
const command = this.loadCommand(pluginName, details, key);
|
|
// Grab and extract deprecated events
|
|
command.lifecycleEvents = _.map(command.lifecycleEvents, event => {
|
|
if (_.startsWith(event, 'deprecated#')) {
|
|
// Extract event and optional redirect
|
|
const transformedEvent = /^deprecated#(.*?)(?:->(.*?))?$/.exec(event);
|
|
this.deprecatedEvents[`${command.key}:${transformedEvent[1]}`] =
|
|
transformedEvent[2] || null;
|
|
return transformedEvent[1];
|
|
}
|
|
return event;
|
|
});
|
|
this.commands[key] = _.merge({}, this.commands[key], command);
|
|
});
|
|
}
|
|
|
|
loadHooks(pluginInstance) {
|
|
const pluginName = pluginInstance.constructor.name;
|
|
_.forEach(pluginInstance.hooks, (hook, event) => {
|
|
let target = event;
|
|
const baseEvent = _.replace(event, /^(?:after:|before:)/, '');
|
|
if (_.has(this.deprecatedEvents, baseEvent)) {
|
|
const redirectedEvent = this.deprecatedEvents[baseEvent];
|
|
if (process.env.SLS_DEBUG) {
|
|
this.serverless.cli.log(`WARNING: Plugin ${pluginName} uses deprecated hook ${event},
|
|
use ${redirectedEvent} hook instead`);
|
|
}
|
|
if (redirectedEvent) {
|
|
target = _.replace(event, baseEvent, redirectedEvent);
|
|
}
|
|
}
|
|
this.hooks[target] = this.hooks[target] || [];
|
|
this.hooks[target].push({
|
|
pluginName,
|
|
hook,
|
|
});
|
|
});
|
|
}
|
|
|
|
loadVariableResolvers(pluginInstance) {
|
|
const pluginName = pluginInstance.constructor.name;
|
|
for (const [variablePrefix, resolverOrOptions] of _.entries(
|
|
pluginInstance.variableResolvers || {}
|
|
)) {
|
|
let options = resolverOrOptions;
|
|
if (!options) {
|
|
this.serverless.cli.log(
|
|
`Warning! Ignoring falsy variableResolver for ${variablePrefix} in ${pluginName}.`
|
|
);
|
|
continue;
|
|
}
|
|
if (_.isFunction(resolverOrOptions)) {
|
|
options = {
|
|
resolver: resolverOrOptions,
|
|
};
|
|
}
|
|
if (!_.isObject(options)) {
|
|
throw new Error(
|
|
`Custom variable resolver {${variablePrefix}: ${JSON.stringify(
|
|
options
|
|
)}} defined by ${pluginName} isn't an object`
|
|
);
|
|
} else if (!variablePrefix.match(/[0-9a-zA-Z_-]+/)) {
|
|
throw new Error(
|
|
`Custom variable resolver prefix ${variablePrefix} defined by ${pluginName} may only contain alphanumeric characters, hyphens or underscores.`
|
|
);
|
|
}
|
|
if (!_.isFunction(options.resolver)) {
|
|
throw new Error(
|
|
`Custom variable resolver for ${variablePrefix} defined by ${pluginName} specifies a resolver that isn't a function: ${options.resolver}`
|
|
);
|
|
}
|
|
if (options.isDisabledAtPrepopulation && typeof options.serviceName !== 'string') {
|
|
throw new Error(
|
|
`Custom variable resolver for ${variablePrefix} defined by ${pluginName} specifies isDisabledAtPrepopulation but doesn't provide a string for a name`
|
|
);
|
|
}
|
|
this.serverless.variables.variableResolvers.push({
|
|
regex: new RegExp(`^${variablePrefix}:`),
|
|
resolver: options.resolver.bind(this.serverless.variables),
|
|
isDisabledAtPrepopulation: options.isDisabledAtPrepopulation || false,
|
|
serviceName: options.serviceName || null,
|
|
});
|
|
}
|
|
}
|
|
|
|
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 (!_.isEmpty(stack)) {
|
|
const currentCommands = stack.pop();
|
|
const commands = currentCommands.commands;
|
|
const target = currentCommands.target;
|
|
_.forOwn(commands, (command, name) => {
|
|
if (command.type !== 'entrypoint') {
|
|
_.set(target, name, _.omit(command, 'commands'));
|
|
if (_.some(command.commands, 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 (!_.isEmpty(stack)) {
|
|
const currentAlias = stack.pop();
|
|
const aliases = currentAlias.aliases;
|
|
const target = currentAlias.target;
|
|
_.forOwn(aliases, (alias, name) => {
|
|
if (name === 'command') {
|
|
return;
|
|
}
|
|
if (alias.command) {
|
|
const commandPath = _.join(_.split(alias.command, ':'), '.commands.');
|
|
_.set(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 = _.reduce(
|
|
aliasArray,
|
|
(__, 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 ? _.split(aliasCommandTarget, ':') : commandsArray;
|
|
|
|
return _.reduce(
|
|
commandOrAlias,
|
|
(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 user is using a top level command properly, but sub commands are not
|
|
if (this.serverless.cli.loadedCommands[commandOrAlias[0]]) {
|
|
const errorMessage = [`"${name}" is not a valid sub command. Run "serverless `];
|
|
for (let i = 0; commandOrAlias[i] !== name; i++) {
|
|
errorMessage.push(`${commandOrAlias[i]}`);
|
|
if (commandOrAlias[i + 1] !== name) {
|
|
errorMessage.push(' ');
|
|
}
|
|
}
|
|
errorMessage.push('" to see a more helpful error message for this command.');
|
|
throw new this.serverless.classes.Error(errorMessage.join(''));
|
|
}
|
|
|
|
// top level command isn't valid. give a suggestion
|
|
const commandName = commandOrAlias.slice(0, index + 1).join(' ');
|
|
const suggestedCommand = getCommandSuggestion(
|
|
commandName,
|
|
this.serverless.cli.loadedCommands
|
|
);
|
|
const errorMessage = [
|
|
`Serverless command "${commandName}" not found. Did you mean "${suggestedCommand}"?`,
|
|
' Run "serverless help" for a list of all available commands.',
|
|
].join('');
|
|
throw new this.serverless.classes.Error(errorMessage);
|
|
},
|
|
{ commands: this.commands }
|
|
);
|
|
}
|
|
|
|
getEvents(command) {
|
|
return _.flatMap(command.lifecycleEvents, event => [
|
|
`before:${command.key}:${event}`,
|
|
`${command.key}:${event}`,
|
|
`after:${command.key}:${event}`,
|
|
]);
|
|
}
|
|
|
|
getPlugins() {
|
|
return this.plugins;
|
|
}
|
|
|
|
getHooks(events) {
|
|
return _.flatMap([].concat(events), event => this.hooks[event] || []);
|
|
}
|
|
|
|
invoke(commandsArray, allowEntryPoints) {
|
|
const command = this.getCommand(commandsArray, allowEntryPoints);
|
|
|
|
this.convertShortcutsIntoOptions(command);
|
|
this.validateServerlessConfigDependency(command);
|
|
this.assignDefaultOptions(command);
|
|
this.validateOptions(command);
|
|
|
|
const events = this.getEvents(command);
|
|
const hooks = this.getHooks(events);
|
|
|
|
if (process.env.SLS_DEBUG) {
|
|
this.serverless.cli.log(`Invoke ${_.join(commandsArray, ':')}`);
|
|
if (hooks.length === 0) {
|
|
const warningMessage = 'Warning: The command you entered did not catch on any hooks';
|
|
this.serverless.cli.log(warningMessage);
|
|
}
|
|
}
|
|
|
|
return BbPromise.reduce(hooks, (__, hook) => hook.hook(), null).catch(
|
|
TerminateHookChain,
|
|
() => {
|
|
if (process.env.SLS_DEBUG) {
|
|
this.serverless.cli.log(`Terminate ${_.join(commandsArray, ':')}`);
|
|
}
|
|
return BbPromise.resolve();
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Invokes the given command and starts the command's lifecycle.
|
|
* This method can be called by plugins directly to spawn a separate sub lifecycle.
|
|
*/
|
|
spawn(commandsArray, options) {
|
|
let commands = commandsArray;
|
|
if (_.isString(commandsArray)) {
|
|
commands = _.split(commandsArray, ':');
|
|
}
|
|
return this.invoke(commands, true).then(() => {
|
|
if (_.get(options, 'terminateLifecycleAfterExecution', false)) {
|
|
return BbPromise.reject(new TerminateHookChain(commands));
|
|
}
|
|
return BbPromise.resolve();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called by the CLI to start a public command.
|
|
*/
|
|
run(commandsArray) {
|
|
// first initialize hooks
|
|
return this.getHooks(['initialize'])
|
|
.reduce((chain, { hook }) => chain.then(hook), BbPromise.resolve())
|
|
.then(() => this.invoke(commandsArray));
|
|
}
|
|
|
|
/**
|
|
* 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 ('configDependent' in command && command.configDependent) {
|
|
if (!this.serverlessConfigFile) {
|
|
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 this.serverless.classes.Error(msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
validateOptions(command) {
|
|
_.forEach(command.options, (value, key) => {
|
|
if (value.required && (this.cliOptions[key] === true || !this.cliOptions[key])) {
|
|
let requiredThings = `the --${key} option`;
|
|
|
|
if (value.shortcut) {
|
|
requiredThings += ` / -${value.shortcut} shortcut`;
|
|
}
|
|
let errorMessage = `This command requires ${requiredThings}.`;
|
|
|
|
if (value.usage) {
|
|
errorMessage = `${errorMessage} Usage: ${value.usage}`;
|
|
}
|
|
|
|
throw new this.serverless.classes.Error(errorMessage);
|
|
}
|
|
|
|
if (
|
|
_.isPlainObject(value.customValidation) &&
|
|
value.customValidation.regularExpression instanceof RegExp &&
|
|
_.isString(value.customValidation.errorMessage) &&
|
|
!value.customValidation.regularExpression.test(this.cliOptions[key])
|
|
) {
|
|
throw new this.serverless.classes.Error(value.customValidation.errorMessage);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateAutocompleteCacheFile() {
|
|
const commands = _.clone(this.getCommands());
|
|
const cacheFile = {
|
|
commands: {},
|
|
validationHash: '',
|
|
};
|
|
|
|
_.forEach(commands, (commandObj, commandName) => {
|
|
const command = commandObj;
|
|
if (!command.options) {
|
|
command.options = {};
|
|
}
|
|
if (!command.commands) {
|
|
command.commands = {};
|
|
}
|
|
cacheFile.commands[commandName] = Object.keys(command.options)
|
|
.map(option => `--${option}`)
|
|
.concat(Object.keys(command.commands));
|
|
});
|
|
|
|
const serverlessConfigFileHash = crypto
|
|
.createHash('sha256')
|
|
.update(JSON.stringify(this.serverlessConfigFile))
|
|
.digest('hex');
|
|
cacheFile.validationHash = serverlessConfigFileHash;
|
|
const cacheFilePath = getCacheFilePath(this.serverless.config.servicePath);
|
|
|
|
return writeFile(cacheFilePath, cacheFile);
|
|
}
|
|
|
|
convertShortcutsIntoOptions(command) {
|
|
_.forEach(command.options, (optionObject, optionKey) => {
|
|
if (
|
|
optionObject.shortcut &&
|
|
_.includes(Object.keys(this.cliOptions), optionObject.shortcut)
|
|
) {
|
|
Object.keys(this.cliOptions).forEach(option => {
|
|
if (option === optionObject.shortcut) {
|
|
this.cliOptions[optionKey] = this.cliOptions[option];
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
assignDefaultOptions(command) {
|
|
_.forEach(command.options, (value, key) => {
|
|
if (value.default != null && (!this.cliOptions[key] || this.cliOptions[key] === true)) {
|
|
this.cliOptions[key] = value.default;
|
|
}
|
|
});
|
|
}
|
|
|
|
asyncPluginInit() {
|
|
return BbPromise.all(this.plugins.map(plugin => plugin.asyncInit && plugin.asyncInit()));
|
|
}
|
|
}
|
|
|
|
module.exports = PluginManager;
|