66
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

VueAdvent Calendar 2019

Day 14

【Vue.js】【ESLint】スコープ付きCSSの利用されていないセレクターを検出するESLintプラグイン作った

Last updated at Posted at 2019-12-13

この記事は、Vue Advent Calendar 2019 #1 の14日目の記事です。


こんにちは。社内ではLintおじさんという二つ名を襲名していて、@ota-meshiという冷やかし感満載なアカウントで割と真面目にやってるつもりの者です。

本記事では、社内でこそこそ作っていたVue.jsのスコープ付きCSS用のESLintの拡張ルールの一部を先日npmで公開したので、その紹介とどんな感じで作ったのかを書こうと思います。

まず、公開したものは以下です。

下記リンクからブラウザ上で試すことができます。
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であれば)以下のように設定して利用できます。

.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を走査し、datacomputedのプロパティを探して、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へのプロパティのマッピング

無理でした。datacomputedのプロパティの、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でStarつけてくれると励みになりますm(_ _)m

あとがき

このESLint拡張ルールですが、台風で流れたVue Fesのランチ(弊社はランチスポンサーする予定でした)で少し紹介する予定でしたが、行き場を失ったのでこの場で紹介させていただきしました。
来年もVue Fesあるといいなー。

あと。そうそう。僕、今年、Vue.jsのメンバーになりました:tada: 関わってくださった皆様ありがとうござます!これからもお宜しくお願いします!

66
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
66
35

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?