1import { ExpoAppManifest } from '@expo/config';
2import { ModPlatform } from '@expo/config-plugins';
3import { BundleOutput } from '@expo/dev-server';
4import minimatch from 'minimatch';
5import path from 'path';
6
7import * as Log from '../log';
8import { resolveGoogleServicesFile } from '../start/server/middleware/resolveAssets';
9import { uniqBy } from '../utils/array';
10import { Asset, saveAssetsAsync } from './saveAssets';
11
12const debug = require('debug')('expo:export:exportAssets') as typeof console.log;
13
14/**
15 * Resolves the assetBundlePatterns from the manifest and returns a list of assets to bundle.
16 *
17 * @modifies {exp}
18 */
19export async function resolveAssetBundlePatternsAsync(
20  projectRoot: string,
21  exp: Pick<ExpoAppManifest, 'bundledAssets' | 'assetBundlePatterns'>,
22  assets: Asset[]
23) {
24  if (!exp.assetBundlePatterns?.length || !assets.length) {
25    delete exp.assetBundlePatterns;
26    return exp;
27  }
28  // Convert asset patterns to a list of asset strings that match them.
29  // Assets strings are formatted as `asset_<hash>.<type>` and represent
30  // the name that the file will have in the app bundle. The `asset_` prefix is
31  // needed because android doesn't support assets that start with numbers.
32
33  const fullPatterns: string[] = exp.assetBundlePatterns.map((p: string) =>
34    path.join(projectRoot, p)
35  );
36
37  logPatterns(fullPatterns);
38
39  const allBundledAssets = assets
40    .map((asset) => {
41      const shouldBundle = shouldBundleAsset(asset, fullPatterns);
42      if (shouldBundle) {
43        debug(`${shouldBundle ? 'Include' : 'Exclude'} asset ${asset.files?.[0]}`);
44        return asset.fileHashes.map(
45          (hash) => 'asset_' + hash + ('type' in asset && asset.type ? '.' + asset.type : '')
46        );
47      }
48      return [];
49    })
50    .flat();
51
52  // The assets returned by the RN packager has duplicates so make sure we
53  // only bundle each once.
54  exp.bundledAssets = [...new Set(allBundledAssets)];
55  delete exp.assetBundlePatterns;
56
57  return exp;
58}
59
60function logPatterns(patterns: string[]) {
61  // Only log the patterns in debug mode, if they aren't already defined in the app.json, then all files will be targeted.
62  Log.log('\nProcessing asset bundle patterns:');
63  patterns.forEach((p) => Log.log('- ' + p));
64}
65
66function shouldBundleAsset(asset: Asset, patterns: string[]) {
67  const file = asset.files?.[0];
68  return !!(
69    '__packager_asset' in asset &&
70    asset.__packager_asset &&
71    file &&
72    patterns.some((pattern) => minimatch(file, pattern))
73  );
74}
75
76export async function exportAssetsAsync(
77  projectRoot: string,
78  {
79    exp,
80    outputDir,
81    bundles,
82  }: {
83    exp: ExpoAppManifest;
84    bundles: Partial<Record<ModPlatform, BundleOutput>>;
85    outputDir: string;
86  }
87) {
88  const assets: Asset[] = uniqBy(
89    Object.values(bundles).flatMap((bundle) => bundle!.assets),
90    (asset) => asset.hash
91  );
92
93  if (assets[0]?.fileHashes) {
94    Log.log('Saving assets');
95    await saveAssetsAsync(projectRoot, { assets, outputDir });
96  }
97
98  // Add google services file if it exists
99  await resolveGoogleServicesFile(projectRoot, exp);
100
101  // Updates the manifest to reflect additional asset bundling + configs
102  await resolveAssetBundlePatternsAsync(projectRoot, exp, assets);
103
104  return { exp, assets };
105}
106