1
/
5

BabelプラグインをRust (SWC) に移植して、JavaScriptのコンパイルを爆速にする 〜プラグイン作成編〜

はじめに

こんにちは、WantedlyのDX (Developer Experience) チームでインターンをしていた田村です。今回のインターンではWantedlyのフロントエンドのトランスパイラをBabelからSWCに移行することを目標に調査を行いました。BabelからSWCの移行方法については前の記事(基本編)をご覧ください。本記事では簡単なSWCカスタムプラグインを作成することを通して、プラグインの書き方、使い方、テストを紹介します。本記事は、BabelプラグインをSWCに移植する際に必要な知識は一通り網羅しています。

環境

  • cargo 1.59.0 (49d8809dc 2022-02-10)
  • npm 8.1.2

また、この後インストールするツールのバージョンは以下の通りです。

  • @swc/cli 0.1.55
  • @swc/core 1.2.154
  • swc_cli 0.13.0

必要なツールのセットアップ

Rustをwasmにコンパイルできるようにターゲットを追加します。

rustup target add wasm32-wasi wasm32-unknown-unknown

次にswc_cliをインストールします。

cargo install swc_cli

プラグインのテンプレートの作成

SWCカスタムプラグインの作成は以下のコマンドで行います。

swc plugin new ${プラグイン名} --target-type wasm32-wasi

ここではプラグイン名はtest_pluginとして実行します。

$ swc plugin new test_plugin --target-type wasm32-wasi 

✅ Successfully created test_plugin.
If you haven't, please ensure to add target via "rustup target add wasm32-wasi"
$

コマンドを実行するとソースファイルやpackage.jsonが自動的に作られます。プラグイン本体の処理はsrc以下に記述することになります。

$ tree test_plugin -I target
test_plugin
├── Cargo.lock
├── Cargo.toml
├── package.json
└── src
    └── lib.rs

1 directory, 4 files
$

トランスフォームの記述

src/lib.rsには元から以下のようなボイラープレートコードが入っています。

use swc_plugin::{ast::*, plugin_transform};

pub struct TransformVisitor;

impl VisitMut for TransformVisitor {
    // Implement necessary visit_mut_* methods for actual custom transform.
    // ...
}

/// An example plugin function with macro support.
/// ...
#[plugin_transform]
pub fn process_transform(program: Program, _plugin_config: String, _context: String) -> Program {
    program.fold_with(&mut as_folder(TransformVisitor))
}

process_transform関数はプラグインの処理の実質的なエントリーポイントになっていて、引数として構造体Programをとり、これにトランスフォームを施したものを返します。ProgramはJavaScriptの抽象構文木(AST)を表しています。JavaScriptのASTに触れたことのない方には、AST explorerで雰囲気を掴んでいただくことをおすすめします。

補足: 複数回に渡ってトランスフォームを施したい場合は、VisitMutトレイトを実装した別の構造体を用意して、process_transform関数内でfold_with関数をメソッドチェーンとして使うことが出来ます。

まずは試しに、JavaScriptのソースコード中に出てくるconsole.log(_)をconsole.log("Hello World")に変形する処理を実装してみましょう。このトランスフォームはconsole.log関数の呼び出しを起点に行われるので、以下のようにvisit_mut_call_expr関数を作成します。

use swc_plugin::syntax_pos::DUMMY_SP;

impl VisitMut for TransformVisitor {
    fn visit_mut_call_expr(&mut self, call: &mut CallExpr) {
        if let Callee::Expr(expr) = &call.callee {
            if let Expr::Member(MemberExpr { obj, prop, .. }) = &**expr {
                if let MemberProp::Ident(ident) = prop {
                    if ident.sym == *"log" {
                        if let Expr::Ident(ident) = &**obj {
                            if ident.sym == *"console" {
                                call.args[0].expr = Box::new(Expr::Lit(Lit::Str(Str {
                                    span: DUMMY_SP,
                                    has_escape: false,
                                    kind: StrKind::default(),
                                    value: JsWord::from("Hello World"),
                                })));
                            }
                        }
                    }
                }
            }
        }
    }
}

このコードは、ネストしたifで関数呼び出しがconsole.log()の形になっているか調べ、その場合のみ、console.logの引数を新たな式"Hello World" で置き換えます。第2引数でASTが渡されるので、新たに作成したノードで上書きすることでASTを更新します。実際に渡されるASTは、AST explorerや最新版のDocs.rsを参考にしてください。

またVisitMutトレイトでは、関数呼び出し以外のvisitorパターンも存在しており、Docs.rsで利用可能な関数の一覧が確認できます。例えば、Import文のトランスフォームや追加を行いたい場合は、それぞれvisit_mut_import関数とvisit_mut_program関数を利用します。

以上でプラグインのコードは全て完成しました。

Rustプラグインをコンパイルするには以下を実行します。(target/wasm32-wasi/release/test_plugin.wasmが作られます。)

cargo build --release --target wasm32-wasi

テストの作成

cargo.tomlの[dependencies]以下を次のように書き直します。

[dependencies]
swc_plugin = "0.30.0"

[dev-dependencies]
testing = "0.18.1"
swc_ecmascript = { version = "0.123.0", features = ["parser"] }
swc_ecma_transforms_testing = "0.65.0"
swc_common = { version = "0.17.9", features = ["sourcemap"] }

testingクレートは複数のテストを自動作成するためのライブラリです。また、SWCのクレート(swc_***)のバージョンの指定によってはコンパイル出来なくなることがあるので注意してください。

次に、src/test.rsを作成して以下を記述します:

use super::TransformVisitor;
use std::path::PathBuf;
use swc_ecma_transforms_testing::{test, test_fixture};
use swc_ecmascript::parser::{EsConfig, Syntax};
use swc_plugin::ast::*;
use testing::fixture;

fn syntax() -> Syntax {
    Syntax::Es(EsConfig {
        jsx: true,
        ..Default::default()
    })
}

fn transformer() -> impl Fold {
    as_folder(TransformVisitor)
}

#[fixture("fixture/**/input.js")]
fn replacer_console_log(input: PathBuf) {
    let output = input.parent().unwrap().join("output.js");
    test_fixture(syntax(), &|_tr| transformer(), &input, &output);
}

このプラグインのテスト対象であるトランスフォーマーはTransformerVisitor構造体のみですが、複数のトランスフォームを順番に施したい場合は以下のようにします。

use swc_common::chain;
fn transformer() -> impl Fold {
    chain!(as_folder(TransformerVisitor), as_folder(TransformerVisitor2))
}

続いて、src/lib.rsにtestモジュールを追加します。

#[cfg(test)]
mod test;

これでテストを実行するコードが完成しました。続いて、テストの入力と期待される出力を記述します。先ほど利用したfixtureアトリビュート(#[fixture(...)]で指定したパス (cargoプロジェクトのルートから計算したパス) にinput.jsを作成します。また同じディレクトリにoutput.jsを作成します。ここでは、例として、fixture/smoke_test/index.jsとfixture/smoke_test/output.jsを作成してみます。

fixture/smoke_test/index.js:

console.log("ABC");
let a = 100;
console.error(a);

fixture/smoke_test/output.js:

console.log("Hello World");
let a = 100;
console.error(a);

テストを実行する前に、cargoのターゲットの設定を変更する必要があります。デフォルトでwasm32-wasiが有効になっているので、.cargo/configを以下のようにコメントアウトしておきます。

[build]
# target = "wasm32-wasi" 

これで、cargo testコマンドでテストを実行することができます。

SWCで利用する

ここでは通常のSWCでのカスタムプラグインの使い方を説明します。2022年3月現在、Next.jsのSWC (Next-SWC) はカスタムプラグインをサポートしていません。そのため、現時点でこのカスタムプラグインをNext.jsで利用することは出来ません。

@swc/cliから実際にこのプラグインを使ってみます。適当なnpmプロジェクトを作り、cdで移動してください。最初に、以下のコマンドで@swc/cliをインストールします。

npm i -D @swc/cli @swc/core

次に.swcrcを作成します:

{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": true,
      "dynamicImport": false,
      "privateMethod": false,
      "functionBind": false,
      "exportDefaultFrom": false,
      "exportNamespaceFrom": false,
      "decorators": false,
      "decoratorsBeforeExport": false,
      "topLevelAwait": false,
      "importMeta": false
    },
    "transform": null,
    "target": "es5",
    "loose": false,
    "externalHelpers": false,
    "keepClassNames": false,
    "experimental": {
      "plugins": [
        [
          "./test_plugin.wasm",
          {}
        ]
      ]
    }
  }
}

.swcrcの記述方法についてはConfiguring SWCをを参考にしてください。(experimentalの箇所はドキュメント化されていないことがあります。)

続いて、先ほどコンパイルしたプラグイン (target/wasm32-wasi/release/test_plugin.wasm) をnpmプロジェクトの直下にコピーします。

次に、トランスパイルするファイルを作成します。ここでは適当な場所にtest.jsという名前のファイルを作成し、以下を記述します:

console.log("ABC");
let a = 100;
console.error(a);

以下のコマンドでトランスパイルを行います。

npx swc ./test.js -o output.js
// output.js
console.log("Hello World");
var a = 100;
console.error(a);


//# sourceMappingURL=output.js.map

実際に、console.log("ABC")がconsole.log("Hello World")に置換されていることが確認できました。

最後に

この記事では、プラグインの作成、テスト、利用の方法を説明しました。BabelプラグインをSWCに移植する上で書き方の違いはありますが、実装する上で困ることはあまりないように思います。今のところ、プラグインの記述方法についての記事は日本語はおろか英語の記事でもほとんどありません。現時点でのプラグインの最新情報として参考にしていただければ幸いです。また、本記事で作成したプラグインはGitHubで公開しています。


GitHub - tamaroning/swc-plugin-test
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or window. Reload to refresh your session. Reload to refresh your session.
https://github.com/tamaroning/swc-plugin-test

参考

Wantedly, Inc.では一緒に働く仲間を募集しています
8 いいね!
8 いいね!
今週のランキング
Wantedly, Inc.からお誘い
この話題に共感したら、メンバーと話してみませんか?