diff --git a/README.md b/README.md
index 25a487e8..7110e699 100644
--- a/README.md
+++ b/README.md
@@ -81,19 +81,19 @@ If you need more control over the timing of releases you have a couple of option
### Release steps
-After running the tests the command `semantic-release` will execute the following steps:
+After running the tests, the command `semantic-release` will execute the following steps:
-| Step | Description |
-|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| Verify Conditions | Verify all the conditions to proceed with the release with the [verify conditions plugins](docs/usage/plugins.md#verifyconditions-plugin). |
-| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
-| Analyze commits | Determine the type of release with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release. |
-| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
-| Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
-| Create Git tag | Create a Git tag corresponding to the new release version |
-| Prepare | Prepare the release with the [prepare plugins](docs/usage/plugins.md#prepare-plugin). |
-| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
-| Notify | Notify of new releases or errors with the [success](docs/usage/plugins.md#success-plugin) and [fail](docs/usage/plugins.md#fail-plugin) plugins. |
+| Step | Description |
+|-------------------|---------------------------------------------------------------------------------------------------------------------------------|
+| Verify Conditions | Verify all the conditions to proceed with the release. |
+| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
+| Analyze commits | Determine the type of release based on the commits added since the last release. |
+| Verify release | Verify the release conformity. |
+| Generate notes | Generate release notes for the commits added since the last release. |
+| Create Git tag | Create a Git tag corresponding to the new release version. |
+| Prepare | Prepare the release. |
+| Publish | Publish the release. |
+| Notify | Notify of new releases or errors. |
## Documentation
diff --git a/cli.js b/cli.js
index 7583bb1d..1ef5f5ba 100755
--- a/cli.js
+++ b/cli.js
@@ -22,6 +22,7 @@ Usage:
.option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'})
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
+ .option('p', {alias: 'plugins', describe: 'Plugins', ...stringList, group: 'Options'})
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'})
.option('verify-conditions', {...stringList, group: 'Plugins'})
diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md
index f3fe4388..6db546f8 100644
--- a/docs/usage/configuration.md
+++ b/docs/usage/configuration.md
@@ -92,6 +92,18 @@ The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by
**Note**: The `tagFormat` must contain the `version` variable exactly once and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
+### plugins
+
+Type: `Array`
+Default: `['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', '@semantic-release/npm', '@semantic-release/github']`
+CLI arguments: `-p`, `--plugins`
+
+Define the list of plugins to use. Plugins will run in series, in the order defined, for each [steps](../../README.md#release-steps) if they implement it.
+
+Plugins configuration can defined by wrapping the name and an options object in an array.
+
+See [Plugins configuration](plugins.md#configuration) for more details.
+
### dryRun
Type: `Boolean`
diff --git a/docs/usage/plugins.md b/docs/usage/plugins.md
index d86632b9..beaf1950 100644
--- a/docs/usage/plugins.md
+++ b/docs/usage/plugins.md
@@ -1,114 +1,78 @@
# Plugins
-Each [release step](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
+Each [release step](../../README.md#release-steps) is implemented by configurable plugins. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
-See [plugins list](../extending/plugins-list.md).
+A plugin is a npm module that can implement one or more of the following steps:
-## Plugin types
+| Step | Accept multiple | Required | Description |
+|--------------------|-----------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `verifyConditions` | Yes | No | Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc... |
+| `analyzeCommits` | No | Yes | Responsible for determining the type of the next release (`major`, `minor` or `patch`). |
+| `verifyRelease` | Yes | No | Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. |
+| `generateNotes` | Yes | No | Responsible for generating the content of the release note. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of each plugin output. |
+| `prepare` | Yes | No | Responsible for preparing the release, for example creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets and pushing a commit. |
+| `publish` | Yes | No | Responsible for publishing the release. |
+| `success` | Yes | No | Responsible for notifying of a new release. |
+| `fail` | Yes | No | Responsible for notifying of a failed release. |
-### verifyConditions plugin
+See [available plugins](../extending/plugins-list.md).
-Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...
+## Plugins configuration
-Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).
-Optional.
-Accept multiple plugins.
+Each plugin must be installed and configured with the [`plugins` options](./configuration.md#plugins) by specifying the list of plugins by npm module name.
-### analyzeCommits plugin
+```bash
+$ npm install @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/npm -D
+```
-Responsible for determining the type of the next release (`major`, `minor` or `patch`).
-
-Default implementation: [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer).
-Required.
-Accept only one plugin.
-
-### verifyRelease plugin
-
-Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`.
-
-Default implementation: none.
-Optional.
-Accept multiple plugins.
-
-### generateNotes plugin
-
-Responsible for generating release notes. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of plugin output.
-
-Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
-Optional.
-Accept multiple plugins.
-
-### prepare plugin
-
-Responsible for preparing the release, including:
-- Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets.
-- Create and push commits
-
-Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#prepare).
-Optional.
-Accept multiple plugins.
-
-### publish plugin
-
-Responsible for publishing the release.
-
-Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#publish) and [@semantic-release/github](https://github.com/semantic-release/github#publish).
-Optional.
-Accept multiple plugins.
-
-### success plugin
-
-Responsible for notifying of a new release.
-
-Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).
-Optional.
-Accept multiple plugins.
-
-### fail plugin
-
-Responsible for notifying of a failed release.
-
-Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#fail).
-Optional.
-Accept multiple plugins.
-
-## Configuration
-
-Plugin can be configured by specifying the plugin's module name or file path directly as a `String` or within the `path` key of an `Object`.
-
-Plugins specific options can be set similarly to the other **semantic-release** [options](configuration.md#options) or within the plugin `Object`. Plugins options defined along with the other **semantic-release** [options](configuration.md#options) will apply to all plugins. Options defined within the plugin `Object` will apply to that specific plugin.
-
-For example:
```json
{
- "release": {
- "verifyConditions": [
- {
- "path": "@semantic-release/exec",
- "cmd": "verify-conditions.sh"
- },
- "@semantic-release/npm",
- "@semantic-release/github"
- ],
- "analyzeCommits": "custom-plugin",
- "verifyRelease": [
- {
- "path": "@semantic-release/exec",
- "cmd": "verify-release.sh"
- },
- ],
- "generateNotes": "./build/my-plugin.js",
- "githubUrl": "https://my-ghe.com",
- "githubApiPathPrefix": "/api-prefix"
- }
+ "plugins": ["@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/npm"]
+}
+```
+
+## Plugin ordering
+
+For each [release step](../../README.md#release-steps) the plugins that implement that step will be executed in the order in which the are defined.
+
+```json
+{
+ "plugins": [
+ "@semantic-release/commit-analyzer",
+ "@semantic-release/release-notes-generator",
+ "@semantic-release/npm",
+ "@semantic-release/git"
+ ]
+}
+```
+
+With this configuration **semantic-release** will:
+- execute the `verifyConditions` implementation of `@semantic-release/npm` then `@semantic-release/git`
+- execute the `analyzeCommits` implementation of `@semantic-release/commit-analyzer`
+- execute the `prepare` implementation of `@semantic-release/npm` then `@semantic-release/git`
+- execute the `generateNotes` implementation of `@semantic-release/release-notes-generator`
+- execute the `publish` implementation of `@semantic-release/npm`
+
+## Plugin options
+
+A plugin options can specified by wrapping the name and an options object in an array. Options configured this way will be passed only to that specific plugin.
+
+Global plugin options can defined at the root of the **semantic-release** configuration object. Options configured this way will be passed to all plugins.
+
+```json
+{
+ "plugins": [
+ "@semantic-release/commit-analyzer",
+ "@semantic-release/release-notes-generator",
+ ["@semantic-release/github", {
+ "assets": ["dist/**"]
+ }],
+ "@semantic-release/git"
+ ],
+ "preset": "angular"
}
```
With this configuration:
-- the `custom-plugin` npm module will be used to [analyze commits](#analyzecommits-plugin)
-- the `./build/my-plugin.js` script will be used to [generate release notes](#generatenotes-plugin)
-- the [`@semantic-release/exec`](https://github.com/semantic-release/exec), [`@semantic-release/npm`](https://github.com/semantic-release/npm) and [`@semantic-release/github`](https://github.com/semantic-release/github) plugins will be used to [verify conditions](#verifyconditions-plugin)
-- the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin will be used to [verify the release](#verifyrelease-plugin)
-- the `cmd` option will be set to `verify-conditions.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify conditions](#verifyconditions-plugin)
-- the `cmd` option will be set to `verify-release.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify the release](#verifyrelease-plugin)
-- the `githubUrl` and `githubApiPathPrefix` options will be set to respectively `https://my-ghe.com` and `/api-prefix` for all plugins
+- All plugins will receive the `preset` option, which will be used by both `@semantic-release/commit-analyzer` and `@semantic-release/release-notes-generator` (and ignored by `@semantic-release/github` and `@semantic-release/git`)
+- The `@semantic-release/github` plugin will receive the `assets` options (`@semantic-release/git` will not receive it and therefore will use it's default value for that option)
diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js
index a63b81ee..ccc60ef1 100644
--- a/lib/definitions/errors.js
+++ b/lib/definitions/errors.js
@@ -61,9 +61,17 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`
required ? 'is required and ' : ''
}must be ${
multiple ? 'a single or an array of plugins' : 'a single plugin'
- } definition. A plugin definition is either a string or an object with a \`path\` property.
+ } definition. A plugin definition is an npm module name, optionnaly wrapped in an array with an object.
Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`,
+ }),
+ EPLUGINSCONF: ({plugin}) => ({
+ 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, optionnaly wrapped in an array with an object.
+
+The invalid configuration is \`${stringify(plugin)}\`.`,
}),
EPLUGIN: ({pluginName, type}) => ({
message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`,
diff --git a/lib/definitions/plugins.js b/lib/definitions/plugins.js
index bac4c6c7..97138e88 100644
--- a/lib/definitions/plugins.js
+++ b/lib/definitions/plugins.js
@@ -6,13 +6,12 @@ const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
module.exports = {
verifyConditions: {
- default: ['@semantic-release/npm', '@semantic-release/github'],
multiple: true,
required: false,
pipelineConfig: () => ({settleAll: true}),
},
analyzeCommits: {
- default: '@semantic-release/commit-analyzer',
+ default: ['@semantic-release/commit-analyzer'],
multiple: false,
required: true,
outputValidator: output => !output || RELEASE_TYPE.includes(output),
@@ -23,13 +22,11 @@ module.exports = {
postprocess: ([result]) => result,
},
verifyRelease: {
- default: false,
multiple: true,
required: false,
pipelineConfig: () => ({settleAll: true}),
},
generateNotes: {
- default: ['@semantic-release/release-notes-generator'],
multiple: true,
required: false,
outputValidator: output => !output || isString(output),
@@ -45,7 +42,6 @@ module.exports = {
postprocess: (results, {env}) => hideSensitive(env)(results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR)),
},
prepare: {
- default: ['@semantic-release/npm'],
multiple: true,
required: false,
pipelineConfig: ({generateNotes}, logger) => ({
@@ -64,7 +60,6 @@ module.exports = {
}),
},
publish: {
- default: ['@semantic-release/npm', '@semantic-release/github'],
multiple: true,
required: false,
outputValidator: output => !output || isPlainObject(output),
@@ -78,14 +73,12 @@ module.exports = {
}),
},
success: {
- default: ['@semantic-release/github'],
multiple: true,
required: false,
pipelineConfig: () => ({settleAll: true}),
preprocess: ({releases, env, ...inputs}) => ({...inputs, env, releases: hideSensitiveValues(env, releases)}),
},
fail: {
- default: ['@semantic-release/github'],
multiple: true,
required: false,
pipelineConfig: () => ({settleAll: true}),
diff --git a/lib/get-config.js b/lib/get-config.js
index 3983e6f7..6ee269ea 100644
--- a/lib/get-config.js
+++ b/lib/get-config.js
@@ -1,4 +1,4 @@
-const {castArray, pickBy, isUndefined, isNull, isString, isPlainObject} = require('lodash');
+const {castArray, pickBy, isNil, isString, isPlainObject} = require('lodash');
const readPkgUp = require('read-pkg-up');
const cosmiconfig = require('cosmiconfig');
const resolveFrom = require('resolve-from');
@@ -35,7 +35,7 @@ module.exports = async (context, opts) => {
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
// so those plugin will be loaded relatively to the config file
Object.entries(extendsOpts).reduce((pluginsPath, [option, value]) => {
- if (PLUGINS_DEFINITIONS[option]) {
+ if (PLUGINS_DEFINITIONS[option] || option === 'plugins') {
castArray(value)
.filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
.map(plugin => (isString(plugin) ? plugin : plugin.path))
@@ -57,8 +57,14 @@ module.exports = async (context, opts) => {
branch: 'master',
repositoryUrl: (await pkgRepoUrl({normalize: false, cwd})) || (await repoUrl({cwd, env})),
tagFormat: `v\${version}`,
+ plugins: [
+ '@semantic-release/commit-analyzer',
+ '@semantic-release/release-notes-generator',
+ '@semantic-release/npm',
+ '@semantic-release/github',
+ ],
// Remove `null` and `undefined` options so they can be replaced with default ones
- ...pickBy(options, option => !isUndefined(option) && !isNull(option)),
+ ...pickBy(options, option => !isNil(option)),
};
debug('options values: %O', options);
diff --git a/lib/get-git-auth-url.js b/lib/get-git-auth-url.js
index e19e366a..fe9493e5 100644
--- a/lib/get-git-auth-url.js
+++ b/lib/get-git-auth-url.js
@@ -1,5 +1,5 @@
const {parse, format} = require('url');
-const {isUndefined} = require('lodash');
+const {isNil} = require('lodash');
const gitUrlParse = require('git-url-parse');
const hostedGitInfo = require('hosted-git-info');
const {verifyAuth} = require('./git');
@@ -44,7 +44,7 @@ module.exports = async ({cwd, env, options: {repositoryUrl, branch}}) => {
try {
await verifyAuth(repositoryUrl, branch, {cwd, env});
} catch (error) {
- const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(env[envVar]));
+ const envVar = Object.keys(GIT_TOKENS).find(envVar => !isNil(env[envVar]));
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
diff --git a/lib/plugins/index.js b/lib/plugins/index.js
index 12a2055c..7f7e5708 100644
--- a/lib/plugins/index.js
+++ b/lib/plugins/index.js
@@ -1,52 +1,89 @@
-const {identity, isPlainObject, omit, castArray, isUndefined} = require('lodash');
+const {identity, isPlainObject, omit, castArray, isNil, isString} = require('lodash');
const AggregateError = require('aggregate-error');
const getError = require('../get-error');
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
-const {validateConfig} = require('./utils');
+const {validatePlugin, validateStep, loadPlugin, parseConfig} = require('./utils');
const pipeline = require('./pipeline');
const normalize = require('./normalize');
module.exports = (context, pluginsPath) => {
- const {options, logger} = context;
+ let {options, logger} = context;
const errors = [];
- const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce(
+
+ 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;
+
+ if (isPlainObject(plugin)) {
+ Object.entries(plugin).forEach(([type, func]) => {
+ if (PLUGINS_DEFINITIONS[type]) {
+ plugins[type] = [...(plugins[type] || []), [func, config]];
+ }
+ });
+ } else {
+ errors.push(getError('EPLUGINSCONF', {plugin}));
+ }
+ } else {
+ errors.push(getError('EPLUGINSCONF', {plugin}));
+ }
+
+ return plugins;
+ }, {})
+ : [];
+
+ if (errors.length > 0) {
+ throw new AggregateError(errors);
+ }
+
+ options = {...plugins, ...options};
+
+ const pluginsConf = Object.entries(PLUGINS_DEFINITIONS).reduce(
(
- plugins,
+ pluginsConf,
[type, {multiple, required, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
) => {
let pluginOpts;
- if (isUndefined(options[type])) {
+ if (isNil(options[type]) && def) {
pluginOpts = def;
} else {
- const defaultPaths = castArray(def);
- // If an object is passed and the path is missing, set the default one for single plugins
- if (isPlainObject(options[type]) && !options[type].path && defaultPaths.length === 1) {
- [options[type].path] = defaultPaths;
+ // If an object is passed and the path is missing, merge it with step options
+ if (isPlainObject(options[type]) && !options[type].path) {
+ options[type] = castArray(plugins[type]).map(
+ plugin => (plugin ? [plugin[0], Object.assign(plugin[1], options[type])] : plugin)
+ );
}
- if (!validateConfig({multiple, required}, options[type])) {
+ if (!validateStep({multiple, required}, options[type])) {
errors.push(getError('EPLUGINCONF', {type, multiple, required, pluginConf: options[type]}));
- return plugins;
+ return pluginsConf;
}
pluginOpts = options[type];
}
const steps = castArray(pluginOpts).map(pluginOpt =>
- normalize({...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS))}, type, pluginOpt, pluginsPath)
+ normalize(
+ {...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS), 'plugins')},
+ type,
+ pluginOpt,
+ pluginsPath
+ )
);
- plugins[type] = async input =>
+ pluginsConf[type] = async input =>
postprocess(
- await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)),
+ await pipeline(steps, pipelineConfig && pipelineConfig(pluginsConf, logger))(await preprocess(input)),
input
);
- return plugins;
+ return pluginsConf;
},
- {}
+ plugins
);
if (errors.length > 0) {
throw new AggregateError(errors);
}
- return plugins;
+
+ return pluginsConf;
};
diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js
index d71bd840..f7cf97e6 100644
--- a/lib/plugins/normalize.js
+++ b/lib/plugins/normalize.js
@@ -1,22 +1,18 @@
-const {dirname} = require('path');
-const {isString, isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash');
-const resolveFrom = require('resolve-from');
+const {isPlainObject, isFunction, noop, cloneDeep, omit} = require('lodash');
const getError = require('../get-error');
const {extractErrors} = require('../utils');
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
+const {loadPlugin, parseConfig} = require('./utils');
-module.exports = ({cwd, stdout, stderr, options, logger}, type, pluginOpt, pluginsPath) => {
+module.exports = (context, type, pluginOpt, pluginsPath) => {
+ const {stdout, stderr, options, logger} = context;
if (!pluginOpt) {
return noop;
}
- const {path, ...config} = isString(pluginOpt) || isFunction(pluginOpt) ? {path: pluginOpt} : pluginOpt;
+ const [path, config] = parseConfig(pluginOpt);
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
-
- const basePath = pluginsPath[path]
- ? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path]))
- : __dirname;
- const plugin = isFunction(path) ? path : require(resolveFrom.silent(basePath, path) || resolveFrom(cwd, path));
+ const plugin = loadPlugin(context, path, pluginsPath);
let func;
if (isFunction(plugin)) {
diff --git a/lib/plugins/utils.js b/lib/plugins/utils.js
index 1ece5627..368ac092 100644
--- a/lib/plugins/utils.js
+++ b/lib/plugins/utils.js
@@ -1,18 +1,68 @@
-const {isString, isFunction, castArray} = require('lodash');
+const {dirname} = require('path');
+const {isString, isFunction, castArray, isArray, isPlainObject, isNil} = require('lodash');
+const resolveFrom = require('resolve-from');
-const validateSingleConfig = conf => {
+const validateStepArrayDefinition = conf =>
+ isArray(conf) &&
+ (conf.length === 1 || conf.length === 2) &&
+ (isString(conf[0]) || isFunction(conf[0])) &&
+ (isNil(conf[1]) || isPlainObject(conf[1]));
+
+const validateSingleStep = conf => {
+ if (validateStepArrayDefinition(conf)) {
+ return true;
+ }
conf = castArray(conf);
- return conf.length === 1 && (isString(conf[0]) || isString(conf[0].path) || isFunction(conf[0]));
+
+ if (conf.length !== 1) {
+ return false;
+ }
+
+ const [path, config] = parseConfig(conf[0]);
+ return (isString(path) || isFunction(path)) && isPlainObject(config);
};
-const validateMultipleConfig = conf => castArray(conf).every(conf => validateSingleConfig(conf));
+const validateMultipleStep = conf => {
+ return conf.every(conf => validateSingleStep(conf));
+};
-const validateConfig = ({multiple, required}, conf) => {
+function validatePlugin(conf) {
+ return (
+ isString(conf) ||
+ (isArray(conf) &&
+ (conf.length === 1 || conf.length === 2) &&
+ (isString(conf[0]) || isPlainObject(conf[0])) &&
+ (isNil(conf[1]) || isPlainObject(conf[1]))) ||
+ (isPlainObject(conf) && (isNil(conf.path) || isString(conf.path) || isPlainObject(conf.path)))
+ );
+}
+
+function validateStep({multiple, required}, conf) {
conf = castArray(conf).filter(Boolean);
if (required) {
- return Boolean(conf) && conf.length >= 1 && (multiple ? validateMultipleConfig : validateSingleConfig)(conf);
+ return conf.length >= 1 && (multiple ? validateMultipleStep : validateSingleStep)(conf);
}
- return conf.length === 0 || (multiple ? validateMultipleConfig : validateSingleConfig)(conf);
-};
+ return conf.length === 0 || (multiple ? validateMultipleStep : validateSingleStep)(conf);
+}
-module.exports = {validateConfig};
+function loadPlugin({cwd}, path, pluginsPath) {
+ const basePath = pluginsPath[path]
+ ? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path]))
+ : __dirname;
+ return isFunction(path) ? path : require(resolveFrom.silent(basePath, path) || resolveFrom(cwd, path));
+}
+
+function parseConfig(plugin) {
+ let path;
+ let config;
+ if (isArray(plugin)) {
+ [path, config] = plugin;
+ } else if (isPlainObject(plugin) && !isNil(plugin.path)) {
+ ({path, ...config} = plugin);
+ } else {
+ path = plugin;
+ }
+ return [path, config || {}];
+}
+
+module.exports = {validatePlugin, validateStep, loadPlugin, parseConfig};
diff --git a/package.json b/package.json
index 4670530f..26580671 100644
--- a/package.json
+++ b/package.json
@@ -19,11 +19,11 @@
"Pierre Vanduynslager (https://twitter.com/@pvdlg_)"
],
"dependencies": {
- "@semantic-release/commit-analyzer": "^6.0.0",
+ "@semantic-release/commit-analyzer": "^6.1.0",
"@semantic-release/error": "^2.2.0",
- "@semantic-release/github": "^5.0.0",
- "@semantic-release/npm": "^5.0.1",
- "@semantic-release/release-notes-generator": "^7.0.0",
+ "@semantic-release/github": "^5.1.0",
+ "@semantic-release/npm": "^5.0.5",
+ "@semantic-release/release-notes-generator": "^7.1.0",
"aggregate-error": "^1.0.0",
"cosmiconfig": "^5.0.1",
"debug": "^4.0.0",
diff --git a/test/cli.test.js b/test/cli.test.js
index 1d21a58d..07778dc2 100644
--- a/test/cli.test.js
+++ b/test/cli.test.js
@@ -33,6 +33,9 @@ test.serial('Pass options to semantic-release API', async t => {
'https://github/com/owner/repo.git',
'-t',
`v\${version}`,
+ '-p',
+ 'plugin1',
+ 'plugin2',
'-e',
'config1',
'config2',
@@ -68,6 +71,7 @@ test.serial('Pass options to semantic-release API', async t => {
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
+ t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']);
t.is(run.args[0][0].analyzeCommits, 'analyze');
@@ -94,6 +98,9 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
'https://github/com/owner/repo.git',
'--tag-format',
`v\${version}`,
+ '--plugins',
+ 'plugin1',
+ 'plugin2',
'--extends',
'config1',
'config2',
@@ -106,6 +113,7 @@ test.serial('Pass options to semantic-release API with alias arguments', async t
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
+ t.deepEqual(run.args[0][0].plugins, ['plugin1', 'plugin2']);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.is(run.args[0][0].dryRun, true);
@@ -151,6 +159,8 @@ test.serial('Do not set properties in option for which arg is not in command lin
t.false('debug' in run.args[0][0]);
t.false('r' in run.args[0][0]);
t.false('t' in run.args[0][0]);
+ t.false('p' in run.args[0][0]);
+ t.false('e' in run.args[0][0]);
});
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
diff --git a/test/get-config.test.js b/test/get-config.test.js
index c34c3855..1cc99612 100644
--- a/test/get-config.test.js
+++ b/test/get-config.test.js
@@ -8,6 +8,13 @@ import {stub} from 'sinon';
import yaml from 'js-yaml';
import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
+const DEFAULT_PLUGINS = [
+ '@semantic-release/commit-analyzer',
+ '@semantic-release/release-notes-generator',
+ '@semantic-release/npm',
+ '@semantic-release/github',
+];
+
test.beforeEach(t => {
t.context.plugins = stub().returns({});
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
@@ -69,6 +76,7 @@ test('Read options from package.json', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), {release: options});
@@ -89,6 +97,7 @@ test('Read options from .releaserc.yml', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json in repository root
await writeFile(path.resolve(cwd, '.releaserc.yml'), yaml.safeDump(options));
@@ -109,6 +118,7 @@ test('Read options from .releaserc.json', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json in repository root
await outputJson(path.resolve(cwd, '.releaserc.json'), options);
@@ -129,6 +139,7 @@ test('Read options from .releaserc.js', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json in repository root
await writeFile(path.resolve(cwd, '.releaserc.js'), `module.exports = ${JSON.stringify(options)}`);
@@ -149,6 +160,7 @@ test('Read options from release.config.js', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json in repository root
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${JSON.stringify(options)}`);
@@ -176,6 +188,7 @@ test('Prioritise CLI/API parameters over file configuration and git repo', async
branch: 'branch_cli',
repositoryUrl: 'http://cli-url.com/owner/package',
tagFormat: `cli\${version}`,
+ plugins: false,
};
const pkg = {release: pkgOptions, repository: 'git@host.null:owner/module.git'};
// Create package.json in repository root
@@ -199,6 +212,7 @@ test('Read configuration from file path in "extends"', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json and shareable.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
@@ -226,6 +240,7 @@ test('Read configuration from module path in "extends"', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json and shareable.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
@@ -259,6 +274,7 @@ test('Read configuration from an array of paths in "extends"', async t => {
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
branch: 'test_branch',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json and shareable.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
@@ -296,6 +312,7 @@ test('Prioritize configuration from config file over "extends"', async t => {
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json and shareable.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
@@ -341,6 +358,7 @@ test('Prioritize configuration from cli/API options over "extends"', async t =>
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
branch: 'test_branch2',
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json, shareable1.json and shareable2.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
@@ -366,11 +384,13 @@ test('Allow to unset properties defined in shareable config with "null"', async
analyzeCommits: null,
branch: 'test_branch',
repositoryUrl: 'https://host.null/owner/module.git',
+ plugins: null,
};
const options1 = {
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
+ plugins: ['test-plugin'],
};
// Create package.json and shareable.json in repository root
await outputJson(path.resolve(cwd, 'package.json'), {release: pkgOptions});
@@ -378,19 +398,25 @@ test('Allow to unset properties defined in shareable config with "null"', async
const {options} = await t.context.getConfig({cwd});
- // Verify the options contains the plugin config from shareable.json
- t.deepEqual(options, {...omit(options1, 'analyzeCommits'), ...omit(pkgOptions, ['extends', 'analyzeCommits'])});
- // Verify the plugins module is called with the plugin options from shareable.json
+ // Verify the options contains the plugin config from shareable.json and the default `plugins`
+ t.deepEqual(options, {
+ ...omit(options1, ['analyzeCommits']),
+ ...omit(pkgOptions, ['extends', 'analyzeCommits']),
+ plugins: DEFAULT_PLUGINS,
+ });
+ // Verify the plugins module is called with the plugin options from shareable.json and the default `plugins`
t.deepEqual(t.context.plugins.args[0][0], {
options: {
...omit(options1, 'analyzeCommits'),
...omit(pkgOptions, ['extends', 'analyzeCommits']),
+ plugins: DEFAULT_PLUGINS,
},
cwd,
});
t.deepEqual(t.context.plugins.args[0][1], {
generateNotes: './shareable.json',
analyzeCommits: './shareable.json',
+ 'test-plugin': './shareable.json',
});
});
@@ -407,6 +433,7 @@ test('Allow to unset properties defined in shareable config with "undefined"', a
generateNotes: 'generateNotes',
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
tagFormat: `v\${version}`,
+ plugins: false,
};
// Create package.json and release.config.js in repository root
await writeFile(path.resolve(cwd, 'release.config.js'), `module.exports = ${format(pkgOptions)}`);
diff --git a/test/index.test.js b/test/index.test.js
index 6c08debb..e5c39a5d 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -64,6 +64,7 @@ test('Plugins are called with expected values', async t => {
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
const options = {
...config,
+ plugins: false,
verifyConditions: [verifyConditions1, verifyConditions2],
analyzeCommits,
verifyRelease,
@@ -359,6 +360,7 @@ test('Log all "verifyConditions" errors', async t => {
const config = {branch: 'master', repositoryUrl, tagFormat: `v\${version}`};
const options = {
...config,
+ plugins: false,
verifyConditions: [stub().rejects(new AggregateError([error1, error2])), stub().rejects(error3)],
fail,
};
diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js
index fbf045a6..96ae1d8e 100644
--- a/test/plugins/normalize.test.js
+++ b/test/plugins/normalize.test.js
@@ -152,7 +152,7 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin
t.regex(error.details, /2/);
});
-test('Plugin is called with "pluginConfig" (omitting "path", adding global config) and input', async t => {
+test('Plugin is called with "pluginConfig" (with object definition) and input', async t => {
const pluginFunction = stub().resolves();
const pluginConf = {path: pluginFunction, conf: 'confValue'};
const options = {global: 'globalValue'};
@@ -167,6 +167,21 @@ test('Plugin is called with "pluginConfig" (omitting "path", adding global confi
);
});
+test('Plugin is called with "pluginConfig" (with array definition) and input', async t => {
+ const pluginFunction = stub().resolves();
+ const pluginConf = [pluginFunction, {conf: 'confValue'}];
+ const options = {global: 'globalValue'};
+ const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
+ await plugin({param: 'param'});
+
+ t.true(
+ pluginFunction.calledWithMatch(
+ {conf: 'confValue', global: 'globalValue'},
+ {param: 'param', logger: t.context.logger}
+ )
+ );
+});
+
test('Prevent plugins to modify "pluginConfig"', async t => {
const pluginFunction = stub().callsFake(pluginConfig => {
pluginConfig.conf.subConf = 'otherConf';
diff --git a/test/plugins/plugins.test.js b/test/plugins/plugins.test.js
index 90059645..b42c39d4 100644
--- a/test/plugins/plugins.test.js
+++ b/test/plugins/plugins.test.js
@@ -29,7 +29,7 @@ test('Export default plugins', t => {
t.is(typeof plugins.fail, 'function');
});
-test('Export plugins based on config', t => {
+test('Export plugins based on steps config', t => {
const plugins = getPlugins(
{
cwd,
@@ -55,6 +55,88 @@ test('Export plugins based on config', t => {
t.is(typeof plugins.fail, 'function');
});
+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(
+ {cwd, logger: t.context.logger, options: {plugins: [plugin1, plugin2], verifyRelease: () => {}}},
+ {}
+ );
+
+ await plugins.verifyConditions({});
+ t.true(plugin1.verifyConditions.calledOnce);
+ t.true(plugin2.verifyConditions.calledOnce);
+
+ await plugins.publish({});
+ t.true(plugin1.publish.calledOnce);
+
+ await plugins.verifyRelease({});
+ t.true(plugin2.verifyRelease.notCalled);
+
+ // Verify the module returns a function for each plugin
+ t.is(typeof plugins.verifyConditions, 'function');
+ t.is(typeof plugins.analyzeCommits, 'function');
+ t.is(typeof plugins.verifyRelease, 'function');
+ t.is(typeof plugins.generateNotes, 'function');
+ t.is(typeof plugins.prepare, 'function');
+ t.is(typeof plugins.publish, 'function');
+ t.is(typeof plugins.success, 'function');
+ t.is(typeof plugins.fail, 'function');
+});
+
+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}}, {});
+
+ await plugins.verifyConditions({});
+ t.true(plugin1.verifyConditions.calledOnce);
+
+ await plugins.publish({});
+ t.true(plugin1.publish.calledOnce);
+
+ // Verify the module returns a function for each plugin
+ t.is(typeof plugins.verifyConditions, 'function');
+ t.is(typeof plugins.analyzeCommits, 'function');
+ t.is(typeof plugins.verifyRelease, 'function');
+ t.is(typeof plugins.generateNotes, 'function');
+ t.is(typeof plugins.prepare, 'function');
+ t.is(typeof plugins.publish, 'function');
+ t.is(typeof plugins.success, 'function');
+ t.is(typeof plugins.fail, 'function');
+});
+
+test('Merge global options, "plugins" options and sptep 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(
+ {
+ cwd,
+ logger: t.context.logger,
+ options: {globalOpt: 'global', plugins: [plugin1, plugin2], verifyRelease: [plugin3]},
+ },
+ {}
+ );
+
+ await plugins.verifyConditions({});
+ t.deepEqual(plugin1[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
+ t.deepEqual(plugin2[0].verifyConditions.args[0][0], {globalOpt: 'global', pluginOpt2: 'plugin2'});
+
+ await plugins.publish({});
+ t.deepEqual(plugin1[0].publish.args[0][0], {globalOpt: 'global', pluginOpt1: 'plugin1'});
+
+ await plugins.verifyRelease({});
+ t.deepEqual(plugin3[0].args[0][0], {globalOpt: 'global', pluginOpt3: 'plugin3'});
+});
+
+test('Unknown steps of plugins configured in "plugins" are ignored', t => {
+ const plugin1 = {verifyConditions: () => {}, unknown: () => {}};
+ const plugins = getPlugins({cwd, logger: t.context.logger, options: {plugins: [plugin1]}}, {});
+
+ t.is(typeof plugins.verifyConditions, 'function');
+ t.is(plugins.unknown, undefined);
+});
+
test('Export plugins loaded from the dependency of a shareable config module', async t => {
const cwd = tempy.directory();
await copy(
@@ -121,11 +203,23 @@ test('Export plugins loaded from the dependency of a shareable config file', asy
test('Use default when only options are passed for a single plugin', t => {
const analyzeCommits = {};
const generateNotes = {};
+ const publish = {};
const success = () => {};
const fail = [() => {}];
const plugins = getPlugins(
- {cwd, logger: t.context.logger, options: {analyzeCommits, generateNotes, success, fail}},
+ {
+ cwd,
+ logger: t.context.logger,
+ options: {
+ plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'],
+ analyzeCommits,
+ generateNotes,
+ publish,
+ success,
+ fail,
+ },
+ },
{}
);
@@ -159,14 +253,20 @@ test('Merge global options with plugin options', async t => {
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
});
-test('Throw an error if plugins configuration are invalid', t => {
+test('Throw an error for each invalid plugin configuration', t => {
const errors = [
...t.throws(() =>
getPlugins(
{
cwd,
logger: t.context.logger,
- options: {verifyConditions: {}, analyzeCommits: [], verifyRelease: [{}], generateNotes: [{path: null}]},
+ options: {
+ plugins: ['@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator'],
+ verifyConditions: 1,
+ analyzeCommits: [],
+ verifyRelease: [{}],
+ generateNotes: [{path: null}],
+ },
},
{}
)
@@ -182,3 +282,38 @@ test('Throw an error if plugins configuration are invalid', t => {
t.is(errors[3].name, 'SemanticReleaseError');
t.is(errors[3].code, 'EPLUGINCONF');
});
+
+test('Throw EPLUGINSCONF error if the "plugins" option contains an old plugin definition (returns a function)', t => {
+ const errors = [
+ ...t.throws(() =>
+ getPlugins(
+ {
+ cwd,
+ logger: t.context.logger,
+ options: {plugins: ['./test/fixtures/multi-plugin', './test/fixtures/plugin-noop', () => {}]},
+ },
+ {}
+ )
+ ),
+ ];
+
+ t.is(errors[0].name, 'SemanticReleaseError');
+ t.is(errors[0].code, 'EPLUGINSCONF');
+ t.is(errors[1].name, 'SemanticReleaseError');
+ t.is(errors[1].code, 'EPLUGINSCONF');
+});
+
+test('Throw EPLUGINSCONF error for each invalid definition if the "plugins" option', t => {
+ const errors = [
+ ...t.throws(() =>
+ getPlugins({cwd, logger: t.context.logger, options: {plugins: [1, {path: 1}, [() => {}, {}, {}]]}}, {})
+ ),
+ ];
+
+ t.is(errors[0].name, 'SemanticReleaseError');
+ t.is(errors[0].code, 'EPLUGINSCONF');
+ t.is(errors[1].name, 'SemanticReleaseError');
+ t.is(errors[1].code, 'EPLUGINSCONF');
+ t.is(errors[2].name, 'SemanticReleaseError');
+ t.is(errors[2].code, 'EPLUGINSCONF');
+});
diff --git a/test/plugins/utils.test.js b/test/plugins/utils.test.js
index 9a1bf350..b7c31e39 100644
--- a/test/plugins/utils.test.js
+++ b/test/plugins/utils.test.js
@@ -1,58 +1,306 @@
import test from 'ava';
-import {validateConfig} from '../../lib/plugins/utils';
+import {validatePlugin, validateStep, loadPlugin, parseConfig} from '../../lib/plugins/utils';
-test('Validate multiple/optional plugin configuration', t => {
+test('validatePlugin', t => {
+ const path = 'plugin-module';
+ const options = {option1: 'value1', option2: 'value2'};
+
+ t.true(validatePlugin(path), 'String definition');
+ t.true(validatePlugin({publish: () => {}}), 'Object definition');
+ t.true(validatePlugin([path]), 'Array definition');
+ t.true(validatePlugin([path, options]), 'Array definition with options');
+ t.true(validatePlugin([{publish: () => {}}, options]), 'Array definition with options and path as object');
+ t.true(validatePlugin({path}), 'Object with path definition');
+ t.true(validatePlugin({path, ...options}), 'Object with path definition with options');
+ t.true(
+ validatePlugin({path: {publish: () => {}}, ...options}),
+ 'Object with path definition with options and path as object'
+ );
+
+ t.false(validatePlugin(1), 'String definition, wrong path');
+ t.false(validatePlugin([]), 'Array definition, missing path');
+ t.false(validatePlugin([path, options, {}]), 'Array definition, additional parameter');
+ t.false(validatePlugin([1]), 'Array definition, wrong path');
+ t.false(validatePlugin([path, 1]), 'Array definition, wrong options');
+ t.false(validatePlugin({path: 1}), 'Object definition, wrong path');
+});
+
+test('validateStep: multiple/optional plugin configuration', t => {
const type = {multiple: true, required: false};
- t.false(validateConfig(type, {}));
- t.false(validateConfig(type, {path: null}));
- t.true(validateConfig(type, {path: 'plugin-path.js'}));
- t.true(validateConfig(type));
- t.true(validateConfig(type, 'plugin-path.js'));
- t.true(validateConfig(type, ['plugin-path.js']));
- t.true(validateConfig(type, () => {}));
- t.true(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
+ // Empty config
+ t.true(validateStep(type));
+ t.true(validateStep(type, []));
+
+ // Single value definition
+ t.true(validateStep(type, 'plugin-path.js'));
+ t.true(validateStep(type, () => {}));
+ t.true(validateStep(type, ['plugin-path.js']));
+ t.true(validateStep(type, [() => {}]));
+ t.false(validateStep(type, {}));
+ t.false(validateStep(type, [{}]));
+
+ // Array type definition
+ t.true(validateStep(type, [['plugin-path.js']]));
+ t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
+ t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
+ t.false(validateStep(type, [['plugin-path.js', 1]]));
+
+ // Object type definition
+ t.true(validateStep(type, {path: 'plugin-path.js'}));
+ t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
+ t.true(validateStep(type, {path: () => {}, options: 'value'}));
+ t.false(validateStep(type, {path: null}));
+
+ // Considered as an Array of 2 definitions and not as one Array definition in case of a muliple plugin type
+ t.false(validateStep(type, [() => {}, {options: 'value'}]));
+ t.false(validateStep(type, ['plugin-path.js', {options: 'value'}]));
+
+ // Multiple definitions
+ t.true(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', 1],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: null},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
});
-test('Validate multiple/required plugin configuration', t => {
+test('validateStep: multiple/required plugin configuration', t => {
const type = {multiple: true, required: true};
- t.false(validateConfig(type, {}));
- t.false(validateConfig(type, {path: null}));
- t.false(validateConfig(type));
- t.true(validateConfig(type, {path: 'plugin-path.js'}));
- t.true(validateConfig(type, 'plugin-path.js'));
- t.true(validateConfig(type, ['plugin-path.js']));
- t.true(validateConfig(type, () => {}));
- t.true(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
+ // Empty config
+ t.false(validateStep(type));
+ t.false(validateStep(type, []));
+
+ // Single value definition
+ t.true(validateStep(type, 'plugin-path.js'));
+ t.true(validateStep(type, () => {}));
+ t.true(validateStep(type, ['plugin-path.js']));
+ t.true(validateStep(type, [() => {}]));
+ t.false(validateStep(type, {}));
+ t.false(validateStep(type, [{}]));
+
+ // Array type definition
+ t.true(validateStep(type, [['plugin-path.js']]));
+ t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
+ t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
+ t.false(validateStep(type, [['plugin-path.js', 1]]));
+
+ // Object type definition
+ t.true(validateStep(type, {path: 'plugin-path.js'}));
+ t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
+ t.true(validateStep(type, {path: () => {}, options: 'value'}));
+ t.false(validateStep(type, {path: null}));
+
+ // Considered as an Array of 2 definitions and not as one Array definition in the case of a muliple plugin type
+ t.false(validateStep(type, [() => {}, {options: 'value'}]));
+ t.false(validateStep(type, ['plugin-path.js', {options: 'value'}]));
+
+ // Multiple definitions
+ t.true(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', 1],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: null},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
});
-test('Validate single/required plugin configuration', t => {
+test('validateStep: single/required plugin configuration', t => {
const type = {multiple: false, required: true};
- t.false(validateConfig(type, {}));
- t.false(validateConfig(type, {path: null}));
- t.false(validateConfig(type, []));
- t.false(validateConfig(type));
- t.false(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
+ // Empty config
+ t.false(validateStep(type));
+ t.false(validateStep(type, []));
- t.true(validateConfig(type, {path: 'plugin-path.js'}));
- t.true(validateConfig(type, 'plugin-path.js'));
- t.true(validateConfig(type, ['plugin-path.js']));
- t.true(validateConfig(type, () => {}));
+ // Single value definition
+ t.true(validateStep(type, 'plugin-path.js'));
+ t.true(validateStep(type, () => {}));
+ t.true(validateStep(type, ['plugin-path.js']));
+ t.true(validateStep(type, [() => {}]));
+ t.false(validateStep(type, {}));
+ t.false(validateStep(type, [{}]));
+
+ // Array type definition
+ t.true(validateStep(type, [['plugin-path.js']]));
+ t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
+ t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
+ t.false(validateStep(type, [['plugin-path.js', 1]]));
+
+ // Object type definition
+ t.true(validateStep(type, {path: 'plugin-path.js'}));
+ t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
+ t.true(validateStep(type, {path: () => {}, options: 'value'}));
+ t.false(validateStep(type, {path: null}));
+
+ // Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type
+ t.true(validateStep(type, [() => {}, {options: 'value'}]));
+ t.true(validateStep(type, ['plugin-path.js', {options: 'value'}]));
+
+ // Multiple definitions
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
});
-test('Validate single/optional plugin configuration', t => {
+test('validateStep: single/optional plugin configuration', t => {
const type = {multiple: false, required: false};
- t.false(validateConfig(type, {}));
- t.false(validateConfig(type, {path: null}));
- t.false(validateConfig(type, [{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
+ // Empty config
+ t.true(validateStep(type));
+ t.true(validateStep(type, []));
- t.true(validateConfig(type));
- t.true(validateConfig(type, []));
- t.true(validateConfig(type, {path: 'plugin-path.js'}));
- t.true(validateConfig(type, 'plugin-path.js'));
- t.true(validateConfig(type, ['plugin-path.js']));
- t.true(validateConfig(type, () => {}));
+ // Single value definition
+ t.true(validateStep(type, 'plugin-path.js'));
+ t.true(validateStep(type, () => {}));
+ t.true(validateStep(type, ['plugin-path.js']));
+ t.true(validateStep(type, [() => {}]));
+ t.false(validateStep(type, {}));
+ t.false(validateStep(type, [{}]));
+
+ // Array type definition
+ t.true(validateStep(type, [['plugin-path.js']]));
+ t.true(validateStep(type, [['plugin-path.js', {options: 'value'}]]));
+ t.true(validateStep(type, [[() => {}, {options: 'value'}]]));
+ t.false(validateStep(type, [['plugin-path.js', 1]]));
+
+ // Object type definition
+ t.true(validateStep(type, {path: 'plugin-path.js'}));
+ t.true(validateStep(type, {path: 'plugin-path.js', options: 'value'}));
+ t.true(validateStep(type, {path: () => {}, options: 'value'}));
+ t.false(validateStep(type, {path: null}));
+
+ // Considered as one Array definition and not as an Array of 2 definitions in case of single plugin type
+ t.true(validateStep(type, [() => {}, {options: 'value'}]));
+ t.true(validateStep(type, ['plugin-path.js', {options: 'value'}]));
+
+ // Multiple definitions
+ t.false(
+ validateStep(type, [
+ 'plugin-path.js',
+ () => {},
+ ['plugin-path.js'],
+ ['plugin-path.js', {options: 'value'}],
+ [() => {}, {options: 'value'}],
+ {path: 'plugin-path.js'},
+ {path: 'plugin-path.js', options: 'value'},
+ {path: () => {}, options: 'value'},
+ ])
+ );
+});
+
+test('loadPlugin', 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'),
+ loadPlugin({cwd}, './plugin-noop', {'./plugin-noop': './test/fixtures'}),
+ 'From a shareable config context'
+ );
+ t.is(func, loadPlugin({cwd}, func, {}), 'Defined as a function');
+});
+
+test('parseConfig', t => {
+ const path = 'plugin-module';
+ const options = {option1: 'value1', option2: 'value2'};
+
+ t.deepEqual(parseConfig(path), [path, {}], 'String definition');
+ t.deepEqual(parseConfig({path}), [path, {}], 'Object definition');
+ t.deepEqual(parseConfig({path, ...options}), [path, options], 'Object definition with options');
+ t.deepEqual(parseConfig([path]), [path, {}], 'Array definition');
+ t.deepEqual(parseConfig([path, options]), [path, options], 'Array definition with options');
});