@tabula-css/eslint-plugin
Tabula の強制レイヤー: 閉じた語彙を厳格なゲートにするフラット設定 ESLint プラグイン。レジストリのみを読み込みます — インポートグラフに Tailwind は含まれません。
インストール
npm install --save-dev @tabula-css/eslint-plugineslint(ピア、>=9.0.0)と並ぶ開発時依存です。
概要
tabula build が閉じた語彙を導出し、tabula scan が CI 内ですべてのソースファイルをそれに照らして掃引するのに対し、@tabula-css/eslint-plugin はエディタとプリコミット向けのゲートです: レジストリ外のクラス、ランタイムで組み立てられたクラス名、あるいは期限切れの例外は、すべて TAB-E/TAB-W 診断コード付きの lint エラーとして、入力しているそばからライブに現れます。生成された registry.json を直接読み込み — Tailwind をインポートすることは決してありません — そのため lint は高速なままです。15 個の tabula/* ルールが提供され、2 つのプリセット(strict、migration)にグループ化されています。
有効化
eslint.config.js(フラット設定):
import tabula from '@tabula-css/eslint-plugin';
export default [
{
...tabula.configs.strict,
files: ['**/*.{ts,tsx}'],
settings: {
tabula: { registry: '.tabula/registry.json' },
},
},
];strict は 15 個のルールすべてをエラーにします。既存のコードベースに導入する場合は、代わりに tabula.configs.migration を使ってください: 4 つの禁止ルール(no-runtime-class-construction、no-unregistered-arbitrary-value、no-theme-variant、no-important)は厳格なエラーのまま残り、残りは warn に緩和されるため、段階的に切り替えを進めることができます。
settings.tabula
| Key | Default | Meaning |
|---|---|---|
registry | cwd から遡って .tabula/registry.json を探索 | registry.json への明示的なパス。 |
callees | cn、cx、clsx、cva、tv、variants、twMerge、classnames | 引数がクラスのシンクとして扱われる呼び出し式。 |
classAttributes | className、class | クラスのシンクとして扱われる JSX 属性名。 |
textLeafComponents | [] | inherited-property-boundary のために、プロジェクトがテキストリーフをレンダリングすると宣言するコンポーネント名(<Card>)。 |
classMapSources | /\.classmap(\.(c|m)?[jt]sx?)?$/ | 指定されたルックアップマップモジュール(no-runtime-class-construction の A7 形式)のファイル名パターン。 |
classNameSources | /^@tabula-css\/merge$/ | ブランド付きの ClassName 型を供給してよいモジュール指定子。 |
now | 実時計 | exception-scope の有効期限チェック向けの、決定論的なテストのための ISO 日付の上書き。 |
ルール
tabula/registry-required
読み込み可能でスキーマ的に valid なレジストリを要求します — これは、他のすべてのレジストリに基づくルールが共有する前提条件です。コンテンツ系のルールをオフにしていてもプロジェクトがこれを要求できるよう、単独でも公開されています。
// fails: no readable registry.json at the configured/discovered pathtabula/no-runtime-class-construction
ランタイムでクラス文字列を組み立てることを禁止します。Tailwind のスキャナーは完全なリテラル文字列しか認識しないため、`p-${n}` は静かに何にもコンパイルされません。条件式(cond && "x"、三項演算子、リテラルの配列/オブジェクト)、モジュールスコープの const ルックアップマップ、*.classmap.ts モジュールからのインポート、cva()/tv()/variants() の結果、ClassName 型の素通りパラメータは許可されます。
// ❌ violating
<div className={`p-${size}`} />
// ✅ passing
const PADDING = { sm: 'p-sm', md: 'p-md' } as const;
<div className={PADDING[size]} />tabula/no-unregistered-arbitrary-value
例外として登録されていない [...] を含む候補(任意値、任意プロパティ、任意バリアント、任意修飾子)をすべて禁止します。自動修正は決して行われません。
// ❌ violating
<div className="w-[347px]" />
// ✅ passing — after `tabula except add` registers it
<div className="w-hero-legacy-width" />tabula/no-unknown-class
そのユーティリティが語彙に登録されていないクラスを禁止します。まずレジストリに依存しない禁止リストを報告し(そのため space-x-4 は誤字の修正を提案するのではなく、それが なぜ 禁止されているのかを説明します)、それ以外についてはレーベンシュタイン距離 2 以内の「もしかして」候補を提示します。自動修正は行いません。
// ❌ violating
<div className="bg-surfac" />
// Unknown class `bg-surfac`. Did you mean `bg-surface`?
// ✅ passing
<div className="bg-surface" />tabula/no-theme-variant
dark:、light:、そしてあらゆる [data-theme…]/[prefers-color-scheme…] バリアントを禁止します。テーマはトークン軸です — bg-surface はすでにすべてのテーマの値を持っているため、バリアントは軸モデルが取り除いたはずの分岐を再び持ち込みます。レジストリに依存しません(レジストリが一切なくても発火します)。
// ❌ violating
<div className="bg-white dark:bg-gray-900" />
// ✅ passing
<div className="bg-surface" />tabula/no-important
!important マーカーを禁止します。これは rank が決するカスケードモデルから逸脱するものです。(サイレントな自動修正ではなく)提案として提示されます。これを取り除くとレンダリングされる挙動が変わり得るためです。
// ❌ violating
<div className="!p-md" />
// ✅ passing
<div className="p-md" />tabula/class-order
トークンが完全に既知で、任意でなく、!important でもないクラス文字列に対して、正規の順序 — 条件帯、次に昇順のユーティリティランク — を強制します。並べ替えによって自動修正されます(順序を保っても振る舞いは変わりません。マージが健全である限り、1 つのクラス属性内の順序は計算されるスタイルに影響しないためです)。
// ❌ violating
<div className="hover:bg-surface-raised bg-surface" />
// ✅ passing (autofixed)
<div className="bg-surface hover:bg-surface-raised" />tabula/no-conflicting-classes
1 つの静的な文字列内にある、条件を共有し、かつ(同じスロットと交わる(冗長 — より高いランクのクラスを残すことで自動修正されます)か、アトミックなクラス(例: sr-only)と同じ CSS プロパティを宣言するスロット書き込みクラスとを組にする(曖昧 — 自動修正なし。作者が選択しなければなりません))既知の 2 つのクラスを禁止します。
// ❌ violating (autofixed to `p-8`)
<div className="p-4 p-8" />
// ❌ violating, no autofix — both declare `position`
<div className="sr-only absolute" />tabula/require-merge
2 つ以上のクラスソース(+ による連結、配列リテラル)を組み合わせる className 式に対して cn() を経由するよう要求します。マージされていない連結は優先順位が未定義です。オペランドをラップすることで自動修正されます。テンプレートリテラルによる結合は代わりに no-runtime-class-construction の管轄です。
// ❌ violating
<div className={base + ' ' + className} />
// ✅ passing (autofixed)
<div className={cn(base, className)} />tabula/classname-last
className 引数が cn() ファミリーの呼び出しの最後の引数であることを要求し、呼び出し元によるオーバーライドが常に勝つようにします。末尾へ移動することで自動修正されます(スプレッド引数がある場合はスキップされます。並べ替えが安全ではないためです)。
// ❌ violating
<div className={cn(className, 'p-md')} />
// ✅ passing (autofixed)
<div className={cn('p-md', className)} />tabula/named-group-only
すべての group/peer マーカーとその利用側が名前を持つこと(group/card、group-hover/card:)を要求します。裸の group では、「これはどの祖先のことか?」がツリー全体を読まないと分からなくなります。
// ❌ violating
<div className="group"><span className="group-hover:opacity-100" /></div>
// ✅ passing
<div className="group/card"><span className="group-hover/card:opacity-100" /></div>tabula/group-marker-exists
名前付きの group-* 利用側が、同一ファイル内の祖先に対応する group/<name> マーカーを持つことを要求します。このファイル内でマーカーが見つからない場合は TAB-W301(エラーには決してなりません)に格下げされます — それは、linter からは見えない親コンポーネント内に正当に存在している場合があるためです。
// ⚠ warning — no `group/card` ancestor found in this file
<span className="group-hover/card:opacity-100" />tabula/peer-source-order
名前付きの peer-* 利用側が、ソース順でその peer/<name> マーカーの兄弟に後続することを要求します。これは、peer が依拠する :has()/一般兄弟関係に対する DOM 自身の ~ の要件を反映したものです。
// ⚠ warning — `peer/email` must precede this element
<span className="peer-invalid/email:text-danger" />
<input className="peer/email" />tabula/inherited-property-boundary
base プロファイルレベルの下でのみ(strict の下では不活性)、継承可能なプロパティのユーティリティ(text-*、font-*、leading-*、tracking-*、ink-*)を、テキストリーフタグ、または明示的に scope-text とマークされた要素に制限します — これは、base の typography が継承する唯一のメカニズムを封じ込める、コンパイル時側の半分です。違反している内在的な要素に scope-text を挿入することで自動修正されます。コンポーネントの境界は自動修正できず、TAB-W301 に格下げされます。
// ❌ violating — <div> is a container, not a text leaf
<div className="text-sm">…</div>
// ✅ passing
<div className="scope-text text-sm">…</div>tabula/exception-scope
登録された例外クラスを、その allowedIn glob と expires の日付に制限します。スコープ外での使用や期限切れ後の使用は、その例外自身の書類が制限するはずだった閉じた語彙を、静かに再び開いてしまいます。
// ❌ violating — used outside tokens/exceptions.tokens.json's allowedIn glob
<div className="w-hero-legacy-width" /> // in a file not matching `src/marketing/hero.tsx`エクスポートされる設定
| Export | What it does |
|---|---|
tabula.configs.strict | すべてのルールを error にします。 |
tabula.configs.migration | 4 つの禁止ルール(no-runtime-class-construction、no-unregistered-arbitrary-value、no-theme-variant、no-important)は error のまま残り、それ以外はすべて warn になります。段階的な導入向けです。 |
@tabula-css/eslint-plugin/banlist
import { BANLIST, betterTailwindcssBanlist, matchBan } from '@tabula-css/eslint-plugin/banlist';@tabula-css/core のレジストリに依存しない禁止テーブルを再エクスポートします: BANLIST(凍結されたパターンリスト — space-*、divide-*、*:、**:、任意のコンビネータ、in-*、rtl:/ltr: — Banned mechanisms を参照)、matchBan(token)(一致する禁止 id、または undefined を返します)、そしてプロジェクト自身の eslint-plugin-better-tailwindcss 設定に同じパターンを配線するための betterTailwindcssBanlist()。
関連パッケージ
@tabula-css/registry— これらのルールが読み取るアーティファクト。@tabula-css/merge— いくつかのルールが前提とするランタイムのcn()。@tabula-css/cli—tabula scan(CI 側の掃引)とtabula canary(strictプリセットに対してフィクスチャを lint し、すべてのルールがまだ発火することを証明します)。- Getting started — 一連のセットアップウォークスルー。
- Concepts — それぞれの禁止メカニズムがなぜ禁止されているのか。