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