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