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