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