diff --git a/packages/inquirer/inquirer.test.mts b/packages/inquirer/inquirer.test.mts index 06a431b1c3..b24dfafaa8 100644 --- a/packages/inquirer/inquirer.test.mts +++ b/packages/inquirer/inquirer.test.mts @@ -783,6 +783,64 @@ describe('inquirer.prompt(...)', () => { }); }); +describe('createPromptSession', () => { + it('should expose a Reactive subject across a session', async () => { + const localPrompt = inquirer.createPromptModule(); + 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(); + 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({ diff --git a/packages/inquirer/src/index.mts b/packages/inquirer/src/index.mts index 4da48ff1bc..3bfb54976a 100644 --- a/packages/inquirer/src/index.mts +++ b/packages/inquirer/src/index.mts @@ -98,9 +98,8 @@ export function createPromptModule< questions: PromptSession, answers?: Partial, ): PromptReturnType { - const runner = new PromptsRunner(promptModule.prompts, opt); - - const promptPromise = runner.run(questions, answers); + const runner = promptModule.createPromptSession({ answers }); + const promptPromise = runner.run(questions); return Object.assign(promptPromise, { ui: runner }); } @@ -124,6 +123,12 @@ export function createPromptModule< promptModule.prompts = { ...builtInPrompts }; }; + promptModule.createPromptSession = function ({ + answers, + }: { answers?: Partial } = {}) { + return new PromptsRunner(promptModule.prompts, { ...opt, answers }); + }; + return promptModule; } diff --git a/packages/inquirer/src/ui/prompt.mts b/packages/inquirer/src/ui/prompt.mts index 4bd262c306..c01650e663 100644 --- a/packages/inquirer/src/ui/prompt.mts +++ b/packages/inquirer/src/ui/prompt.mts @@ -1,8 +1,8 @@ /* 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, @@ -10,7 +10,9 @@ import { reduce, isObservable, Observable, + Subject, lastValueFrom, + tap, } from 'rxjs'; import runAsync from 'run-async'; import MuteStream from 'mute-stream'; @@ -40,11 +42,7 @@ export const _ = { pointer = pointer[key] as Record; }); }, - 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) @@ -193,23 +191,43 @@ function isPromptConstructor( */ export default class PromptsRunner { private prompts: PromptCollection; - answers: Partial = {}; - process: Observable = EMPTY; + answers: Partial; + 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 } = {}, + ) { 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, answers?: Partial): Promise { + async run = PromptSession>( + questions: Session, + ): Promise { this.abortController = new AbortController(); - // Keep global reference to the answers - this.answers = typeof answers === 'object' ? { ...answers } : {}; - let obs: Observable>; if (isQuestionArray(questions)) { obs = from(questions); @@ -224,34 +242,36 @@ export default class PromptsRunner { ); } else { // Case: Called with a single question config - obs = from([questions]); + obs = from([questions as AnyQuestion]); } - 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; + 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()); @@ -389,10 +409,7 @@ export default class PromptsRunner { }; private shouldRun = async (question: AnyQuestion): Promise => { - if ( - question.askAnswered !== true && - _.get(this.answers, question.name) !== undefined - ) { + if (question.askAnswered !== true && this.answers[question.name] !== undefined) { return false; }