xref: /expo/packages/@expo/cli/src/start/startAsync.ts (revision c7b9eeee)
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