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 isExporting: true, 80 }); 81 82 const metroServer = await metro.runMetro(config, { 83 watch: false, 84 }); 85 86 const buildAsync = async (bundle: BundleOptions): Promise<BundleOutput> => { 87 const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; 88 const isHermes = isEnableHermesManaged(expoConfig, bundle.platform); 89 const bundleOptions: MetroBundleOptions = { 90 ...Server.DEFAULT_BUNDLE_OPTIONS, 91 bundleType: 'bundle', 92 platform: bundle.platform, 93 entryFile: bundle.entryPoint, 94 dev: bundle.dev ?? false, 95 minify: !isHermes && (bundle.minify ?? !bundle.dev), 96 inlineSourceMap: false, 97 sourceMapUrl: bundle.sourceMapUrl, 98 createModuleIdFactory: config.serializer.createModuleIdFactory, 99 onProgress: (transformedFileCount: number, totalFileCount: number) => { 100 if (!options.quiet) { 101 reporter.update({ 102 buildID, 103 type: 'bundle_transform_progressed', 104 transformedFileCount, 105 totalFileCount, 106 }); 107 } 108 }, 109 }; 110 const bundleDetails = { 111 ...bundleOptions, 112 buildID, 113 }; 114 reporter.update({ 115 buildID, 116 type: 'bundle_build_started', 117 bundleDetails, 118 }); 119 try { 120 const { code, map } = await metroServer.build(bundleOptions); 121 const [assets, css] = await Promise.all([ 122 getAssets(metroServer, bundleOptions), 123 // metroServer.getAssets(bundleOptions), 124 getCssModulesFromBundler(config, metroServer.getBundler(), bundleOptions), 125 ]); 126 127 reporter.update({ 128 buildID, 129 type: 'bundle_build_done', 130 }); 131 return { code, map, assets: assets as readonly BundleAssetWithFileHashes[], css }; 132 } catch (error) { 133 reporter.update({ 134 buildID, 135 type: 'bundle_build_failed', 136 }); 137 138 throw error; 139 } 140 }; 141 142 const maybeAddHermesBundleAsync = async ( 143 bundle: BundleOptions, 144 bundleOutput: BundleOutput 145 ): Promise<BundleOutput> => { 146 const { platform } = bundle; 147 const isHermesManaged = isEnableHermesManaged(expoConfig, platform); 148 if (isHermesManaged) { 149 const platformTag = chalk.bold( 150 { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform 151 ); 152 153 reporter.terminal.log(`${platformTag} Building Hermes bytecode for the bundle`); 154 155 const hermesBundleOutput = await buildHermesBundleAsync( 156 projectRoot, 157 bundleOutput.code, 158 bundleOutput.map!, 159 bundle.minify ?? !bundle.dev 160 ); 161 bundleOutput.hermesBytecodeBundle = hermesBundleOutput.hbc; 162 bundleOutput.hermesSourcemap = hermesBundleOutput.sourcemap; 163 } 164 return bundleOutput; 165 }; 166 167 try { 168 const intermediateOutputs = await Promise.all(bundles.map((bundle) => buildAsync(bundle))); 169 const bundleOutputs: BundleOutput[] = []; 170 for (let i = 0; i < bundles.length; ++i) { 171 // hermesc does not support parallel building even we spawn processes. 172 // we should build them sequentially. 173 bundleOutputs.push(await maybeAddHermesBundleAsync(bundles[i], intermediateOutputs[i])); 174 } 175 return bundleOutputs; 176 } catch (error) { 177 // New line so errors don't show up inline with the progress bar 178 console.log(''); 179 throw error; 180 } finally { 181 metroServer.end(); 182 } 183} 184 185// Forked out of Metro because the `this._getServerRootDir()` doesn't match the development 186// behavior. 187export async function getAssets( 188 metro: Metro.Server, 189 options: MetroBundleOptions 190): Promise<readonly AssetData[]> { 191 const { entryFile, onProgress, resolverOptions, transformOptions } = splitBundleOptions(options); 192 193 // @ts-expect-error: _bundler isn't exposed on the type. 194 const dependencies = await metro._bundler.getDependencies( 195 [entryFile], 196 transformOptions, 197 resolverOptions, 198 { onProgress, shallow: false, lazy: false } 199 ); 200 201 // @ts-expect-error 202 const _config = metro._config as ConfigT; 203 204 return await getMetroAssets(dependencies, { 205 processModuleFilter: _config.serializer.processModuleFilter, 206 assetPlugins: _config.transformer.assetPlugins, 207 platform: transformOptions.platform!, 208 projectRoot: _config.projectRoot, // this._getServerRootDir(), 209 publicPath: _config.transformer.publicPath, 210 }); 211} 212