Files
babel/babel.config.ts
T

644 lines
19 KiB
TypeScript

import pathUtils from "node:path";
import fs from "node:fs";
import type { PluginItem } from "@babel/core";
import {
parseSync,
traverse,
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 babelCorePackageJson from "./packages/babel-core/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 = "22.18";
// 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 babelCorePackageJson.version.replace(
/0*$/,
"999999999"
);
}
return babelCorePackageJson.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: traverse.explode({
"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;
}