1import { Platform } from '@expo/config';
2import crypto from 'crypto';
3import fs from 'fs/promises';
4import path from 'path';
5
6import { createMetadataJson } from './createMetadataJson';
7import { BundleOutput } from './fork-bundleAsync';
8import { Asset } from './saveAssets';
9
10const debug = require('debug')('expo:export:write') as typeof console.log;
11
12/**
13 * @param props.platform native platform for the bundle
14 * @param props.hash crypto hash for the bundle contents
15 * @returns filename for the JS bundle.
16 */
17function createBundleFileName({ platform, hash }: { platform: string; hash: string }): string {
18  return `${platform}-${hash}.js`;
19}
20
21/**
22 * @param bundle JS bundle as a string
23 * @returns crypto hash for the provided bundle
24 */
25function createBundleHash(bundle: string | Uint8Array): string {
26  return crypto.createHash('md5').update(bundle).digest('hex');
27}
28
29export async function writeBundlesAsync({
30  bundles,
31  outputDir,
32}: {
33  bundles: Partial<Record<Platform, Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'>>>;
34  outputDir: string;
35}) {
36  const hashes: Partial<Record<Platform, string>> = {};
37  const fileNames: Partial<Record<Platform, string>> = {};
38
39  for (const [platform, bundleOutput] of Object.entries(bundles) as [
40    Platform,
41    Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'>
42  ][]) {
43    const bundle = bundleOutput.hermesBytecodeBundle ?? bundleOutput.code;
44    const hash = createBundleHash(bundle);
45    const fileName = createBundleFileName({ platform, hash });
46
47    hashes[platform] = hash;
48    fileNames[platform] = fileName;
49    await fs.writeFile(path.join(outputDir, fileName), bundle);
50  }
51
52  return { hashes, fileNames };
53}
54
55type SourceMapWriteResult = {
56  platform: string;
57  fileName: string;
58  hash: string;
59  map: string;
60  comment: string;
61};
62
63export async function writeSourceMapsAsync({
64  bundles,
65  hashes,
66  fileNames,
67  outputDir,
68}: {
69  bundles: Record<
70    string,
71    Pick<BundleOutput, 'hermesSourcemap' | 'map' | 'hermesBytecodeBundle' | 'code'>
72  >;
73  hashes?: Record<string, string>;
74  fileNames?: Record<string, string>;
75  outputDir: string;
76}): Promise<SourceMapWriteResult[]> {
77  return (
78    await Promise.all(
79      Object.entries(bundles).map(async ([platform, bundle]) => {
80        const sourceMap = bundle.hermesSourcemap ?? bundle.map;
81        if (!sourceMap) {
82          debug(`Skip writing sourcemap (platform: ${platform})`);
83          return null;
84        }
85
86        const hash =
87          hashes?.[platform] ?? createBundleHash(bundle.hermesBytecodeBundle ?? bundle.code);
88        const mapName = `${platform}-${hash}.map`;
89        await fs.writeFile(path.join(outputDir, mapName), sourceMap);
90
91        const jsBundleFileName = fileNames?.[platform] ?? createBundleFileName({ platform, hash });
92        const jsPath = path.join(outputDir, jsBundleFileName);
93
94        // Add correct mapping to sourcemap paths
95        const mappingComment = `\n//# sourceMappingURL=${mapName}`;
96        await fs.appendFile(jsPath, mappingComment);
97        return {
98          platform,
99          fileName: mapName,
100          hash,
101          map: sourceMap,
102          comment: mappingComment,
103        };
104      })
105    )
106  ).filter(Boolean) as SourceMapWriteResult[];
107}
108
109export async function writeMetadataJsonAsync({
110  outputDir,
111  bundles,
112  fileNames,
113}: {
114  outputDir: string;
115  bundles: Record<string, Pick<BundleOutput, 'assets'>>;
116  fileNames: Record<string, string>;
117}) {
118  const contents = createMetadataJson({
119    bundles,
120    fileNames,
121  });
122  const metadataPath = path.join(outputDir, 'metadata.json');
123  debug(`Writing metadata.json to ${metadataPath}`);
124  await fs.writeFile(metadataPath, JSON.stringify(contents));
125  return contents;
126}
127
128export async function writeAssetMapAsync({
129  outputDir,
130  assets,
131}: {
132  outputDir: string;
133  assets: Asset[];
134}) {
135  // Convert the assets array to a k/v pair where the asset hash is the key and the asset is the value.
136  const contents = Object.fromEntries(assets.map((asset) => [asset.hash, asset]));
137  await fs.writeFile(path.join(outputDir, 'assetmap.json'), JSON.stringify(contents));
138  return contents;
139}
140
141export async function writeDebugHtmlAsync({
142  outputDir,
143  fileNames,
144}: {
145  outputDir: string;
146  fileNames: Record<string, string>;
147}) {
148  // Make a debug html so user can debug their bundles
149  const contents = `
150      ${Object.values(fileNames)
151        .map((fileName) => `<script src="${path.join('bundles', fileName)}"></script>`)
152        .join('\n      ')}
153      Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab.
154      You can see a red colored folder containing the original source code from your bundle.
155      `;
156
157  await fs.writeFile(path.join(outputDir, 'debug.html'), contents);
158  return contents;
159}
160