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 { createBundlesAsync } from './createBundles'; 10import { exportAssetsAsync } from './exportAssets'; 11import { getPublicExpoManifestAsync } from './getPublicExpoManifest'; 12import { printBundleSizes } from './printBundleSizes'; 13import { Options } from './resolveOptions'; 14import { 15 writeAssetMapAsync, 16 writeBundlesAsync, 17 writeDebugHtmlAsync, 18 writeMetadataJsonAsync, 19 writeSourceMapsAsync, 20} from './writeContents'; 21 22/** 23 * The structure of the outputDir will be: 24 * 25 * ``` 26 * ├── assets 27 * │ └── * 28 * ├── bundles 29 * │ ├── android-01ee6e3ab3e8c16a4d926c91808d5320.js 30 * │ └── ios-ee8206cc754d3f7aa9123b7f909d94ea.js 31 * └── metadata.json 32 * ``` 33 */ 34export async function exportAppAsync( 35 projectRoot: string, 36 { 37 platforms, 38 outputDir, 39 clear, 40 dev, 41 dumpAssetmap, 42 dumpSourcemap, 43 }: Pick<Options, 'dumpAssetmap' | 'dumpSourcemap' | 'dev' | 'clear' | 'outputDir' | 'platforms'> 44): Promise<void> { 45 const exp = await getPublicExpoManifestAsync(projectRoot); 46 47 const publicPath = path.resolve(projectRoot, env.EXPO_PUBLIC_FOLDER); 48 49 const outputPath = path.resolve(projectRoot, outputDir); 50 const assetsPath = path.join(outputPath, 'assets'); 51 const bundlesPath = path.join(outputPath, 'bundles'); 52 53 await Promise.all([assetsPath, bundlesPath].map(ensureDirectoryAsync)); 54 55 await copyPublicFolderAsync(publicPath, outputDir); 56 57 // Run metro bundler and create the JS bundles/source maps. 58 const bundles = await createBundlesAsync( 59 projectRoot, 60 { resetCache: !!clear }, 61 { 62 platforms, 63 dev, 64 useDevServer: true, 65 // TODO: Disable source map generation if we aren't outputting them. 66 } 67 ); 68 69 // Log bundle size info to the user 70 printBundleSizes(bundles); 71 72 // Write the JS bundles to disk, and get the bundle file names (this could change with async chunk loading support). 73 const { hashes, fileNames } = await writeBundlesAsync({ bundles, outputDir: bundlesPath }); 74 75 Log.log('Finished saving JS Bundles'); 76 77 if (fileNames.web) { 78 // If web exists, then write the template HTML file. 79 await fs.promises.writeFile( 80 path.join(outputPath, 'index.html'), 81 await createTemplateHtmlFromExpoConfigAsync(projectRoot, { 82 scripts: [`/bundles/${fileNames.web}`], 83 }) 84 ); 85 86 // Save assets like a typical bundler, preserving the file paths on web. 87 const saveAssets = importCliSaveAssetsFromProject(projectRoot); 88 await Promise.all( 89 Object.entries(bundles).map(([platform, bundle]) => { 90 return saveAssets( 91 // @ts-expect-error: tolerable type mismatches: unused `readonly` (common in Metro) and `undefined` instead of `null`. 92 bundle.assets, 93 platform, 94 outputPath 95 ); 96 }) 97 ); 98 } 99 100 const { assets } = await exportAssetsAsync(projectRoot, { 101 exp, 102 outputDir: outputPath, 103 bundles, 104 }); 105 106 if (dumpAssetmap) { 107 Log.log('Dumping asset map'); 108 await writeAssetMapAsync({ outputDir: outputPath, assets }); 109 } 110 111 // build source maps 112 if (dumpSourcemap) { 113 Log.log('Dumping source maps'); 114 await writeSourceMapsAsync({ 115 bundles, 116 hashes, 117 outputDir: bundlesPath, 118 fileNames, 119 }); 120 121 Log.log('Preparing additional debugging files'); 122 // If we output source maps, then add a debug HTML file which the user can open in 123 // the web browser to inspect the output like web. 124 await writeDebugHtmlAsync({ 125 outputDir: outputPath, 126 fileNames, 127 }); 128 } 129 130 // Generate a `metadata.json` and the export is complete. 131 await writeMetadataJsonAsync({ outputDir, bundles, fileNames }); 132} 133 134/** 135 * Copy the contents of the public folder into the output folder. 136 * This enables users to add static files like `favicon.ico` or `serve.json`. 137 * 138 * The contents of this folder are completely universal since they refer to 139 * static network requests which fall outside the scope of React Native's magic 140 * platform resolution patterns. 141 */ 142async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) { 143 if (fs.existsSync(publicFolder)) { 144 await copyAsync(publicFolder, outputFolder); 145 } 146} 147