1import fs from 'fs'; 2import path from 'path'; 3 4import { createBundlesAsync } from './createBundles'; 5import { exportAssetsAsync, exportCssAssetsAsync } from './exportAssets'; 6import { unstable_exportStaticAsync } from './exportStaticAsync'; 7import { getVirtualFaviconAssetsAsync } from './favicon'; 8import { getPublicExpoManifestAsync } from './getPublicExpoManifest'; 9import { printBundleSizes } from './printBundleSizes'; 10import { Options } from './resolveOptions'; 11import { 12 writeAssetMapAsync, 13 writeBundlesAsync, 14 writeDebugHtmlAsync, 15 writeMetadataJsonAsync, 16 writeSourceMapsAsync, 17} from './writeContents'; 18import * as Log from '../log'; 19import { importCliSaveAssetsFromProject } from '../start/server/metro/resolveFromProject'; 20import { createTemplateHtmlFromExpoConfigAsync } from '../start/server/webTemplate'; 21import { copyAsync, ensureDirectoryAsync } from '../utils/dir'; 22import { env } from '../utils/env'; 23import { setNodeEnv } from '../utils/nodeEnv'; 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({ 109 bundles, 110 useWebSSG, 111 outputDir: bundlesPath, 112 }); 113 114 Log.log('Finished saving JS Bundles'); 115 116 if (platforms.includes('web')) { 117 if (useWebSSG) { 118 await unstable_exportStaticAsync(projectRoot, { 119 outputDir: outputPath, 120 // TODO: Expose 121 minify, 122 includeMaps: dumpSourcemap, 123 }); 124 Log.log('Finished saving static files'); 125 } else { 126 const cssLinks = await exportCssAssetsAsync({ 127 outputDir, 128 bundles, 129 }); 130 let html = await createTemplateHtmlFromExpoConfigAsync(projectRoot, { 131 scripts: [`/bundles/${fileNames.web}`], 132 cssLinks, 133 }); 134 // Add the favicon assets to the HTML. 135 const modifyHtml = await getVirtualFaviconAssetsAsync(projectRoot, outputDir); 136 if (modifyHtml) { 137 html = modifyHtml(html); 138 } 139 // Generate SPA-styled HTML file. 140 // If web exists, then write the template HTML file. 141 await fs.promises.writeFile(path.join(staticFolder, 'index.html'), html); 142 } 143 144 // TODO: Use a different mechanism for static web. 145 if (bundles.web) { 146 // Save assets like a typical bundler, preserving the file paths on web. 147 const saveAssets = importCliSaveAssetsFromProject(projectRoot); 148 await Promise.all( 149 Object.entries(bundles).map(([platform, bundle]) => { 150 return saveAssets(bundle.assets, platform, staticFolder, undefined); 151 }) 152 ); 153 } 154 } 155 156 // Can be empty during web-only SSG. 157 // TODO: Use same asset system across platforms again. 158 if (Object.keys(fileNames).length) { 159 const { assets } = await exportAssetsAsync(projectRoot, { 160 exp, 161 outputDir: staticFolder, 162 bundles, 163 }); 164 165 if (dumpAssetmap) { 166 Log.log('Dumping asset map'); 167 await writeAssetMapAsync({ outputDir: staticFolder, assets }); 168 } 169 // build source maps 170 if (dumpSourcemap) { 171 Log.log('Dumping source maps'); 172 await writeSourceMapsAsync({ 173 bundles, 174 hashes, 175 outputDir: bundlesPath, 176 fileNames, 177 }); 178 179 Log.log('Preparing additional debugging files'); 180 // If we output source maps, then add a debug HTML file which the user can open in 181 // the web browser to inspect the output like web. 182 await writeDebugHtmlAsync({ 183 outputDir: staticFolder, 184 fileNames, 185 }); 186 } 187 188 // Generate a `metadata.json` and the export is complete. 189 await writeMetadataJsonAsync({ outputDir: staticFolder, bundles, fileNames }); 190 } 191} 192 193/** 194 * Copy the contents of the public folder into the output folder. 195 * This enables users to add static files like `favicon.ico` or `serve.json`. 196 * 197 * The contents of this folder are completely universal since they refer to 198 * static network requests which fall outside the scope of React Native's magic 199 * platform resolution patterns. 200 */ 201async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) { 202 if (fs.existsSync(publicFolder)) { 203 await copyAsync(publicFolder, outputFolder); 204 } 205} 206