Skip to content

Commit

Permalink
feat: basic user config validation
Browse files Browse the repository at this point in the history
  • Loading branch information
armano2 committed Jan 30, 2021
1 parent 8ee0e9e commit d315e02
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 126 deletions.
2 changes: 1 addition & 1 deletion @commitlint/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ function getSeed(flags: CliFlags): Seed {
: {parserPreset: flags['parser-preset']};
}

function selectParserOpts(parserPreset: ParserPreset) {
function selectParserOpts(parserPreset: ParserPreset | undefined) {
if (typeof parserPreset !== 'object') {
return undefined;
}
Expand Down
44 changes: 26 additions & 18 deletions @commitlint/load/src/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ test('extends-empty should have no rules', async () => {
const actual = await load({}, {cwd});

expect(actual.rules).toMatchObject({});
expect(actual.parserPreset).not.toBeDefined();
});

test('uses seed as configured', async () => {
Expand Down Expand Up @@ -127,8 +128,9 @@ test('uses seed with parserPreset', async () => {
{cwd}
);

expect(actual.name).toBe('./conventional-changelog-custom');
expect(actual.parserOpts).toMatchObject({
expect(actual).toBeDefined();
expect(actual!.name).toBe('./conventional-changelog-custom');
expect(actual!.parserOpts).toMatchObject({
headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/,
});
});
Expand Down Expand Up @@ -252,8 +254,9 @@ test('parser preset overwrites completely instead of merging', async () => {
const cwd = await gitBootstrap('fixtures/parser-preset-override');
const actual = await load({}, {cwd});

expect(actual.parserPreset.name).toBe('./custom');
expect(actual.parserPreset.parserOpts).toMatchObject({
expect(actual.parserPreset).toBeDefined();
expect(actual.parserPreset!.name).toBe('./custom');
expect(actual.parserPreset!.parserOpts).toMatchObject({
headerPattern: /.*/,
});
});
Expand All @@ -262,8 +265,9 @@ test('recursive extends with parserPreset', async () => {
const cwd = await gitBootstrap('fixtures/recursive-parser-preset');
const actual = await load({}, {cwd});

expect(actual.parserPreset.name).toBe('./conventional-changelog-custom');
expect(actual.parserPreset.parserOpts).toMatchObject({
expect(actual.parserPreset).toBeDefined();
expect(actual.parserPreset!.name).toBe('./conventional-changelog-custom');
expect(actual.parserPreset!.parserOpts).toMatchObject({
headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/,
});
});
Expand Down Expand Up @@ -386,11 +390,12 @@ test('resolves parser preset from conventional commits', async () => {
const cwd = await npmBootstrap('fixtures/parser-preset-conventionalcommits');
const actual = await load({}, {cwd});

expect(actual.parserPreset.name).toBe(
expect(actual.parserPreset).toBeDefined();
expect(actual.parserPreset!.name).toBe(
'conventional-changelog-conventionalcommits'
);
expect(typeof actual.parserPreset.parserOpts).toBe('object');
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
/^(\w*)(?:\((.*)\))?!?: (.*)$/
);
});
Expand All @@ -399,9 +404,10 @@ test('resolves parser preset from conventional angular', async () => {
const cwd = await npmBootstrap('fixtures/parser-preset-angular');
const actual = await load({}, {cwd});

expect(actual.parserPreset.name).toBe('conventional-changelog-angular');
expect(typeof actual.parserPreset.parserOpts).toBe('object');
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
expect(actual.parserPreset).toBeDefined();
expect(actual.parserPreset!.name).toBe('conventional-changelog-angular');
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
/^(\w*)(?:\((.*)\))?: (.*)$/
);
});
Expand All @@ -416,9 +422,10 @@ test('recursive resolves parser preset from conventional atom', async () => {

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

expect(actual.parserPreset.name).toBe('conventional-changelog-atom');
expect(typeof actual.parserPreset.parserOpts).toBe('object');
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
expect(actual.parserPreset).toBeDefined();
expect(actual.parserPreset!.name).toBe('conventional-changelog-atom');
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
/^(:.*?:) (.*)$/
);
});
Expand All @@ -429,11 +436,12 @@ test('resolves parser preset from conventional commits without factory support',
);
const actual = await load({}, {cwd});

expect(actual.parserPreset.name).toBe(
expect(actual.parserPreset).toBeDefined();
expect(actual.parserPreset!.name).toBe(
'conventional-changelog-conventionalcommits'
);
expect(typeof actual.parserPreset.parserOpts).toBe('object');
expect((actual.parserPreset.parserOpts as any).headerPattern).toEqual(
expect(typeof actual.parserPreset!.parserOpts).toBe('object');
expect((actual.parserPreset!.parserOpts as any).headerPattern).toEqual(
/^(\w*)(?:\((.*)\))?!?: (.*)$/
);
});
129 changes: 59 additions & 70 deletions @commitlint/load/src/load.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import Path from 'path';

import merge from 'lodash/merge';
import mergeWith from 'lodash/mergeWith';
import pick from 'lodash/pick';
import union from 'lodash/union';
import resolveFrom from 'resolve-from';

Expand All @@ -12,18 +10,15 @@ import {
UserConfig,
LoadOptions,
QualifiedConfig,
UserPreset,
QualifiedRules,
ParserPreset,
PluginRecords,
} from '@commitlint/types';

import loadPlugin from './utils/load-plugin';
import {loadConfig} from './utils/load-config';
import {loadParserOpts} from './utils/load-parser-opts';
import {loadParser} from './utils/load-parser-opts';
import {pickConfig} from './utils/pick-config';

const w = <T>(_: unknown, b: ArrayLike<T> | null | undefined | false) =>
Array.isArray(b) ? b : undefined;
import {validateConfig} from './utils/validators';

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

// Merge passed config with file based options
const config = pickConfig(merge({}, loaded ? loaded.config : null, seed));

const opts = merge(
{extends: [], rules: {}, formatter: '@commitlint/format'},
pick(config, 'extends', 'plugins', 'ignores', 'defaultIgnores')
const config = pickConfig(
merge(
{
rules: {},
formatter: '@commitlint/format',
helpUrl:
'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
},
loaded ? loaded.config : null,
seed
)
);

// Resolve parserPreset key
Expand All @@ -56,75 +57,63 @@ export default async function load(
}

// Resolve extends key
const extended = resolveExtends(opts, {
const extended = resolveExtends(config, {
prefix: 'commitlint-config',
cwd: base,
parserPreset: config.parserPreset,
});

const preset = (pickConfig(
mergeWith(extended, config, w)
) as unknown) as UserPreset;
preset.plugins = {};

// TODO: check if this is still necessary with the new factory based conventional changelog parsers
// config.extends = Array.isArray(config.extends) ? config.extends : [];

// Resolve parser-opts from preset
if (typeof preset.parserPreset === 'object') {
preset.parserPreset.parserOpts = await loadParserOpts(
preset.parserPreset.name,
// TODO: fix the types for factory based conventional changelog parsers
preset.parserPreset as any
);
}

// Resolve config-relative formatter module
if (typeof config.formatter === 'string') {
preset.formatter =
resolveFrom.silent(base, config.formatter) || config.formatter;
}

// Read plugins from extends
if (Array.isArray(extended.plugins)) {
config.plugins = union(config.plugins, extended.plugins || []);
}

// resolve plugins
if (Array.isArray(config.plugins)) {
config.plugins.forEach((plugin) => {
if (typeof plugin === 'string') {
loadPlugin(preset.plugins, plugin, process.env.DEBUG === 'true');
} else {
preset.plugins.local = plugin;
}
});
}
validateConfig(extended);

let plugins: PluginRecords = {};
// TODO: this object merging should be done in resolveExtends
union(
// Read plugins from config
Array.isArray(config.plugins) ? config.plugins : [],
// Read plugins from extends
Array.isArray(extended.plugins) ? extended.plugins : []
).forEach((plugin) => {
if (typeof plugin === 'string') {
plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true');
} else {
plugins.local = plugin;
}
});

const rules = preset.rules ? preset.rules : {};
const qualifiedRules = (
const rules = (
await Promise.all(
Object.entries(rules || {}).map((entry) => executeRule<any>(entry))
Object.entries({
...(typeof extended.rules === 'object' ? extended.rules || {} : {}),
...(typeof config.rules === 'object' ? config.rules || {} : {}),
}).map((entry) => executeRule(entry))
)
).reduce<QualifiedRules>((registry, item) => {
const [key, value] = item as any;
(registry as any)[key] = value;
// type of `item` can be null, but Object.entries always returns key pair
const [key, value] = item!;
registry[key] = value;
return registry;
}, {});

const helpUrl =
typeof config.helpUrl === 'string'
? config.helpUrl
: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint';

return {
extends: preset.extends!,
formatter: preset.formatter!,
parserPreset: preset.parserPreset! as ParserPreset,
ignores: preset.ignores!,
defaultIgnores: preset.defaultIgnores!,
plugins: preset.plugins!,
rules: qualifiedRules,
helpUrl,
// TODO: check if this is still necessary with the new factory based conventional changelog parsers
// TODO: should this function return this? as those values are already resolved
extends: Array.isArray(extended.extends)
? extended.extends
: typeof extended.extends === 'string'
? [extended.extends]
: [],
// Resolve config-relative formatter module
formatter:
resolveFrom.silent(base, extended.formatter) || extended.formatter,
// Resolve parser-opts from preset
parserPreset: await loadParser(extended.parserPreset),
ignores: extended.ignores,
defaultIgnores: extended.defaultIgnores,
plugins: plugins,
rules: rules,
helpUrl:
typeof extended.helpUrl === 'string'
? extended.helpUrl
: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint',
};
}
59 changes: 36 additions & 23 deletions @commitlint/load/src/utils/load-parser-opts.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,61 @@
export async function loadParserOpts(
parserName: string,
pendingParser: Promise<any>
) {
import {ParserPreset} from '@commitlint/types';
import {
isObjectLike,
isParserOptsFunction,
isPromiseLike,
validateParser,
} from './validators';

export async function loadParser(
pendingParser: unknown
): Promise<ParserPreset | undefined> {
if (!pendingParser) {
return undefined;
}
// Await for the module, loaded with require
const parser = await pendingParser;

validateParser(parser);

// Await parser opts if applicable
if (
typeof parser === 'object' &&
typeof parser.parserOpts === 'object' &&
typeof parser.parserOpts.then === 'function'
) {
return (await parser.parserOpts).parserOpts;
if (isPromiseLike(parser.parserOpts)) {
parser.parserOpts = ((await parser.parserOpts) as any).parserOpts;
return parser;
}

// Create parser opts from factory
if (
typeof parser === 'object' &&
typeof parser.parserOpts === 'function' &&
parserName.startsWith('conventional-changelog-')
isParserOptsFunction(parser) &&
parser.name.startsWith('conventional-changelog-')
) {
return await new Promise((resolve) => {
const result = parser.parserOpts((_: never, opts: {parserOpts: any}) => {
resolve(opts.parserOpts);
return new Promise((resolve) => {
const result = parser.parserOpts((_: never, opts) => {
resolve({
...parser,
parserOpts: opts.parserOpts,
});
});

// If result has data or a promise, the parser doesn't support factory-init
// due to https://github.com/nodejs/promises-debugging/issues/16 it just quits, so let's use this fallback
if (result) {
Promise.resolve(result).then((opts) => {
resolve(opts.parserOpts);
resolve({
...parser,
parserOpts: opts.parserOpts,
});
});
}
return;
});
}

// Pull nested paserOpts, might happen if overwritten with a module in main config
// Pull nested parserOpts, might happen if overwritten with a module in main config
if (
typeof parser === 'object' &&
typeof parser.parserOpts === 'object' &&
isObjectLike(parser.parserOpts) &&
typeof parser.parserOpts.parserOpts === 'object'
) {
return parser.parserOpts.parserOpts;
parser.parserOpts = parser.parserOpts.parserOpts;
}

return parser.parserOpts;
return parser;
}

0 comments on commit d315e02

Please sign in to comment.