Skip to content

Commit 2c71a7e

Browse files
authored
support multiple scopes and multiple cases & fix sentence-case is not consistent with commitlint/cli (#2806)
* refactor(cz-commitlint): set "keywords" and "bugs" in package.json * docs(cz-commitlint): make the introduction more readable * feat(cz-commitlint): support multiple scopes BREAKING CHANGE: add prompt.settings configuration Closes #2782 * fix(cz-commitlint): support multiple cases & fix to sentence-case * style(cz-commitlint): prettieer fix & remove meaningless comment
1 parent 7199826 commit 2c71a7e

13 files changed

+279
-50
lines changed

@commitlint/cz-commitlint/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
}
3838
},
3939
"dependencies": {
40+
"@commitlint/ensure": "^13.2.0",
4041
"@commitlint/load": "^13.2.1",
4142
"@commitlint/types": "^13.2.0",
4243
"chalk": "^4.1.0",
@@ -48,6 +49,7 @@
4849
"inquirer": "^8.0.0"
4950
},
5051
"devDependencies": {
51-
"@types/inquirer": "^8.0.0"
52+
"@types/inquirer": "^8.0.0",
53+
"commitizen": "^4.2.4"
5254
}
5355
}

@commitlint/cz-commitlint/src/Question.test.ts

+52-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ const QUESTION_CONFIG = {
1515
messages: MESSAGES,
1616
};
1717

18+
const caseFn = (input: string | string[], delimiter?: string) =>
19+
(Array.isArray(input) ? input : [input])
20+
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
21+
.join(delimiter);
22+
1823
describe('name', () => {
1924
test('should throw error when name is not a meaningful string', () => {
2025
expect(
@@ -47,7 +52,7 @@ describe('name', () => {
4752
});
4853

4954
describe('type', () => {
50-
test('should return "list" type when enumList is array', () => {
55+
test('should return "list" type when enumList is array and multipleSelectDefaultDelimiter is undefined', () => {
5156
const question = new Question('scope', {
5257
...QUESTION_CONFIG,
5358
enumList: ['cli', 'core'],
@@ -57,6 +62,17 @@ describe('type', () => {
5762
expect(question).not.toHaveProperty('transformer');
5863
});
5964

65+
test('should return "checkbox" type when enumList is array and multipleSelectDefaultDelimiter is defined', () => {
66+
const question = new Question('scope', {
67+
...QUESTION_CONFIG,
68+
enumList: ['cli', 'core'],
69+
multipleSelectDefaultDelimiter: ',',
70+
}).question;
71+
expect(question).toHaveProperty('type', 'checkbox');
72+
expect(question).toHaveProperty('choices', ['cli', 'core']);
73+
expect(question).not.toHaveProperty('transformer');
74+
});
75+
6076
test('should contain "skip" list item when enumList is array and skip is true', () => {
6177
const question = new Question('scope', {
6278
...QUESTION_CONFIG,
@@ -184,13 +200,46 @@ describe('filter', () => {
184200
test('should auto fix case and full-stop', () => {
185201
const question = new Question('body', {
186202
...QUESTION_CONFIG,
187-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
203+
caseFn,
188204
fullStopFn: (input: string) => input + '!',
189205
}).question;
190206

191207
expect(question.filter?.('xxxx', {})).toBe('Xxxx!');
192208
});
193209

210+
test('should transform each item with same case when input is array', () => {
211+
const question = new Question('body', {
212+
...QUESTION_CONFIG,
213+
caseFn,
214+
fullStopFn: (input: string) => input + '!',
215+
}).question;
216+
217+
expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx,Yyyy!');
218+
});
219+
220+
test('should concat items with multipleSelectDefaultDelimiter when input is array', () => {
221+
const question = new Question('body', {
222+
...QUESTION_CONFIG,
223+
caseFn,
224+
fullStopFn: (input: string) => input + '!',
225+
multipleSelectDefaultDelimiter: '|',
226+
}).question;
227+
228+
expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx|Yyyy!');
229+
});
230+
231+
test('should split the string to items when multipleValueDelimiters is defined', () => {
232+
const question = new Question('body', {
233+
...QUESTION_CONFIG,
234+
caseFn,
235+
fullStopFn: (input: string) => input + '!',
236+
multipleValueDelimiters: /,|\|/g,
237+
}).question;
238+
239+
expect(question.filter?.('xxxx,yyyy|zzzz', {})).toBe('Xxxx,Yyyy|Zzzz!');
240+
expect(question.filter?.('xxxx-yyyy-zzzz', {})).toBe('Xxxx-yyyy-zzzz!');
241+
});
242+
194243
test('should works well when does not pass caseFn/fullStopFn', () => {
195244
const question = new Question('body', {
196245
...QUESTION_CONFIG,
@@ -252,7 +301,7 @@ describe('transformer', () => {
252301
test('should auto transform case and full-stop', () => {
253302
const question = new Question('body', {
254303
...QUESTION_CONFIG,
255-
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
304+
caseFn,
256305
fullStopFn: (input: string) => input + '!',
257306
}).question;
258307

@commitlint/cz-commitlint/src/Question.ts

+33-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export type QuestionConfig = {
1616
name: string;
1717
value: string;
1818
}> | null;
19+
multipleValueDelimiters?: RegExp;
20+
multipleSelectDefaultDelimiter?: string;
1921
fullStopFn?: FullStopFn;
2022
caseFn?: CaseFn;
2123
};
@@ -29,6 +31,8 @@ export default class Question {
2931
private title: string;
3032
private caseFn: CaseFn;
3133
private fullStopFn: FullStopFn;
34+
private multipleValueDelimiters?: RegExp;
35+
private multipleSelectDefaultDelimiter?: string;
3236
constructor(
3337
name: PromptName,
3438
{
@@ -42,6 +46,8 @@ export default class Question {
4246
caseFn,
4347
maxLength,
4448
minLength,
49+
multipleValueDelimiters,
50+
multipleSelectDefaultDelimiter,
4551
}: QuestionConfig
4652
) {
4753
if (!name || typeof name !== 'string')
@@ -53,11 +59,16 @@ export default class Question {
5359
this.title = title ?? '';
5460
this.skip = skip ?? false;
5561
this.fullStopFn = fullStopFn ?? ((_: string) => _);
56-
this.caseFn = caseFn ?? ((_: string) => _);
62+
this.caseFn =
63+
caseFn ??
64+
((input: string | string[], delimiter?: string) =>
65+
Array.isArray(input) ? input.join(delimiter) : input);
66+
this.multipleValueDelimiters = multipleValueDelimiters;
67+
this.multipleSelectDefaultDelimiter = multipleSelectDefaultDelimiter;
5768

5869
if (enumList && Array.isArray(enumList)) {
5970
this._question = {
60-
type: 'list',
71+
type: multipleSelectDefaultDelimiter ? 'checkbox' : 'list',
6172
choices: skip
6273
? [
6374
...enumList,
@@ -140,8 +151,25 @@ export default class Question {
140151
return true;
141152
}
142153

143-
protected filter(input: string): string {
144-
return this.caseFn(this.fullStopFn(input));
154+
protected filter(input: string | string[]): string {
155+
let toCased;
156+
if (Array.isArray(input)) {
157+
toCased = this.caseFn(input, this.multipleSelectDefaultDelimiter);
158+
} else if (this.multipleValueDelimiters) {
159+
const segments = input.split(this.multipleValueDelimiters);
160+
const casedString = this.caseFn(segments, ',');
161+
const casedSegments = casedString.split(',');
162+
toCased = input.replace(
163+
new RegExp(`[^${this.multipleValueDelimiters.source}]+`, 'g'),
164+
(segment) => {
165+
return casedSegments[segments.indexOf(segment)];
166+
}
167+
);
168+
} else {
169+
toCased = this.caseFn(input);
170+
}
171+
172+
return this.fullStopFn(toCased);
145173
}
146174

147175
protected transformer(input: string, _answers: Answers): string {
@@ -154,7 +182,7 @@ export default class Question {
154182
output.length <= this.maxLength && output.length >= this.minLength
155183
? chalk.green
156184
: chalk.red;
157-
return color('(' + output.length + ') ' + input);
185+
return color('(' + output.length + ') ' + output);
158186
}
159187

160188
protected decorateMessage(_answers: Answers): string {

@commitlint/cz-commitlint/src/SectionHeader.test.ts

+35-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
import {RuleConfigSeverity} from '@commitlint/types';
2-
import {combineCommitMessage, getQuestions} from './SectionHeader';
2+
import {
3+
combineCommitMessage,
4+
getQuestions,
5+
getQuestionConfig,
6+
} from './SectionHeader';
37
import {setPromptConfig} from './store/prompts';
48
import {setRules} from './store/rules';
9+
10+
beforeEach(() => {
11+
setRules({});
12+
setPromptConfig({});
13+
});
514
describe('getQuestions', () => {
615
test("should contain 'type','scope','subject'", () => {
716
const questions = getQuestions();
@@ -36,6 +45,31 @@ describe('getQuestions', () => {
3645
});
3746
});
3847

48+
describe('getQuestionConfig', () => {
49+
test("should 'scope' supports multiple items separated with ',\\/'", () => {
50+
const config = getQuestionConfig('scope');
51+
expect(config).toEqual(
52+
expect.objectContaining({
53+
multipleValueDelimiters: /\/|\\|,/g,
54+
})
55+
);
56+
});
57+
58+
test("should 'scope' supports multiple select separated with settings.scopeEnumSeparator", () => {
59+
setPromptConfig({
60+
settings: {
61+
scopeEnumSeparator: '/',
62+
},
63+
});
64+
const config = getQuestionConfig('scope');
65+
expect(config).toEqual(
66+
expect.objectContaining({
67+
multipleSelectDefaultDelimiter: '/',
68+
})
69+
);
70+
});
71+
});
72+
3973
describe('combineCommitMessage', () => {
4074
test('should return correct string when type,scope,subject are not empty', () => {
4175
const commitMessage = combineCommitMessage({

@commitlint/cz-commitlint/src/SectionHeader.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {PromptName, RuleField} from '@commitlint/types';
22
import {Answers, DistinctQuestion} from 'inquirer';
33
import Question, {QuestionConfig} from './Question';
44
import getRuleQuestionConfig from './services/getRuleQuestionConfig';
5+
import {getPromptSettings} from './store/prompts';
56

67
export class HeaderQuestion extends Question {
78
headerMaxLength: number;
@@ -47,8 +48,13 @@ export function getQuestions(): Array<DistinctQuestion> {
4748
}
4849

4950
headerRuleFields.forEach((name) => {
50-
const questionConfig = getRuleQuestionConfig(name);
51+
const questionConfig = getQuestionConfig(name);
5152
if (questionConfig) {
53+
if (name === 'scope') {
54+
questionConfig.multipleSelectDefaultDelimiter =
55+
getPromptSettings()['scopeEnumSeparator'];
56+
questionConfig.multipleValueDelimiters = /\/|\\|,/g;
57+
}
5258
const instance = new HeaderQuestion(
5359
name,
5460
questionConfig,
@@ -60,3 +66,19 @@ export function getQuestions(): Array<DistinctQuestion> {
6066
});
6167
return questions;
6268
}
69+
70+
export function getQuestionConfig(
71+
name: RuleField
72+
): ReturnType<typeof getRuleQuestionConfig> {
73+
const questionConfig = getRuleQuestionConfig(name);
74+
75+
if (questionConfig) {
76+
if (name === 'scope') {
77+
questionConfig.multipleSelectDefaultDelimiter =
78+
getPromptSettings()['scopeEnumSeparator'];
79+
questionConfig.multipleValueDelimiters = /\/|\\|,/g;
80+
}
81+
}
82+
83+
return questionConfig;
84+
}

@commitlint/cz-commitlint/src/store/defaultPromptConfigs.ts

+3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export default {
2+
settings: {
3+
scopeEnumSeparator: ',',
4+
},
25
messages: {
36
skip: '(press enter to skip)',
47
max: '(max %d chars)',

@commitlint/cz-commitlint/src/store/prompts.test.ts

+24
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import * as prompts from './prompts';
22

33
let getPromptQuestions: typeof prompts.getPromptQuestions;
44
let getPromptMessages: typeof prompts.getPromptMessages;
5+
let getPromptSettings: typeof prompts.getPromptSettings;
56
let setPromptConfig: typeof prompts.setPromptConfig;
67

78
beforeEach(() => {
89
jest.resetModules();
10+
getPromptSettings = require('./prompts').getPromptSettings;
911
getPromptMessages = require('./prompts').getPromptMessages;
1012
getPromptQuestions = require('./prompts').getPromptQuestions;
1113
setPromptConfig = require('./prompts').setPromptConfig;
@@ -106,4 +108,26 @@ describe('setPromptConfig', () => {
106108
});
107109
expect(getPromptMessages()).toEqual(initialMessages);
108110
});
111+
112+
test('should settings scopeEnumSeparator be set when value is ",\\/"', () => {
113+
setPromptConfig({
114+
settings: {
115+
scopeEnumSeparator: '/',
116+
},
117+
});
118+
expect(getPromptSettings()).toEqual({
119+
scopeEnumSeparator: '/',
120+
});
121+
122+
const processExit = jest
123+
.spyOn(process, 'exit')
124+
.mockImplementation(() => undefined as never);
125+
setPromptConfig({
126+
settings: {
127+
scopeEnumSeparator: '-',
128+
},
129+
});
130+
expect(processExit).toHaveBeenCalledWith(1);
131+
processExit.mockClear();
132+
});
109133
});

@commitlint/cz-commitlint/src/store/prompts.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const store: {
1111
};
1212

1313
export function setPromptConfig(newPromptConfig: UserPromptConfig): void {
14-
const {messages, questions} = newPromptConfig;
14+
const {settings, messages, questions} = newPromptConfig;
1515
if (messages) {
1616
const requiredMessageKeys = Object.keys(defaultPromptConfigs.messages);
1717
requiredMessageKeys.forEach((key: string) => {
@@ -25,6 +25,22 @@ export function setPromptConfig(newPromptConfig: UserPromptConfig): void {
2525
if (questions && isPlainObject(questions)) {
2626
store[storeKey]['questions'] = questions;
2727
}
28+
29+
if (settings && isPlainObject(settings)) {
30+
if (
31+
settings['scopeEnumSeparator'] &&
32+
!/^\/|\\|,$/.test(settings['scopeEnumSeparator'])
33+
) {
34+
console.log(
35+
`prompt.settings.scopeEnumSeparator must be one of ',', '\\', '/'.`
36+
);
37+
process.exit(1);
38+
}
39+
store[storeKey]['settings'] = {
40+
...defaultPromptConfigs.settings,
41+
...settings,
42+
};
43+
}
2844
}
2945

3046
export function getPromptMessages(): Readonly<PromptConfig['messages']> {
@@ -34,3 +50,7 @@ export function getPromptMessages(): Readonly<PromptConfig['messages']> {
3450
export function getPromptQuestions(): Readonly<PromptConfig['questions']> {
3551
return (store[storeKey] && store[storeKey]['questions']) ?? {};
3652
}
53+
54+
export function getPromptSettings(): Readonly<PromptConfig['settings']> {
55+
return (store[storeKey] && store[storeKey]['settings']) ?? {};
56+
}

0 commit comments

Comments
 (0)