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