17c98c357SEvan Baconimport chalk from 'chalk'; 26d6b81f9SEvan Baconimport fs from 'fs'; 3dc51e206SEvan Baconimport path from 'path'; 4dc51e206SEvan Bacon 5dc51e206SEvan Baconimport { createBundlesAsync } from './createBundles'; 69b2597baSEvan Baconimport { exportAssetsAsync, exportCssAssetsAsync } from './exportAssets'; 70a6ddb20SEvan Baconimport { unstable_exportStaticAsync } from './exportStaticAsync'; 842637653SEvan Baconimport { getVirtualFaviconAssetsAsync } from './favicon'; 9dc51e206SEvan Baconimport { getPublicExpoManifestAsync } from './getPublicExpoManifest'; 107c98c357SEvan Baconimport { persistMetroAssetsAsync } from './persistMetroAssets'; 11dc51e206SEvan Baconimport { printBundleSizes } from './printBundleSizes'; 12dc51e206SEvan Baconimport { Options } from './resolveOptions'; 13dc51e206SEvan Baconimport { 14dc51e206SEvan Bacon writeAssetMapAsync, 15dc51e206SEvan Bacon writeBundlesAsync, 16dc51e206SEvan Bacon writeDebugHtmlAsync, 17dc51e206SEvan Bacon writeMetadataJsonAsync, 18dc51e206SEvan Bacon writeSourceMapsAsync, 19dc51e206SEvan Bacon} from './writeContents'; 208a424bebSJames Ideimport * as Log from '../log'; 218a424bebSJames Ideimport { createTemplateHtmlFromExpoConfigAsync } from '../start/server/webTemplate'; 228a424bebSJames Ideimport { copyAsync, ensureDirectoryAsync } from '../utils/dir'; 238a424bebSJames Ideimport { env } from '../utils/env'; 248a424bebSJames Ideimport { setNodeEnv } from '../utils/nodeEnv'; 25dc51e206SEvan Bacon 26dc51e206SEvan Bacon/** 27dc51e206SEvan Bacon * The structure of the outputDir will be: 28dc51e206SEvan Bacon * 29dc51e206SEvan Bacon * ``` 30dc51e206SEvan Bacon * ├── assets 31dc51e206SEvan Bacon * │ └── * 32dc51e206SEvan Bacon * ├── bundles 33dc51e206SEvan Bacon * │ ├── android-01ee6e3ab3e8c16a4d926c91808d5320.js 34dc51e206SEvan Bacon * │ └── ios-ee8206cc754d3f7aa9123b7f909d94ea.js 35dc51e206SEvan Bacon * └── metadata.json 36dc51e206SEvan Bacon * ``` 37dc51e206SEvan Bacon */ 38dc51e206SEvan Baconexport async function exportAppAsync( 39dc51e206SEvan Bacon projectRoot: string, 40dc51e206SEvan Bacon { 41dc51e206SEvan Bacon platforms, 42dc51e206SEvan Bacon outputDir, 43dc51e206SEvan Bacon clear, 44dc51e206SEvan Bacon dev, 45dc51e206SEvan Bacon dumpAssetmap, 46dc51e206SEvan Bacon dumpSourcemap, 471a3d836eSEvan Bacon minify, 481a3d836eSEvan Bacon }: Pick< 491a3d836eSEvan Bacon Options, 501a3d836eSEvan Bacon 'dumpAssetmap' | 'dumpSourcemap' | 'dev' | 'clear' | 'outputDir' | 'platforms' | 'minify' 511a3d836eSEvan Bacon > 52dc51e206SEvan Bacon): Promise<void> { 532dd43328SEvan Bacon setNodeEnv(dev ? 'development' : 'production'); 546a750d06SEvan Bacon require('@expo/env').load(projectRoot); 552dd43328SEvan Bacon 56*2d4e7de9SEvan Bacon const exp = await getPublicExpoManifestAsync(projectRoot, { 57*2d4e7de9SEvan Bacon // Web doesn't require validation. 58*2d4e7de9SEvan Bacon skipValidation: platforms.length === 1 && platforms[0] === 'web', 59*2d4e7de9SEvan Bacon }); 60dc51e206SEvan Bacon 6146f023faSEvan Bacon const useServerRendering = ['static', 'server'].includes(exp.web?.output ?? ''); 627c98c357SEvan Bacon const basePath = (exp.experiments?.basePath?.replace(/\/+$/, '') ?? '').trim(); 637c98c357SEvan Bacon 647c98c357SEvan Bacon // Print out logs 657c98c357SEvan Bacon if (basePath) { 667c98c357SEvan Bacon Log.log(); 677c98c357SEvan Bacon Log.log(chalk.gray`Using (experimental) base path: ${basePath}`); 687c98c357SEvan Bacon // Warn if not using an absolute path. 697c98c357SEvan Bacon if (!basePath.startsWith('/')) { 707c98c357SEvan Bacon Log.log( 717c98c357SEvan Bacon chalk.yellow` Base path does not start with a slash. Requests will not be absolute.` 727c98c357SEvan Bacon ); 737c98c357SEvan Bacon } 747c98c357SEvan Bacon } 759580591fSEvan Bacon 766d6b81f9SEvan Bacon const publicPath = path.resolve(projectRoot, env.EXPO_PUBLIC_FOLDER); 776d6b81f9SEvan Bacon 78dc51e206SEvan Bacon const outputPath = path.resolve(projectRoot, outputDir); 790a6ddb20SEvan Bacon const staticFolder = outputPath; 800a6ddb20SEvan Bacon const assetsPath = path.join(staticFolder, 'assets'); 810a6ddb20SEvan Bacon const bundlesPath = path.join(staticFolder, 'bundles'); 82dc51e206SEvan Bacon 83dc51e206SEvan Bacon await Promise.all([assetsPath, bundlesPath].map(ensureDirectoryAsync)); 84dc51e206SEvan Bacon 850a6ddb20SEvan Bacon await copyPublicFolderAsync(publicPath, staticFolder); 866d6b81f9SEvan Bacon 87dc51e206SEvan Bacon // Run metro bundler and create the JS bundles/source maps. 88dc51e206SEvan Bacon const bundles = await createBundlesAsync( 89dc51e206SEvan Bacon projectRoot, 90dc51e206SEvan Bacon { resetCache: !!clear }, 91dc51e206SEvan Bacon { 92dc51e206SEvan Bacon platforms, 931a3d836eSEvan Bacon minify, 949580591fSEvan Bacon // TODO: Breaks asset exports 9546f023faSEvan Bacon // platforms: useServerRendering 9646f023faSEvan Bacon // ? platforms.filter((platform) => platform !== 'web') 9746f023faSEvan Bacon // : platforms, 98dc51e206SEvan Bacon dev, 99dc51e206SEvan Bacon // TODO: Disable source map generation if we aren't outputting them. 100dc51e206SEvan Bacon } 101dc51e206SEvan Bacon ); 102dc51e206SEvan Bacon 1039580591fSEvan Bacon const bundleEntries = Object.entries(bundles); 1049580591fSEvan Bacon if (bundleEntries.length) { 105dc51e206SEvan Bacon // Log bundle size info to the user 106e330c216SEvan Bacon printBundleSizes( 107e330c216SEvan Bacon Object.fromEntries( 1089580591fSEvan Bacon bundleEntries.map(([key, value]) => { 109e330c216SEvan Bacon if (!dumpSourcemap) { 110e330c216SEvan Bacon return [ 111e330c216SEvan Bacon key, 112e330c216SEvan Bacon { 113e330c216SEvan Bacon ...value, 114e330c216SEvan Bacon // Remove source maps from the bundles if they aren't going to be written. 115e330c216SEvan Bacon map: undefined, 116e330c216SEvan Bacon }, 117e330c216SEvan Bacon ]; 118e330c216SEvan Bacon } 119e330c216SEvan Bacon 120e330c216SEvan Bacon return [key, value]; 121e330c216SEvan Bacon }) 122e330c216SEvan Bacon ) 123e330c216SEvan Bacon ); 1249580591fSEvan Bacon } 125dc51e206SEvan Bacon 126dc51e206SEvan Bacon // Write the JS bundles to disk, and get the bundle file names (this could change with async chunk loading support). 1270769e63bSEvan Bacon const { hashes, fileNames } = await writeBundlesAsync({ 1280769e63bSEvan Bacon bundles, 12946f023faSEvan Bacon useServerRendering, 1300769e63bSEvan Bacon outputDir: bundlesPath, 1310769e63bSEvan Bacon }); 132dc51e206SEvan Bacon 133dc51e206SEvan Bacon Log.log('Finished saving JS Bundles'); 1349580591fSEvan Bacon 1359580591fSEvan Bacon if (platforms.includes('web')) { 13646f023faSEvan Bacon if (useServerRendering) { 1370a6ddb20SEvan Bacon await unstable_exportStaticAsync(projectRoot, { 1380a6ddb20SEvan Bacon outputDir: outputPath, 1391a3d836eSEvan Bacon minify, 1407c98c357SEvan Bacon basePath, 141573b0ea7SEvan Bacon includeMaps: dumpSourcemap, 14246f023faSEvan Bacon // @ts-expect-error: server not on type yet 14346f023faSEvan Bacon exportServer: exp.web?.output === 'server', 1440a6ddb20SEvan Bacon }); 1450a6ddb20SEvan Bacon Log.log('Finished saving static files'); 1460a6ddb20SEvan Bacon } else { 1479580591fSEvan Bacon const cssLinks = await exportCssAssetsAsync({ 1489580591fSEvan Bacon outputDir, 1499580591fSEvan Bacon bundles, 1507c98c357SEvan Bacon basePath, 1519580591fSEvan Bacon }); 15242637653SEvan Bacon let html = await createTemplateHtmlFromExpoConfigAsync(projectRoot, { 1537c98c357SEvan Bacon scripts: [`${basePath}/bundles/${fileNames.web}`], 1549b2597baSEvan Bacon cssLinks, 15542637653SEvan Bacon }); 15642637653SEvan Bacon // Add the favicon assets to the HTML. 1577c98c357SEvan Bacon const modifyHtml = await getVirtualFaviconAssetsAsync(projectRoot, { 1587c98c357SEvan Bacon outputDir, 1597c98c357SEvan Bacon basePath, 1607c98c357SEvan Bacon }); 16142637653SEvan Bacon if (modifyHtml) { 16242637653SEvan Bacon html = modifyHtml(html); 16342637653SEvan Bacon } 16442637653SEvan Bacon // Generate SPA-styled HTML file. 16542637653SEvan Bacon // If web exists, then write the template HTML file. 16642637653SEvan Bacon await fs.promises.writeFile(path.join(staticFolder, 'index.html'), html); 1670a6ddb20SEvan Bacon } 1686d6b81f9SEvan Bacon 1690769e63bSEvan Bacon // TODO: Use a different mechanism for static web. 1700769e63bSEvan Bacon if (bundles.web) { 1716d6b81f9SEvan Bacon // Save assets like a typical bundler, preserving the file paths on web. 1727c98c357SEvan Bacon // TODO: Update React Native Web to support loading files from asset hashes. 1737c98c357SEvan Bacon await persistMetroAssetsAsync(bundles.web.assets, { 1747c98c357SEvan Bacon platform: 'web', 1757c98c357SEvan Bacon outputDirectory: staticFolder, 1767c98c357SEvan Bacon basePath, 1777c98c357SEvan Bacon }); 1786d6b81f9SEvan Bacon } 1790769e63bSEvan Bacon } 1806d6b81f9SEvan Bacon 1810769e63bSEvan Bacon // Can be empty during web-only SSG. 1820769e63bSEvan Bacon // TODO: Use same asset system across platforms again. 1830769e63bSEvan Bacon if (Object.keys(fileNames).length) { 184dc51e206SEvan Bacon const { assets } = await exportAssetsAsync(projectRoot, { 185dc51e206SEvan Bacon exp, 1860a6ddb20SEvan Bacon outputDir: staticFolder, 187dc51e206SEvan Bacon bundles, 188dc51e206SEvan Bacon }); 189dc51e206SEvan Bacon 190dc51e206SEvan Bacon if (dumpAssetmap) { 191dc51e206SEvan Bacon Log.log('Dumping asset map'); 1920a6ddb20SEvan Bacon await writeAssetMapAsync({ outputDir: staticFolder, assets }); 193dc51e206SEvan Bacon } 194dc51e206SEvan Bacon // build source maps 195dc51e206SEvan Bacon if (dumpSourcemap) { 196dc51e206SEvan Bacon Log.log('Dumping source maps'); 197dc51e206SEvan Bacon await writeSourceMapsAsync({ 198dc51e206SEvan Bacon bundles, 199dc51e206SEvan Bacon hashes, 200dc51e206SEvan Bacon outputDir: bundlesPath, 201dc51e206SEvan Bacon fileNames, 202dc51e206SEvan Bacon }); 203dc51e206SEvan Bacon 204dc51e206SEvan Bacon Log.log('Preparing additional debugging files'); 205dc51e206SEvan Bacon // If we output source maps, then add a debug HTML file which the user can open in 206dc51e206SEvan Bacon // the web browser to inspect the output like web. 207dc51e206SEvan Bacon await writeDebugHtmlAsync({ 2080a6ddb20SEvan Bacon outputDir: staticFolder, 209dc51e206SEvan Bacon fileNames, 210dc51e206SEvan Bacon }); 211dc51e206SEvan Bacon } 212dc51e206SEvan Bacon 213dc51e206SEvan Bacon // Generate a `metadata.json` and the export is complete. 2140a6ddb20SEvan Bacon await writeMetadataJsonAsync({ outputDir: staticFolder, bundles, fileNames }); 215dc51e206SEvan Bacon } 2160769e63bSEvan Bacon} 2176d6b81f9SEvan Bacon 2186d6b81f9SEvan Bacon/** 2196d6b81f9SEvan Bacon * Copy the contents of the public folder into the output folder. 2206d6b81f9SEvan Bacon * This enables users to add static files like `favicon.ico` or `serve.json`. 2216d6b81f9SEvan Bacon * 2226d6b81f9SEvan Bacon * The contents of this folder are completely universal since they refer to 2236d6b81f9SEvan Bacon * static network requests which fall outside the scope of React Native's magic 2246d6b81f9SEvan Bacon * platform resolution patterns. 2256d6b81f9SEvan Bacon */ 2266d6b81f9SEvan Baconasync function copyPublicFolderAsync(publicFolder: string, outputFolder: string) { 2276d6b81f9SEvan Bacon if (fs.existsSync(publicFolder)) { 2286d6b81f9SEvan Bacon await copyAsync(publicFolder, outputFolder); 2296d6b81f9SEvan Bacon } 2306d6b81f9SEvan Bacon} 231