1// Copyright © 2023 650 Industries.
2// Copyright (c) Meta Platforms, Inc. and affiliates.
3//
4// Forks https://github.com/facebook/metro/blob/b80d9a0f638ee9fb82ff69cd3c8d9f4309ca1da2/packages/metro/src/index.flow.js#L57
5// and adds the ability to access the bundler instance.
6import http from 'http';
7import https from 'https';
8import { RunServerOptions, Server } from 'metro';
9import { ConfigT } from 'metro-config';
10import { InspectorProxy } from 'metro-inspector-proxy';
11import { parse } from 'url';
12
13import { env } from '../../../utils/env';
14import { createInspectorProxy, ExpoInspectorProxy } from './inspector-proxy';
15import {
16  importMetroCreateWebsocketServerFromProject,
17  importMetroFromProject,
18  importMetroHmrServerFromProject,
19  importMetroInspectorProxyFromProject,
20} from './resolveFromProject';
21
22export const runServer = async (
23  projectRoot: string,
24  config: ConfigT,
25  {
26    hasReducedPerformance = false,
27    host,
28    onError,
29    onReady,
30    secureServerOptions,
31    waitForBundler = false,
32    websocketEndpoints = {},
33    watch,
34  }: RunServerOptions
35): Promise<{ server: http.Server | https.Server; metro: Server }> => {
36  const Metro = importMetroFromProject(projectRoot);
37
38  const createWebsocketServer = importMetroCreateWebsocketServerFromProject(projectRoot);
39
40  const { InspectorProxy } = importMetroInspectorProxyFromProject(projectRoot);
41
42  const MetroHmrServer = importMetroHmrServerFromProject(projectRoot);
43
44  // await earlyPortCheck(host, config.server.port);
45
46  // if (secure != null || secureCert != null || secureKey != null) {
47  //   // eslint-disable-next-line no-console
48  //   console.warn(
49  //     chalk.inverse.yellow.bold(' DEPRECATED '),
50  //     'The `secure`, `secureCert`, and `secureKey` options are now deprecated. ' +
51  //       'Please use the `secureServerOptions` object instead to pass options to ' +
52  //       "Metro's https development server.",
53  //   );
54  // }
55  // Lazy require
56  const connect = require('connect');
57
58  const serverApp = connect();
59
60  const { middleware, end, metroServer } = await Metro.createConnectMiddleware(config, {
61    // @ts-expect-error
62    hasReducedPerformance,
63    waitForBundler,
64    watch,
65  });
66
67  serverApp.use(middleware);
68
69  let inspectorProxy: InspectorProxy | ExpoInspectorProxy | null = null;
70  if (config.server.runInspectorProxy && env.EXPO_USE_CUSTOM_INSPECTOR_PROXY) {
71    inspectorProxy = createInspectorProxy(config.projectRoot);
72  } else if (config.server.runInspectorProxy) {
73    inspectorProxy = new InspectorProxy(config.projectRoot);
74  }
75
76  let httpServer: http.Server | https.Server;
77
78  if (secureServerOptions != null) {
79    httpServer = https.createServer(secureServerOptions, serverApp);
80  } else {
81    httpServer = http.createServer(serverApp);
82  }
83  return new Promise<{ server: http.Server | https.Server; metro: Server }>((resolve, reject) => {
84    httpServer.on('error', (error) => {
85      if (onError) {
86        onError(error);
87      }
88      reject(error);
89      end();
90    });
91
92    httpServer.listen(config.server.port, host, () => {
93      if (onReady) {
94        onReady(httpServer);
95      }
96
97      Object.assign(websocketEndpoints, {
98        ...(inspectorProxy ? { ...inspectorProxy.createWebSocketListeners(httpServer) } : {}),
99        '/hot': createWebsocketServer({
100          websocketServer: new MetroHmrServer(
101            metroServer.getBundler(),
102            metroServer.getCreateModuleId(),
103            config
104          ),
105        }),
106      });
107
108      httpServer.on('upgrade', (request, socket, head) => {
109        const { pathname } = parse(request.url!);
110        if (pathname != null && websocketEndpoints[pathname]) {
111          websocketEndpoints[pathname].handleUpgrade(request, socket, head, (ws) => {
112            websocketEndpoints[pathname].emit('connection', ws, request);
113          });
114        } else {
115          socket.destroy();
116        }
117      });
118
119      if (inspectorProxy) {
120        // TODO(hypuk): Refactor inspectorProxy.processRequest into separate request handlers
121        // so that we could provide routes (/json/list and /json/version) here.
122        // Currently this causes Metro to give warning about T31407894.
123        // $FlowFixMe[method-unbinding] added when improving typing for this parameters
124        serverApp.use(inspectorProxy.processRequest.bind(inspectorProxy));
125      }
126
127      resolve({ server: httpServer, metro: metroServer });
128    });
129
130    // Disable any kind of automatic timeout behavior for incoming
131    // requests in case it takes the packager more than the default
132    // timeout of 120 seconds to respond to a request.
133    httpServer.timeout = 0;
134
135    httpServer.on('close', () => {
136      end();
137    });
138  });
139};
140