🎨

似て非なるCSS Module Scripts

2021/08/27に公開

昨年くらいからNext.jsの影響もあり、CSS Modulesの人気がまた高まってるように感じますね。State of CSSのCSS-in-JSランキングでも満足度、興味、利用率、認知度などどれをとっても1~2位に入ってました。

CSS Moduels自体はReactのJSXなどと同様、JSを拡張し利用できる技術です。しかし、このアプローチを有用と思った人たちが「JSやブラウザの仕様としてCSSをimportできたらいいのにね」と思いChromeに実装されつつあるのが「CSS Module Scripts」です。

仕様になったならいい話じゃん!で終わればいいんですが、なんとこの「CSS Module Scripts」、「CSS Modules」と仕様が違うんですね。現状まだwebpackでは「CSS Module Scripts」は未サポートですが、今後サポートするつもりっぽいのでこの2つの違いについて今後我々利用者は知っておく必要があるわけです。
ということで、本稿では執筆時点の情報をまとめておこうかと思います。

CSS Modulesおさらい

CSS ModulesはJSでCSSをimportして利用する技術です。
https://github.com/css-modules/css-modules

import styles from "./style.css";
// import { className } from "./style.css";

element.innerHTML = '<div class="' + styles.className + '">';

1行目でCSSをimportしていますね。これをbuildするとstyles.classNameはハッシュ化された一意なクラス名になっています。importは基本的にはJSに対して行うものですが、webpackのcss-loaderなどを利用することでCSSもimportできるようになります。逆に言うと、普通にJSの環境構築しただけではすぐには使えません。
Reactなどでよく使われてて、Next.jsがビルドインサポートしてたりもします(Vueでも使えますがややこしくなるので割愛)。

import React from 'react'
import styles from './index.css'

export const Title: React.FC = ({ children }) => (
  <h1 className={styles.title}>{children}</h1>
)

ちなみにこのCSS Modules自体はReact登場後割とすぐ登場した古の技術で、2015年くらいにはもう存在する技術でした。CSS-in-JSの人気ライブラリのstyled-componentsのv1は、おそらく2016年くらいで、後発的な技術です。

CSS Module Scripts

さて、本題のCSS Module Scriptsです。
https://web.dev/css-module-scripts/

import sheet from './styles.css' assert { type: 'css' };
document.adoptedStyleSheets = [sheet];
// web componentの場合
// shadowRoot.adoptedStyleSheets = [sheet];

CSS Modulesとの違いとしては以下らへんですね。

  • import時にファイル名の後にassert { type: 'css' }という構文が必要
  • defaultとしてimportした実態がCSSStyleSheetオブジェクト

assert構文の追加

import時に後ろの方にある見慣れないassert { type: 'css' }というのがありますが、これは最近JSに追加されたimport assertionと呼ばれる構文です。
元はCSSやJSONをimportしようっていうSynthetic Module(呼び方が古いかも?)と呼ばれる構想に対し、セキュリティ的な懸念などが払拭できず明示的にファイルタイプをassertする必要があるよねっということで追加された、いわばCSS Module Scriptsを見据えて追加された構文です。

幸か不幸か、これのおかげでCSS Modulesかどうかは判別できるのですが、CSS ModulesとCSS Module Scriptsで全く同じ構文で読み込むものが違ったりしたら相当ややこしかった気がしますね。。。

defaultのimportの違い

利用方法の違いからもわかる通り、CSSのimport時の実態からして異なります。
CSS Module ScriptsはCSSStyleSheetのインスタンスがdefault importされますが、CSS Modulesの方はTypescriptで言うとRecrod<string, string>になります。
つまり、assert付きでimportするとこれまで通りのようなCSS Modulesのような機能は実現できないんです。

import React from 'react'
import styles from './index.css' assert { type: 'css' }

export const Title: React.FC = ({ children }) => (
  // `styles.title` is undefined.
  <h1 className={styles.title}>{children}</h1>
)

これは非常にややこしく、背景とか仕組みを知らないと何が違うか直感的にはわかりづらいだろうし、「なんで仕様合わせなかったんだろう?」って思ってしまいますよね。
ただでさえJSXやTypescriptのように、拡張したJSを扱うのがデファクトという時代に仕様か拡張かみたいなのを意識する必要があるとなっても辛いところがあるなぁと思います。

webpack/css-loaderのスタンス

普段CSS Modules使ってる我々からするとこれが普段利用できるようになったタイミングで「こう言う風にかけるけどちょっと意味違うから使わないでね」という説明をするなり、lint作成するなりしないとややこしいなぁって思ってました。
というところで、このややこしい仕様違いの状況をwebpackとかどう考えてるだろう?と疑問に思ったので、issue立ててそもそもサポートするつもりあるのか聞いてみました。

https://github.com/webpack/webpack/issues/14063

結構すぐ回答もらえてまとめるとこんな感じでした。

  • サポートはしたいけどまだ実装予定まではない
  • css-loaderとしてはやはりassert { type: 'css' }があるなしでCSSStyleSheetを返すかどうか判断することになりそう
  • 何かしらcss-loader側で改善はしたい

筆者の提案:type: 'cssModules'の追加

css-loaderやCSS Modulesの第一人者のalexander-akaitが回答してくれてたので、「type: "cssModules"を追加して、これが指定されたらこれまで通りCSS Modulesとして振る舞うとかどうでしょう?」という提案をしてみました。

// CSSStyleSheet
import styles from './index.css' assert { type: 'css' }
// CSS Modules Record
import styles from './index.css' assert { type: 'cssModules' }

返事は「もうちょっと考えてみるよ」みたいな感じだったんで、あんま響かなかったのかもしれませんが、何かしら改善はしてくれそうな雰囲気だったので今後のアップデート気長に待とうかと思います。

まとめ

今すぐCSS Modules利用者に影響があるって話ではないですが、同じような名前で用途が異なるという結構ややこしい話でした。jxckさんもまとめてましたが、この段階に至っては全く同じ「CSS Modules」って呼ばれてましたし、今回の件はJS界隈をとりまく仕様と拡張の間の根深い問題の一端が表出したように感じました。

あと今回紹介したimport assertion、Typescriptがデファクトなこの時代にtype importと並びで書くと初見混乱するというか、わかりにくい気もしました。

import bar from './bar.json' assert { type: 'json' }
import type { foo } from "./foo"; 

今後もJSの「仕様」か「拡張」かを意識しなきゃいけないケースは、徐々に増えていくのかもしれませんね。

Discussion