css-modules-kit は CSS Modules のためのツールセットです。何ができるのか、どんな設計で作られているのかは以下の記事を見てください。
この記事では css-modules-kit の内部設計について紹介してみます。今回は CSS Modules のパースについてです。
3種類のツールと core パッケージについて
css-modules-kit は codegen, ts-plugin, linter-plugin (eslint-plugin or stylelint-plugin) の 3 種類のツールから構成される。それぞれ独立したパッケージになってるのだけど、コア部分は core に切り出されてて、3 種類のツールから依存されてる。
.module.css のパーサー
core には色々なロジックや utility が置かれているのだけど、その中に .module.css のパーサーがある。packages/core/src/parser にそのコードがある。
内部では postcss で AST に変換して、それを走査して必要な情報を集めて返してる。一般的に「パーサー」というと AST を返すものをイメージするけど、それとはちょっと違う。css-modules-kit においては AST から余計な情報を削ぎ落としたデータ構造である CSSModule をパーサーから返してる。
CSSModule の構造は packages/core/src/type.ts で定義されてる。
// https://github.com/mizdra/css-modules-kit/blob/89f11a54af1dc86b344e151b63bc2708486c31bb/packages/core/src/type.ts#L98-L121 export interface CSSModule { fileName: string; text: string; /** `.foo {}` や `@keyframes bar {}` などで定義されるトークンの情報 */ localTokens: Token[]; /** * `@import './other.module.css';` や `@value val from './other.module.css'` などを使った * トークンの import 文の情報。今回の説明は省略。 */ tokenImporters: TokenImporter[]; /** パース時に検出された構文エラーなど。今回の説明は省略。 */ diagnostics: DiagnosticWithLocation[]; } export interface Token { name: string; loc: Location; declarationLoc?: Location; } export interface Location { start: Position; end: Position; } export interface Position { line: number; column: number; offset: number; }
「Token」という謎の用語があるけど、これは css-modules-kit の内部用語で、.module.css から export されるアイテムを表してる。クラス名とか @keyframes で定義されたキーフレームの名前とか、@value val: #123; による変数とか。export されるのはクラス名だけではないので、「Token」という用語を導入している。
それを踏まえて型を見てもらうと分かるが、トークンの名前や位置情報、構文エラーなどの情報が入ってる。それ以外の情報、例えばプロパティ に関するものは全く持たない。
独自のデータ構造を返している理由
生の postcss の AST を返さずに、独自のデータ構造を返しているのには色々な理由がある。
理由1: CSS Modules として .tsx 側に export されるもの以外の情報が不要だから
単に不要なものは持たないほうが、シンプルになって codegen などから扱いやすい。それはそう。
理由2: postcss に依存しないようにしたかったから
postcss に非依存にすることで、postcss 以外の CSS パーサーに後から差し替え可能にしたかった。
というのも今主要な CSS パーサーには 「postcss」「csstree」「lightningcss」の 3 つがある。css-modules-kit を最初に作るときにどれを使うか悩んだのだけど、結局 postcss を採用した。postcss が優れているからとかではなくて、単に
id:mizdra が以前作った happy-css-modules というツールの内部で postcss で CSS Modules をパースしていて、そのコードを参考に実装できたから。
postcss はそこそこ枯れていて良いのだけど、まあまあ微妙なところがある。例えばパフォーマンスや後方互換性を理由にプロパティの value やセレクター部分のパースが本体ではサポートされてなくて、別のライブラリに頼らないといけない。
- プロパティの value 部分のパーサー
- https://www.npmjs.com/package/postcss-value-parser
- 依存がゼロだけど 4 年前から更新が止まってる
- 古すぎて心配だけど、特にバグにも遭遇しないので css-modules-kit ではこれを使ってる
- https://www.npmjs.com/package/postcss-values-parser
- 最近も更新されている
- 依存に husky などが入ってて心配
- https://www.npmjs.com/package/postcss-value-parser
- セレクター部分のパーサー
- https://www.npmjs.com/package/postcss-selector-parser
- これ一択。css-modules-kit でもこれ使ってる。
- postcss 公式が publish してる
- postcss の Node の位置情報と postcss-selector-parser が返す Node の位置情報を足し合わせないと、ソースコード上での Node の位置情報が正しく取得できないのが面倒だけど、それ以外は特に不満ない
- https://www.npmjs.com/package/postcss-selector-parser
ここ最近のサプライチェーン攻撃の流行を考えるとあんまり依存関係増やしたくはなくて、困ってる。csstree なら value 部分もセレクターもそれだけでパースできるみたいなので、その点を考えると csstree のほうが良いなーと思ってる。
lightningcss は Rust 製で速くて良いと思うけど、パフォーマンスを理由にパーサーの API が Node.js 向けに提供されてない。Rust 向けにだけ提供されてる。もし css-modules-kit で使うなら、AST にして CSSModule 型のデータを生成するところまでを Rust で書いて、それを Node.js に転送する、みたいな実装をしないといけない。やったらできるけど面倒だし、Rust <=> Node.js 間のデータ転送にコストが掛かるので、速くなるのかよくわからない。
postcss から csstree なり lightningcss なり乗り換えるとしても、ちゃんと互換性を維持したまま乗り換えられるかは正直不透明だなと思う。css-modules-kit が欲しい情報が、AST に含まれていないと安全に移行できないと思う。クラス名やキーフレーム名の位置情報を正確に取得できるのかとかね。postcss も最初は一部情報が欠けてて、
id:mizdra が PR 送って直していたりしたくらいなので、csstree やlightningcss もきっと同じようなことになるだろう。とにかく面倒な予感がする。
- Add `CssSyntaxError.offset` and `CssSyntaxError.endOffset` · Issue #2022 · postcss/postcss · GitHub
- Calling `Node#rangeBy()`/`Node#positionBy()` with no arguments throws an error · Issue #2028 · postcss/postcss · GitHub
- Missing `offset` property of `Node.Position` in some cases · Issue #2029 · postcss/postcss · GitHub
とまあパーサーを差し替えたい気持ちはあるけど、色々な理由があって進んでない。けど将来的に差し替えられるように、データ構造は工夫してる。
typescript-go が来たら、それを受けて css-modules-kit も Go にしましょうみたいな機運が出てくるかもなとは思ってて、そうしたら lightningcss なり検討するかも。まあまだ先の話だなと思う。