18d307f52SEvan Baconimport assert from 'assert';
28d307f52SEvan Baconimport chalk from 'chalk';
38d307f52SEvan Bacon
48a424bebSJames Ideimport { BLT, printHelp, printItem, printQRCode, printUsage, StartOptions } from './commandsTable';
58d307f52SEvan Baconimport * as Log from '../../log';
6fd055557SKudo Chienimport { delayAsync } from '../../utils/delay';
78d307f52SEvan Baconimport { learnMore } from '../../utils/link';
8065a44f7SCedric van Puttenimport { openBrowserAsync } from '../../utils/open';
98d307f52SEvan Baconimport { selectAsync } from '../../utils/prompts';
108d307f52SEvan Baconimport { DevServerManager } from '../server/DevServerManager';
11fd055557SKudo Chienimport {
12fd055557SKudo Chien  addReactDevToolsReloadListener,
13fd055557SKudo Chien  startReactDevToolsProxyAsync,
14fd055557SKudo Chien} from '../server/ReactDevToolsProxy';
15*edeec536SEvan Baconimport {
16*edeec536SEvan Bacon  openJsInspector,
17*edeec536SEvan Bacon  queryAllInspectorAppsAsync,
18*edeec536SEvan Bacon} from '../server/middleware/inspector/JsInspector';
198d307f52SEvan Bacon
20474a7a4bSEvan Baconconst debug = require('debug')('expo:start:interface:interactiveActions') as typeof console.log;
21474a7a4bSEvan Bacon
228d307f52SEvan Bacon/** Wraps the DevServerManager and adds an interface for user actions. */
238d307f52SEvan Baconexport class DevServerManagerActions {
248d307f52SEvan Bacon  constructor(private devServerManager: DevServerManager) {}
258d307f52SEvan Bacon
268d307f52SEvan Bacon  printDevServerInfo(
278d307f52SEvan Bacon    options: Pick<StartOptions, 'devClient' | 'isWebSocketsEnabled' | 'platforms'>
288d307f52SEvan Bacon  ) {
298d307f52SEvan Bacon    // If native dev server is running, print its URL.
308d307f52SEvan Bacon    if (this.devServerManager.getNativeDevServerPort()) {
31212e3a1aSEric Samelson      const devServer = this.devServerManager.getDefaultDevServer();
328d307f52SEvan Bacon      try {
33212e3a1aSEric Samelson        const nativeRuntimeUrl = devServer.getNativeRuntimeUrl()!;
34212e3a1aSEric Samelson        const interstitialPageUrl = devServer.getRedirectUrl();
358d307f52SEvan Bacon
36212e3a1aSEric Samelson        printQRCode(interstitialPageUrl ?? nativeRuntimeUrl);
37212e3a1aSEric Samelson
38212e3a1aSEric Samelson        if (interstitialPageUrl) {
39212e3a1aSEric Samelson          Log.log(
40212e3a1aSEric Samelson            printItem(
41212e3a1aSEric Samelson              chalk`Choose an app to open your project at {underline ${interstitialPageUrl}}`
42212e3a1aSEric Samelson            )
43212e3a1aSEric Samelson          );
44212e3a1aSEric Samelson        }
45212e3a1aSEric Samelson        Log.log(printItem(chalk`Metro waiting on {underline ${nativeRuntimeUrl}}`));
46a7e47f4dSEvan Bacon        if (options.devClient === false) {
478d307f52SEvan Bacon          // TODO: if development build, change this message!
48a7e47f4dSEvan Bacon          Log.log(
49a7e47f4dSEvan Bacon            printItem('Scan the QR code above with Expo Go (Android) or the Camera app (iOS)')
50a7e47f4dSEvan Bacon          );
51a7e47f4dSEvan Bacon        } else {
52a7e47f4dSEvan Bacon          Log.log(
53a7e47f4dSEvan Bacon            printItem(
54a7e47f4dSEvan Bacon              'Scan the QR code above to open the project in a development build. ' +
55a7e47f4dSEvan Bacon                learnMore('https://expo.fyi/start')
56a7e47f4dSEvan Bacon            )
57a7e47f4dSEvan Bacon          );
58a7e47f4dSEvan Bacon        }
598d307f52SEvan Bacon      } catch (error) {
60a7e47f4dSEvan Bacon        console.log('err', error);
618d307f52SEvan Bacon        // @ts-ignore: If there is no development build scheme, then skip the QR code.
628d307f52SEvan Bacon        if (error.code !== 'NO_DEV_CLIENT_SCHEME') {
638d307f52SEvan Bacon          throw error;
648d307f52SEvan Bacon        } else {
65212e3a1aSEric Samelson          const serverUrl = devServer.getDevServerUrl();
668d307f52SEvan Bacon          Log.log(printItem(chalk`Metro waiting on {underline ${serverUrl}}`));
678d307f52SEvan Bacon          Log.log(printItem(`Linking is disabled because the client scheme cannot be resolved.`));
688d307f52SEvan Bacon        }
698d307f52SEvan Bacon      }
708d307f52SEvan Bacon    }
718d307f52SEvan Bacon
726d6b81f9SEvan Bacon    const webDevServer = this.devServerManager.getWebDevServer();
736d6b81f9SEvan Bacon    const webUrl = webDevServer?.getDevServerUrl({ hostType: 'localhost' });
748d307f52SEvan Bacon    if (webUrl) {
758d307f52SEvan Bacon      Log.log();
766d6b81f9SEvan Bacon      Log.log(printItem(chalk`Web is waiting on {underline ${webUrl}}`));
778d307f52SEvan Bacon    }
788d307f52SEvan Bacon
798d307f52SEvan Bacon    printUsage(options, { verbose: false });
808d307f52SEvan Bacon    printHelp();
818d307f52SEvan Bacon    Log.log();
828d307f52SEvan Bacon  }
838d307f52SEvan Bacon
848d307f52SEvan Bacon  async openJsInspectorAsync() {
858d307f52SEvan Bacon    Log.log('Opening JavaScript inspector in the browser...');
8657a0d514SKudo Chien    const metroServerOrigin = this.devServerManager.getDefaultDevServer().getJsInspectorBaseUrl();
8723e1175dSKudo Chien    assert(metroServerOrigin, 'Metro dev server is not running');
888d307f52SEvan Bacon    const apps = await queryAllInspectorAppsAsync(metroServerOrigin);
898d307f52SEvan Bacon    if (!apps.length) {
908d307f52SEvan Bacon      Log.warn(
9148103a3dSEvan Bacon        `No compatible apps connected. JavaScript Debugging can only be used with the Hermes engine. ${learnMore(
928d307f52SEvan Bacon          'https://docs.expo.dev/guides/using-hermes/'
938d307f52SEvan Bacon        )}`
948d307f52SEvan Bacon      );
958d307f52SEvan Bacon      return;
968d307f52SEvan Bacon    }
977a619cc1SEvan Bacon    try {
988d307f52SEvan Bacon      for (const app of apps) {
997a619cc1SEvan Bacon        await openJsInspector(app);
1007a619cc1SEvan Bacon      }
1017a619cc1SEvan Bacon    } catch (error: any) {
1027a619cc1SEvan Bacon      Log.error('Failed to open JavaScript inspector. This is often an issue with Google Chrome.');
1037a619cc1SEvan Bacon      Log.exception(error);
1048d307f52SEvan Bacon    }
1058d307f52SEvan Bacon  }
1068d307f52SEvan Bacon
1078d307f52SEvan Bacon  reloadApp() {
1088d307f52SEvan Bacon    Log.log(`${BLT} Reloading apps`);
1098d307f52SEvan Bacon    // Send reload requests over the dev servers
1108d307f52SEvan Bacon    this.devServerManager.broadcastMessage('reload');
1118d307f52SEvan Bacon  }
1128d307f52SEvan Bacon
1138d307f52SEvan Bacon  async openMoreToolsAsync() {
1148d307f52SEvan Bacon    try {
1158d307f52SEvan Bacon      // Options match: Chrome > View > Developer
1168d307f52SEvan Bacon      const value = await selectAsync(chalk`Dev tools {dim (native only)}`, [
1178d307f52SEvan Bacon        { title: 'Inspect elements', value: 'toggleElementInspector' },
1188d307f52SEvan Bacon        { title: 'Toggle performance monitor', value: 'togglePerformanceMonitor' },
1198d307f52SEvan Bacon        { title: 'Toggle developer menu', value: 'toggleDevMenu' },
1208d307f52SEvan Bacon        { title: 'Reload app', value: 'reload' },
121fd055557SKudo Chien        { title: 'Start React devtools', value: 'startReactDevTools' },
1228d307f52SEvan Bacon        // TODO: Maybe a "View Source" option to open code.
1238d307f52SEvan Bacon        // Toggling Remote JS Debugging is pretty rough, so leaving it disabled.
1248d307f52SEvan Bacon        // { title: 'Toggle Remote Debugging', value: 'toggleRemoteDebugging' },
1258d307f52SEvan Bacon      ]);
126fd055557SKudo Chien      if (value === 'startReactDevTools') {
127fd055557SKudo Chien        this.startReactDevToolsAsync();
128fd055557SKudo Chien      } else {
1298d307f52SEvan Bacon        this.devServerManager.broadcastMessage('sendDevCommand', { name: value });
130fd055557SKudo Chien      }
13129975bfdSEvan Bacon    } catch (error: any) {
132474a7a4bSEvan Bacon      debug(error);
1338d307f52SEvan Bacon      // do nothing
1348d307f52SEvan Bacon    } finally {
1358d307f52SEvan Bacon      printHelp();
1368d307f52SEvan Bacon    }
1378d307f52SEvan Bacon  }
1388d307f52SEvan Bacon
139fd055557SKudo Chien  async startReactDevToolsAsync() {
140fd055557SKudo Chien    await startReactDevToolsProxyAsync();
141fd055557SKudo Chien    const url = this.devServerManager.getDefaultDevServer().getReactDevToolsUrl();
142fd055557SKudo Chien    await openBrowserAsync(url);
143fd055557SKudo Chien    addReactDevToolsReloadListener(() => {
144fd055557SKudo Chien      this.reconnectReactDevTools();
145fd055557SKudo Chien    });
146fd055557SKudo Chien    this.reconnectReactDevTools();
147fd055557SKudo Chien  }
148fd055557SKudo Chien
149fd055557SKudo Chien  async reconnectReactDevTools() {
150fd055557SKudo Chien    // Wait a little time for react-devtools to be initialized in browser
151fd055557SKudo Chien    await delayAsync(3000);
152fd055557SKudo Chien    this.devServerManager.broadcastMessage('sendDevCommand', { name: 'reconnectReactDevTools' });
153fd055557SKudo Chien  }
154fd055557SKudo Chien
1558d307f52SEvan Bacon  toggleDevMenu() {
1568d307f52SEvan Bacon    Log.log(`${BLT} Toggling dev menu`);
1578d307f52SEvan Bacon    this.devServerManager.broadcastMessage('devMenu');
1588d307f52SEvan Bacon  }
1598d307f52SEvan Bacon}
160