Skip to content

@tabula-css/eslint-plugin

Tabula의 강제 계층: 닫힌 어휘를 강력한 게이트로 만드는 플랫 설정(flat-config) ESLint 플러그인입니다. 레지스트리만 읽으며 — import 그래프에 Tailwind가 없습니다.

설치

bash
npm install --save-dev @tabula-css/eslint-plugin

개발 의존성이며, eslint(피어, >=9.0.0)와 함께 설치합니다.

개요

tabula build가 닫힌 어휘를 도출하고 tabula scan이 CI에서 모든 소스 파일을 훑는 반면, @tabula-css/eslint-plugin은 에디터 및 프리커밋 대면 게이트입니다: 레지스트리 밖의 클래스, 런타임에 조립된 클래스 이름, 또는 오래된 예외는 모두 TAB-E/TAB-W 진단 코드가 붙은 린트 오류가 되며, 타이핑하는 즉시 반영됩니다. 생성된 registry.json을 직접 읽으며 — Tailwind는 결코 import하지 않으므로 — 린팅은 계속 빠릅니다. 열다섯 개의 tabula/* 규칙이 두 개의 프리셋(strict, migration)으로 묶여 제공됩니다.

활성화

eslint.config.js(플랫 설정):

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을 사용하십시오: 네 가지 금지 규칙(no-runtime-class-construction, no-unregistered-arbitrary-value, no-theme-variant, no-important)은 계속 강한 오류로 남고, 나머지는 warn으로 완화되어 점진적으로 전환할 수 있습니다.

settings.tabula

기본값의미
registrycwd에서 위로 올라가며 .tabula/registry.json을 찾음registry.json에 대한 명시적 경로.
calleescn, cx, clsx, cva, tv, variants, twMerge, classnames인자가 클래스 싱크로 취급되는 호출 표현식.
classAttributesclassName, 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

다른 모든 레지스트리 기반 규칙이 공유하는 전제 조건인, 읽을 수 있고 스키마상 유효한 레지스트리를 요구합니다. 콘텐츠 규칙이 꺼져 있어도 프로젝트가 이를 요구할 수 있도록 독립적으로 노출됩니다.

ts
// fails: no readable registry.json at the configured/discovered path

tabula/no-runtime-class-construction

런타임에 클래스 문자열을 조립하는 것을 금지합니다. Tailwind의 스캐너는 완전한 리터럴 문자열만 인식하므로, `p-${n}`은 조용히 아무것도 컴파일하지 않습니다. 조건문(cond && "x", 삼항 연산자, 리터럴의 배열/객체), 모듈 스코프 const 조회 맵, *.classmap.ts 모듈로부터의 import, cva()/tv()/variants()의 결과, ClassName 타입의 통과 매개변수는 허용됩니다.

tsx
// ❌ 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

예외로 등록되지 않은 [...]를 포함하는 후보(임의 값, 속성, 변형, 또는 수식자)를 모두 금지합니다. 결코 자동 수정되지 않습니다.

tsx
// ❌ 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 이하의 "혹시 이거였나요?" 제안을 제공합니다. 결코 자동 수정하지 않습니다.

tsx
// ❌ 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는 이미 모든 테마의 값을 담고 있으므로, 변형은 축 모델이 제거한 분기를 다시 들여옵니다. 레지스트리와 무관합니다(레지스트리가 전혀 없어도 발동합니다).

tsx
// ❌ violating
<div className="bg-white dark:bg-gray-900" />

// ✅ passing
<div className="bg-surface" />

tabula/no-important

!important 마커를 금지합니다; 이는 순위가 캐스케이드를 결정하는 모델을 벗어나기 때문입니다. 조용한 자동 수정이 아니라 제안으로만 제공되는데, 이를 제거하면 렌더링된 동작이 바뀔 수 있기 때문입니다.

tsx
// ❌ violating
<div className="!p-md" />

// ✅ passing
<div className="p-md" />

tabula/class-order

토큰이 모두 알려져 있고, 임의값이 아니며, !important가 아닌 모든 클래스 문자열에 대해 정규 순서(조건 대역, 그다음 오름차순 유틸리티 순위)를 강제합니다. 재정렬로 자동 수정됩니다(병합이 건전한 이상 하나의 클래스 속성 안에서의 순서는 계산된 스타일에 영향을 주지 않으므로 동작을 보존합니다).

tsx
// ❌ violating
<div className="hover:bg-surface-raised bg-surface" />

// ✅ passing (autofixed)
<div className="bg-surface hover:bg-surface-raised" />

tabula/no-conflicting-classes

하나의 정적 문자열 안에서 조건을 공유하며 같은 슬롯과 교차하는(중복 — 더 높은 순위의 클래스를 남기는 방식으로 자동 수정) 두 개의 알려진 클래스나, 원자적 클래스(예: sr-only)와 같은 CSS 속성을 선언하는 슬롯 기록 클래스를 짝짓는 것(모호함 — 자동 수정 없음, 작성자가 선택해야 함)을 금지합니다.

tsx
// ❌ 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

두 개 이상의 클래스 출처를 결합하는(+ 연결, 배열 리터럴) className 표현식이 cn()을 거치도록 요구합니다; 병합되지 않은 연결은 우선순위가 정의되지 않습니다. 피연산자를 감싸는 방식으로 자동 수정됩니다. 템플릿 리터럴 결합은 대신 no-runtime-class-construction의 소관입니다.

tsx
// ❌ violating
<div className={base + ' ' + className} />

// ✅ passing (autofixed)
<div className={cn(base, className)} />

tabula/classname-last

className 인자가 cn() 계열 호출의 마지막 인자여야 함을 요구하여, 호출자의 오버라이드가 항상 이기도록 합니다. 끝으로 옮기는 방식으로 자동 수정됩니다(스프레드 인자 아래에서는 재정렬이 안전하지 않으므로 건너뜁니다).

tsx
// ❌ 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은 트리 전체를 읽지 않고서는 "이것이 어느 조상인가?"를 답할 수 없게 만듭니다.

tsx
// ❌ 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로 강등됩니다(결코 오류가 아닙니다) — 마커가 린터가 볼 수 없는 부모 컴포넌트에 정당하게 존재할 수도 있기 때문입니다.

tsx
// ⚠ 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 자체의 ~ 요구 사항을 그대로 반영합니다.

tsx
// ⚠ 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 타이포그래피에서 상속되는 유일한 메커니즘을 담아내는 작업의 컴파일 타임 절반입니다. 위반하는 본질적(intrinsic) 요소에 scope-text를 삽입하는 방식으로 자동 수정됩니다; 컴포넌트 경계는 자동 수정할 수 없어 TAB-W301로 강등됩니다.

tsx
// ❌ 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 글롭과 expires 날짜로 제한합니다; 범위 밖에서 사용하거나 만료 이후에 사용하면 그 예외의 서류 자체가 경계 짓기 위해 존재하는 닫힌 어휘를 조용히 다시 열어버립니다.

tsx
// ❌ 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`

내보낸 설정

내보내기하는 일
tabula.configs.strict모든 규칙이 error.
tabula.configs.migration네 가지 금지 규칙(no-runtime-class-construction, no-unregistered-arbitrary-value, no-theme-variant, no-important)은 error로 유지되고, 나머지는 점진적 도입을 위해 warn.

@tabula-css/eslint-plugin/banlist

ts
import { BANLIST, betterTailwindcssBanlist, matchBan } from '@tabula-css/eslint-plugin/banlist';

@tabula-css/core의 레지스트리와 무관한 금지 테이블을 다시 내보냅니다: BANLIST(고정된 패턴 목록 — space-*, divide-*, *:, **:, 임의 결합자, in-*, rtl:/ltr:금지된 메커니즘 참고), matchBan(token)(일치하는 금지 id 또는 undefined를 반환), 그리고 프로젝트 자체의 eslint-plugin-better-tailwindcss 설정에 동일한 패턴을 배선하기 위한 betterTailwindcssBanlist().

함께 보기

  • @tabula-css/registry — 이 규칙들이 읽는 아티팩트.
  • @tabula-css/merge — 여러 규칙이 전제하는 런타임 cn().
  • @tabula-css/clitabula scan(CI 측 훑기)과 tabula canary(모든 규칙이 여전히 발동하는지 증명하기 위해 strict 프리셋에 대해 픽스처를 린트).
  • 시작하기 — 설정 전체 과정을 다루는 안내서.
  • 핵심 개념 — 각각의 금지된 메커니즘이 금지된 이유.

Released under the MIT License.