xref: /expo/packages/@expo/cli/src/export/exportApp.ts (revision 2fd75d6d)
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 { createBundlesAsync } from './createBundles';
10import { exportAssetsAsync } from './exportAssets';
11import { getPublicExpoManifestAsync } from './getPublicExpoManifest';
12import { printBundleSizes } from './printBundleSizes';
13import { Options } from './resolveOptions';
14import {
15  writeAssetMapAsync,
16  writeBundlesAsync,
17  writeDebugHtmlAsync,
18  writeMetadataJsonAsync,
19  writeSourceMapsAsync,
20} from './writeContents';
21
22/**
23 * The structure of the outputDir will be:
24 *
25 * ```
26 * ├── assets
27 * │   └── *
28 * ├── bundles
29 * │   ├── android-01ee6e3ab3e8c16a4d926c91808d5320.js
30 * │   └── ios-ee8206cc754d3f7aa9123b7f909d94ea.js
31 * └── metadata.json
32 * ```
33 */
34export async function exportAppAsync(
35  projectRoot: string,
36  {
37    platforms,
38    outputDir,
39    clear,
40    dev,
41    dumpAssetmap,
42    dumpSourcemap,
43  }: Pick<Options, 'dumpAssetmap' | 'dumpSourcemap' | 'dev' | 'clear' | 'outputDir' | 'platforms'>
44): Promise<void> {
45  const exp = await getPublicExpoManifestAsync(projectRoot);
46
47  const publicPath = path.resolve(projectRoot, env.EXPO_PUBLIC_FOLDER);
48
49  const outputPath = path.resolve(projectRoot, outputDir);
50  const assetsPath = path.join(outputPath, 'assets');
51  const bundlesPath = path.join(outputPath, 'bundles');
52
53  await Promise.all([assetsPath, bundlesPath].map(ensureDirectoryAsync));
54
55  await copyPublicFolderAsync(publicPath, outputDir);
56
57  // Run metro bundler and create the JS bundles/source maps.
58  const bundles = await createBundlesAsync(
59    projectRoot,
60    { resetCache: !!clear },
61    {
62      platforms,
63      dev,
64      useDevServer: true,
65      // TODO: Disable source map generation if we aren't outputting them.
66    }
67  );
68
69  // Log bundle size info to the user
70  printBundleSizes(bundles);
71
72  // Write the JS bundles to disk, and get the bundle file names (this could change with async chunk loading support).
73  const { hashes, fileNames } = await writeBundlesAsync({ bundles, outputDir: bundlesPath });
74
75  Log.log('Finished saving JS Bundles');
76
77  if (fileNames.web) {
78    // If web exists, then write the template HTML file.
79    await fs.promises.writeFile(
80      path.join(outputPath, 'index.html'),
81      await createTemplateHtmlFromExpoConfigAsync(projectRoot, {
82        scripts: [`/bundles/${fileNames.web}`],
83      })
84    );
85
86    // Save assets like a typical bundler, preserving the file paths on web.
87    const saveAssets = importCliSaveAssetsFromProject(projectRoot);
88    await Promise.all(
89      Object.entries(bundles).map(([platform, bundle]) => {
90        return saveAssets(
91          // @ts-expect-error: tolerable type mismatches: unused `readonly` (common in Metro) and `undefined` instead of `null`.
92          bundle.assets,
93          platform,
94          outputPath
95        );
96      })
97    );
98  }
99
100  const { assets } = await exportAssetsAsync(projectRoot, {
101    exp,
102    outputDir: outputPath,
103    bundles,
104  });
105
106  if (dumpAssetmap) {
107    Log.log('Dumping asset map');
108    await writeAssetMapAsync({ outputDir: outputPath, assets });
109  }
110
111  // build source maps
112  if (dumpSourcemap) {
113    Log.log('Dumping source maps');
114    await writeSourceMapsAsync({
115      bundles,
116      hashes,
117      outputDir: bundlesPath,
118      fileNames,
119    });
120
121    Log.log('Preparing additional debugging files');
122    // If we output source maps, then add a debug HTML file which the user can open in
123    // the web browser to inspect the output like web.
124    await writeDebugHtmlAsync({
125      outputDir: outputPath,
126      fileNames,
127    });
128  }
129
130  // Generate a `metadata.json` and the export is complete.
131  await writeMetadataJsonAsync({ outputDir, bundles, fileNames });
132}
133
134/**
135 * Copy the contents of the public folder into the output folder.
136 * This enables users to add static files like `favicon.ico` or `serve.json`.
137 *
138 * The contents of this folder are completely universal since they refer to
139 * static network requests which fall outside the scope of React Native's magic
140 * platform resolution patterns.
141 */
142async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) {
143  if (fs.existsSync(publicFolder)) {
144    await copyAsync(publicFolder, outputFolder);
145  }
146}
147