honey-css-modules/モチベーション
happy-css-modules のイマイチなところを改善したものを作りたい。要は次世代版 happy-css-modules。
happy-css-modules の問題点
1.*.module.cssを移動した時に、*.tsxに書かれている import 文のパスが自動的に書き換わらない
例えば以下のようにsrc/Button.tsxからsrc/Button.module.cssを import していた場合...
code:src/Button.tsx
import styles from './Button.module.css';
export function Button() {
return <button className={styles.button}>Click me!</button>
}
src/Button.module.cssをsrc/components/Button.module.cssに移動したら、src/Button.tsxは以下のように自動的に書き換わって欲しい
code:src/Button.tsx
import styles from './components/Button.module.css';
// ...
しかし、現状このように書き換わらない
mizdra.icon 当然書き換わって欲しい
2. token (.class_name { ... }や@value red: #FF0000など、*.module.cssから export されるものの総称) の rename が期待通りに機能しない
VS Code で*.module.css内に書かれた token を rename しても、*.tsx側まで変更してくれない
その逆も然り
mizdra.icon 理想的には rename できて欲しい
3. VS Code の Find all references が期待通りに機能しない
*.module.css上で token を Find all references したら、*.tsx から参照されている場所を表示してほしいが...
現状できない
mizdra.icon 理想的には Find all references できてほしい
4. src/ディレクトリ配下に*.module.css.d.ts/*.module.css.d.ts.mapが生成されて邪魔
VS Code であればfiles.excludeで非表示にできるが...全てのエディタで同様のことができる訳ではない
エディタ上で非表示にできても、ファイルシステム上には引き続き存在していて、煩わしい
しかし現状オプトインになってる
多くのユーザは煩わしい状態が続いている
mizdra.icon --outDirを必須としたい
5. CSS Variables の export に対応していない
lightningcss では*.module.cssから CSS Variables を export できる (dashedIdents: trueでオプトイン)
しかし happy-css-modules が出力する型定義ファイルには、CSS Variables が含まれていない
補足: Next.js ではdashedIdents: trueを設定せずに lightningcss を使っている
そのため、現状ではそこまで困っているユーザは居ないはず
mizdra.icon CSS Variables の export に対応したい
6. Sass/Less/Postcss によるトランスパイルが辛い
Sass/Less では.box { color: red; &_inner { color: blue; } }と書くと、.box { color: red; } .box .box_inner { color: blue; }にトランスパイルされる
その結果、token レベルではboxとbox_innerが export される
Postcss も plugin で似たようなことができる
export される token を決定するには、一度トランスパイルしないといけない
そのため、happy-css-modules 内部では Sass/Less/Postcss によるトランスパイルを行っている
トランスパイルをした後に、export される token を抽出したり、その token のソースコード上での位置情報を計算してる
token の位置情報の計算がトランスパイル後のコードに対して行われるため、code jump がおかしない挙動になる
本当は7行目だけに jump すべきだけど、10行目も jump 先の候補になってしまっている
https://gyazo.com/3381cbf48b75efc70ab3138087ab8f27
これは 10 行目が.a_2 .a_2_1 { dummy: '' }へとトランスパイルされ、.a_2が出現してしまうため
mizdra.icon トランスパイルやめたい
7. --localsConvention辛い
これがあるせいで、.module.css側では.foo_barだけど、*.tsx側では.fooBarというケースが出てくる
rename 機能を実装する際の足かせになっている
mizdra.icon --localsConventionやめたい
解決策
問題1〜3 については、tsserver が*.module.cssを認識していないがために発生している。tsserver はtsconfig.jsonのincludeオプションなどで指定されたファイルを、プロジェクトを構成するファイルとして認識する。しかし通常それらのファイルは*.{ts,tsx,cts,mts}に制限されていて、.module.cssは含まれない。つまり tsserver は*.module.css.d.tsを認識するが、*.module.cssは認識しない。そのため、*.module.css.d.tsを移動したときは import 文のパスが自動的に書き換わるが、*.module.cssを移動したときは書き換わらない。
ところで Vue.js では、TypeScript Language Service Plugin で tssserver の振る舞いをカスタマイズしている。これにより、tsserver が*.vueを認識できるようになり、問題1〜3を回避できるようになっている。
honey-css-modules でもVue.js と同じアプローチを採用したい。Volar.js 使ったらできるはず。 また、問題4〜7 を解決するために、以下のような設計にしたい。
--outDirを必須とする
これで CSS Variables の export に対応できるはず
トランスパイルやめる
Parent Selector (&) の展開は諦める
Sass/Less もサポートしないことにして、CSS だけサポートする
Parent Selector 的なことは CSS Nesting で代用してもらう
--localsConvention廃止
rename 機能を実現するためにも、廃止しておく
細かい設計方針
大方針は決まったので、細かいところを詰めていく。
@value廃止する
CSS Modules で定義されている変数構文のこと
そもそも lightningcss がサポートしていない
CSS Variables を代わりに使ってくれという世界観
The @value rule – superseded by standard CSS variables.
CSS Variables では代替できない場面もあるので微妙なところだと思うけど...
lightningcss がサポートしていない以上、実装が難しい
honey-css-modules でもサポートしないことにしたい
*.module.css.d.ts.mapの生成やめたい
Volar.js/TypeScript Language Service Plugin で実装するのだから、生成しなくてもコードジャンプは実現可能なはず
生成しないようにしてみる
@importのサポートどうするか
@importを使うと、別のスタイルシートの中身を展開できる
code:src/a.module.css
@import './b.module.css';
.a {}
code:src/b.module.css
.b {}
↓
code:dist/a.module.css
.a {}
.b {}
code:dist/a.module.css.d.ts
export default const styles:
& { a: string }
& Pick<(typeof import('./b.module.css'))'default', 'b'>; よく使っているのを見かけるけど、実装がかなり大変
Pick<(typeof import('./b.module.css'))['default'], 'b'>を生成するために、b.module.cssからexportされるtoken一覧を解析しないといけない
a.module.css.d.tsの生成のために、b.module.cssの解析が必要
ファイルごとに独立して処理するのが難しくなる
どうするか
Pick<(typeof import('@/b.module.css'))['default'], 'b'>ではなく、(typeof import('@/b.module.css'))['default']にしてみては?
b.module.cssから export される token 全てが欲しいので、それで十分なはず
つまりこう
code:src/a.module.css
@import './b.module.css';
.a {}
code:src/b.module.css
.b {}
↓
code:dist/a.module.css
.a {}
.b {}
code:dist/a.module.css.d.ts
export default const styles:
& { a: string }
& (typeof import('./b.module.css'))'default'; tsconfig.json の paths オプションに"@/*": ["./src/*"]設定されていれば、型定義ファイルをちゃんと処理できるはず
mizdra.icon だいぶシンプルになって良いのでは!
mizdra.icon happy-css-modules でも(typeof import('./b.module.css'))['default']にするのを検討して、何かしらの理由でやめた記憶があるのだけど、どういう理由だったかなー...
思い出せない
@import '...'の...の部分の resolve どうする?
Vite や Webpack では、@import '...'の...の部分には様々な形式の文字列が書ける
code:import-specifiers.module.css
@import './a.module.css';
/* './a.module.css' に resolve される */
@import 'a.module.css';
/*
* Vite で resolve.alias オプションに { '@': './src' } を設定していた場合、
* ./src/a.module.css に resolve される。
*/
@import '@/a.module.css';
/*
* node_modules/bootstrap/package.json に書いてある内容をもとに、
* 解決先が決まる。様々なルールがあるが、bootstrap の場合は
* package.json の style field に書かれている
* node_modules/bootstrap/dist/css/bootstrap.css へと resolve される。
*/
@import '~bootstrap';
結構面倒な処理なので、できれば honey-css-modules 実装したくないが...
実装しておかないと、@import '@/a.module.css'で import しているファイルが外部ファイルかどうか判定できなくなると思う
happy-css-modules では外部ファイルを @import している場合、生成する型定義ファイルをちょっと変えている
この処理の出し分けをするためにも、resolve はしておいたほうが良いと思う
*.tsx側から新しい token を簡単に定義できるようにしたい
これを honey-css-modules 向けに実装したい
stylelint/biome 向けに未使用の token を検知する linter rule を用意したい
やったらできるんじゃないか...?
型定義ファイルを生成する部分が形になったら考えたら良い
エディタで1.module.cssに未保存の変更がある時に、import styles from './1.module.css'のstylesの型が古くなる問題を何とかしたい
https://scrapbox.io/files/6733538d80804f6ecd8fbac6.mov
Language Server はファイルシステム上に出力された型定義ファイルを見るのではなくて、編集中の1.module.cssの内容をもとに型定義ファイルをインメモリで生成して、それを使って型検査すれば、解決できるはず
オンデマンドで1.module.cssの解析をしないければならないので、Language Server の実行コストが若干高まるのがデメリット
まあ大したことないんじゃないか
逆にこの問題に対処することで、tscによる型検査とエディタ上の型検査の結果が異なるトラブルも発生する
この問題を解決すべきか結構悩ましい
mizdra.icon まあ一旦 Language Server は型定義ファイルをインメモリで生成する作りにしてみる
hcmコマンドに--watchオプションを実装しない
コード書いているときはLanguage Server が型定義ファイルをインメモリで生成するから、hcm --watchで都度ファイルシステムに書き出さなくても良いはず
--watchオプション要らないはず
依存関係を小さくしたい
glob => fs.glob
minimatch => path.matchesGlob
chokidar => --watchオプション削除により、そもそも chokidar 相当のパッケージが不要になったはず
chalk => util.styleText
enhanced-resolve => oxc-resolver
postcss => lightningcss
Rust で書かない
Rust 使う必要性を感じない
むしろ Volar.js を使うために、JavaScript で書かなければいけない
次はコードのインターフェイスを考える。