こうこく
作 ▸

Astro 5.1 + SCSSプロジェクトにStylelint導入する

いま『Webアプリケーションアクセシビリティ』を読んでいて、あんま関係ないけどStylelintの存在を初めて知ったので、サイト・ブログ用のAstroプロジェクトに導入してみることにした。

astro 5.1.1stylelint 16.12.0
もくじ

前提

今回Stylelintを導入するプロジェクトは、Astroを使ったもの。

SCSSを使用してて、だいたい半分くらいのスタイルは *.scss ファイルに書いてimport、もう半分くらいのスタイルは *.astro ファイル内の <style lang="scss"> タグ内に記述してる。また、部分的にはSCSSを使わず *.css ファイルをimportしている箇所もある。

MDXは使用していないことを留意のうえ、上記全部をカバーしていきたい。

Stylelint導入

だいたいStylelintのGetting Startedと、AstroのエディタのセットアップのStylelint項を見る。

まず、各種パッケージをインストール。

インストールコマンド
pnpm install --save-dev stylelint stylelint-config-standard-scss postcss-html stylelint-config-html
  • stylelint … Stylelint本体
  • stylelint-config-standard-scss … SCSS用のStylelint設定。stylelint-config-standardstylelint-config-recommended-scssの内容を含むらしい。あと、stylelint-scssもこれに含まれてるから、別途インストールは不要らしい。
  • postcss-html, stylelint-config-html … HTMLファイルやAstroファイルの <style> タグ内のスタイルもチェックできるようにする。

次に、プロジェクトルートに以下の内容で .stylelintrc.mjs を作成。これがstylelintの設定ファイルとなる。

.stylelintrc.mjs
/** @type {import('stylelint').Config} */
export default {
  extends: ['stylelint-config-standard-scss', 'stylelint-config-html'],
  rules: {
    // Astroの疑似クラス :global は指摘させない (後述)
    'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global'] }],
  },
  overrides: [
    // *.astroファイルのカスタムプロパティは指摘させない (後述)
    {
      files: ['*.astro'],
      rules: {
        'custom-property-pattern': null,
      },
    },
  ],
};

あと、自分はthe-new-css-resetをCSSファイルにコピペしてimportしているので、stylelintの対象から除外するために .stylelintignore も作成。

.stylelintignore
the-new-css-reset.css

最後に、自分はVSCodeを使っているので、VSCode用のstylelint拡張機能をインストール。

インストールコマンド
code --install-extension stylelint.vscode-stylelint

この拡張機能用に、VSCodeの settings.json に以下を追加。

settings.json (抜粋)
{
  // stylelintの拡張機能の対象にするファイルの拡張子
  "stylelint.validate": ["css", "scss", "html", "astro"],

  // 保存時に自動で --fix するように
  "editor.codeActionsOnSave": {
    "source.fixAll.stylelint": "explicit"
  },
}

実行してみる

どんなもんか見てみましょう

StylelintでProblemsが203件検出されたVSCodeのスクリーンショット

あばばばばwwww そんなにだめ!?

以下の通りにコマンドラインから実行して、総エラー件数を確認してみる。

コマンド
npx stylelint "**/*.{css,scss,html,astro}"
実行結果 (抜粋)
✖ 449 problems (449 errors, 0 warnings)
  412 errors potentially fixable with the "--fix" option.

思ったより……か?

次項に、ここまでの設定を使って自分のプロジェクトで出たものをいくつか抜粋して掲載しておく。

Note

pnpmを使ってるから実行コマンドは pnpm dlx でもいいはずだと思うんだけど、Error: Cannot resolve custom syntax module "postcss-html". Check that module "postcss-html" is available and spelled correctly. エラーが出るので諦めて npx 使った。一応、GitHubのIssueにそれっぽいのがあったので↓に貼っておく。

Cannot resolve custom syntax module "postcss-scss" · Issue #2 · stylelint-scss/stylelint-config-standard-scss

出てきたもの実例

Tip

だいたいのルールは、オプションでどの書き方に統一するかを指定できる。

selector-pseudo-class-no-unknown

Unexpected unknown pseudo-class selector ":global" (selector-pseudo-class-no-unknown)

存在しない疑似クラスが使われている。

ul {
  margin: 1.5rem 0 1.5rem 0.5rem;
  &:global(> li) {
    margin: 0.2rem 0;
    padding-left: 1.4rem;
    @include list-mark-light;
  }
}

……が、:globalはAstroで使える疑似クラスなので、指摘してほしくない。

.stylelintrc.mjs に以下を追加して無視してもらう。

.stylelintrc.mjs
export default {
  rules: {
    'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global'] }],
  },
};

custom-property-pattern

Expected custom property name "--maxWidth" to be kebab-case (custom-property-pattern)

カスタムプロパティがケバブケースでない。

<style lang="scss" define:vars={{ maxWidth: `${maxWidth}px` }}>
  .container {
    margin: 0 auto;
    max-width: var(--maxWidth);
  }
  main {
    padding: 0 0.5rem;
  }
</style>

……が、Astroの define:vars で渡したカスタムプロパティは公式サンプルでもキャメルケースなので、指摘してほしくない。

.stylelintrc.mjs に以下を追加して、*.astro ファイル内のカスタムプロパティは無視してもらう。

.stylelintrc.mjs
export default {
  overrides: [
    {
      files: ['*.astro'],
      rules: {
        'custom-property-pattern': null,
      },
    },
  ],
};

function-url-quotes

Expected quotes around "url" function argument (function-url-quotes)

url() 関数の引数にクォートが無い。

:root {
  --article-section-header-img: url(/assets/common/header-kiriukun-colored.svg);
}
:root {
  --article-section-header-img: url('/assets/common/header-kiriukun-colored.svg');
}

クォートつけられるの知らなかった。

selector-pseudo-element-colon-notation

Expected double colon pseudo-element notation (selector-pseudo-element-colon-notation)

疑似要素の ::: になっている。

blockquote:before,
blockquote:after,
q:before,
q:after {
  content: '';
}
blockquote::before,
blockquote::after,
q::before,
q::after {
  content: '';
}

シンプルに書き間違いで恥ずかしい。感謝。

rule-empty-line-before

Expected empty line before rule (rule-empty-line-before)

ルールのブロックの前に空行が無い。

a {
  text-decoration: underline;
  transition: color 0.08s linear;
  &:link {
    color: var(--link-color);
  }
  &:visited {
    color: var(--link-visited-color);
  }
}
a {
  text-decoration: underline;
  transition: color 0.08s linear;

  &:link {
    color: var(--link-color);
  }

  &:visited {
    color: var(--link-visited-color);
  }
}

これは見逃してほしい。OFFにする。

property-no-vendor-prefix

Unexpected vendor-prefix "-webkit-text-size-adjust" (property-no-vendor-prefix)

Autoprefixerがなんとかしてくれるプロパティにベンダープレフィックスが書かれている。

html {
  -webkit-text-size-adjust: 100%;
}
html {
  text-size-adjust: 100%;
}

-webkit-text-size-adjust に関しては思考停止で書いてた……。

selector-class-pattern

Expected class selector ".c_red" to be kebab-case (selector-class-pattern)

クラスセレクタがケバブケースでない。

.c_red {
  color: #f33 !important;
}
.c-red {
  color: #f33 !important;
}

昔から使ってる汎用クラスだから移行が面倒で……。

color-function-notation

Expected modern color-function notation (color-function-notation)

色の指定方法が古い。

code {
  margin: 0 1px;
  padding: 0.2rem;
  border-radius: 0.2rem;
  background-color: var(--code-bg-color);
  text-shadow: 0 1px 1px hsla(0, 0%, 0%, 0.3);
}
code {
  margin: 0 1px;
  padding: 0.2rem;
  border-radius: 0.2rem;
  background-color: var(--code-bg-color);
  text-shadow: 0 1px 1px hsl(0deg 0% 0% / 30%);
}

知らなかった。そんな違ったのか。

media-feature-range-notation

Expected "context" media feature range notation (media-feature-range-notation)

範囲指定するメディアクエリは >= とか <= とかで書くべき (Media Queries Level 4)。

@mixin header-barisan {
  @media (min-resolution: 2dppx) {
    border-image-source: url('/assets/common/border-barisan_base@2x.gif');
    border-image-slice: 0 20 4 36;
    background-image: url('/assets/common/border-barisan_top@2x.gif');
  }
}
@mixin header-barisan {
  @media (min-resolution >= 2dppx) {
    border-image-source: url('/assets/common/border-barisan_base@2x.gif');
    border-image-slice: 0 20 4 36;
    background-image: url('/assets/common/border-barisan_top@2x.gif');
  }
}

Retinaディスプレイ用に高解像度の素材を用意してたやつ。主要ブラウザは軒並み対応したらしいけど、他ならぬ自分がiOSのアップデートをサボってる人種だからなんとも……。今回はオプション prefix で従来の表記にしておく。

no-descending-specificity

Expected selector "img" to come before selector ".markdown-content figure.img img" (no-descending-specificity)

より具体的なセレクタ=優先度が高いセレクタが、それよりも低い優先度のセレクタの手前に書かれている。

セレクタの優先度とソース上の順序を合わせるべきということです。

.tl-ev-label {
  display: flex;
  column-gap: 0.35rem;
  margin-left: 0.4rem;
  line-height: 1;
  font-size: 0.85rem;
}
.tl-ev-label + .tl-ev-content {
  margin-top: -0.6rem;
  padding-top: 1rem;
}
.tl-ev-content {
  border: 1px solid hsla(var(--text-color-hsl), 0.4);
}
.tl-ev-label {
  display: flex;
  column-gap: 0.35rem;
  margin-left: 0.4rem;
  line-height: 1;
  font-size: 0.85rem;
}
.tl-ev-content {
  border: 1px solid hsla(var(--text-color-hsl), 0.4);
}
.tl-ev-label + .tl-ev-content {
  margin-top: -0.6rem;
  padding-top: 1rem;
}

言われてみると「なるほど」と思うが、絶対に併用しないことがわかってるところまで言われると厳しく感じるかも。行単位で無効化するのがよさそう。

font-family-name-quotes

Unexpected quotes around "Meiryo" (font-family-name-quotes)

フォント名にクォートがついている。

html {
  font-size: 14px;
  font-family: '游ゴシック体', 'Yu Gothic', 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', 'メイリオ', 'Meiryo', 'MS Pゴシック',
    'MS PGothic', sans-serif;
  text-size-adjust: 100%;
}
html {
  font-size: 14px;
  font-family: '游ゴシック体', 'Yu Gothic', 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, 'MS Pゴシック',
    'MS PGothic', sans-serif;
  text-size-adjust: 100%;
}

なぜ Meiryo だけエラー? と思ったけど、空白・数字・ハイフン以外の句読点が入ってないからつけなくていいよ、ということらしい。個人的にはそういうの面倒だから、オプション always-unless-keyword でキーワード以外全部クォート必須に変更する。

scss/double-slash-comment-empty-line-before

Expected empty line before comment (scss/double-slash-comment-empty-line-before)

// コメントの前に空行が無い。

a,
button {
  &:focus {
    outline: auto;
    outline-offset: 3px;
    // outline: none;
    // box-shadow:
    //   0 0 0 3px var(--accent-color),
    //   0 0 0 5px var(--inverted-gb-color);
  }
}
a,
button {
  &:focus {
    outline: auto;
    outline-offset: 3px;

    // outline: none;
    // box-shadow:
    //   0 0 0 3px var(--accent-color),
    //   0 0 0 5px var(--inverted-gb-color);
  }
}

お試しで書いたものをコメントアウトしてそのまま残したりするので、これはほっといてほしい。OFFにする。

最終的な設定ファイル

今回はこんな感じになった。

.stylelintrc.mjs
/** @type {import('stylelint').Config} */
export default {
  extends: ['stylelint-config-standard-scss', 'stylelint-config-html'],
  rules: {
    'declaration-empty-line-before': null,
    'media-feature-range-notation': 'prefix',
    'font-family-name-quotes': 'always-unless-keyword',
    'comment-empty-line-before': null,
    'rule-empty-line-before': null,
    'at-rule-empty-line-before': null,
    'scss/double-slash-comment-empty-line-before': null,
    'selector-class-pattern': null,
    'selector-id-pattern': null,
    'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global'] }],
  },
  overrides: [
    {
      files: ['*.astro'],
      rules: {
        'custom-property-pattern': null,
      },
    },
  ],
};

これで --fix かけたら軒並み消えたので、終わり。

この記事に何かあればこちらまで (非公開)