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