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'; 14 15import { CSSAsset, getCssModulesFromBundler } from '../start/server/metro/getCssModulesFromBundler'; 16import { loadMetroConfigAsync } from '../start/server/metro/instantiateMetro'; 17 18export type MetroDevServerOptions = LoadOptions & { 19 logger: import('@expo/bunyan'); 20 quiet?: boolean; 21}; 22export type BundleOptions = { 23 entryPoint: string; 24 platform: 'android' | 'ios' | 'web'; 25 dev?: boolean; 26 minify?: boolean; 27 sourceMapUrl?: string; 28}; 29export type BundleAssetWithFileHashes = Metro.AssetData & { 30 fileHashes: string[]; // added by the hashAssets asset plugin 31}; 32export type BundleOutput = { 33 code: string; 34 map?: string; 35 hermesBytecodeBundle?: Uint8Array; 36 hermesSourcemap?: string; 37 css: CSSAsset[]; 38 assets: readonly BundleAssetWithFileHashes[]; 39}; 40 41let nextBuildID = 0; 42 43// Fork of @expo/dev-server bundleAsync to add Metro logging back. 44 45async function assertEngineMismatchAsync(projectRoot: string, exp: ExpoConfig, platform: Platform) { 46 const isHermesManaged = isEnableHermesManaged(exp, platform); 47 48 const paths = getConfigFilePaths(projectRoot); 49 const configFilePath = paths.dynamicConfigPath ?? paths.staticConfigPath ?? 'app.json'; 50 await maybeThrowFromInconsistentEngineAsync( 51 projectRoot, 52 configFilePath, 53 platform, 54 isHermesManaged 55 ); 56} 57 58export async function bundleAsync( 59 projectRoot: string, 60 expoConfig: ExpoConfig, 61 options: MetroDevServerOptions, 62 bundles: BundleOptions[] 63): Promise<BundleOutput[]> { 64 // Assert early so the user doesn't have to wait until bundling is complete to find out that 65 // Hermes won't be available. 66 await Promise.all( 67 bundles.map(({ platform }) => assertEngineMismatchAsync(projectRoot, expoConfig, platform)) 68 ); 69 70 const metro = importMetroFromProject(projectRoot); 71 const Server = importMetroServerFromProject(projectRoot); 72 73 const { config, reporter } = await loadMetroConfigAsync(projectRoot, options, { 74 exp: expoConfig, 75 }); 76 77 const metroServer = await metro.runMetro(config, { 78 watch: false, 79 }); 80 81 const buildAsync = async (bundle: BundleOptions): Promise<BundleOutput> => { 82 const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; 83 const isHermes = isEnableHermesManaged(expoConfig, bundle.platform); 84 const bundleOptions: Metro.BundleOptions = { 85 ...Server.DEFAULT_BUNDLE_OPTIONS, 86 bundleType: 'bundle', 87 platform: bundle.platform, 88 entryFile: bundle.entryPoint, 89 dev: bundle.dev ?? false, 90 minify: !isHermes && (bundle.minify ?? !bundle.dev), 91 inlineSourceMap: false, 92 sourceMapUrl: bundle.sourceMapUrl, 93 createModuleIdFactory: config.serializer.createModuleIdFactory, 94 onProgress: (transformedFileCount: number, totalFileCount: number) => { 95 if (!options.quiet) { 96 reporter.update({ 97 buildID, 98 type: 'bundle_transform_progressed', 99 transformedFileCount, 100 totalFileCount, 101 }); 102 } 103 }, 104 }; 105 const bundleDetails = { 106 ...bundleOptions, 107 buildID, 108 }; 109 reporter.update({ 110 buildID, 111 type: 'bundle_build_started', 112 // @ts-expect-error: TODO 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