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