babel预设&插件制作

103 阅读3分钟

image.png

Path

Path 是表示两个节点之间连接的对象

例如,如果有下面这样一个节点及其子节点︰

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  ...
}

将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:

{
  "parent": {
    "type": "FunctionDeclaration",
    "id": {...},
    ....
  },
  "node": {
    "type": "Identifier",
    "name": "square"
  }
}

同时它还包含关于该路径的其他元数据:

{
  "parent": {...},
  "node": {...},
  "hub": {...},
  "contexts": [],
  "data": {},
  "shouldSkip": false,
  "shouldStop": false,
  "removed": false,
  "state": null,
  "opts": null,
  "skipKeys": null,
  "parentPath": null,
  "context": null,
  "container": null,
  "listKey": null,
  "inList": false,
  "parentKey": null,
  "key": null,
  "scope": null,
  "type": null,
  "typeAnnotation": null
}

Babel插件开发

尝试空的插件

  1. npm init -y
  1. npm i @bale/core -D
  1. 编写插件
module.exports = () => {
  return {
    visitor: {
      // 逻辑
    }
  }
}
  1. 测试
const {transformSync} = require('@babel/core')
const Myplugin  = require('./index')

const code = `
let b = 90
let c =90

console.log(b+c)
`
let config = {
  plugins: [Myplugin]
}
const out =  transformSync(code,config)

console.log(out.code)

编写

module.exports = function ({ types: t }) {
  return {
    visitor: {
      Identifier(path) {
        const parentNodeIsIfStatement = t.isIfStatement(path.parent);
        const isDebug = path.node.name === "DEBUG";

        if (isDebug && parentNodeIsIfStatement) {
          const stringNode = t.stringLiteral("DEBUG");
          path.replaceWith(stringNode);
        }
      },

      StringLiteral(path) {
        const parentNodeIsIfStatement = t.isIfStatement(path.parent);
        const isDebug = path.node.value === "DEBUG";

        if (isDebug && parentNodeIsIfStatement) {
          if (process.env.NODE_ENV === "production") {
            path.parentPath.remove();
          }
        }
      },
    },
  };
};

State

state是所有节点函数的第二个对象

  • state.opts是在编写.babelrc 插件的参数

进阶

生产AST

const { transformSync, parse, traverse, types, transformFromAstSync } = require('@babel/core')

Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)

解析 (代码到AST)

const code1 = `
  let a  = 2
`
const ast = parse(code1)

转换(AST到代码)

const ast = parse('const a = 1')
const { code } = transformFromAstSync(ast)
console.log(code)

遍历操作

AST树官方链接 :实时观察代码产生的AST

traverse(ast, {
  enter(nodePath) {
      console.log(nodePath.type)
  }
})
>>>output
        Program
        VariableDeclaration
        VariableDeclarator
        Identifier
        NumericLiteral

操作

遍历的所有函数(函数与节点类型同名)都会得到两个变量 Nodepath, state

NodePath是用来描述节点关系的结构,通过traverse中的节点方法暴露出来。

常用的属性/方法包括:

  • NodePath.node 节点本身
  • NodePath.parentPath 父级节点
  • NodePath.type 节点类型字符串
  • NodePath.scope 语法上下文,能得到全局变量等信息
  • NodePath.traverse() 自遍历
  • NodePath.replaceWith() 节点替换
  • NodePath.remove() 节点删除

使用types创建AST

  • types.identifier("TEST")

定义插件

暴露的 visitor 就是 traverse 的第二个参数

module.exports = ({types: t}) => {
  let count =0
  return {
    visitor: { 
      Identifier(path, state) {
        const name = path.node.name
        if(name !== "DEBUG"){
          return
        }
        if(t.isIfStatement(path.parent)){
          path.replaceWith(t.stringLiteral("DEBUG"))
        }
      },
      StringLiteral(path, {opts:{useDebug}}) {
        console.log(++count)
        console.log(path.type)
        const value = path.node.value
        if(value !== "DEBUG"){
          return
        }
        if(t.isIfStatement(path.parent) && useDebug ){
          path.parentPath.remove()
        }
      }
    },
  }
}

调用插件

const ast = parse('const a = 1')
const { code } = transformFromAstSync(
    ast,
    null,
    { plugins: [ plugin ] }  // 这里
)
console.log(code)

@babel/types

const { types } = require('@babel/core')

import * as types from '@babel/types'

既是这里的types也是定义插件第一个参数 ({types: t})

generatortransfrom
可以基于任何类型ast节点输出代码只能基于完整ast
代码压缩、引号选择等简单配置完整代码配置,支持插件

创建节点

  • 节点类型同名方法直接调用
import * as types from '@babel/types'
import gen from '@babel/generator'

const log = (node: types.Node) => {
    console.log(gen(node).code)
}

log(types.stringLiteral('string'))
log(types.numericLiteral(10e4))
log(types.booleanLiteral(0.5 > Math.random()))
log(types.regExpLiteral('\.jsx?$', 'g'))

//创建Array
log(
    types.arrayExpression([
        types.stringLiteral('string'),
        types.numericLiteral(10e4),
        types.booleanLiteral(0.5 > Math.random()),
        types.regExpLiteral('\.jsx?$', 'g')
    ])
)
// 创建Object
log(
    types.objectExpression([
        types.objectProperty(
            types.identifier('a'),
            types.nullLiteral()
        ),
        types.objectProperty(
            // 字符串类型 key
            types.stringLiteral('*'),
            types.arrayExpression([]),
        ),
        types.objectProperty(
            types.identifier('id'),
            types.identifier('id'),
            false,
            // shorthand 对 { id: id } 简写为 { id }
            true
        ),
        types.objectProperty(
            types.memberExpression(
                types.identifier('props'),
                types.identifier('class')
            ),
            types.booleanLiteral(true),
            // 计算值 key
            true
        )
    ])
)

// 创建函数
function foo(arg1) {
  console.log(arg1);
}
log(
    types.functionDeclaration(
        types.identifier('foo'),
        [types.identifier('arg1')],
        types.blockStatement([
            types.expressionStatement(
                types.callExpression(
                    types.identifier('console.log'),
                    [types.identifier('arg1')]
                )
            )
        ])
    )
)

源码

插件

发布npm包暴露需要使用的插件对象,babel插件的本质就是一个函数

module.exports = ({types: t}) => {
  return {
    visitor: {
      Identifier(path, state) {
        const name = path.node.name
        if(name !== "DEBUG"){
             return
        }
        if(t.isIfStatement(path.parent)){
          path.replaceWith(t.stringLiteral("DEBUG"))
        }
      },
      StringLiteral(path, {opts:{useDebug}}) {
        const value = path.node.value
        console.log('useDebug', useDebug, value)
        if(value !== "DEBUG"){
          return
        }
        if(t.isIfStatement(path.parentPath) && useDebug ){
          path.parentPath.remove()
        }
      }
    }
  }
}

预设

预设的第二个参数是用户自定义的的参数

预设的制作主要:

  1. 创建一个package.json 把需要用的插件安装到 dependencies
  2. 暴露一个文件用于声明预设即可
const Myplugin = require("babel-plugin-lf-debug");
module.exports = (api, opts= {})=>{
  console.log('opts', opts)
  return {
    plugins: [[Myplugin ,{ useDebug: opts.useDebug }]]
  }
}

参考

  1. @babel/types深度应用
  2. babel官方网站
  3. babel手册