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 { importExpoMetroConfigFromProject } 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 = importExpoMetroConfigFromProject(projectRoot); 51 52 let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options }); 53 54 const bundlerPlatforms = getPlatformBundlers(exp); 55 56 config = await withMetroMultiPlatformAsync(projectRoot, config, bundlerPlatforms); 57 58 logEventAsync('metro config', getMetroProperties(projectRoot, exp, config)); 59 60 return { 61 config, 62 setEventReporter: (logger: (event: any) => void) => (reportEvent = logger), 63 reporter: terminalReporter, 64 }; 65} 66 67/** The most generic possible setup for Metro bundler. */ 68export async function instantiateMetroAsync( 69 metroBundler: MetroBundlerDevServer, 70 options: Omit<MetroDevServerOptions, 'logger'> 71): Promise<{ 72 metro: Metro.Server; 73 server: http.Server; 74 middleware: any; 75 messageSocket: MessageSocket; 76}> { 77 const projectRoot = metroBundler.projectRoot; 78 79 // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this. 80 const { exp } = getConfig(projectRoot, { 81 skipSDKVersionRequirement: true, 82 skipPlugins: true, 83 }); 84 85 const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync( 86 projectRoot, 87 options, 88 { exp } 89 ); 90 91 const { middleware, websocketEndpoints, eventsSocketEndpoint, messageSocketEndpoint } = 92 createDevServerMiddleware(projectRoot, { 93 port: metroConfig.server.port, 94 watchFolders: metroConfig.watchFolders, 95 }); 96 97 const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware; 98 // @ts-expect-error: can't mutate readonly config 99 metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => { 100 if (customEnhanceMiddleware) { 101 metroMiddleware = customEnhanceMiddleware(metroMiddleware, server); 102 } 103 return middleware.use(metroMiddleware); 104 }; 105 106 middleware.use(createDebuggerTelemetryMiddleware(projectRoot, exp)); 107 108 const { server, metro } = await runServer(metroBundler, metroConfig, { 109 hmrEnabled: true, 110 websocketEndpoints, 111 watch: isWatchEnabled(), 112 }); 113 114 setEventReporter(eventsSocketEndpoint.reportEvent); 115 116 return { 117 metro, 118 server, 119 middleware, 120 messageSocket: messageSocketEndpoint, 121 }; 122} 123 124/** 125 * Simplify and communicate if Metro is running without watching file updates,. 126 * Exposed for testing. 127 */ 128export function isWatchEnabled() { 129 if (env.CI) { 130 Log.log( 131 chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.` 132 ); 133 } 134 135 return !env.CI; 136} 137