1import { ExpoConfig, getConfigFilePaths, Platform } from '@expo/config'; 2import { 3 buildHermesBundleAsync, 4 isEnableHermesManaged, 5 maybeThrowFromInconsistentEngineAsync, 6} from '@expo/dev-server/build/HermesBundler'; 7import { 8 importMetroFromProject, 9 importMetroServerFromProject, 10} from '@expo/dev-server/build/metro/importMetroFromProject'; 11import { LoadOptions } from '@expo/metro-config'; 12import chalk from 'chalk'; 13import Metro from 'metro'; 14 15import { loadMetroConfigAsync } from '../start/server/metro/instantiateMetro'; 16 17export type MetroDevServerOptions = LoadOptions & { 18 logger: import('@expo/bunyan'); 19 quiet?: boolean; 20}; 21export type BundleOptions = { 22 entryPoint: string; 23 platform: 'android' | 'ios' | 'web'; 24 dev?: boolean; 25 minify?: boolean; 26 sourceMapUrl?: string; 27}; 28export type BundleAssetWithFileHashes = Metro.AssetData & { 29 fileHashes: string[]; // added by the hashAssets asset plugin 30}; 31export type BundleOutput = { 32 code: string; 33 map?: string; 34 hermesBytecodeBundle?: Uint8Array; 35 hermesSourcemap?: string; 36 assets: readonly BundleAssetWithFileHashes[]; 37}; 38 39let nextBuildID = 0; 40 41// Fork of @expo/dev-server bundleAsync to add Metro logging back. 42 43async function assertEngineMismatchAsync(projectRoot: string, exp: ExpoConfig, platform: Platform) { 44 const isHermesManaged = isEnableHermesManaged(exp, platform); 45 46 const paths = getConfigFilePaths(projectRoot); 47 const configFilePath = paths.dynamicConfigPath ?? paths.staticConfigPath ?? 'app.json'; 48 await maybeThrowFromInconsistentEngineAsync( 49 projectRoot, 50 configFilePath, 51 platform, 52 isHermesManaged 53 ); 54} 55 56export async function bundleAsync( 57 projectRoot: string, 58 expoConfig: ExpoConfig, 59 options: MetroDevServerOptions, 60 bundles: BundleOptions[] 61): Promise<BundleOutput[]> { 62 // Assert early so the user doesn't have to wait until bundling is complete to find out that 63 // Hermes won't be available. 64 await Promise.all( 65 bundles.map(({ platform }) => assertEngineMismatchAsync(projectRoot, expoConfig, platform)) 66 ); 67 68 const metro = importMetroFromProject(projectRoot); 69 const Server = importMetroServerFromProject(projectRoot); 70 71 const { config, reporter } = await loadMetroConfigAsync(projectRoot, options, { 72 exp: expoConfig, 73 }); 74 75 const metroServer = await metro.runMetro(config, { 76 watch: false, 77 }); 78 79 const buildAsync = async (bundle: BundleOptions): Promise<BundleOutput> => { 80 const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; 81 const isHermes = isEnableHermesManaged(expoConfig, bundle.platform); 82 const bundleOptions: Metro.BundleOptions = { 83 ...Server.DEFAULT_BUNDLE_OPTIONS, 84 bundleType: 'bundle', 85 platform: bundle.platform, 86 entryFile: bundle.entryPoint, 87 dev: bundle.dev ?? false, 88 minify: !isHermes && (bundle.minify ?? !bundle.dev), 89 inlineSourceMap: false, 90 sourceMapUrl: bundle.sourceMapUrl, 91 createModuleIdFactory: config.serializer.createModuleIdFactory, 92 onProgress: (transformedFileCount: number, totalFileCount: number) => { 93 if (!options.quiet) { 94 reporter.update({ 95 buildID, 96 type: 'bundle_transform_progressed', 97 transformedFileCount, 98 totalFileCount, 99 }); 100 } 101 }, 102 }; 103 const bundleDetails = { 104 ...bundleOptions, 105 buildID, 106 }; 107 reporter.update({ 108 buildID, 109 type: 'bundle_build_started', 110 // @ts-expect-error: TODO 111 bundleDetails, 112 }); 113 try { 114 const { code, map } = await metroServer.build(bundleOptions); 115 const assets = (await metroServer.getAssets( 116 bundleOptions 117 )) as readonly BundleAssetWithFileHashes[]; 118 reporter.update({ 119 buildID, 120 type: 'bundle_build_done', 121 }); 122 return { code, map, assets }; 123 } catch (error) { 124 reporter.update({ 125 buildID, 126 type: 'bundle_build_failed', 127 }); 128 129 throw error; 130 } 131 }; 132 133 const maybeAddHermesBundleAsync = async ( 134 bundle: BundleOptions, 135 bundleOutput: BundleOutput 136 ): Promise<BundleOutput> => { 137 const { platform } = bundle; 138 const isHermesManaged = isEnableHermesManaged(expoConfig, platform); 139 if (isHermesManaged) { 140 const platformTag = chalk.bold( 141 { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform 142 ); 143 144 reporter.terminal.log(`${platformTag} Building Hermes bytecode for the bundle`); 145 146 const hermesBundleOutput = await buildHermesBundleAsync( 147 projectRoot, 148 bundleOutput.code, 149 bundleOutput.map!, 150 bundle.minify ?? !bundle.dev 151 ); 152 bundleOutput.hermesBytecodeBundle = hermesBundleOutput.hbc; 153 bundleOutput.hermesSourcemap = hermesBundleOutput.sourcemap; 154 } 155 return bundleOutput; 156 }; 157 158 try { 159 const intermediateOutputs = await Promise.all(bundles.map((bundle) => buildAsync(bundle))); 160 const bundleOutputs: BundleOutput[] = []; 161 for (let i = 0; i < bundles.length; ++i) { 162 // hermesc does not support parallel building even we spawn processes. 163 // we should build them sequentially. 164 bundleOutputs.push(await maybeAddHermesBundleAsync(bundles[i], intermediateOutputs[i])); 165 } 166 return bundleOutputs; 167 } catch (error) { 168 // New line so errors don't show up inline with the progress bar 169 console.log(''); 170 throw error; 171 } finally { 172 metroServer.end(); 173 } 174} 175