@@ -123,6 +123,15 @@ export type PromptFn<Value = any, Config = any> = (
123
123
*/
124
124
export type PromptCollection = Record < string , PromptFn | LegacyPromptConstructor > ;
125
125
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
+
126
135
class TTYError extends Error {
127
136
override name = 'TTYError' ;
128
137
isTtyError = true ;
@@ -186,6 +195,138 @@ function isPromptConstructor(
186
195
) ;
187
196
}
188
197
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
+
189
330
/**
190
331
* Base interface class other can inherits from
191
332
*/
@@ -227,6 +368,18 @@ export default class PromptsRunner<A extends Answers> {
227
368
) : Promise < A > {
228
369
this . abortController = new AbortController ( ) ;
229
370
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
+
230
383
let obs : Observable < AnyQuestion < A > > ;
231
384
if ( isQuestionArray ( questions ) ) {
232
385
obs = from ( questions ) ;
@@ -252,172 +405,58 @@ export default class PromptsRunner<A extends Answers> {
252
405
. pipe (
253
406
concatMap ( ( question ) =>
254
407
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
+ ) ,
261
416
) . pipe ( filter ( ( val ) => val != null ) ) ,
262
417
) ,
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
+ ) ,
264
432
)
265
433
. pipe ( tap ( ( answer ) => this . process . next ( answer ) ) ) ,
266
434
) ,
267
435
)
268
436
. 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
+ ) ,
273
447
) ,
274
448
)
275
449
. 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
- } ) )
397
450
. finally ( ( ) => {
398
- cleanupSignal ?.( ) ;
399
451
cleanupModuleSignal ?.( ) ;
452
+ this . close ( ) ;
400
453
} ) ;
401
- } ;
454
+ }
402
455
403
456
/**
404
457
* Close the interface and cleanup listeners
405
458
*/
406
459
close = ( ) => {
407
460
this . abortController ?. abort ( ) ;
408
461
} ;
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
- } ;
423
462
}
0 commit comments