1import { ExpoConfig, getConfig, getConfigFilePaths, Platform } from '@expo/config'; 2import { 3 buildHermesBundleAsync, 4 isEnableHermesManaged, 5 maybeThrowFromInconsistentEngineAsync, 6} from '@expo/dev-server/build/HermesBundler'; 7import { 8 importExpoMetroConfigFromProject, 9 importMetroFromProject, 10 importMetroServerFromProject, 11} from '@expo/dev-server/build/metro/importMetroFromProject'; 12import { LoadOptions } from '@expo/metro-config'; 13import chalk from 'chalk'; 14import Metro from 'metro'; 15import { Terminal } from 'metro-core'; 16 17import { WebSupportProjectPrerequisite } from '../start/doctor/web/WebSupportProjectPrerequisite'; 18import { MetroTerminalReporter } from '../start/server/metro/MetroTerminalReporter'; 19import { withMetroMultiPlatform } from '../start/server/metro/withMetroMultiPlatform'; 20import { getPlatformBundlers } from '../start/server/platformBundlers'; 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 assets: readonly BundleAssetWithFileHashes[]; 42}; 43 44function getExpoMetroConfig( 45 projectRoot: string, 46 { logger }: Pick<MetroDevServerOptions, 'logger'> 47): typeof import('@expo/metro-config') { 48 try { 49 return importExpoMetroConfigFromProject(projectRoot); 50 } catch { 51 // If expo isn't installed, use the unversioned config and warn about installing expo. 52 } 53 54 const unversionedVersion = require('@expo/metro-config/package.json').version; 55 logger.info( 56 { tag: 'expo' }, 57 chalk.gray( 58 `\u203A Unversioned ${chalk.bold`@expo/metro-config@${unversionedVersion}`} is being used. Bundling apps may not work as expected, and is subject to breaking changes. Install ${chalk.bold`expo`} or set the app.json sdkVersion to use a stable version of @expo/metro-config.` 59 ) 60 ); 61 62 return require('@expo/metro-config'); 63} 64 65let nextBuildID = 0; 66 67// Fork of @expo/dev-server bundleAsync to add Metro logging back. 68 69async function assertEngineMismatchAsync(projectRoot: string, exp: ExpoConfig, platform: Platform) { 70 const isHermesManaged = isEnableHermesManaged(exp, platform); 71 72 const paths = getConfigFilePaths(projectRoot); 73 const configFilePath = paths.dynamicConfigPath ?? paths.staticConfigPath ?? 'app.json'; 74 await maybeThrowFromInconsistentEngineAsync( 75 projectRoot, 76 configFilePath, 77 platform, 78 isHermesManaged 79 ); 80} 81 82export async function bundleAsync( 83 projectRoot: string, 84 expoConfig: ExpoConfig, 85 options: MetroDevServerOptions, 86 bundles: BundleOptions[] 87): Promise<BundleOutput[]> { 88 // Assert early so the user doesn't have to wait until bundling is complete to find out that 89 // Hermes won't be available. 90 await Promise.all( 91 bundles.map(({ platform }) => assertEngineMismatchAsync(projectRoot, expoConfig, platform)) 92 ); 93 94 const metro = importMetroFromProject(projectRoot); 95 const Server = importMetroServerFromProject(projectRoot); 96 97 const terminal = new Terminal(process.stdout); 98 const terminalReporter = new MetroTerminalReporter(projectRoot, terminal); 99 100 const reporter = { 101 update(event: any) { 102 terminalReporter.update(event); 103 }, 104 }; 105 106 const ExpoMetroConfig = getExpoMetroConfig(projectRoot, options); 107 108 const { exp } = getConfig(projectRoot, { skipSDKVersionRequirement: true }); 109 let config = await ExpoMetroConfig.loadAsync(projectRoot, { reporter, ...options }); 110 111 const bundlerPlatforms = getPlatformBundlers(exp); 112 113 if (bundlerPlatforms.web === 'metro') { 114 await new WebSupportProjectPrerequisite(projectRoot).assertAsync(); 115 } 116 117 config = withMetroMultiPlatform(projectRoot, config, bundlerPlatforms); 118 119 const metroServer = await metro.runMetro(config, { 120 watch: false, 121 }); 122 123 const buildAsync = async (bundle: BundleOptions): Promise<BundleOutput> => { 124 const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; 125 const bundleOptions: Metro.BundleOptions = { 126 ...Server.DEFAULT_BUNDLE_OPTIONS, 127 bundleType: 'bundle', 128 platform: bundle.platform, 129 entryFile: bundle.entryPoint, 130 dev: bundle.dev ?? false, 131 minify: bundle.minify ?? !bundle.dev, 132 inlineSourceMap: false, 133 sourceMapUrl: bundle.sourceMapUrl, 134 createModuleIdFactory: config.serializer.createModuleIdFactory, 135 onProgress: (transformedFileCount: number, totalFileCount: number) => { 136 if (!options.quiet) { 137 terminalReporter.update({ 138 buildID, 139 type: 'bundle_transform_progressed', 140 transformedFileCount, 141 totalFileCount, 142 }); 143 } 144 }, 145 }; 146 const bundleDetails = { 147 ...bundleOptions, 148 buildID, 149 }; 150 terminalReporter.update({ 151 buildID, 152 type: 'bundle_build_started', 153 // @ts-expect-error: TODO 154 bundleDetails, 155 }); 156 try { 157 const { code, map } = await metroServer.build(bundleOptions); 158 const assets = (await metroServer.getAssets( 159 bundleOptions 160 )) as readonly BundleAssetWithFileHashes[]; 161 terminalReporter.update({ 162 buildID, 163 type: 'bundle_build_done', 164 }); 165 return { code, map, assets }; 166 } catch (error) { 167 terminalReporter.update({ 168 buildID, 169 type: 'bundle_build_failed', 170 }); 171 172 throw error; 173 } 174 }; 175 176 const maybeAddHermesBundleAsync = async ( 177 bundle: BundleOptions, 178 bundleOutput: BundleOutput 179 ): Promise<BundleOutput> => { 180 const { platform } = bundle; 181 const isHermesManaged = isEnableHermesManaged(expoConfig, platform); 182 if (isHermesManaged) { 183 const platformTag = chalk.bold( 184 { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform 185 ); 186 187 terminalReporter.terminal.log(`${platformTag} Building Hermes bytecode for the bundle`); 188 189 const hermesBundleOutput = await buildHermesBundleAsync( 190 projectRoot, 191 bundleOutput.code, 192 bundleOutput.map, 193 bundle.minify 194 ); 195 bundleOutput.hermesBytecodeBundle = hermesBundleOutput.hbc; 196 bundleOutput.hermesSourcemap = hermesBundleOutput.sourcemap; 197 } 198 return bundleOutput; 199 }; 200 201 try { 202 const intermediateOutputs = await Promise.all(bundles.map((bundle) => buildAsync(bundle))); 203 const bundleOutputs: BundleOutput[] = []; 204 for (let i = 0; i < bundles.length; ++i) { 205 // hermesc does not support parallel building even we spawn processes. 206 // we should build them sequentially. 207 bundleOutputs.push(await maybeAddHermesBundleAsync(bundles[i], intermediateOutputs[i])); 208 } 209 return bundleOutputs; 210 } catch (error) { 211 // New line so errors don't show up inline with the progress bar 212 console.log(''); 213 throw error; 214 } finally { 215 metroServer.end(); 216 } 217} 218