1/**
2 * Copyright © 2023 650 Industries.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7import fs from 'fs';
8import path from 'path';
9
10// A list of the Node.js standard library modules.
11export const NODE_STDLIB_MODULES = [
12  'assert',
13  'async_hooks',
14  'buffer',
15  'child_process',
16  'cluster',
17  'crypto',
18  'dgram',
19  'dns',
20  'domain',
21  'events',
22  'fs',
23  'fs/promises',
24  'http',
25  'https',
26  'net',
27  'os',
28  'path',
29  'punycode',
30  'querystring',
31  'readline',
32  'repl',
33  'stream',
34  'string_decoder',
35  'tls',
36  'tty',
37  'url',
38  'util',
39  'v8',
40  'vm',
41  'zlib',
42];
43
44export const EXTERNAL_REQUIRE_POLYFILL = '.expo/metro/polyfill.js';
45export const EXTERNAL_REQUIRE_NATIVE_POLYFILL = '.expo/metro/polyfill.native.js';
46export const METRO_EXTERNALS_FOLDER = '.expo/metro/externals';
47
48export function getNodeExternalModuleId(fromModule: string, moduleId: string) {
49  return path.relative(
50    path.dirname(fromModule),
51    path.join(METRO_EXTERNALS_FOLDER, moduleId, 'index.js')
52  );
53}
54
55export async function setupNodeExternals(projectRoot: string) {
56  await tapExternalRequirePolyfill(projectRoot);
57  await tapNodeShims(projectRoot);
58}
59
60async function tapExternalRequirePolyfill(projectRoot: string) {
61  await fs.promises.mkdir(path.join(projectRoot, path.dirname(EXTERNAL_REQUIRE_POLYFILL)), {
62    recursive: true,
63  });
64  await fs.promises.writeFile(
65    path.join(projectRoot, EXTERNAL_REQUIRE_POLYFILL),
66    'global.$$require_external = typeof window === "undefined" ? require : () => null;'
67  );
68  await fs.promises.writeFile(
69    path.join(projectRoot, EXTERNAL_REQUIRE_NATIVE_POLYFILL),
70    'global.$$require_external = (moduleId) => {throw new Error(`Node.js standard library module ${moduleId} is not available in this JavaScript environment`);}'
71  );
72}
73
74export function isNodeExternal(moduleName: string): string | null {
75  const moduleId = moduleName.replace(/^node:/, '');
76  if (NODE_STDLIB_MODULES.includes(moduleId)) {
77    return moduleId;
78  }
79  return null;
80}
81
82function tapNodeShimContents(moduleId: string): string {
83  return `module.exports = $$require_external('node:${moduleId}');`;
84}
85
86// Ensure Node.js shims which require using `$$require_external` are available inside the project.
87async function tapNodeShims(projectRoot: string) {
88  const externals: Record<string, string> = {};
89  for (const moduleId of NODE_STDLIB_MODULES) {
90    const shimDir = path.join(projectRoot, METRO_EXTERNALS_FOLDER, moduleId);
91    const shimPath = path.join(shimDir, 'index.js');
92    externals[moduleId] = shimPath;
93
94    if (!fs.existsSync(shimPath)) {
95      await fs.promises.mkdir(shimDir, { recursive: true });
96      await fs.promises.writeFile(shimPath, tapNodeShimContents(moduleId));
97    }
98  }
99}
100