19ba03fb0SWill Schurmanimport { ExpoConfig } from '@expo/config';
2dc51e206SEvan Baconimport { ModPlatform } from '@expo/config-plugins';
39b2597baSEvan Baconimport fs from 'fs';
4dc51e206SEvan Baconimport minimatch from 'minimatch';
5dc51e206SEvan Baconimport path from 'path';
6dc51e206SEvan Bacon
78a424bebSJames Ideimport { BundleOutput } from './fork-bundleAsync';
88a424bebSJames Ideimport { Asset, saveAssetsAsync } from './saveAssets';
9dc51e206SEvan Baconimport * as Log from '../log';
10dc51e206SEvan Baconimport { resolveGoogleServicesFile } from '../start/server/middleware/resolveAssets';
11dc51e206SEvan Baconimport { uniqBy } from '../utils/array';
12dc51e206SEvan Bacon
13474a7a4bSEvan Baconconst debug = require('debug')('expo:export:exportAssets') as typeof console.log;
14474a7a4bSEvan Bacon
15dc51e206SEvan Bacon/**
16dc51e206SEvan Bacon * Resolves the assetBundlePatterns from the manifest and returns a list of assets to bundle.
17dc51e206SEvan Bacon *
18dc51e206SEvan Bacon * @modifies {exp}
19dc51e206SEvan Bacon */
209ba03fb0SWill Schurmanexport async function resolveAssetBundlePatternsAsync<T extends ExpoConfig>(
21dc51e206SEvan Bacon  projectRoot: string,
229ba03fb0SWill Schurman  exp: T,
23dc51e206SEvan Bacon  assets: Asset[]
249ba03fb0SWill Schurman): Promise<Omit<T, 'assetBundlePatterns'> & { bundledAssets?: string[] }> {
25dc51e206SEvan Bacon  if (!exp.assetBundlePatterns?.length || !assets.length) {
26dc51e206SEvan Bacon    delete exp.assetBundlePatterns;
27dc51e206SEvan Bacon    return exp;
28dc51e206SEvan Bacon  }
29dc51e206SEvan Bacon  // Convert asset patterns to a list of asset strings that match them.
30dc51e206SEvan Bacon  // Assets strings are formatted as `asset_<hash>.<type>` and represent
31dc51e206SEvan Bacon  // the name that the file will have in the app bundle. The `asset_` prefix is
32dc51e206SEvan Bacon  // needed because android doesn't support assets that start with numbers.
33dc51e206SEvan Bacon
34dc51e206SEvan Bacon  const fullPatterns: string[] = exp.assetBundlePatterns.map((p: string) =>
35dc51e206SEvan Bacon    path.join(projectRoot, p)
36dc51e206SEvan Bacon  );
37dc51e206SEvan Bacon
38dc51e206SEvan Bacon  logPatterns(fullPatterns);
39dc51e206SEvan Bacon
40dc51e206SEvan Bacon  const allBundledAssets = assets
41dc51e206SEvan Bacon    .map((asset) => {
42dc51e206SEvan Bacon      const shouldBundle = shouldBundleAsset(asset, fullPatterns);
43dc51e206SEvan Bacon      if (shouldBundle) {
44474a7a4bSEvan Bacon        debug(`${shouldBundle ? 'Include' : 'Exclude'} asset ${asset.files?.[0]}`);
45dc51e206SEvan Bacon        return asset.fileHashes.map(
46dc51e206SEvan Bacon          (hash) => 'asset_' + hash + ('type' in asset && asset.type ? '.' + asset.type : '')
47dc51e206SEvan Bacon        );
48dc51e206SEvan Bacon      }
49dc51e206SEvan Bacon      return [];
50dc51e206SEvan Bacon    })
51dc51e206SEvan Bacon    .flat();
52dc51e206SEvan Bacon
53dc51e206SEvan Bacon  // The assets returned by the RN packager has duplicates so make sure we
54dc51e206SEvan Bacon  // only bundle each once.
559ba03fb0SWill Schurman  (exp as any).bundledAssets = [...new Set(allBundledAssets)];
56dc51e206SEvan Bacon  delete exp.assetBundlePatterns;
57dc51e206SEvan Bacon
58dc51e206SEvan Bacon  return exp;
59dc51e206SEvan Bacon}
60dc51e206SEvan Bacon
61dc51e206SEvan Baconfunction logPatterns(patterns: string[]) {
62dc51e206SEvan Bacon  // Only log the patterns in debug mode, if they aren't already defined in the app.json, then all files will be targeted.
63dc51e206SEvan Bacon  Log.log('\nProcessing asset bundle patterns:');
64dc51e206SEvan Bacon  patterns.forEach((p) => Log.log('- ' + p));
65dc51e206SEvan Bacon}
66dc51e206SEvan Bacon
67dc51e206SEvan Baconfunction shouldBundleAsset(asset: Asset, patterns: string[]) {
68dc51e206SEvan Bacon  const file = asset.files?.[0];
69dc51e206SEvan Bacon  return !!(
70dc51e206SEvan Bacon    '__packager_asset' in asset &&
71dc51e206SEvan Bacon    asset.__packager_asset &&
72dc51e206SEvan Bacon    file &&
73dc51e206SEvan Bacon    patterns.some((pattern) => minimatch(file, pattern))
74dc51e206SEvan Bacon  );
75dc51e206SEvan Bacon}
76dc51e206SEvan Bacon
77dc51e206SEvan Baconexport async function exportAssetsAsync(
78dc51e206SEvan Bacon  projectRoot: string,
79dc51e206SEvan Bacon  {
80dc51e206SEvan Bacon    exp,
81dc51e206SEvan Bacon    outputDir,
82dc51e206SEvan Bacon    bundles,
83dc51e206SEvan Bacon  }: {
849ba03fb0SWill Schurman    exp: ExpoConfig;
85dc51e206SEvan Bacon    bundles: Partial<Record<ModPlatform, BundleOutput>>;
86dc51e206SEvan Bacon    outputDir: string;
87dc51e206SEvan Bacon  }
88dc51e206SEvan Bacon) {
89dc51e206SEvan Bacon  const assets: Asset[] = uniqBy(
90dc51e206SEvan Bacon    Object.values(bundles).flatMap((bundle) => bundle!.assets),
91dc51e206SEvan Bacon    (asset) => asset.hash
92dc51e206SEvan Bacon  );
93dc51e206SEvan Bacon
94dc51e206SEvan Bacon  if (assets[0]?.fileHashes) {
95dc51e206SEvan Bacon    Log.log('Saving assets');
96dc51e206SEvan Bacon    await saveAssetsAsync(projectRoot, { assets, outputDir });
97dc51e206SEvan Bacon  }
98dc51e206SEvan Bacon
99dc51e206SEvan Bacon  // Add google services file if it exists
100dc51e206SEvan Bacon  await resolveGoogleServicesFile(projectRoot, exp);
101dc51e206SEvan Bacon
102dc51e206SEvan Bacon  // Updates the manifest to reflect additional asset bundling + configs
103dc51e206SEvan Bacon  await resolveAssetBundlePatternsAsync(projectRoot, exp, assets);
104dc51e206SEvan Bacon
105dc51e206SEvan Bacon  return { exp, assets };
106dc51e206SEvan Bacon}
1079b2597baSEvan Bacon
1089b2597baSEvan Baconexport async function exportCssAssetsAsync({
1099b2597baSEvan Bacon  outputDir,
1109b2597baSEvan Bacon  bundles,
111*7c98c357SEvan Bacon  basePath,
1129b2597baSEvan Bacon}: {
1139b2597baSEvan Bacon  bundles: Partial<Record<ModPlatform, BundleOutput>>;
1149b2597baSEvan Bacon  outputDir: string;
115*7c98c357SEvan Bacon  basePath: string;
1169b2597baSEvan Bacon}) {
1179b2597baSEvan Bacon  const assets = uniqBy(
1189b2597baSEvan Bacon    Object.values(bundles).flatMap((bundle) => bundle!.css),
1199b2597baSEvan Bacon    (asset) => asset.filename
1209b2597baSEvan Bacon  );
1219b2597baSEvan Bacon
1229b2597baSEvan Bacon  const cssDirectory = assets[0]?.filename;
1239b2597baSEvan Bacon  if (!cssDirectory) return [];
1249b2597baSEvan Bacon
1259b2597baSEvan Bacon  await fs.promises.mkdir(path.join(outputDir, path.dirname(cssDirectory)), { recursive: true });
1269b2597baSEvan Bacon
1279b2597baSEvan Bacon  await Promise.all(
1289b2597baSEvan Bacon    assets.map((v) => fs.promises.writeFile(path.join(outputDir, v.filename), v.source))
1299b2597baSEvan Bacon  );
1309b2597baSEvan Bacon
131*7c98c357SEvan Bacon  return assets.map((v) => basePath + '/' + v.filename);
1329b2597baSEvan Bacon}
133