From 44aff67c755e2ab6209d35a4a80812c4d2f1a1ec Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Sun, 1 Sep 2024 20:25:27 -0300 Subject: [PATCH 01/15] simplify promptModule return --- packages/inquirer/src/index.mts | 9 ++------- packages/inquirer/src/ui/prompt.mts | 8 +++----- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index 6bedbeaf6..0de17eb7f 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -93,13 +93,8 @@ export function createPromptModule(opt?: StreamOptions) { ): PromptReturnType { const runner = new PromptsRunner(promptModule.prompts, opt); - try { - return runner.run(questions, answers); - } catch (error) { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - const promise = Promise.reject(error); - return Object.assign(promise, { ui: runner }); - } + const promptPromise = runner.run(questions, answers); + return Object.assign(promptPromise, { ui: runner }); } promptModule.prompts = { ...defaultPrompts }; diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index 7d4d65fe5..d331244e5 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -212,14 +212,14 @@ export default class PromptsRunner { this.prompts = prompts; } - run( + async run( questions: | QuestionArray | QuestionAnswerMap | QuestionObservable | Question, answers?: Partial, - ): Promise & { ui: PromptsRunner } { + ): Promise { // Keep global reference to the answers this.answers = typeof answers === 'object' ? { ...answers } : {}; @@ -244,7 +244,7 @@ export default class PromptsRunner { this.process = obs.pipe(concatMap((question) => this.processQuestion(question))); - const promise = lastValueFrom( + return lastValueFrom( this.process.pipe( reduce((answersObj, answer: { name: string; answer: unknown }) => { _.set(answersObj, answer.name, answer.answer); @@ -255,8 +255,6 @@ export default class PromptsRunner { () => this.onCompletion(), (error: Error) => this.onError(error), ) as Promise; - - return Object.assign(promise, { ui: this }); } /** From c9f07474c4123f07f12b42b83ca6d84a92f032c0 Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Thu, 29 Aug 2024 16:56:03 -0300 Subject: [PATCH 02/15] feat: export prompt config types --- packages/checkbox/src/index.mts | 2 +- packages/confirm/src/index.mts | 2 +- packages/editor/src/index.mts | 2 +- packages/expand/src/index.mts | 2 +- packages/input/src/index.mts | 2 +- packages/number/src/index.mts | 2 +- packages/password/src/index.mts | 2 +- packages/prompts/src/index.mts | 10 ++++++++++ packages/rawlist/src/index.mts | 2 +- packages/search/src/index.mts | 2 +- packages/select/src/index.mts | 2 +- 11 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index 8ac07a5c3..be6d306fc 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -72,7 +72,7 @@ type NormalizedChoice = { checked: boolean; }; -type CheckboxConfig< +export type CheckboxConfig< Value, ChoicesObject = | ReadonlyArray diff --git a/packages/confirm/src/index.mts b/packages/confirm/src/index.mts index ed01ec53a..8e92bd1b7 100644 --- a/packages/confirm/src/index.mts +++ b/packages/confirm/src/index.mts @@ -9,7 +9,7 @@ import { } from '@inquirer/core'; import type { PartialDeep } from '@inquirer/type'; -type ConfirmConfig = { +export type ConfirmConfig = { message: string; default?: boolean; transformer?: (value: boolean) => string; diff --git a/packages/editor/src/index.mts b/packages/editor/src/index.mts index 055273cbf..7f09e1472 100644 --- a/packages/editor/src/index.mts +++ b/packages/editor/src/index.mts @@ -12,7 +12,7 @@ import { } from '@inquirer/core'; import type { PartialDeep, InquirerReadline } from '@inquirer/type'; -type EditorConfig = { +export type EditorConfig = { message: string; default?: string; postfix?: string; diff --git a/packages/expand/src/index.mts b/packages/expand/src/index.mts index d1ec68aef..a416a2ad8 100644 --- a/packages/expand/src/index.mts +++ b/packages/expand/src/index.mts @@ -59,7 +59,7 @@ type NormalizedChoice = { key: Key; }; -type ExpandConfig< +export type ExpandConfig< Value, ChoicesObject = readonly { key: Key; name: string }[] | readonly Choice[], > = { diff --git a/packages/input/src/index.mts b/packages/input/src/index.mts index 9127a4c91..7fe55717b 100644 --- a/packages/input/src/index.mts +++ b/packages/input/src/index.mts @@ -10,7 +10,7 @@ import { } from '@inquirer/core'; import type { PartialDeep } from '@inquirer/type'; -type InputConfig = { +export type InputConfig = { message: string; default?: string; required?: boolean; diff --git a/packages/number/src/index.mts b/packages/number/src/index.mts index f75e3d1fd..755a49376 100644 --- a/packages/number/src/index.mts +++ b/packages/number/src/index.mts @@ -10,7 +10,7 @@ import { } from '@inquirer/core'; import type { PartialDeep } from '@inquirer/type'; -type NumberConfig = { +export type NumberConfig = { message: string; default?: number; min?: number; diff --git a/packages/password/src/index.mts b/packages/password/src/index.mts index 8a4b627ef..e8ea1fe04 100644 --- a/packages/password/src/index.mts +++ b/packages/password/src/index.mts @@ -10,7 +10,7 @@ import { import ansiEscapes from 'ansi-escapes'; import type { PartialDeep } from '@inquirer/type'; -type PasswordConfig = { +export type PasswordConfig = { message: string; mask?: boolean | string; validate?: (value: string) => boolean | string | Promise; diff --git a/packages/prompts/src/index.mts b/packages/prompts/src/index.mts index 19263757b..8a9e53eb9 100644 --- a/packages/prompts/src/index.mts +++ b/packages/prompts/src/index.mts @@ -1,10 +1,20 @@ export { default as checkbox, Separator } from '@inquirer/checkbox'; +export * from '@inquirer/checkbox'; export { default as editor } from '@inquirer/editor'; +export * from '@inquirer/editor'; export { default as confirm } from '@inquirer/confirm'; +export * from '@inquirer/confirm'; export { default as input } from '@inquirer/input'; +export * from '@inquirer/input'; export { default as number } from '@inquirer/number'; +export * from '@inquirer/number'; export { default as expand } from '@inquirer/expand'; +export * from '@inquirer/expand'; export { default as rawlist } from '@inquirer/rawlist'; +export * from '@inquirer/rawlist'; export { default as password } from '@inquirer/password'; +export * from '@inquirer/password'; export { default as search } from '@inquirer/search'; +export * from '@inquirer/search'; export { default as select } from '@inquirer/select'; +export * from '@inquirer/select'; diff --git a/packages/rawlist/src/index.mts b/packages/rawlist/src/index.mts index 4744cc44f..ee723fb33 100644 --- a/packages/rawlist/src/index.mts +++ b/packages/rawlist/src/index.mts @@ -28,7 +28,7 @@ type NormalizedChoice = { key: string; }; -type RawlistConfig< +export type RawlistConfig< Value, ChoicesObject = | ReadonlyArray diff --git a/packages/search/src/index.mts b/packages/search/src/index.mts index 879caf17c..b70bf6cc5 100644 --- a/packages/search/src/index.mts +++ b/packages/search/src/index.mts @@ -53,7 +53,7 @@ type NormalizedChoice = { disabled: boolean | string; }; -type SearchConfig< +export type SearchConfig< Value, ChoicesObject = | ReadonlyArray diff --git a/packages/select/src/index.mts b/packages/select/src/index.mts index 4a3959c1e..687988ff5 100644 --- a/packages/select/src/index.mts +++ b/packages/select/src/index.mts @@ -57,7 +57,7 @@ type NormalizedChoice = { disabled: boolean | string; }; -type SelectConfig< +export type SelectConfig< Value, ChoicesObject = | ReadonlyArray From fffae75f78b5c91624d765ee16e5ba38ed37bfff Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Mon, 2 Sep 2024 01:48:07 -0300 Subject: [PATCH 03/15] feat: add type tests --- eslint.config.js | 11 ++ packages/inquirer/type-test/checkbox.mts | 67 +++++++++++ packages/inquirer/type-test/editor.mts | 23 ++++ packages/inquirer/type-test/expand.mts | 39 +++++++ packages/inquirer/type-test/hierarchical.mts | 104 ++++++++++++++++++ packages/inquirer/type-test/input.mts | 51 +++++++++ packages/inquirer/type-test/list.mts | 38 +++++++ packages/inquirer/type-test/nested-call.mts | 20 ++++ packages/inquirer/type-test/password.mts | 30 +++++ packages/inquirer/type-test/pizza.mts | 98 +++++++++++++++++ packages/inquirer/type-test/rawlist.mts | 32 ++++++ packages/inquirer/type-test/recursive.mts | 34 ++++++ .../type-test/rx-observable-array.mts | 47 ++++++++ .../type-test/rx-observable-create.mts | 40 +++++++ packages/inquirer/type-test/tsconfig.json | 8 ++ packages/inquirer/type-test/when.mts | 41 +++++++ 16 files changed, 683 insertions(+) create mode 100644 packages/inquirer/type-test/checkbox.mts create mode 100644 packages/inquirer/type-test/editor.mts create mode 100644 packages/inquirer/type-test/expand.mts create mode 100644 packages/inquirer/type-test/hierarchical.mts create mode 100644 packages/inquirer/type-test/input.mts create mode 100644 packages/inquirer/type-test/list.mts create mode 100644 packages/inquirer/type-test/nested-call.mts create mode 100644 packages/inquirer/type-test/password.mts create mode 100644 packages/inquirer/type-test/pizza.mts create mode 100644 packages/inquirer/type-test/rawlist.mts create mode 100644 packages/inquirer/type-test/recursive.mts create mode 100644 packages/inquirer/type-test/rx-observable-array.mts create mode 100644 packages/inquirer/type-test/rx-observable-create.mts create mode 100644 packages/inquirer/type-test/tsconfig.json create mode 100644 packages/inquirer/type-test/when.mts diff --git a/eslint.config.js b/eslint.config.js index 5b5873ed2..b2045d591 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -80,5 +80,16 @@ export default tseslint.config( ], }, }, + { + files: ['packages/inquirer/type-test/**'], + rules: { + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + }, + }, eslintPluginPrettierRecommended, ); diff --git a/packages/inquirer/type-test/checkbox.mts b/packages/inquirer/type-test/checkbox.mts new file mode 100644 index 000000000..3035516d4 --- /dev/null +++ b/packages/inquirer/type-test/checkbox.mts @@ -0,0 +1,67 @@ +import inquirer from '../src/index.mjs'; + +/** + * Checkbox list examples + */ +inquirer + .prompt([ + { + type: 'checkbox', + message: 'Select toppings', + name: 'toppings', + choices: [ + new inquirer.Separator(' = The Meats = '), + { + value: 'Pepperoni', + }, + { + value: 'Ham', + }, + { + value: 'Ground Meat', + }, + { + value: 'Bacon', + }, + new inquirer.Separator(' = The Cheeses = '), + { + value: 'Mozzarella', + checked: true, + }, + { + value: 'Cheddar', + }, + { + value: 'Parmesan', + }, + new inquirer.Separator(' = The usual ='), + { + value: 'Mushroom', + }, + { + value: 'Tomato', + }, + new inquirer.Separator(' = The extras = '), + { + value: 'Pineapple', + }, + { + value: 'Olives', + disabled: 'out of stock', + }, + { + value: 'Extra cheese', + }, + ], + validate(answer) { + if (answer.length === 0) { + return 'You must choose at least one topping.'; + } + + return true; + }, + }, + ]) + .then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); + }); diff --git a/packages/inquirer/type-test/editor.mts b/packages/inquirer/type-test/editor.mts new file mode 100644 index 000000000..c87d925cb --- /dev/null +++ b/packages/inquirer/type-test/editor.mts @@ -0,0 +1,23 @@ +import inquirer from '../src/index.mjs'; + +/** + * Editor prompt example + */ + +inquirer + .prompt([ + { + type: 'editor', + name: 'bio', + message: 'Please write a short bio of at least 3 lines.', + validate(text) { + if (text.split('\n').length < 3) { + return 'Must be at least 3 lines.'; + } + return true; + }, + }, + ]) + .then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); + }); diff --git a/packages/inquirer/type-test/expand.mts b/packages/inquirer/type-test/expand.mts new file mode 100644 index 000000000..26cdc45d2 --- /dev/null +++ b/packages/inquirer/type-test/expand.mts @@ -0,0 +1,39 @@ +import inquirer from '../src/index.mjs'; + +/** + * Expand list examples + */ +inquirer + .prompt([ + { + type: 'expand', + message: 'Conflict on `file.js`: ', + name: 'overwrite', + choices: [ + { + key: 'y', + name: 'Overwrite', + value: 'overwrite', + }, + { + key: 'a', + name: 'Overwrite this one and all next', + value: 'overwrite_all', + }, + { + key: 'd', + name: 'Show diff', + value: 'diff', + }, + // new inquirer.Separator(), + { + key: 'x', + name: 'Abort', + value: 'abort', + }, + ], + }, + ]) + .then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); + }); diff --git a/packages/inquirer/type-test/hierarchical.mts b/packages/inquirer/type-test/hierarchical.mts new file mode 100644 index 000000000..563c78698 --- /dev/null +++ b/packages/inquirer/type-test/hierarchical.mts @@ -0,0 +1,104 @@ +import inquirer from '../src/index.mjs'; + +/** + * Represents answers provided by the user. + */ +interface RPGAnswers { + /** + * The weapon chosen by the user. + */ + weapon: string; + + /** + * The direction chosen by the user. + */ + direction: 'Forward' | 'Right' | 'Left' | 'Back'; +} + +function main() { + console.log('You find youself in a small room, there is a door in front of you.'); + exitHouse(); +} + +function exitHouse() { + /** + * Heirarchical conversation example + */ + inquirer + .prompt({ + type: 'list', + name: 'direction', + message: 'Which direction would you like to go?', + choices: ['Forward', 'Right', 'Left', 'Back'], + }) + .then((answers) => { + if (answers.direction === 'Forward') { + console.log('You find yourself in a forest'); + console.log( + 'There is a wolf in front of you; a friendly looking dwarf to the right and an impasse to the left.', + ); + encounter1(); + } else { + console.log('You cannot go that way. Try again'); + exitHouse(); + } + }); +} + +function encounter1() { + inquirer.prompt(directionsPrompt).then((answers) => { + const direction = answers.direction; + if (direction === 'Forward') { + console.log('You attempt to fight the wolf'); + console.log( + 'Theres a stick and some stones lying around you could use as a weapon', + ); + encounter2b(); + } else if (direction === 'Right') { + console.log('You befriend the dwarf'); + console.log('He helps you kill the wolf. You can now move forward'); + encounter2a(); + } else { + console.log('You cannot go that way'); + encounter1(); + } + }); +} + +function encounter2a() { + inquirer.prompt(directionsPrompt).then((answers) => { + const direction = answers.direction; + if (direction === 'Forward') { + let output = 'You find a painted wooden sign that says:'; + output += ' \n'; + output += ' ____ _____ ____ _____ \n'; + output += '(_ _)( _ )( _ \\( _ ) \n'; + output += ' )( )(_)( )(_) ))(_)( \n'; + output += ' (__) (_____)(____/(_____) \n'; + console.log(output); + } else { + console.log('You cannot go that way'); + encounter2a(); + } + }); +} + +function encounter2b() { + inquirer + .prompt({ + type: 'list', + name: 'weapon', + message: 'Pick one', + choices: [ + 'Use the stick', + 'Grab a large rock', + 'Try and make a run for it', + 'Attack the wolf unarmed', + ], + }) + .then(() => { + console.log('The wolf mauls you. You die. The end.'); + }); +} + +main(); diff --git a/packages/inquirer/type-test/input.mts b/packages/inquirer/type-test/input.mts new file mode 100644 index 000000000..aa54fc3b6 --- /dev/null +++ b/packages/inquirer/type-test/input.mts @@ -0,0 +1,51 @@ +import inquirer from '../src/index.mjs'; + +/** + * Input prompt example + */ +inquirer + .prompt([ + { + type: 'input', + name: 'first_name', + message: "What's your first name", + }, + { + type: 'input', + name: 'last_name', + message: "What's your last name", + default() { + return 'Doe'; + }, + }, + { + type: 'input', + name: 'fav_color', + message: "What's your favorite color", + transformer(color, flags) { + if (flags.isFinal) { + return color + '!'; + } + + return color; + }, + }, + { + type: 'input', + name: 'phone', + message: "What's your phone number", + validate(value) { + const pass = value.match( + /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, + ); + if (pass) { + return true; + } + + return 'Please enter a valid phone number'; + }, + }, + ]) + .then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); + }); diff --git a/packages/inquirer/type-test/list.mts b/packages/inquirer/type-test/list.mts new file mode 100644 index 000000000..0d6133e2f --- /dev/null +++ b/packages/inquirer/type-test/list.mts @@ -0,0 +1,38 @@ +import inquirer from '../src/index.mjs'; + +/** + * List prompt example + */ +inquirer + .prompt([ + { + type: 'list', + name: 'theme', + message: 'What do you want to do?', + choices: [ + 'Order a pizza', + 'Make a reservation', + new inquirer.Separator(), + 'Ask for opening hours', + /* + { + name: 'Contact support', + disabled: 'Unavailable at this time', + }, + */ + 'Talk to the receptionist', + ], + }, + { + type: 'list', + name: 'size', + message: 'What size do you need?', + choices: ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], + filter(val) { + return val.toLowerCase(); + }, + }, + ]) + .then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); + }); diff --git a/packages/inquirer/type-test/nested-call.mts b/packages/inquirer/type-test/nested-call.mts new file mode 100644 index 000000000..b9ae8036b --- /dev/null +++ b/packages/inquirer/type-test/nested-call.mts @@ -0,0 +1,20 @@ +import inquirer from '../src/index.mjs'; + +/** + * Nested Inquirer call + */ +inquirer + .prompt({ + type: 'list', + name: 'chocolate', + message: () => "What's your favorite chocolate?", + choices: ['Mars', 'Oh Henry', 'Hershey'], + }) + .then(() => { + inquirer.prompt({ + type: 'list', + name: 'beverage', + message: 'And your favorite beverage?', + choices: ['Pepsi', 'Coke', '7up', 'Mountain Dew', 'Red Bull'], + }); + }); diff --git a/packages/inquirer/type-test/password.mts b/packages/inquirer/type-test/password.mts new file mode 100644 index 000000000..e2ccb7b68 --- /dev/null +++ b/packages/inquirer/type-test/password.mts @@ -0,0 +1,30 @@ +import inquirer from '../src/index.mjs'; + +/** + * Password prompt example + */ +const requireLetterAndNumber = (value: string) => { + if (/\w/.test(value) && /\d/.test(value)) { + return true; + } + + return 'Password need to have at least a letter and a number'; +}; + +inquirer + .prompt([ + { + type: 'password', + message: 'Enter a password', + name: 'password1', + validate: requireLetterAndNumber, + }, + { + type: 'password', + message: 'Enter a masked password', + name: 'password2', + mask: '*', + validate: requireLetterAndNumber, + }, + ]) + .then((answers) => console.log(JSON.stringify(answers, null, ' '))); diff --git a/packages/inquirer/type-test/pizza.mts b/packages/inquirer/type-test/pizza.mts new file mode 100644 index 000000000..407509f7a --- /dev/null +++ b/packages/inquirer/type-test/pizza.mts @@ -0,0 +1,98 @@ +import inquirer from '../src/index.mjs'; + +/** + * Pizza delivery prompt example + * run example by writing `node pizza.js` in your console + */ +console.log('Hi, welcome to Node Pizza'); + +inquirer + .prompt([ + { + type: 'confirm', + name: 'toBeDelivered', + message: 'Is this for delivery?', + default: false, + }, + { + type: 'input', + name: 'phone', + message: "What's your phone number?", + validate(value) { + const pass = value.match( + /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, + ); + if (pass) { + return true; + } + + return 'Please enter a valid phone number'; + }, + }, + { + type: 'list', + name: 'size', + message: 'What size do you need?', + choices: ['Large', 'Medium', 'Small'], + filter(val) { + return val.toLowerCase(); + }, + }, + { + type: 'input', + name: 'quantity', + message: 'How many do you need?', + validate(value) { + const valid = !Number.isNaN(Number.parseFloat(value)); + return valid || 'Please enter a number'; + }, + filter: Number, + }, + { + type: 'expand', + name: 'toppings', + message: 'What about the toppings?', + choices: [ + { + key: 'p', + name: 'Pepperoni and cheese', + value: 'PepperoniCheese', + }, + { + key: 'a', + name: 'All dressed', + value: 'alldressed', + }, + { + key: 'w', + name: 'Hawaiian', + value: 'hawaiian', + }, + ], + }, + { + type: 'rawlist', + name: 'beverage', + message: 'You also get a free 2L beverage', + choices: ['Pepsi', '7up', 'Coke'], + }, + { + type: 'input', + name: 'comments', + message: 'Any comments on your purchase experience?', + default: 'Nope, all good!', + }, + { + type: 'list', + name: 'prize', + message: 'For leaving a comment, you get a freebie', + choices: ['cake', 'fries'], + when(answers) { + return answers.comments !== 'Nope, all good!'; + }, + }, + ]) + .then((answers) => { + console.log('\nOrder receipt:'); + console.log(JSON.stringify(answers, null, ' ')); + }); diff --git a/packages/inquirer/type-test/rawlist.mts b/packages/inquirer/type-test/rawlist.mts new file mode 100644 index 000000000..e83cb5370 --- /dev/null +++ b/packages/inquirer/type-test/rawlist.mts @@ -0,0 +1,32 @@ +import inquirer from '../src/index.mjs'; + +/** + * Raw List prompt example + */ +inquirer + .prompt([ + { + type: 'rawlist', + name: 'theme', + message: 'What do you want to do?', + choices: [ + 'Order a pizza', + 'Make a reservation', + new inquirer.Separator(), + 'Ask opening hours', + 'Talk to the receptionist', + ], + }, + { + type: 'rawlist', + name: 'size', + message: 'What size do you need', + choices: ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], + filter(val) { + return val.toLowerCase(); + }, + }, + ]) + .then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); + }); diff --git a/packages/inquirer/type-test/recursive.mts b/packages/inquirer/type-test/recursive.mts new file mode 100644 index 000000000..f119767aa --- /dev/null +++ b/packages/inquirer/type-test/recursive.mts @@ -0,0 +1,34 @@ +import inquirer from '../src/index.mjs'; + +/** + * Recursive prompt example + * Allows user to choose when to exit prompt + */ +const output: string[] = []; + +function ask() { + inquirer + .prompt([ + { + type: 'input', + name: 'tvShow', + message: "What's your favorite TV show?", + }, + { + type: 'confirm', + name: 'askAgain', + message: 'Want to enter another TV show favorite (just hit enter for YES)?', + default: true, + }, + ]) + .then((answers) => { + output.push(answers.tvShow); + if (answers.askAgain) { + ask(); + } else { + console.log('Your favorite TV Shows:', output.join(', ')); + } + }); +} + +ask(); diff --git a/packages/inquirer/type-test/rx-observable-array.mts b/packages/inquirer/type-test/rx-observable-array.mts new file mode 100644 index 000000000..a35a6ec56 --- /dev/null +++ b/packages/inquirer/type-test/rx-observable-array.mts @@ -0,0 +1,47 @@ +import inquirer from '../src/index.mjs'; +import { from } from 'rxjs'; + +inquirer + .prompt( + from([ + { + type: 'input', + name: 'first_name', + message: "What's your first name", + }, + { + type: 'input', + name: 'last_name', + message: "What's your last name", + default() { + return 'Doe'; + }, + }, + { + type: 'input', + name: 'phone', + message: "What's your phone number", + validate(value) { + const pass = value.match( + /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, + ); + if (pass) { + return true; + } + + return 'Please enter a valid phone number'; + }, + }, + ]), + ) + .ui.process.subscribe( + (ans) => { + console.log('Answer is:', ans); + }, + (err) => { + console.log('Error:', err); + }, + () => { + console.log('Completed'); + }, + ); diff --git a/packages/inquirer/type-test/rx-observable-create.mts b/packages/inquirer/type-test/rx-observable-create.mts new file mode 100644 index 000000000..47798ca3d --- /dev/null +++ b/packages/inquirer/type-test/rx-observable-create.mts @@ -0,0 +1,40 @@ +import inquirer, { Question } from '../src/index.mjs'; +import { Observable, Observer } from 'rxjs'; + +const observe = Observable.create((obs: Observer) => { + obs.next({ + type: 'input', + name: 'first_name', + message: "What's your first name", + }); + + obs.next({ + type: 'input', + name: 'last_name', + message: "What's your last name", + default() { + return 'Doe'; + }, + }); + + obs.next({ + type: 'input', + name: 'phone', + message: "What's your phone number", + validate(value) { + const pass = value.match( + /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, + ); + if (pass) { + return true; + } + + return 'Please enter a valid phone number'; + }, + }); + obs.complete(); +}); + +inquirer.prompt(observe).then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); +}); diff --git a/packages/inquirer/type-test/tsconfig.json b/packages/inquirer/type-test/tsconfig.json new file mode 100644 index 000000000..9a4657889 --- /dev/null +++ b/packages/inquirer/type-test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.test.json", + "include": ["./**/*.mts"], + "compilerOptions": { + "skipLibCheck": false, + "declaration": true + } +} diff --git a/packages/inquirer/type-test/when.mts b/packages/inquirer/type-test/when.mts new file mode 100644 index 000000000..ccda64198 --- /dev/null +++ b/packages/inquirer/type-test/when.mts @@ -0,0 +1,41 @@ +import inquirer, { Answers } from '../src/index.mjs'; + +function likesFood(aFood: string) { + return (answers: Answers) => { + return answers[aFood]; + }; +} + +inquirer + .prompt([ + { + type: 'confirm', + name: 'bacon', + message: 'Do you like bacon?', + }, + { + type: 'input', + name: 'favorite', + message: 'Bacon lover, what is your favorite type of bacon?', + when(answers) { + return answers.bacon; + }, + }, + { + type: 'confirm', + name: 'pizza', + message: 'Ok... Do you like pizza?', + when(answers) { + return !likesFood('bacon')(answers); + }, + }, + { + type: 'input', + name: 'favorite', + message: 'Whew! What is your favorite type of pizza?', + when: likesFood('pizza'), + }, + ]) + .then((answers) => { + console.log(JSON.stringify(answers, null, ' ')); + }); From 033f79b33c0b462bbdec7b6a85f8ef87c0a26645 Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Mon, 2 Sep 2024 01:56:45 -0300 Subject: [PATCH 04/15] Revert "feat: add type tests" This reverts commit 621aa2d988592ef21c92fd6f69f4e61353ac81b8. --- eslint.config.js | 11 -- packages/inquirer/type-test/checkbox.mts | 67 ----------- packages/inquirer/type-test/editor.mts | 23 ---- packages/inquirer/type-test/expand.mts | 39 ------- packages/inquirer/type-test/hierarchical.mts | 104 ------------------ packages/inquirer/type-test/input.mts | 51 --------- packages/inquirer/type-test/list.mts | 38 ------- packages/inquirer/type-test/nested-call.mts | 20 ---- packages/inquirer/type-test/password.mts | 30 ----- packages/inquirer/type-test/pizza.mts | 98 ----------------- packages/inquirer/type-test/rawlist.mts | 32 ------ packages/inquirer/type-test/recursive.mts | 34 ------ .../type-test/rx-observable-array.mts | 47 -------- .../type-test/rx-observable-create.mts | 40 ------- packages/inquirer/type-test/tsconfig.json | 8 -- packages/inquirer/type-test/when.mts | 41 ------- 16 files changed, 683 deletions(-) delete mode 100644 packages/inquirer/type-test/checkbox.mts delete mode 100644 packages/inquirer/type-test/editor.mts delete mode 100644 packages/inquirer/type-test/expand.mts delete mode 100644 packages/inquirer/type-test/hierarchical.mts delete mode 100644 packages/inquirer/type-test/input.mts delete mode 100644 packages/inquirer/type-test/list.mts delete mode 100644 packages/inquirer/type-test/nested-call.mts delete mode 100644 packages/inquirer/type-test/password.mts delete mode 100644 packages/inquirer/type-test/pizza.mts delete mode 100644 packages/inquirer/type-test/rawlist.mts delete mode 100644 packages/inquirer/type-test/recursive.mts delete mode 100644 packages/inquirer/type-test/rx-observable-array.mts delete mode 100644 packages/inquirer/type-test/rx-observable-create.mts delete mode 100644 packages/inquirer/type-test/tsconfig.json delete mode 100644 packages/inquirer/type-test/when.mts diff --git a/eslint.config.js b/eslint.config.js index b2045d591..5b5873ed2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -80,16 +80,5 @@ export default tseslint.config( ], }, }, - { - files: ['packages/inquirer/type-test/**'], - rules: { - '@typescript-eslint/no-unsafe-argument': 'off', - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-call': 'off', - '@typescript-eslint/no-floating-promises': 'off', - '@typescript-eslint/no-unsafe-return': 'off', - '@typescript-eslint/no-unsafe-member-access': 'off', - }, - }, eslintPluginPrettierRecommended, ); diff --git a/packages/inquirer/type-test/checkbox.mts b/packages/inquirer/type-test/checkbox.mts deleted file mode 100644 index 3035516d4..000000000 --- a/packages/inquirer/type-test/checkbox.mts +++ /dev/null @@ -1,67 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Checkbox list examples - */ -inquirer - .prompt([ - { - type: 'checkbox', - message: 'Select toppings', - name: 'toppings', - choices: [ - new inquirer.Separator(' = The Meats = '), - { - value: 'Pepperoni', - }, - { - value: 'Ham', - }, - { - value: 'Ground Meat', - }, - { - value: 'Bacon', - }, - new inquirer.Separator(' = The Cheeses = '), - { - value: 'Mozzarella', - checked: true, - }, - { - value: 'Cheddar', - }, - { - value: 'Parmesan', - }, - new inquirer.Separator(' = The usual ='), - { - value: 'Mushroom', - }, - { - value: 'Tomato', - }, - new inquirer.Separator(' = The extras = '), - { - value: 'Pineapple', - }, - { - value: 'Olives', - disabled: 'out of stock', - }, - { - value: 'Extra cheese', - }, - ], - validate(answer) { - if (answer.length === 0) { - return 'You must choose at least one topping.'; - } - - return true; - }, - }, - ]) - .then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); - }); diff --git a/packages/inquirer/type-test/editor.mts b/packages/inquirer/type-test/editor.mts deleted file mode 100644 index c87d925cb..000000000 --- a/packages/inquirer/type-test/editor.mts +++ /dev/null @@ -1,23 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Editor prompt example - */ - -inquirer - .prompt([ - { - type: 'editor', - name: 'bio', - message: 'Please write a short bio of at least 3 lines.', - validate(text) { - if (text.split('\n').length < 3) { - return 'Must be at least 3 lines.'; - } - return true; - }, - }, - ]) - .then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); - }); diff --git a/packages/inquirer/type-test/expand.mts b/packages/inquirer/type-test/expand.mts deleted file mode 100644 index 26cdc45d2..000000000 --- a/packages/inquirer/type-test/expand.mts +++ /dev/null @@ -1,39 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Expand list examples - */ -inquirer - .prompt([ - { - type: 'expand', - message: 'Conflict on `file.js`: ', - name: 'overwrite', - choices: [ - { - key: 'y', - name: 'Overwrite', - value: 'overwrite', - }, - { - key: 'a', - name: 'Overwrite this one and all next', - value: 'overwrite_all', - }, - { - key: 'd', - name: 'Show diff', - value: 'diff', - }, - // new inquirer.Separator(), - { - key: 'x', - name: 'Abort', - value: 'abort', - }, - ], - }, - ]) - .then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); - }); diff --git a/packages/inquirer/type-test/hierarchical.mts b/packages/inquirer/type-test/hierarchical.mts deleted file mode 100644 index 563c78698..000000000 --- a/packages/inquirer/type-test/hierarchical.mts +++ /dev/null @@ -1,104 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Represents answers provided by the user. - */ -interface RPGAnswers { - /** - * The weapon chosen by the user. - */ - weapon: string; - - /** - * The direction chosen by the user. - */ - direction: 'Forward' | 'Right' | 'Left' | 'Back'; -} - -function main() { - console.log('You find youself in a small room, there is a door in front of you.'); - exitHouse(); -} - -function exitHouse() { - /** - * Heirarchical conversation example - */ - inquirer - .prompt({ - type: 'list', - name: 'direction', - message: 'Which direction would you like to go?', - choices: ['Forward', 'Right', 'Left', 'Back'], - }) - .then((answers) => { - if (answers.direction === 'Forward') { - console.log('You find yourself in a forest'); - console.log( - 'There is a wolf in front of you; a friendly looking dwarf to the right and an impasse to the left.', - ); - encounter1(); - } else { - console.log('You cannot go that way. Try again'); - exitHouse(); - } - }); -} - -function encounter1() { - inquirer.prompt(directionsPrompt).then((answers) => { - const direction = answers.direction; - if (direction === 'Forward') { - console.log('You attempt to fight the wolf'); - console.log( - 'Theres a stick and some stones lying around you could use as a weapon', - ); - encounter2b(); - } else if (direction === 'Right') { - console.log('You befriend the dwarf'); - console.log('He helps you kill the wolf. You can now move forward'); - encounter2a(); - } else { - console.log('You cannot go that way'); - encounter1(); - } - }); -} - -function encounter2a() { - inquirer.prompt(directionsPrompt).then((answers) => { - const direction = answers.direction; - if (direction === 'Forward') { - let output = 'You find a painted wooden sign that says:'; - output += ' \n'; - output += ' ____ _____ ____ _____ \n'; - output += '(_ _)( _ )( _ \\( _ ) \n'; - output += ' )( )(_)( )(_) ))(_)( \n'; - output += ' (__) (_____)(____/(_____) \n'; - console.log(output); - } else { - console.log('You cannot go that way'); - encounter2a(); - } - }); -} - -function encounter2b() { - inquirer - .prompt({ - type: 'list', - name: 'weapon', - message: 'Pick one', - choices: [ - 'Use the stick', - 'Grab a large rock', - 'Try and make a run for it', - 'Attack the wolf unarmed', - ], - }) - .then(() => { - console.log('The wolf mauls you. You die. The end.'); - }); -} - -main(); diff --git a/packages/inquirer/type-test/input.mts b/packages/inquirer/type-test/input.mts deleted file mode 100644 index aa54fc3b6..000000000 --- a/packages/inquirer/type-test/input.mts +++ /dev/null @@ -1,51 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Input prompt example - */ -inquirer - .prompt([ - { - type: 'input', - name: 'first_name', - message: "What's your first name", - }, - { - type: 'input', - name: 'last_name', - message: "What's your last name", - default() { - return 'Doe'; - }, - }, - { - type: 'input', - name: 'fav_color', - message: "What's your favorite color", - transformer(color, flags) { - if (flags.isFinal) { - return color + '!'; - } - - return color; - }, - }, - { - type: 'input', - name: 'phone', - message: "What's your phone number", - validate(value) { - const pass = value.match( - /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, - ); - if (pass) { - return true; - } - - return 'Please enter a valid phone number'; - }, - }, - ]) - .then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); - }); diff --git a/packages/inquirer/type-test/list.mts b/packages/inquirer/type-test/list.mts deleted file mode 100644 index 0d6133e2f..000000000 --- a/packages/inquirer/type-test/list.mts +++ /dev/null @@ -1,38 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * List prompt example - */ -inquirer - .prompt([ - { - type: 'list', - name: 'theme', - message: 'What do you want to do?', - choices: [ - 'Order a pizza', - 'Make a reservation', - new inquirer.Separator(), - 'Ask for opening hours', - /* - { - name: 'Contact support', - disabled: 'Unavailable at this time', - }, - */ - 'Talk to the receptionist', - ], - }, - { - type: 'list', - name: 'size', - message: 'What size do you need?', - choices: ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], - filter(val) { - return val.toLowerCase(); - }, - }, - ]) - .then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); - }); diff --git a/packages/inquirer/type-test/nested-call.mts b/packages/inquirer/type-test/nested-call.mts deleted file mode 100644 index b9ae8036b..000000000 --- a/packages/inquirer/type-test/nested-call.mts +++ /dev/null @@ -1,20 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Nested Inquirer call - */ -inquirer - .prompt({ - type: 'list', - name: 'chocolate', - message: () => "What's your favorite chocolate?", - choices: ['Mars', 'Oh Henry', 'Hershey'], - }) - .then(() => { - inquirer.prompt({ - type: 'list', - name: 'beverage', - message: 'And your favorite beverage?', - choices: ['Pepsi', 'Coke', '7up', 'Mountain Dew', 'Red Bull'], - }); - }); diff --git a/packages/inquirer/type-test/password.mts b/packages/inquirer/type-test/password.mts deleted file mode 100644 index e2ccb7b68..000000000 --- a/packages/inquirer/type-test/password.mts +++ /dev/null @@ -1,30 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Password prompt example - */ -const requireLetterAndNumber = (value: string) => { - if (/\w/.test(value) && /\d/.test(value)) { - return true; - } - - return 'Password need to have at least a letter and a number'; -}; - -inquirer - .prompt([ - { - type: 'password', - message: 'Enter a password', - name: 'password1', - validate: requireLetterAndNumber, - }, - { - type: 'password', - message: 'Enter a masked password', - name: 'password2', - mask: '*', - validate: requireLetterAndNumber, - }, - ]) - .then((answers) => console.log(JSON.stringify(answers, null, ' '))); diff --git a/packages/inquirer/type-test/pizza.mts b/packages/inquirer/type-test/pizza.mts deleted file mode 100644 index 407509f7a..000000000 --- a/packages/inquirer/type-test/pizza.mts +++ /dev/null @@ -1,98 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Pizza delivery prompt example - * run example by writing `node pizza.js` in your console - */ -console.log('Hi, welcome to Node Pizza'); - -inquirer - .prompt([ - { - type: 'confirm', - name: 'toBeDelivered', - message: 'Is this for delivery?', - default: false, - }, - { - type: 'input', - name: 'phone', - message: "What's your phone number?", - validate(value) { - const pass = value.match( - /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, - ); - if (pass) { - return true; - } - - return 'Please enter a valid phone number'; - }, - }, - { - type: 'list', - name: 'size', - message: 'What size do you need?', - choices: ['Large', 'Medium', 'Small'], - filter(val) { - return val.toLowerCase(); - }, - }, - { - type: 'input', - name: 'quantity', - message: 'How many do you need?', - validate(value) { - const valid = !Number.isNaN(Number.parseFloat(value)); - return valid || 'Please enter a number'; - }, - filter: Number, - }, - { - type: 'expand', - name: 'toppings', - message: 'What about the toppings?', - choices: [ - { - key: 'p', - name: 'Pepperoni and cheese', - value: 'PepperoniCheese', - }, - { - key: 'a', - name: 'All dressed', - value: 'alldressed', - }, - { - key: 'w', - name: 'Hawaiian', - value: 'hawaiian', - }, - ], - }, - { - type: 'rawlist', - name: 'beverage', - message: 'You also get a free 2L beverage', - choices: ['Pepsi', '7up', 'Coke'], - }, - { - type: 'input', - name: 'comments', - message: 'Any comments on your purchase experience?', - default: 'Nope, all good!', - }, - { - type: 'list', - name: 'prize', - message: 'For leaving a comment, you get a freebie', - choices: ['cake', 'fries'], - when(answers) { - return answers.comments !== 'Nope, all good!'; - }, - }, - ]) - .then((answers) => { - console.log('\nOrder receipt:'); - console.log(JSON.stringify(answers, null, ' ')); - }); diff --git a/packages/inquirer/type-test/rawlist.mts b/packages/inquirer/type-test/rawlist.mts deleted file mode 100644 index e83cb5370..000000000 --- a/packages/inquirer/type-test/rawlist.mts +++ /dev/null @@ -1,32 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Raw List prompt example - */ -inquirer - .prompt([ - { - type: 'rawlist', - name: 'theme', - message: 'What do you want to do?', - choices: [ - 'Order a pizza', - 'Make a reservation', - new inquirer.Separator(), - 'Ask opening hours', - 'Talk to the receptionist', - ], - }, - { - type: 'rawlist', - name: 'size', - message: 'What size do you need', - choices: ['Jumbo', 'Large', 'Standard', 'Medium', 'Small', 'Micro'], - filter(val) { - return val.toLowerCase(); - }, - }, - ]) - .then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); - }); diff --git a/packages/inquirer/type-test/recursive.mts b/packages/inquirer/type-test/recursive.mts deleted file mode 100644 index f119767aa..000000000 --- a/packages/inquirer/type-test/recursive.mts +++ /dev/null @@ -1,34 +0,0 @@ -import inquirer from '../src/index.mjs'; - -/** - * Recursive prompt example - * Allows user to choose when to exit prompt - */ -const output: string[] = []; - -function ask() { - inquirer - .prompt([ - { - type: 'input', - name: 'tvShow', - message: "What's your favorite TV show?", - }, - { - type: 'confirm', - name: 'askAgain', - message: 'Want to enter another TV show favorite (just hit enter for YES)?', - default: true, - }, - ]) - .then((answers) => { - output.push(answers.tvShow); - if (answers.askAgain) { - ask(); - } else { - console.log('Your favorite TV Shows:', output.join(', ')); - } - }); -} - -ask(); diff --git a/packages/inquirer/type-test/rx-observable-array.mts b/packages/inquirer/type-test/rx-observable-array.mts deleted file mode 100644 index a35a6ec56..000000000 --- a/packages/inquirer/type-test/rx-observable-array.mts +++ /dev/null @@ -1,47 +0,0 @@ -import inquirer from '../src/index.mjs'; -import { from } from 'rxjs'; - -inquirer - .prompt( - from([ - { - type: 'input', - name: 'first_name', - message: "What's your first name", - }, - { - type: 'input', - name: 'last_name', - message: "What's your last name", - default() { - return 'Doe'; - }, - }, - { - type: 'input', - name: 'phone', - message: "What's your phone number", - validate(value) { - const pass = value.match( - /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, - ); - if (pass) { - return true; - } - - return 'Please enter a valid phone number'; - }, - }, - ]), - ) - .ui.process.subscribe( - (ans) => { - console.log('Answer is:', ans); - }, - (err) => { - console.log('Error:', err); - }, - () => { - console.log('Completed'); - }, - ); diff --git a/packages/inquirer/type-test/rx-observable-create.mts b/packages/inquirer/type-test/rx-observable-create.mts deleted file mode 100644 index 47798ca3d..000000000 --- a/packages/inquirer/type-test/rx-observable-create.mts +++ /dev/null @@ -1,40 +0,0 @@ -import inquirer, { Question } from '../src/index.mjs'; -import { Observable, Observer } from 'rxjs'; - -const observe = Observable.create((obs: Observer) => { - obs.next({ - type: 'input', - name: 'first_name', - message: "What's your first name", - }); - - obs.next({ - type: 'input', - name: 'last_name', - message: "What's your last name", - default() { - return 'Doe'; - }, - }); - - obs.next({ - type: 'input', - name: 'phone', - message: "What's your phone number", - validate(value) { - const pass = value.match( - /^([01])?[\s.-]?\(?(\d{3})\)?[\s.-]?(\d{3})[\s.-]?(\d{4})\s?((?:#|ext\.?\s?|x\.?\s?)(?:\d+)?)?$/i, - ); - if (pass) { - return true; - } - - return 'Please enter a valid phone number'; - }, - }); - obs.complete(); -}); - -inquirer.prompt(observe).then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); -}); diff --git a/packages/inquirer/type-test/tsconfig.json b/packages/inquirer/type-test/tsconfig.json deleted file mode 100644 index 9a4657889..000000000 --- a/packages/inquirer/type-test/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../../tsconfig.test.json", - "include": ["./**/*.mts"], - "compilerOptions": { - "skipLibCheck": false, - "declaration": true - } -} diff --git a/packages/inquirer/type-test/when.mts b/packages/inquirer/type-test/when.mts deleted file mode 100644 index ccda64198..000000000 --- a/packages/inquirer/type-test/when.mts +++ /dev/null @@ -1,41 +0,0 @@ -import inquirer, { Answers } from '../src/index.mjs'; - -function likesFood(aFood: string) { - return (answers: Answers) => { - return answers[aFood]; - }; -} - -inquirer - .prompt([ - { - type: 'confirm', - name: 'bacon', - message: 'Do you like bacon?', - }, - { - type: 'input', - name: 'favorite', - message: 'Bacon lover, what is your favorite type of bacon?', - when(answers) { - return answers.bacon; - }, - }, - { - type: 'confirm', - name: 'pizza', - message: 'Ok... Do you like pizza?', - when(answers) { - return !likesFood('bacon')(answers); - }, - }, - { - type: 'input', - name: 'favorite', - message: 'Whew! What is your favorite type of pizza?', - when: likesFood('pizza'), - }, - ]) - .then((answers) => { - console.log(JSON.stringify(answers, null, ' ')); - }); From 43d639d51f8c9170f429a4ee15896d629e4af695 Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Thu, 29 Aug 2024 09:35:36 -0300 Subject: [PATCH 05/15] feat: rework inquirer types --- packages/inquirer/inquirer.test.mts | 62 +++++++------ packages/inquirer/src/index.mts | 81 +++++++++++------ packages/inquirer/src/types.mts | 132 +++++++++++++++------------- packages/inquirer/src/ui/prompt.mts | 103 +++++++++++----------- 4 files changed, 211 insertions(+), 167 deletions(-) diff --git a/packages/inquirer/inquirer.test.mts b/packages/inquirer/inquirer.test.mts index ad26064b9..edad33cb0 100644 --- a/packages/inquirer/inquirer.test.mts +++ b/packages/inquirer/inquirer.test.mts @@ -8,21 +8,28 @@ import os from 'node:os'; import stream from 'node:stream'; import tty from 'node:tty'; import { vi, expect, beforeEach, afterEach, describe, it, expectTypeOf } from 'vitest'; -import { Observable } from 'rxjs'; +import { of } from 'rxjs'; import type { InquirerReadline } from '@inquirer/type'; import inquirer, { type QuestionMap } from './src/index.mjs'; -import type { Answers, Question } from './src/types.mjs'; +import type { Answers } from './src/types.mjs'; import { _ } from './src/ui/prompt.mjs'; declare module './src/index.mjs' { interface QuestionMap { - stub: { answer?: string | boolean; message: string }; + stub: { answer?: string | boolean; message: string; default?: string }; stub2: { answer?: string | boolean; message: string; default: string }; - stubSelect: { choices: { value: string }[] }; + stubSelect: { choices: string[] }; failing: { message: string }; } } +type TestQuestions = { + stub: { answer?: string | boolean; message: string }; + stub2: { answer?: string | boolean; message: string; default: string }; + stubSelect: { choices: string[] }; + failing: { message: string }; +}; + function throwFunc(step: string) { throw new Error(`askAnswered Error ${step}`); } @@ -110,27 +117,24 @@ describe('inquirer.prompt(...)', () => { it('takes an Observable', async () => { const answers = await inquirer.prompt( - new Observable>((subscriber) => { - subscriber.next({ + of( + { type: 'stub', name: 'q1', message: 'message', answer: true, - }); - setTimeout(() => { - subscriber.next({ - type: 'stub', - name: 'q2', - message: 'message', - answer: false, - }); - subscriber.complete(); - }, 30); - }), + } as const, + { + type: 'stub', + name: 'q2', + message: 'message', + answer: false, + } as const, + ), ); expect(answers).toEqual({ q1: true, q2: false }); - expectTypeOf(answers).toEqualTypeOf<{ q1: boolean; q2: boolean }>(); + expectTypeOf(answers).toEqualTypeOf<{ q1: any; q2: any }>(); }); }); @@ -273,7 +277,6 @@ describe('inquirer.prompt(...)', () => { name: 'name2', answer: 'foo', message(answers) { - // @ts-expect-error TODO fix answer types passed in getters. expectTypeOf(answers).toEqualTypeOf>(); expect(answers).toEqual({ name1: 'bar' }); const goOn = this.async(); @@ -299,7 +302,7 @@ describe('inquirer.prompt(...)', () => { type: 'stub', name: 'name', message: 'message', - default(answers: { name1: string }) { + default(answers) { expect(answers.name1).toEqual('bar'); return 'foo'; }, @@ -337,7 +340,6 @@ describe('inquirer.prompt(...)', () => { message: 'message', default(answers) { goesInDefault = true; - // @ts-expect-error TODO fix answer types passed in getters. expectTypeOf(answers).toEqualTypeOf>(); expect(answers).toEqual({ name1: 'bar' }); const goOn = this.async(); @@ -413,7 +415,6 @@ describe('inquirer.prompt(...)', () => { name: 'name', message: 'message', choices(answers) { - // @ts-expect-error TODO fix answer types passed in getters. expectTypeOf(answers).toEqualTypeOf>(); expect(answers).toEqual({ name1: 'bar' }); return stubChoices; @@ -581,7 +582,6 @@ describe('inquirer.prompt(...)', () => { answer: 'answer from running', when(answers) { expect(answers).toEqual({ q1: 'bar' }); - // @ts-expect-error TODO fix answer types passed in getters. expectTypeOf(answers).toEqualTypeOf>(); goesInWhen = true; @@ -773,7 +773,9 @@ describe('Non-TTY checks', () => { }); it('Throw an exception when run in non-tty', async () => { - const localPrompt = inquirer.createPromptModule({ skipTTYChecks: false }); + const localPrompt = inquirer.createPromptModule({ + skipTTYChecks: false, + }); localPrompt.registerPrompt('stub', StubPrompt); const promise = localPrompt([ @@ -787,7 +789,7 @@ describe('Non-TTY checks', () => { }); it("Don't throw an exception when run in non-tty by default ", async () => { - const localPrompt = inquirer.createPromptModule(); + const localPrompt = inquirer.createPromptModule(); localPrompt.registerPrompt('stub', StubPrompt); await localPrompt([ @@ -805,7 +807,9 @@ describe('Non-TTY checks', () => { }); it("Don't throw an exception when run in non-tty and skipTTYChecks is true ", async () => { - const localPrompt = inquirer.createPromptModule({ skipTTYChecks: true }); + const localPrompt = inquirer.createPromptModule({ + skipTTYChecks: true, + }); localPrompt.registerPrompt('stub', StubPrompt); await localPrompt([ @@ -823,7 +827,7 @@ describe('Non-TTY checks', () => { }); it("Don't throw an exception when run in non-tty and custom input is provided async ", async () => { - const localPrompt = inquirer.createPromptModule({ + const localPrompt = inquirer.createPromptModule({ input: new stream.Readable({ // We must have a default read implementation // for this to work, if not it will error out @@ -849,7 +853,7 @@ describe('Non-TTY checks', () => { }); it('Throw an exception when run in non-tty and custom input is provided with skipTTYChecks: false', async () => { - const localPrompt = inquirer.createPromptModule({ + const localPrompt = inquirer.createPromptModule({ input: new stream.Readable(), skipTTYChecks: false, }); @@ -871,7 +875,7 @@ describe('Non-TTY checks', () => { const input = new tty.ReadStream(fs.openSync('/dev/tty', 'r+')); // Uses manually opened tty as input instead of process.stdin - const localPrompt = inquirer.createPromptModule({ + const localPrompt = inquirer.createPromptModule({ input, skipTTYChecks: false, }); diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index 0de17eb7f..cdd32c000 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -16,7 +16,7 @@ import { search, Separator, } from '@inquirer/prompts'; -import type { Prettify, UnionToIntersection } from '@inquirer/type'; +import type { Prettify } from '@inquirer/type'; import { default as PromptsRunner } from './ui/prompt.mjs'; import type { PromptCollection, @@ -25,12 +25,15 @@ import type { } from './ui/prompt.mjs'; import type { Answers, - Question, - QuestionAnswerMap, - QuestionArray, - QuestionObservable, + CustomQuestions, + LegacyQuestion, + Question as BuiltInQuestion, StreamOptions, + QuestionMap, + NamedLegacyQuestion, + Named, } from './types.mjs'; +import { Observable } from 'rxjs'; export type { QuestionMap } from './types.mjs'; @@ -56,42 +59,68 @@ type PromptReturnType = Promise> & { /** * Create a new self-contained prompt module. */ -export function createPromptModule(opt?: StreamOptions) { +export function createPromptModule< + Prompts extends Record> = never, +>(opt?: StreamOptions) { function promptModule< - const AnswerList extends readonly Answers[], + const A extends Answers, PrefilledAnswers extends Answers = object, >( - questions: { [I in keyof AnswerList]: Question }, + questions: ( + | Named>, Extract> + | Named< + CustomQuestions, Prompts>, + Extract + > + )[], answers?: PrefilledAnswers, - ): PromptReturnType>; + ): PromptReturnType>; function promptModule< - const Map extends QuestionAnswerMap, - const A extends Answers>, + const A extends Answers, PrefilledAnswers extends Answers = object, - >(questions: Map, answers?: PrefilledAnswers): PromptReturnType; + >( + questions: { + [name in keyof A]: + | BuiltInQuestion> + | CustomQuestions, Prompts>; + }, + answers?: PrefilledAnswers, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): PromptReturnType>>; function promptModule< const A extends Answers, PrefilledAnswers extends Answers = object, >( - questions: QuestionObservable, + questions: Observable< + | Named>, Extract> + | Named< + CustomQuestions, Prompts>, + Extract + > + >, answers?: PrefilledAnswers, - ): PromptReturnType; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): PromptReturnType>>; function promptModule< - const A extends Answers, + K extends string = string, PrefilledAnswers extends Answers = object, >( - questions: Question, + questions: // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Named & PrefilledAnswers>, K> + // eslint-disable-next-line @typescript-eslint/no-explicit-any + | Named & PrefilledAnswers, Prompts>, K>, answers?: PrefilledAnswers, - ): PromptReturnType; - function promptModule( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ): PromptReturnType>; + function promptModule( questions: - | QuestionArray - | QuestionAnswerMap - | QuestionObservable - | Question, - answers?: Partial, - ): PromptReturnType { - const runner = new PromptsRunner(promptModule.prompts, opt); + | NamedLegacyQuestion[] + | Record> + | Observable> + | NamedLegacyQuestion, + answers?: Partial, + ): PromptReturnType { + const runner = new PromptsRunner(promptModule.prompts, opt); const promptPromise = runner.run(questions, answers); return Object.assign(promptPromise, { ui: runner }); @@ -123,7 +152,7 @@ export function createPromptModule(opt?: StreamOptions) { /** * Public CLI helper interface */ -const prompt = createPromptModule(); +const prompt = createPromptModule>(); // Expose helper functions on the top level for easiest usage by common users function registerPrompt(name: string, newPrompt: LegacyPromptConstructor) { diff --git a/packages/inquirer/src/types.mts b/packages/inquirer/src/types.mts index f531c4973..e4482f77c 100644 --- a/packages/inquirer/src/types.mts +++ b/packages/inquirer/src/types.mts @@ -1,21 +1,19 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { input, - select, - number, - confirm, - rawlist, - expand, - checkbox, - password, - editor, + InputConfig, + SelectConfig, + CheckboxConfig, + ConfirmConfig, + NumberConfig, + RawlistConfig, + ExpandConfig, + PasswordConfig, + EditorConfig, } from '@inquirer/prompts'; -import type { Prettify, KeyUnion, DistributiveMerge, Pick } from '@inquirer/type'; -import { Observable } from 'rxjs'; +import type { DistributiveMerge, Prettify } from '@inquirer/type'; -export type Answers = { - [key in Key]: any; -}; +export type Answers = Record; type AsyncCallbackFunction = ( ...args: [error: null | undefined, value: R] | [error: Error, value: undefined] @@ -23,65 +21,75 @@ type AsyncCallbackFunction = ( type AsyncGetterFunction = ( this: { async: () => AsyncCallbackFunction }, - answers: Partial, + answers: Prettify>, ) => void | R | Promise; +/** + * Allows to inject a custom question type into inquirer module. + * + * @example + * ```ts + * declare module './src/index.mjs' { + * interface QuestionMap { + * custom: { message: string }; + * } + * } + * ``` + * + * Globally defined question types are not correct. + */ export interface QuestionMap { - input: Parameters[0]; - select: Parameters[0]; - /** @deprecated `list` is now named `select` */ - list: Parameters[0]; - number: Parameters[0]; - confirm: Parameters[0]; - rawlist: Parameters[0]; - expand: Parameters[0]; - checkbox: Parameters[0]; - password: Parameters[0]; - editor: Parameters[0]; + // Dummy key to avoid empty object type + __dummy: { message: string }; } -type PromptConfigMap = { - [key in keyof QuestionMap]: Readonly< - DistributiveMerge< - QuestionMap[keyof QuestionMap], - { - type: keyof QuestionMap; - name: KeyUnion; - when?: AsyncGetterFunction> | boolean; - askAnswered?: boolean; - message: - | Pick - | AsyncGetterFunction< - Pick, - Prettify - >; - choices?: - | Pick - | string[] - | AsyncGetterFunction< - Pick | string[], - Prettify - >; - default?: - | Pick - | AsyncGetterFunction< - Pick | string[], - Prettify - >; - } - > - >; +type KeyValueOrAsyncGetterFunction = + T extends Record ? T[k] | AsyncGetterFunction : never; + +export type Named = T & { name: N }; + +export type LegacyQuestion = { + type: Type; + askAnswered?: boolean; + when?: boolean | AsyncGetterFunction; }; -export type Question = PromptConfigMap[keyof PromptConfigMap]; +export type NamedLegacyQuestion = Named< + LegacyQuestion +>; -export type QuestionAnswerMap = Readonly<{ - [name in KeyUnion]: Omit, 'name'>; -}>; +export type LegacyAsyncQuestion< + Type extends string, + Q extends Record, + A extends Answers, +> = DistributiveMerge< + Q, + LegacyQuestion & { + filter?(input: any, answers: A): any; + message: KeyValueOrAsyncGetterFunction; + default?: KeyValueOrAsyncGetterFunction; + choices?: KeyValueOrAsyncGetterFunction; + } +>; -export type QuestionArray = readonly Question[]; +export type Question = + | LegacyAsyncQuestion<'confirm', ConfirmConfig, A> + | LegacyAsyncQuestion<'expand', ExpandConfig>, A> + | LegacyAsyncQuestion<'editor', EditorConfig, A> + | LegacyAsyncQuestion<'input', InputConfig, A> + | LegacyAsyncQuestion<'list', SelectConfig>, A> + | LegacyAsyncQuestion<'number', NumberConfig, A> + | LegacyAsyncQuestion<'password', PasswordConfig, A> + | LegacyAsyncQuestion<'rawlist', RawlistConfig>, A> + | LegacyAsyncQuestion<'select', SelectConfig>, A> + | LegacyAsyncQuestion<'checkbox', CheckboxConfig>, A>; -export type QuestionObservable = Observable>; +export type CustomQuestions< + A extends Answers, + Q extends Record>, +> = { + [key in Extract]: Readonly>; +}[Extract]; 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 d331244e5..15fda93ac 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -18,10 +18,8 @@ import type { InquirerReadline } from '@inquirer/type'; import ansiEscapes from 'ansi-escapes'; import type { Answers, - Question, - QuestionAnswerMap, - QuestionArray, - QuestionObservable, + LegacyQuestion, + NamedLegacyQuestion, StreamOptions, } from '../types.mjs'; @@ -64,11 +62,11 @@ export 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>( +function fetchAsyncQuestionProperty>( question: Q, prop: string, answers: A, -) { +): Observable> { if (prop in question) { const propGetter = question[prop as keyof Q]; if (typeof propGetter === 'function') { @@ -162,21 +160,21 @@ function setupReadlineOptions(opt: StreamOptions = {}) { function isQuestionArray( questions: - | QuestionArray - | QuestionAnswerMap - | QuestionObservable - | Question, -): questions is QuestionArray { + | NamedLegacyQuestion[] + | Record> + | Observable> + | NamedLegacyQuestion, +): questions is NamedLegacyQuestion[] { return Array.isArray(questions); } function isQuestionMap( questions: - | QuestionArray - | QuestionAnswerMap - | QuestionObservable - | Question, -): questions is QuestionAnswerMap { + | NamedLegacyQuestion[] + | Record> + | Observable> + | NamedLegacyQuestion, +): questions is Record> { return Object.values(questions).every( (maybeQuestion) => typeof maybeQuestion === 'object' && @@ -214,28 +212,26 @@ export default class PromptsRunner { async run( questions: - | QuestionArray - | QuestionAnswerMap - | QuestionObservable - | Question, + | NamedLegacyQuestion[] + | Record> + | Observable> + | NamedLegacyQuestion, answers?: Partial, ): Promise { // Keep global reference to the answers this.answers = typeof answers === 'object' ? { ...answers } : {}; - let obs: Observable>; + let obs: Observable>; if (isQuestionArray(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]: [string, Omit, 'name'>]): Question => { - return Object.assign({}, question, { name }) as Question; - }, - ), + Object.entries(questions).map(([name, question]): NamedLegacyQuestion => { + return Object.assign({}, question, { name }); + }), ); } else { // Case: Called with a single question config @@ -271,7 +267,7 @@ export default class PromptsRunner { return Promise.reject(error); } - processQuestion(question: Question) { + processQuestion(question: NamedLegacyQuestion) { question = { ...question }; return defer(() => { const obs = of(question); @@ -288,30 +284,33 @@ export default class PromptsRunner { concatMap((question) => fetchAsyncQuestionProperty(question, 'choices', this.answers), ), - concatMap((question) => { - const { choices } = question; - if (Array.isArray(choices)) { - // @ts-expect-error question type is too loose - question.choices = choices.map( - (choice: string | number | { value?: string; name: string }) => { - if (typeof choice === 'string' || typeof choice === 'number') { - return { name: choice, value: choice }; - } else if (!('value' in choice)) { - return { ...choice, value: choice.name }; - } - return choice; - }, - ); - } - - return of(question); - }), + concatMap( + (question: NamedLegacyQuestion): Observable> => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + let { choices } = question as any; + if (Array.isArray(choices)) { + choices = choices.map( + (choice: string | number | { value?: string; name: string }) => { + if (typeof choice === 'string' || typeof choice === 'number') { + return { name: choice, value: choice }; + } else if (!('value' in choice)) { + return { ...choice, value: choice.name }; + } + return choice; + }, + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + return of({ ...question, choices }); + }, + ), concatMap((question) => this.fetchAnswer(question)), ); }); } - fetchAnswer(question: Question) { + fetchAnswer(question: NamedLegacyQuestion) { const prompt = this.prompts[question.type]; if (prompt == null) { @@ -383,7 +382,9 @@ export default class PromptsRunner { } }; - setDefaultType = (question: Question): Observable> => { + setDefaultType = ( + question: NamedLegacyQuestion, + ): Observable> => { // Default type to input if (!this.prompts[question.type]) { question = Object.assign({}, question, { type: 'input' }); @@ -392,7 +393,9 @@ export default class PromptsRunner { return defer(() => of(question)); }; - filterIfRunnable = (question: Question): Observable> => { + filterIfRunnable = ( + question: NamedLegacyQuestion, + ): Observable> => { if ( question.askAnswered !== true && _.get(this.answers, question.name) !== undefined @@ -417,7 +420,7 @@ export default class PromptsRunner { } return; }), - ).pipe(filter((val): val is Question => val != null)), + ).pipe(filter((val): val is NamedLegacyQuestion => val != null)), ); }; } From fa4ada0a1e875e6ad1aff899fdcbf0454a33f415 Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Mon, 2 Sep 2024 02:09:31 -0300 Subject: [PATCH 06/15] use Context type --- packages/inquirer/src/types.mts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/inquirer/src/types.mts b/packages/inquirer/src/types.mts index e4482f77c..729b9ab1f 100644 --- a/packages/inquirer/src/types.mts +++ b/packages/inquirer/src/types.mts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { - input, InputConfig, SelectConfig, CheckboxConfig, @@ -11,7 +10,7 @@ import type { PasswordConfig, EditorConfig, } from '@inquirer/prompts'; -import type { DistributiveMerge, Prettify } from '@inquirer/type'; +import type { Context, DistributiveMerge, Prettify } from '@inquirer/type'; export type Answers = Record; @@ -91,6 +90,4 @@ export type CustomQuestions< [key in Extract]: Readonly>; }[Extract]; -export type StreamOptions = Prettify< - Parameters[1] & { skipTTYChecks?: boolean } ->; +export type StreamOptions = Prettify; From 545ad89e13df253a94cfa836c60371419dc02ea8 Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Mon, 2 Sep 2024 08:14:08 -0300 Subject: [PATCH 07/15] simplify --- packages/inquirer/src/index.mts | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index cdd32c000..c4002e386 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -62,17 +62,13 @@ type PromptReturnType = Promise> & { export function createPromptModule< Prompts extends Record> = never, >(opt?: StreamOptions) { + type Question = BuiltInQuestion | CustomQuestions; + type NamedQuestion = Named, N>; function promptModule< const A extends Answers, PrefilledAnswers extends Answers = object, >( - questions: ( - | Named>, Extract> - | Named< - CustomQuestions, Prompts>, - Extract - > - )[], + questions: NamedQuestion, Extract>[], answers?: PrefilledAnswers, ): PromptReturnType>; function promptModule< @@ -80,9 +76,7 @@ export function createPromptModule< PrefilledAnswers extends Answers = object, >( questions: { - [name in keyof A]: - | BuiltInQuestion> - | CustomQuestions, Prompts>; + [name in keyof A]: Question>; }, answers?: PrefilledAnswers, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -92,26 +86,17 @@ export function createPromptModule< PrefilledAnswers extends Answers = object, >( questions: Observable< - | Named>, Extract> - | Named< - CustomQuestions, Prompts>, - Extract - > + NamedQuestion, Extract> >, answers?: PrefilledAnswers, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): PromptReturnType>>; + ): PromptReturnType>; function promptModule< - K extends string = string, + const A extends Answers, PrefilledAnswers extends Answers = object, >( - questions: // eslint-disable-next-line @typescript-eslint/no-explicit-any - | Named & PrefilledAnswers>, K> - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | Named & PrefilledAnswers, Prompts>, K>, + questions: NamedQuestion>, answers?: PrefilledAnswers, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): PromptReturnType>; + ): PromptReturnType; function promptModule( questions: | NamedLegacyQuestion[] From 076b072884032a335e0d27f56449f15b310885d5 Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Mon, 2 Sep 2024 08:54:23 -0300 Subject: [PATCH 08/15] drop type from method --- packages/inquirer/src/ui/prompt.mts | 40 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index 15fda93ac..a73a0f047 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -284,27 +284,25 @@ export default class PromptsRunner { concatMap((question) => fetchAsyncQuestionProperty(question, 'choices', this.answers), ), - concatMap( - (question: NamedLegacyQuestion): Observable> => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - let { choices } = question as any; - if (Array.isArray(choices)) { - choices = choices.map( - (choice: string | number | { value?: string; name: string }) => { - if (typeof choice === 'string' || typeof choice === 'number') { - return { name: choice, value: choice }; - } else if (!('value' in choice)) { - return { ...choice, value: choice.name }; - } - return choice; - }, - ); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - return of({ ...question, choices }); - }, - ), + concatMap((question) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + let { choices } = question as any; + if (Array.isArray(choices)) { + choices = choices.map( + (choice: string | number | { value?: string; name: string }) => { + if (typeof choice === 'string' || typeof choice === 'number') { + return { name: choice, value: choice }; + } else if (!('value' in choice)) { + return { ...choice, value: choice.name }; + } + return choice; + }, + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + return of({ ...question, choices }); + }), concatMap((question) => this.fetchAnswer(question)), ); }); From 9fe0f5a47cb025c982bf85732f39528eda49424c Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Mon, 2 Sep 2024 17:25:11 -0300 Subject: [PATCH 09/15] use types from prompts --- packages/inquirer/src/types.mts | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/inquirer/src/types.mts b/packages/inquirer/src/types.mts index 729b9ab1f..35ac68d56 100644 --- a/packages/inquirer/src/types.mts +++ b/packages/inquirer/src/types.mts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { - InputConfig, - SelectConfig, - CheckboxConfig, - ConfirmConfig, - NumberConfig, - RawlistConfig, - ExpandConfig, - PasswordConfig, - EditorConfig, + input, + select, + checkbox, + confirm, + number, + rawlist, + expand, + password, + editor, } from '@inquirer/prompts'; import type { Context, DistributiveMerge, Prettify } from '@inquirer/type'; @@ -72,16 +72,16 @@ export type LegacyAsyncQuestion< >; export type Question = - | LegacyAsyncQuestion<'confirm', ConfirmConfig, A> - | LegacyAsyncQuestion<'expand', ExpandConfig>, A> - | LegacyAsyncQuestion<'editor', EditorConfig, A> - | LegacyAsyncQuestion<'input', InputConfig, A> - | LegacyAsyncQuestion<'list', SelectConfig>, A> - | LegacyAsyncQuestion<'number', NumberConfig, A> - | LegacyAsyncQuestion<'password', PasswordConfig, A> - | LegacyAsyncQuestion<'rawlist', RawlistConfig>, A> - | LegacyAsyncQuestion<'select', SelectConfig>, A> - | LegacyAsyncQuestion<'checkbox', CheckboxConfig>, A>; + | LegacyAsyncQuestion<'confirm', Parameters[0], A> + | LegacyAsyncQuestion<'expand', Parameters[0], A> + | LegacyAsyncQuestion<'editor', Parameters[0], A> + | LegacyAsyncQuestion<'input', Parameters[0], A> + | LegacyAsyncQuestion<'list', Parameters[0], A> + | LegacyAsyncQuestion<'number', Parameters[0], A> + | LegacyAsyncQuestion<'password', Parameters[0], A> + | LegacyAsyncQuestion<'rawlist', Parameters[0], A> + | LegacyAsyncQuestion<'select', Parameters[0], A> + | LegacyAsyncQuestion<'checkbox', Parameters[0], A>; export type CustomQuestions< A extends Answers, From edb4dabb0bdc319639eae1104551be4b335f56cd Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Mon, 2 Sep 2024 17:25:46 -0300 Subject: [PATCH 10/15] Revert "feat: export prompt config types" This reverts commit c9f07474c4123f07f12b42b83ca6d84a92f032c0. --- packages/checkbox/src/index.mts | 2 +- packages/confirm/src/index.mts | 2 +- packages/editor/src/index.mts | 2 +- packages/expand/src/index.mts | 2 +- packages/input/src/index.mts | 2 +- packages/number/src/index.mts | 2 +- packages/password/src/index.mts | 2 +- packages/prompts/src/index.mts | 10 ---------- packages/rawlist/src/index.mts | 2 +- packages/search/src/index.mts | 2 +- packages/select/src/index.mts | 2 +- 11 files changed, 10 insertions(+), 20 deletions(-) diff --git a/packages/checkbox/src/index.mts b/packages/checkbox/src/index.mts index be6d306fc..8ac07a5c3 100644 --- a/packages/checkbox/src/index.mts +++ b/packages/checkbox/src/index.mts @@ -72,7 +72,7 @@ type NormalizedChoice = { checked: boolean; }; -export type CheckboxConfig< +type CheckboxConfig< Value, ChoicesObject = | ReadonlyArray diff --git a/packages/confirm/src/index.mts b/packages/confirm/src/index.mts index 8e92bd1b7..ed01ec53a 100644 --- a/packages/confirm/src/index.mts +++ b/packages/confirm/src/index.mts @@ -9,7 +9,7 @@ import { } from '@inquirer/core'; import type { PartialDeep } from '@inquirer/type'; -export type ConfirmConfig = { +type ConfirmConfig = { message: string; default?: boolean; transformer?: (value: boolean) => string; diff --git a/packages/editor/src/index.mts b/packages/editor/src/index.mts index 7f09e1472..055273cbf 100644 --- a/packages/editor/src/index.mts +++ b/packages/editor/src/index.mts @@ -12,7 +12,7 @@ import { } from '@inquirer/core'; import type { PartialDeep, InquirerReadline } from '@inquirer/type'; -export type EditorConfig = { +type EditorConfig = { message: string; default?: string; postfix?: string; diff --git a/packages/expand/src/index.mts b/packages/expand/src/index.mts index a416a2ad8..d1ec68aef 100644 --- a/packages/expand/src/index.mts +++ b/packages/expand/src/index.mts @@ -59,7 +59,7 @@ type NormalizedChoice = { key: Key; }; -export type ExpandConfig< +type ExpandConfig< Value, ChoicesObject = readonly { key: Key; name: string }[] | readonly Choice[], > = { diff --git a/packages/input/src/index.mts b/packages/input/src/index.mts index 7fe55717b..9127a4c91 100644 --- a/packages/input/src/index.mts +++ b/packages/input/src/index.mts @@ -10,7 +10,7 @@ import { } from '@inquirer/core'; import type { PartialDeep } from '@inquirer/type'; -export type InputConfig = { +type InputConfig = { message: string; default?: string; required?: boolean; diff --git a/packages/number/src/index.mts b/packages/number/src/index.mts index 755a49376..f75e3d1fd 100644 --- a/packages/number/src/index.mts +++ b/packages/number/src/index.mts @@ -10,7 +10,7 @@ import { } from '@inquirer/core'; import type { PartialDeep } from '@inquirer/type'; -export type NumberConfig = { +type NumberConfig = { message: string; default?: number; min?: number; diff --git a/packages/password/src/index.mts b/packages/password/src/index.mts index e8ea1fe04..8a4b627ef 100644 --- a/packages/password/src/index.mts +++ b/packages/password/src/index.mts @@ -10,7 +10,7 @@ import { import ansiEscapes from 'ansi-escapes'; import type { PartialDeep } from '@inquirer/type'; -export type PasswordConfig = { +type PasswordConfig = { message: string; mask?: boolean | string; validate?: (value: string) => boolean | string | Promise; diff --git a/packages/prompts/src/index.mts b/packages/prompts/src/index.mts index 8a9e53eb9..19263757b 100644 --- a/packages/prompts/src/index.mts +++ b/packages/prompts/src/index.mts @@ -1,20 +1,10 @@ export { default as checkbox, Separator } from '@inquirer/checkbox'; -export * from '@inquirer/checkbox'; export { default as editor } from '@inquirer/editor'; -export * from '@inquirer/editor'; export { default as confirm } from '@inquirer/confirm'; -export * from '@inquirer/confirm'; export { default as input } from '@inquirer/input'; -export * from '@inquirer/input'; export { default as number } from '@inquirer/number'; -export * from '@inquirer/number'; export { default as expand } from '@inquirer/expand'; -export * from '@inquirer/expand'; export { default as rawlist } from '@inquirer/rawlist'; -export * from '@inquirer/rawlist'; export { default as password } from '@inquirer/password'; -export * from '@inquirer/password'; export { default as search } from '@inquirer/search'; -export * from '@inquirer/search'; export { default as select } from '@inquirer/select'; -export * from '@inquirer/select'; diff --git a/packages/rawlist/src/index.mts b/packages/rawlist/src/index.mts index ee723fb33..4744cc44f 100644 --- a/packages/rawlist/src/index.mts +++ b/packages/rawlist/src/index.mts @@ -28,7 +28,7 @@ type NormalizedChoice = { key: string; }; -export type RawlistConfig< +type RawlistConfig< Value, ChoicesObject = | ReadonlyArray diff --git a/packages/search/src/index.mts b/packages/search/src/index.mts index b70bf6cc5..879caf17c 100644 --- a/packages/search/src/index.mts +++ b/packages/search/src/index.mts @@ -53,7 +53,7 @@ type NormalizedChoice = { disabled: boolean | string; }; -export type SearchConfig< +type SearchConfig< Value, ChoicesObject = | ReadonlyArray diff --git a/packages/select/src/index.mts b/packages/select/src/index.mts index 687988ff5..4a3959c1e 100644 --- a/packages/select/src/index.mts +++ b/packages/select/src/index.mts @@ -57,7 +57,7 @@ type NormalizedChoice = { disabled: boolean | string; }; -export type SelectConfig< +type SelectConfig< Value, ChoicesObject = | ReadonlyArray From bac0b00ad09c718568fcdea77d1495cbbd9813fc Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Mon, 2 Sep 2024 16:41:25 -0400 Subject: [PATCH 11/15] Simplify naming and exports --- packages/inquirer/inquirer.test.mts | 2 - packages/inquirer/src/index.mts | 20 +++++----- packages/inquirer/src/types.mts | 45 ++++++++++++----------- packages/inquirer/src/ui/prompt.mts | 57 ++++++++++++----------------- 4 files changed, 56 insertions(+), 68 deletions(-) diff --git a/packages/inquirer/inquirer.test.mts b/packages/inquirer/inquirer.test.mts index edad33cb0..8d27b6592 100644 --- a/packages/inquirer/inquirer.test.mts +++ b/packages/inquirer/inquirer.test.mts @@ -635,7 +635,6 @@ describe('inquirer.prompt(...)', () => { it('should not run prompt if answer exists for question', async () => { const answers = await inquirer.prompt( - // @ts-expect-error Passing wrong type on purpose. [ { type: 'input', @@ -655,7 +654,6 @@ describe('inquirer.prompt(...)', () => { it('should not run prompt if nested answer exists for question', async () => { const answers = await inquirer.prompt( - // @ts-expect-error Passing wrong type on purpose. [ { type: 'input', diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index c4002e386..69e72ca0a 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -25,13 +25,11 @@ import type { } from './ui/prompt.mjs'; import type { Answers, - CustomQuestions, - LegacyQuestion, - Question as BuiltInQuestion, + CustomQuestion, + BuiltInQuestion, StreamOptions, QuestionMap, - NamedLegacyQuestion, - Named, + AnyQuestion, } from './types.mjs'; import { Observable } from 'rxjs'; @@ -62,8 +60,8 @@ type PromptReturnType = Promise> & { export function createPromptModule< Prompts extends Record> = never, >(opt?: StreamOptions) { - type Question = BuiltInQuestion | CustomQuestions; - type NamedQuestion = Named, N>; + type Question = BuiltInQuestion | CustomQuestion; + type NamedQuestion = Question & { name: N }; function promptModule< const A extends Answers, PrefilledAnswers extends Answers = object, @@ -99,10 +97,10 @@ export function createPromptModule< ): PromptReturnType; function promptModule( questions: - | NamedLegacyQuestion[] - | Record> - | Observable> - | NamedLegacyQuestion, + | AnyQuestion[] + | Record, 'name'>> + | Observable> + | AnyQuestion, answers?: Partial, ): PromptReturnType { const runner = new PromptsRunner(promptModule.prompts, opt); diff --git a/packages/inquirer/src/types.mts b/packages/inquirer/src/types.mts index 35ac68d56..47cbd0dd8 100644 --- a/packages/inquirer/src/types.mts +++ b/packages/inquirer/src/types.mts @@ -1,14 +1,15 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { - input, - select, +import { checkbox, confirm, - number, - rawlist, + editor, expand, + input, + number, password, - editor, + rawlist, + search, + select, } from '@inquirer/prompts'; import type { Context, DistributiveMerge, Prettify } from '@inquirer/type'; @@ -28,7 +29,7 @@ type AsyncGetterFunction = ( * * @example * ```ts - * declare module './src/index.mjs' { + * declare module 'inquirer' { * interface QuestionMap { * custom: { message: string }; * } @@ -45,25 +46,23 @@ export interface QuestionMap { type KeyValueOrAsyncGetterFunction = T extends Record ? T[k] | AsyncGetterFunction : never; -export type Named = T & { name: N }; - -export type LegacyQuestion = { +export type AnyQuestion = { + name: string; type: Type; askAnswered?: boolean; when?: boolean | AsyncGetterFunction; }; -export type NamedLegacyQuestion = Named< - LegacyQuestion ->; - -export type LegacyAsyncQuestion< +type LegacyAsyncQuestion< Type extends string, Q extends Record, A extends Answers, > = DistributiveMerge< Q, - LegacyQuestion & { + { + type: Type; + askAnswered?: boolean; + when?: boolean | AsyncGetterFunction; filter?(input: any, answers: A): any; message: KeyValueOrAsyncGetterFunction; default?: KeyValueOrAsyncGetterFunction; @@ -71,19 +70,21 @@ export type LegacyAsyncQuestion< } >; -export type Question = +export type BuiltInQuestion = + | LegacyAsyncQuestion<'checkbox', Parameters[0], A> | LegacyAsyncQuestion<'confirm', Parameters[0], A> - | LegacyAsyncQuestion<'expand', Parameters[0], A> | LegacyAsyncQuestion<'editor', Parameters[0], A> + | LegacyAsyncQuestion<'expand', Parameters[0], A> | LegacyAsyncQuestion<'input', Parameters[0], A> - | LegacyAsyncQuestion<'list', Parameters[0], A> | LegacyAsyncQuestion<'number', Parameters[0], A> | LegacyAsyncQuestion<'password', Parameters[0], A> | LegacyAsyncQuestion<'rawlist', Parameters[0], A> - | LegacyAsyncQuestion<'select', Parameters[0], A> - | LegacyAsyncQuestion<'checkbox', Parameters[0], A>; + | LegacyAsyncQuestion<'search', Parameters[0], A> + // Alias list type to select; it's been renamed. + | LegacyAsyncQuestion<'list', Parameters[0], A> + | LegacyAsyncQuestion<'select', Parameters[0], A>; -export type CustomQuestions< +export type CustomQuestion< A extends Answers, Q extends Record>, > = { diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index c31003bfe..e25c50ae9 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -16,12 +16,7 @@ import runAsync from 'run-async'; import MuteStream from 'mute-stream'; import type { InquirerReadline } from '@inquirer/type'; import ansiEscapes from 'ansi-escapes'; -import type { - Answers, - LegacyQuestion, - NamedLegacyQuestion, - StreamOptions, -} from '../types.mjs'; +import type { Answers, AnyQuestion, StreamOptions } from '../types.mjs'; export const _ = { set: (obj: Record, path: string = '', value: unknown): void => { @@ -62,11 +57,11 @@ export 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>( +function fetchAsyncQuestionProperty>( question: Q, prop: string, answers: A, -): Observable> { +): Observable> { if (prop in question) { const propGetter = question[prop as keyof Q]; if (typeof propGetter === 'function') { @@ -160,21 +155,21 @@ function setupReadlineOptions(opt: StreamOptions = {}) { function isQuestionArray( questions: - | NamedLegacyQuestion[] - | Record> - | Observable> - | NamedLegacyQuestion, -): questions is NamedLegacyQuestion[] { + | AnyQuestion[] + | Record, 'name'>> + | Observable> + | AnyQuestion, +): questions is AnyQuestion[] { return Array.isArray(questions); } function isQuestionMap( questions: - | NamedLegacyQuestion[] - | Record> - | Observable> - | NamedLegacyQuestion, -): questions is Record> { + | AnyQuestion[] + | Record, 'name'>> + | Observable> + | AnyQuestion, +): questions is Record, 'name'>> { return Object.values(questions).every( (maybeQuestion) => typeof maybeQuestion === 'object' && @@ -212,16 +207,16 @@ export default class PromptsRunner { async run( questions: - | NamedLegacyQuestion[] - | Record> - | Observable> - | NamedLegacyQuestion, + | AnyQuestion[] + | Record, 'name'>> + | Observable> + | AnyQuestion, answers?: Partial, ): Promise { // Keep global reference to the answers this.answers = typeof answers === 'object' ? { ...answers } : {}; - let obs: Observable>; + let obs: Observable>; if (isQuestionArray(questions)) { obs = from(questions); } else if (isObservable(questions)) { @@ -229,7 +224,7 @@ export default class PromptsRunner { } else if (isQuestionMap(questions)) { // Case: Called with a set of { name: question } obs = from( - Object.entries(questions).map(([name, question]): NamedLegacyQuestion => { + Object.entries(questions).map(([name, question]): AnyQuestion => { return Object.assign({}, question, { name }); }), ); @@ -252,7 +247,7 @@ export default class PromptsRunner { .finally(() => this.close()); } - processQuestion(question: NamedLegacyQuestion) { + processQuestion(question: AnyQuestion) { question = { ...question }; return defer(() => { const obs = of(question); @@ -293,7 +288,7 @@ export default class PromptsRunner { }); } - fetchAnswer(question: NamedLegacyQuestion) { + fetchAnswer(question: AnyQuestion) { const prompt = this.prompts[question.type]; if (prompt == null) { @@ -365,9 +360,7 @@ export default class PromptsRunner { } }; - setDefaultType = ( - question: NamedLegacyQuestion, - ): Observable> => { + setDefaultType = (question: AnyQuestion): Observable> => { // Default type to input if (!this.prompts[question.type]) { question = Object.assign({}, question, { type: 'input' }); @@ -376,9 +369,7 @@ export default class PromptsRunner { return defer(() => of(question)); }; - filterIfRunnable = ( - question: NamedLegacyQuestion, - ): Observable> => { + filterIfRunnable = (question: AnyQuestion): Observable> => { if ( question.askAnswered !== true && _.get(this.answers, question.name) !== undefined @@ -403,7 +394,7 @@ export default class PromptsRunner { } return; }), - ).pipe(filter((val): val is NamedLegacyQuestion => val != null)), + ).pipe(filter((val): val is AnyQuestion => val != null)), ); }; } From a49933a605ddabeb4260a03ca1de033941b36d6b Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Mon, 2 Sep 2024 16:26:50 -0400 Subject: [PATCH 12/15] Fix test broken type --- packages/inquirer/inquirer.test.mts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/inquirer/inquirer.test.mts b/packages/inquirer/inquirer.test.mts index 8d27b6592..589c07285 100644 --- a/packages/inquirer/inquirer.test.mts +++ b/packages/inquirer/inquirer.test.mts @@ -30,7 +30,7 @@ type TestQuestions = { failing: { message: string }; }; -function throwFunc(step: string) { +function throwFunc(step: any): any { throw new Error(`askAnswered Error ${step}`); } @@ -639,9 +639,9 @@ describe('inquirer.prompt(...)', () => { { type: 'input', name: 'prefilled', - when: throwFunc.bind(undefined, 'when'), - validate: throwFunc.bind(undefined, 'validate'), - transformer: throwFunc.bind(undefined, 'transformer'), + when: throwFunc, + validate: throwFunc, + transformer: throwFunc, message: 'message', default: 'newValue', }, @@ -658,9 +658,9 @@ describe('inquirer.prompt(...)', () => { { type: 'input', name: 'prefilled.nested', - when: throwFunc.bind(undefined, 'when'), - validate: throwFunc.bind(undefined, 'validate'), - transformer: throwFunc.bind(undefined, 'transformer'), + when: throwFunc, + validate: throwFunc, + transformer: throwFunc, message: 'message', default: 'newValue', }, From a4edbdb63ce4e7fa1ec5367559378bc82362adae Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Mon, 2 Sep 2024 16:37:25 -0400 Subject: [PATCH 13/15] refactor refactor --- packages/inquirer/src/types.mts | 26 +++++++++++++------------- packages/inquirer/src/ui/prompt.mts | 6 +++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/inquirer/src/types.mts b/packages/inquirer/src/types.mts index 47cbd0dd8..9ca89d68f 100644 --- a/packages/inquirer/src/types.mts +++ b/packages/inquirer/src/types.mts @@ -53,7 +53,7 @@ export type AnyQuestion = { when?: boolean | AsyncGetterFunction; }; -type LegacyAsyncQuestion< +type QuestionWithGetters< Type extends string, Q extends Record, A extends Answers, @@ -71,24 +71,24 @@ type LegacyAsyncQuestion< >; export type BuiltInQuestion = - | LegacyAsyncQuestion<'checkbox', Parameters[0], A> - | LegacyAsyncQuestion<'confirm', Parameters[0], A> - | LegacyAsyncQuestion<'editor', Parameters[0], A> - | LegacyAsyncQuestion<'expand', Parameters[0], A> - | LegacyAsyncQuestion<'input', Parameters[0], A> - | LegacyAsyncQuestion<'number', Parameters[0], A> - | LegacyAsyncQuestion<'password', Parameters[0], A> - | LegacyAsyncQuestion<'rawlist', Parameters[0], A> - | LegacyAsyncQuestion<'search', Parameters[0], A> + | QuestionWithGetters<'checkbox', Parameters[0], A> + | QuestionWithGetters<'confirm', Parameters[0], A> + | QuestionWithGetters<'editor', Parameters[0], A> + | QuestionWithGetters<'expand', Parameters[0], A> + | QuestionWithGetters<'input', Parameters[0], A> + | QuestionWithGetters<'number', Parameters[0], A> + | QuestionWithGetters<'password', Parameters[0], A> + | QuestionWithGetters<'rawlist', Parameters[0], A> + | QuestionWithGetters<'search', Parameters[0], A> // Alias list type to select; it's been renamed. - | LegacyAsyncQuestion<'list', Parameters[0], A> - | LegacyAsyncQuestion<'select', Parameters[0], A>; + | QuestionWithGetters<'list', Parameters[0], A> + | QuestionWithGetters<'select', Parameters[0], A>; export type CustomQuestion< A extends Answers, Q extends Record>, > = { - [key in Extract]: Readonly>; + [key in Extract]: Readonly>; }[Extract]; export type StreamOptions = Prettify; diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index e25c50ae9..a109400ff 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -123,7 +123,7 @@ class TTYError extends Error { isTtyError = true; } -function setupReadlineOptions(opt: StreamOptions = {}) { +function setupReadlineOptions(opt: StreamOptions) { // Inquirer 8.x: // opt.skipTTYChecks = opt.skipTTYChecks === undefined ? opt.input !== undefined : opt.skipTTYChecks; opt.skipTTYChecks = opt.skipTTYChecks === undefined ? true : opt.skipTTYChecks; @@ -197,10 +197,10 @@ export default class PromptsRunner { answers: Partial = {}; process: Observable = EMPTY; onClose?: () => void; - opt?: StreamOptions; + opt: StreamOptions; rl?: InquirerReadline; - constructor(prompts: PromptCollection, opt?: StreamOptions) { + constructor(prompts: PromptCollection, opt: StreamOptions = {}) { this.opt = opt; this.prompts = prompts; } From bfc596afa5089d0b61f7738b11e1cb2b5bb69ba3 Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Mon, 2 Sep 2024 16:55:56 -0400 Subject: [PATCH 14/15] Remove eslint-disables --- packages/inquirer/src/index.mts | 3 +-- packages/inquirer/src/ui/prompt.mts | 13 ++++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index 69e72ca0a..6b5364af1 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -77,8 +77,7 @@ export function createPromptModule< [name in keyof A]: Question>; }, answers?: PrefilledAnswers, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): PromptReturnType>>; + ): PromptReturnType>>>; function promptModule< const A extends Answers, PrefilledAnswers extends Answers = object, diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index a109400ff..ecc6675a0 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -265,10 +265,8 @@ export default class PromptsRunner { fetchAsyncQuestionProperty(question, 'choices', this.answers), ), concatMap((question) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - let { choices } = question as any; - if (Array.isArray(choices)) { - choices = choices.map( + if ('choices' in question && Array.isArray(question.choices)) { + const choices = question.choices.map( (choice: string | number | { value?: string; name: string }) => { if (typeof choice === 'string' || typeof choice === 'number') { return { name: choice, value: choice }; @@ -278,10 +276,11 @@ export default class PromptsRunner { return choice; }, ); + + return of({ ...question, choices }); } - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - return of({ ...question, choices }); + return of(question); }), concatMap((question) => this.fetchAnswer(question)), ); @@ -394,7 +393,7 @@ export default class PromptsRunner { } return; }), - ).pipe(filter((val): val is AnyQuestion => val != null)), + ).pipe(filter((val) => val != null)), ); }; } From 03653863de314539324a989ed47b31427d0e7ace Mon Sep 17 00:00:00 2001 From: Simon Boudrias Date: Mon, 2 Sep 2024 17:37:51 -0400 Subject: [PATCH 15/15] Stricter input types --- packages/inquirer/src/index.mts | 21 ++++++++++----------- packages/inquirer/src/types.mts | 9 ++++++++- packages/inquirer/src/ui/prompt.mts | 23 ++++------------------- 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index 6b5364af1..6c414dd1a 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -29,7 +29,6 @@ import type { BuiltInQuestion, StreamOptions, QuestionMap, - AnyQuestion, } from './types.mjs'; import { Observable } from 'rxjs'; @@ -61,12 +60,14 @@ export function createPromptModule< Prompts extends Record> = never, >(opt?: StreamOptions) { type Question = BuiltInQuestion | CustomQuestion; - type NamedQuestion = Question & { name: N }; + type NamedQuestion = Question & { + name: Extract; + }; function promptModule< const A extends Answers, PrefilledAnswers extends Answers = object, >( - questions: NamedQuestion, Extract>[], + questions: NamedQuestion>[], answers?: PrefilledAnswers, ): PromptReturnType>; function promptModule< @@ -82,24 +83,22 @@ export function createPromptModule< const A extends Answers, PrefilledAnswers extends Answers = object, >( - questions: Observable< - NamedQuestion, Extract> - >, + questions: Observable>>, answers?: PrefilledAnswers, ): PromptReturnType>; function promptModule< const A extends Answers, PrefilledAnswers extends Answers = object, >( - questions: NamedQuestion>, + questions: NamedQuestion, answers?: PrefilledAnswers, ): PromptReturnType; function promptModule( questions: - | AnyQuestion[] - | Record, 'name'>> - | Observable> - | AnyQuestion, + | NamedQuestion[] + | Record> + | Observable> + | NamedQuestion, answers?: Partial, ): PromptReturnType { const runner = new PromptsRunner(promptModule.prompts, opt); diff --git a/packages/inquirer/src/types.mts b/packages/inquirer/src/types.mts index 9ca89d68f..1a3d49da9 100644 --- a/packages/inquirer/src/types.mts +++ b/packages/inquirer/src/types.mts @@ -12,6 +12,7 @@ import { select, } from '@inquirer/prompts'; import type { Context, DistributiveMerge, Prettify } from '@inquirer/type'; +import { Observable } from 'rxjs'; export type Answers = Record; @@ -47,8 +48,8 @@ type KeyValueOrAsyncGetterFunction = T extends Record ? T[k] | AsyncGetterFunction : never; export type AnyQuestion = { - name: string; type: Type; + name: string; askAnswered?: boolean; when?: boolean | AsyncGetterFunction; }; @@ -91,4 +92,10 @@ export type CustomQuestion< [key in Extract]: Readonly>; }[Extract]; +export type PromptSession> = + | Q[] + | Record> + | Observable + | Q; + export type StreamOptions = Prettify; diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index ecc6675a0..b0bf282db 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -16,7 +16,7 @@ import runAsync from 'run-async'; import MuteStream from 'mute-stream'; import type { InquirerReadline } from '@inquirer/type'; import ansiEscapes from 'ansi-escapes'; -import type { Answers, AnyQuestion, StreamOptions } from '../types.mjs'; +import type { Answers, AnyQuestion, PromptSession, StreamOptions } from '../types.mjs'; export const _ = { set: (obj: Record, path: string = '', value: unknown): void => { @@ -154,21 +154,13 @@ function setupReadlineOptions(opt: StreamOptions) { } function isQuestionArray( - questions: - | AnyQuestion[] - | Record, 'name'>> - | Observable> - | AnyQuestion, + questions: PromptSession>, ): questions is AnyQuestion[] { return Array.isArray(questions); } function isQuestionMap( - questions: - | AnyQuestion[] - | Record, 'name'>> - | Observable> - | AnyQuestion, + questions: PromptSession>, ): questions is Record, 'name'>> { return Object.values(questions).every( (maybeQuestion) => @@ -205,14 +197,7 @@ export default class PromptsRunner { this.prompts = prompts; } - async run( - questions: - | AnyQuestion[] - | Record, 'name'>> - | Observable> - | AnyQuestion, - answers?: Partial, - ): Promise { + async run(questions: PromptSession>, answers?: Partial): Promise { // Keep global reference to the answers this.answers = typeof answers === 'object' ? { ...answers } : {};