1b6b91c50SEvan Baconimport { ExpoConfig, getConfig } from '@expo/config'; 2b6b91c50SEvan Baconimport type { LoadOptions } from '@expo/metro-config'; 30f61c876SCedric van Puttenimport chalk from 'chalk'; 4edeec536SEvan Baconimport { Server as ConnectServer } from 'connect'; 58d307f52SEvan Baconimport http from 'http'; 633643b60SEvan Baconimport type Metro from 'metro'; 78d307f52SEvan Baconimport { Terminal } from 'metro-core'; 8429dc7fcSEvan Baconimport semver from 'semver'; 9429dc7fcSEvan Baconimport { URL } from 'url'; 108d307f52SEvan Bacon 118a424bebSJames Ideimport { MetroBundlerDevServer } from './MetroBundlerDevServer'; 128a424bebSJames Ideimport { MetroTerminalReporter } from './MetroTerminalReporter'; 13edeec536SEvan Baconimport { importCliServerApiFromProject, importExpoMetroConfig } from './resolveFromProject'; 1446f023faSEvan Baconimport { getRouterDirectoryModuleIdWithManifest } from './router'; 158a424bebSJames Ideimport { runServer } from './runServer-fork'; 168a424bebSJames Ideimport { withMetroMultiPlatformAsync } from './withMetroMultiPlatform'; 17edeec536SEvan Baconimport { MetroDevServerOptions } from '../../../export/fork-bundleAsync'; 180f61c876SCedric van Puttenimport { Log } from '../../../log'; 19d42dd5d4SCedric van Puttenimport { getMetroProperties } from '../../../utils/analytics/getMetroProperties'; 20d42dd5d4SCedric van Puttenimport { createDebuggerTelemetryMiddleware } from '../../../utils/analytics/metroDebuggerMiddleware'; 21d42dd5d4SCedric van Puttenimport { logEventAsync } from '../../../utils/analytics/rudderstackClient'; 220f61c876SCedric van Puttenimport { env } from '../../../utils/env'; 230a6ddb20SEvan Baconimport { getMetroServerRoot } from '../middleware/ManifestMiddleware'; 24edeec536SEvan Baconimport createJsInspectorMiddleware from '../middleware/inspector/createJsInspectorMiddleware'; 25edeec536SEvan Baconimport { prependMiddleware, replaceMiddlewareWith } from '../middleware/mutations'; 26edeec536SEvan Baconimport { remoteDevtoolsCorsMiddleware } from '../middleware/remoteDevtoolsCorsMiddleware'; 27edeec536SEvan Baconimport { remoteDevtoolsSecurityHeadersMiddleware } from '../middleware/remoteDevtoolsSecurityHeadersMiddleware'; 28429dc7fcSEvan Baconimport { ServerNext, ServerRequest, ServerResponse } from '../middleware/server.types'; 29edeec536SEvan Baconimport { suppressRemoteDebuggingErrorMiddleware } from '../middleware/suppressErrorMiddleware'; 306d6b81f9SEvan Baconimport { getPlatformBundlers } from '../platformBundlers'; 318d307f52SEvan Bacon 328d307f52SEvan Bacon// From expo/dev-server but with ability to use custom logger. 338d307f52SEvan Bacontype MessageSocket = { 348d307f52SEvan Bacon broadcast: (method: string, params?: Record<string, any> | undefined) => void; 358d307f52SEvan Bacon}; 368d307f52SEvan Bacon 37429dc7fcSEvan Baconfunction gteSdkVersion(exp: Pick<ExpoConfig, 'sdkVersion'>, sdkVersion: string): boolean { 38429dc7fcSEvan Bacon if (!exp.sdkVersion) { 39429dc7fcSEvan Bacon return false; 40429dc7fcSEvan Bacon } 41429dc7fcSEvan Bacon 42429dc7fcSEvan Bacon if (exp.sdkVersion === 'UNVERSIONED') { 43429dc7fcSEvan Bacon return true; 44429dc7fcSEvan Bacon } 45429dc7fcSEvan Bacon 46429dc7fcSEvan Bacon try { 47429dc7fcSEvan Bacon return semver.gte(exp.sdkVersion, sdkVersion); 48429dc7fcSEvan Bacon } catch { 49429dc7fcSEvan Bacon throw new Error(`${exp.sdkVersion} is not a valid version. Must be in the form of x.y.z`); 50429dc7fcSEvan Bacon } 51429dc7fcSEvan Bacon} 52429dc7fcSEvan Bacon 53b6b91c50SEvan Baconexport async function loadMetroConfigAsync( 548d307f52SEvan Bacon projectRoot: string, 55b6b91c50SEvan Bacon options: LoadOptions, 56b6b91c50SEvan Bacon { 57b6b91c50SEvan Bacon exp = getConfig(projectRoot, { skipSDKVersionRequirement: true, skipPlugins: true }).exp, 58429dc7fcSEvan Bacon isExporting, 59429dc7fcSEvan Bacon }: { exp?: ExpoConfig; isExporting: boolean } 60b6b91c50SEvan Bacon) { 618d307f52SEvan Bacon let reportEvent: ((event: any) => void) | undefined; 620a6ddb20SEvan Bacon const serverRoot = getMetroServerRoot(projectRoot); 638d307f52SEvan Bacon 648d307f52SEvan Bacon const terminal = new Terminal(process.stdout); 650a6ddb20SEvan Bacon const terminalReporter = new MetroTerminalReporter(serverRoot, terminal); 668d307f52SEvan Bacon 678d307f52SEvan Bacon const reporter = { 688d307f52SEvan Bacon update(event: any) { 698d307f52SEvan Bacon terminalReporter.update(event); 708d307f52SEvan Bacon if (reportEvent) { 718d307f52SEvan Bacon reportEvent(event); 728d307f52SEvan Bacon } 738d307f52SEvan Bacon }, 748d307f52SEvan Bacon }; 758d307f52SEvan Bacon 76036e9444SEvan Bacon const ExpoMetroConfig = importExpoMetroConfig(projectRoot); 77b6b91c50SEvan Bacon let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options }); 78b6b91c50SEvan Bacon 79429dc7fcSEvan Bacon if ( 80429dc7fcSEvan Bacon // Requires SDK 50 for expo-assets hashAssetPlugin change. 81429dc7fcSEvan Bacon !exp.sdkVersion || 82429dc7fcSEvan Bacon gteSdkVersion(exp, '50.0.0') 83429dc7fcSEvan Bacon ) { 84429dc7fcSEvan Bacon if (isExporting) { 85429dc7fcSEvan Bacon // This token will be used in the asset plugin to ensure the path is correct for writing locally. 86429dc7fcSEvan Bacon // @ts-expect-error: typed as readonly. 877c98c357SEvan Bacon config.transformer.publicPath = `/assets?export_path=${ 887c98c357SEvan Bacon (exp.experiments?.basePath ?? '') + '/assets' 897c98c357SEvan Bacon }`; 90429dc7fcSEvan Bacon } else { 91429dc7fcSEvan Bacon // @ts-expect-error: typed as readonly 92429dc7fcSEvan Bacon config.transformer.publicPath = '/assets/?unstable_path=.'; 93429dc7fcSEvan Bacon } 947c98c357SEvan Bacon } else { 957c98c357SEvan Bacon if (isExporting && exp.experiments?.basePath) { 967c98c357SEvan Bacon // This token will be used in the asset plugin to ensure the path is correct for writing locally. 977c98c357SEvan Bacon // @ts-expect-error: typed as readonly. 987c98c357SEvan Bacon config.transformer.publicPath = exp.experiments?.basePath; 997c98c357SEvan Bacon } 100429dc7fcSEvan Bacon } 101429dc7fcSEvan Bacon 102f479be69SEvan Bacon const platformBundlers = getPlatformBundlers(exp); 103b6b91c50SEvan Bacon 104f479be69SEvan Bacon config = await withMetroMultiPlatformAsync(projectRoot, { 10546f023faSEvan Bacon routerDirectory: getRouterDirectoryModuleIdWithManifest(projectRoot, exp), 106f479be69SEvan Bacon config, 107f479be69SEvan Bacon platformBundlers, 108*1c8e3ea0SMark Lawlor isTsconfigPathsEnabled: exp.experiments?.tsconfigPaths ?? true, 1099580591fSEvan Bacon webOutput: exp.web?.output ?? 'single', 110f479be69SEvan Bacon }); 111b6b91c50SEvan Bacon 112b6b91c50SEvan Bacon logEventAsync('metro config', getMetroProperties(projectRoot, exp, config)); 113b6b91c50SEvan Bacon 114b6b91c50SEvan Bacon return { 115b6b91c50SEvan Bacon config, 116b6b91c50SEvan Bacon setEventReporter: (logger: (event: any) => void) => (reportEvent = logger), 117b6b91c50SEvan Bacon reporter: terminalReporter, 118b6b91c50SEvan Bacon }; 119b6b91c50SEvan Bacon} 120b6b91c50SEvan Bacon 121b6b91c50SEvan Bacon/** The most generic possible setup for Metro bundler. */ 122b6b91c50SEvan Baconexport async function instantiateMetroAsync( 12303d43e7dSCedric van Putten metroBundler: MetroBundlerDevServer, 124429dc7fcSEvan Bacon options: Omit<MetroDevServerOptions, 'logger'>, 125429dc7fcSEvan Bacon { isExporting }: { isExporting: boolean } 126b6b91c50SEvan Bacon): Promise<{ 127b6b91c50SEvan Bacon metro: Metro.Server; 128b6b91c50SEvan Bacon server: http.Server; 129b6b91c50SEvan Bacon middleware: any; 130b6b91c50SEvan Bacon messageSocket: MessageSocket; 131b6b91c50SEvan Bacon}> { 13203d43e7dSCedric van Putten const projectRoot = metroBundler.projectRoot; 13303d43e7dSCedric van Putten 1346d6b81f9SEvan Bacon // TODO: When we bring expo/metro-config into the expo/expo repo, then we can upstream this. 135e87e8ea8SEvan Bacon const { exp } = getConfig(projectRoot, { 136e87e8ea8SEvan Bacon skipSDKVersionRequirement: true, 137e87e8ea8SEvan Bacon skipPlugins: true, 138e87e8ea8SEvan Bacon }); 139d42dd5d4SCedric van Putten 140b6b91c50SEvan Bacon const { config: metroConfig, setEventReporter } = await loadMetroConfigAsync( 141b6b91c50SEvan Bacon projectRoot, 142b6b91c50SEvan Bacon options, 143429dc7fcSEvan Bacon { exp, isExporting } 144b6b91c50SEvan Bacon ); 145d42dd5d4SCedric van Putten 146edeec536SEvan Bacon const { createDevServerMiddleware, securityHeadersMiddleware } = 147edeec536SEvan Bacon importCliServerApiFromProject(projectRoot); 148edeec536SEvan Bacon 149edeec536SEvan Bacon const { middleware, messageSocketEndpoint, eventsSocketEndpoint, websocketEndpoints } = 150edeec536SEvan Bacon createDevServerMiddleware({ 1518d307f52SEvan Bacon port: metroConfig.server.port, 1528d307f52SEvan Bacon watchFolders: metroConfig.watchFolders, 1538d307f52SEvan Bacon }); 1548d307f52SEvan Bacon 155edeec536SEvan Bacon // securityHeadersMiddleware does not support cross-origin requests for remote devtools to get the sourcemap. 156edeec536SEvan Bacon // We replace with the enhanced version. 157edeec536SEvan Bacon replaceMiddlewareWith( 158edeec536SEvan Bacon middleware as ConnectServer, 159edeec536SEvan Bacon securityHeadersMiddleware, 160edeec536SEvan Bacon remoteDevtoolsSecurityHeadersMiddleware 161edeec536SEvan Bacon ); 162edeec536SEvan Bacon 163edeec536SEvan Bacon middleware.use(remoteDevtoolsCorsMiddleware); 164edeec536SEvan Bacon 165edeec536SEvan Bacon prependMiddleware(middleware, suppressRemoteDebuggingErrorMiddleware); 166edeec536SEvan Bacon 167edeec536SEvan Bacon middleware.use('/inspector', createJsInspectorMiddleware()); 168edeec536SEvan Bacon 169edeec536SEvan Bacon // TODO: We can probably drop this now. 1708d307f52SEvan Bacon const customEnhanceMiddleware = metroConfig.server.enhanceMiddleware; 171b6b91c50SEvan Bacon // @ts-expect-error: can't mutate readonly config 1728d307f52SEvan Bacon metroConfig.server.enhanceMiddleware = (metroMiddleware: any, server: Metro.Server) => { 1738d307f52SEvan Bacon if (customEnhanceMiddleware) { 1748d307f52SEvan Bacon metroMiddleware = customEnhanceMiddleware(metroMiddleware, server); 1758d307f52SEvan Bacon } 1768d307f52SEvan Bacon return middleware.use(metroMiddleware); 1778d307f52SEvan Bacon }; 1788d307f52SEvan Bacon 179d42dd5d4SCedric van Putten middleware.use(createDebuggerTelemetryMiddleware(projectRoot, exp)); 180d42dd5d4SCedric van Putten 18103d43e7dSCedric van Putten const { server, metro } = await runServer(metroBundler, metroConfig, { 1828d307f52SEvan Bacon hmrEnabled: true, 183da5824c9SKudo Chien // @ts-expect-error: Inconsistent `websocketEndpoints` type between metro and @react-native-community/cli-server-api 1848a782c0fSEvan Bacon websocketEndpoints, 1850f61c876SCedric van Putten watch: isWatchEnabled(), 1868d307f52SEvan Bacon }); 1878d307f52SEvan Bacon 188429dc7fcSEvan Bacon prependMiddleware(middleware, (req: ServerRequest, res: ServerResponse, next: ServerNext) => { 189429dc7fcSEvan Bacon // If the URL is a Metro asset request, then we need to skip all other middleware to prevent 190429dc7fcSEvan Bacon // the community CLI's serve-static from hosting `/assets/index.html` in place of all assets if it exists. 191429dc7fcSEvan Bacon // /assets/?unstable_path=. 192429dc7fcSEvan Bacon if (req.url) { 193429dc7fcSEvan Bacon const url = new URL(req.url!, 'http://localhost:8000'); 194429dc7fcSEvan Bacon if (url.pathname.match(/^\/assets\/?/) && url.searchParams.get('unstable_path') != null) { 195429dc7fcSEvan Bacon return metro.processRequest(req, res, next); 196429dc7fcSEvan Bacon } 197429dc7fcSEvan Bacon } 198429dc7fcSEvan Bacon return next(); 199429dc7fcSEvan Bacon }); 200429dc7fcSEvan Bacon 201b6b91c50SEvan Bacon setEventReporter(eventsSocketEndpoint.reportEvent); 2028a782c0fSEvan Bacon 2038a782c0fSEvan Bacon return { 20433643b60SEvan Bacon metro, 2058a782c0fSEvan Bacon server, 2068a782c0fSEvan Bacon middleware, 2078a782c0fSEvan Bacon messageSocket: messageSocketEndpoint, 2088a782c0fSEvan Bacon }; 2098a782c0fSEvan Bacon} 2100f61c876SCedric van Putten 2110f61c876SCedric van Putten/** 2120f61c876SCedric van Putten * Simplify and communicate if Metro is running without watching file updates,. 2130f61c876SCedric van Putten * Exposed for testing. 2140f61c876SCedric van Putten */ 2150f61c876SCedric van Puttenexport function isWatchEnabled() { 2160f61c876SCedric van Putten if (env.CI) { 2170f61c876SCedric van Putten Log.log( 2180f61c876SCedric van Putten chalk`Metro is running in CI mode, reloads are disabled. Remove {bold CI=true} to enable watch mode.` 2190f61c876SCedric van Putten ); 2200f61c876SCedric van Putten } 2210f61c876SCedric van Putten 2220f61c876SCedric van Putten return !env.CI; 2230f61c876SCedric van Putten} 224