@tabula-css/merge
Tabula のランタイムマージ: cn()、resolve()、dyn()。レジストリのみを読み込みます — インポートグラフに Tailwind は含まれません — プロファイルの数学的な核です。
インストール
@tabula-css/merge はランタイム依存であり、ルート README のクイックスタートの一部です。
npm install @tabula-css/merge @tabula-css/preset概要
@tabula-css/registry が生成するレジストリが与えられると、cn() は競合する 2 つのユーティリティクラスのうちどちらが勝つかを決定論的に決定します — ヒューリスティクスはなく、ランタイムで Tailwind に問い合わせることもありません。コンセプト § マージ:カスケードのシミュレーションではなく、全順序 が説明しているとおり、マージはブラウザのカスケード(詳細度、ソース順、!important)を決してシミュレートしません。レジストリの凍結された rank によって順序付けられた、各クラスの正規スロットに対する純粋な畳み込みを実行します。resolve() は同じ畳み込みを実行して、1 つの要素についての完全で検査可能なスタイリングモデルを生成します — これは、エージェント(あるいはテスト)がブラウザが実際に何をレンダリングするかを正確に予測するために実行できるアルゴリズムです。dyn() は、トークンパイプラインが決してクラスに焼き込むことができない、真に動的な値(プログレスバーの幅など)のための唯一の許可されたエスケープハッチです。
これらの関数のいずれかを呼び出す前に、ランタイムは生成されたレジストリ(通常は tabula build が書き出す .tabula/registry.json)で一度だけ設定されている必要があります。
エクスポート
設定
export function configure(input: string | RegistryFile): RegistryReader;
export function configureReader(reader: RegistryReader): RegistryReader;
export function getRegistry(): RegistryReader; // throws MissingRegistryError if unconfigured
export function isConfigured(): boolean;
export function reset(): void; // test isolation onlyconfigure() は readRegistry() が受け付けるものとまったく同じもの — パース済みの RegistryFile、JSON 文字列、またはファイルシステムパス — を受け付け、レジストリのスキーマバージョンがこのビルドの @tabula-css/merge が理解するものと一致しない場合は RegistryReadError(TAB-E302 診断を伴う)を投げます。アプリケーション起動時に一度だけ呼び出してください。
import { configure } from '@tabula-css/merge';
import registry from '../.tabula/registry.json';
configure(registry);このパッケージの他のすべてのエクスポートは、最後に設定されたレジストリを読み取ります。configure() より前に cn()/resolve()/dyn() を呼び出すと MissingRegistryError(TAB-E303)が投げられます。
cn()
export const MAX_CLASSES: number; // 512
export function cn(...inputs: ClassValue[]): string;
export function clearOpaqueWarnings(): void; // test isolationマージ関数です。inputs は clsx スタイルの値を受け付けます — 文字列、数値、null/undefined/false(スキップされます)、ネストした配列、そして truthy なキーがその(空白区切りの)クラス名を寄与するオブジェクトです。cn() はすべてのフラグメントをフラット化しつつ、各候補の元となったトップレベルフラグメントのインデックスを保持します。(pseudoElement, condition, slot) の各キーについて、それを所有するのはより後のフラグメントを持つクラスであり、1 つのフラグメント内でのみレジストリのより高いランクが勝ちます。これにより cn(base, className) は健全なオーバーライドの仕組みになります: 呼び出し元の className フラグメントは、自身の内部ランクに関わらず、そのスロットで常に勝ちます。
import { cn } from '@tabula-css/merge';
cn('p-md', 'pt-sm'); // → "p-md pt-sm" (later fragment overrides just padding-top)
cn('pt-sm', 'p-md'); // → "p-md" (p-md is later AND covers padding-top; pt-sm owns nothing)
cn('bg-surface', className); // → the caller's className fragment always wins its slotsバリアントチェーン(hover:bg-accent-hover、sm:hover:p-md)が登録済みとみなされるのは、その正規の昇順ランクのチェーンプレフィックス × ベースユーティリティのファミリーが、宣言済みのバリアントプロダクトである場合、あるいはその厳密なクラスがチェーン例外である場合だけです。cn() は、それをレジストリのリーダーの hasChain()/hasChainClass() アクセサーを通じて読み取ります:登録済みの集合の外にあるチェーンは CSS を一切生成しないため(プリセットは宣言済みのプロダクトに対してしかルールを生成しません)、cn() はそれを他の未知のクラスとまったく同様に扱います――静かに通過させるのではなく、下記の TAB-E300 の経路をたどります。パラメトリックバリアント(group-*/peer-*/aria-*/data-*)は閉包の対象範囲外であり、ここではゲートされません。
cn() は**本番環境ではすべての入力に対して全域的(total)**です: 未登録のクラス(TAB-E300。未宣言のバリアントチェーンを含みます)は、一度だけの console.error を伴って opaque なまま素通りし、サイズ超過の候補リスト(TAB-E305、MAX_CLASSES を超えるもの)もそのままマージされます。開発環境では、どちらの場合も代わりに例外を投げます — UnknownClassError または MaxClassesError — さらにマージ健全性の自己チェック(T2)も行われます: cn() は各スロットの勝者をランクから独立に再計算し、レジストリの宣言された順序がそれと食い違う場合は MergeSoundnessError(TAB-E301)を投げます。これはレジストリ自体がその不変条件に違反していることを意味します。非有限のランクに解決されるクラスは、両方のモードで RankIntegrityError を投げます — 通常の未登録クラスとは異なり、これには正しいフォールバックが存在しないためです。clearOpaqueWarnings() は、プロセスごとに 1 回だけの警告管理をリセットします(アプリケーションコードではなくテストスイートで使用されます)。
resolve()
export function resolve(classString: string, axes?: AxisState, vars?: DynamicVars): ResolvedStyle;
export function resolveAxisValue(value: unknown, axes: AxisState): string;resolve() は cn() と同じ順序非依存の畳み込みを実行しますが、単一の クラス文字列に対して、与えられた軸の状態(例: { theme: 'dark' })で実行し、マージされた文字列ではなく完全に検査可能なモデルを返します — カスケードのシミュレーションも DOM もありません。cn() とは異なり、未知のクラスは例外を投げられるのではなく 報告 されます: resolve() は、ソースとレジストリの間のドリフトも含めて、クラス文字列が実際に含むものをそのままモデル化することを意図しています。
export interface ResolvedStyle {
readonly base: Readonly<Record<string, string>>;
readonly conditions: readonly ResolvedCondition[]; // media / self / group / peer, by condition rank
readonly ambient: Readonly<Record<string, { readonly value: string; readonly source: string }>>;
readonly atomic: readonly ResolvedAtomic[];
readonly dependencies: readonly ResolvedDependency[]; // declared group/peer/container dependencies
readonly unknown: readonly string[];
readonly warnings: readonly Diagnostic[]; // e.g. TAB-W401 for a registered exception
}import { resolve } from '@tabula-css/merge';
resolve('bg-surface hover:bg-surface-raised', { theme: 'dark' });
// → { base: { 'background-color': '#0b0b0c' },
// conditions: [{ condition: 'hover', when: '&:where(:hover)', declarations: { 'background-color': '#1a1a1c' } }],
// … }3 番目の任意引数 vars は、コンポーネントのライブな dyn() の寄与をモデル化します — DynamicVars レコードが、インラインスタイルの優先度(クラスで設定されたカスタムプロパティより上、レジストリの initialValue より上)で置換され、これはインラインスタイルがカスケード内で占める位置と正確に一致します。これは dyn() 自身によって検証されるため、無効な var は dyn() が投げるのと同じエラーを投げます。省略した場合、結果はすべて動的な値なしで resolve() を呼び出した場合とバイト単位で同一のままです。resolveAxisValue() は、1 つの軸マップされた(またはリテラルの)値を AxisState に対して解決する、より低レベルなヘルパーです — 独自の部分モデルを構築する呼び出し元向けに公開されています。
dyn()
export type DynamicVars = Readonly<Record<`--d-${string}`, string>>;
export const MAX_DYNAMIC_VALUE_LENGTH: number; // 256
export function dyn(vars: DynamicVars): Record<string, string>;許可されたインラインスタイルのエスケープハッチです(Draft A §4.4)。dyn() は、すべてのキーをレジストリの --d-* 動的カスタムプロパティに照らして検証し(未登録のキーは開発環境では UnregisteredDynamicPropertyError/TAB-E304 を投げ、本番環境では一度だけの警告とともに破棄されます)、すべての値を境界のある形式フィルターに照らして検証します: 文字列でなければならず、MAX_DYNAMIC_VALUE_LENGTH を超えず、; { } " ' < > & \ @ のいずれも、また制御文字も含んではいけません — これらは CSS 宣言を終端させたり style="…" 属性からエスケープしたりし得る句読点です。拒否された値は、開発環境では InvalidDynamicValueError/TAB-E306 を投げ、本番環境では破棄されます。クラス文字列自体は静的で登録済みのまま(例: w-progress)です。dyn() は、そのクラスが読み取るカスタムプロパティの値だけを供給します。
import { dyn, cn } from '@tabula-css/merge';
<div className={cn('w-progress')} style={dyn({ '--d-progress': `${percent}%` })} />この値フィルターは意図的に形式フィルターであり、CSS の型チェッカーではありません — "73.4%" がその特定のプロパティにとって合法な <percentage> かどうかは、(tabula.config.json で宣言され、TAB-E127 を通じてビルド時に強制される)@property 構文が実際に決めることです。
variants()
export interface VariantsConfig<V extends VariantGroups> {
readonly base?: string;
readonly variants: V;
readonly compoundVariants?: readonly CompoundVariant<V>[];
readonly defaultVariants?: VariantProps<V>;
}
export type VariantsFn<V extends VariantGroups> = (props?: VariantCallProps<V>) => string;
export function variants<V extends VariantGroups>(config: VariantsConfig<V>): VariantsFn<V>;CVA 相当の、静的に解析可能なバリアントコンポーザーです。設定内のすべてのクラスは単なるリテラル文字列であるため(ESLint プラグインのシンクハーベスターは、cva/tv の設定とまったく同じようにそれを読み取ります)、合成は cn() を通じて行われます — したがって結果は、他のあらゆる cn() 呼び出しと同じ健全性・正規化・冪等性の保証を引き継ぎます。
import { variants } from '@tabula-css/merge';
const button = variants({
base: 'inline-flex rounded-md',
variants: {
intent: { primary: 'bg-brand ink-on-brand', ghost: 'ink-text' },
size: { sm: 'px-sm', md: 'px-md' },
},
compoundVariants: [{ intent: 'primary', size: 'sm', class: 'gap-xs' }],
defaultVariants: { intent: 'primary', size: 'md' },
});
button({ intent: 'ghost', className }); // caller's className is merged last, so it winsグループが定義していない値を指定するオプションは、開発環境では例外を投げ、本番環境では無視されます(そのグループは何も寄与しません)。これは cn() の fail-visible-in-dev / fail-open-in-prod の姿勢と一致します。
エラー
export class TabulaMergeError extends Error {
readonly code: string;
readonly diagnostics: readonly Diagnostic[];
}投げられるすべてのエラーは TabulaMergeError を継承し、安定した TAB-Exxx コードと @tabula-css/core からの構造化された Diagnostic を持ちます。
| エラー | コード | 発生元 | 条件 |
|---|---|---|---|
UnknownClassError | TAB-E300 | cn() | 開発環境において、未登録のクラス(レーベンシュタイン距離が最も近い候補の提案付き)。 |
MergeSoundnessError | TAB-E301 | cn() | T2 の開発環境自己チェックが、レジストリの宣言されたランク順序がそれ自体と食い違うことを検出した場合。 |
RankIntegrityError | TAB-E301 | cn() | あるクラスが非有限のランクに解決される場合 — 安全なフォールバックが存在しないため、本番環境でも発生します。 |
MissingRegistryError | TAB-E303 | getRegistry() | configure() より前に cn()/resolve()/dyn() が呼び出された場合。 |
MaxClassesError | TAB-E305 | cn() | 開発環境において、候補数が MAX_CLASSES を超える場合。 |
UnregisteredDynamicPropertyError | TAB-E304 | dyn() | 開発環境において、あるキーが登録済みの --d-* 動的カスタムプロパティではない場合。 |
InvalidDynamicValueError | TAB-E306 | dyn() | 開発環境において、ある値が形式フィルター(誤った型、長すぎる、安全でない句読点)を通らない場合。 |
型
export type ClassName = string & { readonly __tabulaClassName?: never };
export type ClassValue = string | number | boolean | null | undefined | ClassDictionary | ClassValue[];
export interface ClassDictionary { readonly [className: string]: boolean | null | undefined; }
export type { RegistryReader, RegistryFile } from '@tabula-css/registry/read';ClassName は、厳格な公称型というよりは説明的なブランドです(そのマーカーは任意であるため、単なる文字列リテラルも依然として代入可能です)。これは、className プロパティを、呼び出し元にすべての文字列をラップさせることなく、ESLint プラグインの no-runtime-class-construction ルールが認識する許可された素通り値としてマークするものです。
低レベルヘルパー
マージの上に構築されるツール(ESLint プラグインのシンク解析、カスタムのクラス文字列インスペクター)向けに公開されています: parseToken()(1 つのクラストークン — バリアントチェーンとユーティリティ — をレジストリに照らしてパースします。決して例外を投げません)、flatten()(cn() が内部で使う clsx スタイルの入力フラット化)、levenshtein()/nearest()(編集距離による「もしかして」提案)、isDev()(このパッケージ内のすべてのガードが使う NODE_ENV ベースの開発/本番判定)、warnOnce()/clearWarnOnce()(共有される本番環境の劣化チャネル — 一意なキーごとに一度だけの console.error)。
関連パッケージ
@tabula-css/registry— ここにあるすべての関数が読み取るRegistryReader。@tabula-css/core— これらのエラーが利用するTAB-Exxx診断カタログ。@tabula-css/preset— レジストリが記述する Tailwind のエントリスタイルシートを生成します。@tabula-css/react—cn()の上に構築された舗装された道のコンポーネント。- コンセプト § マージ:カスケードのシミュレーションではなく、全順序 — このパッケージが実装するアルゴリズム。