開発記録
copy-and-own で取り込んだ UI コンポーネントは、誰が保守するのか
このサイトのローカルな Button、共通トークン、CLI 設定を例に、カスタマイズした差分、依存関係、更新範囲の管理を考えます。上流のバージョンだけでは、サイト全体の動作は保証できません。
- コンポーネント設計
- bejamas/ui
- 保守
- Astro
コンポーネントのソースコードをプロジェクトに取り込めば、角丸や属性の既定値は直接変更できます。ただ、次のバージョンが上流から提供されたときには、別の判断が必要です。どこまで置き換えてよいのか、どの部分がすでにこのサイト独自の動作になっているのかを見極めます。
このサイトでは、bejamas/ui の copy-and-own 方式を採用しています。CLI はコンポーネントを取得する入口であり、ページが実際に参照するのはプロジェクト内のソースコードです。Bejamas の CLI ドキュメントでも、追加先は alias で指定された場所と説明され、書き込み予定の内容を確認する --dry-run が用意されています。[1]
私は、こうしたコンポーネントをローカルのコードとして保守します。上流は引き続き有用な参照先ですが、更新するかどうかはバージョン番号だけで決めず、実際の利用箇所に照らして判断します。
カスタマイズしたコンポーネントには独自の契約が生まれます
このサイトの Button は as="a"、as="button"、as="summary" に対応しており、呼び出し側が用途に合わせて要素を選べます。as を指定しない場合は button を出力します。button を出力するときは type="button" を既定値にしますが、呼び出し側で type="submit" を明示すれば上書きできます。
これらの選択は、すでに各ページでの使い方に関わっています。作品カードの Demo へのリンクには as="a" と href を渡し、About ページのアバター拡大には操作用のボタンを使っています。見た目が同じ別のファイルで Button を置き換えるだけでは、こうした用途がすべて維持されたとは言えません。
Button は、このサイトの色とモーションのトークンも参照し、focus-visible と reduced-motion の分岐も残しています。これは、この記録で扱うローカル実装の一部です。上流のすべてのバージョンに備わる既定の仕様とは区別する必要があります。
保守のために残すべきなのは、後からたどれる差分です。どの既定値を変え、どの利用箇所がそれに依存し、出力をどう確認するのかを記録します。このサイトのドキュメントには、Button のローカルな type の振る舞いをすでに記載しています。今後その契約を変える場合も、会話の中だけで済ませず、説明を一緒に更新する必要があります。
CLI、コンポーネント、依存パッケージは別々の更新対象です
このサイトでは、Bejamas CLI を開発依存に追加し、独自に定義した ui:add script 経由で使っています。一方、ローカルの Button 実装は class-variance-authority と cn を参照し、cn はさらに clsx と tailwind-merge を使っています。
関係を簡略化すると、次のようになります。
CLI → ソースコードの取得または生成
ページ → ローカルの Button → variants と class 結合の補助関数
CLI を更新しても、すべてのローカルな Button が更新されるわけではありません。Button のファイルを手元に残していても、ほかのパッケージへの依存がなくなるわけではありません。この二つは分けて確認する必要があります。
この記録の構成では、manifest で CLI のバージョンを固定し、lockfile に依存関係の解決結果を保存しています。ただし、それだけでは、次にリモートから取得するコンポーネントの内容まで保証できません。ツールのバージョンが固定されていても、まず書き込み予定の内容を見てから、実際の差分を確認するほうが確実です。
最初の導入ノートでは、一つの Button をサイトに組み込む方法を扱いました。このノートでは、組み込んだ後、その差分を誰が継続して保守するのかを考えます。
更新前に、上書きしてはいけない部分を整理します
私はまず、ローカルで守る契約を短くまとめ、上流の候補版と比較します。以下は、このサイトの Button を例にした保守時の確認項目です。このノートのために実施したアップグレードの結果ではありません。
| 維持する責任 | 比較時に確認する箇所 |
|---|---|
| ナビゲーションが実際のリンクであること | as="a"、href、実際に出力されるタグ |
| 操作によって意図せずフォームを送信しないこと | ローカルの type="button" 既定値と明示的な上書き |
| 呼び出し側でスタイルを調整できること | variants、class、cn の結合結果 |
| 色の定義元を一か所に保つこと | コンポーネントが参照するセマンティックトークンと共通スタイル |
| キーボード操作と動きを減らす設定に対応すること | フォーカス表示、状態別スタイル、reduced-motion の分岐 |
共通コンポーネントの変更は複数のページに影響するため、デモページだけの比較では足りません。候補版で prop 名、slot、データ属性が変わる場合は、既存の利用箇所をたどり、それらのインターフェースで移動先、見た目、動作を設定していないか確認する必要があります。
必要な上流の修正は取り込めますが、ファイル全体を上書きする必要はありません。一部のロジックだけが必要なら、ローカルの契約を維持して変更範囲を絞るほうが、今回何を変えたのか説明しやすくなります。これは私の保守上の判断であり、あらゆるプロジェクトの更新コストを測定した結果ではありません。
ソースが独立して見えても、スタイルはサイト全体に影響します
このサイトでは、意味に基づく色の対応付けを共通 CSS に集約しています。Button は各ページで別々にカラーコードを書くのではなく、その名前を参照します。コンポーネントをサイトのデザインに合わせて調整できる一方で、トークンの変更が一つの Button を超えて影響する可能性もあります。
コンポーネントの追加時も同じです。.astro ファイルを一つ追加するつもりでも、実際の変更には補助関数、依存関係、グローバル CSS が含まれることがあります。そのため、このサイトの導入ルールでは CLI の書き込み内容を確認し、base reset を一式そのまま受け入れず、カスタマイズ済みのコンポーネントを無条件に上書きしないようにしています。
私は「このコンポーネントに必要なスタイル」と「今回、サイト全体のスタイルを変える必要があるか」を分けて考えます。前者は必須の依存関係かもしれませんが、後者には別途、理由と範囲が必要です。
新しい要件や必要な修正がなければ、上流に variant が一つ増えても、サイト側に追加する義務はありません。保守の目的は新しい選択肢をすべて取り込むことではなく、実際に使っている契約を維持することです。
確認項目は実際の使い方に合わせます
Button のタグ、属性、class の結合は、実際にレンダリングされた HTML から確認できます。ただし、フォーカスが分かりやすいか、タッチ操作がしやすいかは、該当する画面で確認する必要があります。どのページからも参照されていない variant は、サイト全体をビルドしてもレンダリングされるとは限りません。
インタラクティブなコンポーネントには、さらに別の責任があります。このサイトでは、作者紹介に Popover、記事の目次に Drawer を使っています。Button の出力を調べても、それらの開閉やフォーカス、閉じる際の動作を検証した代わりにはなりません。
更新を引き継ぐ際は、どのローカルな契約を変更し、どの利用箇所を確認し、どの操作や見た目が未確認なのかを明示する必要があります。それがあれば、次の保守担当者は何を続けて確認すべきか判断できます。「コンポーネントを更新しました」という一言だけでは、そこまで分かりません。
記録の範囲
このノートは、原稿で記録したサイトの Button、class 結合の補助関数、CLI 設定、共通トークン、各ページの利用箇所をもとにまとめています。更新時の比較表は保守の提案です。このノートのためにコンポーネントをアップグレードしたり、registry の内容を再取得したり、保守工数を測定したりしたわけではありません。既存のすべてのインタラクションが新たな受け入れ確認に合格したと主張するものでもありません。
copy-and-own は、コードを直接変更する余地を与える一方、ローカルな差分の管理をプロジェクトに残します。私が選ぶのは、説明できる契約を保ち、そのうえで上流のどの変更を採用するか決めることです。