12beab412SEvan Baconimport { Platform } from '@expo/config'; 2dc51e206SEvan Baconimport crypto from 'crypto'; 3dc51e206SEvan Baconimport fs from 'fs/promises'; 4dc51e206SEvan Baconimport path from 'path'; 5dc51e206SEvan Bacon 6dc51e206SEvan Baconimport { createMetadataJson } from './createMetadataJson'; 7e330c216SEvan Baconimport { BundleOutput } from './fork-bundleAsync'; 8dc51e206SEvan Baconimport { Asset } from './saveAssets'; 9dc51e206SEvan Bacon 10e330c216SEvan Baconconst debug = require('debug')('expo:export:write') as typeof console.log; 11e330c216SEvan Bacon 12dc51e206SEvan Bacon/** 13dc51e206SEvan Bacon * @param props.platform native platform for the bundle 14864ec879SEvan Bacon * @param props.format extension to use for the name 15dc51e206SEvan Bacon * @param props.hash crypto hash for the bundle contents 16dc51e206SEvan Bacon * @returns filename for the JS bundle. 17dc51e206SEvan Bacon */ 18864ec879SEvan Baconfunction createBundleFileName({ 19864ec879SEvan Bacon platform, 20864ec879SEvan Bacon format, 21864ec879SEvan Bacon hash, 22864ec879SEvan Bacon}: { 23864ec879SEvan Bacon platform: string; 24864ec879SEvan Bacon format: 'javascript' | 'bytecode'; 25864ec879SEvan Bacon hash: string; 26864ec879SEvan Bacon}): string { 27864ec879SEvan Bacon return `${platform}-${hash}.${format === 'javascript' ? 'js' : 'hbc'}`; 28dc51e206SEvan Bacon} 29dc51e206SEvan Bacon 30dc51e206SEvan Bacon/** 31dc51e206SEvan Bacon * @param bundle JS bundle as a string 32dc51e206SEvan Bacon * @returns crypto hash for the provided bundle 33dc51e206SEvan Bacon */ 34dc51e206SEvan Baconfunction createBundleHash(bundle: string | Uint8Array): string { 35dc51e206SEvan Bacon return crypto.createHash('md5').update(bundle).digest('hex'); 36dc51e206SEvan Bacon} 37dc51e206SEvan Bacon 38dc51e206SEvan Baconexport async function writeBundlesAsync({ 39dc51e206SEvan Bacon bundles, 40dc51e206SEvan Bacon outputDir, 41*46f023faSEvan Bacon useServerRendering, 42dc51e206SEvan Bacon}: { 432beab412SEvan Bacon bundles: Partial<Record<Platform, Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'>>>; 44dc51e206SEvan Bacon outputDir: string; 45*46f023faSEvan Bacon useServerRendering?: boolean; 46dc51e206SEvan Bacon}) { 472beab412SEvan Bacon const hashes: Partial<Record<Platform, string>> = {}; 482beab412SEvan Bacon const fileNames: Partial<Record<Platform, string>> = {}; 49dc51e206SEvan Bacon 502beab412SEvan Bacon for (const [platform, bundleOutput] of Object.entries(bundles) as [ 512beab412SEvan Bacon Platform, 528a424bebSJames Ide Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'>, 532beab412SEvan Bacon ][]) { 540769e63bSEvan Bacon // TODO: Move native to use the newer `_expo/...` bundle writing system. 55*46f023faSEvan Bacon if (platform === 'web' && useServerRendering) { 560769e63bSEvan Bacon continue; 570769e63bSEvan Bacon } 58dc51e206SEvan Bacon const bundle = bundleOutput.hermesBytecodeBundle ?? bundleOutput.code; 59dc51e206SEvan Bacon const hash = createBundleHash(bundle); 60864ec879SEvan Bacon const fileName = createBundleFileName({ 61864ec879SEvan Bacon platform, 62864ec879SEvan Bacon format: bundleOutput.hermesBytecodeBundle ? 'bytecode' : 'javascript', 63864ec879SEvan Bacon hash, 64864ec879SEvan Bacon }); 65dc51e206SEvan Bacon 66dc51e206SEvan Bacon hashes[platform] = hash; 67dc51e206SEvan Bacon fileNames[platform] = fileName; 68dc51e206SEvan Bacon await fs.writeFile(path.join(outputDir, fileName), bundle); 69dc51e206SEvan Bacon } 70dc51e206SEvan Bacon 71dc51e206SEvan Bacon return { hashes, fileNames }; 72dc51e206SEvan Bacon} 73dc51e206SEvan Bacon 74e330c216SEvan Bacontype SourceMapWriteResult = { 75e330c216SEvan Bacon platform: string; 76e330c216SEvan Bacon fileName: string; 77e330c216SEvan Bacon hash: string; 78e330c216SEvan Bacon map: string; 79e330c216SEvan Bacon comment: string; 80e330c216SEvan Bacon}; 81e330c216SEvan Bacon 82dc51e206SEvan Baconexport async function writeSourceMapsAsync({ 83dc51e206SEvan Bacon bundles, 84dc51e206SEvan Bacon hashes, 85dc51e206SEvan Bacon fileNames, 86dc51e206SEvan Bacon outputDir, 87dc51e206SEvan Bacon}: { 88dc51e206SEvan Bacon bundles: Record< 89dc51e206SEvan Bacon string, 90dc51e206SEvan Bacon Pick<BundleOutput, 'hermesSourcemap' | 'map' | 'hermesBytecodeBundle' | 'code'> 91dc51e206SEvan Bacon >; 92dc51e206SEvan Bacon hashes?: Record<string, string>; 93dc51e206SEvan Bacon fileNames?: Record<string, string>; 94dc51e206SEvan Bacon outputDir: string; 95e330c216SEvan Bacon}): Promise<SourceMapWriteResult[]> { 96e330c216SEvan Bacon return ( 97e330c216SEvan Bacon await Promise.all( 98dc51e206SEvan Bacon Object.entries(bundles).map(async ([platform, bundle]) => { 99dc51e206SEvan Bacon const sourceMap = bundle.hermesSourcemap ?? bundle.map; 100e330c216SEvan Bacon if (!sourceMap) { 101e330c216SEvan Bacon debug(`Skip writing sourcemap (platform: ${platform})`); 102e330c216SEvan Bacon return null; 103e330c216SEvan Bacon } 104e330c216SEvan Bacon 105dc51e206SEvan Bacon const hash = 106dc51e206SEvan Bacon hashes?.[platform] ?? createBundleHash(bundle.hermesBytecodeBundle ?? bundle.code); 107dc51e206SEvan Bacon const mapName = `${platform}-${hash}.map`; 108dc51e206SEvan Bacon await fs.writeFile(path.join(outputDir, mapName), sourceMap); 109dc51e206SEvan Bacon 110864ec879SEvan Bacon const jsBundleFileName = 111864ec879SEvan Bacon fileNames?.[platform] ?? 112864ec879SEvan Bacon createBundleFileName({ 113864ec879SEvan Bacon platform, 114864ec879SEvan Bacon format: bundle.hermesBytecodeBundle ? 'bytecode' : 'javascript', 115864ec879SEvan Bacon hash, 116864ec879SEvan Bacon }); 117dc51e206SEvan Bacon const jsPath = path.join(outputDir, jsBundleFileName); 118dc51e206SEvan Bacon 119dc51e206SEvan Bacon // Add correct mapping to sourcemap paths 120dc51e206SEvan Bacon const mappingComment = `\n//# sourceMappingURL=${mapName}`; 121dc51e206SEvan Bacon await fs.appendFile(jsPath, mappingComment); 122dc51e206SEvan Bacon return { 123dc51e206SEvan Bacon platform, 124dc51e206SEvan Bacon fileName: mapName, 125dc51e206SEvan Bacon hash, 126dc51e206SEvan Bacon map: sourceMap, 127dc51e206SEvan Bacon comment: mappingComment, 128dc51e206SEvan Bacon }; 129dc51e206SEvan Bacon }) 130e330c216SEvan Bacon ) 131e330c216SEvan Bacon ).filter(Boolean) as SourceMapWriteResult[]; 132dc51e206SEvan Bacon} 133dc51e206SEvan Bacon 134dc51e206SEvan Baconexport async function writeMetadataJsonAsync({ 135dc51e206SEvan Bacon outputDir, 136dc51e206SEvan Bacon bundles, 137dc51e206SEvan Bacon fileNames, 138dc51e206SEvan Bacon}: { 139dc51e206SEvan Bacon outputDir: string; 140dc51e206SEvan Bacon bundles: Record<string, Pick<BundleOutput, 'assets'>>; 141dc51e206SEvan Bacon fileNames: Record<string, string>; 142dc51e206SEvan Bacon}) { 143dc51e206SEvan Bacon const contents = createMetadataJson({ 144dc51e206SEvan Bacon bundles, 145dc51e206SEvan Bacon fileNames, 146dc51e206SEvan Bacon }); 1472bd64340SEvan Bacon const metadataPath = path.join(outputDir, 'metadata.json'); 1482bd64340SEvan Bacon debug(`Writing metadata.json to ${metadataPath}`); 1492bd64340SEvan Bacon await fs.writeFile(metadataPath, JSON.stringify(contents)); 150dc51e206SEvan Bacon return contents; 151dc51e206SEvan Bacon} 152dc51e206SEvan Bacon 153dc51e206SEvan Baconexport async function writeAssetMapAsync({ 154dc51e206SEvan Bacon outputDir, 155dc51e206SEvan Bacon assets, 156dc51e206SEvan Bacon}: { 157dc51e206SEvan Bacon outputDir: string; 158dc51e206SEvan Bacon assets: Asset[]; 159dc51e206SEvan Bacon}) { 160dc51e206SEvan Bacon // Convert the assets array to a k/v pair where the asset hash is the key and the asset is the value. 161dc51e206SEvan Bacon const contents = Object.fromEntries(assets.map((asset) => [asset.hash, asset])); 162dc51e206SEvan Bacon await fs.writeFile(path.join(outputDir, 'assetmap.json'), JSON.stringify(contents)); 163dc51e206SEvan Bacon return contents; 164dc51e206SEvan Bacon} 165dc51e206SEvan Bacon 166dc51e206SEvan Baconexport async function writeDebugHtmlAsync({ 167dc51e206SEvan Bacon outputDir, 168dc51e206SEvan Bacon fileNames, 169dc51e206SEvan Bacon}: { 170dc51e206SEvan Bacon outputDir: string; 171dc51e206SEvan Bacon fileNames: Record<string, string>; 172dc51e206SEvan Bacon}) { 173dc51e206SEvan Bacon // Make a debug html so user can debug their bundles 174dc51e206SEvan Bacon const contents = ` 175dc51e206SEvan Bacon ${Object.values(fileNames) 176dc51e206SEvan Bacon .map((fileName) => `<script src="${path.join('bundles', fileName)}"></script>`) 177dc51e206SEvan Bacon .join('\n ')} 178dc51e206SEvan Bacon Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab. 179dc51e206SEvan Bacon You can see a red colored folder containing the original source code from your bundle. 180dc51e206SEvan Bacon `; 181dc51e206SEvan Bacon 182dc51e206SEvan Bacon await fs.writeFile(path.join(outputDir, 'debug.html'), contents); 183dc51e206SEvan Bacon return contents; 184dc51e206SEvan Bacon} 185