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