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