Slide 1

Slide 1 text

TypeScript Language Service Plugin で CSS Modules の開発体験を改善する id:mizdra 2025/05/24 TSKaigi 2025 1

Slide 2

Slide 2 text

自己紹介 ● mizdra (みずどら) ● インターネット大好き ● 株式会社はてな ○ フロントエンドエキスパート ○ 社内のフロントエンド啓発活動 2

Slide 3

Slide 3 text

はてなサマーインターンシップ2025 3

Slide 4

Slide 4 text

4 今日のテーマ CSS Modules の 開発体験の改善

Slide 5

Slide 5 text

CSS Modules とは ● CSS をローカルスコープ化する仕組み ● CSS はグローバルスコープ ○ .foo { .. } はページ内全ての .foo に適用される ● CSS Modules を使うと... ○ 特定のコンポーネントの .foo にだけ適用できる 5

Slide 6

Slide 6 text

例: Article コンポーネント 6

Slide 7

Slide 7 text

ビルドすると、以下のように変換される 7

Slide 8

Slide 8 text

CSS Modules の欠点 ● エディタの言語機能が動作しない ● 言語機能: コーディングを補助する機能のこと ○ コードジャンプ ○ Find All References ○ Hover Info ○ … 8

Slide 9

Slide 9 text

動作しない言語機能の例 ● コードジャンプ ○ 型定義に飛んでしまう (※1) ● Find All Refefences ○ .css 側から実行すると... ○ .tsx が表示されない ● Rename ○ .tsx 側から実行すると... ○ .css 側が rename されない 9 ※1 declare module "*.module.css" {...} と書かれた型定義がプロジェクト内に 存在する時の話。

Slide 10

Slide 10 text

なぜ言語機能が動作しないのか ● それを理解するために... ○ 言語機能がどう実装されてるのか、知っておく必要がある 10

Slide 11

Slide 11 text

言語機能はどう実装されているのか ● いくつか実装方法はあるが... ○ Language Server Protocol に則った方法が主流 ● Language Server Protocol (LSP) ○ 2016 年に Microsoft が発表 ○ エディタに言語機能を実装するための標準仕様 11

Slide 12

Slide 12 text

LSP の基本コンセプト ● 『Language Server Protocol の仕様 及び実装方法 (*2)』より ○ ● エディタとは別に Language Server (LS) がある ○ これが言語機能を実装してる ● エディタは LS を介し、ユーザに言語機能を提供 12 LSPの基礎コンセプトは、IDEがサポートしていた各機能(言語サービスと 呼ばれます)を標準化し、エディタ本体から分離するというものです。 *2: https://zenn.dev/mtshiba/books/language_server_protocol/viewer/02_introduction より引用

Slide 13

Slide 13 text

LSP による通信の様子 (ざっくり) 13

Slide 14

Slide 14 text

LSP に従ってさえいれば動く 14 JavaScript の LS は どの LSP 対応エディタでも動く LSP 対応エディタは どの LS 搭載言語も扱える

Slide 15

Slide 15 text

通常 LS は言語ごとに分かれてる 15 .ts .css エディタは言語ごとに専用の LS と通信 TS の言語機能を実装 (※3) CSS の言語機能を実装 (※4) ※3 厳密には JavaScript の言語機能も提供 ※4 厳密には Less や Sass の言語機能も提供 tsserver vscode-css-languageserver

Slide 16

Slide 16 text

改めて: なぜ CSS Modules で 言語機能が動作しないのか ● TS の LS は TS 向けの言語機能しか実装してない ○ .css 向けの言語機能は未実装 ● クラスの Rename を行うと... ○ TS の LS は *.tsx の Rename だけ行う ● つまり言語ごとに LS が分かれているため... ○ TS/CSS 横断の言語機能を提供できない 16

Slide 17

Slide 17 text

この問題を解決するため... ● いくつかの補助ツールが作られてきた ● その代表例が「React CSS Modules」 17

Slide 18

Slide 18 text

React CSS modules ● VS Code 拡張 ● 拡張機能向けの API を用いて... ○ 主要な言語機能をサポート 18 リポジトリ: https://github.com/Viijay-Kr/react-ts-css

Slide 19

Slide 19 text

React CSS Modules を使えば... ● CSS Modules の欠点はほぼ解決する ● …のだけど不満もある 19

Slide 20

Slide 20 text

VS Code でしか動かない ● React CSS Modules が VS Code 拡張なので ● 「VS Code 使ってくれ!」で良いかもしれないが... ○ ツールの都合で VS Code 勧めたくない ● VS Code 以外でも動くツールが欲しい 20

Slide 21

Slide 21 text

21 という訳で そういうツールを作った

Slide 22

Slide 22 text

22 CSS Modules Kit

Slide 23

Slide 23 text

CSS Modules Kit ● CSS Modules のためのツールキット ● 主要な言語機能をサポート ● LSP をサポートするエディタで動く ○ VS Code, Zed は動作確認済み ○ NeoVim / Emacs は動作確認してないけど...動くはず 23

Slide 24

Slide 24 text

24 デモ

Slide 25

Slide 25 text

デモ: 紹介する言語機能 ● コードジャンプ ● Find All References ● Rename ● className prop の補完 ● クラスを追加するショートカット 25

Slide 26

Slide 26 text

デモ: className prop の補完 ● 通常 className と打って補完すると... ○ className="" と入力される ● ところで CSS Modules では... ○ className={styles.xxx} と書くことがほとんど ● そこで CSS Modules Kit では ○ className={} と入力するようカスタマイズしてる 26

Slide 27

Slide 27 text

デモ: クラスを追加するショートカット ● .css 側にないクラスを Quick Fix から追加できる 27

Slide 28

Slide 28 text

デモ: その他の言語機能 ● 他にも色々ある ● 詳しくは README 見てください ○ https://github.com/mizdra/css-modules-kit 28

Slide 29

Slide 29 text

29 デモおわり

Slide 30

Slide 30 text

CSS Modules Kit の構成 ● 3種類のツールから構成 ● codegen ○ *.module.css.d.ts を生成する CLI ツール ● ts-plugin ○ エディタに言語機能を提供するツール ● linter-plugin ○ CSS Modules 向けの linter rule を提供する plugin 30

Slide 31

Slide 31 text

CSS Modules Kit の構成 ● 3種類のツールから構成 ● codegen ○ *.module.css.d.ts を生成する CLI ツール ● ts-plugin ○ エディタに言語機能を提供するツール ● linter-plugin ○ CSS Modules 向けの linter rule を提供する plugin 31 これについて解説

Slide 32

Slide 32 text

ts-plugin の仕組み ● TypeScript Language Service Plugin を使ってる ○ TypeScript の LS (tsserver) の Plugin 機構 ○ LSP request を横取りし、response を書き換えられる ■ tsserver の挙動をカスタマイズできる ● これを使い... ○ tsserver が .css をサポートするよう、拡張してる 32

Slide 33

Slide 33 text

ts-plugin による拡張イメージ 33 .ts .css TS + CSS の言語機能を実装 (※3) CSS の言語機能を実装 (※4) tsserver vscode-css-languageserver .css ts-plugin

Slide 34

Slide 34 text

ts-plugin の実装 (エントリポイント) 34 ● Plugin の実体を作って、それを export ● Volar.js を使い、CSS をサポートする Plugin を定義

Slide 35

Slide 35 text

Volar.js ● 言語ツール (LS, Linter, …)を作るためのフレームワーク ○ Vue や Astro で使われてる ● 詳しくは Vue Fes Japan の発表資料見て! 35 https://speakerdeck.com/mizdra/vue-language-server-karasheng-mareta-volar- dot-js-to-soregami-meruke-neng-xing

Slide 36

Slide 36 text

Volar.js ● 非 TS ファイルを tsserver に読ませる機能がある ○ .vue とか .astro とか ● ts-plugin ではこれを利用し... ○ .css を tsserver に読み込ませてる 36

Slide 37

Slide 37 text

ts-plugin の実装 (再掲) 37 ① Language Service Plugin を定義する utility を import

Slide 38

Slide 38 text

ts-plugin の実装 (再掲) 38 ② utility で Language Service Plugin を定義

Slide 39

Slide 39 text

ts-plugin の実装 (再掲) 39 ③ CSSLanguagePlugin を挿入

Slide 40

Slide 40 text

CSSLanguagePlugin の実装 40 createVirtualCode が キモ

Slide 41

Slide 41 text

CSSLanguagePlugin の実装 41 ① .css => .d.ts に変換 ② .d.ts を Volar.js に返す

Slide 42

Slide 42 text

.css => .d.ts に変換 42 export される オブジェクトの 型定義へ変換

Slide 43

Slide 43 text

こうすることで... ● .module.css を TS コードに偽装させられる ● styles に型が付く 43

Slide 44

Slide 44 text

CSSLanguagePlugin の実体 44 mapping オブジェクトを生成

Slide 45

Slide 45 text

コードの mapping ● TS コードの偽装だけでは... ○ コードジャンプや Rename ができない ○ .module.css ⇔ .d.ts の対応関係が不明なため ● そこで、Volar.js にその対応関係を教えている 45

Slide 46

Slide 46 text

● 先ほどの .module.css/.d.ts に対し... ○ 以下の mapping オブジェクトを生成 46 mapping オブジェクトの生成

Slide 47

Slide 47 text

作ったオブジェクトを Volar.js に渡す 47 ここで渡す

Slide 48

Slide 48 text

mapping を渡すことで... ● Volar.js が対応関係を認識できる ● いくつかの言語機能が使えるように ○ コードジャンプ ○ Rename ○ Find All References ○ … 48

Slide 49

Slide 49 text

その他の言語機能の実装 ● Volar.js により自動で実装されるのは... ○ ナビゲーション系の言語機能だけ (コードジャンプとか) ● それ以外は自分でコードを書いて、実装してる ○ className={} の補完を例に解説 49

Slide 50

Slide 50 text

className={...} の補完の実装 50

Slide 51

Slide 51 text

className={...} の補完の実装 51 標準の handler を上書き

Slide 52

Slide 52 text

className={...} の補完の実装 52 "..." を {...} に置換

Slide 53

Slide 53 text

ts-plugin の注意点 ● ts-plugin は TS/CSS 横断の言語機能のみ実装 ○ コードジャンプ、Rename、Find All References ● CSS の標準的な言語機能は未実装 ○ プロパティの補完、CSS 構文エラーの表示、色のプレビュー ○ これは CSS の LS でしか実装されてない ● 良い開発体験を得るには... ○ TS/CSS 両方の LS を共存させないといけない 53

Slide 54

Slide 54 text

再掲: ts-plugin による拡張イメージ 54 .ts .css TS + CSS の言語機能を提供 (※3) CSS の言語機能を提供 (※4) tsserver vscode-css-languageserver .css ts-plugin 両方を .css の LS として使用してる ※3 厳密には JavaScript の言語機能も提供 ※4 厳密には Less や Sass の言語機能も提供

Slide 55

Slide 55 text

1ファイルに対する複数 LS の割り当て ● LSP の仕様では規定されてない ○ エディタが独自に実装してる ● (少なくとも) 主要なエディタは実装してる ○ NeoVim, Emacs, VS Code, Zed ● 実装してないエディタでは ts-plugin 動かないかも 55

Slide 56

Slide 56 text

56 開発体験を改善する その他の工夫

Slide 57

Slide 57 text

工夫1: tsc による型検査のサポート ● ts-plugin により styles に型が付くが... ○ ts-plugin は LS の挙動を変えるだけ ○ tsc では styles に型が付かない! ● そこで... ○ .module.css.d.ts をファイルに書き出すツールを提供 ■ codegen ○ これを併用することで、tsc でも厳密な型検査が可能 57

Slide 58

Slide 58 text

工夫2: 壊れかけのファイルのサポート ● エディタで編集中の CSS ファイルは不完全 ○ } や ; が抜けてたり ● 素朴に ts-plugin で扱おうとすると... ○ CSS をパースする段階でコケてしまう ● そこで... ○ postcss-safe-parser を使用 ■ 構文エラーがあってもパースを継続する ○ 壊れかけの CSS でも言語機能が提供できるように 58

Slide 59

Slide 59 text

工夫2: 壊れかけのファイルのサポート ● 常に構文エラーを無視して欲しい訳では無い ○ エディタで編集してない時は基本そう ○ ミスに素早く気づきたいので、むしろ報告してほしい ● そこで codegen では... ○ 普通のパーサ (postcss) を使用 59

Slide 60

Slide 60 text

工夫3: linter-plugin の提供 ● CSS Modules 向けの静的検査をしたくなる ○ 未使用のクラスの警告など ● そのためのツールも提供 ○ stylint-plugin / eslint-plugin ○ どっちも同じ rule を提供 60

Slide 61

Slide 61 text

61 CSS Modules Kit が 抱えている課題

Slide 62

Slide 62 text

課題1: ts-plugin のセットアップが面倒 ● ts-plugin のセットアップ方法がエディタごとに異なる ○ VS Code: 拡張機能を入れるだけでOK ○ 他のエディタ: 少し設定が必要 62

Slide 63

Slide 63 text

例: Zed におけるセットアップ 1. 「CSS Modules Kit」拡張機能をインストール ○ これで ts-plugin が tsserver に読み込まれるように 2. settings.json に下図の設定を追加 ○ .css の LS として vtsls (tsserver のラッパー) を登録 63

Slide 64

Slide 64 text

例: NeoVim におけるセットアップ 1. npm i -g @css-modules-kit/ts-plugin 2. init.lua に右の設定を追記 ○ plugin を tsserver にロードさせる ○ .css の LS として tsserver を登録 64

Slide 65

Slide 65 text

課題2: VS Code で .css から Rename できない ● .css から Rename すると... ○ .css 側は Rename されるが .tsx 側はされない ● TS/CSS の LS で Rename request が取り合いになってる ○ (VS Code では) CSS の LS が優先される模様 ○ ts-plugin に Rename request が届かないため... ■ 言語横断で Rename できない ● 今のところ良い解決策なし 65

Slide 66

Slide 66 text

66 今後の展望・まとめ

Slide 67

Slide 67 text

今後の展望 ● 対応エディタ増やす ○ リクエストあれば教えて! ● 便利な言語機能を追加する ○ ホバーで CSS のソースを表示したい ○ AI でなにか ● tsgo どうしよう ○ Language Service Plugin は .vue / .astro でも使われてる ○ 全く動かないことはないだろうと思ってる 67

Slide 68

Slide 68 text

まとめ ● CSS Modules の言語機能が動作しない問題 ○ VS Code 拡張で解決できるが、VS Code でしか動かない ● そこで CSS Modules Kit 作った ○ 多くのエディタに対応するため Language Service Plugin を使用 ○ Volar.js を使って、.css を tsserver に読み込ませる ● いくつか課題はあるものの... ○ 開発体験を改善する豊富な言語機能が使えるように ● 是非使ってみてください 68