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