From 3f6d95c036e660e85c637220c2350fc404f6e4a4 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 10 Jan 2021 21:18:53 +0100 Subject: [PATCH 01/24] feat: basic user config validation --- @commitlint/cli/src/cli.ts | 2 +- @commitlint/load/src/load.test.ts | 44 +++--- @commitlint/load/src/load.ts | 129 ++++++++---------- .../load/src/utils/load-parser-opts.ts | 59 ++++---- @commitlint/load/src/utils/validators.ts | 59 ++++++++ @commitlint/resolve-extends/package.json | 1 + @commitlint/resolve-extends/src/index.ts | 11 +- @commitlint/resolve-extends/tsconfig.json | 3 +- @commitlint/types/src/load.ts | 10 +- 9 files changed, 192 insertions(+), 126 deletions(-) create mode 100644 @commitlint/load/src/utils/validators.ts diff --git a/@commitlint/cli/src/cli.ts b/@commitlint/cli/src/cli.ts index b8d4b127a2..0992120221 100644 --- a/@commitlint/cli/src/cli.ts +++ b/@commitlint/cli/src/cli.ts @@ -323,7 +323,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; } diff --git a/@commitlint/load/src/load.test.ts b/@commitlint/load/src/load.test.ts index 09d2db53d5..ff3530a0c1 100644 --- a/@commitlint/load/src/load.test.ts +++ b/@commitlint/load/src/load.test.ts @@ -22,6 +22,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 () => { @@ -128,8 +129,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*)(?:\((.*)\))?-(.*)$/, }); }); @@ -253,8 +255,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: /.*/, }); }); @@ -263,8 +266,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*)(?:\((.*)\))?-(.*)$/, }); }); @@ -387,11 +391,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*)(?:\((.*)\))?!?: (.*)$/ ); }); @@ -400,9 +405,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*)(?:\((.*)\))?: (.*)$/ ); }); @@ -418,9 +424,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( /^(:.*?:) (.*)$/ ); }, 10000); @@ -431,11 +438,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*)(?:\((.*)\))?!?: (.*)$/ ); }); diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index f6b2e7d002..d03c18d0bd 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -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'; @@ -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 = (_: unknown, b: ArrayLike | null | undefined | false) => - Array.isArray(b) ? b : undefined; +import {validateConfig} from './utils/validators'; export default async function load( seed: UserConfig = {}, @@ -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 @@ -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(entry)) + Object.entries({ + ...(typeof extended.rules === 'object' ? extended.rules || {} : {}), + ...(typeof config.rules === 'object' ? config.rules || {} : {}), + }).map((entry) => executeRule(entry)) ) ).reduce((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', }; } diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts index 86d5b5b700..5380f529ba 100644 --- a/@commitlint/load/src/utils/load-parser-opts.ts +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -1,48 +1,61 @@ -export async function loadParserOpts( - parserName: string, - pendingParser: Promise -) { +import {ParserPreset} from '@commitlint/types'; +import { + isObjectLike, + isParserOptsFunction, + isPromiseLike, + validateParser, +} from './validators'; + +export async function loadParser( + pendingParser: unknown +): Promise { + 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; } diff --git a/@commitlint/load/src/utils/validators.ts b/@commitlint/load/src/utils/validators.ts new file mode 100644 index 0000000000..f26da6aaf5 --- /dev/null +++ b/@commitlint/load/src/utils/validators.ts @@ -0,0 +1,59 @@ +export function isObjectLike(obj: unknown): obj is Record { + return Boolean(obj) && typeof obj === 'object'; // typeof null === 'object' +} + +export function isPromiseLike(obj: unknown): obj is Promise { + return ( + (typeof obj === 'object' || typeof obj === 'function') && + typeof (obj as any).then === 'function' + ); +} + +export function isParserOptsFunction>( + obj: T +): obj is T & { + parserOpts: ( + cb: (_: never, parserOpts: Record) => unknown + ) => Record | undefined; +} { + return typeof obj.parserOpts === 'function'; +} + +export function validateConfig( + config: Record +): asserts config is { + formatter: string; + ignores?: ((commit: string) => boolean)[]; + defaultIgnores?: boolean; + [key: string]: unknown; +} { + if (!isObjectLike(config)) { + throw new Error('Invalid configuration, parserPreset must be an object'); + } + if (typeof config.formatter !== 'string') { + throw new Error('Invalid configuration, formatter must be a string'); + } + if (config.ignores && !Array.isArray(config.ignores)) { + throw new Error('Invalid configuration, ignores must ba an array'); + } + if ( + typeof config.defaultIgnores !== 'boolean' && + typeof config.defaultIgnores !== 'undefined' + ) { + throw new Error('Invalid configuration, defaultIgnores must ba true/false'); + } +} + +export function validateParser( + parser: unknown +): asserts parser is {name: string; path: string; [key: string]: unknown} { + if (!isObjectLike(parser)) { + throw new Error('Invalid configuration, parserPreset must be an object'); + } + if (typeof parser.name !== 'string') { + throw new Error('Invalid configuration, parserPreset must have a name'); + } + if (typeof parser.path !== 'string') { + throw new Error('Invalid configuration, parserPreset must have a name'); + } +} diff --git a/@commitlint/resolve-extends/package.json b/@commitlint/resolve-extends/package.json index 8d69557250..9cfb50fa09 100644 --- a/@commitlint/resolve-extends/package.json +++ b/@commitlint/resolve-extends/package.json @@ -38,6 +38,7 @@ "@types/lodash": "^4.14.161" }, "dependencies": { + "@commitlint/types": "^11.0.0", "import-fresh": "^3.0.0", "lodash": "^4.17.19", "resolve-from": "^5.0.0", diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index f8c786c18b..351c3d7dd3 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -4,6 +4,7 @@ import 'resolve-global'; import resolveFrom from 'resolve-from'; import merge from 'lodash/merge'; import mergeWith from 'lodash/mergeWith'; +import {UserConfig} from '@commitlint/types'; const importFresh = require('import-fresh'); @@ -12,12 +13,6 @@ export interface ResolvedConfig { [key: string]: unknown; } -export interface ResolveExtendsConfig { - parserPreset?: unknown; - extends?: string | string[]; - [key: string]: unknown; -} - export interface ResolveExtendsContext { cwd?: string; parserPreset?: unknown; @@ -28,7 +23,7 @@ export interface ResolveExtendsContext { } export default function resolveExtends( - config: ResolveExtendsConfig = {}, + config: UserConfig = {}, context: ResolveExtendsContext = {} ) { const {extends: e} = config; @@ -46,7 +41,7 @@ export default function resolveExtends( } function loadExtends( - config: ResolveExtendsConfig = {}, + config: UserConfig = {}, context: ResolveExtendsContext = {} ): ResolvedConfig[] { const {extends: e} = config; diff --git a/@commitlint/resolve-extends/tsconfig.json b/@commitlint/resolve-extends/tsconfig.json index 49479bf34f..119e645565 100644 --- a/@commitlint/resolve-extends/tsconfig.json +++ b/@commitlint/resolve-extends/tsconfig.json @@ -6,5 +6,6 @@ "outDir": "./lib" }, "include": ["./src"], - "exclude": ["./src/**/*.test.ts", "./lib/**/*"] + "exclude": ["./src/**/*.test.ts", "./lib/**/*"], + "references": [{"path": "../types"}] } diff --git a/@commitlint/types/src/load.ts b/@commitlint/types/src/load.ts index 481b477e0f..2dfd8fd389 100644 --- a/@commitlint/types/src/load.ts +++ b/@commitlint/types/src/load.ts @@ -14,10 +14,10 @@ export interface LoadOptions { } export interface UserConfig { - extends?: string[]; + extends?: string | string[]; formatter?: string; rules?: Partial; - parserPreset?: string | ParserPreset; + parserPreset?: string | ParserPreset | Promise; ignores?: ((commit: string) => boolean)[]; defaultIgnores?: boolean; plugins?: (string | Plugin)[]; @@ -40,9 +40,9 @@ export interface QualifiedConfig { extends: string[]; formatter: string; rules: QualifiedRules; - parserPreset: ParserPreset; - ignores: ((commit: string) => boolean)[]; - defaultIgnores: boolean; + parserPreset?: ParserPreset; + ignores?: ((commit: string) => boolean)[]; + defaultIgnores?: boolean; plugins: PluginRecords; helpUrl: string; } From 786e400c2caec91d47a13571668baf735410775e Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 10 Jan 2021 22:53:13 +0100 Subject: [PATCH 02/24] fix: simplify config resolution and fix issue #327 --- @commitlint/load/src/load.ts | 24 +++------ @commitlint/load/src/utils/validators.ts | 21 ++++++-- @commitlint/resolve-extends/src/index.test.ts | 47 ++++++++++++++++++ @commitlint/resolve-extends/src/index.ts | 49 +++++++++++++------ 4 files changed, 104 insertions(+), 37 deletions(-) diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index d03c18d0bd..8fd64d9f24 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -1,7 +1,7 @@ import Path from 'path'; import merge from 'lodash/merge'; -import union from 'lodash/union'; +import uniq from 'lodash/uniq'; import resolveFrom from 'resolve-from'; import executeRule from '@commitlint/execute-rule'; @@ -35,6 +35,8 @@ export default async function load( const config = pickConfig( merge( { + extends: [], + plugins: [], rules: {}, formatter: '@commitlint/format', helpUrl: @@ -57,7 +59,7 @@ export default async function load( } // Resolve extends key - const extended = resolveExtends(config, { + const extended = resolveExtends(config as any, { prefix: 'commitlint-config', cwd: base, parserPreset: config.parserPreset, @@ -66,13 +68,7 @@ export default async function load( 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) => { + uniq(extended.plugins || []).forEach((plugin) => { if (typeof plugin === 'string') { plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true'); } else { @@ -82,10 +78,7 @@ export default async function load( const rules = ( await Promise.all( - Object.entries({ - ...(typeof extended.rules === 'object' ? extended.rules || {} : {}), - ...(typeof config.rules === 'object' ? config.rules || {} : {}), - }).map((entry) => executeRule(entry)) + Object.entries(extended.rules || {}).map((entry) => executeRule(entry)) ) ).reduce((registry, item) => { // type of `item` can be null, but Object.entries always returns key pair @@ -111,9 +104,6 @@ export default async function load( defaultIgnores: extended.defaultIgnores, plugins: plugins, rules: rules, - helpUrl: - typeof extended.helpUrl === 'string' - ? extended.helpUrl - : 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint', + helpUrl: extended.helpUrl, }; } diff --git a/@commitlint/load/src/utils/validators.ts b/@commitlint/load/src/utils/validators.ts index f26da6aaf5..3027707da2 100644 --- a/@commitlint/load/src/utils/validators.ts +++ b/@commitlint/load/src/utils/validators.ts @@ -1,3 +1,5 @@ +import {Plugin, RulesConfig} from '@commitlint/types'; + export function isObjectLike(obj: unknown): obj is Record { return Boolean(obj) && typeof obj === 'object'; // typeof null === 'object' } @@ -25,22 +27,33 @@ export function validateConfig( formatter: string; ignores?: ((commit: string) => boolean)[]; defaultIgnores?: boolean; + plugins?: (Plugin | string)[]; + rules: Partial; + helpUrl: string; [key: string]: unknown; } { if (!isObjectLike(config)) { - throw new Error('Invalid configuration, parserPreset must be an object'); + throw new Error('Invalid configuration, `parserPreset` must be an object'); } if (typeof config.formatter !== 'string') { - throw new Error('Invalid configuration, formatter must be a string'); + throw new Error('Invalid configuration, `formatter` must be a string'); } if (config.ignores && !Array.isArray(config.ignores)) { - throw new Error('Invalid configuration, ignores must ba an array'); + throw new Error('Invalid configuration, `ignores` must ba an array'); + } + if (config.plugins && !Array.isArray(config.plugins)) { + throw new Error('Invalid configuration, `plugins` must ba an array'); } if ( typeof config.defaultIgnores !== 'boolean' && typeof config.defaultIgnores !== 'undefined' ) { - throw new Error('Invalid configuration, defaultIgnores must ba true/false'); + throw new Error( + 'Invalid configuration, `defaultIgnores` must ba true/false' + ); + } + if (typeof config.helpUrl !== 'string') { + throw new Error('Invalid configuration, `helpUrl` must be a string'); } } diff --git a/@commitlint/resolve-extends/src/index.test.ts b/@commitlint/resolve-extends/src/index.test.ts index b386d001c4..6a0da9e303 100644 --- a/@commitlint/resolve-extends/src/index.test.ts +++ b/@commitlint/resolve-extends/src/index.test.ts @@ -348,3 +348,50 @@ test('should fall back to conventional-changelog-lint-config prefix', () => { }, }); }); + +// https://github.com/conventional-changelog/commitlint/issues/327 +test('parserPreset should resolve correctly in extended configuration', () => { + const input = {extends: ['extender-name'], zero: 'root'}; + + const require = (id: string) => { + switch (id) { + case 'extender-name': + return { + extends: ['recursive-extender-name'], + parserPreset: { + parserOpts: { + issuePrefixes: ['#', '!', '&', 'no-references'], + referenceActions: null, + }, + }, + }; + case 'recursive-extender-name': + return { + parserPreset: { + parserOpts: { + issuePrefixes: ['#', '!'], + }, + }, + }; + default: + return {}; + } + }; + + const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext; + + const actual = resolveExtends(input, ctx); + + const expected = { + extends: ['extender-name'], + parserPreset: { + parserOpts: { + issuePrefixes: ['#', '!', '&', 'no-references'], + referenceActions: null, + }, + }, + zero: 'root', + }; + + expect(actual).toEqual(expected); +}); diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index 351c3d7dd3..e6318860c6 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -4,10 +4,14 @@ import 'resolve-global'; import resolveFrom from 'resolve-from'; import merge from 'lodash/merge'; import mergeWith from 'lodash/mergeWith'; -import {UserConfig} from '@commitlint/types'; const importFresh = require('import-fresh'); +export interface ResolveExtendsConfig { + extends?: string | string[]; + [key: string]: unknown; +} + export interface ResolvedConfig { parserPreset?: unknown; [key: string]: unknown; @@ -22,32 +26,45 @@ export interface ResolveExtendsContext { require?(id: string): T; } +function mergeStrategy(objValue: unknown, srcValue: unknown, key: string) { + if (key === 'parserPreset') { + if (typeof srcValue !== 'object') { + return objValue; + } + } else if (key === 'rules') { + if (typeof objValue !== 'object') { + return srcValue; + } + } else if (key === 'plugins') { + if (!Array.isArray(objValue)) { + return srcValue; + } + } else if (Array.isArray(objValue)) { + return srcValue; + } +} + export default function resolveExtends( - config: UserConfig = {}, + config: ResolveExtendsConfig = {}, context: ResolveExtendsContext = {} -) { +): ResolvedConfig { const {extends: e} = config; - const extended = loadExtends(config, context).reduce( - (r, {extends: _, ...c}) => - mergeWith(r, c, (objValue, srcValue) => { - if (Array.isArray(objValue)) { - return srcValue; - } - }), + const extended = loadExtends(config, context); + extended.push(config); + return extended.reduce( + (r, {extends: _, ...c}) => mergeWith(r, c, mergeStrategy), e ? {extends: e} : {} ); - - return merge({}, extended, config); } function loadExtends( - config: UserConfig = {}, + config: ResolveExtendsConfig = {}, context: ResolveExtendsContext = {} ): ResolvedConfig[] { const {extends: e} = config; - const ext = e ? (Array.isArray(e) ? e : [e]) : []; + const ext = e ? (Array.isArray(e) ? [...e] : [e]) : []; - return ext.reduce((configs, raw) => { + return ext.reverse().reduce((configs, raw) => { const load = context.require || require; const resolved = resolveConfig(raw, context); const c = load(resolved); @@ -73,7 +90,7 @@ function loadExtends( config.parserPreset = parserPreset; } - return [...configs, ...loadExtends(c, ctx), c]; + return [...loadExtends(c, ctx), c, ...configs]; }, []); } From 6f0069038b22f6d9643edb202226a32689e6f3d3 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 10 Jan 2021 22:59:48 +0100 Subject: [PATCH 03/24] fix: remove no longer needed function --- @commitlint/resolve-extends/src/index.ts | 37 ++++++++++++------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index e6318860c6..c27b4d7337 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -26,24 +26,6 @@ export interface ResolveExtendsContext { require?(id: string): T; } -function mergeStrategy(objValue: unknown, srcValue: unknown, key: string) { - if (key === 'parserPreset') { - if (typeof srcValue !== 'object') { - return objValue; - } - } else if (key === 'rules') { - if (typeof objValue !== 'object') { - return srcValue; - } - } else if (key === 'plugins') { - if (!Array.isArray(objValue)) { - return srcValue; - } - } else if (Array.isArray(objValue)) { - return srcValue; - } -} - export default function resolveExtends( config: ResolveExtendsConfig = {}, context: ResolveExtendsContext = {} @@ -52,7 +34,24 @@ export default function resolveExtends( const extended = loadExtends(config, context); extended.push(config); return extended.reduce( - (r, {extends: _, ...c}) => mergeWith(r, c, mergeStrategy), + (r, {extends: _, ...c}) => + mergeWith(r, c, (objValue, srcValue, key) => { + if (key === 'parserPreset') { + if (typeof srcValue !== 'object') { + return objValue; + } + } else if (key === 'rules') { + if (typeof objValue !== 'object') { + return srcValue; + } + } else if (key === 'plugins') { + if (!Array.isArray(objValue)) { + return srcValue; + } + } else if (Array.isArray(objValue)) { + return srcValue; + } + }), e ? {extends: e} : {} ); } From a7f92d8b9c05ef2241042f38157aad1a0ca2c871 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 10 Jan 2021 23:09:41 +0100 Subject: [PATCH 04/24] fix: disable some unwanted validations --- @commitlint/load/src/utils/load-parser-opts.ts | 12 +++++------- @commitlint/load/src/utils/validators.ts | 14 -------------- @commitlint/types/src/load.ts | 4 ++-- 3 files changed, 7 insertions(+), 23 deletions(-) diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts index 5380f529ba..f168fd642a 100644 --- a/@commitlint/load/src/utils/load-parser-opts.ts +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -1,10 +1,5 @@ import {ParserPreset} from '@commitlint/types'; -import { - isObjectLike, - isParserOptsFunction, - isPromiseLike, - validateParser, -} from './validators'; +import {isObjectLike, isParserOptsFunction, isPromiseLike} from './validators'; export async function loadParser( pendingParser: unknown @@ -15,7 +10,9 @@ export async function loadParser( // Await for the module, loaded with require const parser = await pendingParser; - validateParser(parser); + if (!isObjectLike(parser)) { + throw new Error('Invalid configuration, `parserPreset` must be an object'); + } // Await parser opts if applicable if (isPromiseLike(parser.parserOpts)) { @@ -26,6 +23,7 @@ export async function loadParser( // Create parser opts from factory if ( isParserOptsFunction(parser) && + typeof parser.name === 'string' && parser.name.startsWith('conventional-changelog-') ) { return new Promise((resolve) => { diff --git a/@commitlint/load/src/utils/validators.ts b/@commitlint/load/src/utils/validators.ts index 3027707da2..7b76e17b88 100644 --- a/@commitlint/load/src/utils/validators.ts +++ b/@commitlint/load/src/utils/validators.ts @@ -56,17 +56,3 @@ export function validateConfig( throw new Error('Invalid configuration, `helpUrl` must be a string'); } } - -export function validateParser( - parser: unknown -): asserts parser is {name: string; path: string; [key: string]: unknown} { - if (!isObjectLike(parser)) { - throw new Error('Invalid configuration, parserPreset must be an object'); - } - if (typeof parser.name !== 'string') { - throw new Error('Invalid configuration, parserPreset must have a name'); - } - if (typeof parser.path !== 'string') { - throw new Error('Invalid configuration, parserPreset must have a name'); - } -} diff --git a/@commitlint/types/src/load.ts b/@commitlint/types/src/load.ts index 2dfd8fd389..c24a81bfb9 100644 --- a/@commitlint/types/src/load.ts +++ b/@commitlint/types/src/load.ts @@ -48,7 +48,7 @@ export interface QualifiedConfig { } export interface ParserPreset { - name: string; - path: string; + name?: string; + path?: string; parserOpts?: unknown; } From ea26a8ed07041996061b1b76e3ae9381a6aad637 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 10 Jan 2021 23:35:28 +0100 Subject: [PATCH 05/24] fix: improve config validation --- @commitlint/load/src/load.ts | 12 +++-- .../load/src/utils/load-parser-opts.ts | 13 +++-- @commitlint/load/src/utils/validators.ts | 47 +++++++++++++------ 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index 8fd64d9f24..fc9231c140 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -38,9 +38,6 @@ export default async function load( extends: [], plugins: [], rules: {}, - formatter: '@commitlint/format', - helpUrl: - 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint', }, loaded ? loaded.config : null, seed @@ -67,6 +64,15 @@ export default async function load( validateConfig(extended); + if (!extended.formatter) { + extended.formatter = '@commitlint/format'; + } + + if (!extended.helpUrl) { + extended.helpUrl = + 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; + } + let plugins: PluginRecords = {}; uniq(extended.plugins || []).forEach((plugin) => { if (typeof plugin === 'string') { diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts index f168fd642a..9978f54172 100644 --- a/@commitlint/load/src/utils/load-parser-opts.ts +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -1,5 +1,10 @@ import {ParserPreset} from '@commitlint/types'; -import {isObjectLike, isParserOptsFunction, isPromiseLike} from './validators'; +import { + isObjectLike, + isParserOptsFunction, + isPromiseLike, + validateParser, +} from './validators'; export async function loadParser( pendingParser: unknown @@ -10,9 +15,7 @@ export async function loadParser( // Await for the module, loaded with require const parser = await pendingParser; - if (!isObjectLike(parser)) { - throw new Error('Invalid configuration, `parserPreset` must be an object'); - } + validateParser(parser); // Await parser opts if applicable if (isPromiseLike(parser.parserOpts)) { @@ -23,7 +26,7 @@ export async function loadParser( // Create parser opts from factory if ( isParserOptsFunction(parser) && - typeof parser.name === 'string' && + parser.name && parser.name.startsWith('conventional-changelog-') ) { return new Promise((resolve) => { diff --git a/@commitlint/load/src/utils/validators.ts b/@commitlint/load/src/utils/validators.ts index 7b76e17b88..40c44f9245 100644 --- a/@commitlint/load/src/utils/validators.ts +++ b/@commitlint/load/src/utils/validators.ts @@ -1,4 +1,4 @@ -import {Plugin, RulesConfig} from '@commitlint/types'; +import {UserConfig} from '@commitlint/types'; export function isObjectLike(obj: unknown): obj is Record { return Boolean(obj) && typeof obj === 'object'; // typeof null === 'object' @@ -21,21 +21,22 @@ export function isParserOptsFunction>( return typeof obj.parserOpts === 'function'; } -export function validateConfig( - config: Record -): asserts config is { - formatter: string; - ignores?: ((commit: string) => boolean)[]; - defaultIgnores?: boolean; - plugins?: (Plugin | string)[]; - rules: Partial; - helpUrl: string; - [key: string]: unknown; -} { +export function validateConfig>( + config: T +): asserts config is Omit & T { if (!isObjectLike(config)) { throw new Error('Invalid configuration, `parserPreset` must be an object'); } - if (typeof config.formatter !== 'string') { + if ( + config.extends && + typeof config.extends !== 'string' && + !Array.isArray(config.extends) + ) { + throw new Error( + 'Invalid configuration, `extends` must be a array or string' + ); + } + if (config.formatter && typeof config.formatter !== 'string') { throw new Error('Invalid configuration, `formatter` must be a string'); } if (config.ignores && !Array.isArray(config.ignores)) { @@ -44,7 +45,11 @@ export function validateConfig( if (config.plugins && !Array.isArray(config.plugins)) { throw new Error('Invalid configuration, `plugins` must ba an array'); } + if (config.rules && typeof config.rules !== 'object') { + throw new Error('Invalid configuration, `rules` must ba an object'); + } if ( + config.defaultIgnores && typeof config.defaultIgnores !== 'boolean' && typeof config.defaultIgnores !== 'undefined' ) { @@ -52,7 +57,21 @@ export function validateConfig( 'Invalid configuration, `defaultIgnores` must ba true/false' ); } - if (typeof config.helpUrl !== 'string') { + if (config.helpUrl && typeof config.helpUrl !== 'string') { throw new Error('Invalid configuration, `helpUrl` must be a string'); } } + +export function validateParser( + parser: unknown +): asserts parser is {name?: string; path?: string; [key: string]: unknown} { + if (!isObjectLike(parser)) { + throw new Error('Invalid configuration, `parserPreset` must be an object'); + } + if (parser.name && typeof parser.name !== 'string') { + throw new Error('Invalid configuration, `parserPreset` must have a name'); + } + if (parser.path && typeof parser.path !== 'string') { + throw new Error('Invalid configuration, `parserPreset` must have a name'); + } +} From fdf9ed184edde7632e2d27aba57bb7b7f3a3d83d Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 10 Jan 2021 23:43:10 +0100 Subject: [PATCH 06/24] fix: remove redundant validation --- @commitlint/load/src/utils/validators.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/@commitlint/load/src/utils/validators.ts b/@commitlint/load/src/utils/validators.ts index 40c44f9245..b0219396e5 100644 --- a/@commitlint/load/src/utils/validators.ts +++ b/@commitlint/load/src/utils/validators.ts @@ -48,11 +48,7 @@ export function validateConfig>( if (config.rules && typeof config.rules !== 'object') { throw new Error('Invalid configuration, `rules` must ba an object'); } - if ( - config.defaultIgnores && - typeof config.defaultIgnores !== 'boolean' && - typeof config.defaultIgnores !== 'undefined' - ) { + if (config.defaultIgnores && typeof config.defaultIgnores !== 'boolean') { throw new Error( 'Invalid configuration, `defaultIgnores` must ba true/false' ); From 787fff42932ad11a509302946ef784efac799c77 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 10 Jan 2021 23:48:45 +0100 Subject: [PATCH 07/24] fix: use reduceRight instead of reverse --- @commitlint/resolve-extends/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index c27b4d7337..051824365f 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -61,9 +61,9 @@ function loadExtends( context: ResolveExtendsContext = {} ): ResolvedConfig[] { const {extends: e} = config; - const ext = e ? (Array.isArray(e) ? [...e] : [e]) : []; + const ext = e ? (Array.isArray(e) ? e : [e]) : []; - return ext.reverse().reduce((configs, raw) => { + return ext.reduceRight((configs, raw) => { const load = context.require || require; const resolved = resolveConfig(raw, context); const c = load(resolved); From f30080d67f81009aeb53dc16dc6b2c7ad0dfee29 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 11 Jan 2021 00:02:55 +0100 Subject: [PATCH 08/24] fix: rollback some code --- @commitlint/load/src/load.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index fc9231c140..01216377dc 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -64,15 +64,6 @@ export default async function load( validateConfig(extended); - if (!extended.formatter) { - extended.formatter = '@commitlint/format'; - } - - if (!extended.helpUrl) { - extended.helpUrl = - 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; - } - let plugins: PluginRecords = {}; uniq(extended.plugins || []).forEach((plugin) => { if (typeof plugin === 'string') { @@ -93,6 +84,15 @@ export default async function load( return registry; }, {}); + if (!extended.formatter) { + extended.formatter = '@commitlint/format'; + } + + if (!extended.helpUrl) { + extended.helpUrl = + 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; + } + return { // 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 From 737704e9b0664745e47de4d3f9e5ea3e0b202f91 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 11 Jan 2021 00:13:57 +0100 Subject: [PATCH 09/24] fix: drop invalid type casts --- @commitlint/load/src/load.ts | 2 +- @commitlint/load/src/utils/pick-config.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index 01216377dc..ae53c4b0f9 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -56,7 +56,7 @@ export default async function load( } // Resolve extends key - const extended = resolveExtends(config as any, { + const extended = resolveExtends(config, { prefix: 'commitlint-config', cwd: base, parserPreset: config.parserPreset, diff --git a/@commitlint/load/src/utils/pick-config.ts b/@commitlint/load/src/utils/pick-config.ts index e5e01181c0..757ed0f702 100644 --- a/@commitlint/load/src/utils/pick-config.ts +++ b/@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 => pick( input, 'extends', From 0a131f779dc54c9d709b3fe4f32e24361c1ca881 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 11 Jan 2021 00:15:03 +0100 Subject: [PATCH 10/24] fix: rollback unnecessary changes --- @commitlint/resolve-extends/src/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index 051824365f..682dcc9fdc 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -7,13 +7,13 @@ import mergeWith from 'lodash/mergeWith'; const importFresh = require('import-fresh'); -export interface ResolveExtendsConfig { - extends?: string | string[]; +export interface ResolvedConfig { + parserPreset?: unknown; [key: string]: unknown; } -export interface ResolvedConfig { - parserPreset?: unknown; +export interface ResolveExtendsConfig { + extends?: string | string[]; [key: string]: unknown; } From a76e70e8b5231df6b5f4ddae1aa07ac1afc2f426 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 15 Jan 2021 22:12:08 +0100 Subject: [PATCH 11/24] fix: rollback config validation --- @commitlint/load/src/load.ts | 11 +-- .../load/src/utils/load-parser-opts.ts | 39 ++++++---- @commitlint/load/src/utils/validators.ts | 73 ------------------- 3 files changed, 30 insertions(+), 93 deletions(-) delete mode 100644 @commitlint/load/src/utils/validators.ts diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index ae53c4b0f9..196c18b13a 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -16,9 +16,8 @@ import { import loadPlugin from './utils/load-plugin'; import {loadConfig} from './utils/load-config'; -import {loadParser} from './utils/load-parser-opts'; +import {loadParserOpts} from './utils/load-parser-opts'; import {pickConfig} from './utils/pick-config'; -import {validateConfig} from './utils/validators'; export default async function load( seed: UserConfig = {}, @@ -56,13 +55,11 @@ export default async function load( } // Resolve extends key - const extended = resolveExtends(config, { + const extended = (resolveExtends(config, { prefix: 'commitlint-config', cwd: base, parserPreset: config.parserPreset, - }); - - validateConfig(extended); + }) as unknown) as UserConfig; let plugins: PluginRecords = {}; uniq(extended.plugins || []).forEach((plugin) => { @@ -105,7 +102,7 @@ export default async function load( formatter: resolveFrom.silent(base, extended.formatter) || extended.formatter, // Resolve parser-opts from preset - parserPreset: await loadParser(extended.parserPreset), + parserPreset: await loadParserOpts(extended.parserPreset), ignores: extended.ignores, defaultIgnores: extended.defaultIgnores, plugins: plugins, diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts index 9978f54172..3b78db4b25 100644 --- a/@commitlint/load/src/utils/load-parser-opts.ts +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -1,22 +1,35 @@ import {ParserPreset} from '@commitlint/types'; -import { - isObjectLike, - isParserOptsFunction, - isPromiseLike, - validateParser, -} from './validators'; - -export async function loadParser( - pendingParser: unknown + +function isObjectLike(obj: unknown): obj is Record { + return Boolean(obj) && typeof obj === 'object'; // typeof null === 'object' +} + +function isPromiseLike(obj: unknown): obj is Promise { + return ( + (typeof obj === 'object' || typeof obj === 'function') && + typeof (obj as any).then === 'function' + ); +} + +function isParserOptsFunction( + obj: T +): obj is T & { + parserOpts: ( + cb: (_: never, parserOpts: Record) => unknown + ) => Record | undefined; +} { + return typeof obj.parserOpts === 'function'; +} + +export async function loadParserOpts( + pendingParser: string | ParserPreset | Promise | undefined ): Promise { - if (!pendingParser) { + if (!pendingParser || typeof pendingParser === 'string') { return undefined; } // Await for the module, loaded with require const parser = await pendingParser; - validateParser(parser); - // Await parser opts if applicable if (isPromiseLike(parser.parserOpts)) { parser.parserOpts = ((await parser.parserOpts) as any).parserOpts; @@ -26,7 +39,7 @@ export async function loadParser( // Create parser opts from factory if ( isParserOptsFunction(parser) && - parser.name && + typeof parser.name === 'string' && parser.name.startsWith('conventional-changelog-') ) { return new Promise((resolve) => { diff --git a/@commitlint/load/src/utils/validators.ts b/@commitlint/load/src/utils/validators.ts deleted file mode 100644 index b0219396e5..0000000000 --- a/@commitlint/load/src/utils/validators.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {UserConfig} from '@commitlint/types'; - -export function isObjectLike(obj: unknown): obj is Record { - return Boolean(obj) && typeof obj === 'object'; // typeof null === 'object' -} - -export function isPromiseLike(obj: unknown): obj is Promise { - return ( - (typeof obj === 'object' || typeof obj === 'function') && - typeof (obj as any).then === 'function' - ); -} - -export function isParserOptsFunction>( - obj: T -): obj is T & { - parserOpts: ( - cb: (_: never, parserOpts: Record) => unknown - ) => Record | undefined; -} { - return typeof obj.parserOpts === 'function'; -} - -export function validateConfig>( - config: T -): asserts config is Omit & T { - if (!isObjectLike(config)) { - throw new Error('Invalid configuration, `parserPreset` must be an object'); - } - if ( - config.extends && - typeof config.extends !== 'string' && - !Array.isArray(config.extends) - ) { - throw new Error( - 'Invalid configuration, `extends` must be a array or string' - ); - } - if (config.formatter && typeof config.formatter !== 'string') { - throw new Error('Invalid configuration, `formatter` must be a string'); - } - if (config.ignores && !Array.isArray(config.ignores)) { - throw new Error('Invalid configuration, `ignores` must ba an array'); - } - if (config.plugins && !Array.isArray(config.plugins)) { - throw new Error('Invalid configuration, `plugins` must ba an array'); - } - if (config.rules && typeof config.rules !== 'object') { - throw new Error('Invalid configuration, `rules` must ba an object'); - } - if (config.defaultIgnores && typeof config.defaultIgnores !== 'boolean') { - throw new Error( - 'Invalid configuration, `defaultIgnores` must ba true/false' - ); - } - if (config.helpUrl && typeof config.helpUrl !== 'string') { - throw new Error('Invalid configuration, `helpUrl` must be a string'); - } -} - -export function validateParser( - parser: unknown -): asserts parser is {name?: string; path?: string; [key: string]: unknown} { - if (!isObjectLike(parser)) { - throw new Error('Invalid configuration, `parserPreset` must be an object'); - } - if (parser.name && typeof parser.name !== 'string') { - throw new Error('Invalid configuration, `parserPreset` must have a name'); - } - if (parser.path && typeof parser.path !== 'string') { - throw new Error('Invalid configuration, `parserPreset` must have a name'); - } -} From 25b98f8cf3bbbe3fe50f3fddbf2b8d2b71d35742 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 15 Jan 2021 22:25:39 +0100 Subject: [PATCH 12/24] fix: add missing type-guards and restore order --- @commitlint/load/src/load.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index 196c18b13a..d801afbf0d 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -70,6 +70,15 @@ export default async function load( } }); + if (!extended.formatter || typeof extended.formatter !== 'string') { + extended.formatter = '@commitlint/format'; + } + + if (!extended.helpUrl || typeof extended.helpUrl !== 'string') { + extended.helpUrl = + 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; + } + const rules = ( await Promise.all( Object.entries(extended.rules || {}).map((entry) => executeRule(entry)) @@ -81,15 +90,6 @@ export default async function load( return registry; }, {}); - if (!extended.formatter) { - extended.formatter = '@commitlint/format'; - } - - if (!extended.helpUrl) { - extended.helpUrl = - 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; - } - return { // 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 From f1ce3c57b64f1175ec8f4c016e6738bd7bf00bde Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 15 Jan 2021 22:27:18 +0100 Subject: [PATCH 13/24] fix: one more order change --- @commitlint/load/src/load.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index d801afbf0d..18d77a22cc 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -61,15 +61,6 @@ export default async function load( parserPreset: config.parserPreset, }) as unknown) as UserConfig; - let plugins: PluginRecords = {}; - uniq(extended.plugins || []).forEach((plugin) => { - if (typeof plugin === 'string') { - plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true'); - } else { - plugins.local = plugin; - } - }); - if (!extended.formatter || typeof extended.formatter !== 'string') { extended.formatter = '@commitlint/format'; } @@ -79,6 +70,15 @@ export default async function load( 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; } + let plugins: PluginRecords = {}; + uniq(extended.plugins || []).forEach((plugin) => { + if (typeof plugin === 'string') { + plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true'); + } else { + plugins.local = plugin; + } + }); + const rules = ( await Promise.all( Object.entries(extended.rules || {}).map((entry) => executeRule(entry)) From 8a2169fe963f13d08b0c96f9e19d4ca9ac4069f5 Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 15 Jan 2021 22:30:19 +0100 Subject: [PATCH 14/24] fix: add one more missing type guard --- @commitlint/load/src/load.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index 18d77a22cc..4ff22804b4 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -71,13 +71,15 @@ export default async function load( } let plugins: PluginRecords = {}; - uniq(extended.plugins || []).forEach((plugin) => { - if (typeof plugin === 'string') { - plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true'); - } else { - plugins.local = plugin; - } - }); + if (Array.isArray(extended.plugins)) { + uniq(extended.plugins || []).forEach((plugin) => { + if (typeof plugin === 'string') { + plugins = loadPlugin(plugins, plugin, process.env.DEBUG === 'true'); + } else { + plugins.local = plugin; + } + }); + } const rules = ( await Promise.all( From 207e756f75094e3d6a66bd4e243082f74d88303a Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 15 Jan 2021 23:58:59 +0100 Subject: [PATCH 15/24] fix: remove unused types reference --- @commitlint/resolve-extends/package.json | 1 - @commitlint/resolve-extends/tsconfig.json | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/@commitlint/resolve-extends/package.json b/@commitlint/resolve-extends/package.json index 9cfb50fa09..8d69557250 100644 --- a/@commitlint/resolve-extends/package.json +++ b/@commitlint/resolve-extends/package.json @@ -38,7 +38,6 @@ "@types/lodash": "^4.14.161" }, "dependencies": { - "@commitlint/types": "^11.0.0", "import-fresh": "^3.0.0", "lodash": "^4.17.19", "resolve-from": "^5.0.0", diff --git a/@commitlint/resolve-extends/tsconfig.json b/@commitlint/resolve-extends/tsconfig.json index 119e645565..49479bf34f 100644 --- a/@commitlint/resolve-extends/tsconfig.json +++ b/@commitlint/resolve-extends/tsconfig.json @@ -6,6 +6,5 @@ "outDir": "./lib" }, "include": ["./src"], - "exclude": ["./src/**/*.test.ts", "./lib/**/*"], - "references": [{"path": "../types"}] + "exclude": ["./src/**/*.test.ts", "./lib/**/*"] } From 24411771079c2b4d6873d4c1c9ef3b304b58066a Mon Sep 17 00:00:00 2001 From: Armano Date: Sat, 16 Jan 2021 02:24:54 +0100 Subject: [PATCH 16/24] fix: add additional unit tests --- @commitlint/resolve-extends/src/index.test.ts | 69 +++++++++++++++++++ @commitlint/resolve-extends/src/index.ts | 4 +- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/@commitlint/resolve-extends/src/index.test.ts b/@commitlint/resolve-extends/src/index.test.ts index 6a0da9e303..fd71e16036 100644 --- a/@commitlint/resolve-extends/src/index.test.ts +++ b/@commitlint/resolve-extends/src/index.test.ts @@ -349,6 +349,75 @@ test('should fall back to conventional-changelog-lint-config prefix', () => { }); }); +test('plugins should be merged correctly', () => { + const input = {extends: ['extender-name'], zero: 'root'}; + + const require = (id: string) => { + switch (id) { + case 'extender-name': + return {extends: ['recursive-extender-name'], plugins: ['test']}; + case 'recursive-extender-name': + return { + extends: ['second-recursive-extender-name'], + plugins: ['test2'], + }; + case 'second-recursive-extender-name': + return {plugins: ['test3']}; + default: + return {}; + } + }; + + const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext; + + const actual = resolveExtends(input, ctx); + + const expected = { + extends: ['extender-name'], + plugins: ['test', 'test2', 'test3'], + zero: 'root', + }; + + expect(actual).toEqual(expected); +}); + +test('rules should be merged correctly', () => { + const input = {extends: ['extender-name'], rules: {test1: ['base', '1']}}; + + const require = (id: string) => { + switch (id) { + case 'extender-name': + return { + extends: ['recursive-extender-name'], + rules: {test2: [id, '2']}, + }; + case 'recursive-extender-name': + return { + extends: ['second-recursive-extender-name'], + rules: {test1: [id, '3']}, + }; + case 'second-recursive-extender-name': + return {rules: {test2: [id, '4']}}; + default: + return {}; + } + }; + + const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext; + + const actual = resolveExtends(input, ctx); + + const expected = { + extends: ['extender-name'], + rules: { + test1: ['base', '1'], + test2: ['extender-name', '2'], + }, + }; + + expect(actual).toEqual(expected); +}); + // https://github.com/conventional-changelog/commitlint/issues/327 test('parserPreset should resolve correctly in extended configuration', () => { const input = {extends: ['extender-name'], zero: 'root'}; diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index 682dcc9fdc..9ba0e0eeb0 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -45,8 +45,8 @@ export default function resolveExtends( return srcValue; } } else if (key === 'plugins') { - if (!Array.isArray(objValue)) { - return srcValue; + if (Array.isArray(objValue)) { + return srcValue.concat(objValue); } } else if (Array.isArray(objValue)) { return srcValue; From b76b844bcfaebccd2dd685a45bed075baa16d13d Mon Sep 17 00:00:00 2001 From: Armano Date: Sat, 16 Jan 2021 02:33:58 +0100 Subject: [PATCH 17/24] fix: add additional regression tests - remove also unnecessary type check --- @commitlint/resolve-extends/src/index.test.ts | 43 ++++++++++++++++--- @commitlint/resolve-extends/src/index.ts | 6 +-- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/@commitlint/resolve-extends/src/index.test.ts b/@commitlint/resolve-extends/src/index.test.ts index fd71e16036..b49d7f9d9a 100644 --- a/@commitlint/resolve-extends/src/index.test.ts +++ b/@commitlint/resolve-extends/src/index.test.ts @@ -321,11 +321,7 @@ test('should fall back to conventional-changelog-lint-config prefix', () => { const require = (id: string) => { switch (id) { case 'conventional-changelog-lint-config-extender-name': - return { - rules: { - fallback: true, - }, - }; + return {rules: {fallback: true}}; default: return {}; } @@ -435,13 +431,46 @@ test('parserPreset should resolve correctly in extended configuration', () => { }, }; case 'recursive-extender-name': + return {parserPreset: {parserOpts: {issuePrefixes: ['#', '!']}}}; + default: + return {}; + } + }; + + const ctx = {resolve: id, require: jest.fn(require)} as ResolveExtendsContext; + + const actual = resolveExtends(input, ctx); + + const expected = { + extends: ['extender-name'], + parserPreset: { + parserOpts: { + issuePrefixes: ['#', '!', '&', 'no-references'], + referenceActions: null, + }, + }, + zero: 'root', + }; + + expect(actual).toEqual(expected); +}); + +test('parserPreset should be merged correctly', () => { + const input = {extends: ['extender-name'], zero: 'root'}; + + const require = (id: string) => { + switch (id) { + case 'extender-name': return { + extends: ['recursive-extender-name'], parserPreset: { parserOpts: { - issuePrefixes: ['#', '!'], + referenceActions: null, }, }, }; + case 'recursive-extender-name': + return {parserPreset: {parserOpts: {issuePrefixes: ['#', '!']}}}; default: return {}; } @@ -455,7 +484,7 @@ test('parserPreset should resolve correctly in extended configuration', () => { extends: ['extender-name'], parserPreset: { parserOpts: { - issuePrefixes: ['#', '!', '&', 'no-references'], + issuePrefixes: ['#', '!'], referenceActions: null, }, }, diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index 9ba0e0eeb0..5917a4e802 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -36,11 +36,7 @@ export default function resolveExtends( return extended.reduce( (r, {extends: _, ...c}) => mergeWith(r, c, (objValue, srcValue, key) => { - if (key === 'parserPreset') { - if (typeof srcValue !== 'object') { - return objValue; - } - } else if (key === 'rules') { + if (key === 'rules') { if (typeof objValue !== 'object') { return srcValue; } From c5663fa5f577c7a8a4f9056a27cc65d7e7601e58 Mon Sep 17 00:00:00 2001 From: Armano Date: Sat, 16 Jan 2021 02:35:13 +0100 Subject: [PATCH 18/24] fix: remove more unnecessary code changes --- @commitlint/resolve-extends/src/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index 5917a4e802..d1a453694e 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -36,11 +36,7 @@ export default function resolveExtends( return extended.reduce( (r, {extends: _, ...c}) => mergeWith(r, c, (objValue, srcValue, key) => { - if (key === 'rules') { - if (typeof objValue !== 'object') { - return srcValue; - } - } else if (key === 'plugins') { + if (key === 'plugins') { if (Array.isArray(objValue)) { return srcValue.concat(objValue); } From f35b1c6d855a94292638bc5ad3e8116fe748a73d Mon Sep 17 00:00:00 2001 From: Armano Date: Sat, 16 Jan 2021 02:38:43 +0100 Subject: [PATCH 19/24] fix: correct order of merging plugins --- @commitlint/resolve-extends/src/index.test.ts | 2 +- @commitlint/resolve-extends/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/@commitlint/resolve-extends/src/index.test.ts b/@commitlint/resolve-extends/src/index.test.ts index b49d7f9d9a..98099af3dd 100644 --- a/@commitlint/resolve-extends/src/index.test.ts +++ b/@commitlint/resolve-extends/src/index.test.ts @@ -370,7 +370,7 @@ test('plugins should be merged correctly', () => { const expected = { extends: ['extender-name'], - plugins: ['test', 'test2', 'test3'], + plugins: ['test3', 'test2', 'test'], zero: 'root', }; diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index d1a453694e..597a9db556 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -38,7 +38,7 @@ export default function resolveExtends( mergeWith(r, c, (objValue, srcValue, key) => { if (key === 'plugins') { if (Array.isArray(objValue)) { - return srcValue.concat(objValue); + return objValue.concat(srcValue); } } else if (Array.isArray(objValue)) { return srcValue; From 124760bf47dd57d7e2ce24c4a5f2eb84246c9613 Mon Sep 17 00:00:00 2001 From: Armano Date: Sat, 16 Jan 2021 02:41:31 +0100 Subject: [PATCH 20/24] fix: add missing type check --- @commitlint/load/src/utils/load-parser-opts.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts index 3b78db4b25..13b36557f6 100644 --- a/@commitlint/load/src/utils/load-parser-opts.ts +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -29,6 +29,9 @@ export async function loadParserOpts( } // Await for the module, loaded with require const parser = await pendingParser; + if (typeof pendingParser !== 'object') { + return undefined; + } // Await parser opts if applicable if (isPromiseLike(parser.parserOpts)) { From c38923e35e13764f11dcde529994673939f1c6ef Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 17 Jan 2021 05:45:21 +0100 Subject: [PATCH 21/24] fix: remove invalid type check --- @commitlint/load/src/utils/load-parser-opts.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts index 13b36557f6..104d3392a4 100644 --- a/@commitlint/load/src/utils/load-parser-opts.ts +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -5,10 +5,7 @@ function isObjectLike(obj: unknown): obj is Record { } function isPromiseLike(obj: unknown): obj is Promise { - return ( - (typeof obj === 'object' || typeof obj === 'function') && - typeof (obj as any).then === 'function' - ); + return typeof obj === 'object' && typeof (obj as any).then === 'function'; } function isParserOptsFunction( From ee0650d96b455f5c2ed60afd99dec9010e6130d3 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 17 Jan 2021 07:18:10 +0100 Subject: [PATCH 22/24] fix: remove redundant code --- @commitlint/load/src/utils/load-parser-opts.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/@commitlint/load/src/utils/load-parser-opts.ts b/@commitlint/load/src/utils/load-parser-opts.ts index 104d3392a4..d82d196f60 100644 --- a/@commitlint/load/src/utils/load-parser-opts.ts +++ b/@commitlint/load/src/utils/load-parser-opts.ts @@ -5,7 +5,7 @@ function isObjectLike(obj: unknown): obj is Record { } function isPromiseLike(obj: unknown): obj is Promise { - return typeof obj === 'object' && typeof (obj as any).then === 'function'; + return isObjectLike(obj) && typeof (obj as any).then === 'function'; } function isParserOptsFunction( @@ -21,14 +21,11 @@ function isParserOptsFunction( export async function loadParserOpts( pendingParser: string | ParserPreset | Promise | undefined ): Promise { - if (!pendingParser || typeof pendingParser === 'string') { + if (!pendingParser || typeof pendingParser !== 'object') { return undefined; } // Await for the module, loaded with require const parser = await pendingParser; - if (typeof pendingParser !== 'object') { - return undefined; - } // Await parser opts if applicable if (isPromiseLike(parser.parserOpts)) { From 5b5285b9712e6502513aa666e17b82cd5e20ebf4 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 17 Jan 2021 23:27:00 +0100 Subject: [PATCH 23/24] feat: implement config validation --- @commitlint/cli/src/cli.ts | 5 +- @commitlint/cli/src/types.ts | 5 - @commitlint/config-validator/package.json | 34 +++++ .../src/__snapshots__/validate.test.ts.snap | 129 ++++++++++++++++++ .../src/commitlint.schema.json | 96 +++++++++++++ .../config-validator/src/formatErrors.ts | 45 ++++++ .../config-validator/src/validate.test.ts | 67 +++++++++ @commitlint/config-validator/src/validate.ts | 47 +++++++ @commitlint/config-validator/tsconfig.json | 11 ++ .../fixtures/outer-scope/commitlint.config.js | 8 +- .../child-scope/commitlint.config.js | 8 +- .../recursive-extends-js/.commitlintrc.js | 4 +- .../first-extended/index.js | 4 +- .../first-extended/second-extended/index.js | 4 +- .../recursive-extends-json/.commitlintrc.json | 2 +- .../first-extended/index.js | 4 +- .../first-extended/second-extended/index.js | 4 +- .../first-extended/index.js | 4 +- .../first-extended/second-extended/index.js | 4 +- .../recursive-extends-package/package.json | 5 +- .../recursive-extends-yaml/.commitlintrc.yml | 2 +- .../first-extended/index.js | 4 +- .../first-extended/second-extended/index.js | 4 +- .../recursive-extends/commitlint.config.js | 4 +- .../first-extended/commitlint.config.js | 4 +- .../first-extended/second-extended/index.js | 4 +- .../specify-config-file/commitlint.config.js | 6 +- .../config/commitlint.config.js | 4 +- .../trash-extend/commitlint.config.js | 4 +- @commitlint/load/fixtures/trash-extend/one.js | 4 +- .../fixtures/trash-file/commitlint.config.js | 6 +- @commitlint/load/package.json | 1 + @commitlint/load/src/load.test.ts | 58 ++++---- @commitlint/load/src/load.ts | 36 ++--- @commitlint/load/src/utils/pick-config.ts | 14 -- @commitlint/load/tsconfig.json | 1 + @commitlint/resolve-extends/package.json | 2 + @commitlint/resolve-extends/src/index.test.ts | 46 ++++--- @commitlint/resolve-extends/src/index.ts | 27 ++-- @commitlint/resolve-extends/tsconfig.json | 4 +- @commitlint/types/src/load.ts | 11 +- tsconfig.json | 1 + tsconfig.shared.json | 1 + yarn.lock | 22 +-- 44 files changed, 575 insertions(+), 185 deletions(-) create mode 100644 @commitlint/config-validator/package.json create mode 100644 @commitlint/config-validator/src/__snapshots__/validate.test.ts.snap create mode 100644 @commitlint/config-validator/src/commitlint.schema.json create mode 100644 @commitlint/config-validator/src/formatErrors.ts create mode 100644 @commitlint/config-validator/src/validate.test.ts create mode 100644 @commitlint/config-validator/src/validate.ts create mode 100644 @commitlint/config-validator/tsconfig.json delete mode 100644 @commitlint/load/src/utils/pick-config.ts diff --git a/@commitlint/cli/src/cli.ts b/@commitlint/cli/src/cli.ts index 688cb0f1e4..eeec77be7d 100644 --- a/@commitlint/cli/src/cli.ts +++ b/@commitlint/cli/src/cli.ts @@ -8,7 +8,7 @@ import resolveGlobal from 'resolve-global'; import yargs from 'yargs'; import util from 'util'; -import {CliFlags, Seed} from './types'; +import {CliFlags} from './types'; import { LintOptions, LintOutcome, @@ -16,6 +16,7 @@ import { ParserPreset, QualifiedConfig, Formatter, + UserConfig, } from '@commitlint/types'; import {CliError} from './cli-error'; @@ -332,7 +333,7 @@ function getEditValue(flags: CliFlags) { return edit; } -function getSeed(flags: CliFlags): Seed { +function getSeed(flags: CliFlags): UserConfig { const n = (flags.extends || []).filter( (i): i is string => typeof i === 'string' ); diff --git a/@commitlint/cli/src/types.ts b/@commitlint/cli/src/types.ts index fd76023cc3..598c24a20b 100644 --- a/@commitlint/cli/src/types.ts +++ b/@commitlint/cli/src/types.ts @@ -18,8 +18,3 @@ export interface CliFlags { _: string[]; $0: string; } - -export interface Seed { - extends?: string[]; - parserPreset?: string; -} diff --git a/@commitlint/config-validator/package.json b/@commitlint/config-validator/package.json new file mode 100644 index 0000000000..267036fd33 --- /dev/null +++ b/@commitlint/config-validator/package.json @@ -0,0 +1,34 @@ +{ + "name": "@commitlint/config-validator", + "version": "11.0.0", + "description": "commitizen prompt using commitlint.config.js", + "main": "./lib/validate.js", + "files": [ + "lib/" + ], + "scripts": { + "deps": "dep-check", + "pkg": "pkg-check --skip-import" + }, + "repository": { + "type": "git", + "url": "https://github.com/conventional-changelog/commitlint.git" + }, + "author": "Mario Nebl ", + "license": "MIT", + "bugs": { + "url": "https://github.com/conventional-changelog/commitlint/issues" + }, + "homepage": "https://github.com/conventional-changelog/commitlint#readme", + "engines": { + "node": ">=v10" + }, + "devDependencies": { + "@commitlint/utils": "^11.0.0" + }, + "dependencies": { + "@commitlint/types": "^11.0.0", + "ajv": "^6.12.6" + }, + "gitHead": "cb565dfcca3128380b9b3dc274aedbcae34ce5ca" +} diff --git a/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap b/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap new file mode 100644 index 0000000000..dc21028dee --- /dev/null +++ b/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap @@ -0,0 +1,129 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`validation should fail for defaultIgnoresNotBoolean 1`] = ` +"Commitlint configuration in defaultIgnoresNotBoolean.js is invalid: + - Property \\"defaultIgnores\\" has the wrong type - should be boolean. +" +`; + +exports[`validation should fail for extendsAsObject 1`] = ` +"Commitlint configuration in extendsAsObject.js is invalid: + - Property \\"extends\\" has the wrong type - should be array. + - Property \\"extends\\" has the wrong type - should be string. + - \\"extends\\" should match exactly one schema in oneOf. Value: {\\"test\\":1}. +" +`; + +exports[`validation should fail for extendsWithFunction 1`] = ` +"Commitlint configuration in extendsWithFunction.js is invalid: + - Property \\"extends[0]\\" has the wrong type - should be string. + - Property \\"extends\\" has the wrong type - should be string. + - \\"extends\\" should match exactly one schema in oneOf. Value: [null]. +" +`; + +exports[`validation should fail for formatterAsObject 1`] = ` +"Commitlint configuration in formatterAsObject.js is invalid: + - Property \\"formatter\\" has the wrong type - should be string. +" +`; + +exports[`validation should fail for helpUrlAsArray 1`] = ` +"Commitlint configuration in helpUrlAsArray.js is invalid: + - Property \\"helpUrl\\" has the wrong type - should be string. +" +`; + +exports[`validation should fail for helpUrlNotString 1`] = ` +"Commitlint configuration in helpUrlNotString.js is invalid: + - Property \\"helpUrl\\" has the wrong type - should be string. +" +`; + +exports[`validation should fail for ignoresFunction 1`] = ` +"Commitlint configuration in ignoresFunction.js is invalid: + - Property \\"ignores\\" has the wrong type - should be array. +" +`; + +exports[`validation should fail for ignoresNotFunction 1`] = ` +"Commitlint configuration in ignoresNotFunction.js is invalid: + - \\"ignores[0]\\" should be a function. Value: 1. +" +`; + +exports[`validation should fail for parserPreset 1`] = ` +"Commitlint configuration in parserPreset.js is invalid: + - Property \\"parserPreset\\" has the wrong type - should be string. + - Property \\"parserPreset\\" has the wrong type - should be object. + - \\"parserPreset\\" should match exactly one schema in oneOf. Value: []. +" +`; + +exports[`validation should fail for pluginsNotArray 1`] = ` +"Commitlint configuration in pluginsNotArray.js is invalid: + - Property \\"plugins\\" has the wrong type - should be array. +" +`; + +exports[`validation should fail for rules1 1`] = ` +"Commitlint configuration in rules1.js is invalid: + - \\"rules['a'][0]\\" should be equal to one of the allowed values. Value: 3. + - \\"rules['a']\\" should match exactly one schema in oneOf. Value: [3]. +" +`; + +exports[`validation should fail for rules2 1`] = ` +"Commitlint configuration in rules2.js is invalid: + - \\"rules['b']\\" should NOT have more than 3 items. Value: [1,\\"test\\",2,2]. + - \\"rules['b']\\" should match exactly one schema in oneOf. Value: [1,\\"test\\",2,2]. +" +`; + +exports[`validation should fail for rules3 1`] = ` +"Commitlint configuration in rules3.js is invalid: + - \\"rules['c']\\" should NOT have fewer than 1 items. Value: []. + - \\"rules['c']\\" should match exactly one schema in oneOf. Value: []. +" +`; + +exports[`validation should fail for rules4 1`] = ` +"Commitlint configuration in rules4.js is invalid: + - Property \\"rules['d'][0]\\" has the wrong type - should be number. + - \\"rules['d'][0]\\" should be equal to one of the allowed values. Value: []. + - \\"rules['d']\\" should match exactly one schema in oneOf. Value: [[],[],[]]. +" +`; + +exports[`validation should fail for rules5 1`] = ` +"Commitlint configuration in rules5.js is invalid: + - Property \\"rules['e']\\" has the wrong type - should be array. + - \\"rules['e']\\" should match exactly one schema in oneOf. Value: {}. +" +`; + +exports[`validation should fail for rulesAsArray 1`] = ` +"Commitlint configuration in rulesAsArray.js is invalid: + - Property \\"rules\\" has the wrong type - should be object. +" +`; + +exports[`validation should fail for whenConfigIsNotObject 1`] = ` +"Commitlint configuration in whenConfigIsNotObject.js is invalid: + - Config has the wrong type - should be object. +" +`; + +exports[`validation should fail for whenConfigIsNotObject2 1`] = ` +"Commitlint configuration in whenConfigIsNotObject2.js is invalid: + - Config has the wrong type - should be object. +" +`; + +exports[`validation should fail for withPluginsAsObject 1`] = ` +"Commitlint configuration in withPluginsAsObject.js is invalid: + - Property \\"plugins[0]\\" has the wrong type - should be string. + - \\"plugins[0]\\" should have required property '.rules'. Value: {}. + - \\"plugins[0]\\" should match some schema in anyOf. Value: {}. +" +`; diff --git a/@commitlint/config-validator/src/commitlint.schema.json b/@commitlint/config-validator/src/commitlint.schema.json new file mode 100644 index 0000000000..0e03f5f211 --- /dev/null +++ b/@commitlint/config-validator/src/commitlint.schema.json @@ -0,0 +1,96 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "default": {}, + "type": "object", + "definitions": { + "rule": { + "oneOf": [ + { + "description": "A rule", + "type": "array", + "items": [ + { + "description": "Level: 0 disables the rule. For 1 it will be considered a warning, for 2 an error", + "type": "number", + "enum": [0, 1, 2] + }, + { + "description": "Applicable: always|never: never inverts the rule", + "type": "string", + "enum": ["always", "never"] + }, + { + "description": "Value: the value for this rule" + } + ], + "minItems": 1, + "maxItems": 3, + "additionalItems": false + } + ] + } + }, + "properties": { + "extends": { + "description": "Resolveable ids to commitlint configurations to extend", + "oneOf": [ + { + "type": "array", + "items": {"type": "string"} + }, + {"type": "string"} + ] + }, + "parserPreset": { + "description": "Resolveable id to conventional-changelog parser preset to import and use", + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "name": {"type": "string"}, + "path": {"type": "string"}, + "parserOpts": {} + }, + "additionalProperties": true + } + ] + }, + "helpUrl": { + "description": "Custom URL to show upon failure", + "type": "string" + }, + "formatter": { + "description": "Resolveable id to package, from node_modules, which formats the output", + "type": "string" + }, + "rules": { + "description": "Rules to check against", + "type": "object", + "propertyNames": {"type": "string"}, + "additionalProperties": {"$ref": "#/definitions/rule"} + }, + "plugins": { + "description": "Resolveable ids of commitlint plugins from node_modules", + "type": "array", + "items": { + "anyOf": [ + {"type": "string"}, + { + "required": ["rules"], + "rules": {} + } + ] + } + }, + "ignores": { + "type": "array", + "items": {"typeof": "function"}, + "description": "Additional commits to ignore, defined by ignore matchers" + }, + "defaultIgnores": { + "description": "Whether commitlint uses the default ignore rules", + "type": "boolean" + } + } +} diff --git a/@commitlint/config-validator/src/formatErrors.ts b/@commitlint/config-validator/src/formatErrors.ts new file mode 100644 index 0000000000..ef1fbacc4b --- /dev/null +++ b/@commitlint/config-validator/src/formatErrors.ts @@ -0,0 +1,45 @@ +import {ErrorObject} from 'ajv'; + +/** + * Formats an array of schema validation errors. + * @param errors An array of error messages to format. + * @returns Formatted error message + * Based on https://github.com/eslint/eslint/blob/master/lib/shared/config-validator.js#L237-L261 + */ +export function formatErrors(errors: ErrorObject[]): string { + return errors + .map((error) => { + if ( + error.keyword === 'additionalProperties' && + 'additionalProperty' in error.params + ) { + const formattedPropertyPath = error.dataPath.length + ? `${error.dataPath.slice(1)}.${error.params.additionalProperty}` + : error.params.additionalProperty; + + return `Unexpected top-level property "${formattedPropertyPath}"`; + } + if (error.keyword === 'type') { + const formattedField = error.dataPath.slice(1); + if (!formattedField) { + return `Config has the wrong type - ${error.message}`; + } + return `Property "${formattedField}" has the wrong type - ${error.message}`; + } + const field = + (error.dataPath[0] === '.' + ? error.dataPath.slice(1) + : error.dataPath) || 'Config'; + if (error.keyword === 'typeof') { + return `"${field}" should be a ${error.schema}. Value: ${JSON.stringify( + error.data + )}`; + } + + return `"${field}" ${error.message}. Value: ${JSON.stringify( + error.data + )}`; + }) + .map((message) => `\t- ${message}.\n`) + .join(''); +} diff --git a/@commitlint/config-validator/src/validate.test.ts b/@commitlint/config-validator/src/validate.test.ts new file mode 100644 index 0000000000..5f183476c2 --- /dev/null +++ b/@commitlint/config-validator/src/validate.test.ts @@ -0,0 +1,67 @@ +import {validateConfig} from './validate'; +import {UserConfig} from '@commitlint/types'; + +const validSchemas: Record = { + empty: {}, + withEmptyExtends: {extends: []}, + withStringExtends: {extends: 'test'}, + withSingleExtends: {extends: ['test']}, + withMultipleExtends: {extends: ['test', 'test2']}, + withFormatter: {formatter: ''}, + withHelpUrl: {helpUrl: ''}, + withRules: {rules: {a: [0], b: [1, 'never'], c: [2, 'never', true]}}, + withParserPresetString: {parserPreset: 'test'}, + withParserPresetObject: {parserPreset: {}}, + withParserPresetObject2: {parserPreset: {name: 'string', path: 'string'}}, + withParserPresetObjectPromise: { + parserPreset: Promise.resolve({name: 'string'}), + }, + withParserPresetOpts: {parserPreset: {parserOpts: {test: 1}}}, + withParserPresetOptsPromise: { + parserPreset: {parserOpts: Promise.resolve({test: 1})}, + }, + withEmptyIgnores: {ignores: []}, + withIgnores: {ignores: [() => true]}, + withDefaultIgnoresTrue: {defaultIgnores: true}, + withDefaultIgnoresFalse: {defaultIgnores: false}, + withEmptyPlugins: {plugins: []}, + withPluginsAsString: {plugins: ['test']}, + withPluginsAsObject: {plugins: [{rules: {}}]}, + shouldSkipAllowAdditionalProperties: {foo: 1}, +}; + +const invalidSchemas: Record = { + whenConfigIsNotObject: [], + whenConfigIsNotObject2: '', + extendsAsObject: {extends: {test: 1}}, + extendsWithFunction: {extends: [() => true]}, + formatterAsObject: {formatter: {}}, + helpUrlAsArray: {helpUrl: []}, + rulesAsArray: {rules: ['a']}, + rules1: {rules: {a: [3]}}, + rules2: {rules: {b: [1, 'test', 2, 2]}}, + rules3: {rules: {c: []}}, + rules4: {rules: {d: [[], [], []]}}, + rules5: {rules: {e: {}}}, + parserPreset: {parserPreset: []}, + ignoresFunction: {ignores: () => true}, + ignoresNotFunction: {ignores: [1]}, + defaultIgnoresNotBoolean: {defaultIgnores: 'true'}, + pluginsNotArray: {plugins: 'test'}, + withPluginsAsObject: {plugins: [{}]}, + helpUrlNotString: {helpUrl: {}}, +}; + +describe('validation should pass for', () => { + test.each(Object.entries(validSchemas))('%s', (file, config) => { + expect(() => validateConfig(`${file}.js`, config)).not.toThrowError(); + }); +}); + +describe('validation should fail for', () => { + test.each(Object.entries(invalidSchemas))('%s', (file, config) => { + expect(() => + validateConfig(`${file}.js`, config) + ).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/@commitlint/config-validator/src/validate.ts b/@commitlint/config-validator/src/validate.ts new file mode 100644 index 0000000000..ac87f3b8c4 --- /dev/null +++ b/@commitlint/config-validator/src/validate.ts @@ -0,0 +1,47 @@ +import Ajv from 'ajv'; +import {UserConfig} from '@commitlint/types'; +import schema from './commitlint.schema.json'; +import {formatErrors} from './formatErrors'; + +const TYPE_OF = [ + 'undefined', + 'string', + 'number', + 'object', + 'function', + 'boolean', + 'symbol', +]; + +export function validateConfig( + source: string, + config: unknown +): asserts config is UserConfig { + const ajv = new Ajv({ + meta: false, + useDefaults: true, + validateSchema: false, + missingRefs: 'ignore', + verbose: true, + schemaId: 'auto', + }); + + ajv.addKeyword('typeof', { + validate: function typeOfFunc(schema: any, data: any) { + return typeof data === schema; + }, + metaSchema: {type: 'string', enum: TYPE_OF}, + schema: true, + }); + + const validate = ajv.compile(schema); + const isValid = validate(config); + + if (!isValid && validate.errors && validate.errors.length) { + throw new Error( + `Commitlint configuration in ${source} is invalid:\n${formatErrors( + validate.errors + )}` + ); + } +} diff --git a/@commitlint/config-validator/tsconfig.json b/@commitlint/config-validator/tsconfig.json new file mode 100644 index 0000000000..65f2e6c397 --- /dev/null +++ b/@commitlint/config-validator/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.shared.json", + "compilerOptions": { + "composite": true, + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./src/**/*.ts", "./src/**/*.json"], + "exclude": ["./src/**/*.test.ts", "./lib/**/*"], + "references": [{"path": "../types"}] +} diff --git a/@commitlint/load/fixtures/outer-scope/commitlint.config.js b/@commitlint/load/fixtures/outer-scope/commitlint.config.js index 23554f588f..9b27c320a7 100644 --- a/@commitlint/load/fixtures/outer-scope/commitlint.config.js +++ b/@commitlint/load/fixtures/outer-scope/commitlint.config.js @@ -1,7 +1,7 @@ module.exports = { rules: { - outer: true, - inner: false, - child: false - } + outer: [1, 'never', true], + inner: [1, 'never', false], + child: [1, 'never', false], + }, }; diff --git a/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js b/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js index 925d251382..d2c04c8054 100644 --- a/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js +++ b/@commitlint/load/fixtures/outer-scope/inner-scope/child-scope/commitlint.config.js @@ -1,7 +1,7 @@ module.exports = { rules: { - outer: false, - inner: false, - child: true - } + outer: [2, 'always', false], + inner: [2, 'always', false], + child: [2, 'always', true], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js b/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js index 7564fdc432..f90e771292 100644 --- a/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js +++ b/@commitlint/load/fixtures/recursive-extends-js/.commitlintrc.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./first-extended'], rules: { - zero: 0 - } + zero: [0, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js index 4317428ad1..d26b0ff209 100644 --- a/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-js/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js index d199d354da..64caae544a 100644 --- a/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-js/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json b/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json index 9a5e8e1d9f..97335f47a5 100644 --- a/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json +++ b/@commitlint/load/fixtures/recursive-extends-json/.commitlintrc.json @@ -1,6 +1,6 @@ { "extends": ["./first-extended"], "rules": { - "zero": 0 + "zero": [0, "never"] } } diff --git a/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js index 4317428ad1..8bc1854ccc 100644 --- a/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-json/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js index d199d354da..b0902ace44 100644 --- a/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-json/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js index 4317428ad1..d26b0ff209 100644 --- a/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-package/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js index d199d354da..b0902ace44 100644 --- a/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-package/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-package/package.json b/@commitlint/load/fixtures/recursive-extends-package/package.json index 1818049244..704cc882f1 100644 --- a/@commitlint/load/fixtures/recursive-extends-package/package.json +++ b/@commitlint/load/fixtures/recursive-extends-package/package.json @@ -4,7 +4,10 @@ "./first-extended" ], "rules": { - "zero": 0 + "zero": [ + 0, + "never" + ] } } } diff --git a/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml b/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml index a5ef7fdffd..c7c751a959 100644 --- a/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml +++ b/@commitlint/load/fixtures/recursive-extends-yaml/.commitlintrc.yml @@ -1,4 +1,4 @@ extends: - './first-extended' rules: - zero: 0 + zero: [0, 'never'] diff --git a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js index 4317428ad1..d26b0ff209 100644 --- a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/index.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js index d199d354da..64caae544a 100644 --- a/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends-yaml/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends/commitlint.config.js b/@commitlint/load/fixtures/recursive-extends/commitlint.config.js index 7564fdc432..f90e771292 100644 --- a/@commitlint/load/fixtures/recursive-extends/commitlint.config.js +++ b/@commitlint/load/fixtures/recursive-extends/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./first-extended'], rules: { - zero: 0 - } + zero: [0, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js b/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js index 4317428ad1..8bc1854ccc 100644 --- a/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js +++ b/@commitlint/load/fixtures/recursive-extends/first-extended/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { extends: ['./second-extended'], rules: { - one: 1 - } + one: [1, 'always'], + }, }; diff --git a/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js b/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js index d199d354da..b0902ace44 100644 --- a/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js +++ b/@commitlint/load/fixtures/recursive-extends/first-extended/second-extended/index.js @@ -1,5 +1,5 @@ module.exports = { rules: { - two: 2 - } + two: [2, 'never'], + }, }; diff --git a/@commitlint/load/fixtures/specify-config-file/commitlint.config.js b/@commitlint/load/fixtures/specify-config-file/commitlint.config.js index 1fd91e7bb1..31855af125 100644 --- a/@commitlint/load/fixtures/specify-config-file/commitlint.config.js +++ b/@commitlint/load/fixtures/specify-config-file/commitlint.config.js @@ -1,6 +1,6 @@ module.exports = { rules: { - foo: 'hello', - bar: 'world' - } + foo: [1, 'never', 'hello'], + bar: [1, 'never', 'world'], + }, }; diff --git a/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js b/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js index be422cfda5..fbf4c8adc9 100644 --- a/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js +++ b/@commitlint/load/fixtures/specify-config-file/config/commitlint.config.js @@ -1,5 +1,5 @@ module.exports = { rules: { - foo: 'bar' - } + foo: [1, 'always', 'bar'], + }, }; diff --git a/@commitlint/load/fixtures/trash-extend/commitlint.config.js b/@commitlint/load/fixtures/trash-extend/commitlint.config.js index 3b41dace2b..7986e40f48 100644 --- a/@commitlint/load/fixtures/trash-extend/commitlint.config.js +++ b/@commitlint/load/fixtures/trash-extend/commitlint.config.js @@ -2,6 +2,6 @@ module.exports = { extends: ['./one'], zero: '0', rules: { - zero: 0 - } + zero: [0, 'always', 'zero'], + }, }; diff --git a/@commitlint/load/fixtures/trash-extend/one.js b/@commitlint/load/fixtures/trash-extend/one.js index 60f3a3530d..ae7db74d52 100644 --- a/@commitlint/load/fixtures/trash-extend/one.js +++ b/@commitlint/load/fixtures/trash-extend/one.js @@ -1,6 +1,6 @@ module.exports = { one: 1, rules: { - one: 1 - } + one: [1, 'always', 'one'], + }, }; diff --git a/@commitlint/load/fixtures/trash-file/commitlint.config.js b/@commitlint/load/fixtures/trash-file/commitlint.config.js index a7a8e43bd8..477faba44e 100644 --- a/@commitlint/load/fixtures/trash-file/commitlint.config.js +++ b/@commitlint/load/fixtures/trash-file/commitlint.config.js @@ -2,7 +2,7 @@ module.exports = { foo: 'bar', baz: 'bar', rules: { - foo: 'bar', - baz: 'bar' - } + foo: [1, 'always', 'bar'], + baz: [1, 'always', 'bar'], + }, }; diff --git a/@commitlint/load/package.json b/@commitlint/load/package.json index bbf2cf9aee..47f6b9a108 100644 --- a/@commitlint/load/package.json +++ b/@commitlint/load/package.json @@ -42,6 +42,7 @@ "dependencies": { "@commitlint/execute-rule": "^11.0.0", "@commitlint/resolve-extends": "^11.0.0", + "@commitlint/config-validator": "^11.0.0", "@commitlint/types": "^11.0.0", "chalk": "^4.0.0", "cosmiconfig": "^7.0.0", diff --git a/@commitlint/load/src/load.test.ts b/@commitlint/load/src/load.test.ts index ff3530a0c1..22cd2b2cbf 100644 --- a/@commitlint/load/src/load.test.ts +++ b/@commitlint/load/src/load.test.ts @@ -165,8 +165,8 @@ test('respects cwd option', async () => { extends: ['./second-extended'], plugins: {}, rules: { - one: 1, - two: 2, + one: [1, 'always'], + two: [2, 'never'], }, }); }); @@ -180,9 +180,9 @@ test('recursive extends', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'always'], + two: [2, 'never'], }, }); }); @@ -196,9 +196,9 @@ test('recursive extends with json file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'always'], + two: [2, 'never'], }, }); }); @@ -212,9 +212,9 @@ test('recursive extends with yaml file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'never'], + two: [2, 'always'], }, }); }); @@ -228,9 +228,9 @@ test('recursive extends with js file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'never'], + two: [2, 'always'], }, }); }); @@ -244,9 +244,9 @@ test('recursive extends with package.json file', async () => { extends: ['./first-extended'], plugins: {}, rules: { - zero: 0, - one: 1, - two: 2, + zero: [0, 'never'], + one: [1, 'never'], + two: [2, 'never'], }, }); }); @@ -273,7 +273,7 @@ test('recursive extends with parserPreset', async () => { }); }); -test('ignores unknow keys', async () => { +test('ignores unknown keys', async () => { const cwd = await gitBootstrap('fixtures/trash-file'); const actual = await load({}, {cwd}); @@ -282,13 +282,13 @@ test('ignores unknow keys', async () => { extends: [], plugins: {}, rules: { - foo: 'bar', - baz: 'bar', + foo: [1, 'always', 'bar'], + baz: [1, 'always', 'bar'], }, }); }); -test('ignores unknow keys recursively', async () => { +test('ignores unknown keys recursively', async () => { const cwd = await gitBootstrap('fixtures/trash-extend'); const actual = await load({}, {cwd}); @@ -297,8 +297,8 @@ test('ignores unknow keys recursively', async () => { extends: ['./one'], plugins: {}, rules: { - zero: 0, - one: 1, + zero: [0, 'always', 'zero'], + one: [1, 'always', 'one'], }, }); }); @@ -314,9 +314,9 @@ test('find up from given cwd', async () => { extends: [], plugins: {}, rules: { - child: true, - inner: false, - outer: false, + child: [2, 'always', true], + inner: [2, 'always', false], + outer: [2, 'always', false], }, }); }); @@ -331,9 +331,9 @@ test('find up config from outside current git repo', async () => { extends: [], plugins: {}, rules: { - child: false, - inner: false, - outer: true, + child: [1, 'never', false], + inner: [1, 'never', false], + outer: [1, 'never', true], }, }); }); diff --git a/@commitlint/load/src/load.ts b/@commitlint/load/src/load.ts index 4ff22804b4..eef79ceb5e 100644 --- a/@commitlint/load/src/load.ts +++ b/@commitlint/load/src/load.ts @@ -6,6 +6,7 @@ import resolveFrom from 'resolve-from'; import executeRule from '@commitlint/execute-rule'; import resolveExtends from '@commitlint/resolve-extends'; +import {validateConfig} from '@commitlint/config-validator'; import { UserConfig, LoadOptions, @@ -17,7 +18,6 @@ import { import loadPlugin from './utils/load-plugin'; import {loadConfig} from './utils/load-config'; import {loadParserOpts} from './utils/load-parser-opts'; -import {pickConfig} from './utils/pick-config'; export default async function load( seed: UserConfig = {}, @@ -26,21 +26,21 @@ export default async function load( const cwd = typeof options.cwd === 'undefined' ? process.cwd() : options.cwd; const loaded = await loadConfig(cwd, options.file); const base = loaded && loaded.filepath ? Path.dirname(loaded.filepath) : cwd; - - // TODO: validate loaded.config against UserConfig type - // Might amount to breaking changes, defer until 9.0.0 + let config: UserConfig = {}; + if (loaded) { + validateConfig(loaded.filepath || '', loaded.config); + config = loaded.config; + } // Merge passed config with file based options - const config = pickConfig( - merge( - { - extends: [], - plugins: [], - rules: {}, - }, - loaded ? loaded.config : null, - seed - ) + config = merge( + { + extends: [], + plugins: [], + rules: {}, + }, + config, + seed ); // Resolve parserPreset key @@ -55,17 +55,17 @@ export default async function load( } // Resolve extends key - const extended = (resolveExtends(config, { + const extended = resolveExtends(config, { prefix: 'commitlint-config', cwd: base, parserPreset: config.parserPreset, - }) as unknown) as UserConfig; + }); - if (!extended.formatter || typeof extended.formatter !== 'string') { + if (!extended.formatter) { extended.formatter = '@commitlint/format'; } - if (!extended.helpUrl || typeof extended.helpUrl !== 'string') { + if (!extended.helpUrl) { extended.helpUrl = 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint'; } diff --git a/@commitlint/load/src/utils/pick-config.ts b/@commitlint/load/src/utils/pick-config.ts deleted file mode 100644 index 757ed0f702..0000000000 --- a/@commitlint/load/src/utils/pick-config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import pick from 'lodash/pick'; - -export const pickConfig = (input: unknown): Record => - pick( - input, - 'extends', - 'rules', - 'plugins', - 'parserPreset', - 'formatter', - 'ignores', - 'defaultIgnores', - 'helpUrl' - ); diff --git a/@commitlint/load/tsconfig.json b/@commitlint/load/tsconfig.json index 51cdcedab0..1b645658c1 100644 --- a/@commitlint/load/tsconfig.json +++ b/@commitlint/load/tsconfig.json @@ -10,6 +10,7 @@ "references": [ {"path": "../execute-rule"}, {"path": "../resolve-extends"}, + {"path": "../config-validator"}, {"path": "../types"} ] } diff --git a/@commitlint/resolve-extends/package.json b/@commitlint/resolve-extends/package.json index 8d69557250..43aef93ab3 100644 --- a/@commitlint/resolve-extends/package.json +++ b/@commitlint/resolve-extends/package.json @@ -38,6 +38,8 @@ "@types/lodash": "^4.14.161" }, "dependencies": { + "@commitlint/config-validator": "^11.0.0", + "@commitlint/types": "^11.0.0", "import-fresh": "^3.0.0", "lodash": "^4.17.19", "resolve-from": "^5.0.0", diff --git a/@commitlint/resolve-extends/src/index.test.ts b/@commitlint/resolve-extends/src/index.test.ts index 98099af3dd..87c00f82b9 100644 --- a/@commitlint/resolve-extends/src/index.test.ts +++ b/@commitlint/resolve-extends/src/index.test.ts @@ -1,4 +1,5 @@ import resolveExtends, {ResolveExtendsContext} from '.'; +import {UserConfig} from '@commitlint/types'; const id = (id: unknown) => id; @@ -213,17 +214,17 @@ test('propagates contents recursively', () => { }); test('propagates contents recursively with overlap', () => { - const input = {extends: ['extender-name']}; + const input: UserConfig = {extends: ['extender-name']}; - const require = (id: string) => { + const require = (id: string): UserConfig => { switch (id) { case 'extender-name': return { extends: ['recursive-extender-name'], - rules: {rule: ['zero', 'one']}, + rules: {rule: [1, 'always']}, }; case 'recursive-extender-name': - return {rules: {rule: ['two', 'three', 'four']}}; + return {rules: {rule: [2, 'never', 'four']}}; default: return {}; } @@ -233,10 +234,10 @@ test('propagates contents recursively with overlap', () => { const actual = resolveExtends(input, ctx); - const expected = { + const expected: UserConfig = { extends: ['extender-name'], rules: { - rule: ['zero', 'one'], + rule: [1, 'always'], }, }; @@ -244,14 +245,14 @@ test('propagates contents recursively with overlap', () => { }); test('extends rules from left to right with overlap', () => { - const input = {extends: ['left', 'right']}; + const input: UserConfig = {extends: ['left', 'right']}; - const require = (id: string) => { + const require = (id: string): UserConfig => { switch (id) { case 'left': - return {rules: {a: true}}; + return {rules: {a: [0, 'never', true]}}; case 'right': - return {rules: {a: false, b: true}}; + return {rules: {a: [0, 'never', false], b: [0, 'never', true]}}; default: return {}; } @@ -261,11 +262,11 @@ test('extends rules from left to right with overlap', () => { const actual = resolveExtends(input, ctx); - const expected = { + const expected: UserConfig = { extends: ['left', 'right'], rules: { - a: false, - b: true, + a: [0, 'never', false], + b: [0, 'never', true], }, }; @@ -378,22 +379,25 @@ test('plugins should be merged correctly', () => { }); test('rules should be merged correctly', () => { - const input = {extends: ['extender-name'], rules: {test1: ['base', '1']}}; + const input: UserConfig = { + extends: ['extender-name'], + rules: {test1: [1, 'never', 'base']}, + }; - const require = (id: string) => { + const require = (id: string): UserConfig => { switch (id) { case 'extender-name': return { extends: ['recursive-extender-name'], - rules: {test2: [id, '2']}, + rules: {test2: [2, 'never', id]}, }; case 'recursive-extender-name': return { extends: ['second-recursive-extender-name'], - rules: {test1: [id, '3']}, + rules: {test1: [0, 'never', id]}, }; case 'second-recursive-extender-name': - return {rules: {test2: [id, '4']}}; + return {rules: {test2: [1, 'never', id]}}; default: return {}; } @@ -403,11 +407,11 @@ test('rules should be merged correctly', () => { const actual = resolveExtends(input, ctx); - const expected = { + const expected: UserConfig = { extends: ['extender-name'], rules: { - test1: ['base', '1'], - test2: ['extender-name', '2'], + test1: [1, 'never', 'base'], + test2: [2, 'never', 'extender-name'], }, }; diff --git a/@commitlint/resolve-extends/src/index.ts b/@commitlint/resolve-extends/src/index.ts index 597a9db556..d9f2ba6758 100644 --- a/@commitlint/resolve-extends/src/index.ts +++ b/@commitlint/resolve-extends/src/index.ts @@ -2,21 +2,12 @@ import path from 'path'; import 'resolve-global'; import resolveFrom from 'resolve-from'; -import merge from 'lodash/merge'; import mergeWith from 'lodash/mergeWith'; +import {validateConfig} from '@commitlint/config-validator'; +import {UserConfig} from '@commitlint/types'; const importFresh = require('import-fresh'); -export interface ResolvedConfig { - parserPreset?: unknown; - [key: string]: unknown; -} - -export interface ResolveExtendsConfig { - extends?: string | string[]; - [key: string]: unknown; -} - export interface ResolveExtendsContext { cwd?: string; parserPreset?: unknown; @@ -27,9 +18,9 @@ export interface ResolveExtendsContext { } export default function resolveExtends( - config: ResolveExtendsConfig = {}, + config: UserConfig = {}, context: ResolveExtendsContext = {} -): ResolvedConfig { +): UserConfig { const {extends: e} = config; const extended = loadExtends(config, context); extended.push(config); @@ -49,18 +40,18 @@ export default function resolveExtends( } function loadExtends( - config: ResolveExtendsConfig = {}, + config: UserConfig = {}, context: ResolveExtendsContext = {} -): ResolvedConfig[] { +): UserConfig[] { const {extends: e} = config; const ext = e ? (Array.isArray(e) ? e : [e]) : []; - return ext.reduceRight((configs, raw) => { + return ext.reduceRight((configs, raw) => { const load = context.require || require; const resolved = resolveConfig(raw, context); const c = load(resolved); const cwd = path.dirname(resolved); - const ctx = merge({}, context, {cwd}); + const ctx = {...context, cwd}; // Resolve parser preset if none was present before if ( @@ -81,6 +72,8 @@ function loadExtends( config.parserPreset = parserPreset; } + validateConfig(resolved, config); + return [...loadExtends(c, ctx), c, ...configs]; }, []); } diff --git a/@commitlint/resolve-extends/tsconfig.json b/@commitlint/resolve-extends/tsconfig.json index 49479bf34f..d424776e00 100644 --- a/@commitlint/resolve-extends/tsconfig.json +++ b/@commitlint/resolve-extends/tsconfig.json @@ -2,9 +2,11 @@ "extends": "../../tsconfig.shared.json", "compilerOptions": { "composite": true, + "isolatedModules": false, "rootDir": "./src", "outDir": "./lib" }, "include": ["./src"], - "exclude": ["./src/**/*.test.ts", "./lib/**/*"] + "exclude": ["./src/**/*.test.ts", "./lib/**/*"], + "references": [{"path": "../types"}, {"path": "../config-validator"}] } diff --git a/@commitlint/types/src/load.ts b/@commitlint/types/src/load.ts index c24a81bfb9..546a3ca70f 100644 --- a/@commitlint/types/src/load.ts +++ b/@commitlint/types/src/load.ts @@ -22,16 +22,7 @@ export interface UserConfig { defaultIgnores?: boolean; plugins?: (string | Plugin)[]; helpUrl?: string; -} - -export interface UserPreset { - extends?: string[]; - formatter?: string; - rules?: Partial; - parserPreset?: string | ParserPreset; - ignores?: ((commit: string) => boolean)[]; - defaultIgnores?: boolean; - plugins: PluginRecords; + [key: string]: unknown; } export type QualifiedRules = Partial>; diff --git a/tsconfig.json b/tsconfig.json index 4dec545ae4..4af014cdaa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "references": [ {"path": "@packages/test-environment"}, {"path": "@packages/test"}, + {"path": "@commitlint/config-validator"}, {"path": "@commitlint/ensure"}, {"path": "@commitlint/execute-rule"}, {"path": "@commitlint/format"}, diff --git a/tsconfig.shared.json b/tsconfig.shared.json index ad1bb5892d..387485948c 100644 --- a/tsconfig.shared.json +++ b/tsconfig.shared.json @@ -7,6 +7,7 @@ "sourceMap": true, "module": "commonjs", "esModuleInterop": true, + "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "strict": true, "allowUnreachableCode": false, diff --git a/yarn.lock b/yarn.lock index cd53ccffc3..c2f360bb43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2360,7 +2360,7 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.5.5, ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.6: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2370,26 +2370,6 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^6.12.3: - version "6.12.5" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" - integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ajv@^6.5.5: - version "6.11.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" - integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== - dependencies: - fast-deep-equal "^3.1.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - ajv@^7.0.2: version "7.0.2" resolved "https://registry.npmjs.org/ajv/-/ajv-7.0.2.tgz#04ccc89a93449c64382fe0846d45a7135d986dbc" From 90bf68b1810e7d8ecb9991e40f743a82575b4000 Mon Sep 17 00:00:00 2001 From: Armano Date: Sun, 28 Nov 2021 13:54:11 +0100 Subject: [PATCH 24/24] fix: allow to use function as a rule --- .../config-validator/src/__snapshots__/validate.test.ts.snap | 5 +++++ @commitlint/config-validator/src/commitlint.schema.json | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap b/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap index dc21028dee..bb626b6ee2 100644 --- a/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap +++ b/@commitlint/config-validator/src/__snapshots__/validate.test.ts.snap @@ -69,6 +69,7 @@ exports[`validation should fail for pluginsNotArray 1`] = ` exports[`validation should fail for rules1 1`] = ` "Commitlint configuration in rules1.js is invalid: - \\"rules['a'][0]\\" should be equal to one of the allowed values. Value: 3. + - \\"rules['a']\\" should be a function. Value: [3]. - \\"rules['a']\\" should match exactly one schema in oneOf. Value: [3]. " `; @@ -76,6 +77,7 @@ exports[`validation should fail for rules1 1`] = ` exports[`validation should fail for rules2 1`] = ` "Commitlint configuration in rules2.js is invalid: - \\"rules['b']\\" should NOT have more than 3 items. Value: [1,\\"test\\",2,2]. + - \\"rules['b']\\" should be a function. Value: [1,\\"test\\",2,2]. - \\"rules['b']\\" should match exactly one schema in oneOf. Value: [1,\\"test\\",2,2]. " `; @@ -83,6 +85,7 @@ exports[`validation should fail for rules2 1`] = ` exports[`validation should fail for rules3 1`] = ` "Commitlint configuration in rules3.js is invalid: - \\"rules['c']\\" should NOT have fewer than 1 items. Value: []. + - \\"rules['c']\\" should be a function. Value: []. - \\"rules['c']\\" should match exactly one schema in oneOf. Value: []. " `; @@ -91,6 +94,7 @@ exports[`validation should fail for rules4 1`] = ` "Commitlint configuration in rules4.js is invalid: - Property \\"rules['d'][0]\\" has the wrong type - should be number. - \\"rules['d'][0]\\" should be equal to one of the allowed values. Value: []. + - \\"rules['d']\\" should be a function. Value: [[],[],[]]. - \\"rules['d']\\" should match exactly one schema in oneOf. Value: [[],[],[]]. " `; @@ -98,6 +102,7 @@ exports[`validation should fail for rules4 1`] = ` exports[`validation should fail for rules5 1`] = ` "Commitlint configuration in rules5.js is invalid: - Property \\"rules['e']\\" has the wrong type - should be array. + - \\"rules['e']\\" should be a function. Value: {}. - \\"rules['e']\\" should match exactly one schema in oneOf. Value: {}. " `; diff --git a/@commitlint/config-validator/src/commitlint.schema.json b/@commitlint/config-validator/src/commitlint.schema.json index 0e03f5f211..62e70c356e 100644 --- a/@commitlint/config-validator/src/commitlint.schema.json +++ b/@commitlint/config-validator/src/commitlint.schema.json @@ -26,6 +26,10 @@ "minItems": 1, "maxItems": 3, "additionalItems": false + }, + { + "description": "A rule", + "typeof": "function" } ] }