import { ExpoConfig, getConfigFilePaths, Platform } from '@expo/config'; import type { LoadOptions } from '@expo/metro-config'; import chalk from 'chalk'; import Metro, { AssetData } from 'metro'; import getMetroAssets from 'metro/src/DeltaBundler/Serializers/getAssets'; import splitBundleOptions from 'metro/src/lib/splitBundleOptions'; import type { BundleOptions as MetroBundleOptions } from 'metro/src/shared/types'; import { ConfigT } from 'metro-config'; import { buildHermesBundleAsync, isEnableHermesManaged, maybeThrowFromInconsistentEngineAsync, } from './exportHermes'; import { CSSAsset, getCssModulesFromBundler } from '../start/server/metro/getCssModulesFromBundler'; import { loadMetroConfigAsync } from '../start/server/metro/instantiateMetro'; import { importMetroFromProject, importMetroServerFromProject, } from '../start/server/metro/resolveFromProject'; export type MetroDevServerOptions = LoadOptions & { quiet?: boolean; }; export type BundleOptions = { entryPoint: string; platform: 'android' | 'ios' | 'web'; dev?: boolean; minify?: boolean; sourceMapUrl?: string; }; export type BundleAssetWithFileHashes = Metro.AssetData & { fileHashes: string[]; // added by the hashAssets asset plugin }; export type BundleOutput = { code: string; map?: string; hermesBytecodeBundle?: Uint8Array; hermesSourcemap?: string; css: CSSAsset[]; assets: readonly BundleAssetWithFileHashes[]; }; let nextBuildID = 0; async function assertEngineMismatchAsync(projectRoot: string, exp: ExpoConfig, platform: Platform) { const isHermesManaged = isEnableHermesManaged(exp, platform); const paths = getConfigFilePaths(projectRoot); const configFilePath = paths.dynamicConfigPath ?? paths.staticConfigPath ?? 'app.json'; await maybeThrowFromInconsistentEngineAsync( projectRoot, configFilePath, platform, isHermesManaged ); } export async function bundleAsync( projectRoot: string, expoConfig: ExpoConfig, options: MetroDevServerOptions, bundles: BundleOptions[] ): Promise { // Assert early so the user doesn't have to wait until bundling is complete to find out that // Hermes won't be available. await Promise.all( bundles.map(({ platform }) => assertEngineMismatchAsync(projectRoot, expoConfig, platform)) ); const metro = importMetroFromProject(projectRoot); const Server = importMetroServerFromProject(projectRoot); const { config, reporter } = await loadMetroConfigAsync(projectRoot, options, { exp: expoConfig, isExporting: true, }); const metroServer = await metro.runMetro(config, { watch: false, }); const buildAsync = async (bundle: BundleOptions): Promise => { const buildID = `bundle_${nextBuildID++}_${bundle.platform}`; const isHermes = isEnableHermesManaged(expoConfig, bundle.platform); const bundleOptions: MetroBundleOptions = { ...Server.DEFAULT_BUNDLE_OPTIONS, bundleType: 'bundle', platform: bundle.platform, entryFile: bundle.entryPoint, dev: bundle.dev ?? false, minify: !isHermes && (bundle.minify ?? !bundle.dev), inlineSourceMap: false, sourceMapUrl: bundle.sourceMapUrl, createModuleIdFactory: config.serializer.createModuleIdFactory, onProgress: (transformedFileCount: number, totalFileCount: number) => { if (!options.quiet) { reporter.update({ buildID, type: 'bundle_transform_progressed', transformedFileCount, totalFileCount, }); } }, }; const bundleDetails = { ...bundleOptions, buildID, }; reporter.update({ buildID, type: 'bundle_build_started', bundleDetails, }); try { const { code, map } = await metroServer.build(bundleOptions); const [assets, css] = await Promise.all([ getAssets(metroServer, bundleOptions), getCssModulesFromBundler(config, metroServer.getBundler(), bundleOptions), ]); reporter.update({ buildID, type: 'bundle_build_done', }); return { code, map, assets: assets as readonly BundleAssetWithFileHashes[], css }; } catch (error) { reporter.update({ buildID, type: 'bundle_build_failed', }); throw error; } }; const maybeAddHermesBundleAsync = async ( bundle: BundleOptions, bundleOutput: BundleOutput ): Promise => { const { platform } = bundle; const isHermesManaged = isEnableHermesManaged(expoConfig, platform); if (isHermesManaged) { const platformTag = chalk.bold( { ios: 'iOS', android: 'Android', web: 'Web' }[platform] || platform ); reporter.terminal.log(`${platformTag} Building Hermes bytecode for the bundle`); const hermesBundleOutput = await buildHermesBundleAsync( projectRoot, bundleOutput.code, bundleOutput.map!, bundle.minify ?? !bundle.dev ); bundleOutput.hermesBytecodeBundle = hermesBundleOutput.hbc; bundleOutput.hermesSourcemap = hermesBundleOutput.sourcemap; } return bundleOutput; }; try { const intermediateOutputs = await Promise.all(bundles.map((bundle) => buildAsync(bundle))); const bundleOutputs: BundleOutput[] = []; for (let i = 0; i < bundles.length; ++i) { // hermesc does not support parallel building even we spawn processes. // we should build them sequentially. bundleOutputs.push(await maybeAddHermesBundleAsync(bundles[i], intermediateOutputs[i])); } return bundleOutputs; } catch (error) { // New line so errors don't show up inline with the progress bar console.log(''); throw error; } finally { metroServer.end(); } } // Forked out of Metro because the `this._getServerRootDir()` doesn't match the development // behavior. export async function getAssets( metro: Metro.Server, options: MetroBundleOptions ): Promise { const { entryFile, onProgress, resolverOptions, transformOptions } = splitBundleOptions(options); // @ts-expect-error: _bundler isn't exposed on the type. const dependencies = await metro._bundler.getDependencies( [entryFile], transformOptions, resolverOptions, { onProgress, shallow: false, lazy: false } ); // @ts-expect-error const _config = metro._config as ConfigT; return await getMetroAssets(dependencies, { processModuleFilter: _config.serializer.processModuleFilter, assetPlugins: _config.transformer.assetPlugins, platform: transformOptions.platform!, projectRoot: _config.projectRoot, // this._getServerRootDir(), publicPath: _config.transformer.publicPath, }); }