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 useDevServer: true, 65 // TODO: Disable source map generation if we aren't outputting them. 66 } 67 ); 68 69 // Log bundle size info to the user 70 printBundleSizes( 71 Object.fromEntries( 72 Object.entries(bundles).map(([key, value]) => { 73 if (!dumpSourcemap) { 74 return [ 75 key, 76 { 77 ...value, 78 // Remove source maps from the bundles if they aren't going to be written. 79 map: undefined, 80 }, 81 ]; 82 } 83 84 return [key, value]; 85 }) 86 ) 87 ); 88 89 // Write the JS bundles to disk, and get the bundle file names (this could change with async chunk loading support). 90 const { hashes, fileNames } = await writeBundlesAsync({ bundles, outputDir: bundlesPath }); 91 92 Log.log('Finished saving JS Bundles'); 93 94 if (fileNames.web) { 95 // If web exists, then write the template HTML file. 96 await fs.promises.writeFile( 97 path.join(outputPath, 'index.html'), 98 await createTemplateHtmlFromExpoConfigAsync(projectRoot, { 99 scripts: [`/bundles/${fileNames.web}`], 100 }) 101 ); 102 103 // Save assets like a typical bundler, preserving the file paths on web. 104 const saveAssets = importCliSaveAssetsFromProject(projectRoot); 105 await Promise.all( 106 Object.entries(bundles).map(([platform, bundle]) => { 107 return saveAssets( 108 // @ts-expect-error: tolerable type mismatches: unused `readonly` (common in Metro) and `undefined` instead of `null`. 109 bundle.assets, 110 platform, 111 outputPath, 112 undefined 113 ); 114 }) 115 ); 116 } 117 118 const { assets } = await exportAssetsAsync(projectRoot, { 119 exp, 120 outputDir: outputPath, 121 bundles, 122 }); 123 124 if (dumpAssetmap) { 125 Log.log('Dumping asset map'); 126 await writeAssetMapAsync({ outputDir: outputPath, assets }); 127 } 128 129 // build source maps 130 if (dumpSourcemap) { 131 Log.log('Dumping source maps'); 132 await writeSourceMapsAsync({ 133 bundles, 134 hashes, 135 outputDir: bundlesPath, 136 fileNames, 137 }); 138 139 Log.log('Preparing additional debugging files'); 140 // If we output source maps, then add a debug HTML file which the user can open in 141 // the web browser to inspect the output like web. 142 await writeDebugHtmlAsync({ 143 outputDir: outputPath, 144 fileNames, 145 }); 146 } 147 148 // Generate a `metadata.json` and the export is complete. 149 await writeMetadataJsonAsync({ outputDir: outputPath, bundles, fileNames }); 150} 151 152/** 153 * Copy the contents of the public folder into the output folder. 154 * This enables users to add static files like `favicon.ico` or `serve.json`. 155 * 156 * The contents of this folder are completely universal since they refer to 157 * static network requests which fall outside the scope of React Native's magic 158 * platform resolution patterns. 159 */ 160async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) { 161 if (fs.existsSync(publicFolder)) { 162 await copyAsync(publicFolder, outputFolder); 163 } 164} 165