Skip to content

Commit

Permalink
feat: adds support for plugins (#228)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimeonC committed Mar 11, 2019
1 parent 9e2cf60 commit 289e105
Show file tree
Hide file tree
Showing 11 changed files with 435 additions and 9 deletions.
11 changes: 10 additions & 1 deletion @commitlint/cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,16 @@ async function main(options) {
const loadOpts = {cwd: flags.cwd, file: flags.config};
const loaded = await load(getSeed(flags), loadOpts);
const parserOpts = selectParserOpts(loaded.parserPreset);
const opts = parserOpts ? {parserOpts} : {parserOpts: {}};
const opts = {
parserOpts: {},
plugins: {}
};
if (parserOpts) {
opts.parserOpts = parserOpts;
}
if (loaded.plugins) {
opts.plugins = loaded.plugins;
}
const format = loadFormatter(loaded, flags);

// Strip comments if reading from `.git/COMMIT_EDIT_MSG`
Expand Down
3 changes: 2 additions & 1 deletion @commitlint/lint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@
"cross-env": "5.1.1",
"execa": "0.9.0",
"globby": "8.0.1",
"rimraf": "2.6.1"
"rimraf": "2.6.1",
"proxyquire": "2.1.0"
},
"dependencies": {
"@commitlint/is-ignored": "^7.5.1",
Expand Down
19 changes: 15 additions & 4 deletions @commitlint/lint/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import util from 'util';
import isIgnored from '@commitlint/is-ignored';
import parse from '@commitlint/parse';
import implementations from '@commitlint/rules';
import {toPairs} from 'lodash';
import {toPairs, values} from 'lodash';

const buildCommitMesage = ({header, body, footer}) => {
let message = header;
Expand All @@ -27,13 +27,24 @@ export default async (message, rules = {}, opts = {}) => {
// Parse the commit message
const parsed = await parse(message, undefined, opts.parserOpts);

const mergedImplementations = Object.assign({}, implementations);
if (opts.plugins) {
values(opts.plugins).forEach(plugin => {
if (plugin.rules) {
Object.keys(plugin.rules).forEach(ruleKey => {
mergedImplementations[ruleKey] = plugin.rules[ruleKey];
});
}
});
}

// Find invalid rules configs
const missing = Object.keys(rules).filter(
name => typeof implementations[name] !== 'function'
name => typeof mergedImplementations[name] !== 'function'
);

if (missing.length > 0) {
const names = Object.keys(implementations);
const names = Object.keys(mergedImplementations);
throw new RangeError(
`Found invalid rule names: ${missing.join(
', '
Expand Down Expand Up @@ -120,7 +131,7 @@ export default async (message, rules = {}, opts = {}) => {
return null;
}

const rule = implementations[name];
const rule = mergedImplementations[name];

const [valid, message] = rule(parsed, when, value);

Expand Down
40 changes: 40 additions & 0 deletions @commitlint/lint/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,46 @@ test('fails for custom issue prefix', async t => {
t.false(report.valid);
});

test('fails for custom plugin rule', async t => {
const report = await lint(
'somehting #1',
{
'plugin-rule': [2, 'never']
},
{
plugins: {
'plugin-example': {
rules: {
'plugin-rule': () => [false]
}
}
}
}
);

t.false(report.valid);
});

test('passes for custom plugin rule', async t => {
const report = await lint(
'somehting #1',
{
'plugin-rule': [2, 'never']
},
{
plugins: {
'plugin-example': {
rules: {
'plugin-rule': () => [true]
}
}
}
}
);

t.true(report.valid);
});

test('returns original message only with commit header', async t => {
const message = 'foo: bar';
const report = await lint(message);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
extends: [],
plugins: ['example', '@scope/example']
};
15 changes: 12 additions & 3 deletions @commitlint/load/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import resolveExtends from '@commitlint/resolve-extends';
import cosmiconfig from 'cosmiconfig';
import {toPairs, merge, mergeWith, pick} from 'lodash';
import resolveFrom from 'resolve-from';
import loadPlugin from './utils/loadPlugin';

const w = (a, b) => (Array.isArray(b) ? b : undefined);
const valid = input =>
pick(input, 'extends', 'rules', 'parserPreset', 'formatter');
pick(input, 'extends', 'plugins', 'rules', 'parserPreset', 'formatter');

export default async (seed = {}, options = {cwd: process.cwd()}) => {
const loaded = await loadConfig(options.cwd, options.file);
Expand All @@ -16,8 +17,8 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => {
// Merge passed config with file based options
const config = valid(merge(loaded.config, seed));
const opts = merge(
{extends: [], rules: {}, formatter: '@commitlint/format'},
pick(config, 'extends')
{extends: [], plugins: [], rules: {}, formatter: '@commitlint/format'},
pick(config, 'extends', 'plugins')
);

// Resolve parserPreset key
Expand Down Expand Up @@ -55,6 +56,14 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => {
resolveFrom.silent(base, config.formatter) || config.formatter;
}

// resolve plugins
preset.plugins = {};
if (config.plugins && config.plugins.length) {
config.plugins.forEach(pluginKey => {
loadPlugin(preset.plugins, pluginKey, process.env.DEBUG === 'true');
});
}

// Execute rule config functions if needed
const executed = await Promise.all(
['rules']
Expand Down
52 changes: 52 additions & 0 deletions @commitlint/load/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import resolveFrom from 'resolve-from';

import load from '.';

const proxyquire = require('proxyquire')
.noCallThru()
.noPreserveCache();

test('extends-empty should have no rules', async t => {
const cwd = await git.bootstrap('fixtures/extends-empty');
const actual = await load({}, {cwd});
Expand All @@ -24,6 +28,41 @@ test('rules should be loaded from specify config file', async t => {
t.is(actual.rules.foo, 'bar');
});

test('plugins should be loaded from seed', async t => {
const plugin = {'@global': true};
const scopedPlugin = {'@global': true};
const stubbedLoad = proxyquire('.', {
'commitlint-plugin-example': plugin,
'@scope/commitlint-plugin-example': scopedPlugin
});

const cwd = await git.bootstrap('fixtures/extends-empty');
const actual = await stubbedLoad(
{plugins: ['example', '@scope/example']},
{cwd}
);
t.deepEqual(actual.plugins, {
example: plugin,
'@scope/example': scopedPlugin
});
});

test('plugins should be loaded from config', async t => {
const plugin = {'@global': true};
const scopedPlugin = {'@global': true};
const stubbedLoad = proxyquire('.', {
'commitlint-plugin-example': plugin,
'@scope/commitlint-plugin-example': scopedPlugin
});

const cwd = await git.bootstrap('fixtures/extends-plugins');
const actual = await stubbedLoad({}, {cwd});
t.deepEqual(actual.plugins, {
example: plugin,
'@scope/example': scopedPlugin
});
});

test('uses seed with parserPreset', async t => {
const cwd = await git.bootstrap('fixtures/parser-preset');
const {parserPreset: actual} = await load(
Expand Down Expand Up @@ -61,6 +100,7 @@ test('respects cwd option', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: ['./second-extended'],
plugins: {},
rules: {
one: 1,
two: 2
Expand All @@ -74,6 +114,7 @@ test('recursive extends', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: ['./first-extended'],
plugins: {},
rules: {
zero: 0,
one: 1,
Expand All @@ -89,6 +130,7 @@ test('recursive extends with json file', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: ['./first-extended'],
plugins: {},
rules: {
zero: 0,
one: 1,
Expand All @@ -104,6 +146,7 @@ test('recursive extends with yaml file', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: ['./first-extended'],
plugins: {},
rules: {
zero: 0,
one: 1,
Expand All @@ -119,6 +162,7 @@ test('recursive extends with js file', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: ['./first-extended'],
plugins: {},
rules: {
zero: 0,
one: 1,
Expand All @@ -134,6 +178,7 @@ test('recursive extends with package.json file', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: ['./first-extended'],
plugins: {},
rules: {
zero: 0,
one: 1,
Expand Down Expand Up @@ -169,6 +214,7 @@ test('ignores unknow keys', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: [],
plugins: {},
rules: {
foo: 'bar',
baz: 'bar'
Expand All @@ -183,6 +229,7 @@ test('ignores unknow keys recursively', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: ['./one'],
plugins: {},
rules: {
zero: 0,
one: 1
Expand All @@ -200,6 +247,7 @@ test('find up from given cwd', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: [],
plugins: {},
rules: {
child: true,
inner: false,
Expand All @@ -216,6 +264,7 @@ test('find up config from outside current git repo', async t => {
t.deepEqual(actual, {
formatter: '@commitlint/format',
extends: [],
plugins: {},
rules: {
child: false,
inner: false,
Expand All @@ -231,6 +280,7 @@ test('respects formatter option', async t => {
t.deepEqual(actual, {
formatter: 'commitlint-junit',
extends: [],
plugins: {},
rules: {}
});
});
Expand All @@ -242,6 +292,7 @@ test('resolves formatter relative from config directory', async t => {
t.deepEqual(actual, {
formatter: resolveFrom(cwd, './formatters/custom.js'),
extends: [],
plugins: {},
rules: {}
});
});
Expand All @@ -253,6 +304,7 @@ test('returns formatter name when unable to resolve from config directory', asyn
t.deepEqual(actual, {
formatter: './doesnt/exists.js',
extends: [],
plugins: {},
rules: {}
});
});
74 changes: 74 additions & 0 deletions @commitlint/load/src/utils/loadPlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import path from 'path';
import chalk from 'chalk';
import {normalizePackageName, getShorthandName} from './pluginNaming';

export default function loadPlugin(plugins, pluginName, debug = false) {
const longName = normalizePackageName(pluginName);
const shortName = getShorthandName(longName);
let plugin = null;

if (pluginName.match(/\s+/u)) {
const whitespaceError = new Error(
`Whitespace found in plugin name '${pluginName}'`
);

whitespaceError.messageTemplate = 'whitespace-found';
whitespaceError.messageData = {
pluginName: longName
};
throw whitespaceError;
}

const pluginKey = longName === pluginName ? shortName : pluginName;

if (!plugins[pluginKey]) {
try {
plugin = require(longName);
} catch (pluginLoadErr) {
try {
// Check whether the plugin exists
require.resolve(longName);
} catch (missingPluginErr) {
// If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
console.error(chalk.red(`Failed to load plugin ${longName}.`));
missingPluginErr.message = `Failed to load plugin ${pluginName}: ${
missingPluginErr.message
}`;
missingPluginErr.messageTemplate = 'plugin-missing';
missingPluginErr.messageData = {
pluginName: longName,
commitlintPath: path.resolve(__dirname, '../..')
};
throw missingPluginErr;
}

// Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
throw pluginLoadErr;
}

// This step is costly, so skip if debug is disabled
if (debug) {
const resolvedPath = require.resolve(longName);

let version = null;

try {
version = require(`${longName}/package.json`).version;
} catch (e) {
// Do nothing
}

const loadedPluginAndVersion = version
? `${longName}@${version}`
: `${longName}, version unknown`;

console.log(
chalk.blue(
`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${resolvedPath})`
)
);
}

plugins[pluginKey] = plugin;
}
}

0 comments on commit 289e105

Please sign in to comment.