prettier-plugin-tailwindcssを適用したらスタイルが壊れた話
皆さんこんにちは。株式会社バベルでエンジニアをしている uhyo です。バベルが提供しているaileadというプロダクトではスタイリングにTailwind CSSを使用しています。
恥ずかしながら、これまでTailwind CSSを十分に活用するための環境が整っていませんでした。具体的には、Tailwind CSSのクラス名を書くところにリンターが適用されていなかったり、クラスの並び順がとても自由で開発に支障をきたしたりしていました。
そこで、今回prettier-plugin-tailwindcssをコードベースに導入しました。これは、言わずと知れたコードフォーマッターPrettier向けのプラグインで、Tailwind CSSのクラス名をいい感じに並び替えることでコードに少し秩序をもたらしてくれるものです。
コードフォーマッターの原則は、コードの挙動を変えないことです。そのため、理想的にはprettier-plugin-tailwindcssを適用してもプロダクトの見た目が変わることはないはずでした。しかし、現実は甘くなく、我々のプロダクトでは見事にスタイルが壊れてしまったのです。幸い、この変更のリリースまでに1週間ほど余裕を持たせていたため、本番環境にデプロイされる前に問題に気づくことができました。
この記事ではどのようにprettier-plugin-tailwindcssがスタイルを壊したのか、そしてその対策について解説します。
prettier-plugin-tailwindcssの動き
prettier-plugin-tailwindcssの動きについて解説しておきます。基本的には、例えばReactであればJSXのclassName
属性に与えられた文字列が並び替えの対象となります。
例えばこんな感じのdivがあった場合、
<div className="relative flex justify-center items-center w-12 h-full text-gray-800">Hello</div>
prettier-plugin-tailwindcssは、className
属性の値を次のように並び替えます。
<div className="relative flex h-full w-12 items-center justify-center text-gray-800">Hello</div>
何となく少し分かりやすくなったような気がします。(順番が内部の定義順に由来しているからか、px
→py
の順なのにh
→w
になっているなど完璧にセマンティックな順番になっていない点が個人的にあまり好きではないですが)
CSSでは、クラス名の順番は結果に全く影響しません。後に書かれたクラス名のほうが優先されるといったことはありません(スタイルの適用順に関してはクラス名の並び方が参照されることはなく、CSSの宣言のカスケーディングのルールにしたがって決められます)。そのため、どのように並び替えてもコードの意味には影響を与えないはずです。
どのようにスタイルが壊れたか
しかしながら、我々のプロダクトではprettier-plugin-tailwindcssが行った修正が原因でスタイルが崩れてしまう事件が発生しました。スタイル崩れの原因となった変更はこれです。
-<div className="relative w-full px-1 z-10">
+<div className="relative z-10 w-full px-1">
どこがまずいかお分かりでしょうか。
そう、変更前のクラス名においてpx-1
とz-10
の間のスペースが全角スペース (U+3000) なのです。prettier-plugin-tailwindcssの挙動は順番を入れ替えつつスペースはそのままにするというもので、変更後は全角スペースがw-full
とpx-1
の間にあります。
実は、Web仕様上クラス名の区切りとして使えるのはASCII Whitespaceだけです。つまり全角スペースはクラス名の区切りではなく、上の場合はpx-1 z-10
という名前の1つのクラスとしてブラウザに解釈されていました。このことは次のようなJavaScriptコードで確かめられます。
const div = document.createElement('div');
div.className = "relative w-full px-1 z-10";
[...div.classList] // (3) ['relative', 'w-full', 'px-1 z-10']
一方で、prettier-plugin-tailwindcssはクラス名の区切りを正規表現の\s
で判定していたため、U+3000のような非ASCIIの空白も区切りとして扱っていました。ここで実際のセマンティクスとの乖離が生じたため、prettierを走らせるとブラウザが認識するクラス名が変わってしまい、実際の挙動が変わってしまう事態に至ったのです。これはprettier-plugin-tailwindcssのバグです。全角スペースなんか混ぜるのが悪いとはいえ、与えられたクラス名文字列の意味を勝手に変えてはいけません。
全角スペースが混ざると両隣のクラスが巻き込まれて無効化されるので意図したデザインにならないはずです。そのため大抵の場合は気づけそうですが、CSSというのはデザインの変更に伴って気付かないうちに形骸化することは日常茶飯事なので、テストやQAでは気づけない場合もあります。上の例ではpx-1
とz-10
が意味を成していませんでしたが、px-1
はともかくz-10
はこの時点では不要でした。むしろ、prettier-plugin-tailwindcssの適用後にz-10
が意図せず有効化されたことによって逆にデザイン崩れが発生したのです。
対策
筆者がこの問題を認識したとき、まずこの問題の影響範囲を知る必要がありました。そのためにはクラス文字列に全角スペース (U+3000) が混ざっているところを見つける必要がありますが、良く考えたらそもそもクラス名に全角スペースが混ざっているのに気づけないのは良くありません。それはそうですね。そして、このような問題は機械的に検知すべきです。
実はこのプロダクトではeslint-plugin-tailwindcssが導入されていませんでした。このプラグインにはno-custom-classname
というTailwindのクラス名以外を弾いてくれるルールがあるため、これを導入すればpx-1 z-10
のように間に全角スペースが入ったクラス名を検知できるようになります。
……と思っていたのですが、実はeslint-plugin-tailwindcssにも同じバグがあったため、全角スペースが混ざっていても検知できませんでした。
真の対策
筆者がprettier-plugin-tailwindcssとeslint-plugin-tailwindcssにそれぞれプルリクを送り、修正されました。
- https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/166
- https://github.com/francoismassart/eslint-plugin-tailwindcss/pull/246
eslint-plugin-tailwindcssは3.12.1でこの変更がリリースされています。prettier-plugin-tailwindcssはマージされていますが、リリース時期は不明です。
とりあえずeslint-plugin-tailwindcssを最新版に上げてエラーがないことを確認すれば、この記事で紹介した問題は回避できます。とても安心ですね。
まとめ
この記事では、Tailwindのクラス名に全角スペースが混ざってしまったことにより発生した問題とその対応策を紹介しました。
顧客満足度No.1*の商談解析クラウド ailead(エーアイリード)を提供しています。 AIが商談データを自動で収集・解析・可視化することで、営業現場の業務効率化と「売れる」営業人材の育成を実現します。 ailead.app/
Discussion
エディタでの視覚化
zenkaku - Visual Studio Marketplace +全角の全面使用禁止
VSCode拡張機能のご紹介ありがとうございます。視覚化されることで素早くミスに気づけそうですね。しかし、人間の注意力に頼るよりも機械的にチェックされる仕組みのほうがよいと考えたので今回は機械的にチェックする方法を紹介しました。
はい、この記事では私の修正によって、 eslint-plugin-tailwindcss を用いてクラス名に全角が使用禁止になるようになった事例を紹介しました。
masakinihirota 氏は、コード内は半角スペースが基本であり、全角スペースと半角スペースの間違いなどあってはならないこと、基本中の基本だ、ということを言われているかと思われますが。
ツールに依存するのではなく基本的なことを押さえてからテクニカルな問題に取り組むと良いと感じました。
ちなみに、tailwind のクラス順序変えは僕も嫌いですね。
そもそもユーティリティファースト思想(bootstrap 先祖返り?)に違和感を覚えており、過去資産の産廃感が何とも切ないです。
コメントありがとうございます。全角スペースと半角スペースを間違えないというのは基本中の基本ということは同意します。しかし、今回の記事のような現象は、知らなくて全角スペースを書いたのではなく、半角スペースを入力しようとして間違えたものだと思っています。人間というのはミスをするものです。
では、間違えないようにするにはどうすれば良いでしょうか? 私の意見では、「頑張って気を付ける」とか、「エディタの可視化を目視で確認する」とかいうのは良い考えではありません。ツールを正しく設定し、人間の注意力に頼らなくても間違いを防げるようにするのが良いやり方だと考えています。だから、「ツールに依存するのではなく」というような考え方には同意できません。
この記事では、ツールが正しく設定できるまでの道のりを紹介しました。
属人化防止ですね。分かります。
その思想そのものが「ツール依存である」ということにはお気付きでない?
また、ツール依存を非難しているつもりもありません。
僕もツール依存して生計を立てている者のひとりですしw
「なぜ、そのような設定をするのか」ということが根底にあっての間違い防止であってほしいですね、
という願いを込めたつもりです。
誰かが作ったものに便乗してテクニカルを論じている訳ですし。
ツールが正しく設定できるまでの道のり
を、なぜ、行う必要があるのか。ここに属人化防止を当てたいものです。
うまく伝われば幸いです。