1import { Platform } from '@expo/config'; 2import { BundleOutput } from '@expo/dev-server'; 3import crypto from 'crypto'; 4import fs from 'fs/promises'; 5import path from 'path'; 6 7import { createMetadataJson } from './createMetadataJson'; 8import { Asset } from './saveAssets'; 9 10/** 11 * @param props.platform native platform for the bundle 12 * @param props.hash crypto hash for the bundle contents 13 * @returns filename for the JS bundle. 14 */ 15function createBundleFileName({ platform, hash }: { platform: string; hash: string }): string { 16 return `${platform}-${hash}.js`; 17} 18 19/** 20 * @param bundle JS bundle as a string 21 * @returns crypto hash for the provided bundle 22 */ 23function createBundleHash(bundle: string | Uint8Array): string { 24 return crypto.createHash('md5').update(bundle).digest('hex'); 25} 26 27export async function writeBundlesAsync({ 28 bundles, 29 outputDir, 30}: { 31 bundles: Partial<Record<Platform, Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'>>>; 32 outputDir: string; 33}) { 34 const hashes: Partial<Record<Platform, string>> = {}; 35 const fileNames: Partial<Record<Platform, string>> = {}; 36 37 for (const [platform, bundleOutput] of Object.entries(bundles) as [ 38 Platform, 39 Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'> 40 ][]) { 41 const bundle = bundleOutput.hermesBytecodeBundle ?? bundleOutput.code; 42 const hash = createBundleHash(bundle); 43 const fileName = createBundleFileName({ platform, hash }); 44 45 hashes[platform] = hash; 46 fileNames[platform] = fileName; 47 await fs.writeFile(path.join(outputDir, fileName), bundle); 48 } 49 50 return { hashes, fileNames }; 51} 52 53export async function writeSourceMapsAsync({ 54 bundles, 55 hashes, 56 fileNames, 57 outputDir, 58}: { 59 bundles: Record< 60 string, 61 Pick<BundleOutput, 'hermesSourcemap' | 'map' | 'hermesBytecodeBundle' | 'code'> 62 >; 63 hashes?: Record<string, string>; 64 fileNames?: Record<string, string>; 65 outputDir: string; 66}) { 67 return Promise.all( 68 Object.entries(bundles).map(async ([platform, bundle]) => { 69 const sourceMap = bundle.hermesSourcemap ?? bundle.map; 70 const hash = 71 hashes?.[platform] ?? createBundleHash(bundle.hermesBytecodeBundle ?? bundle.code); 72 const mapName = `${platform}-${hash}.map`; 73 await fs.writeFile(path.join(outputDir, mapName), sourceMap); 74 75 const jsBundleFileName = fileNames?.[platform] ?? createBundleFileName({ platform, hash }); 76 const jsPath = path.join(outputDir, jsBundleFileName); 77 78 // Add correct mapping to sourcemap paths 79 const mappingComment = `\n//# sourceMappingURL=${mapName}`; 80 await fs.appendFile(jsPath, mappingComment); 81 return { 82 platform, 83 fileName: mapName, 84 hash, 85 map: sourceMap, 86 comment: mappingComment, 87 }; 88 }) 89 ); 90} 91 92export async function writeMetadataJsonAsync({ 93 outputDir, 94 bundles, 95 fileNames, 96}: { 97 outputDir: string; 98 bundles: Record<string, Pick<BundleOutput, 'assets'>>; 99 fileNames: Record<string, string>; 100}) { 101 const contents = createMetadataJson({ 102 bundles, 103 fileNames, 104 }); 105 await fs.writeFile(path.join(outputDir, 'metadata.json'), JSON.stringify(contents)); 106 return contents; 107} 108 109export async function writeAssetMapAsync({ 110 outputDir, 111 assets, 112}: { 113 outputDir: string; 114 assets: Asset[]; 115}) { 116 // Convert the assets array to a k/v pair where the asset hash is the key and the asset is the value. 117 const contents = Object.fromEntries(assets.map((asset) => [asset.hash, asset])); 118 await fs.writeFile(path.join(outputDir, 'assetmap.json'), JSON.stringify(contents)); 119 return contents; 120} 121 122export async function writeDebugHtmlAsync({ 123 outputDir, 124 fileNames, 125}: { 126 outputDir: string; 127 fileNames: Record<string, string>; 128}) { 129 // Make a debug html so user can debug their bundles 130 const contents = ` 131 ${Object.values(fileNames) 132 .map((fileName) => `<script src="${path.join('bundles', fileName)}"></script>`) 133 .join('\n ')} 134 Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab. 135 You can see a red colored folder containing the original source code from your bundle. 136 `; 137 138 await fs.writeFile(path.join(outputDir, 'debug.html'), contents); 139 return contents; 140} 141