🐙

SWCのプラグインを自作してみる

2023/02/02に公開

SWCが気になっていたのでちょっと触ってみました。

https://swc.rs/

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(){})"#
);

のように書き換えることで、テストできます。

今回作成したプラグインのソースコードは下のリポジトリにおいてあります。

https://github.com/sakamuuy/sample_swc_plugin

触ってみて

慣れの問題もあると思うのですが、ちょっとしたことを実装するのでも割と大変でした。(もう少しすんなり行けるだろうと予想していた...w)
次は実用性のあるSWCプラグインを目指して、なにか作ってみたいです。
SWC自体のまだ使ってない機能もあるので、個人開発などで積極的に利用してみたいと思います。

Discussion