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