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