Skip to content

Commit

Permalink
implement createPromptSession
Browse files Browse the repository at this point in the history
  • Loading branch information
mshima committed Sep 17, 2024
1 parent bd45397 commit f7dfeef
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 43 deletions.
58 changes: 58 additions & 0 deletions packages/inquirer/inquirer.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,64 @@ describe('inquirer.prompt(...)', () => {
});
});

describe('createPromptSession', () => {
it('should expose a Reactive subject across a session', async () => {
const localPrompt = inquirer.createPromptModule<TestQuestions>();
localPrompt.registerPrompt('stub', StubPrompt);
const session = localPrompt.createPromptSession();
const spy = vi.fn();

await session.run([
{
type: 'stub',
name: 'nonSubscribed',
message: 'nonSubscribedMessage',
answer: 'nonSubscribedAnswer',
},
]);

session.process.subscribe(spy);
expect(spy).not.toHaveBeenCalled();

await session.run([
{
type: 'stub',
name: 'name1',
message: 'message',
answer: 'bar',
},
{
type: 'stub',
name: 'name',
message: 'message',
answer: 'doe',
},
]);

expect(spy).toHaveBeenCalledWith({ name: 'name1', answer: 'bar' });
expect(spy).toHaveBeenCalledWith({ name: 'name', answer: 'doe' });
});

it('should return proxy object as prefilled answers', async () => {
const localPrompt = inquirer.createPromptModule<TestQuestions>();
localPrompt.registerPrompt('stub', StubPrompt);

const proxy = new Proxy({ prefilled: 'prefilled' }, {});
const session = localPrompt.createPromptSession({ answers: proxy });

const answers = await session.run([
{
type: 'stub',
name: 'nonSubscribed',
message: 'nonSubscribedMessage',
answer: 'nonSubscribedAnswer',
},
]);

expect(answers).toBe(proxy);
});
});

describe('AbortSignal support', () => {
it('throws on aborted signal', async () => {
const localPrompt = inquirer.createPromptModule<TestQuestions>({
Expand Down
11 changes: 8 additions & 3 deletions packages/inquirer/src/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,8 @@ export function createPromptModule<
questions: PromptSession<A>,
answers?: Partial<A>,
): PromptReturnType<A> {
const runner = new PromptsRunner<A>(promptModule.prompts, opt);

const promptPromise = runner.run(questions, answers);
const runner = promptModule.createPromptSession<A>({ answers });
const promptPromise = runner.run(questions);
return Object.assign(promptPromise, { ui: runner });
}

Expand All @@ -124,6 +123,12 @@ export function createPromptModule<
promptModule.prompts = { ...builtInPrompts };
};

promptModule.createPromptSession = function <A extends Answers>({
answers,
}: { answers?: Partial<A> } = {}) {
return new PromptsRunner<A>(promptModule.prompts, { ...opt, answers });
};

return promptModule;
}

Expand Down
97 changes: 57 additions & 40 deletions packages/inquirer/src/ui/prompt.mts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */
import readline from 'node:readline';
import { isProxy } from 'node:util/types';
import {
defer,
EMPTY,
from,
of,
concatMap,
filter,
reduce,
isObservable,
Observable,
Subject,
lastValueFrom,
tap,
} from 'rxjs';
import runAsync from 'run-async';
import MuteStream from 'mute-stream';
Expand Down Expand Up @@ -40,11 +42,7 @@ export const _ = {
pointer = pointer[key] as Record<string, unknown>;
});
},
get: (
obj: object,
path: string | number | symbol = '',
defaultValue?: unknown,
): any => {
get: (obj: object, path: string = '', defaultValue?: unknown): any => {
const travel = (regexp: RegExp) =>
String.prototype.split
.call(path, regexp)
Expand Down Expand Up @@ -193,22 +191,42 @@ function isPromptConstructor(
*/
export default class PromptsRunner<A extends Answers> {
private prompts: PromptCollection;
answers: Partial<A> = {};
process: Observable<any> = EMPTY;
answers: Partial<A>;
process: Subject<{ name: string; answer: any }> = new Subject();
private abortController: AbortController = new AbortController();
private opt: StreamOptions;

constructor(prompts: PromptCollection, opt: StreamOptions = {}) {
constructor(
prompts: PromptCollection,
{ answers = {}, ...opt }: StreamOptions & { answers?: Partial<A> } = {},
) {
this.opt = opt;
this.prompts = prompts;

this.answers = isProxy(answers)
? answers
: new Proxy(
{ ...answers },
{
get: (target, prop) => {
if (typeof prop !== 'string') {
return;
}
return _.get(target, prop);
},
set: (target, prop: string, value) => {
_.set(target, prop, value);
return true;
},
},
);
}

async run(questions: PromptSession<A>, answers?: Partial<A>): Promise<A> {
async run<Session extends PromptSession<A> = PromptSession<A>>(
questions: Session,
): Promise<A> {
this.abortController = new AbortController();

// Keep global reference to the answers
this.answers = typeof answers === 'object' ? { ...answers } : {};

let obs: Observable<AnyQuestion<A>>;
if (isQuestionArray(questions)) {
obs = from(questions);
Expand All @@ -223,34 +241,36 @@ export default class PromptsRunner<A extends Answers> {
);
} else {
// Case: Called with a single question config
obs = from([questions]);
obs = from([questions as AnyQuestion<A>]);
}

this.process = obs.pipe(
concatMap((question) =>
of(question).pipe(
return lastValueFrom(
obs
.pipe(
concatMap((question) =>
from(
this.shouldRun(question).then((shouldRun: boolean | void) => {
if (shouldRun) {
return question;
}
return;
}),
).pipe(filter((val) => val != null)),
of(question)
.pipe(
concatMap((question) =>
from(
this.shouldRun(question).then((shouldRun: boolean | void) => {
if (shouldRun) {
return question;
}
return;
}),
).pipe(filter((val) => val != null)),
),
concatMap((question) => defer(() => from(this.fetchAnswer(question)))),
)
.pipe(tap((answer) => this.process.next(answer))),
),
concatMap((question) => defer(() => from(this.fetchAnswer(question)))),
)
.pipe(
reduce((answersObj: Record<string, any>, answer: { name: string; answer: unknown }) => {
answersObj[answer.name] = answer.answer;
return answersObj;
}, this.answers),
),
),
);

return lastValueFrom(
this.process.pipe(
reduce((answersObj, answer: { name: string; answer: unknown }) => {
_.set(answersObj, answer.name, answer.answer);
return answersObj;
}, this.answers),
),
)
.then(() => this.answers as A)
.finally(() => this.close());
Expand Down Expand Up @@ -388,10 +408,7 @@ export default class PromptsRunner<A extends Answers> {
};

private shouldRun = async (question: AnyQuestion<A>): Promise<boolean> => {
if (
question.askAnswered !== true &&
_.get(this.answers, question.name) !== undefined
) {
if (question.askAnswered !== true && this.answers[question.name] !== undefined) {
return false;
}

Expand Down

0 comments on commit f7dfeef

Please sign in to comment.