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