From 02179c8c4874fae1e5db9bd0758e0c982d06a144 Mon Sep 17 00:00:00 2001 From: Armano Date: Mon, 4 Jan 2021 19:27:27 +0100 Subject: [PATCH 1/8] feat(prompt): rewrite codebase to use inquirer --- @commitlint/prompt-cli/cli.js | 6 +- @commitlint/prompt-cli/package.json | 1 + @commitlint/prompt/package.json | 9 +- @commitlint/prompt/src/index.ts | 13 +- @commitlint/prompt/src/input.ts | 65 ++-- .../prompt/src/inquirer/InputCustomPrompt.ts | 150 ++++++++++ @commitlint/prompt/src/inquirer/inquirer.d.ts | 24 ++ @commitlint/prompt/src/library/format.test.ts | 55 ++++ @commitlint/prompt/src/library/format.ts | 28 +- .../prompt/src/library/get-prompt.test.ts | 100 ------- @commitlint/prompt/src/library/get-prompt.ts | 283 +++++------------- @commitlint/prompt/src/library/types.ts | 35 +-- @commitlint/prompt/tsconfig.json | 2 +- yarn.lock | 182 ++--------- 14 files changed, 385 insertions(+), 568 deletions(-) create mode 100644 @commitlint/prompt/src/inquirer/InputCustomPrompt.ts create mode 100644 @commitlint/prompt/src/inquirer/inquirer.d.ts create mode 100644 @commitlint/prompt/src/library/format.test.ts delete mode 100644 @commitlint/prompt/src/library/get-prompt.test.ts diff --git a/@commitlint/prompt-cli/cli.js b/@commitlint/prompt-cli/cli.js index 16f957e07a..716bb4dde7 100755 --- a/@commitlint/prompt-cli/cli.js +++ b/@commitlint/prompt-cli/cli.js @@ -1,10 +1,8 @@ #!/usr/bin/env node const execa = require('execa'); +const inquirer = require('inquirer'); const {prompter} = require('@commitlint/prompt'); -const _ = undefined; -const prompt = () => prompter(_, commit); - main().catch((err) => { setTimeout(() => { throw err; @@ -21,7 +19,7 @@ function main() { process.exit(1); } }) - .then(() => prompt()); + .then(() => prompter(inquirer, commit)); } function isStageEmpty() { diff --git a/@commitlint/prompt-cli/package.json b/@commitlint/prompt-cli/package.json index 9e1c05946c..3ee574cbc2 100644 --- a/@commitlint/prompt-cli/package.json +++ b/@commitlint/prompt-cli/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@commitlint/prompt": "^11.0.0", + "inquirer": "^6.5.2", "execa": "^5.0.0" }, "gitHead": "cb565dfcca3128380b9b3dc274aedbcae34ce5ca" diff --git a/@commitlint/prompt/package.json b/@commitlint/prompt/package.json index 9266b75a7d..f39215dc43 100644 --- a/@commitlint/prompt/package.json +++ b/@commitlint/prompt/package.json @@ -37,14 +37,15 @@ }, "devDependencies": { "@commitlint/utils": "^11.0.0", - "commitizen": "4.2.2" + "@types/inquirer": "^6.5.0", + "@commitlint/types": "^11.0.0", + "commitizen": "4.2.2", + "inquirer": "^6.5.2" }, "dependencies": { "@commitlint/load": "^11.0.0", "chalk": "^4.0.0", - "lodash": "^4.17.19", - "throat": "^5.0.0", - "vorpal": "^1.12.0" + "lodash": "^4.17.19" }, "gitHead": "cb565dfcca3128380b9b3dc274aedbcae34ce5ca" } diff --git a/@commitlint/prompt/src/index.ts b/@commitlint/prompt/src/index.ts index 4a6261422b..408d581b44 100644 --- a/@commitlint/prompt/src/index.ts +++ b/@commitlint/prompt/src/index.ts @@ -1,17 +1,18 @@ -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import vorpal from 'vorpal'; +import inquirer from 'inquirer'; import input from './input'; type Commit = (input: string) => void; /** * Entry point for commitizen - * @param _ inquirer instance passed by commitizen, unused + * @param cz inquirer instance passed by commitizen * @param commit callback to execute with complete commit message * @return generated commit message */ -export async function prompter(_: unknown, commit: Commit): Promise { - const message = await input(vorpal); +export async function prompter( + cz: typeof inquirer, + commit: Commit +): Promise { + const message = await input(cz.prompt); commit(message); } diff --git a/@commitlint/prompt/src/input.ts b/@commitlint/prompt/src/input.ts index bb34fcf62f..4e4537dcc8 100644 --- a/@commitlint/prompt/src/input.ts +++ b/@commitlint/prompt/src/input.ts @@ -1,13 +1,13 @@ import load from '@commitlint/load'; -import throat from 'throat'; +import {DistinctQuestion, PromptModule} from 'inquirer'; import format from './library/format'; import getPrompt from './library/get-prompt'; import settings from './settings'; -import {InputSetting, Prompter, Result} from './library/types'; -import {getHasName, getMaxLength, getRules} from './library/utils'; +import {InputSetting, Result} from './library/types'; -export default input; +import {getHasName, getMaxLength, getRules} from './library/utils'; +import InputCustomPrompt from './inquirer/InputCustomPrompt'; /** * Get user input by interactive prompt based on @@ -15,15 +15,7 @@ export default input; * @param prompter * @return commit message */ -async function input(prompter: () => Prompter): Promise { - const results: Result = { - type: null, - scope: null, - subject: null, - body: null, - footer: null, - }; - +export default async function input(prompter: PromptModule): Promise { const {rules} = await load(); const parts = ['type', 'scope', 'subject', 'body', 'footer'] as const; const headerParts = ['type', 'scope', 'subject']; @@ -33,31 +25,28 @@ async function input(prompter: () => Prompter): Promise { ); const maxLength = getMaxLength(headerLengthRule); - await Promise.all( - parts.map( - throat(1, async (input) => { - const inputRules = getRules(input, rules); - const inputSettings: InputSetting = settings[input]; - - if (headerParts.includes(input) && maxLength < Infinity) { - inputSettings.header = { - length: maxLength, - }; - } - - results[input] = await getPrompt(input, { - rules: inputRules, - settings: inputSettings, - results, - prompter, - }); - }) - ) - ).catch((err) => { + try { + const questions: DistinctQuestion[] = []; + prompter.registerPrompt('input-custom', InputCustomPrompt); + + for (const input of parts) { + const inputSettings: InputSetting = settings[input]; + const inputRules = getRules(input, rules); + if (headerParts.includes(input) && maxLength < Infinity) { + inputSettings.header = { + length: maxLength, + }; + } + const question = getPrompt(input, inputRules, inputSettings); + if (question) { + questions.push(question); + } + } + + const results = await prompter(questions); + return format(results); + } catch (err) { console.error(err); return ''; - }); - - // Return the results - return format(results); + } } diff --git a/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts b/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts new file mode 100644 index 0000000000..46d20a202b --- /dev/null +++ b/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts @@ -0,0 +1,150 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// +import {Interface as ReadlineInterface, Key} from 'readline'; + +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import InputPrompt from 'inquirer/lib/prompts/input'; +import observe from 'inquirer/lib/utils/events'; +import type {Subscription} from 'rxjs/internal/Subscription'; + +import Answers = inquirer.Answers; +import InputCustomOptions = inquirer.InputCustomOptions; +import Validator = inquirer.Validator; +import SuccessfulPromptStateData = inquirer.prompts.SuccessfulPromptStateData; + +interface KeyDescriptor { + value: string; + key: Key; +} + +export default class InputCustomPrompt< + TQuestion extends InputCustomOptions = InputCustomOptions +> extends InputPrompt { + private lineSubscription: Subscription; + private readonly tabCompletion: string[]; + + constructor( + question: TQuestion, + readLine: ReadlineInterface, + answers: Answers + ) { + super(question, readLine, answers); + + if (this.opt.log) { + this.rl.write(this.opt.log(answers)); + } + + if (!this.opt.maxLength) { + this.throwParamError('maxLength'); + } + + const events = observe(this.rl); + this.lineSubscription = events.keypress.subscribe( + this.onKeyPress2.bind(this) + ); + this.tabCompletion = (this.opt.tabCompletion || []) + .map((item) => item.value) + .sort((a, b) => a.localeCompare(b)); + + this.opt.validate = this.extendedValidate(this.opt.validate); + } + + onEnd(state: SuccessfulPromptStateData): void { + this.lineSubscription.unsubscribe(); + super.onEnd(state); + } + + extendedValidate(validate?: Validator): Validator { + return (input, answers) => { + if (input.length > this.opt.maxLength(answers)) { + return 'Input contains too many characters!'; + } + if (this.opt.required && input.trim().length === 0) { + // Show help if enum is defined and input may not be empty + return `⚠ ${chalk.bold(this.opt.name)} may not be empty.`; + } + + if ( + input.length > 0 && + this.tabCompletion.length > 0 && + !this.tabCompletion.includes(input) + ) { + return `⚠ ${chalk.bold( + this.opt.name + )} must be one of ${this.tabCompletion.join(', ')}.`; + } + + if (validate) { + return validate(input, answers); + } + return true; + }; + } + + /** + * @see https://nodejs.org/api/readline.html#readline_rl_write_data_key + * @see https://nodejs.org/api/readline.html#readline_rl_line + */ + updateLine(line: string): void { + /* eslint-disable @typescript-eslint/ban-ts-comment */ + // @ts-ignore + this.rl.line = line; + // @ts-ignore + this.rl.write(null, {ctrl: true, name: 'e'}); + } + + onKeyPress2(e: KeyDescriptor): void { + if (e.key.name === 'tab' && this.tabCompletion.length > 0) { + let line = this.rl.line.trim(); + if (line.length > 0) { + for (const item of this.tabCompletion) { + if (item.startsWith(line) && item !== line) { + line = item; + break; + } + } + } + this.updateLine(line); + } + } + + measureInput(input: string): number { + if (this.opt.filter) { + return this.opt.filter(input).length; + } + return input.length; + } + + render(error?: string): void { + const answered = this.status === 'answered'; + + let bottomContent = ''; + let message = this.getQuestion(); + const length = this.measureInput(this.rl.line); + + if (answered) { + message += chalk.cyan(this.answer); + } else if (this.opt.transformer) { + message += this.opt.transformer(this.rl.line, this.answers, {}); + } + + if (error) { + bottomContent = chalk.red('>> ') + error; + } else if (!answered) { + const maxLength = this.opt.maxLength(this.answers); + if (maxLength < Infinity) { + const lengthRemaining = maxLength - length; + const color = + lengthRemaining <= 5 + ? chalk.red + : lengthRemaining <= 10 + ? chalk.yellow + : chalk.grey; + bottomContent = color(`${lengthRemaining} characters left`); + } + } + + this.screen.render(message, bottomContent); + } +} diff --git a/@commitlint/prompt/src/inquirer/inquirer.d.ts b/@commitlint/prompt/src/inquirer/inquirer.d.ts new file mode 100644 index 0000000000..3c178eac46 --- /dev/null +++ b/@commitlint/prompt/src/inquirer/inquirer.d.ts @@ -0,0 +1,24 @@ +import {Answers, InputQuestionOptions} from 'inquirer'; + +declare module 'inquirer' { + interface InputCustomCompletionOption { + value: string; + description?: string; + } + + export interface InputCustomOptions + extends InputQuestionOptions { + /** + * @inheritdoc + */ + type?: 'input-custom'; + required?: boolean; + log?(answers?: T): string; + tabCompletion?: InputCustomCompletionOption[]; + maxLength(answers?: T): number; + } + + interface QuestionMap { + 'input-custom': InputCustomOptions; + } +} diff --git a/@commitlint/prompt/src/library/format.test.ts b/@commitlint/prompt/src/library/format.test.ts new file mode 100644 index 0000000000..ae20b3b1ab --- /dev/null +++ b/@commitlint/prompt/src/library/format.test.ts @@ -0,0 +1,55 @@ +import {Result} from './types'; +import format from './format'; + +test('should return empty string', () => { + const result: Result = {}; + expect(format(result)).toBe(' '); +}); + +test('should omit scope', () => { + const result: Result = { + type: 'fix', + subject: 'test', + }; + expect(format(result)).toBe('fix: test'); +}); + +test('should include scope', () => { + const result: Result = { + type: 'fix', + scope: 'prompt', + subject: 'test', + }; + expect(format(result)).toBe('fix(prompt): test'); +}); + +test('should include body', () => { + const result: Result = { + type: 'fix', + scope: 'prompt', + subject: 'test', + body: 'some body', + }; + expect(format(result)).toBe('fix(prompt): test\nsome body'); +}); + +test('should include footer', () => { + const result: Result = { + type: 'fix', + scope: 'prompt', + subject: 'test', + footer: 'some footer', + }; + expect(format(result)).toBe('fix(prompt): test\nsome footer'); +}); + +test('should include body and footer', () => { + const result: Result = { + type: 'fix', + scope: 'prompt', + subject: 'test', + body: 'some body', + footer: 'some footer', + }; + expect(format(result)).toBe('fix(prompt): test\nsome body\nsome footer'); +}); diff --git a/@commitlint/prompt/src/library/format.ts b/@commitlint/prompt/src/library/format.ts index 54f7cf9355..e2d3076b9b 100644 --- a/@commitlint/prompt/src/library/format.ts +++ b/@commitlint/prompt/src/library/format.ts @@ -1,7 +1,5 @@ import chalk from 'chalk'; -import {Result} from './types'; - -export default format; +import {Result, ResultPart} from './types'; /** * Get formatted commit message @@ -9,21 +7,29 @@ export default format; * @param debug show debug information in commit message * @return formatted debug message */ -function format(input: Result, debug = false): string { +export default function format(input: Result, debug = false): string { + const defaultInput = { + type: undefined, + scope: undefined, + subject: undefined, + body: undefined, + footer: undefined, + ...input, + }; const results = debug - ? Object.entries(input).reduce((registry, [name, value]) => { - registry[name as 'type' | 'scope' | 'subject' | 'body' | 'footer'] = - value === null ? chalk.grey(`<${name}>`) : chalk.bold(value); + ? Object.entries(defaultInput).reduce((registry, [name, value]) => { + registry[name as ResultPart] = + value === undefined ? chalk.grey(`<${name}>`) : chalk.bold(value); return registry; }, {}) - : input; + : defaultInput; // Return formatted string const {type, scope, subject, body, footer} = results; return [ - `${type}${scope ? '(' : ''}${scope}${scope ? ')' : ''}${ - type || scope ? ':' : '' - } ${subject}`, + `${type || ''}${scope ? `(${scope})` : ''}${type || scope ? ':' : ''} ${ + subject || '' + }`, body, footer, ] diff --git a/@commitlint/prompt/src/library/get-prompt.test.ts b/@commitlint/prompt/src/library/get-prompt.test.ts deleted file mode 100644 index 831fbcab8a..0000000000 --- a/@commitlint/prompt/src/library/get-prompt.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import getPrompt from './get-prompt'; -import {Prompter, PrompterCommand} from './types'; - -test('throws without params', () => { - expect(() => (getPrompt as any)()).toThrow('Missing prompter function'); -}); - -test('throws with incompatible prompter', () => { - expect(() => - getPrompt('type', { - prompter: (() => ({})) as any, - }) - ).toThrow('prompt.removeAllListeners'); -}); - -test('returns input unaltered without rules', async () => { - const message = await getPrompt('type', { - prompter: stub('foobar'), - }); - - expect(message).toEqual('foobar'); -}); - -function stub(input = '') { - return function stubPrompter(): Prompter { - const called: any[] = []; - const actions: any[] = []; - - const instanceCommand: PrompterCommand = { - description(...args) { - called.push([instanceCommand.description, args]); - return instanceCommand; - }, - action(...args) { - actions.push(args[0]); - called.push([instanceCommand.action, args]); - return instanceCommand; - }, - }; - - function redraw(...args: any[]): void { - called.push([instance.log, args]); - } - redraw.done = function (...args: any[]) { - called.push([instance.ui.redraw.done, args]); - }; - - const instance: Prompter & {called: any[]} = { - called, - addListener(...args): void { - called.push([instance.addListener, args]); - }, - catch(...args) { - called.push([instance.catch, args]); - return instanceCommand; - }, - command(...args) { - called.push([instance.command, args]); - return instanceCommand; - }, - exec(...args) { - called.push([instance.command, args]); - return Promise.resolve(); - }, - delimiter(...args) { - called.push([instance.delimiter, args]); - return instance; - }, - log(...args) { - called.push([instance.log, args]); - return instance; - }, - removeAllListeners(...args) { - called.push([instance.removeAllListeners, args]); - }, - show(...args) { - called.push([instance.show, args]); - return instance; - }, - ui: { - log(...args) { - called.push([instance.log, args]); - }, - input(...args) { - called.push([instance.log, args]); - return args[0]!; - }, - redraw, - }, - }; - - setTimeout(() => { - actions[0]({ - text: Array.isArray(input) ? input : [input], - }); - }, 0); - - return instance; - }; -} diff --git a/@commitlint/prompt/src/library/get-prompt.ts b/@commitlint/prompt/src/library/get-prompt.ts index 73a9cb7f15..776322cf66 100644 --- a/@commitlint/prompt/src/library/get-prompt.ts +++ b/@commitlint/prompt/src/library/get-prompt.ts @@ -1,6 +1,8 @@ import chalk from 'chalk'; -import type {InputSetting, Prompter, Result, RuleEntry} from './types'; +import {InputCustomOptions, InputQuestion, ListQuestion} from 'inquirer'; + +import type {InputSetting, RuleEntry, Result, ResultPart} from './types'; import format from './format'; import getForcedCaseFn from './get-forced-case-fn'; @@ -8,232 +10,99 @@ import getForcedLeadingFn from './get-forced-leading-fn'; import meta from './meta'; import { enumRuleIsActive, - ruleIsNotApplicable, - ruleIsApplicable, - ruleIsActive, getHasName, getMaxLength, + ruleIsActive, + ruleIsApplicable, + ruleIsNotApplicable, } from './utils'; +const EOL = '\n'; + /** * Get a cli prompt based on rule configuration * @param type type of the data to gather - * @param context rules to parse + * @param rules + * @param settings * @return prompt instance */ export default function getPrompt( - type: string, - context: { - rules?: RuleEntry[]; - settings?: InputSetting; - results?: Result; - prompter?: () => Prompter; - } = {} -): Promise { - const {rules = [], settings = {}, results = {}, prompter} = context; - - if (typeof prompter !== 'function') { - throw new TypeError('Missing prompter function in getPrompt context'); - } - - const prompt = prompter(); - - if (typeof prompt.removeAllListeners !== 'function') { - throw new TypeError( - 'getPrompt: prompt.removeAllListeners is not a function' - ); - } - - if (typeof prompt.command !== 'function') { - throw new TypeError('getPrompt: prompt.command is not a function'); - } - - if (typeof prompt.catch !== 'function') { - throw new TypeError('getPrompt: prompt.catch is not a function'); - } - - if (typeof prompt.addListener !== 'function') { - throw new TypeError('getPrompt: prompt.addListener is not a function'); - } - - if (typeof prompt.log !== 'function') { - throw new TypeError('getPrompt: prompt.log is not a function'); - } - - if (typeof prompt.delimiter !== 'function') { - throw new TypeError('getPrompt: prompt.delimiter is not a function'); - } - - if (typeof prompt.show !== 'function') { - throw new TypeError('getPrompt: prompt.show is not a function'); - } - - const enumRule = rules.filter(getHasName('enum')).find(enumRuleIsActive); - - const emptyRule = rules.find(getHasName('empty')); - - const mustBeEmpty = - emptyRule && ruleIsActive(emptyRule) && ruleIsApplicable(emptyRule); - - const mayNotBeEmpty = - emptyRule && ruleIsActive(emptyRule) && ruleIsNotApplicable(emptyRule); - - const mayBeEmpty = !mayNotBeEmpty; + type: ResultPart, + rules: RuleEntry[] = [], + settings: InputSetting = {} +): + | InputQuestion + | ListQuestion + | InputCustomOptions + | null { + const emptyRule = rules.filter(getHasName('empty')).find(ruleIsActive); + + const mustBeEmpty = emptyRule ? ruleIsApplicable(emptyRule) : false; if (mustBeEmpty) { - prompt.removeAllListeners('keypress'); - prompt.removeAllListeners('client_prompt_submit'); - prompt.ui.redraw.done(); - return Promise.resolve(undefined); + return null; } - const caseRule = rules.find(getHasName('case')); - - const forceCaseFn = getForcedCaseFn(caseRule); - - const leadingBlankRule = rules.find(getHasName('leading-blank')); + const required = emptyRule ? ruleIsNotApplicable(emptyRule) : false; - const forceLeadingBlankFn = getForcedLeadingFn(leadingBlankRule); + const forceCaseFn = getForcedCaseFn(rules.find(getHasName('case'))); + const forceLeadingBlankFn = getForcedLeadingFn( + rules.find(getHasName('leading-blank')) + ); const maxLengthRule = rules.find(getHasName('max-length')); const inputMaxLength = getMaxLength(maxLengthRule); - const headerLength = settings.header ? settings.header.length : Infinity; - - const remainingHeaderLength = headerLength - ? headerLength - - [ - results.type, - results.scope, - results.scope ? '()' : '', - results.type && results.scope ? ':' : '', - results.subject, - ].join('').length - : Infinity; - - const maxLength = Math.min(inputMaxLength, remainingHeaderLength); - - return new Promise((resolve) => { - // Add the defined enums as sub commands if applicable - if (enumRule) { - const [, [, , enums]] = enumRule; - - enums.forEach((enumerable) => { - const enumSettings = (settings.enumerables || {})[enumerable] || {}; - prompt - .command(enumerable) - .description(enumSettings.description || '') - .action(() => { - prompt.removeAllListeners(); - prompt.ui.redraw.done(); - return resolve(forceLeadingBlankFn(forceCaseFn(enumerable))); - }); - }); - } else { - prompt.catch('[text...]').action((parameters) => { - const {text = ''} = parameters; - prompt.removeAllListeners(); - prompt.ui.redraw.done(); - return resolve(forceLeadingBlankFn(forceCaseFn(text.join(' ')))); - }); - } - - if (mayBeEmpty) { - // Add an easy exit command - prompt - .command(':skip') - .description('Skip the input if possible.') - .action(() => { - prompt.removeAllListeners(); - prompt.ui.redraw.done(); - resolve(''); - }); - } - - // Handle empty input - const onSubmit = (input: string) => { - if (input.length > 0) { - return; - } - - // Show help if enum is defined and input may not be empty - if (mayNotBeEmpty) { - prompt.ui.log(chalk.yellow(`⚠ ${chalk.bold(type)} may not be empty.`)); - } - - if (mayBeEmpty) { - prompt.ui.log( - chalk.blue( - `ℹ Enter ${chalk.bold(':skip')} to omit ${chalk.bold(type)}.` - ) - ); - } - - if (enumRule) { - prompt.exec('help'); - } - }; - - const drawRemaining = (length: number) => { - if (length < Infinity) { - const colors = [ - { - threshold: 5, - color: chalk.red, - }, - { - threshold: 10, - color: chalk.yellow, - }, - { - threshold: Infinity, - color: chalk.grey, - }, - ]; + const enumRule = rules.filter(getHasName('enum')).find(enumRuleIsActive); - const el = colors.find((item) => item.threshold >= length); - const color = el ? el.color : chalk.grey; - prompt.ui.redraw(color(`${length} characters left`)); + return { + type: 'input-custom', + name: type, + message: `${type}:`, + validate(): boolean | string { + return true; + }, + tabCompletion: enumRule + ? enumRule[1][2].map((enumerable) => { + const value = forceLeadingBlankFn(forceCaseFn(enumerable)); + const enumSettings = (settings.enumerables || {})[enumerable] || {}; + return { + value: value, + description: enumSettings.description || '', + }; + }) + : [], + required, + log(answers?: Result) { + let prefix = + `${chalk.white('Please enter a')} ${chalk.bold(type)}: ${meta({ + optional: !required, + required: required, + 'tab-completion': typeof enumRule !== 'undefined', + header: typeof settings.header !== 'undefined', + 'multi-line': settings.multiline, + })}` + EOL; + + if (settings.description) { + prefix += chalk.grey(`${settings.description}`) + EOL; } - }; - - const onKey = (event: {value: string}) => { - const sanitized = forceCaseFn(event.value); - const cropped = sanitized.slice(0, maxLength); - - // We **could** do live editing, but there are some quirks to solve - /* const live = merge({}, results, { - [type]: cropped - }); - prompt.ui.redraw(`\n\n${format(live, true)}\n\n`); */ - - if (maxLength) { - drawRemaining(maxLength - cropped.length); + if (answers) { + prefix += EOL + `${format(answers, true)}` + EOL; } - prompt.ui.input(cropped); - }; - - prompt.addListener('keypress', onKey); - prompt.addListener('client_prompt_submit', onSubmit); - - prompt.log( - `\n\nPlease enter a ${chalk.bold(type)}: ${meta({ - optional: !mayNotBeEmpty, - required: mayNotBeEmpty, - 'tab-completion': typeof enumRule !== 'undefined', - header: typeof settings.header !== 'undefined', - 'multi-line': settings.multiline, - })}` - ); - - if (settings.description) { - prompt.log(chalk.grey(`${settings.description}\n`)); - } - - prompt.log(`\n\n${format(results, true)}\n\n`); - - drawRemaining(maxLength); - - prompt.delimiter(`❯ ${type}:`).show(); - }); + return prefix + EOL; + }, + maxLength(res: Result) { + const headerLength = settings.header ? settings.header.length : Infinity; + const header = `${res.type}${res.scope ? `(${res.scope})` : ''}${ + res.type || res.scope ? ': ' : '' + }${res.subject}`; + const remainingHeaderLength = headerLength + ? headerLength - header.length + : Infinity; + return Math.min(inputMaxLength, remainingHeaderLength); + }, + transformer(value: string) { + return forceCaseFn(value); + }, + }; } diff --git a/@commitlint/prompt/src/library/types.ts b/@commitlint/prompt/src/library/types.ts index 0f05c3cd6f..e873e4a36a 100644 --- a/@commitlint/prompt/src/library/types.ts +++ b/@commitlint/prompt/src/library/types.ts @@ -19,37 +19,6 @@ export type InputSetting = { }; }; -export type Result = Partial< - Record<'type' | 'scope' | 'subject' | 'body' | 'footer', null | string> ->; +export type ResultPart = 'type' | 'scope' | 'subject' | 'body' | 'footer'; -export interface PrompterCommand { - description(value: string): this; - action( - action: (args: { - [key: string]: any; - options: { - [key: string]: any; - }; - }) => Promise | void - ): this; -} - -export interface Prompter { - delimiter(value: string): this; - show(): this; - exec(command: string): Promise; - log(text?: string): void; - catch(command: string, description?: string): PrompterCommand; - command(command: string, description?: string): PrompterCommand; - removeAllListeners(input?: string): void; - addListener(input: string, cb: (event: any) => void): void; - ui: { - log(text?: string): void; - input(text?: string): string; - redraw: { - (text: string, ...texts: string[]): void; - done(): void; - }; - }; -} +export type Result = Partial>; diff --git a/@commitlint/prompt/tsconfig.json b/@commitlint/prompt/tsconfig.json index 2a6d93a0fa..119e645565 100644 --- a/@commitlint/prompt/tsconfig.json +++ b/@commitlint/prompt/tsconfig.json @@ -7,5 +7,5 @@ }, "include": ["./src"], "exclude": ["./src/**/*.test.ts", "./lib/**/*"], - "references": [{"path": "../cli"}] + "references": [{"path": "../types"}] } diff --git a/yarn.lock b/yarn.lock index a403db7668..58c09cdf5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2300,6 +2300,14 @@ dependencies: "@types/node" "*" +"@types/inquirer@^6.5.0": + version "6.5.0" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" + integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw== + dependencies: + "@types/through" "*" + rxjs "^6.4.0" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.1" resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -2403,6 +2411,13 @@ resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff" integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== +"@types/through@*": + version "0.0.30" + resolved "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" + integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg== + dependencies: + "@types/node" "*" + "@types/tmp@^0.2.0": version "0.2.0" resolved "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.0.tgz#e3f52b4d7397eaa9193592ef3fdd44dc0af4298c" @@ -2680,11 +2695,6 @@ ansi-colors@^4.1.1: resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: - version "1.4.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" - integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= - ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -2960,15 +2970,6 @@ babel-plugin-jest-hoist@^26.6.2: "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-polyfill@^6.3.14: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" - integrity sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM= - dependencies: - babel-runtime "^6.26.0" - core-js "^2.5.0" - regenerator-runtime "^0.10.5" - babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -2995,14 +2996,6 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-runtime@^6.26.0: - version "6.26.0" - resolved "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" - integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= - dependencies: - core-js "^2.4.0" - regenerator-runtime "^0.11.0" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -3323,7 +3316,7 @@ chalk@4.1.0, chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1: +chalk@^1.1.1: version "1.1.3" resolved "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= @@ -3411,13 +3404,6 @@ cli-boxes@^2.2.0: resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== -cli-cursor@^1.0.1, cli-cursor@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= - dependencies: - restore-cursor "^1.0.1" - cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" @@ -3440,11 +3426,6 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" -cli-width@^1.0.1: - version "1.1.1" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz#a4d293ef67ebb7b88d4a4d42c0ccf00c4d1e366d" - integrity sha1-pNKT72frt7iNSk1CwMzwDE0eNm0= - cli-width@^2.0.0: version "2.2.1" resolved "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" @@ -3860,11 +3841,6 @@ core-js-compat@^3.6.2: browserslist "^4.8.3" semver "7.0.0" -core-js@^2.4.0, core-js@^2.5.0: - version "2.6.11" - resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" - integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -4757,11 +4733,6 @@ execa@^5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= - exit@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -4915,14 +4886,6 @@ figlet@^1.1.1: resolved "https://registry.npmjs.org/figlet/-/figlet-1.5.0.tgz#2db4d00a584e5155a96080632db919213c3e003c" integrity sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww== -figures@^1.3.5: - version "1.7.0" - resolved "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - figures@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -5791,11 +5754,6 @@ imurmurhash@^0.1.4: resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -in-publish@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" - integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= - indent-string@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" @@ -5850,24 +5808,6 @@ init-package-json@^1.10.3: validate-npm-package-license "^3.0.1" validate-npm-package-name "^3.0.0" -inquirer@0.11.0: - version "0.11.0" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-0.11.0.tgz#7448bfa924092af311d47173bbab990cae2bb027" - integrity sha1-dEi/qSQJKvMR1HFzu6uZDK4rsCc= - dependencies: - ansi-escapes "^1.1.0" - ansi-regex "^2.0.0" - chalk "^1.0.0" - cli-cursor "^1.0.1" - cli-width "^1.0.1" - figures "^1.3.5" - lodash "^3.3.1" - readline2 "^1.0.1" - run-async "^0.1.0" - rx-lite "^3.1.2" - strip-ansi "^3.0.0" - through "^2.3.6" - inquirer@6.5.0: version "6.5.0" resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz#2303317efc9a4ea7ec2e2df6f86569b734accf42" @@ -5887,7 +5827,7 @@ inquirer@6.5.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@6.5.2, inquirer@^6.2.0: +inquirer@6.5.2, inquirer@^6.2.0, inquirer@^6.5.2: version "6.5.2" resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== @@ -7142,7 +7082,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, lodash@^3.3.1, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1, lodash@^4.5.1: +lodash@4.17.15, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: version "4.17.19" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== @@ -7154,14 +7094,6 @@ log-symbols@^4.0.0: dependencies: chalk "^4.0.0" -log-update@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" - integrity sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE= - dependencies: - ansi-escapes "^1.0.0" - cli-cursor "^1.0.2" - log-update@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" @@ -7581,11 +7513,6 @@ multimatch@^3.0.0: arrify "^1.0.1" minimatch "^3.0.4" -mute-stream@0.0.5: - version "0.0.5" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" - integrity sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA= - mute-stream@0.0.7: version "0.0.7" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -7678,11 +7605,6 @@ node-int64@^0.4.0: resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-localstorage@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/node-localstorage/-/node-localstorage-0.6.0.tgz#45a0601c6932dfde6644a23361f1be173c75d3af" - integrity sha1-RaBgHGky395mRKIzYfG+Fzx1068= - node-modules-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" @@ -7929,11 +7851,6 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= - onetime@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" @@ -8764,15 +8681,6 @@ readdirp@~3.5.0: dependencies: picomatch "^2.2.1" -readline2@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz#41059608ffc154757b715d9989d199ffbf372e35" - integrity sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU= - dependencies: - code-point-at "^1.0.0" - is-fullwidth-code-point "^1.0.0" - mute-stream "0.0.5" - redent@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -8801,16 +8709,6 @@ regenerate@^1.4.0: resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg== -regenerator-runtime@^0.10.5: - version "0.10.5" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" - integrity sha1-M2w+/BIgrc7dosn6tntaeVWjNlg= - -regenerator-runtime@^0.11.0: - version "0.11.1" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" - integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== - regenerator-runtime@^0.13.4: version "0.13.5" resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" @@ -9069,14 +8967,6 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= - dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -9134,13 +9024,6 @@ rsvp@^4.8.4: resolved "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== -run-async@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" - integrity sha1-yK1KXhEGYeQCp9IbUw4AnyX444k= - dependencies: - once "^1.3.0" - run-async@^2.2.0: version "2.4.1" resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -9158,11 +9041,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rx-lite@^3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" - integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= - rxjs@^6.4.0: version "6.5.4" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" @@ -10430,22 +10308,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vorpal@^1.12.0: - version "1.12.0" - resolved "https://registry.npmjs.org/vorpal/-/vorpal-1.12.0.tgz#4be7b2a4e48f8fcfc9cf3648c419d311c522159d" - integrity sha1-S+eypOSPj8/JzzZIxBnTEcUiFZ0= - dependencies: - babel-polyfill "^6.3.14" - chalk "^1.1.0" - in-publish "^2.0.0" - inquirer "0.11.0" - lodash "^4.5.1" - log-update "^1.0.2" - minimist "^1.2.0" - node-localstorage "^0.6.0" - strip-ansi "^3.0.0" - wrap-ansi "^2.0.0" - w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" @@ -10574,14 +10436,6 @@ wordwrap@~0.0.2: resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" From 33ed2c021c6fded36f2fd4a65149c073bbc46c97 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 6 Jan 2021 06:55:30 +0100 Subject: [PATCH 2/8] fix(prompt): simplify logic used to compute maxLength --- @commitlint/prompt/src/library/get-prompt.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/@commitlint/prompt/src/library/get-prompt.ts b/@commitlint/prompt/src/library/get-prompt.ts index 776322cf66..426e0e8395 100644 --- a/@commitlint/prompt/src/library/get-prompt.ts +++ b/@commitlint/prompt/src/library/get-prompt.ts @@ -64,10 +64,9 @@ export default function getPrompt( }, tabCompletion: enumRule ? enumRule[1][2].map((enumerable) => { - const value = forceLeadingBlankFn(forceCaseFn(enumerable)); const enumSettings = (settings.enumerables || {})[enumerable] || {}; return { - value: value, + value: forceLeadingBlankFn(forceCaseFn(enumerable)), description: enumSettings.description || '', }; }) @@ -92,13 +91,15 @@ export default function getPrompt( return prefix + EOL; }, maxLength(res: Result) { - const headerLength = settings.header ? settings.header.length : Infinity; - const header = `${res.type}${res.scope ? `(${res.scope})` : ''}${ - res.type || res.scope ? ': ' : '' - }${res.subject}`; - const remainingHeaderLength = headerLength - ? headerLength - header.length - : Infinity; + let remainingHeaderLength = Infinity; + if (settings.header && settings.header.length) { + const header = format({ + type: res.type, + scope: res.scope, + subject: res.subject, + }); + remainingHeaderLength = settings.header.length - header.length; + } return Math.min(inputMaxLength, remainingHeaderLength); }, transformer(value: string) { From 64c9cc8c7b5485002aaccc2eaf32f1d90d688bd2 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 6 Jan 2021 07:47:06 +0100 Subject: [PATCH 3/8] test(prompt): add basic input test --- @commitlint/prompt/src/input.test.ts | 104 +++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 @commitlint/prompt/src/input.test.ts diff --git a/@commitlint/prompt/src/input.test.ts b/@commitlint/prompt/src/input.test.ts new file mode 100644 index 0000000000..47b6c9a87f --- /dev/null +++ b/@commitlint/prompt/src/input.test.ts @@ -0,0 +1,104 @@ +import {Answers, PromptModule, QuestionCollection} from 'inquirer'; +import input from './input'; +import InputCustomPrompt from './inquirer/InputCustomPrompt'; +import chalk from 'chalk'; + +jest.mock( + '@commitlint/load', + () => { + return () => require('@commitlint/config-angular'); + }, + { + virtual: true, + } +); + +test('should work with all fields filled', async () => { + const prompt = stub({ + 'input-custom': { + type: 'fix', + scope: 'test', + subject: 'subject', + body: 'body', + footer: 'footer', + }, + }); + const message = await input(prompt); + expect(message).toEqual('fix(test): subject\n' + 'body\n' + 'footer'); +}); + +test('should work without scope', async () => { + const prompt = stub({ + 'input-custom': { + type: 'fix', + scope: '', + subject: 'subject', + body: 'body', + footer: 'footer', + }, + }); + const message = await input(prompt); + expect(message).toEqual('fix: subject\n' + 'body\n' + 'footer'); +}); + +test('should fail without type', async () => { + const spy = jest.spyOn(console, 'error').mockImplementation(); + const prompt = stub({ + 'input-custom': { + type: '', + scope: '', + subject: '', + body: '', + footer: '', + }, + }); + const message = await input(prompt); + expect(message).toEqual(''); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenLastCalledWith( + new Error(`⚠ ${chalk.bold('type')} may not be empty.`) + ); + spy.mockRestore(); +}); + +function stub(config: Record>): PromptModule { + const prompt = async (questions: QuestionCollection): Promise => { + const result: Answers = {}; + const resolvedConfig = Array.isArray(questions) ? questions : [questions]; + for (const promptConfig of resolvedConfig) { + const configType = promptConfig.type || 'input'; + const questions = config[configType]; + if (!questions) { + throw new Error(`Unexpected config type: ${configType}`); + } + const answer = questions[promptConfig.name!]; + if (answer == null) { + throw new Error(`Unexpected config name: ${promptConfig.name}`); + } + let validate = promptConfig.validate; + if (promptConfig.type === 'input-custom') { + const customInput = new InputCustomPrompt( + promptConfig, + {write: () => true} as any, + result + ) as any; + validate = customInput.opt.validate.bind(customInput); + } + if (validate) { + const validationResult = validate(answer, result); + if (validationResult !== true) { + throw new Error(validationResult || undefined); + } + } + + result[promptConfig.name!] = answer; + } + return result; + }; + prompt.registerPrompt = () => { + return prompt; + }; + prompt.restoreDefaultPrompts = () => true; + prompt.prompts = {}; + return (prompt as any) as PromptModule; +} From f6c8b7d90a6abdf9336da36af56a58f826d2bd1a Mon Sep 17 00:00:00 2001 From: Armano Date: Fri, 8 Jan 2021 18:27:12 +0100 Subject: [PATCH 4/8] fix(prompt): small code refactor --- .eslintrc.js | 1 + @commitlint/prompt/src/input.test.ts | 12 +-- .../prompt/src/inquirer/InputCustomPrompt.ts | 31 -------- @commitlint/prompt/src/inquirer/inquirer.d.ts | 1 - @commitlint/prompt/src/library/get-prompt.ts | 74 +++++++++++-------- 5 files changed, 47 insertions(+), 72 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 21cb57c31e..9159baf58c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -51,6 +51,7 @@ module.exports = { '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/no-inferrable-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/triple-slash-reference': 'off', // TODO: enable those rules? 'no-empty': 'off', diff --git a/@commitlint/prompt/src/input.test.ts b/@commitlint/prompt/src/input.test.ts index 47b6c9a87f..003d500aa1 100644 --- a/@commitlint/prompt/src/input.test.ts +++ b/@commitlint/prompt/src/input.test.ts @@ -1,6 +1,6 @@ import {Answers, PromptModule, QuestionCollection} from 'inquirer'; +/// import input from './input'; -import InputCustomPrompt from './inquirer/InputCustomPrompt'; import chalk from 'chalk'; jest.mock( @@ -75,15 +75,7 @@ function stub(config: Record>): PromptModule { if (answer == null) { throw new Error(`Unexpected config name: ${promptConfig.name}`); } - let validate = promptConfig.validate; - if (promptConfig.type === 'input-custom') { - const customInput = new InputCustomPrompt( - promptConfig, - {write: () => true} as any, - result - ) as any; - validate = customInput.opt.validate.bind(customInput); - } + const validate = promptConfig.validate; if (validate) { const validationResult = validate(answer, result); if (validationResult !== true) { diff --git a/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts b/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts index 46d20a202b..8ac0ec3dd5 100644 --- a/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts +++ b/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference /// import {Interface as ReadlineInterface, Key} from 'readline'; @@ -10,7 +9,6 @@ import type {Subscription} from 'rxjs/internal/Subscription'; import Answers = inquirer.Answers; import InputCustomOptions = inquirer.InputCustomOptions; -import Validator = inquirer.Validator; import SuccessfulPromptStateData = inquirer.prompts.SuccessfulPromptStateData; interface KeyDescriptor { @@ -46,8 +44,6 @@ export default class InputCustomPrompt< this.tabCompletion = (this.opt.tabCompletion || []) .map((item) => item.value) .sort((a, b) => a.localeCompare(b)); - - this.opt.validate = this.extendedValidate(this.opt.validate); } onEnd(state: SuccessfulPromptStateData): void { @@ -55,33 +51,6 @@ export default class InputCustomPrompt< super.onEnd(state); } - extendedValidate(validate?: Validator): Validator { - return (input, answers) => { - if (input.length > this.opt.maxLength(answers)) { - return 'Input contains too many characters!'; - } - if (this.opt.required && input.trim().length === 0) { - // Show help if enum is defined and input may not be empty - return `⚠ ${chalk.bold(this.opt.name)} may not be empty.`; - } - - if ( - input.length > 0 && - this.tabCompletion.length > 0 && - !this.tabCompletion.includes(input) - ) { - return `⚠ ${chalk.bold( - this.opt.name - )} must be one of ${this.tabCompletion.join(', ')}.`; - } - - if (validate) { - return validate(input, answers); - } - return true; - }; - } - /** * @see https://nodejs.org/api/readline.html#readline_rl_write_data_key * @see https://nodejs.org/api/readline.html#readline_rl_line diff --git a/@commitlint/prompt/src/inquirer/inquirer.d.ts b/@commitlint/prompt/src/inquirer/inquirer.d.ts index 3c178eac46..06d2304ab2 100644 --- a/@commitlint/prompt/src/inquirer/inquirer.d.ts +++ b/@commitlint/prompt/src/inquirer/inquirer.d.ts @@ -12,7 +12,6 @@ declare module 'inquirer' { * @inheritdoc */ type?: 'input-custom'; - required?: boolean; log?(answers?: T): string; tabCompletion?: InputCustomCompletionOption[]; maxLength(answers?: T): number; diff --git a/@commitlint/prompt/src/library/get-prompt.ts b/@commitlint/prompt/src/library/get-prompt.ts index 426e0e8395..81abe7fc73 100644 --- a/@commitlint/prompt/src/library/get-prompt.ts +++ b/@commitlint/prompt/src/library/get-prompt.ts @@ -1,6 +1,5 @@ import chalk from 'chalk'; - -import {InputCustomOptions, InputQuestion, ListQuestion} from 'inquirer'; +import {InputCustomOptions} from 'inquirer'; import type {InputSetting, RuleEntry, Result, ResultPart} from './types'; @@ -30,11 +29,7 @@ export default function getPrompt( type: ResultPart, rules: RuleEntry[] = [], settings: InputSetting = {} -): - | InputQuestion - | ListQuestion - | InputCustomOptions - | null { +): InputCustomOptions | null { const emptyRule = rules.filter(getHasName('empty')).find(ruleIsActive); const mustBeEmpty = emptyRule ? ruleIsApplicable(emptyRule) : false; @@ -55,23 +50,53 @@ export default function getPrompt( const enumRule = rules.filter(getHasName('enum')).find(enumRuleIsActive); + const tabCompletion = enumRule + ? enumRule[1][2].map((enumerable) => { + const enumSettings = (settings.enumerables || {})[enumerable] || {}; + return { + value: forceLeadingBlankFn(forceCaseFn(enumerable)), + description: enumSettings.description || '', + }; + }) + : []; + + const maxLength = (res: Result) => { + let remainingHeaderLength = Infinity; + if (settings.header && settings.header.length) { + const header = format({ + type: res.type, + scope: res.scope, + subject: res.subject, + }); + remainingHeaderLength = settings.header.length - header.length; + } + return Math.min(inputMaxLength, remainingHeaderLength); + }; + return { type: 'input-custom', name: type, message: `${type}:`, - validate(): boolean | string { + validate(input, answers) { + if (input.length > maxLength(answers || {})) { + return 'Input contains too many characters!'; + } + if (required && input.trim().length === 0) { + // Show help if enum is defined and input may not be empty + return `⚠ ${chalk.bold(type)} may not be empty.`; + } + + const tabValues = tabCompletion.map((item) => item.value); + if ( + input.length > 0 && + tabValues.length > 0 && + !tabValues.includes(input) + ) { + return `⚠ ${chalk.bold(type)} must be one of ${tabValues.join(', ')}.`; + } return true; }, - tabCompletion: enumRule - ? enumRule[1][2].map((enumerable) => { - const enumSettings = (settings.enumerables || {})[enumerable] || {}; - return { - value: forceLeadingBlankFn(forceCaseFn(enumerable)), - description: enumSettings.description || '', - }; - }) - : [], - required, + tabCompletion, log(answers?: Result) { let prefix = `${chalk.white('Please enter a')} ${chalk.bold(type)}: ${meta({ @@ -90,18 +115,7 @@ export default function getPrompt( } return prefix + EOL; }, - maxLength(res: Result) { - let remainingHeaderLength = Infinity; - if (settings.header && settings.header.length) { - const header = format({ - type: res.type, - scope: res.scope, - subject: res.subject, - }); - remainingHeaderLength = settings.header.length - header.length; - } - return Math.min(inputMaxLength, remainingHeaderLength); - }, + maxLength, transformer(value: string) { return forceCaseFn(value); }, From 6a41f928059658fdedbd0a1e7e2efb0a522c8350 Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 12 Jan 2021 18:37:07 +0100 Subject: [PATCH 5/8] fix: correct linting issues, add missing dependencies --- @commitlint/prompt/package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/@commitlint/prompt/package.json b/@commitlint/prompt/package.json index 6a3a24ee7b..baf8a6c2f4 100644 --- a/@commitlint/prompt/package.json +++ b/@commitlint/prompt/package.json @@ -37,16 +37,17 @@ }, "devDependencies": { "@commitlint/utils": "^11.0.0", - "@types/inquirer": "^6.5.0", "@commitlint/types": "^11.0.0", - "commitizen": "4.2.2", - "inquirer": "^6.5.2" + "@commitlint/config-angular": "^11.0.0", + "@types/inquirer": "^6.5.0", + "commitizen": "4.2.2" }, "dependencies": { "@commitlint/load": "^11.0.0", "@commitlint/types": "^11.0.0", "chalk": "^4.0.0", - "lodash": "^4.17.19" + "lodash": "^4.17.19", + "inquirer": "^6.5.2" }, "gitHead": "cb565dfcca3128380b9b3dc274aedbcae34ce5ca" } From 1ca392c3f647dcc324d3c142235284334d251410 Mon Sep 17 00:00:00 2001 From: Armano Date: Tue, 12 Jan 2021 19:19:34 +0100 Subject: [PATCH 6/8] fix: add missing tsconfig reference --- @commitlint/prompt/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/@commitlint/prompt/tsconfig.json b/@commitlint/prompt/tsconfig.json index 119e645565..76dd5e38cb 100644 --- a/@commitlint/prompt/tsconfig.json +++ b/@commitlint/prompt/tsconfig.json @@ -7,5 +7,5 @@ }, "include": ["./src"], "exclude": ["./src/**/*.test.ts", "./lib/**/*"], - "references": [{"path": "../types"}] + "references": [{"path": "../types"}, {"path": "../load"}] } From b9d840cec8941a9a196057d9e7b2f301f7f6122b Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 20 Jan 2021 21:27:23 +0100 Subject: [PATCH 7/8] fix: update lock file after merge --- @commitlint/prompt/package.json | 2 +- yarn.lock | 43 ++------------------------------- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/@commitlint/prompt/package.json b/@commitlint/prompt/package.json index 2b33616fb9..edd35a9cf7 100644 --- a/@commitlint/prompt/package.json +++ b/@commitlint/prompt/package.json @@ -40,7 +40,7 @@ "@commitlint/types": "^12.0.0", "@commitlint/config-angular": "^12.0.0", "@types/inquirer": "^6.5.0", - "commitizen": "4.2.2" + "commitizen": "^4.2.3" }, "dependencies": { "@commitlint/load": "^12.0.0", diff --git a/yarn.lock b/yarn.lock index da9a9960b9..bc0b5bdbc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3274,7 +3274,7 @@ commander@~2.20.3: resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commitizen@4.2.3: +commitizen@^4.0.3, commitizen@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.2.3.tgz#088d0ef72500240d331b11e02e288223667c1475" integrity sha512-pYlYEng7XMV2TW4xtjDKBGqeJ0Teq2zyRSx2S3Ml1XAplHSlJZK8vm1KdGclpMEZuGafbS5TeHXIVnHk8RWIzQ== @@ -3294,26 +3294,6 @@ commitizen@4.2.3: strip-bom "4.0.0" strip-json-comments "3.0.1" -commitizen@^4.0.3: - version "4.1.2" - resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.1.2.tgz#6095eb825fd3f0d3611df88e6803c69b23307e9a" - integrity sha512-LBxTQKHbVgroMz9ohpm86N+GfJobonGyvDc3zBGdZazbwCLz2tqLa48Rf2TnAdKx7/06W1i1R3SXUt5QW97qVQ== - dependencies: - cachedir "2.2.0" - cz-conventional-changelog "3.2.0" - dedent "0.7.0" - detect-indent "6.0.0" - find-node-modules "2.0.0" - find-root "1.1.0" - fs-extra "8.1.0" - glob "7.1.4" - inquirer "6.5.0" - is-utf8 "^0.2.1" - lodash "4.17.15" - minimist "1.2.5" - strip-bom "4.0.0" - strip-json-comments "3.0.1" - compare-func@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" @@ -5495,25 +5475,6 @@ init-package-json@^1.10.3: validate-npm-package-license "^3.0.1" validate-npm-package-name "^3.0.0" -inquirer@6.5.0: - version "6.5.0" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz#2303317efc9a4ea7ec2e2df6f86569b734accf42" - integrity sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - inquirer@6.5.2, inquirer@^6.2.0, inquirer@^6.5.2: version "6.5.2" resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" @@ -6748,7 +6709,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: +lodash@^4.17.12, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1: version "4.17.19" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== From 3ab85e2fd02dcc3252cc4930f75c3cce69938353 Mon Sep 17 00:00:00 2001 From: Armano Date: Wed, 20 Jan 2021 22:13:07 +0100 Subject: [PATCH 8/8] fix: correct issue with mac-os tab completion --- @commitlint/prompt/src/inquirer/InputCustomPrompt.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts b/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts index 8ac0ec3dd5..779f2b1db1 100644 --- a/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts +++ b/@commitlint/prompt/src/inquirer/InputCustomPrompt.ts @@ -56,11 +56,9 @@ export default class InputCustomPrompt< * @see https://nodejs.org/api/readline.html#readline_rl_line */ updateLine(line: string): void { - /* eslint-disable @typescript-eslint/ban-ts-comment */ - // @ts-ignore - this.rl.line = line; - // @ts-ignore - this.rl.write(null, {ctrl: true, name: 'e'}); + this.rl.write(null as any, {ctrl: true, name: 'b'}); + this.rl.write(null as any, {ctrl: true, name: 'd'}); + this.rl.write(line.substr(this.rl.line.length)); } onKeyPress2(e: KeyDescriptor): void {