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