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