stylelint によるスタイリング宗教統一

はじめに

CSS の記述は自由度が高いため、個人的な好みによって記述方法に差異が生まれやすいです。 「半角、一行の開ける or 開けない」 であったり、「小数点での記述時に一桁目に 0 をつける or つけない 」 であったり、人の数だけ宗教が生まれます。 コーディング宗教問題はなかなか解決の難しい問題ですし、チーム内での宗教戦争すら起こり得ます。

複数人での作業の場合、基本的にはチーム内で共有された聖書(コーディングルール)に則って作業を行うわけですが、聖書をいちいち確認しながら記述していくのもなかなかに骨の折れる作業です。

そこで stylelint の出番です。 コーディングルールを開発環境に設定し、エラーチェックを自動化することで平和への一歩を踏み出していきましょう。

本記事では、stylelint の導入からチームのコーディングルールにあわせたルールの設定まで行なっていきます。

stylelint とは

stylelint は CSS のエラーチェックおよび整形ツールです。 公式ページでは、以下のように銘打たれています。

A mighty, modern linter that helps you avoid errors and enforce conventions in your styles. エラーを回避し、スタイルルールを強制するための強力でモダンなリンター(意訳)

stylelint の導入

早速ですが stylelint を導入していきましょう。 stylelint を使用することでルールから逸脱した CSS の記述やミスなどを防ぎ、さらに自動で修正可能なものは自動整形まで行うことが可能です。

まずは公式ガイドに則って導入を進めていきます。 npm を使用して stylelint 本体と、ベースとなるルールが記述された stylelint-config-standard をインストールします。

npm install --save-dev stylelint stylelint-config-standard

続けて以下のように記載した .stylelint.json をプロジェクトルートに配置します。 この記述によって stylelint-config-standard に記述されたルールが .stylelint.json に継承されるようになります。

  • .stylelint.json
{
  "extends": "stylelint-config-standard"
}

最後に以下のコマンドで、stylelint による記述エラーチェックを実行します。

npx stylelint "**/*.css"

stylelint-config-standard のルールから逸脱している記述は、実行時にエラーとして出力されます。 また --fixオプションとして付与してコマンド実行することで、自動整形が行われます。

Example output(stylelint 公式ページより) スクリーンショット 2020-06-26 17.26.42.png

stylelint のルール設定

次にチームのコーディングルールに合わせて stylelint の設定をします。 全ルールを記述していくのはかなり手間なので、公開されている既存のルールを extends し、そこに必要なルールを追加・上書きをします。

既存ルールとして stylelint-config-recommended および stylelint-config-standard が有名なようですので、これらを extends することによって設定されるルールを確認し、ルール選定を行なっていきます。

npm trend によると、stylelint-config-recommended の方が若干人気があるようです。

stylelint-config-recommended の設定ルールが記載されている index.js をチェックします。

"use strict";

module.exports = {
  rules: {
    "at-rule-no-unknown": true, // @ 指定の記述が正しいか
    "block-no-empty": true, // 空の宣言ブロックをチェック
    "color-no-invalid-hex": true, // 16進数表記が正しいか
    "comment-no-empty": true, // 空コメントをチェック
    "declaration-block-no-duplicate-properties": [
      true, // 同一セレクタ内で同じプロパティをチェック
      {
        ignore: ["consecutive-duplicates-with-different-values"], // 異なる値を持つ、連続する重複するプロパティを無視(フォールバックとして同一プロパティが指定されることがあるため)
      },
    ],
    "declaration-block-no-shorthand-property-overrides": true, // 宣言ブロック内でのショートハンドプロパティによる上書きをチェック
    "font-family-no-duplicate-names": true, // font-family の重複指定をチェック
    "font-family-no-missing-generic-family-keyword": true, // 総称フォントファミリーの指定を要求
    "function-calc-no-invalid": true, // calc() の表記が正しいか
    "function-calc-no-unspaced-operator": true, // calc() 内の演算子左右にスペースが入っているか
    "function-linear-gradient-no-nonstandard-direction": true, // linear-gradient の表記チェック
    "keyframe-declaration-no-important": true, // keyframes 内での !important をチェック
    "media-feature-name-no-unknown": true, // メディアクエリの指定が正しいか
    "no-descending-specificity": true, // 同一セレクタの記述順が詳細度の低いものから順になっているか
    "no-duplicate-at-import-rules": true, // import 宣言に被りがないか
    "no-duplicate-selectors": true, // 重複するセレクタがないか
    "no-empty-source": true, // 空の CSS ファイルがないか
    "no-extra-semicolons": true, // セミコロンの重複がないか
    "no-invalid-double-slash-comments": true, // // で始まるコメントアウトがないか
    "property-no-unknown": true, // 存在しないプロパティが使われていないか
    "selector-pseudo-class-no-unknown": true, // hover など、擬似クラスの名前が正しいか
    "selector-pseudo-element-no-unknown": true, // before など、擬似要素の名前が正しいか
    "selector-type-no-unknown": true, // 指定されている HTML 要素が正しいか
    "string-no-newline": true, // content 内などで文字列に改行が入っていないか
    "unit-no-unknown": true, // 単位の記述(px, em など)が正しいか
  },
};

stylelint-config-standard による設定ルール

stylelint-config-standardindex.js を確認してみると、先に紹介した stylelint-config-recommended の設定を継承し、追加で細かいルールを上書きしているようです。 こちらで設定されているルールの詳細は公式ページの stylelint: List of rules をご参照ください。

"use strict";

module.exports = {
  extends: "stylelint-config-recommended",
  rules: {
    "at-rule-empty-line-before": [
      "always",
      {
        except: ["blockless-after-same-name-blockless", "first-nested"],
        ignore: ["after-comment"],
      },
    ],
    "at-rule-name-case": "lower",
    "at-rule-name-space-after": "always-single-line",
    "at-rule-semicolon-newline-after": "always",
    "block-closing-brace-empty-line-before": "never",
    "block-closing-brace-newline-after": "always",
    "block-closing-brace-newline-before": "always-multi-line",
    "block-closing-brace-space-before": "always-single-line",
    "block-opening-brace-newline-after": "always-multi-line",
    "block-opening-brace-space-after": "always-single-line",
    "block-opening-brace-space-before": "always",
    "color-hex-case": "lower",
    "color-hex-length": "short",
    "comment-empty-line-before": [
      "always",
      {
        except: ["first-nested"],
        ignore: ["stylelint-commands"],
      },
    ],
    "comment-whitespace-inside": "always",
    "custom-property-empty-line-before": [
      "always",
      {
        except: ["after-custom-property", "first-nested"],
        ignore: ["after-comment", "inside-single-line-block"],
      },
    ],
    "declaration-bang-space-after": "never",
    "declaration-bang-space-before": "always",
    "declaration-block-semicolon-newline-after": "always-multi-line",
    "declaration-block-semicolon-space-after": "always-single-line",
    "declaration-block-semicolon-space-before": "never",
    "declaration-block-single-line-max-declarations": 1,
    "declaration-block-trailing-semicolon": "always",
    "declaration-colon-newline-after": "always-multi-line",
    "declaration-colon-space-after": "always-single-line",
    "declaration-colon-space-before": "never",
    "declaration-empty-line-before": [
      "always",
      {
        except: ["after-declaration", "first-nested"],
        ignore: ["after-comment", "inside-single-line-block"],
      },
    ],
    "function-comma-newline-after": "always-multi-line",
    "function-comma-space-after": "always-single-line",
    "function-comma-space-before": "never",
    "function-max-empty-lines": 0,
    "function-name-case": "lower",
    "function-parentheses-newline-inside": "always-multi-line",
    "function-parentheses-space-inside": "never-single-line",
    "function-whitespace-after": "always",
    indentation: 2,
    "length-zero-no-unit": true,
    "max-empty-lines": 1,
    "media-feature-colon-space-after": "always",
    "media-feature-colon-space-before": "never",
    "media-feature-name-case": "lower",
    "media-feature-parentheses-space-inside": "never",
    "media-feature-range-operator-space-after": "always",
    "media-feature-range-operator-space-before": "always",
    "media-query-list-comma-newline-after": "always-multi-line",
    "media-query-list-comma-space-after": "always-single-line",
    "media-query-list-comma-space-before": "never",
    "no-eol-whitespace": true,
    "no-missing-end-of-source-newline": true,
    "number-leading-zero": "always",
    "number-no-trailing-zeros": true,
    "property-case": "lower",
    "rule-empty-line-before": [
      "always-multi-line",
      {
        except: ["first-nested"],
        ignore: ["after-comment"],
      },
    ],
    "selector-attribute-brackets-space-inside": "never",
    "selector-attribute-operator-space-after": "never",
    "selector-attribute-operator-space-before": "never",
    "selector-combinator-space-after": "always",
    "selector-combinator-space-before": "always",
    "selector-descendant-combinator-no-non-space": true,
    "selector-list-comma-newline-after": "always",
    "selector-list-comma-space-before": "never",
    "selector-max-empty-lines": 0,
    "selector-pseudo-class-case": "lower",
    "selector-pseudo-class-parentheses-space-inside": "never",
    "selector-pseudo-element-case": "lower",
    "selector-pseudo-element-colon-notation": "double",
    "selector-type-case": "lower",
    "unit-case": "lower",
    "value-keyword-case": "lower",
    "value-list-comma-newline-after": "always-multi-line",
    "value-list-comma-space-after": "always-single-line",
    "value-list-comma-space-before": "never",
    "value-list-max-empty-lines": 0,
  },
};

実用にあたってのカスタマイズ

stylelint-config-standard は、そのまま使用するなら良いかもしれませんが、チームのコーディングルールに合わせて細かいカスタマイズを行うなら少し融通が効きにくいかもしれません。 設定ルールの少なさによる見通しの良さもあり、stylelint-config-recommended をカスタマイズして使用することとしました。

npm scripts の設定

毎回 npx コマンドやディレクトリパスを打つのも手間ですので、npm run stylelint でコマンドを実行できるように package.json にスクリプトを記述しておきます。 ディレクトリパスは開発環境に応じて随時変更して下さい。

  • package.json(追加したscripts部分のみ)
  "scripts": {
    "stylelint": "stylelint 'src/_scss/**/*.scss'",
    "stylelint:fix": "stylelint --fix 'src/_scss/**/*.scss'"
  }

.stylelint.json のカスタマイズ

stylelint-config-recommended をベースとして設定を追加します。

  • .stylelint.json
{
  "extends": "stylelint-config-recommended"
  "rules": {
    "at-rule-no-unknown": null, // SCSS @ の記述でエラーが出るため stylelint-config-recommended の設定を null で上書き
    "block-opening-brace-space-before": "always" // { の前に必ずスペースを入れる
    "declaration-colon-space-after": "always", // コロンの後に必ずスペースを入れる
    "string-quotes": "double", // クオーテーションはダブルを使用
    "selector-list-comma-newline-after": "always" // セレクターリストのカンマ後は改行
    "color-hex-case": "lower", // 16 進数での色指定は小文字
    "color-hex-length": "short", // 省略可能であれば 16 進数での色指定を省略する
    "length-zero-no-unit": true, // 値 0 の指定に単位を付与しない
    "number-leading-zero": "always", // 1 以下の小数で頭の 0 を省略しない
    "rule-empty-line-before": [
      "always-multi-line", // 複数行ルールの前に空の行を要求
      {
        "ignore": ["after-comment", "first-nested"] // コメント後、およびネストされているルールの最初の子は無視する
      }
    ]
  }
}

叩き台ですが、これで一旦弊社コーディングルールに沿った形での設定を行うことができました。

まとめ

運用していく上で変更や追加などあれば随時変更をおこなっていきます。 今回導入した stylelint-config-recommended ですが、自分が普段使用している SCSS での記述には最適化されていません。 公式スタートガイドにて、

SCSS での使用の場合、stylelint-config-sass-guidelines を使いたい場合があるでしょう

というような記述を見つけたので、こちらのほうが適しているのではと考えています。 こちらで設定されるルールは未確認ですので、後ほど確認し良さそうであれば変更予定です。