1import { ExpoAppManifest } from '@expo/config'; 2import { ModPlatform } from '@expo/config-plugins'; 3import fs from 'fs'; 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 { BundleOutput } from './fork-bundleAsync'; 11import { Asset, saveAssetsAsync } from './saveAssets'; 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( 21 projectRoot: string, 22 exp: Pick<ExpoAppManifest, 'bundledAssets' | 'assetBundlePatterns'>, 23 assets: Asset[] 24) { 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.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: ExpoAppManifest; 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}: { 112 bundles: Partial<Record<ModPlatform, BundleOutput>>; 113 outputDir: string; 114}) { 115 const assets = uniqBy( 116 Object.values(bundles).flatMap((bundle) => bundle!.css), 117 (asset) => asset.filename 118 ); 119 120 const cssDirectory = assets[0]?.filename; 121 if (!cssDirectory) return []; 122 123 await fs.promises.mkdir(path.join(outputDir, path.dirname(cssDirectory)), { recursive: true }); 124 125 await Promise.all( 126 assets.map((v) => fs.promises.writeFile(path.join(outputDir, v.filename), v.source)) 127 ); 128 129 return assets.map((v) => '/' + v.filename); 130} 131