現場に残る Vuex の map ヘルパーのコードベースを消し去り、 Vue 3 時代の型に追従するための CLI ツールを TypeScript Compiler API で実現する

## はじめに

Vue.js 製アプリケーションのレガシーコードベースにおいて、頻繁に課題となるのが mapGetters のような map ヘルパーのメソッドです。
これらはショートハンド的に使えて過去には便利なケースもありましたが、現在ではほとんど利用されることもなくなりました。

それもそのはず。現在のフロントエンド開発の主流となる言語は JavaScript ではなく TypeScript となっています。しかし、 map 系ヘルパーはその構造から任意の文字列を受け取った上でオブジェクトに影響を及ぼす形となっており、根本的に型システムとの相性が悪い存在です。
これを利用している限り、 Vue Component において map ヘルパーから this に生えたものは、型もつかなければそもそも this に生えていることすら TypeScript 側で検知できず、コンパイルエラーとなってしまいます。

そのため、今では Vue.js + TypeScript でのプロジェクトでは利用されることがほとんどなくなった map ヘルパーですが、記述時点で TypeScript が導入されていなかったコードベースでは、利便性からこれらのヘルパー関数が利用されているコードが残っていることもしばしばあります。

すぐに置き換えられると理想ですが、ネームスペースのあるなしなどの都合で一括置換で終わりといかないのがなかなかつらいところ。

今回はそんな課題を解決するため、7月のオリンピック連休を生かして `vuex-map-purge` という CLI ツールを作ってみました。

この記事では、簡単にそのモチベーションと利用方法、内部の構造をご紹介します。

## vuex-map-purge について

https://github.com/potato4d/vuex-map-purge

`vuex-map-purge` は、その名の通り map ヘルパー、 `mapGetters` と `mapMutations`, そして `mapActions` を分解し、 this にそれ相当の methods または computed を定義してくれる CLI ツールとなります。

例によって例のごとく MIT ライセンスの OSS です。よかったら star とかつけてもらえると。

具体的には、例えばこのような Vuex が利用されている JavaScript あるいは TypeScript のコードベースがあったとき

```vue:example.vue
<template>
  <div></div>
</template>

<script>
import Vue from 'vue'
import { mapActions } from 'vuex'

export default Vue.extend({
  methods: {
    ...mapActions(['loginUser']),
    ...mapActions('ui', ['switchToEditorView'])
  }
})
</script>
```

以下のような、 map ヘルパーを削除したコードベースへと変換してくれます。TypeScript の場合は unknown で型が定義され、 JavaScript のコードベースの場合は型定義をスキップします。

```vue:example.vue
<template>
  <div></div>
</template>

<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
  methods: {
    loginUser(payload?: unknown) {
      return this.$store.dispatch('loginUser', payload)
    },
    switchToEditorView(payload?: unknown) {
      return this.$store.dispatch('ui/switchToEditorView', payload)
    },
  },
})
</script>
```

### 導入と利用

基本的にプロジェクトローカルではなく、手元の Node.js 環境にグローバルに導入して実行します。

```bash
$ npm i -g vuex-map-purge
```

執筆時点 (v0.1.2) では特に CLI オプションはなく、対象となるディレクトリを glob 形式の文字列として渡すことで実行できます。
アプリケーション内で glob での走査を行うため、 Prettier などと同様の感覚で引数を渡してください。

```fish
$ vuex-map-purge './src/**/*.vue'
```

vuex-map-purge は purge だけ行いますが、標準出力に影響のあったファイルを出力するため、実際の利用時は xargs などとの併用をオススメします。

```fish
$ vuex-map-purge './src/**/*.vue' | xargs prettier --write
```

このように実行することで、完全な purge が可能です。

### なぜこれを利用するのか

TypeScript との親和性はもちろんですが、 **来たる Vue 3 への準備** が大きなモチベーションです。
これは README にも記述されています。

> Vuex 4.0 fixes a problem that Generics had with the Store in the previous Vuex, making it possible to build a more type-safe system.
> 
> However, Vuex's mapXXX utility, which exists in Vuex, does not solve the type problem and hinders future type-safe coding.
> 
> As a result, we needed a tool to eliminate mapXXX from existing Vue.js projects as soon as possible.

Vue 3 時代に利用可能となる Vuex 4.0 では、 Vuex が 3.x 時代まで抱えていた `Vuex.Store<T>` の `T` が `any` でハードコーディングされている問題が改善されています。

これによって、 **this.$store からアクセスするストア構造にユーザー側で型を付与することが可能** となります。
これは大きな Vuex + TypeScript の改善であり、自分たちで Vuex をラップしたような層を用意する必要がなくなります。

ですが前述の通り、 map ヘルパーは文字列とオブジェクトの複雑なマッピングにより実現しており、これ自体の型定義は改善されないように見えます。

そのため、現時点では **Vue 3 時代にストアの型を完全に守るためには、 map ヘルパーを取り除く必要がある** という状態です。

これまではどのみち `Vuex.Store<any>` のために移行の大きなモチベーションが沸かない人もいたかと思いますが、これからはやらない意味がなくなるため、需要も出てくるかなと思って開発しました。

### しくみについて

今回、この purge のために TypeScript Compiler API を利用してみました。

これは TypeScript のパッケージに含まれるコンパイラの挙動に介入するための API であり、ざっくりいうと今回は以下のようなことをしています。

- AST ベースで mapXXX を検知し、中の構造をチェック
  - その中で、 AST の種別によって名前空間付きの定義か、ルートの名前空間であるかなどをチェック
- 上記でチェックした内容をもとにコードを生成し、 this 内にフィールドとして定義を追加
  - AST 上で正しいコードであることが担保された形で this へとメソッドなどを気軽に生やすことができる
- 結果をコードテキストとして出力する

今回 Compiler API を利用したのは、以前 ESLint の独自ルールを制定しているときに AST を JavaScript で触るのが辛かったため、 **型に強い AST 関連のツールキットがほしい** というモチベーションでした。

TS なしで AST 触るのって鬼のように console.log してテストコードにしていく以外無理ゲーな気がしてるんですが、何か良いやり方あるんですかね……

実際に行っているステップは以下です。

1. glob パッケージで glob を判定し、対象となるファイルを洗い出す
2. cheerio で `<script>` ブロックを抜き出す
3. Compiler API に対して自作した transformer (自作 TS プラグインみたいなもの) を渡して変換を実行
4. Compiler API が吐き出したコードを `<script>` ブロックの中身に設定
5. File I/O で書き出す
6. 書き出したファイルのパスを標準出力に書き出す

本来は Vue の SFC パーサーを正しいものを利用するべきですが、パース自体はできてもパースしたものを再度書き直す処理ができるパーサーが見つからなかったので今回はこのスタイルです。

ちょっとしたリファクタリング程度なら正規表現で行うことも多いと思いますが、TypeScript Compiler API で AST を操作する場合は、基本的には想定するコード以外はスキップした後に、該当するコードだけに処理を行うことができるため、考慮漏れが起きづらいことや、テストコードとの親和性が非常に高いのが良い点かなと思いました。

AST に少しなれるとリファクタリングの効率化が進みそうなので、よかったらコードなど参考にしてもらえればと思います。

https://github.com/potato4d/vuex-map-purge/blob/master/transformers/purgeMapActions.ts

### 未実装の feature について

そんなわけで publish したばかりの vuex-map-purge ですが、現時点では対応できていない仕組みがいくつか存在するため、注意が必要です。

- `<script>` を含むコードベースをうまく変換できない
  - SFC パーサーを導入していないことが原因であるため、近日中に対応します。
- mapXXX の Object 記法の対応
  - 私が見た中ではこれの利用ケースがほぼ無いため実装から省いています
  - 今後実装予定自体はありますが、私自身が目にすることがないケースのため、モチベーションのある方は PR いただけると幸いです
- `store` 内の型定義の反映
  - これは Nuxt.js に限定するなどの場合はストアの構造が割れているため簡単ですが、プロジェクトによってディレクトリ構成が不明なため省いています
  - 今後オプションで型定義を渡すなどで解決される可能性はあります
- mapState の対応
  - mapState を利用することをやめましょう

上記以外にもなにか要望などあれば、Issue にお願いいたします。

https://github.com/potato4d/vuex-map-purge/issues

## おわりに

今回はコードベースを楽に改善したいモチベーションが半分、型定義が十分な AST を触るツールとしての TypeScript Compiler API を利用してみたかったというのが半分でのツール作成となりました。

Vue 3 と合わせて利用可能となる Vuex 4 では、 `Vuex.Store<T>` の `any` ハードコーディングが修正され、 store の型定義を正しく引き回すことができるようになります。

Vue 3 のコードベースにおいてどの程度 Vuex が利用されるかは未知数ですが、 依然として Vue 2.x からのマイグレーションでは、切っても切り離せない重要な役割になるのではないでしょうか。

そんな中、 TypeScript と親和性の低い map ヘルパーは常に課題として残り続けます。

早期に課題を解決するためにも、 vuex-map-purge が役に立てば幸いです。