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