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