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