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