xref: /expo/packages/@expo/cli/src/start/startAsync.ts (revision 2fd75d6d)
1import { ExpoConfig, getConfig } from '@expo/config';
2import chalk from 'chalk';
3
4import * as Log from '../log';
5import getDevClientProperties from '../utils/analytics/getDevClientProperties';
6import { logEvent } from '../utils/analytics/rudderstackClient';
7import { env } from '../utils/env';
8import { installExitHooks } from '../utils/exit';
9import { profile } from '../utils/profile';
10import { validateDependenciesVersionsAsync } from './doctor/dependencies/validateDependenciesVersions';
11import { TypeScriptProjectPrerequisite } from './doctor/typescript/TypeScriptProjectPrerequisite';
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  const { exp, pkg } = profile(getConfig)(projectRoot);
73
74  const platformBundlers = getPlatformBundlers(exp);
75
76  if (!options.forceManifestType) {
77    const easUpdatesUrlRegex = /^https:\/\/(staging-)?u\.expo\.dev/;
78    const isEasUpdatesUrl = exp.updates?.url ? easUpdatesUrlRegex.test(exp.updates.url) : false;
79    options.forceManifestType = isEasUpdatesUrl ? 'expo-updates' : 'classic';
80  }
81
82  const [defaultOptions, startOptions] = await getMultiBundlerStartOptions(
83    projectRoot,
84    options,
85    settings,
86    platformBundlers
87  );
88
89  const devServerManager = new DevServerManager(projectRoot, defaultOptions);
90
91  // Validations
92
93  if (options.web || settings.webOnly) {
94    await devServerManager.ensureProjectPrerequisiteAsync(WebSupportProjectPrerequisite);
95  }
96
97  await devServerManager.ensureProjectPrerequisiteAsync(TypeScriptProjectPrerequisite);
98
99  if (!settings.webOnly && !options.devClient) {
100    await profile(validateDependenciesVersionsAsync)(projectRoot, exp, pkg);
101  }
102
103  // Some tracking thing
104
105  if (options.devClient) {
106    track(projectRoot, exp);
107  }
108
109  await profile(devServerManager.startAsync.bind(devServerManager))(startOptions);
110
111  // Open project on devices.
112  await profile(openPlatformsAsync)(devServerManager, options);
113
114  // Present the Terminal UI.
115  if (!env.CI) {
116    await profile(startInterfaceAsync)(devServerManager, {
117      platforms: exp.platforms ?? ['ios', 'android', 'web'],
118    });
119  } else {
120    // Display the server location in CI...
121    const url = devServerManager.getDefaultDevServer()?.getDevServerUrl();
122    if (url) {
123      Log.log(chalk`Waiting on {underline ${url}}`);
124    }
125  }
126
127  // Final note about closing the server.
128  const logLocation = settings.webOnly ? 'in the browser console' : 'below';
129  Log.log(
130    chalk`Logs for your project will appear ${logLocation}.${
131      env.CI ? '' : chalk.dim(` Press Ctrl+C to exit.`)
132    }`
133  );
134}
135
136function track(projectRoot: string, exp: ExpoConfig) {
137  logEvent('dev client start command', {
138    status: 'started',
139    ...getDevClientProperties(projectRoot, exp),
140  });
141  installExitHooks(() => {
142    logEvent('dev client start command', {
143      status: 'finished',
144      ...getDevClientProperties(projectRoot, exp),
145    });
146    // UnifiedAnalytics.flush();
147  });
148}
149