Skip to content

CSS 거버넌스 ​

Tabula의 지역성 주장 — 엘리먼트의 외관은 그 자신의 마크업으로부터 결정 가능하다 — 은 모두 class 속성에 관한 것이다. 스타일시트는 다른 방식으로, 즉 셀렉터로 엘리먼트에 닿는다. 그래서 프로젝트 자신의 .css 파일은 전체 모델이 한 줄로 무너질 수 있는 유일한 표면이며, 이 레이어가 존재하기 전까지는 이 시스템의 어떤 명령도 그것을 열어보지 않았다.

이 페이지는 이제 그것들을 무엇이 통제하는지, 무엇을 커버하지 않는지, 그리고 두 게이트가 어디서 어긋날 수 있는지를 다룬다.

다섯 가지 규칙 ​

두 게이트 모두 같은 코드를 실행한다 — 규칙 커널은 packages/stylelint-plugin/src/kernel.ts에 있다. stylelint 플러그인은 에디터를 위해 이를 감싸고, tabula check:css는 CI를 위해 파일들을 그 위로 순회한다. 두 번 작성했다면 어긋났을 것이고, 그 어긋남은 가장 중요한 방향으로 조용히 일어났을 것이다: 에디터가 표시하는 것을 고치고, CI는 다른 무언가를 표시하며, 어떤 에디터도 언급하지 않는 규칙은 규칙이기를 멈춘다.

코드stylelint 규칙금지 대상이유
TAB-E221tabula/no-apply@apply클래스 목록을 손으로 작성한 셀렉터로 조합한다 — 프로파일이 제거하는 바로 그 간접성이다. Tailwind 창시자 스스로도 부인한 방식이다(draft-c §3.2 ✚16).
TAB-E222tabula/no-raw-rules승인된 엔트리 밖, 프로젝트 CSS 안의 손으로 작성된 규칙.card .title { color: red }는 셀렉터로 스타일링하므로, 엘리먼트가 자신의 마크업만으로 읽히지 않게 된다(draft-c §3.2 ✚18).
TAB-E223tabula/no-scoped-custom-property:root/html 이외에 정의된 --tb-* / --d-*T17 — 시스템 전체에서 가장 중요한 검사다(SPEC J6). 토큰이 <div> 위에서 재정의될 수 있는 순간, 그것을 쓰는 모든 자손은 자신의 마크업만으로는 읽을 수 없게 된다.
TAB-E224tabula/no-theme-inline@theme inlineT20 — inline은 빌드 시점에 토큰 값을 유틸리티에 대입하여, 모든 조건부 토큰을 컴파일해서 없애 버린다. 다크 모드는 아무 진단도 없이 동작을 멈춘다. shadcn 생태계의 기본값이므로, 그 생태계로 훈련된 에이전트는 이를 향해 손을 뻗는다.
TAB-E225tabula/no-important-css프로젝트 CSS 안의 !important모든 유틸리티는 특정성 (0,1,0)으로 정규화되어 있어 오직 rank만이 충돌을 결정한다. !important 하나가 declaration을 그 증명 밖으로 내보낸다.

따라서 프로젝트 .css 파일은 다음만을 담을 수 있다: @import, @source, @utility, @custom-variant, @charset, 본문 없는 @layer a, b; 순서 선언, 그리고 주석. 그 외에는 아무것도 없다.

두 개의 게이트 ​

CI / 빌드 시점에서

bash
tabula check:css                       # every project .css file + the emitted stylesheets
tabula build --check                   # drift + scan + check:css, the one command CI runs
tabula build --check --no-css          # drift + scan only; the CSS gate must be opted OUT of
tabula check:css --css-entry src/theme/entry.css   # designate a non-conventional entry

에디터에서 — .stylelintrc.json, 이 저장소 자신의 것을 그대로 미러링:

json
{
  "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
  },
  "overrides": [
    { "files": ["src/app.css"], "rules": { "tabula/no-raw-rules": [true, { "sanctionedEntry": true }] } }
  ]
}

@tabula-css/stylelint-plugin/config는 이 규칙 블록을 내보내므로, 복사하는 대신 스프레드할 수 있다. stylelint는 선택적 peer dependency다: 플러그인의 커널은 import 그래프에 stylelint를 전혀 갖고 있지 않으며, 이것이 CLI가 런타임 의존성으로 저작 도구(authoring tool)를 끌어들이지 않고도 이를 사용할 수 있게 해준다.

승인된 엔트리 ​

모든 앱에는 @import "../.tabula/source.css";와 앱 자신의 루트 레벨 커스텀 프로퍼티를 담는 스타일시트가 하나 필요하다. 그 파일 — 오직 그 파일만 — 이 TAB-E222에서 면제된다.

나머지 네 가지에서는 면제되지 않는다. 엔트리 안의 :root 블록은 괜찮지만, 엔트리 안의 --tb-color-accent: red는 어디에 나타나든 TAB-E223이다. examples/reference-ui/src/app.css를 참고하라, 이것이 풀어본(worked) 예제다.

파일이 엔트리가 되는 방법:

  • 관례에 의해 — src/app.css, src/index.css, src/styles.css, app/globals.css와 몇 개 더(packages/cli/src/commands/check-css.ts의 DEFAULT_CSS_ENTRIES);
  • 또는 명시적으로 — tabula check:css --css-entry <path>, 반복 가능.

두 게이트가 어긋날 수 있는 유일한 지점은 바로 이 목록이다: check:css는 플래그와 관례에서 이를 읽고, stylelint는 .stylelintrc의 overrides 블록에서 이를 읽는다. 둘을 동일하게 유지하라. 만약 서로 어긋난다면, 배포 여부를 결정하는 것은 CI 게이트다.

결정 사항과 불일치, 기록됨 ​

1. Draft C의 코드는 존중되지 않는다. Draft C는 이 불변식들에 PLANAR-E210(T17), PLANAR-E211(T20), PLANAR-E212(✚18)이라는 번호를 부여한다. 이 세 번호는 CSS 레이어가 작성되기 전에 이미 이 카탈로그의 빌드 불변식 I1/I2/I3에 사용되고 있었다. 두 가지를 뜻하는 코드는 아무도 알아보지 못하는 코드보다 더 나쁘다 — tabula explain TAB-E212는 하나의 설명 텍스트만 출력할 수 있고, 그것은 독자를 손으로 쓴 .card p가 아니라 rank 충돌 쪽으로 잘못 이끌 것이다. 그래서 이 계열은 대신 E221–E225에서 새로 만들어졌으며, 각 카탈로그 항목은 draft와의 대응 관계를 기록한다.

2. T17은 생성된 스타일시트에 대해서는 다르게 읽혀야 하고, 실제로 그래야 한다. 문서에 쓰인 대로의 T17은 모든 프로파일 커스텀 프로퍼티가 오직 :root에서만 정의될 수 있다고 말한다. 프로파일 자신의 출력은 의도적으로 그것을 위반한다: .ring-accent { --tb-ring-color: … }와 .shadow-sm { --tb-shadow: … }는 엘리먼트별 합성(composition) 프로퍼티로, inherits: false와 초기값을 갖고 선언된다. 그래서:

  • 프로젝트 CSS — 루트 대상 밖에서의 어떤 --tb-*/--d-* 정의든 예외 없이 위반이다. 프로젝트 CSS가 프로파일 프로퍼티를 정의해야 할 정당한 이유는 전혀 없다; 엘리먼트별 값을 위한 승인된 채널은 dyn()이며, 이는 스타일시트가 아니라 인라인 스타일을 쓴다.
  • 생성된 CSS — 루트 대상 밖에서의 정의는, 그 프로퍼티가 생성된 세트 어딘가의 루트 대상에서 정의되어 있는 경우에만 위반이다, 즉 테마 토큰인 경우다. 이것이 T17이 잡으려는 것(루트 아래에서 재정의된 토큰)을 정확히 잡아내면서도 프로파일 자신의 올바른 출력을 실패시키지 않는다.

SPEC은 두 번째 것을 말해야 한다. 그런데 첫 번째 것을 말하고 있다. 검사를 약화시켜 해결하는 대신 여기 기록해 둔다.

3. @utility는 엘리먼트 바인딩이다. @utility card-shell { --tb-color-accent: red }는 그것을 가진 모든 엘리먼트에서 테마 토큰을 재정의하는 클래스로 컴파일된다 — 프로젝트 CSS가 담을 수 있는 그 유일한 at-rule을 입은, 같은 T17 위반이다. 커널은 이런 이유로 @utility 안의 declaration을 엘리먼트 스코프로 취급한다.

4. 엔트리 목록은 tabula.config.json 안에 있어야 한다. 그것의 영구적인 자리는 설정 스키마 (packages/core/src/schemas/config.ts) 안의 scan.sources 옆에 있는 css.entries 키이며, 그 스키마의 additionalProperties: false는 인식되지 않는 css 키가 무시되는 대신 검증에서 실패하게 만든다. 이 레이어를 만든 태스크는 그 파일을 소유하지 않았으므로, 목록은 오늘은 관례와 --css-entry다. 이를 옮기는 것은 한 개 키를 바꾸는 일이며, 위에서 언급한 어긋남 지점을 제거할 것이다.

5. 파싱 불가능한 스타일시트는 건너뛸 대상이 아니라 발견 사항이다. PostCSS가 파싱할 수 없는 .css 파일은 TAB-E222로 보고된다. 건너뛴다면 "문법적으로 깨져 있음"이 이 페이지의 모든 규칙을 피해가는 가장 저렴한 방법이 될 것이다.

이 레이어가 여전히 커버하지 않는 것 ​

  • T18 — 루트 아래의 축 속성. <div data-theme="dark">는 비활성이며(생성된 셀렉터는 :root[data-theme=…]이므로 그 속성은 아예 효과가 없다), ESLint 규칙 tabula/no-axis-attribute-below-root가 저자에게 이를 설명해줄 것이다. CSS에서는 .panel[data-theme="dark"] 셀렉터가 축 위반이 아니라 손으로 작성된 규칙(TAB-E222)으로 걸린다 — 잘못된 이름이지만 올바른 결과다.
  • 프로젝트가 작성한 @source 줄 — 폐쇄성의 구멍, 여전히 열려 있음. 스타일시트 중 하나에 있는 @source "./src"는 Tailwind의 소스 스캐닝을 다시 켜며, 테마 네임스페이스는 여전히 정의되어 있으므로 bg-red-500과 p-4가 다시 방출되기 시작한다. check:css는 이를 표시하지 않는다, draft-c §3.2 ✚18이 @source를 프로젝트 CSS가 담을 수 있는 at-rule 목록에 포함시키기 때문이다. 그 목록과 review A7의 폐쇄성 논증은 서로 모순되며, 이 레이어는 그 목록을 구현한다. SPEC에서 이것이 해결되기 전까지는: .tabula/source.css의 @import를 프로젝트 안의 유일한 Tailwind 엔트리로 유지하고, @source 줄을 추가하지 말라. grep -rn '@source' src가 그 검사다.
  • 임포트된 서드파티 CSS. 게이트는 프로젝트 트리를 순회할 뿐 node_modules는 순회하지 않는다. 임포트한 벤더 스타일시트는 이전과 마찬가지로 전혀 검토되지 않는다.
  • 런타임 <style> 주입. 컴포넌트가 런타임에 문서에 써넣는 무엇이든 여기의 모든 게이트 밖에 있다.

Released under the MIT License.