CSS Modulesを型安全にする仕組み
CSS Modulesのつらみ
styled-componentsなどのCSS-in-JS系ライブラリからCSS Modulesに移行すると、クラス名の補完が効かないことでフラァストレーション⚡️を感じることはありませんか?
私はめちゃめちゃありました。
そこで私のチームではtyped-css-modulesというライブラリを使用して、
CSSファイルから自動でクラス名の型を生成し安寧を手に入れています。
そんな便利なtyped-css-modulesについてコードリーディングし仕組みを追っていこう!というのがこの記事の目的です。
typed-css-modulesの使い方
- src/
| myStyle.module.css
上記のようなファイル構成のプロジェクトがあり、myStyle.module.cssの中身は以下のようになっているとします。
.foo-bar {
margin-top: 10px;
}
typed-css-modulesをインストールした状態でtcm(typed-css-modulesの略)コマンドを実行します。
tcm src
実行後型ファイルが生成されます。
- src/
| myStyle.css
| myStyle.css.d.ts [生成された型ファイル]
declare const styles: {
readonly "foo-bar": string;
};
export = styles;
この型ファイルのおかげで、CSSファイルをstyles
という名前でdefault importすることで型による補完やチェックが効くようになります。
import styles from './myStyle.css'
export const Component: React.FC = () => {
return (
<div className={styles['foo-bar'] }> ...
//↑補完が効く!
);
};
存在しないクラスの参照ももちろん型エラーに!!最高!!
typed-css-modules内部で何が起こっているのか
型生成の仕組みを実現するために、下記の流れで処理を行っていきます。
- module.cssファイルを全て精査する
- module.cssファイル内にあるクラスを抽出する
- 抽出したクラス名を元に型ファイルを生成する
1. module.cssファイルを全て精査する
globでパターンに合うファイルを全て取得し、writeFileという関数に渡しています。
特に不思議な箇所はありません。
2. module.cssファイル内にあるクラスを抽出する
一番複雑な処理ですが、css-modules-loader-coreという外部パッケージの処理をコピーして実装されています。
css-modules-loader-coreの関数のインターフェースは次のとおりです。
core.load( sourceString , sourcePath , pathFetcher ) =>
Promise({ injectableSource, exportTokens })
このPromiseでラップされているexportTokens
の戻り値は以下のようになっています。
exportTokens {
'appForm-fieldLabel-title': '_applications_flow_src_features_AppForm_Canvas_Field_FieldLabel_FieldLabel_module__appForm-fieldLabel-title',
'appForm-fieldLabel-required': '_applications_flow_src_features_AppForm_Canvas_Field_FieldLabel_FieldLabel_module__appForm-fieldLabel-required'
}
このオブジェクトのキーを取得するとこでクラス名を抽出できることがわかります。
3. 抽出したクラス名を元に型ファイルを生成する
rawTokenList
はcss-modules-loader-coreから取得したクラス名の配列です。
これらをmapして
readonly "foo-bar": string;
といったresultを生成していきます。
stylesの型定義と先程生成したresultを並べていきます。
これで型定義ファイルの一丁あがりです🍜
declare const styles: {
readonly "foo-bar": string; // result
};
export = styles;
css-modules-loader-coreの仕組み
css-modules-loader-coreにメインの実装があります。以上!
…だとちょっと寂しい気もするので、css-modules-loader-coreについても軽くコードリーディングしてみたいと思います。
以下はcss-modules-loader-coreの記述です。
exportTokensにクラス名のキーが入っているのですが、postcssのプラグインを登録することでexportTokensを取得しています。
https://postcss.org/api/ でpostcssのインターフェースを確認すると、第一引数にAcceptedPlugin[]
を受け取れるようです。
コールバックの第一引数にcssのノードが格納されるので、ここからクラス名を取得できます。
まとめ
typed-css-modulesの内部ではcss-modules-loader-coreの機能を用いてクラス名を取得していました。
css-modules-loader-coreはpostcssのプラグインという形で実装されており、pluginのコールバックの第一引数からクラス名を取得しています。
もしクラス名を羅列・取得したい機会があれば同じ方法で実装できそうですね🏋️♀️
おまけ:型ファイルの削除について
実はtyped-css-modulesの仕組みだとwatchでcssファイルが削除されても型ファイルが残ってしまいます。(実はこれを解決したくてtyped-css-modulesのコードリーディングを始めました。)
typed-css-modulesのwatchの実装にはchokidarのchokidar.watch()
が使われているので、下記のようにファイルが削除された時用のhookを作り、紐づく型ファイルを削除することができます。
const watcher = chokidar.watch([filesPattern.replace(/\\/g, '/')]);
watcher.on('add', writeFile);
watcher.on('change', writeFile);
watcher.on('unlink', deleteFile);
PRを作ったので、もし全貌に興味のある方はみてください。
Discussion