Skip to content

Commit

Permalink
support multiple scopes and multiple cases & fix sentence-case is not…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
curly210102 committed Oct 26, 2021
1 parent 7199826 commit 2c71a7e
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 50 deletions.
4 changes: 3 additions & 1 deletion @commitlint/cz-commitlint/package.json
Expand Up @@ -37,6 +37,7 @@
}
},
"dependencies": {
"@commitlint/ensure": "^13.2.0",
"@commitlint/load": "^13.2.1",
"@commitlint/types": "^13.2.0",
"chalk": "^4.1.0",
Expand All @@ -48,6 +49,7 @@
"inquirer": "^8.0.0"
},
"devDependencies": {
"@types/inquirer": "^8.0.0"
"@types/inquirer": "^8.0.0",
"commitizen": "^4.2.4"
}
}
55 changes: 52 additions & 3 deletions @commitlint/cz-commitlint/src/Question.test.ts
Expand Up @@ -15,6 +15,11 @@ const QUESTION_CONFIG = {
messages: MESSAGES,
};

const caseFn = (input: string | string[], delimiter?: string) =>
(Array.isArray(input) ? input : [input])
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
.join(delimiter);

describe('name', () => {
test('should throw error when name is not a meaningful string', () => {
expect(
Expand Down Expand Up @@ -47,7 +52,7 @@ describe('name', () => {
});

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

test('should return "checkbox" type when enumList is array and multipleSelectDefaultDelimiter is defined', () => {
const question = new Question('scope', {
...QUESTION_CONFIG,
enumList: ['cli', 'core'],
multipleSelectDefaultDelimiter: ',',
}).question;
expect(question).toHaveProperty('type', 'checkbox');
expect(question).toHaveProperty('choices', ['cli', 'core']);
expect(question).not.toHaveProperty('transformer');
});

test('should contain "skip" list item when enumList is array and skip is true', () => {
const question = new Question('scope', {
...QUESTION_CONFIG,
Expand Down Expand Up @@ -184,13 +200,46 @@ describe('filter', () => {
test('should auto fix case and full-stop', () => {
const question = new Question('body', {
...QUESTION_CONFIG,
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
caseFn,
fullStopFn: (input: string) => input + '!',
}).question;

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

test('should transform each item with same case when input is array', () => {
const question = new Question('body', {
...QUESTION_CONFIG,
caseFn,
fullStopFn: (input: string) => input + '!',
}).question;

expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx,Yyyy!');
});

test('should concat items with multipleSelectDefaultDelimiter when input is array', () => {
const question = new Question('body', {
...QUESTION_CONFIG,
caseFn,
fullStopFn: (input: string) => input + '!',
multipleSelectDefaultDelimiter: '|',
}).question;

expect(question.filter?.(['xxxx', 'yyyy'], {})).toBe('Xxxx|Yyyy!');
});

test('should split the string to items when multipleValueDelimiters is defined', () => {
const question = new Question('body', {
...QUESTION_CONFIG,
caseFn,
fullStopFn: (input: string) => input + '!',
multipleValueDelimiters: /,|\|/g,
}).question;

expect(question.filter?.('xxxx,yyyy|zzzz', {})).toBe('Xxxx,Yyyy|Zzzz!');
expect(question.filter?.('xxxx-yyyy-zzzz', {})).toBe('Xxxx-yyyy-zzzz!');
});

test('should works well when does not pass caseFn/fullStopFn', () => {
const question = new Question('body', {
...QUESTION_CONFIG,
Expand Down Expand Up @@ -252,7 +301,7 @@ describe('transformer', () => {
test('should auto transform case and full-stop', () => {
const question = new Question('body', {
...QUESTION_CONFIG,
caseFn: (input: string) => input[0].toUpperCase() + input.slice(1),
caseFn,
fullStopFn: (input: string) => input + '!',
}).question;

Expand Down
38 changes: 33 additions & 5 deletions @commitlint/cz-commitlint/src/Question.ts
Expand Up @@ -16,6 +16,8 @@ export type QuestionConfig = {
name: string;
value: string;
}> | null;
multipleValueDelimiters?: RegExp;
multipleSelectDefaultDelimiter?: string;
fullStopFn?: FullStopFn;
caseFn?: CaseFn;
};
Expand All @@ -29,6 +31,8 @@ export default class Question {
private title: string;
private caseFn: CaseFn;
private fullStopFn: FullStopFn;
private multipleValueDelimiters?: RegExp;
private multipleSelectDefaultDelimiter?: string;
constructor(
name: PromptName,
{
Expand All @@ -42,6 +46,8 @@ export default class Question {
caseFn,
maxLength,
minLength,
multipleValueDelimiters,
multipleSelectDefaultDelimiter,
}: QuestionConfig
) {
if (!name || typeof name !== 'string')
Expand All @@ -53,11 +59,16 @@ export default class Question {
this.title = title ?? '';
this.skip = skip ?? false;
this.fullStopFn = fullStopFn ?? ((_: string) => _);
this.caseFn = caseFn ?? ((_: string) => _);
this.caseFn =
caseFn ??
((input: string | string[], delimiter?: string) =>
Array.isArray(input) ? input.join(delimiter) : input);
this.multipleValueDelimiters = multipleValueDelimiters;
this.multipleSelectDefaultDelimiter = multipleSelectDefaultDelimiter;

if (enumList && Array.isArray(enumList)) {
this._question = {
type: 'list',
type: multipleSelectDefaultDelimiter ? 'checkbox' : 'list',
choices: skip
? [
...enumList,
Expand Down Expand Up @@ -140,8 +151,25 @@ export default class Question {
return true;
}

protected filter(input: string): string {
return this.caseFn(this.fullStopFn(input));
protected filter(input: string | string[]): string {
let toCased;
if (Array.isArray(input)) {
toCased = this.caseFn(input, this.multipleSelectDefaultDelimiter);
} else if (this.multipleValueDelimiters) {
const segments = input.split(this.multipleValueDelimiters);
const casedString = this.caseFn(segments, ',');
const casedSegments = casedString.split(',');
toCased = input.replace(
new RegExp(`[^${this.multipleValueDelimiters.source}]+`, 'g'),
(segment) => {
return casedSegments[segments.indexOf(segment)];
}
);
} else {
toCased = this.caseFn(input);
}

return this.fullStopFn(toCased);
}

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

protected decorateMessage(_answers: Answers): string {
Expand Down
36 changes: 35 additions & 1 deletion @commitlint/cz-commitlint/src/SectionHeader.test.ts
@@ -1,7 +1,16 @@
import {RuleConfigSeverity} from '@commitlint/types';
import {combineCommitMessage, getQuestions} from './SectionHeader';
import {
combineCommitMessage,
getQuestions,
getQuestionConfig,
} from './SectionHeader';
import {setPromptConfig} from './store/prompts';
import {setRules} from './store/rules';

beforeEach(() => {
setRules({});
setPromptConfig({});
});
describe('getQuestions', () => {
test("should contain 'type','scope','subject'", () => {
const questions = getQuestions();
Expand Down Expand Up @@ -36,6 +45,31 @@ describe('getQuestions', () => {
});
});

describe('getQuestionConfig', () => {
test("should 'scope' supports multiple items separated with ',\\/'", () => {
const config = getQuestionConfig('scope');
expect(config).toEqual(
expect.objectContaining({
multipleValueDelimiters: /\/|\\|,/g,
})
);
});

test("should 'scope' supports multiple select separated with settings.scopeEnumSeparator", () => {
setPromptConfig({
settings: {
scopeEnumSeparator: '/',
},
});
const config = getQuestionConfig('scope');
expect(config).toEqual(
expect.objectContaining({
multipleSelectDefaultDelimiter: '/',
})
);
});
});

describe('combineCommitMessage', () => {
test('should return correct string when type,scope,subject are not empty', () => {
const commitMessage = combineCommitMessage({
Expand Down
24 changes: 23 additions & 1 deletion @commitlint/cz-commitlint/src/SectionHeader.ts
Expand Up @@ -2,6 +2,7 @@ import {PromptName, RuleField} from '@commitlint/types';
import {Answers, DistinctQuestion} from 'inquirer';
import Question, {QuestionConfig} from './Question';
import getRuleQuestionConfig from './services/getRuleQuestionConfig';
import {getPromptSettings} from './store/prompts';

export class HeaderQuestion extends Question {
headerMaxLength: number;
Expand Down Expand Up @@ -47,8 +48,13 @@ export function getQuestions(): Array<DistinctQuestion> {
}

headerRuleFields.forEach((name) => {
const questionConfig = getRuleQuestionConfig(name);
const questionConfig = getQuestionConfig(name);
if (questionConfig) {
if (name === 'scope') {
questionConfig.multipleSelectDefaultDelimiter =
getPromptSettings()['scopeEnumSeparator'];
questionConfig.multipleValueDelimiters = /\/|\\|,/g;
}
const instance = new HeaderQuestion(
name,
questionConfig,
Expand All @@ -60,3 +66,19 @@ export function getQuestions(): Array<DistinctQuestion> {
});
return questions;
}

export function getQuestionConfig(
name: RuleField
): ReturnType<typeof getRuleQuestionConfig> {
const questionConfig = getRuleQuestionConfig(name);

if (questionConfig) {
if (name === 'scope') {
questionConfig.multipleSelectDefaultDelimiter =
getPromptSettings()['scopeEnumSeparator'];
questionConfig.multipleValueDelimiters = /\/|\\|,/g;
}
}

return questionConfig;
}
3 changes: 3 additions & 0 deletions @commitlint/cz-commitlint/src/store/defaultPromptConfigs.ts
@@ -1,4 +1,7 @@
export default {
settings: {
scopeEnumSeparator: ',',
},
messages: {
skip: '(press enter to skip)',
max: '(max %d chars)',
Expand Down
24 changes: 24 additions & 0 deletions @commitlint/cz-commitlint/src/store/prompts.test.ts
Expand Up @@ -2,10 +2,12 @@ import * as prompts from './prompts';

let getPromptQuestions: typeof prompts.getPromptQuestions;
let getPromptMessages: typeof prompts.getPromptMessages;
let getPromptSettings: typeof prompts.getPromptSettings;
let setPromptConfig: typeof prompts.setPromptConfig;

beforeEach(() => {
jest.resetModules();
getPromptSettings = require('./prompts').getPromptSettings;
getPromptMessages = require('./prompts').getPromptMessages;
getPromptQuestions = require('./prompts').getPromptQuestions;
setPromptConfig = require('./prompts').setPromptConfig;
Expand Down Expand Up @@ -106,4 +108,26 @@ describe('setPromptConfig', () => {
});
expect(getPromptMessages()).toEqual(initialMessages);
});

test('should settings scopeEnumSeparator be set when value is ",\\/"', () => {
setPromptConfig({
settings: {
scopeEnumSeparator: '/',
},
});
expect(getPromptSettings()).toEqual({
scopeEnumSeparator: '/',
});

const processExit = jest
.spyOn(process, 'exit')
.mockImplementation(() => undefined as never);
setPromptConfig({
settings: {
scopeEnumSeparator: '-',
},
});
expect(processExit).toHaveBeenCalledWith(1);
processExit.mockClear();
});
});
22 changes: 21 additions & 1 deletion @commitlint/cz-commitlint/src/store/prompts.ts
Expand Up @@ -11,7 +11,7 @@ const store: {
};

export function setPromptConfig(newPromptConfig: UserPromptConfig): void {
const {messages, questions} = newPromptConfig;
const {settings, messages, questions} = newPromptConfig;
if (messages) {
const requiredMessageKeys = Object.keys(defaultPromptConfigs.messages);
requiredMessageKeys.forEach((key: string) => {
Expand All @@ -25,6 +25,22 @@ export function setPromptConfig(newPromptConfig: UserPromptConfig): void {
if (questions && isPlainObject(questions)) {
store[storeKey]['questions'] = questions;
}

if (settings && isPlainObject(settings)) {
if (
settings['scopeEnumSeparator'] &&
!/^\/|\\|,$/.test(settings['scopeEnumSeparator'])
) {
console.log(
`prompt.settings.scopeEnumSeparator must be one of ',', '\\', '/'.`
);
process.exit(1);
}
store[storeKey]['settings'] = {
...defaultPromptConfigs.settings,
...settings,
};
}
}

export function getPromptMessages(): Readonly<PromptConfig['messages']> {
Expand All @@ -34,3 +50,7 @@ export function getPromptMessages(): Readonly<PromptConfig['messages']> {
export function getPromptQuestions(): Readonly<PromptConfig['questions']> {
return (store[storeKey] && store[storeKey]['questions']) ?? {};
}

export function getPromptSettings(): Readonly<PromptConfig['settings']> {
return (store[storeKey] && store[storeKey]['settings']) ?? {};
}

0 comments on commit 2c71a7e

Please sign in to comment.