xref: /expo/packages/@expo/cli/src/export/exportApp.ts (revision 6d147401)
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