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