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