xref: /expo/packages/@expo/cli/src/start/startAsync.ts (revision 33643b60)
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
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    // After the server starts, we can start attempting to bootstrap TypeScript.
104    await devServerManager.bootstrapTypeScriptAsync();
105  }
106
107  if (!settings.webOnly && !options.devClient) {
108    await profile(validateDependenciesVersionsAsync)(projectRoot, exp, pkg);
109  }
110
111  // Some tracking thing
112
113  if (options.devClient) {
114    await trackAsync(projectRoot, exp);
115  }
116
117  // Open project on devices.
118  await profile(openPlatformsAsync)(devServerManager, options);
119
120  // Present the Terminal UI.
121  if (isInteractive()) {
122    await profile(startInterfaceAsync)(devServerManager, {
123      platforms: exp.platforms ?? ['ios', 'android', 'web'],
124    });
125  } else {
126    // Display the server location in CI...
127    const url = devServerManager.getDefaultDevServer()?.getDevServerUrl();
128    if (url) {
129      Log.log(chalk`Waiting on {underline ${url}}`);
130    }
131  }
132
133  // Final note about closing the server.
134  const logLocation = settings.webOnly ? 'in the browser console' : 'below';
135  Log.log(
136    chalk`Logs for your project will appear ${logLocation}.${
137      isInteractive() ? chalk.dim(` Press Ctrl+C to exit.`) : ''
138    }`
139  );
140}
141
142async function trackAsync(projectRoot: string, exp: ExpoConfig): Promise<void> {
143  await logEventAsync('dev client start command', {
144    status: 'started',
145    ...getDevClientProperties(projectRoot, exp),
146  });
147  installExitHooks(async () => {
148    await logEventAsync('dev client start command', {
149      status: 'finished',
150      ...getDevClientProperties(projectRoot, exp),
151    });
152    // UnifiedAnalytics.flush();
153  });
154}
155