Skip to content

Commit

Permalink
Fix(typescript): Allow extending inquirer with custom prompts
Browse files Browse the repository at this point in the history
  • Loading branch information
SBoudrias committed Jul 16, 2024
1 parent 950158b commit 8523249
Show file tree
Hide file tree
Showing 7 changed files with 506 additions and 357 deletions.
32 changes: 29 additions & 3 deletions packages/inquirer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,12 +423,38 @@ Licensed under the MIT license.

You can build custom prompts, or use open sourced ones. See [`@inquirer/core` documentation for building custom prompts](https://github.com/SBoudrias/Inquirer.js/tree/master/packages/core).

You can either call the custom prompts directly, or you can register them:
You can either call the custom prompts directly (preferred), or you can register them (depreciated):

```js
import CustomPrompt from '$$$/custom-prompt';
import customPrompt from '$$$/custom-prompt';

inquirer.registerPrompt('custom', CustomPrompt);
// 1. Preferred solution with new plugins
const answer = await customPrompt({ ...config });

// 2. Depreciated interface (or for old plugins)
inquirer.registerPrompt('custom', customPrompt);
const answers = await inquirer.prompt([
{
type: 'custom',
...config,
},
]);
```

When using Typescript and `registerPrompt`, you'll also need to define your prompt signature. Since Typescript is static, we cannot infer available plugins from function calls.

```ts
import customPrompt from '$$$/custom-prompt';

declare module 'inquirer' {
interface QuestionMap {
// 1. Easiest option
custom: Parameters<typeof customPrompt>[0];

// 2. Or manually define the prompt config
custom_alt: { message: string; option: number[] };
}
}
```

### Prompts
Expand Down
56 changes: 44 additions & 12 deletions packages/inquirer/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
editor,
Separator,
} from '@inquirer/prompts';
import type { Prettify } from '@inquirer/type';
import type { Prettify, UnionToIntersection } from '@inquirer/type';
import { default as PromptsRunner } from './ui/prompt.mjs';
import type {
PromptCollection,
Expand All @@ -31,6 +31,8 @@ import type {
StreamOptions,
} from './types.mjs';

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

const defaultPrompts: PromptCollection = {
input,
select,
Expand All @@ -45,24 +47,56 @@ const defaultPrompts: PromptCollection = {
editor,
};

type PromptReturnType<T> =
| (Promise<Prettify<T>> & {
ui: PromptsRunner<Prettify<T>>;
})
| never;

/**
* Create a new self-contained prompt module.
*/
export function createPromptModule(opt?: StreamOptions) {
function promptModule<T extends Answers>(
function promptModule<
const AnswerList extends readonly Answers[],
PrefilledAnswers extends Answers = object,
>(
questions: { [I in keyof AnswerList]: Question<PrefilledAnswers & AnswerList[I]> },
answers?: PrefilledAnswers,
): PromptReturnType<PrefilledAnswers & UnionToIntersection<AnswerList[number]>>;
function promptModule<
const Map extends QuestionAnswerMap<A>,
const A extends Answers<Extract<keyof Map, string>>,
PrefilledAnswers extends Answers = object,
>(questions: Map, answers?: PrefilledAnswers): PromptReturnType<PrefilledAnswers & A>;
function promptModule<
const A extends Answers,
PrefilledAnswers extends Answers = object,
>(
questions: QuestionObservable<A>,
answers?: PrefilledAnswers,
): PromptReturnType<PrefilledAnswers & A>;
function promptModule<
const A extends Answers,
PrefilledAnswers extends Answers = object,
>(
questions: Question<A>,
answers?: PrefilledAnswers,
): PromptReturnType<PrefilledAnswers & A>;
function promptModule(
questions:
| QuestionArray<T>
| QuestionAnswerMap<T>
| QuestionObservable<T>
| Question<T>,
answers?: Partial<T>,
): Promise<Prettify<T>> & { ui: PromptsRunner<T> } {
const runner = new PromptsRunner<T>(promptModule.prompts, opt);
| QuestionArray<Answers>
| QuestionAnswerMap<Answers>
| QuestionObservable<Answers>
| Question<Answers>,
answers?: Partial<Answers>,
): PromptReturnType<Answers> {
const runner = new PromptsRunner(promptModule.prompts, opt);

try {
return runner.run(questions, answers);
} catch (error) {
const promise = Promise.reject<T>(error);
const promise = Promise.reject(error);
return Object.assign(promise, { ui: runner });
}
}
Expand All @@ -87,8 +121,6 @@ export function createPromptModule(opt?: StreamOptions) {
promptModule.prompts = { ...defaultPrompts };
};

promptModule.restoreDefaultPrompts();

return promptModule;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ declare module 'run-async' {
func: F,
) => (...args: Parameters<F>) => Promise<ExtractPromise<ReturnType<F>>>;

export = runAsync;
export default runAsync;
}
100 changes: 59 additions & 41 deletions packages/inquirer/src/types.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,76 @@ import {
password,
editor,
} from '@inquirer/prompts';
import type { Prettify } from '@inquirer/type';
import type { Prettify, KeyUnion, DistributiveMerge, Pick } from '@inquirer/type';
import { Observable } from 'rxjs';

// eslint-disable-next-line @typescript-eslint/ban-types
type LiteralUnion<T extends F, F = string> = T | (F & {});
type KeyUnion<T> = LiteralUnion<Extract<keyof T, string>>;

export type Answers = {
[key: string]: any;
export type Answers<Key extends string = string> = {
[key in Key]: any;
};

type whenFunction<T extends Answers> =
| ((answers: Partial<T>) => boolean | Promise<boolean>)
| ((this: { async: () => () => void }, answers: Partial<T>) => void);
type AsyncCallbackFunction<R> = (
...args: [error: null | undefined, value: R] | [error: Error, value: undefined]
) => void;

type InquirerFields<T extends Answers> = {
name: KeyUnion<T>;
when?: boolean | whenFunction<T>;
askAnswered?: boolean;
};
type AsyncGetterFunction<R, A extends Answers> = (
this: { async: () => AsyncCallbackFunction<R> },
answers: Partial<A>,
) => void | R | Promise<R>;

interface QuestionMap<T extends Answers> {
input: Prettify<{ type: 'input' } & Parameters<typeof input>[0] & InquirerFields<T>>;
select: Prettify<{ type: 'select' } & Parameters<typeof select>[0] & InquirerFields<T>>;
list: Prettify<{ type: 'list' } & Parameters<typeof select>[0] & InquirerFields<T>>;
number: Prettify<{ type: 'number' } & Parameters<typeof number>[0] & InquirerFields<T>>;
confirm: Prettify<
{ type: 'confirm' } & Parameters<typeof confirm>[0] & InquirerFields<T>
>;
rawlist: Prettify<
{ type: 'rawlist' } & Parameters<typeof rawlist>[0] & InquirerFields<T>
>;
expand: Prettify<{ type: 'expand' } & Parameters<typeof expand>[0] & InquirerFields<T>>;
checkbox: Prettify<
{ type: 'checkbox' } & Parameters<typeof checkbox>[0] & InquirerFields<T>
>;
password: Prettify<
{ type: 'password' } & Parameters<typeof password>[0] & InquirerFields<T>
>;
editor: Prettify<{ type: 'editor' } & Parameters<typeof editor>[0] & InquirerFields<T>>;
export interface QuestionMap {
input: Parameters<typeof input>[0];
select: Parameters<typeof select>[0];
/** @deprecated `list` is now named `select` */
list: Parameters<typeof select>[0];
number: Parameters<typeof number>[0];
confirm: Parameters<typeof confirm>[0];
rawlist: Parameters<typeof rawlist>[0];
expand: Parameters<typeof expand>[0];
checkbox: Parameters<typeof checkbox>[0];
password: Parameters<typeof password>[0];
editor: Parameters<typeof editor>[0];
}

export type Question<T extends Answers> = QuestionMap<T>[keyof QuestionMap<T>];
type PromptConfigMap<A extends Answers> = {
[key in keyof QuestionMap]: DistributiveMerge<
QuestionMap[keyof QuestionMap],
{
type: keyof QuestionMap;
name: KeyUnion<A>;
when?: AsyncGetterFunction<boolean, Prettify<A>> | boolean;
askAnswered?: boolean;
message:
| Pick<QuestionMap[keyof QuestionMap], 'message'>
| AsyncGetterFunction<
Pick<QuestionMap[keyof QuestionMap], 'message'>,
Prettify<A>
>;
choices?:
| Pick<QuestionMap[keyof QuestionMap], 'choices'>
| string[]
| AsyncGetterFunction<
Pick<QuestionMap[keyof QuestionMap], 'choices'> | string[],
Prettify<A>
>;
default?:
| Pick<QuestionMap[keyof QuestionMap], 'default'>
| AsyncGetterFunction<
Pick<QuestionMap[keyof QuestionMap], 'default'> | string[],
Prettify<A>
>;
}
>;
};

export type QuestionAnswerMap<T extends Answers> = Record<
KeyUnion<T>,
Prettify<Omit<Question<T>, 'name'>>
>;
export type Question<A extends Answers> = PromptConfigMap<A>[keyof PromptConfigMap<A>];

export type QuestionAnswerMap<A extends Answers> =
| { [name in KeyUnion<A>]: Omit<Question<A>, 'name'> }
| never;

export type QuestionArray<T extends Answers> = Question<T>[];
export type QuestionArray<A extends Answers> = Question<A>[] | never;

export type QuestionObservable<T extends Answers> = Observable<Question<T>>;
export type QuestionObservable<A extends Answers> = Observable<Question<A>> | never;

export type StreamOptions = Prettify<
Parameters<typeof input>[1] & { skipTTYChecks?: boolean }
Expand Down
Loading

0 comments on commit 8523249

Please sign in to comment.