1const { loadMetroConfigAsync } = require('@expo/cli/build/src/start/server/metro/instantiateMetro');
2const { resolveEntryPoint } = require('@expo/config/paths');
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    // Load the metro config the same way it would be loaded in Expo CLI.
56    // This ensures dynamic features like tsconfig paths can be used.
57    metroConfig = (
58      await loadMetroConfigAsync(projectRoot, {
59        // No config options can be passed to this point.
60      })
61    ).config;
62  } catch (e) {
63    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.`;
64    if (process.env.EAS_BUILD) {
65      message +=
66        '\nIf you are using environment variables in app.config.js, verify that you have set them in your EAS Build profile configuration or secrets.';
67    }
68    throw new Error(message);
69  }
70
71  let assets;
72  try {
73    assets = await fetchAssetManifestAsync(platform, projectRoot, entryFile, metroConfig);
74  } catch (e) {
75    throw new Error(
76      "Error loading assets JSON from Metro. Ensure you've followed all expo-updates installation steps correctly. " +
77        e.message
78    );
79  }
80
81  const manifest = {
82    id: crypto.randomUUID(),
83    commitTime: new Date().getTime(),
84    assets: [],
85  };
86
87  assets.forEach(function (asset) {
88    if (!asset.fileHashes) {
89      throw new Error(
90        '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.'
91      );
92    }
93    filterPlatformAssetScales(platform, asset.scales).forEach(function (scale, index) {
94      const assetInfoForManifest = {
95        name: asset.name,
96        type: asset.type,
97        scale,
98        packagerHash: asset.fileHashes[index],
99        subdirectory: asset.httpServerLocation,
100      };
101      if (platform === 'ios') {
102        assetInfoForManifest.nsBundleDir = getIosDestinationDir(asset);
103        assetInfoForManifest.nsBundleFilename =
104          scale === 1 ? asset.name : asset.name + '@' + scale + 'x';
105      } else if (platform === 'android') {
106        assetInfoForManifest.scales = asset.scales;
107        assetInfoForManifest.resourcesFilename = getAndroidResourceIdentifier(asset);
108        assetInfoForManifest.resourcesFolder = getAndroidResourceFolderName(asset);
109      }
110      manifest.assets.push(assetInfoForManifest);
111    });
112  });
113
114  fs.writeFileSync(path.join(destinationDir, 'app.manifest'), JSON.stringify(manifest));
115})().catch((e) => {
116  // Wrap in regex to make it easier for log parsers (like `@expo/xcpretty`) to find this error.
117  e.message = `@build-script-error-begin\n${e.message}\n@build-script-error-end\n`;
118  console.error(e);
119  process.exit(1);
120});
121
122// See https://developer.android.com/guide/topics/resources/drawable-resource.html
123const drawableFileTypes = new Set(['gif', 'jpeg', 'jpg', 'png', 'svg', 'webp', 'xml']);
124function getAndroidResourceFolderName(asset) {
125  return drawableFileTypes.has(asset.type) ? 'drawable' : 'raw';
126}
127
128// copied from react-native/Libraries/Image/assetPathUtils.js
129function getAndroidResourceIdentifier(asset) {
130  const folderPath = getBasePath(asset);
131  return (folderPath + '/' + asset.name)
132    .toLowerCase()
133    .replace(/\//g, '_') // Encode folder structure in file name
134    .replace(/([^a-z0-9_])/g, '') // Remove illegal chars
135    .replace(/^assets_/, ''); // Remove "assets_" prefix
136}
137
138function getIosDestinationDir(asset) {
139  // react-native-cli replaces `..` with `_` when embedding assets in the iOS app bundle
140  // https://github.com/react-native-community/cli/blob/0a93be1a42ed1fb05bb0ebf3b82d58b2dd920614/packages/cli/src/commands/bundle/getAssetDestPathIOS.ts
141  return getBasePath(asset).replace(/\.\.\//g, '_');
142}
143
144// copied from react-native/Libraries/Image/assetPathUtils.js
145function getBasePath(asset) {
146  let basePath = asset.httpServerLocation;
147  if (basePath[0] === '/') {
148    basePath = basePath.substr(1);
149  }
150  return basePath;
151}
152
153// Spawn a Metro server to get the asset manifest
154async function fetchAssetManifestAsync(platform, projectRoot, entryFile, metroConfig) {
155  // Project-level babel config does not load unless we change to the
156  // projectRoot before instantiating the server
157  process.chdir(projectRoot);
158
159  const server = new Server(metroConfig);
160
161  const requestOpts = {
162    entryFile,
163    dev: false,
164    minify: false,
165    platform,
166  };
167
168  let assetManifest;
169  let error;
170  try {
171    assetManifest = await server.getAssets({
172      ...Server.DEFAULT_BUNDLE_OPTIONS,
173      ...requestOpts,
174    });
175  } catch (e) {
176    error = e;
177  } finally {
178    server.end();
179  }
180
181  if (error) {
182    throw error;
183  }
184
185  return assetManifest;
186}
187