1import { ExpoConfig, getConfig } from '@expo/config'; 2import chalk from 'chalk'; 3 4import * as Log from '../log'; 5import getDevClientProperties from '../utils/analytics/getDevClientProperties'; 6import { logEventAsync } from '../utils/analytics/rudderstackClient'; 7import { installExitHooks } from '../utils/exit'; 8import { isInteractive } from '../utils/interactive'; 9import { setNodeEnv } from '../utils/nodeEnv'; 10import { profile } from '../utils/profile'; 11import { validateDependenciesVersionsAsync } from './doctor/dependencies/validateDependenciesVersions'; 12import { WebSupportProjectPrerequisite } from './doctor/web/WebSupportProjectPrerequisite'; 13import { startInterfaceAsync } from './interface/startInterface'; 14import { Options, resolvePortsAsync } from './resolveOptions'; 15import { BundlerStartOptions } from './server/BundlerDevServer'; 16import { DevServerManager, MultiBundlerStartOptions } from './server/DevServerManager'; 17import { openPlatformsAsync } from './server/openPlatforms'; 18import { getPlatformBundlers, PlatformBundlers } from './server/platformBundlers'; 19 20async function getMultiBundlerStartOptions( 21 projectRoot: string, 22 { forceManifestType, ...options }: Options, 23 settings: { webOnly?: boolean }, 24 platformBundlers: PlatformBundlers 25): Promise<[BundlerStartOptions, MultiBundlerStartOptions]> { 26 const commonOptions: BundlerStartOptions = { 27 mode: options.dev ? 'development' : 'production', 28 devClient: options.devClient, 29 forceManifestType, 30 privateKeyPath: options.privateKeyPath ?? undefined, 31 https: options.https, 32 maxWorkers: options.maxWorkers, 33 resetDevServer: options.clear, 34 minify: options.minify, 35 location: { 36 hostType: options.host, 37 scheme: options.scheme, 38 }, 39 }; 40 const multiBundlerSettings = await resolvePortsAsync(projectRoot, options, settings); 41 42 const optionalBundlers: Partial<PlatformBundlers> = { ...platformBundlers }; 43 // In the default case, we don't want to start multiple bundlers since this is 44 // a bit slower. Our priority (for legacy) is native platforms. 45 if (!options.web) { 46 delete optionalBundlers['web']; 47 } 48 49 const bundlers = [...new Set(Object.values(optionalBundlers))]; 50 const multiBundlerStartOptions = bundlers.map((bundler) => { 51 const port = 52 bundler === 'webpack' ? multiBundlerSettings.webpackPort : multiBundlerSettings.metroPort; 53 return { 54 type: bundler, 55 options: { 56 ...commonOptions, 57 port, 58 }, 59 }; 60 }); 61 62 return [commonOptions, multiBundlerStartOptions]; 63} 64 65export async function startAsync( 66 projectRoot: string, 67 options: Options, 68 settings: { webOnly?: boolean } 69) { 70 Log.log(chalk.gray(`Starting project at ${projectRoot}`)); 71 72 setNodeEnv(options.dev ? 'development' : 'production'); 73 require('@expo/env').load(projectRoot); 74 const { exp, pkg } = profile(getConfig)(projectRoot); 75 76 const platformBundlers = getPlatformBundlers(exp); 77 78 if (!options.forceManifestType) { 79 const easUpdatesUrlRegex = /^https:\/\/(staging-)?u\.expo\.dev/; 80 const isEasUpdatesUrl = exp.updates?.url ? easUpdatesUrlRegex.test(exp.updates.url) : false; 81 options.forceManifestType = isEasUpdatesUrl ? 'expo-updates' : 'classic'; 82 } 83 84 const [defaultOptions, startOptions] = await getMultiBundlerStartOptions( 85 projectRoot, 86 options, 87 settings, 88 platformBundlers 89 ); 90 91 const devServerManager = new DevServerManager(projectRoot, defaultOptions); 92 93 // Validations 94 95 if (options.web || settings.webOnly) { 96 await devServerManager.ensureProjectPrerequisiteAsync(WebSupportProjectPrerequisite); 97 } 98 99 // Start the server as soon as possible. 100 await profile(devServerManager.startAsync.bind(devServerManager))(startOptions); 101 102 if (!settings.webOnly) { 103 await devServerManager.watchEnvironmentVariables(); 104 105 // After the server starts, we can start attempting to bootstrap TypeScript. 106 await devServerManager.bootstrapTypeScriptAsync(); 107 } 108 109 if (!settings.webOnly && !options.devClient) { 110 await profile(validateDependenciesVersionsAsync)(projectRoot, exp, pkg); 111 } 112 113 // Some tracking thing 114 115 if (options.devClient) { 116 await trackAsync(projectRoot, exp); 117 } 118 119 // Open project on devices. 120 await profile(openPlatformsAsync)(devServerManager, options); 121 122 // Present the Terminal UI. 123 if (isInteractive()) { 124 await profile(startInterfaceAsync)(devServerManager, { 125 platforms: exp.platforms ?? ['ios', 'android', 'web'], 126 }); 127 } else { 128 // Display the server location in CI... 129 const url = devServerManager.getDefaultDevServer()?.getDevServerUrl(); 130 if (url) { 131 Log.log(chalk`Waiting on {underline ${url}}`); 132 } 133 } 134 135 // Final note about closing the server. 136 const logLocation = settings.webOnly ? 'in the browser console' : 'below'; 137 Log.log( 138 chalk`Logs for your project will appear ${logLocation}.${ 139 isInteractive() ? chalk.dim(` Press Ctrl+C to exit.`) : '' 140 }` 141 ); 142} 143 144async function trackAsync(projectRoot: string, exp: ExpoConfig): Promise<void> { 145 await logEventAsync('dev client start command', { 146 status: 'started', 147 ...getDevClientProperties(projectRoot, exp), 148 }); 149 installExitHooks(async () => { 150 await logEventAsync('dev client start command', { 151 status: 'finished', 152 ...getDevClientProperties(projectRoot, exp), 153 }); 154 // UnifiedAnalytics.flush(); 155 }); 156} 157