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