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