1import chalk from 'chalk'; 2 3import * as Log from '../../log'; 4import { openInEditorAsync } from '../../utils/editor'; 5import { AbortCommandError } from '../../utils/errors'; 6import { getAllSpinners, ora } from '../../utils/ora'; 7import { getProgressBar, setProgressBar } from '../../utils/progress'; 8import { addInteractionListener, pauseInteractions } from '../../utils/prompts'; 9import { WebSupportProjectPrerequisite } from '../doctor/web/WebSupportProjectPrerequisite'; 10import { DevServerManager } from '../server/DevServerManager'; 11import { KeyPressHandler } from './KeyPressHandler'; 12import { BLT, printHelp, printUsage, StartOptions } from './commandsTable'; 13import { DevServerManagerActions } from './interactiveActions'; 14 15const debug = require('debug')('expo:start:interface:startInterface') as typeof console.log; 16 17const CTRL_C = '\u0003'; 18const CTRL_D = '\u0004'; 19const CTRL_L = '\u000C'; 20 21const PLATFORM_SETTINGS: Record< 22 string, 23 { name: string; key: 'android' | 'ios'; launchTarget: 'emulator' | 'simulator' } 24> = { 25 android: { 26 name: 'Android', 27 key: 'android', 28 launchTarget: 'emulator', 29 }, 30 ios: { 31 name: 'iOS', 32 key: 'ios', 33 launchTarget: 'simulator', 34 }, 35}; 36 37export async function startInterfaceAsync( 38 devServerManager: DevServerManager, 39 options: Pick<StartOptions, 'platforms'> 40) { 41 const actions = new DevServerManagerActions(devServerManager); 42 43 const isWebSocketsEnabled = devServerManager.getDefaultDevServer()?.isTargetingNative(); 44 45 const usageOptions = { 46 isWebSocketsEnabled, 47 devClient: devServerManager.options.devClient, 48 ...options, 49 }; 50 51 actions.printDevServerInfo(usageOptions); 52 53 const onPressAsync = async (key: string) => { 54 // Auxillary commands all escape. 55 switch (key) { 56 case CTRL_C: 57 case CTRL_D: { 58 // Prevent terminal UI from accepting commands while the process is closing. 59 // Without this, fast typers will close the server then start typing their 60 // next command and have a bunch of unrelated things pop up. 61 pauseInteractions(); 62 63 const spinners = getAllSpinners(); 64 spinners.forEach((spinner) => { 65 spinner.fail(); 66 }); 67 68 const currentProgress = getProgressBar(); 69 if (currentProgress) { 70 currentProgress.terminate(); 71 setProgressBar(null); 72 } 73 const spinner = ora({ text: 'Stopping server', color: 'white' }).start(); 74 try { 75 await devServerManager.stopAsync(); 76 spinner.stopAndPersist({ text: 'Stopped server', symbol: `\u203A` }); 77 // @ts-ignore: Argument of type '"SIGINT"' is not assignable to parameter of type '"disconnect"'. 78 process.emit('SIGINT'); 79 80 // TODO: Is this the right place to do this? 81 process.exit(); 82 } catch (error) { 83 spinner.fail('Failed to stop server'); 84 throw error; 85 } 86 break; 87 } 88 case CTRL_L: 89 return Log.clear(); 90 case '?': 91 return printUsage(usageOptions, { verbose: true }); 92 } 93 94 // Optionally enabled 95 96 if (isWebSocketsEnabled) { 97 switch (key) { 98 case 'm': 99 return actions.toggleDevMenu(); 100 case 'M': 101 return actions.openMoreToolsAsync(); 102 } 103 } 104 105 const { platforms = ['ios', 'android', 'web'] } = options; 106 107 if (['i', 'a'].includes(key.toLowerCase())) { 108 const platform = key.toLowerCase() === 'i' ? 'ios' : 'android'; 109 110 const shouldPrompt = ['I', 'A'].includes(key); 111 if (shouldPrompt) { 112 Log.clear(); 113 } 114 115 const server = devServerManager.getDefaultDevServer(); 116 const settings = PLATFORM_SETTINGS[platform]; 117 118 Log.log(`${BLT} Opening on ${settings.name}...`); 119 120 if (server.isTargetingNative() && !platforms.includes(settings.key)) { 121 Log.warn( 122 chalk`${settings.name} is disabled, enable it by adding {bold ${settings.key}} to the platforms array in your app.json or app.config.js` 123 ); 124 } else { 125 try { 126 await server.openPlatformAsync(settings.launchTarget, { shouldPrompt }); 127 printHelp(); 128 } catch (error: any) { 129 if (!(error instanceof AbortCommandError)) { 130 Log.exception(error); 131 } 132 } 133 } 134 // Break out early. 135 return; 136 } 137 138 switch (key) { 139 case 'w': { 140 try { 141 await devServerManager.ensureProjectPrerequisiteAsync(WebSupportProjectPrerequisite); 142 if (!platforms.includes('web')) { 143 platforms.push('web'); 144 options.platforms?.push('web'); 145 } 146 } catch (e: any) { 147 Log.warn(e.message); 148 break; 149 } 150 151 const isDisabled = !platforms.includes('web'); 152 if (isDisabled) { 153 debug('Web is disabled'); 154 // Use warnings from the web support setup. 155 break; 156 } 157 158 // Ensure the Webpack dev server is running first 159 if (!devServerManager.getWebDevServer()) { 160 debug('Starting up webpack dev server'); 161 await devServerManager.ensureWebDevServerRunningAsync(); 162 // When this is the first time webpack is started, reprint the connection info. 163 actions.printDevServerInfo(usageOptions); 164 } 165 166 Log.log(`${BLT} Open in the web browser...`); 167 try { 168 await devServerManager.getWebDevServer()?.openPlatformAsync('desktop'); 169 printHelp(); 170 } catch (error: any) { 171 if (!(error instanceof AbortCommandError)) { 172 Log.exception(error); 173 } 174 } 175 break; 176 } 177 case 'c': 178 Log.clear(); 179 return actions.printDevServerInfo(usageOptions); 180 case 'j': 181 return actions.openJsInspectorAsync(); 182 case 'r': 183 return actions.reloadApp(); 184 case 'o': 185 Log.log(`${BLT} Opening the editor...`); 186 return openInEditorAsync(devServerManager.projectRoot); 187 } 188 }; 189 190 const keyPressHandler = new KeyPressHandler(onPressAsync); 191 192 const listener = keyPressHandler.createInteractionListener(); 193 194 addInteractionListener(listener); 195 196 // Start observing... 197 keyPressHandler.startInterceptingKeyStrokes(); 198} 199