Skip to content

Commit 8927649

Browse files
committed
implement createPromptSession
1 parent 9e29035 commit 8927649

File tree

3 files changed

+123
-43
lines changed

3 files changed

+123
-43
lines changed

packages/inquirer/inquirer.test.mts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,64 @@ describe('inquirer.prompt(...)', () => {
783783
});
784784
});
785785

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

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

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

210-
// Keep global reference to the answers
211-
this.answers = typeof answers === 'object' ? { ...answers } : {};
212-
213231
let obs: Observable<AnyQuestion<A>>;
214232
if (isQuestionArray(questions)) {
215233
obs = from(questions);
@@ -224,34 +242,36 @@ export default class PromptsRunner<A extends Answers> {
224242
);
225243
} else {
226244
// Case: Called with a single question config
227-
obs = from([questions]);
245+
obs = from([questions as AnyQuestion<A>]);
228246
}
229247

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

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

0 commit comments

Comments
 (0)