Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(plugins): switched from require to await import() when loading plugins #2558

Merged
merged 6 commits into from Sep 17, 2022
2 changes: 1 addition & 1 deletion lib/definitions/errors.js
Expand Up @@ -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)}\`.`,
}),
Expand Down
46 changes: 28 additions & 18 deletions lib/plugins/index.js
Expand Up @@ -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]) => {
Expand All @@ -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 {
Expand All @@ -34,7 +35,7 @@ module.exports = (context, pluginsPath) => {
errors.push(getError('EPLUGINSCONF', {plugin}));
}

return plugins;
return pluginsList;
}, {})
: [];

Expand All @@ -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;
Expand All @@ -60,34 +65,39 @@ 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
);
if (errors.length > 0) {
throw new AggregateError(errors);
}

return pluginsConf;
return pluginsConfig;
};
4 changes: 2 additions & 2 deletions lib/plugins/normalize.js
Expand Up @@ -5,15 +5,15 @@ 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;
}

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);

Expand Down
7 changes: 5 additions & 2 deletions lib/plugins/utils.js
Expand Up @@ -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) {
Expand Down
67 changes: 35 additions & 32 deletions test/plugins/normalize.test.js
Expand Up @@ -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',
Expand All @@ -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'},
Expand All @@ -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',
});

Expand All @@ -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',
});

Expand All @@ -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',
});

Expand All @@ -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',
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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');
Expand All @@ -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',
Expand All @@ -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'},
Expand All @@ -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', {})
);

Expand All @@ -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,
}
);
});