12beab412SEvan Baconimport { Platform } from '@expo/config';
2dc51e206SEvan Baconimport crypto from 'crypto';
3dc51e206SEvan Baconimport fs from 'fs/promises';
4dc51e206SEvan Baconimport path from 'path';
5dc51e206SEvan Bacon
6dc51e206SEvan Baconimport { createMetadataJson } from './createMetadataJson';
7e330c216SEvan Baconimport { BundleOutput } from './fork-bundleAsync';
8dc51e206SEvan Baconimport { Asset } from './saveAssets';
9dc51e206SEvan Bacon
10e330c216SEvan Baconconst debug = require('debug')('expo:export:write') as typeof console.log;
11e330c216SEvan Bacon
12dc51e206SEvan Bacon/**
13dc51e206SEvan Bacon * @param props.platform native platform for the bundle
14864ec879SEvan Bacon * @param props.format extension to use for the name
15dc51e206SEvan Bacon * @param props.hash crypto hash for the bundle contents
16dc51e206SEvan Bacon * @returns filename for the JS bundle.
17dc51e206SEvan Bacon */
18864ec879SEvan Baconfunction createBundleFileName({
19864ec879SEvan Bacon  platform,
20864ec879SEvan Bacon  format,
21864ec879SEvan Bacon  hash,
22864ec879SEvan Bacon}: {
23864ec879SEvan Bacon  platform: string;
24864ec879SEvan Bacon  format: 'javascript' | 'bytecode';
25864ec879SEvan Bacon  hash: string;
26864ec879SEvan Bacon}): string {
27864ec879SEvan Bacon  return `${platform}-${hash}.${format === 'javascript' ? 'js' : 'hbc'}`;
28dc51e206SEvan Bacon}
29dc51e206SEvan Bacon
30dc51e206SEvan Bacon/**
31dc51e206SEvan Bacon * @param bundle JS bundle as a string
32dc51e206SEvan Bacon * @returns crypto hash for the provided bundle
33dc51e206SEvan Bacon */
34dc51e206SEvan Baconfunction createBundleHash(bundle: string | Uint8Array): string {
35dc51e206SEvan Bacon  return crypto.createHash('md5').update(bundle).digest('hex');
36dc51e206SEvan Bacon}
37dc51e206SEvan Bacon
38dc51e206SEvan Baconexport async function writeBundlesAsync({
39dc51e206SEvan Bacon  bundles,
40dc51e206SEvan Bacon  outputDir,
41*46f023faSEvan Bacon  useServerRendering,
42dc51e206SEvan Bacon}: {
432beab412SEvan Bacon  bundles: Partial<Record<Platform, Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'>>>;
44dc51e206SEvan Bacon  outputDir: string;
45*46f023faSEvan Bacon  useServerRendering?: boolean;
46dc51e206SEvan Bacon}) {
472beab412SEvan Bacon  const hashes: Partial<Record<Platform, string>> = {};
482beab412SEvan Bacon  const fileNames: Partial<Record<Platform, string>> = {};
49dc51e206SEvan Bacon
502beab412SEvan Bacon  for (const [platform, bundleOutput] of Object.entries(bundles) as [
512beab412SEvan Bacon    Platform,
528a424bebSJames Ide    Pick<BundleOutput, 'hermesBytecodeBundle' | 'code'>,
532beab412SEvan Bacon  ][]) {
540769e63bSEvan Bacon    // TODO: Move native to use the newer `_expo/...` bundle writing system.
55*46f023faSEvan Bacon    if (platform === 'web' && useServerRendering) {
560769e63bSEvan Bacon      continue;
570769e63bSEvan Bacon    }
58dc51e206SEvan Bacon    const bundle = bundleOutput.hermesBytecodeBundle ?? bundleOutput.code;
59dc51e206SEvan Bacon    const hash = createBundleHash(bundle);
60864ec879SEvan Bacon    const fileName = createBundleFileName({
61864ec879SEvan Bacon      platform,
62864ec879SEvan Bacon      format: bundleOutput.hermesBytecodeBundle ? 'bytecode' : 'javascript',
63864ec879SEvan Bacon      hash,
64864ec879SEvan Bacon    });
65dc51e206SEvan Bacon
66dc51e206SEvan Bacon    hashes[platform] = hash;
67dc51e206SEvan Bacon    fileNames[platform] = fileName;
68dc51e206SEvan Bacon    await fs.writeFile(path.join(outputDir, fileName), bundle);
69dc51e206SEvan Bacon  }
70dc51e206SEvan Bacon
71dc51e206SEvan Bacon  return { hashes, fileNames };
72dc51e206SEvan Bacon}
73dc51e206SEvan Bacon
74e330c216SEvan Bacontype SourceMapWriteResult = {
75e330c216SEvan Bacon  platform: string;
76e330c216SEvan Bacon  fileName: string;
77e330c216SEvan Bacon  hash: string;
78e330c216SEvan Bacon  map: string;
79e330c216SEvan Bacon  comment: string;
80e330c216SEvan Bacon};
81e330c216SEvan Bacon
82dc51e206SEvan Baconexport async function writeSourceMapsAsync({
83dc51e206SEvan Bacon  bundles,
84dc51e206SEvan Bacon  hashes,
85dc51e206SEvan Bacon  fileNames,
86dc51e206SEvan Bacon  outputDir,
87dc51e206SEvan Bacon}: {
88dc51e206SEvan Bacon  bundles: Record<
89dc51e206SEvan Bacon    string,
90dc51e206SEvan Bacon    Pick<BundleOutput, 'hermesSourcemap' | 'map' | 'hermesBytecodeBundle' | 'code'>
91dc51e206SEvan Bacon  >;
92dc51e206SEvan Bacon  hashes?: Record<string, string>;
93dc51e206SEvan Bacon  fileNames?: Record<string, string>;
94dc51e206SEvan Bacon  outputDir: string;
95e330c216SEvan Bacon}): Promise<SourceMapWriteResult[]> {
96e330c216SEvan Bacon  return (
97e330c216SEvan Bacon    await Promise.all(
98dc51e206SEvan Bacon      Object.entries(bundles).map(async ([platform, bundle]) => {
99dc51e206SEvan Bacon        const sourceMap = bundle.hermesSourcemap ?? bundle.map;
100e330c216SEvan Bacon        if (!sourceMap) {
101e330c216SEvan Bacon          debug(`Skip writing sourcemap (platform: ${platform})`);
102e330c216SEvan Bacon          return null;
103e330c216SEvan Bacon        }
104e330c216SEvan Bacon
105dc51e206SEvan Bacon        const hash =
106dc51e206SEvan Bacon          hashes?.[platform] ?? createBundleHash(bundle.hermesBytecodeBundle ?? bundle.code);
107dc51e206SEvan Bacon        const mapName = `${platform}-${hash}.map`;
108dc51e206SEvan Bacon        await fs.writeFile(path.join(outputDir, mapName), sourceMap);
109dc51e206SEvan Bacon
110864ec879SEvan Bacon        const jsBundleFileName =
111864ec879SEvan Bacon          fileNames?.[platform] ??
112864ec879SEvan Bacon          createBundleFileName({
113864ec879SEvan Bacon            platform,
114864ec879SEvan Bacon            format: bundle.hermesBytecodeBundle ? 'bytecode' : 'javascript',
115864ec879SEvan Bacon            hash,
116864ec879SEvan Bacon          });
117dc51e206SEvan Bacon        const jsPath = path.join(outputDir, jsBundleFileName);
118dc51e206SEvan Bacon
119dc51e206SEvan Bacon        // Add correct mapping to sourcemap paths
120dc51e206SEvan Bacon        const mappingComment = `\n//# sourceMappingURL=${mapName}`;
121dc51e206SEvan Bacon        await fs.appendFile(jsPath, mappingComment);
122dc51e206SEvan Bacon        return {
123dc51e206SEvan Bacon          platform,
124dc51e206SEvan Bacon          fileName: mapName,
125dc51e206SEvan Bacon          hash,
126dc51e206SEvan Bacon          map: sourceMap,
127dc51e206SEvan Bacon          comment: mappingComment,
128dc51e206SEvan Bacon        };
129dc51e206SEvan Bacon      })
130e330c216SEvan Bacon    )
131e330c216SEvan Bacon  ).filter(Boolean) as SourceMapWriteResult[];
132dc51e206SEvan Bacon}
133dc51e206SEvan Bacon
134dc51e206SEvan Baconexport async function writeMetadataJsonAsync({
135dc51e206SEvan Bacon  outputDir,
136dc51e206SEvan Bacon  bundles,
137dc51e206SEvan Bacon  fileNames,
138dc51e206SEvan Bacon}: {
139dc51e206SEvan Bacon  outputDir: string;
140dc51e206SEvan Bacon  bundles: Record<string, Pick<BundleOutput, 'assets'>>;
141dc51e206SEvan Bacon  fileNames: Record<string, string>;
142dc51e206SEvan Bacon}) {
143dc51e206SEvan Bacon  const contents = createMetadataJson({
144dc51e206SEvan Bacon    bundles,
145dc51e206SEvan Bacon    fileNames,
146dc51e206SEvan Bacon  });
1472bd64340SEvan Bacon  const metadataPath = path.join(outputDir, 'metadata.json');
1482bd64340SEvan Bacon  debug(`Writing metadata.json to ${metadataPath}`);
1492bd64340SEvan Bacon  await fs.writeFile(metadataPath, JSON.stringify(contents));
150dc51e206SEvan Bacon  return contents;
151dc51e206SEvan Bacon}
152dc51e206SEvan Bacon
153dc51e206SEvan Baconexport async function writeAssetMapAsync({
154dc51e206SEvan Bacon  outputDir,
155dc51e206SEvan Bacon  assets,
156dc51e206SEvan Bacon}: {
157dc51e206SEvan Bacon  outputDir: string;
158dc51e206SEvan Bacon  assets: Asset[];
159dc51e206SEvan Bacon}) {
160dc51e206SEvan Bacon  // Convert the assets array to a k/v pair where the asset hash is the key and the asset is the value.
161dc51e206SEvan Bacon  const contents = Object.fromEntries(assets.map((asset) => [asset.hash, asset]));
162dc51e206SEvan Bacon  await fs.writeFile(path.join(outputDir, 'assetmap.json'), JSON.stringify(contents));
163dc51e206SEvan Bacon  return contents;
164dc51e206SEvan Bacon}
165dc51e206SEvan Bacon
166dc51e206SEvan Baconexport async function writeDebugHtmlAsync({
167dc51e206SEvan Bacon  outputDir,
168dc51e206SEvan Bacon  fileNames,
169dc51e206SEvan Bacon}: {
170dc51e206SEvan Bacon  outputDir: string;
171dc51e206SEvan Bacon  fileNames: Record<string, string>;
172dc51e206SEvan Bacon}) {
173dc51e206SEvan Bacon  // Make a debug html so user can debug their bundles
174dc51e206SEvan Bacon  const contents = `
175dc51e206SEvan Bacon      ${Object.values(fileNames)
176dc51e206SEvan Bacon        .map((fileName) => `<script src="${path.join('bundles', fileName)}"></script>`)
177dc51e206SEvan Bacon        .join('\n      ')}
178dc51e206SEvan Bacon      Open up this file in Chrome. In the JavaScript developer console, navigate to the Source tab.
179dc51e206SEvan Bacon      You can see a red colored folder containing the original source code from your bundle.
180dc51e206SEvan Bacon      `;
181dc51e206SEvan Bacon
182dc51e206SEvan Bacon  await fs.writeFile(path.join(outputDir, 'debug.html'), contents);
183dc51e206SEvan Bacon  return contents;
184dc51e206SEvan Bacon}
185