import { getConfig , getWebOutputPath } from '@expo/config' ; import { isAvailableAsync , sharpAsync } from '@expo/image-utils' ; import JsonFile from '@expo/json-file' ; import chalk from 'chalk' ; import crypto from 'crypto' ; import { ensureDirSync , existsSync , move , readFile , statSync , unlinkSync , writeFileSync , } from 'fs-extra' ; import { sync as globSync } from 'glob' ; import { cpus } from 'os' ; import pLimit from 'p-limit' ; import { basename , join , parse , relative } from 'path' ; import prettyBytes from 'pretty-bytes' ; import temporary from 'tempy' ; export type AssetOptimizationState = Record < string , boolean > ; async function readAssetJsonAsync ( projectRoot ) { const dirPath = join ( projectRoot , '.expo-shared' ) ; ensureDirSync ( dirPath ) ; const readmeFilePath = join ( dirPath , 'README.md' ) ; if ( ! existsSync ( readmeFilePath ) ) { writeFileSync ( readmeFilePath , `> Why do I have a folder named ".expo-shared" in my project?

The ".expo-shared" folder is created when running commands that produce state that is intended to be shared with all developers on the project. For example, "npx expo-optimize".

> What does the "assets.json" file contain?

The "assets.json" file describes the assets that have been optimized through "expo-optimize" and do not need to be processed again.

> Should I commit the ".expo-shared" folder?

Yes, you should share the ".expo-shared" folder with your collaborators.
` ) ; } const assetJson = new JsonFile < AssetOptimizationState > ( join ( dirPath , 'assets.json' ) ) ; if ( ! existsSync ( assetJson . file ) ) { console . log ( ) ; console . log ( chalk . magenta ( `\u203A Creating ${ chalk . bold ( '.expo-shared/assets.json' ) } in the project's root directory.` ) ) ; console . log ( chalk . magenta `\u203A This file is autogenerated and should not be edited directly.` ) ; console . log ( chalk . magenta `\u203A You should commit this to git so that asset state is shared between collaborators.` ) ; console . log ( ) ; await assetJson . writeAsync ( { } ) ; } const assetInfo = await assetJson . readAsync ( ) ; return { assetJson , assetInfo } ; } async function optimizeImageAsync ( inputPath , quality ) { const outputPath = temporary . directory ( ) ; await sharpAsync ( { input : inputPath , output : outputPath , quality , adaptiveFiltering : true } ) ; return join ( outputPath , basename ( inputPath ) ) ; } function createNewFilename ( imagePath ) { const { dir , name , ext } = parse ( imagePath ) ; return join ( dir , `${ name }.orig${ ext }` ) ; } async function getAssetFilesAsync ( projectRoot , options ) { const { exp } = getConfig ( projectRoot , { skipSDKVersionRequirement : true } ) ; const webOutputPath = await getWebOutputPath ( exp ) ; const { assetBundlePatterns } = exp ; const globOptions = { cwd : projectRoot , ignore : [ '**/node_modules/**' , '**/ios/**' , '**/android/**' , `**/${ webOutputPath }/**` ] } ; const allFiles = [ ] ; const patterns = assetBundlePatterns || [ '**/*' ] ; patterns . forEach ( ( pattern ) => { allFiles . push ( ... globSync ( pattern , globOptions ) ) ; } ) ; const included = options && options . include ? [ ... globSync ( options . include , globOptions ) ] : allFiles ; const toExclude = new Set ( ) ; if ( options && options . exclude ) { globSync ( options . exclude , globOptions ) . forEach ( file => toExclude . add ( file ) ) ; } const excluded = included . filter ( file => ! toExclude . has ( file ) ) ; const filtered = options && options . exclude ? excluded : included ; return { allFiles : filterImages ( allFiles , projectRoot ) , selectedFiles : filterImages ( filtered , projectRoot ) } ; } function filterImages ( files , projectRoot ) { const regex = /\.(png|jpg|jpeg)$/ ; const withDirectory = files . map ( file => `${ projectRoot }/${ file }` . replace ( '//' , '/' ) ) ; const allImages = withDirectory . filter ( file => regex . test ( file . toLowerCase ( ) ) ) ; return allImages ; } async function calculateHash ( filePath ) { const contents = await new Promise < Buffer > ( ( resolve , reject ) => readFile ( filePath , ( err , data ) => { if ( err ) { return reject ( err ) ; } resolve ( data ) ; } ) ) ; return crypto . createHash ( 'sha256' ) . update ( contents ) . digest ( 'hex' ) ; } export type OptimizationOptions = { quality ? : number ; include ? : string ; exclude ? : string ; save ? : boolean ; } ; async function pool < T , R > ( items , process ) { const parallelism = Math . max ( 1 , cpus ( ) . length - 1 ) ; const limit = pLimit ( parallelism ) ; return Promise . all ( items . map ( item => limit ( ( ) => process ( item ) ) ) ) ; } export async function isProjectOptimized ( projectRoot , options ) { if ( ! existsSync ( join ( projectRoot , '.expo-shared/assets.json' ) ) ) { return false ; } const { selectedFiles } = await getAssetFilesAsync ( projectRoot , options ) ; const { assetInfo } = await readAssetJsonAsync ( projectRoot ) ; let optimized = true ; const result = await pool ( selectedFiles , async ( file ) => { optimized &&= assetInfo [ await calculateHash ( file ) ] ; return optimized ; } ) ; return result . every ( Boolean ) ; } export async function optimizeAsync ( projectRoot = './' , options = { } ) { console . log ( ) ; console . log ( chalk . bold `\u203A Optimizing assets...` ) ; const { assetJson , assetInfo } = await readAssetJsonAsync ( projectRoot ) ; const outdated = new Set < string > ( ) ; for ( const fileHash in assetInfo ) outdated . add ( fileHash ) ; let totalSaved = 0 ; const { allFiles , selectedFiles } = await getAssetFilesAsync ( projectRoot , options ) ; const hashes = Object . fromEntries ( await pool ( allFiles , async ( filePath ) => { const hash = await calculateHash ( filePath ) ; if ( assetInfo [ hash ] ) { outdated . delete ( hash ) ; } return [ filePath , hash ] ; } ) ) ; outdated . forEach ( outdatedHash => { delete assetInfo [ outdatedHash ] ; } ) ; const { include , exclude , save } = options ; const quality = options . quality == null ? 80 : options . quality ; const images = include || exclude ? selectedFiles : allFiles ; const poolResult = await pool ( images , async ( image ) => { const logLines = [ ] ; const hash = hashes [ image ] ; if ( assetInfo [ hash ] ) { return logLines ; } if ( ! ( await isAvailableAsync ( ) ) ) { logLines . push ( chalk . bold . red ( `\u203A Cannot optimize images without sharp-cli.\n\u203A Run this command again after successfully installing sharp with \`${ chalk . magenta `npm install -g sharp-cli` }\`` ) ) ; return logLines ; } const { size : prevSize } = statSync ( image ) ; const newName = createNewFilename ( image ) ; logLines . push ( `\u203A Checking ${ chalk . reset . bold ( relative ( projectRoot , image ) ) }` ) ; const optimizedImage = await optimizeImageAsync ( image , quality ) ; const { size : newSize } = statSync ( optimizedImage ) ; const amountSaved = prevSize - newSize ; if ( amountSaved > 0 ) { await move ( image , newName ) ; await move ( optimizedImage , image ) ; } else { assetInfo [ hash ] = true ; logLines . push ( chalk . dim ( amountSaved === 0 ? ` \u203A Skipping: Original was identical in size.` : ` \u203A Skipping: Original was ${ prettyBytes ( amountSaved * - 1 ) } smaller.` ) ) ; return logLines ; } const newHash = await calculateHash ( image ) ; assetInfo [ newHash ] = true ; if ( save ) { if ( hash === newHash ) { logLines . push ( chalk . gray ( `\u203A Compressed asset ${ image } is identical to the original. Using original instead.` ) ) ; unlinkSync ( newName ) ; } else { logLines . push ( chalk . gray ( `\u203A Saving original asset to ${ newName }` ) ) ; assetInfo [ hash ] = true ; } } else { unlinkSync ( newName ) ; } if ( amountSaved ) { totalSaved += amountSaved ; logLines . push ( chalk . magenta ( `\u203A Saved ${ prettyBytes ( amountSaved ) }` ) ) ; } else { logLines . push ( chalk . gray ( `\u203A Nothing to compress.` ) ) ; } return logLines ; } ) ; poolResult . forEach ( logLines => logLines . forEach ( line => console . log ( line ) ) ) ; console . log ( ) ; if ( totalSaved === 0 ) { console . log ( chalk . yellow `\u203A All assets were fully optimized already.` ) ; } else { console . log ( chalk . bold ( `\u203A Finished compressing assets. ${ chalk . green ( prettyBytes ( totalSaved ) ) } saved.` ) ) ; } assetJson . writeAsync ( assetInfo ) ; }