feat: support sharable configuration

Adds the options `extends`, which can be defined via configuration file or CLI arguments to a single path or an array of paths of shareable configuration.
A shareable configuration is a file or a module that can be loaded with `require`.
Options is defined by merging in the following order of priority:
- CLI/API
- Configuration file
- Shareable configuration (from right to left)

Options set in a shareable configuration can be unset by setting it to `null` or `undefined` in the main configuration file. If a default value applies to this property it will be used.
This commit is contained in:
Pierre Vanduynslager
2017-12-22 14:22:30 -05:00
parent 2fc538b607
commit 754b420fd6
14 changed files with 508 additions and 59 deletions
+32 -15
View File
@@ -10,27 +10,45 @@ test.beforeEach(t => {
});
test('Normalize and load plugin from string', t => {
const plugin = normalize('verifyConditions', {}, './test/fixtures/plugin-noop', t.context.logger);
const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/plugin-noop', t.context.logger);
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
});
test('Normalize and load plugin from object', t => {
const plugin = normalize('publish', {}, {path: './test/fixtures/plugin-noop'}, t.context.logger);
const plugin = normalize('publish', {}, {}, {path: './test/fixtures/plugin-noop'}, t.context.logger);
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'publish', './test/fixtures/plugin-noop']);
});
test('Normalize and load plugin from a base file path', t => {
const plugin = normalize(
'verifyConditions',
{'./plugin-noop': './test/fixtures'},
{},
'./plugin-noop',
t.context.logger
);
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], [
'Load plugin %s from %s in shareable config %s',
'verifyConditions',
'./plugin-noop',
'./test/fixtures',
]);
});
test('Normalize and load plugin from function', t => {
const plugin = normalize('', {}, () => {}, t.context.logger);
const plugin = normalize('', {}, {}, () => {}, t.context.logger);
t.is(typeof plugin, 'function');
});
test('Normalize and load plugin that retuns multiple functions', t => {
const plugin = normalize('verifyConditions', {}, './test/fixtures/multi-plugin', t.context.logger);
const plugin = normalize('verifyConditions', {}, {}, './test/fixtures/multi-plugin', t.context.logger);
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin %s from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
@@ -38,7 +56,7 @@ test('Normalize and load plugin that retuns multiple functions', t => {
test('Wrap plugin in a function that validate the output of the plugin', async t => {
const pluginFunction = stub().resolves(1);
const plugin = normalize('', {}, pluginFunction, t.context.logger, {
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger, {
validator: output => output === 1,
message: 'The output must be 1.',
});
@@ -54,7 +72,7 @@ test('Plugin is called with "pluginConfig" (omitting "path", adding global confi
const pluginFunction = stub().resolves();
const conf = {path: pluginFunction, conf: 'confValue'};
const globalConf = {global: 'globalValue'};
const plugin = normalize('', globalConf, conf, t.context.logger);
const plugin = normalize('', {}, globalConf, conf, t.context.logger);
await plugin('param');
t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param'));
@@ -66,7 +84,7 @@ test('Prevent plugins to modify "pluginConfig"', async t => {
});
const conf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
const globalConf = {globalConf: {globalSubConf: 'originalGlobalConf'}};
const plugin = normalize('', globalConf, conf, t.context.logger);
const plugin = normalize('', {}, globalConf, conf, t.context.logger);
await plugin();
t.is(conf.conf.subConf, 'originalConf');
@@ -78,7 +96,7 @@ test('Prevent plugins to modify its input', async t => {
options.param.subParam = 'otherParam';
});
const input = {param: {subParam: 'originalSubParam'}};
const plugin = normalize('', {}, pluginFunction, t.context.logger);
const plugin = normalize('', {}, {}, pluginFunction, t.context.logger);
await plugin(input);
t.is(input.param.subParam, 'originalSubParam');
@@ -92,7 +110,7 @@ test('Return noop if the plugin is not defined', t => {
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('', {}, './test/fixtures/plugin-result-config', t.context.logger);
const plugin = normalize('', {}, {}, './test/fixtures/plugin-result-config', t.context.logger);
const pluginResult = await plugin();
t.deepEqual(pluginResult.pluginConfig, {});
@@ -100,18 +118,17 @@ 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('', {}, {path: './test/fixtures/plugin-result-config'}, t.context.logger);
const plugin = normalize('', {}, {}, {path: './test/fixtures/plugin-result-config'}, t.context.logger);
const pluginResult = await plugin();
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Throws an error if the plugin return an object without the expected plugin function', t => {
const error = t.throws(
() => normalize('inexistantPlugin', {}, './test/fixtures/multi-plugin', t.context.logger),
Error
);
const error = t.throws(() => normalize('inexistantPlugin', {}, {}, './test/fixtures/multi-plugin', t.context.logger));
t.is(error.code, 'EPLUGINCONF');
t.is(error.name, 'SemanticReleaseError');
t.is(
error.message,
'The inexistantPlugin plugin must be a function, or an object with a function in the property inexistantPlugin.'
@@ -119,7 +136,7 @@ test('Throws an error if the plugin return an object without the expected plugin
});
test('Throws an error if the plugin is not found', t => {
const error = t.throws(() => normalize('inexistantPlugin', {}, 'non-existing-path', t.context.logger), Error);
const error = t.throws(() => normalize('inexistantPlugin', {}, {}, 'non-existing-path', t.context.logger), Error);
t.is(error.message, "Cannot find module 'non-existing-path'");
t.is(error.code, 'MODULE_NOT_FOUND');