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