buddywang / blog

0 stars 0 forks source link

babel插件 #30

Open buddywang opened 3 years ago

buddywang commented 3 years ago

问题

babel 流程(转译器流程)

转译插件开发思路

  1. 确定我们需要处理的节点类型
  2. 处理节点
  3. 返回新的节点
// 插件长这样
// export default function(babel) {
export default function({types: t}) {
  return {
    visitor: {
      Identifier(path, state) {},
      ASTNodeTypeHere: {
        enter(path, state) {},
        exit() {}
      }
    }
  };
};

数据结构

node

ast 由 node 组成

Paths(路径)

Path 是表示两个节点之间连接的对象,在某种意义上,Path 是一个节点在树中的位置以及关于该节点各种信息的响应式 Reactive 表示。

{
  "parent": {...},
  "node": {...},
  "contexts": [],
  "data": {},
  "removed": false,
  "state": null,
  "parentPath": null,
  "context": null,
  "container": null,
  "parentKey": null,
  "key": null,
  "scope": null,
  "type": null,
  "typeAnnotation": null,
  ...
}

通常通过操作 path 来更改 node

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};

Scopes(作用域)

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...],
  ...
}

Bindings(绑定)

所有引用属于特定的作用域,引用和作用域的这种关系被称作:绑定(binding)。单个绑定看起来像这样︰

{
  identifier: node,
  scope: scope,
  path: path,
  kind: 'var',

  referenced: true,
  references: 3,
  referencePaths: [path, path, path],

  constant: false,
  constantViolations: [path],
  ...
}

有了这些信息你就可以查找一个绑定的所有引用,并且知道这是什么类型的绑定(参数,定义等等),查找它所属的作用域,或者拷贝它的标识符。你甚至可以知道它是不是常量,如果不是,那么是哪个路径修改了它。

babel提供的工具包

工具包 功能
@babel/parser babel里的JavaScript解析器,用于解析代码生成ast
@babel/core babel的核心功能的组合封装,例如将parser、traverse、generator功能组合起来向外提供transform方法
@babel/generator 将ast转换成代码
@babel/code-frame 提供打印定位代码位置的能力,用于显示报错提示
@babel/runtime 一个包含运行时 helper 方法和一个 regenerator-runtime 的库,不常用,一般在 @babel/plugin-transform-runtime 里被调用
@babel/template 从字符串模板生成ast
@babel/traverse 遍历ast
@babel/types 提供构建ast节点和验证ast节点的能力

例子

下面的插件例子功能是把代码里 console.xxx 输出日志的语句删除,第一位参数是 "do not move" 的保留:

const simpleDeleteLogPlugin = ({ types, template }, options, dirname) => {
    return {
        visitor: {
            CallExpression(path, state) {
                const calleeName = path.get('callee').toString();
                const arg = path.node.arguments
                if (calleeName.indexOf('console') !== -1) {
                    if (arg.length > 0 && types.isStringLiteral(arg[0], { value: "do not move" })) {
                        // do not move
                    } else {
                        path.remove()
                    }
                }
            }
        }
    }
}

module.exports = simpleDeleteLogPlugin;

使用结果:

const { transformFromAstSync } = require('@babel/core');
const parser = require('@babel/parser');
const simpleDeleteLogPlugin = require('./plugin/demo-plugin');

const sourceCode = `
    console.log(console.log(123))
    function a() {
        const obj = { b: 2 }
        console.log(obj);
        return obj
    }
    const c = new Promise()
    const b = a()
    console.log('do not move', b)`

const ast = parser.parse(sourceCode, {
    sourceType: 'unambiguous'
});

const { code } = transformFromAstSync(ast, sourceCode, {
    plugins: [
        simpleDeleteLogPlugin
    ]
});

console.log(code);

// 输出结果
// require("core-js/modules/es.object.to-string.js");

// require("core-js/modules/es.promise.js");

// function a() {
//   var obj = {
//     b: 2
//   };
//   return obj;
// }

// var c = new Promise();
// var b = a();
// console.log('do not move', b);

参考