-
Notifications
You must be signed in to change notification settings - Fork 905
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(prompt): rewrite codebase to use inquirer - UPDATED with current…
… master (#2697) * feat(prompt): rewrite codebase to use inquirer * fix(prompt): simplify logic used to compute maxLength * test(prompt): add basic input test * fix(prompt): small code refactor * fix: correct linting issues, add missing dependencies * fix: add missing tsconfig reference * fix: update lock file after merge * fix: correct issue with mac-os tab completion * chore: code review * fix: integrate review feedback * style: prettier Co-authored-by: Armano <armano2@users.noreply.github.com>
- Loading branch information
1 parent
42b3984
commit 5105f43
Showing
16 changed files
with
474 additions
and
580 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,10 @@ | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore | ||
import vorpal from 'vorpal'; | ||
import input from './input'; | ||
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 commit callback to execute with complete commit message | ||
* @return {void} | ||
*/ | ||
export function prompter(_: unknown, commit: Commit): void { | ||
input(vorpal).then((message) => { | ||
export function prompter(cz: typeof inquirer, commit: Commit): void { | ||
input(cz.prompt).then((message) => { | ||
commit(message); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import {Answers, PromptModule, QuestionCollection} from 'inquirer'; | ||
/// <reference path="./inquirer/inquirer.d.ts" /> | ||
import {input} from './input'; | ||
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<string, Record<string, unknown>>): PromptModule { | ||
const prompt = async (questions: QuestionCollection): Promise<any> => { | ||
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}`); | ||
} | ||
const validate = promptConfig.validate; | ||
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/// <reference path="./inquirer.d.ts" /> | ||
import chalk from 'chalk'; | ||
import inquirer from 'inquirer'; | ||
import InputPrompt from 'inquirer/lib/prompts/input'; | ||
import observe from 'inquirer/lib/utils/events'; | ||
import {Interface as ReadlineInterface, Key} from 'readline'; | ||
import type {Subscription} from 'rxjs/internal/Subscription'; | ||
|
||
import Answers = inquirer.Answers; | ||
import InputCustomOptions = inquirer.InputCustomOptions; | ||
import SuccessfulPromptStateData = inquirer.prompts.SuccessfulPromptStateData; | ||
|
||
interface KeyDescriptor { | ||
value: string; | ||
key: Key; | ||
} | ||
|
||
export default class InputCustomPrompt< | ||
TQuestion extends InputCustomOptions = InputCustomOptions | ||
> extends InputPrompt<TQuestion> { | ||
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)); | ||
} | ||
|
||
onEnd(state: SuccessfulPromptStateData): void { | ||
this.lineSubscription.unsubscribe(); | ||
super.onEnd(state); | ||
} | ||
|
||
/** | ||
* @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 { | ||
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 { | ||
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 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, {}); | ||
} | ||
|
||
let bottomContent = ''; | ||
|
||
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); | ||
} | ||
} |
Oops, something went wrong.