1import { prependMiddleware } from '@expo/dev-server';
2
3import { getFreePortAsync } from '../../../utils/port';
4import { BundlerDevServer, BundlerStartOptions, DevServerInstance } from '../BundlerDevServer';
5import { HistoryFallbackMiddleware } from '../middleware/HistoryFallbackMiddleware';
6import { InterstitialPageMiddleware } from '../middleware/InterstitialPageMiddleware';
7import { RuntimeRedirectMiddleware } from '../middleware/RuntimeRedirectMiddleware';
8import { ServeStaticMiddleware } from '../middleware/ServeStaticMiddleware';
9import { instantiateMetroAsync } from './instantiateMetro';
10
11/** Default port to use for apps running in Expo Go. */
12const EXPO_GO_METRO_PORT = 19000;
13
14/** Default port to use for apps that run in standard React Native projects or Expo Dev Clients. */
15const DEV_CLIENT_METRO_PORT = 8081;
16
17export class MetroBundlerDevServer extends BundlerDevServer {
18  get name(): string {
19    return 'metro';
20  }
21
22  async resolvePortAsync(options: Partial<BundlerStartOptions> = {}): Promise<number> {
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    return port;
34  }
35
36  protected async startImplementationAsync(
37    options: BundlerStartOptions
38  ): Promise<DevServerInstance> {
39    options.port = await this.resolvePortAsync(options);
40    this.urlCreator = this.getUrlCreator(options);
41
42    const parsedOptions = {
43      port: options.port,
44      maxWorkers: options.maxWorkers,
45      resetCache: options.resetDevServer,
46
47      // Use the unversioned metro config.
48      // TODO: Deprecate this property when expo-cli goes away.
49      unversioned: false,
50    };
51
52    const { server, middleware, messageSocket } = await instantiateMetroAsync(
53      this.projectRoot,
54      parsedOptions
55    );
56
57    const manifestMiddleware = await this.getManifestMiddlewareAsync(options);
58
59    // We need the manifest handler to be the first middleware to run so our
60    // routes take precedence over static files. For example, the manifest is
61    // served from '/' and if the user has an index.html file in their project
62    // then the manifest handler will never run, the static middleware will run
63    // and serve index.html instead of the manifest.
64    // https://github.com/expo/expo/issues/13114
65    prependMiddleware(middleware, manifestMiddleware);
66
67    middleware.use(new InterstitialPageMiddleware(this.projectRoot).getHandler());
68
69    const deepLinkMiddleware = new RuntimeRedirectMiddleware(this.projectRoot, {
70      onDeepLink: ({ runtime }) => {
71        // eslint-disable-next-line no-useless-return
72        if (runtime === 'expo') return;
73        // TODO: Some heavy analytics...
74      },
75      getLocation: ({ runtime }) => {
76        if (runtime === 'custom') {
77          return this.urlCreator?.constructDevClientUrl();
78        } else {
79          return this.urlCreator?.constructUrl({
80            scheme: 'exp',
81          });
82        }
83      },
84    });
85    middleware.use(deepLinkMiddleware.getHandler());
86
87    // Append support for redirecting unhandled requests to the index.html page on web.
88    if (this.isTargetingWeb()) {
89      // This MUST be after the manifest middleware so it doesn't have a chance to serve the template `public/index.html`.
90      middleware.use(new ServeStaticMiddleware(this.projectRoot).getHandler());
91
92      // This MUST run last since it's the fallback.
93      middleware.use(new HistoryFallbackMiddleware(manifestMiddleware.internal).getHandler());
94    }
95    // Extend the close method to ensure that we clean up the local info.
96    const originalClose = server.close.bind(server);
97
98    server.close = (callback?: (err?: Error) => void) => {
99      return originalClose((err?: Error) => {
100        this.instance = null;
101        callback?.(err);
102      });
103    };
104
105    return {
106      server,
107      location: {
108        // The port is the main thing we want to send back.
109        port: options.port,
110        // localhost isn't always correct.
111        host: 'localhost',
112        // http is the only supported protocol on native.
113        url: `http://localhost:${options.port}`,
114        protocol: 'http',
115      },
116      middleware,
117      messageSocket,
118    };
119  }
120
121  protected getConfigModuleIds(): string[] {
122    return ['./metro.config.js', './metro.config.json', './rn-cli.config.js'];
123  }
124}
125