1import { ExpoConfig, getConfig, getConfigFilePaths, Platform } from '@expo/config'; 2import { 3 buildHermesBundleAsync, 4 isEnableHermesManaged, 5 maybeThrowFromInconsistentEngineAsync, 6} from '@expo/dev-server/build/HermesBundler'; 7import { 8 importExpoMetroConfigFromProject, 9 importMetroFromProject, 10 importMetroServerFromProject, 11} from '@expo/dev-server/build/metro/importMetroFromProject'; 12import { LoadOptions } from '@expo/metro-config'; 13import chalk from 'chalk'; 14import Metro from 'metro'; 15import { Terminal } from 'metro-core'; 16 17import { MetroTerminalReporter } from '../start/server/metro/MetroTerminalReporter'; 18import { withMetroMultiPlatformAsync } from '../start/server/metro/withMetroMultiPlatform'; 19import { getPlatformBundlers } from '../start/server/platformBundlers'; 20import { getMetroProperties } from '../utils/analytics/getMetroProperties'; 21import { logEventAsync } from '../utils/analytics/rudderstackClient'; 22 23export type MetroDevServerOptions = LoadOptions & { 24 logger: import('@expo/bunyan'); 25 quiet?: boolean; 26}; 27export type BundleOptions = { 28 entryPoint: string; 29 platform: 'android' | 'ios' | 'web'; 30 dev?: boolean; 31 minify?: boolean; 32 sourceMapUrl?: string; 33}; 34export type BundleAssetWithFileHashes = Metro.AssetData & { 35 fileHashes: string[]; // added by the hashAssets asset plugin 36}; 37export type BundleOutput = { 38 code: string; 39 map?: string; 40 hermesBytecodeBundle?: Uint8Array; 41 hermesSourcemap?: string; 42 assets: readonly BundleAssetWithFileHashes[]; 43}; 44 45function getExpoMetroConfig( 46 projectRoot: string, 47 { logger }: Pick<MetroDevServerOptions, 'logger'> 48): typeof import('@expo/metro-config') { 49 try { 50 return importExpoMetroConfigFromProject(projectRoot); 51 } catch { 52 // If expo isn't installed, use the unversioned config and warn about installing expo. 53 } 54 55 const unversionedVersion = require('@expo/metro-config/package.json').version; 56 logger.info( 57 { tag: 'expo' }, 58 chalk.gray( 59 `\u203A Unversioned ${chalk.bold`@expo/metro-config@${unversionedVersion}`} is being used. Bundling apps may not work as expected, and is subject to breaking changes. Install ${chalk.bold`expo`} or set the app.json sdkVersion to use a stable version of @expo/metro-config.` 60 ) 61 ); 62 63 return require('@expo/metro-config'); 64} 65 66let nextBuildID = 0; 67 68// Fork of @expo/dev-server bundleAsync to add Metro logging back. 69 70async function assertEngineMismatchAsync(projectRoot: string, exp: ExpoConfig, platform: Platform) { 71 const isHermesManaged = isEnableHermesManaged(exp, platform); 72 73 const paths = getConfigFilePaths(projectRoot); 74 const configFilePath = paths.dynamicConfigPath ?? paths.staticConfigPath ?? 'app.json'; 75 await maybeThrowFromInconsistentEngineAsync( 76 projectRoot, 77 configFilePath, 78 platform, 79 isHermesManaged 80 ); 81} 82 83export async function bundleAsync( 84 projectRoot: string, 85 expoConfig: ExpoConfig, 86 options: MetroDevServerOptions, 87 bundles: BundleOptions[] 88): Promise<BundleOutput[]> { 89 // Assert early so the user doesn't have to wait until bundling is complete to find out that 90 // Hermes won't be available. 91 await Promise.all( 92 bundles.map(({ platform }) => assertEngineMismatchAsync(projectRoot, expoConfig, platform)) 93 ); 94 95 const metro = importMetroFromProject(projectRoot); 96 const Server = importMetroServerFromProject(projectRoot); 97 98 const terminal = new Terminal(process.stdout); 99 const terminalReporter = new MetroTerminalReporter(projectRoot, terminal); 100 101 const reporter = { 102 update(event: any) { 103 terminalReporter.update(event); 104 }, 105 }; 106 107 const ExpoMetroConfig = getExpoMetroConfig(projectRoot, options); 108 109 const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true }); 110 let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options }); 111 112 const bundlerPlatforms = getPlatformBundlers(exp); 113 114 config = await withMetroMultiPlatformAsync(projectRoot, config, bundlerPlatforms); 115 116 logEventAsync('metro config', getMetroProperties(projectRoot, exp, config)); 117 118 const metroServer = await metro.runMetro(config, { 119 watch: false, 120 }); 121 122 const buildAsync = async (bundle: BundleOptions): Promise<BundleOutput> => { 123 const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; 124 const isHermes = isEnableHermesManaged(expoConfig, bundle.platform); 125 const bundleOptions: Metro.BundleOptions = { 126 ...Server.DEFAULT_BUNDLE_OPTIONS, 127 bundleType: 'bundle', 128 platform: bundle.platform, 129 entryFile: bundle.entryPoint, 130 dev: bundle.dev ?? false, 131 minify: !isHermes && (bundle.minify ?? !bundle.dev), 132 inlineSourceMap: false, 133 sourceMapUrl: bundle.sourceMapUrl, 134 createModuleIdFactory: config.serializer.createModuleIdFactory, 135 onProgress: (transformedFileCount: number, totalFileCount: number) => { 136 if (!options.quiet) { 137 terminalReporter.update({ 138 buildID, 139 type: 'bundle_transform_progressed', 140 transformedFileCount, 141 totalFileCount, 142 }); 143 } 144 }, 145 }; 146 const bundleDetails = { 147 ...bundleOptions, 148 buildID, 149 }; 150 terminalReporter.update({ 151 buildID, 152 type: 'bundle_build_started', 153 // @ts-expect-error: TODO 154 bundleDetails, 155 }); 156 try { 157 const { code, map } = await metroServer.build(bundleOptions); 158 const assets = (await metroServer.getAssets( 159 bundleOptions 160 )) as readonly BundleAssetWithFileHashes[]; 161 terminalReporter.update({ 162 buildID, 163 type: 'bundle_build_done', 164 }); 165 return { code, map, assets }; 166 } catch (error) { 167 terminalReporter.update({ 168 buildID, 169 type: 'bundle_build_failed', 170 }); 171 172 throw error; 173 } 174 }; 175 176 const maybeAddHermesBundleAsync = async ( 177 bundle: BundleOptions, 178 bundleOutput: BundleOutput 179 ): Promise<BundleOutput> => { 180 const { platform } = bundle; 181 const isHermesManaged = isEnableHermesManaged(expoConfig, platform); 182 if (isHermesManaged) { 183 const platformTag = chalk.bold( 184 { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform 185 ); 186 187 terminalReporter.terminal.log(`${platformTag} Building Hermes bytecode for the bundle`); 188 189 const hermesBundleOutput = await buildHermesBundleAsync( 190 projectRoot, 191 bundleOutput.code, 192 bundleOutput.map!, 193 bundle.minify ?? !bundle.dev 194 ); 195 bundleOutput.hermesBytecodeBundle = hermesBundleOutput.hbc; 196 bundleOutput.hermesSourcemap = hermesBundleOutput.sourcemap; 197 } 198 return bundleOutput; 199 }; 200 201 try { 202 const intermediateOutputs = await Promise.all(bundles.map((bundle) => buildAsync(bundle))); 203 const bundleOutputs: BundleOutput[] = []; 204 for (let i = 0; i < bundles.length; ++i) { 205 // hermesc does not support parallel building even we spawn processes. 206 // we should build them sequentially. 207 bundleOutputs.push(await maybeAddHermesBundleAsync(bundles[i], intermediateOutputs[i])); 208 } 209 return bundleOutputs; 210 } catch (error) { 211 // New line so errors don't show up inline with the progress bar 212 console.log(''); 213 throw error; 214 } finally { 215 metroServer.end(); 216 } 217} 218