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