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