Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix(inquirer): Rework type interface #1531

Merged
merged 16 commits into from
Sep 2, 2024
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