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, 'devClient' | '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 's': { 140 Log.clear(); 141 if (await devServerManager.toggleRuntimeMode()) { 142 usageOptions.devClient = devServerManager.options.devClient; 143 return actions.printDevServerInfo(usageOptions); 144 } 145 break; 146 } 147 case 'w': { 148 try { 149 await devServerManager.ensureProjectPrerequisiteAsync(WebSupportProjectPrerequisite); 150 if (!platforms.includes('web')) { 151 platforms.push('web'); 152 options.platforms?.push('web'); 153 } 154 } catch (e: any) { 155 Log.warn(e.message); 156 break; 157 } 158 159 const isDisabled = !platforms.includes('web'); 160 if (isDisabled) { 161 debug('Web is disabled'); 162 // Use warnings from the web support setup. 163 break; 164 } 165 166 // Ensure the Webpack dev server is running first 167 if (!devServerManager.getWebDevServer()) { 168 debug('Starting up webpack dev server'); 169 await devServerManager.ensureWebDevServerRunningAsync(); 170 // When this is the first time webpack is started, reprint the connection info. 171 actions.printDevServerInfo(usageOptions); 172 } 173 174 Log.log(`${BLT} Open in the web browser...`); 175 try { 176 await devServerManager.getWebDevServer()?.openPlatformAsync('desktop'); 177 printHelp(); 178 } catch (error: any) { 179 if (!(error instanceof AbortCommandError)) { 180 Log.exception(error); 181 } 182 } 183 break; 184 } 185 case 'c': 186 Log.clear(); 187 return actions.printDevServerInfo(usageOptions); 188 case 'j': 189 return actions.openJsInspectorAsync(); 190 case 'r': 191 return actions.reloadApp(); 192 case 'o': 193 Log.log(`${BLT} Opening the editor...`); 194 return openInEditorAsync(devServerManager.projectRoot); 195 } 196 }; 197 198 const keyPressHandler = new KeyPressHandler(onPressAsync); 199 200 const listener = keyPressHandler.createInteractionListener(); 201 202 addInteractionListener(listener); 203 204 // Start observing... 205 keyPressHandler.startInterceptingKeyStrokes(); 206} 207