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