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