Vue3に向けてfilterを自動変換したいよね

Vue3からはfilterが廃止されます。
何も考えずアップデートすると今までのコードが突如として動かなくなる悲しい場面に遭遇するわけです。

filter依存コードを手動で変換していくのはしんどいです。
特に、突然composition APIに移行するのはなおさらしんどいです。

そこで一旦options APIのままVue3に対応したコードを生成する[eslint plugin](https://www.npmjs.com/package/eslint-plugin-destroy-vue-filter)を作りました。

npmに何か公開するのも、なんならqiitaに投稿するのも人生初です。

## 対象
- composition APIへの過渡期として、filterを利用したVue2以前のコードを自動的にVue3に移行したい
- Options APIを使用している
- Global filterを1ファイルで登録している
- `const { filterX } = this.$options.filters`とかいう使い方をして **いない** 


## 何が起きるの？怖いんだけど
2フェイズに分かれています。
### フェイズ1
- global filterをexportに変換
- 既存のfilterの名前をそのまま使えます
- filterと同名のでimportされた関数は`filterName as _filterName`みたいに名前を変更しています

before
      
```typescript
import Vue from 'vue';
import def, { numeric } from '~/utils/Strings';
import dollar, { yen, euro } from '~/utils/Money';

Vue.filter('stringDefaultFilter', def);

Vue.filter('percent', (value: string) => `${value}%`);

Vue.filter(
  'numeric',
  (value: string) => `${numeric(value)}`,
);
Vue.filter('dollar', dollar);
Vue.filter('yen', yen);
Vue.filter('eur', euro);

Vue.filter('func', function(xxx: string) {
  return `xxx${xxx}xxx`;
});
```

after

```typescript
import def, { numeric as _numeric } from '~/utils/Strings';
import dollar, { yen, euro } from '~/utils/Money';

export const stringDefaultFilter = def;

export const percent = (value: string) => `${value}%`;

export const numeric = (value: string) => `${_numeric(value)}`;
export { dollar };
export { yen };
export const eur = euro;

export function func(xxx: string) {
  return `xxx${xxx}xxx`;
}
```

### フェイズ2
- フェイズ1でexportした関数をimport
- パイプによるfilterを関数呼び出しに変換
- `(this.)$options.filters`も変換
- メソッドとしてマッピング
- local filterにも対応

before

```vue
<template>
  <div>
    <p>{{ x.y.z | localFilterA }}</p>
    <p>{{ id[0] | localFilterB }}</p>
    <p>{{ $options.filters.filterA(prop) | localFilterC }}</p>
    <p>{{ prop | localFilterA | filterB }}</p>
    <p>{{ methodFilter(prop) }}</p>
    <p :id="id | idFilter">XXX</p>
    <p :id="$options.filters.idFilter2(id) | localIdFilter">XXX</p>
    <p :id="$options.filters.idFilter3(id)">XXX</p>
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  filters: {
    localFilterA(value: string): string {
      return value.toUpperCase();
    },
    localFilterB(value: number): string {
      return value.toString();
    },
    localFilterC(obj: { [key: string]: string }): string {
      const texts = Object.entries(obj).reduce((acc: string[], [k, v]) => {
        acc.push(`${k}: ${v}`);
        return acc;
      }, []);
      return texts.join('\n');
    },
    localIdFilter(id: string): string {
      return id.slice(0, 5);
    },
  },
  methods: {
    methodFilter(x: string): string {
      return this.$options.filters.importedFilter(x);
    },
  },
})
</script>
```

after 

```vue

<template>
  <div>
    <p>{{ localFilterA(x.y.z) }}</p>
    <p>{{ localFilterB(id[0]) }}</p>
    <p>{{ localFilterC(filterA(prop)) }}</p>
    <p>{{ filterB(localFilterA(prop)) }}</p>
    <p>{{ methodFilter(prop) }}</p>
    <p :id="idFilter(id)">XXX</p>
    <p :id="localIdFilter(idFilter2(id))">XXX</p>
    <p :id="idFilter3(id)">XXX</p>
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
import { filterA, filterB, idFilter, idFilter2, idFilter3, importedFilter } from 'path/to/filters.ts';
export default Vue.extend({
  
  methods: {
    methodFilter(x: string): string {
      return this.importedFilter(x);
    },
    filterA,
    filterB,
    idFilter,
    idFilter2,
    idFilter3,
    importedFilter,
    localFilterA(value: string): string {
      return value.toUpperCase();
    },
    localFilterB(value: number): string {
      return value.toString();
    },
    localFilterC(obj: { [key: string]: string }): string {
      const texts = Object.entries(obj).reduce((acc: string[], [k, v]) => {
        acc.push(`${k}: ${v}`);
        return acc;
      }, []);
      return texts.join('\n');
    },
    localIdFilter(id: string): string {
      return id.slice(0, 5);
    },
  },
})
</script>
```

## 使い方
上記対象に当てはまった方には、使い方です。(そうじゃない方は諦めてPR送るかコード読んでいじるかしてください)
### インストール

```shell
$ npm install -D eslint-plugin-destroy-vue-filter
```

### 準備
eslint設定ファイルを2個作成することをおすすめします。

- フェイズ１用

ファイル名は好きに(以下、.eslintrc.convert-filterとします)

```json:.eslintrc.convert-filter

{
  "plugins": ["destroy-vue-filter"],
  "rules": {
    "destroy-vue-filter/convert-filter": 1,
    "destroy-vue-filter/resolve-callee": 1
  }
}
```

- フェイズ2用

ファイル名は好きに(以下、.eslintrc.destroy-filterとします)

```json:.eslintrc.destroy-filter
{
  "plugins": ["destroy-vue-filter"],
  "rules": {
    "destroy-vue-filter/destroy-filter": ["error", "<global filterを登録しているファイル>"]
  }
}
```

### 実行
以下を実行してください

```shell
eslint -c .eslintrc.convert-filter --fix <global filterを登録しているファイル>
eslint -c .eslintrc.destroy-filter --fix **/*.vue
```

## なぜeslint？
- Vue/typescriptをパースしてVue/typescriptに書き戻せるツールが他に見つからなかった(あったら教えて下さい)
- linterのくせに基本一回しか使わないけどまあいいよね
- 複数箇所でglobal filterを登録していたらお手上げです。無理
- `const { filterX } = this.$options.filters`みたいな使い方も変数のスコープをめっちゃ見なきゃならないので無理
- 正直会社でのユースケース(+ 私が思いつく適当に複雑なパターン)に合わせました
- 僕は適当なんだ。すまない
- ASTを頑張っていじくってきた経緯からgitのlogはグダグダです。見るな
- 正直動かなかったら言ってください。僕はフロントエンドエンジニャー歴がとても短いです