xref: /expo/packages/@expo/cli/src/export/exportApp.ts (revision 5bd7a65d)
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(
71    Object.fromEntries(
72      Object.entries(bundles).map(([key, value]) => {
73        if (!dumpSourcemap) {
74          return [
75            key,
76            {
77              ...value,
78              // Remove source maps from the bundles if they aren't going to be written.
79              map: undefined,
80            },
81          ];
82        }
83
84        return [key, value];
85      })
86    )
87  );
88
89  // Write the JS bundles to disk, and get the bundle file names (this could change with async chunk loading support).
90  const { hashes, fileNames } = await writeBundlesAsync({ bundles, outputDir: bundlesPath });
91
92  Log.log('Finished saving JS Bundles');
93
94  if (fileNames.web) {
95    // If web exists, then write the template HTML file.
96    await fs.promises.writeFile(
97      path.join(outputPath, 'index.html'),
98      await createTemplateHtmlFromExpoConfigAsync(projectRoot, {
99        scripts: [`/bundles/${fileNames.web}`],
100      })
101    );
102
103    // Save assets like a typical bundler, preserving the file paths on web.
104    const saveAssets = importCliSaveAssetsFromProject(projectRoot);
105    await Promise.all(
106      Object.entries(bundles).map(([platform, bundle]) => {
107        return saveAssets(
108          // @ts-expect-error: tolerable type mismatches: unused `readonly` (common in Metro) and `undefined` instead of `null`.
109          bundle.assets,
110          platform,
111          outputPath,
112          undefined
113        );
114      })
115    );
116  }
117
118  const { assets } = await exportAssetsAsync(projectRoot, {
119    exp,
120    outputDir: outputPath,
121    bundles,
122  });
123
124  if (dumpAssetmap) {
125    Log.log('Dumping asset map');
126    await writeAssetMapAsync({ outputDir: outputPath, assets });
127  }
128
129  // build source maps
130  if (dumpSourcemap) {
131    Log.log('Dumping source maps');
132    await writeSourceMapsAsync({
133      bundles,
134      hashes,
135      outputDir: bundlesPath,
136      fileNames,
137    });
138
139    Log.log('Preparing additional debugging files');
140    // If we output source maps, then add a debug HTML file which the user can open in
141    // the web browser to inspect the output like web.
142    await writeDebugHtmlAsync({
143      outputDir: outputPath,
144      fileNames,
145    });
146  }
147
148  // Generate a `metadata.json` and the export is complete.
149  await writeMetadataJsonAsync({ outputDir: outputPath, bundles, fileNames });
150}
151
152/**
153 * Copy the contents of the public folder into the output folder.
154 * This enables users to add static files like `favicon.ico` or `serve.json`.
155 *
156 * The contents of this folder are completely universal since they refer to
157 * static network requests which fall outside the scope of React Native's magic
158 * platform resolution patterns.
159 */
160async function copyPublicFolderAsync(publicFolder: string, outputFolder: string) {
161  if (fs.existsSync(publicFolder)) {
162    await copyAsync(publicFolder, outputFolder);
163  }
164}
165