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