diff --git a/@commitlint/prompt/src/library/get-prompt.ts b/@commitlint/prompt/src/library/get-prompt.ts index 73a9cb7f15..ef90d965ea 100644 --- a/@commitlint/prompt/src/library/get-prompt.ts +++ b/@commitlint/prompt/src/library/get-prompt.ts @@ -14,6 +14,7 @@ import { getHasName, getMaxLength, } from './utils'; +import {EnumRuleOptions} from '@commitlint/types'; /** * Get a cli prompt based on rule configuration @@ -68,7 +69,9 @@ export default function getPrompt( throw new TypeError('getPrompt: prompt.show is not a function'); } - const enumRule = rules.filter(getHasName('enum')).find(enumRuleIsActive); + const enumRule = (rules as Array>) + .filter(getHasName('enum')) + .find(enumRuleIsActive); const emptyRule = rules.find(getHasName('empty')); @@ -118,7 +121,7 @@ export default function getPrompt( if (enumRule) { const [, [, , enums]] = enumRule; - enums.forEach((enumerable) => { + (enums as string[]).forEach((enumerable) => { const enumSettings = (settings.enumerables || {})[enumerable] || {}; prompt .command(enumerable) diff --git a/@commitlint/prompt/src/library/types.ts b/@commitlint/prompt/src/library/types.ts index 0f05c3cd6f..b36cefa529 100644 --- a/@commitlint/prompt/src/library/types.ts +++ b/@commitlint/prompt/src/library/types.ts @@ -1,9 +1,9 @@ import {RuleConfigCondition, RuleConfigSeverity} from '@commitlint/types'; -export type RuleEntry = +export type RuleEntry = | [string, Readonly<[RuleConfigSeverity.Disabled]>] | [string, Readonly<[RuleConfigSeverity, RuleConfigCondition]>] - | [string, Readonly<[RuleConfigSeverity, RuleConfigCondition, unknown]>]; + | [string, Readonly<[RuleConfigSeverity, RuleConfigCondition, C]>]; export type InputSetting = { description?: string; diff --git a/@commitlint/prompt/src/library/utils.test.ts b/@commitlint/prompt/src/library/utils.test.ts index 3d69313456..4c6ade829d 100644 --- a/@commitlint/prompt/src/library/utils.test.ts +++ b/@commitlint/prompt/src/library/utils.test.ts @@ -1,4 +1,5 @@ -import {RuleConfigSeverity} from '@commitlint/types'; +import {EnumRuleOptions, RuleConfigSeverity} from '@commitlint/types'; +import {RuleEntry} from './types'; import { enumRuleIsActive, @@ -85,14 +86,19 @@ test('getMaxLength', () => { }); test('check enum rule filters', () => { - const rules: any = { + const rules: Record[1]> = { 'enum-string': [RuleConfigSeverity.Warning, 'always', ['1', '2', '3']], 'type-enum': [RuleConfigSeverity.Error, 'always', ['build', 'chore', 'ci']], 'scope-enum': [RuleConfigSeverity.Error, 'never', ['cli', 'core', 'lint']], 'bar-enum': [RuleConfigSeverity.Disabled, 'always', ['foo', 'bar', 'baz']], + 'extendable-scope-enum': [ + RuleConfigSeverity.Disabled, + 'always', + {values: ['foo', 'bar', 'baz']}, + ], }; - let enumRule = getRules('type', rules) + let enumRule = getRules('type', rules) .filter(getHasName('enum')) .find(enumRuleIsActive); expect(enumRule).toEqual([ @@ -100,22 +106,27 @@ test('check enum rule filters', () => { [2, 'always', ['build', 'chore', 'ci']], ]); - enumRule = getRules('string', rules) + enumRule = getRules('string', rules) .filter(getHasName('enum')) .find(enumRuleIsActive); expect(enumRule).toEqual(undefined); - enumRule = getRules('enum', rules) + enumRule = getRules('enum', rules) .filter(getHasName('string')) .find(enumRuleIsActive); expect(enumRule).toEqual(['enum-string', [1, 'always', ['1', '2', '3']]]); - enumRule = getRules('bar', rules) + enumRule = getRules('bar', rules) + .filter(getHasName('enum')) + .find(enumRuleIsActive); + expect(enumRule).toEqual(undefined); + + enumRule = getRules('scope', rules) .filter(getHasName('enum')) .find(enumRuleIsActive); expect(enumRule).toEqual(undefined); - enumRule = getRules('scope', rules) + enumRule = getRules('extendable-scope', rules) .filter(getHasName('enum')) .find(enumRuleIsActive); expect(enumRule).toEqual(undefined); diff --git a/@commitlint/prompt/src/library/utils.ts b/@commitlint/prompt/src/library/utils.ts index 4eb6e74d73..d1e3a28770 100644 --- a/@commitlint/prompt/src/library/utils.ts +++ b/@commitlint/prompt/src/library/utils.ts @@ -1,4 +1,9 @@ -import {QualifiedRules, RuleConfigSeverity} from '@commitlint/types'; +import { + QualifiedRules, + RuleConfigSeverity, + EnumRuleOptions, + EnumRuleExtendableOptions, +} from '@commitlint/types'; import {RuleEntry} from './types'; /** @@ -25,7 +30,7 @@ export function getRulePrefix(id: string): string | null { * Get a predicate matching rule definitions with a given name */ export function getHasName(name: string) { - return ( + return = RuleEntry>( rule: RuleEntry ): rule is Exclude => getRuleName(rule[0]) === name; } @@ -35,7 +40,10 @@ export function getHasName(name: string) { * @param rule to check * @return if the rule definition is active */ -export function ruleIsActive( +export function ruleIsActive< + C = unknown, + T extends RuleEntry = RuleEntry +>( rule: T ): rule is Exclude]> { const [, value] = rule; @@ -50,11 +58,11 @@ export function ruleIsActive( * @param rule to check * @return if the rule definition is applicable */ -export function ruleIsApplicable( - rule: RuleEntry +export function ruleIsApplicable( + rule: RuleEntry ): rule is | [string, Readonly<[RuleConfigSeverity, 'always']>] - | [string, Readonly<[RuleConfigSeverity, 'always', unknown]>] { + | [string, Readonly<[RuleConfigSeverity, 'always', C]>] { const [, value] = rule; if (value && Array.isArray(value)) { return value[1] === 'always'; @@ -79,19 +87,32 @@ export function ruleIsNotApplicable( return false; } +function enumConfigIsExtendable( + config?: EnumRuleOptions +): config is EnumRuleExtendableOptions { + return !Array.isArray(config); +} + export function enumRuleIsActive( - rule: RuleEntry + rule: RuleEntry ): rule is [ string, Readonly< - [RuleConfigSeverity.Warning | RuleConfigSeverity.Error, 'always', string[]] + [ + RuleConfigSeverity.Warning | RuleConfigSeverity.Error, + 'always', + EnumRuleOptions + ] > ] { + let config: EnumRuleOptions | undefined; return ( ruleIsActive(rule) && ruleIsApplicable(rule) && - Array.isArray(rule[1][2]) && - rule[1][2].length > 0 + !!(config = rule[1][2]) && + (!enumConfigIsExtendable(config) + ? config.length > 0 + : Array.isArray(config.values) && config.values.length > 0) ); } @@ -101,9 +122,12 @@ export function enumRuleIsActive( * @param rules rules to search in * @return rules matching the prefix search */ -export function getRules(prefix: string, rules: QualifiedRules): RuleEntry[] { +export function getRules( + prefix: string, + rules: QualifiedRules +): RuleEntry[] { return Object.entries(rules).filter( - (rule): rule is RuleEntry => getRulePrefix(rule[0]) === prefix + (rule): rule is RuleEntry => getRulePrefix(rule[0]) === prefix ); } diff --git a/@commitlint/rules/src/scope-enum.test.ts b/@commitlint/rules/src/scope-enum.test.ts index f624d7bce8..e79a81aded 100644 --- a/@commitlint/rules/src/scope-enum.test.ts +++ b/@commitlint/rules/src/scope-enum.test.ts @@ -6,6 +6,7 @@ const messages = { superfluous: 'foo(): baz', empty: 'foo: baz', multiple: 'foo(bar,baz): qux', + scoped: 'foo(@a/b): test', }; const parsed = { @@ -13,6 +14,7 @@ const parsed = { superfluous: parse(messages.superfluous), empty: parse(messages.empty), multiple: parse(messages.multiple), + scoped: parse(messages.scoped), }; test('scope-enum with plain message and always should succeed empty enum', async () => { @@ -21,6 +23,15 @@ test('scope-enum with plain message and always should succeed empty enum', async expect(actual).toEqual(expected); }); +test('scope-enum allows custom delimiters', async () => { + const [actual] = scopeEnum(await parsed.scoped, 'always', { + values: ['@a/b'], + delimiter: /,/g, + }); + const expected = true; + expect(actual).toEqual(expected); +}); + test('scope-enum with plain message and never should error empty enum', async () => { const [actual] = scopeEnum(await parsed.plain, 'never', []); const expected = false; diff --git a/@commitlint/rules/src/scope-enum.ts b/@commitlint/rules/src/scope-enum.ts index e368df5a6a..f9cf0cd202 100644 --- a/@commitlint/rules/src/scope-enum.ts +++ b/@commitlint/rules/src/scope-enum.ts @@ -2,18 +2,27 @@ import * as ensure from '@commitlint/ensure'; import message from '@commitlint/message'; import {SyncRule} from '@commitlint/types'; -export const scopeEnum: SyncRule = ( - parsed, - when = 'always', - value = [] -) => { +export const scopeEnum: SyncRule< + string[] | {delimiter?: RegExp; values?: string[]} +> = (parsed, when = 'always', config = []) => { if (!parsed.scope) { return [true, '']; } + let delimiters = /\/|\\|,/g; + let value: string[] = []; + if (!Array.isArray(config)) { + if (config.delimiter) { + delimiters = config.delimiter; + } + value = config.values || []; + } else { + value = config; + } + // Scopes may contain slash or comma delimiters to separate them and mark them as individual segments. // This means that each of these segments should be tested separately with `ensure`. - const delimiters = /\/|\\|,/g; + const scopeSegments = parsed.scope.split(delimiters); const negated = when === 'never'; diff --git a/@commitlint/types/src/rules.ts b/@commitlint/types/src/rules.ts index efc69d0cc6..4958925871 100644 --- a/@commitlint/types/src/rules.ts +++ b/@commitlint/types/src/rules.ts @@ -83,9 +83,16 @@ export type LengthRuleConfig = RuleConfig< V, number >; +export interface EnumRuleExtendableOptions { + values: string[]; + [k: string]: any; +} + +export type EnumRuleOptions = EnumRuleExtendableOptions | string[]; + export type EnumRuleConfig = RuleConfig< V, - string[] + EnumRuleOptions >; export type RulesConfig = { diff --git a/docs/reference-rules.md b/docs/reference-rules.md index 06454f1e3c..392f54f432 100644 --- a/docs/reference-rules.md +++ b/docs/reference-rules.md @@ -190,6 +190,13 @@ Infinity ``` [] ``` + or if you want to customize delimiter regex for [multiple scopes](https://commitlint.js.org/#/concepts-commit-conventions?id=multiple-scopes) + ``` + { + values: [], + delimiter: /,/g + } + ``` #### scope-case