如何写一个源代码转换babel插件

1,019 阅读2分钟

AST介绍

定义

抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构

作用

  • 编辑器的错误提示、代码格式化、代码高亮、代码自动补全
  • elintpretiier 对代码错误或风格的检查
  • webpack 通过 babel 转译 javascript 语法 js编译编译运行的第一步就是生成AST树

词法分析

词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元。 词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。 最终,整个代码将被分割进一个tokens列表(或者说一维数组)

语法分析

语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误

AST展示

image.png

具体属性展示可参考:astexplorer.net/

Babel介绍

定义

Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中

作用

  • 语法转换 (es6转换为es5)
  • 通过 Polyfill 方式在目标环境中添加缺失的特性 (通过引入第三方 polyfill 模块,例如 core-js
  • 源码转换 (将源码进行统一的变化升级)

Babel的配置方式

  • babel.config.js (注意需要module.export)
  • .babelrc (qrn采用的方式)

使用方式

1.通过shell指令 babel --plugins @babel/plugin-transform-arrow-functions script.js
2.使用api

require("@babel/core").transformSync("code", { plugins: ["@babel/plugin-transform-arrow-functions"], });

Plugin与Presets

1.执行顺序:

  • 插件在Presets前运行
  • 插件顺序从前往后
  • 顺序是颠倒的 2.插件是具体功能的体现,粒度越小使用起来越方便。Preset是一组插件的集合

Babel插件开发常用插件预览

image.png

开发流程演示

image.png 需转换的代码

import React, { Component } from 'react';
const fs = require('fs');
class ClassComponent extends Component {
    constructor(props) {
        super(props);
    }

    componentDidMount() {
        console.log('componentDidMount');
        // const a = 2;
    }

    render() {
        return (
            <div>
                <span>demo</span>
            </div>
        )
    }
}
export default ClassComponent;

function fun() {
    console.log("data");
}
const x: number = 123;
type Props = $ReadOnly<{||}>;
type State = {|clicked: string|};

源代码转换片段

"use strict";

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const babel = require('@babel/core');
const types = require("@babel/types");
const fs = require('fs');
const path = require('path');
const logger = require("../logger");

const REG = /.js$|.jsx$|.ts$|.tsx$/;
const FILTER_DIR = ['node_modules', '.git'];


function compile(code, fileName) {
    const visitor = {
        CallExpression(path) {
            const node = path.node;
            //处理require引用文件路径变化
            if (node.callee?.name === 'require' && node?.arguments[0]?.value === 'fs') {
                node.arguments[0].value = 'path';
                node.arguments[0].raw = 'path';
            }
        },
        ReturnStatement(path) { //处理组件名和属性变化
            const node  = path.node;
            // const argument = types.expre
            if (node?.argument?.type === 'JSXElement') {
                node.argument.openingElement = types.jsxOpeningElement(types.jsxIdentifier('Fragment'), [types.jsxAttribute(types.jsxIdentifier('id'), types.stringLiteral('aaa'))], false);
                node.argument.closingElement = types.jsxClosingElement(types.jsxIdentifier('Fragment'));
            }
        },
        ClassBody(path) { //处理组件生命周期发生变化
            const node = path.node;
            node?.body?.forEach((item) => {
                if (item.key.name === 'componentDidMount') {
                    item.key.name = 'componentDidUpdate';
                }
            })
        },
        ImportDeclaration(path) { //处理import应用路径发生变化
            const node = path.node;
            if (node?.source?.value === 'react') {
                node.source.value = 'react-dom';
                node.source.raw = 'react-dom';
            }
        }
    }

    //   method 1
    // const ast = parser.parse(code, {
    //     sourceType: "module",
    //     plugins: [
    //         "jsx",
    //         ["flow"]
    //     ]
    // });
    // traverse.default(ast, visitor);
    // const result = generator.default(ast, {}, code);
    // return result;

    //  method 2
    const result = babel.transform(code, {
        plugins: [
            {visitor},
            '@babel/plugin-syntax-jsx',
            '@babel/plugin-syntax-flow',
            // ['@babel/plugin-syntax-typescript', {isTSX: true}],
        ]
    })
    return result;

}

function traverseFile(dir) {
    fs.readdirSync(dir).forEach((item) => {
        const filePath = path.join(dir,item);
        const stat = fs.lstatSync(filePath);
        if (stat.isDirectory()) {
            if (!FILTER_DIR.includes(item)) {
                if (fs.existsSync(filePath)) {
                    traverseFile(filePath);
                }
            }
        } else {
            if (REG.test(item)) {
                if (fs.existsSync(filePath)) {
                    const content = fs.readFileSync(filePath, 'utf-8');
                    const newCode = compile(content,filePath)
                    fs.writeFile(filePath,newCode.code, err => {
                        if (err) {
                            logger.error(err,`更新代码写入错误`);
                        } else {
                            logger.success(`升级文件${filePath}的代码成功`);
                        }
                    })
                } else {
                    logger.error(`${filePath}文件不存在`);
                }
            }
        }
    })
}

module.exports = (options) => {
    const args = options.args;
    if (args.length === 0) {
        traverseFile(process.cwd());
    } else {
        args.forEach((item) => {
            //绝对路径
            if (fs.existsSync(item)) {
                traverseFile(item);
            } else {
                //相对路径
                const filePath = path.join(process.cwd(),item);
                if (fs.existsSync(filePath)) {
                    traverseFile(filePath);
                } else {
                    logger.error(`路径${item}和${filePath}都不存在`);
                }
            }
        })
    }
    // traverseFile(process.cwd())
    // const filePath = path.join(process.cwd(), './src/lib/demo.ts');
    // if (fs.existsSync(filePath)) {
    //     const content = fs.readFileSync(filePath, 'utf-8');
    //     const newCode = compile(content, 'demo.ts');
    //     console.log(newCode, 'new_code');
    //     fs.writeFile('./test.ts',newCode.code, err => {
    //         if (err) throw err;
    //         logger.success(`升级文件${filePath}的代码成功`);
    //     })
    // } else {
    //     logger.error(`${filePath}文件不存在`);
    // }
    
};

转换后的代码

import React, { Component } from "react-dom";

const fs = require("path");

class ClassComponent extends Component {
  constructor(props) {
    super(props);
  }

  componentDidUpdate() {
    console.log('componentDidMount'); // const a = 2;
  }

  render() {
    return <Fragment id="aaa">
                <span>demo</span>
            </Fragment>;
  }

}

export default ClassComponent;

function fun() {
  console.log("data");
}

const x: number = 123;
type Props = $ReadOnly<{||}>;
type State = {|
  clicked: string
|};

参考

-Babel插件开发手册
-Babel用户手册
-Babel官网
-生成AST节点参考文档
-AST转换
-Babel Github