Vue.js + TSLint を Vue.js + ESLint へ移行しました

こんにちは、HITO-Link 開発チームのテックリードの楳田 (@wwwumeda) です。

TSLint が非推奨となり、多くのプロジェクトが ESLint へ移行していると思いますが、我々が開発している HITO-Link プロジェクトでも TSLint を利用していましたので ESLint へ移植しました。

本記事では移植したときの手順や設定内容を記載しています。

はじめに

現在開発中のシステムは Vue CLI でプロジェクトテンプレートを作成しそれをもとに開発していますので、移行手順も Vue CLI の利用を前提としています。 Vue CLI は ESLint の設定を抽出するために利用したので、本記事をもとに移行する場合は Vue CLI 関連の手順を読み飛ばして構いません。

以下のような手順で実施しました。

  1. Vue CLI でプロジェクトを新規に作成(linter オプションで ESLint を選択)
  2. 作成されたファイル(主に package.json と.eslintrc.js)をもとに新旧を比較して修正
  3. TSLint で設定していた Lint のルールを ESLint へ移植
  4. ESLint の新しいルールで既存のコードを自動修正(コマンドライン実行)
  5. 自動修正しきれない Lint エラーを手動で修正

クライアント開発環境は以下のとおりです。

  • OS : Windows 7 Professional (稀に Windows 10 Professional)
  • エディター : Visual Studio Code

移行手順

Vue CLI でプロジェクトの作成

Vue CLI をインストール(アップデート)します。

npm install -g @vue/cli

作成される設定を確認するため、任意のプロジェクトを作成します。

vue create hello-world

最初に Manually select features を選択し、それ以降でいくつかのオプションを選択していき、linter の設定で ESLint を選択します。 ここでは ESLint + Prettier を選択しました。 TSLint は deprecated になっています。

f:id:hito-link-editor:20191030175349p:plain

Prettier を利用すると、以下の Vue が推奨する属性折返しのルールと競合するので、このルールを適用したい場合は Prettier を利用しないほうがいいかもしれません。 推奨するスタイル(属性ごとに折り返し)で実装してもそうでない実装(属性が 1 行にまとめられた状態)にフォーマットされてしまいます。 Prettier の 1 行あたりの最大文字数(printWidth)を超えたら推奨スタイルにフォーマットされます。

これに対応する以下の Lint ルールを調整しても解消しませんでした。

  • vue/max-attributes-per-line

    https://github.com/vuejs/eslint-plugin-vue/blob/master/docs/rules/max-attributes-per-line.md

    個人的には、このルールを重視するよりも Prettier を利用する価値のほうが高いと思います。 チーム開発する上では属人的なコードが排除され、一貫性が保たれている方が価値が高いはずです。 ですので、このスタイルに準拠できない問題については妥協しています。

vue create コマンドで作成された package.json と .eslintrc.js は以下のようになりました。 これらを開発プロジェクト用にカスタマイズしていきます。

package.json

{
  "devDependencies": {
    "@vue/cli-plugin-babel": "^4.0.0",
    "@vue/cli-plugin-eslint": "^4.0.0",
    "@vue/cli-plugin-pwa": "^4.0.0",
    "@vue/cli-plugin-router": "^4.0.0",
    "@vue/cli-plugin-typescript": "^4.0.0",
    "@vue/cli-plugin-vuex": "^4.0.0",
    "@vue/cli-service": "^4.0.0",
    "@vue/eslint-config-prettier": "^5.0.0",
    "@vue/eslint-config-typescript": "^4.0.0",
    "eslint": "^5.16.0",
    "eslint-plugin-prettier": "^3.1.1",
    "eslint-plugin-vue": "^5.0.0",
    "prettier": "^1.18.2",
    "sass": "^1.19.0",
    "sass-loader": "^8.0.0",
    "typescript": "~3.5.3",
    "vue-template-compiler": "^2.6.10"
  }
}

.eslintrc.js

module.exports = {
  root: true,
  env: {
    node: true
  },
  extends: ['plugin:vue/essential', '@vue/prettier', '@vue/typescript'],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
  },
  parserOptions: {
    parser: '@typescript-eslint/parser'
  },
  overrides: [
    {
      files: [
        '**/__tests__/*.{j,t}s?(x)',
        '**/tests/unit/**/*.spec.{j,t}s?(x)'
      ],
      env: {
        jest: true
      }
    }
  ]
};

package.json の修正

まずは package.json を変更します。 package.json から TSLint のモジュールを削除し ESLint のモジュールを追加します。

  "devDependencies": {
-    "tslint-config-prettier": "^1.18.0",
-    "tslint-lines-between-class-members": "^1.3.1",
+    "@typescript-eslint/eslint-plugin": "^2.6.0",
+    "@typescript-eslint/parser": "^2.6.0",
+    "@vue/cli-plugin-eslint": "^4.0.5",
+    "@vue/eslint-config-prettier": "^5.0.0",
+    "@vue/eslint-config-typescript": "^4.0.0",
+    "eslint": "^5.16.0",
+    "eslint-plugin-no-null": "^1.0.2",
+    "eslint-plugin-prettier": "^3.1.0",
+    "eslint-plugin-simple-import-sort": "^4.0.0",
+    "eslint-plugin-vue": "^5.0.0",
+    "prettier": "^1.18.2",
  }

Vue CLI のテンプレートの依存関係が古く、ESLint の最新ルールがが適用できなかったので、以下を追加しています。

  "devDependencies": {
+    "@typescript-eslint/eslint-plugin": "^2.6.0",
+    "@typescript-eslint/parser": "^2.6.0",
  }

TSLint と同じ観点のルールを追加するため、以下のルールを追加しています。 ESLint のデフォルトでは対応できませんでした。

  "devDependencies": {
+    "eslint-plugin-no-null": "^1.0.2",
+    "eslint-plugin-simple-import-sort": "^4.0.0",
  }

eslint-plugin-no-null は TSLint の eslint-plugin-no-null からの移植です。

eslint-plugin-simple-import-sort は TSLint の ordered-imports からの移植です。 公式では、TSLint の ordered-imports と対になるルールは import/order とありますが、import/order は import 順をソートしてくれないので追加しています。 ソート順を変更するとデグレの可能性があり、ESLint は破壊的な変更があり得る場合は自動修正しないようです。当然といえば当然ですね。 このルールを追加すると実際にデグレが発生しましたので、テスト工数が取れないようであれば追加しないことをおすすめします。

今回は main.ts だけでこのルールを無効化し、import 順をもとに戻せばデグレが解消しました。

/* eslint-disable simple-import-sort/sort */

package.json を変更したので以下を実行します。

npm install

VSCode 起動中の場合、上記実行後に再起動が必要になります。

.eslintrc.js の修正

TSLint のルールを ESLint のルールに置き換えるときは以下のサイトが参考になります。 https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/ROADMAP.md

今まで利用していた TSLint のルール(tslint.json) を参考にしつつ、 .eslintrc.js を以下のように変更しています。 開発しながら調整していくと思います。

module.exports = {
-  extends: ['plugin:vue/essential', '@vue/prettier', '@vue/typescript'],
+  extends: ['plugin:vue/recommended', '@vue/prettier', '@vue/typescript'],
+  plugins: ['simple-import-sort', 'no-null'],
  rules: {
-    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
-    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
+    'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
+    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
+    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
+    'no-null/no-null': 'error',
+    'no-shadow': 'error',
+    'no-unused-expressions': 'error',
+    'no-warning-comments': 'warn',
+    'prefer-const': 'error',
+    'simple-import-sort/sort': 'error',
+    'sort-imports': 'off',
+    '@typescript-eslint/ban-ts-ignore': 'error',
+    '@typescript-eslint/camelcase': ['error', { properties: 'never' }],
+    '@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }],
+    '@typescript-eslint/explicit-member-accessibility': 'error',
+    '@typescript-eslint/no-array-constructor': 'error',
+    '@typescript-eslint/no-explicit-any': 'error',
+    '@typescript-eslint/no-inferrable-types': 'error',
+    '@typescript-eslint/no-non-null-assertion': 'warn',
+    '@typescript-eslint/no-this-alias': 'error',
+    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+    '@typescript-eslint/no-use-before-define': 'error',
+    '@typescript-eslint/prefer-for-of': 'error'
  },
  parserOptions: {
    parser: '@typescript-eslint/parser'
  },
};

一番大きな変更としては、plugin:vue/essentialplugin:vue/recommended に変更している点です。 これにより、Vue のスタイルガイドの必須ルールだけでなく推奨ルールにも準拠できますし、自動修正のカバー範囲も大きくなります。 今まではコードレビューで指摘していた内容が自動修正されるので、書きっぷりの指摘が減り、ロジックのコードレビューに専念できます。

Vue のスタイルガイドにはいいことが書いてありますので一読することをおすすめします。 Vue のオフィシャルガイドラインは読みやすいですね。

vuejs.org

上記 Vue 関連のルール変更とは別に TypeScript の推奨ルールを以下からコピーして追加しています。 https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/src/configs/recommended.json

このルールは extendsplugin:@typescript-eslint/recommended を定義することで適用できますが、あえてしていません。 extends の最後に追加するとエラーが発生し解消が手間そうだし、先頭に追加すると Vue のルールで上書きされるので、上書き後の差分を再定義するかたちでも良かったですが、勉強のついでに必要と判断したものをコピーしつつ微調整しています。

VSCode の settings.json の修正

以前は vetur のフォーマット機能のお世話になっていましたが、新しい設定では Lint によるフォーマットがより強力になりましたし、vetur のフォーマット機能と競合するので、フォーマッタの設定を none に変更しています。

{
  "editor.formatOnSave": true,
-  "tslint.autoFixOnSave": true,
+  "eslint.autoFixOnSave": true,
+  "eslint.validate": [
+    "javascript",
+    "javascriptreact",
+    { "language": "typescript", "autoFix": true },
+    { "language": "typescriptreact", "autoFix": true },
+    { "language": "vue", "autoFix": true }
+  ],
-  "vetur.format.defaultFormatter.html": "prettyhtml",
-  "vetur.format.defaultFormatter.ts": "prettier",
+  "vetur.format.defaultFormatter.html": "none",
+  "vetur.format.defaultFormatter.ts": "none"
}

tslint.json の削除

tslint.json は不要なので削除します。

自動修正の実行と手動修正の実施

以下を実行することで Lint チェックと自動修正が実行されます。

npm run lint

TSLint のルールを ESLint で完全に再現することはできなかったので、自動修正された後に残ってしまった Lint エラーはすべて手動で修正しました。 ルールを off にするほうが工数的には楽でしょうが、そこは逃げずにがんばりました。

まとめ

TSLint が非推奨となることがきっかけで ESLint に移行しましたが、設定を見直すことで .vue ファイルの template タグ内もフォーマットが効きやすくなりました。 今まではコードレビューで指摘していたようなことが Lint にお任せできるようになったため、開発効率も上がりそうです。 Vue + TypeScript(TSLint) + Prettier を利用した開発プロジェクトでは早めの移行をおすすめします。 TSLint と ESLint のルールを完全一致させるのは難しいので、ある程度は手動で修正が必要です。 単純作業ですがなかなか大変でした。

ESLint に移行することで強化された Lint 警告やフォーマットのサンプル

ESLint に移行することで自動でチェックできるようになったコードのサンプルをいくつか記載します。 今まではコードレビューでチェックしていました。

もともと Vue の ESLint で定義されていましたが、TSLint を使っていることが原因?(設定だけの問題?)でチェックできておらず、今回の対応でチェックできるようになったコードのサンプルです。

template 内での this を禁止します。 自動修正できないので手動修正が必要です。

<!-- Bad -->
<MyComponent :foo="this.foo" />
<!-- Good -->
<MyComponent :foo="foo" />

属性の定義順がルール通りかどうかをチェックします。 これは自動修正できますが、1 回では修正しきれないことがあります。

<!-- Bad -->
<MyComponent @click="callback" :foo="foo" v-if="bar" />
<!-- Good -->
<MyComponent v-if="bar" :foo="foo" @click="callback" />

属性名がハイフン区切りかどうかをチェックします。 これは自動修正できます。

<!-- Bad -->
<MyComponent :fooBar="fooBar" />
<!-- Good -->
<MyComponent :foo-bar="fooBar" />
  • mustache 記法のフォーマット

{{ }} 内の前後に空白を入れ、不要な空白は削除してくれます。 実装者によって書きっぷりがばらばらだったので、このフォーマットは非常にありがたいです。

<!-- フォーマット前 -->
<MyComponent>{{slot}}</MyComponent>
<!-- フォーマット後 -->
<MyComponent>{{ slot }}</MyComponent>
  • コールバックのフォーマット

こんな実装はあまりしないでしょうが、以下のようにフォーマットされるようになりコーディングが快適になります。

<!-- フォーマット前 -->
<MyComponent @click="()=>{if(foo){func1();}else{func2();}}" />
<!-- フォーマット後 -->
<MyComponent
  @click="
    () => {
      if (foo) {
        func1();
      } else {
        func2();
      }
    }
  "
/>