1b6b91c50SEvan Baconimport { ExpoConfig, getConfigFilePaths, Platform } from '@expo/config'; 2036e9444SEvan Baconimport type { LoadOptions } from '@expo/metro-config'; 3dc51e206SEvan Baconimport chalk from 'chalk'; 4557aaacdSEvan Baconimport Metro, { AssetData } from 'metro'; 51f98cdd7SEvan Baconimport getMetroAssets from 'metro/src/DeltaBundler/Serializers/getAssets'; 6557aaacdSEvan Baconimport splitBundleOptions from 'metro/src/lib/splitBundleOptions'; 7da5824c9SKudo Chienimport type { BundleOptions as MetroBundleOptions } from 'metro/src/shared/types'; 81f98cdd7SEvan Baconimport { ConfigT } from 'metro-config'; 9dc51e206SEvan Bacon 10*edeec536SEvan Baconimport { 11*edeec536SEvan Bacon buildHermesBundleAsync, 12*edeec536SEvan Bacon isEnableHermesManaged, 13*edeec536SEvan Bacon maybeThrowFromInconsistentEngineAsync, 14*edeec536SEvan Bacon} from './exportHermes'; 159580591fSEvan Baconimport { CSSAsset, getCssModulesFromBundler } from '../start/server/metro/getCssModulesFromBundler'; 16b6b91c50SEvan Baconimport { loadMetroConfigAsync } from '../start/server/metro/instantiateMetro'; 17*edeec536SEvan Baconimport { 18*edeec536SEvan Bacon importMetroFromProject, 19*edeec536SEvan Bacon importMetroServerFromProject, 20*edeec536SEvan Bacon} from '../start/server/metro/resolveFromProject'; 21dc51e206SEvan Bacon 22dc51e206SEvan Baconexport type MetroDevServerOptions = LoadOptions & { 23dc51e206SEvan Bacon quiet?: boolean; 24dc51e206SEvan Bacon}; 25dc51e206SEvan Baconexport type BundleOptions = { 26dc51e206SEvan Bacon entryPoint: string; 27dc51e206SEvan Bacon platform: 'android' | 'ios' | 'web'; 28dc51e206SEvan Bacon dev?: boolean; 29dc51e206SEvan Bacon minify?: boolean; 30dc51e206SEvan Bacon sourceMapUrl?: string; 31dc51e206SEvan Bacon}; 32dc51e206SEvan Baconexport type BundleAssetWithFileHashes = Metro.AssetData & { 33dc51e206SEvan Bacon fileHashes: string[]; // added by the hashAssets asset plugin 34dc51e206SEvan Bacon}; 35dc51e206SEvan Baconexport type BundleOutput = { 36dc51e206SEvan Bacon code: string; 37e330c216SEvan Bacon map?: string; 38dc51e206SEvan Bacon hermesBytecodeBundle?: Uint8Array; 39dc51e206SEvan Bacon hermesSourcemap?: string; 409b2597baSEvan Bacon css: CSSAsset[]; 41dc51e206SEvan Bacon assets: readonly BundleAssetWithFileHashes[]; 42dc51e206SEvan Bacon}; 43dc51e206SEvan Bacon 44dc51e206SEvan Baconlet nextBuildID = 0; 45dc51e206SEvan Bacon 4637d1352bSEvan Baconasync function assertEngineMismatchAsync(projectRoot: string, exp: ExpoConfig, platform: Platform) { 4737d1352bSEvan Bacon const isHermesManaged = isEnableHermesManaged(exp, platform); 4837d1352bSEvan Bacon 4937d1352bSEvan Bacon const paths = getConfigFilePaths(projectRoot); 5037d1352bSEvan Bacon const configFilePath = paths.dynamicConfigPath ?? paths.staticConfigPath ?? 'app.json'; 5137d1352bSEvan Bacon await maybeThrowFromInconsistentEngineAsync( 5237d1352bSEvan Bacon projectRoot, 5337d1352bSEvan Bacon configFilePath, 5437d1352bSEvan Bacon platform, 5537d1352bSEvan Bacon isHermesManaged 5637d1352bSEvan Bacon ); 5737d1352bSEvan Bacon} 5837d1352bSEvan Bacon 59dc51e206SEvan Baconexport async function bundleAsync( 60dc51e206SEvan Bacon projectRoot: string, 61dc51e206SEvan Bacon expoConfig: ExpoConfig, 62dc51e206SEvan Bacon options: MetroDevServerOptions, 63dc51e206SEvan Bacon bundles: BundleOptions[] 64dc51e206SEvan Bacon): Promise<BundleOutput[]> { 6537d1352bSEvan Bacon // Assert early so the user doesn't have to wait until bundling is complete to find out that 6637d1352bSEvan Bacon // Hermes won't be available. 6737d1352bSEvan Bacon await Promise.all( 6837d1352bSEvan Bacon bundles.map(({ platform }) => assertEngineMismatchAsync(projectRoot, expoConfig, platform)) 6937d1352bSEvan Bacon ); 7037d1352bSEvan Bacon 71dc51e206SEvan Bacon const metro = importMetroFromProject(projectRoot); 72dc51e206SEvan Bacon const Server = importMetroServerFromProject(projectRoot); 73dc51e206SEvan Bacon 74b6b91c50SEvan Bacon const { config, reporter } = await loadMetroConfigAsync(projectRoot, options, { 75b6b91c50SEvan Bacon exp: expoConfig, 76429dc7fcSEvan Bacon isExporting: true, 77b6b91c50SEvan Bacon }); 78d42dd5d4SCedric van Putten 79dc51e206SEvan Bacon const metroServer = await metro.runMetro(config, { 80dc51e206SEvan Bacon watch: false, 81dc51e206SEvan Bacon }); 82dc51e206SEvan Bacon 83dc51e206SEvan Bacon const buildAsync = async (bundle: BundleOptions): Promise<BundleOutput> => { 8437d1352bSEvan Bacon const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; 8562f156c1SKudo Chien const isHermes = isEnableHermesManaged(expoConfig, bundle.platform); 86da5824c9SKudo Chien const bundleOptions: MetroBundleOptions = { 87dc51e206SEvan Bacon ...Server.DEFAULT_BUNDLE_OPTIONS, 88dc51e206SEvan Bacon bundleType: 'bundle', 89dc51e206SEvan Bacon platform: bundle.platform, 90dc51e206SEvan Bacon entryFile: bundle.entryPoint, 91dc51e206SEvan Bacon dev: bundle.dev ?? false, 9262f156c1SKudo Chien minify: !isHermes && (bundle.minify ?? !bundle.dev), 93dc51e206SEvan Bacon inlineSourceMap: false, 94dc51e206SEvan Bacon sourceMapUrl: bundle.sourceMapUrl, 95dc51e206SEvan Bacon createModuleIdFactory: config.serializer.createModuleIdFactory, 96dc51e206SEvan Bacon onProgress: (transformedFileCount: number, totalFileCount: number) => { 97dc51e206SEvan Bacon if (!options.quiet) { 98b6b91c50SEvan Bacon reporter.update({ 99dc51e206SEvan Bacon buildID, 100dc51e206SEvan Bacon type: 'bundle_transform_progressed', 101dc51e206SEvan Bacon transformedFileCount, 102dc51e206SEvan Bacon totalFileCount, 103dc51e206SEvan Bacon }); 104dc51e206SEvan Bacon } 105dc51e206SEvan Bacon }, 106dc51e206SEvan Bacon }; 10737d1352bSEvan Bacon const bundleDetails = { 10837d1352bSEvan Bacon ...bundleOptions, 10937d1352bSEvan Bacon buildID, 11037d1352bSEvan Bacon }; 111b6b91c50SEvan Bacon reporter.update({ 112dc51e206SEvan Bacon buildID, 113dc51e206SEvan Bacon type: 'bundle_build_started', 11437d1352bSEvan Bacon bundleDetails, 115dc51e206SEvan Bacon }); 11637d1352bSEvan Bacon try { 117dc51e206SEvan Bacon const { code, map } = await metroServer.build(bundleOptions); 1189b2597baSEvan Bacon const [assets, css] = await Promise.all([ 119557aaacdSEvan Bacon getAssets(metroServer, bundleOptions), 1209b2597baSEvan Bacon getCssModulesFromBundler(config, metroServer.getBundler(), bundleOptions), 1219b2597baSEvan Bacon ]); 1229b2597baSEvan Bacon 123b6b91c50SEvan Bacon reporter.update({ 124dc51e206SEvan Bacon buildID, 125dc51e206SEvan Bacon type: 'bundle_build_done', 126dc51e206SEvan Bacon }); 1279b2597baSEvan Bacon return { code, map, assets: assets as readonly BundleAssetWithFileHashes[], css }; 12837d1352bSEvan Bacon } catch (error) { 129b6b91c50SEvan Bacon reporter.update({ 13037d1352bSEvan Bacon buildID, 13137d1352bSEvan Bacon type: 'bundle_build_failed', 13237d1352bSEvan Bacon }); 13337d1352bSEvan Bacon 13437d1352bSEvan Bacon throw error; 13537d1352bSEvan Bacon } 136dc51e206SEvan Bacon }; 137dc51e206SEvan Bacon 138dc51e206SEvan Bacon const maybeAddHermesBundleAsync = async ( 139dc51e206SEvan Bacon bundle: BundleOptions, 140dc51e206SEvan Bacon bundleOutput: BundleOutput 141dc51e206SEvan Bacon ): Promise<BundleOutput> => { 142dc51e206SEvan Bacon const { platform } = bundle; 143dc51e206SEvan Bacon const isHermesManaged = isEnableHermesManaged(expoConfig, platform); 144dc51e206SEvan Bacon if (isHermesManaged) { 145dc51e206SEvan Bacon const platformTag = chalk.bold( 146dc51e206SEvan Bacon { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform 147dc51e206SEvan Bacon ); 14837d1352bSEvan Bacon 149b6b91c50SEvan Bacon reporter.terminal.log(`${platformTag} Building Hermes bytecode for the bundle`); 15037d1352bSEvan Bacon 151dc51e206SEvan Bacon const hermesBundleOutput = await buildHermesBundleAsync( 152dc51e206SEvan Bacon projectRoot, 153dc51e206SEvan Bacon bundleOutput.code, 154e330c216SEvan Bacon bundleOutput.map!, 15562f156c1SKudo Chien bundle.minify ?? !bundle.dev 156dc51e206SEvan Bacon ); 157dc51e206SEvan Bacon bundleOutput.hermesBytecodeBundle = hermesBundleOutput.hbc; 158dc51e206SEvan Bacon bundleOutput.hermesSourcemap = hermesBundleOutput.sourcemap; 159dc51e206SEvan Bacon } 160dc51e206SEvan Bacon return bundleOutput; 161dc51e206SEvan Bacon }; 162dc51e206SEvan Bacon 163dc51e206SEvan Bacon try { 164dc51e206SEvan Bacon const intermediateOutputs = await Promise.all(bundles.map((bundle) => buildAsync(bundle))); 165dc51e206SEvan Bacon const bundleOutputs: BundleOutput[] = []; 166dc51e206SEvan Bacon for (let i = 0; i < bundles.length; ++i) { 167dc51e206SEvan Bacon // hermesc does not support parallel building even we spawn processes. 168dc51e206SEvan Bacon // we should build them sequentially. 169dc51e206SEvan Bacon bundleOutputs.push(await maybeAddHermesBundleAsync(bundles[i], intermediateOutputs[i])); 170dc51e206SEvan Bacon } 171dc51e206SEvan Bacon return bundleOutputs; 17237d1352bSEvan Bacon } catch (error) { 17337d1352bSEvan Bacon // New line so errors don't show up inline with the progress bar 17437d1352bSEvan Bacon console.log(''); 17537d1352bSEvan Bacon throw error; 176dc51e206SEvan Bacon } finally { 177dc51e206SEvan Bacon metroServer.end(); 178dc51e206SEvan Bacon } 179dc51e206SEvan Bacon} 180557aaacdSEvan Bacon 181557aaacdSEvan Bacon// Forked out of Metro because the `this._getServerRootDir()` doesn't match the development 182557aaacdSEvan Bacon// behavior. 183fd2402c1SEvan Baconexport async function getAssets( 184557aaacdSEvan Bacon metro: Metro.Server, 185557aaacdSEvan Bacon options: MetroBundleOptions 186557aaacdSEvan Bacon): Promise<readonly AssetData[]> { 187557aaacdSEvan Bacon const { entryFile, onProgress, resolverOptions, transformOptions } = splitBundleOptions(options); 188557aaacdSEvan Bacon 189557aaacdSEvan Bacon // @ts-expect-error: _bundler isn't exposed on the type. 190557aaacdSEvan Bacon const dependencies = await metro._bundler.getDependencies( 191557aaacdSEvan Bacon [entryFile], 192557aaacdSEvan Bacon transformOptions, 193557aaacdSEvan Bacon resolverOptions, 194557aaacdSEvan Bacon { onProgress, shallow: false, lazy: false } 195557aaacdSEvan Bacon ); 196557aaacdSEvan Bacon 197557aaacdSEvan Bacon // @ts-expect-error 198557aaacdSEvan Bacon const _config = metro._config as ConfigT; 199557aaacdSEvan Bacon 200557aaacdSEvan Bacon return await getMetroAssets(dependencies, { 201557aaacdSEvan Bacon processModuleFilter: _config.serializer.processModuleFilter, 202557aaacdSEvan Bacon assetPlugins: _config.transformer.assetPlugins, 2031f98cdd7SEvan Bacon platform: transformOptions.platform!, 204557aaacdSEvan Bacon projectRoot: _config.projectRoot, // this._getServerRootDir(), 205557aaacdSEvan Bacon publicPath: _config.transformer.publicPath, 206557aaacdSEvan Bacon }); 207557aaacdSEvan Bacon} 208