diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index c109f2afab..dad72ba4bc 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -68,7 +68,7 @@ Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`, message: 'The `plugins` configuration is invalid.', details: `The [plugins](${linkify( 'docs/usage/configuration.md#plugins' - )}) option must be an array of plugin definions. A plugin definition is an npm module name, optionally wrapped in an array with an object. + )}) option must be an array of plugin definitions. A plugin definition is an npm module name, optionally wrapped in an array with an object. The invalid configuration is \`${stringify(plugin)}\`.`, }), diff --git a/lib/plugins/index.js b/lib/plugins/index.js index 7fd2286380..47ff577a30 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -6,15 +6,16 @@ const {validatePlugin, validateStep, loadPlugin, parseConfig} = require('./utils const pipeline = require('./pipeline'); const normalize = require('./normalize'); -module.exports = (context, pluginsPath) => { +module.exports = async (context, pluginsPath) => { let {options, logger} = context; const errors = []; const plugins = options.plugins - ? castArray(options.plugins).reduce((plugins, plugin) => { + ? await castArray(options.plugins).reduce(async (eventualPluginsList, plugin) => { + const pluginsList = await eventualPluginsList; if (validatePlugin(plugin)) { const [name, config] = parseConfig(plugin); - plugin = isString(name) ? loadPlugin(context, name, pluginsPath) : name; + plugin = isString(name) ? await loadPlugin(context, name, pluginsPath) : name; if (isPlainObject(plugin)) { Object.entries(plugin).forEach(([type, func]) => { @@ -24,7 +25,7 @@ module.exports = (context, pluginsPath) => { writable: false, enumerable: true, }); - plugins[type] = [...(plugins[type] || []), [func, config]]; + pluginsList[type] = [...(pluginsList[type] || []), [func, config]]; } }); } else { @@ -34,7 +35,7 @@ module.exports = (context, pluginsPath) => { errors.push(getError('EPLUGINSCONF', {plugin})); } - return plugins; + return pluginsList; }, {}) : []; @@ -44,9 +45,13 @@ module.exports = (context, pluginsPath) => { options = {...plugins, ...options}; - const pluginsConf = Object.entries(PLUGINS_DEFINITIONS).reduce( - (pluginsConf, [type, {required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]) => { + const pluginsConfig = await Object.entries(PLUGINS_DEFINITIONS).reduce( + async ( + eventualPluginsConfigAccumulator, + [type, {required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}] + ) => { let pluginOptions; + const pluginsConfigAccumulator = await eventualPluginsConfigAccumulator; if (isNil(options[type]) && def) { pluginOptions = def; @@ -60,28 +65,33 @@ module.exports = (context, pluginsPath) => { if (!validateStep({required}, options[type])) { errors.push(getError('EPLUGINCONF', {type, required, pluginConf: options[type]})); - return pluginsConf; + return pluginsConfigAccumulator; } pluginOptions = options[type]; } - const steps = castArray(pluginOptions).map((pluginOpt) => - normalize( - {...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')}, - type, - pluginOpt, - pluginsPath + const steps = await Promise.all( + castArray(pluginOptions).map(async (pluginOpt) => + normalize( + {...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')}, + type, + pluginOpt, + pluginsPath + ) ) ); - pluginsConf[type] = async (input) => + pluginsConfigAccumulator[type] = async (input) => postprocess( - await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)), + await pipeline( + steps, + pipelineConfig && pipelineConfig(pluginsConfigAccumulator, logger) + )(await preprocess(input)), input ); - return pluginsConf; + return pluginsConfigAccumulator; }, plugins ); @@ -89,5 +99,5 @@ module.exports = (context, pluginsPath) => { throw new AggregateError(errors); } - return pluginsConf; + return pluginsConfig; }; diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js index c9f2ab62cd..d44400d24c 100644 --- a/lib/plugins/normalize.js +++ b/lib/plugins/normalize.js @@ -5,7 +5,7 @@ const {extractErrors} = require('../utils'); const PLUGINS_DEFINITIONS = require('../definitions/plugins'); const {loadPlugin, parseConfig} = require('./utils'); -module.exports = (context, type, pluginOpt, pluginsPath) => { +module.exports = async (context, type, pluginOpt, pluginsPath) => { const {stdout, stderr, options, logger} = context; if (!pluginOpt) { return noop; @@ -13,7 +13,7 @@ module.exports = (context, type, pluginOpt, pluginsPath) => { const [name, config] = parseConfig(pluginOpt); const pluginName = name.pluginName ? name.pluginName : isFunction(name) ? `[Function: ${name.name}]` : name; - const plugin = loadPlugin(context, name, pluginsPath); + const plugin = await loadPlugin(context, name, pluginsPath); debug(`options for ${pluginName}/${type}: %O`, config); diff --git a/lib/plugins/utils.js b/lib/plugins/utils.js index 69bba2768b..2b69da41a5 100644 --- a/lib/plugins/utils.js +++ b/lib/plugins/utils.js @@ -44,11 +44,14 @@ function validateStep({required}, conf) { return conf.length === 0 || validateSteps(conf); } -function loadPlugin({cwd}, name, pluginsPath) { +async function loadPlugin({cwd}, name, pluginsPath) { const basePath = pluginsPath[name] ? dirname(resolveFrom.silent(__dirname, pluginsPath[name]) || resolveFrom(cwd, pluginsPath[name])) : __dirname; - return isFunction(name) ? name : require(resolveFrom.silent(basePath, name) || resolveFrom(cwd, name)); + + // See https://github.com/mysticatea/eslint-plugin-node/issues/250 + // eslint-disable-next-line node/no-unsupported-features/es-syntax + return isFunction(name) ? name : (await import(resolveFrom.silent(basePath, name) || resolveFrom(cwd, name))).default; } function parseConfig(plugin) { diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js index 784714b010..65a7350b20 100644 --- a/test/plugins/normalize.test.js +++ b/test/plugins/normalize.test.js @@ -19,8 +19,8 @@ test.beforeEach((t) => { }; }); -test('Normalize and load plugin from string', (t) => { - const plugin = normalize( +test('Normalize and load plugin from string', async (t) => { + const plugin = await normalize( {cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './test/fixtures/plugin-noop', @@ -32,8 +32,8 @@ test('Normalize and load plugin from string', (t) => { t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop"']); }); -test('Normalize and load plugin from object', (t) => { - const plugin = normalize( +test('Normalize and load plugin from object', async (t) => { + const plugin = await normalize( {cwd, options: {}, logger: t.context.logger}, 'publish', {path: './test/fixtures/plugin-noop'}, @@ -45,8 +45,8 @@ test('Normalize and load plugin from object', (t) => { t.deepEqual(t.context.success.args[0], ['Loaded plugin "publish" from "./test/fixtures/plugin-noop"']); }); -test('Normalize and load plugin from a base file path', (t) => { - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-noop', { +test('Normalize and load plugin from a base file path', async (t) => { + const plugin = await normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-noop', { './plugin-noop': './test/fixtures', }); @@ -58,7 +58,7 @@ test('Normalize and load plugin from a base file path', (t) => { }); test('Wrap plugin in a function that add the "pluginName" to the error"', async (t) => { - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-error', { + const plugin = await normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-error', { './plugin-error': './test/fixtures', }); @@ -68,7 +68,7 @@ test('Wrap plugin in a function that add the "pluginName" to the error"', async }); test('Wrap plugin in a function that add the "pluginName" to multiple errors"', async (t) => { - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-errors', { + const plugin = await normalize({cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './plugin-errors', { './plugin-errors': './test/fixtures', }); @@ -78,16 +78,16 @@ test('Wrap plugin in a function that add the "pluginName" to multiple errors"', } }); -test('Normalize and load plugin from function', (t) => { +test('Normalize and load plugin from function', async (t) => { const pluginFunction = () => {}; - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {}); + const plugin = await normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {}); t.is(plugin.pluginName, '[Function: pluginFunction]'); t.is(typeof plugin, 'function'); }); -test('Normalize and load plugin that retuns multiple functions', (t) => { - const plugin = normalize( +test('Normalize and load plugin that retuns multiple functions', async (t) => { + const plugin = await normalize( {cwd, options: {}, logger: t.context.logger}, 'verifyConditions', './test/fixtures/multi-plugin', @@ -100,7 +100,7 @@ test('Normalize and load plugin that retuns multiple functions', (t) => { test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async (t) => { const analyzeCommits = stub().resolves(2); - const plugin = normalize( + const plugin = await normalize( {cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger}, 'analyzeCommits', analyzeCommits, @@ -118,7 +118,7 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async (t) => { const generateNotes = stub().resolves(2); - const plugin = normalize( + const plugin = await normalize( {cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger}, 'generateNotes', generateNotes, @@ -136,7 +136,7 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the test('Wrap "publish" plugin in a function that validate the output of the plugin', async (t) => { const publish = stub().resolves(2); - const plugin = normalize( + const plugin = await normalize( {cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger}, 'publish', publish, @@ -154,7 +154,7 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin test('Wrap "addChannel" plugin in a function that validate the output of the plugin', async (t) => { const addChannel = stub().resolves(2); - const plugin = normalize( + const plugin = await normalize( {cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger}, 'addChannel', addChannel, @@ -174,7 +174,7 @@ test('Plugin is called with "pluginConfig" (with object definition) and input', const pluginFunction = stub().resolves(); const pluginConf = {path: pluginFunction, conf: 'confValue'}; const options = {global: 'globalValue'}; - const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); + const plugin = await normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); await plugin({options: {}, param: 'param'}); t.true( @@ -189,7 +189,7 @@ test('Plugin is called with "pluginConfig" (with array definition) and input', a const pluginFunction = stub().resolves(); const pluginConf = [pluginFunction, {conf: 'confValue'}]; const options = {global: 'globalValue'}; - const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); + const plugin = await normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); await plugin({options: {}, param: 'param'}); t.true( @@ -206,7 +206,7 @@ test('Prevent plugins to modify "pluginConfig"', async (t) => { }); const pluginConf = {path: pluginFunction, conf: {subConf: 'originalConf'}}; const options = {globalConf: {globalSubConf: 'originalGlobalConf'}}; - const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); + const plugin = await normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {}); await plugin({options: {}}); t.is(pluginConf.conf.subConf, 'originalConf'); @@ -218,21 +218,21 @@ test('Prevent plugins to modify its input', async (t) => { options.param.subParam = 'otherParam'; }); const input = {param: {subParam: 'originalSubParam'}, options: {}}; - const plugin = normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {}); + const plugin = await normalize({cwd, options: {}, logger: t.context.logger}, '', pluginFunction, {}); await plugin(input); t.is(input.param.subParam, 'originalSubParam'); }); -test('Return noop if the plugin is not defined', (t) => { - const plugin = normalize({cwd, options: {}, logger: t.context.logger}); +test('Return noop if the plugin is not defined', async (t) => { + const plugin = await normalize({cwd, options: {}, logger: t.context.logger}); t.is(plugin, noop); }); test('Always pass a defined "pluginConfig" for plugin defined with string', async (t) => { // Call the normalize function with the path of a plugin that returns its config - const plugin = normalize( + const plugin = await normalize( {cwd, options: {}, logger: t.context.logger}, '', './test/fixtures/plugin-result-config', @@ -245,7 +245,7 @@ test('Always pass a defined "pluginConfig" for plugin defined with string', asyn test('Always pass a defined "pluginConfig" for plugin defined with path', async (t) => { // Call the normalize function with the path of a plugin that returns its config - const plugin = normalize( + const plugin = await normalize( {cwd, options: {}, logger: t.context.logger}, '', {path: './test/fixtures/plugin-result-config'}, @@ -256,8 +256,8 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async t.deepEqual(pluginResult.pluginConfig, {}); }); -test('Throws an error if the plugin return an object without the expected plugin function', (t) => { - const error = t.throws(() => +test('Throws an error if the plugin return an object without the expected plugin function', async (t) => { + const error = await t.throwsAsync(() => normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', './test/fixtures/multi-plugin', {}) ); @@ -267,10 +267,13 @@ test('Throws an error if the plugin return an object without the expected plugin t.truthy(error.details); }); -test('Throws an error if the plugin is not found', (t) => { - t.throws(() => normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', 'non-existing-path', {}), { - message: /Cannot find module 'non-existing-path'/, - code: 'MODULE_NOT_FOUND', - instanceOf: Error, - }); +test('Throws an error if the plugin is not found', async (t) => { + await t.throwsAsync( + () => normalize({cwd, options: {}, logger: t.context.logger}, 'inexistantPlugin', 'non-existing-path', {}), + { + message: /Cannot find module 'non-existing-path'/, + code: 'MODULE_NOT_FOUND', + instanceOf: Error, + } + ); }); diff --git a/test/plugins/plugins.test.js b/test/plugins/plugins.test.js index 417e7b0cbe..435459a267 100644 --- a/test/plugins/plugins.test.js +++ b/test/plugins/plugins.test.js @@ -15,8 +15,8 @@ test.beforeEach((t) => { t.context.logger = {log: t.context.log, success: t.context.success, scope: () => t.context.logger}; }); -test('Export default plugins', (t) => { - const plugins = getPlugins({cwd, options: {}, logger: t.context.logger}, {}); +test('Export default plugins', async (t) => { + const plugins = await getPlugins({cwd, options: {}, logger: t.context.logger}, {}); // Verify the module returns a function for each plugin t.is(typeof plugins.verifyConditions, 'function'); @@ -29,8 +29,8 @@ test('Export default plugins', (t) => { t.is(typeof plugins.fail, 'function'); }); -test('Export plugins based on steps config', (t) => { - const plugins = getPlugins( +test('Export plugins based on steps config', async (t) => { + const plugins = await getPlugins( { cwd, logger: t.context.logger, @@ -58,11 +58,10 @@ test('Export plugins based on steps config', (t) => { test('Export plugins based on "plugins" config (array)', async (t) => { const plugin1 = {verifyConditions: stub(), publish: stub()}; const plugin2 = {verifyConditions: stub(), verifyRelease: stub()}; - const plugins = getPlugins( + const plugins = await getPlugins( {cwd, logger: t.context.logger, options: {plugins: [plugin1, [plugin2, {}]], verifyRelease: () => {}}}, {} ); - await plugins.verifyConditions({options: {}}); t.true(plugin1.verifyConditions.calledOnce); t.true(plugin2.verifyConditions.calledOnce); @@ -86,7 +85,7 @@ test('Export plugins based on "plugins" config (array)', async (t) => { test('Export plugins based on "plugins" config (single definition)', async (t) => { const plugin1 = {verifyConditions: stub(), publish: stub()}; - const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: plugin1}}, {}); + const plugins = await getPlugins({cwd, logger: t.context.logger, options: {plugins: plugin1}}, {}); await plugins.verifyConditions({options: {}}); t.true(plugin1.verifyConditions.calledOnce); @@ -109,7 +108,7 @@ test('Merge global options, "plugins" options and step options', async (t) => { const plugin1 = [{verifyConditions: stub(), publish: stub()}, {pluginOpt1: 'plugin1'}]; const plugin2 = [{verifyConditions: stub()}, {pluginOpt2: 'plugin2'}]; const plugin3 = [stub(), {pluginOpt3: 'plugin3'}]; - const plugins = getPlugins( + const plugins = await getPlugins( { cwd, logger: t.context.logger, @@ -129,9 +128,9 @@ test('Merge global options, "plugins" options and step options', async (t) => { t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'}); }); -test('Unknown steps of plugins configured in "plugins" are ignored', (t) => { +test('Unknown steps of plugins configured in "plugins" are ignored', async (t) => { const plugin1 = {verifyConditions: () => {}, unknown: () => {}}; - const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1]}}, {}); + const plugins = await getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1]}}, {}); t.is(typeof plugins.verifyConditions, 'function'); t.is(plugins.unknown, undefined); @@ -145,7 +144,7 @@ test('Export plugins loaded from the dependency of a shareable config module', a ); await outputFile(path.resolve(cwd, 'node_modules/shareable-config/index.js'), ''); - const plugins = getPlugins( + const plugins = await getPlugins( { cwd, logger: t.context.logger, @@ -175,7 +174,7 @@ test('Export plugins loaded from the dependency of a shareable config file', asy await copy('./test/fixtures/plugin-noop.js', path.resolve(cwd, 'plugin/plugin-noop.js')); await outputFile(path.resolve(cwd, 'shareable-config.js'), ''); - const plugins = getPlugins( + const plugins = await getPlugins( { cwd, logger: t.context.logger, @@ -200,14 +199,14 @@ test('Export plugins loaded from the dependency of a shareable config file', asy t.is(typeof plugins.fail, 'function'); }); -test('Use default when only options are passed for a single plugin', (t) => { +test('Use default when only options are passed for a single plugin', async (t) => { const analyzeCommits = {}; const generateNotes = {}; const publish = {}; const success = () => {}; const fail = [() => {}]; - const plugins = getPlugins( + const plugins = await getPlugins( { cwd, logger: t.context.logger, @@ -235,7 +234,7 @@ test('Use default when only options are passed for a single plugin', (t) => { }); test('Merge global options with plugin options', async (t) => { - const plugins = getPlugins( + const plugins = await getPlugins( { cwd, logger: t.context.logger, @@ -253,9 +252,9 @@ test('Merge global options with plugin options', async (t) => { t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'}); }); -test('Throw an error for each invalid plugin configuration', (t) => { +test('Throw an error for each invalid plugin configuration', async (t) => { const errors = [ - ...t.throws(() => + ...(await t.throwsAsync(() => getPlugins( { cwd, @@ -270,7 +269,7 @@ test('Throw an error for each invalid plugin configuration', (t) => { }, {} ) - ), + )), ]; t.is(errors[0].name, 'SemanticReleaseError'); @@ -283,9 +282,9 @@ test('Throw an error for each invalid plugin configuration', (t) => { t.is(errors[3].code, 'EPLUGINCONF'); }); -test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', (t) => { +test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', async (t) => { const errors = [ - ...t.throws(() => + ...(await t.throwsAsync(() => getPlugins( { cwd, @@ -294,7 +293,7 @@ test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin de }, {} ) - ), + )), ]; t.is(errors[0].name, 'SemanticReleaseError'); @@ -303,11 +302,11 @@ test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin de t.is(errors[1].code, 'EPLUGINSCONF'); }); -test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', (t) => { +test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', async (t) => { const errors = [ - ...t.throws(() => + ...(await t.throwsAsync(() => getPlugins({cwd, logger: t.context.logger, options: {plugins: [1, {path: 1}, [() => {}, {}, {}]]}}, {}) - ), + )), ]; t.is(errors[0].name, 'SemanticReleaseError'); diff --git a/test/plugins/utils.test.js b/test/plugins/utils.test.js index 99fa42d93b..e9f14dc765 100644 --- a/test/plugins/utils.test.js +++ b/test/plugins/utils.test.js @@ -189,17 +189,17 @@ test('validateStep: required plugin configuration', (t) => { ); }); -test('loadPlugin', (t) => { +test('loadPlugin', async (t) => { const cwd = process.cwd(); const func = () => {}; - t.is(require('../fixtures/plugin-noop'), loadPlugin({cwd: './test/fixtures'}, './plugin-noop', {}), 'From cwd'); + t.is(require('../fixtures/plugin-noop'), await loadPlugin({cwd: './test/fixtures'}, './plugin-noop', {}), 'From cwd'); t.is( require('../fixtures/plugin-noop'), - loadPlugin({cwd}, './plugin-noop', {'./plugin-noop': './test/fixtures'}), + await loadPlugin({cwd}, './plugin-noop', {'./plugin-noop': './test/fixtures'}), 'From a shareable config context' ); - t.is(func, loadPlugin({cwd}, func, {}), 'Defined as a function'); + t.is(func, await loadPlugin({cwd}, func, {}), 'Defined as a function'); }); test('parseConfig', (t) => {