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