Skip to content

Commit

Permalink
Fix(inquirer): Rework type interface (#1531)
Browse files Browse the repository at this point in the history
Ref #1527

------------

Co-authored-by: Simon Boudrias <admin@simonboudrias.com>
  • Loading branch information
mshima and SBoudrias committed Sep 2, 2024
1 parent 9b60356 commit 00caff1
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 174 deletions.
78 changes: 40 additions & 38 deletions packages/inquirer/inquirer.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,29 @@ 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 };
}
}

function throwFunc(step: 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: any): any {
throw new Error(`askAnswered Error ${step}`);
}

Expand Down Expand Up @@ -110,27 +117,24 @@ describe('inquirer.prompt(...)', () => {

it('takes an Observable', async () => {
const answers = await inquirer.prompt(
new Observable<Question<{ q1: boolean; q2: boolean }>>((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 }>();
});
});

Expand Down Expand Up @@ -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<Partial<{ name1: any; name2: any }>>();
expect(answers).toEqual({ name1: 'bar' });
const goOn = this.async();
Expand All @@ -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';
},
Expand Down Expand Up @@ -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<Partial<{ name1: any; q2: any }>>();
expect(answers).toEqual({ name1: 'bar' });
const goOn = this.async();
Expand Down Expand Up @@ -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<Partial<{ name1: any; name: any }>>();
expect(answers).toEqual({ name1: 'bar' });
return stubChoices;
Expand Down Expand Up @@ -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<Partial<{ q1: any; q2: any }>>();

goesInWhen = true;
Expand Down Expand Up @@ -635,14 +635,13 @@ 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',
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',
},
Expand All @@ -655,14 +654,13 @@ 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',
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',
},
Expand Down Expand Up @@ -773,7 +771,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<TestQuestions>({
skipTTYChecks: false,
});
localPrompt.registerPrompt('stub', StubPrompt);

const promise = localPrompt([
Expand All @@ -787,7 +787,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<TestQuestions>();
localPrompt.registerPrompt('stub', StubPrompt);

await localPrompt([
Expand All @@ -805,7 +805,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<TestQuestions>({
skipTTYChecks: true,
});
localPrompt.registerPrompt('stub', StubPrompt);

await localPrompt([
Expand All @@ -823,7 +825,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<TestQuestions>({
input: new stream.Readable({
// We must have a default read implementation
// for this to work, if not it will error out
Expand All @@ -849,7 +851,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<TestQuestions>({
input: new stream.Readable(),
skipTTYChecks: false,
});
Expand All @@ -871,7 +873,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<TestQuestions>({
input,
skipTTYChecks: false,
});
Expand Down
58 changes: 34 additions & 24 deletions packages/inquirer/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -25,12 +25,12 @@ import type {
} from './ui/prompt.mjs';
import type {
Answers,
Question,
QuestionAnswerMap,
QuestionArray,
QuestionObservable,
CustomQuestion,
BuiltInQuestion,
StreamOptions,
QuestionMap,
} from './types.mjs';
import { Observable } from 'rxjs';

export type { QuestionMap } from './types.mjs';

Expand All @@ -56,42 +56,52 @@ type PromptReturnType<T> = Promise<Prettify<T>> & {
/**
* Create a new self-contained prompt module.
*/
export function createPromptModule(opt?: StreamOptions) {
export function createPromptModule<
Prompts extends Record<string, Record<string, unknown>> = never,
>(opt?: StreamOptions) {
type Question<A extends Answers> = BuiltInQuestion<A> | CustomQuestion<A, Prompts>;
type NamedQuestion<A extends Answers> = Question<A> & {
name: Extract<keyof A, string>;
};
function promptModule<
const AnswerList extends readonly Answers[],
const A extends Answers,
PrefilledAnswers extends Answers = object,
>(
questions: { [I in keyof AnswerList]: Question<PrefilledAnswers & AnswerList[I]> },
questions: NamedQuestion<Prettify<PrefilledAnswers & A>>[],
answers?: PrefilledAnswers,
): PromptReturnType<PrefilledAnswers & UnionToIntersection<AnswerList[number]>>;
): PromptReturnType<Prettify<PrefilledAnswers & A>>;
function promptModule<
const Map extends QuestionAnswerMap<A>,
const A extends Answers<Extract<keyof Map, string>>,
const A extends Answers,
PrefilledAnswers extends Answers = object,
>(questions: Map, answers?: PrefilledAnswers): PromptReturnType<PrefilledAnswers & A>;
>(
questions: {
[name in keyof A]: Question<Prettify<PrefilledAnswers & A>>;
},
answers?: PrefilledAnswers,
): PromptReturnType<Prettify<PrefilledAnswers & Answers<Extract<keyof A, string>>>>;
function promptModule<
const A extends Answers,
PrefilledAnswers extends Answers = object,
>(
questions: QuestionObservable<A>,
questions: Observable<NamedQuestion<Prettify<PrefilledAnswers & A>>>,
answers?: PrefilledAnswers,
): PromptReturnType<PrefilledAnswers & A>;
): PromptReturnType<Prettify<PrefilledAnswers & A>>;
function promptModule<
const A extends Answers,
PrefilledAnswers extends Answers = object,
>(
questions: Question<A>,
questions: NamedQuestion<A & PrefilledAnswers>,
answers?: PrefilledAnswers,
): PromptReturnType<PrefilledAnswers & A>;
function promptModule(
function promptModule<A extends Answers>(
questions:
| QuestionArray<Answers>
| QuestionAnswerMap<Answers>
| QuestionObservable<Answers>
| Question<Answers>,
answers?: Partial<Answers>,
): PromptReturnType<Answers> {
const runner = new PromptsRunner(promptModule.prompts, opt);
| NamedQuestion<A>[]
| Record<keyof A, Question<A>>
| Observable<NamedQuestion<A>>
| NamedQuestion<A>,
answers?: Partial<A>,
): PromptReturnType<A> {
const runner = new PromptsRunner<A>(promptModule.prompts, opt);

const promptPromise = runner.run(questions, answers);
return Object.assign(promptPromise, { ui: runner });
Expand Down Expand Up @@ -123,7 +133,7 @@ export function createPromptModule(opt?: StreamOptions) {
/**
* Public CLI helper interface
*/
const prompt = createPromptModule();
const prompt = createPromptModule<Omit<QuestionMap, '__dummy'>>();

// Expose helper functions on the top level for easiest usage by common users
function registerPrompt(name: string, newPrompt: LegacyPromptConstructor) {
Expand Down
Loading

0 comments on commit 00caff1

Please sign in to comment.