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 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 // After the server starts, we can start attempting to bootstrap TypeScript. 104 await devServerManager.bootstrapTypeScriptAsync(); 105 } 106 107 if (!settings.webOnly && !options.devClient) { 108 await profile(validateDependenciesVersionsAsync)(projectRoot, exp, pkg); 109 } 110 111 // Some tracking thing 112 113 if (options.devClient) { 114 await trackAsync(projectRoot, exp); 115 } 116 117 // Open project on devices. 118 await profile(openPlatformsAsync)(devServerManager, options); 119 120 // Present the Terminal UI. 121 if (isInteractive()) { 122 await profile(startInterfaceAsync)(devServerManager, { 123 platforms: exp.platforms ?? ['ios', 'android', 'web'], 124 }); 125 } else { 126 // Display the server location in CI... 127 const url = devServerManager.getDefaultDevServer()?.getDevServerUrl(); 128 if (url) { 129 Log.log(chalk`Waiting on {underline ${url}}`); 130 } 131 } 132 133 // Final note about closing the server. 134 const logLocation = settings.webOnly ? 'in the browser console' : 'below'; 135 Log.log( 136 chalk`Logs for your project will appear ${logLocation}.${ 137 isInteractive() ? chalk.dim(` Press Ctrl+C to exit.`) : '' 138 }` 139 ); 140} 141 142async function trackAsync(projectRoot: string, exp: ExpoConfig): Promise<void> { 143 await logEventAsync('dev client start command', { 144 status: 'started', 145 ...getDevClientProperties(projectRoot, exp), 146 }); 147 installExitHooks(async () => { 148 await logEventAsync('dev client start command', { 149 status: 'finished', 150 ...getDevClientProperties(projectRoot, exp), 151 }); 152 // UnifiedAnalytics.flush(); 153 }); 154} 155