@@ -123,6 +123,15 @@ export type PromptFn<Value = any, Config = any> = (
123123 */
124124export 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+
126135class 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