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