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  await fs.writeFile(path.join(outputDir, 'metadata.json'), JSON.stringify(contents));
123  return contents;
124}
125
126export async function writeAssetMapAsync({
127  outputDir,
128  assets,
129}: {
130  outputDir: string;
131  assets: Asset[];
132}) {
133  // Convert the assets array to a k/v pair where the asset hash is the key and the asset is the value.
134  const contents = Object.fromEntries(assets.map((asset) => [asset.hash, asset]));
135  await fs.writeFile(path.join(outputDir, 'assetmap.json'), JSON.stringify(contents));
136  return contents;
137}
138
139export async function writeDebugHtmlAsync({
140  outputDir,
141  fileNames,
142}: {
143  outputDir: string;
144  fileNames: Record<string, string>;
145}) {
146  // Make a debug html so user can debug their bundles
147  const contents = `
148      ${Object.values(fileNames)
149        .map((fileName) => `<script src="${path.join('bundles', fileName)}"></script>`)
150        .join('\n      ')}
151      Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab.
152      You can see a red colored folder containing the original source code from your bundle.
153      `;
154
155  await fs.writeFile(path.join(outputDir, 'debug.html'), contents);
156  return contents;
157}
158