SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Pug を HTML に移行して .vue ファイル の template タグ内に ESLint を効かせた話

こんにちは!スマートキャンプでインサイドセールスに特化した SaaSを作っているエンジニアの中川です。

上記プロダクトのフロントエンドは Vue.js を用いて開発しているのですが、 その中で SFC 内のtemplateタグで使用していた Pug をやめて HTML に移行した件をこの記事ではお話しようと思います。

また、実際にtemplateタグに ESLint を効かせてみて発覚したエラーや警告のなかで数が多かったものや、これから Vue 3 に移行していく上で対応する必要があったルールを紹介します。

背景

まずは、なぜ Pug から HTML に移行する判断に至ったのかについて理由を説明します。

eslint-plugin-vue が効かない

いきなりですが、これが最大の理由です。

eslint-plugin-vueは、読んで字の如く、ESLint のプラグインとして.vueファイルの Lint を行えるようにするものであり、公式のプラグインも用意されていてデファクトスタンダードとなっています。

.vueファイルの Lint ということで、当然ですが.vueファイル内の<script>のみならず、<template>に対しても Lint が実行され、エラーとなるコードや推奨されないコードを検出してくれることを期待しますが、<template lang="pug">のように、Pug で書かれると、パースすることが出来ず、結果として template タグに対して Lint が実行されません。

つまり、プロジェクトの状態としては、

  • ESLint, eslint-plugin-vue はパッケージとしてインストールされている
  • .vueファイルに対して ESLint の実行も出来る
  • が、暗黙的に<template lang="pug">内は Lint の対象とならず、エラーや警告が検出されない

といったものになっていました。

ソースコードのうち一方は Lint され、他方では Lint されないという状態は、開発者がそれ認識した上で”適切に”気を配る必要があるという意味で、Lint がまったく無い状態と同じ、もしくはそれ以上に危ういものと考えました。

(事実、筆者はこのプロジェクトまで Pug を触ったことがなかったこともあり、恥ずかしながら template タグにeslint-plugin-vueが効いていない状態であることに気付いていませんでした。)

また、eslint-plugin-vueを効かせたかった理由として、到来する Vue 3 への準備を進めたかったことも大きいです。

現在、eslint-plugin-vueのバージョンをnextとしてインストールし Vue 3 向けのルールを有効化することで、Vue 3 で新しく追加・もしくは廃止される機能やシンタックスに対しての Lint が効くようになります。

あらかじめこの Lint を効かせておくことで、Vue 3 のリリース時に大きな手間をかけずにアップグレードしたいといった狙いがありました。

チームに Pug 推進派がいない

プロジェクト黎明期に Pug を推進していたメンバーはすでにチームを離れていたため、現在の開発チームに Pug を推進しているメンバーはおらず、むしろ HTML で書きたいメンバーが存在している状況になっていました。

このような状態は、当然メンバーのモチベーション維持が難しく、また、その技術(今回でいえば Pug)を追っているメンバーがいないことでメンテナンス面で不安が生じます。

(2020/09/04 追記)誤解を与える表現になっていたかもしれないので、念の為補足します。前任の Pug を推進してくれていたメンバーは事業上の理由によって別チームに異動しており、プロジェクト初期にガッツリ貢献してくれたことに感謝こそすれ、負の感情はまったくありません。ありがとうございました!🙏

Pug を HTML に移行するには

Pug を HTML に移行するために、AST など静的解析を使って変換するようなツールがないか調べたところ、vue-pug-to-htmlというまさに今回の移行にうってつけな変換ツールを発見し、さらにそこから fork する形で、@plaidev/pug-to-htmlという変換ツールが公開されていたので、今回はそちらを有り難く利用させて頂きました。

また、利用にあたっては制作者である株式会社プレイド様のテックブログに詳細が記載されており、大変参考になりました。

1,100 超えコンポーネントの Jade / Pug テンプレートを移行した話 | PLAID engineer blog

実際に導入する方法やハマりどころも上記記事にまとめてくださっているので、こちらで再度手順を記載するようなことは行いません。 この場を借りてお礼申し上げます。ありがとうございました!

Vue 3 に準拠した Lint ルールを導入する

これで Pug がすべて HTML になり、Lint を実行する準備が整いましたが、前述の通り、Lint には Vue 3 に準拠したルールを使用したかったため、以下のリンクに従って導入を進めました。

User Guide | eslint-plugin-vue

また、ルールの適用にあたっては、ルールの優先度ごとにまとめたいくつかのルールセットが用意されているので、それらの中から推奨されているvue/vue3-recommendedのルールセットを使用しました。

ルールセットの内訳としてはエラーを抑止するためのvue/vue3-essential、可読性を向上させるためのvue/vue3-strongly-recommended、任意の選択と認知的オーバーヘッドの最小化のためのvue/vue3-recommendedと用意されており、それぞれのルールセットは前の段階のルールセットを包含するような関係にあります。

Lint を実行してみる

これで、templateタグに対して Vue 3 に準拠したルールで Lint を実行する準備が整いました。

これまで Lint されていなかった膨大なコンポーネント(約 300 ファイル)に対して一気に Lint をかけるのは怖いですが、勇気を振り絞って実行してみます。

ESLintの実行結果

...なるほど。ある程度覚悟していたとはいえ、実際の数字を目にすると心にくるものがありますね。

とはいえこれを見て見ぬ振りをするわけにもいかないので、順番に対処していきました。

ここからは、実際に修正していったうえで、特に目ぼしいルールをいくつか紹介していきます。

対応した目ぼしいルール

vue-require-v-for-key

これは Vue を触ったことがある方であれば一度は目にするようなお馴染みのルールではありますが、今回 template タグに対して初めて Lint が一斉に当たったこともあり、このルール違反が多数検出されました。

対処法としては非常に簡単で、:keyとして対象のv-for内で一意になるような値を設定するだけでよいのですが、今回はまとめてそれを行わなくてはいけなかったことが難点で、特に機能追加などなく長期間触っていなかったようなコンポーネントに対してこの修正をするのは、まず何をv-forで回しているのか、それは直接:keyとして設定出来るものなのか、出来なければ、どのプロパティがそれにあたるものなのか(key,id,etc...)を見極める作業が発生してしまい、非常に労力がかかりました。

vue/require-v-for-key | eslint-plugin-vue

vue/valid-v-slot

v-slotディレクティブに対して適切な使用法を定めるルールですが、検出数はそれほど多くはなかったものの、これもまた人力を必要とする点で労力がかかりました。

templateタグ以外に対してv-slotを使用していた問題

v-slotは本来templateタグ以外のタグに対しての使用は推奨されていないのですが、使用箇所が散見されました。

対処法としては、対象のタグをtemplateタグで囲い、そのtemplateタグのディレクティブとしてv-slotを記述することが必要になります。

これも前述のvue-require-v-for-keyの問題と同じく、templateタグで囲う範囲を見極める手作業が発生するため、手間・難易度ともに高くなりました。

そのほか

v-slot は挙動が複雑になりがちなこともあり、vue/valid-v-slotルールのなかにもいくつも規則が存在するため、気になられた方は一度以下のリンクをチェックしてみることをおすすめします。

vue/valid-v-slot | eslint-plugin-vue

Vue 3 にアップグレードするまで対応出来なかったルール

以下は Vue 3 によって機能追加もしくは廃止されるものに対してのルールになります。

当然準備段階ではバージョンが 2.x なので愚直にこれらのルールの通りに直しても動かないため、これらのルールは.eslintrc.jsrulesプロパティにおいてoffに指定しました。

準備として Vue 3 用の Lint ルールを先取りして適用したときにのみ発生するような状態なのでこの情報を活用するシーンはあまりないかもしれませんが、備忘としてまとめておきます。

vue/no-deprecated-v-bind-sync

2.x 時代に使えたv-bind:hoge.sync="fuga"といったシンタックスが廃止されたことを Lint するルールで、Vue 3 からは単純にv-bind:hoge="fuga"もしくは:hoge="fuga"とすることで同様の挙動となります。

vue/no-deprecated-v-bind-sync | eslint-plugin-vue

vue/no-deprecated-v-on-native-modifier

Vue 3 では@keydown.enter.native="onKeydownEnter"のように記述していた.nativeシンタックスが不要になりました。

vue/no-deprecated-v-on-native-modifier | eslint-plugin-vue

同内容の RFC を見てみるに、v-on listeners used on a component will fallthrough and be registered as native listeners on the child component root. .native modifier is no longer needed.とのことなので、純粋に.nativeをつけなくてもよくなった、ということのようです。

rfcs/active-rfcs/0031-attr-fallthrough.md at master · vuejs/rfcs · GitHub

vue/no-deprecated-destroyed-lifecycle

これはコンポーネントのライフサイクルとして 2.x 時代に存在していたbeforeDestroydestroyedが廃止されたことを Lint するルールです。Vue 3 では代わりにbeforeUnmountunmountedが存在しています。

こちらの RFC はちょっと見つけられなかったのですが、以下のページ中のコードから、おそらく既に存在しているbeforeMountmountedとあわせて、mount という単語に集約させたかったのではないかなと推測しました。

vue/no-deprecated-destroyed-lifecycle | eslint-plugin-vue

vue/no-deprecated-functional-template

Vue 2.x には functional template が存在しているかと思いますが、その廃止を Lint するルールになります。

vue/no-deprecated-functional-template | eslint-plugin-vue

以下の RFC によくまとめられていますが、JavaScript 内でimport { h } from 'vue'など DOM を生成するための関数を使用することで同様のことを実現出来るのでfunctional templateを使う必要は無い、という意図のようです。

Vue 3 から使用できる(厳密には分離していますが)Composition API にも見られますが、関数に処理を閉じ込めたうえで、コンポーネントでは必要な関数を適宜インポートして使用するだけといった価値観の流れにあるルールなのかなと推察しました。

rfcs/active-rfcs/0007-functional-async-api-change.md at master · vuejs/rfcs · GitHub

まとめ

上記のものやその他のルール違反をつぶしていった結果、無事に Lint が通るようになりました!

普段の開発に忙殺されてメンテナンスがつい後手に回るようなことはよくありますが、溜まり溜まった負債を一気に解消しようとするのはやはり体力も精神力も必要と痛感したので、今後はこまめに負債と向き合う機会を意識的に設けていきたいです!

今回の対応のなかで、ここでは紹介しきれない細かな問題(Vuetify の 1 系が Vue 2.6 から導入されたスロットのシンタックスに対応しきれていないこと、などなど。。。)もあったりしたので、話を聞いてみたい方は筆者の Twitter @let_mktにDMいただけると嬉しいです!

また、ここがおかしいなどもあれば上記 Twitter 宛に教えていただけると有り難いです。