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