import { Platform } from '@expo/config'; import { BundleOutput } from '@expo/dev-server'; import crypto from 'crypto'; import fs from 'fs/promises'; import path from 'path'; import { createMetadataJson } from './createMetadataJson'; import { Asset } from './saveAssets'; /** * @param props.platform native platform for the bundle * @param props.hash crypto hash for the bundle contents * @returns filename for the JS bundle. */ function createBundleFileName({ platform, hash }: { platform: string; hash: string }): string { return `${platform}-${hash}.js`; } /** * @param bundle JS bundle as a string * @returns crypto hash for the provided bundle */ function createBundleHash(bundle: string | Uint8Array): string { return crypto.createHash('md5').update(bundle).digest('hex'); } export async function writeBundlesAsync({ bundles, outputDir, }: { bundles: Partial>>; outputDir: string; }) { const hashes: Partial> = {}; const fileNames: Partial> = {}; for (const [platform, bundleOutput] of Object.entries(bundles) as [ Platform, Pick ][]) { const bundle = bundleOutput.hermesBytecodeBundle ?? bundleOutput.code; const hash = createBundleHash(bundle); const fileName = createBundleFileName({ platform, hash }); hashes[platform] = hash; fileNames[platform] = fileName; await fs.writeFile(path.join(outputDir, fileName), bundle); } return { hashes, fileNames }; } export async function writeSourceMapsAsync({ bundles, hashes, fileNames, outputDir, }: { bundles: Record< string, Pick >; hashes?: Record; fileNames?: Record; outputDir: string; }) { return Promise.all( Object.entries(bundles).map(async ([platform, bundle]) => { const sourceMap = bundle.hermesSourcemap ?? bundle.map; const hash = hashes?.[platform] ?? createBundleHash(bundle.hermesBytecodeBundle ?? bundle.code); const mapName = `${platform}-${hash}.map`; await fs.writeFile(path.join(outputDir, mapName), sourceMap); const jsBundleFileName = fileNames?.[platform] ?? createBundleFileName({ platform, hash }); const jsPath = path.join(outputDir, jsBundleFileName); // Add correct mapping to sourcemap paths const mappingComment = `\n//# sourceMappingURL=${mapName}`; await fs.appendFile(jsPath, mappingComment); return { platform, fileName: mapName, hash, map: sourceMap, comment: mappingComment, }; }) ); } export async function writeMetadataJsonAsync({ outputDir, bundles, fileNames, }: { outputDir: string; bundles: Record>; fileNames: Record; }) { const contents = createMetadataJson({ bundles, fileNames, }); await fs.writeFile(path.join(outputDir, 'metadata.json'), JSON.stringify(contents)); return contents; } export async function writeAssetMapAsync({ outputDir, assets, }: { outputDir: string; assets: Asset[]; }) { // Convert the assets array to a k/v pair where the asset hash is the key and the asset is the value. const contents = Object.fromEntries(assets.map((asset) => [asset.hash, asset])); await fs.writeFile(path.join(outputDir, 'assetmap.json'), JSON.stringify(contents)); return contents; } export async function writeDebugHtmlAsync({ outputDir, fileNames, }: { outputDir: string; fileNames: Record; }) { // Make a debug html so user can debug their bundles const contents = ` ${Object.values(fileNames) .map((fileName) => ``) .join('\n ')} Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab. You can see a red colored folder containing the original source code from your bundle. `; await fs.writeFile(path.join(outputDir, 'debug.html'), contents); return contents; }