1import { openJsInspector, queryAllInspectorAppsAsync } from '@expo/dev-server';
2import assert from 'assert';
3import openBrowserAsync from 'better-opn';
4import chalk from 'chalk';
5
6import * as Log from '../../log';
7import { delayAsync } from '../../utils/delay';
8import { learnMore } from '../../utils/link';
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        // TODO: if development build, change this message!
44        Log.log(printItem('Scan the QR code above with Expo Go (Android) or the Camera app (iOS)'));
45      } catch (error) {
46        // @ts-ignore: If there is no development build scheme, then skip the QR code.
47        if (error.code !== 'NO_DEV_CLIENT_SCHEME') {
48          throw error;
49        } else {
50          const serverUrl = devServer.getDevServerUrl();
51          Log.log(printItem(chalk`Metro waiting on {underline ${serverUrl}}`));
52          Log.log(printItem(`Linking is disabled because the client scheme cannot be resolved.`));
53        }
54      }
55    }
56
57    const webDevServer = this.devServerManager.getWebDevServer();
58    const webUrl = webDevServer?.getDevServerUrl({ hostType: 'localhost' });
59    if (webUrl) {
60      Log.log();
61      Log.log(printItem(chalk`Web is waiting on {underline ${webUrl}}`));
62    }
63
64    printUsage(options, { verbose: false });
65    printHelp();
66    Log.log();
67  }
68
69  async openJsInspectorAsync() {
70    Log.log('Opening JavaScript inspector in the browser...');
71    const metroServerOrigin = this.devServerManager.getDefaultDevServer().getJsInspectorBaseUrl();
72    assert(metroServerOrigin, 'Metro dev server is not running');
73    const apps = await queryAllInspectorAppsAsync(metroServerOrigin);
74    if (!apps.length) {
75      Log.warn(
76        `No compatible apps connected. JavaScript Debugging can only be used with the Hermes engine. ${learnMore(
77          'https://docs.expo.dev/guides/using-hermes/'
78        )}`
79      );
80      return;
81    }
82    try {
83      for (const app of apps) {
84        await openJsInspector(app);
85      }
86    } catch (error: any) {
87      Log.error('Failed to open JavaScript inspector. This is often an issue with Google Chrome.');
88      Log.exception(error);
89    }
90  }
91
92  reloadApp() {
93    Log.log(`${BLT} Reloading apps`);
94    // Send reload requests over the dev servers
95    this.devServerManager.broadcastMessage('reload');
96  }
97
98  async openMoreToolsAsync() {
99    try {
100      // Options match: Chrome > View > Developer
101      const value = await selectAsync(chalk`Dev tools {dim (native only)}`, [
102        { title: 'Inspect elements', value: 'toggleElementInspector' },
103        { title: 'Toggle performance monitor', value: 'togglePerformanceMonitor' },
104        { title: 'Toggle developer menu', value: 'toggleDevMenu' },
105        { title: 'Reload app', value: 'reload' },
106        { title: 'Start React devtools', value: 'startReactDevTools' },
107        // TODO: Maybe a "View Source" option to open code.
108        // Toggling Remote JS Debugging is pretty rough, so leaving it disabled.
109        // { title: 'Toggle Remote Debugging', value: 'toggleRemoteDebugging' },
110      ]);
111      if (value === 'startReactDevTools') {
112        this.startReactDevToolsAsync();
113      } else {
114        this.devServerManager.broadcastMessage('sendDevCommand', { name: value });
115      }
116    } catch (error: any) {
117      debug(error);
118      // do nothing
119    } finally {
120      printHelp();
121    }
122  }
123
124  async startReactDevToolsAsync() {
125    await startReactDevToolsProxyAsync();
126    const url = this.devServerManager.getDefaultDevServer().getReactDevToolsUrl();
127    await openBrowserAsync(url);
128    addReactDevToolsReloadListener(() => {
129      this.reconnectReactDevTools();
130    });
131    this.reconnectReactDevTools();
132  }
133
134  async reconnectReactDevTools() {
135    // Wait a little time for react-devtools to be initialized in browser
136    await delayAsync(3000);
137    this.devServerManager.broadcastMessage('sendDevCommand', { name: 'reconnectReactDevTools' });
138  }
139
140  toggleDevMenu() {
141    Log.log(`${BLT} Toggling dev menu`);
142    this.devServerManager.broadcastMessage('devMenu');
143  }
144}
145