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