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