Skip to content

Commit

Permalink
feat(prompt): use enquirer directly
Browse files Browse the repository at this point in the history
re #34
  • Loading branch information
cenk1cenk2 committed Jun 2, 2020
1 parent ea461a7 commit b34e9d0
Show file tree
Hide file tree
Showing 8 changed files with 442 additions and 374 deletions.
614 changes: 369 additions & 245 deletions README.md

Large diffs are not rendered by default.

24 changes: 17 additions & 7 deletions examples/get-user-input.example.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { Listr } from '@root/index'
import { Listr } from '../src/index'
import { Logger } from '@utils/logger'

interface Ctx {
input: boolean
input: boolean | Record<string, boolean>
}

const logger = new Logger({ useIcons: false })
Expand All @@ -16,12 +16,19 @@ async function main (): Promise<void> {
task = new Listr<Ctx>([
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<boolean> => ctx.input = await task.prompt<boolean>('Toggle', { message: 'Do you love me?' })
task: async (ctx, task): Promise<Record<string, boolean>> => ctx.input = await task.prompt<{ test: boolean, other: boolean }>([
{
type: 'Toggle', name: 'test', message: 'test input?'
},
{
type: 'Toggle', name: 'other', message: 'other input?'
}
])
},
{
title: 'Now I will show the input value.',
task: (ctx, task): void => {
task.output = String(ctx.input)
task.output = JSON.stringify(ctx.input)
},
options: {
persistentOutput: true
Expand All @@ -41,7 +48,7 @@ async function main (): Promise<void> {
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt<boolean>('Toggle', { message: 'Do you love me?' })
ctx.input = await task.prompt<boolean>({ type: 'Toggle', message: 'Do you love me?' })
// do something
if (ctx.input === false) {
throw new Error(':/')
Expand All @@ -62,7 +69,9 @@ async function main (): Promise<void> {
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt<boolean>('Select', { message: 'Do you love me?', choices: [ 'test', 'test', 'test', 'test' ] })
ctx.input = await task.prompt<boolean>({
type: 'Select', message: 'Do you love me?', choices: [ 'test', 'test', 'test', 'test' ]
})
}
}
], { concurrent: false })
Expand All @@ -79,7 +88,8 @@ async function main (): Promise<void> {
{
title: 'This task will get your input.',
task: async (ctx, task): Promise<void> => {
ctx.input = await task.prompt<boolean>('Survey', {
ctx.input = await task.prompt({
type: 'Survey',
name: 'experience',
message: 'Please rate your experience',
scale: [
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@
"chalk": "^4.0.0",
"cli-cursor": "^3.1.0",
"cli-truncate": "^2.1.0",
"enquirer": "^2.3.5",
"figures": "^3.2.0",
"indent-string": "^4.0.0",
"log-update": "^4.0.0",
Expand All @@ -74,6 +73,7 @@
"@types/uuid": "^7.0.2",
"cz-conventional-changelog": "3.2.0",
"delay": "^4.3.0",
"enquirer": "^2.3.5",
"eslint": "^7.1.0",
"husky": "^4.2.5",
"jest": "^26.0.1",
Expand All @@ -92,4 +92,4 @@
"path": "./node_modules/cz-conventional-changelog"
}
}
}
}
4 changes: 2 additions & 2 deletions src/interfaces/listr.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { DefaultRenderer } from '@renderer/default.renderer'
import { SilentRenderer } from '@renderer/silent.renderer'
import { VerboseRenderer } from '@renderer/verbose.renderer'
import { Listr } from '@root/index'
import { PromptOptionsType, PromptTypes } from '@utils/prompt.interface'
import { PromptOptions } from '@utils/prompt.interface'

export type ListrContext = any

Expand Down Expand Up @@ -66,7 +66,7 @@ export interface ListrTaskWrapper<Ctx, Renderer extends ListrRendererFactory> {
report(error: Error): void
skip(message: string): void
run(ctx?: Ctx, task?: ListrTaskWrapper<Ctx, Renderer>): Promise<void>
prompt<T = any, P extends PromptTypes = PromptTypes>(type: P, options: PromptOptionsType<P>): Promise<T>
prompt<T = any>(options: PromptOptions | PromptOptions[]): Promise<T>
stdout(): NodeJS.WritableStream
}

Expand Down
12 changes: 8 additions & 4 deletions src/lib/task-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { stateConstants } from '@interfaces/state.constants'
import { Task } from '@lib/task'
import { Listr } from '@root/index'
import { createPrompt } from '@utils/prompt'
import { PromptOptionsType, PromptTypes } from '@utils/prompt.interface'
import { PromptOptions } from '@utils/prompt.interface'

export class TaskWrapper<Ctx, Renderer extends ListrRendererFactory> implements ListrTaskWrapper<Ctx, Renderer> {
constructor (public task: Task<Ctx, ListrRendererFactory>, public errors: ListrError[]) {}
Expand Down Expand Up @@ -70,12 +70,16 @@ export class TaskWrapper<Ctx, Renderer extends ListrRendererFactory> implements
}
}

public async prompt<T = any, P extends PromptTypes = PromptTypes>(type: P, options: PromptOptionsType<P>): Promise<T> {
public async prompt<T = any>(options: PromptOptions | PromptOptions[]): Promise<T> {
this.task.prompt = true

Object.assign(options, { stdout: this.stdout() })
const response = await createPrompt.bind(this)(options)

return createPrompt.bind(this)(type, options)
if (Object.keys(response).length === 1) {
return response.default
} else {
return response
}
}

public stdout (): NodeJS.WriteStream & NodeJS.WritableStream {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export class Task<Ctx, Renderer extends ListrRendererFactory> extends Subject<Li
// catch prompt error, this was the best i could do without going crazy
if (this.prompt instanceof PromptError) {
// eslint-disable-next-line no-ex-assign
error = new Error('Cancelled the prompt.')
error = new Error(this.prompt.message)
}

// report error
Expand Down
21 changes: 18 additions & 3 deletions src/utils/prompt.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import Enquirer from 'enquirer'

import { PromptError } from '@interfaces/listr.interface'

export type PromptOptions = ArrayPromptOptions | BooleanPromptOptions | StringPromptOptions | NumberPromptOptions | SnippetPromptOptions | SortPromptOptions | BasePromptOptions
export type PromptOptions = Unionize<
{
[K in PromptTypes]-?: { type: K } & PromptOptionsType<K>
}
>

export type Unionize<T extends object> = {
[P in keyof T]: T[P]
}[keyof T]

interface BasePromptOptions {
name?: string | (() => string)
Expand Down Expand Up @@ -70,6 +78,11 @@ interface SortPromptOptions extends BasePromptOptions {
numbered?: boolean
}

interface SurveyPromptOptions extends ArrayPromptOptions {
scale: BasePromptOptions[]
margin: [number, number, number, number]
}

interface QuizPromptOptions extends ArrayPromptOptions {
correctChoice: number
}
Expand Down Expand Up @@ -133,12 +146,14 @@ export type PromptOptionsType<T> = T extends 'AutoComplete'
: T extends 'Sort'
? SortPromptOptions
: T extends 'Survey'
? ArrayPromptOptions
? SurveyPromptOptions
: T extends 'Text'
? StringPromptOptions
: T extends 'Toggle'
? TogglePromptOptions
: any
: T extends Enquirer.Prompt
? any
: any

export interface PromptSettings {
error?: boolean
Expand Down
135 changes: 25 additions & 110 deletions src/utils/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,8 @@
import { Prompt } from 'enquirer'
import {
AutoComplete,
BasicAuth,
Confirm,
Editable,
Form,
Input,
Invisible,
List,
MultiSelect,
Numeral,
Password,
Quiz,
Scale,
Select,
Snippet,
Sort,
Survey,
Text,
Toggle
} from 'enquirer/lib/prompts'
import { PromptOptions, PromptSettings } from './prompt.interface'
import { PromptError } from '@interfaces/listr.interface'
import { TaskWrapper } from '@root/lib/task-wrapper'

import { PromptOptionsType, PromptSettings, PromptTypes } from './prompt.interface'
import { ListrError, PromptError } from '@interfaces/listr.interface'
import { TaskWrapper } from '@lib/task-wrapper'

export function newPrompt<T extends PromptTypes> (type: T, options: PromptOptionsType<T>): Prompt {
let prompt: Prompt
switch (type.toString().toLocaleLowerCase()) {
case 'autocomplete':
prompt = new AutoComplete(options)
break
case 'basicauth':
prompt = new BasicAuth(options)
break
case 'confirm':
prompt = new Confirm(options)
break
case 'editable':
prompt = new Editable(options)
break
case 'form':
prompt = new Form(options)
break
case 'input':
prompt = new Input(options)
break
case 'invisible':
prompt = new Invisible(options)
break
case 'list':
prompt = new List(options)
break
case 'multiselect':
prompt = new MultiSelect(options)
break
case 'numeral':
prompt = new Numeral(options)
break
case 'password':
prompt = new Password(options)
break
case 'quiz':
prompt = new Quiz(options)
break
case 'scale':
prompt = new Scale(options)
break
case 'select':
prompt = new Select(options)
break
case 'snippet':
prompt = new Snippet(options)
break
case 'sort':
prompt = new Sort(options)
break
case 'survey':
prompt = new Survey(options)
break
case 'text':
prompt = new Text(options)
break
case 'toggle':
prompt = new Toggle(options)
break
default:
throw new ListrError('No prompt type this was not supposed to happen.')
}
return prompt
}

type PromptClass = new (options: any) => Prompt
function isPromptClass (SomeClass: any): SomeClass is PromptClass {
try {
new SomeClass({})
return true
} catch {
return false
}
}

export function createPrompt<T extends PromptTypes> (type: T | PromptClass, options: PromptOptionsType<T>, settings?: PromptSettings): Promise<any> {
export async function createPrompt (options: PromptOptions | PromptOptions[], settings?: PromptSettings): Promise<any> {
// override cancel callback
let cancelCallback: PromptSettings['cancelCallback']
if (settings?.cancelCallback) {
Expand All @@ -110,18 +11,32 @@ export function createPrompt<T extends PromptTypes> (type: T | PromptClass, opti
cancelCallback = defaultCancelCallback
}

// if this is a custom prompt
if (isPromptClass(type)) {
return new type(options).on('cancel', cancelCallback.bind(this)).run()
} else {
return newPrompt(type, options).on('cancel', cancelCallback.bind(this)).run()
if (!Array.isArray(options)) {
options = options = [ { ...options, name: 'default' } ]
} else if (options.length === 1) {
options = options.reduce((o, option) => {
return [ ...o, Object.assign(option, { name: 'default' }) ]
}, [])
}

options = options.reduce((o, option) => {
return [ ...o, Object.assign(option, { stdout: this.stdout(), onCancel: cancelCallback.bind(this)(settings) }) ]
}, [])

try {
const { prompt } = (await import('enquirer') as any).default
// if this is a custom prompt
return prompt(options as any)
} catch (e) {
this.task.prompt = new PromptError('Enquirer is a peer dependency that must be installed seperately.')
}
}

function defaultCancelCallback (settings: PromptSettings): string | Error | PromptError {
function defaultCancelCallback (settings: PromptSettings): string | Error | PromptError | void {
const errorMsg = 'Cancelled prompt.'

if (settings?.error === true) {
throw new PromptError(errorMsg)
throw new Error(errorMsg)
} else if (this instanceof TaskWrapper) {
this.task.prompt = new PromptError(errorMsg)
} else {
Expand Down

0 comments on commit b34e9d0

Please sign in to comment.