Skip to content

Commit cea4564

Browse files
SimeonCmarionebl
authored andcommitted
feat: adds support for plugins (#228) (#588)
1 parent 4ee4544 commit cea4564

File tree

11 files changed

+435
-9
lines changed

11 files changed

+435
-9
lines changed

@commitlint/cli/src/cli.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,16 @@ async function main(options) {
139139
const loadOpts = {cwd: flags.cwd, file: flags.config};
140140
const loaded = await load(getSeed(flags), loadOpts);
141141
const parserOpts = selectParserOpts(loaded.parserPreset);
142-
const opts = parserOpts ? {parserOpts} : {parserOpts: {}};
142+
const opts = {
143+
parserOpts: {},
144+
plugins: {}
145+
};
146+
if (parserOpts) {
147+
opts.parserOpts = parserOpts;
148+
}
149+
if (loaded.plugins) {
150+
opts.plugins = loaded.plugins;
151+
}
143152
const format = loadFormatter(loaded, flags);
144153

145154
// Strip comments if reading from `.git/COMMIT_EDIT_MSG`

@commitlint/lint/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@
6767
"cross-env": "5.1.1",
6868
"execa": "0.9.0",
6969
"globby": "8.0.1",
70-
"rimraf": "2.6.1"
70+
"rimraf": "2.6.1",
71+
"proxyquire": "2.1.0"
7172
},
7273
"dependencies": {
7374
"@commitlint/is-ignored": "^7.5.1",

@commitlint/lint/src/index.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import util from 'util';
22
import isIgnored from '@commitlint/is-ignored';
33
import parse from '@commitlint/parse';
44
import implementations from '@commitlint/rules';
5-
import {toPairs} from 'lodash';
5+
import {toPairs, values} from 'lodash';
66

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

30+
const mergedImplementations = Object.assign({}, implementations);
31+
if (opts.plugins) {
32+
values(opts.plugins).forEach(plugin => {
33+
if (plugin.rules) {
34+
Object.keys(plugin.rules).forEach(ruleKey => {
35+
mergedImplementations[ruleKey] = plugin.rules[ruleKey];
36+
});
37+
}
38+
});
39+
}
40+
3041
// Find invalid rules configs
3142
const missing = Object.keys(rules).filter(
32-
name => typeof implementations[name] !== 'function'
43+
name => typeof mergedImplementations[name] !== 'function'
3344
);
3445

3546
if (missing.length > 0) {
36-
const names = Object.keys(implementations);
47+
const names = Object.keys(mergedImplementations);
3748
throw new RangeError(
3849
`Found invalid rule names: ${missing.join(
3950
', '
@@ -120,7 +131,7 @@ export default async (message, rules = {}, opts = {}) => {
120131
return null;
121132
}
122133

123-
const rule = implementations[name];
134+
const rule = mergedImplementations[name];
124135

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

@commitlint/lint/src/index.test.js

+40
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,46 @@ test('fails for custom issue prefix', async t => {
184184
t.false(report.valid);
185185
});
186186

187+
test('fails for custom plugin rule', async t => {
188+
const report = await lint(
189+
'somehting #1',
190+
{
191+
'plugin-rule': [2, 'never']
192+
},
193+
{
194+
plugins: {
195+
'plugin-example': {
196+
rules: {
197+
'plugin-rule': () => [false]
198+
}
199+
}
200+
}
201+
}
202+
);
203+
204+
t.false(report.valid);
205+
});
206+
207+
test('passes for custom plugin rule', async t => {
208+
const report = await lint(
209+
'somehting #1',
210+
{
211+
'plugin-rule': [2, 'never']
212+
},
213+
{
214+
plugins: {
215+
'plugin-example': {
216+
rules: {
217+
'plugin-rule': () => [true]
218+
}
219+
}
220+
}
221+
}
222+
);
223+
224+
t.true(report.valid);
225+
});
226+
187227
test('returns original message only with commit header', async t => {
188228
const message = 'foo: bar';
189229
const report = await lint(message);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
extends: [],
3+
plugins: ['example', '@scope/example']
4+
};

@commitlint/load/src/index.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import resolveExtends from '@commitlint/resolve-extends';
44
import cosmiconfig from 'cosmiconfig';
55
import {toPairs, merge, mergeWith, pick} from 'lodash';
66
import resolveFrom from 'resolve-from';
7+
import loadPlugin from './utils/loadPlugin';
78

89
const w = (a, b) => (Array.isArray(b) ? b : undefined);
910
const valid = input =>
10-
pick(input, 'extends', 'rules', 'parserPreset', 'formatter');
11+
pick(input, 'extends', 'plugins', 'rules', 'parserPreset', 'formatter');
1112

1213
export default async (seed = {}, options = {cwd: process.cwd()}) => {
1314
const loaded = await loadConfig(options.cwd, options.file);
@@ -16,8 +17,8 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => {
1617
// Merge passed config with file based options
1718
const config = valid(merge(loaded.config, seed));
1819
const opts = merge(
19-
{extends: [], rules: {}, formatter: '@commitlint/format'},
20-
pick(config, 'extends')
20+
{extends: [], plugins: [], rules: {}, formatter: '@commitlint/format'},
21+
pick(config, 'extends', 'plugins')
2122
);
2223

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

59+
// resolve plugins
60+
preset.plugins = {};
61+
if (config.plugins && config.plugins.length) {
62+
config.plugins.forEach(pluginKey => {
63+
loadPlugin(preset.plugins, pluginKey, process.env.DEBUG === 'true');
64+
});
65+
}
66+
5867
// Execute rule config functions if needed
5968
const executed = await Promise.all(
6069
['rules']

@commitlint/load/src/index.test.js

+52
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import resolveFrom from 'resolve-from';
55

66
import load from '.';
77

8+
const proxyquire = require('proxyquire')
9+
.noCallThru()
10+
.noPreserveCache();
11+
812
test('extends-empty should have no rules', async t => {
913
const cwd = await git.bootstrap('fixtures/extends-empty');
1014
const actual = await load({}, {cwd});
@@ -24,6 +28,41 @@ test('rules should be loaded from specify config file', async t => {
2428
t.is(actual.rules.foo, 'bar');
2529
});
2630

31+
test('plugins should be loaded from seed', async t => {
32+
const plugin = {'@global': true};
33+
const scopedPlugin = {'@global': true};
34+
const stubbedLoad = proxyquire('.', {
35+
'commitlint-plugin-example': plugin,
36+
'@scope/commitlint-plugin-example': scopedPlugin
37+
});
38+
39+
const cwd = await git.bootstrap('fixtures/extends-empty');
40+
const actual = await stubbedLoad(
41+
{plugins: ['example', '@scope/example']},
42+
{cwd}
43+
);
44+
t.deepEqual(actual.plugins, {
45+
example: plugin,
46+
'@scope/example': scopedPlugin
47+
});
48+
});
49+
50+
test('plugins should be loaded from config', async t => {
51+
const plugin = {'@global': true};
52+
const scopedPlugin = {'@global': true};
53+
const stubbedLoad = proxyquire('.', {
54+
'commitlint-plugin-example': plugin,
55+
'@scope/commitlint-plugin-example': scopedPlugin
56+
});
57+
58+
const cwd = await git.bootstrap('fixtures/extends-plugins');
59+
const actual = await stubbedLoad({}, {cwd});
60+
t.deepEqual(actual.plugins, {
61+
example: plugin,
62+
'@scope/example': scopedPlugin
63+
});
64+
});
65+
2766
test('uses seed with parserPreset', async t => {
2867
const cwd = await git.bootstrap('fixtures/parser-preset');
2968
const {parserPreset: actual} = await load(
@@ -61,6 +100,7 @@ test('respects cwd option', async t => {
61100
t.deepEqual(actual, {
62101
formatter: '@commitlint/format',
63102
extends: ['./second-extended'],
103+
plugins: {},
64104
rules: {
65105
one: 1,
66106
two: 2
@@ -74,6 +114,7 @@ test('recursive extends', async t => {
74114
t.deepEqual(actual, {
75115
formatter: '@commitlint/format',
76116
extends: ['./first-extended'],
117+
plugins: {},
77118
rules: {
78119
zero: 0,
79120
one: 1,
@@ -89,6 +130,7 @@ test('recursive extends with json file', async t => {
89130
t.deepEqual(actual, {
90131
formatter: '@commitlint/format',
91132
extends: ['./first-extended'],
133+
plugins: {},
92134
rules: {
93135
zero: 0,
94136
one: 1,
@@ -104,6 +146,7 @@ test('recursive extends with yaml file', async t => {
104146
t.deepEqual(actual, {
105147
formatter: '@commitlint/format',
106148
extends: ['./first-extended'],
149+
plugins: {},
107150
rules: {
108151
zero: 0,
109152
one: 1,
@@ -119,6 +162,7 @@ test('recursive extends with js file', async t => {
119162
t.deepEqual(actual, {
120163
formatter: '@commitlint/format',
121164
extends: ['./first-extended'],
165+
plugins: {},
122166
rules: {
123167
zero: 0,
124168
one: 1,
@@ -134,6 +178,7 @@ test('recursive extends with package.json file', async t => {
134178
t.deepEqual(actual, {
135179
formatter: '@commitlint/format',
136180
extends: ['./first-extended'],
181+
plugins: {},
137182
rules: {
138183
zero: 0,
139184
one: 1,
@@ -169,6 +214,7 @@ test('ignores unknow keys', async t => {
169214
t.deepEqual(actual, {
170215
formatter: '@commitlint/format',
171216
extends: [],
217+
plugins: {},
172218
rules: {
173219
foo: 'bar',
174220
baz: 'bar'
@@ -183,6 +229,7 @@ test('ignores unknow keys recursively', async t => {
183229
t.deepEqual(actual, {
184230
formatter: '@commitlint/format',
185231
extends: ['./one'],
232+
plugins: {},
186233
rules: {
187234
zero: 0,
188235
one: 1
@@ -200,6 +247,7 @@ test('find up from given cwd', async t => {
200247
t.deepEqual(actual, {
201248
formatter: '@commitlint/format',
202249
extends: [],
250+
plugins: {},
203251
rules: {
204252
child: true,
205253
inner: false,
@@ -216,6 +264,7 @@ test('find up config from outside current git repo', async t => {
216264
t.deepEqual(actual, {
217265
formatter: '@commitlint/format',
218266
extends: [],
267+
plugins: {},
219268
rules: {
220269
child: false,
221270
inner: false,
@@ -231,6 +280,7 @@ test('respects formatter option', async t => {
231280
t.deepEqual(actual, {
232281
formatter: 'commitlint-junit',
233282
extends: [],
283+
plugins: {},
234284
rules: {}
235285
});
236286
});
@@ -242,6 +292,7 @@ test('resolves formatter relative from config directory', async t => {
242292
t.deepEqual(actual, {
243293
formatter: resolveFrom(cwd, './formatters/custom.js'),
244294
extends: [],
295+
plugins: {},
245296
rules: {}
246297
});
247298
});
@@ -253,6 +304,7 @@ test('returns formatter name when unable to resolve from config directory', asyn
253304
t.deepEqual(actual, {
254305
formatter: './doesnt/exists.js',
255306
extends: [],
307+
plugins: {},
256308
rules: {}
257309
});
258310
});
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import path from 'path';
2+
import chalk from 'chalk';
3+
import {normalizePackageName, getShorthandName} from './pluginNaming';
4+
5+
export default function loadPlugin(plugins, pluginName, debug = false) {
6+
const longName = normalizePackageName(pluginName);
7+
const shortName = getShorthandName(longName);
8+
let plugin = null;
9+
10+
if (pluginName.match(/\s+/u)) {
11+
const whitespaceError = new Error(
12+
`Whitespace found in plugin name '${pluginName}'`
13+
);
14+
15+
whitespaceError.messageTemplate = 'whitespace-found';
16+
whitespaceError.messageData = {
17+
pluginName: longName
18+
};
19+
throw whitespaceError;
20+
}
21+
22+
const pluginKey = longName === pluginName ? shortName : pluginName;
23+
24+
if (!plugins[pluginKey]) {
25+
try {
26+
plugin = require(longName);
27+
} catch (pluginLoadErr) {
28+
try {
29+
// Check whether the plugin exists
30+
require.resolve(longName);
31+
} catch (missingPluginErr) {
32+
// If the plugin can't be resolved, display the missing plugin error (usually a config or install error)
33+
console.error(chalk.red(`Failed to load plugin ${longName}.`));
34+
missingPluginErr.message = `Failed to load plugin ${pluginName}: ${
35+
missingPluginErr.message
36+
}`;
37+
missingPluginErr.messageTemplate = 'plugin-missing';
38+
missingPluginErr.messageData = {
39+
pluginName: longName,
40+
commitlintPath: path.resolve(__dirname, '../..')
41+
};
42+
throw missingPluginErr;
43+
}
44+
45+
// Otherwise, the plugin exists and is throwing on module load for some reason, so print the stack trace.
46+
throw pluginLoadErr;
47+
}
48+
49+
// This step is costly, so skip if debug is disabled
50+
if (debug) {
51+
const resolvedPath = require.resolve(longName);
52+
53+
let version = null;
54+
55+
try {
56+
version = require(`${longName}/package.json`).version;
57+
} catch (e) {
58+
// Do nothing
59+
}
60+
61+
const loadedPluginAndVersion = version
62+
? `${longName}@${version}`
63+
: `${longName}, version unknown`;
64+
65+
console.log(
66+
chalk.blue(
67+
`Loaded plugin ${pluginName} (${loadedPluginAndVersion}) (from ${resolvedPath})`
68+
)
69+
);
70+
}
71+
72+
plugins[pluginKey] = plugin;
73+
}
74+
}

0 commit comments

Comments
 (0)