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..982ee9c20f 100644 --- a/lib/plugins/index.js +++ b/lib/plugins/index.js @@ -6,36 +6,41 @@ 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) => { - if (validatePlugin(plugin)) { - const [name, config] = parseConfig(plugin); - plugin = isString(name) ? loadPlugin(context, name, pluginsPath) : name; + ? await castArray(options.plugins).reduce(async (plugins, plugin) => { + const plugins2 = await plugins; + if (validatePlugin(plugin)) { + const [name, config] = parseConfig(plugin); + plugin = isString(name) ? await loadPlugin(context, name, pluginsPath) : name; - if (isPlainObject(plugin)) { - Object.entries(plugin).forEach(([type, func]) => { - if (PLUGINS_DEFINITIONS[type]) { - Reflect.defineProperty(func, 'pluginName', { - value: isPlainObject(name) ? 'Inline plugin' : name, - writable: false, - enumerable: true, - }); - plugins[type] = [...(plugins[type] || []), [func, config]]; - } - }); - } else { - errors.push(getError('EPLUGINSCONF', {plugin})); - } + console.log({plugins: options.plugins}) + console.dir(plugin, {depth: null}) + console.log(isPlainObject(plugin)) + if (isPlainObject(plugin)) { + Object.entries(plugin).forEach(([type, func]) => { + console.log({type, name}) + if (PLUGINS_DEFINITIONS[type]) { + Reflect.defineProperty(func, 'pluginName', { + value: isPlainObject(name) ? 'Inline plugin' : name, + writable: false, + enumerable: true, + }); + plugins2[type] = [...(plugins2[type] || []), [func, config]]; + } + }); } else { errors.push(getError('EPLUGINSCONF', {plugin})); } + } else { + errors.push(getError('EPLUGINSCONF', {plugin})); + } - return plugins; - }, {}) + return plugins2; + }, {}) : []; if (errors.length > 0) { @@ -44,9 +49,16 @@ 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 pluginsConf = await Object.entries(PLUGINS_DEFINITIONS).reduce( + async (pluginsConf, [type, { + required, + default: def, + pipelineConfig, + postprocess = identity, + preprocess = identity + }]) => { let pluginOptions; + const pluginsConf2 = await pluginsConf; if (isNil(options[type]) && def) { pluginOptions = def; @@ -60,28 +72,28 @@ module.exports = (context, pluginsPath) => { if (!validateStep({required}, options[type])) { errors.push(getError('EPLUGINCONF', {type, required, pluginConf: options[type]})); - return pluginsConf; + return pluginsConf2; } pluginOptions = options[type]; } - const steps = castArray(pluginOptions).map((pluginOpt) => - normalize( + const steps = await Promise.all(castArray(pluginOptions).map(async (pluginOpt) => + await normalize( {...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')}, type, pluginOpt, pluginsPath ) - ); + )); - pluginsConf[type] = async (input) => + pluginsConf2[type] = async (input) => postprocess( - await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)), + await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf2, logger))(await preprocess(input)), input ); - return pluginsConf; + return pluginsConf2; }, plugins ); diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js index c9f2ab62cd..e01dbf4421 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,10 +13,12 @@ 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); + console.log({plugin}) + console.dir(plugin, {depth: null}) let func; if (isFunction(plugin)) { func = plugin.bind(null, cloneDeep({...options, ...config})); diff --git a/lib/plugins/utils.js b/lib/plugins/utils.js index 69bba2768b..4c7c38d44d 100644 --- a/lib/plugins/utils.js +++ b/lib/plugins/utils.js @@ -44,11 +44,11 @@ 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)); + return isFunction(name) ? name : (await import(resolveFrom.silent(basePath, name)) || resolveFrom(cwd, name)); } function parseConfig(plugin) { diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js index 784714b010..725f48d36d 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,8 +267,9 @@ 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', {}), { +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..ccde05f714 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, @@ -38,7 +38,8 @@ test('Export plugins based on steps config', (t) => { verifyConditions: ['./test/fixtures/plugin-noop', {path: './test/fixtures/plugin-noop'}], generateNotes: './test/fixtures/plugin-noop', analyzeCommits: {path: './test/fixtures/plugin-noop'}, - verifyRelease: () => {}, + verifyRelease: () => { + }, }, }, {} @@ -58,11 +59,11 @@ 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: () => {}}}, {} ); - +console.dir(plugins, {depth: null}) await plugins.verifyConditions({options: {}}); t.true(plugin1.verifyConditions.calledOnce); t.true(plugin2.verifyConditions.calledOnce); @@ -86,7 +87,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 +110,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 +130,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 +146,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 +176,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 +201,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 +236,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 +254,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, @@ -283,9 +284,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, @@ -303,9 +304,9 @@ 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}, [() => {}, {}, {}]]}}, {}) ), ]; 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) => {