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