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