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