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