Skip to content

fix(extractor): compile Tailwind classes one by one with v4 variants and declarations - #705

Merged
owjs3901 merged 2 commits into
mainfrom
fix/tailwind-classes
Oct 8, 2026
Merged

owjs3901 merged 2 commits into
mainfrom
fix/tailwind-classes

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

문제

devup 컴포넌트(Box 등) className의 Tailwind class를 빌드 타임에 변환하는 기능(기본 켜짐)이 다음 경우에 조용히 틀렸습니다(#687 필수 항목).

  • TW-01 알아본 class가 하나라도 있으면 className 전체를 생성 class로 다시 만들어, Tailwind가 정의하지 않는 class(card, prose, 분석용 hook 등)가 지워졌습니다. 템플릿·조건식에서는 부분 문자열 치환으로 my-p-4-class가 my-padding-…-class로 바뀌었습니다.
  • TW-02 모르는 variant(data-active:, not-hover:, max-md:, @sm:, [&>*]: 등)를 버리고 utility를 무조건 적용했습니다.
  • TW-04 선언이 여럿인 utility가 하나만 냈습니다(size-6→width만, rounded-t-lg→왼쪽 위만, truncate·sr-only·not-sr-only·text-sm 등).
  • TW-06 translate/rotate/scale/skew가 모두 transform 하나를 써서 서로 덮어썼고, -translate-x-[13px]의 부호가 사라졌습니다.
  • TW-10 임의 값을 문자열로 잘라 grid-cols-[200px_1fr]이 repeat(200px 1fr, …)가 됐고, \_, [property:value], bg-(--x), 타입 힌트가 동작하지 않았습니다.
  • TW-13 hover:focus:가 자손 선택자(.x:hover:focus .x)가 됐고, hover에 (hover: hover) 가드가 없었으며, group/peer가 Tailwind의 group/peer 표시 class를 쓰지 않았습니다.
  • 조사 중 추가로 찾은 버그: devup 컴포넌트의 템플릿 className에서 보간 앞뒤에 공백을 끼워 넣어 `icon-${name}`이 icon- ${name}, ${a}${b}가 ${a} ${b}가 됐습니다(Tailwind와 무관, main에서도 재현).

변경

  • class를 하나씩 변환합니다. variant와 utility를 모두 이해한 class만 그 스타일의 class들로 바꾸고, 나머지는 원문 그대로 둡니다. 템플릿에서 보간에 붙은 토큰(p-${size}, ${tone}-text)도 그대로 둡니다. 문자열 치환은 없앴습니다.
  • variant (Tailwind v4 기준, Tailwind CSS 4.3.3 출력과 대조)
    • 왼쪽부터 &에 대입해 겹칩니다: hover:focus: → &:hover:focus, *:hover: → :is(& > *):hover
    • hover(group-hover, peer-hover 포함)는 @media (hover: hover) 안에 둡니다
    • group-*/peer-* → &:is(:where(.group):hover *), &:is(:where(.peer):checked ~ *), 이름 있는 group-hover/item
    • 추가: data-*, data-[…], aria-*(불린 속성 9개), aria-[…], has-[…], has-<상태>, nth-*, *, **, supports-[…], [&…], [@media …]·[@supports …]·[@container …], optional, user-valid, user-invalid, inert, details-content, inverted-colors, noscript, pointer-*, any-pointer-*
    • v4에 맞춤: open → &:is([open], :popover-open, :open), rtl/ltr → &:where(:dir(rtl), …), selection/marker는 자손 포함(선택자마다 따로 규칙), before/after는 content: var(--tw-content) 추가
    • dark:는 결정대로 devup theme(:root[data-theme=dark] &), breakpoint는 devup 단계를 그대로 씁니다
    • 모르는 variant(not-*, in-*, max-*, min-[…], @container, starting, v4에 없는 screen 등)가 있으면 class 전체를 원문 그대로 둡니다
  • utility
    • 선언 전부: size-*, rounded-{t,r,b,l,s,e}[-*], truncate, sr-only, not-sr-only, text-{size}(v4 line-height, leading-*가 있으면 그쪽 우선), text-sm/6·/[1.7]·/tight·/(--x), leading-*(--tw-leading 함께)
    • translate/scale/skew는 --tw-* 변수와 translate·scale·transform 개별 속성으로, rotate는 rotate 속성으로 써서 함께 써도 합쳐집니다. 음수는 음수로 남습니다(임의 값은 calc(v * -1))
    • 쓰는 변수는 Tailwind처럼 @property로 등록합니다(상속 안 함, 초기값 있음). 전역 CSS에 한 번만 나갑니다(여러 파일이 써도 하나)
    • 임의 값: 괄호 짝 검사, _→공백(url() 안과 var()의 이름은 제외), \_→_, 수학 함수 안 연산자 주위 공백(Tailwind addWhitespaceAroundMathOperators 이식), 타입 힌트(length:, color: 등, 모르는 힌트는 원문 유지), [property:value], root-(--변수), 값 종류 추론(text-[2rem]→font-size, text-[#fff]→color, border-[3px]→width 등)
    • 접두어만 같으면 받아들이던 fallback을 없앴습니다: select-wrapper, order-summary, cursor-foo 같은 class와 align-*, whitespace-*, hyphens-*, tracking-*, touch-*, pointer-events-*, mix-blend-*, bg-blend-*, aspect-*, columns-*, col/row-start/end-*는 Tailwind 값일 때만 변환합니다
  • 템플릿 className 병합: 보간에 붙은 텍스트는 붙은 채로 두고, 서로 다른 class 목록 사이에만 공백 하나를 둡니다. 문자열을 템플릿에 넣을 때 \, `, ${를 escape합니다

동작 변화(의도한 것)

  • hover:는 hover할 수 있는 기기에서만 적용됩니다(v4와 같음)
  • group-hover: 등은 role="group"/data-group이 아니라 group class를 표시로 씁니다(Tailwind 마크업과 같음)
  • 측면 radius, text-* line-height, leading-*가 v4 값입니다
  • 위 fallback에 해당하던 class는 이제 원문 그대로 남습니다

새로 생기는 오류

없습니다. 이해하지 못한 class는 빌드 오류가 아니라 원문 유지입니다.

남은 항목(#687 권장)

space-*/divide-* 자식 선택자(TW-05), 기본 팔레트·theme 값(TW-08), filter 합성(TW-07), gradient(TW-09), !·/50 modifier(TW-11), keyframes(TW-12), 나머지 v4 utility(TW-14), clsx·배열(TW-15), 문서(TW-17), conformance 테스트(TW-18)

검증

  • 원래 근거 fixture(trigger-static, wrong-partial-replacement, wrong-variants, wrong-multideclaration, wrong-arbitrary)를 실제 WASM으로 probe: 모두 기대한 출력
  • Tailwind CSS 4.3.3으로 variant·utility·임의 값 출력을 만들어 선택자, @media (hover: hover), 선언 목록, 값 디코딩을 대조
  • cargo test --workspace 통과, cargo clippy --workspace --all-targets -- -D warnings 통과, cargo fmt
  • sheet 파이프라인 테스트: 두 파일이 써도 @property 한 번, translate 합성, ::before content, hover 가드 CSS 확인
  • bun test 5474 pass, 커버리지 100%

W40 main 반영 (2026-10-06)

  • 2026-10-06: main의 a935315c34b45aeda6dc156e29a5c76df51b76bd까지 병합했습니다 (병합 head: 031e8b542343419c37d160fa9851411bbc83193b).
  • 충돌 해결: libs/extractor/src/tailwind.rs는 feat(extractor): compile containers, masks, 3D transforms, functional @utility and class helpers #749 참조처럼 새 level/conditions/declarations 구조와 제거된 옛 헬퍼의 삭제를 유지했습니다. main의 Rust1.99 assertion, RSC 구현과 게시 버전을 보존했고 bun install로 lockfile 버전을 맞췄습니다.
  • 검증: CI 37423333964 success; publish/benchmark/landing-next-e2e/vinext-rsc-css-e2e 모두 통과, MERGEABLE/CLEAN. Ubuntu Rust100%(9890/9890), Bun5482 pass/0 fail 및 함수·라인100%, landing 두 모드130개씩 통과. 직렬 install/WASM/루트, Rust1.99 clippy 및 전체 훅 통과. 실제 WASM 출력: <div className="card a b c d e unknown:p-2" />; .a{padding:1rem}, .c{--tw-translate-x:1rem}, .d{translate:var(--tw-translate-x) var(--tw-translate-y)}, .e{rotate:45deg}, @media(hover:hover){.b:hover{background-color:#EF4444}}를 확인했습니다. Windows 원래 줄 형식 tarpaulin97.97%와 LSP 시간 초과는 기록했으며 Ubuntu 필수 gate는100%입니다.
  • 새로 생기는 오류: 이번 병합으로 추가되는 오류는 없습니다. 기존 PR의 동작 및 남는 한계는 유지합니다.

…and declarations

Classes Tailwind does not define stay in the className as written, a class with a variant the compiler does not understand is left unchanged instead of applying unconditionally, and utilities emit every declaration Tailwind v4 writes: multi-declaration utilities in full, translate/rotate/scale/skew through their own properties so they compose, and arbitrary values read with Tailwind's grammar. Template classNames keep text that runs into an interpolation attached to it.

Refs #687

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/wasm@1.0.83 → 1.0.84 - bindings/devup-ui-wasm/package.json

Patch

  • Tailwind classes in a className: each class compiles on its own and every class Tailwind does not define (custom, plugin and analytics classes) stays as written, where any recognized class used to rebuild the whole className without them and could rewrite part of another class; a class touching a template interpolation (icon-${name}) is left as written, and template classNames no longer get spaces inserted around interpolations. A class whose variant is not understood stays as written instead of applying unconditionally; variants follow Tailwind v4: stacked variants compose (hover:focus: matches the element itself), hover is guarded by @media (hover: hover), group-/peer- use the group/peer marker classes (and group-hover/name), and data-, aria-, has-, nth-, , **, supports-[…] and [&…]/[@media …] variants compile. Utilities emit all their declarations (size-, rounded-t-, truncate, sr-only, not-sr-only, text-sm with its line height, text-sm/6, leading-), translate/rotate/scale/skew compose through Tailwind's individual properties and registered variables (@Property) with negative values kept negative, and arbitrary values follow Tailwind's grammar: spaces for underscores except in url() and var() names, _ for an underscore, spaces around math operators, type hints, [property:value] and (--variable) shorthands. Words that only share a utility's prefix (select-wrapper, order-summary, cursor-foo) are no longer compiled

@devup-ui/bun-plugin@1.0.22 → 1.0.23 - packages/bun-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/wasm' via a local workspace dependency

@devup-ui/components@0.1.60 → 0.1.61 - packages/components/package.json

Patch

  • Auto-update: depends on '@devup-ui/vite-plugin' via a local workspace dependency

@devup-ui/next-plugin@1.0.90 → 1.0.91 - packages/next-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/wasm' via a local workspace dependency

@devup-ui/rsbuild-plugin@1.0.67 → 1.0.68 - packages/rsbuild-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/wasm' via a local workspace dependency

@devup-ui/vite-plugin@1.0.73 → 1.0.74 - packages/vite-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/wasm' via a local workspace dependency

@devup-ui/webpack-plugin@1.0.71 → 1.0.72 - packages/webpack-plugin/package.json

Patch

  • Auto-update: depends on '@devup-ui/wasm' via a local workspace dependency

@codecov

codecov Bot commented Oct 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
libs/extractor/src/lib.rs 100.00% <ø> (ø)
libs/extractor/src/prop_modify_utils.rs 100.00% <100.00%> (ø)
libs/extractor/src/tailwind.rs 100.00% <ø> (ø)
libs/sheet/src/lib.rs 100.00% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Resolve libs/extractor/src/tailwind.rs by preserving the new TailwindClass declaration/condition structure and the removal of the obsolete parser helper, matching the main integration in #749. Keep the Rust 1.99 assertion fixes and regenerate bun.lock workspace metadata for main published versions.

Refs #687

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
owjs3901 added a commit that referenced this pull request Oct 6, 2026
Merge #705 head 031e8b5, which contains main a935315. Resolve libs/css/src/theme_tokens.rs by retaining the color/shadow token test and the Rust 1.99 typography assertion together, byte-for-byte matching #749; preserve all main integration and published versions.

Refs #687

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@owjs3901
owjs3901 merged commit 11790be into main Oct 8, 2026
5 checks passed
@owjs3901
owjs3901 deleted the fix/tailwind-classes branch October 8, 2026 05:56
owjs3901 added a commit that referenced this pull request Oct 8, 2026
No conflicted files. The exact original #705 head 031e8b5 is already an ancestor; retain its implementation with the ours strategy.
owjs3901 added a commit that referenced this pull request Oct 8, 2026
Resolved libs/extractor/src/tailwind.rs by retaining #749's nested declarations alongside #705's per-class v4 compilation and #734/#739 theme and utility integration.

Resolved packages/bun-plugin/src/plugin.ts by retaining #703's SourceLoader/scanner/build stylesheet emission and #749's Tailwind watcher with its returned file list. Resolved packages/rsbuild-plugin/src/plugin.ts by combining #704's synchronous complete stylesheet helpers with #749's stat-based Tailwind watch invalidation.

Resolved packages/rsbuild-plugin/src/__tests__/checkout-isolation.test.ts and plugin.test.ts by retaining #704's onBeforeBuild/rootPath setup alongside all Tailwind watcher tests. After reproducing four failing stylesheet tests, their shared test setup now selects the second Rspack config callback (stylesheet completion), while the first remains the Tailwind watcher. No snapshots or lockfiles were hand-merged, no PR intent was removed, and #734/#739/#749 remain separate PRs.

Refs #687

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant