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 { builtinModules } from 'module';
9import path from 'path';
10
11// A list of the Node.js standard library modules that are currently
12// available,
13export const NODE_STDLIB_MODULES: string[] = [
14  'fs/promises',
15  ...(
16    builtinModules ||
17    // @ts-expect-error
18    (process.binding ? Object.keys(process.binding('natives')) : []) ||
19    []
20  ).filter((x) => !/^_|^(internal|v8|node-inspect)\/|\//.test(x) && !['sys'].includes(x)),
21].sort();
22
23export const EXTERNAL_REQUIRE_POLYFILL = '.expo/metro/polyfill.js';
24export const EXTERNAL_REQUIRE_NATIVE_POLYFILL = '.expo/metro/polyfill.native.js';
25export const METRO_EXTERNALS_FOLDER = '.expo/metro/externals';
26
27export function getNodeExternalModuleId(fromModule: string, moduleId: string) {
28  return path.relative(
29    path.dirname(fromModule),
30    path.join(METRO_EXTERNALS_FOLDER, moduleId, 'index.js')
31  );
32}
33
34export async function setupNodeExternals(projectRoot: string) {
35  await tapExternalRequirePolyfill(projectRoot);
36  await tapNodeShims(projectRoot);
37}
38
39async function tapExternalRequirePolyfill(projectRoot: string) {
40  await fs.promises.mkdir(path.join(projectRoot, path.dirname(EXTERNAL_REQUIRE_POLYFILL)), {
41    recursive: true,
42  });
43  await fs.promises.writeFile(
44    path.join(projectRoot, EXTERNAL_REQUIRE_POLYFILL),
45    'global.$$require_external = typeof window === "undefined" ? require : () => null;'
46  );
47  await fs.promises.writeFile(
48    path.join(projectRoot, EXTERNAL_REQUIRE_NATIVE_POLYFILL),
49    'global.$$require_external = (moduleId) => {throw new Error(`Node.js standard library module ${moduleId} is not available in this JavaScript environment`);}'
50  );
51}
52
53export function isNodeExternal(moduleName: string): string | null {
54  const moduleId = moduleName.replace(/^node:/, '');
55  if (NODE_STDLIB_MODULES.includes(moduleId)) {
56    return moduleId;
57  }
58  return null;
59}
60
61function tapNodeShimContents(moduleId: string): string {
62  return `module.exports = $$require_external('node:${moduleId}');`;
63}
64
65// Ensure Node.js shims which require using `$$require_external` are available inside the project.
66async function tapNodeShims(projectRoot: string) {
67  const externals: Record<string, string> = {};
68  for (const moduleId of NODE_STDLIB_MODULES) {
69    const shimDir = path.join(projectRoot, METRO_EXTERNALS_FOLDER, moduleId);
70    const shimPath = path.join(shimDir, 'index.js');
71    externals[moduleId] = shimPath;
72
73    if (!fs.existsSync(shimPath)) {
74      await fs.promises.mkdir(shimDir, { recursive: true });
75      await fs.promises.writeFile(shimPath, tapNodeShimContents(moduleId));
76    }
77  }
78}
79