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