Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support multiple scopes and multiple cases & fix sentence-case is not consistent with commitlint/cli #2806

Merged
merged 13 commits into from Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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']) ?? {};
}