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 }); 123 Log.log('Finished saving static files'); 124 } else { 125 const cssLinks = await exportCssAssetsAsync({ 126 outputDir, 127 bundles, 128 }); 129 let html = await createTemplateHtmlFromExpoConfigAsync(projectRoot, { 130 scripts: [`/bundles/${fileNames.web}`], 131 cssLinks, 132 }); 133 // Add the favicon assets to the HTML. 134 const modifyHtml = await getVirtualFaviconAssetsAsync(projectRoot, outputDir); 135 if (modifyHtml) { 136 html = modifyHtml(html); 137 } 138 // Generate SPA-styled HTML file. 139 // If web exists, then write the template HTML file. 140 await fs.promises.writeFile(path.join(staticFolder, 'index.html'), html); 141 } 142 143 // TODO: Use a different mechanism for static web. 144 if (bundles.web) { 145 // Save assets like a typical bundler, preserving the file paths on web. 146 const saveAssets = importCliSaveAssetsFromProject(projectRoot); 147 await Promise.all( 148 Object.entries(bundles).map(([platform, bundle]) => { 149 return saveAssets(bundle.assets, platform, staticFolder, undefined); 150 }) 151 ); 152 } 153 } 154 155 // Can be empty during web-only SSG. 156 // TODO: Use same asset system across platforms again. 157 if (Object.keys(fileNames).length) { 158 const { assets } = await exportAssetsAsync(projectRoot, { 159 exp, 160 outputDir: staticFolder, 161 bundles, 162 }); 163 164 if (dumpAssetmap) { 165 Log.log('Dumping asset map'); 166 await writeAssetMapAsync({ outputDir: staticFolder, assets }); 167 } 168 // build source maps 169 if (dumpSourcemap) { 170 Log.log('Dumping source maps'); 171 await writeSourceMapsAsync({ 172 bundles, 173 hashes, 174 outputDir: bundlesPath, 175 fileNames, 176 }); 177 178 Log.log('Preparing additional debugging files'); 179 // If we output source maps, then add a debug HTML file which the user can open in 180 // the web browser to inspect the output like web. 181 await writeDebugHtmlAsync({ 182 outputDir: staticFolder, 183 fileNames, 184 }); 185 } 186 187 // Generate a `metadata.json` and the export is complete. 188 await writeMetadataJsonAsync({ outputDir: staticFolder, bundles, fileNames }); 189 } 190} 191 192/** 193 * Copy the contents of the public folder into the output folder. 194 * This enables users to add static files like `favicon.ico` or `serve.json`. 195 * 196 * The contents of this folder are completely universal since they refer to 197 * static network requests which fall outside the scope of React Native's magic 198 * platform resolution patterns. 199 */ 200async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) { 201 if (fs.existsSync(publicFolder)) { 202 await copyAsync(publicFolder, outputFolder); 203 } 204} 205