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