この記事は、Vue Advent Calendar 2019 #1 の14日目の記事です。
こんにちは。社内ではLintおじさんという二つ名を襲名していて、@ota-meshiという冷やかし感満載なアカウントで割と真面目にやってるつもりの者です。
本記事では、社内でこそこそ作っていたVue.jsのスコープ付きCSS用のESLintの拡張ルールの一部を先日npmで公開したので、その紹介とどんな感じで作ったのかを書こうと思います。
まず、公開したものは以下です。
- ドキュメント eslint-plugin-vue-scoped-css
- npm - eslint-plugin-vue-scoped-css
- GitHub - future-architect/eslint-plugin-vue-scoped-css
下記リンクからブラウザ上で試すことができます。
https://future-architect.github.io/eslint-plugin-vue-scoped-css/playground/
特徴
Vue.jsの単一ファイルコンポーネントで利用できる、スコープ付きCSS関連のLintルールを提供しています。
動機
eslint-plugin-vueという.vue
ファイルをLintできる素晴らしいESLintプラグインがありますが、これはCSS(<style>
ブロック)関連の情報は関知しません。
また、.vue
ファイルでも利用できるCSSのリンターである、stylelintという素晴らしいツールもありますが、こちらはCSS以外の情報(例えば<template>
ブロック)は関知しません。
これらのツールで実現できない、Vue.jsのスコープ付きCSS特有の静的検証を行いたかったというのが、このESLintプラグインを作成した動機です。
(stylelintプラグインで作成しても良かったんじゃないの?というのはありますが、.vue
ファイル用の神パーサーであるvue-eslint-parserを使いたかったのもありESLintプラグインで作成しました。)
使用方法
インストール
npmでインストールします。
npm install --save-dev eslint eslint-plugin-vue-scoped-css
設定
詳しくは以下を参照してください。
https://future-architect.github.io/eslint-plugin-vue-scoped-css/user-guide/#usage
このプラグインは設定構成を提供しているので、ESLint - Using the configuration from a pluginにある方法を利用して、(例えば.eslintrc.js
であれば)以下のように設定して利用できます。
module.exports = {
extends: [
// ...
// ... 既にあなたの利用している設定
// ...
// 下記を追加
'plugin:vue-scoped-css/recommended'
// ルールを自分で個別に設定したい場合は下記を追加
// 'plugin:vue-scoped-css/base'
],
rules: {
// 個別にルールを設定する場合は下記のように追加。
// 'vue-scoped-css/no-unused-selector': 'error'
}
}
提供するLintルール
vue-scoped-css/no-unused-selector
このプラグインの目玉機能です。(というかこのルールを作りたくてこのプラグインを作り始めました。)
<style scoped>
内のCSSセレクターの内、<template>
ブロックで利用されていないCSSセレクターを検出します。
このルールによって、リファクタリングしていくうちに使われなくなったのにウッカリ残してしまって、利用していない無駄なCSSをスッキリ消していくのに役立ちます。
<template>
<div id="foo">
<input class="bar">
</div>
</template>
<style scoped>
/* ✗ BAD */
ul,
.foo,
#bar {
}
/* ✓ GOOD */
div,
#foo,
.bar {
}
</style>
vue-scoped-css/no-unused-keyframes
こちらは、利用していない@keyframes
を検出します。
たまに勘違いする人がいると思うのですが(自分も勘違いしていました)、<style scoped>
内で宣言した@keyframes
は、その<style scoped>
内でしか利用できません。
なので、<style scoped>
内で宣言した@keyframes
は、その<style scoped>
内で利用すべきです。利用しない場合は、無駄なデッドコードとなります。
<style scoped>
.item {
animation-name: slidein;
}
/* ✗ BAD */
@keyframes fadein {
}
/* ✓ GOOD */
@keyframes slidein {
}
</style>
vue-scoped-css/require-scoped
scoped
が付与されていない、<style>
タグを検出します。
私のような、スコープ付きCSS大好き人間が<style scoped>
を強制したい場合に利用できます。
唯一App.vue
にだけscoped
無しの<style>
を許可したいような場合は、.eslintrc.**
をうまいこと構成・配置するか、<script>
内にeslintの構成コメントを利用して除外するといいと思います。
<!-- ✗ BAD -->
<style>
</style>
<!-- ✓ GOOD -->
<style scoped>
</style>
このルールはESLint v6.7で追加されたSuggestions APIに対応していまして、Suggestionからscoped
属性を追加できるようにしています。VSCodeが対応したらクイックフィックスから選択できるようになるかもしれません。
vue-scoped-css/require-selector-used-inside
先に紹介したvue-scoped-css/no-unused-selectorのもっと強制する版です。
<style scoped>
内のCSSセレクターの内、<template>
ブロックで利用されていないCSSセレクターを検出します。
こちらのルールは定義したCSSセレクターの各セレクター要素が全て<template>
ブロックで利用されていない場合、レポートされます。
どういうことかと言いますと、
<template>
<div>
<input class="foo">
</div>
</template>
<style scoped>
.theme .foo {}
</style>
とあった場合、.theme .foo
の.foo
は利用できる場合があります。
theme
クラスをページのルート要素に付与して.foo
のスタイルを変更するみたいなことができます。(この使い方が想定されているかどうかは知らないです。)
そのため、vue-scoped-css/no-unused-selectorでは検出しません。
いや、そんな使い方しないよ!やめてよ!という人はvue-scoped-css/require-selector-used-insideを有効にすると、全てのセレクター要素が<template>
ブロックで利用されていない場合にエラーになります。
vue-scoped-css/no-parsing-error
CSSのパースエラーを報告します。
パースエラーはstylelintとか使っておけば確認できるので必要ないんですけど、このプラグインのルール達がパースエラーで動作しないとき、「パースエラーだから動かないんだよ」ってことに気がつくためのルールです。通常は不要なルールです。
構文サポート
CSSとSCSSとStylusをサポートしています。
他は要望があればやるかもしれません。lessは全く知らないので誰か助けて。
作り
機能的な紹介は以上です。ここから先は作った時の思い出です。
CSSとセレクターのパース
CSSのパースにはPostCSSを利用しています。
そして、SCSSのパースにはpostcss-scssを使っています。
そして、セレクターのパースにはpostcss-selector-parserを使っています。
PostCSSファミリー万歳です。
あと、Stylusのパースには「VueファイルのStylusをstylelintしたかった話」の時に作った、postcss-stylを使っています。
セレクターのネストの解決
自作しました。車輪の再発明感ハンパないのですが、正しい箇所にエラーをレポートしたかったので、ここは自作に踏み切りました。
CSSの&
セレクター・@nest
・SCSSのネストに対応しているはずです。一応Stylusのセレクターも一部対応してます。
CSSセレクターと<template>
のマッピング
vue-scoped-css/no-unused-selectorとかやるために<template>
内のタグを走査します。
これがなかなか大変でした。全部書くと長いので(既に長いですが)class
属性についてだけ思い出を残します。
静的class
属性のマッピング
これは余裕です。普通のHTML的に探し出すだけです。
<template>
<div class="foo" /> <!-- そのまま書いてあるので比較的簡単 -->
</template>
v-bind:class
のインライン式のマッピング
ちょっと大変でしたが、神パーサーvue-eslint-parserがいい感じのASTを返してくれるのでまだなんとかなります。
<template>
<div v-bind:class="['foo', {'bar': true}]" /> <!-- ASTの解析が必要 -->
</template>
v-bind:class
へのプロパティのマッピング
そろそろきついです。Vueオブジェクト探し出して、ASTを走査し、data
とcomputed
のプロパティを探して、return
部分を解析して頑張りました。
<template>
<div v-bind:class="classes1"> <!-- dataを見ると`foo`が入る -->
<div v-bind:class="classes2" /> <!-- computedを見ると`bar`が入る -->
</div>
</template>
<script>
export default {
data () {
return {
classes1: ['foo']
}
},
computed: {
classes2 () {
return [{'bar': true}]
}
}
}
</script>
v-bind:class
への文字列結合のマッピング
きついです。テンプレートリテラルや、文字列結合があった場合は、部分一致で検証しています。
<template>
<div v-bind:class="`foo-${kind}`" /> <!-- `foo-bar`は一致するとみなす -->
</template>
<style scoped>
.foo-bar {}
</style>
classList.add
のマッピング
きついです。classListによるclass操作は$el
や$refs
経由での操作を探し出してマッピングしました。
<template>
<div> <!-- $elには`classList.add('foo')`されることがある -->
<div ref="div1"> <!-- ref名`div1`には`classList.add('bar')`されることがある -->
<div>
</template>
<script>
export default {
mounted () {
this.$el.classList.add('foo')
},
methods: {
onClick() {
this.$refs.div1.classList.add('bar')
}
}
}
</script>
複雑なv-bind:class
へのプロパティのマッピング
無理でした。data
やcomputed
のプロパティの、return
部分だけで解決できない情報で、さらにeslint-utilsのgetStaticValue
で解決できない情報は諦めて、検出対象から除外しています。
<template>
<div v-bind:class="classes" /> <!-- 何が入るかわからないので全てのクラスが一致するとみなす。 -->
</template>
<script>
import CONST from './const-data'
export default {
computed: {
classes () {
return [{[CONST.CLASS_FOO]: true}]
}
}
}
</script>
まとめ
と、色々頑張った結果、概ね未使用CSSセレクターを検出することができたと思います。
結構頑張ったのでスコープ付きCSS使っている方はぜひ使ってみて欲しいです。
もし使ってみて良かったらGitHubでつけてくれると励みになりますm(_ _)m
あとがき
このESLint拡張ルールですが、台風で流れたVue Fesのランチ(弊社はランチスポンサーする予定でした)で少し紹介する予定でしたが、行き場を失ったのでこの場で紹介させていただきしました。
来年もVue Fesあるといいなー。
あと。そうそう。僕、今年、Vue.jsのメンバーになりました 関わってくださった皆様ありがとうござます!これからもお宜しくお願いします!