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