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