AST
的目的是访问树中的每个节点,并对其进行相应的处理,例如执行、分析或者转换npm i esprima estraverse escodegen -S
let esprima = require('esprima');//把JS源代码转成AST语法树
let estraverse = require('estraverse');///遍历语法树,修改树上的节点
let escodegen = require('escodegen');//把AST语法树重新转换成代码
let code = `function ast(){}`;
let ast = esprima.parse(code);
let indent = 0;
const padding = ()=>" ".repeat(indent);
estraverse.traverse(ast,{
enter(node){
console.log(padding()+node.type+'进入');
if(node.type === 'FunctionDeclaration'){
node.id.name = 'newAst';
}
indent+=2;
},
leave(node){
indent-=2;
console.log(padding()+node.type+'离开');
}
});
Program进入
FunctionDeclaration进入
Identifier进入
Identifier离开
BlockStatement进入
BlockStatement离开
FunctionDeclaration离开
Program离开
ECMAScript 2015+
的代码,使它在旧的浏览器或者环境中也能够运行type
命名的方法,当遍历 AST 的时候,如果匹配上 type,就会执行对应的方法转换前
const sum = (a,b)=>{
console.log(this);
return a+b;
}
转换后
var _this = this;
const sum = function (a, b) {
console.log(_this);
return a + b;
};
npm i @babel/core @babel/types -D
实现
// Babel 的编译器,核心 API 都在这里面,比如常见的 transform、parse,并实现了插件功能
const babelCore = require('@babel/core');
//用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法
const types = require('@babel/types');
//const arrowFunctions = require('babel-plugin-transform-es2015-arrow-functions');
const arrowFunctions2 = {
visitor: {
//当遍历语法遇到箭头函数的时候,执行此函数,参数是箭头函数的节点路径对象
ArrowFunctionExpression(path) {
const { node } = path;
hoistFunctionEnvironment(path);
node.type = 'FunctionExpression';
let body = node.body;
//如果body不是一个块级语句的话 isXX用来判断某个AST语法树节点是不是某种类型
if (!types.isBlockStatement(body)) {
//https://babeljs.io/docs/babel-types.html
node.body = types.blockStatement([
types.returnStatement(body)
]);
}
}
}
}
function getThisBindingIdentifier(scope) {
for (const bindingName in scope.bindings) {
const binding = scope.bindings[bindingName];
if (binding.kind === 'const' || binding.kind === 'let' || binding.kind === 'var') {
const initValue = binding.path.node.init;
if (types.isThisExpression(initValue)) {
return binding.identifier;
}
}
}
return null;
}
/**
* 1.在函数的外部声明一个变量_this,值是this
* 2.在函数体内把所有的this变成_this
* @param {*} path
*/
function hoistFunctionEnvironment(path) {
//indParent(callback)从当前节点一直向上找到根节点(不包括自己)
const thisEnv = path.findParent(parent => {
//如果这个父节点是一个普通函数,或者是一个根节点的话返回此节点
return (parent.isFunction() && !parent.isArrowFunctionExpression()) || parent.isProgram()
});
//1.需要确定在当前的作用域内是否使用到了this
let thisPaths = getThisPaths(path);
let thisBinding = getThisBindingIdentifier(thisEnv.scope);
if (!thisBinding) {
thisBinding = types.identifier(thisEnv.scope.generateUid('this'));
thisEnv.scope.push({
id: thisBinding,
init: types.thisExpression()
});
}
if (thisPaths.length > 0) {
thisPaths.forEach(thisPath => {
thisPath.replaceWith(thisBinding);
});
}
}
function getThisPaths(path) {
let thisPaths = [];
//判断path的子节点
path.traverse({
FunctionDeclaration(path) {
path.skip();
},
ThisExpression(thisPath) {
thisPaths.push(thisPath);
}
});
return thisPaths;
}
let sourceCode = `
const sum = (a,b)=>{
console.log(this);
function multiply(){
console.log(this);
}
return a+b;
}
const minus = (a,b)=>{
console.log(this);
const divide = (a,b)=>{
console.log(this);
return a/b;
}
return a-b;
}
`;
let targetSource = babelCore.transform(sourceCode, {
plugins: [
arrowFunctions2
]
});
console.log(targetSource.code);
es6
class Person {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
es5
function Person(name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
};
实现
//babel核心模块
const core = require('@babel/core');
//用来生成或者判断节点的AST语法树的节点
let types = require("@babel/types");
//let transformClassesPlugin = require('@babel/plugin-transform-classes');
let transformClassesPlugin = {
visitor: {
//如果是箭头函数,那么就会进来此函数,参数是箭头函数的节点路径对象
//path代表路径,node代表路径上的节点
ClassDeclaration(path) {
let node = path.node;
let id = node.id;//Identifier name:Person
let methods = node.body.body;//Array<MethodDefinition>
let nodes = [];
methods.forEach(method => {
if (method.kind === 'constructor') {
let constructorFunction = types.functionDeclaration(
id,
method.params,
method.body
);
nodes.push(constructorFunction);
} else {
let memberExpression = types.memberExpression(
types.memberExpression(
id, types.identifier('prototype')
), method.key
)
let functionExpression = types.functionExpression(
null,
method.params,
method.body
)
let assignmentExpression = types.assignmentExpression(
'=',
memberExpression,
functionExpression
);
nodes.push(assignmentExpression);
}
})
if (nodes.length === 1) {
//单节点用replaceWith
//path代表路径,用nodes[0]这个新节点替换旧path上现有老节点node ClassDeclaration
path.replaceWith(nodes[0]);
} else {
//多节点用replaceWithMultiple
path.replaceWithMultiple(nodes);
}
}
}
}
let sourceCode = `
class Person{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
`;
let targetSource = core.transform(sourceCode, {
plugins: [transformClassesPlugin]
});
console.log(targetSource.code);
const {transformSync} = require('@babel/core');
const types = require('@babel/types');
const path = require('path');
const sourceCode = `
console.log("hello");
`;
const visitor = {
CallExpression(nodePath,state){
const {node} = nodePath;
if(types.isMemberExpression(node.callee)){
if(node.callee.object.name === 'console'){
if(['log','warn','info','error','debug'].includes(node.callee.property.name)){
const {line,column} = node.loc.start;
// 获取相对于当前文件的文件名并将反斜杠替换为正斜杠
const relativeFileName = path.relative(__dirname, state.file.opts.filename).replace(/\\/g, '/');
// 将文件名和位置信息插入到参数列表的开头
node.arguments.unshift(types.stringLiteral(`${relativeFileName} ${line}:${column}`));
}
}
}
}
}
function logParamPlugin(){
return {
visitor
}
}
const {code} = transformSync(sourceCode,{
filename:'any.js',
plugins:[logParamPlugin()]
});
console.log(code);
state
用于在遍历过程中在AST节点之间传递数据的方式const {transformSync} = require('@babel/core');
const types = require('@babel/types');
const path = require('path');
const autoLoggerPlugin = require('./autoLoggerPlugin');
const sourceCode = `
let _logger2 = 'xxx';
function sum(a,b){
return a+b;
}
const multiply = function(a,b){
return a*b;
}
const minis = (a,b)=>a-b;
class Math{
divide(a,b){
return a/b;
}
}
`;
const {code} = transformSync(sourceCode,{
filename:'some.js',
plugins:[autoLoggerPlugin({
fnNames:['sum'],
libName:'logger',//把获取业务数据的逻辑写在logger里
params:['a','b','c']
})]
});
console.log(code);
const types = require('@babel/types');
const pathLib = require('path');
const importModuleHelper = require('@babel/helper-module-imports');
const template = require('@babel/template');
function autoLoggerPlugin(options){
return {
visitor:{
Program:{
//state 可以在遍历过程保存和传递状态
enter(path,state){
let loggerId;
path.traverse({
ImportDeclaration(path){
debugger
//获取导入库的名称
//const libName = path.node.source.value;
//jquery.find 在path的下层属性中寻找属性名为source的路径path,
const libName = path.get('source').node.value;
//如果此导入语句导入的第三方模块和配置的日志第三方库名称一样
if(options.libName === libName){
const specifierPath = path.get('specifiers.0');
if(specifierPath.isImportDefaultSpecifier()
|| specifierPath.isImportSpecifier()
||specifierPath.isImportNamespaceSpecifier()){
loggerId=specifierPath.node.local;
}
path.stop();//停止遍历查找
}
}
});
//如果遍历完Program,loggerId还是空的,那说明在源码中尚未导入logger模块
if(!loggerId){
loggerId = importModuleHelper.addDefault(path,options.libName,{
//在Program作用域内生成一个不会与当前作用域内变量重复的变量名
nameHint:path.scope.generateUid(options.libName)
});
}
//使用template模块生成一个ast语法树节点,把一个字符串变成节点
state.loggerNode = template.statement(`LOGGER_PLACE();`)({
LOGGER_PLACE:loggerId.name
})
//state.loggerNode = types.expressionStatement(types.callExpression(loggerId,[]));
}
},
"FunctionDeclaration|FunctionExpression|ArrowFunctionExpression|ClassMethod"(path,state){
const {node} = path;
let fnName;
if(node.type === 'FunctionDeclaration'){
fnName=node.id.name;
}
if(options.fnNames.includes(fnName)){
if(types.isBlockStatement(node.body)){
node.body.body.unshift(state.loggerNode);
}else {
const newNode = types.blockStatement([
state.loggerNode,
types.returnStatement(node.body)
]);
path.get('body').replaceWith(newNode);
}
}
}
}
}
}
module.exports = autoLoggerPlugin;
const {transformSync} = require('@babel/core');
const types = require('@babel/types');
const path = require('path');
const noConsolePlugin = require('./noConsolePlugin');
const sourceCode = `
var a = 1;
console.log(a);
var b = 2;
`;
const {code} = transformSync(sourceCode,{
filename:'./some.js',
plugins:[noConsolePlugin({
fix:true
})]
});
console.log(code);
eslintPlugin.js
function noConsolePlugin(options){
return {
pre(file){
file.set('errors',[]);
},
visitor:{
CallExpression(path,state){
const {node} = path;
const errors = state.file.get('errors');
if(node.callee.object && node.callee.object.name === 'console'){
const stackTraceLimit = Error.stackTraceLimit;
Error.stackTraceLimit = 0;
errors.push(path.buildCodeFrameError(`代码中不能出现console语句`,Error));
Error.stackTraceLimit = stackTraceLimit;
if(options.fix){//如果需要自动修复,就删除此语句
path.parentPath.remove();
}
}
}
},
post(file){
console.log(...file.get('errors'));
}
}
}
module.exports = noConsolePlugin;
const {transformSync} = require('@babel/core');
const uglifyPlugin = require('./uglifyPlugin');
const sourceCode = `
var age = 12;
console.log(age);
var name = 'zhufeng';
console.log(name)
`;
const {code} = transformSync(sourceCode,{
filename:'./some.js',
plugins:[uglifyPlugin()]
});
console.log(code);
uglifyPlugin.js
function uglifyPlugin(options){
return {
visitor:{
//捕获所有的作用域的节点
Scopable(path){
//遍历作用域内所有的绑定,也就是变量
Object.entries(path.scope.bindings).forEach(([key,binding])=>{
const newName = path.scope.generateUid('_');
binding.path.scope.rename(key,newName);
});
}
}
}
}
module.exports = uglifyPlugin;
const {transformSync} = require('@babel/core');
const tscPlugin = require('./tscPlugin');
const sourceCode = `
var age:number = "aaa";
`;
const {code} = transformSync(sourceCode,{
parserOpts:{plugins:["typescript"]},
filename:'./some.js',
plugins:[tscPlugin()]
});
console.log(code);
tscPlugin.js
const typeAnnotationMap= {
TSNumberKeyword:'NumberLiteral',
TSStringKeyword:'StringLiteral'
}
function tscPlugin(){
return {
pre(file){
file.set('errors',[]);
},
visitor:{
VariableDeclarator(path,state){
const errors = state.file.get('errors');
const {node} = path;
const idType = typeAnnotationMap[node.id.typeAnnotation.typeAnnotation.type];
const initType = node.init.type;
if(idType !== initType){
Error.stackTraceLimit = 0;
errors.push(path.buildCodeFrameError(
`无法把${initType}赋值给${idType}`,Error
));
}
}
},
post(file){
console.log(...file.get('errors'));
}
}
}
module.exports = tscPlugin;
import { flatten, concat } from "lodash";
转换为
import flatten from "lodash/flatten";
import concat from "lodash/flatten";
npm i webpack webpack-cli babel-plugin-import -D
const path = require("path");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve("dist"),
filename: "bundle.js",
},
module: {
rules: [
{
test: /\.js$/,
{
loader:'babel-loader',
options:{
"plugins": [[
path.resolve('./plugins/babel-plugin-import.js')
, {
"libraryDirectory": "",
"libraryName": "lodash"
}]]
}
},
},
],
},
};
编译顺序为首先
plugins
从左往右,然后presets
从右往左
plugins\babel-plugin-import.js
const types = require('@babel/types');
const template = require('@babel/template');
function babelPluginImport(){
return {
visitor:{
ImportDeclaration(path,state){
const {node} = path;
const {specifiers} = node;
const {libraryName,libraryDirectory='lib'} = state.opts;
if(node.source.value === libraryName
&&(!types.isImportDefaultSpecifier(specifiers[0]))){
const newImportDeclarations = specifiers.map(specifier=>{
return template.statement(
`import ${specifier.local.name} from '${libraryName}/${specifier.imported.name}';`
)();
/* return types.importDeclaration(
[types.importDefaultSpecifier(specifier.local)],
types.stringLiteral(libraryDirectory?
`${libraryName}/${libraryDirectory}/${specifier.imported.name}`
:`${libraryName}/${specifier.imported.name}`)
); */
})
path.replaceWithMultiple(newImportDeclarations);
}
}
}
}
}
module.exports = babelPluginImport;
babel-plugin-transform-runtime
后,Babel 就会使用 babel-runtime 下的工具函数const { transformSync } = require('@babel/core');
const tscCheckPlugin = require('./tscCheckPlugin');
const sourceCode = `
var age:number="12";
`;
const { code } = transformSync(sourceCode, {
parserOpts: { plugins: ['typescript'] },
plugins: [tscCheckPlugin()]
});
console.log(code);
tscCheckPlugin.js
// 定义一个类型注解映射对象
const TypeAnnotationMap = {
TSNumberKeyword: "NumericLiteral"
};
// 定义 eslintPlugin
const eslintPlugin = () => {
return {
// 在遍历开始前执行
pre(file) {
// 为当前文件设置一个空的 errors 数组
file.set('errors', []);
},
visitor: {
// 当访问 VariableDeclarator 节点时触发
VariableDeclarator(path, state) {
// 获取之前设置的 errors 数组
const errors = state.file.get('errors');
const { node } = path;
// 获取变量声明的类型注解类型
const idType = TypeAnnotationMap[node.id.typeAnnotation.typeAnnotation.type];
// 获取变量初始值的类型
const initType = node.init.type;
// 打印变量声明类型和初始值类型
console.log(idType, initType);
// 如果变量声明类型与初始值类型不匹配
if (idType !== initType) {
// 将错误信息添加到 errors 数组
errors.push(path.get('init').buildCodeFrameError(`无法把${initType}类型赋值给${idType}类型`, Error));
}
}
},
// 遍历结束后执行
post(file) {
// 在控制台输出 errors 数组的内容
console.log(...file.get('errors'));
}
}
};
// 导出 eslintPlugin
module.exports = eslintPlugin;
const babel = require('@babel/core');
function transformType(type){
switch(type){
case 'TSNumberKeyword':
case 'NumberTypeAnnotation':
return 'number'
case 'TSStringKeyword':
case 'StringTypeAnnotation':
return 'string'
}
}
const tscCheckPlugin = () => {
return {
pre(file) {
file.set('errors', []);
},
visitor: {
AssignmentExpression(path,state){
const errors = state.file.get('errors');
const variable = path.scope.getBinding(path.get('left'));
const variableAnnotation = variable.path.get('id').getTypeAnnotation();
const variableType = transformType(variableAnnotation.typeAnnotation.type);
const valueType = transformType(path.get('right').getTypeAnnotation().type);
if (variableType !== valueType){
Error.stackTraceLimit = 0;
errors.push(
path.get('init').buildCodeFrameError(`无法把${valueType}赋值给${variableType}`, Error)
);
}
}
},
post(file) {
console.log(...file.get('errors'));
}
}
}
let sourceCode = `
var age:number;
age = "12";
`;
const result = babel.transform(sourceCode, {
parserOpts:{plugins:['typescript']},
plugins: [tscCheckPlugin()]
})
console.log(result.code);
const babel = require('@babel/core');
function transformType(type){
switch(type){
case 'TSNumberKeyword':
case 'NumberTypeAnnotation':
return 'number'
case 'TSStringKeyword':
case 'StringTypeAnnotation':
return 'string'
}
}
const tscCheckPlugin = () => {
return {
pre(file) {
file.set('errors', []);
},
visitor: {
CallExpression(path,state){
const errors = state.file.get('errors');
const trueTypes = path.node.typeParameters.params.map(param=>transformType(param.type));
const argumentsTypes = path.get('arguments').map(arg=>transformType(arg.getTypeAnnotation().type));
const calleePath = path.scope.getBinding(path.get('callee').node.name).path;
const genericMap=new Map();
calleePath.node.typeParameters.params.map((item, index) => {
genericMap[item.name] = trueTypes[index];
});
const paramsTypes = calleePath.get('params').map(arg=>{
const typeAnnotation = arg.getTypeAnnotation().typeAnnotation;
if(typeAnnotation.type === 'TSTypeReference'){
return genericMap[typeAnnotation.typeName.name];
}else{
return transformType(type);
}
});
Error.stackTraceLimit = 0;
paramsTypes.forEach((type,index)=>{
console.log(type,argumentsTypes[index]);
if(type !== argumentsTypes[index]){
errors.push(
path.get(`arguments.${index}`).buildCodeFrameError(`实参${argumentsTypes[index]}不能匹配形参${type}`, Error)
);
}
});
}
},
post(file) {
console.log(...file.get('errors'));
}
}
}
let sourceCode = `
function join<T>(a:T,b:T):string{
return a+b;
}
join<number>(1,'2');
`;
const result = babel.transform(sourceCode, {
parserOpts:{plugins:['typescript']},
plugins: [tscCheckPlugin()]
})
console.log(result.code);
const babel = require('@babel/core');
function transformType(type){
switch(type){
case 'TSNumberKeyword':
case 'NumberTypeAnnotation':
return 'number'
case 'TSStringKeyword':
case 'StringTypeAnnotation':
return 'string'
case 'TSLiteralType':
return 'literal';
default:
return type;
}
}
const tscCheckPlugin = () => {
return {
pre(file) {
file.set('errors', []);
},
visitor: {
TSTypeAliasDeclaration(path){
const typeName = path.node.id.name;
const typeInfo = {
typeParams:path.node.typeParameters.params.map(item =>item.name),//['K']
typeAnnotation:path.getTypeAnnotation()//{checkType,extendsType,trueType,falseType}
}
path.scope.setData(typeName,typeInfo)
},
CallExpression(path,state){
const errors = state.file.get('errors');
const trueTypes = path.node.typeParameters.params.map(param=>{
//TSTypeReference typeName=Infer typeParameters=[]
if(param.type === 'TSTypeReference'){
const name = param.typeName.name;//Infer
const {typeParams,typeAnnotation} = path.scope.getData(name);//typeParams=['K']
const trueTypeParams = typeParams.reduce((memo, name, index) => {
memo[name] = param.typeParameters.params[index].type;//TSLiteralType
return memo;
},{}); //trueTypeParams={K:'TSLiteralType'}
const {checkType,extendsType,trueType,falseType} = typeAnnotation;
let check=checkType.type;
if(check === 'TSTypeReference'){
check = trueTypeParams[checkType.typeName.name]
}
if (transformType(check) === transformType(extendsType.type)) {
return transformType(trueType.type);
} else {
return transformType(falseType.type);
}
}else{
return transformType(param.type);
}
});
const argumentsTypes = path.get('arguments').map(arg=>transformType(arg.getTypeAnnotation().type));
const calleePath = path.scope.getBinding(path.get('callee').node.name).path;
const genericMap=new Map();
calleePath.node.typeParameters.params.map((item, index) => {
genericMap[item.name] = trueTypes[index];
});
const paramsTypes = calleePath.get('params').map(arg=>{
const typeAnnotation = arg.getTypeAnnotation().typeAnnotation;
if(typeAnnotation.type === 'TSTypeReference'){
return genericMap[typeAnnotation.typeName.name];
}else{
return transformType(type);
}
});
Error.stackTraceLimit = 0;
paramsTypes.forEach((type,index)=>{
if(type !== argumentsTypes[index]){
errors.push(
path.get(`arguments.${index}`).buildCodeFrameError(`实参${argumentsTypes[index]}不能匹配形参${type}`, Error)
);
}
});
}
},
post(file) {
console.log(...file.get('errors'));
}
}
}
let sourceCode = `
type Infer<K> = K extends 'number' ? number : string;
function sum<T>(a: T, b: T) {
}
sum<Infer<'number'>>(1, 2);
`;
const result = babel.transform(sourceCode, {
parserOpts:{plugins:['typescript']},
plugins: [tscCheckPlugin()]
})
console.log(result.code);