1import { ExpoConfig } from '@expo/config';
2import { ModPlatform } from '@expo/config-plugins';
3import fs from 'fs';
4import minimatch from 'minimatch';
5import path from 'path';
6
7import { BundleOutput } from './fork-bundleAsync';
8import { Asset, saveAssetsAsync } from './saveAssets';
9import * as Log from '../log';
10import { resolveGoogleServicesFile } from '../start/server/middleware/resolveAssets';
11import { uniqBy } from '../utils/array';
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<T extends ExpoConfig>(
21  projectRoot: string,
22  exp: T,
23  assets: Asset[]
24): Promise<Omit<T, 'assetBundlePatterns'> & { bundledAssets?: string[] }> {
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 as any).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: ExpoConfig;
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  basePath,
112}: {
113  bundles: Partial<Record<ModPlatform, BundleOutput>>;
114  outputDir: string;
115  basePath: string;
116}) {
117  const assets = uniqBy(
118    Object.values(bundles).flatMap((bundle) => bundle!.css),
119    (asset) => asset.filename
120  );
121
122  const cssDirectory = assets[0]?.filename;
123  if (!cssDirectory) return [];
124
125  await fs.promises.mkdir(path.join(outputDir, path.dirname(cssDirectory)), { recursive: true });
126
127  await Promise.all(
128    assets.map((v) => fs.promises.writeFile(path.join(outputDir, v.filename), v.source))
129  );
130
131  return assets.map((v) => basePath + '/' + v.filename);
132}
133