Skip to content

fix: vanilla-extract/Emotion/styled-components compat, global layers and typography precedence - #677

Open
owjs3901 wants to merge 32 commits into
mainfrom
fix/compat-layer-and-typography-bugs
Open

owjs3901 wants to merge 32 commits into
mainfrom
fix/compat-layer-and-typography-bugs

Conversation

@owjs3901

@owjs3901 owjs3901 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

#675 → #676 → 이 PR 순서로 머지해 주세요. CI가 main 대상 PR에서만 돌기 때문에 base를 main으로 열었습니다. diff에는 앞 PR 커밋이 함께 보이고, 이 PR의 변경은 마지막 4개 커밋입니다.

문제

미검증 후보로 남겨 둔 버그를 모두 재현해서 확인했습니다. 확인하는 과정에서 같은 계열 버그도 함께 찾았습니다.

  1. Emotion·styled-components 객체 스타일의 숫자가 ×4 스페이싱으로 해석됨
    • 예: css({ padding: 8 })가 32px로 나옴(원래 8px)
    • 같은 계열 버그:
      • styled('div', { shouldForwardProp })(...)가 컴포넌트 대신 React 요소를 export함(런타임 크래시)
      • styled.div(a, b)는 변환되지 않음
      • styled.div([a, b])는 스타일이 사라짐
      • css(a, b)는 ""가 됨
  2. VE 이름이 파일 간에 충돌
    • createVar는 --var-0, createContainer는 __container_0__로 고정되어 파일마다 같은 이름이 나옴
    • layer()의 값이 "undefined"가 됨
    • 테마 변수 이름도 파일 간에 충돌함. 예: 두 파일의 createGlobalTheme이 모두 --color-brand-global_theme_0-0을 만들어 서로 덮어씀
  3. VE 추가 버그
    • styleVariants: 같은 파일 안에서 tone.primary가 undefined라서 조합과 셀렉터가 깨짐. map 함수 형태를 무시함
    • 이름을 호출 순서로 매칭함. 그래서 { sm: style(...) }처럼 중첩된 호출이 있으면 그 뒤의 스타일이 전부 엉뚱한 이름에 붙음
    • createThemeContract export는 ReferenceError가 남
    • createVar()가 var()로 감싸지 않은 이름을 반환해서 `calc(${v} * 2)`가 깨짐
    • createTheme(tokens, debugId), 인자 3개짜리 createGlobalTheme, 대입하지 않은 createGlobalTheme이 무시됨
    • 조합이 한 단계만 이어짐
    • '@layer': {...}를 무시함
    • 레이어 선언 순서가 알파벳순으로 정렬됨
    • 가져온 쪽이 모두 사용되지 않는 .css.ts에서 TypeScript 변환이 export {};를 남겨 실행이 실패함
  4. 전역 @layer와 반응형을 함께 쓰면 레이어가 사라짐
    • 반응형 값에서는 @layer가 통째로 사라짐
    • 시트에서는 레이어 안 선언의 breakpoint 레벨이 누락됨
  5. typography
    • globalCss({ h1: { typography, fontSize } })에서 프리셋이 명시한 값을 모든 breakpoint에서 이김
    • 반응형 typography와 셀렉터 안 typography는 더 넓은 breakpoint에서 명시값을 이김

수정

  • vanilla-extract(vanilla_extract.rs 재작성)
    • 실행한 뒤 각 top-level 변수가 실제로 가진 값을 보고 스타일 이름을 붙입니다. 중첩 호출은 _veN으로 이름 붙이고, 일반 데이터 export는 값을 그대로 코드로 냅니다.
    • 생성되는 모든 이름을 {debugId}-{파일번호}-{n} 형식으로 파일마다 고유하게, 결정론적으로 만듭니다.
    • createVar/fallbackVar/createTheme/createGlobalTheme/contract/layer 동작을 VE와 똑같이 맞췄습니다.
    • globalLayer, createGlobalThemeContract, assignVars, globalFontFace, @property 선언을 추가했습니다.
    • variant를 일반 스타일로 처리하고 조합을 여러 단계까지 이어서 적용합니다.
  • Emotion·styled-components
    • 객체 스타일의 숫자를 px로 바꿉니다. 단, Devup 단축 prop(p, bg 등)은 스케일을 유지합니다.
    • 여러 인자와 배열을 병합합니다. 뒤 값이 이기고, 중첩 규칙은 합칩니다.
    • styled(tag, options)를 팩토리로 인식합니다.
    • css()가 클래스 참조와 규칙 객체를 함께 조합합니다.
  • layer
    • '@layer': { [name]: rules } 레코드 형태를 지원합니다.
    • 전역 @layer가 중첩·반응형 선언까지 적용됩니다.
    • 시트는 레이어 안의 선언도 일반 경로와 같은 writer로 출력합니다(레벨, 셀렉터, at-rule 유지).
  • 커스텀 속성
    • --* 값의 숫자를 스페이싱 스케일로 바꾸지 않고 그대로 둡니다(예: --columns: 4가 16px이 되던 문제).
  • typography
    • 같은 셀렉터에 명시한 속성이 있으면, 그 속성이 시작하는 breakpoint부터 프리셋 선언을 건너뜁니다.

테스트

  • 새 테스트
    • VE 단위 테스트 14개
    • layer·typography 파이프라인 테스트
    • Emotion·styled-components 스냅샷과 단위 테스트
  • 변경된 스냅샷은 모두 직접 검토했습니다.
  • 검증 결과
    • cargo fmt, clippy -D warnings(기본 빌드와 lite wasm) 통과
    • cargo test --workspace 통과
    • bun test 5231 pass(커버리지 100%), 회귀 테스트 6 pass
  • 재빌드한 wasm으로 위 버그를 모두 다시 재현해서 고쳐진 것을 확인했습니다.

참고 (아직 남은 한계)

  • .css.ts끼리 import하는 경우는 여전히 지원하지 않습니다. 모듈 로더가 필요해서 별도로 설계해야 합니다.
  • styled-components의 .attrs()/.withConfig()와, cond && style 같은 조건부 조합 조각은 지원하지 않습니다.
  • 파일 하나에 styled import가 여러 개 있으면 마지막 것만 인식합니다(기존 한계).

커버리지

  • libs/extractor/tests/vanilla_extract.rs 통합 테스트로 VE API 전체를 라이브러리 빌드를 거쳐 실행합니다.
  • CollectedStyles::resolve의 selector 인자를 impl Fn에서 fn 포인터로 바꿔 단형화된 복사본이 하나만 생기게 했습니다. CI는 rustfmt max_width = 100000으로 다시 포맷한 뒤 tarpaulin을 돌리기 때문에, 보고된 줄 번호(801)가 로컬 소스가 아니라 이 분기를 가리키고 있었습니다.

owjs3901 and others added 30 commits September 27, 2026 00:39
…cking

Boa's Context::default() reads std::time::Instant, which panics on wasm32-unknown-unknown without the js feature. Every .css.ts extraction crashed in the full WASM; the lite build is unaffected. Add a wasm-bindgen test and a bun regression that runs in CI.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…en-prefix rules

prefer-media-shorthand suggests _motionReduce, _print and the other media shorthands for _media entries and @media keys that spell out the same query. no-typography-token-prefix reports typography=$heading, which names no preset. Both are auto-fixable and part of the recommended config.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Media shorthand queries, at-rule key parsing, query normalization, media query combination (merge, never-match, nest) and the deterministic order at-rule blocks are emitted in. StyleSelector::At now carries its enclosing at-rules and owning file.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…xtract references

Nest _print, _media and @media keys with selectors in both directions (props, globalCss, template literals, Tailwind variants, StyleX conditions), apply typography conditionally, and keep responsive globalCss values in the base layer. vanilla-extract selectors, values and globalStyle now resolve references to styles, keyframes, fonts and themes, and createVar/fontFace/layer exports no longer shift onto each other.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Rules order by selector group before breakpoint level and at-rules follow every plain rule; globalCss and custom layers keep SELECTOR_ORDER instead of sorting selectors alphabetically. Adjacent rules with the same selector share one block, and a declaration written next to a conditional typography preset wins over it.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add _motionReduce, _motionSafe, _portrait, _landscape, _contrastMore, _contrastLess and _forcedColors, accept @media/@supports/@container keys, allow top-level at-rules in globalCss, and drop the never-matching _speech.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
typography takes the bare preset key, so the examples no longer write typography=$heading.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… units

vanilla-extract styles add px to numbers (in .css.ts files and in style/globalStyle/keyframes calls of ordinary modules) and StyleX adds px/ms to static and dynamic numbers, instead of Devup UI's spacing scale. The unitless property list now matches vanilla-extract's, so strokeWidth, columns, zoom, orphans and similar keep bare numbers everywhere.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…gine

Remove the static .css.ts fast path so development and production builds evaluate stylesheets the same way and produce the same class names and CSS.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ponents object styles

vanilla-extract: name styles after the variable holding them instead of by call order, generate file-unique names for vars, containers, layers, fonts and theme variables, follow vanilla-extract's createVar/fallbackVar/createTheme/createGlobalTheme/contract/layer semantics, resolve styleVariants variants and transitive composition, and export plain values as data. Emotion and styled-components: numbers in object styles mean pixels, several style arguments or arrays merge, styled(tag, options) is a factory, and css() composes class names passed with rule objects. '@layer' records and a global '@layer' cover nested and responsive declarations, and typography presets yield to declarations written beside them.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Layered properties of any kind are written with the regular writer, one layer at a time, so a layer keeps breakpoints, selectors and at-rules. A typography atom skips the declarations its value lists as written beside it from the breakpoint they start at.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…graphy precedence

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The loop left a region tarpaulin maps past the function on Linux; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Tarpaulin's Linux report still attributed a line past the function to its closures; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The vec! expansion is the region tarpaulin maps past the function on Linux; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Reading a rule list cannot fail, so the error path tarpaulin kept reporting as uncovered past the function is gone; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Its output is asserted by test_font_faces; tarpaulin's Linux report maps the loop exit onto a line it never counts however the loop is written.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Shaped like compose so tarpaulin's Linux report maps its loop, and the coverage exclusion is gone again; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ctor

The callers store the rules, so the helper ends in its value; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Tarpaulin merges the unit-test build and the library build that other targets link; the latter never ran the font-face helper, which left one region at zero. The helper is split into single-rule and list parts.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
A number on a custom property was read as the spacing scale (--columns: 4 became 16px), which broke theme tokens and var() math. The vanilla-extract API now also runs through the library build in an integration test.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…function

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
owjs3901 and others added 2 commits September 27, 2026 16:05
resolve took impl Fn, so the always-selector caller got its own copy whose plain class-name branch never runs; CI coverage reported that branch as uncovered. A fn pointer keeps one copy. createVar returns to its original shape.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… the library

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

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

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/wasm@1.0.81 → 1.0.82 - bindings/devup-ui-wasm/package.json

Patch

  • Fix at-rule composition and cascade: _print/_media/@media … nest correctly with selectors in both directions and inside globalCss, nested media queries merge (print and (…)), breakpoints put media types first, distribute query lists, and nest not … queries, and at-rules are emitted after all breakpoint values while pseudo selectors keep SELECTOR_ORDER across breakpoints. Add _motionReduce, _motionSafe, _portrait, _landscape, _contrastMore, _contrastLess and _forcedColors, remove the never-matching _speech, normalize query whitespace, support Emotion-style '@media …'/'&:hover' object keys, fix template literals losing declarations before @media or selectors around nested at-rules, apply typography conditionally under selectors, and drop stale global at-rules on re-extraction
  • Fix vanilla-extract stylesheets: styles are named after the variable holding them, so style calls nested in objects no longer shift every later name onto the wrong styles and such objects export their classes; createVar, createContainer, layer, fontFace and theme variables get names unique to each stylesheet instead of colliding across files, createVar returns var() so template literals and fallbackVar work, createTheme(tokens, debugId), the three-argument createGlobalTheme and unassigned createGlobalTheme calls produce their variables, createThemeContract exports, styleVariants variants resolve in selectors and compositions and accept a map function, composition keeps transitive bases, '@layer' records, globalLayer, createGlobalThemeContract, assignVars and globalFontFace work, and layers keep their declaration order. Emotion and styled-components object styles read numbers as pixels (Devup UI shorthands keep the spacing scale), compose several style arguments or arrays, and accept Emotion's styled(tag, options) factory; css() composes class names passed with rule objects. A global '@layer' now covers responsive, selector and at-rule declarations, and layered declarations keep their breakpoints. Custom properties keep their value instead of reading a number as the spacing scale. A typography preset no longer overrides a property written beside it at a wider breakpoint, in components and globalCss
  • Fix the full WASM build panicking on every vanilla-extract .css.ts file (Boa read the clock without its js feature), resolve vanilla-extract references to other styles, keyframes, fonts and themes in selectors, values and globalStyle, keep globalCss pseudo selectors in SELECTOR_ORDER across breakpoints, merge adjacent rules with the same selector, and let declarations written next to a conditional typography preset win over it. Add the prefer-media-shorthand and no-typography-token-prefix ESLint rules
  • Numbers in vanilla-extract and StyleX styles keep their library's meaning instead of Devup UI's spacing scale: vanilla-extract styles add px in .css.ts files and in style(), globalStyle() and keyframes() calls inside ordinary modules, StyleX adds px/ms to static values and to numbers passed to dynamic styles, and unitless properties such as columnCount, strokeWidth, orphans or zoom keep a bare number everywhere. The Next.js plugin now runs every .css.ts module on the full engine instead of a separate static fast path, so development and production builds produce the same output

@devup-ui/bun-plugin@1.0.20 → 1.0.21 - packages/bun-plugin/package.json

Patch

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

@devup-ui/components@0.1.58 → 0.1.59 - packages/components/package.json

Patch

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

@devup-ui/eslint-plugin@1.0.20 → 1.0.21 - packages/eslint-plugin/package.json

Patch

  • Fix the full WASM build panicking on every vanilla-extract .css.ts file (Boa read the clock without its js feature), resolve vanilla-extract references to other styles, keyframes, fonts and themes in selectors, values and globalStyle, keep globalCss pseudo selectors in SELECTOR_ORDER across breakpoints, merge adjacent rules with the same selector, and let declarations written next to a conditional typography preset win over it. Add the prefer-media-shorthand and no-typography-token-prefix ESLint rules

@devup-ui/next-plugin@1.0.88 → 1.0.89 - packages/next-plugin/package.json

Patch

  • Numbers in vanilla-extract and StyleX styles keep their library's meaning instead of Devup UI's spacing scale: vanilla-extract styles add px in .css.ts files and in style(), globalStyle() and keyframes() calls inside ordinary modules, StyleX adds px/ms to static values and to numbers passed to dynamic styles, and unitless properties such as columnCount, strokeWidth, orphans or zoom keep a bare number everywhere. The Next.js plugin now runs every .css.ts module on the full engine instead of a separate static fast path, so development and production builds produce the same output

@devup-ui/react@1.0.43 → 1.0.44 - packages/react/package.json

Patch

  • Fix at-rule composition and cascade: _print/_media/@media … nest correctly with selectors in both directions and inside globalCss, nested media queries merge (print and (…)), breakpoints put media types first, distribute query lists, and nest not … queries, and at-rules are emitted after all breakpoint values while pseudo selectors keep SELECTOR_ORDER across breakpoints. Add _motionReduce, _motionSafe, _portrait, _landscape, _contrastMore, _contrastLess and _forcedColors, remove the never-matching _speech, normalize query whitespace, support Emotion-style '@media …'/'&:hover' object keys, fix template literals losing declarations before @media or selectors around nested at-rules, apply typography conditionally under selectors, and drop stale global at-rules on re-extraction

@devup-ui/reset-css@1.0.30 → 1.0.31 - packages/reset-css/package.json

Patch

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

@devup-ui/rsbuild-plugin@1.0.65 → 1.0.66 - packages/rsbuild-plugin/package.json

Patch

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

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

Patch

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

@devup-ui/webpack-plugin@1.0.69 → 1.0.70 - packages/webpack-plugin/package.json

Patch

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

@codecov

codecov Bot commented Sep 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
libs/css/src/at_rule.rs 100.00% <100.00%> (ø)
libs/css/src/lib.rs 100.00% <100.00%> (ø)
libs/css/src/style_selector.rs 100.00% <100.00%> (ø)
libs/css/src/theme_tokens.rs 100.00% <100.00%> (ø)
libs/extractor/src/css_utils.rs 100.00% <100.00%> (ø)
...xtractor/src/extract_style/extract_static_style.rs 100.00% <100.00%> (ø)
.../extractor/extract_global_style_from_expression.rs 100.00% <100.00%> (ø)
...tor/src/extractor/extract_style_from_expression.rs 100.00% <100.00%> (ø)
...tractor/src/extractor/extract_style_from_styled.rs 100.00% <100.00%> (ø)
...tractor/src/extractor/extract_style_from_stylex.rs 100.00% <100.00%> (ø)
... and 19 more
🚀 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.

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