SWC 插件开发 #

插件基础 #

什么是 SWC 插件? #

SWC 插件是用 Rust 编写并编译为 WebAssembly 的代码转换器,可以扩展 SWC 的功能:

text
┌─────────────────────────────────────────────────────────────┐
│                     SWC 插件架构                             │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  源代码 ───► 解析 ───► AST ───► 插件转换 ───► AST ───► 生成  │
│                              │                               │
│                              ▼                               │
│                    ┌──────────────┐                         │
│                    │   插件系统    │                         │
│                    ├──────────────┤                         │
│                    │  Plugin 1    │                         │
│                    │  Plugin 2    │                         │
│                    │  Plugin 3    │                         │
│                    └──────────────┘                         │
│                                                              │
└─────────────────────────────────────────────────────────────┘

插件类型 #

类型 描述 用途
转换插件 修改 AST 代码转换、语法扩展
分析插件 分析代码 代码检查、统计
生成插件 生成代码 文档生成、测试生成

环境准备 #

安装 Rust #

bash
# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 添加 wasm 目标
rustup target add wasm32-wasi
rustup target add wasm32-unknown-unknown

安装 SWC 插件工具 #

bash
# 安装 swc-cli
cargo install swc-cli

# 安装 wasm-pack(可选)
cargo install wasm-pack

创建插件项目 #

使用模板 #

bash
# 使用官方模板
cargo generate swc-project/swc-plugin-template my-plugin

手动创建 #

bash
# 创建项目
mkdir my-plugin
cd my-plugin
cargo init --lib

Cargo.toml 配置 #

toml
[package]
name = "my-swc-plugin"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
swc_core = { version = "0.75", features = ["ecma_plugin_transform"] }

[dev-dependencies]
swc_ecma_parser = "0.138"
swc_ecma_transforms_testing = "0.134"
testing = "0.31"

[profile.release]
lto = true

插件结构 #

基本结构 #

rust
// src/lib.rs
use swc_core::ecma::{
    ast::*,
    visit::{as_folder, FoldWith, VisitMut, VisitMutWith},
    plugin::plugin_transform,
};

pub struct MyPlugin;

impl VisitMut for MyPlugin {
    fn visit_mut_module(&mut self, module: &mut Module) {
        module.visit_mut_children_with(self);
    }
    
    fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
        // 处理函数调用
    }
}

#[plugin_transform]
pub fn process_transform(program: Program, _config: serde_json::Value) -> Program {
    program.fold_with(&mut as_folder(MyPlugin))
}

带配置的插件 #

rust
use swc_core::ecma::{
    ast::*,
    visit::{as_folder, FoldWith},
    plugin::plugin_transform,
};
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    pub option1: bool,
    pub option2: String,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            option1: true,
            option2: "default".to_string(),
        }
    }
}

pub struct MyPlugin {
    config: Config,
}

impl MyPlugin {
    pub fn new(config: Config) -> Self {
        Self { config }
    }
}

impl VisitMut for MyPlugin {
    fn visit_mut_module(&mut self, module: &mut Module) {
        module.visit_mut_children_with(self);
    }
}

#[plugin_transform]
pub fn process_transform(program: Program, config: serde_json::Value) -> Program {
    let config = serde_json::from_value(config).unwrap_or_default();
    program.fold_with(&mut as_folder(MyPlugin::new(config)))
}

AST 转换 #

访问节点 #

rust
use swc_core::ecma::{
    ast::*,
    visit::VisitMut,
};

impl VisitMut for MyPlugin {
    // 访问函数声明
    fn visit_mut_fn_decl(&mut self, decl: &mut FnDecl) {
        println!("Found function: {}", decl.ident.sym);
        decl.visit_mut_children_with(self);
    }
    
    // 访问变量声明
    fn visit_mut_var_decl(&mut self, decl: &mut VarDecl) {
        for decl in &mut decl.decls {
            if let Some(init) = &mut decl.init {
                // 处理变量初始化
            }
        }
    }
    
    // 访问函数调用
    fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
        if let Callee::Expr(callee) = &mut expr.callee {
            if let Expr::Ident(ident) = &**callee {
                if ident.sym == "console" {
                    // 处理 console 调用
                }
            }
        }
    }
}

修改节点 #

rust
use swc_core::ecma::{
    ast::*,
    visit::VisitMut,
};
use swc_core::common::{Span, DUMMY_SP};
use swc_core::ecma::utils::ExprFactory;

impl VisitMut for MyPlugin {
    // 将 console.log 转换为自定义 logger
    fn visit_mut_call_expr(&mut self, expr: &mut CallExpr) {
        expr.visit_mut_children_with(self);
        
        if let Callee::Expr(callee) = &mut expr.callee {
            if let Expr::Member(member) = &mut **callee {
                if let Expr::Ident(obj) = &*member.obj {
                    if obj.sym == "console" {
                        if let MemberProp::Ident(prop) = &member.prop {
                            if prop.sym == "log" {
                                // 创建新的调用表达式
                                let new_callee = Ident::new(
                                    "customLogger".into(),
                                    DUMMY_SP,
                                ).into();
                                
                                expr.callee = Callee::Expr(Box::new(new_callee));
                            }
                        }
                    }
                }
            }
        }
    }
}

创建新节点 #

rust
use swc_core::ecma::{
    ast::*,
    utils::ExprFactory,
};
use swc_core::common::{Span, DUMMY_SP};

// 创建标识符
fn create_ident(name: &str) -> Ident {
    Ident::new(name.into(), DUMMY_SP)
}

// 创建字符串字面量
fn create_string_literal(value: &str) -> Expr {
    Expr::Lit(Lit::Str(Str {
        span: DUMMY_SP,
        value: value.into(),
        raw: None,
    }))
}

// 创建数字字面量
fn create_number_literal(value: f64) -> Expr {
    Expr::Lit(Lit::Num(Number {
        span: DUMMY_SP,
        value,
        raw: None,
    }))
}

// 创建函数调用
fn create_call_expr(callee: &str, args: Vec<Expr>) -> Expr {
    Expr::Call(CallExpr {
        span: DUMMY_SP,
        callee: Callee::Expr(Box::new(Expr::Ident(create_ident(callee)))),
        args: args.into_iter().map(|arg| arg.as_arg()).collect(),
        type_args: None,
    })
}

// 创建对象表达式
fn create_object_expr(properties: Vec<PropOrSpread>) -> Expr {
    Expr::Object(ObjectLit {
        span: DUMMY_SP,
        props: properties,
    })
}

实用插件示例 #

自动添加日志插件 #

rust
use swc_core::ecma::{
    ast::*,
    visit::{as_folder, FoldWith, VisitMut, VisitMutWith},
    plugin::plugin_transform,
};
use swc_core::common::DUMMY_SP;

pub struct AutoLogPlugin;

impl VisitMut for AutoLogPlugin {
    fn visit_mut_fn_decl(&mut self, decl: &mut FnDecl) {
        let fn_name = decl.ident.sym.clone();
        
        if let Some(body) = &mut decl.function.body {
            // 在函数开头添加日志
            let log_stmt = Stmt::Expr(ExprStmt {
                span: DUMMY_SP,
                expr: Box::new(Expr::Call(CallExpr {
                    span: DUMMY_SP,
                    callee: Callee::Expr(Box::new(Expr::Member(MemberExpr {
                        span: DUMMY_SP,
                        obj: Box::new(Expr::Ident(Ident::new("console".into(), DUMMY_SP))),
                        prop: MemberProp::Ident(Ident::new("log".into(), DUMMY_SP)),
                    }))),
                    args: vec![Expr::Lit(Lit::Str(Str {
                        span: DUMMY_SP,
                        value: format!("Entering function: {}", fn_name).into(),
                        raw: None,
                    })).as_arg()],
                    type_args: None,
                })),
            });
            
            body.stmts.insert(0, log_stmt);
        }
        
        decl.visit_mut_children_with(self);
    }
}

#[plugin_transform]
pub fn process_transform(program: Program, _config: serde_json::Value) -> Program {
    program.fold_with(&mut as_folder(AutoLogPlugin))
}

移除 console 插件 #

rust
use swc_core::ecma::{
    ast::*,
    visit::{as_folder, FoldWith, VisitMut, VisitMutWith},
    plugin::plugin_transform,
};
use serde::Deserialize;

#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    pub exclude: Vec<String>,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            exclude: vec!["error".to_string(), "warn".to_string()],
        }
    }
}

pub struct RemoveConsolePlugin {
    config: Config,
}

impl RemoveConsolePlugin {
    pub fn new(config: Config) -> Self {
        Self { config }
    }
}

impl VisitMut for RemoveConsolePlugin {
    fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
        stmts.retain(|stmt| {
            if let Stmt::Expr(expr_stmt) = stmt {
                if let Expr::Call(call) = &*expr_stmt.expr {
                    if let Callee::Expr(callee) = &call.callee {
                        if let Expr::Member(member) = &**callee {
                            if let Expr::Ident(obj) = &*member.obj {
                                if obj.sym == "console" {
                                    if let MemberProp::Ident(prop) = &member.prop {
                                        return self.config.exclude.contains(&prop.sym.to_string());
                                    }
                                }
                            }
                        }
                    }
                }
            }
            true
        });
        
        for stmt in stmts {
            stmt.visit_mut_children_with(self);
        }
    }
}

#[plugin_transform]
pub fn process_transform(program: Program, config: serde_json::Value) -> Program {
    let config = serde_json::from_value(config).unwrap_or_default();
    program.fold_with(&mut as_folder(RemoveConsolePlugin::new(config)))
}

组件名称注入插件 #

rust
use swc_core::ecma::{
    ast::*,
    visit::{as_folder, FoldWith, VisitMut, VisitMutWith},
    plugin::plugin_transform,
};
use swc_core::common::DUMMY_SP;

pub struct ComponentNamePlugin;

impl VisitMut for ComponentNamePlugin {
    fn visit_mut_class_expr(&mut self, expr: &mut ClassExpr) {
        if let Some(ident) = &expr.ident {
            if let Some(body) = &mut expr.class.body {
                // 添加静态属性 displayName
                body.push(ClassMember::ClassProp(ClassProp {
                    span: DUMMY_SP,
                    value: Some(Box::new(Expr::Lit(Lit::Str(Str {
                        span: DUMMY_SP,
                        value: ident.sym.clone(),
                        raw: None,
                    })))),
                    type_ann: None,
                    is_static: true,
                    decorators: vec![],
                    accessibility: None,
                    is_abstract: false,
                    is_optional: false,
                    is_override: false,
                    readonly: false,
                    declare: false,
                    definite: false,
                    key: PropName::Ident(Ident::new("displayName".into(), DUMMY_SP)),
                }));
            }
        }
        
        expr.visit_mut_children_with(self);
    }
}

#[plugin_transform]
pub fn process_transform(program: Program, _config: serde_json::Value) -> Program {
    program.fold_with(&mut as_folder(ComponentNamePlugin))
}

编译和发布 #

编译插件 #

bash
# 编译为 wasm
cargo build --target wasm32-wasi --release

# 输出位置
# target/wasm32-wasi/release/my_plugin.wasm

发布到 npm #

bash
# 创建 package.json
npm init -y

# 复制 wasm 文件
mkdir -p wasm
cp target/wasm32-wasi/release/my_plugin.wasm wasm/

# 发布
npm publish

package.json 示例 #

json
{
  "name": "swc-plugin-my-plugin",
  "version": "1.0.0",
  "description": "My SWC plugin",
  "main": "wasm/my_plugin.wasm",
  "files": [
    "wasm/"
  ],
  "keywords": [
    "swc",
    "plugin"
  ],
  "license": "MIT"
}

使用插件 #

配置插件 #

json
{
  "jsc": {
    "experimental": {
      "plugins": [
        ["swc-plugin-my-plugin", {
          "option1": true,
          "option2": "value"
        }]
      ]
    }
  }
}

安装插件 #

bash
npm install swc-plugin-my-plugin

测试插件 #

单元测试 #

rust
#[cfg(test)]
mod tests {
    use super::*;
    use swc_ecma_parser::Parser;
    use swc_ecma_parser::StringInput;
    use swc_ecma_parser::Syntax;
    use swc_common::sync::Lrc;
    use swc_common::SourceMap;
    use swc_common::FileName;
    
    fn parse(code: &str) -> Program {
        let cm: Lrc<SourceMap> = Default::default();
        let fm = cm.new_source_file(FileName::Anon, code.into());
        
        let mut parser = Parser::new(
            Syntax::Es(Default::default()),
            StringInput::from(&*fm),
            None,
        );
        
        parser.parse_program().unwrap()
    }
    
    #[test]
    fn test_plugin() {
        let code = r#"
            function test() {
                console.log("hello");
            }
        "#;
        
        let mut program = parse(code);
        program = process_transform(program, serde_json::Value::Null);
        
        // 验证转换结果
    }
}

使用测试宏 #

rust
use swc_ecma_transforms_testing::test;

test!(
    Default::default(),
    |_| MyPlugin,
    basic_test,
    r#"console.log("hello");"#,
    r#"customLogger("hello");"#
);

最佳实践 #

1. 性能优化 #

rust
// 避免不必要的遍历
impl VisitMut for MyPlugin {
    fn visit_mut_module(&mut self, module: &mut Module) {
        // 只处理需要的节点
        for item in &mut module.body {
            if let ModuleItem::Stmt(stmt) = item {
                if let Stmt::Decl(Decl::Fn(_)) = stmt {
                    stmt.visit_mut_with(self);
                }
            }
        }
    }
}

2. 错误处理 #

rust
use swc_core::common::errors::{HANDLER, Spanned};

impl VisitMut for MyPlugin {
    fn visit_mut_expr(&mut self, expr: &mut Expr) {
        if let Expr::Call(call) = expr {
            if let Callee::Expr(callee) = &call.callee {
                if let Expr::Ident(ident) = &**callee {
                    if ident.sym == "deprecatedFunc" {
                        HANDLER.with(|handler| {
                            handler
                                .struct_span_err(
                                    ident.span,
                                    "deprecatedFunc is deprecated",
                                )
                                .emit();
                        });
                    }
                }
            }
        }
    }
}

3. 配置验证 #

rust
impl Config {
    pub fn validate(&self) -> Result<(), String> {
        if self.option1 && self.option2.is_empty() {
            return Err("option2 is required when option1 is true".to_string());
        }
        Ok(())
    }
}

#[plugin_transform]
pub fn process_transform(program: Program, config: serde_json::Value) -> Program {
    let config: Config = serde_json::from_value(config).unwrap_or_default();
    
    if let Err(e) = config.validate() {
        HANDLER.with(|handler| {
            handler.err(&e);
        });
        return program;
    }
    
    program.fold_with(&mut as_folder(MyPlugin::new(config)))
}

下一步 #

现在你已经掌握了 SWC 插件开发,接下来学习 工具集成 了解如何将 SWC 集成到各种工具中!

最后更新:2026-03-28