1import assert from 'assert'; 2import chalk from 'chalk'; 3 4import { BLT, printHelp, printItem, printQRCode, printUsage, StartOptions } from './commandsTable'; 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 { 16 openJsInspector, 17 queryAllInspectorAppsAsync, 18} from '../server/middleware/inspector/JsInspector'; 19 20const debug = require('debug')('expo:start:interface:interactiveActions') as typeof console.log; 21 22/** Wraps the DevServerManager and adds an interface for user actions. */ 23export class DevServerManagerActions { 24 constructor(private devServerManager: DevServerManager) {} 25 26 printDevServerInfo( 27 options: Pick<StartOptions, 'devClient' | 'isWebSocketsEnabled' | 'platforms'> 28 ) { 29 // If native dev server is running, print its URL. 30 if (this.devServerManager.getNativeDevServerPort()) { 31 const devServer = this.devServerManager.getDefaultDevServer(); 32 try { 33 const nativeRuntimeUrl = devServer.getNativeRuntimeUrl()!; 34 const interstitialPageUrl = devServer.getRedirectUrl(); 35 36 printQRCode(interstitialPageUrl ?? nativeRuntimeUrl); 37 38 if (interstitialPageUrl) { 39 Log.log( 40 printItem( 41 chalk`Choose an app to open your project at {underline ${interstitialPageUrl}}` 42 ) 43 ); 44 } 45 Log.log(printItem(chalk`Metro waiting on {underline ${nativeRuntimeUrl}}`)); 46 if (options.devClient === false) { 47 // TODO: if development build, change this message! 48 Log.log( 49 printItem('Scan the QR code above with Expo Go (Android) or the Camera app (iOS)') 50 ); 51 } else { 52 Log.log( 53 printItem( 54 'Scan the QR code above to open the project in a development build. ' + 55 learnMore('https://expo.fyi/start') 56 ) 57 ); 58 } 59 } catch (error) { 60 console.log('err', error); 61 // @ts-ignore: If there is no development build scheme, then skip the QR code. 62 if (error.code !== 'NO_DEV_CLIENT_SCHEME') { 63 throw error; 64 } else { 65 const serverUrl = devServer.getDevServerUrl(); 66 Log.log(printItem(chalk`Metro waiting on {underline ${serverUrl}}`)); 67 Log.log(printItem(`Linking is disabled because the client scheme cannot be resolved.`)); 68 } 69 } 70 } 71 72 const webDevServer = this.devServerManager.getWebDevServer(); 73 const webUrl = webDevServer?.getDevServerUrl({ hostType: 'localhost' }); 74 if (webUrl) { 75 Log.log(); 76 Log.log(printItem(chalk`Web is waiting on {underline ${webUrl}}`)); 77 } 78 79 printUsage(options, { verbose: false }); 80 printHelp(); 81 Log.log(); 82 } 83 84 async openJsInspectorAsync() { 85 Log.log('Opening JavaScript inspector in the browser...'); 86 const metroServerOrigin = this.devServerManager.getDefaultDevServer().getJsInspectorBaseUrl(); 87 assert(metroServerOrigin, 'Metro dev server is not running'); 88 const apps = await queryAllInspectorAppsAsync(metroServerOrigin); 89 if (!apps.length) { 90 Log.warn( 91 `No compatible apps connected. JavaScript Debugging can only be used with the Hermes engine. ${learnMore( 92 'https://docs.expo.dev/guides/using-hermes/' 93 )}` 94 ); 95 return; 96 } 97 try { 98 for (const app of apps) { 99 await openJsInspector(app); 100 } 101 } catch (error: any) { 102 Log.error('Failed to open JavaScript inspector. This is often an issue with Google Chrome.'); 103 Log.exception(error); 104 } 105 } 106 107 reloadApp() { 108 Log.log(`${BLT} Reloading apps`); 109 // Send reload requests over the dev servers 110 this.devServerManager.broadcastMessage('reload'); 111 } 112 113 async openMoreToolsAsync() { 114 try { 115 // Options match: Chrome > View > Developer 116 const value = await selectAsync(chalk`Dev tools {dim (native only)}`, [ 117 { title: 'Inspect elements', value: 'toggleElementInspector' }, 118 { title: 'Toggle performance monitor', value: 'togglePerformanceMonitor' }, 119 { title: 'Toggle developer menu', value: 'toggleDevMenu' }, 120 { title: 'Reload app', value: 'reload' }, 121 { title: 'Start React devtools', value: 'startReactDevTools' }, 122 // TODO: Maybe a "View Source" option to open code. 123 // Toggling Remote JS Debugging is pretty rough, so leaving it disabled. 124 // { title: 'Toggle Remote Debugging', value: 'toggleRemoteDebugging' }, 125 ]); 126 if (value === 'startReactDevTools') { 127 this.startReactDevToolsAsync(); 128 } else { 129 this.devServerManager.broadcastMessage('sendDevCommand', { name: value }); 130 } 131 } catch (error: any) { 132 debug(error); 133 // do nothing 134 } finally { 135 printHelp(); 136 } 137 } 138 139 async startReactDevToolsAsync() { 140 await startReactDevToolsProxyAsync(); 141 const url = this.devServerManager.getDefaultDevServer().getReactDevToolsUrl(); 142 await openBrowserAsync(url); 143 addReactDevToolsReloadListener(() => { 144 this.reconnectReactDevTools(); 145 }); 146 this.reconnectReactDevTools(); 147 } 148 149 async reconnectReactDevTools() { 150 // Wait a little time for react-devtools to be initialized in browser 151 await delayAsync(3000); 152 this.devServerManager.broadcastMessage('sendDevCommand', { name: 'reconnectReactDevTools' }); 153 } 154 155 toggleDevMenu() { 156 Log.log(`${BLT} Toggling dev menu`); 157 this.devServerManager.broadcastMessage('devMenu'); 158 } 159} 160