1import { ExpoConfig, getConfig } from '@expo/config'; 2import { MetroDevServerOptions } from '@expo/dev-server'; 3import type { LoadOptions } from '@expo/metro-config'; 4import chalk from 'chalk'; 5import http from 'http'; 6import type Metro from 'metro'; 7import { Terminal } from 'metro-core'; 8 9import { MetroBundlerDevServer } from './MetroBundlerDevServer'; 10import { MetroTerminalReporter } from './MetroTerminalReporter'; 11import { importExpoMetroConfig } from './resolveFromProject'; 12import { getRouterDirectory } from './router'; 13import { runServer } from './runServer-fork'; 14import { withMetroMultiPlatformAsync } from './withMetroMultiPlatform'; 15import { Log } from '../../../log'; 16import { getMetroProperties } from '../../../utils/analytics/getMetroProperties'; 17import { createDebuggerTelemetryMiddleware } from '../../../utils/analytics/metroDebuggerMiddleware'; 18import { logEventAsync } from '../../../utils/analytics/rudderstackClient'; 19import { env } from '../../../utils/env'; 20import { getMetroServerRoot } from '../middleware/ManifestMiddleware'; 21import { createDevServerMiddleware } from '../middleware/createDevServerMiddleware'; 22import { getPlatformBundlers } from '../platformBundlers'; 23 24// From expo/dev-server but with ability to use custom logger. 25type MessageSocket = { 26 broadcast: (method: string, params?: Record<string, any> | undefined) => void; 27}; 28 29export async function loadMetroConfigAsync( 30 projectRoot: string, 31 options: LoadOptions, 32 { 33 exp = getConfig(projectRoot, { skipSDKVersionRequirement: true, skipPlugins: true }).exp, 34 }: { exp?: ExpoConfig } = {} 35) { 36 let reportEvent: ((event: any) => void) | undefined; 37 const serverRoot = getMetroServerRoot(projectRoot); 38 39 const terminal = new Terminal(process.stdout); 40 const terminalReporter = new MetroTerminalReporter(serverRoot, terminal); 41 42 const reporter = { 43 update(event: any) { 44 terminalReporter.update(event); 45 if (reportEvent) { 46 reportEvent(event); 47 } 48 }, 49 }; 50 51 const ExpoMetroConfig = importExpoMetroConfig(projectRoot); 52 let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options }); 53 54 const platformBundlers = getPlatformBundlers(exp); 55 56 config = await withMetroMultiPlatformAsync(projectRoot, { 57 routerDirectory: exp.extra?.router?.unstable_src ?? getRouterDirectory(projectRoot), 58 config, 59 platformBundlers, 60 isTsconfigPathsEnabled: !!exp.experiments?.tsconfigPaths, 61 webOutput: exp.web?.output ?? 'single', 62 }); 63 64 logEventAsync('metro config', getMetroProperties(projectRoot, exp, config)); 65 66 return { 67 config, 68 setEventReporter: (logger: (event: any) => void) => (reportEvent = logger), 69 reporter: terminalReporter, 70 }; 71} 72 73/** The most generic possible setup for Metro bundler. */ 74export async function instantiateMetroAsync( 75 metroBundler: MetroBundlerDevServer, 76 options: Omit<MetroDevServerOptions, 'logger'> 77): Promise<{ 78 metro: Metro.Server; 79 server: http.Server; 80 middleware: any; 81 messageSocket: MessageSocket; 82}> { 83 const projectRoot = metroBundler.projectRoot; 84 85 // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this. 86 const { exp } = getConfig(projectRoot, { 87 skipSDKVersionRequirement: true, 88 skipPlugins: true, 89 }); 90 91 const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync( 92 projectRoot, 93 options, 94 { exp } 95 ); 96 97 const { middleware, websocketEndpoints, eventsSocketEndpoint, messageSocketEndpoint } = 98 createDevServerMiddleware(projectRoot, { 99 port: metroConfig.server.port, 100 watchFolders: metroConfig.watchFolders, 101 }); 102 103 const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware; 104 // @ts-expect-error: can't mutate readonly config 105 metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => { 106 if (customEnhanceMiddleware) { 107 metroMiddleware = customEnhanceMiddleware(metroMiddleware, server); 108 } 109 return middleware.use(metroMiddleware); 110 }; 111 112 middleware.use(createDebuggerTelemetryMiddleware(projectRoot, exp)); 113 114 const { server, metro } = await runServer(metroBundler, metroConfig, { 115 hmrEnabled: true, 116 // @ts-expect-error: Inconsistent `websocketEndpoints` type between metro and @react-native-community/cli-server-api 117 websocketEndpoints, 118 watch: isWatchEnabled(), 119 }); 120 121 setEventReporter(eventsSocketEndpoint.reportEvent); 122 123 return { 124 metro, 125 server, 126 middleware, 127 messageSocket: messageSocketEndpoint, 128 }; 129} 130 131/** 132 * Simplify and communicate if Metro is running without watching file updates,. 133 * Exposed for testing. 134 */ 135export function isWatchEnabled() { 136 if (env.CI) { 137 Log.log( 138 chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.` 139 ); 140 } 141 142 return !env.CI; 143} 144