Skip to content

fix(extractor): scope nested selectors, keep content strings and reject selectors that select nothing - #702

Open
owjs3901 wants to merge 2 commits into
mainfrom
fix/selectors
Open

owjs3901 wants to merge 2 commits into
mainfrom
fix/selectors

Conversation

@owjs3901

Copy link
Copy Markdown
Contributor

문제

selector와 관련해 CSS가 잘못 나오거나 아무 효과 없는 CSS가 나오는 문제 5건(#685 필수 항목)입니다.

  • SC-08: styled 템플릿의 .child { }, > p { }가 컴포넌트 class 없이 전역 규칙 .child{…}, > p{…}로 나왔습니다. 관계없는 요소까지 스타일이 적용되고, > p처럼 결합자로 시작하는 규칙은 무효입니다.
  • SC-09: content: "x"가 content:x로, content: ""가 content:로 따옴표를 잃었습니다. 둘 다 브라우저가 버리는 값입니다. font-family용 따옴표 제거 최적화가 content에도 적용된 것이 원인입니다.
  • JSX-04: _groupNthChild={{ params: ['2n'] }}가 :is(...):nth-child .a(2n)처럼 params를 class 뒤에 붙여 무효 selector가 됐습니다.
  • JSX-05: 타입이 허용하는 문자열 selector 값(_hover="external-class")이 아무 오류 없이 사라졌습니다.
  • TOOL-06: selectors={{ definitelyNotASelector: … }}가 .a:definitely-not-a-selector처럼 아무것도 선택하지 않는 CSS를 만들었습니다.

조사하면서 같은 원인으로 생기는 문제를 추가로 발견해 함께 고쳤습니다.

  • selectors의 'div p', '> p' 키가 &:div p, &:> p처럼 무효 selector가 됐습니다.
  • 부모 selector 아래에 둔 bare 키(hover, [data-x])가 부모를 잃고 전역 selector가 됐습니다.
  • groupHover, themeDark 같은 camelCase bare 키가 :group-hover로 나왔습니다.
  • '.myClass' 키가 .my-class로 바뀌었습니다.
  • 목록 selector 안에 중첩한 규칙이 첫 selector 아래에만 적용됐습니다(h1, div { span {} }).
  • _part, _slotted 등 pseudo-element가 콜론 하나로 나왔습니다.

변경

  • 중첩 규칙 (styled-components·Emotion 규칙): &가 없는 selector는 규칙 기준으로 해석합니다.
    • :·::로 시작하면 해당 요소 자신에 적용합니다(.a:hover).
    • 그 외에는 자손을 선택합니다(.a .child, .a > p).
    • 목록은 selector마다 따로 해석합니다(h1, &:after → .a h1,.a:after).
    • 목록 안에 중첩한 규칙은 목록의 selector마다 적용합니다(stylis와 같은 곱).
  • content: 따옴표 제거 최적화 대상에서 뺐습니다. 작성한 문자열이 그대로 나옵니다.
  • params: 인자를 받는 pseudo 중 아직 인자가 없는 마지막 것에 붙입니다. _hover 안에 중첩한 group 형태나 _not(.x) 뒤의 _nthChild에도 맞게 붙습니다.
  • selectors bare 키
    • hover, focusVisible 같은 이름은 pseudo-class입니다. 알려지지 않은 이름은 빌드 에러입니다.
    • :·[로 시작하면 요소 자신에 적용합니다.
    • 그 외에는 자손 selector입니다.
    • 부모 selector 아래에서도 같은 규칙을 적용하고, class 이름의 대소문자를 유지합니다.
  • 이름 확인: _name props, selectors의 _name 키, globalCss의 _name 키가 CSS pseudo-class·pseudo-element(csstype-extra 기준), 그 group 형태, theme-*, 미디어 축약형 중 하나인지 확인합니다.
  • 문자열 값: selector 아래의 문자열은 CSS 텍스트로 읽습니다. 선언이 하나도 없는 문자열은 빌드 에러입니다. false·null은 기존처럼 "없음"으로 처리합니다.
  • 문서: Selector 문서에 params, selectors의 키 규칙, 중첩 규칙, 문자열 값 규칙을 추가했습니다.

새로 생기는 오류

모두 파일:줄:열과 해당 코드를 함께 알려줍니다.

  1. 아무것도 선택하지 않는 selector 이름
    • 예: <Box _notASelector={…}>, selectors={{ definitelyNotASelector: … }}, selectors={{ div: … }}, globalCss({ _nope: … })
    • 메시지: cannot use `_notASelector` at build time: a selector key names a pseudo-class or pseudo-element, as `_hover` or `hover`, or is a selector, as `&:hover`, `& > p` or `.parent &`
    • 해결: pseudo-class 이름을 고치거나 '& div', '& > p'처럼 selector로 씁니다. vendor pseudo는 '&:-webkit-autofill'처럼 씁니다.
  2. CSS 텍스트가 아닌 문자열 selector 값
    • 예: _hover="external-class", _hover={[{ … }, 'external-class']}, css({ _focus: 'red' })
    • 메시지: cannot use `"external-class"` at build time: a selector takes styles, an object such as `{ color: 'red' }` or CSS text such as `color: red`
    • 해결: 객체나 CSS 텍스트로 씁니다.

저장소의 앱·컴포넌트·벤치마크·e2e 소스 202개를 새 WASM으로 추출했을 때 새 오류가 난 파일은 없습니다.

출력 변화

  • 템플릿에서 & 없이 쓴 중첩 규칙이 전역에서 컴포넌트 범위로 바뀝니다. 이전에는 관계없는 요소에도 적용됐습니다.
  • content 값이 작성한 따옴표 그대로 나옵니다('hello world'는 작은따옴표를 유지).

검증

  • cargo clippy --workspace --all-targets -- -D warnings, cargo fmt --check, cargo test --workspace 통과
  • 추가 테스트
    • sheet test_nested_selector_pipeline: 중첩·목록·content·params·selectors 키·false 값의 CSS 출력
    • extractor test_selectors_that_select_nothing_are_errors: 9개 경우의 오류 메시지와 위치
    • css test_split_selector_list, test_is_selector_name, test_nest_selector(목록 부모), test_add_selector_params(5개 경우)
  • WASM을 다시 빌드한 뒤 bun test 5474 pass, 커버리지 100%
  • probe 재현 결과
    • 70-child-selector.tsx: .b .child{color:red}.a > p{color:blue}
    • 39-before-content.tsx: content:"→", content:'hello world'
    • 28-advanced-selectors.tsx: :is([role=group],[data-group]):nth-child(2n) .g
    • 30-selector-values.tsx: 클래스 문자열 두 곳이 빌드 에러, false는 오류 없음

Refs #685

owjs3901 and others added 2 commits October 1, 2026 01:02
…ct selectors that select nothing

A nested rule without & in a template became a global rule, content lost the quotes of its strings, params of a group selector went after the class, and a selector key or string value that selects nothing gave dead CSS or vanished. Nested rules are now relative to their rule as in styled-components and Emotion, content keeps its strings, params go on the pseudo-class that takes them, and unknown selector names and strings that are not CSS text are build errors.

Refs #685

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Bare selectors keys no longer reach SelectorSeparator::Space through StyleSelector::from, so test it where it is defined.

Refs #685

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.82 → 1.0.83 - bindings/devup-ui-wasm/package.json

Patch

  • globalCss: theme tokens ($text) resolve to var(--text) in global rules instead of being written as invalid $text; imports given as { url, query } objects with unquoted keys are emitted instead of dropped; an '@layer name' key puts the rules it holds in that cascade layer (layers nest as base.reset) instead of flattening them into invalid declarations, and a layer name that is not one identifier is a build error
  • Selectors: a nested rule without & in a styled or css template is scoped to the component instead of becoming a global rule (.child {} and > p {} select descendants, :hover {} and ::before {} the component), and a rule nested in a selector list applies under each of its selectors; content keeps the quotes of its strings (content: 'x', content: ""); params go on the pseudo-class that takes them, so _groupNthChild gives :is(...):nth-child(2n) .a also when nested; a bare selectors key is a pseudo-class name, an attribute of the element or a descendant selector ('div p', '> p'), under a parent too, and keeps the case of class names; ::part(), ::slotted(), ::highlight(), ::picker() and the view-transition pseudo-elements get two colons. New build errors: a _name prop or selector key that names nothing (_notASelector, selectors={{ div: ... }}), and a string under a selector that is not CSS text (_hover="external-class")
  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

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

Patch

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

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

Patch

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

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

Patch

  • no-duplicate-value, no-useless-responsive, no-useless-tailing-nulls, no-typography-token-prefix and prefer-media-shorthand only report and fix values the build reads as styles: style props of Box, Flex and the other style components and the arguments of css, globalCss and keyframes, through style objects, responsive arrays, conditions and spreads. Arrays and keys in props the component passes through (data-, aria-, event handlers, HTML attributes, props, styleVars), in arguments of other functions and under imports/fontFaces/params are left alone, where autofix used to rewrite them; styles of a component nested in another's prop are checked too. css-utils-literal-only reads a css() or keyframes() result held in a const of any scope as static, as the build does

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

Patch

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

@devup-ui/plugin-utils@1.0.16 → 1.0.17 - packages/plugin-utils/package.json

Patch

  • Theme CSS: a color only one variant defines is defined in that variant; a variant renders dark only when it is named dark or listed as dark in the new theme.colorScheme, others light, and light-dark() only encodes a light default with one dark variant; theme names that are not CSS identifiers are quoted in [data-theme]; typography emits fontStyle and textTransform; color variables come out in a fixed order. $tokens with dashes (text-primary) resolve instead of breaking into var(--text)-primary, and dotted length and shadow names are declared with dashes like their references. registerTheme reports, with the theme path, a token name the $token syntax cannot reach and two tokens of a variant that become the same CSS variable (a-b and a.b)

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

Patch

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

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

Patch

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

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

Patch

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

@codecov

codecov Bot commented Sep 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
libs/css/src/constant.rs 100.00% <ø> (ø)
libs/css/src/lib.rs 100.00% <100.00%> (ø)
libs/css/src/optimize_multi_css_value.rs 100.00% <ø> (ø)
libs/css/src/selector_separator.rs 100.00% <ø> (ø)
libs/css/src/style_selector.rs 100.00% <100.00%> (ø)
libs/extractor/src/css_utils.rs 100.00% <100.00%> (ø)
.../extractor/extract_global_style_from_expression.rs 100.00% <100.00%> (ø)
...src/extractor/extract_keyframes_from_expression.rs 100.00% <100.00%> (ø)
...tor/src/extractor/extract_style_from_expression.rs 100.00% <100.00%> (ø)
.../extractor/extract_style_from_member_expression.rs 100.00% <ø> (ø)
... and 5 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