styled-components で書かれたReactComponentPackageを CSS Modules に変更する
やる事 & 前提条件の説明
猫のLGTM画像共有サービス LGTMeow を運用しています。
UIのスタイリングは styled-components で作られていますが、CSS Modulesに置き換えます。
このサービスは去年デザイン変更をしています。その時の記事は下記になります。
上記の記事を見ると分かるのですが、色々な経緯でUIPackageとアプリケーション側が分かれているので今回主に変更するのは下記のUI側のpackageのほうになります。
やろうと思った経緯
Next.js 13.4から有効になった AppRouter では大きな変更点の1つとして Server Components が利用可能になっています。
しかし下記のドキュメントを見ると分かるように styled-components のようなJSランタイムを必要とするCSS in JSライブラリはServer Componentsで利用できません。
最初はゼロランタイムのCSS in JS ライブラリである vanilla-extract を利用する予定でしたが調査したところ、何とか動作はするけど、公式にサポートされていると名言されている訳ではないので一番無難なCSS Modulesを使う事にしました。
以下はその時の調査結果をまとめたスクラップです。
仕事でも同じ構成のpackageを運用しているので、まずは個人開発で運用しているpackageから試してみる事にしました。
これが終わったら LGTMeow も AppRouterへの移行を進めていく予定です。
最初の一歩
src/styles/mixins.ts という共有のCSSをまとめたファイルがあります。
これは styled-components の css 関数で作られているのですが、これと同じ役割を持つ src/styles/globals.css を作成しました。
Buttonのスタイリングがここに入っている事に違和感がありますが、このあたりは後で直します。
:root {
  --primary-color: #eb7c06;
  --primary-variant-color: #f0a14e;
  --variant-color: #ffd184;
  --sub-color: #f2ebdf;
  --sub-variant-color: #fffcf6;
  --text-color: #362e2b;
  --sub-text-color: #8e7e78;
  --white-color: #ffffff;
  --background-color: #faf9f7;
  --media-query-default-size: 767px;
}
.button-base {
  display: flex;
  flex: none;
  flex-direction: row;
  flex-grow: 0;
  gap: 10px;
  align-items: center;
  order: 1;
  width: fit-content;
  padding: 7px 20px;
  cursor: pointer;
  border-radius: 4px;
  box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
}
.button-base:hover {
  opacity: 0.8;
}
.button-text {
  flex: none;
  flex-grow: 0;
  order: 1;
  font-family: Roboto, sans-serif;
  font-size: 16px;
  font-style: normal;
  font-weight: 400;
  line-height: 16px;
  color: var(--sub-color);
}
.button-text:hover {
  opacity: 0.8;
}
このCSSを利用してまず最初にFooterのComponentを置き換える事にしました。
その時にやった対応は下記のPRの通りです。
typed-css-modules を使ってCSS Modulesの型定義を生成しています。これにより存在しないクラス名などを指定した際に気がつけるようになっています。
この状態でBuildを実施すると dist/style.css が生成されます。
しかし少し問題が発生しました。
- 
dist/style.cssにsrc/styles/globals.cssの内容が反映されていない - 
dist/style.cssが公開されていないので packageの利用側で以下のようにimportする必要があった 
import 'node_modules/@nekochans/lgtm-cat-ui/dist/globals.css';
import 'node_modules/@nekochans/lgtm-cat-ui/dist/style.css';
対策としてpackageのエントリーポイントである src/index.ts から src/styles/globals.css を読み込むようにしました。
これで dist/style.css に必要なCSSが全て反映されます。
さらに import '@nekochans/lgtm-cat-ui/style.css'; でCSSをimport出来るように package.json に設定を追加しました。この時の対応が下記のPRになります。
これでpackage利用側のアプリケーションの src/pages/_app.tsx に以下を追加する事で正常に今までと同じFooterのデザインが反映されるようになりました。
import '@nekochans/lgtm-cat-ui/style.css';
偶然見つけたのだが typed-css-modules をより便利にした happy-css-modules というpackageも存在するようだ。
作者は日本人の方で最近も更新が継続されているので、良さそうな気がした。
余裕があれば導入してみようかなと思う。
デザイン崩れをどうやって検知するか?
Chromatic というサービスを使ってStorybookをデプロイしています。
このサービスはStorybookを利用したビジュアルリグレッションテスト(VRT)も可能になっています。
これを利用してデザイン崩れが発生していない事を担保しています。
デザインに差分がある場合は以下のように差分が閲覧出来ます。

この仕組みのおかげでデグレを恐れずにデザイン変更が出来ますが、無料プランなのでビジュアルリグレッションテスト(VRT)の実行回数に上限があります。具体的には月に5000回のスナップショットを上限としていてそれを超えるとビジュアルリグレッションテスト(VRT)が出来なくなります。(Storybookのデプロイは可能です)
ビジュアルリグレッションテスト(VRT)がないとデザイン崩れを確認するのが辛いので、もしもスナップショットの上限に達してしまった場合は一時的に課金するか、Playwright +  reg-suit 等の別のツールでビジュアルリグレッションテスト(VRT)を実行する等の対応が必要になると思っています。
自分の場合は使える時間が限られているので一時的に課金する方向で乗り切ろうかと思っています。
全てやりきったがスナップショットの無料枠内である5000に収める事が出来た。

以下のドキュメントにもあるように onlyChanged: true を設定するとさらにスナップショットの消費量を抑えられそうなので後でやってみる。
忘れないようにissue化しておいた。
置換えを効率的に行う為に生成AIを利用する
Browse with Bingを有効にしたChatGPTを活用しています。
私は本格的にプロンプトエンジニアリングを本格的には学んでいませんが、十分に置換え対象のComponentを吐き出してくれます。
私はChatGPTに対して課金を行っているのでGPT4のモデルを使ったり、Browse with Bingを有効に出来ますが、おそらく無料プランでGPT3.5を利用していてもこのくらい単純な変更であれば問題なく行う事が出来ると思います。
HeaderComponentの置換え
UIで言うと以下の部分を置換えていく。

LanguageMenu の置換え
以下が対応時のPR。
若干リンクの箇所でデザイン崩れが発生したのでChatGPTが提案してきたコードをベースにしつつリファクタリングを行った。
と言ってもほとんど問題なく置き換える事が出来た。

LanguageButtonの置換え
こちらが対応時のPR。
こちらも問題なく置換えは出来たのだが、1つ新しい発見があった。
CSS変数はメディアクエリ内では利用出来ないらしい。
調べてみたがCSSの仕様で、メディアクエリはパース時に解釈され、変数は後で解決されるので参照出来ない事が理由らしい。
なので一旦ハードコードで対応した。
VRTでも検出されなかった理由はレスポンシブデザインだったから、Storybookの viewport を変更して初めて気がつく事が出来た。(当たり前だがVRTも万能ではない)
正しいデザイン

崩れたデザイン(メディアクエリ内でCSS変数が使えないのでブレークポイントが設定されておらず崩れている)

直接関係はないが この記事 を見てるとレスポンシブやる時は今後はコンテナクエリはを使ったほうが良いのか?等も思った。
このデザインはFigmaのデータをインスペクトして得られたCSSをベースにしているのでメディアクエリで実装している。(FigmaがメディアクエリでCSSを吐き出す為)
Figmaの設定をいじってコンテナクエリベースのCSSを吐き出すように変更出来るのか?とか一瞬頭によぎったが、脱線しそうなので今はやめておこうと思う。
あとFigmaが吐き出した物をベースにしているのもあって元々のマークアップの構造が微妙だったのでChatGPTに色々とアクセシビリティの事を聞いてみた。
対応したほうが良いとは思ったが本筋の目的からズレてしまうのでissue化して後ほど対応しようと思う。
GlobalMenuの置換え
以下の通り。特に問題はなかった。
Headerの置換え
これでHeaderを構成する全てのComponentの置換えが出来た。
Button系のComponentを置換え
以下が対応時のPR、特に難しい事はなかった。強いて言えばCSSのbackgroundイメージ画像を指定する際に  url(/src/components/Button/images/slash.png) のようにフルパスで指定する必要がある事くらい。
Error用のComponentの置換え
こちらは特に問題はなかった。
画像アップロード系のComponentの作成
UploadForm があまりにも大きいComponentなのでPRを2つに分割した。
ここまでChatGPTに置換えを依頼していたが、ここまで大きいComponentになるとChatGPTも一回でこちらが欲しいComponentを生成する事は難しく何度かやり取りが必要だった。
やはりComponentが大きいと変更に対して弱いので、時間がある時にリファクタリングが必要だと感じた。
とは言えChromaticのVRTのおかげで変更時の検証はかなり楽だと感じた。
LGTM画像を表示するComponentの置換え
以下の通り、特に難しい事はなかった。
残りのComponentを全て置換え
後は似たような作業が続くだけなので置換え、それぞれのPRのリンクを貼っておく。
これで全てのComponentの置換えが完了した。
Stylelint、ESLint の設定から styled-components に関連するルールを削除
Stylelintの設定変更
stylelintの設定 & package構成を変更し通常のCSSをターゲットにするように変更。
今までは styled-components を利用していたので https://github.com/kufu/stylelint-config-smarthr にお世話になっていたがこれは styled-components をベースにした物なので削除しました。(今までありがとうございます。)
ルールは極力シンプルにしたかったので stylelint-config-recess-order と stylelint-config-prettier だけを利用するようにした。
ESLintの設定変更
eslint-plugin-styled-components-varname を削除した。
以下が対応時のPR。整形結果に結構差分が出てしまったので、移行をするという意志決定をした時に早めに対応すれば良かったなと思った。
styled-components の削除
以下のPRを参照。
最初にCSS Modules の置き換えで不要になったファイルを削除、置き換え忘れのComponentがあったのでそれも一緒に置き換えてある。
置き換え忘れのComponentは一個だったので同じPRでやってしまったが複数あった場合はPRを分けたほうが良いと思う。
次に styled-components の削除、ドキュメントからも styled-components の記述を削除している。
リリース
スタイリングを CSS Modules に変更したバージョンのpackageをリリース。
アプリケーション側は最新のUI Package(@nekochans/lgtm-cat-ui)を利用するようにして styled-components への依存箇所を削除。
アプリケーション側にも少しだけ styled-components でスタイリングされていた部分があったのでそれもCSS Modules に置き換えている。
CSS Modules の型定義だが今回は happy-css-modules というpackageを利用した。
使ってみた感想としては単純に typed-css-modules の上位互換と言っても良いので今後はこちらを使おうと思う。
思ったより時間がかかってしまったがこれでCSS Modules への移行は完了。
今後はApp Routerへの移行を進めていく予定。