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