1import { ExpoConfig, getConfigFilePaths, Platform } from '@expo/config'; 2import type { LoadOptions } from '@expo/metro-config'; 3import chalk from 'chalk'; 4import Metro, { AssetData } from 'metro'; 5import getMetroAssets from 'metro/src/DeltaBundler/Serializers/getAssets'; 6import splitBundleOptions from 'metro/src/lib/splitBundleOptions'; 7import type { BundleOptions as MetroBundleOptions } from 'metro/src/shared/types'; 8import { ConfigT } from 'metro-config'; 9 10import { 11 buildHermesBundleAsync, 12 isEnableHermesManaged, 13 maybeThrowFromInconsistentEngineAsync, 14} from './exportHermes'; 15import { CSSAsset, getCssModulesFromBundler } from '../start/server/metro/getCssModulesFromBundler'; 16import { loadMetroConfigAsync } from '../start/server/metro/instantiateMetro'; 17import { 18 importMetroFromProject, 19 importMetroServerFromProject, 20} from '../start/server/metro/resolveFromProject'; 21 22export type MetroDevServerOptions = LoadOptions & { 23 quiet?: boolean; 24}; 25export type BundleOptions = { 26 entryPoint: string; 27 platform: 'android' | 'ios' | 'web'; 28 dev?: boolean; 29 minify?: boolean; 30 sourceMapUrl?: string; 31}; 32export type BundleAssetWithFileHashes = Metro.AssetData & { 33 fileHashes: string[]; // added by the hashAssets asset plugin 34}; 35export type BundleOutput = { 36 code: string; 37 map?: string; 38 hermesBytecodeBundle?: Uint8Array; 39 hermesSourcemap?: string; 40 css: CSSAsset[]; 41 assets: readonly BundleAssetWithFileHashes[]; 42}; 43 44let nextBuildID = 0; 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 isExporting: true, 77 }); 78 79 const metroServer = await metro.runMetro(config, { 80 watch: false, 81 }); 82 83 const buildAsync = async (bundle: BundleOptions): Promise<BundleOutput> => { 84 const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; 85 const isHermes = isEnableHermesManaged(expoConfig, bundle.platform); 86 const bundleOptions: MetroBundleOptions = { 87 ...Server.DEFAULT_BUNDLE_OPTIONS, 88 bundleType: 'bundle', 89 platform: bundle.platform, 90 entryFile: bundle.entryPoint, 91 dev: bundle.dev ?? false, 92 minify: !isHermes && (bundle.minify ?? !bundle.dev), 93 inlineSourceMap: false, 94 sourceMapUrl: bundle.sourceMapUrl, 95 createModuleIdFactory: config.serializer.createModuleIdFactory, 96 onProgress: (transformedFileCount: number, totalFileCount: number) => { 97 if (!options.quiet) { 98 reporter.update({ 99 buildID, 100 type: 'bundle_transform_progressed', 101 transformedFileCount, 102 totalFileCount, 103 }); 104 } 105 }, 106 }; 107 const bundleDetails = { 108 ...bundleOptions, 109 buildID, 110 }; 111 reporter.update({ 112 buildID, 113 type: 'bundle_build_started', 114 bundleDetails, 115 }); 116 try { 117 const { code, map } = await metroServer.build(bundleOptions); 118 const [assets, css] = await Promise.all([ 119 getAssets(metroServer, bundleOptions), 120 getCssModulesFromBundler(config, metroServer.getBundler(), bundleOptions), 121 ]); 122 123 reporter.update({ 124 buildID, 125 type: 'bundle_build_done', 126 }); 127 return { code, map, assets: assets as readonly BundleAssetWithFileHashes[], css }; 128 } catch (error) { 129 reporter.update({ 130 buildID, 131 type: 'bundle_build_failed', 132 }); 133 134 throw error; 135 } 136 }; 137 138 const maybeAddHermesBundleAsync = async ( 139 bundle: BundleOptions, 140 bundleOutput: BundleOutput 141 ): Promise<BundleOutput> => { 142 const { platform } = bundle; 143 const isHermesManaged = isEnableHermesManaged(expoConfig, platform); 144 if (isHermesManaged) { 145 const platformTag = chalk.bold( 146 { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform 147 ); 148 149 reporter.terminal.log(`${platformTag} Building Hermes bytecode for the bundle`); 150 151 const hermesBundleOutput = await buildHermesBundleAsync( 152 projectRoot, 153 bundleOutput.code, 154 bundleOutput.map!, 155 bundle.minify ?? !bundle.dev 156 ); 157 bundleOutput.hermesBytecodeBundle = hermesBundleOutput.hbc; 158 bundleOutput.hermesSourcemap = hermesBundleOutput.sourcemap; 159 } 160 return bundleOutput; 161 }; 162 163 try { 164 const intermediateOutputs = await Promise.all(bundles.map((bundle) => buildAsync(bundle))); 165 const bundleOutputs: BundleOutput[] = []; 166 for (let i = 0; i < bundles.length; ++i) { 167 // hermesc does not support parallel building even we spawn processes. 168 // we should build them sequentially. 169 bundleOutputs.push(await maybeAddHermesBundleAsync(bundles[i], intermediateOutputs[i])); 170 } 171 return bundleOutputs; 172 } catch (error) { 173 // New line so errors don't show up inline with the progress bar 174 console.log(''); 175 throw error; 176 } finally { 177 metroServer.end(); 178 } 179} 180 181// Forked out of Metro because the `this._getServerRootDir()` doesn't match the development 182// behavior. 183export async function getAssets( 184 metro: Metro.Server, 185 options: MetroBundleOptions 186): Promise<readonly AssetData[]> { 187 const { entryFile, onProgress, resolverOptions, transformOptions } = splitBundleOptions(options); 188 189 // @ts-expect-error: _bundler isn't exposed on the type. 190 const dependencies = await metro._bundler.getDependencies( 191 [entryFile], 192 transformOptions, 193 resolverOptions, 194 { onProgress, shallow: false, lazy: false } 195 ); 196 197 // @ts-expect-error 198 const _config = metro._config as ConfigT; 199 200 return await getMetroAssets(dependencies, { 201 processModuleFilter: _config.serializer.processModuleFilter, 202 assetPlugins: _config.transformer.assetPlugins, 203 platform: transformOptions.platform!, 204 projectRoot: _config.projectRoot, // this._getServerRootDir(), 205 publicPath: _config.transformer.publicPath, 206 }); 207} 208