Skip to content

fix(css): preserve quoted selector and interpolation boundaries - #795

Merged
owjs3901 merged 4 commits into
mainfrom
fix/quoted-css-boundaries
Oct 20, 2026
Merged

owjs3901 merged 4 commits into
mainfrom
fix/quoted-css-boundaries

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Refs #686 (PR4; #793 fix/quoted-css-strings 위에 쌓은 후속 수정이며 #793이 먼저 병합되어야 합니다. 현재 base는 main이고 retarget은 사용자가 결정합니다.)

요약

#793이 남긴 선택자·at-rule prelude·템플릿 보간 위치의 문자열 경계를 수정합니다. 문자열 안의 &, 괄호, 쉼표, 세미콜론, 중괄호, 공백은 문법으로 오인하지 않으며 원래 바이트를 유지합니다. 스타일링 런타임, 의존성, 해시 이름은 추가하지 않습니다.

원인

선택자 중첩과 최종 CSS 출력은 &를 단순 치환했고, media 조건 조합은 문자열 안의 괄호·쉼표를 깊이와 목록 구분자로 세었습니다. interpolation_place도 문자열을 구분하지 않고 앞뒤의 ;, {, }, :를 검색했으므로 content:'a;${p => p.label}' 같은 값 보간을 선택자로 잘못 거부했습니다. 별도의 mixin 깊이 계산과 선택자 params 탐색도 같은 종류의 문제를 가졌습니다.

동작

  • 공유 QuoteState와 원본 slice 기반 탐색·치환으로 선택자 중첩, 최종 class 치환, Tailwind 조합, params, media 목록을 처리합니다. 실제 nesting &만 바꾸고 문자열·escape 안의 &는 유지합니다.
  • 템플릿 선택자 [title="&"]는 자손 선택자이고, selectors 객체의 같은 키는 element 자체의 attribute 선택자입니다. 기존 두 의미를 유지하며 실제 anchor만 보완합니다.
  • 보간 위치 판정은 앞 quasi의 quote/escape 상태를 뒤 quasi까지 유지합니다. 뒤 quasi 전체를 매번 새 String으로 합치지 않으며, 문자열 안의 brace가 top-level mixin을 nested mixin으로 바꾸지 않습니다.
  • CSS Syntax의 문자열 토큰 규칙을 따릅니다. escape되지 않은 LF·CR·FF는 bad-string을 끝내고 이후 바깥 텍스트의 최적화를 재개합니다. 그 경계의 원본 줄바꿈은 삭제하지 않습니다. escape된 LF·CR·CRLF·FF와 hexadecimal escape가 소비하는 공백은 계속 문자열 안에 있습니다.
  • 닫는 quote 없이 EOF까지 이어지는 문자열은 끝까지 데이터로 보존합니다. 말미 공백·탭도 문자열 안에 있으면 generic trim으로 지우지 않습니다. 서로 다른 EOF 선택자 payload가 같은 counter key로 합쳐지지 않습니다.
  • 기존 EOF-open 문자열 3건에 붙인 말미 탭을 지우던 테스트 기대값만 위 승인된 계약에 맞게 수정합니다. 닫힌 문자열의 바깥 공백 제거와 기존 snapshot·font-list quote canonicalization·cascade 순서는 바꾸지 않습니다.

런타임 코드

새 imported helper나 runtime callable은 없습니다. generator는 수정하지 않았으며, 정적인 선택자·prelude 수정은 CSS와 class 문자열만 바꿉니다. main에서 잘못 거부되던 문자열 안의 값 보간은 기존 styled/CSS-variable 출력 경로를 사용합니다. 실제 single-CSS 공개 출력에서 확인한 값 식은 다음과 같습니다(각 행은 해당 fixture의 독립 출력입니다).

...{ "--b": `'a;${(p=>p.label)(rest)}'` },
...{ "--b": `'${(p=>p.label)(rest)}{'` },
...{ "--b": `'a}${(p=>p.label)(rest)}'` },
...{ "--b": `'a;${(p=>p.a)(rest)}{${(p=>p.b)(rest)}'` },
...{ "--b": `'a;${(p=>(trace.push(`value`),p.label))(rest)}'` },

상수 보간으로 여는 quote를 제공한 경우도 두 번째 식과 같습니다. callback은 사용자가 작성한 원래 식이며, 마지막 사례는 실제 render마다 trace === ['value']로 정확히 한 번 실행됩니다. 비교용 기존 unquoted 값은 ...{ "--b": (p=>p.label)(rest) }입니다. 그 밖의 forwardRef wrapper, props의 Object.fromEntries(Object.entries(...).filter(...)), theme/read-prop 제외, as/forwardedAs/ref 전달, className의 filter(Boolean).join(" "), ...style 병합은 이 기존 경로와 동일합니다. 함수·클래스·CSS-variable helper를 새로 삽입하지 않습니다.

새로 생기는 오류

새 build-error 종류는 추가하지 않습니다. build-time에 알 수 없는 선택자·prelude 보간은 기존 located 오류로 남고, 문자열 안의 선언 값 보간을 잘못 거부하던 경우는 정상적인 기존 CSS-variable 경로로 컴파일됩니다. 실제 공개 probe의 정확한 오류는 다음과 같습니다(두 CSS 모드 모두 동일).

W70f-pr4-probe.tsx:1:108: Cannot place `(p) => p.label` at build time: an interpolation in a selector or a property name must be a literal or a constant
W70f-pr4-probe.tsx:1:119: Cannot place `(p) => p.label` at build time: an interpolation in a selector or a property name must be a literal or a constant

각각 &[title="x:${p=>p.label}"]{color:red}와 @supports (content:"x:${p=>p.label}"){color:red}의 원본 arrow span입니다. 주어진 예의 import와 fixture wrapper를 포함한 위치이며, 사용자의 파일에서는 해당 원본 위치로 표시됩니다.

남는 한계

  • 이 변경은 CSS 문자열 경계와 해당 경계에 의존하는 추출·최적화의 수정이지 완전한 CSS 유효성 검사기는 아닙니다. malformed CSS 자체를 정상 CSS로 만들어 주지 않고, bad-string/EOF 경계에서 데이터와 이후 문법을 구분합니다.
  • 기존 font-list 항목의 따옴표 정규화, selector/media 정렬 정책은 이번 범위가 아닙니다. 브라우저 computed style을 검증했다고 주장하지 않습니다.

검증

  • main 7188d5ff에서 구현 전 RED를 먼저 고정했습니다(CSS 5 fail / extractor 6 fail). fix(css): keep quoted strings out of value rewriting #793 head ea8356ae에서도 남은 CSS 7 fail / extractor 3 fail을 확인한 뒤 수정했습니다. 추가 selector/escape/recovery/EOF/hexadecimal 경계도 별도 RED 후 GREEN입니다.
  • 최종 native workspace 6,079 pass / 0 fail, fmt와 모든 target·feature의 strict Clippy 통과. borrowing assertion을 포함한 해당 target도 6/6 통과했습니다. 기존 snapshot 변경은 없습니다.
  • fresh WASM, root build, 배포 entry-point 검증 통과. 실제 공개 API·React native SSR 48/48 행 PASS, 48행 모두 결정론적: 24 fixture × 두 CSS 모드 × 두 번 reset 추출, 실제 SSR 88회와 located 거부 8회입니다. 정확한 quoted payload, class-rule 연결, dynamic 값 호출 1회를 검증했으며 브라우저 computed style 결과는 아닙니다.
  • 실제 CSS 예: .a[title="a&b"]{color:red}, @media print and (name:"a),b"){...}, .a{content:var(--b)}. 줄바꿈 이후 0.5px는 .5px로 최적화되지만 EOF 문자열 '0.5 의 두 공백은 유지됩니다.
  • Bun 6,631 pass / 0 fail / 함수·라인 100%, lint 0 errors(main에서 상속한 TS warning 20건만). stable Rust 1.99.1, Bun 1.4.3, wasm-pack 0.15.0, jobs 2, 기본 stack, CI unset, native 6 GiB 제한에서 통과했습니다.
  • 수동 QA 후 단일 Oracle gate review APPROVE / HIGH, blockers 없음. Rust LSP daemon이 응답하지 않아 clean-LSP는 주장하지 않습니다.
  • 검증한 전체 트리만 commit했습니다. 구현 커밋 5f366fdc는 tree e161b9a0에서 전체 hook 1회 PASS(837초, native 12 GiB cap, raw peak 4.45 GiB)입니다. 그 뒤 local 추가 줄 커버리지에서 실제 closing-brace 분기가 빠진 것을 찾아 제품 코드 변경 없이 global pseudo/닫힌 nested block 회귀 두 건을 보강한 별도 커밋 e2e6e227을 만들었고, tree d9fd32f7에서 전체 hook 1회 PASS(661초, raw peak 4.44 GiB)했습니다. amend·hook skip·coverage exclusion은 없습니다.
  • Windows LLVM 전체 참고 수치는 **98.29%**이며 Linux 100% 증명으로 사용하지 않습니다. 추가 코드 중 남은 Windows 0-hit 행은 실제 global pseudo 회귀가 통과한 호출의 상수 인자 b'&' 행 하나이며, 이 플랫폼의 line-table 결과를 감추거나 제외하지 않았습니다. 실제 CI/Linux Rust 100% 결과는 아래에서 별도로 확인합니다.
  • 실제 CI 39412813327 SUCCESS, head e2e6e22756f769e7cc6bb3576af33a2d04645c79와 일치합니다. publish job 118594540394에서 Linux Rust 100.00% (15,799/15,799 lines), Bun 6,631 pass / 0 fail / 함수·라인 100%, 별도 preload regression 9/9를 직접 로그로 확인했습니다. publish·benchmark·wasm-size·landing-next-e2e·react-18·vinext-rsc-css-e2e·webpack-extends-theme·release-compat 모두 PASS이며 release/deploy는 PR에서 의도대로 SKIPPED입니다. CI rerun은 없었습니다.
  • 마지막 확인에서 PR OPEN / non-draft / MERGEABLE / CLEAN, main은 7188d5ff입니다. hook 이후 실제 공개 WASM·SSR도 다시 48/48 결정론적 PASS했습니다. #791과 fix(css): keep quoted strings out of value rewriting #793 역시 CLEAN이며 병합·닫기·승인·draft 전환·retarget은 하지 않았습니다.

owjs3901 and others added 4 commits October 20, 2026 15:41
Refs #686

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #686

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #686

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #686

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

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

codecov Bot commented Oct 20, 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/optimize_multi_css_value.rs 100.00% <100.00%> (ø)
libs/css/src/optimize_value.rs 100.00% <100.00%> (ø)
libs/css/src/quoted.rs 100.00% <100.00%> (ø)
libs/css/src/rm_css_comment.rs 100.00% <100.00%> (ø)
libs/css/src/style_selector.rs 100.00% <100.00%> (ø)
libs/css/src/utils.rs 100.00% <100.00%> (ø)
libs/extractor/src/css_prop.rs 100.00% <ø> (ø)
libs/extractor/src/css_utils.rs 100.00% <100.00%> (ø)
... and 4 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.

@owjs3901
owjs3901 merged commit 770aaac into main Oct 20, 2026
11 checks passed
@owjs3901
owjs3901 deleted the fix/quoted-css-boundaries branch October 20, 2026 12:30
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