1import { getConfig } from '@expo/config'; 2import { MetroDevServerOptions } from '@expo/dev-server'; 3import chalk from 'chalk'; 4import http from 'http'; 5import Metro from 'metro'; 6import { Terminal } from 'metro-core'; 7 8import { Log } from '../../../log'; 9import { env } from '../../../utils/env'; 10import { createDevServerMiddleware } from '../middleware/createDevServerMiddleware'; 11import { getPlatformBundlers } from '../platformBundlers'; 12import { MetroTerminalReporter } from './MetroTerminalReporter'; 13import { importExpoMetroConfigFromProject, importMetroFromProject } from './resolveFromProject'; 14import { withMetroMultiPlatformAsync } from './withMetroMultiPlatform'; 15 16// From expo/dev-server but with ability to use custom logger. 17type MessageSocket = { 18 broadcast: (method: string, params?: Record<string, any> | undefined) => void; 19}; 20 21/** The most generic possible setup for Metro bundler. */ 22export async function instantiateMetroAsync( 23 projectRoot: string, 24 options: Omit<MetroDevServerOptions, 'logger'> 25): Promise<{ 26 server: http.Server; 27 middleware: any; 28 messageSocket: MessageSocket; 29}> { 30 let reportEvent: ((event: any) => void) | undefined; 31 32 const Metro = importMetroFromProject(projectRoot); 33 const ExpoMetroConfig = importExpoMetroConfigFromProject(projectRoot); 34 35 const terminal = new Terminal(process.stdout); 36 const terminalReporter = new MetroTerminalReporter(projectRoot, terminal); 37 38 const reporter = { 39 update(event: any) { 40 terminalReporter.update(event); 41 if (reportEvent) { 42 reportEvent(event); 43 } 44 }, 45 }; 46 47 let metroConfig = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options }); 48 49 // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this. 50 const { exp } = getConfig(projectRoot, { 51 skipSDKVersionRequirement: true, 52 skipPlugins: true, 53 }); 54 const platformBundlers = getPlatformBundlers(exp); 55 metroConfig = await withMetroMultiPlatformAsync(projectRoot, metroConfig, platformBundlers); 56 57 const { 58 middleware, 59 attachToServer, 60 61 // New 62 websocketEndpoints, 63 eventsSocketEndpoint, 64 messageSocketEndpoint, 65 } = createDevServerMiddleware(projectRoot, { 66 port: metroConfig.server.port, 67 watchFolders: metroConfig.watchFolders, 68 }); 69 70 const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware; 71 // @ts-ignore can't mutate readonly config 72 metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => { 73 if (customEnhanceMiddleware) { 74 metroMiddleware = customEnhanceMiddleware(metroMiddleware, server); 75 } 76 return middleware.use(metroMiddleware); 77 }; 78 79 const server = await Metro.runServer(metroConfig, { 80 hmrEnabled: true, 81 websocketEndpoints, 82 // @ts-expect-error Property was added in 0.73.4, remove this statement when updating Metro 83 watch: isWatchEnabled(), 84 }); 85 86 if (attachToServer) { 87 // Expo SDK 44 and lower 88 const { messageSocket, eventsSocket } = attachToServer(server); 89 reportEvent = eventsSocket.reportEvent; 90 91 return { 92 server, 93 middleware, 94 messageSocket, 95 }; 96 } else { 97 // RN +68 -- Expo SDK +45 98 reportEvent = eventsSocketEndpoint.reportEvent; 99 100 return { 101 server, 102 middleware, 103 messageSocket: messageSocketEndpoint, 104 }; 105 } 106} 107 108/** 109 * Simplify and communicate if Metro is running without watching file updates,. 110 * Exposed for testing. 111 */ 112export function isWatchEnabled() { 113 if (env.CI) { 114 Log.log( 115 chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.` 116 ); 117 } 118 119 return !env.CI; 120} 121