1import { getConfig } from '@expo/config';
2import { MetroDevServerOptions } from '@expo/dev-server';
3import http from 'http';
4import Metro from 'metro';
5import { Terminal } from 'metro-core';
6
7import { createDevServerMiddleware } from '../middleware/createDevServerMiddleware';
8import { getPlatformBundlers } from '../platformBundlers';
9import { MetroTerminalReporter } from './MetroTerminalReporter';
10import { importExpoMetroConfigFromProject, importMetroFromProject } from './resolveFromProject';
11import { getAppRouterRelativeEntryPath } from './router';
12import { withMetroMultiPlatform } from './withMetroMultiPlatform';
13
14// From expo/dev-server but with ability to use custom logger.
15type MessageSocket = {
16  broadcast: (method: string, params?: Record<string, any> | undefined) => void;
17};
18
19/** The most generic possible setup for Metro bundler. */
20export async function instantiateMetroAsync(
21  projectRoot: string,
22  options: Omit<MetroDevServerOptions, 'logger'>
23): Promise<{
24  server: http.Server;
25  middleware: any;
26  messageSocket: MessageSocket;
27}> {
28  let reportEvent: ((event: any) => void) | undefined;
29
30  const Metro = importMetroFromProject(projectRoot);
31  const ExpoMetroConfig = importExpoMetroConfigFromProject(projectRoot);
32
33  const terminal = new Terminal(process.stdout);
34  const terminalReporter = new MetroTerminalReporter(projectRoot, terminal);
35
36  const reporter = {
37    update(event: any) {
38      terminalReporter.update(event);
39      if (reportEvent) {
40        reportEvent(event);
41      }
42    },
43  };
44
45  let metroConfig = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options });
46
47  // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this.
48  const { exp } = getConfig(projectRoot, {
49    skipSDKVersionRequirement: true,
50    skipPlugins: true,
51  });
52  const platformBundlers = getPlatformBundlers(exp);
53  metroConfig = withMetroMultiPlatform(projectRoot, metroConfig, platformBundlers);
54
55  // Auto pick App entry: this is injected with Babel.
56  process.env.EXPO_ROUTER_APP_ROOT = getAppRouterRelativeEntryPath(projectRoot);
57  process.env.EXPO_PROJECT_ROOT = process.env.EXPO_PROJECT_ROOT ?? projectRoot;
58
59  const {
60    middleware,
61    attachToServer,
62
63    // New
64    websocketEndpoints,
65    eventsSocketEndpoint,
66    messageSocketEndpoint,
67  } = createDevServerMiddleware(projectRoot, {
68    port: metroConfig.server.port,
69    watchFolders: metroConfig.watchFolders,
70  });
71
72  const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware;
73  // @ts-ignore can't mutate readonly config
74  metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => {
75    if (customEnhanceMiddleware) {
76      metroMiddleware = customEnhanceMiddleware(metroMiddleware, server);
77    }
78    return middleware.use(metroMiddleware);
79  };
80
81  const server = await Metro.runServer(metroConfig, {
82    hmrEnabled: true,
83    websocketEndpoints,
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