1import { ExpoConfig, getConfig } from '@expo/config';
2import { MetroDevServerOptions } from '@expo/dev-server';
3import type { LoadOptions } from '@expo/metro-config';
4import chalk from 'chalk';
5import http from 'http';
6import type Metro from 'metro';
7import { Terminal } from 'metro-core';
8
9import { Log } from '../../../log';
10import { getMetroProperties } from '../../../utils/analytics/getMetroProperties';
11import { createDebuggerTelemetryMiddleware } from '../../../utils/analytics/metroDebuggerMiddleware';
12import { logEventAsync } from '../../../utils/analytics/rudderstackClient';
13import { env } from '../../../utils/env';
14import { getMetroServerRoot } from '../middleware/ManifestMiddleware';
15import { createDevServerMiddleware } from '../middleware/createDevServerMiddleware';
16import { getPlatformBundlers } from '../platformBundlers';
17import { MetroBundlerDevServer } from './MetroBundlerDevServer';
18import { MetroTerminalReporter } from './MetroTerminalReporter';
19import { importExpoMetroConfig } from './resolveFromProject';
20import { runServer } from './runServer-fork';
21import { withMetroMultiPlatformAsync } from './withMetroMultiPlatform';
22
23// From expo/dev-server but with ability to use custom logger.
24type MessageSocket = {
25  broadcast: (method: string, params?: Record<string, any> | undefined) => void;
26};
27
28export async function loadMetroConfigAsync(
29  projectRoot: string,
30  options: LoadOptions,
31  {
32    exp = getConfig(projectRoot, { skipSDKVersionRequirement: true, skipPlugins: true }).exp,
33  }: { exp?: ExpoConfig } = {}
34) {
35  let reportEvent: ((event: any) => void) | undefined;
36  const serverRoot = getMetroServerRoot(projectRoot);
37
38  const terminal = new Terminal(process.stdout);
39  const terminalReporter = new MetroTerminalReporter(serverRoot, terminal);
40
41  const reporter = {
42    update(event: any) {
43      terminalReporter.update(event);
44      if (reportEvent) {
45        reportEvent(event);
46      }
47    },
48  };
49
50  const ExpoMetroConfig = importExpoMetroConfig(projectRoot);
51  let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options });
52
53  const platformBundlers = getPlatformBundlers(exp);
54
55  config = await withMetroMultiPlatformAsync(projectRoot, {
56    config,
57    platformBundlers,
58    isTsconfigPathsEnabled: !!exp.experiments?.tsconfigPaths,
59  });
60
61  logEventAsync('metro config', getMetroProperties(projectRoot, exp, config));
62
63  return {
64    config,
65    setEventReporter: (logger: (event: any) => void) => (reportEvent = logger),
66    reporter: terminalReporter,
67  };
68}
69
70/** The most generic possible setup for Metro bundler. */
71export async function instantiateMetroAsync(
72  metroBundler: MetroBundlerDevServer,
73  options: Omit<MetroDevServerOptions, 'logger'>
74): Promise<{
75  metro: Metro.Server;
76  server: http.Server;
77  middleware: any;
78  messageSocket: MessageSocket;
79}> {
80  const projectRoot = metroBundler.projectRoot;
81
82  // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this.
83  const { exp } = getConfig(projectRoot, {
84    skipSDKVersionRequirement: true,
85    skipPlugins: true,
86  });
87
88  const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync(
89    projectRoot,
90    options,
91    { exp }
92  );
93
94  const { middleware, websocketEndpoints, eventsSocketEndpoint, messageSocketEndpoint } =
95    createDevServerMiddleware(projectRoot, {
96      port: metroConfig.server.port,
97      watchFolders: metroConfig.watchFolders,
98    });
99
100  const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware;
101  // @ts-expect-error: can't mutate readonly config
102  metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => {
103    if (customEnhanceMiddleware) {
104      metroMiddleware = customEnhanceMiddleware(metroMiddleware, server);
105    }
106    return middleware.use(metroMiddleware);
107  };
108
109  middleware.use(createDebuggerTelemetryMiddleware(projectRoot, exp));
110
111  const { server, metro } = await runServer(metroBundler, metroConfig, {
112    hmrEnabled: true,
113    websocketEndpoints,
114    watch: isWatchEnabled(),
115  });
116
117  setEventReporter(eventsSocketEndpoint.reportEvent);
118
119  return {
120    metro,
121    server,
122    middleware,
123    messageSocket: messageSocketEndpoint,
124  };
125}
126
127/**
128 * Simplify and communicate if Metro is running without watching file updates,.
129 * Exposed for testing.
130 */
131export function isWatchEnabled() {
132  if (env.CI) {
133    Log.log(
134      chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.`
135    );
136  }
137
138  return !env.CI;
139}
140