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