1import assert from 'assert'; 2import prompts, { Choice, Options, PromptObject, PromptType } from 'prompts'; 3 4import * as Log from '../log'; 5import { CI } from './env'; 6import { AbortCommandError, CommandError } from './errors'; 7 8export type Question<V extends string = string> = PromptObject<V> & { 9 optionsPerPage?: number; 10}; 11 12export interface ExpoChoice<T> extends Choice { 13 value: T; 14} 15 16export { PromptType }; 17 18type PromptOptions = { nonInteractiveHelp?: string } & Options; 19 20export type NamelessQuestion = Omit<Question<'value'>, 'name' | 'type'>; 21 22type InteractionOptions = { pause: boolean; canEscape?: boolean }; 23 24type InteractionCallback = (options: InteractionOptions) => void; 25 26/** Interaction observers for detecting when keystroke tracking should pause/resume. */ 27const listeners: InteractionCallback[] = []; 28 29export default async function prompt( 30 questions: Question | Question[], 31 { nonInteractiveHelp, ...options }: PromptOptions = {} 32) { 33 questions = Array.isArray(questions) ? questions : [questions]; 34 if (CI && questions.length !== 0) { 35 let message = `Input is required, but 'npx expo' is in non-interactive mode.\n`; 36 if (nonInteractiveHelp) { 37 message += nonInteractiveHelp; 38 } else { 39 const question = questions[0]; 40 const questionMessage = 41 typeof question.message === 'function' 42 ? question.message(undefined, {}, question) 43 : question.message; 44 45 message += `Required input:\n${(questionMessage || '').trim().replace(/^/gm, '> ')}`; 46 } 47 throw new CommandError('NON_INTERACTIVE', message); 48 } 49 50 pauseInteractions(); 51 try { 52 const results = await prompts(questions, { 53 onCancel() { 54 throw new AbortCommandError(); 55 }, 56 ...options, 57 }); 58 59 return results; 60 } finally { 61 resumeInteractions(); 62 } 63} 64 65/** 66 * Create a standard yes/no confirmation that can be cancelled. 67 * 68 * @param questions 69 * @param options 70 */ 71export async function confirmAsync( 72 questions: NamelessQuestion, 73 options?: PromptOptions 74): Promise<boolean> { 75 const { value } = await prompt( 76 { 77 initial: true, 78 ...questions, 79 name: 'value', 80 type: 'confirm', 81 }, 82 options 83 ); 84 return value ?? null; 85} 86 87/** Select an option from a list of options. */ 88export async function selectAsync<T>( 89 message: string, 90 choices: ExpoChoice<T>[], 91 options?: PromptOptions 92): Promise<T> { 93 const { value } = await prompt( 94 { 95 message, 96 choices, 97 name: 'value', 98 type: 'select', 99 }, 100 options 101 ); 102 return value ?? null; 103} 104 105export const promptAsync = prompt; 106 107/** Used to pause/resume interaction observers while prompting (made for TerminalUI). */ 108export function addInteractionListener(callback: InteractionCallback) { 109 listeners.push(callback); 110} 111 112export function removeInteractionListener(callback: InteractionCallback) { 113 const listenerIndex = listeners.findIndex((_callback) => _callback === callback); 114 assert( 115 listenerIndex >= 0, 116 'removeInteractionListener(): cannot remove an unregistered event listener.' 117 ); 118 listeners.splice(listenerIndex, 1); 119} 120 121/** Notify all listeners that keypress observations must pause. */ 122export function pauseInteractions(options: Omit<InteractionOptions, 'pause'> = {}) { 123 Log.debug('Interaction observers paused'); 124 for (const listener of listeners) { 125 listener({ pause: true, ...options }); 126 } 127} 128 129/** Notify all listeners that keypress observations can start.. */ 130export function resumeInteractions(options: Omit<InteractionOptions, 'pause'> = {}) { 131 Log.debug('Interaction observers resumed'); 132 for (const listener of listeners) { 133 listener({ pause: false, ...options }); 134 } 135} 136