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 { importExpoMetroConfigFromProject } 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 = importExpoMetroConfigFromProject(projectRoot);
51
52  let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options });
53
54  const bundlerPlatforms = getPlatformBundlers(exp);
55
56  config = await withMetroMultiPlatformAsync(projectRoot, config, bundlerPlatforms);
57
58  logEventAsync('metro config', getMetroProperties(projectRoot, exp, config));
59
60  return {
61    config,
62    setEventReporter: (logger: (event: any) => void) => (reportEvent = logger),
63    reporter: terminalReporter,
64  };
65}
66
67/** The most generic possible setup for Metro bundler. */
68export async function instantiateMetroAsync(
69  metroBundler: MetroBundlerDevServer,
70  options: Omit<MetroDevServerOptions, 'logger'>
71): Promise<{
72  metro: Metro.Server;
73  server: http.Server;
74  middleware: any;
75  messageSocket: MessageSocket;
76}> {
77  const projectRoot = metroBundler.projectRoot;
78
79  // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this.
80  const { exp } = getConfig(projectRoot, {
81    skipSDKVersionRequirement: true,
82    skipPlugins: true,
83  });
84
85  const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync(
86    projectRoot,
87    options,
88    { exp }
89  );
90
91  const { middleware, websocketEndpoints, eventsSocketEndpoint, messageSocketEndpoint } =
92    createDevServerMiddleware(projectRoot, {
93      port: metroConfig.server.port,
94      watchFolders: metroConfig.watchFolders,
95    });
96
97  const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware;
98  // @ts-expect-error: can't mutate readonly config
99  metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => {
100    if (customEnhanceMiddleware) {
101      metroMiddleware = customEnhanceMiddleware(metroMiddleware, server);
102    }
103    return middleware.use(metroMiddleware);
104  };
105
106  middleware.use(createDebuggerTelemetryMiddleware(projectRoot, exp));
107
108  const { server, metro } = await runServer(metroBundler, metroConfig, {
109    hmrEnabled: true,
110    websocketEndpoints,
111    watch: isWatchEnabled(),
112  });
113
114  setEventReporter(eventsSocketEndpoint.reportEvent);
115
116  return {
117    metro,
118    server,
119    middleware,
120    messageSocket: messageSocketEndpoint,
121  };
122}
123
124/**
125 * Simplify and communicate if Metro is running without watching file updates,.
126 * Exposed for testing.
127 */
128export function isWatchEnabled() {
129  if (env.CI) {
130    Log.log(
131      chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.`
132    );
133  }
134
135  return !env.CI;
136}
137