1import assert from 'assert';
2import chalk from 'chalk';
3
4import { BLT, printHelp, printItem, printQRCode, printUsage, StartOptions } from './commandsTable';
5import * as Log from '../../log';
6import { delayAsync } from '../../utils/delay';
7import { learnMore } from '../../utils/link';
8import { openBrowserAsync } from '../../utils/open';
9import { selectAsync } from '../../utils/prompts';
10import { DevServerManager } from '../server/DevServerManager';
11import {
12  addReactDevToolsReloadListener,
13  startReactDevToolsProxyAsync,
14} from '../server/ReactDevToolsProxy';
15import {
16  openJsInspector,
17  queryAllInspectorAppsAsync,
18} from '../server/middleware/inspector/JsInspector';
19
20const debug = require('debug')('expo:start:interface:interactiveActions') as typeof console.log;
21
22/** Wraps the DevServerManager and adds an interface for user actions. */
23export class DevServerManagerActions {
24  constructor(private devServerManager: DevServerManager) {}
25
26  printDevServerInfo(
27    options: Pick<StartOptions, 'devClient' | 'isWebSocketsEnabled' | 'platforms'>
28  ) {
29    // If native dev server is running, print its URL.
30    if (this.devServerManager.getNativeDevServerPort()) {
31      const devServer = this.devServerManager.getDefaultDevServer();
32      try {
33        const nativeRuntimeUrl = devServer.getNativeRuntimeUrl()!;
34        const interstitialPageUrl = devServer.getRedirectUrl();
35
36        printQRCode(interstitialPageUrl ?? nativeRuntimeUrl);
37
38        if (interstitialPageUrl) {
39          Log.log(
40            printItem(
41              chalk`Choose an app to open your project at {underline ${interstitialPageUrl}}`
42            )
43          );
44        }
45        Log.log(printItem(chalk`Metro waiting on {underline ${nativeRuntimeUrl}}`));
46        if (options.devClient === false) {
47          // TODO: if development build, change this message!
48          Log.log(
49            printItem('Scan the QR code above with Expo Go (Android) or the Camera app (iOS)')
50          );
51        } else {
52          Log.log(
53            printItem(
54              'Scan the QR code above to open the project in a development build. ' +
55                learnMore('https://expo.fyi/start')
56            )
57          );
58        }
59      } catch (error) {
60        console.log('err', error);
61        // @ts-ignore: If there is no development build scheme, then skip the QR code.
62        if (error.code !== 'NO_DEV_CLIENT_SCHEME') {
63          throw error;
64        } else {
65          const serverUrl = devServer.getDevServerUrl();
66          Log.log(printItem(chalk`Metro waiting on {underline ${serverUrl}}`));
67          Log.log(printItem(`Linking is disabled because the client scheme cannot be resolved.`));
68        }
69      }
70    }
71
72    const webDevServer = this.devServerManager.getWebDevServer();
73    const webUrl = webDevServer?.getDevServerUrl({ hostType: 'localhost' });
74    if (webUrl) {
75      Log.log();
76      Log.log(printItem(chalk`Web is waiting on {underline ${webUrl}}`));
77    }
78
79    printUsage(options, { verbose: false });
80    printHelp();
81    Log.log();
82  }
83
84  async openJsInspectorAsync() {
85    Log.log('Opening JavaScript inspector in the browser...');
86    const metroServerOrigin = this.devServerManager.getDefaultDevServer().getJsInspectorBaseUrl();
87    assert(metroServerOrigin, 'Metro dev server is not running');
88    const apps = await queryAllInspectorAppsAsync(metroServerOrigin);
89    if (!apps.length) {
90      Log.warn(
91        `No compatible apps connected. JavaScript Debugging can only be used with the Hermes engine. ${learnMore(
92          'https://docs.expo.dev/guides/using-hermes/'
93        )}`
94      );
95      return;
96    }
97    try {
98      for (const app of apps) {
99        await openJsInspector(app);
100      }
101    } catch (error: any) {
102      Log.error('Failed to open JavaScript inspector. This is often an issue with Google Chrome.');
103      Log.exception(error);
104    }
105  }
106
107  reloadApp() {
108    Log.log(`${BLT} Reloading apps`);
109    // Send reload requests over the dev servers
110    this.devServerManager.broadcastMessage('reload');
111  }
112
113  async openMoreToolsAsync() {
114    try {
115      // Options match: Chrome > View > Developer
116      const value = await selectAsync(chalk`Dev tools {dim (native only)}`, [
117        { title: 'Inspect elements', value: 'toggleElementInspector' },
118        { title: 'Toggle performance monitor', value: 'togglePerformanceMonitor' },
119        { title: 'Toggle developer menu', value: 'toggleDevMenu' },
120        { title: 'Reload app', value: 'reload' },
121        { title: 'Start React devtools', value: 'startReactDevTools' },
122        // TODO: Maybe a "View Source" option to open code.
123        // Toggling Remote JS Debugging is pretty rough, so leaving it disabled.
124        // { title: 'Toggle Remote Debugging', value: 'toggleRemoteDebugging' },
125      ]);
126      if (value === 'startReactDevTools') {
127        this.startReactDevToolsAsync();
128      } else {
129        this.devServerManager.broadcastMessage('sendDevCommand', { name: value });
130      }
131    } catch (error: any) {
132      debug(error);
133      // do nothing
134    } finally {
135      printHelp();
136    }
137  }
138
139  async startReactDevToolsAsync() {
140    await startReactDevToolsProxyAsync();
141    const url = this.devServerManager.getDefaultDevServer().getReactDevToolsUrl();
142    await openBrowserAsync(url);
143    addReactDevToolsReloadListener(() => {
144      this.reconnectReactDevTools();
145    });
146    this.reconnectReactDevTools();
147  }
148
149  async reconnectReactDevTools() {
150    // Wait a little time for react-devtools to be initialized in browser
151    await delayAsync(3000);
152    this.devServerManager.broadcastMessage('sendDevCommand', { name: 'reconnectReactDevTools' });
153  }
154
155  toggleDevMenu() {
156    Log.log(`${BLT} Toggling dev menu`);
157    this.devServerManager.broadcastMessage('devMenu');
158  }
159}
160