From df13190e17d72f668af79714c3262c41e3a36c9f Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Mon, 8 Jul 2024 17:29:03 -0400 Subject: [PATCH] Fix(inquirer): Improve ReturnType of inquirer.prompt (not perfect, but better) --- packages/inquirer/src/index.mts | 17 +++---- packages/inquirer/src/types.mts | 62 ++++++++++++++---------- packages/inquirer/src/ui/prompt.mts | 20 ++++---- packages/inquirer/test/inquirer.test.mts | 26 +++++----- 4 files changed, 65 insertions(+), 60 deletions(-) diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index ffb256b80..79d786fcc 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -15,6 +15,7 @@ import { editor, Separator, } from '@inquirer/prompts'; +import type { Prettify } from '@inquirer/type'; import { default as PromptsRunner } from './ui/prompt.mjs'; import type { PromptCollection, @@ -48,23 +49,21 @@ const defaultPrompts: PromptCollection = { * Create a new self-contained prompt module. */ export function createPromptModule(opt?: StreamOptions) { - function promptModule( + function promptModule( questions: - | Question + | QuestionArray | QuestionAnswerMap | QuestionObservable - | QuestionArray, + | Question, answers?: Partial, - ): Promise & { ui: PromptsRunner } { + ): Promise> & { ui: PromptsRunner } { const runner = new PromptsRunner(promptModule.prompts, opt); try { return runner.run(questions, answers); } catch (error) { - const promise = Promise.reject(error); - // @ts-expect-error Monkey patch the UI on the promise object so - promise.ui = runner; - return promise as Promise & { ui: PromptsRunner }; + const promise = Promise.reject(error); + return Object.assign(promise, { ui: runner }); } } @@ -85,7 +84,7 @@ export function createPromptModule(opt?: StreamOptions) { * Register the defaults provider prompts */ promptModule.restoreDefaultPrompts = function () { - this.prompts = { ...defaultPrompts }; + promptModule.prompts = { ...defaultPrompts }; }; promptModule.restoreDefaultPrompts(); diff --git a/packages/inquirer/src/types.mts b/packages/inquirer/src/types.mts index ad08fed5c..391abdaa9 100644 --- a/packages/inquirer/src/types.mts +++ b/packages/inquirer/src/types.mts @@ -13,43 +13,55 @@ import { import type { Prettify } from '@inquirer/type'; import { Observable } from 'rxjs'; -export type Answers = { [key: string]: any }; - -interface QuestionMap { - input: { type: 'input' } & Parameters[0]; - select: { type: 'select' } & Parameters[0]; - /** @deprecated Prompt type `list` is renamed to `select` */ - list: { type: 'list' } & Parameters[0]; - number: { type: 'number' } & Parameters[0]; - confirm: { type: 'confirm' } & Parameters[0]; - rawlist: { type: 'rawlist' } & Parameters[0]; - expand: { type: 'expand' } & Parameters[0]; - checkbox: { type: 'checkbox' } & Parameters[0]; - password: { type: 'password' } & Parameters[0]; - editor: { type: 'editor' } & Parameters[0]; -} +// eslint-disable-next-line @typescript-eslint/ban-types +type LiteralUnion = T | (F & {}); +type KeyUnion = LiteralUnion>; + +export type Answers = { + [key: string]: any; +}; -type whenFunction = +type whenFunction = | ((answers: Partial) => boolean | Promise) | ((this: { async: () => () => void }, answers: Partial) => void); -type InquirerFields = { - name: keyof T; +type InquirerFields = { + name: KeyUnion; when?: boolean | whenFunction; askAnswered?: boolean; }; -export type Question = QuestionMap[keyof QuestionMap] & - InquirerFields; +interface QuestionMap { + input: Prettify<{ type: 'input' } & Parameters[0] & InquirerFields>; + select: Prettify<{ type: 'select' } & Parameters[0] & InquirerFields>; + list: Prettify<{ type: 'list' } & Parameters[0] & InquirerFields>; + number: Prettify<{ type: 'number' } & Parameters[0] & InquirerFields>; + confirm: Prettify< + { type: 'confirm' } & Parameters[0] & InquirerFields + >; + rawlist: Prettify< + { type: 'rawlist' } & Parameters[0] & InquirerFields + >; + expand: Prettify<{ type: 'expand' } & Parameters[0] & InquirerFields>; + checkbox: Prettify< + { type: 'checkbox' } & Parameters[0] & InquirerFields + >; + password: Prettify< + { type: 'password' } & Parameters[0] & InquirerFields + >; + editor: Prettify<{ type: 'editor' } & Parameters[0] & InquirerFields>; +} + +export type Question = QuestionMap[keyof QuestionMap]; -export type QuestionAnswerMap = Record< - keyof T, - Omit, 'name'> +export type QuestionAnswerMap = Record< + KeyUnion, + Prettify, 'name'>> >; -export type QuestionArray = Array>; +export type QuestionArray = Question[]; -export type QuestionObservable = Observable>; +export type QuestionObservable = Observable>; export type StreamOptions = Prettify< Parameters[1] & { skipTTYChecks?: boolean } diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index e1584ea0f..285fa81d6 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -161,8 +161,8 @@ function setupReadlineOptions(opt: StreamOptions = {}) { }; } -function isQuestionMap( - questions: Question | QuestionAnswerMap | QuestionArray, +function isQuestionMap( + questions: QuestionArray | QuestionAnswerMap | Question, ): questions is QuestionAnswerMap { return Object.values(questions).every( (maybeQuestion) => @@ -185,7 +185,7 @@ function isPromptConstructor( /** * Base interface class other can inherits from */ -export default class PromptsRunner extends Base { +export default class PromptsRunner extends Base { prompts: PromptCollection; answers: Partial = {}; process: Observable; @@ -202,12 +202,12 @@ export default class PromptsRunner extends Base { run( questions: - | Question + | QuestionArray | QuestionAnswerMap | QuestionObservable - | QuestionArray, + | Question, answers?: Partial, - ): Promise & { ui: PromptsRunner } { + ): Promise & { ui: PromptsRunner } { // Keep global reference to the answers this.answers = typeof answers === 'object' ? { ...answers } : {}; @@ -244,11 +244,9 @@ export default class PromptsRunner extends Base { ).then( () => this.onCompletion(), (error) => this.onError(error), - ); + ) as Promise; - // @ts-expect-error Monkey patch the UI on the promise object so - promise.ui = this; - return promise as Promise & { ui: PromptsRunner }; + return Object.assign(promise, { ui: this }); } /** @@ -386,7 +384,7 @@ export default class PromptsRunner extends Base { } return; }), - ).pipe(filter((val): val is Question => val != null)), + ).pipe(filter((val): val is Question => val != null)), ); } } diff --git a/packages/inquirer/test/inquirer.test.mts b/packages/inquirer/test/inquirer.test.mts index dbfdc4002..1a0be0c43 100644 --- a/packages/inquirer/test/inquirer.test.mts +++ b/packages/inquirer/test/inquirer.test.mts @@ -156,12 +156,10 @@ describe('inquirer.prompt', () => { }); it('should take a single prompt and return answer', async () => { - const config = { + const answers = await inquirer.prompt({ type: 'stub', name: 'q1', - }; - - const answers = await inquirer.prompt(config); + }); expect(answers).toEqual({ q1: 'bar' }); }); @@ -352,7 +350,7 @@ describe('inquirer.prompt', () => { } inquirer.registerPrompt('stubSelect', FakeSelect); - const prompts = [ + await inquirer.prompt([ { type: 'stub', name: 'name1', @@ -363,19 +361,18 @@ describe('inquirer.prompt', () => { type: 'stubSelect', name: 'name', message: 'message', - choices(answers) { + choices(answers: { name1: string }) { expect(answers.name1).toEqual('bar'); return stubChoices; }, }, - ]; - - await inquirer.prompt(prompts); + ]); }); it('should expose the Reactive interface', async () => { const spy = vi.fn(); - const prompts = [ + + const promise = inquirer.prompt([ { type: 'stub', name: 'name1', @@ -388,9 +385,7 @@ describe('inquirer.prompt', () => { message: 'message', answer: 'doe', }, - ]; - - const promise = inquirer.prompt(prompts); + ]); promise.ui.process.subscribe(spy); await promise; @@ -616,7 +611,7 @@ describe('inquirer.prompt', () => { }); it('should not run prompt if nested answer exists for question', async () => { - const answers = await inquirer.prompt( + const answers = await inquirer.prompt<{ prefilled: { nested: string } }>( [ { type: 'input', @@ -624,6 +619,7 @@ describe('inquirer.prompt', () => { when: throwFunc.bind(undefined, 'when'), validate: throwFunc.bind(undefined, 'validate'), transformer: throwFunc.bind(undefined, 'transformer'), + // @ts-expect-error ignoring this unused field for test purpose filter: throwFunc.bind(undefined, 'filter'), message: 'message', default: 'newValue', @@ -633,7 +629,7 @@ describe('inquirer.prompt', () => { prefilled: { nested: 'prefilled' }, }, ); - expect(answers['prefilled'].nested).toEqual('prefilled'); + expect(answers.prefilled.nested).toEqual('prefilled'); }); it('should run prompt if answer exists for question and askAnswered is set', async () => {