検索/ブラウザ・サーバー・ネイティブで正しく動くCSS-in-JSライブラリを保守する

ブラウザ・サーバー・ネイティブで正しく動くCSS-in-JSライブラリを保守する

標準のCSSに沿ったCSS-in-JSライブラリの開発をAIに手伝わせる時に使う。スタイルを標準のCSSに限り、どのランタイムでも壊れない書き方、仕様を元に先にテストを書く進め方、リリースノートと資料の書き分け、本物のコードを動かすデモの作り方を決める。

このAIルールを使うには
## スタイルは標準のCSSだけを受け付け、設定したデフォルト値を優先する 記述するスタイルの表面を、実際に標準準拠したCSSと一致させる。これによりメンタルモデルが持ち運び可能なまま保たれ、APIが独自方言へと逸脱するのを防ぐ。 - 記述するスタイルでは、標準のプロパティ名・綴り・値のみを受け付ける。非標準のエイリアス(例:プラットフォーム専用propのkebab-case名)を作らない。プラットフォーム固有のスタイルキーは、テンプレート言語ではなく、明示的なエスケープハッチ(インラインstyleオブジェクト、または上流側のプラットフォームサポート)に置く。 - ネイティブレイヤーの役割は、慣用的なCSSを同等のプラットフォームプリミティブへマッピングし、ネイティブランタイムが追いつくまでできる範囲でポリフィルすることである。受け付ける入力文法を拡張するためのものではない。 ### 設定済みデフォルトの優先順位 - 設計上、著者側が設定した属性のデフォルト値は常に呼び出し元が渡したpropsより優先される。これは意図的な契約であり、バグではない。 - エスケープハッチはデフォルト設定の関数形式であり、渡ってきたpropsを受け取って条件付きでフォールバックできる(呼び出し元の値を維持する、またはデフォルト値を供給する)。 - 例外:呼び出し元が特定のpropに明示的に`undefined`を渡した場合、そのpropの設定済みデフォルトは抑制される。 ## 決められたパッケージマネージャーとコミットの書き方で作業する - 各セッションの開始時には、現在のコンシューマー向け機能サマリーを読み、記憶に残っている挙動ではなく今日サポートされている表面に対して作業する。 - プロジェクトで指定されたパッケージマネージャーと、それに紐づくコマンドを使う。別のものに差し替えない。 - 開発サーバーは自分で起動しない。必要な場合はメンテナーに起動を依頼する。 - コミットプレフィックスは Conventional Commits(`feat|fix|chore|refactor|test|docs|style|perf|build|ci`)を使う。メッセージは短くするのを基本とし、タイトルのみ、またはタイトルに簡潔な一文を添える程度にする。 - すべての文章(コメント、ドキュメント、テストタイトル、ユーザーに見える文字列)で、一貫した英語のスペリング規則を一つに統一する。仕様書や標準からの逐語引用の中でのみ、元の綴りを保持する。 ## サーバー・ブラウザ・ネイティブを見分け、ネイティブにDOMのコードを入れない 複数のランタイムに出荷されるコードは、単一の環境を前提としてはならない。複数の独立したシグナルから環境を検出し、互換性のないコードをエントリーポイントの境界で隔離する。 - 環境検出を単一の万能フラグに畳み込まない。少なくとも次の3つを区別する:ビルド時のサーバー定数(サーバービルドでのみtrue)、ランタイムのサーバーコンポーネント条件(一部のフレームワークはクライアント専用のcontext APIを持たないビルドを提供する)、SSRエントリーが設定するリクエスト単位のサーバーフラグ。それぞれの判定は、対象とするシナリオを捉えるのに必要な最小限のシグナルの部分集合だけにゲートする。よくある罠:ビルド時のサーバー定数だけでDOM処理を抑制すると、jsdom配下のサーバービルドのテスト経路(DOM変更が実際に発生する)が壊れる。DOMに関する判定はランタイム/SSRのシグナルでゲートすること。 - ネイティブのエントリーポイントは、value importを通じてDOM/ブラウザコードを推移的にインポートしてはならない。`instanceof`ではなく、型のみのインポートと、(共有シンボルレジストリによる)ブランド付きランタイムタグを使う。ビルドされたネイティブバンドル内に`document.`参照がゼロであることをgrepで検証する。一部のネイティブランタイムは、`document`参照が存在するだけでモジュール評価時に失敗する。 - 全ランタイムに存在するとは限らないフレームワークフック(例:サーバーコンポーネントでは未定義になるrefフック)は、デッドコード除去のためにビルド時のサーバー定数の裏側にゲートしなければならない。ランタイムの`typeof hook === 'function'`チェックでガードしてはならない。それは条件付きフック呼び出しになってしまう。 - 出力を生成する経路は正規のものを一つだけ保つ。出力を変更する際は、並行する経路を追加するのではなく、生成側とパーサー/コンパイラーのテストスイートを拡張する。 ## ビルド時の定数で不要なコードを除き、エントリーポイントの振り分けをテストで固める バンドラーがビルド時に除去できるものと、ランタイムにしか存在しないものを分離し、エントリーポイントのマッピングには意図を持って取り組む。 - ビルド時定数(サーバービルド、ネイティブビルド、ネイティブWebバリアント、開発モード)はリテラルに置き換えられ、バンドラーが到達不能な分岐をデッドコード除去できるようにする。開発専用の連続した領域は、その最も外側をひとつのdevガードでまとめて包むのが望ましい。warnヘルパーだけに処理のスキップを頼らず、メッセージ文字列や警告の引数をdevガードの外側で構築しないこと。 - `typeof window !== 'undefined'`のようなランタイムチェックはツリーシェイクできない。バンドルから物理的にコードを取り除く必要がある場合は、ランタイムチェックではなくビルド時定数を使う。 - エントリー/サブパスのマッピングは、サイレントな破壊の温床になりやすい。プラットフォーム拡張子による解決(ネイティブバンドラーにプラットフォームごとのファイルを選ばせる方式)は、一部のネイティブバンドラーが全プラットフォームに適用してしまう`browser`オブジェクトマップよりも安全である。後者はWebブリッジをネイティブへサイレントにルーティングしてしまうことがある。`.js`のフォールバックは安全なクロスプラットフォームのブリッジであるべきで、プラットフォーム固有のエンジンにしてはならない。これらの解決はresolution/tree-shakeテストで固定する。 - 複合プロジェクトで型解決を壊す場合は、条件付き`exports`よりも`browser`フィールドを優先する。公開するすべてのサブパスディレクトリを、パッケージの`files`リストに追加する。 ## 先に失敗するテストを書き、速さの改善は実際の使い方で測る - 厳格なテスト駆動開発がリポジトリ全体で強制される。本番コードを変更する前に、望む契約を表現する失敗するテストを追加または拡張し、それをグリーンにするのに十分な分だけ実装する。変更なしでは失敗するテストを伴わないユーザーに見える挙動は出荷しない。機能追加以外の作業でも同じred-greenの規律に従う。 - 最適化は必ず、単純化された最良ケースではなく現実的なワークロードに対してマイクロベンチマークする。単体では勝っていても、実際の呼び出し元が通る経路を悪化させる変更は元に戻す。 - 低メモリ圧とモノモーフィックな関数を目指して最適化する。 - 仕様に基づく挙動はすべて、要約や参考サイトではなく、また記憶にも頼らず、権威ある仕様書の現行エディタードラフトに根拠を置く。そのセッションのために該当セクションを再取得し、それを固定するテストの直上に規範となる条項をコメントとして逐語引用し、明確に名付けたspec-complianceブロック内でルールごとに1つのテストを書く。仕様が沈黙している、または曖昧な場合は、テストの隣に逸脱内容を文書化し、追跡issueへリンクする。これは仕様に触れるすべての変更に適用される。 ## 仕様のルールを1つずつ先にテストにしてから、ネイティブ向けのポリフィルを作る ネイティブランタイムのスタイルエンジンがCSS機能を理解できない場合はポリフィルするが、内部をリファクタリングする前に挙動を仕様に固定すること。 新規または見直し対象のポリフィルの手順(仕様に基づく検証ブロック全般にも適用): 1. そのセッション用に現行のエディタードラフトを取得する。条項番号や文言は変動するため、古い抜粋を権威あるものとして扱わない。 2. 仕様の各ルールを、その逐語テキストを直上に引用した上で、該当スイート内の名前付きspec-complianceブロック内の、ちょうど1つのテストへ翻訳する。 3. 先にテストを書き、現時点でどれが失敗するかを記録する。 4. 通るようになるまで実装する。合格しているテストを実装に合わせて作り直さないこと。仕様こそが真実の情報源である。 5. リグレッションがないか、ネイティブと本体の全スイートを実行する。 spec-complianceブロックがグリーンになれば、内部実装(トークナイズ、AST、評価器)は自由にリファクタリングしてよい。そのブロックがユーザーに見える挙動を固定してくれる。 - ランタイムのバージョンが出荷される、サポート状況が変わる、あるいはポリフィルが追加・削除・変更されるたびに、コンシューマー向けのネイティブ互換性台帳を常に最新に保つ。 - 実行可能なネイティブの回避策が存在しない機能は、問題となる構文の名前、実行できない理由、具体的な代替案を示した、安定したコードを持つ重複排除済みの警告を発し、意味不明なものを出荷するのではなく宣言をドロップする(nullへフォールバックする)。 - バリアントのパリティ:ブラウザが既にある機能を出荷している場合は、生のCSSをそのまま通す。ブラウザがまだ出荷していないプロパティを出力してはならない。ネイティブ専用のprop昇格やenum拡張は、ネイティブ専用のビルド定数の裏側にゲートする。各ポリフィルのspecブロックには、Webバリアントを切り替えてその可視出力を固定するパリティのサブスイートを設け、あるバリアントに適用されない規範的ルールは、逸脱コメントと同じ厳密さで明示的に免除する。 ## リリースノートには利用者に見える変化だけを書き、資料ごとに置き場所を分ける すべての成果物のトーンと内容を、その対象読者に合わせる。 - コンシューマー向けの文章(リリースノート、変更履歴のエントリ、PRの説明、公開ドキュメント)は、ソースコードを読んでいない人にも分かる言葉で、ユーザーから見える挙動だけを記述する。サポートされているものとされていないもの、そしてその変更自体の境界を明記し、無関係なギャップを列挙するのではなく互換性マトリクスにリンクする。内部APIの名前、仕組み、コードパスは書かない。それらはコミット本文とコントリビューターガイドに置く。 - 挙動は現在そうであるとおりに記述する。リリース済みのどのバージョンにも出荷されていない状態について、旅程を語るような言い回し(「以前は」「もはや〜ない」「現在は」「〜を置き換える」)は避ける。まだリリースされていない機能に対する修正は、その機能自身のリリースノートに折り込む。 - リリースノートはユーザーに見える変更(修正、新機能、破壊的変更)に対してのみ作成する。内部リファクタリング、ツール整備、テスト、同じ未リリースバージョンで導入されたコードに対する修正はスキップする。 - 自動生成ファイル(変更履歴、生成されたエラーマップ)は手で編集しない。それらのツールを通して再生成する。 - コントリビューター向けドキュメントでは特定のAIプロバイダー名を出さない。「AIコーディングアシスタント」のような中立的な表現を使う。 - PR本文、issue本文、コメントでは文章をハードラップしない。1行1段落なら折り返しは自動で行われるが、ハードラップはモバイル表示や引用表示で崩れて見える。コミットメッセージの本文は72桁の慣習を保ってよい。 ### ドキュメントの置き場所 - コントリビューターガイドには、ソースコードのコメントに自然な置き場所のない、横断的なルール・不変条件・経験知を置く。丁寧に説明された仕組みはここには置かない。 - 独立した知識ドキュメントは、落とし穴や経験知を記録するものであり、機能の仕組みを一から解説する場所ではない。仕組みについてはソースコードが権威を持つ。 - ソースレベルの「なぜ」は、コードの隣のコメント/JSDocに置く。本番のソースコードや警告メッセージの中で仕様の条文を再掲しない。アンカー、規範的な引用、根拠はそれを固定するテストに置く。警告メッセージは問題となる構文と具体的な代替案だけを名指しし、それ以外は書かない。 ## 測って確かめた速い書き方を使い、本物のコードが動くデモを作る 主要なJavaScriptエンジン上でマイクロベンチマークによって検証されたパターン(出発点となる仮説として扱い、文脈の中で再計測すること): - 文字列の`+=`連結は、あらゆる規模で`array.push()` + `join()`に勝る。 - `{...props, extra}`によるスプレッドは、`Object.assign`や`for..in`によるコピーに勝る。 - `for..in`は、`Object.keys()`にループを組み合わせるより勝る。 - コンパイル済みの正規表現はマップにキャッシュする。処理を丸ごとスキップするための`indexOf`による事前チェックの方が、さらに大きな効果を持つ。 - タイトなループでは、手動の`+`連結がテンプレートリテラルに勝る。 - (フレームワークの要素タグを使って)生の要素オブジェクトを構築する方が、フレームワークの要素ファクトリーよりはるかに高速である。 - `new Array(n)`はholey配列を生成し、型フィードバックを汚染する。ホットな配列の事前サイズ指定は避ける。 エンジンと言語の落とし穴: - `private`修飾子は、constに代入された匿名クラス式には使用できない。 - 型のみのstar importであっても、型システムがそれを取り除くにもかかわらず、バンドラーのモジュール解決をトリガーすることがある。 デモとビジュアルQAの誠実性(挙動を証明すると謳うショーケース全般に対して): - 偽のデモは一切許容しない。すべてのウィジェットは、それが実証すると謳っている実際のランタイム経路を実際に動かさなければならない。ある機能があるプラットフォームで動かせない場合は、そのセルを派手に失敗させる(レイアウト崩れ、警告、グリフの欠落)。その失敗自体がデモである。仕様を模倣するモックで代替してはならず、実際の経路が発火したことの代わりとして合否の色をハードコードしてもならない。 - ショーケースではプラットフォーム分岐のないオムニプラットフォームコンポーネントを使う。プラットフォームのばらつきに対する回避策はライブラリ側に置き、アプリ側には置かない。 - 動作しているものと壊れているものが、腕の長さの距離からでも一目でわかるように、マクロな差異(レイアウトのずれ、色の氾濫、有無)で表現し、すべてのウィジェットがライトモードとダークモードの両方で正しく読めるようにする。