import { ExpoAppManifest , ExpoConfig } from '@expo/config' ; import { BundleAssetWithFileHashes , BundleOutput } from '@expo/dev-server' ; import assert from 'assert' ; import FormData from 'form-data' ; import fs from 'fs-extra' ; import chunk from 'lodash/chunk' ; import get from 'lodash/get' ; import set from 'lodash/set' ; import uniqBy from 'lodash/uniqBy' ; import md5hex from 'md5hex' ; import minimatch from 'minimatch' ; import path from 'path' ; import urljoin from 'url-join' ; import { ApiV2 , ExpSchema , Logger as logger , ProjectUtils , UserManager } from './internal' ; const EXPO_CDN = 'https://classic-assets.eascdn.net' ; type ManifestAsset = { fileHashes : string [ ] ; files : string [ ] ; hash : string ; } ; export type Asset = ManifestAsset | BundleAssetWithFileHashes ; type ManifestResolutionError = Error & { localAssetPath ? : string ; manifestField ? : string ; } ; type BundlesByPlatform = { android ? : BundleOutput ; ios ? : BundleOutput ; } ; type ExportAssetsOptions = { projectRoot : string ; exp : ExpoAppManifest ; hostedUrl : string ; assetPath : string ; bundles : BundlesByPlatform ; outputDir ? : string ; experimentalBundle ? : boolean ; } ; export async function resolveGoogleServicesFile ( projectRoot , manifest ) { if ( manifest . android ?. googleServicesFile ) { const contents = await fs . readFile ( path . resolve ( projectRoot , manifest . android . googleServicesFile ) , 'utf8' ) ; manifest . android . googleServicesFile = contents ; } if ( manifest . ios ?. googleServicesFile ) { const contents = await fs . readFile ( path . resolve ( projectRoot , manifest . ios . googleServicesFile ) , 'base64' ) ; manifest . ios . googleServicesFile = contents ; } } param manifest returns async function getAssetFieldPathsForManifestAsync ( manifest ) { const sdkAssetFieldPaths = await ExpSchema . getAssetSchemasAsync ( manifest . sdkVersion ) ; return sdkAssetFieldPaths . filter ( assetSchema => get ( manifest , assetSchema ) ) ; } export async function resolveManifestAssets ( { projectRoot , manifest , resolver , strict = false , } ) { try { const assetSchemas = await getAssetFieldPathsForManifestAsync ( manifest ) ; const urls = await Promise . all ( assetSchemas . map ( async ( manifestField ) => { const pathOrURL = get ( manifest , manifestField ) ; if ( /^https?:\/\// . test ( pathOrURL ) ) { return pathOrURL ; } else if ( fs . existsSync ( path . resolve ( projectRoot , pathOrURL ) ) ) { return await resolver ( pathOrURL ) ; } else { const err = new Error ( 'Could not resolve local asset.' ) ; err . localAssetPath = pathOrURL ; err . manifestField = manifestField ; throw err ; } } ) ) ; assetSchemas . forEach ( ( manifestField , index ) => set ( manifest , `${ manifestField }Url` , urls [ index ] ) ) ; } catch ( e ) { let logMethod = ProjectUtils . logWarning ; if ( strict ) { logMethod = ProjectUtils . logError ; } if ( e . localAssetPath ) { logMethod ( projectRoot , 'expo' , `Unable to resolve asset "${ e . localAssetPath }" from "${ e . manifestField }" in your app.json or app.config.js` ) ; } else { logMethod ( projectRoot , 'expo' , `Warning: Unable to resolve manifest assets. Icons might not work. ${ e . message }.` ) ; } if ( strict ) { throw new Error ( 'Resolving assets failed.' ) ; } } } modifies async function _configureExpForAssets ( projectRoot , exp , assets ) { await resolveGoogleServicesFile ( projectRoot , exp ) ; if ( exp . assetBundlePatterns ) { const fullPatterns = exp . assetBundlePatterns . map ( ( p ) => path . join ( projectRoot , p ) ) ; logger . global . info ( '\nProcessing asset bundle patterns:' ) ; fullPatterns . forEach ( p => logger . global . info ( '- ' + p ) ) ; const bundledAssets = new Set < string > ( ) ; for ( const asset of assets ) { const file = asset . files && asset . files [ 0 ] ; const shouldBundle = '__packager_asset' in asset && asset . __packager_asset && file && fullPatterns . some ( ( p ) => minimatch ( file , p ) ) ; ProjectUtils . logDebug ( projectRoot , 'expo' , `${ shouldBundle ? 'Include' : 'Exclude' } asset ${ file }` ) ; if ( shouldBundle ) { asset . fileHashes . forEach ( hash => bundledAssets . add ( 'asset_' + hash + ( 'type' in asset && asset . type ? '.' + asset . type : '' ) ) ) ; } } exp . bundledAssets = [ ... bundledAssets ] ; delete exp . assetBundlePatterns ; } return exp ; } export async function publishAssetsAsync ( options ) { return exportAssetsAsync ( { ... options , hostedUrl : EXPO_CDN , assetPath : '~assets' } ) ; } export async function exportAssetsAsync ( { projectRoot , exp , hostedUrl , assetPath , outputDir , bundles , experimentalBundle , } ) { logger . global . info ( 'Analyzing assets' ) ; let assets ; if ( experimentalBundle ) { assert ( outputDir , 'outputDir must be specified when exporting to EAS' ) ; assets = uniqBy ( Object . values ( bundles ) . flatMap ( bundle => bundle ! . assets ) , asset => asset . hash ) ; } else { const assetCdnPath = urljoin ( hostedUrl , assetPath ) ; assets = await collectAssets ( projectRoot , exp , assetCdnPath , bundles ) ; } logger . global . info ( 'Saving assets' ) ; if ( assets . length > 0 && assets [ 0 ] . fileHashes ) { if ( outputDir ) { await saveAssetsAsync ( projectRoot , assets , outputDir ) ; } else { await uploadAssetsAsync ( projectRoot , assets ) ; } } else { logger . global . info ( { quiet : true } , 'No assets to upload, skipped.' ) ; } await _configureExpForAssets ( projectRoot , exp , assets ) ; return { exp , assets } ; } param paths async function fetchMissingAssetsAsync ( paths ) { const user = await UserManager . ensureLoggedInAsync ( ) ; const api = ApiV2 . clientForUser ( user ) ; const result = await api . postAsync ( 'assets/metadata' , { keys : paths } ) ; const metas = result . metadata ; const missing = paths . filter ( key => ! metas [ key ] . exists ) ; return missing ; } function logAssetTask ( projectRoot , action , pathName ) { ProjectUtils . logDebug ( projectRoot , 'expo' , `${ action } ${ pathName }` ) ; const relativePath = pathName . replace ( projectRoot , '' ) ; logger . global . info ( { quiet : true } , `${ action } ${ relativePath }` ) ; } async function uploadAssetsAsync ( projectRoot , assets ) { const paths = collectAssetPaths ( assets ) ; const missing = await fetchMissingAssetsAsync ( Object . keys ( paths ) ) ; if ( missing . length === 0 ) { logger . global . info ( { quiet : true } , `No assets changed, skipped.` ) ; return ; } const keyChunks = chunk ( missing , 5 ) ; for ( const keys of keyChunks ) { const formData = new FormData ( ) ; for ( const key of keys ) { const pathName = paths [ key ] ; logAssetTask ( projectRoot , 'uploading' , pathName ) ; formData . append ( key , fs . createReadStream ( pathName ) , pathName ) ; } const user = await UserManager . ensureLoggedInAsync ( ) ; const api = ApiV2 . clientForUser ( user ) ; await api . uploadFormDataAsync ( 'assets/upload' , formData ) ; } } function collectAssetPaths ( assets ) { const paths = { } ; assets . forEach ( asset => { asset . files . forEach ( ( path , index ) => { paths [ asset . fileHashes [ index ] ] = path ; } ) ; } ) ; return paths ; } async function saveAssetsAsync ( projectRoot , assets , outputDir ) { const paths = collectAssetPaths ( assets ) ; const keyChunks = chunk ( Object . keys ( paths ) , 5 ) ; for ( const keys of keyChunks ) { const promises = [ ] ; for ( const key of keys ) { const pathName = paths [ key ] ; logAssetTask ( projectRoot , 'saving' , pathName ) ; const assetPath = path . resolve ( outputDir , 'assets' , key ) ; promises . push ( fs . copy ( pathName , assetPath ) ) ; } await Promise . all ( promises ) ; } logger . global . info ( 'Files successfully saved.' ) ; } param { string } hostedAssetPrefix modifies async function collectAssets ( projectRoot , exp , hostedAssetPrefix , bundles ) { const manifestAssets = [ ] ; await resolveManifestAssets ( { projectRoot , manifest : exp , async resolver ( assetPath ) { const absolutePath = path . resolve ( projectRoot , assetPath ) ; const contents = await fs . readFile ( absolutePath ) ; const hash = md5hex ( contents ) ; manifestAssets . push ( { files : [ absolutePath ] , fileHashes : [ hash ] , hash } ) ; return urljoin ( hostedAssetPrefix , hash ) ; } , strict : true } ) ; return [ ... Object . values ( bundles ) . flatMap ( bundle => bundle ! . assets ) , ... manifestAssets ] ; }