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 if (exp.updates?.useClassicUpdates) { 80 options.forceManifestType = 'classic'; 81 } else { 82 const classicUpdatesUrlRegex = /^(staging\.)?exp\.host/; 83 let parsedUpdatesUrl: { hostname: string | null } = { hostname: null }; 84 if (exp.updates?.url) { 85 try { 86 parsedUpdatesUrl = new URL(exp.updates.url); 87 } catch { 88 Log.error( 89 `Failed to parse \`updates.url\` in this project's app config. ${exp.updates.url} is not a valid URL.` 90 ); 91 } 92 } 93 const isClassicUpdatesUrl = parsedUpdatesUrl.hostname 94 ? classicUpdatesUrlRegex.test(parsedUpdatesUrl.hostname) 95 : false; 96 options.forceManifestType = isClassicUpdatesUrl ? 'classic' : 'expo-updates'; 97 } 98 } 99 100 const [defaultOptions, startOptions] = await getMultiBundlerStartOptions( 101 projectRoot, 102 options, 103 settings, 104 platformBundlers 105 ); 106 107 const devServerManager = new DevServerManager(projectRoot, defaultOptions); 108 109 // Validations 110 111 if (options.web || settings.webOnly) { 112 await devServerManager.ensureProjectPrerequisiteAsync(WebSupportProjectPrerequisite); 113 } 114 115 // Start the server as soon as possible. 116 await profile(devServerManager.startAsync.bind(devServerManager))(startOptions); 117 118 if (!settings.webOnly) { 119 await devServerManager.watchEnvironmentVariables(); 120 121 // After the server starts, we can start attempting to bootstrap TypeScript. 122 await devServerManager.bootstrapTypeScriptAsync(); 123 } 124 125 if (!settings.webOnly && !options.devClient) { 126 await profile(validateDependenciesVersionsAsync)(projectRoot, exp, pkg); 127 } 128 129 // Some tracking thing 130 131 if (options.devClient) { 132 await trackAsync(projectRoot, exp); 133 } 134 135 // Open project on devices. 136 await profile(openPlatformsAsync)(devServerManager, options); 137 138 // Present the Terminal UI. 139 if (isInteractive()) { 140 await profile(startInterfaceAsync)(devServerManager, { 141 platforms: exp.platforms ?? ['ios', 'android', 'web'], 142 }); 143 } else { 144 // Display the server location in CI... 145 const url = devServerManager.getDefaultDevServer()?.getDevServerUrl(); 146 if (url) { 147 Log.log(chalk`Waiting on {underline ${url}}`); 148 } 149 } 150 151 // Final note about closing the server. 152 const logLocation = settings.webOnly ? 'in the browser console' : 'below'; 153 Log.log( 154 chalk`Logs for your project will appear ${logLocation}.${ 155 isInteractive() ? chalk.dim(` Press Ctrl+C to exit.`) : '' 156 }` 157 ); 158} 159 160async function trackAsync(projectRoot: string, exp: ExpoConfig): Promise<void> { 161 await logEventAsync('dev client start command', { 162 status: 'started', 163 ...getDevClientProperties(projectRoot, exp), 164 }); 165 installExitHooks(async () => { 166 await logEventAsync('dev client start command', { 167 status: 'finished', 168 ...getDevClientProperties(projectRoot, exp), 169 }); 170 // UnifiedAnalytics.flush(); 171 }); 172} 173