🚨

eslint-plugin-tailwindcssを触ってみる

2021/06/27に公開

eslintの設定めんどくさいですよね😖
Next.jsではv11からeslintがビルドインになったので、前から気になっていたeslint-plugin-tailwindcssを触ってみたいと思います。
なお、今回はnpx create-next-app --tsしたものにtailwindcssを追加したものでやっていきます😎

公式はこちら
https://github.com/francoismassart/eslint-plugin-tailwindcss

モチベーション

  • 人によって順番が違うclassの並びを自動で修正したい
  • typoを検知したい
  • breaking changeなどでclass名が無効の場合検知したい
  • tailwind.confg.jsを解析してエラーを吐いてほしい

機能

  • classNameを自動で並び替えてくれる
  • tailwindのclass名や指定されたcss以外のclassだとエラーを出す
  • 同一のプロパティが重複している場合エラーを出す
  • JIT対応

こんな感じ🤔
https://github.com/francoismassart/eslint-plugin-tailwindcss#supported-rules

index.tsxをtailwind仕様に書き換える

index.tsxは下記のリポジトリwith tailwindcssから拝借
https://github.com/vercel/next.js/blob/canary/examples/with-tailwindcss/pages/index.js

index.tsx
import Head from 'next/head'
import Image from "next/image"

export default function Home() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen py-2">
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center">
        <h1 className="text-6xl font-bold">
          Welcome to{' '}
          <a className="text-blue-600" href="https://nextjs.org">
            Next.js!
          </a>
        </h1>

        <p className="mt-3 text-2xl">
          Get started by editing{' '}
          <code className="p-3 font-mono text-lg bg-gray-100 rounded-md">
            pages/index.js
          </code>
        </p>

        <div className="flex flex-wrap items-center justify-around max-w-4xl mt-6 sm:w-full">
          <a
            href="https://nextjs.org/docs"
            className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
          >
            <h3 className="text-2xl font-bold">Documentation &rarr;</h3>
            <p className="mt-4 text-xl">
              Find in-depth information about Next.js features and API.
            </p>
          </a>

          <a
            href="https://nextjs.org/learn"
            className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
          >
            <h3 className="text-2xl font-bold">Learn &rarr;</h3>
            <p className="mt-4 text-xl">
              Learn about Next.js in an interactive course with quizzes!
            </p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/master/examples"
            className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
          >
            <h3 className="text-2xl font-bold">Examples &rarr;</h3>
            <p className="mt-4 text-xl">
              Discover and deploy boilerplate example Next.js projects.
            </p>
          </a>

          <a
            href="https://vercel.com/import?filter=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
            className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
          >
            <h3 className="text-2xl font-bold">Deploy &rarr;</h3>
            <p className="mt-4 text-xl">
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

      <footer className="flex items-center justify-center w-full h-24 border-t">
        <a
          className="flex items-center justify-center"
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} className="h-4 ml-2" />
        </a>
      </footer>
    </div>
  )
}

セットアップ

eslint-plugin-tailwindcssをインストール

$ npm i -D eslint-plugin-tailwindcss

.eslintrcに追記

.eslintrc
{
  "extends": [
    "next",
    "next/core-web-vitals",
+   "plugin:tailwindcss/recommended"
  ],
+ "plugins": ["tailwindcss"]
}

とりあえず、リンターを走らせてみる

npm run lintを実行

$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5

./src/pages/index.tsx
6:10  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
12:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
27:14  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
30:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
40:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
50:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
60:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
70:15  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
72:11  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
78:77  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order

めちゃくちゃ怒られる😖

自動修正させる

scriptsにfixを追記

package.json
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
+   "fix:lint": "next lint --fix"
  },

npm run fix:lintを実行

$ npm run fix:lint

> next-eslint-plugin-tailwindcss@0.1.0 fix:lint
> next lint --fix

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
✔ No ESLint warnings or errors
index.tsx
import Head from 'next/head'
import Image from "next/image"

export default function Home() {
  return (
-   <div className="flex flex-col items-center justify-center min-h-screen py-2">
+   <div className="flex flex-col justify-center items-center py-2 min-h-screen">
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

-     <main className="flex flex-col items-center justify-center w-full flex-1 px-20 text-center">
+     <main className="flex flex-col flex-1 justify-center items-center px-20 w-full text-center">
        <h1 className="text-6xl font-bold">
          Welcome to{' '}
          <a className="text-blue-600" href="https://nextjs.org">
            Next.js!
          </a>
        </h1>

        <p className="mt-3 text-2xl">
          Get started by editing{' '}
          <code className="p-3 font-mono text-lg bg-gray-100 rounded-md">
            pages/index.js
          </code>
        </p>

-       <div className="flex flex-wrap items-center justify-around max-w-4xl mt-6 sm:w-full">
+       <div className="flex flex-wrap justify-around items-center mt-6 sm:w-full max-w-4xl">
          <a
            href="https://nextjs.org/docs"
-           className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
+           className="p-6 mt-6 w-96 text-left hover:text-blue-600 focus:text-blue-600 rounded-xl border"
          >
            <h3 className="text-2xl font-bold">Documentation &rarr;</h3>
            <p className="mt-4 text-xl">
              Find in-depth information about Next.js features and API.
            </p>
          </a>

          <a
            href="https://nextjs.org/learn"
-           className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
+           className="p-6 mt-6 w-96 text-left hover:text-blue-600 focus:text-blue-600 rounded-xl border"
          >
            <h3 className="text-2xl font-bold">Learn &rarr;</h3>
            <p className="mt-4 text-xl">
              Learn about Next.js in an interactive course with quizzes!
            </p>
          </a>

          <a
            href="https://github.com/vercel/next.js/tree/master/examples"
-           className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
+           className="p-6 mt-6 w-96 text-left hover:text-blue-600 focus:text-blue-600 rounded-xl border"
          >
            <h3 className="text-2xl font-bold">Examples &rarr;</h3>
            <p className="mt-4 text-xl">
              Discover and deploy boilerplate example Next.js projects.
            </p>
          </a>

          <a
            href="https://vercel.com/import?filter=next.js&utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
-           className="p-6 mt-6 text-left border w-96 rounded-xl hover:text-blue-600 focus:text-blue-600"
+           className="p-6 mt-6 w-96 text-left hover:text-blue-600 focus:text-blue-600 rounded-xl border"
          >
            <h3 className="text-2xl font-bold">Deploy &rarr;</h3>
            <p className="mt-4 text-xl">
              Instantly deploy your Next.js site to a public URL with Vercel.
            </p>
          </a>
        </div>
      </main>

-     <footer className="flex items-center justify-center w-full h-24 border-t">
+     <footer className="flex justify-center items-center w-full h-24 border-t">
        <a
-         className="flex items-center justify-center"
+         className="flex justify-center items-center"
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
-         <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} className="h-4 ml-2" />
+         <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} className="ml-2 h-4" />
        </a>
      </footer>
    </div>
  )
}

並び順がきれいに揃った!😺

typoしてみる

index.tsx
<h1 className="text-6x font-bold">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5

./src/pages/index.tsx
13:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
13:13  Warning: Classname 'text-6x' is not a Tailwind CSS class!  tailwindcss/no-custom-classname

Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules

Warning: Classname 'text-6x' is not a Tailwind CSS class! tailwindcss/no-custom-classnameエラーが出た!

Breaking Changeをしてみる

https://tailwindcss.com/docs/upgrading-to-v2#update-renamed-utility-classes

1xではwhitespace-no-wrapだったものが2xではwhitespace-nowrapに変わったので試してみる

index.tsx
<h1 className="text-6x font-bold whitespace-no-wrap">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5

./src/pages/index.tsx
13:13  Warning: Classname 'whitespace-no-wrap' is not a Tailwind CSS class!  tailwindcss/no-custom-classname

Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules

こっちもWarning: Classname 'whitespace-no-wrap' is not a Tailwind CSS class! tailwindcss/no-custom-classnameと出た

重複を試してみる

index.tsx
<h1 className="text-5xl text-6xl font-bold">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5

./src/pages/index.tsx
13:13  Error: Classnames text-5xl, text-6xl are conflicting!  tailwindcss/no-contradicting-classname

Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules

Error: Classnames text-5xl, text-6xl are conflicting! tailwindcss/no-contradicting-classnameエラーが出た

JITで試してみる

tailwind.confg.js
module.exports = {
  purge: [
    './src/**/*.tsx',
    './src/**/*.ts',
  ],
+ mode: 'jit',
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

index.tsx
<h1 className="text-[10rem] font-bold">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
✔ No ESLint warnings or errors

ちゃんと対応してる😺

単位を間違えていて動かない場合

index.tsx
<h1 className="text-[10re] font-bold">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
✔ No ESLint warnings or errors

エラーは出ないらしい

tailwindの命名に則らない形で記述する

index.tsx
<h1 className="text-size-[10rem] font-bold">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5

./src/pages/index.tsx
13:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order
13:13  Warning: Classname 'text-size-[10rem]' is not a Tailwind CSS class!  tailwindcss/no-custom-classname

Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules

Warning: Classname 'text-size-[10rem]' is not a Tailwind CSS class! tailwindcss/no-custom-classnameエラーが出る

tailwind.configを読んでくれるか試してみる

tailwind.confg.js
module.exports = {
  purge: [
    './src/**/*.tsx',
    './src/**/*.ts',
  ],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {
      fontSize: {
+       '10xl': '10rem',
      }
    },
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

index.tsx
<h1 className="text-10xl font-bold">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
✔ No ESLint warnings or errors

独自のutilityを追加してみる

tailwind.confg.js
+ const plugin = require('tailwindcss/plugin');

module.exports = {
  purge: [
    './src/**/*.tsx',
    './src/**/*.ts',
  ],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
+   plugin(({ addUtilities }) => {
+     const newUtilities = {
+       '.skew-10deg': {
+         transform: 'skewY(-10deg)',
+       }
+     }
+     addUtilities(newUtilities)
+  })
  ]
}

index.tsx
<h1 className="text-6xl font-bold skew-10deg">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5

./src/pages/index.tsx
13:13  Warning: Classname 'skew-10deg' is not a Tailwind CSS class!  tailwindcss/no-custom-classname

Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules

へーエラー出るのか😂
設定間違えたかな🤔

エラー出ないようにする

ホワイトリストに正規表現でぶちこめばいいらしい

.eslintrc
{
  "extends": [
    "next",
    "next/core-web-vitals",
    "plugin:tailwindcss/recommended"
  ],
  "plugins": ["tailwindcss"],
  "settings": {
    "tailwindcss": {
+     "whitelist": ["skew\\-[1]?[\\d]deg"]
    }
  }
}
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
✔ No ESLint warnings or errors

エラーが消えた😺
正直めんどくさいので、.eslintrc.eslintrc.jsに変更して独自のutilityの値の配列を渡してあげれば自動で同期できそう
tailwindのfullConfigからpluginsの値が取得できない?っぽい

ついでに自動同期

newUtilities.js
exports.newUtilities = {
  '.skew-10deg': {
    transform: 'skewY(-10deg)',
  }
};
tailwind.config.js
const plugin = require('tailwindcss/plugin');
const newUtilities = require('./newUtilities');

module.exports = {
  purge: [
    './src/**/*.tsx',
    './src/**/*.ts',
  ],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [
+   plugin(({ addUtilities }) => addUtilities(newUtilities))
  ]
};
.eslintrc.js
+const {newUtilities} = require('./newUtilities');

module.exports = {
  "extends": [
    "next",
    "next/core-web-vitals",
    "plugin:tailwindcss/recommended"
  ],
  "plugins": ["tailwindcss"],
  "settings": {
    "tailwindcss": {
+     "whitelist": Object.keys(newUtilities).map((n) => n.replace(/^\./,''))
    }
  }
};

global cssやCSS Modulesを試す

globals.css

globals.css
html,
body {
  padding: 0;
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
    Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}

a {
  color: inherit;
  text-decoration: none;
}

* {
  box-sizing: border-box;
}

+.h1 {
+ font-size: 10rem;
+}
index.tsx
<h1 className="h1 font-bold">
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5

./src/pages/index.tsx
13:13  Warning: Invalid Tailwind CSS classnames order  tailwindcss/classnames-order

Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules

並び替えのエラーしか出ていないので、大丈夫みたい

CSS Modules

Home.module.css
.h1 {
  font-size: 10rem;
}
index.tsx
import styles from './Home.module.css';
<h1 className={`${styles.h1} font-bold`}>
  Welcome to{' '}
  <a className="text-blue-600" href="https://nextjs.org">
    Next.js!
  </a>
</h1>
$ npm run lint

> next-eslint-plugin-tailwindcss@0.1.0 lint
> next lint

info  - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
✔ No ESLint warnings or errors

まとめ

  • 治安が悪くなりがちなtailwindのclassがいい感じになりそう
  • pluginsとjitには注意が必要そう。

個人的にはbreakpointsでソートされている方が好みなので、オプションのgroupByResponsivetrueにしたいと思う。

今回使ったリポジトリ

https://github.com/hisho/next-eslint-plugin-tailwindcss

Discussion