1const { resolveEntryPoint } = require('@expo/config/paths');
2const { loadAsync } = require('@expo/metro-config');
3const crypto = require('crypto');
4const fs = require('fs');
5const Server = require('metro/src/Server');
6const path = require('path');
7
8const filterPlatformAssetScales = require('./filterPlatformAssetScales');
9
10function findUpProjectRoot(cwd) {
11  if (['.', path.sep].includes(cwd)) return null;
12
13  if (fs.existsSync(path.join(cwd, 'package.json'))) {
14    return cwd;
15  } else {
16    return findUpProjectRoot(path.dirname(cwd));
17  }
18}
19
20/** Resolve the relative entry file using Expo's resolution method. */
21function getRelativeEntryPoint(projectRoot, platform) {
22  const entry = resolveEntryPoint(projectRoot, { platform });
23  if (entry) {
24    return path.relative(projectRoot, entry);
25  }
26  return entry;
27}
28
29(async function () {
30  const platform = process.argv[2];
31  const possibleProjectRoot = findUpProjectRoot(process.argv[3]);
32  const destinationDir = process.argv[4];
33  const entryFile =
34    process.argv[5] ||
35    process.env.ENTRY_FILE ||
36    getRelativeEntryPoint(possibleProjectRoot, platform) ||
37    'index.js';
38
39  // Remove projectRoot validation when we no longer support React Native <= 62
40  let projectRoot;
41  if (fs.existsSync(path.join(possibleProjectRoot, entryFile))) {
42    projectRoot = path.resolve(possibleProjectRoot);
43  } else if (fs.existsSync(path.join(possibleProjectRoot, '..', entryFile))) {
44    projectRoot = path.resolve(possibleProjectRoot, '..');
45  } else {
46    throw new Error(
47      'Error loading application entry point. If your entry point is not index.js, please set ENTRY_FILE environment variable with your app entry point.'
48    );
49  }
50
51  process.chdir(projectRoot);
52
53  let metroConfig;
54  try {
55    metroConfig = await loadAsync(projectRoot);
56  } catch (e) {
57    let message = `Error loading Metro config and Expo app config: ${e.message}\n\nMake sure your project is configured properly and your app.json / app.config.js is valid.`;
58    if (process.env.EAS_BUILD) {
59      message +=
60        '\nIf you are using environment variables in app.config.js, verify that you have set them in your EAS Build profile configuration or secrets.';
61    }
62    throw new Error(message);
63  }
64
65  let assets;
66  try {
67    assets = await fetchAssetManifestAsync(platform, projectRoot, entryFile, metroConfig);
68  } catch (e) {
69    throw new Error(
70      "Error loading assets JSON from Metro. Ensure you've followed all expo-updates installation steps correctly. " +
71        e.message
72    );
73  }
74
75  const manifest = {
76    id: crypto.randomUUID(),
77    commitTime: new Date().getTime(),
78    assets: [],
79  };
80
81  assets.forEach(function (asset) {
82    if (!asset.fileHashes) {
83      throw new Error(
84        'The hashAssetFiles Metro plugin is not configured. You need to add a metro.config.js to your project that configures Metro to use this plugin. See https://github.com/expo/expo/blob/main/packages/expo-updates/README.md#metroconfigjs for an example.'
85      );
86    }
87    filterPlatformAssetScales(platform, asset.scales).forEach(function (scale, index) {
88      const assetInfoForManifest = {
89        name: asset.name,
90        type: asset.type,
91        scale,
92        packagerHash: asset.fileHashes[index],
93        subdirectory: asset.httpServerLocation,
94      };
95      if (platform === 'ios') {
96        assetInfoForManifest.nsBundleDir = getIosDestinationDir(asset);
97        assetInfoForManifest.nsBundleFilename =
98          scale === 1 ? asset.name : asset.name + '@' + scale + 'x';
99      } else if (platform === 'android') {
100        assetInfoForManifest.scales = asset.scales;
101        assetInfoForManifest.resourcesFilename = getAndroidResourceIdentifier(asset);
102        assetInfoForManifest.resourcesFolder = getAndroidResourceFolderName(asset);
103      }
104      manifest.assets.push(assetInfoForManifest);
105    });
106  });
107
108  fs.writeFileSync(path.join(destinationDir, 'app.manifest'), JSON.stringify(manifest));
109})().catch((e) => {
110  // Wrap in regex to make it easier for log parsers (like `@expo/xcpretty`) to find this error.
111  e.message = `@build-script-error-begin\n${e.message}\n@build-script-error-end\n`;
112  console.error(e);
113  process.exit(1);
114});
115
116// See https://developer.android.com/guide/topics/resources/drawable-resource.html
117const drawableFileTypes = new Set(['gif', 'jpeg', 'jpg', 'png', 'svg', 'webp', 'xml']);
118function getAndroidResourceFolderName(asset) {
119  return drawableFileTypes.has(asset.type) ? 'drawable' : 'raw';
120}
121
122// copied from react-native/Libraries/Image/assetPathUtils.js
123function getAndroidResourceIdentifier(asset) {
124  const folderPath = getBasePath(asset);
125  return (folderPath + '/' + asset.name)
126    .toLowerCase()
127    .replace(/\//g, '_') // Encode folder structure in file name
128    .replace(/([^a-z0-9_])/g, '') // Remove illegal chars
129    .replace(/^assets_/, ''); // Remove "assets_" prefix
130}
131
132function getIosDestinationDir(asset) {
133  // react-native-cli replaces `..` with `_` when embedding assets in the iOS app bundle
134  // https://github.com/react-native-community/cli/blob/0a93be1a42ed1fb05bb0ebf3b82d58b2dd920614/packages/cli/src/commands/bundle/getAssetDestPathIOS.ts
135  return getBasePath(asset).replace(/\.\.\//g, '_');
136}
137
138// copied from react-native/Libraries/Image/assetPathUtils.js
139function getBasePath(asset) {
140  let basePath = asset.httpServerLocation;
141  if (basePath[0] === '/') {
142    basePath = basePath.substr(1);
143  }
144  return basePath;
145}
146
147// Spawn a Metro server to get the asset manifest
148async function fetchAssetManifestAsync(platform, projectRoot, entryFile, metroConfig) {
149  // Project-level babel config does not load unless we change to the
150  // projectRoot before instantiating the server
151  process.chdir(projectRoot);
152
153  const server = new Server(metroConfig);
154
155  const requestOpts = {
156    entryFile,
157    dev: false,
158    minify: false,
159    platform,
160  };
161
162  let assetManifest;
163  let error;
164  try {
165    assetManifest = await server.getAssets({
166      ...Server.DEFAULT_BUNDLE_OPTIONS,
167      ...requestOpts,
168    });
169  } catch (e) {
170    error = e;
171  } finally {
172    server.end();
173  }
174
175  if (error) {
176    throw error;
177  }
178
179  return assetManifest;
180}
181