157eba0f9SEvan Bacon/**
257eba0f9SEvan Bacon * Copyright © 2023 650 Industries.
357eba0f9SEvan Bacon *
457eba0f9SEvan Bacon * This source code is licensed under the MIT license found in the
557eba0f9SEvan Bacon * LICENSE file in the root directory of this source tree.
657eba0f9SEvan Bacon */
757eba0f9SEvan Baconimport fs from 'fs';
8*8be58d21SEvan Baconimport { builtinModules } from 'module';
957eba0f9SEvan Baconimport path from 'path';
1057eba0f9SEvan Bacon
11*8be58d21SEvan Bacon// A list of the Node.js standard library modules that are currently
12*8be58d21SEvan Bacon// available,
13*8be58d21SEvan Baconexport const NODE_STDLIB_MODULES: string[] = [
1457eba0f9SEvan Bacon  'fs/promises',
15*8be58d21SEvan Bacon  ...(
16*8be58d21SEvan Bacon    builtinModules ||
17*8be58d21SEvan Bacon    // @ts-expect-error
18*8be58d21SEvan Bacon    (process.binding ? Object.keys(process.binding('natives')) : []) ||
19*8be58d21SEvan Bacon    []
20*8be58d21SEvan Bacon  ).filter((x) => !/^_|^(internal|v8|node-inspect)\/|\//.test(x) && !['sys'].includes(x)),
21*8be58d21SEvan Bacon].sort();
2257eba0f9SEvan Bacon
2357eba0f9SEvan Baconexport const EXTERNAL_REQUIRE_POLYFILL = '.expo/metro/polyfill.js';
2457eba0f9SEvan Baconexport const EXTERNAL_REQUIRE_NATIVE_POLYFILL = '.expo/metro/polyfill.native.js';
2557eba0f9SEvan Baconexport const METRO_EXTERNALS_FOLDER = '.expo/metro/externals';
2657eba0f9SEvan Bacon
2757eba0f9SEvan Baconexport function getNodeExternalModuleId(fromModule: string, moduleId: string) {
2857eba0f9SEvan Bacon  return path.relative(
2957eba0f9SEvan Bacon    path.dirname(fromModule),
3057eba0f9SEvan Bacon    path.join(METRO_EXTERNALS_FOLDER, moduleId, 'index.js')
3157eba0f9SEvan Bacon  );
3257eba0f9SEvan Bacon}
3357eba0f9SEvan Bacon
3457eba0f9SEvan Baconexport async function setupNodeExternals(projectRoot: string) {
3557eba0f9SEvan Bacon  await tapExternalRequirePolyfill(projectRoot);
3657eba0f9SEvan Bacon  await tapNodeShims(projectRoot);
3757eba0f9SEvan Bacon}
3857eba0f9SEvan Bacon
3957eba0f9SEvan Baconasync function tapExternalRequirePolyfill(projectRoot: string) {
4057eba0f9SEvan Bacon  await fs.promises.mkdir(path.join(projectRoot, path.dirname(EXTERNAL_REQUIRE_POLYFILL)), {
4157eba0f9SEvan Bacon    recursive: true,
4257eba0f9SEvan Bacon  });
4357eba0f9SEvan Bacon  await fs.promises.writeFile(
4457eba0f9SEvan Bacon    path.join(projectRoot, EXTERNAL_REQUIRE_POLYFILL),
4557eba0f9SEvan Bacon    'global.$$require_external = typeof window === "undefined" ? require : () => null;'
4657eba0f9SEvan Bacon  );
4757eba0f9SEvan Bacon  await fs.promises.writeFile(
4857eba0f9SEvan Bacon    path.join(projectRoot, EXTERNAL_REQUIRE_NATIVE_POLYFILL),
4957eba0f9SEvan Bacon    'global.$$require_external = (moduleId) => {throw new Error(`Node.js standard library module ${moduleId} is not available in this JavaScript environment`);}'
5057eba0f9SEvan Bacon  );
5157eba0f9SEvan Bacon}
5257eba0f9SEvan Bacon
5357eba0f9SEvan Baconexport function isNodeExternal(moduleName: string): string | null {
5457eba0f9SEvan Bacon  const moduleId = moduleName.replace(/^node:/, '');
5557eba0f9SEvan Bacon  if (NODE_STDLIB_MODULES.includes(moduleId)) {
5657eba0f9SEvan Bacon    return moduleId;
5757eba0f9SEvan Bacon  }
5857eba0f9SEvan Bacon  return null;
5957eba0f9SEvan Bacon}
6057eba0f9SEvan Bacon
6157eba0f9SEvan Baconfunction tapNodeShimContents(moduleId: string): string {
6257eba0f9SEvan Bacon  return `module.exports = $$require_external('node:${moduleId}');`;
6357eba0f9SEvan Bacon}
6457eba0f9SEvan Bacon
6557eba0f9SEvan Bacon// Ensure Node.js shims which require using `$$require_external` are available inside the project.
6657eba0f9SEvan Baconasync function tapNodeShims(projectRoot: string) {
6757eba0f9SEvan Bacon  const externals: Record<string, string> = {};
6857eba0f9SEvan Bacon  for (const moduleId of NODE_STDLIB_MODULES) {
6957eba0f9SEvan Bacon    const shimDir = path.join(projectRoot, METRO_EXTERNALS_FOLDER, moduleId);
7057eba0f9SEvan Bacon    const shimPath = path.join(shimDir, 'index.js');
7157eba0f9SEvan Bacon    externals[moduleId] = shimPath;
7257eba0f9SEvan Bacon
7357eba0f9SEvan Bacon    if (!fs.existsSync(shimPath)) {
7457eba0f9SEvan Bacon      await fs.promises.mkdir(shimDir, { recursive: true });
7557eba0f9SEvan Bacon      await fs.promises.writeFile(shimPath, tapNodeShimContents(moduleId));
7657eba0f9SEvan Bacon    }
7757eba0f9SEvan Bacon  }
7857eba0f9SEvan Bacon}
79