文章目录
Introduction
Babel is a JavaScript compiler that transforms code through plugins. Understanding how to write Babel plugins enables you to create custom code transformations, tooling, and automated refactoring. This article covers the fundamental concepts and APIs, then builds a real plugin from scratch.
Understanding Babel Pipeline
Before writing plugins, understand how Babel processes code:
- Parse: Source code → AST (Abstract Syntax Tree)
- Transform: AST → Modified AST (via plugins/presets)
- Generate: Modified AST → Output code
Babel Plugin Structure
A Babel plugin is a function that receives babel object and returns a visitor object:
// Basic plugin structure
module.exports = function (babel) {
// babel contains: types, template, traverse, generate, etc.
const { types: t } = babel;
return {
// Visitor object - keys are AST node types
visitor: {
// Called when traversing Identifier nodes
Identifier(path, state) {
// Transform logic here
},
},
};
};
Core APIs
types (t)
The types module provides AST node constructors and type checking:
const { types: t } = babel;
// Type checking
t.isIdentifier(node); // Check node type
t.isIdentifier(node, { name: "x" }); // Check with properties
t.isNumericLiteral(node); // Check for number literals
// Node construction
t.identifier("x"); // Create: x
t.numericLiteral(42); // Create: 42
t.stringLiteral("hello"); // Create: 'hello'
t.binaryExpression("+", left, right); // Create: left + right
t.callExpression(callee, args); // Create: callee(args)
t.memberExpression(obj, prop); // Create: obj.prop
t.arrowFunctionExpression(params, body); // Create: (params) => body
t.functionDeclaration(id, params, body); // Create: function id(params) {}
t.variableDeclaration("const", [declarator]); // Create: const x = y
t.objectExpression([property]); // Create: { key: value }
t.arrayExpression([elements]); // Create: [elements]
t.templateLiteral(quasis, exprs); // Create: `string ${expr}`
// Node manipulation
t.cloneNode(node); // Deep clone a node
t.removeProperties(node); // Remove special properties
t.validate(node, key, value); // Validate node property
traverse
The traverse module provides manual AST traversal:
const traverse = require("@babel/traverse").default;
// Traverse AST manually
traverse(ast, {
Identifier(path) {
console.log(path.node.name);
// Traverse into children (default: true)
// path.skip(); // Skip children
// path.stop(); // Stop traversal
},
FunctionDeclaration(path) {
// Access parent node
const parent = path.parent;
// Access sibling nodes
const siblings = path.parentPath.container;
// Get scope
const scope = path.scope;
},
});
template
The template module creates AST from template strings:
const template = require("@babel/template").default;
// Simple template
const buildRequire = template(`
require(SOURCE)
`);
// Usage: buildRequire({ SOURCE: t.stringLiteral('./module') })
// Template with placeholders
const buildWrapper = template(`
(function() {
BODY;
})()
`);
// Usage: buildWrapper({ BODY: statementNode })
// Template with exports
const buildExport = template(
`
module.exports = EXPRESSION
`,
{ plugins: ["proposal-export-default-from"] }
);
generate
The generate module converts AST back to code:
const generate = require("@babel/generate").default;
// Generate code from AST
const output = generate(
ast,
{
sourceMaps: true, // Generate source maps
comments: true, // Include comments
compact: false, // Minify output
},
code
);
// output = { code: '...', map: sourceMap }
Understanding Path and State
Path Object
The path object represents the location and traversal of a node:
Identifier(path) {
// Node properties
path.node; // The current AST node
path.parent; // Parent node
path.parentPath; // Parent path
// Traversal control
path.skip(); // Skip traversing children
path.stop(); // Stop entire traversal
// Node replacement
path.replaceWith(node); // Replace with single node
path.replaceWithMultiple(nodes); // Replace with multiple nodes
path.replaceWithSourceString(code); // Replace with code string
// Node removal
path.remove(); // Remove this node
// Scope info
path.scope; // Current scope
// Binding info
path.getBinding('name'); // Get variable binding
path.bindings; // All bindings in scope
}
State Object
The state object contains plugin options and metadata:
// Plugin receives options
module.exports = function (babel) {
return {
visitor: {
Program(path, state) {
// state.opts = { optionName: value } from plugin options
// state.filename = current file name
// state.file = { ast, code, opts }
},
},
};
};
// Usage: { plugins: [ ['plugin-name', { optionName: value }] ] }
Plugin Example: Auto Dependency Injection
Let’s build a practical plugin that automatically injects dependencies:
// transform-deps.js - Auto inject dependencies based on usage
module.exports = function (babel) {
const { types: t, template } = babel;
// Template for import statement
const importTemplate = template(`
import IDENTIFIER from 'MODULE';
`);
return {
visitor: {
// Handle program entry - inject at top level
Program: {
enter(path, state) {
// Collect all used identifiers that need injection
const depsToInject = new Set();
const options = state.opts || {};
const importMap = options.imports || {};
// Collect used identifiers throughout the file
path.traverse({
Identifier(idPath) {
const name = idPath.node.name;
// Skip if it's a declaration or local variable
if (idPath.scope.hasBinding(name)) return;
// Check if this identifier needs injection
if (importMap[name]) {
depsToInject.add(name);
}
},
});
// Get existing imports to avoid duplicates
const existingImports = new Set();
path.traverse({
ImportDeclaration(importPath) {
importPath.node.specifiers.forEach(spec => {
if (t.isImportDefaultSpecifier(spec)) {
existingImports.add(spec.local.name);
}
});
},
});
// Inject missing imports at the top
const body = path.node.body;
let insertIndex = 0;
// Skip shebang if present
if (t.isStringLiteral(body[0]) && body[0].value.startsWith("#!")) {
insertIndex = 1;
}
// Insert imports in reverse order to maintain index
const depsArray = Array.from(depsToInject).filter(
dep => !existingImports.has(dep)
);
depsArray.forEach(depName => {
const importNode = importTemplate({
IDENTIFIER: t.identifier(depName),
MODULE: t.stringLiteral(importMap[depName]),
});
body.splice(insertIndex, 0, importNode);
});
},
},
// Transform: console.log -> logger.log
CallExpression(path, state) {
const callee = path.node.callee;
const options = state.opts || {};
const transforms = options.transforms || {};
// Transform console.log
if (
t.isMemberExpression(callee) &&
t.isIdentifier(callee.object, { name: "console" }) &&
t.isIdentifier(callee.property) &&
transforms.console
) {
// Replace console.log with logger.log
path.node.callee = t.memberExpression(
t.identifier("logger"),
callee.property
);
}
},
},
};
};
Plugin Usage Configuration
// .babelrc or babel.config.js
{
"plugins": [
[
"./transform-deps.js",
{
"imports": {
"React": "react",
"lodash": "lodash",
"axios": "axios"
},
"functionDeps": {
"fetchData": [{ "name": "fetch", "source": "./utils/fetch" }]
},
"transforms": {
"console": true,
"promisify": true
}
}
]
]
}
Common Plugin Patterns
Pattern 1: Conditional Transformation
Identifier(path, state) {
// Only transform in certain contexts
if (!state.opts.enable) return;
// Only transform in specific files
if (state.filename?.includes('node_modules')) return;
// Apply transformation
}
Pattern 2: Scope-Aware Transformation
Identifier(path) {
const name = path.node.name;
// Get binding information
const binding = path.scope.getBinding(name);
if (!binding) {
// This is a free variable (not declared locally)
// Safe to transform
}
}
Pattern 3: Replace with Multiple Nodes
ReturnStatement(path) {
// Replace single return with multiple statements
path.replaceWithMultiple([
t.expressionStatement(
t.callExpression(
t.identifier('console.log'),
[t.stringLiteral('returning')]
)
),
path.node // Keep original return
]);
}
Pattern 4: Hoist Function Definitions
FunctionDeclaration(path) {
// Get binding and all references
const binding = path.scope.getBinding(path.node.id.name);
if (binding && binding.references > 0) {
// Move to top of parent scope
path.scope.parent.push({
kind: 'var',
declare: true,
id: path.node.id,
init: t.functionExpression(
null,
path.node.params,
path.node.body
)
});
path.remove();
}
}
Best Practices
-
Always check node types before manipulation
if (!t.isIdentifier(node)) return; -
Handle scope correctly to avoid variable conflicts
const binding = path.scope.getBinding("varName"); if (binding?.scope !== path.scope) return; -
Use template literals instead of building AST manually
// Good template(`require(SOURCE)`); // Avoid t.callExpression(t.identifier("require"), [source]); -
Consider edge cases:
// Handle missing properties const name = path.node.id?.name; // Handle empty arrays if (path.node.body.body?.length === 0) return; -
Test thoroughly with various input patterns
Testing Babel Plugins
// test-plugin.js
const babel = require("@babel/core");
const plugin = require("./transform-deps");
const code = `
const x = 1;
console.log(x);
`;
const result = babel.transformSync(code, {
plugins: [[plugin, { transforms: { console: true } }]],
filename: "test.js",
});
console.log(result.code);
const core = require('@babel/core');
const types = require('@babel/types');
const template = require('@babel/template').default;
const importModule = require('@babel/helper-module-imports');
const autoTrackerPlugin = (options) => {
return {
visitor: {
Program: {
enter(path, state) {
let loggerId;
path.traverse({
ImportDeclaration(path) {
const importedModuleName = path.get('source').node.value;
if (importedModuleName == options.name) {
const specifierPath = path.get('specifiers.0');
if (specifierPath.isImportDefaultSpecifier() ||
specifierPath.isImportSpecifier() ||
specifierPath.isImportNamespaceSpecifier()) {
loggerId = specifierPath.node.local.name;
path.stop();
}
}
}
});
if (!loggerId) {
loggerId = importModule.addDefault(path, options.name, {
nameHint: path.scope.generateUid(options.name)
});
}
state.loggerNode = template.statement(`LOGGER();`)({
LOGGER: loggerId
});
},
},
"FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ClassMethod"(path, state) {
const { node } = path;
if (types.isBlockStatement(node.body)) {
node.body.body.unshift(state.loggerNode);
} else {
const newNode = types.blockStatement([
state.loggerNode,
types.expressionStatement(node.body)
]);
path.get('body').replaceWith(newNode);
}
}
}
};
};
import _logger from "logger";
function sum(a, b) {
_logger();
return a + b;
}
const multiply = function (a, b) {
_logger();
return a * b;
};
const minus = (a, b) => {
_logger();
return a - b;
};
class Calculator {
// Calculator类的实现
}