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