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