插件备忘单
¥Plugin cheatsheet
本页描述了实现 ecmascript 插件的已知难点。
¥This page describes the known hard points for implementing plugins for ecmascript.
你可能会发现 https://rustdoc.swc.rs/swc (opens in a new tab) 上的文档很有用,特别是当你正在处理访客或 Id
问题时。
¥You may find documentation at https://rustdoc.swc.rs/swc (opens in a new tab) useful, especially if you are dealing with a visitor or Id
issue.
了解类型
¥Understanding types
JsWord
String
分配,源代码的 'text'-s 有一个特殊的特性。这些是很多重复的。显然,如果你的变量名为 foo
,你需要多次使用 foo
。因此 SWC 会实习该字符串以减少分配次数。
¥String
allocates, and 'text'-s of source code has a special trait.
Those are lots of duplicates. Obviously, if your variable is named foo
, you need to use foo
multiple times.
So SWC interns the string to reduce the number of allocations.
JsWord
是一个被保留的字符串类型。你可以从 &str
或 String
创建 JsWord
。使用 .into()
转换为 JsWord
。
¥JsWord
is a string type that is interned.
You can create a JsWord
from &str
, or from a String
.
Use .into()
to convert to JsWord
.
Ident
、Id
、Mark
、SyntaxContext
SWC 使用特殊的系统来管理变量。详情请参见 Ident
的 rustdoc (opens in a new tab)。
¥SWC uses a special system for managing variables.
See the rustdoc for Ident
(opens in a new tab) for details.
常见问题
¥Common issues
获取输入的 AST 表示
¥Getting AST representation of input
SWC 在线运行 (opens in a new tab) 支持从输入代码中获取 AST。
¥SWC Playground (opens in a new tab) supports getting AST from the input code.
SWC 的变量管理
¥Variable management of SWC
错误报告
¥Error reporting
参见 swc_common::errors::Handler
的 rustdoc (opens in a new tab)。
¥See rustdoc for swc_common::errors::Handler
(opens in a new tab).
比较 JsWord
和 &str
¥Comparing JsWord
with &str
如果你不知道 JsWord
是什么,请参阅 swc_atoms 的 rustdoc (opens in a new tab)。
¥If you don't know what JsWord
is, see the rustdoc for swc_atoms (opens in a new tab).
你可以通过执行 &val
创建 &str
,其中 val
是 JsWord
类型的变量。
¥You can create &str
by doing &val
where val
is a variable of type JsWord
.
匹配 Box<T>
¥Matching Box<T>
你将需要使用 match
来匹配各个节点,包括 Box<T>
。出于性能原因,所有表达式都以盒装形式存储。(Box<Expr>
)
¥You will need to use match
to match on various nodes, including Box<T>
.
For performance reason, all expressions are stored in a boxed form. (Box<Expr>
)
SWC 将调用表达式的被调用者存储为 Callee
枚举,并且它有 Box<Expr>
。
¥SWC stores callee of call expressions as a Callee
enum, and it has 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 类型
¥Changing AST type
如果你想把 ExportDefaultDecl
改成 ExportDefaultExpr
,你应该从 visit_mut_module_decl
开始。
¥If you want to change ExportDefaultDecl
to ExportDefaultExpr
, you should do it from visit_mut_module_decl
.
插入新节点
¥Inserting new nodes
如果要注入新的 Stmt
,则需要将值存储在结构中,然后从 visit_mut_stmts
或 visit_mut_module_items
注入。参见 解构核心变换 (opens in a new tab)。
¥If you want to inject a new Stmt
, you need to store the value in the struct, and inject it from visit_mut_stmts
or visit_mut_module_items
.
See a destructuring core transform (opens in a new tab).
struct MyPlugin {
stmts: Vec<Stmt>,
}
提示
¥Tips
装饰器和 TypeScript 类型
¥Decorators and TypeScript types
这些是在调用插件之前处理的。因此你无法从 Wasm 插件访问它们。做出此设计决定是为了让 Wasm 插件更易于编写,让 Wasm 二进制文件更小。
¥Those are handled before your plugin is called. So you can't access them from the Wasm plugin. This design decision is made to make Wasm plugins easier to write and the Wasm binary smaller.
-
跟踪问题:https://github.com/swc-project/swc/issues/9132 (opens in a new tab)
¥Tracking issue: https://github.com/swc-project/swc/issues/9132 (opens in a new tab)
测试时的注释
¥Comments while testing
你可以使你的传递在 C: Comments
上通用。test_fixture
提供 &mut Tester
,它有 comments
字段。
¥You can make your pass generic over C: Comments
. test_fixture
provides &mut Tester
, which has comments
field.
测试时应用 resolver
¥Apply resolver
while testing
SWC 在应用 resolver
(opens in a new tab) 之后应用插件,因此最好用它来测试你的转换。正如 resolver
的 rustdoc 中所写,如果你需要引用全局变量(例如 __dirname
、require
)或用户编写的顶层绑定,则必须使用正确的 SyntaxContext
。
¥SWC applies plugin after applying resolver
(opens in a new tab), so it's better to test your transform with it.
As written in the rustdoc for the resolver
, you have to use correct SyntaxContext
if you need to reference global variable (e.g. __dirname
, require
) or top-level bindings written by the user.
fn tr() -> impl Pass {
(
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; });"
);
让你的处理程序无状态
¥Make your handlers stateless
假设我们要处理函数表达式中的所有数组表达式。你可以向访问者添加一个标志来检查我们是否在函数表达式中。你会忍不住去做
¥Let's say we are going to handle all array expressions in a function expression. You can add a flag to the visitor to check if we are in a function expression. You will be tempted to do
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
}
}
}
但这不能处理
¥but this cannot handle
const foo = function () {
const arr = [1, 2, 3];
const bar = function () {};
const arr2 = [2, 4, 6];
}
访问 bar
后,in_fn_expr
就是 false
。你必须做
¥After visiting bar
, in_fn_expr
is false
.
You have to do
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
}
}
}
反而。
¥instead.
使用 @swc/jest
进行测试
¥Test with @swc/jest
你可以通过将插件添加到 jest.config.js
来测试 @swc/jest
的转换。
¥You can test your transform with @swc/jest
by adding your plugin to your 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)
¥See https://github.com/jantimon/css-variable/blob/main/test/swc/jest.config.js (opens in a new tab)
Path
是 unix 之一,而 FileName 可以是主机操作系统之一
¥Path
is one of unix, while FileName can be one of host OS
这是因为编译为 wasm 时使用了 linux 版本的 Path
代码。因此,你可能需要在插件中将 \\
替换为 /
。由于 /
是 Windows 中的有效路径分隔符,因此这是有效的做法。
¥This is because linux version of Path
code is used while compiling to wasm.
So you may need to replace \\
with /
in your plugin.
As /
is a valid path separator in windows, it's valid thing to do.
注释
¥Comments
注释属于节点的跨度。(lo 表示前导注释,hi 表示尾随注释)如果要向节点添加前导注释,可以执行 PluginCommentsProxy.add_leading(span.lo, comment);
。
¥A comment belongs to a span of the node. (lo for leading comments, hi for trailing comments)
If you want to add a leading comment to a node, you can do PluginCommentsProxy.add_leading(span.lo, comment);
.
参见 PluginCommentsProxy (opens in a new tab)。
¥See PluginCommentsProxy (opens in a new tab).
所有权模型(Rust)
¥Ownership model (of rust)
本节与
swc
本身无关。但这里对此进行了描述,因为它是 API 几乎所有棘手问题的原因。¥This section is not about
swc
itself. But this is described at here because it's the cause of almost all trickyness of APIs.
在 Rust 中,只有一个变量可以拥有数据,并且最多有一个对其的可变引用。此外,如果要修改数据,你需要拥有该值或具有对该值的可变引用。
¥In rust, only one variable can own a data, and there's at most one mutable reference to it. Also, you need to own the value or have a mutable reference to it if you want to modify the data.
但最多只有一个所有者/可变引用,因此这意味着如果你对某个值有可变引用,则其他代码无法修改该值。每个更新操作都应该由拥有该值或对其具有可变引用的代码执行。因此,像 node.delete
这样的 babel API 的实现起来非常棘手。由于你的代码拥有对 AST 某些部分的所有权或可变引用,因此 SWC 无法修改 AST。
¥But there's at most one owner/mutable reference, so it means if you have a mutable reference to a value, other code cannot modify the value.
Every update operation should performed by the code which owns the value or has a mutable reference to it.
So, some of babel APIs like node.delete
is super tricky to implement.
As your code has ownership or mutable refernce to some part of AST, SWC cannot modify the AST.
棘手的操作
¥Tricky operations
删除节点
¥Deleting node
比方说,我们想在下面的代码中删除名为 bar
的变量。
¥Let's say, we want to drop the variable named bar
in the code below.
var foo = 1;
var bar = 1;
有两种方法可以做到这一点。
¥There are two ways to do this.
标记和删除
¥Mark & Delete
第一种方法是将其标记为无效并稍后将其删除。这通常更方便。
¥The first way is to mark it as invalid and delete it later. This is typically more convenient.
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(..)))
});
}
}
从父处理程序中删除
¥Delete from the parent handler
删除节点的另一种方法是将其从父处理程序中删除。如果你只想删除父节点为特定类型的节点,这会很有用。
¥Another way to delete the node is deleting it from the parent handler. This can be useful if you want to delete the node only if the parent node is specific type.
例如 你不想在删除自由变量语句时触及 for 循环中的变量。
¥e.g. You don't want to touch the variables in for loops while deleting free variable statements.
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(..)))
});
}
}
从子节点的处理程序引用父节点
¥Referencing parent node from handler of child node
这包括 paths
和 scope
的使用。
¥This includes usage of paths
and scope
.
缓存有关 AST 节点的一些信息
¥Caching some information about an AST node
你有两种方法可以使用父节点的信息。首先,你可以从父节点处理程序预先计算信息。或者,你可以克隆父节点并在子节点处理程序中使用它。
¥You have two way to use informantion from a parent node. For first, you can precompute information from the parent node handler. Alternatively, you can clone the parent node and use it in the child node handler.
babel API 的替代品
¥Alternatives for babel APIs
generateUidIdentifier
这将返回一个具有单调递增整数后缀的唯一标识符。swc
不提供 API 来执行此操作,因为有一种非常简单的方法可以执行此操作。你可以在转换器类型中存储整数字段,并在调用 quote_ident!
或 private_ident!
时使用它。
¥This returns a unique identifier with a monotonically increasing integer suffix.
swc
does not provide API to do this, because there's a very easy way to do this.
You can store an integer field in transformer type and use it while calling quote_ident!
or 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
不支持向上遍历。这是因为向上遍历需要在子节点存储父节点的信息,这需要使用 Rust 中的 Arc
或 Mutex
等类型。
¥Upward traversal is not supported by swc
.
It's because upward traversal requires storing information about parent at children nodes, which requires using types like Arc
or Mutex
in rust.
你应该自上而下,而不是向上遍历。例如,如果你想从变量赋值或赋值推断 jsx 组件的名称,你可以在访问 VarDecl
和/或 AssignExpr
时存储组件的 name
并从组件处理程序中使用它。
¥Instead of traversing upward, you should make it top-down.
For example, if you want to infer name of a jsx component from variable assignments or assignments, you can store name
of component while visiting VarDecl
and/or AssignExpr
and use it from the component handler.
state.file.get
/state.file.set
你可以简单地将值存储在转换结构中,因为转换结构的实例仅处理一个文件。
¥You can simply store the value in the transform struct as an instance of transform struct only process one file.