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