1import fs from 'fs'; 2import path from 'path'; 3 4import * as Log from '../log'; 5import { importCliSaveAssetsFromProject } from '../start/server/metro/resolveFromProject'; 6import { createTemplateHtmlFromExpoConfigAsync } from '../start/server/webTemplate'; 7import { copyAsync, ensureDirectoryAsync } from '../utils/dir'; 8import { env } from '../utils/env'; 9import { createBundlesAsync } from './createBundles'; 10import { exportAssetsAsync } from './exportAssets'; 11import { getPublicExpoManifestAsync } from './getPublicExpoManifest'; 12import { printBundleSizes } from './printBundleSizes'; 13import { Options } from './resolveOptions'; 14import { 15 writeAssetMapAsync, 16 writeBundlesAsync, 17 writeDebugHtmlAsync, 18 writeMetadataJsonAsync, 19 writeSourceMapsAsync, 20} from './writeContents'; 21 22/** 23 * The structure of the outputDir will be: 24 * 25 * ``` 26 * ├── assets 27 * │ └── * 28 * ├── bundles 29 * │ ├── android-01ee6e3ab3e8c16a4d926c91808d5320.js 30 * │ └── ios-ee8206cc754d3f7aa9123b7f909d94ea.js 31 * └── metadata.json 32 * ``` 33 */ 34export async function exportAppAsync( 35 projectRoot: string, 36 { 37 platforms, 38 outputDir, 39 clear, 40 dev, 41 dumpAssetmap, 42 dumpSourcemap, 43 }: Pick<Options, 'dumpAssetmap' | 'dumpSourcemap' | 'dev' | 'clear' | 'outputDir' | 'platforms'> 44): Promise<void> { 45 const exp = await getPublicExpoManifestAsync(projectRoot); 46 47 const publicPath = path.resolve(projectRoot, env.EXPO_PUBLIC_FOLDER); 48 49 const outputPath = path.resolve(projectRoot, outputDir); 50 const assetsPath = path.join(outputPath, 'assets'); 51 const bundlesPath = path.join(outputPath, 'bundles'); 52 53 await Promise.all([assetsPath, bundlesPath].map(ensureDirectoryAsync)); 54 55 await copyPublicFolderAsync(publicPath, outputDir); 56 57 // Run metro bundler and create the JS bundles/source maps. 58 const bundles = await createBundlesAsync( 59 projectRoot, 60 { resetCache: !!clear }, 61 { 62 platforms, 63 dev, 64 // TODO: Disable source map generation if we aren't outputting them. 65 } 66 ); 67 68 // Log bundle size info to the user 69 printBundleSizes( 70 Object.fromEntries( 71 Object.entries(bundles).map(([key, value]) => { 72 if (!dumpSourcemap) { 73 return [ 74 key, 75 { 76 ...value, 77 // Remove source maps from the bundles if they aren't going to be written. 78 map: undefined, 79 }, 80 ]; 81 } 82 83 return [key, value]; 84 }) 85 ) 86 ); 87 88 // Write the JS bundles to disk, and get the bundle file names (this could change with async chunk loading support). 89 const { hashes, fileNames } = await writeBundlesAsync({ bundles, outputDir: bundlesPath }); 90 91 Log.log('Finished saving JS Bundles'); 92 93 if (fileNames.web) { 94 // If web exists, then write the template HTML file. 95 await fs.promises.writeFile( 96 path.join(outputPath, 'index.html'), 97 await createTemplateHtmlFromExpoConfigAsync(projectRoot, { 98 scripts: [`/bundles/${fileNames.web}`], 99 }) 100 ); 101 102 // Save assets like a typical bundler, preserving the file paths on web. 103 const saveAssets = importCliSaveAssetsFromProject(projectRoot); 104 await Promise.all( 105 Object.entries(bundles).map(([platform, bundle]) => { 106 return saveAssets( 107 // @ts-expect-error: tolerable type mismatches: unused `readonly` (common in Metro) and `undefined` instead of `null`. 108 bundle.assets, 109 platform, 110 outputPath, 111 undefined 112 ); 113 }) 114 ); 115 } 116 117 const { assets } = await exportAssetsAsync(projectRoot, { 118 exp, 119 outputDir: outputPath, 120 bundles, 121 }); 122 123 if (dumpAssetmap) { 124 Log.log('Dumping asset map'); 125 await writeAssetMapAsync({ outputDir: outputPath, assets }); 126 } 127 128 // build source maps 129 if (dumpSourcemap) { 130 Log.log('Dumping source maps'); 131 await writeSourceMapsAsync({ 132 bundles, 133 hashes, 134 outputDir: bundlesPath, 135 fileNames, 136 }); 137 138 Log.log('Preparing additional debugging files'); 139 // If we output source maps, then add a debug HTML file which the user can open in 140 // the web browser to inspect the output like web. 141 await writeDebugHtmlAsync({ 142 outputDir: outputPath, 143 fileNames, 144 }); 145 } 146 147 // Generate a `metadata.json` and the export is complete. 148 await writeMetadataJsonAsync({ outputDir: outputPath, bundles, fileNames }); 149} 150 151/** 152 * Copy the contents of the public folder into the output folder. 153 * This enables users to add static files like `favicon.ico` or `serve.json`. 154 * 155 * The contents of this folder are completely universal since they refer to 156 * static network requests which fall outside the scope of React Native's magic 157 * platform resolution patterns. 158 */ 159async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) { 160 if (fs.existsSync(publicFolder)) { 161 await copyAsync(publicFolder, outputFolder); 162 } 163} 164