Skip to content

fix(extractor): preserve conditional-spread precedence in styled objects - #776

Merged
owjs3901 merged 2 commits into
mainfrom
fix/conditional-spread-precedence
Oct 14, 2026
Merged

owjs3901 merged 2 commits into
mainfrom
fix/conditional-spread-precedence

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Refs #682, #695, #689, #693 (2026-10-11 사용자 결정 Q2, Q6). main의 11790bef에서 독립적으로 수정하며, 다른 PR의 선행 병합은 필요하지 않습니다.

요약

styleOrder가 없는 하나의 styled() 스타일 객체에서 조건부 스프레드 뒤에 작성한 명시 속성이 먼저 작성한 값을 덮어쓰도록 수정합니다. 객체 키의 대체는 JavaScript처럼 얕게 처리하며, CSS 클래스 문자열 순서나 전역 스타일시트 순서를 바꾸는 방식으로 해결하지 않습니다.

styled-components/Emotion 마이그레이션 문서에는 런타임에만 알 수 있는 클래스명을 반환하는 styled 콜백이 허용되지 않고, 알려진 클래스나 규칙 객체 사이의 조건 선택은 지원된다는 짧은 안내를 추가합니다. 변경팩은 Patch입니다.

동작

const Card = styled('div', {
  ...(cond ? { color: 'red' } : { color: 'purple' }),
  color: 'orange',
})

기존에는 명시한 orange 클래스와 조건부 스프레드의 클래스가 함께 남아 CSS 정렬 결과에 따라 앞서 작성한 값이 이겼습니다. 수정 후에는 cond의 두 결과 모두 나중에 명시한 orange가 이깁니다. 반대로 명시 속성 뒤에 스프레드가 있으면, 선택된 스프레드가 그 키를 가진 경우에만 뒤의 값이 이기고 키가 없으면 앞의 값이 유지됩니다.

  • 여러 스프레드, 스프레드 사이의 명시 속성, 중첩 선택자에서도 같은 키 대체 규칙을 적용합니다.
  • 선택자 객체와 반응형 배열은 이전 값 전체를 대체합니다. 서로 다른 스타일 인자 사이의 기존 중첩 규칙 합성은 바꾸지 않습니다.
  • 스타일링 런타임이나 새 의존성은 추가하지 않습니다. 조건 선택에 필요한 값은 기존 렌더 함수 안에서 순서대로 한 번씩 읽습니다.

소유권 확인: PR #756 (fix/js-semantics, f2fc4517)을 개인 임시 디렉터리에 git archive로 내보내 새 WASM을 빌드한 뒤, 정확한 W38m 소스(711바이트, SHA256 11ce25b30f6bf23d339975ddb7b1812093dce4c16d931524f53b94486c8d85bf)를 검증했습니다. main과 #756 모두 single CSS / per-file CSS에서 두 요소의 계산 색이 rgb(255, 0, 0)이므로 #756의 이미 해결된 변경과 겹치지 않습니다.

수정 전 정확한 W38m 출력에서 선택한 클래스는 f i l a c d g c d j이고, 루트 CSS는 .i{color:orange}.f{color:purple}.d{color:red} 순서입니다. 클래스 토큰 순서가 아닌 실제 Chromium 계산 스타일로 빨간색 우선순위를 확인했습니다.

수정 후 같은 W38m 소스의 실제 선택 클래스는 두 요소 모두 g j a c e h이며, 루트 선언은 .g{color:orange}만 남습니다. 나머지 실제 CSS는 .j:hover{color:blue}.a:focus{color:green}.b:focus{color:navy}.f:active{color:orange}.e:active{color:yellow}.h:disabled{color:gray}.i:disabled{color:pink}.d:visited{color:black}.c:visited{color:silver}입니다. single CSS와 per-file CSS 모두 계산 색은 rgb(255, 165, 0), hover는 rgb(0, 0, 255)입니다.

첫 최종 검토에서 추가로 발견한 세 경계도 함께 회귀 검증했습니다: 기존에 읽을 수 있던 정적 템플릿 키, 부호가 있는 숫자·문자열·템플릿의 shorthand 배율 보존, 합성 클래스의 원래 식별자와 생성 바인딩의 충돌 방지. 렌더 시 p: +2는 8px, m: -2는 -8px이며, 원래 합성한 클래스도 두 조건 결과 모두 유지됩니다.

새로 생기는 오류

없습니다. 다음은 새 오류가 아니라 기존에 의도적으로 거부하던 콜백의 위치가 포함된 진단이며, 문서에만 명시합니다.

test.tsx:2:1: Cannot compose `(p) => { return p.cls; }` at build time: each style must be a rule object, a class, or a condition choosing between them

남는 한계

main과 #756에서 styled 객체의 효과가 선언 시점이 아니라 렌더마다 반복되는 현상은 별도 생성 코드 원인입니다. 이번 변경은 그 평가 시점을 옮기지 않으며, 정확한 W38m의 기존 효과 순서 및 횟수를 보존합니다: built,get,before,spread,between,spread,after,get,before,spread,between,spread,after.

검증

  • 수정 전 Rust 회귀: 12개 중 예상한 8개 실패, 무조건 스프레드와 기존 콜백 진단 4개 통과.
  • Chromium 153: CSS 모드마다 동일한 104개 조합을 다시 검증하여 54개 실패에서 0개 실패로 바뀌었습니다. 실제 button의 rest/hover/focus/active/disabled 10개 관찰과 추가 경계 8개 관찰도 모드마다 모두 통과했습니다.
  • cargo fmt --all, Rust 1.99 clippy --workspace --all-targets -- -D warnings, 전체 직렬 Rust 테스트, fresh WASM 및 루트 빌드 통과.
  • bun test: 5,498개 통과, 0개 실패, 함수·라인 커버리지 모두 100%. bun lint: 0개 오류, 기존 경고 2개만 남습니다.
  • 기존 스냅샷은 모두 변경하지 않았습니다. 의도적으로 바뀐 기존 스냅샷 목록: 없음. 새 출력은 동작 단언으로 검증합니다.
  • 실제 프로세스 전체는 검증한 Windows Job Object로 빌드·테스트 6GB, 커밋 훅 12GB 제한을 적용합니다. 모든 Rust 컴파일은 CARGO_BUILD_JOBS=2, 컴파일러/브라우저 lane은 하나씩 실행합니다.
  • 소스 최종 검토: Oracle APPROVE/HIGH. Rust LSP는 계속 시간 초과라 성공했다고 주장하지 않으며, 컴파일·clippy·실행 테스트로 확인했습니다.
  • 첫 Linux 실행 38332568689에서는 W51 새 테스트의 rstest 속성 순서 때문에 전역 맵 잠금이 첫 case에만 적용되어 기존 테마 스냅샷 ID가 섞였습니다. 스냅샷이나 단언은 바꾸지 않고 W51 소유 5개 테스트 묶음의 serial 속성을 마지막 case 뒤로 이동했습니다(aac3626e). 전체 정상 병렬 Rust 테스트와 Rust 1.99 clippy, 정상 훅이 통과했습니다.
  • 수정된 Linux CI 38337751915는 aac3626e1922c3722fa10edb32659225b4434d50에서 전체 성공했습니다. publish, benchmark, landing-next-e2e, vinext-rsc-css-e2e가 모두 통과했으며, Next 및 RSC CSS의 두 CSS 모드 E2E도 성공했습니다.
  • Linux Rust 커버리지: 100.00%, 10,027/10,027 라인 (--fail-under 100). Bun: 5,498개 통과, 0개 실패, 함수·라인 모두 100%. 정상 커밋 훅의 lint, tarpaulin 분석·전체 Rust 테스트, Bun 테스트도 모두 통과했습니다. 일반 Windows 포맷 LLVM의 98.00%는 승인된 정책에 따라 로컬 임계값이 아니며, 실제 Linux 100% 게이트를 통과한 뒤 ready로 전환합니다.

owjs3901 and others added 2 commits October 13, 2026 10:22
Refs #682 #695 #689 #693

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #682 #695 #689 #693

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

Patch

  • Respect source-order property replacement across conditional spreads in styled rule objects, and document that styled callbacks cannot return runtime class names. No new build errors; existing located Cannot compose errors remain unchanged.
  • style-order-range only checks styleOrder where the build reads it: a prop of a Devup UI component and a top-level key of a style object passed to css/globalCss/keyframes. A styleOrder key in nested data, in a prop value such as an element passed as a prop, or in a call of a function sharing a name with an Object.prototype member is no longer reported. The array, typography and media rules already skip non-style data, and css-utils-literal-only accepts a css()/keyframes() result held in a const inside a function. No new build errors.
  • 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

  • Bun plugin works with Bun.build: DevupUI() (from @devup-ui/bun-plugin/register) serves the generated stylesheet to the bundler's CSS loader once every other module is loaded, so the build emits a CSS output with the styles of the whole bundle instead of an empty module; class names are short there and readable under the runtime (debug option). Files importing the packages Devup UI takes the place of (@emotion/react, @emotion/styled, styled-components, @vanilla-extract/css), @stylexjs/stylex, or a subpath or re-export of these or @devup-ui/react, are compiled instead of skipped

@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/eslint-plugin@1.0.22 → 1.0.23 - packages/eslint-plugin/package.json

Patch

  • style-order-range only checks styleOrder where the build reads it: a prop of a Devup UI component and a top-level key of a style object passed to css/globalCss/keyframes. A styleOrder key in nested data, in a prop value such as an element passed as a prop, or in a call of a function sharing a name with an Object.prototype member is no longer reported. The array, typography and media rules already skip non-style data, and css-utils-literal-only accepts a css()/keyframes() result held in a const inside a function. No new build errors.

@devup-ui/next-plugin@1.0.90 → 1.0.91 - 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.17 → 1.0.18 - packages/plugin-utils/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

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

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

@devup-ui/vite-plugin@1.0.73 → 1.0.74 - 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.71 → 1.0.72 - packages/webpack-plugin/package.json

Patch

  • Webpack and Rsbuild builds hold every style in their stylesheets: a stylesheet module is built on its first import, so styles extracted later (the global styles of files webpack or Rsbuild reached late, often outside src) were missing from the output. The files the entries reach under src are now extracted before the build, the shared base on disk is kept current, and when a stylesheet still changed during the build it is compiled once more, writing only the complete result. Rsbuild serves each stylesheet from the current sheet instead of a module-wide string kept only when it grew, which left edits that shortened the CSS and later builds in the same process stale. plugin-utils adds computeReachableFiles

@codecov

codecov Bot commented Oct 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
...tractor/src/extractor/extract_style_from_styled.rs 100.00% <100.00%> (ø)
libs/extractor/src/extractor/styled_record.rs 100.00% <100.00%> (ø)
...xtractor/src/extractor/styled_record_evaluation.rs 100.00% <100.00%> (ø)
libs/extractor/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.

@owjs3901
owjs3901 marked this pull request as ready for review October 13, 2026 02:52
@owjs3901
owjs3901 merged commit d42f584 into main Oct 14, 2026
5 checks passed
@owjs3901
owjs3901 deleted the fix/conditional-spread-precedence branch October 14, 2026 05:58
owjs3901 added a commit that referenced this pull request Oct 14, 2026
Refs #688 #707 #776

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 14, 2026
Refs #688 #707 #776 #702 #780

Includes exact patches 7960b48 and b71ca4e

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