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