18d307f52SEvan Baconimport chalk from 'chalk'; 28d307f52SEvan Bacon 3*8a424bebSJames Ideimport { KeyPressHandler } from './KeyPressHandler'; 4*8a424bebSJames Ideimport { BLT, printHelp, printUsage, StartOptions } from './commandsTable'; 5*8a424bebSJames Ideimport { DevServerManagerActions } from './interactiveActions'; 68d307f52SEvan Baconimport * as Log from '../../log'; 78d307f52SEvan Baconimport { openInEditorAsync } from '../../utils/editor'; 88d307f52SEvan Baconimport { AbortCommandError } from '../../utils/errors'; 98d307f52SEvan Baconimport { getAllSpinners, ora } from '../../utils/ora'; 108d307f52SEvan Baconimport { getProgressBar, setProgressBar } from '../../utils/progress'; 118d307f52SEvan Baconimport { addInteractionListener, pauseInteractions } from '../../utils/prompts'; 128d307f52SEvan Baconimport { WebSupportProjectPrerequisite } from '../doctor/web/WebSupportProjectPrerequisite'; 138d307f52SEvan Baconimport { DevServerManager } from '../server/DevServerManager'; 148d307f52SEvan Bacon 15474a7a4bSEvan Baconconst debug = require('debug')('expo:start:interface:startInterface') as typeof console.log; 16474a7a4bSEvan Bacon 178d307f52SEvan Baconconst CTRL_C = '\u0003'; 188d307f52SEvan Baconconst CTRL_D = '\u0004'; 198d307f52SEvan Baconconst CTRL_L = '\u000C'; 208d307f52SEvan Bacon 218d307f52SEvan Baconconst PLATFORM_SETTINGS: Record< 228d307f52SEvan Bacon string, 238d307f52SEvan Bacon { name: string; key: 'android' | 'ios'; launchTarget: 'emulator' | 'simulator' } 248d307f52SEvan Bacon> = { 258d307f52SEvan Bacon android: { 268d307f52SEvan Bacon name: 'Android', 278d307f52SEvan Bacon key: 'android', 288d307f52SEvan Bacon launchTarget: 'emulator', 298d307f52SEvan Bacon }, 308d307f52SEvan Bacon ios: { 318d307f52SEvan Bacon name: 'iOS', 328d307f52SEvan Bacon key: 'ios', 338d307f52SEvan Bacon launchTarget: 'simulator', 348d307f52SEvan Bacon }, 358d307f52SEvan Bacon}; 368d307f52SEvan Bacon 378d307f52SEvan Baconexport async function startInterfaceAsync( 388d307f52SEvan Bacon devServerManager: DevServerManager, 39a7e47f4dSEvan Bacon options: Pick<StartOptions, 'devClient' | 'platforms'> 408d307f52SEvan Bacon) { 418d307f52SEvan Bacon const actions = new DevServerManagerActions(devServerManager); 428d307f52SEvan Bacon 438d307f52SEvan Bacon const isWebSocketsEnabled = devServerManager.getDefaultDevServer()?.isTargetingNative(); 448d307f52SEvan Bacon 458d307f52SEvan Bacon const usageOptions = { 468d307f52SEvan Bacon isWebSocketsEnabled, 478d307f52SEvan Bacon devClient: devServerManager.options.devClient, 488d307f52SEvan Bacon ...options, 498d307f52SEvan Bacon }; 508d307f52SEvan Bacon 518d307f52SEvan Bacon actions.printDevServerInfo(usageOptions); 528d307f52SEvan Bacon 538d307f52SEvan Bacon const onPressAsync = async (key: string) => { 548d307f52SEvan Bacon // Auxillary commands all escape. 558d307f52SEvan Bacon switch (key) { 568d307f52SEvan Bacon case CTRL_C: 578d307f52SEvan Bacon case CTRL_D: { 588d307f52SEvan Bacon // Prevent terminal UI from accepting commands while the process is closing. 598d307f52SEvan Bacon // Without this, fast typers will close the server then start typing their 608d307f52SEvan Bacon // next command and have a bunch of unrelated things pop up. 618d307f52SEvan Bacon pauseInteractions(); 628d307f52SEvan Bacon 638d307f52SEvan Bacon const spinners = getAllSpinners(); 648d307f52SEvan Bacon spinners.forEach((spinner) => { 658d307f52SEvan Bacon spinner.fail(); 668d307f52SEvan Bacon }); 678d307f52SEvan Bacon 688d307f52SEvan Bacon const currentProgress = getProgressBar(); 698d307f52SEvan Bacon if (currentProgress) { 708d307f52SEvan Bacon currentProgress.terminate(); 718d307f52SEvan Bacon setProgressBar(null); 728d307f52SEvan Bacon } 738d307f52SEvan Bacon const spinner = ora({ text: 'Stopping server', color: 'white' }).start(); 748d307f52SEvan Bacon try { 758d307f52SEvan Bacon await devServerManager.stopAsync(); 768d307f52SEvan Bacon spinner.stopAndPersist({ text: 'Stopped server', symbol: `\u203A` }); 778d307f52SEvan Bacon // @ts-ignore: Argument of type '"SIGINT"' is not assignable to parameter of type '"disconnect"'. 788d307f52SEvan Bacon process.emit('SIGINT'); 798d307f52SEvan Bacon 808d307f52SEvan Bacon // TODO: Is this the right place to do this? 818d307f52SEvan Bacon process.exit(); 828d307f52SEvan Bacon } catch (error) { 838d307f52SEvan Bacon spinner.fail('Failed to stop server'); 848d307f52SEvan Bacon throw error; 858d307f52SEvan Bacon } 868d307f52SEvan Bacon break; 878d307f52SEvan Bacon } 888d307f52SEvan Bacon case CTRL_L: 898d307f52SEvan Bacon return Log.clear(); 908d307f52SEvan Bacon case '?': 918d307f52SEvan Bacon return printUsage(usageOptions, { verbose: true }); 928d307f52SEvan Bacon } 938d307f52SEvan Bacon 948d307f52SEvan Bacon // Optionally enabled 958d307f52SEvan Bacon 968d307f52SEvan Bacon if (isWebSocketsEnabled) { 978d307f52SEvan Bacon switch (key) { 988d307f52SEvan Bacon case 'm': 998d307f52SEvan Bacon return actions.toggleDevMenu(); 1008d307f52SEvan Bacon case 'M': 1018d307f52SEvan Bacon return actions.openMoreToolsAsync(); 1028d307f52SEvan Bacon } 1038d307f52SEvan Bacon } 1048d307f52SEvan Bacon 1058d307f52SEvan Bacon const { platforms = ['ios', 'android', 'web'] } = options; 1068d307f52SEvan Bacon 1078d307f52SEvan Bacon if (['i', 'a'].includes(key.toLowerCase())) { 1088d307f52SEvan Bacon const platform = key.toLowerCase() === 'i' ? 'ios' : 'android'; 1098d307f52SEvan Bacon 1108d307f52SEvan Bacon const shouldPrompt = ['I', 'A'].includes(key); 1118d307f52SEvan Bacon if (shouldPrompt) { 1128d307f52SEvan Bacon Log.clear(); 1138d307f52SEvan Bacon } 1148d307f52SEvan Bacon 1158d307f52SEvan Bacon const server = devServerManager.getDefaultDevServer(); 1168d307f52SEvan Bacon const settings = PLATFORM_SETTINGS[platform]; 1178d307f52SEvan Bacon 1188d307f52SEvan Bacon Log.log(`${BLT} Opening on ${settings.name}...`); 1198d307f52SEvan Bacon 1208d307f52SEvan Bacon if (server.isTargetingNative() && !platforms.includes(settings.key)) { 1218d307f52SEvan Bacon Log.warn( 1228d307f52SEvan Bacon chalk`${settings.name} is disabled, enable it by adding {bold ${settings.key}} to the platforms array in your app.json or app.config.js` 1238d307f52SEvan Bacon ); 1248d307f52SEvan Bacon } else { 1258d307f52SEvan Bacon try { 1268d307f52SEvan Bacon await server.openPlatformAsync(settings.launchTarget, { shouldPrompt }); 1278d307f52SEvan Bacon printHelp(); 12829975bfdSEvan Bacon } catch (error: any) { 12929975bfdSEvan Bacon if (!(error instanceof AbortCommandError)) { 13029975bfdSEvan Bacon Log.exception(error); 1318d307f52SEvan Bacon } 1328d307f52SEvan Bacon } 1338d307f52SEvan Bacon } 1348d307f52SEvan Bacon // Break out early. 1358d307f52SEvan Bacon return; 1368d307f52SEvan Bacon } 1378d307f52SEvan Bacon 1388d307f52SEvan Bacon switch (key) { 139a7e47f4dSEvan Bacon case 's': { 140a7e47f4dSEvan Bacon Log.clear(); 141a7e47f4dSEvan Bacon if (await devServerManager.toggleRuntimeMode()) { 142a7e47f4dSEvan Bacon usageOptions.devClient = devServerManager.options.devClient; 143a7e47f4dSEvan Bacon return actions.printDevServerInfo(usageOptions); 144a7e47f4dSEvan Bacon } 145a7e47f4dSEvan Bacon break; 146a7e47f4dSEvan Bacon } 1478d307f52SEvan Bacon case 'w': { 1488d307f52SEvan Bacon try { 1498d307f52SEvan Bacon await devServerManager.ensureProjectPrerequisiteAsync(WebSupportProjectPrerequisite); 1508d307f52SEvan Bacon if (!platforms.includes('web')) { 1518d307f52SEvan Bacon platforms.push('web'); 1528d307f52SEvan Bacon options.platforms?.push('web'); 1538d307f52SEvan Bacon } 1548d307f52SEvan Bacon } catch (e: any) { 1558d307f52SEvan Bacon Log.warn(e.message); 1568d307f52SEvan Bacon break; 1578d307f52SEvan Bacon } 1588d307f52SEvan Bacon 1598d307f52SEvan Bacon const isDisabled = !platforms.includes('web'); 1608d307f52SEvan Bacon if (isDisabled) { 161474a7a4bSEvan Bacon debug('Web is disabled'); 1628d307f52SEvan Bacon // Use warnings from the web support setup. 1638d307f52SEvan Bacon break; 1648d307f52SEvan Bacon } 1658d307f52SEvan Bacon 1668d307f52SEvan Bacon // Ensure the Webpack dev server is running first 1678d307f52SEvan Bacon if (!devServerManager.getWebDevServer()) { 168474a7a4bSEvan Bacon debug('Starting up webpack dev server'); 1698d307f52SEvan Bacon await devServerManager.ensureWebDevServerRunningAsync(); 1708d307f52SEvan Bacon // When this is the first time webpack is started, reprint the connection info. 1718d307f52SEvan Bacon actions.printDevServerInfo(usageOptions); 1728d307f52SEvan Bacon } 1738d307f52SEvan Bacon 1748d307f52SEvan Bacon Log.log(`${BLT} Open in the web browser...`); 1758d307f52SEvan Bacon try { 17629975bfdSEvan Bacon await devServerManager.getWebDevServer()?.openPlatformAsync('desktop'); 1778d307f52SEvan Bacon printHelp(); 17829975bfdSEvan Bacon } catch (error: any) { 17929975bfdSEvan Bacon if (!(error instanceof AbortCommandError)) { 18029975bfdSEvan Bacon Log.exception(error); 1818d307f52SEvan Bacon } 1828d307f52SEvan Bacon } 1838d307f52SEvan Bacon break; 1848d307f52SEvan Bacon } 1858d307f52SEvan Bacon case 'c': 1868d307f52SEvan Bacon Log.clear(); 1878d307f52SEvan Bacon return actions.printDevServerInfo(usageOptions); 1888d307f52SEvan Bacon case 'j': 1898d307f52SEvan Bacon return actions.openJsInspectorAsync(); 1908d307f52SEvan Bacon case 'r': 1918d307f52SEvan Bacon return actions.reloadApp(); 1928d307f52SEvan Bacon case 'o': 1938d307f52SEvan Bacon Log.log(`${BLT} Opening the editor...`); 1948d307f52SEvan Bacon return openInEditorAsync(devServerManager.projectRoot); 1958d307f52SEvan Bacon } 1968d307f52SEvan Bacon }; 1978d307f52SEvan Bacon 1988d307f52SEvan Bacon const keyPressHandler = new KeyPressHandler(onPressAsync); 1998d307f52SEvan Bacon 2008d307f52SEvan Bacon const listener = keyPressHandler.createInteractionListener(); 2018d307f52SEvan Bacon 2028d307f52SEvan Bacon addInteractionListener(listener); 2038d307f52SEvan Bacon 2048d307f52SEvan Bacon // Start observing... 2058d307f52SEvan Bacon keyPressHandler.startInterceptingKeyStrokes(); 2068d307f52SEvan Bacon} 207