1import { ExpoConfig, getConfig } from '@expo/config'; 2import assert from 'assert'; 3 4import * as Log from '../../log'; 5import { FileNotifier } from '../../utils/FileNotifier'; 6import { logEvent } from '../../utils/analytics/rudderstackClient'; 7import { resolveWithTimeout } from '../../utils/delay'; 8import { ProjectPrerequisite } from '../doctor/Prerequisite'; 9import * as AndroidDebugBridge from '../platforms/android/adb'; 10import { BundlerDevServer, BundlerStartOptions } from './BundlerDevServer'; 11 12export type MultiBundlerStartOptions = { 13 type: keyof typeof BUNDLERS; 14 options?: BundlerStartOptions; 15}[]; 16 17const devServers: BundlerDevServer[] = []; 18 19const BUNDLERS = { 20 webpack: () => 21 require('./webpack/WebpackBundlerDevServer') 22 .WebpackBundlerDevServer as typeof import('./webpack/WebpackBundlerDevServer').WebpackBundlerDevServer, 23 metro: () => 24 require('./metro/MetroBundlerDevServer') 25 .MetroBundlerDevServer as typeof import('./metro/MetroBundlerDevServer').MetroBundlerDevServer, 26}; 27 28/** Manages interacting with multiple dev servers. */ 29export class DevServerManager { 30 private projectPrerequisites: ProjectPrerequisite[] = []; 31 32 constructor( 33 public projectRoot: string, 34 /** Keep track of the original CLI options for bundlers that are started interactively. */ 35 public options: BundlerStartOptions 36 ) { 37 this.watchBabelConfig(); 38 } 39 40 private watchBabelConfig() { 41 const notifier = new FileNotifier(this.projectRoot, [ 42 './babel.config.js', 43 './babel.config.json', 44 './.babelrc.json', 45 './.babelrc', 46 './.babelrc.js', 47 ]); 48 49 notifier.startObserving(); 50 51 return notifier; 52 } 53 54 /** Lazily load and assert a project-level prerequisite. */ 55 async ensureProjectPrerequisiteAsync(PrerequisiteClass: typeof ProjectPrerequisite) { 56 let prerequisite = this.projectPrerequisites.find( 57 (prerequisite) => prerequisite instanceof PrerequisiteClass 58 ); 59 if (!prerequisite) { 60 prerequisite = new PrerequisiteClass(this.projectRoot); 61 this.projectPrerequisites.push(prerequisite); 62 } 63 await prerequisite.assertAsync(); 64 } 65 66 /** 67 * Sends a message over web sockets to all connected devices, 68 * does nothing when the dev server is not running. 69 * 70 * @param method name of the command. In RN projects `reload`, and `devMenu` are available. In Expo Go, `sendDevCommand` is available. 71 * @param params extra event info to send over the socket. 72 */ 73 broadcastMessage(method: 'reload' | 'devMenu' | 'sendDevCommand', params?: Record<string, any>) { 74 devServers.forEach((server) => { 75 server.broadcastMessage(method, params); 76 }); 77 } 78 79 /** Get the port for the dev server (either Webpack or Metro) that is hosting code for React Native runtimes. */ 80 getNativeDevServerPort() { 81 const server = devServers.find((server) => server.isTargetingNative()); 82 return server?.getInstance()?.location.port ?? null; 83 } 84 85 /** Get the first server that targets web. */ 86 getWebDevServer() { 87 const server = devServers.find((server) => server.isTargetingWeb()); 88 return server ?? null; 89 } 90 91 getDefaultDevServer(): BundlerDevServer { 92 // Return the first native dev server otherwise return the first dev server. 93 const server = devServers.find((server) => server.isTargetingNative()); 94 const defaultServer = server ?? devServers[0]; 95 assert(defaultServer, 'No dev servers are running'); 96 return defaultServer; 97 } 98 99 async ensureWebDevServerRunningAsync() { 100 const [server] = devServers.filter((server) => server.isTargetingWeb()); 101 if (server) { 102 return; 103 } 104 Log.debug('Starting webpack dev server'); 105 return this.startAsync([ 106 { 107 type: 'webpack', 108 options: this.options, 109 }, 110 ]); 111 } 112 113 /** Start all dev servers. */ 114 async startAsync(startOptions: MultiBundlerStartOptions): Promise<ExpoConfig> { 115 const { exp } = getConfig(this.projectRoot); 116 117 logEvent('Start Project', { 118 sdkVersion: exp.sdkVersion ?? null, 119 }); 120 121 // Start all dev servers... 122 for (const { type, options } of startOptions) { 123 const BundlerDevServerClass = await BUNDLERS[type](); 124 const server = new BundlerDevServerClass(this.projectRoot, !!options?.devClient); 125 await server.startAsync(options ?? this.options); 126 devServers.push(server); 127 } 128 129 return exp; 130 } 131 132 /** Stop all servers including ADB. */ 133 async stopAsync(): Promise<void> { 134 await resolveWithTimeout( 135 () => 136 Promise.allSettled([ 137 // Stop all dev servers 138 ...devServers.map((server) => server.stopAsync()), 139 // Stop ADB 140 AndroidDebugBridge.getServer().stopAsync(), 141 ]), 142 { 143 timeout: 3000, 144 } 145 ); 146 } 147} 148