1import { prependMiddleware } from '@expo/dev-server';
2
3import { getFreePortAsync } from '../../../utils/port';
4import { BundlerDevServer, BundlerStartOptions, DevServerInstance } from '../BundlerDevServer';
5import { UrlCreator } from '../UrlCreator';
6import { InterstitialPageMiddleware } from '../middleware/InterstitialPageMiddleware';
7import { RuntimeRedirectMiddleware } from '../middleware/RuntimeRedirectMiddleware';
8import { instantiateMetroAsync } from './instantiateMetro';
9
10/** Default port to use for apps running in Expo Go. */
11const EXPO_GO_METRO_PORT = 19000;
12
13/** Default port to use for apps that run in standard React Native projects or Expo Dev Clients. */
14const DEV_CLIENT_METRO_PORT = 8081;
15
16export class MetroBundlerDevServer extends BundlerDevServer {
17  get name(): string {
18    return 'metro';
19  }
20
21  async startAsync(options: BundlerStartOptions): Promise<DevServerInstance> {
22    await this.stopAsync();
23    const port =
24      // If the manually defined port is busy then an error should be thrown...
25      options.port ??
26      // Otherwise use the default port based on the runtime target.
27      (options.devClient
28        ? // Don't check if the port is busy if we're using the dev client since most clients are hardcoded to 8081.
29          Number(process.env.RCT_METRO_PORT) || DEV_CLIENT_METRO_PORT
30        : // Otherwise (running in Expo Go) use a free port that falls back on the classic 19000 port.
31          await getFreePortAsync(EXPO_GO_METRO_PORT));
32
33    this.urlCreator = new UrlCreator(options.location, {
34      port,
35      getTunnelUrl: this.getTunnelUrl.bind(this),
36    });
37
38    const parsedOptions = {
39      port,
40      maxWorkers: options.maxWorkers,
41      resetCache: options.resetDevServer,
42
43      // Use the unversioned metro config.
44      // TODO: Deprecate this property when expo-cli goes away.
45      unversioned: false,
46    };
47
48    const { server, middleware, messageSocket } = await instantiateMetroAsync(
49      this.projectRoot,
50      parsedOptions
51    );
52
53    const manifestMiddleware = await this.getManifestMiddlewareAsync(options);
54
55    // We need the manifest handler to be the first middleware to run so our
56    // routes take precedence over static files. For example, the manifest is
57    // served from '/' and if the user has an index.html file in their project
58    // then the manifest handler will never run, the static middleware will run
59    // and serve index.html instead of the manifest.
60    // https://github.com/expo/expo/issues/13114
61    prependMiddleware(middleware, manifestMiddleware);
62
63    middleware.use(new InterstitialPageMiddleware(this.projectRoot).getHandler());
64
65    const deepLinkMiddleware = new RuntimeRedirectMiddleware(this.projectRoot, {
66      onDeepLink: ({ runtime }) => {
67        // eslint-disable-next-line no-useless-return
68        if (runtime === 'expo') return;
69        // TODO: Some heavy analytics...
70      },
71      getLocation: ({ runtime }) => {
72        if (runtime === 'custom') {
73          return this.urlCreator?.constructDevClientUrl();
74        } else {
75          return this.urlCreator?.constructUrl({
76            scheme: 'exp',
77          });
78        }
79      },
80    });
81    middleware.use(deepLinkMiddleware.getHandler());
82
83    // Extend the close method to ensure that we clean up the local info.
84    const originalClose = server.close.bind(server);
85
86    server.close = (callback?: (err?: Error) => void) => {
87      return originalClose((err?: Error) => {
88        this.instance = null;
89        callback?.(err);
90      });
91    };
92
93    this.setInstance({
94      server,
95      location: {
96        // The port is the main thing we want to send back.
97        port,
98        // localhost isn't always correct.
99        host: 'localhost',
100        // http is the only supported protocol on native.
101        url: `http://localhost:${port}`,
102        protocol: 'http',
103      },
104      middleware,
105      messageSocket,
106    });
107
108    await this.postStartAsync(options);
109
110    return this.instance!;
111  }
112
113  protected getConfigModuleIds(): string[] {
114    return ['./metro.config.js', './metro.config.json', './rn-cli.config.js'];
115  }
116}
117