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