Skip to content

fix(extractor): preserve JavaScript semantics and eliminate redundant arrow wrappers - #756

Open
owjs3901 wants to merge 84 commits into
mainfrom
fix/js-semantics
Open

owjs3901 wants to merge 84 commits into
mainfrom
fix/js-semantics

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

JSX·styled 변환에서 불필요한 arrow와 중간 값을 없애고, 생성 코드를 그 코드가 뜻하는 가장 짧은 형태로 씁니다. JavaScript의 평가 순서와 단일 평가, 예외 시점은 그대로 지킵니다.

합성 프로토콜·const를 필요한 곳에만 (이번 갱신)

합성 프로토콜을 필요한 곳에만 둡니다 (5020ecc)

  • styled 컴포넌트는 다른 모듈이 확장하거나 css prop·값으로 쓰일 때만 __devupCss0/__devupForwarded0 합성 프로토콜이 필요합니다.
  • export되지 않고, 이 파일에서 css 없는 JSX 태그로만 렌더되는 컴포넌트는 이제 이 프로토콜 없이 렌더합니다.
  • 읽기는 이름이 아니라 oxc semantic의 symbol로 판단합니다. 그래서 같은 이름이 가려진 경우에도 틀리지 않습니다.

빌드가 아는 객체의 멤버를 값으로 씁니다 (7d0dce8)

// 전
const __devupRecord4 = { id: __devupProps.id };
const __devupRecord5 = { ...__devupRecord4, className: undefined };
... = { ...__devupProps, ...__devupRecord5, style: { ...__devupRecord5.style, color: "green" } };

// 후
... = { ...__devupProps, id: __devupProps.id, className: undefined, style: { color: "green" } };
전 후
아무 코드도 바꾸지 않는 생성 객체 const의 멤버 읽기 리터럴 값, 쓴 적 없는 키는 void 0, 복사해 온 객체가 있으면 그 객체의 멤버
spread로 한 번만 읽히는 객체 const spread하는 자리에서 바로 만듦
구조 분해 대상 객체에서 뒤에 같은 키로 다시 쓰는 앞쪽 값 제거 (읽어도 아무 일이 없는 값만)
[void 0, "", a].filter(Boolean).join(" ") [a].filter(Boolean).join(" ")
{ ...void 0, ...null, a } { a }
`${""}x${"y"}` `xy`

다음 경우는 모두 그대로 둡니다. 동작이 바뀔 수 있기 때문입니다.

  • 객체가 대입·delete·메서드 호출·값 전달을 받는 경우
  • 키가 __proto__·getter·Object.prototype 상속 멤버인 경우
  • 복사 원본의 이름이 가려진 경우
  • undefined가 매개변수로 가려진 경우

값이 바뀔 수 없는 한 번 읽는 const는 읽는 자리로 옮깁니다 (5e19491)

// 전
const StyledDiv = __devupForwardRef((__devupRefProps, __devupRef) => {
	const { style, className, as: DevupAs = "div", forwardedAs, ...rest } = __devupRefProps;
	rest.ref = __devupRef;
	const __devupRecord0 = primaryColor;
	const __devupRecord1 = padding;
	return <DevupAs {...} className={...} style={{ "--a": __devupRecord1, "--b": __devupRecord0, ...style }} />;
});

// 후
const StyledDiv = __devupForwardRef(({ style, className, as: DevupAs = "div", forwardedAs, ...rest }, __devupRef) =>
	<DevupAs {...} ref={__devupRef} className={...} style={{ "--a": padding, "--b": primaryColor, ...style }} />);

선언 순서와 읽는 순서가 달라서 남아 있던 const입니다. 그 사이에 어떤 코드가 실행되더라도 값이 같은 경우에만 옮깁니다.

  • 리터럴
  • 재할당이 없고 이미 초기화된 바인딩. 같은 함수의 앞쪽 let/const, 또는 컴포넌트를 만드는 문장보다 앞선 최상위 선언이어야 합니다. 그래서 TDZ 시점도 바뀌지 않습니다.
  • 그 키를 쓰는 코드가 없는 object rest(rest.bg)의 멤버
  • 위 값들의 ||·?:·===·!

다음 경우는 그대로 둡니다.

  • 함수 호출, 다른 객체의 멤버
  • 매개변수, var
  • 읽는 위치가 함수·루프·블록 안인 경우
  • 앞쪽 함수 선언이 그 const를 읽는 경우
  • rest에 대입·delete·메서드 호출·값 전달이 있는 경우
  • 상속 키

생성 코드 전체는 187,580 → 186,677 바이트이고, 남은 생성 const는 123 → 107개입니다.

아무도 필요로 하지 않는 const와 문장을 지웁니다 (76c15eb)

  • 함수 안에서 읽히지 않고 계산해도 아무 일이 없는 생성 const를 지웁니다.
  • 쓰기만 하는 생성 배열·객체를 지웁니다. 각 대입은 대입하는 값만 남깁니다.
  • 짧은 리터럴(true, 0, null, void 0) const는 읽는 자리마다 값을 씁니다. 그래서 __devupRecord13 ? a : b 같은 조건이 접힙니다.
  • 평가해도 아무 일이 없는 문장(rest;, "a";)을 지웁니다. a && b;에서 b가 아무 일도 하지 않으면 a;로 줄입니다.
  • 최상위 const는 다른 스크립트가 읽을 수 있어서 그대로 둡니다.
// 전 (조건부 mixin이 이미 적용된 class라서 결과를 쓰지 않던 분기)
const __devupRecord7 = ["0"];
__devupRecord7["0"] = "0";
rest.active && (__devupRecord7["0"] = "1", true);
const __devupRecord2 = __devupRecord7["0"] === "1";
// 후: 모두 제거

남은 const 104개를 하나씩 확인했고, 모두 필요한 것입니다.

유형 예 필요한 이유
합성 프로토콜 (export·확장되는 컴포넌트) __devupRecord4 && __devupRecord4[3], (__devupRecord6) => {...} 여러 클로저와 hook 호출이 공유
두 번 이상 읽는 값 const __devupBranch0 = a === b ? \${a}px` : void 0` className과 style에서 같은 값을 한 번만 평가
동적 태그 const DevupAs0 = ["div", "section"][b] || "div" JSX 태그 이름은 식별자여야 함
읽는 순서가 다른 사용자 코드 func(), obj.color, 선언되지 않은 전역 cond 옮기면 호출·getter·ReferenceError 순서가 바뀜. 사이에 있는 ${className}이 toString을 부를 수 있음
최상위 const 스크립트에서 읽히는 생성 값 모듈이 아닐 수 있음

생성 코드 전체는 186,677 → 186,246 바이트이고, 생성 const는 107 → 104개입니다.

새 규칙은 각각 단위 테스트로 적용되는 경우와 그대로 두는 경우를 모두 확인합니다. 스냅샷 전체 생성 코드는 211,356 → 186,246 바이트입니다.

로컬 메모리가 부족해서 pre-commit 훅(tarpaulin 포함) 대신 CI로 검증합니다. 로컬에서는 extractor 전체 테스트, clippy -D warnings, fmt가 통과했습니다. llvm-cov 기준으로 이번에 추가한 코드는 커버리지 100%입니다.

한 번만 읽히는 const 제거 (이전 갱신)

// 전
const Ext = __devupForwardRef((__devupRefProps, __devupRef) => {
	const __devupProps = { ...__devupRefProps, ref: __devupRef };
	const { style, className, as: DevupAs = "input", forwardedAs, ...rest } = { ...__devupProps, type: "text", title: "base", type: "password" };
	return <DevupAs {...__devupOmit0(rest)} as={forwardedAs} className={...} style={style} />;
});

// 후
const Ext = __devupForwardRef(({ style, className, as: DevupAs = "input", forwardedAs, ...rest }, __devupRef) =>
	<DevupAs {...__devupOmit0(rest)} ref={__devupRef} type="password" title="base" as={forwardedAs} className={...} style={style} />);
전 후
forwardRef 본문에서 props를 구조 분해하고 rest.ref = ref 매개변수에서 구조 분해, ref와 .attrs 상수는 JSX 속성
const __devupX = v; const p = __devupX; const p = v; (익명 함수는 이름이 바뀌므로 제외)
빌드가 텍스트로 만든 코드 다시 파싱해 peephole 규칙이 모두 적용
b ? <div /> : <div /> b, <div />
className={__devupBranch0 ? "a" : ""} (DOM 요소) className={__devupBranch0 && "a"}
결과를 쓰지 않는 매개변수·함수 읽기 ((cond, "a"), a === b) 제거. let·const·import·class는 TDZ 순서 때문에 유지

같은 키를 여러 번 넣은 .attrs는 처음 들어간 자리에 마지막 값을 써서 props 순서를 지킵니다. 스냅샷 전체 생성 코드는 220,938 → 211,356 바이트입니다.

생성 코드 단순화

IIFE를 푼 뒤 도는 peephole 단계(libs/extractor/src/peephole.rs, peephole_omit.rs)를 추가하고, styled 렌더러가 스스로 쓴 값을 다시 검사하던 코드를 없앴습니다.

// 전
const { style, className, as: DevupAs = "div", forwardedAs, ...rest } = {
	...__devupRefProps,
	ref: __devupRef
};
return <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== "$" && k !== "theme"))}
  as={forwardedAs} className={["a b", className].filter(Boolean).join(" ")} style={style} />;

// 후 (같은 모듈에 styled 컴포넌트가 둘 이상일 때)
function __devupOmit0(__devupOmitted) {
	return Object.fromEntries(Object.entries(__devupOmitted).filter(([k]) => k[0] !== "$" && k !== "theme"));
}
const { style, className, as: DevupAs = "div", forwardedAs, ...rest } = __devupRefProps;
rest.ref = __devupRef;
return <DevupAs {...__devupOmit0(rest)} as={forwardedAs} className={className ? `a b ${className}` : "a b"} style={style} />;
전 후
{...props, ref}를 복사한 뒤 구조 분해 props를 구조 분해하고 rest.ref = ref
["a b", className].filter(Boolean).join(" ") className ? `a b ${className}` : "a b"
모듈마다 반복되는 transient prop 필터 모듈 함수 하나 (__devupOmitN)
[2] !== void 0, [1] === void 0처럼 빌드가 쓴 레코드를 다시 검사 생성 단계에서 결정
parent ? ["e"] : ["e"], c ? true : false, c ? d : false ["e"], 조건만 읽는 형태
const __devupRecord2 = __devupRecord1, const __devupRecord8 = rest 원래 이름을 바로 읽음
(0, f)(), "a" || x, x ?? void 0, `${`${x}`}` f(), "a", x, `${x}`
<Dyn {...{...r, a: v}} />, style={{...{"--b": a}, ...s}} spread를 풀어 prop으로
__devupSpreadN?.x (객체 리터럴을 담은 이름) .x
두 분기가 같은 식인 삼항 조건은 부수효과만 위해 실행하고 값 하나로

각 규칙은 읽는 동작이 바뀌는 경우를 그대로 둡니다.

  • (0, a.b)(), (0, eval)(): this 바인딩과 간접 eval을 지키려고 남깁니다.
  • x = (0, function(){}): 함수가 x라는 이름을 얻지 않도록 남깁니다.
  • 선언되지 않은 전역의 typeof, 멤버나 이름 두 개를 읽는 className 결합: 두 번 읽으면 getter나 ReferenceError가 달라져서 남깁니다.
  • spread 안의 key, getter로 정의된 ref, 패턴이 ref를 직접 꺼내는 구조 분해: 그대로 둡니다.
  • k 말고 다른 이름을 읽는 필터, Object를 직접 선언한 모듈: 필터를 공유하지 않습니다.
  • let·var·매개변수를 가리키는 별칭: 그대로 둡니다.

알려진 가장자리 경우가 두 가지 있습니다. 누군가 Object.prototype에 ref setter를 심어 두었다면 rest.ref = … 대입이 다르게 동작합니다. 또 대상 const가 TDZ일 때 별칭을 선언하면, 원래 그 자리에서 나던 에러가 이름을 처음 읽는 시점으로 옮겨집니다.

이전 갱신 이후 포함된 다른 변경

  • 상수 문자열·배열의 length, 모듈이 상수로 계산할 수 있는 컴포넌트 style prop(color={darken(0.4, PRIMARY)})을 빌드 시점에 계산합니다.
  • CommonJS export를 최상위 문장 순서대로 읽습니다(module.exports를 다시 할당한 경우 포함).
  • 알 수 없는 코드에 넘겨진 바인딩을 읽는 style 값은 빌드 시점에 고정하지 않습니다.
  • generated let 슬롯과 쉼표 대입 대신 인자와 const를 씁니다. 아무도 읽지 않는 generated 선언을 지우고, 한 값만 갖는 분기 패킷을 풉니다.
  • as가 이름 사이에서 고르는 요소는 앞쪽 props를 먼저 평가한 뒤 정적 요소로 나눕니다.
  • 런타임 태그를 쓰는 팩토리 호출을 캡처를 방문한 뒤 다시 만듭니다. 안쪽 호출이 바깥 호출을 가로채던 버그를 고쳤습니다.
  • 알려진 JSX spread 항목({...{ animation: … }})의 style 결과를 정적 클래스로 읽습니다.
  • 의존성 갱신(oxc 0.158). 추출 엔진이 napi로 옮겨 가면서 WASM 크기 게이트와 크기 전용 wasm-opt 단계를 제거했습니다.

이전 본문의 정적 스타일 선택({lg: …, md: …}[size] || ""), 평가 순서·단일 평가 보존, styled 렌더 중간 함수 병합 내용은 그대로 유지됩니다.

검증

검증 커밋: c5a43bc3731461ca7358de6f7df3a7e0b77428a8
검증 소스 트리: 7850779312ec01198651f1b461c2ead98c385e30

로컬 검증 결과
Rust 전체 (cargo test --workspace) 전부 통과
extractor 유닛 12,313개 통과 / 실패 0
peephole 단위 테스트 181개, peephole.rs·peephole_omit.rs 라인 커버리지 100%
Clippy (-D warnings, 모든 target·feature)·fmt·ESLint 통과
Vite 872개 통과 / 함수·라인 커버리지 100%

스냅샷은 바뀐 diff를 패턴별로 묶어 하나씩 확인했습니다. 런타임 실행 테스트(ref·children·theme·$ prop 전달, getter, this, eval, 함수 이름 추론)도 모두 통과합니다.

CI

publish를 포함한 모든 검증이 통과합니다. Rust 커버리지는 100.00%(27,171/27,171줄)입니다.

CI는 tarpaulin을 돌리기 전에 소스를 max_width = 100000으로 다시 포맷합니다. 그래서 CI가 보고하는 줄 번호는 체크인된 소스와 다릅니다. 같은 설정으로 포맷한 사본에서 확인해 보니, 보고된 줄은 모두 실제로 실행되지 않은 분기였습니다. 다음과 같이 정리했습니다(ccc0ea5).

  • 도달할 수 없는 else { return }: 앞에서 이미 확인한 조건이라 실행될 수 없는 분기입니다. 헬퍼 함수와 let 체인으로 합쳤습니다. 헬퍼의 실패 경로는 다른 곳에서 실제로 실행됩니다.
    • 대상: unwrap_iife, styled_inline_simplify, peephole_known, peephole_omit, build_time_values, visit.rs
  • 실제로 도달하는 거부·경계 경로: 단위 테스트를 추가했습니다.
    • 대상: defined(), omission의 거부 조건, handed, CommonJS module.exports, without_props

이 과정에서 드러난 문제도 함께 고쳤습니다.

JSX 안의 시퀀스 (fdc7ae9)

  • 두 갈래가 같은 삼항식은 (test, value)로 접힙니다. 이 식이 JSX 속성 안에서 괄호 없이 출력되어 파싱되지 않았고, 그래서 Landing 빌드가 실패했습니다.
  • 이제 JSX 표현식 안에서는 [test, value][1]로 씁니다.
  • 스냅샷 746개의 출력 코드를 모두 파싱해서 문법 오류가 없는 것도 확인했습니다.

출력 형태 변경에 맞춘 JS 테스트 (b16528a, fcd1181)

  • attrs 값이 JSX 속성 role="note"로 바뀐 것에 맞췄습니다.
  • 클래스가 className ? `… ${className}` : "…" 형태로 바뀐 것에 맞췄습니다.

🤖 Generated with Claude Code

owjs3901 and others added 11 commits October 5, 2026 00:13
Refs #695

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 4, 2026 •

Copy link
Copy Markdown
Contributor

Changepacks

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

Patch

  • A project module re-exporting Devup UI (export { Box, css } from '@devup-ui/react', export * from '@devup-ui/react', export { styled as s }, export * as Devup, a default export of an import, chains of barrels and the compat entry) now works: a file importing Box or css from it compiles exactly as if it imported from the package, following the barrel through the resolver (relative paths, tsconfig paths, package exports), and hasDevupUI sees such a file. Members read from a namespace or default import of the package (Devup['css'], const { css } = Devup, Devup.css as a value) compile where the build can follow them, Devup.x for a name the package compiles being read like the named import. New build errors, located at the use: what a barrel re-exports but the build cannot read (an unresolvable export * or module, a namespace of modules re-exporting the package) is an error where its name is used instead of a runtime crash, and a namespace member read by a key the build cannot know (Devup[key]), written, or destructured in a form it cannot follow (a rest, a computed key, a default value, a nested pattern, an exported declaration, or members mixed with ones that stay at runtime) is an error that names the members to read. A namespace passed on whole (Object.keys(Devup), spyOn(Devup, 'css'), export default Devup) is left as it is
  • css(a, b) composing classes whose styles the build knows (a const bound to css() in the file, or a css() result another module exports with a rule object every value of which is known) merges their styles: a later part's declaration replaces an earlier one's for the same property, selector, breakpoint and layer, also under conditions (css(base, cond && danger)) and for ||/?? parts, where the classes used to be joined and the stylesheet order picked the winner. vanilla-extract style([a, b]) passes each composed style as its own argument, so the later one wins, and a style composed again later (style([a, b, a])) is kept. Classes the build does not know (strings, props, CSS Modules) are kept as they are
  • styled(Base) extending a styled component the file binds to a const renders Base's tag directly with Base's styles composed under its own, so the extension's declarations replace Base's for the same property, selector, breakpoint and layer instead of losing to them by stylesheet order; Base's attrs apply before the extension's. attrs merge as styled-components merges them: className is joined with the caller's, style is merged and other props replace earlier ones. On an element, a className or style spread after the explicit prop now wins, as React merges props in the order they are written
  • Emotion's now compiles at build time with no runtime: the element becomes what its child function ({ css, cx, theme }) => ... renders, each css and cx call becomes the classes it composes (later styles replace earlier ones; css reads strings as CSS text and its tagged templates split around mixins; cx keeps unknown classes, reads { name: condition } objects as conditional classes and composes nested css and cx calls), and theme.a.b in their values becomes var(--a-b). Numbers in css rules are px lengths, as Emotion reads them, and top-level constants are inlined. New build errors, each at file:line:col: a without exactly one child function taking { css, cx, theme } that gives what it renders at once (attributes, other children, rest or default parameters, async or generator functions, several statements); reading the theme other than as theme.a.b in a css value; using css or cx other than by calling them; a cx object with a spread, getter or method; parts css and cx cannot compose (runtime calls, spreads, a condition choosing between styles and classes). @devup-ui/react/compat exports ClassNames with the ClassNamesContent and ClassNamesArg types, and the @emotion/react declarations include them
  • Emotion and styled-components component selectors now compile at build time: a styled component the file defines that another style selects (${Child} { ... } in CSS text, [Child] or [&:hover ${Child}] as a rule key) gets a short marker class, which only such components get, and the selector reads it as .marker; a component extending it, or an element inlining it for its css prop, keeps the marker. Selectors written without & now nest under the component as stylis nests them (ul { ... } is & ul, a selector list nests each selector, :hover applies to the component itself) instead of becoming global rules, and rule-object keys starting with ., #, [, *, >, + or ~ are read as nested selectors instead of properties
  • Emotion's css prop compiles at build time while @emotion/react is aliased: on tags and Devup UI components always, and on every element once the file imports @emotion/react or @emotion/styled or names @emotion/react in a @jsxImportSource pragma; jsx, jsxs and jsxDEV from @emotion/react/jsx-runtime (or jsx-dev-runtime) and jsx from @emotion/react compile it too, so libraries built with Emotion's JSX runtime do as well. The prop becomes the element's className, with CSS variables in its style for values only the runtime gives: arrays and conditions compose with a later part replacing what an earlier one sets, css() classes the file knows compose by their styles, strings and templates are CSS text, css`` mixins split the text around them, a function of the theme reads theme.a.b as var(--a-b), unitless numbers are px as in Emotion (constants included), and a className holding known css() classes overrides the prop as Emotion's registered classes do. A styled component the file defines renders its tag in the element's place when the prop overrides its styles and it renders a tag with no attrs or props read and the element has no spread, as or forwardedAs. Emotion's JSX runtime imports become react/jsx-runtime, the pragma names react, and jsx comes from @devup-ui/react/compat (React's createElement); @devup-ui/react/compat/css-prop types the prop on React.Attributes. New build errors, each with file:line:column and the code: a css prop part the build cannot read (a call, an element), a style object declared inside a function or with let, a binding only running the module gives or code changes, a theme function that does more than return rules or reads the theme other than as theme.a.b in a value, an interpolation CSS text cannot place or a mixin inside a nested rule, and a css prop overriding a styled component's styles where its tag cannot be rendered in place. Composing css() with a part that reads a known binding (such as a keyframes name) now reads its value
  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source
  • 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.
  • hasDevupUI takes the import aliases and is true for a file extraction changes: one importing a redirected package (@emotion/styled, ...), naming @emotion/react in a @jsxImportSource pragma, or in a project whose tsconfig jsxImportSource is @emotion/react, so the Bun plugin no longer skips files whose only Devup UI is Emotion's css prop or an aliased import. Aliases of Devup UI now compile like the original: export const B = Box, const c = css; export { c as cc }, export const D = Devup and export const C = Devup.css in a barrel (the barrel module exports them as re-exports of the package, and what imports them follows them), export { Box } after an import, export default Box, const D = Devup namespace aliases, and const inner = css declared inside a function. A css() or keyframes() const that a function reads before its declaration compiles. New build error: a css()/keyframes() const read before its declaration runs (outside a function) is located and says to move the declaration above its first read
  • Preserve JavaScript semantics for exact imported enum and Math values including negative zero, lexical constants, binding-aware CommonJS require, React createElement calls, type-only imports, and source-ordered props evaluation with native spread snapshots and CSS-variable fallbacks without a styling runtime. Migrate UI-kit style defaults by pure reordering and test reset CSS through compiled output. Report located build errors for runtime styleOrder values, unknowable whole-object style overrides, opaque selector literals, genuinely unmovable lazy logical suspension, Devup UI namespaces passed whole to runtime code, and immediate style reads in the temporal dead zone of lexical bindings; deferred initialized reads keep folding and pre-assignment var reads remain dynamic.
  • Resolve compiled styling APIs and components by their lexical binding so shadowed local names, including minified library identifiers, remain ordinary JavaScript and JSX. No new build errors; existing diagnostics now apply only to the compiled binding they describe.
  • Styled components the build generates are wrapped in React's forwardRef, so a ref passed to them reaches the element they render on React 18 as well as React 19; the file imports forwardRef from react when it defines one
  • Styled components no longer pass every prop to the tag they render: $ props, theme and the props their style functions or attrs read are kept away from a tag unless it takes them as attributes, and shouldForwardProp (Emotion options or styled-components withConfig) is evaluated at build time. A shouldForwardProp the build cannot evaluate is a build error naming the file, line and code, with the forms it accepts. An element using a styled component the file defines drops props the component neither reads nor passes on; spread props are passed as written
  • Styled components render what their as prop names, defaulting to the tag or component they were defined with, and pass forwardedAs on as as, as styled-components and Emotion do; as used to reach the rendered tag as an attribute. Component.withComponent(target) on a styled component the file binds to a const builds a component rendering the same styles and attrs as target (a tag name or a component JSX can name), where it used to call a method the generated function does not have and throw
  • 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
  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source
  • hasDevupUI takes the import aliases and is true for a file extraction changes: one importing a redirected package (@emotion/styled, ...), naming @emotion/react in a @jsxImportSource pragma, or in a project whose tsconfig jsxImportSource is @emotion/react, so the Bun plugin no longer skips files whose only Devup UI is Emotion's css prop or an aliased import. Aliases of Devup UI now compile like the original: export const B = Box, const c = css; export { c as cc }, export const D = Devup and export const C = Devup.css in a barrel (the barrel module exports them as re-exports of the package, and what imports them follows them), export { Box } after an import, export default Box, const D = Devup namespace aliases, and const inner = css declared inside a function. A css() or keyframes() const that a function reads before its declaration compiles. New build error: a css()/keyframes() const read before its declaration runs (outside a function) is located and says to move the declaration above its first read

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

Patch

  • Preserve JavaScript semantics for exact imported enum and Math values including negative zero, lexical constants, binding-aware CommonJS require, React createElement calls, type-only imports, and source-ordered props evaluation with native spread snapshots and CSS-variable fallbacks without a styling runtime. Migrate UI-kit style defaults by pure reordering and test reset CSS through compiled output. Report located build errors for runtime styleOrder values, unknowable whole-object style overrides, opaque selector literals, genuinely unmovable lazy logical suspension, Devup UI namespaces passed whole to runtime code, and immediate style reads in the temporal dead zone of lexical bindings; deferred initialized reads keep folding and pre-assignment var reads remain dynamic.

@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

  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source

@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
  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source

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

Patch

  • Emotion's now compiles at build time with no runtime: the element becomes what its child function ({ css, cx, theme }) => ... renders, each css and cx call becomes the classes it composes (later styles replace earlier ones; css reads strings as CSS text and its tagged templates split around mixins; cx keeps unknown classes, reads { name: condition } objects as conditional classes and composes nested css and cx calls), and theme.a.b in their values becomes var(--a-b). Numbers in css rules are px lengths, as Emotion reads them, and top-level constants are inlined. New build errors, each at file:line:col: a without exactly one child function taking { css, cx, theme } that gives what it renders at once (attributes, other children, rest or default parameters, async or generator functions, several statements); reading the theme other than as theme.a.b in a css value; using css or cx other than by calling them; a cx object with a spread, getter or method; parts css and cx cannot compose (runtime calls, spreads, a condition choosing between styles and classes). @devup-ui/react/compat exports ClassNames with the ClassNamesContent and ClassNamesArg types, and the @emotion/react declarations include them
  • Emotion's css prop compiles at build time while @emotion/react is aliased: on tags and Devup UI components always, and on every element once the file imports @emotion/react or @emotion/styled or names @emotion/react in a @jsxImportSource pragma; jsx, jsxs and jsxDEV from @emotion/react/jsx-runtime (or jsx-dev-runtime) and jsx from @emotion/react compile it too, so libraries built with Emotion's JSX runtime do as well. The prop becomes the element's className, with CSS variables in its style for values only the runtime gives: arrays and conditions compose with a later part replacing what an earlier one sets, css() classes the file knows compose by their styles, strings and templates are CSS text, css`` mixins split the text around them, a function of the theme reads theme.a.b as var(--a-b), unitless numbers are px as in Emotion (constants included), and a className holding known css() classes overrides the prop as Emotion's registered classes do. A styled component the file defines renders its tag in the element's place when the prop overrides its styles and it renders a tag with no attrs or props read and the element has no spread, as or forwardedAs. Emotion's JSX runtime imports become react/jsx-runtime, the pragma names react, and jsx comes from @devup-ui/react/compat (React's createElement); @devup-ui/react/compat/css-prop types the prop on React.Attributes. New build errors, each with file:line:column and the code: a css prop part the build cannot read (a call, an element), a style object declared inside a function or with let, a binding only running the module gives or code changes, a theme function that does more than return rules or reads the theme other than as theme.a.b in a value, an interpolation CSS text cannot place or a mixin inside a nested rule, and a css prop overriding a styled component's styles where its tag cannot be rendered in place. Composing css() with a part that reads a known binding (such as a keyframes name) now reads its value
  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source

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

Patch

  • Preserve JavaScript semantics for exact imported enum and Math values including negative zero, lexical constants, binding-aware CommonJS require, React createElement calls, type-only imports, and source-ordered props evaluation with native spread snapshots and CSS-variable fallbacks without a styling runtime. Migrate UI-kit style defaults by pure reordering and test reset CSS through compiled output. Report located build errors for runtime styleOrder values, unknowable whole-object style overrides, opaque selector literals, genuinely unmovable lazy logical suspension, Devup UI namespaces passed whole to runtime code, and immediate style reads in the temporal dead zone of lexical bindings; deferred initialized reads keep folding and pre-assignment var reads remain dynamic.

@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
  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source

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

Patch

  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source

@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
  • A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source

@codecov

codecov Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
libs/css/src/file_map.rs 100.00% <ø> (ø)
libs/css/src/theme_tokens.rs 100.00% <ø> (ø)
libs/extractor/src/as_visit.rs 100.00% <100.00%> (ø)
libs/extractor/src/barrel.rs 100.00% <ø> (ø)
libs/extractor/src/barrel/aliases.rs 100.00% <100.00%> (ø)
libs/extractor/src/barrel/namespace_aliases.rs 100.00% <100.00%> (ø)
libs/extractor/src/bound_style_callbacks.rs 100.00% <ø> (ø)
libs/extractor/src/build_time_values.rs 100.00% <100.00%> (ø)
libs/extractor/src/build_time_values/exact_math.rs 100.00% <100.00%> (ø)
libs/extractor/src/build_time_values_execute.rs 100.00% <ø> (ø)
... and 64 more

... and 44 files with indirect coverage changes

🚀 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.

Merge updated parent PR 750 (74afb76), containing main 11790be. Conflicts: libs/extractor/src/prop_modify_utils.rs and conditional Tailwind snapshot. Share main's per-class Tailwind compiler between normal generation and PR 756 class capture, retaining semantic IDs and single evaluation. Regenerate the conditional snapshot to combine main's extra line-height declaration with the captured controller; retain all assertions and update five exact template-code expectations for PR 756 single-coercion IIFEs. Full workspace tests, fresh WASM/root builds and real WASM probe pass; no snapshots were hand-merged.

Refs #686

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 8, 2026
No conflicted files; retain PR changes and merge updated parent #756, which contains main 11790be.
owjs3901 added a commit that referenced this pull request Oct 8, 2026
Merge #756 at f2fc451, retaining the
style-order metadata policy and forwarding/callback provenance from faff4a0.
Keep the current scope-aware style-order-range rule and clarify that nested
style metadata is checked while ordinary props and data are ignored.

Coordinator-approved integration reconciliation: the incoming
test_tailwind_keeps_other_classes cases 07, 08, 09, 14 and 15 retain the
current once-only __devupValue0 controller captures; case 10 folds the
truthy literal a without reading the right side of OR. See the source-fed
failure evidence D/W38g-756-test-conflict.md and successful executed controls
W38g-756-tailwind-qa3.log. Existing HEAD assertions and goldens are unchanged.

Reclassify exactly the two incoming ESLint fixtures containing nested zero
and a computed styleOrder zero from valid to invalid, retaining their source
and ordinary props/data controls. #766 reads metadata at those positions;
#756 alone predated it. Pin the 2+1 original diagnostic locations, as recorded
in W38g-756-eslint-focused.log. Rule production behavior is unchanged.

Fresh gates: Rust1.99 clippy, default workspace4119/0, rebuilt WASM/root,
Bun6472/0 aggregate100%, lint0errors/2baselinewarnings, original48probes with
only the incoming1.0.82-to1.0.83 banner delta, and named literal/wrapper QA.
The incoming guarded-hover/active cascade finding is reported separately,
not fixed by this merge, per coordinator attribution instructions.

Refs #684 #685

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 8, 2026
Merge #757 at 78ef660 after the genuine
#756 integration975f3371. Recursive merge bases74afb760 and6d0c9c83 produce
an identical tree3c14e7e427686d8802d1e0722afc33515d3eed28 with no conflicts
or source changes. Preserve both exact base histories and normal hook gates.

Keep the approved six incoming Tailwind expectation and two incoming ESLint
fixture reconciliations from the prior merge, current JSX capture/constant-fold
semantics, located metadata errors, and existing HEAD assertions/goldens.
F195 and the remaining coverage units are separate subsequent changes.

Refs #684 #685

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 8, 2026
Refs #690, #693

Merge exact f2fc451 retaining the current native/readback/provenance and incoming namespace/JS-semantics changes, without resetting merge state. Resolve five combined source/snapshot seams, use direct private tracking fields and current evaluate_located test API, and bind the incoming reachable-file helper to existing resolveSourceFile. Coordinator-approved merge-test alignments: lexical local require is not a module request; genuine unbound require keeps its original helper.ts:1:25 semantic site and unchanged cause/Fix/no-publication, while reference-free exploration falls back to entry.tsx:2:23 ingress; eight native consumer cases evaluate independent runtime sentinels, exact once-only getters/calls and wrong-slot/restoration instead of generated identifier spellings; only positive g uses valid styleOrder={undefined}, with the complete original invalid JSX retained as the located a.tsx:9:34 negative. All other fixtures and oracles remain unchanged; incoming capture/spread snapshot preserves the existing CJS_DYNAMIC alias. Full workspace4719, Rust1.99clippy, fresh WASM/root, Bun5535, preservation/API/carrier/graph and50both-mode browser positive/wrong-class/restoration gates passed. The prior advisory B Oracle result was lost to tool failure; coordinator authorized full-diff self-review and objective gates, not a claimed clean Oracle review. No contextual carrier/plugin delivery or out-of-scope Devup-only composition fix is included.

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

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

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
owjs3901 and others added 30 commits November 11, 2026 23:33
…ue branches

A generated declaration nothing reads, holding a value made only to be
named, goes. One that only names a parameter or an import nothing
writes or rebinds goes too, its reads reading that name instead, so
`<__devupRecord10 />` after `const __devupRecord10 = Dyn` is `<Dyn />`.
A generated arrow left only returning a value is written as it.

A branch packet `test ? [true, value] : [false]` whose value is never
`undefined` holds that value itself: which branch ran is whether it is
defined, or whether it is truthy where it always is when given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed values

A branch capture whose one branch gives a single value never
`undefined` and whose other gives none is now built as that value,
`test ? value : void 0`, where it is made, instead of being repacked
after the fact from the shape of its packet.

The build ran code for a style value even where the module handed the
binding it reads to unknown code, as long as the build could not read
that binding itself: `const made = make(); watch(made);` let
`css({ p: f() })` bake `made.p` in although `watch` may change it. What
such code reads is now checked where it runs: a value it hands on that
holds an object there computes nothing, leaving the error the build
gives for any value known only at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onJS exports in order

- A constant string or array has its length, and a string its UTF-16
  units, so `PRIMARY.length` is the number it is.
- A style prop of a component the module computes from constants with
  its own code, such as `color={darken(0.4, PRIMARY)}`, is computed like
  the arguments of `css()`. An element prop reads a hole or `undefined`
  as it is, so a value JSON would turn into `null` is left to runtime.
- A conditional whose branches each give the styles one whole value is
  captured as that one value, so `a === b ? `${a}` : `${a}`` no longer
  repacks a flag no class reads.
- A string a template literal holds as a value is written as its text,
  except in a tagged template, whose tag reads the parts as written.
- `CommonJS` exports are read as the top-level statements leave them in
  order: `module.exports = { A: 'a' }; module.exports = { A: 'b' }`
  exports `b`. Once `module.exports` holds another object, `exports.x`
  writes to the one no importer reads, which the build baked in before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ches only for its effects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build wrote checks whose answer it had just written, fallbacks never
reached and values wrapped to be unwrapped again. Each is now written as
the code it means, running the same steps in the same order:

- The styled renderer no longer checks the record it wrote itself:
  `[2] !== void 0` and `[1] === void 0` fold where the build knows the
  slot, and a composing component reads the same classes as its own.
- A pass after the IIFEs unwrap simplifies what remains: `(0, f)()` to
  `f()`, string-only `[...].filter(Boolean).join(" ")` to its text,
  `"a" || x` and `x ?? void 0` to their left side, conditions read only
  for whether they hold (`c ? true : false`, `c ? d : false`, a value
  guarded by its own guard), conditionals of a known or repeated test,
  templates held in templates, objects spread in place in objects and
  JSX, `?.` on a generated name holding an object, and leading values a
  sequence reads to drop.
- A generated constant declared as another is read as that one, found
  by scope, so components repeating the same names lose them too.
- A conditional whose branches are the same code keeps its condition
  only for its effects; the restore `branch_capture` did for it is gone.

Each rule keeps what reading changes: a member or name called through a
sequence keeps its `this` and indirect `eval`, a function keeps the name
it would infer, `typeof` is only dropped for declared names, and `key`
stays spread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… array

`["a b", className].filter(Boolean).join(" ")`, which every styled
component renders with, is written as `className ? `a b ${className}` :
"a b"`: the same test `filter` runs and the same text `join` writes,
without building an array and calling two methods on each render.

The name is read once more to test it, so only a declared or generated
name takes this form; a member, two names or texts a template would read
apart stay as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…op filter

- `const { ..., ...rest } = { ...props, ref }` is written as
  `const { ..., ...rest } = props; rest.ref = ref;`: the rest is a new
  object either way and takes `ref` where it did, so the props are no
  longer copied twice on each render.
- `Object.fromEntries(Object.entries(rest).filter(([k]) => ...))`, which
  each styled component writes, is written once per module as a function
  when more than one component drops the same props. Only a predicate
  reading nothing but its key moves, and none does where the module
  declares its own `Object`.
- A generated constant declared as a constant or an import of the module
  is read as that name, as one declared as another generated name was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Read code the build wrote as text as code, so every peephole rule sees it
- Render forwardRef components as one arrow: destructure props in the
  parameter and pass `ref` and constant attrs as JSX attributes
- Copy `.attrs` objects once (`rest.k = v`) instead of building `__devupProps`
- Inline a generated constant into the declaration that reads it next
- Collapse `c ? x : x` to `(c, x)`, and drop reads of parameters and
  functions from sequences
- Write a DOM `className` of a defined-or-undefined branch as `b && "a"`

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…thout the composition protocol

A styled component reading runtime values carried the composition protocol
(`__devupCss0`, the record tuple and the hook calls) even when nothing could
extend it at runtime. Only a component the module exports, or one given
`css` or read as a value, can receive it; every other one renders directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated constants no code changes are recorded as they are built, so a
member read of one becomes its literal, `void 0` when nothing writes it, or
the member of the object it was copied from. An object read only to be
spread is built where it is spread, an object pattern no longer reads
properties a later key overrides, `filter(Boolean)` loses parts written
falsy, objects lose spreads of nothing, and templates splice in strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… value cannot change

A generated constant read once now moves to where it is read when evaluating
its value there does nothing and gives the same value: a literal, a binding
nothing reassigns that an earlier statement initialized, a member of an
object rest whose key no code changes, or tests of those. Where a moved
value read a style variable, the styled renderer loses its body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…needs

Inside a function, a generated constant no code reads goes when computing
it does nothing, one only written into goes with each write left as the
value it assigns, and one bound to a short literal is written in place of
each read. A statement whose evaluation does nothing goes too, and `a && b`
whose `b` does nothing is kept as `a`. The top level keeps its constants,
which a script may read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI formats the sources to one line per call before measuring, so an
`else { return }` no input reaches gets a line of its own. Checks that
another already made are folded into helpers shared with a path that
fails them (the generated call and arrow `unwrap_iife` reads, the return
it unwraps, a sequence `split` and `omit_ahead` take apart, a captured
class), let chains replace the rest, and the paths inputs do reach gain
tests: values `defined` tells apart, omissions a key, default or rest
rules out, a body not ending in a return, an empty object nothing reads,
a name written through a pattern, handed values of a namespace or of a
change, a CommonJS module whose exports the build cannot read, and props
with nothing withheld.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The attrs of an imported styled component are now written straight onto
the element as `role="note"` rather than through an object literal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…className prop

A styled component now writes its classes as
`className ? `... ${className}` : "..."`, so the hoist check reads the
classes written when no className is given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A condition with two equal branches is written as `(test, value)`, and
the code generator drops the parentheses, which JSX needs around a comma
operator: `stroke={theme === "light", "#6159D4"}` does not parse. Inside
a JSX expression the sequence is written as `[test, value][1]`, which
evaluates its parts in the same order and gives the same value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le does not hold

A literal key into a literal table, such as `[1, 0.5][-10]`, `{ a: 1 }["wrong"]`
or a hole in `[1, , null][1]`, always reads `undefined`. The extractor emitted a
runtime lookup for it, with CSS for every branch and an empty `class=""`.

Now such a selection reaches the member extractor as written, as on main:
- an array: any numeric literal key
- an object: a string, a template with no expressions, or a non-negative
  integer, when the table holds the key or it is not a name every object
  inherits

The table may evaluate nothing: only literal values and plain properties, with
holes at its top level. This assumes the built-in prototypes are untouched, as
main did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rops

A spread the build cannot read in full reaches the element as it is. When it
names styles, those styles never became classes. They landed on the DOM as
props, with no error.

The build now refuses two such spreads:
- `<Box {...base} />` where code changes `base`. This restores the error main
  gave.
- An object that names styles next to a key only the running code knows. For
  example, `{ p: 1, [k]: 1 }` or `{ ...rest, p: 1 }`. The build cannot tell
  which of its styles that key replaces. This covers JSX and `jsx()` calls.

An object with no style keys still passes through as it is.

Contract tests that pinned the old pass-through now expect the build error.
Fixtures that used such an object for something else now give those keys at
runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as props

`<Box {...(on ? { p: 1 } : rest)} />` compiled with no styles. When `on` held,
`p` reached the DOM as a prop. Main refused it with "cannot use `rest`", and
the build does so again.

A spread's sides are read as the build reads rules:
- a side whose styles the build reads, next to one it cannot read, is refused
  at the side it cannot read
- an object among them that names styles next to keys only the running code
  knows is refused as before

`on && { … }` compiled with no styles too, though it spreads what
`on ? { … } : null` does: a falsy left side is a primitive. It is now written
as that conditional and compiles to a class.

Contract tests that pinned raw values for a literal next to runtime props now
expect the build error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d styles behind guards

`jsx(Box, { ...(on ? { p: 1 } : null) })` and `{ ...(on && { p: 1 }) }`
compiled with no class, and `p` reached the element as a prop. Main
compiled them. They are now read as `<Box {...(on ? { p: 1 } : null)} />`
is: each side's styles become a conditional class, and the props a side
names next to its styles still reach the element.

`<Box {...(rest || knownImport)} />` with an imported `{ bg: 'red' }`
passed `bg` on as a prop, as only conditional spreads read imported
objects. A logical spread now reads them when a side names styles, so
- `rest || knownImport` is refused at `rest`
- `cond && knownImport` compiles to a class
A logical spread of imported objects naming no styles is left as written.

A side `(on, { p: 1 })` spreads its last expression and is read so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ay does not hold

`<Flex opacity={[1, 0.5][+10]} />` always reads `undefined`, yet it
emitted both opacity classes and a runtime lookup table. `+10` is the
number `10` as `-10` is `-10`, so the literal index is now kept for the
member extractor, which selects no style: `<div className="a" />`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A style value written longer than the one value it always holds emitted a
class for every value it was written with, and a runtime lookup among them:
- `opacity={[1, 0.5]['10']}` emitted both classes, though it reads `undefined`
- `opacity={[1, 0.5]['1']}` emitted `1` too, though it reads `0.5`
- `[1, 0.5][0 + 1]`, `[1, 0.5][(0, 1)]`, `[1, 0.5][true ? 0 : 1]` kept the table
- `!0 ? 1 : 0.5` kept both classes and the test
- `[1, 0.5, 0.2][c ? 0 : 1]` emitted `0.2`, which no key reads
- `({ a: 1, a: 0.5 }).a`, `({ ...{ a: 1 }, a: 0.5 }).a` and `(0, 0.5)` were
  left to the runtime as a CSS variable

Style values are now written as the value they hold before the build reads
them: a literal test picks its side, numbers add up, a literal key into a
literal table reads what it holds there (`void 0` where it holds nothing),
and a conditional key into a table of literals chooses between what each
key reads. Only literals are left out, whose evaluation the code cannot
observe, so the code runs the same steps; a `styleOrder` is still reported
as written.

This holds for JSX props, `jsx()` props, the `css` prop, `css()`, `css`
templates and `styled`. `styled` reads `void 0` as `undefined`, no style.

A `jsx()` conditional spread whose props are captured for a call after it
is now covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Style values are folded before the extractor reads them, so its own
literal tests now see only `undefined`, which a binding may name and the
fold leaves as written. The extractor reads a literal test as the fold
does, and its nullish test so reads `void 0` as `undefined`.

Covered:
- `undefined` on the left of `&&`, `||`, `??` and as a test, alone and
  captured around calls
- a styled lookup whose key no fold reads
- tables whose left-out entries are nested arrays and objects
- a string on the left of a captured logical

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`<Box {...(rest || { bg: 'red' })} />` and `{...(rest ?? knownImport)}` are
build errors now: the build would otherwise hand the fallback's styles to
the element as props. no-unknown-style-spread still reported them, and its
agreement tests still expected the build to pass them through.

The rule now repeats no build error, as for the mixed conditionals: a
spread whose runtime side sits next to a side the build reads styles from
is not reported. A side the build reads is an object literal naming keys,
an import or a const holding one, of a style-capable type. Empty literal,
DOM-only and computed fallbacks are passed through, and still report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions

`jsx(Box, { ...styles })`, `jsx(Box, { bg: theme.bg })` and the
`createElement` forms never inlined the imported constants their props
read, so the build passed the object through at runtime or set a CSS
variable, where `<Box {...styles} />` compiles to a static class. The
props of a call building a component of the package are now read as its
JSX attributes are, spreads included, so both forms compile alike: a
spread mixing an import with a runtime value is the same build error.

`styled.div`opacity: ${undefined};`` set a CSS variable holding nothing,
and `${false}` wrote `opacity: false`. styled-components and Emotion
interpolate `null`, `false` and `undefined` as no text, so the build now
writes them as none, and drops a declaration left without a value, as
browsers do. An `undefined` a binding names is still read as written.

The signed-infinity alias test pinned the old dynamic call output; the
call now reads `NaN`, as the JSX element already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stable clippy now reports `map(f).unwrap_or_default()`, which it asks to
write as `map_or_default(f)`, and a clone of a value read for the last
time. CI installs stable, so the lint step failed on code this branch
did not change. The rewrites are clippy's own and change no behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No code installs the hook, and the optional dependency is enabled only by
`--all-features`. Current cargo reports it as unused, which the lint step
denies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI installs stable, which became rustc 1.100.0 on 2026-11-13. Its
coverage instrumentation reports the head line of a match arm with a
block body as uncovered even when its body runs, so cargo-tarpaulin
0.37.6 measured 97.56% across files this branch never touched, and the
100% threshold failed. The new clippy lints failed the lint step the
same way.

Pin the toolchain to 1.99.1, the last release CI passed on, and revert
the rewrites made for the 1.100 clippy, which 1.99.1 does not ask for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Return CI to the latest stable Rust, as the workflow promises, and keep
the rewrites the 1.100 clippy asks for: `map(f).unwrap_or_default()` as
`map_or_default(f)`, and no clone of a value read for the last time.
They compile on 1.99.1 as well.

Only the coverage run stays on 1.99.1. cargo-tarpaulin 0.37.6, its latest
release, misreads the coverage rustc 1.100 records: it reports the first
line of match arms, impls and macros whose bodies run as uncovered, 662
lines across files no test misses. Neither tarpaulin's ptrace engine
(90.5%) nor cargo-llvm-cov reproduces the measure on 1.100 either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vite 8.3.8, vinext 1.4.5, next-intl 4.15.1, prettier 3.10.1,
eslint-plugin-mdx 3.9.5, @babel/core 8.1.2, StyleX 0.19.2 and errno
0.3.15. Every other Rust crate is already at its latest release, and the
React 18 example keeps React 18, which it exists to test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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