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