repo_description: 🐠 Babel is a compiler for writing next generation JavaScript.
repo_topics: ast babel compiler es2015 es6 flavortown javascript
code: import pathUtils from "node:path";
  import fs from "node:fs";
  import type { PluginItem } from "@babel/core";
  import {
    parseSync,
    type InputOptions,
    type types as t,
    type NodePath,
    type PluginObject,
    type PluginAPI,
    type ConfigAPI,
  } from "@babel/core";
  import packageJson from "./package.json" with { type: "json" };
  import pluginToggleBooleanFlag from "./scripts/babel-plugin-toggle-boolean-flag/plugin.ts";
  import pluginBitDecorator from "./scripts/babel-plugin-bit-decorator/plugin.ts";
  import pluginTransformNodeProtocolImport from "./scripts/babel-plugin-transform-node-protocol-import/plugin.ts";
  import { commonJS } from "$repo-utils";
  
  let jestSnapshot = false;
  if (typeof it === "function") {
    // Jest loads the Babel config to parse file and update inline snapshots.
    // This is ok, as it's not loading the Babel config to test Babel itself.
    if (!new Error().stack!.includes("jest-snapshot")) {
      throw new Error("Monorepo root's babel.config.js loaded by a test.");
    }
    jestSnapshot = true;
  }
  
  const { __dirname } = commonJS(import.meta.url);
  
  function normalize(src: string) {
    return src.replace(/\//, pathUtils.sep);
  }
  
  export default function (api: ConfigAPI) {
    const env = api.env();
  
    const sources = ["packages/*/src", "codemods/*/src", "eslint/*/src"];
  
    const envOpts = {
      shippedProposals: true,
      modules: false,
      exclude: [
        "transform-typeof-symbol",
        // We need to enable useBuiltIns
        "transform-object-rest-spread",
      ],
      debug: false,
    };
  
    const presetTsOpts = {
      onlyRemoveTypeImports: true,
      optimizeConstEnums: true,
    };
  
    // These are "safe" assumptions, that we can enable globally
    const assumptions = {
      constantSuper: true,
      ignoreFunctionLength: true,
      ignoreToPrimitiveHint: true,
      mutableTemplateObject: true,
      noClassCalls: true,
      noDocumentAll: true,
      noNewArrows: true,
      setClassMethods: true,
      setComputedProperties: true,
      setSpreadProperties: true,
      skipForOfIteratorClosing: true,
      superIsCallableConstructor: true,
    };
  
    // These are "less safe": we only enable them on our own code
    // and not when compiling dependencies.
    const sourceAssumptions = {
      objectRestNoSymbols: true,
      pureGetters: true,
      setPublicClassFields: true,
    };
  
    const parserAssumptions = {
      iterableIsArray: true,
    };
  
    let targets = {};
    let replaceTSImportExtension = true;
    let ignoreLib = true;
  
    const nodeVersion = "20.19";
    // The vast majority of our src files are modules, but we use
    // unambiguous to keep things simple until we get around to renaming
    // the modules to be more easily distinguished from CommonJS
    const unambiguousSources = [
      ...sources,
      "packages/*/test",
      "codemods/*/test",
      "eslint/*/test",
    ];
  
    switch (env) {
      // Configs used during bundling builds.
      case "standalone":
        replaceTSImportExtension = false;
        ignoreLib = false;
        // rollup-commonjs will converts node_modules to ESM
        unambiguousSources.push(
          "/**/node_modules",
          "packages/babel-preset-env/data",
          "packages/babel-compat-data",
          "packages/babel-runtime/regenerator"
        );
        targets = { ie: 7 };
        break;
      case "rollup":
        replaceTSImportExtension = false;
        ignoreLib = false;
        // rollup-commonjs will converts node_modules to ESM
        unambiguousSources.push(
          "/**/node_modules",
          "packages/babel-preset-env/data",
          "packages/babel-compat-data"
        );
        targets = { node: nodeVersion };
        break;
      case "test-legacy": // In test-legacy environment, we build babel on latest node but test on minimum supported legacy versions
      // fall through
      case "production":
        // Config during builds before publish.
        targets = { node: nodeVersion };
        break;
      case "test":
        targets = { node: "current" };
        break;
      case "development":
        envOpts.debug = true;
        targets = { node: "current" };
        break;
    }
  
    const config: InputOptions = {
      targets,
      assumptions,
      babelrc: false,
      browserslistConfigFile: false,
  
      // Our dependencies are all standard CommonJS, along with all sorts of
      // other random files in Babel's codebase, so we use script as the default,
      // and then mark actual modules as modules farther down.
      sourceType: "script",
      comments: false,
      ignore: (
        [
          // These may not be strictly necessary with the newly-limited scope of
          // babelrc searching, but including them for now because we had them
          // in our .babelignore before.
          "packages/*/test/fixtures",
          ignoreLib ? "packages/*/lib" : null,
          "packages/babel-standalone/babel.js",
        ].filter(Boolean) as string[]
      ).map(normalize),
      parserOpts: {
        createImportExpressions: true,
      },
      presets: [
        // presets are applied from right to left
        ["@babel/env", envOpts],
        ["@babel/preset-typescript", presetTsOpts],
      ],
      plugins: [
        env === "standalone" && [
          "@babel/transform-object-rest-spread",
          { useBuiltIns: true },
        ],
        pluginBitDecorator,
        pluginTransformNodeProtocolImport,
      ].filter(Boolean) as PluginItem[],
      overrides: [
        {
          test: ["packages/babel-parser"].map(normalize),
          plugins: [
            "babel-plugin-transform-charcodes",
            pluginBabelParserTokenType,
          ],
          assumptions: parserAssumptions,
        },
        {
          test: [
            "packages/babel-code-frame",
            "packages/babel-generator",
            "packages/babel-helper-create-class-features-plugin",
            "packages/babel-helper-string-parser",
            "packages/babel-helper-validator-identifier",
          ].map(normalize),
          plugins: ["babel-plugin-transform-charcodes"],
        },
        {
          test: ["packages/babel-generator/src"].map(normalize),
          plugins: [pluginGeneratorOptimization],
        },
        {
          test: sources.map(normalize),
          assumptions: sourceAssumptions,
          plugins: [
            transformNamedBabelTypesImportToDestructuring,
            replaceTSImportExtension ? pluginReplaceTSImportExtension : null,
  
            [
              pluginToggleBooleanFlag,
              { name: "IS_STANDALONE", value: env === "standalone" },
              "flag-IS_STANDALONE",
            ],
            [
              pluginToggleBooleanFlag,
              {
                name: "process.env.IS_PUBLISH",
                value: bool(process.env.IS_PUBLISH),
              },
              "flag-IS_PUBLISH",
            ],
            [
              pluginToggleBooleanFlag,
              {
                name: "process.env.BABEL_9_BREAKING",
                value: process.env.STRIP_BABEL_VERSION_FLAG
                  ? bool(process.env.BABEL_9_BREAKING)
                  : null,
              },
              "flag-BABEL_9_BREAKING",
            ],
  
            pluginPackageJsonMacro,
  
            [
              pluginRequiredVersionMacro,
              {
                overwrite(requiredVersion: string) {
                  if (!process.env.IS_PUBLISH || env === "standalone") {
                    if (bool(process.env.BABEL_9_BREAKING)) {
                      // Match packages/babel-core/src/index.ts
                      return packageJson.version + "999999999";
                    }
                    return packageJson.version;
                  }
                  if (packageJson.version.includes("-")) {
                    // for pre-releases
                    requiredVersion = `${requiredVersion} || ${packageJson.version}`;
                  }
                  return requiredVersion;
                },
              },
            ],
          ].filter(Boolean) as PluginItem[],
        },
        {
          test: unambiguousSources.map(normalize),
          sourceType: "unambiguous",
        } satisfies InputOptions,
      ].filter(Boolean),
    };
  
    if (jestSnapshot) {
      config.plugins = [];
      config.presets = [];
      config.overrides = [];
      config.parserOpts = {
        plugins: ["typescript"],
      };
      config.sourceType = "unambiguous";
    }
  
    return config;
  }
  
  // env vars from the cli are always strings, so !!ENV_VAR returns true for "false"
  function bool(value: unknown) {
    return Boolean(value) && value !== "false" && value !== "0";
  }
  
  // A minimum semver GTE implementation
  // Limitation:
  // - it only supports comparing major and minor version, assuming Node.js will never ship
  //   features in patch release so we will never need to compare a version with "1.2.3"
  //
  // @example
  // semverGte("8.10", "8.9") // true
  // semverGte("8.9", "8.9") // true
  // semverGte("9.0", "8.9") // true
  // semverGte("8.9", "8.10") // false
  // TODO: figure out how to inject it to the `@babel/template` usage so we don't need to
  // copy and paste it.
  // `((v,w)=>(v=v.split("."),w=w.split("."),+v[0]>+w[0]||v[0]==w[0]&&+v[1]>=+w[1]))`;
  
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  function pluginPolyfillsOldNode() {
    const polyfills: {
      name: string;
      necessary: (path: NodePath<t.MemberExpression>) => boolean;
      supported: (path: NodePath<t.MemberExpression>) => boolean;
      replacement: () => t.Expression;
    }[] = [
      // EXAMPLE:
      // {
      //   name: "Object.entries",
      //   necessary({ parent, node }) {
      //     // To avoid infinite replacement loops
      //     return !t.isLogicalExpression(parent, { operator: "||", left: node });
      //   },
      //   supported: path =>
      //     path.parentPath.isCallExpression({ callee: path.node }),
      //   replacement: template`Object.entries || (o => Object.keys(o).map(k => [k, o[k]]))`,
      // },
    ];
  
    return {
      visitor: {
        MemberExpression(path) {
          for (const polyfill of polyfills) {
            if (!path.matchesPattern(polyfill.name)) continue;
  
            if (!polyfill.necessary(path)) return;
            if (!polyfill.supported(path)) {
              throw path.buildCodeFrameError(
                `This '${polyfill.name}' usage is not supported by the inline polyfill.\n` +
                  path.parentPath.toString()
              );
            }
  
            path.replaceWith(polyfill.replacement());
  
            break;
          }
        },
      },
    } satisfies PluginObject;
  }
  
  function pluginPackageJsonMacro({ types: t }: PluginAPI) {
    const fnName = "PACKAGE_JSON";
  
    return {
      visitor: {
        ReferencedIdentifier(path) {
          if (path.isIdentifier({ name: fnName })) {
            throw path.buildCodeFrameError(
              `"${fnName}" is only supported in member expressions.`
            );
          }
        },
        MemberExpression(path) {
          if (!path.get("object").isIdentifier({ name: fnName })) return;
  
          if (path.node.computed) {
            throw path.buildCodeFrameError(
              `"${fnName}" does not support computed properties.`
            );
          }
          const field = (path.node.property as t.Identifier).name;
  
          // TODO: When dropping old Node.js versions, use require.resolve
          // instead of looping through the folders hierarchy
  
          let pkg;
          for (let dir = pathUtils.dirname(this.filename!); ; ) {
            try {
              pkg = fs.readFileSync(pathUtils.join(dir, "package.json"), "utf8");
              break;
            } catch (_) {}
  
            const prev = dir;
            dir = pathUtils.resolve(dir, "..");
  
            // We are in the root and didn't find a package.json file
            if (dir === prev) return;
          }
  
          const value = JSON.parse(pkg)[field];
          path.replaceWith(t.valueToNode(value));
        },
      },
    } satisfies PluginObject;
  }
  
  function pluginRequiredVersionMacro(
    { types: t }: PluginAPI,
    {
      overwrite,
    }: {
      overwrite: (requiredVersion: string, filename: string) => string | null;
    }
  ) {
    const fnName = "REQUIRED_VERSION";
  
    return {
      visitor: {
        ReferencedIdentifier(path) {
          if (path.isIdentifier({ name: fnName })) {
            throw path.buildCodeFrameError(
              `"${fnName}" is only supported in call expressions.`
            );
          }
        },
        CallExpression(path) {
          if (!path.get("callee").isIdentifier({ name: fnName })) return;
  
          if (path.node.arguments.length !== 1) {
            throw path.buildCodeFrameError(
              `"${fnName}" expects exactly one argument.`
            );
          }
  
          const arg = path.get("arguments.0").evaluate().value;
          if (!arg) {
            throw path.buildCodeFrameError(
              `"${fnName}" expects a literal argument.`
            );
          }
  
          const version = overwrite(arg, this.filename!);
          if (version != null) {
            path.replaceWith(t.stringLiteral(version));
            return;
          }
  
          path.replaceWith(path.node.arguments[0]);
        },
      },
    } satisfies PluginObject;
  }
  
  // transform `import { x } from "@babel/types"` to `import * as _t from "@babel/types"; const { x } = _t;
  function transformNamedBabelTypesImportToDestructuring({
    types: {
      cloneNode,
      importNamespaceSpecifier,
      objectPattern,
      objectProperty,
      variableDeclarator,
      variableDeclaration,
    },
  }: PluginAPI) {
    return {
      name: "transform-babel-types-named-imports",
      visitor: {
        ImportDeclaration(path) {
          const { node } = path;
          if (
            node.importKind === "value" &&
            node.source.value === "@babel/types" &&
            node.specifiers[0].type === "ImportSpecifier"
          ) {
            const hoistedDestructuringProperties = [];
            for (const {
              imported,
              local,
            } of node.specifiers as t.ImportSpecifier[]) {
              hoistedDestructuringProperties.push(
                objectProperty(
                  imported,
                  local,
                  false,
                  (imported as t.Identifier).name === local.name
                )
              );
            }
            const babelTypeNsImport = path.scope.generateUidIdentifier("t");
            node.specifiers = [importNamespaceSpecifier(babelTypeNsImport)];
            path.insertAfter([
              variableDeclaration("const", [
                variableDeclarator(
                  objectPattern(hoistedDestructuringProperties),
                  cloneNode(babelTypeNsImport)
                ),
              ]),
            ]);
          }
        },
      },
    } satisfies PluginObject;
  }
  
  function pluginReplaceTSImportExtension() {
    return {
      visitor: {
        "ImportDeclaration|ExportDeclaration"({
          node,
        }: NodePath<t.ImportDeclaration | t.ExportDeclaration>) {
          if (!("source" in node)) return;
          const { source } = node;
          if (source) {
            source.value = source.value.replace(/(\.[mc]?)ts$/, "$1js");
          }
        },
        TSImportEqualsDeclaration({ node }) {
          const { moduleReference } = node;
          if (moduleReference.type !== "TSExternalModuleReference") return;
          const { expression } = moduleReference;
          expression.value = expression.value.replace(/(\.[mc]?)ts$/, "$1js");
        },
      },
    } satisfies PluginObject;
  }
  
  const tokenTypesMapping = new Map();
  const tokenTypeSourcePath = "./packages/babel-parser/src/tokenizer/types.ts";
  
  function getTokenTypesMapping() {
    if (tokenTypesMapping.size === 0) {
      const tokenTypesAst = parseSync(
        fs.readFileSync(tokenTypeSourcePath, {
          encoding: "utf-8",
        }),
        {
          configFile: false,
          parserOpts: { attachComment: false, plugins: ["typescript"] },
        }
      )!;
  
      let typesDeclaration;
      for (const n of tokenTypesAst.program.body) {
        if (n.type === "ExportNamedDeclaration" && n.exportKind === "value") {
          // @ts-expect-error checked by the if
          const declarations = n.declaration!.declarations;
          if (declarations !== undefined) typesDeclaration = declarations[0];
          if (
            typesDeclaration !== undefined &&
            typesDeclaration.id.name === "types"
          ) {
            break;
          }
        }
      }
      if (typesDeclaration === undefined) {
        throw new Error(
          "The plugin can not find TokenType definition in " + tokenTypeSourcePath
        );
      }
  
      const tokenTypesDefinition = typesDeclaration.init.expression.properties;
      for (let i = 0; i < tokenTypesDefinition.length; i++) {
        tokenTypesMapping.set(tokenTypesDefinition[i].key.name, i);
      }
    }
    return tokenTypesMapping;
  }
  
  function pluginBabelParserTokenType({
    types: { isIdentifier, numericLiteral },
  }: PluginAPI) {
    const tokenTypesMapping = getTokenTypesMapping();
    return {
      visitor: {
        MemberExpression(path) {
          const { node } = path;
          if (
            isIdentifier(node.object, { name: "tt" }) &&
            isIdentifier(node.property) &&
            !node.computed
          ) {
            const tokenName = node.property.name;
            const tokenType = tokenTypesMapping.get(node.property.name);
            if (tokenType === undefined) {
              throw path.buildCodeFrameError(
                `${tokenName} is not defined in ${tokenTypeSourcePath}`
              );
            }
            path.replaceWith(numericLiteral(tokenType));
          }
        },
      },
    } as PluginObject;
  }
  
  function pluginGeneratorOptimization({ types: t }: PluginAPI) {
    const generatorNames: string[] = [];
    fs.globSync(
      pathUtils.join(__dirname, "./packages/babel-generator/src/generators/*.ts")
    ).forEach(file => {
      if (file.endsWith("deprecated.ts")) return;
      const content = fs.readFileSync(file, "utf8");
      const ast = parseSync(content, {
        configFile: false,
        parserOpts: { plugins: ["typescript"] },
      });
      t.traverseFast(ast, node => {
        let name;
        if (t.isExportSpecifier(node)) {
          name = t.isStringLiteral(node.exported)
            ? node.exported.value
            : node.exported.name;
        } else if (
          t.isExportNamedDeclaration(node) &&
          t.isFunctionDeclaration(node.declaration)
        ) {
          name = node.declaration.id!.name;
        }
        if (name && !name.startsWith("_")) generatorNames.push(name);
      });
    });
    generatorNames.sort();
    return {
      visitor: {
        CallExpression: {
          exit(path) {
            const { callee, arguments: args } = path.node;
            if (t.isIdentifier(callee) && callee.name === "__node") {
              if (!t.isStringLiteral(args[0])) {
                throw new Error("Expected StringLiteral");
              }
              const type = args[0].value;
              const generatorIndex = generatorNames.indexOf(type);
              if (generatorIndex === -1) {
                throw path.buildCodeFrameError(`Unknown generator type: ${type}`);
              }
              path.replaceWith(t.numericLiteral(generatorIndex));
              return;
            }
  
            if (
              t.isMemberExpression(callee) &&
              t.isIdentifier(callee.property) &&
              t.isThisExpression(callee.object)
            ) {
              if (
                callee.property.name === "token" &&
                args.length === 1 &&
                t.isStringLiteral(args[0])
              ) {
                const str = args[0].value;
                if (str.length === 1) {
                  callee.property.name = "tokenChar";
                  args[0] = t.numericLiteral(str.charCodeAt(0));
                }
              }
            }
          },
        },
      },
    } satisfies PluginObject;
  }
  