1import { ExpoConfig } from '@expo/config'; 2import { BundleAssetWithFileHashes } from '@expo/dev-server'; 3import fs from 'fs/promises'; 4import path from 'path'; 5 6import { getAssetSchemasAsync } from '../../../api/getExpoSchema'; 7import * as Log from '../../../log'; 8import { fileExistsAsync } from '../../../utils/dir'; 9import { CommandError } from '../../../utils/errors'; 10import { get, set } from '../../../utils/obj'; 11import { validateUrl } from '../../../utils/url'; 12 13type ManifestAsset = { fileHashes: string[]; files: string[]; hash: string }; 14 15export type Asset = ManifestAsset | BundleAssetWithFileHashes; 16 17type ManifestResolutionError = Error & { 18 localAssetPath?: string; 19 manifestField?: string; 20}; 21 22/** Inline the contents of each platform's `googleServicesFile` so runtimes can access them. */ 23export async function resolveGoogleServicesFile( 24 projectRoot: string, 25 manifest: Pick<ExpoConfig, 'android' | 'ios'> 26) { 27 if (manifest.android?.googleServicesFile) { 28 try { 29 const contents = await fs.readFile( 30 path.resolve(projectRoot, manifest.android.googleServicesFile), 31 'utf8' 32 ); 33 manifest.android.googleServicesFile = contents; 34 } catch { 35 Log.warn( 36 `Could not parse Expo config: android.googleServicesFile: "${manifest.android.googleServicesFile}"` 37 ); 38 // Delete the field so Expo Go doesn't attempt to read it. 39 delete manifest.android.googleServicesFile; 40 } 41 } 42 if (manifest.ios?.googleServicesFile) { 43 try { 44 const contents = await fs.readFile( 45 path.resolve(projectRoot, manifest.ios.googleServicesFile), 46 'base64' 47 ); 48 manifest.ios.googleServicesFile = contents; 49 } catch { 50 Log.warn( 51 `Could not parse Expo config: ios.googleServicesFile: "${manifest.ios.googleServicesFile}"` 52 ); 53 // Delete the field so Expo Go doesn't attempt to read it. 54 delete manifest.ios.googleServicesFile; 55 } 56 } 57 return manifest; 58} 59 60/** 61 * Get all fields in the manifest that match assets, then filter the ones that aren't set. 62 * 63 * @param manifest 64 * @returns Asset fields that the user has set like ["icon", "splash.image", ...] 65 */ 66export async function getAssetFieldPathsForManifestAsync(manifest: ExpoConfig): Promise<string[]> { 67 // String array like ["icon", "notification.icon", "loading.icon", "loading.backgroundImage", "ios.icon", ...] 68 const sdkAssetFieldPaths = await getAssetSchemasAsync(manifest.sdkVersion); 69 return sdkAssetFieldPaths.filter((assetSchema) => get(manifest, assetSchema)); 70} 71 72/** Resolve all assets in the app.json inline. */ 73export async function resolveManifestAssets( 74 projectRoot: string, 75 { 76 manifest, 77 resolver, 78 }: { 79 manifest: ExpoConfig; 80 resolver: (assetPath: string) => Promise<string>; 81 } 82) { 83 try { 84 // Asset fields that the user has set like ["icon", "splash.image"] 85 const assetSchemas = await getAssetFieldPathsForManifestAsync(manifest); 86 // Get the URLs 87 const urls = await Promise.all( 88 assetSchemas.map(async (manifestField) => { 89 const pathOrURL = get(manifest, manifestField); 90 // URL 91 if (validateUrl(pathOrURL, { requireProtocol: true })) { 92 return pathOrURL; 93 } 94 95 // File path 96 if (await fileExistsAsync(path.resolve(projectRoot, pathOrURL))) { 97 return await resolver(pathOrURL); 98 } 99 100 // Unknown 101 const err: ManifestResolutionError = new CommandError( 102 'MANIFEST_ASSET', 103 'Could not resolve local asset: ' + pathOrURL 104 ); 105 err.localAssetPath = pathOrURL; 106 err.manifestField = manifestField; 107 throw err; 108 }) 109 ); 110 111 // Set the corresponding URL fields 112 assetSchemas.forEach((manifestField, index: number) => 113 set(manifest, `${manifestField}Url`, urls[index]) 114 ); 115 } catch (error: any) { 116 if (error.localAssetPath) { 117 Log.warn( 118 `Unable to resolve asset "${error.localAssetPath}" from "${error.manifestField}" in your app.json or app.config.js` 119 ); 120 } else { 121 Log.warn( 122 `Warning: Unable to resolve manifest assets. Icons and fonts might not work. ${error.message}.` 123 ); 124 } 125 } 126} 127