CSSファイルに記述されたクラスをTailwind CSS IntelliSenseで検出できるようにする

VS Codeの拡張機能であるTailwind CSS IntelliSenseを使用する際には、通常、CSSファイルに記述されたクラスは補完されない。というのも、Tailwind CSS IntelliSenseでは、Tailwind CSSの設定ファイル(tailwind.config.js)を基にしてCSSを算出しており、設定ファイルを介さずに実装されたCSSは無視されるからだ。

そのため、公式ドキュメントのAdding Custom StylesにあるようにCSSを記述するだけでは、IntelliSenseが有効にならない。

これに対する解決策は、TailwindプラグインとしてCSSを実装することだ。たとえば次のようにすることで、IntelliSenseからcardクラスを検出できるようになる。

const plugin = require('tailwindcss/plugin');

module.exports = {
  // ...
  plugins: [
    plugin(({ addBase, addComponents, addUtilities, theme }) => {
      addComponents({
        '.card': {
          backgroundColor: theme('colors.white'),
          borderRadius: theme('borderRadius.lg'),
          padding: theme('spacing.6'),
          boxShadow: theme('boxShadow.xl'),
        },
      });
    }),
  ],
};

ただしこの場合、JavaScriptとしてCSSを記述しなければならないという制約がある。普通のCSSと異なる書き方をしなければならないという点では、やや不便だろう。

そこで、代わりに次のような実装をすることで、普通のCSSとして記述されたファイルをプラグインとして読み込むことができる。

const fs = require('fs');
const postcss = require('postcss');
const plugin = require('tailwindcss/plugin');

module.exports = {
  // ...
  plugins: [
    plugin(({ addComponents }) => {
      const content = fs.readFileSync('src/styles/card.css', 'utf8');
      const styles = postcss.parse(content);
      addComponents(styles.nodes);
    }),
  ],
};

card.cssは次のようになる。

.card {
  box-shadow: theme(boxShadow.xl);
  border-radius: theme(borderRadius.lg);
  background-color: theme(colors.white);
  padding: theme(spacing.6);
}

styles.nodesには、CSSをパースしてオブジェクト化された結果が格納されており、これをそのまま読み込むことでスタイルを登録できる。これは、Core pluginsのpreflightの実装方法を参考にしたものだ。

これによってCSSファイルを読み込めるようにはなったが、ただ、さらに言うならば、CSSファイルが増えたり名前が変わったりした際には、その都度プラグインの実装に手を入れる必要があり、余計な手間は生じる。それを省力化すべく、CSSのファイル名に応じて自動的にスタイルが読み込まれる仕組み(convention over configuration)を作ってみよう。

一つ前提として、Tailwindにはレイヤーという概念がある。Tailwindのすべてのスタイルは、base、components、utilitiesのいずれかに分類される必要がある。詳しくは「Why does Tailwind group styles into “layers”?」を参照のこと。

プラグインの引数であるaddBaseaddComponentsaddUtilitiesは、それらのレイヤーに基づいており、ユーザーは、適切なレイヤーに対応する関数を選択して使用することになる。CSSファイルがどのレイヤーに属するかの区分をするために、ファイル名のプレフィックスとしてレイヤー名を含めるという規約を設けるのがよいだろう。

これを踏まえると、次のような実装になった。

/**
 * Loads CSS files through Tailwind’s plugin system to enable IntelliSense support.
 *
 * This plugin scans CSS files located in the `src/styles` directory and appends
 * them to their respective layers based on the file naming convention:
 *
 * - Files named `src/styles/base.{name}.css` are added to the base layer.
 * - Files named `src/styles/components.{name}.css` are added to the components layer.
 * - Files named `src/styles/utilities.{name}.css` are added to the utilities layer.
 */
const cssFiles = plugin(({ addBase, addComponents, addUtilities }) => {
  const dirname = path.join(__dirname, 'src/styles');
  const files = fs.readdirSync(dirname);

  for (const file of files) {
    const matched = /^(base|components|utilities)\..+\.css$/.exec(file);

    if (matched) {
      const layer = matched[1];
      const addStyles = {
        base: addBase,
        components: addComponents,
        utilities: addUtilities,
      }[layer];
      const content = fs.readFileSync(path.join(dirname, file), 'utf8');
      const styles = postcss.parse(content);

      addStyles(styles.nodes);
    }
  }
});

module.exports = {
  // ...
  plugins: [cssFiles],
};

しかし、まだ一つ問題が残る。それは、開発時にビルドツールなどを使ってソースコードの変更を監視する場合、CSSファイルを変更してもそれが検知されないため、再ビルドが自動的に実行されないということだ。

これについての解決策としては、ビルドツールとは別に監視プロセスを設けるという手がある。CSSファイルが変更されるたびにtailwind.config.jsのタイムスタンプを更新することで、ビルドツールが再ビルドを実行するように促すことができる。npm scriptsとして、Chokidar CLIconcurrentlyを使用することで、次のように実現できる。

{
  "scripts": {
    "dev": "concurrently --raw \"npm:dev:*\"",
    "dev:astro": "astro dev",
    "dev:css": "chokidar \"src/styles/{base,components,utilities}.*.css\" -c \"touch tailwind.config.cjs\" -d 0 --silent"
  }
  // ...
}