利用Swc Rust语言编写一个babel-import 功能

2,667 阅读5分钟

背景提要

今天就主要介绍 如何用 swc 这个 rust 知名库来自己实现一个 babel-import, 目前 swc 的文档 还仅仅停留在 transfrom 和 plugin 的层面,其实 swc 是一个丰富 rust 针对 js 语言的 parser 和 generator 库,笔者也是翻阅了很多的 swc的 源码 和 跑过了 swc 的单元测试 掌握了这个能力,希望看到的这篇文章有用的人可以适当点赞。

步骤拆分

  • (旧)已知 babel 的 实现 babel-import 大概需要以下3步

    • 利用 @babel/parser 中 parser 把 code 改变 成 AST 结构
  • 利用 @babel/traverse 中 traverse 把 ast 遍历寻找 对应需要修改的 node 节点。
  • 利用 @babel/generator 中 把第二步 已修改的 ast 进行按 目标语言重新组装。

参考链接:babeljs.io/docs/en/bab…

参考链接:babeljs.io/docs/en/bab…

已知 未知

  • (新) 根据 babel 的类似步骤,了解 swc 对其自己 ast 整套流程,并脱离其 transform 和 plugin 的实现。

    • 找到对应的 parser 方法 ,通过翻阅 swc/core 核心的 rust-doc 锁定 swc_ecma_parser 这个库 有通过该库 rust-doc (docs.rs/swc_ecma_pa…) 掌握了 调用方法。
  • 继续翻阅 swc/project 的源码,swc_ecma_parser 查询 parser后 单元测试 掌握源码内容一般对 ast 都有进行过哪些修改,找到类似 @babel/traverse 的方法。
  • 通过 trasnsform 的源码实现,和 parser的 ast 对象找到最后重新组合 ast 最简调用方法。来替代 generator 这个步骤。

具体源码实现

#[cfg(test)]
mod tests {
    use std::collections::HashMap;
    use std::io::stderr;
    use swc::Compiler;
    use swc::config::SourceMapsConfig;
    use swc::ecmascript::ast::{EsVersion, ImportDecl, ImportNamedSpecifier, ImportStarAsSpecifier, ModuleDecl};
    use swc_common::sync::Lrc;
    use swc_common::{DUMMY_SP, errors::{ColorConfig, Handler}, FileName, FilePathMapping, SourceMap};
    use swc_ecma_parser::{EsConfig, lexer::Lexer, Parser, StringInput, Syntax, TsConfig};
    use swc::ecmascript::ast::{Decl, Module, ModuleItem, Pat, Stmt};
    use swc_atoms::{js_word, JsWord};
    use swc_ecma_visit::FoldWith;
    use swc_ecma_ast::{ImportSpecifier, Str};
    use swc_ecma_transforms::const_modules;
    use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, VisitMut, VisitMutWith};
    use swc_bundler::Bundler;

    #[test]
    fn mut_change() {
        let mut list = vec![1, 2, 3, 4];
        let mut temp_list: Vec<i32> = vec![];
        for num in list.iter_mut() {
            let c = (*num) % 2;
            if c == 0 {
                *num = *num * 4;
                temp_list.push(*num * 4);
            }
        }
        list.extend(temp_list);
        let str = list.iter().map(|x| { return x.to_string(); }).collect::<Vec<std::string::String>>().join("\n");
        println!("{}", str);
    }

    #[test]
    fn transform() {
        let source = "
import React from "react";
import ReactDOM from "react-dom";
import {Button, Input} from "antd";
import Child from "./component/Child";

class Page extends React.Component {
    render() {
        return (
            <div className={"test"}>
                <div>Page</div>
                <Child/>
                <Button>click me</Button>
                <Input/>
            </div>
        );
    }
}

ReactDOM.render(<Page/>, document.getElementById("root"));
";
        let source2 = "\
        function abc(){\
            console.log(123);
        }";


        let cm: Lrc<SourceMap> = Default::default();
        let fm = cm.new_source_file(
            FileName::Custom("test.js".into()),
            source.into(),
        );

        let compiler = Compiler::new(cm.clone());
        let handler = Handler::with_emitter_writer(Box::new(stderr()), Some(compiler.cm.clone()));

        let lexer = Lexer::new(
            // We want to parse ecmascript
            Syntax::Typescript(TsConfig {
                tsx: true,
                decorators: true,
                dynamic_import: true,
                dts: false,
                no_early_errors: false,
                import_assertions: false,
            }),
            // EsVersion defaults to es5
            EsVersion::Es2016,
            StringInput::from(&*fm),
            None,
        );

        let mut parser = Parser::new_from(lexer);

        let list_error = parser.take_errors();
        if list_error.iter().len() > 0 {
            let mut err_msg = "".to_owned();
            for err in list_error {
                let msg = err.into_kind().msg().to_string();
                err_msg.push_str(msg.as_str());
            }
        }

        let mut module = parser
            .parse_module().unwrap();

        println!("parser success");

        let s = serde_json::to_string_pretty(&module).expect("failed to serialize");
        println!("ast json is \n {}", s);

        let mut specifiers = vec![];

        for item in &mut module.body {
            if let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item {
                let source = &*var.src.value;
                if source == "antd" {
                    for specifier in &var.specifiers {
                        match specifier {
                            ImportSpecifier::Named(ref s) => {
                                let ident = format!("{}", s.local.sym);
                                specifiers.push(format!("antd/es/{}/style/index.css", ident.to_lowercase()));
                            }
                            ImportSpecifier::Default(ref s) => {}
                            ImportSpecifier::Namespace(ref ns) => {}
                        }
                    }
                }
            }
        }

        for css_source in specifiers.clone() {
            let css_source_ref = css_source.as_str();
            let dec = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
                span: DUMMY_SP,
                specifiers: vec![],
                src: Str {
                    span: DUMMY_SP,
                    value: JsWord::from(css_source_ref),
                    has_escape: false,
                    kind: Default::default(),
                },
                type_only: false,
                asserts: None,
            }));
            module.body.insert(0, dec);
        }

        let new_res = compiler.print(&module,
                                     None,
                                     None,
                                     false,
                                     EsVersion::Es2020,
                                     SourceMapsConfig::Bool(false),
                                     &Default::default(),
                                     None,
                                     false,
                                     None, ).unwrap();


        println!("gen success");
    }
}

源码导读 (rust 新手必看)

前置依赖

  • 需要用到的 cargo 库 (这里有一些源码中 未提及 是因为 源码并未涉及到 ts 的 ast 和其他 vistit 的情况)

image.png

Parser 拆解 AST

  • 拆解 AST 核心 3步骤 获得 AST 结构
 // 声明一个 针对 swc 源码文件对象

 // 如果 你的 code 是单纯 的 String 第二个文件名可以随便填写

 // 第三个 则是 针对 js-code 的 string 值 

 // var a = 123; 

 // function abc(){ console.log(123);}

 // .... 等等

 let fm = cm.new_source_file(

            FileName::Custom("test.js".into()),

            source.into(),

        );

  

 // 声明 源码 的 Lexer 解析规则 

 // 说人话就是 是不是 ts 是不是 jsx 是不是 有 es2017+ 的语法

 // 最后第三个参数 关联 上述 源码内容      

 let lexer = Lexer::new(

            // We want to parse ecmascript

            Syntax::Es(EsConfig {

                jsx: true,

                ...

            }),

            // EsVersion defaults to es5

            EsVersion::Es2016,

            StringInput::from(&*fm),

            None,

        );

 // 声明一个 Parser 对象

 let mut parser = Parser::new_from(lexer);

 // 在JS 中 每个文件一般默认为一个 Module 即按照 Module 来去 拆解 

 // 获取 "module" 就是整个 文件的 ast 对象

 let mut module = parser.parse_module().unwrap();

根据上述方法 去拆解下列源码 获得 AST 数据 见下图2

import React from "react";

import ReactDOM from "react-dom";

import {Button, Input} from "antd";

import Child from "./component/Child";



class Page extends React.Component {

    render() {

        return (

            <div className={"test"}>

                <div>Page</div>

                <Child/>

                <Button>click me</Button>

                <Input/>

            </div>

        );

    }

}



ReactDOM.render(<Page/>, document.getElementById("root"));

AST 数据 github.com/zhusjfaker/…

image.png

通过上述 图片 我们可以看到 swc 和 AST结构 是和 Babel 有些高度类似的。

Traverse Node 遍历节点 以及修改

  • 在 "module" ast 对象中 匹配到 对应的 node 节点,抽取出关键的信息,然后加工 ast对象

通过上述 图片 我们可以看到 swc 和 AST结构 是和 Babel 有些高度类似的。

Traverse Node 遍历节点 以及修改

  • 在 "module" ast 对象中 匹配到 对应的 node 节点,抽取出关键的信息,然后加工 ast对象
for item in module.body {

            if let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item {

                let source = &*var.src.value;

                if source == "antd" {

                    for specifier in &var.specifiers {

                        match specifier {

                            ImportSpecifier::Named(ref s) => {

                                let ident = format!("{}", s.local.sym);

                                specifiers.push(format!("antd/es/{}/style/index.css", ident.to_lowercase()));

                            }

                            ImportSpecifier::Default(ref s) => {}

                            ImportSpecifier::Namespace(ref ns) => {}

                        }

                    }

                    for css_source in specifiers.clone() {

                        let css_source_ref = css_source.as_str();

                        let dec = ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {

                            span: DUMMY_SP,

                            specifiers: vec![],

                            src: Str {

                                span: DUMMY_SP,

                                value: JsWord::from(css_source_ref),

                                has_escape: false,

                                kind: Default::default(),

                            },

                            type_only: false,

                            asserts: None,

                        }));

                        let body = &mut newmodule.body;

                        body.insert(0, dec);

                    }

                }

            }

        }
  • 以下列出 这段代码 的难点。

    • 在 if 中居然存在 赋值语句 “let ModuleItem::ModuleDecl(ModuleDecl::Import(var)) = item” 这个 是 rust 的 特有 “模式匹配” “结构用法” 可以理解为 在 Module -> ModuleItem 对象 中 -> ModuleDecl 接口 实现了 import 即为 ImportDeclaration 恰好符合 右边 遍历中的 item 时, 则把 item 的 ref 引用 赋值给 var 。

  • 第二个在 JS 的AST 对象中 提取 字符串 并不是 rust 中 std::string:String 类型,而是 swc_atoms::JsWord 那这个两个 类型 怎么来实现 互相转换呢,其实很简单 有两种方法,
  1. 通过简单的 “拆箱” & “装箱”
let source = &*var.src.value;

if source == "antd"
  1. 通过 Rust 底层 Display 实现
// s 是 Js_Word

let ident = format!("{}", s.local.sym);
  • DUMMY_SP “use swc_common::DUMMY_SP” 可以在你 插入新的 代码树 Node 时有效不出错。

重新合成 指定语言

// 声明 compiler 对象

// 这个对象是 你只要调用 swc 必须声明 不管是否是 transform 

// 是 swc::core 中的一个 核心对象

let compiler = Compiler::new(cm.clone());



// 根据 Rust 特性操作 ast 必须在新树上才可以 有权限。

let mut newmodule = module.clone() as Module;



// 有很多人 用 fold_with 来生成 代码 

// 根据源码 查询后 发现不需要 只要需要在 上述 compiler 对象 调用 print 方法 即可

// 获得 新的 代码

let new_res = compiler.print(&newmodule,

                                     None,

                                     None,

                                     false,

                                     EsVersion::Es2020,

                                     SourceMapsConfig::Bool(false),

                                     &Default::default(),

                                     None,

                                     false,

                                     None, ).unwrap();

后续规划

继续推出 rust 编译 wasm 和 napi-rs 和 cgo 调用链接 教程。有兴趣可以持续关注。