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 { profile } from '../utils/profile'; 10import { validateDependenciesVersionsAsync } from './doctor/dependencies/validateDependenciesVersions'; 11import { TypeScriptProjectPrerequisite } from './doctor/typescript/TypeScriptProjectPrerequisite'; 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 const { exp, pkg } = profile(getConfig)(projectRoot); 73 74 const platformBundlers = getPlatformBundlers(exp); 75 76 if (!options.forceManifestType) { 77 const easUpdatesUrlRegex = /^https:\/\/(staging-)?u\.expo\.dev/; 78 const isEasUpdatesUrl = exp.updates?.url ? easUpdatesUrlRegex.test(exp.updates.url) : false; 79 options.forceManifestType = isEasUpdatesUrl ? 'expo-updates' : 'classic'; 80 } 81 82 const [defaultOptions, startOptions] = await getMultiBundlerStartOptions( 83 projectRoot, 84 options, 85 settings, 86 platformBundlers 87 ); 88 89 const devServerManager = new DevServerManager(projectRoot, defaultOptions); 90 91 // Validations 92 93 if (options.web || settings.webOnly) { 94 await devServerManager.ensureProjectPrerequisiteAsync(WebSupportProjectPrerequisite); 95 } 96 97 await devServerManager.ensureProjectPrerequisiteAsync(TypeScriptProjectPrerequisite); 98 99 if (!settings.webOnly && !options.devClient) { 100 await profile(validateDependenciesVersionsAsync)(projectRoot, exp, pkg); 101 } 102 103 // Some tracking thing 104 105 if (options.devClient) { 106 await trackAsync(projectRoot, exp); 107 } 108 109 await profile(devServerManager.startAsync.bind(devServerManager))(startOptions); 110 111 // Open project on devices. 112 await profile(openPlatformsAsync)(devServerManager, options); 113 114 // Present the Terminal UI. 115 if (isInteractive()) { 116 await profile(startInterfaceAsync)(devServerManager, { 117 platforms: exp.platforms ?? ['ios', 'android', 'web'], 118 }); 119 } else { 120 // Display the server location in CI... 121 const url = devServerManager.getDefaultDevServer()?.getDevServerUrl(); 122 if (url) { 123 Log.log(chalk`Waiting on {underline ${url}}`); 124 } 125 } 126 127 // Final note about closing the server. 128 const logLocation = settings.webOnly ? 'in the browser console' : 'below'; 129 Log.log( 130 chalk`Logs for your project will appear ${logLocation}.${ 131 isInteractive() ? chalk.dim(` Press Ctrl+C to exit.`) : '' 132 }` 133 ); 134} 135 136async function trackAsync(projectRoot: string, exp: ExpoConfig): Promise<void> { 137 await logEventAsync('dev client start command', { 138 status: 'started', 139 ...getDevClientProperties(projectRoot, exp), 140 }); 141 installExitHooks(async () => { 142 await logEventAsync('dev client start command', { 143 status: 'finished', 144 ...getDevClientProperties(projectRoot, exp), 145 }); 146 // UnifiedAnalytics.flush(); 147 }); 148} 149