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