Skip to content
文档
插件
ECMAScript
速查表

插件速查表

💡

本页描述了为 ECMAScript 实现插件时已知的难点。

你可能会发现 https://rustdoc.swc.rs/swc (opens in a new tab) 的文档很有用,特别是如果你正在处理访问者或 Id 问题。

理解类型

JsWord

String 会分配内存,而源代码的 'text' 有一个特殊的特性。 这些有很多重复。显然,如果你的变量名为 foo,你需要多次使用 foo。 所以 SWC 对字符串进行内部化以减少内存分配的次数。

JsWord 是一种被内部化的字符串类型。 你可以从 &strString 创建 JsWord。 使用 .into() 转换为 JsWord

Ident, Id, Mark, SyntaxContext

SWC 使用一个特殊的系统来管理变量。 详情请参见 Ident 的 rustdoc (opens in a new tab)

常见问题

获取输入的 AST 表示

SWC Playground (opens in a new tab) 支持从输入代码中获取 AST。

SWC 的变量管理

错误报告

参见 swc_common::errors::Handler 的 rustdoc (opens in a new tab)

比较 JsWord&str

如果你不知道 JsWord 是什么,请参见 swc_atoms 的 rustdoc (opens in a new tab)

你可以通过 &val 创建 &str,其中 valJsWord 类型的变量。

匹配 Box<T>

你需要使用 match 来匹配各种节点,包括 Box<T>。 出于性能原因,所有表达式都以装箱形式存储。(Box<Expr>

SWC 将调用表达式的被调用者存储为 Callee 枚举,并且它有 Box<Expr>

use swc_core::ast::*;
use swc_core::visit::{VisitMut, VisitMutWith};
 
struct MatchExample;
 
impl VisitMut for MatchExample {
    fn visit_mut_callee(&mut self, callee: &mut Callee) {
        callee.visit_mut_children_with(self);
 
        if let Callee::Expr(expr) = callee {
            // expr 是 `Box<Expr>`
            if let Expr::Ident(i) = &mut **expr {
                i.sym = "foo".into();
            }
        }
    }
}

更改 AST 类型

如果你想将 ExportDefaultDecl 更改为 ExportDefaultExpr,你应该从 visit_mut_module_decl 中执行此操作。

插入新节点

如果你想注入一个新的 Stmt,你需要将值存储在结构体中,并从 visit_mut_stmtsvisit_mut_module_items 中注入它。 参见 解构核心转换 (opens in a new tab)

struct MyPlugin {
    stmts: Vec<Stmt>,
}

提示

装饰器和 TypeScript 类型

这些在你的插件被调用之前就已经处理好了。所以你无法从 Wasm 插件中访问它们。 这个设计决定是为了使 Wasm 插件更容易编写,并且使 Wasm 二进制文件更小。

测试时的注释

你可以让你的 pass 泛型化 C: Commentstest_fixture 提供了 &mut Tester,它有 comments 字段。

在测试时应用 resolver

SWC 在应用 resolver (opens in a new tab) 后应用插件,所以最好用它来测试你的转换。 正如 resolver 的 rustdoc 中所写,如果你需要引用全局变量(例如 __dirname, require)或用户编写的顶级绑定,你必须使用正确的 SyntaxContext

fn tr() -> impl Pass {
    (
        resolver(Mark::new(), Mark::new(), false),
        // 大多数转换不关心全局变量,所以不需要 `SyntaxContext`
        your_transform()
    )
}
 
test!(
    Syntax::default(),
    |_| tr(),
    basic,
    // 输入
    "(function a ([a]) { a });",
    // 输出
    "(function a([_a]) { _a; });"
);

让你的处理程序无状态

假设我们要处理函数表达式中的所有数组表达式。 你可以向访问者添加一个标志来检查我们是否在函数表达式中。 你可能会想这样做

struct Transform {
    in_fn_expr: bool
}
 
impl VisitMut for Transform {
    noop_visit_mut_type!();
 
    fn visit_mut_fn_expr(&mut self, n: &mut FnExpr) {
        self.in_fn_expr = true;
        n.visit_mut_children_with(self);
        self.in_fn_expr = false;
    }
 
    fn visit_mut_array_lit(&mut self, n: &mut ArrayLit) {
        if self.in_fn_expr {
            // 做一些事情
        }
    }
}

但这无法处理

const foo = function () {
    const arr = [1, 2, 3];
 
    const bar = function () {};
 
    const arr2 = [2, 4, 6];
}

在访问 bar 后,in_fn_exprfalse。 你必须这样做

struct Transform {
    in_fn_expr: bool
}
 
impl VisitMut for Transform {
    noop_visit_mut_type!();
 
    fn visit_mut_fn_expr(&mut self, n: &mut FnExpr) {
        let old_in_fn_expr = self.in_fn_expr;
        self.in_fn_expr = true;
 
        n.visit_mut_children_with(self);
 
        self.in_fn_expr = old_in_fn_expr;
    }
 
    fn visit_mut_array_lit(&mut self, n: &mut ArrayLit) {
        if self.in_fn_expr {
            // 做一些事情
        }
    }
}

使用 @swc/jest 进行测试

你可以通过将你的插件添加到 jest.config.js 中来使用 @swc/jest 测试你的转换。

jest.config.js
module.exports = {
  rootDir: __dirname,
  moduleNameMapper: {
    "css-variable$": "../../dist",
  },
  transform: {
    "^.+\\.(t|j)sx?$": [
      "@swc/jest",
      {
        jsc: {
          experimental: {
            plugins: [
              [
                require.resolve(
                  "../../swc/target/wasm32-wasi/release/swc_plugin_css_variable.wasm"
                ),
                {
                  basePath: __dirname,
                  displayName: true,
                },
              ],
            ],
          },
        },
      },
    ],
  },
};

参见 https://github.com/jantimon/css-variable/blob/main/test/swc/jest.config.js (opens in a new tab)

Path 是 unix 之一,而 FileName 可以是主机操作系统之一

这是因为在编译到 wasm 时使用了 Path 代码的 linux 版本。 所以你可能需要在你的插件中将 \\ 替换为 /。 由于 / 是 windows 中的有效路径分隔符,这是有效的操作。

注释

注释属于节点的 span。(lo 用于前导注释,hi 用于尾随注释) 如果你想为节点添加前导注释,你可以使用 PluginCommentsProxy.add_leading(span.lo, comment);

参见 PluginCommentsProxy (opens in a new tab)

Rust 的所有权模型

本节不是关于 swc 本身的。但在这里描述是因为它是几乎所有 API 复杂性的原因。

在 Rust 中,只有一个变量可以 拥有 数据,并且最多只能有一个可变引用。 此外,如果你想修改数据,你需要 拥有 该值或拥有其可变引用。

但最多只能有一个所有者/可变引用,所以这意味着如果你有一个值的可变引用,其他代码无法修改该值。 每次更新操作都应该由 拥有 该值或拥有其可变引用的代码执行。 因此,像 node.delete 这样的 babel API 实现起来非常复杂。 由于你的代码拥有或拥有 AST 某些 部分的可变引用,SWC 无法修改 AST。

复杂操作

删除节点

假设我们想删除下面代码中名为 bar 的变量。

var foo = 1;
var bar = 1;

有两种方法可以做到这一点。

标记并删除

第一种方法是将其标记为 无效 并在稍后删除它。 这通常更方便。

use swc_core::ast::*;
use swc_core::visit::{VisitMut,VisitMutWith};
 
impl VisitMut for Remover {
    fn visit_mut_var_declarator(&mut self, v: &mut VarDeclarator) {
        // 在这个例子中不需要这个,但你通常需要这个。
        v.visit_mut_children_with(self);
 
        // v.name 是 `Pat`。
        // 参见 https://rustdoc.swc.rs/swc_ecma_ast/enum.Pat.html
        match v.name {
            // 如果我们想删除节点,我们应该返回 false。
            //
            // 注意 `&*` 在 i.sym 之前。
            // 符号的类型是 `JsWord`,这是一个内部化的字符串。
            Pat::Ident(i) => {
                if &*i.sym == "bar" {
                    // Take::take() 是一个辅助函数,它在节点中存储无效值。
                    // 对于 Pat,它是 `Pat::Invalid`。
                    v.name.take();
                }
            }
            _ => {
                // 如果我们不想删除节点,则不执行任何操作。
            }
        }
    }
 
    fn visit_mut_var_declarators(&mut self, vars: &mut Vec<VarDeclarator>) {
        vars.visit_mut_children_with(self);
 
        vars.retain(|node| {
            // 我们想删除节点,所以我们应该返回 false。
            if node.name.is_invalid() {
                return false
            }
 
            // 如果我们想保留节点,则返回 true。
            true
        });
    }
 
    fn visit_mut_stmt(&mut self, s: &mut Stmt) {
        s.visit_mut_children_with(self);
 
        match s {
            Stmt::Decl(Decl::Var(var)) => {
                if var.decls.is_empty() {
                    // 没有声明器的变量声明是无效的。
                    //
                    // 在此之后,`s` 变为 `Stmt::Empty`。
                    s.take();
                }
            }
            _ => {}
        }
    }
 
    fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
        stmts.visit_mut_children_with(self);
 
        // 我们从语句列表中删除 `Stmt::Empty`。
        // 这是可选的,但如果你不希望在输出中有额外的 `;`,则需要这样做。
        stmts.retain(|s| {
            // 我们使用 `matches` 宏,因为这个匹配很简单。
            !matches!(s, Stmt::Empty(..))
        });
    }
 
    fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
        stmts.visit_mut_children_with(self);
 
        // 这也是必需的,因为顶级语句存储在 `Vec<ModuleItem>` 中。
        stmts.retain(|s| {
            // 我们使用 `matches` 宏,因为这个匹配很简单。
            !matches!(s, ModuleItem::Stmt(Stmt::Empty(..)))
        });
    }
}

从父处理程序中删除

另一种删除节点的方法是从父处理程序中删除它。 如果你只想在父节点是特定类型时删除节点,这可能很有用。

例如,你不想在删除自由变量语句时触及 for 循环中的变量。

use swc_core::ast::*;
use swc_core::visit::{VisitMut,VsiitMutWith};
 
struct Remover;
 
impl VisitMut for Remover {
    fn visit_mut_stmt(&mut self, s: &mut Stmt) {
        // 在这个例子中不需要这个,但只是为了展示你通常需要这个。
        s.visit_mut_children_with(self);
 
        match s {
            Stmt::Decl(Decl::Var(var)) => {
                if var.decls.len() == 1 {
                    match var.decls[0].name {
                        Pat::Ident(i) => {
                            if &*i.sym == "bar" {
                                s.take();
                            }
                        }
                    }
                }
            }
            _ => {}
        }
    }
 
    fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
        stmts.visit_mut_children_with(self);
 
        // 我们在这里做同样的事情。
        stmts.retain(|s| {
            !matches!(s, Stmt::Empty(..))
        });
    }
 
    fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
        stmts.visit_mut_children_with(self);
 
        // 我们在这里做同样的事情。
        stmts.retain(|s| {
            !matches!(s, ModuleItem::Stmt(Stmt::Empty(..)))
        });
    }
}

从子节点的处理程序中引用父节点

这包括 pathsscope 的使用。

缓存有关 AST 节点的一些信息

你有两种方法可以使用来自父节点的信息。 首先,你可以从父节点处理程序中预先计算信息。 或者,你可以克隆父节点并在子节点处理程序中使用它。

Babel API 的替代方案

generateUidIdentifier

这会返回一个带有单调递增整数后缀的唯一标识符。 swc 没有提供 API 来执行此操作,因为有一种非常简单的方法可以做到这一点。 你可以在转换器类型中存储一个整数字段,并在调用 quote_ident!private_ident! 时使用它。

struct Example {
    // 你不需要共享计数器。
    cnt: usize
}
 
impl Example {
    /// 对于属性,可以使用 `quote_ident`。
    pub fn next_property_id(&mut self) -> Ident {
        self.cnt += 1;
        quote_ident!(format!("$_css_{}", self.cnt))
    }
 
    /// 如果你想创建一个安全的变量,你应该使用 `private_ident`
    pub fn next_variable_id(&mut self) -> Ident {
        self.cnt += 1;
        private_ident!(format!("$_css_{}", self.cnt))
    }
}

path.find

swc 不支持向上遍历。 这是因为向上遍历需要在子节点中存储有关父节点的信息,这需要在 Rust 中使用 ArcMutex 等类型。

你应该将其改为自上而下。 例如,如果你想从变量赋值或赋值中推断 jsx 组件的名称,你可以在访问 VarDecl 和/或 AssignExpr 时存储组件的 name,并在组件处理程序中使用它。

state.file.get/state.file.set

你可以简单地将值存储在转换结构体中,因为转换结构体的实例只处理一个文件。