18d307f52SEvan Baconimport { ExpoConfig } from '@expo/config'; 28d307f52SEvan Baconimport fs from 'fs/promises'; 38d307f52SEvan Baconimport path from 'path'; 48d307f52SEvan Bacon 58d307f52SEvan Baconimport { getAssetSchemasAsync } from '../../../api/getExpoSchema'; 6*edeec536SEvan Baconimport { BundleAssetWithFileHashes } from '../../../export/fork-bundleAsync'; 78d307f52SEvan Baconimport * as Log from '../../../log'; 88d307f52SEvan Baconimport { fileExistsAsync } from '../../../utils/dir'; 98d307f52SEvan Baconimport { CommandError } from '../../../utils/errors'; 108d307f52SEvan Baconimport { get, set } from '../../../utils/obj'; 118d307f52SEvan Baconimport { validateUrl } from '../../../utils/url'; 128d307f52SEvan Bacon 138d307f52SEvan Bacontype ManifestAsset = { fileHashes: string[]; files: string[]; hash: string }; 148d307f52SEvan Bacon 158d307f52SEvan Baconexport type Asset = ManifestAsset | BundleAssetWithFileHashes; 168d307f52SEvan Bacon 178d307f52SEvan Bacontype ManifestResolutionError = Error & { 188d307f52SEvan Bacon localAssetPath?: string; 198d307f52SEvan Bacon manifestField?: string; 208d307f52SEvan Bacon}; 218d307f52SEvan Bacon 228d307f52SEvan Bacon/** Inline the contents of each platform's `googleServicesFile` so runtimes can access them. */ 238d307f52SEvan Baconexport async function resolveGoogleServicesFile( 248d307f52SEvan Bacon projectRoot: string, 258d307f52SEvan Bacon manifest: Pick<ExpoConfig, 'android' | 'ios'> 268d307f52SEvan Bacon) { 278d307f52SEvan Bacon if (manifest.android?.googleServicesFile) { 288d307f52SEvan Bacon try { 298d307f52SEvan Bacon const contents = await fs.readFile( 308d307f52SEvan Bacon path.resolve(projectRoot, manifest.android.googleServicesFile), 318d307f52SEvan Bacon 'utf8' 328d307f52SEvan Bacon ); 338d307f52SEvan Bacon manifest.android.googleServicesFile = contents; 3498ecfc87SJames Ide } catch { 358d307f52SEvan Bacon Log.warn( 368d307f52SEvan Bacon `Could not parse Expo config: android.googleServicesFile: "${manifest.android.googleServicesFile}"` 378d307f52SEvan Bacon ); 388d307f52SEvan Bacon // Delete the field so Expo Go doesn't attempt to read it. 398d307f52SEvan Bacon delete manifest.android.googleServicesFile; 408d307f52SEvan Bacon } 418d307f52SEvan Bacon } 428d307f52SEvan Bacon if (manifest.ios?.googleServicesFile) { 438d307f52SEvan Bacon try { 448d307f52SEvan Bacon const contents = await fs.readFile( 458d307f52SEvan Bacon path.resolve(projectRoot, manifest.ios.googleServicesFile), 468d307f52SEvan Bacon 'base64' 478d307f52SEvan Bacon ); 488d307f52SEvan Bacon manifest.ios.googleServicesFile = contents; 4998ecfc87SJames Ide } catch { 508d307f52SEvan Bacon Log.warn( 518d307f52SEvan Bacon `Could not parse Expo config: ios.googleServicesFile: "${manifest.ios.googleServicesFile}"` 528d307f52SEvan Bacon ); 538d307f52SEvan Bacon // Delete the field so Expo Go doesn't attempt to read it. 548d307f52SEvan Bacon delete manifest.ios.googleServicesFile; 558d307f52SEvan Bacon } 568d307f52SEvan Bacon } 578d307f52SEvan Bacon return manifest; 588d307f52SEvan Bacon} 598d307f52SEvan Bacon 608d307f52SEvan Bacon/** 618d307f52SEvan Bacon * Get all fields in the manifest that match assets, then filter the ones that aren't set. 628d307f52SEvan Bacon * 638d307f52SEvan Bacon * @param manifest 648d307f52SEvan Bacon * @returns Asset fields that the user has set like ["icon", "splash.image", ...] 658d307f52SEvan Bacon */ 668d307f52SEvan Baconexport async function getAssetFieldPathsForManifestAsync(manifest: ExpoConfig): Promise<string[]> { 678d307f52SEvan Bacon // String array like ["icon", "notification.icon", "loading.icon", "loading.backgroundImage", "ios.icon", ...] 688d307f52SEvan Bacon const sdkAssetFieldPaths = await getAssetSchemasAsync(manifest.sdkVersion); 698d307f52SEvan Bacon return sdkAssetFieldPaths.filter((assetSchema) => get(manifest, assetSchema)); 708d307f52SEvan Bacon} 718d307f52SEvan Bacon 728d307f52SEvan Bacon/** Resolve all assets in the app.json inline. */ 738d307f52SEvan Baconexport async function resolveManifestAssets( 748d307f52SEvan Bacon projectRoot: string, 758d307f52SEvan Bacon { 768d307f52SEvan Bacon manifest, 778d307f52SEvan Bacon resolver, 78dc51e206SEvan Bacon strict, 798d307f52SEvan Bacon }: { 808d307f52SEvan Bacon manifest: ExpoConfig; 818d307f52SEvan Bacon resolver: (assetPath: string) => Promise<string>; 82dc51e206SEvan Bacon strict?: boolean; 838d307f52SEvan Bacon } 848d307f52SEvan Bacon) { 858d307f52SEvan Bacon try { 868d307f52SEvan Bacon // Asset fields that the user has set like ["icon", "splash.image"] 878d307f52SEvan Bacon const assetSchemas = await getAssetFieldPathsForManifestAsync(manifest); 888d307f52SEvan Bacon // Get the URLs 898d307f52SEvan Bacon const urls = await Promise.all( 908d307f52SEvan Bacon assetSchemas.map(async (manifestField) => { 918d307f52SEvan Bacon const pathOrURL = get(manifest, manifestField); 928d307f52SEvan Bacon // URL 938d307f52SEvan Bacon if (validateUrl(pathOrURL, { requireProtocol: true })) { 948d307f52SEvan Bacon return pathOrURL; 958d307f52SEvan Bacon } 968d307f52SEvan Bacon 978d307f52SEvan Bacon // File path 988d307f52SEvan Bacon if (await fileExistsAsync(path.resolve(projectRoot, pathOrURL))) { 998d307f52SEvan Bacon return await resolver(pathOrURL); 1008d307f52SEvan Bacon } 1018d307f52SEvan Bacon 1028d307f52SEvan Bacon // Unknown 1038d307f52SEvan Bacon const err: ManifestResolutionError = new CommandError( 1048d307f52SEvan Bacon 'MANIFEST_ASSET', 1058d307f52SEvan Bacon 'Could not resolve local asset: ' + pathOrURL 1068d307f52SEvan Bacon ); 1078d307f52SEvan Bacon err.localAssetPath = pathOrURL; 1088d307f52SEvan Bacon err.manifestField = manifestField; 1098d307f52SEvan Bacon throw err; 1108d307f52SEvan Bacon }) 1118d307f52SEvan Bacon ); 1128d307f52SEvan Bacon 1138d307f52SEvan Bacon // Set the corresponding URL fields 1148d307f52SEvan Bacon assetSchemas.forEach((manifestField, index: number) => 1158d307f52SEvan Bacon set(manifest, `${manifestField}Url`, urls[index]) 1168d307f52SEvan Bacon ); 11729975bfdSEvan Bacon } catch (error: any) { 11829975bfdSEvan Bacon if (error.localAssetPath) { 1198d307f52SEvan Bacon Log.warn( 12029975bfdSEvan Bacon `Unable to resolve asset "${error.localAssetPath}" from "${error.manifestField}" in your app.json or app.config.js` 1218d307f52SEvan Bacon ); 1228d307f52SEvan Bacon } else { 1238d307f52SEvan Bacon Log.warn( 12429975bfdSEvan Bacon `Warning: Unable to resolve manifest assets. Icons and fonts might not work. ${error.message}.` 1258d307f52SEvan Bacon ); 1268d307f52SEvan Bacon } 127dc51e206SEvan Bacon 128dc51e206SEvan Bacon if (strict) { 129dc51e206SEvan Bacon throw new CommandError( 130dc51e206SEvan Bacon 'MANIFEST_ASSET', 131dc51e206SEvan Bacon 'Failed to export manifest assets: ' + error.message 132dc51e206SEvan Bacon ); 133dc51e206SEvan Bacon } 1348d307f52SEvan Bacon } 1358d307f52SEvan Bacon} 136