コンテンツへスキップ
ドキュメント
プラグイン
ECMAScript
チートシート

プラグイン チートシート

💡

このページでは、ECMAScript 用プラグインを実装する上で既知の難しいポイントについて説明します。

https://rustdoc.swc.rs/swc (新しいタブで開きます) にドキュメントがあります。特に、ビジターや Id の問題に対処している場合に便利です。

型の理解

JsWord

String はアロケートされ、ソースコードの「テキスト」は特別な特性を持っています。それらは多くの重複があります。明らかに、変数名が foo の場合、foo を複数回使用する必要があります。そのため、SWC はアロケーションの数を減らすために文字列をインターンします。

JsWord はインターンされた文字列型です。&str または String から JsWord を作成できます。.into() を使用して JsWord に変換します。

IdentIdMarkSyntaxContext

SWC は、変数を管理するために特別なシステムを使用します。詳細については、the rustdoc for Ident (新しいタブで開きます) を参照してください。

よくある問題

入力の AST 表現の取得

SWC Playground (新しいタブで開きます) は、入力コードから AST を取得することをサポートしています。

SWC の変数管理

エラー報告

rustdoc for swc_common::errors::Handler (新しいタブで開きます) を参照してください。

JsWord&str の比較

JsWord が何であるかわからない場合は、swc_atoms の rustdoc (新しいタブで開きます) を参照してください。

valJsWord 型の変数である場合、&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 型の変更

ExportDefaultDeclExportDefaultExpr に変更する場合は、visit_mut_module_decl から行う必要があります。

新しいノードの挿入

新しいStmtを挿入したい場合は、構造体に値を格納し、visit_mut_stmtsまたはvisit_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でテストする

@swc/jestを使用して、プラグインをjest.config.jsに追加することで、変換をテストできます。

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

子ノードのハンドラーから親ノードを参照する

これには、pathsscopeの使用が含まれます。

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でArcMutexのような型を使用する必要があるためです。

上向きにトラバースする代わりに、トップダウン方式にする必要があります。例えば、変数代入や代入式からjsxコンポーネントの名前を推論したい場合、VarDeclAssignExpr を訪問する際にコンポーネントのnameを保存し、コンポーネントハンドラーからそれを使用できます。

state.file.get/state.file.set

transform構造体は1つのファイルのみを処理するため、値をtransform構造体のインスタンスとして保存するだけで済みます。

最終更新日 2024年4月15日