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

feat: add support for plugins (#228) #588

Merged
merged 1 commit into from
Apr 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}