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