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