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
- Loading branch information
Showing
14 changed files
with
385 additions
and
568 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<void> { | ||
const message = await input(vorpal); | ||
export async function prompter( | ||
cz: typeof inquirer, | ||
commit: Commit | ||
): Promise<void> { | ||
const message = await input(cz.prompt); | ||
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
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,150 @@ | ||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference | ||
/// <reference path="./inquirer.d.ts" /> | ||
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<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)); | ||
|
||
this.opt.validate = this.extendedValidate(this.opt.validate); | ||
} | ||
|
||
onEnd(state: SuccessfulPromptStateData): void { | ||
this.lineSubscription.unsubscribe(); | ||
super.onEnd(state); | ||
} | ||
|
||
extendedValidate(validate?: Validator<TQuestion>): Validator<TQuestion> { | ||
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); | ||
} | ||
} |
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,24 @@ | ||
import {Answers, InputQuestionOptions} from 'inquirer'; | ||
|
||
declare module 'inquirer' { | ||
interface InputCustomCompletionOption { | ||
value: string; | ||
description?: string; | ||
} | ||
|
||
export interface InputCustomOptions<T extends Answers = Answers> | ||
extends InputQuestionOptions<T> { | ||
/** | ||
* @inheritdoc | ||
*/ | ||
type?: 'input-custom'; | ||
required?: boolean; | ||
log?(answers?: T): string; | ||
tabCompletion?: InputCustomCompletionOption[]; | ||
maxLength(answers?: T): number; | ||
} | ||
|
||
interface QuestionMap<T extends Answers = Answers> { | ||
'input-custom': InputCustomOptions<T>; | ||
} | ||
} |
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,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'); | ||
}); |
Oops, something went wrong.