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