CRXJS Vite PluginでTailwind CSSを使う注意点

· 6 min read

拡張機能のフレームワークCRXJS Vite PluginTailwind CSSを組み合わせて使うと、Tailwindの部分だけHMRが効きません。対策としてViteのバージョンを下げるという手もありますが、懸命とはいえません。

リポジトリのIssue #671をウォッチしていたところ、Viteの設定を追加して解決できることが判明しました。
本記事では、Issueにコメントされていた設定を少し改変して解決策を解説します。

しくみ

Tailwind CSSに関係するCSSファイルのタイプスタンプを更新することでHMRを発火できます。これをViteのプラグインとして取り入れることで問題を解決できます。

プラグインのコード

まずはプラグインのコードです。今回はプロジェクトファイル直下にvite-plugin-touch-global-css.tsという名前で作りました。

先に全体を載せ、次の項目から小分けで解説します。

vite-plugin-touch-global-css.ts
import fs from "node:fs";
import type { Plugin, ViteDevServer } from "vite";
function touchFile(filePath: string): void {
const time = new Date();
fs.utimesSync(filePath, time, time);
}
type TouchGlobalCSSPluginOptions = {
cssFilePath: string;
watchMatch: RegExp;
};
export default function touchGlobalCSSPlugin({
cssFilePath,
watchMatch,
}: TouchGlobalCSSPluginOptions): Plugin {
return {
name: "touch-global-css",
configureServer(server: ViteDevServer) {
server.watcher.on("change", (path: string) => {
if (watchMatch.test(path)) {
touchFile(cssFilePath);
}
});
},
};
}

タイムスタンプの更新

touchFileがファイルのタイムスタンプを更新する関数です。

function touchFile(filePath: string): void {
const time = new Date();
fs.utimesSync(filePath, time, time);
}

プラグインのオプション

プラグインのオプションは2つあります。

type TouchGlobalCSSPluginOptions = {
cssFilePath: string;
watchMatch: RegExp;
};

cssFilePathは次のようなTailwind CSSのディレクティブを書いたファイルを指定します。

例: src/content/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

watchMatchは「保存したらHMRを発火させたいパスの正規表現」にしました。元のIssueはディレクトリではなくファイルのリストを扱っていましたが、筆者は変更しました。これで「srcディレクトリ以下」のような指定ができます。

プラグインとして返す

最後のtouchGlobalCSSPluginが実際にプラグインとして呼ばれるコードです。
ファイルを監視して、変更ファイルが「watchMatchで指定したパス」とマッチしたらcssFilePathのタイムスタンプを更新します。

export default function touchGlobalCSSPlugin({
cssFilePath,
watchMatch,
}: TouchGlobalCSSPluginOptions): Plugin {
return {
name: "touch-global-css",
configureServer(server: ViteDevServer) {
server.watcher.on("change", (path: string) => {
if (watchMatch.test(path)) {
touchFile(cssFilePath);
}
});
},
};
}

Viteのプラグインの書き方をもっと詳しく知りたい方は公式ドキュメントを読みましょう。

vite.config.ts

後はvite.config.tsでプラグインを使うように書き換えるだけです。次の例ではReactを使っていますが、他の場合でもやり方自体は同じです。

vite.config.ts
import path from "node:path";
import { crx } from "@crxjs/vite-plugin";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
import manifest from "./src/manifest";
import viteTouchGlobalCss from "./vite-plugin-touch-global-css";
export default defineConfig({
plugins: [
react(),
crx({ manifest }),
viteTouchGlobalCss({
cssFilePath: path.resolve(__dirname, "src/content/index.css"),
watchMatch: /src/,
}),
],
});

ファイルの監視はsrcディレクトリ以下を対象にしました。

他の対応も列挙

Issueには他の方法も挙がっていましたので一応紹介します。筆者としては上記のTouchプラグインでの対応を推奨します。

Viteのバージョンを落とす

Viteを3まで落として使う方法があります。ただ他のViteのプラグインの関係でバージョンを落とすのが大変面倒くさくなってしまうケースが多いです。

Tailwindのsafelistを活用する

Tailwind CSSの設定のsafelistに書いたクラスは、実際にそのクラスを使っていないくてもCSSに含まれます。このしくみをdevelopmentモードのときだけ活用する解決方法です。

tailwind.config.js
// ...省略
safelist: process.env.NODE_ENV === 'development' ? [{ pattern: /./ }] : [],

ただこれだとすべてのクラスを含んでしまうため、次のようによく使う特定のクラスのみsafelistに突っ込む方法もあります。

tailwind.config.js
// ...省略
safelist: process.env.NODE_ENV === 'development' ?
[
{
// colors
pattern: /\b((bg|text)-[\w-]+)/,
},
{
// padding and margin
pattern: /\b((p|px|py|my|mx|spacing-x|spacing-y)-\d+)/,
},
{
// width, high, and size
pattern: /\b((w|h|s)-\d+)/,
},
] : [],

最近はChrome拡張機能のフレームワークとしてwxtextension.jsもありますが、まだまだCRXJS Vite Pluginも現役ですね。
本当は本体のコードを読んでPull Requestを送るのが一番ですが、メンテナンスが止まっているっぽいのでCRXJS x Tailwind民はしばらくこの対応だけで落ち着きそうです。

初めてViteのプラグインを書くという体験。意外と短いコードだけでもいけるんだな~。