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: simplify config resolution #2398

Merged
merged 25 commits into from Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d315e02
feat: basic user config validation
armano2 Jan 10, 2021
a9477b8
fix: simplify config resolution and fix issue #327
armano2 Jan 10, 2021
01ef361
fix: remove no longer needed function
armano2 Jan 10, 2021
e09fc01
fix: disable some unwanted validations
armano2 Jan 10, 2021
26612a8
fix: improve config validation
armano2 Jan 10, 2021
fb7064c
fix: remove redundant validation
armano2 Jan 10, 2021
b0d4829
fix: use reduceRight instead of reverse
armano2 Jan 10, 2021
ff67ad7
fix: rollback some code
armano2 Jan 10, 2021
3bd8384
fix: drop invalid type casts
armano2 Jan 10, 2021
7fcf5e0
fix: rollback unnecessary changes
armano2 Jan 10, 2021
5a10589
fix: rollback config validation
armano2 Jan 15, 2021
b2af2e0
fix: add missing type-guards and restore order
armano2 Jan 15, 2021
a165e15
fix: one more order change
armano2 Jan 15, 2021
28185b6
fix: add one more missing type guard
armano2 Jan 15, 2021
0c75eb9
fix: remove unused types reference
armano2 Jan 15, 2021
45d1e25
fix: add additional unit tests
armano2 Jan 16, 2021
75d9011
fix: add additional regression tests
armano2 Jan 16, 2021
bd6e958
fix: remove more unnecessary code changes
armano2 Jan 16, 2021
9303234
fix: correct order of merging plugins
armano2 Jan 16, 2021
1e20607
fix: add missing type check
armano2 Jan 16, 2021
b750743
fix: remove invalid type check
armano2 Jan 17, 2021
1c251d2
fix: remove redundant code
armano2 Jan 17, 2021
f1bf2d2
fix: rollback some unnecessary changes
armano2 Jan 29, 2021
2c32314
fix: optimize loadParserOpts
armano2 Jan 29, 2021
19b76c1
Merge branch 'master' into refactor/load
armano2 Nov 15, 2021
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
2 changes: 1 addition & 1 deletion @commitlint/cli/src/cli.ts
Expand Up @@ -373,7 +373,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
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 @@ -268,8 +270,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 @@ -278,8 +281,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 @@ -402,11 +406,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 @@ -415,9 +420,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 @@ -432,9 +438,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 @@ -445,11 +452,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*)(?:\((.*)\))?!?: (.*)$/
);
});
Expand Down
100 changes: 41 additions & 59 deletions @commitlint/load/src/load.ts
Expand Up @@ -2,27 +2,21 @@ import executeRule from '@commitlint/execute-rule';
import resolveExtends from '@commitlint/resolve-extends';
import {
LoadOptions,
ParserPreset,
QualifiedConfig,
QualifiedRules,
PluginRecords,
UserConfig,
UserPreset,
} from '@commitlint/types';
import isPlainObject from 'lodash/isPlainObject';
import merge from 'lodash/merge';
import mergeWith from 'lodash/mergeWith';
import pick from 'lodash/pick';
import union from 'lodash/union';
import uniq from 'lodash/uniq';
import Path from 'path';
import resolveFrom from 'resolve-from';
import {loadConfig} from './utils/load-config';
import {loadParserOpts} from './utils/load-parser-opts';
import loadPlugin from './utils/load-plugin';
import {pickConfig} from './utils/pick-config';

const w = <T>(_: unknown, b: ArrayLike<T> | null | undefined | false) =>
Array.isArray(b) ? b : undefined;

export default async function load(
seed: UserConfig = {},
options: LoadOptions = {}
Expand All @@ -35,11 +29,16 @@ 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(
{
extends: [],
plugins: [],
rules: {},
},
loaded ? loaded.config : null,
seed
)
armano2 marked this conversation as resolved.
Show resolved Hide resolved
);

// Resolve parserPreset key
Expand All @@ -54,59 +53,35 @@ 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 : [];
}) as unknown as UserConfig;

// 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
);
if (!extended.formatter || typeof extended.formatter !== 'string') {
extended.formatter = '@commitlint/format';
}

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

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

// resolve plugins
if (Array.isArray(config.plugins)) {
config.plugins.forEach((plugin) => {
uniq(extended.plugins || []).forEach((plugin) => {
if (typeof plugin === 'string') {
loadPlugin(preset.plugins, plugin, process.env.DEBUG === 'true');
plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true');
} else {
preset.plugins.local = plugin;
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(extended.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;
}, {});

Expand All @@ -118,17 +93,24 @@ export default async function load(
: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint';

const prompt =
preset.prompt && isPlainObject(preset.prompt) ? preset.prompt : {};
extended.prompt && isPlainObject(extended.prompt) ? extended.prompt : {};

return {
extends: preset.extends!,
formatter: preset.formatter!,
parserPreset: preset.parserPreset! as ParserPreset,
ignores: preset.ignores!,
defaultIgnores: preset.defaultIgnores!,
plugins: preset.plugins!,
rules: qualifiedRules,
helpUrl,
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 loadParserOpts(extended.parserPreset),
ignores: extended.ignores,
defaultIgnores: extended.defaultIgnores,
plugins: plugins,
rules: rules,
helpUrl: helpUrl,
prompt,
};
}
77 changes: 50 additions & 27 deletions @commitlint/load/src/utils/load-parser-opts.ts
@@ -1,48 +1,71 @@
import {ParserPreset} from '@commitlint/types';

function isObjectLike(obj: unknown): obj is Record<string, unknown> {
return Boolean(obj) && typeof obj === 'object'; // typeof null === 'object'
}

function isParserOptsFunction<T extends ParserPreset>(
obj: T
): obj is T & {
parserOpts: (...args: any[]) => any;
} {
return typeof obj.parserOpts === 'function';
}

export async function loadParserOpts(
parserName: string,
pendingParser: Promise<any>
) {
pendingParser: string | ParserPreset | Promise<ParserPreset> | undefined
): Promise<ParserPreset | undefined> {
if (!pendingParser || typeof pendingParser !== 'object') {
return undefined;
}
// Await for the module, loaded with require
const parser = await pendingParser;
armano2 marked this conversation as resolved.
Show resolved Hide resolved

// Await parser opts if applicable
if (
typeof parser === 'object' &&
typeof parser.parserOpts === 'object' &&
typeof parser.parserOpts.then === 'function'
) {
return (await parser.parserOpts).parserOpts;
// exit early, no opts to resolve
if (!parser.parserOpts) {
return parser;
}

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

// Create parser opts from factory
if (
typeof parser === 'object' &&
typeof parser.parserOpts === 'function' &&
parserName.startsWith('conventional-changelog-')
isParserOptsFunction(parser) &&
typeof parser.name === 'string' &&
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: any) => {
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
if (
typeof parser === 'object' &&
typeof parser.parserOpts === 'object' &&
typeof parser.parserOpts.parserOpts === 'object'
) {
return parser.parserOpts.parserOpts;
}

return parser.parserOpts;
return parser;
}
3 changes: 1 addition & 2 deletions @commitlint/load/src/utils/pick-config.ts
@@ -1,7 +1,6 @@
import {UserConfig} from '@commitlint/types';
import pick from 'lodash/pick';

export const pickConfig = (input: unknown): UserConfig =>
export const pickConfig = (input: unknown): Record<string, unknown> =>
pick(
input,
'extends',
Expand Down