@tabula-css/stylelint-plugin
Tabula CSS 거버넌스: 다섯 가지 프로젝트-CSS 규칙(@apply 금지, 수작성 규칙 금지, 루트 전용 커스텀 속성, @theme inline 금지, !important 금지)을 stylelint 규칙으로 구현한 것과, tabula check:css가 실행하는 PostCSS 커널입니다.
설치
npm install --save-dev @tabula-css/stylelint-plugin개발 의존성입니다. postcss(^8.4.0)는 필수 피어이고, stylelint(^16.0.0)는 선택적 피어입니다 — ./kernel 내보내기는 stylelint에 전혀 의존하지 않으며, 이 덕분에 @tabula-css/cli의 check:css가 stylelint를 CLI의 그래프에 끌어들이지 않고 이를 재사용할 수 있습니다.
개요
이 프로파일의 모든 ESLint 규칙은 .ts/.tsx를 다스립니다; 이 패키지가 존재하기 전까지는 시스템 안의 그 무엇도 프로젝트 .css 파일을 열어본 적이 없었습니다. import된 스타일시트 안의 수작성 .card .title { color: red }는 다른 모든 게이트가 초록불이어도 지역성(Locality)을 깨뜨렸습니다. @tabula-css/stylelint-plugin은 작성 시점에 이 틈을 메웁니다 — stylelint를 통해 에디터에 연결된 다섯 가지 규칙 — 그리고 규칙 커널을 tabula check:css와 공유하므로, 두 강제 지점은 정확히 같은 PostCSS 로직을 읽고 .css 파일이 담을 수 있는 것에 대해 서로 어긋날 수 없습니다.
활성화
// stylelint.config.js
import tabula from '@tabula-css/stylelint-plugin/config';
export default {
...tabula,
overrides: [
// The sanctioned entry — the one file holding `@import "../.tabula/source.css";` — is
// exempt from `no-raw-rules` only, so it can also hold root-level application CSS.
{ files: ['src/app.css'], rules: { 'tabula/no-raw-rules': [true, { sanctionedEntry: true }] } },
],
};@tabula-css/stylelint-plugin/config는 프로젝트가 자신의 설정에 그대로 이어 붙이는 공유 설정을 내보냅니다: 다섯 규칙 모두 켜져 있고 심각도 완화 없음 — 이들은 ESLint 플러그인이 .tsx에서 강제하는 것과 동일한 닫힌 어휘의 CSS 쪽 절반입니다. overrides 항목 목록을 tabula check:css --css-entry의 플래그와 동일하게 유지하십시오; 두 게이트는 규칙 로직은 공유하지만 설정 파일은 공유하지 않으므로, 허가된 진입점 목록이 둘 사이에서 어긋날 수 있는 유일한 요소입니다.
규칙
아래의 모든 규칙은 프로젝트 CSS(여러분이나 에이전트가 작성한 모든 .css 파일)에 발동하며, 특별히 언급되지 않는 한 .tabula/의 방출된 스타일시트에도 발동합니다(그곳에서의 위반은 작성 실수가 아니라 생성기 버그입니다).
tabula/no-apply
TAB-E221. 어디에서든 @apply를 금지합니다. 이는 클래스 목록을 손으로 쓴 선택자로 합성하는 것 — 정확히 이 프로파일이 제거하는 간접성입니다.
/* ❌ violating */
.card { @apply p-4 rounded-md; }
/* ✅ passing — write the classes on the element instead */tabula/no-raw-rules
TAB-E222. 허가된 진입 스타일시트 밖에서의 수작성 규칙을 금지합니다. 프로젝트 CSS는 최상위에서 @import, @source, @utility, @custom-variant, @charset, 본문 없는 @layer a, b; 순서 선언, 그리고 주석만을 담을 수 있습니다 — 그 밖의 것(스타일 규칙, @theme, @media, @layer { … } 블록)은 클래스가 아니라 선택자로 도달하는 스타일링입니다. 최상위 블록당 한 번, 프로젝트 스코프에서만 보고됩니다.
/* ❌ violating */
.card p { color: red; }
/* ✅ passing */
@import "../.tabula/source.css";(위의 overrides 블록을 통해) { sanctionedEntry: true }를 전달하면 딱 하나의 파일 — @import "../.tabula/source.css"; 줄을 담고 있는 파일 — 을 이 규칙에서만, 그리고 오직 이 규칙에서만 면제합니다.
tabula/no-scoped-custom-property
TAB-E223. 시스템에서 가장 중요한 단일 검사입니다. 루트 주체(:root, html, 또는 속성 선택자로 세분화된 그 둘 중 하나 — :root[data-theme="dark"]는 해당되지만 .card는 해당되지 않습니다) 이외의 곳에서 정의된 프로파일 커스텀 속성(--tb-* / --d-*)을 금지합니다. 프로젝트 CSS가 프로파일 속성을 기록해야 할 정당한 이유는 전혀 없습니다: dyn()이 허가된 요소별 채널이며, 이는 스타일시트가 아니라 인라인 스타일을 기록합니다.
/* ❌ violating */
.panel { --tb-color-accent: red; }
/* ✅ passing */
:root[data-theme="dark"] { --tb-color-accent: #111; }방출된 스코프에서는 이 규칙이 실제로 테마 토큰인(방출된 집합 어딘가의 :root에서 정의된) 속성만 표시합니다 — 프로파일 자체의 유틸리티는 정당하게 요소별 합성 속성(.ring-accent { --tb-ring-color: … })을 기록하는데, 이들은 inherits: false이며 설계상 요소별입니다.
tabula/no-theme-inline
TAB-E224. @theme inline을 금지합니다(공백으로 구분된 옵션으로 매칭되므로 @theme inline reference와 @theme static inline 둘 다 발동합니다). 이는 토큰의 값을 사용 지점에 인라인하여, 루트 축 블록이 더 이상 다른 테마를 위해 이를 재지정할 수 없게 만듭니다 — shadcn 생태계의 함정입니다.
/* ❌ violating */
@theme inline { --tb-color-accent: red; }
/* ✅ passing */
@theme { --tb-color-accent: red; }tabula/no-important-css
TAB-E225. 프로젝트 CSS의 어떤 선언에서도 !important를 금지합니다. 이는 선언을 cn()/resolve()가 의존하는 순위 모델 밖에 둡니다. 방출된 CSS는 생성기 출력이므로, 이 규칙은 방출된 스코프에서는 실행되지 않습니다.
/* ❌ violating */
.card { color: red !important; }
/* ✅ passing */
.card { color: red; }@tabula-css/stylelint-plugin/kernel
import {
checkCss,
CSS_RULE_CODES,
collectRootDefinedProperties,
isRootSubject,
noApply,
noImportantCss,
noRawRules,
noScopedCustomProperty,
noThemeInline,
type CheckCssOptions,
type CssRuleName,
type CssScope,
type CssViolation,
} from '@tabula-css/stylelint-plugin/kernel';위의 모든 규칙 배후에 있는 순수 PostCSS 구현으로, import 그래프에 stylelint도 파일시스템도 없습니다 — 이는 @tabula-css/cli의 check:css 명령이 그대로 import하는 것이며, 그래서 CLI와 에디터 플러그인은 바이트 단위로 동일한 로직을 강제합니다.
| 내보내기 | 시그니처(단순화) | 하는 일 |
|---|---|---|
checkCss | (root: Root, opts: CheckCssOptions) => CssViolation[] | 파싱된 스타일시트에 대해 다섯 규칙을 모두 실행하고, 소스 순서대로 위반 사항을 반환합니다. |
noApply, noRawRules, noScopedCustomProperty, noThemeInline, noImportantCss | (root: Root, opts: CheckCssOptions) => CssViolation[] | 하나의 검사만 독립적으로 필요한 호출자를 위해 각 규칙을 단독으로. |
collectRootDefinedProperties | (root: Root) => Set<string> | 루트 주체에서 정의된 모든 프로파일 커스텀 속성 — no-scoped-custom-property가 방출된 스코프에서 토큰과 합성 속성을 구분하는 데 필요한 테마-토큰 집합. |
isRootSubject | (selector: string) => boolean | 선택자 문자열이 모든 쉼표 분기에서 문서 루트만을 선택하는지 여부. |
CSS_RULE_CODES | Readonly<Record<CssRuleName, ErrorCode>> | 규칙 이름 → TAB-Exxx 매핑으로, 고정되어 있어 CLI와 문서가 플러그인과 어긋날 수 없습니다. |
CheckCssOptions는 scope('project' | 'emitted', 기본값 'project'), 메시지용 file 레이블, sanctionedEntry(no-raw-rules만 면제), 그리고 — 방출된 스코프에서만 — collectRootDefinedProperties에서 얻은 tokenProperties를 담습니다.
@tabula-css/stylelint-plugin/config
import tabula, { config } from '@tabula-css/stylelint-plugin/config';공유 stylelint 설정 객체입니다: { plugins: ['@tabula-css/stylelint-plugin'], rules: { 'tabula/no-apply': true, 'tabula/no-raw-rules': true, 'tabula/no-scoped-custom-property': true, 'tabula/no-theme-inline': true, 'tabula/no-important-css': true } }. 활성화에서 보인 것처럼 여러분 자신의 stylelint.config.js에 이어 붙이십시오.
tabula check:css가 실행하는 것
tabula check:css(그리고 --no-css가 전달되지 않은 tabula build --check)는 모든 프로젝트 .css 파일과 .tabula/의 방출된 스타일시트를 순회하며, 각각에 대해 이 패키지의 checkCss 커널 함수를 호출합니다 — 프로젝트 파일은 scope: 'project'로, 방출된 파일은 scope: 'emitted'와 collectRootDefinedProperties에서 얻은 tokenProperties를 담은 하나의 집합으로 호출합니다. 그 플래그와 종료 동작은 @tabula-css/cli를 참고하십시오.
함께 보기
@tabula-css/cli— 이 패키지의 커널을 공유하는 CI 측 게이트.- 핵심 개념 — 다섯 규칙 각각이 존재하는 이유.
- CSS 거버넌스 — 이 게이트가 아직 다루지 않는 것을 포함한 전체 그림.
- 시작하기 — 허가된 진입 스타일시트를 배선하기.