Skip to content

Commit f7dfeef

Browse files
committed
implement createPromptSession
1 parent bd45397 commit f7dfeef

3 files changed

Lines changed: 123 additions & 43 deletions

File tree

packages/inquirer/inquirer.test.mts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,64 @@ describe('inquirer.prompt(...)', () => {
786786
});
787787
});
788788

789+
describe('createPromptSession', () => {
790+
it('should expose a Reactive subject across a session', async () => {
791+
const localPrompt = inquirer.createPromptModule<TestQuestions>();
792+
localPrompt.registerPrompt('stub', StubPrompt);
793+
const session = localPrompt.createPromptSession();
794+
const spy = vi.fn();
795+
796+
await session.run([
797+
{
798+
type: 'stub',
799+
name: 'nonSubscribed',
800+
message: 'nonSubscribedMessage',
801+
answer: 'nonSubscribedAnswer',
802+
},
803+
]);
804+
805+
session.process.subscribe(spy);
806+
expect(spy).not.toHaveBeenCalled();
807+
808+
await session.run([
809+
{
810+
type: 'stub',
811+
name: 'name1',
812+
message: 'message',
813+
answer: 'bar',
814+
},
815+
{
816+
type: 'stub',
817+
name: 'name',
818+
message: 'message',
819+
answer: 'doe',
820+
},
821+
]);
822+
823+
expect(spy).toHaveBeenCalledWith({ name: 'name1', answer: 'bar' });
824+
expect(spy).toHaveBeenCalledWith({ name: 'name', answer: 'doe' });
825+
});
826+
827+
it('should return proxy object as prefilled answers', async () => {
828+
const localPrompt = inquirer.createPromptModule<TestQuestions>();
829+
localPrompt.registerPrompt('stub', StubPrompt);
830+
831+
const proxy = new Proxy({ prefilled: 'prefilled' }, {});
832+
const session = localPrompt.createPromptSession({ answers: proxy });
833+
834+
const answers = await session.run([
835+
{
836+
type: 'stub',
837+
name: 'nonSubscribed',
838+
message: 'nonSubscribedMessage',
839+
answer: 'nonSubscribedAnswer',
840+
},
841+
]);
842+
843+
expect(answers).toBe(proxy);
844+
});
845+
});
846+
789847
describe('AbortSignal support', () => {
790848
it('throws on aborted signal', async () => {
791849
const localPrompt = inquirer.createPromptModule<TestQuestions>({

packages/inquirer/src/index.mts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ export function createPromptModule<
9898
questions: PromptSession<A>,
9999
answers?: Partial<A>,
100100
): PromptReturnType<A> {
101-
const runner = new PromptsRunner<A>(promptModule.prompts, opt);
102-
103-
const promptPromise = runner.run(questions, answers);
101+
const runner = promptModule.createPromptSession<A>({ answers });
102+
const promptPromise = runner.run(questions);
104103
return Object.assign(promptPromise, { ui: runner });
105104
}
106105

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

126+
promptModule.createPromptSession = function <A extends Answers>({
127+
answers,
128+
}: { answers?: Partial<A> } = {}) {
129+
return new PromptsRunner<A>(promptModule.prompts, { ...opt, answers });
130+
};
131+
127132
return promptModule;
128133
}
129134

packages/inquirer/src/ui/prompt.mts

Lines changed: 57 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-assignment */
22
import readline from 'node:readline';
3+
import { isProxy } from 'node:util/types';
34
import {
45
defer,
5-
EMPTY,
66
from,
77
of,
88
concatMap,
99
filter,
1010
reduce,
1111
isObservable,
1212
Observable,
13+
Subject,
1314
lastValueFrom,
15+
tap,
1416
} from 'rxjs';
1517
import runAsync from 'run-async';
1618
import MuteStream from 'mute-stream';
@@ -40,11 +42,7 @@ export const _ = {
4042
pointer = pointer[key] as Record<string, unknown>;
4143
});
4244
},
43-
get: (
44-
obj: object,
45-
path: string | number | symbol = '',
46-
defaultValue?: unknown,
47-
): any => {
45+
get: (obj: object, path: string = '', defaultValue?: unknown): any => {
4846
const travel = (regexp: RegExp) =>
4947
String.prototype.split
5048
.call(path, regexp)
@@ -193,22 +191,42 @@ function isPromptConstructor(
193191
*/
194192
export default class PromptsRunner<A extends Answers> {
195193
private prompts: PromptCollection;
196-
answers: Partial<A> = {};
197-
process: Observable<any> = EMPTY;
194+
answers: Partial<A>;
195+
process: Subject<{ name: string; answer: any }> = new Subject();
198196
private abortController: AbortController = new AbortController();
199197
private opt: StreamOptions;
200198

201-
constructor(prompts: PromptCollection, opt: StreamOptions = {}) {
199+
constructor(
200+
prompts: PromptCollection,
201+
{ answers = {}, ...opt }: StreamOptions & { answers?: Partial<A> } = {},
202+
) {
202203
this.opt = opt;
203204
this.prompts = prompts;
205+
206+
this.answers = isProxy(answers)
207+
? answers
208+
: new Proxy(
209+
{ ...answers },
210+
{
211+
get: (target, prop) => {
212+
if (typeof prop !== 'string') {
213+
return;
214+
}
215+
return _.get(target, prop);
216+
},
217+
set: (target, prop: string, value) => {
218+
_.set(target, prop, value);
219+
return true;
220+
},
221+
},
222+
);
204223
}
205224

206-
async run(questions: PromptSession<A>, answers?: Partial<A>): Promise<A> {
225+
async run<Session extends PromptSession<A> = PromptSession<A>>(
226+
questions: Session,
227+
): Promise<A> {
207228
this.abortController = new AbortController();
208229

209-
// Keep global reference to the answers
210-
this.answers = typeof answers === 'object' ? { ...answers } : {};
211-
212230
let obs: Observable<AnyQuestion<A>>;
213231
if (isQuestionArray(questions)) {
214232
obs = from(questions);
@@ -223,34 +241,36 @@ export default class PromptsRunner<A extends Answers> {
223241
);
224242
} else {
225243
// Case: Called with a single question config
226-
obs = from([questions]);
244+
obs = from([questions as AnyQuestion<A>]);
227245
}
228246

229-
this.process = obs.pipe(
230-
concatMap((question) =>
231-
of(question).pipe(
247+
return lastValueFrom(
248+
obs
249+
.pipe(
232250
concatMap((question) =>
233-
from(
234-
this.shouldRun(question).then((shouldRun: boolean | void) => {
235-
if (shouldRun) {
236-
return question;
237-
}
238-
return;
239-
}),
240-
).pipe(filter((val) => val != null)),
251+
of(question)
252+
.pipe(
253+
concatMap((question) =>
254+
from(
255+
this.shouldRun(question).then((shouldRun: boolean | void) => {
256+
if (shouldRun) {
257+
return question;
258+
}
259+
return;
260+
}),
261+
).pipe(filter((val) => val != null)),
262+
),
263+
concatMap((question) => defer(() => from(this.fetchAnswer(question)))),
264+
)
265+
.pipe(tap((answer) => this.process.next(answer))),
241266
),
242-
concatMap((question) => defer(() => from(this.fetchAnswer(question)))),
267+
)
268+
.pipe(
269+
reduce((answersObj: Record<string, any>, answer: { name: string; answer: unknown }) => {
270+
answersObj[answer.name] = answer.answer;
271+
return answersObj;
272+
}, this.answers),
243273
),
244-
),
245-
);
246-
247-
return lastValueFrom(
248-
this.process.pipe(
249-
reduce((answersObj, answer: { name: string; answer: unknown }) => {
250-
_.set(answersObj, answer.name, answer.answer);
251-
return answersObj;
252-
}, this.answers),
253-
),
254274
)
255275
.then(() => this.answers as A)
256276
.finally(() => this.close());
@@ -388,10 +408,7 @@ export default class PromptsRunner<A extends Answers> {
388408
};
389409

390410
private shouldRun = async (question: AnyQuestion<A>): Promise<boolean> => {
391-
if (
392-
question.askAnswered !== true &&
393-
_.get(this.answers, question.name) !== undefined
394-
) {
411+
if (question.askAnswered !== true && this.answers[question.name] !== undefined) {
395412
return false;
396413
}
397414

0 commit comments

Comments
 (0)