diff --git a/packages/inquirer/src/ui/baseUI.mts b/packages/inquirer/src/ui/baseUI.mts deleted file mode 100644 index a9330798b..000000000 --- a/packages/inquirer/src/ui/baseUI.mts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Base interface class other can inherits from - */ - -export default class UI { - onClose?: () => void; - - constructor() { - this.onForceClose = this.onForceClose.bind(this); - - // Make sure new prompt start on a newline when closing - process.on('exit', this.onForceClose); - } - - /** - * Handle the ^C exit - */ - onForceClose(): void { - this.close(); - process.kill(process.pid, 'SIGINT'); - console.log(''); - } - - /** - * Close the interface and cleanup listeners - */ - close() { - // Remove events listeners - process.removeListener('exit', this.onForceClose); - - if (typeof this.onClose === 'function') { - this.onClose(); - } - } -} diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index 285fa81d6..a4489ee32 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -13,7 +13,6 @@ import { lastValueFrom, } from 'rxjs'; import runAsync from 'run-async'; -import Base from './baseUI.mjs'; import MuteStream from 'mute-stream'; import type { InquirerReadline } from '@inquirer/type'; import ansiEscapes from 'ansi-escapes'; @@ -64,22 +63,22 @@ const _ = { * Resolve a question property value if it is passed as a function. * This method will overwrite the property on the question object with the received value. */ -function fetchAsyncQuestionProperty, T extends keyof Q>( +function fetchAsyncQuestionProperty>( question: Q, - prop: T, - answers: Answers, + prop: string, + answers: A, ) { - const value = question[prop]; - if (typeof value !== 'function') { - return of(question); + if (prop in question) { + const propGetter = question[prop as keyof Q]; + if (typeof propGetter === 'function') { + return from( + runAsync(propGetter as (...args: any[]) => any)(answers).then((value) => { + return Object.assign(question, { [prop]: value }); + }), + ); + } } - - return from( - runAsync(value as (...args: any[]) => any)(answers).then((value: Q[T]) => { - question[prop] = value; - return question; - }), - ); + return of(question); } export interface PromptBase { @@ -161,9 +160,9 @@ function setupReadlineOptions(opt: StreamOptions = {}) { }; } -function isQuestionMap( - questions: QuestionArray | QuestionAnswerMap | Question, -): questions is QuestionAnswerMap { +function isQuestionMap( + questions: QuestionArray | QuestionAnswerMap | Question, +): questions is QuestionAnswerMap { return Object.values(questions).every( (maybeQuestion) => typeof maybeQuestion === 'object' && @@ -185,47 +184,43 @@ function isPromptConstructor( /** * Base interface class other can inherits from */ -export default class PromptsRunner extends Base { +export default class PromptsRunner { prompts: PromptCollection; - answers: Partial = {}; - process: Observable; + answers: Partial = {}; + process: Observable = EMPTY; + onClose?: () => void; opt?: StreamOptions; rl?: InquirerReadline; constructor(prompts: PromptCollection, opt?: StreamOptions) { - super(); this.opt = opt; this.prompts = prompts; - - this.process = EMPTY; } run( questions: - | QuestionArray - | QuestionAnswerMap - | QuestionObservable - | Question, - answers?: Partial, - ): Promise & { ui: PromptsRunner } { + | QuestionArray + | QuestionAnswerMap + | QuestionObservable + | Question, + answers?: Partial, + ): Promise & { ui: PromptsRunner } { // Keep global reference to the answers this.answers = typeof answers === 'object' ? { ...answers } : {}; - let obs: Observable>; + let obs: Observable>; if (Array.isArray(questions)) { obs = from(questions); } else if (isObservable(questions)) { obs = questions; - } else if (isQuestionMap(questions)) { + } else if (isQuestionMap(questions)) { // Case: Called with a set of { name: question } obs = from( - Object.entries(questions).map(([name, question]): Question => { - // @ts-expect-error TODO should be fixable. - return { - ...question, - name, - }; - }), + Object.entries(questions).map( + ([name, question]: [string, Omit, 'name'>]): Question => { + return Object.assign({}, question, { name }) as Question; + }, + ), ); } else { // Case: Called with a single question config @@ -244,7 +239,7 @@ export default class PromptsRunner extends Base { ).then( () => this.onCompletion(), (error) => this.onError(error), - ) as Promise; + ) as Promise; return Object.assign(promise, { ui: this }); } @@ -263,31 +258,27 @@ export default class PromptsRunner extends Base { return Promise.reject(error); } - processQuestion(question: Question) { + processQuestion(question: Question) { question = { ...question }; return defer(() => { const obs = of(question); return obs.pipe( - concatMap(this.setDefaultType.bind(this)), - concatMap(this.filterIfRunnable.bind(this)), + concatMap(this.setDefaultType), + concatMap(this.filterIfRunnable), concatMap((question) => fetchAsyncQuestionProperty(question, 'message', this.answers), ), concatMap((question) => - // @ts-expect-error question type is too loose fetchAsyncQuestionProperty(question, 'default', this.answers), ), concatMap((question) => - // @ts-expect-error question type is too loose fetchAsyncQuestionProperty(question, 'choices', this.answers), ), concatMap((question) => { - // @ts-expect-error question type is too loose - const { choices } = question; - if (Array.isArray(choices)) { + if ('choices' in question) { // @ts-expect-error question type is too loose - question.choices = choices.map((choice) => { + question.choices = question.choices.map((choice) => { if (typeof choice === 'string') { return { name: choice, value: choice }; } @@ -302,7 +293,7 @@ export default class PromptsRunner extends Base { }); } - fetchAnswer(question: Question) { + fetchAnswer(question: Question) { const prompt = this.prompts[question.type]; if (prompt == null) { @@ -326,6 +317,9 @@ export default class PromptsRunner extends Base { }; this.onClose = onClose; this.rl = rl; + + // Make sure new prompt start on a newline when closing + process.on('exit', this.onForceClose); rl.on('SIGINT', this.onForceClose); const activePrompt = new prompt(question, rl, this.answers); @@ -350,16 +344,37 @@ export default class PromptsRunner extends Base { ); } - setDefaultType(question: Question): Observable> { + /** + * Handle the ^C exit + */ + onForceClose = () => { + this.close(); + process.kill(process.pid, 'SIGINT'); + console.log(''); + }; + + /** + * Close the interface and cleanup listeners + */ + close = () => { + // Remove events listeners + process.removeListener('exit', this.onForceClose); + + if (typeof this.onClose === 'function') { + this.onClose(); + } + }; + + setDefaultType = (question: Question): Observable> => { // Default type to input if (!this.prompts[question.type]) { question.type = 'input'; } return defer(() => of(question)); - } + }; - filterIfRunnable(question: Question): Observable> { + filterIfRunnable = (question: Question): Observable> => { if ( question.askAnswered !== true && _.get(this.answers, question.name) !== undefined @@ -384,7 +399,7 @@ export default class PromptsRunner extends Base { } return; }), - ).pipe(filter((val): val is Question => val != null)), + ).pipe(filter((val): val is Question => val != null)), ); - } + }; }