Skip to content

Commit 3f6d95c

Browse files
committed
feat: basic user config validation
1 parent 3b71d40 commit 3f6d95c

File tree

9 files changed

+192
-126
lines changed

9 files changed

+192
-126
lines changed

@commitlint/cli/src/cli.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ function getSeed(flags: CliFlags): Seed {
323323
: {parserPreset: flags['parser-preset']};
324324
}
325325

326-
function selectParserOpts(parserPreset: ParserPreset) {
326+
function selectParserOpts(parserPreset: ParserPreset | undefined) {
327327
if (typeof parserPreset !== 'object') {
328328
return undefined;
329329
}

@commitlint/load/src/load.test.ts

+26-18
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ test('extends-empty should have no rules', async () => {
2222
const actual = await load({}, {cwd});
2323

2424
expect(actual.rules).toMatchObject({});
25+
expect(actual.parserPreset).not.toBeDefined();
2526
});
2627

2728
test('uses seed as configured', async () => {
@@ -128,8 +129,9 @@ test('uses seed with parserPreset', async () => {
128129
{cwd}
129130
);
130131

131-
expect(actual.name).toBe('./conventional-changelog-custom');
132-
expect(actual.parserOpts).toMatchObject({
132+
expect(actual).toBeDefined();
133+
expect(actual!.name).toBe('./conventional-changelog-custom');
134+
expect(actual!.parserOpts).toMatchObject({
133135
headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/,
134136
});
135137
});
@@ -253,8 +255,9 @@ test('parser preset overwrites completely instead of merging', async () => {
253255
const cwd = await gitBootstrap('fixtures/parser-preset-override');
254256
const actual = await load({}, {cwd});
255257

256-
expect(actual.parserPreset.name).toBe('./custom');
257-
expect(actual.parserPreset.parserOpts).toMatchObject({
258+
expect(actual.parserPreset).toBeDefined();
259+
expect(actual.parserPreset!.name).toBe('./custom');
260+
expect(actual.parserPreset!.parserOpts).toMatchObject({
258261
headerPattern: /.*/,
259262
});
260263
});
@@ -263,8 +266,9 @@ test('recursive extends with parserPreset', async () => {
263266
const cwd = await gitBootstrap('fixtures/recursive-parser-preset');
264267
const actual = await load({}, {cwd});
265268

266-
expect(actual.parserPreset.name).toBe('./conventional-changelog-custom');
267-
expect(actual.parserPreset.parserOpts).toMatchObject({
269+
expect(actual.parserPreset).toBeDefined();
270+
expect(actual.parserPreset!.name).toBe('./conventional-changelog-custom');
271+
expect(actual.parserPreset!.parserOpts).toMatchObject({
268272
headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/,
269273
});
270274
});
@@ -387,11 +391,12 @@ test('resolves parser preset from conventional commits', async () => {
387391
const cwd = await npmBootstrap('fixtures/parser-preset-conventionalcommits');
388392
const actual = await load({}, {cwd});
389393

390-
expect(actual.parserPreset.name).toBe(
394+
expect(actual.parserPreset).toBeDefined();
395+
expect(actual.parserPreset!.name).toBe(
391396
'conventional-changelog-conventionalcommits'
392397
);
393-
expect(typeof actual.parserPreset.parserOpts).toBe('object');
394-
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
398+
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
399+
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
395400
/^(\w*)(?:\((.*)\))?!?: (.*)$/
396401
);
397402
});
@@ -400,9 +405,10 @@ test('resolves parser preset from conventional angular', async () => {
400405
const cwd = await npmBootstrap('fixtures/parser-preset-angular');
401406
const actual = await load({}, {cwd});
402407

403-
expect(actual.parserPreset.name).toBe('conventional-changelog-angular');
404-
expect(typeof actual.parserPreset.parserOpts).toBe('object');
405-
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
408+
expect(actual.parserPreset).toBeDefined();
409+
expect(actual.parserPreset!.name).toBe('conventional-changelog-angular');
410+
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
411+
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
406412
/^(\w*)(?:\((.*)\))?: (.*)$/
407413
);
408414
});
@@ -418,9 +424,10 @@ test('recursive resolves parser preset from conventional atom', async () => {
418424

419425
const actual = await load({}, {cwd});
420426

421-
expect(actual.parserPreset.name).toBe('conventional-changelog-atom');
422-
expect(typeof actual.parserPreset.parserOpts).toBe('object');
423-
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
427+
expect(actual.parserPreset).toBeDefined();
428+
expect(actual.parserPreset!.name).toBe('conventional-changelog-atom');
429+
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
430+
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
424431
/^(:.*?:) (.*)$/
425432
);
426433
}, 10000);
@@ -431,11 +438,12 @@ test('resolves parser preset from conventional commits without factory support',
431438
);
432439
const actual = await load({}, {cwd});
433440

434-
expect(actual.parserPreset.name).toBe(
441+
expect(actual.parserPreset).toBeDefined();
442+
expect(actual.parserPreset!.name).toBe(
435443
'conventional-changelog-conventionalcommits'
436444
);
437-
expect(typeof actual.parserPreset.parserOpts).toBe('object');
438-
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
445+
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
446+
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
439447
/^(\w*)(?:\((.*)\))?!?: (.*)$/
440448
);
441449
});

@commitlint/load/src/load.ts

+59-70
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import Path from 'path';
22

33
import merge from 'lodash/merge';
4-
import mergeWith from 'lodash/mergeWith';
5-
import pick from 'lodash/pick';
64
import union from 'lodash/union';
75
import resolveFrom from 'resolve-from';
86

@@ -12,18 +10,15 @@ import {
1210
UserConfig,
1311
LoadOptions,
1412
QualifiedConfig,
15-
UserPreset,
1613
QualifiedRules,
17-
ParserPreset,
14+
PluginRecords,
1815
} from '@commitlint/types';
1916

2017
import loadPlugin from './utils/load-plugin';
2118
import {loadConfig} from './utils/load-config';
22-
import {loadParserOpts} from './utils/load-parser-opts';
19+
import {loadParser} from './utils/load-parser-opts';
2320
import {pickConfig} from './utils/pick-config';
24-
25-
const w = <T>(_: unknown, b: ArrayLike<T> | null | undefined | false) =>
26-
Array.isArray(b) ? b : undefined;
21+
import {validateConfig} from './utils/validators';
2722

2823
export default async function load(
2924
seed: UserConfig = {},
@@ -37,11 +32,17 @@ export default async function load(
3732
// Might amount to breaking changes, defer until 9.0.0
3833

3934
// Merge passed config with file based options
40-
const config = pickConfig(merge({}, loaded ? loaded.config : null, seed));
41-
42-
const opts = merge(
43-
{extends: [], rules: {}, formatter: '@commitlint/format'},
44-
pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores')
35+
const config = pickConfig(
36+
merge(
37+
{
38+
rules: {},
39+
formatter: '@commitlint/format',
40+
helpUrl:
41+
'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
42+
},
43+
loaded ? loaded.config : null,
44+
seed
45+
)
4546
);
4647

4748
// Resolve parserPreset key
@@ -56,75 +57,63 @@ export default async function load(
5657
}
5758

5859
// Resolve extends key
59-
const extended = resolveExtends(opts, {
60+
const extended = resolveExtends(config, {
6061
prefix: 'commitlint-config',
6162
cwd: base,
6263
parserPreset: config.parserPreset,
6364
});
6465

65-
const preset = (pickConfig(
66-
mergeWith(extended, config, w)
67-
) as unknown) as UserPreset;
68-
preset.plugins = {};
69-
70-
// TODO: check if this is still necessary with the new factory based conventional changelog parsers
71-
// config.extends = Array.isArray(config.extends) ? config.extends : [];
72-
73-
// Resolve parser-opts from preset
74-
if (typeof preset.parserPreset === 'object') {
75-
preset.parserPreset.parserOpts = await loadParserOpts(
76-
preset.parserPreset.name,
77-
// TODO: fix the types for factory based conventional changelog parsers
78-
preset.parserPreset as any
79-
);
80-
}
81-
82-
// Resolve config-relative formatter module
83-
if (typeof config.formatter === 'string') {
84-
preset.formatter =
85-
resolveFrom.silent(base, config.formatter) || config.formatter;
86-
}
87-
88-
// Read plugins from extends
89-
if (Array.isArray(extended.plugins)) {
90-
config.plugins = union(config.plugins, extended.plugins || []);
91-
}
92-
93-
// resolve plugins
94-
if (Array.isArray(config.plugins)) {
95-
config.plugins.forEach((plugin) => {
96-
if (typeof plugin === 'string') {
97-
loadPlugin(preset.plugins, plugin, process.env.DEBUG === 'true');
98-
} else {
99-
preset.plugins.local = plugin;
100-
}
101-
});
102-
}
66+
validateConfig(extended);
67+
68+
let plugins: PluginRecords = {};
69+
// TODO: this object merging should be done in resolveExtends
70+
union(
71+
// Read plugins from config
72+
Array.isArray(config.plugins) ? config.plugins : [],
73+
// Read plugins from extends
74+
Array.isArray(extended.plugins) ? extended.plugins : []
75+
).forEach((plugin) => {
76+
if (typeof plugin === 'string') {
77+
plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true');
78+
} else {
79+
plugins.local = plugin;
80+
}
81+
});
10382

104-
const rules = preset.rules ? preset.rules : {};
105-
const qualifiedRules = (
83+
const rules = (
10684
await Promise.all(
107-
Object.entries(rules || {}).map((entry) => executeRule<any>(entry))
85+
Object.entries({
86+
...(typeof extended.rules === 'object' ? extended.rules || {} : {}),
87+
...(typeof config.rules === 'object' ? config.rules || {} : {}),
88+
}).map((entry) => executeRule(entry))
10889
)
10990
).reduce<QualifiedRules>((registry, item) => {
110-
const [key, value] = item as any;
111-
(registry as any)[key] = value;
91+
// type of `item` can be null, but Object.entries always returns key pair
92+
const [key, value] = item!;
93+
registry[key] = value;
11294
return registry;
11395
}, {});
11496

115-
const helpUrl =
116-
typeof config.helpUrl === 'string'
117-
? config.helpUrl
118-
: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint';
119-
12097
return {
121-
extends: preset.extends!,
122-
formatter: preset.formatter!,
123-
parserPreset: preset.parserPreset! as ParserPreset,
124-
ignores: preset.ignores!,
125-
defaultIgnores: preset.defaultIgnores!,
126-
plugins: preset.plugins!,
127-
rules: qualifiedRules,
128-
helpUrl,
98+
// TODO: check if this is still necessary with the new factory based conventional changelog parsers
99+
// TODO: should this function return this? as those values are already resolved
100+
extends: Array.isArray(extended.extends)
101+
? extended.extends
102+
: typeof extended.extends === 'string'
103+
? [extended.extends]
104+
: [],
105+
// Resolve config-relative formatter module
106+
formatter:
107+
resolveFrom.silent(base, extended.formatter) || extended.formatter,
108+
// Resolve parser-opts from preset
109+
parserPreset: await loadParser(extended.parserPreset),
110+
ignores: extended.ignores,
111+
defaultIgnores: extended.defaultIgnores,
112+
plugins: plugins,
113+
rules: rules,
114+
helpUrl:
115+
typeof extended.helpUrl === 'string'
116+
? extended.helpUrl
117+
: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
129118
};
130119
}
+36-23
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,61 @@
1-
export async function loadParserOpts(
2-
parserName: string,
3-
pendingParser: Promise<any>
4-
) {
1+
import {ParserPreset} from '@commitlint/types';
2+
import {
3+
isObjectLike,
4+
isParserOptsFunction,
5+
isPromiseLike,
6+
validateParser,
7+
} from './validators';
8+
9+
export async function loadParser(
10+
pendingParser: unknown
11+
): Promise<ParserPreset | undefined> {
12+
if (!pendingParser) {
13+
return undefined;
14+
}
515
// Await for the module, loaded with require
616
const parser = await pendingParser;
717

18+
validateParser(parser);
19+
820
// Await parser opts if applicable
9-
if (
10-
typeof parser === 'object' &&
11-
typeof parser.parserOpts === 'object' &&
12-
typeof parser.parserOpts.then === 'function'
13-
) {
14-
return (await parser.parserOpts).parserOpts;
21+
if (isPromiseLike(parser.parserOpts)) {
22+
parser.parserOpts = ((await parser.parserOpts) as any).parserOpts;
23+
return parser;
1524
}
1625

1726
// Create parser opts from factory
1827
if (
19-
typeof parser === 'object' &&
20-
typeof parser.parserOpts === 'function' &&
21-
parserName.startsWith('conventional-changelog-')
28+
isParserOptsFunction(parser) &&
29+
parser.name.startsWith('conventional-changelog-')
2230
) {
23-
return await new Promise((resolve) => {
24-
const result = parser.parserOpts((_: never, opts: {parserOpts: any}) => {
25-
resolve(opts.parserOpts);
31+
return new Promise((resolve) => {
32+
const result = parser.parserOpts((_: never, opts) => {
33+
resolve({
34+
...parser,
35+
parserOpts: opts.parserOpts,
36+
});
2637
});
2738

2839
// If result has data or a promise, the parser doesn't support factory-init
2940
// due to https://github.com/nodejs/promises-debugging/issues/16 it just quits, so let's use this fallback
3041
if (result) {
3142
Promise.resolve(result).then((opts) => {
32-
resolve(opts.parserOpts);
43+
resolve({
44+
...parser,
45+
parserOpts: opts.parserOpts,
46+
});
3347
});
3448
}
49+
return;
3550
});
3651
}
3752

38-
// Pull nested paserOpts, might happen if overwritten with a module in main config
53+
// Pull nested parserOpts, might happen if overwritten with a module in main config
3954
if (
40-
typeof parser === 'object' &&
41-
typeof parser.parserOpts === 'object' &&
55+
isObjectLike(parser.parserOpts) &&
4256
typeof parser.parserOpts.parserOpts === 'object'
4357
) {
44-
return parser.parserOpts.parserOpts;
58+
parser.parserOpts = parser.parserOpts.parserOpts;
4559
}
46-
47-
return parser.parserOpts;
60+
return parser;
4861
}

0 commit comments

Comments
 (0)