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