1import { ExpoConfig, getConfig } from '@expo/config'; 2import type { LoadOptions } from '@expo/metro-config'; 3import chalk from 'chalk'; 4import { Server as ConnectServer } from 'connect'; 5import http from 'http'; 6import type Metro from 'metro'; 7import { Terminal } from 'metro-core'; 8import semver from 'semver'; 9import { URL } from 'url'; 10 11import { MetroBundlerDevServer } from './MetroBundlerDevServer'; 12import { MetroTerminalReporter } from './MetroTerminalReporter'; 13import { importCliServerApiFromProject, importExpoMetroConfig } from './resolveFromProject'; 14import { getRouterDirectoryModuleIdWithManifest } from './router'; 15import { runServer } from './runServer-fork'; 16import { withMetroMultiPlatformAsync } from './withMetroMultiPlatform'; 17import { MetroDevServerOptions } from '../../../export/fork-bundleAsync'; 18import { Log } from '../../../log'; 19import { getMetroProperties } from '../../../utils/analytics/getMetroProperties'; 20import { createDebuggerTelemetryMiddleware } from '../../../utils/analytics/metroDebuggerMiddleware'; 21import { logEventAsync } from '../../../utils/analytics/rudderstackClient'; 22import { env } from '../../../utils/env'; 23import { getMetroServerRoot } from '../middleware/ManifestMiddleware'; 24import createJsInspectorMiddleware from '../middleware/inspector/createJsInspectorMiddleware'; 25import { prependMiddleware, replaceMiddlewareWith } from '../middleware/mutations'; 26import { remoteDevtoolsCorsMiddleware } from '../middleware/remoteDevtoolsCorsMiddleware'; 27import { remoteDevtoolsSecurityHeadersMiddleware } from '../middleware/remoteDevtoolsSecurityHeadersMiddleware'; 28import { ServerNext, ServerRequest, ServerResponse } from '../middleware/server.types'; 29import { suppressRemoteDebuggingErrorMiddleware } from '../middleware/suppressErrorMiddleware'; 30import { getPlatformBundlers } from '../platformBundlers'; 31 32// From expo/dev-server but with ability to use custom logger. 33type MessageSocket = { 34 broadcast: (method: string, params?: Record<string, any> | undefined) => void; 35}; 36 37function gteSdkVersion(exp: Pick<ExpoConfig, 'sdkVersion'>, sdkVersion: string): boolean { 38 if (!exp.sdkVersion) { 39 return false; 40 } 41 42 if (exp.sdkVersion === 'UNVERSIONED') { 43 return true; 44 } 45 46 try { 47 return semver.gte(exp.sdkVersion, sdkVersion); 48 } catch { 49 throw new Error(`${exp.sdkVersion} is not a valid version. Must be in the form of x.y.z`); 50 } 51} 52 53export async function loadMetroConfigAsync( 54 projectRoot: string, 55 options: LoadOptions, 56 { 57 exp = getConfig(projectRoot, { skipSDKVersionRequirement: true, skipPlugins: true }).exp, 58 isExporting, 59 }: { exp?: ExpoConfig; isExporting: boolean } 60) { 61 let reportEvent: ((event: any) => void) | undefined; 62 const serverRoot = getMetroServerRoot(projectRoot); 63 64 const terminal = new Terminal(process.stdout); 65 const terminalReporter = new MetroTerminalReporter(serverRoot, terminal); 66 67 const reporter = { 68 update(event: any) { 69 terminalReporter.update(event); 70 if (reportEvent) { 71 reportEvent(event); 72 } 73 }, 74 }; 75 76 const ExpoMetroConfig = importExpoMetroConfig(projectRoot); 77 let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options }); 78 79 if ( 80 // Requires SDK 50 for expo-assets hashAssetPlugin change. 81 !exp.sdkVersion || 82 gteSdkVersion(exp, '50.0.0') 83 ) { 84 if (isExporting) { 85 // This token will be used in the asset plugin to ensure the path is correct for writing locally. 86 // @ts-expect-error: typed as readonly. 87 config.transformer.publicPath = `/assets?export_path=${ 88 (exp.experiments?.basePath ?? '') + '/assets' 89 }`; 90 } else { 91 // @ts-expect-error: typed as readonly 92 config.transformer.publicPath = '/assets/?unstable_path=.'; 93 } 94 } else { 95 if (isExporting && exp.experiments?.basePath) { 96 // This token will be used in the asset plugin to ensure the path is correct for writing locally. 97 // @ts-expect-error: typed as readonly. 98 config.transformer.publicPath = exp.experiments?.basePath; 99 } 100 } 101 102 const platformBundlers = getPlatformBundlers(exp); 103 104 config = await withMetroMultiPlatformAsync(projectRoot, { 105 routerDirectory: getRouterDirectoryModuleIdWithManifest(projectRoot, exp), 106 config, 107 platformBundlers, 108 isTsconfigPathsEnabled: exp.experiments?.tsconfigPaths ?? true, 109 webOutput: exp.web?.output ?? 'single', 110 }); 111 112 logEventAsync('metro config', getMetroProperties(projectRoot, exp, config)); 113 114 return { 115 config, 116 setEventReporter: (logger: (event: any) => void) => (reportEvent = logger), 117 reporter: terminalReporter, 118 }; 119} 120 121/** The most generic possible setup for Metro bundler. */ 122export async function instantiateMetroAsync( 123 metroBundler: MetroBundlerDevServer, 124 options: Omit<MetroDevServerOptions, 'logger'>, 125 { isExporting }: { isExporting: boolean } 126): Promise<{ 127 metro: Metro.Server; 128 server: http.Server; 129 middleware: any; 130 messageSocket: MessageSocket; 131}> { 132 const projectRoot = metroBundler.projectRoot; 133 134 // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this. 135 const { exp } = getConfig(projectRoot, { 136 skipSDKVersionRequirement: true, 137 skipPlugins: true, 138 }); 139 140 const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync( 141 projectRoot, 142 options, 143 { exp, isExporting } 144 ); 145 146 const { createDevServerMiddleware, securityHeadersMiddleware } = 147 importCliServerApiFromProject(projectRoot); 148 149 const { middleware, messageSocketEndpoint, eventsSocketEndpoint, websocketEndpoints } = 150 createDevServerMiddleware({ 151 port: metroConfig.server.port, 152 watchFolders: metroConfig.watchFolders, 153 }); 154 155 // securityHeadersMiddleware does not support cross-origin requests for remote devtools to get the sourcemap. 156 // We replace with the enhanced version. 157 replaceMiddlewareWith( 158 middleware as ConnectServer, 159 securityHeadersMiddleware, 160 remoteDevtoolsSecurityHeadersMiddleware 161 ); 162 163 middleware.use(remoteDevtoolsCorsMiddleware); 164 165 prependMiddleware(middleware, suppressRemoteDebuggingErrorMiddleware); 166 167 middleware.use('/inspector', createJsInspectorMiddleware()); 168 169 // TODO: We can probably drop this now. 170 const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware; 171 // @ts-expect-error: can't mutate readonly config 172 metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => { 173 if (customEnhanceMiddleware) { 174 metroMiddleware = customEnhanceMiddleware(metroMiddleware, server); 175 } 176 return middleware.use(metroMiddleware); 177 }; 178 179 middleware.use(createDebuggerTelemetryMiddleware(projectRoot, exp)); 180 181 const { server, metro } = await runServer(metroBundler, metroConfig, { 182 hmrEnabled: true, 183 // @ts-expect-error: Inconsistent `websocketEndpoints` type between metro and @react-native-community/cli-server-api 184 websocketEndpoints, 185 watch: isWatchEnabled(), 186 }); 187 188 prependMiddleware(middleware, (req: ServerRequest, res: ServerResponse, next: ServerNext) => { 189 // If the URL is a Metro asset request, then we need to skip all other middleware to prevent 190 // the community CLI's serve-static from hosting `/assets/index.html` in place of all assets if it exists. 191 // /assets/?unstable_path=. 192 if (req.url) { 193 const url = new URL(req.url!, 'http://localhost:8000'); 194 if (url.pathname.match(/^\/assets\/?/) && url.searchParams.get('unstable_path') != null) { 195 return metro.processRequest(req, res, next); 196 } 197 } 198 return next(); 199 }); 200 201 setEventReporter(eventsSocketEndpoint.reportEvent); 202 203 return { 204 metro, 205 server, 206 middleware, 207 messageSocket: messageSocketEndpoint, 208 }; 209} 210 211/** 212 * Simplify and communicate if Metro is running without watching file updates,. 213 * Exposed for testing. 214 */ 215export function isWatchEnabled() { 216 if (env.CI) { 217 Log.log( 218 chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.` 219 ); 220 } 221 222 return !env.CI; 223} 224