SWCのプラグインを自作してみる
SWCが気になっていたのでちょっと触ってみました。
SWCとは
コンパイル、バンドリング両方に対応しています。
コンパイルではJavaScript/TypeScriptをすべての主要なブラウザでサポートされるコードに変換できます。
なお、バンドリングの機能はまだ開発中のようです。
トランスパイル
@swc/cli
と @swc/core
の2つをinstallします。
npm i -D @swc/cli @swc/core
これでトランスパイルできるのですが、オプションで指定しないと出力先は標準出力になります。
とくに何も設定せずとも、TypeScriptもトランスパイルしてくれました。
npx swc ./src/index.ts
出力先は、-o
オプションでファイル指定、-d
オプションでディレクトリ指定できます。
詳しくはこちらを。
transform
もう少しcoreの機能を試してみました。
transformを使うと、変換結果のcodeとsourcemapが取得できるようです。
下のようなコードで動きました。
import * as swc from '@swc/core'
import * as fs from 'fs'
(() => {
const src = fs.readFileSync(`${__dirname}/../src/index.ts`, 'utf8');
swc.transform(src,{
filename: "index.ts",
sourceMaps: true,
}).then((out) => console.log(out))
})()
swc.transform
の第一引数にはソースコードへのパスではなくて、String型のソースコードを渡す必要があります。
parse
parseを使うとASTが取得できるようです。
import * as swc from '@swc/core'
import * as fs from 'fs'
(() => {
const src = fs.readFileSync(`${__dirname}/../src/index.ts`, 'utf8');
swc.parse(src,{
syntax: "typescript",
}).then((module) => {
console.log(module.body)
})
})()
プラグインを作る
本題のプラグインを作っていきます。プラグインはRustで実装します。
準備
まずは swc-cli
をinstallします。
cargo install swc_cli
下のコマンドを実行してプラグインの雛形を作成します。
swc plugin new --target-type wasm32-wasi my-first-plugin
rustup target add wasm32-wasi
実装
生成されたコードにはVisitMut
というトレイトを実装するstructが含まれており、ここにASTを変換する処理を書いていきます。
今回は試しに以下のソースコードのjQuery
を$
に変換する処理を書いてみようと思います。
jQuery(document).ready(function(){});
VisitMut
のトレイトに実装できるメソッドはこちらに一覧化されているので、ここから選択して実装します。
あまり参考になる情報がなく、SWC本体の実装を見に行ったり、パース結果を出力してみたりで模索しながらという感じでした...
先にコード例を貼ってしまうのですが、以下、これを置換する関数の実装になります。
impl VisitMut for TransformVisitor {
fn visit_mut_call_expr(&mut self, n: &mut CallExpr) {
if let Callee::Expr(expr) = &mut n.callee {
if let Expr::Member(MemberExpr { obj, .. }) = &mut **expr {
if let Expr::Call(ca) = &mut **obj {
if let Callee::Expr(e) = &mut ca.callee {
if let Expr::Ident(i) = &mut **e {
if &i.sym == "jQuery" {
i.sym = "$".into();
}
}
}
}
}
}
}
}
SWCにはPlaygroundが用意されており、ここに変換対象のコードを貼り付けるとASTがJSONで帰ってきます。
今回の例ではこのようなASTになっています。(長いので関係ないところは省いています。)
...
"body": [
{
"type": "ExpressionStatement",
"span": {
"start": 0,
"end": 37,
"ctxt": 0
},
"expression": {
"type": "CallExpression",
"span": {
"start": 0,
"end": 36,
"ctxt": 0
},
"callee": {
"type": "MemberExpression",
"span": {
"start": 0,
"end": 22,
"ctxt": 0
},
"object": {
"type": "CallExpression",
"span": {
"start": 0,
"end": 16,
"ctxt": 0
},
"callee": {
"type": "Identifier",
"span": {
"start": 0,
"end": 6,
"ctxt": 1
},
"value": "jQuery",
"optional": false
},
...
}
CallExpression
あたりからパースできると今回のやりたいことが実現できそうと当たりをつけて、visit_mut_call_expr
メソッドを実装しています。
第2引数に対象のASTがミュータブルで受け取れるため、受け取ったASTの子要素を見て期待するEnumかマッチして階層を掘っていき、最後にjQuery
の文字列に$
を再代入する実装になっています。
これでやりたいことは実現できました。
確認方法なのですが、雛形にはテストコードも含まれていて、
test!(
Default::default(),
|_| as_folder(TransformVisitor),
boo,
// 変換前のコード
r#"jQuery(document).ready(function(){})"#,
// 期待するコード
r#"$(document).ready(function(){})"#
);
のように書き換えることで、テストできます。
今回作成したプラグインのソースコードは下のリポジトリにおいてあります。
触ってみて
慣れの問題もあると思うのですが、ちょっとしたことを実装するのでも割と大変でした。(もう少しすんなり行けるだろうと予想していた...w)
次は実用性のあるSWCプラグインを目指して、なにか作ってみたいです。
SWC自体のまだ使ってない機能もあるので、個人開発などで積極的に利用してみたいと思います。
Discussion