xref: /expo/packages/@expo/cli/src/export/exportApp.ts (revision 453643fe)
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 { setNodeEnv } from '../utils/nodeEnv';
10import { createBundlesAsync } from './createBundles';
11import { exportAssetsAsync, exportCssAssetsAsync } from './exportAssets';
12import { unstable_exportStaticAsync } from './exportStaticAsync';
13import { getPublicExpoManifestAsync } from './getPublicExpoManifest';
14import { printBundleSizes } from './printBundleSizes';
15import { Options } from './resolveOptions';
16import {
17  writeAssetMapAsync,
18  writeBundlesAsync,
19  writeDebugHtmlAsync,
20  writeMetadataJsonAsync,
21  writeSourceMapsAsync,
22} from './writeContents';
23
24/**
25 * The structure of the outputDir will be:
26 *
27 * ```
28 * ├── assets
29 * │   └── *
30 * ├── bundles
31 * │   ├── android-01ee6e3ab3e8c16a4d926c91808d5320.js
32 * │   └── ios-ee8206cc754d3f7aa9123b7f909d94ea.js
33 * └── metadata.json
34 * ```
35 */
36export async function exportAppAsync(
37  projectRoot: string,
38  {
39    platforms,
40    outputDir,
41    clear,
42    dev,
43    dumpAssetmap,
44    dumpSourcemap,
45    minify,
46  }: Pick<
47    Options,
48    'dumpAssetmap' | 'dumpSourcemap' | 'dev' | 'clear' | 'outputDir' | 'platforms' | 'minify'
49  >
50): Promise<void> {
51  setNodeEnv(dev ? 'development' : 'production');
52  require('@expo/env').load(projectRoot);
53
54  const exp = await getPublicExpoManifestAsync(projectRoot);
55
56  const useWebSSG = exp.web?.output === 'static';
57
58  const publicPath = path.resolve(projectRoot, env.EXPO_PUBLIC_FOLDER);
59
60  const outputPath = path.resolve(projectRoot, outputDir);
61  const staticFolder = outputPath;
62  const assetsPath = path.join(staticFolder, 'assets');
63  const bundlesPath = path.join(staticFolder, 'bundles');
64
65  await Promise.all([assetsPath, bundlesPath].map(ensureDirectoryAsync));
66
67  await copyPublicFolderAsync(publicPath, staticFolder);
68
69  // Run metro bundler and create the JS bundles/source maps.
70  const bundles = await createBundlesAsync(
71    projectRoot,
72    { resetCache: !!clear },
73    {
74      platforms,
75      minify,
76      // TODO: Breaks asset exports
77      // platforms: useWebSSG ? platforms.filter((platform) => platform !== 'web') : platforms,
78      dev,
79      // TODO: Disable source map generation if we aren't outputting them.
80    }
81  );
82
83  const bundleEntries = Object.entries(bundles);
84  if (bundleEntries.length) {
85    // Log bundle size info to the user
86    printBundleSizes(
87      Object.fromEntries(
88        bundleEntries.map(([key, value]) => {
89          if (!dumpSourcemap) {
90            return [
91              key,
92              {
93                ...value,
94                // Remove source maps from the bundles if they aren't going to be written.
95                map: undefined,
96              },
97            ];
98          }
99
100          return [key, value];
101        })
102      )
103    );
104  }
105
106  // Write the JS bundles to disk, and get the bundle file names (this could change with async chunk loading support).
107  const { hashes, fileNames } = await writeBundlesAsync({ bundles, outputDir: bundlesPath });
108
109  Log.log('Finished saving JS Bundles');
110
111  if (platforms.includes('web')) {
112    if (useWebSSG) {
113      await unstable_exportStaticAsync(projectRoot, {
114        outputDir: outputPath,
115        // TODO: Expose
116        minify,
117      });
118      Log.log('Finished saving static files');
119    } else {
120      const cssLinks = await exportCssAssetsAsync({
121        outputDir,
122        bundles,
123      });
124      // Generate SPA-styled HTML file.
125      // If web exists, then write the template HTML file.
126      await fs.promises.writeFile(
127        path.join(staticFolder, 'index.html'),
128        await createTemplateHtmlFromExpoConfigAsync(projectRoot, {
129          scripts: [`/bundles/${fileNames.web}`],
130          cssLinks,
131        })
132      );
133    }
134
135    // Save assets like a typical bundler, preserving the file paths on web.
136    const saveAssets = importCliSaveAssetsFromProject(projectRoot);
137    await Promise.all(
138      Object.entries(bundles).map(([platform, bundle]) => {
139        return saveAssets(bundle.assets, platform, staticFolder, undefined);
140      })
141    );
142  }
143
144  const { assets } = await exportAssetsAsync(projectRoot, {
145    exp,
146    outputDir: staticFolder,
147    bundles,
148  });
149
150  if (dumpAssetmap) {
151    Log.log('Dumping asset map');
152    await writeAssetMapAsync({ outputDir: staticFolder, assets });
153  }
154
155  // build source maps
156  if (dumpSourcemap) {
157    Log.log('Dumping source maps');
158    await writeSourceMapsAsync({
159      bundles,
160      hashes,
161      outputDir: bundlesPath,
162      fileNames,
163    });
164
165    Log.log('Preparing additional debugging files');
166    // If we output source maps, then add a debug HTML file which the user can open in
167    // the web browser to inspect the output like web.
168    await writeDebugHtmlAsync({
169      outputDir: staticFolder,
170      fileNames,
171    });
172  }
173
174  // Generate a `metadata.json` and the export is complete.
175  await writeMetadataJsonAsync({ outputDir: staticFolder, bundles, fileNames });
176}
177
178/**
179 * Copy the contents of the public folder into the output folder.
180 * This enables users to add static files like `favicon.ico` or `serve.json`.
181 *
182 * The contents of this folder are completely universal since they refer to
183 * static network requests which fall outside the scope of React Native's magic
184 * platform resolution patterns.
185 */
186async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) {
187  if (fs.existsSync(publicFolder)) {
188    await copyAsync(publicFolder, outputFolder);
189  }
190}
191