プラグイン チートシート
このページでは、ECMAScript 用プラグインを実装する上で既知の難しいポイントについて説明します。
https://rustdoc.swc.rs/swc (新しいタブで開きます) にドキュメントがあります。特に、ビジターや Id
の問題に対処している場合に便利です。
型の理解
JsWord
String
はアロケートされ、ソースコードの「テキスト」は特別な特性を持っています。それらは多くの重複があります。明らかに、変数名が foo
の場合、foo
を複数回使用する必要があります。そのため、SWC はアロケーションの数を減らすために文字列をインターンします。
JsWord
はインターンされた文字列型です。&str
または String
から JsWord
を作成できます。.into()
を使用して JsWord
に変換します。
Ident
、Id
、Mark
、SyntaxContext
SWC は、変数を管理するために特別なシステムを使用します。詳細については、the rustdoc for Ident
(新しいタブで開きます) を参照してください。
よくある問題
入力の AST 表現の取得
SWC Playground (新しいタブで開きます) は、入力コードから AST を取得することをサポートしています。
SWC の変数管理
エラー報告
rustdoc for swc_common::errors::Handler
(新しいタブで開きます) を参照してください。
JsWord
と &str
の比較
JsWord
が何であるかわからない場合は、swc_atoms の rustdoc (新しいタブで開きます) を参照してください。
val
が JsWord
型の変数である場合、&val
を実行することで &str
を作成できます。
Box<T>
のマッチング
Box<T>
を含むさまざまなノードをマッチングするには、match
を使用する必要があります。パフォーマンス上の理由から、すべての式はボックス化された形式で保存されます。(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
でテストする
@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はホストOS形式になる可能性がある
これは、wasmにコンパイルする際に、Path
のLinuxバージョンコードが使用されるためです。そのため、プラグイン内で\\
を/
に置き換える必要がある場合があります。/
はWindowsで有効なパス区切り文字であるため、これは有効な処理です。
所有権モデル(Rustの)
このセクションは
swc
自体に関するものではありません。しかし、これがAPIのほとんどすべてのトリッキーさの原因であるため、ここに記載されています。
Rustでは、1つの変数のみがデータを所有でき、それへの可変参照は最大で1つです。また、データを変更するには、値を所有するか、それへの可変参照を持っている必要があります。
しかし、所有者/可変参照は最大で1つなので、値への可変参照を持っている場合、他のコードはその値を変更できないことを意味します。すべての更新操作は、値を所有しているか、それへの可変参照を持っているコードによって実行される必要があります。そのため、node.delete
のようなBabel APIの実装は非常にトリッキーです。あなたのコードがASTの一部への所有権または可変参照を持っている場合、SWCはASTを変更できません。
トリッキーな操作
ノードの削除
ノードは2つのステップで削除できます。
以下のコードで、bar
という名前の変数を削除するとします。
var foo = 1;
var bar = 1;
これを行うには2つの方法があります。
マーク&削除
最初の方法は、それを無効としてマークし、後で削除することです。これは通常、より便利です。
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(..)))
});
}
}
親ハンドラーから削除する
ノードを削除するもう1つの方法は、親ハンドラーから削除することです。これは、親ノードが特定のタイプの場合にのみノードを削除したい場合に役立ちます。
例:フリー変数ステートメントを削除する際に、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ノードに関する情報をキャッシュする
親ノードからの情報を利用するには2つの方法があります。1つ目は、親ノードハンドラーから情報を事前計算することです。または、親ノードを複製して、子ノードハンドラーで使用することもできます。
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
ではサポートされていません。これは、上位へのトラバーサルでは、子ノードに親に関する情報を保存する必要があり、そのためにはRustでArc
やMutex
のような型を使用する必要があるためです。
上向きにトラバースする代わりに、トップダウン方式にする必要があります。例えば、変数代入や代入式からjsxコンポーネントの名前を推論したい場合、VarDecl
や AssignExpr
を訪問する際にコンポーネントのname
を保存し、コンポーネントハンドラーからそれを使用できます。
state.file.get
/state.file.set
transform構造体は1つのファイルのみを処理するため、値をtransform構造体のインスタンスとして保存するだけで済みます。