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