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