跳至内容
文档
插件
ECMAScript
速查表

插件速查表

💡

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

您可能会发现 https://rustdoc.swc.rs/swc (在新标签页中打开) 上的文档很有用,尤其是在处理访问者或 Id 问题时。

理解类型

JsWord

String 会分配,并且源代码的“text”具有特殊的特征。这些有很多重复。显然,如果您的变量名为 foo,您需要多次使用 foo。因此,SWC 会对字符串进行内部化,以减少分配次数。

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

IdentIdMarkSyntaxContext

SWC 使用特殊的系统来管理变量。有关详细信息,请参阅 Ident 的 Rustdoc (在新标签页中打开)

常见问题

获取输入的 AST 表示

SWC 游乐场 (在新标签页中打开) 支持从输入代码获取 AST。

SWC 的变量管理

错误报告

请参阅 swc_common::errors::Handler 的 Rustdoc (在新标签页中打开)

比较 JsWord&str

如果您不知道 JsWord 是什么,请参阅 swc_atoms 的 Rustdoc (在新标签页中打开)

您可以通过执行 &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 is `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 中注入。查看 解构核心转换 (在新标签页中打开)

struct MyPlugin {
    stmts: Vec<Stmt>,
}
 

提示

在测试时应用 resolver

SWC 在应用 resolver (在新标签页中打开) 后应用插件,因此最好使用它来测试你的转换。正如 resolver 的 rustdoc 中所述,如果你需要引用全局变量(例如 __dirnamerequire)或用户编写的顶层绑定,则必须使用正确的 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_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 {
            // Do something
        }
    }
}

代替。

使用 @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 (在新标签页中打开)

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(..)))
        });
    }
}
 

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

这包括使用 pathsscope

缓存有关 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 不支持向上遍历。 这是因为向上遍历需要在子节点中存储有关父节点的信息,这需要使用诸如 ArcMutex 这样的类型在 rust 中。

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

state.file.get/state.file.set

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

上次更新于 2024 年 4 月 15 日