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