插件速查表
此页面描述了为 ECMAScript 实现插件的已知难点。
您可能会发现 https://rustdoc.swc.rs/swc (在新标签页中打开) 上的文档很有用,尤其是在处理访问者或 Id
问题时。
理解类型
JsWord
String
会分配,并且源代码的“text”具有特殊的特征。这些有很多重复。显然,如果您的变量名为 foo
,您需要多次使用 foo
。因此,SWC 会对字符串进行内部化,以减少分配次数。
JsWord
是一种内部化的字符串类型。您可以从 &str
或 String
创建 JsWord
。使用 .into()
转换为 JsWord
。
Ident
、Id
、Mark
、SyntaxContext
SWC 使用特殊的系统来管理变量。有关详细信息,请参阅 Ident
的 Rustdoc (在新标签页中打开)。
常见问题
获取输入的 AST 表示
SWC 游乐场 (在新标签页中打开) 支持从输入代码获取 AST。
SWC 的变量管理
错误报告
请参阅 swc_common::errors::Handler
的 Rustdoc (在新标签页中打开)。
比较 JsWord
和 &str
如果您不知道 JsWord
是什么,请参阅 swc_atoms 的 Rustdoc (在新标签页中打开)。
您可以通过执行 &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 is `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
中注入。查看 解构核心转换 (在新标签页中打开)。
struct MyPlugin {
stmts: Vec<Stmt>,
}
提示
在测试时应用 resolver
SWC 在应用 resolver
(在新标签页中打开) 后应用插件,因此最好使用它来测试你的转换。正如 resolver
的 rustdoc 中所述,如果你需要引用全局变量(例如 __dirname
,require
)或用户编写的顶层绑定,则必须使用正确的 SyntaxContext
。
fn tr() -> impl Fold {
chain!(
resolver(Mark::new(), Mark::new(), false),
// Most of transform does not care about globals so it does not need `SyntaxContext`
your_transform()
)
}
test!(
Syntax::default(),
|_| tr(),
basic,
// input
"(function a ([a]) { a });",
// output
"(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 {
// Do something
}
}
}
但这无法处理
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 {
// Do something
}
}
}
代替。
使用 @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 (在新标签页中打开)
Path
是 Unix 风格的,而 FileName 可以是主机操作系统的风格
这是因为在编译到 wasm 时使用了 Linux 版本的 Path
代码。因此,你可能需要在你的插件中将 \\
替换为 /
。由于 /
是 Windows 中有效的路径分隔符,所以这样做是有效的。
所有权模型(Rust 的)
本节不是关于
swc
本身。但这里描述了它,因为它几乎是所有 API 棘手性的原因。
在 Rust 中,只有一个变量可以 *拥有* 数据,并且最多只有一个可变引用指向它。此外,如果你想修改数据,你需要 *拥有* 该值或对其具有可变引用。
但最多只有一个所有者/可变引用,这意味着如果你对一个值具有可变引用,其他代码就无法修改该值。所有更新操作都应该由 *拥有* 该值或对其具有可变引用的代码执行。因此,一些 Babel API(如 node.delete
)非常难以实现。由于你的代码拥有或对 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) {
// This is not required in this example, but you typically need this.
v.visit_mut_children_with(self);
// v.name is `Pat`.
// See https://rustdoc.swc.rs/swc_ecma_ast/enum.Pat.html
match v.name {
// If we want to delete the node, we should return false.
//
// Note the `&*` before i.sym.
// The type of symbol is `JsWord`, which is an interned string.
Pat::Ident(i) => {
if &*i.sym == "bar" {
// Take::take() is a helper function, which stores invalid value in the node.
// For Pat, it's `Pat::Invalid`.
v.name.take();
}
}
_ => {
// Noop if we don't want to delete the node.
}
}
}
fn visit_mut_var_declarators(&mut self, vars: &mut Vec<VarDeclarator>) {
vars.visit_mut_children_with(self);
vars.retain(|node| {
// We want to remove the node, so we should return false.
if node.name.is_invalid() {
return false
}
// Return true if we want to keep the node.
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() {
// Variable declaration without declarator is invalid.
//
// After this, `s` becomes `Stmt::Empty`.
s.take();
}
}
_ => {}
}
}
fn visit_mut_stmts(&mut self, stmts: &mut Vec<Stmt>) {
stmts.visit_mut_children_with(self);
// We remove `Stmt::Empty` from the statement list.
// This is optional, but it's required if you don't want extra `;` in output.
stmts.retain(|s| {
// We use `matches` macro as this match is trivial.
!matches!(s, Stmt::Empty(..))
});
}
fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
stmts.visit_mut_children_with(self);
// This is also required, because top-level statements are stored in `Vec<ModuleItem>`.
stmts.retain(|s| {
// We use `matches` macro as this match is trivial.
!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) {
// This is not required in this example, but just to show that you typically need this.
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);
// We do same thing here.
stmts.retain(|s| {
!matches!(s, Stmt::Empty(..))
});
}
fn visit_mut_module_items(&mut self, stmts: &mut Vec<ModuleItem>) {
stmts.visit_mut_children_with(self);
// We do same thing here.
stmts.retain(|s| {
!matches!(s, ModuleItem::Stmt(Stmt::Empty(..)))
});
}
}
从子节点的处理程序中引用父节点
这包括使用 paths
和 scope
。
缓存有关 AST 节点的一些信息
你有两种方法可以使用父节点的信息。首先,你可以从父节点处理程序中预先计算信息。或者,你可以克隆父节点并在子节点处理程序中使用它。
Babel API 的替代方案
generateUidIdentifier
这将返回一个带有单调递增整数后缀的唯一标识符。 swc
不提供执行此操作的 API,因为有一个非常简单的方法可以做到这一点。 你可以在转换器类型中存储一个整数字段,并在调用 quote_ident!
或 private_ident!
时使用它。
struct Example {
// You don't need to share counter.
cnt: usize
}
impl Example {
/// For properties, it's okay to use `quote_ident`.
pub fn next_property_id(&mut self) -> Ident {
self.cnt += 1;
quote_ident!(format!("$_css_{}", self.cnt))
}
/// If you want to create a safe variable, you should use `private_ident`
pub fn next_variable_id(&mut self) -> Ident {
self.cnt += 1;
private_ident!(format!("$_css_{}", self.cnt))
}
}
path.find
swc
不支持向上遍历。 这是因为向上遍历需要在子节点中存储有关父节点的信息,这需要使用诸如 Arc
或 Mutex
这样的类型在 rust 中。
与其向上遍历,不如将其改为自上而下。 例如,如果你想从变量赋值或赋值中推断 jsx 组件的名称,你可以在访问 VarDecl
和/或 AssignExpr
时存储组件的 name
,并在组件处理程序中使用它。
state.file.get
/state.file.set
你可以简单地将值存储在转换结构体中,因为转换结构体的实例只处理一个文件。