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