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