插件速查表
本页描述了为 ECMAScript 实现插件时已知的难点。
你可能会发现 https://rustdoc.swc.rs/swc (opens in a new tab) 的文档很有用,特别是如果你正在处理访问者或 Id
问题。
理解类型
JsWord
String
会分配内存,而源代码的 'text' 有一个特殊的特性。
这些有很多重复。显然,如果你的变量名为 foo
,你需要多次使用 foo
。
所以 SWC 对字符串进行内部化以减少内存分配的次数。
JsWord
是一种被内部化的字符串类型。
你可以从 &str
或 String
创建 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
,其中 val
是 JsWord
类型的变量。
匹配 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_stmts
或 visit_mut_module_items
中注入它。
参见 解构核心转换 (opens in a new tab)。
struct MyPlugin {
stmts: Vec<Stmt>,
}
提示
装饰器和 TypeScript 类型
这些在你的插件被调用之前就已经处理好了。所以你无法从 Wasm 插件中访问它们。 这个设计决定是为了使 Wasm 插件更容易编写,并且使 Wasm 二进制文件更小。
测试时的注释
你可以让你的 pass 泛型化 C: Comments
。test_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_expr
是 false
。
你必须这样做
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
测试你的转换。
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(..)))
});
}
}
从子节点的处理程序中引用父节点
这包括 paths
和 scope
的使用。
缓存有关 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 中使用 Arc
或 Mutex
等类型。
你应该将其改为自上而下。
例如,如果你想从变量赋值或赋值中推断 jsx 组件的名称,你可以在访问 VarDecl
和/或 AssignExpr
时存储组件的 name
,并在组件处理程序中使用它。
state.file.get
/state.file.set
你可以简单地将值存储在转换结构体中,因为转换结构体的实例只处理一个文件。