实现一个swc-plugin

swc介绍

swc(speedy web compiler) 是用Rust编写的高性能JavaScript/TypeScript编译器。其核心功能与 Babel 类似,支持编译、打包、代码压缩等功能,也具备插件能力。

swc对比babel优势

  1. 开发语言的优势

swc 是用 Rust 语言开发的,而 babel 是用 JavaScript 语言开发的。这意味着 swc 可以利用 Rust 的性能优势,如无 GC(垃圾回收机制)、系统级语言、多核 CPU 支持等,而 babel 则受限于 JavaScript 的性能问题,如 GC、单线程等。

  1. AST 树透传

swc 在编译代码时并不需要将 AST 树透传下去,而是直接在内存中进行转换。这样可以避免多次遍历 AST 树,提高编译效率。而 babel 则需要将 AST 树传递给每一个插件,这样会增加编译时间和内存消耗

  1. 编译后的代码更快

swc 可以将 JavaScript 代码编译为 WebAssembly,这样可以提高代码的执行速度。而 babel 则只能编译为 JavaScript 代码,无法利用 WebAssembly 的优势。

swc-plugin开发

创建一个swc项目

首先用cargo安装swc_cli

cargo install swc_cli

通过swc生成项目模版

swc plugin new --target-type wasm32-wasi my-first-plugin
rustup target add wasm32-wasi

实现swc-plugin

lib.rs

use swc_core::ecma::{
    ast::Program,
    transforms::testing::test_inline,
    visit::{as_folder, FoldWith, VisitMut},
};
use swc_core::plugin::{plugin_transform, proxies::TransformPluginProgramMetadata};
pub struct TransformVisitor;
// 我们需要为TransformVisitor实现VisitMut特征
// 当遍历到对应的ast时,这些方法将会被调用
impl VisitMut for TransformVisitor {
}
// 项目入口
#[plugin_transform]
pub fn process_transform(program: Program, _metadata: TransformPluginProgramMetadata) -> Program {
    program.fold_with(&mut as_folder(TransformVisitor))
}
// 测试用例
test_inline!(
    Default::default(),
    |_| as_folder(TransformVisitor),
    boo,
    // Input codes
    r#"console.log("transform");"#,
    // Output codes after transformed with plugin
    r#"console.log("transform");"#
);

需求拆分

实现一个swc-plugin

测试驱动开发

了解了上述流程图中的需求后,我们可以采用TTD的开发模式,先编写些测试用例

// case1: 没有try...catch...类型节点
test_inline!(
    Default::default(),
    |_| as_folder(CatchClauseVisitor),
    catch_test,
    // Input codes
    r#"
    let a = 1;
    "#,
    // Output codes after transformed with plugin
    r#"
    let a = 1;
    "#
);
// case2: 未导入包名
test_inline!(
    Default::default(),
    |_| as_folder(CatchClauseVisitor),
    catch_test2,
   // Input codes
    r#"try{
        let a = 1;
    }catch(error){
        
    }"#,
    // Output codes after transformed with plugin
    r#"
    import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
    }catch(error){
        weirwoodErrorReport(error)
    }"#
);
// case3: 导入包名但未导入该方法
test_inline!(
    Default::default(),
    |_| as_folder(CatchClauseVisitor),
    catch_test3,
    // Input codes
   r#"
    import {initMonitor} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
    }catch(err){
    }
    "#,
    // Output codes after transformed with plugin
    r#"
    import {initMonitor, weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
    }catch(err){
        weirwoodErrorReport(err)
    }
    "#
);
// case4: 已导入包名及方法
test_inline!(
    Default::default(),
    |_| as_folder(CatchClauseVisitor),
    catch_test4,
    // Input codes
    r#"
    import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
        let c = 2;
    }catch(error){
        let b = 2;
    }"#,
    // Output codes after transformed with plugin
    r#"
    import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
        let c = 2;
    }catch(error){
        let b = 2;
        weirwoodErrorReport(error);
    }"#
);
// case5: 导入包名和方法,且已在catch中使用
test_inline!(
    Default::default(),
    |_| as_folder(CatchClauseVisitor),
    catch_test5,
    // Input codes
    r#"
    import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
        let c = 2;
    }catch(error){
        let b = 2;
        weirwoodErrorReport(error);
    }"#,
    // Output codes after transformed with plugin
    r#"
    import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
        let c = 2;
    }catch(error){
        let b = 2;
        weirwoodErrorReport(error);
    }"#
);
// case6: try...catch...嵌套情况
test_inline!(
    Default::default(),
    |_| as_folder(CatchClauseVisitor),
    catch_test6,
   // Input codes
    r#"
    try{
        let a = 1;
        let c = 2;
        try {
            let d = 3;
        }catch(err){
            console.log(err);
        }
    }catch(error){
        let b = 2;
    }"#,
    // Output codes after transformed with plugin
    r#"
    import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
        let c = 2;
        try {
            let d = 3;
        }catch(err){
            weirwoodErrorReport(err);
            console.log(err);
        }
    }catch(error){
        weirwoodErrorReport(error);
        let b = 2;
    }"#
);

实现Visitor遍历ast

所有的ast节点类型如下:rustdoc.swc.rs/swc_ecma_vi…

可通过如下查看代码转化后的ast节点类型:

我们主要使用visit_mut_modulevisit_mut_catch_clausevisit_mut_module用来处理import的导入,visit_mut_catch_clause用来捕获try…catch…语句

代码暂时无法公开

分离测试模块

目前我们的测试用例同逻辑代码都是一起写在lib.rs下,通常一个复杂的swc-plugin都是由好几个不同的模块组合而成,所以将测试用例与逻辑代码分离是十分重要的,并测试用例有按模块划分的能力。

我们在src下新建一个tests目录,用来存放测试用例。在tests目录下新建common/mod.rs文件 用来实现通用的测试方法,uv_test.rs文件实现本功能的测试用例,mod.rs用来组织各模块的测试用例

目录结构如下:

实现一个swc-plugin

// common/mod.rs
#[macro_export]
macro_rules! to {
    ($name:ident, $from:expr, $to:expr) => {
        swc_core::ecma::transforms::testing::test_inline!(
            Default::default(),
            |_| swc_core::ecma::visit::as_folder($crate::CatchClauseVisitor::new()),
            $name,
            $from,
            $to
        );
    };
}
// uv_test.rs
use crate::to;
to!(
    catch_test_1,
    // Input codes
    r#"
    import {initMonitor} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
    }catch(err){
    }
    "#,
    // Output codes after transformed with plugin
    r#"
    import {initMonitor, weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
    }catch(err){
        weirwoodErrorReport(err)
    }
    "#
);
to!(
    catch_test_2,
    // Input codes
    r#"try{
        let a = 1;
    }catch(error){
        
    }"#,
    // Output codes after transformed with plugin
    r#"
    import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
    try{
        let a = 1;
    }catch(error){
        weirwoodErrorReport(error)
    }"#
);
// mod.rs
mod common;
mod uv_test;

发布swc-plugin

swc-plugin的发布和Rust library发布不同,不需要走crates.io,发布流程和npm包的发布一样

npm login
npm publish

swc-plugin使用

  1. 准备一个空项目,执行pnpm init进行初始化
  2. 安装 @swc/cli @swc/core,pnpm i -D @swc/cli @swc/core
  3. 安装刚刚发布的swc-plugin pnpm i swc-plugin
  4. 修改.swcrc或者swc-loader中的配置
// .swcrc
{
    "$schema": "https://json.schemastore.org/swcrc",
    "jsc": {
      "parser": {
        "syntax": "ecmascript"
      },
      "target": "es2015",
      "experimental": {
        "plugins": [
            ["swc-plugin",{}],
        ]
      }
    },
    "minify": false
  }
  
  // webpack.config.js 
  // swc-loader
  module: {
        rules: [
            {
                test: /.js$/,
                use: [
                    {
                        loader: 'swc-loader',
                        options: {
                            jsc: {
                                parser: { syntax: 'ecmascript' },
                                experimental: {
                                    plugins:  ["swc-plugin",{}],
                                },
                            },
                        },
                    }
                ],
                exclude: /node_modules/,
            },
        ],
    },
  1. 新建一个source.js
try{
}catch(err){
    console.log(err);
}
  1. 使用swc进行编译npx swc source.js -o output.js
// output.js
 import {weirwoodErrorReport} from "@common/utils/basicAbility/monitor";
try {} catch (err) {
    weirwoodErrorReport(err);
    console.log(err);
}

参考文档:

原文链接:https://juejin.cn/post/7347667306459807795 作者:晨出

(0)
上一篇 2024年3月19日 上午11:09
下一篇 2024年3月19日 下午4:00

相关推荐

发表评论

登录后才能评论