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 9e29035 commit 6a68e86
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 43 deletions.
39 changes: 39 additions & 0 deletions packages/inquirer/inquirer.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,45 @@ 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' });
});
});

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
101 changes: 61 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,23 +191,47 @@ 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;
rl?: InquirerReadline;

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

Check warning on line 208 in packages/inquirer/src/ui/prompt.mts

View check run for this annotation

Codecov / codecov/patch

packages/inquirer/src/ui/prompt.mts#L208

Added line #L208 was not covered by tests
: new Proxy(
{ ...answers },
{
get: (target: any, prop) => {
if (typeof prop === 'string') {
return _.get(target, prop);

Check failure on line 214 in packages/inquirer/src/ui/prompt.mts

View workflow job for this annotation

GitHub Actions / Linting

Unsafe argument of type `any` assigned to a parameter of type `object`
}
return target[prop];

Check failure on line 216 in packages/inquirer/src/ui/prompt.mts

View workflow job for this annotation

GitHub Actions / Linting

Unsafe member access [prop] on an `any` value
},
set: (target: any, prop, value) => {
if (typeof prop === 'string') {
_.set(target, prop, value);

Check failure on line 220 in packages/inquirer/src/ui/prompt.mts

View workflow job for this annotation

GitHub Actions / Linting

Unsafe argument of type `any` assigned to a parameter of type `Record<string, unknown>`
} else {
target[prop] = value;

Check failure on line 222 in packages/inquirer/src/ui/prompt.mts

View workflow job for this annotation

GitHub Actions / Linting

Unsafe member access [prop] on an `any` value
}

Check warning on line 223 in packages/inquirer/src/ui/prompt.mts

View check run for this annotation

Codecov / codecov/patch

packages/inquirer/src/ui/prompt.mts#L222-L223

Added lines #L222 - L223 were not covered by tests
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 @@ -224,34 +246,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: any, answer: { name: string; answer: unknown }) => {
answersObj[answer.name] = answer.answer;

Check failure on line 275 in packages/inquirer/src/ui/prompt.mts

View workflow job for this annotation

GitHub Actions / Linting

Unsafe member access [answer.name] on an `any` value
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 @@ -389,10 +413,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 6a68e86

Please sign in to comment.