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