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 strict, 79 }: { 80 manifest: ExpoConfig; 81 resolver: (assetPath: string) => Promise<string>; 82 strict?: boolean; 83 } 84) { 85 try { 86 // Asset fields that the user has set like ["icon", "splash.image"] 87 const assetSchemas = await getAssetFieldPathsForManifestAsync(manifest); 88 // Get the URLs 89 const urls = await Promise.all( 90 assetSchemas.map(async (manifestField) => { 91 const pathOrURL = get(manifest, manifestField); 92 // URL 93 if (validateUrl(pathOrURL, { requireProtocol: true })) { 94 return pathOrURL; 95 } 96 97 // File path 98 if (await fileExistsAsync(path.resolve(projectRoot, pathOrURL))) { 99 return await resolver(pathOrURL); 100 } 101 102 // Unknown 103 const err: ManifestResolutionError = new CommandError( 104 'MANIFEST_ASSET', 105 'Could not resolve local asset: ' + pathOrURL 106 ); 107 err.localAssetPath = pathOrURL; 108 err.manifestField = manifestField; 109 throw err; 110 }) 111 ); 112 113 // Set the corresponding URL fields 114 assetSchemas.forEach((manifestField, index: number) => 115 set(manifest, `${manifestField}Url`, urls[index]) 116 ); 117 } catch (error: any) { 118 if (error.localAssetPath) { 119 Log.warn( 120 `Unable to resolve asset "${error.localAssetPath}" from "${error.manifestField}" in your app.json or app.config.js` 121 ); 122 } else { 123 Log.warn( 124 `Warning: Unable to resolve manifest assets. Icons and fonts might not work. ${error.message}.` 125 ); 126 } 127 128 if (strict) { 129 throw new CommandError( 130 'MANIFEST_ASSET', 131 'Failed to export manifest assets: ' + error.message 132 ); 133 } 134 } 135} 136