Skip to content

Commit f818c27

Browse files
committed
extract methods
1 parent f7dfeef commit f818c27

File tree

1 file changed

+187
-148
lines changed

1 file changed

+187
-148
lines changed

packages/inquirer/src/ui/prompt.mts

Lines changed: 187 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,15 @@ export type PromptFn<Value = any, Config = any> = (
123123
*/
124124
export type PromptCollection = Record<string, PromptFn | LegacyPromptConstructor>;
125125

126+
type ResolvedQuestion<A extends Answers, Type extends string = string> = AnyQuestion<
127+
A,
128+
Type
129+
> & {
130+
message: string;
131+
default?: any;
132+
choices?: any;
133+
};
134+
126135
class TTYError extends Error {
127136
override name = 'TTYError';
128137
isTtyError = true;
@@ -186,6 +195,138 @@ function isPromptConstructor(
186195
);
187196
}
188197

198+
async function shouldRun<A extends Answers>(
199+
question: AnyQuestion<A>,
200+
answers: Partial<A>,
201+
): Promise<boolean> {
202+
if (question.askAnswered !== true && answers[question.name] !== undefined) {
203+
return false;
204+
}
205+
206+
const { when } = question;
207+
if (typeof when === 'function') {
208+
const shouldRun = await runAsync(when)(answers);
209+
return Boolean(shouldRun);
210+
}
211+
212+
return when !== false;
213+
}
214+
215+
function createLegacyPromptFn<A extends Answers>(
216+
prompt: LegacyPromptConstructor,
217+
answers: Partial<A>,
218+
): PromptFn<A> {
219+
return (q, opt) =>
220+
new Promise<A>((resolve, reject) => {
221+
let cleanupSignal: (() => void) | undefined;
222+
223+
const { signal } = opt;
224+
if (signal.aborted) {
225+
reject(new AbortPromptError({ cause: signal.reason }));
226+
return;
227+
}
228+
229+
const rl = readline.createInterface(setupReadlineOptions(opt)) as InquirerReadline;
230+
231+
const abort = () => {
232+
reject(new AbortPromptError({ cause: signal.reason }));
233+
cleanup();
234+
};
235+
/**
236+
* Handle the ^C exit
237+
*/
238+
const onForceClose = () => {
239+
abort();
240+
process.kill(process.pid, 'SIGINT');
241+
console.log('');
242+
};
243+
244+
const onClose = () => {
245+
process.removeListener('exit', onForceClose);
246+
rl.removeListener('SIGINT', onForceClose);
247+
rl.setPrompt('');
248+
rl.output.unmute();
249+
rl.output.write(ansiEscapes.cursorShow);
250+
rl.output.end();
251+
rl.close();
252+
};
253+
254+
// Make sure new prompt start on a newline when closing
255+
process.on('exit', onForceClose);
256+
rl.on('SIGINT', onForceClose);
257+
258+
const activePrompt = new prompt(q, rl, answers);
259+
260+
const cleanup = () => {
261+
onClose();
262+
cleanupSignal?.();
263+
};
264+
265+
signal.addEventListener('abort', abort);
266+
cleanupSignal = () => {
267+
signal.removeEventListener('abort', abort);
268+
cleanupSignal = undefined;
269+
};
270+
271+
activePrompt.run().then(resolve, reject).finally(cleanup);
272+
});
273+
}
274+
275+
async function prepareQuestion<A extends Answers>(
276+
question: AnyQuestion<A>,
277+
answers: Partial<A>,
278+
) {
279+
const [message, defaultValue, resolvedChoices] = await Promise.all([
280+
fetchAsyncQuestionProperty(question, 'message', answers),
281+
fetchAsyncQuestionProperty(question, 'default', answers),
282+
fetchAsyncQuestionProperty(question, 'choices', answers),
283+
]);
284+
285+
let choices;
286+
if (Array.isArray(resolvedChoices)) {
287+
choices = resolvedChoices.map((choice: unknown) => {
288+
if (typeof choice === 'string' || typeof choice === 'number') {
289+
return { name: choice, value: choice };
290+
} else if (
291+
typeof choice === 'object' &&
292+
choice != null &&
293+
!('value' in choice) &&
294+
'name' in choice
295+
) {
296+
return { ...choice, value: choice.name };
297+
}
298+
return choice;
299+
});
300+
}
301+
302+
return Object.assign({}, question, {
303+
message,
304+
default: defaultValue,
305+
choices,
306+
});
307+
}
308+
309+
async function fetchAnswer<A extends Answers>(
310+
prompt: PromptFn<any, any> | LegacyPromptConstructor | undefined,
311+
question: ResolvedQuestion<A>,
312+
answers: Partial<A>,
313+
context: StreamOptions & { signal: AbortSignal },
314+
) {
315+
if (prompt == null) {
316+
throw new Error(`Prompt for type ${question.type} not found`);
317+
}
318+
319+
const promptFn: PromptFn<A> = isPromptConstructor(prompt)
320+
? createLegacyPromptFn(prompt, answers)
321+
: prompt;
322+
323+
const { filter = (value) => value } = question;
324+
return promptFn(question, context).then((answer: unknown) => ({
325+
name: question.name,
326+
answer: filter(answer, answers),
327+
}));
328+
}
329+
189330
/**
190331
* Base interface class other can inherits from
191332
*/
@@ -227,6 +368,18 @@ export default class PromptsRunner<A extends Answers> {
227368
): Promise<A> {
228369
this.abortController = new AbortController();
229370

371+
let cleanupModuleSignal: (() => void) | undefined;
372+
const { signal: moduleSignal } = this.opt;
373+
if (moduleSignal?.aborted) {
374+
this.abortController.abort(moduleSignal.reason);
375+
} else if (moduleSignal) {
376+
const abort = () => this.abortController?.abort(moduleSignal.reason);
377+
moduleSignal.addEventListener('abort', abort);
378+
cleanupModuleSignal = () => {
379+
moduleSignal.removeEventListener('abort', abort);
380+
};
381+
}
382+
230383
let obs: Observable<AnyQuestion<A>>;
231384
if (isQuestionArray(questions)) {
232385
obs = from(questions);
@@ -252,172 +405,58 @@ export default class PromptsRunner<A extends Answers> {
252405
.pipe(
253406
concatMap((question) =>
254407
from(
255-
this.shouldRun(question).then((shouldRun: boolean | void) => {
256-
if (shouldRun) {
257-
return question;
258-
}
259-
return;
260-
}),
408+
shouldRun<A>(question, this.answers).then(
409+
(shouldRun: boolean | void) => {
410+
if (shouldRun) {
411+
return question;
412+
}
413+
return;
414+
},
415+
),
261416
).pipe(filter((val) => val != null)),
262417
),
263-
concatMap((question) => defer(() => from(this.fetchAnswer(question)))),
418+
concatMap((question) =>
419+
defer(() =>
420+
from(
421+
prepareQuestion(question, this.answers).then((question) =>
422+
fetchAnswer(
423+
this.prompts[question.type] ?? this.prompts['input'],
424+
question,
425+
this.answers,
426+
{ ...this.opt, signal: this.abortController.signal },
427+
),
428+
),
429+
),
430+
),
431+
),
264432
)
265433
.pipe(tap((answer) => this.process.next(answer))),
266434
),
267435
)
268436
.pipe(
269-
reduce((answersObj: Record<string, any>, answer: { name: string; answer: unknown }) => {
270-
answersObj[answer.name] = answer.answer;
271-
return answersObj;
272-
}, this.answers),
437+
reduce(
438+
(
439+
answersObj: Record<string, any>,
440+
answer: { name: string; answer: unknown },
441+
) => {
442+
answersObj[answer.name] = answer.answer;
443+
return answersObj;
444+
},
445+
this.answers,
446+
),
273447
),
274448
)
275449
.then(() => this.answers as A)
276-
.finally(() => this.close());
277-
}
278-
279-
private prepareQuestion = async (question: AnyQuestion<A>) => {
280-
const [message, defaultValue, resolvedChoices] = await Promise.all([
281-
fetchAsyncQuestionProperty(question, 'message', this.answers),
282-
fetchAsyncQuestionProperty(question, 'default', this.answers),
283-
fetchAsyncQuestionProperty(question, 'choices', this.answers),
284-
]);
285-
286-
let choices;
287-
if (Array.isArray(resolvedChoices)) {
288-
choices = resolvedChoices.map((choice: unknown) => {
289-
if (typeof choice === 'string' || typeof choice === 'number') {
290-
return { name: choice, value: choice };
291-
} else if (
292-
typeof choice === 'object' &&
293-
choice != null &&
294-
!('value' in choice) &&
295-
'name' in choice
296-
) {
297-
return { ...choice, value: choice.name };
298-
}
299-
return choice;
300-
});
301-
}
302-
303-
return Object.assign({}, question, {
304-
message,
305-
default: defaultValue,
306-
choices,
307-
type: question.type in this.prompts ? question.type : 'input',
308-
});
309-
};
310-
311-
private fetchAnswer = async (rawQuestion: AnyQuestion<A>) => {
312-
const question = await this.prepareQuestion(rawQuestion);
313-
const prompt = this.prompts[question.type];
314-
315-
if (prompt == null) {
316-
throw new Error(`Prompt for type ${question.type} not found`);
317-
}
318-
319-
let cleanupSignal: (() => void) | undefined;
320-
321-
const promptFn: PromptFn<A> = isPromptConstructor(prompt)
322-
? (q, opt) =>
323-
new Promise<A>((resolve, reject) => {
324-
const { signal } = opt;
325-
if (signal.aborted) {
326-
reject(new AbortPromptError({ cause: signal.reason }));
327-
return;
328-
}
329-
330-
const rl = readline.createInterface(
331-
setupReadlineOptions(opt),
332-
) as InquirerReadline;
333-
334-
/**
335-
* Handle the ^C exit
336-
*/
337-
const onForceClose = () => {
338-
this.close();
339-
process.kill(process.pid, 'SIGINT');
340-
console.log('');
341-
};
342-
343-
const onClose = () => {
344-
process.removeListener('exit', onForceClose);
345-
rl.removeListener('SIGINT', onForceClose);
346-
rl.setPrompt('');
347-
rl.output.unmute();
348-
rl.output.write(ansiEscapes.cursorShow);
349-
rl.output.end();
350-
rl.close();
351-
};
352-
353-
// Make sure new prompt start on a newline when closing
354-
process.on('exit', onForceClose);
355-
rl.on('SIGINT', onForceClose);
356-
357-
const activePrompt = new prompt(q, rl, this.answers);
358-
359-
const cleanup = () => {
360-
onClose();
361-
cleanupSignal?.();
362-
};
363-
364-
const abort = () => {
365-
reject(new AbortPromptError({ cause: signal.reason }));
366-
cleanup();
367-
};
368-
signal.addEventListener('abort', abort);
369-
cleanupSignal = () => {
370-
signal.removeEventListener('abort', abort);
371-
cleanupSignal = undefined;
372-
};
373-
374-
activePrompt.run().then(resolve, reject).finally(cleanup);
375-
})
376-
: prompt;
377-
378-
let cleanupModuleSignal: (() => void) | undefined;
379-
const { signal: moduleSignal } = this.opt;
380-
if (moduleSignal?.aborted) {
381-
this.abortController.abort(moduleSignal.reason);
382-
} else if (moduleSignal) {
383-
const abort = () => this.abortController?.abort(moduleSignal.reason);
384-
moduleSignal.addEventListener('abort', abort);
385-
cleanupModuleSignal = () => {
386-
moduleSignal.removeEventListener('abort', abort);
387-
};
388-
}
389-
390-
const { filter = (value) => value } = question;
391-
const { signal } = this.abortController;
392-
return promptFn(question, { ...this.opt, signal })
393-
.then((answer: unknown) => ({
394-
name: question.name,
395-
answer: filter(answer, this.answers),
396-
}))
397450
.finally(() => {
398-
cleanupSignal?.();
399451
cleanupModuleSignal?.();
452+
this.close();
400453
});
401-
};
454+
}
402455

403456
/**
404457
* Close the interface and cleanup listeners
405458
*/
406459
close = () => {
407460
this.abortController?.abort();
408461
};
409-
410-
private shouldRun = async (question: AnyQuestion<A>): Promise<boolean> => {
411-
if (question.askAnswered !== true && this.answers[question.name] !== undefined) {
412-
return false;
413-
}
414-
415-
const { when } = question;
416-
if (typeof when === 'function') {
417-
const shouldRun = await runAsync(when)(this.answers);
418-
return Boolean(shouldRun);
419-
}
420-
421-
return when !== false;
422-
};
423462
}

0 commit comments

Comments
 (0)