mirror of
https://github.com/hanzoai/babel.git
synced 2026-09-24 16:56:45 +00:00
644 lines
19 KiB
TypeScript
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;
|
|
}
|