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