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