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