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