18d307f52SEvan Baconimport { ExpoConfig, getConfig } from '@expo/config'; 28d307f52SEvan Baconimport assert from 'assert'; 3fdf34e39SEvan Baconimport chalk from 'chalk'; 48d307f52SEvan Bacon 58a424bebSJames Ideimport { BundlerDevServer, BundlerStartOptions } from './BundlerDevServer'; 68a424bebSJames Ideimport { getPlatformBundlers } from './platformBundlers'; 7a7e47f4dSEvan Baconimport { Log } from '../../log'; 88d307f52SEvan Baconimport { FileNotifier } from '../../utils/FileNotifier'; 9ea99eec9SEvan Baconimport { logEventAsync } from '../../utils/analytics/rudderstackClient'; 101117330aSMark Lawlorimport { env } from '../../utils/env'; 118d307f52SEvan Baconimport { ProjectPrerequisite } from '../doctor/Prerequisite'; 1233643b60SEvan Baconimport { TypeScriptProjectPrerequisite } from '../doctor/typescript/TypeScriptProjectPrerequisite'; 13a7e47f4dSEvan Baconimport { printItem } from '../interface/commandsTable'; 148d307f52SEvan Baconimport * as AndroidDebugBridge from '../platforms/android/adb'; 15a7e47f4dSEvan Baconimport { resolveSchemeAsync } from '../resolveOptions'; 168d307f52SEvan Bacon 17474a7a4bSEvan Baconconst debug = require('debug')('expo:start:server:devServerManager') as typeof console.log; 18474a7a4bSEvan Bacon 198d307f52SEvan Baconexport type MultiBundlerStartOptions = { 208d307f52SEvan Bacon type: keyof typeof BUNDLERS; 218d307f52SEvan Bacon options?: BundlerStartOptions; 228d307f52SEvan Bacon}[]; 238d307f52SEvan Bacon 248d307f52SEvan Baconconst devServers: BundlerDevServer[] = []; 258d307f52SEvan Bacon 268d307f52SEvan Baconconst BUNDLERS = { 278d307f52SEvan Bacon webpack: () => 288d307f52SEvan Bacon require('./webpack/WebpackBundlerDevServer') 298d307f52SEvan Bacon .WebpackBundlerDevServer as typeof import('./webpack/WebpackBundlerDevServer').WebpackBundlerDevServer, 308d307f52SEvan Bacon metro: () => 318d307f52SEvan Bacon require('./metro/MetroBundlerDevServer') 328d307f52SEvan Bacon .MetroBundlerDevServer as typeof import('./metro/MetroBundlerDevServer').MetroBundlerDevServer, 338d307f52SEvan Bacon}; 348d307f52SEvan Bacon 358d307f52SEvan Bacon/** Manages interacting with multiple dev servers. */ 368d307f52SEvan Baconexport class DevServerManager { 3733643b60SEvan Bacon private projectPrerequisites: ProjectPrerequisite<any, void>[] = []; 388d307f52SEvan Bacon 395404abc1SEvan Bacon private notifier: FileNotifier | null = null; 405404abc1SEvan Bacon 418d307f52SEvan Bacon constructor( 428d307f52SEvan Bacon public projectRoot: string, 438d307f52SEvan Bacon /** Keep track of the original CLI options for bundlers that are started interactively. */ 448d307f52SEvan Bacon public options: BundlerStartOptions 458d307f52SEvan Bacon ) { 465404abc1SEvan Bacon this.notifier = this.watchBabelConfig(); 478d307f52SEvan Bacon } 488d307f52SEvan Bacon 498d307f52SEvan Bacon private watchBabelConfig() { 50fdf34e39SEvan Bacon const notifier = new FileNotifier( 51fdf34e39SEvan Bacon this.projectRoot, 52fdf34e39SEvan Bacon [ 538d307f52SEvan Bacon './babel.config.js', 548d307f52SEvan Bacon './babel.config.json', 558d307f52SEvan Bacon './.babelrc.json', 568d307f52SEvan Bacon './.babelrc', 578d307f52SEvan Bacon './.babelrc.js', 58fdf34e39SEvan Bacon ], 59fdf34e39SEvan Bacon { 60fdf34e39SEvan Bacon additionalWarning: chalk` You may need to clear the bundler cache with the {bold --clear} flag for your changes to take effect.`, 61fdf34e39SEvan Bacon } 62fdf34e39SEvan Bacon ); 638d307f52SEvan Bacon 648d307f52SEvan Bacon notifier.startObserving(); 658d307f52SEvan Bacon 668d307f52SEvan Bacon return notifier; 678d307f52SEvan Bacon } 688d307f52SEvan Bacon 698d307f52SEvan Bacon /** Lazily load and assert a project-level prerequisite. */ 7033643b60SEvan Bacon async ensureProjectPrerequisiteAsync(PrerequisiteClass: typeof ProjectPrerequisite<any, any>) { 718d307f52SEvan Bacon let prerequisite = this.projectPrerequisites.find( 728d307f52SEvan Bacon (prerequisite) => prerequisite instanceof PrerequisiteClass 738d307f52SEvan Bacon ); 748d307f52SEvan Bacon if (!prerequisite) { 758d307f52SEvan Bacon prerequisite = new PrerequisiteClass(this.projectRoot); 768d307f52SEvan Bacon this.projectPrerequisites.push(prerequisite); 778d307f52SEvan Bacon } 7833643b60SEvan Bacon return await prerequisite.assertAsync(); 798d307f52SEvan Bacon } 808d307f52SEvan Bacon 818d307f52SEvan Bacon /** 828d307f52SEvan Bacon * Sends a message over web sockets to all connected devices, 838d307f52SEvan Bacon * does nothing when the dev server is not running. 848d307f52SEvan Bacon * 858d307f52SEvan Bacon * @param method name of the command. In RN projects `reload`, and `devMenu` are available. In Expo Go, `sendDevCommand` is available. 868d307f52SEvan Bacon * @param params extra event info to send over the socket. 878d307f52SEvan Bacon */ 888d307f52SEvan Bacon broadcastMessage(method: 'reload' | 'devMenu' | 'sendDevCommand', params?: Record<string, any>) { 898d307f52SEvan Bacon devServers.forEach((server) => { 908d307f52SEvan Bacon server.broadcastMessage(method, params); 918d307f52SEvan Bacon }); 928d307f52SEvan Bacon } 938d307f52SEvan Bacon 948d307f52SEvan Bacon /** Get the port for the dev server (either Webpack or Metro) that is hosting code for React Native runtimes. */ 958d307f52SEvan Bacon getNativeDevServerPort() { 968d307f52SEvan Bacon const server = devServers.find((server) => server.isTargetingNative()); 978d307f52SEvan Bacon return server?.getInstance()?.location.port ?? null; 988d307f52SEvan Bacon } 998d307f52SEvan Bacon 1008d307f52SEvan Bacon /** Get the first server that targets web. */ 1018d307f52SEvan Bacon getWebDevServer() { 1028d307f52SEvan Bacon const server = devServers.find((server) => server.isTargetingWeb()); 1038d307f52SEvan Bacon return server ?? null; 1048d307f52SEvan Bacon } 1058d307f52SEvan Bacon 1068d307f52SEvan Bacon getDefaultDevServer(): BundlerDevServer { 1078d307f52SEvan Bacon // Return the first native dev server otherwise return the first dev server. 1088d307f52SEvan Bacon const server = devServers.find((server) => server.isTargetingNative()); 1098d307f52SEvan Bacon const defaultServer = server ?? devServers[0]; 1108d307f52SEvan Bacon assert(defaultServer, 'No dev servers are running'); 1118d307f52SEvan Bacon return defaultServer; 1128d307f52SEvan Bacon } 1138d307f52SEvan Bacon 1148d307f52SEvan Bacon async ensureWebDevServerRunningAsync() { 1158d307f52SEvan Bacon const [server] = devServers.filter((server) => server.isTargetingWeb()); 1168d307f52SEvan Bacon if (server) { 1178d307f52SEvan Bacon return; 1188d307f52SEvan Bacon } 1196d6b81f9SEvan Bacon const { exp } = getConfig(this.projectRoot, { 1206d6b81f9SEvan Bacon skipPlugins: true, 1216d6b81f9SEvan Bacon skipSDKVersionRequirement: true, 1226d6b81f9SEvan Bacon }); 1236d6b81f9SEvan Bacon const bundler = getPlatformBundlers(exp).web; 1246d6b81f9SEvan Bacon debug(`Starting ${bundler} dev server for web`); 1258d307f52SEvan Bacon return this.startAsync([ 1268d307f52SEvan Bacon { 1276d6b81f9SEvan Bacon type: bundler, 1288d307f52SEvan Bacon options: this.options, 1298d307f52SEvan Bacon }, 1308d307f52SEvan Bacon ]); 1318d307f52SEvan Bacon } 1328d307f52SEvan Bacon 133a7e47f4dSEvan Bacon /** Switch between Expo Go and Expo Dev Clients. */ 134a7e47f4dSEvan Bacon async toggleRuntimeMode(isUsingDevClient: boolean = !this.options.devClient): Promise<boolean> { 135a7e47f4dSEvan Bacon const nextMode = isUsingDevClient ? '--dev-client' : '--go'; 136a7e47f4dSEvan Bacon Log.log(printItem(chalk`Switching to {bold ${nextMode}}`)); 137a7e47f4dSEvan Bacon 138a7e47f4dSEvan Bacon const nextScheme = await resolveSchemeAsync(this.projectRoot, { 139a7e47f4dSEvan Bacon devClient: isUsingDevClient, 140a7e47f4dSEvan Bacon // NOTE: The custom `--scheme` argument is lost from this point on. 141a7e47f4dSEvan Bacon }); 142a7e47f4dSEvan Bacon 143a7e47f4dSEvan Bacon this.options.location.scheme = nextScheme; 144a7e47f4dSEvan Bacon this.options.devClient = isUsingDevClient; 145a7e47f4dSEvan Bacon for (const devServer of devServers) { 146a7e47f4dSEvan Bacon devServer.isDevClient = isUsingDevClient; 147a7e47f4dSEvan Bacon const urlCreator = devServer.getUrlCreator(); 148a7e47f4dSEvan Bacon urlCreator.defaults ??= {}; 149a7e47f4dSEvan Bacon urlCreator.defaults.scheme = nextScheme; 150a7e47f4dSEvan Bacon } 151a7e47f4dSEvan Bacon 152a7e47f4dSEvan Bacon debug(`New runtime options (runtime: ${nextMode}):`, this.options); 153a7e47f4dSEvan Bacon return true; 154a7e47f4dSEvan Bacon } 155a7e47f4dSEvan Bacon 1568d307f52SEvan Bacon /** Start all dev servers. */ 1578d307f52SEvan Bacon async startAsync(startOptions: MultiBundlerStartOptions): Promise<ExpoConfig> { 1589580591fSEvan Bacon const { exp } = getConfig(this.projectRoot, { skipSDKVersionRequirement: true }); 1598d307f52SEvan Bacon 160ea99eec9SEvan Bacon await logEventAsync('Start Project', { 1618d307f52SEvan Bacon sdkVersion: exp.sdkVersion ?? null, 1628d307f52SEvan Bacon }); 1638d307f52SEvan Bacon 1646d6b81f9SEvan Bacon const platformBundlers = getPlatformBundlers(exp); 1656d6b81f9SEvan Bacon 1668d307f52SEvan Bacon // Start all dev servers... 1678d307f52SEvan Bacon for (const { type, options } of startOptions) { 1688d307f52SEvan Bacon const BundlerDevServerClass = await BUNDLERS[type](); 1696d6b81f9SEvan Bacon const server = new BundlerDevServerClass( 1706d6b81f9SEvan Bacon this.projectRoot, 1716d6b81f9SEvan Bacon platformBundlers, 1726d6b81f9SEvan Bacon !!options?.devClient 1736d6b81f9SEvan Bacon ); 1748d307f52SEvan Bacon await server.startAsync(options ?? this.options); 1758d307f52SEvan Bacon devServers.push(server); 1768d307f52SEvan Bacon } 1778d307f52SEvan Bacon 1788d307f52SEvan Bacon return exp; 1798d307f52SEvan Bacon } 1808d307f52SEvan Bacon 18133643b60SEvan Bacon async bootstrapTypeScriptAsync() { 1821117330aSMark Lawlor const typescriptPrerequisite = await this.ensureProjectPrerequisiteAsync( 1831117330aSMark Lawlor TypeScriptProjectPrerequisite 1841117330aSMark Lawlor ); 1851117330aSMark Lawlor 1861117330aSMark Lawlor if (env.EXPO_NO_TYPESCRIPT_SETUP) { 18733643b60SEvan Bacon return; 18833643b60SEvan Bacon } 1891117330aSMark Lawlor 19033643b60SEvan Bacon // Optionally, wait for the user to add TypeScript during the 19133643b60SEvan Bacon // development cycle. 19233643b60SEvan Bacon const server = devServers.find((server) => server.name === 'metro'); 19333643b60SEvan Bacon if (!server) { 19433643b60SEvan Bacon return; 19533643b60SEvan Bacon } 1961117330aSMark Lawlor 197*87669a95SMark Lawlor // The dev server shouldn't wait for the typescript services 1981117330aSMark Lawlor if (!typescriptPrerequisite) { 1991117330aSMark Lawlor server.waitForTypeScriptAsync().then(async (success) => { 2001117330aSMark Lawlor if (success) { 201*87669a95SMark Lawlor server.startTypeScriptServices(); 2021117330aSMark Lawlor } 2031117330aSMark Lawlor }); 2041117330aSMark Lawlor } else { 2051117330aSMark Lawlor server.startTypeScriptServices(); 2061117330aSMark Lawlor } 20733643b60SEvan Bacon } 20833643b60SEvan Bacon 2096a750d06SEvan Bacon async watchEnvironmentVariables() { 2106a750d06SEvan Bacon await devServers.find((server) => server.name === 'metro')?.watchEnvironmentVariables(); 2116a750d06SEvan Bacon } 2126a750d06SEvan Bacon 2138d307f52SEvan Bacon /** Stop all servers including ADB. */ 2148d307f52SEvan Bacon async stopAsync(): Promise<void> { 21511a5a4d2SEvan Bacon await Promise.allSettled([ 2165404abc1SEvan Bacon this.notifier?.stopObserving(), 2178d307f52SEvan Bacon // Stop all dev servers 2188d307f52SEvan Bacon ...devServers.map((server) => server.stopAsync()), 2198d307f52SEvan Bacon // Stop ADB 2208d307f52SEvan Bacon AndroidDebugBridge.getServer().stopAsync(), 22111a5a4d2SEvan Bacon ]); 2228d307f52SEvan Bacon } 2238d307f52SEvan Bacon} 224