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