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 const metadataPath = path.join(outputDir, 'metadata.json'); 123 debug(`Writing metadata.json to ${metadataPath}`); 124 await fs.writeFile(metadataPath, JSON.stringify(contents)); 125 return contents; 126} 127 128export async function writeAssetMapAsync({ 129 outputDir, 130 assets, 131}: { 132 outputDir: string; 133 assets: Asset[]; 134}) { 135 // Convert the assets array to a k/v pair where the asset hash is the key and the asset is the value. 136 const contents = Object.fromEntries(assets.map((asset) => [asset.hash, asset])); 137 await fs.writeFile(path.join(outputDir, 'assetmap.json'), JSON.stringify(contents)); 138 return contents; 139} 140 141export async function writeDebugHtmlAsync({ 142 outputDir, 143 fileNames, 144}: { 145 outputDir: string; 146 fileNames: Record<string, string>; 147}) { 148 // Make a debug html so user can debug their bundles 149 const contents = ` 150 ${Object.values(fileNames) 151 .map((fileName) => `<script src="${path.join('bundles', fileName)}"></script>`) 152 .join('\n ')} 153 Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab. 154 You can see a red colored folder containing the original source code from your bundle. 155 `; 156 157 await fs.writeFile(path.join(outputDir, 'debug.html'), contents); 158 return contents; 159} 160