Skip to content

fix(extractor): filter every transient prop from styled rest spreads - #774

Merged
owjs3901 merged 6 commits into
mainfrom
fix/transient-props
Oct 18, 2026
Merged

owjs3901 merged 6 commits into
mainfrom
fix/transient-props

Conversation

@owjs3901

@owjs3901 owjs3901 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Refs #689, #682 (W50: transient props). #711 위에 쌓은 PR이며, #711이 먼저 병합된 뒤 이 PR을 병합해야 합니다. 기준 커밋은 9e4d28abcf769705644818aba5d6bc943b2c6ff3입니다. CI의 publish.yml 실행을 위해 base는 main입니다.

요약

styled-components 5.1의 transient props 규칙대로, 이름이 $로 시작하는 모든 props를 렌더링 대상에 전달하지 않습니다. 소스에서 읽거나 열거하지 않은 이름, 호출자가 런타임 spread로 넘긴 이름, attrs가 만든 이름에도 적용됩니다. 태그와 컴포넌트, as로 바뀐 대상 모두 같은 규칙을 따릅니다.

생성된 styled 컴포넌트의 rest spread에만 짧은 인라인 필터를 넣습니다. 추가 import나 스타일링 런타임은 없고, rest spread가 없는 경로에는 필터가 생기지 않습니다. 스타일 함수와 attrs는 필터 전 props를 계속 읽으므로 $tone 같은 값은 기존 CSS 변수에 정상 반영됩니다. #711의 read props, theme, 빌드 시 shouldForwardProp, 기본 태그 정책은 변경하지 않았습니다.

migration 문서에 transient props 지원을 명시하고, coordinator가 승인한 같은 페이지의 두 오래된 .withConfig() 설명도 고칩니다. shouldForwardProp는 빌드 시 평가하고, displayName/componentId 등 다른 설정 메타데이터는 무시한다는 #711 동작으로 맞춥니다.

동작

실제 WASM 프로브 입력:

import { styled } from '@devup-ui/react'
export const Card = styled.div.withConfig({ shouldForwardProp: () => true })({ p: 2 })

변경 전 실제 생성 코드:

export const Card = ({ style, className, as: DevupAs = "div", forwardedAs, ...rest }) => <DevupAs {...rest} as={forwardedAs} className={["a", className].filter(Boolean).join(" ")} style={style} />;

변경 후 실제 생성 코드:

export const Card = ({ style, className, as: DevupAs = "div", forwardedAs, ...rest }) => <DevupAs {...Object.fromEntries(Object.entries(rest).filter(([k]) => k[0] !== "$"))} as={forwardedAs} className={["a", className].filter(Boolean).join(" ")} style={style} />;

기존 CSS import와 .a{padding:8px}는 그대로입니다. 이 프로브의 생성 코드 크기는 236 → 302 bytes(+66), Bun minify 후 252 → 314 bytes(+62)입니다. rest spread를 전달하는 컴포넌트마다 이 정도 코드와 props 복사/필터 비용이 추가되며, 사용자가 승인한 비용입니다. 스타일 계산을 런타임으로 옮기지는 않습니다.

React 서버 렌더링에서 변경 전에는 $, $id, $other, $forced, $fromAttrs가 대상에 도달해 Invalid attribute/unknown-prop 경고가 발생했고, 컴포넌트 대상은 $ 키를 실제 받았습니다. React가 잘못된 속성을 최종 HTML에서 자체 제거하는 것만으로는 지원을 증명할 수 없으므로, 경고가 전혀 없다는 것과 컴포넌트가 받은 키도 함께 검사합니다. 일반 id, title, data-*는 유지합니다.

새로 생기는 오류

없습니다. #711의 기존 빌드 오류 및 지원 범위를 바꾸지 않습니다.

남는 한계

  • 런타임에만 도착하는 $ 없는 임의의 이름(invented 등)에 대한 전체 유효 속성 검증은 이 작업에 포함하지 않습니다. #711처럼 기존 rest 전달 및 빌드 시 확인 가능한 forwarding 정책을 유지합니다.
  • 이 기준 브랜치의 public styled TypeScript 타입은 .attrs()와 .withConfig()를 선언하지 않습니다. 이 PR에서 타입을 확장하지 않고, 런타임 렌더링 회귀 테스트는 suppression/cast 없는 .test.jsx로 작성했습니다. 별도 #738의 정확한 head 0dcb7174172cb3382bc4a65c5c5f4c5bfeb39e2c는 StyledTemplate.attrs/withConfig를 이미 선언하고 public/compat export도 이를 사용합니다.
  • 기존 Vite/Webpack plugin 테스트가 describe 범위에서 console.error = mock()으로 바꾼 뒤 복구하지 않는 테스트 격리 문제가 발견되었습니다(packages/vite-plugin/src/__tests__/plugin.test.ts:227, packages/webpack-plugin/src/__tests__/plugin.test.ts:174). 이 PR에서는 기존 테스트를 바꾸지 않고 새 SSR 관찰을 한 foreground Bun child에 격리합니다. 경고 문자열을 필터링하거나 assertion을 완화하지 않습니다.

검증

  • 회귀 우선: 변경 전 Rust 생성 코드 실행 12/12 실패(런타임 $ 키가 실제 전달됨), React 서버 렌더링 5/5 실패(실제 경고 또는 컴포넌트 전달 키).
  • 변경 후 Rust 집중 테스트 15/15 통과: 태그/컴포넌트, forwarding 정책 없음/일반 predicate/항상 true, attrs, as, 빈 withholding 목록, 일반 JSX 무변경.
  • cargo fmt --all, cargo +1.99 clippy --workspace --all-targets -- -D warnings, cargo test --workspace: 통과(총 2,458 unit/doc tests, 0 실패).
  • fresh WASM 및 root build, bun test 5,503 통과/0 실패/100% coverage. 실제 SSR fixture 직접 실행도 exit 0이며 모든 시나리오의 warnings가 빈 배열, 태그의 id/title/data-*와 CSS 변수 값이 유지되고 컴포넌트/as 대상에 $ 키가 없음을 확인했습니다. 최종 hook/CI 결과는 아래 진행 상태로 갱신합니다.
  • bun lint: 기존 경고 2개 외 새 오류/경고 없음.
  • 두 commit의 실제 lint/tarpaulin/Bun hook은 통과했습니다. Windows LLVM tarpaulin은 97.74%(14,547/14,883), Bun은 100%를 보고했으므로 로컬 Rust 100%라고 주장하지 않습니다. Linux CI의 별도 100% coverage gate를 반드시 통과해야 ready로 전환합니다.
  • CARGO_BUILD_JOBS=2와 빌드/테스트 descendant tree 6 GiB watchdog을 사용했습니다. Git Bash에서는 PID 계보만으로 자식 메모리를 덜 집계하는 것을 확인해, 최종 전체트리 commit hook에는 Windows kernel Job의 12 GiB cap을 함께 적용했습니다. 실제 전체 Job 피크는 3,296,841,728 bytes로 제한 아래였습니다.
  • LSP는 Rust daemon timeout, JS 서버 미설치, MDX 미설정으로 사용 불가였으며, 실제 compiler/clippy/lint/test 결과로 검증했습니다.
  • #711의 기존 테스트 입력과 의미 assertion은 그대로입니다. 12개 문자열 expectation의 유일한 차이는 필터 wrapper이며 자동 byte audit로 확인했습니다: test_styled_extension_composes_base의 Ext/ObjExt/Twice/FromRuntime/FromChanging/Ordered(6), test_styled_with_component_and_as의 Aside(1), test_styled_prop_forwarding의 tone / $c+tone+theme / x+y+theme / $c / level+theme spread(5).
  • 아래 58개 snapshot의 101개 필터 삽입 외 모든 canonical repository byte가 기준 커밋과 동일함을 자동 audit로 확인했습니다. snapshot 본문의 CSS/style 결과, attrs, 타깃, class/style 및 기존 withholding expression은 동일합니다.
변경된 snapshot 58개 (libs/extractor/src/snapshots/)
extractor__tests__compiled_bindings_never_dangle.snap
extractor__tests__compose_rules_the_module_computes.snap
extractor__tests__compute_what_elements_and_styled_objects_take.snap
extractor__tests__conditional_composition.snap
extractor__tests__constant_objects_arrays_and_enums.snap
extractor__tests__create_global_style_collapses_to_a_null_component.snap
extractor__tests__emotion_and_styled_components_object_styles-2.snap
extractor__tests__emotion_and_styled_components_object_styles.snap
extractor__tests__emotion_global_component_and_import_surface.snap
extractor__tests__import_alias_custom_named_export.snap
extractor__tests__import_alias_emotion_styled.snap
extractor__tests__import_alias_multiple_aliases.snap
extractor__tests__import_alias_renamed_import.snap
extractor__tests__import_alias_styled_components.snap
extractor__tests__inline_imported_constants.snap
extractor__tests__layer_record_places_every_declaration.snap
extractor__tests__namespace_import_of_a_default_only_package_redirects.snap
extractor__tests__package_imported_whole.snap
extractor__tests__style_results_read_as_values.snap
extractor__tests__styled-10.snap
extractor__tests__styled-11.snap
extractor__tests__styled-2.snap
extractor__tests__styled-3.snap
extractor__tests__styled-4.snap
extractor__tests__styled-5.snap
extractor__tests__styled-6.snap
extractor__tests__styled-7.snap
extractor__tests__styled-8.snap
extractor__tests__styled-9.snap
extractor__tests__styled.snap
extractor__tests__styled_accepts_both_call_forms.snap
extractor__tests__styled_components_attrs_and_every_styled_import.snap
extractor__tests__styled_components_import_surface_fully_redirects.snap
extractor__tests__styled_components_of_any_base.snap
extractor__tests__styled_components_theme_resolves_to_css_variables.snap
extractor__tests__styled_object_form_theme_interpolation.snap
extractor__tests__styled_typescript_wrappers.snap
extractor__tests__styled_with_spread.snap
extractor__tests__styled_with_variable-2.snap
extractor__tests__styled_with_variable-3.snap
extractor__tests__styled_with_variable-4.snap
extractor__tests__styled_with_variable-5.snap
extractor__tests__styled_with_variable.snap
extractor__tests__styled_with_variable_like_emotion-2.snap
extractor__tests__styled_with_variable_like_emotion-3.snap
extractor__tests__styled_with_variable_like_emotion-4.snap
extractor__tests__styled_with_variable_like_emotion-5.snap
extractor__tests__styled_with_variable_like_emotion-6.snap
extractor__tests__styled_with_variable_like_emotion-7.snap
extractor__tests__styled_with_variable_like_emotion.snap
extractor__tests__styled_with_variable_like_emotion_props-2.snap
extractor__tests__styled_with_variable_like_emotion_props-3.snap
extractor__tests__styled_with_variable_like_emotion_props-4.snap
extractor__tests__styled_with_variable_like_emotion_props.snap
extractor__tests__styles_the_build_cannot_read.snap
extractor__tests__template_interpolations.snap
extractor__tests__wrong_styled_with_variable_like_emotion_props-2.snap
extractor__tests__wrong_styled_with_variable_like_emotion_props.snap

진행 상태

CI 38246397965는 정확한 head b77cc5d17c1a221babe73f3b0c4573dae8a1bc25에서 success입니다. coordinator의 green 통보 뒤 publish job 114801213376 로그를 직접 확인했습니다.

  • Linux Rust: 100.00% coverage, 10543/10543 lines covered. 새 styled_transient.rs도 15/15입니다.
  • Bun: 5503 pass, 0 fail, functions/lines coverage 모두 100%.
  • 네 job 모두 success: publish(lint, coverage, Bun, 패키지 entry/tarball 검증, landing e2e), benchmark, landing-next-e2e, vinext-rsc-css-e2e. Bun preload race 회귀도 8/8 통과했습니다.

이 결과 확인 후 ready로 전환합니다. #711 위의 stack 및 #711 먼저 병합 조건은 그대로 유지하며, 이 PR을 병합/승인/닫지 않습니다.

2026-10-15 갱신: 갱신된 #711 497aba0 (main 76b1a15 포함)을 병합했습니다. 충돌 파일 styled-components/page.mdx, extract_style_from_styled.rs, extractor/mod.rs, lib.rs, extractor__tests__styled_components_of_any_base.snap, extractor__tests__styled_components_theme_resolves_to_css_variables.snap은 부모 캡처·타깃·필터·문서 동작과 자식 transient 필터를 JSX/TS 모두 보존; 두 스냅샷은 재생성 후 any_base의 실제/eager 타깃·CSS와 theme의 스타일/원래 props 읽기 불변 및 전달용 복사본의 transient 필터만 확인; 캡처별 중첩 IIFE는 단일 IIFE의 순차 const와 (0, initializer)로 바꿔 기본 스택·익명 class/function 이름·guard·단일 평가 순서를 유지하고 RED/GREEN 회귀 및 Oracle APPROVE 확인 방식으로 해결했으며 부모 PR의 동작을 함께 보존했습니다. 새로 생기는 오류 없음.

2026-10-16 갱신: 확정 main a6ecc125를 포함한 갱신된 #711 de94d66d를 병합했습니다. 충돌 파일 없음(자동 병합), 두 부모 7afb427/de94d66d를 확인했으며 부모의 합성·attrs·타깃·props 읽기와 자식 transient 필터 및 기본 스택의 평탄한 단일 평가 바인더를 모두 유지했습니다. 새로 생기는 오류 없음.

기본 스택/CARGO_BUILD_JOBS=2/전체 트리 6 GiB 한도에서 fmt, Rust 1.99 all-target Clippy(-D warnings), 정상 workspace 테스트 4,116개/실패 0개가 통과했습니다. 새 스냅샷 후보 및 수동 스냅샷 변경은 없습니다. fresh WASM/root build 후 lint(기존 React 마이그레이션 경고 9개/오류 0개), Bun 5,914개/실패 0개/함수·라인 커버리지 100%가 통과했습니다. LSP는 기존 미설치/daemon timeout으로 검증 불가이며 clean으로 주장하지 않습니다.

실제 공개 WASM/React SSR에서 부모 시나리오 11개와 transient·attrs·native retarget·custom as·조건별 캡처 순서의 JSX/TS 조합 8개가 모두 통과했습니다. $ 키가 렌더 대상에 전달되지 않고 attrs는 원래 $tone을 읽으며 ordinary props/ref 값/children/호출자 class와 승리 CSS가 유지되고 렌더별 조건은 한 번만 실행됨을 확인했습니다. #774는 #711의 자식이며 #722 형제는 병합하지 않았으므로 forwardRef 지원을 새로 주장하지 않습니다. SSR은 DOM ref 부착을 검증하지 않습니다. 현재 head 0b13225d의 Linux CI는 별도로 확인하여 아래 덧붙입니다.

실제 WASM/공개 API 검증 출력:

import "@devup-ui/react/devup-ui.css";
export function make(on) {
	let count = 0;
	const condition = () => {
		count++;
		return on;
	};
	const Base = (__devupProps) => (({ style, className, as: DevupAs = "div", forwardedAs, ...rest }) => (() => {
		const __devupRecord0 = (0, condition());
		return <DevupAs {...Object.fromEntries(Object.entries((({ "$tone": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== "$"))} as={forwardedAs} className={[(__devupRecord0 ? true : false) ? __devupRecord0 ? "a" : "" : "b", className].filter(Boolean).join(" ")} style={style} />;
	})())(((__devupContext) => ({
		...__devupContext,
		className: [__devupContext.className, __devupProps.className].filter(Boolean).join(" ") || undefined
	}))(((__devupContext) => ((__devupAttrs) => ({
		...__devupContext,
		...__devupAttrs,
		className: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(" ") || undefined,
		style: {
			...__devupContext.style,
			...__devupAttrs.style
		}
	}))({
		...{},
		...((props) => ({
			"data-tone": props.$tone,
			"$fromAttrs": "attrs",
			id: "base"
		}))(__devupContext)
	}))({
		...__devupProps,
		className: undefined
	})));
	const Mid = (__devupProps) => (({ style, className, as: DevupAs = "section", forwardedAs, ...rest }) => (() => {
		const __devupRecord1 = (0, condition());
		return <DevupAs {...Object.fromEntries(Object.entries((({ "$tone": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== "$"))} as={forwardedAs} className={[(__devupRecord1 ? true : false) ? __devupRecord1 ? "a" : "" : "b", className].filter(Boolean).join(" ")} style={style} />;
	})())(((__devupContext) => ({
		...__devupContext,
		className: [__devupContext.className, __devupProps.className].filter(Boolean).join(" ") || undefined
	}))(((__devupContext) => ((__devupAttrs) => ({
		...__devupContext,
		...__devupAttrs,
		className: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(" ") || undefined,
		style: {
			...__devupContext.style,
			...__devupAttrs.style
		}
	}))({
		...{},
		...((props) => ({
			"data-tone": props.$tone,
			"$fromAttrs": "attrs",
			id: "base"
		}))(__devupContext)
	}))({
		...__devupProps,
		className: undefined
	})));
	const Ext = (__devupProps) => (({ style, className, as: DevupAs = "section", forwardedAs, ...rest }) => (() => {
		const __devupRecord0 = (0, condition());
		return <DevupAs {...Object.fromEntries(Object.entries((({ "$tone": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== "$"))} as={forwardedAs} className={[`c ${(__devupRecord0 ? true : false) ? __devupRecord0 ? "a" : "" : "b"}`, className].filter(Boolean).join(" ")} style={style} />;
	})())(((__devupContext) => ({
		...__devupContext,
		className: [__devupContext.className, __devupProps.className].filter(Boolean).join(" ") || undefined
	}))(((__devupContext) => ((__devupAttrs) => ({
		...__devupContext,
		...__devupAttrs,
		className: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(" ") || undefined,
		style: {
			...__devupContext.style,
			...__devupAttrs.style
		}
	}))({
		...{},
		...((props) => ({
			"data-tone": props.$tone,
			"$fromAttrs": "attrs",
			id: "base"
		}))(__devupContext)
	}))({
		...__devupProps,
		className: undefined
	})));
	return {
		Ext,
		count: () => count
	};
}

/*! devup-ui v1.0.83, | Apache License 2.0 | https://devup-ui.com */.a{color:blue}.b{color:red}.c{padding:4px}

import "@devup-ui/react/devup-ui.css";
import { jsx as __devupJsx0 } from "react/jsx-runtime";
export function make(on) {
	let count = 0;
	const condition = () => {
		count++;
		return on;
	};
	const Base = (__devupProps) => (({ style, className, as: DevupAs = "div", forwardedAs, ...rest }) => (() => {
		const __devupRecord0 = (0, condition());
		return __devupJsx0(DevupAs, {
			...Object.fromEntries(Object.entries((({ "$tone": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== "$")),
			className: [(__devupRecord0 ? true : false) ? __devupRecord0 ? "a" : "" : "b", className].filter(Boolean).join(" "),
			style,
			as: forwardedAs
		});
	})())(((__devupContext) => ({
		...__devupContext,
		className: [__devupContext.className, __devupProps.className].filter(Boolean).join(" ") || undefined
	}))(((__devupContext) => ((__devupAttrs) => ({
		...__devupContext,
		...__devupAttrs,
		className: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(" ") || undefined,
		style: {
			...__devupContext.style,
			...__devupAttrs.style
		}
	}))({
		...{},
		...((props) => ({
			"data-tone": props.$tone,
			"$fromAttrs": "attrs",
			id: "base"
		}))(__devupContext)
	}))({
		...__devupProps,
		className: undefined
	})));
	const Mid = (__devupProps) => (({ style, className, as: DevupAs = "section", forwardedAs, ...rest }) => (() => {
		const __devupRecord1 = (0, condition());
		return __devupJsx0(DevupAs, {
			...Object.fromEntries(Object.entries((({ "$tone": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== "$")),
			className: [(__devupRecord1 ? true : false) ? __devupRecord1 ? "a" : "" : "b", className].filter(Boolean).join(" "),
			style,
			as: forwardedAs
		});
	})())(((__devupContext) => ({
		...__devupContext,
		className: [__devupContext.className, __devupProps.className].filter(Boolean).join(" ") || undefined
	}))(((__devupContext) => ((__devupAttrs) => ({
		...__devupContext,
		...__devupAttrs,
		className: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(" ") || undefined,
		style: {
			...__devupContext.style,
			...__devupAttrs.style
		}
	}))({
		...{},
		...((props) => ({
			"data-tone": props.$tone,
			"$fromAttrs": "attrs",
			id: "base"
		}))(__devupContext)
	}))({
		...__devupProps,
		className: undefined
	})));
	const Ext = (__devupProps) => (({ style, className, as: DevupAs = "section", forwardedAs, ...rest }) => (() => {
		const __devupRecord0 = (0, condition());
		return __devupJsx0(DevupAs, {
			...Object.fromEntries(Object.entries((({ "$tone": __devupOmit0, ...__devupDom }) => __devupDom)(rest)).filter(([k]) => k[0] !== "$")),
			className: [`c ${(__devupRecord0 ? true : false) ? __devupRecord0 ? "a" : "" : "b"}`, className].filter(Boolean).join(" "),
			style,
			as: forwardedAs
		});
	})())(((__devupContext) => ({
		...__devupContext,
		className: [__devupContext.className, __devupProps.className].filter(Boolean).join(" ") || undefined
	}))(((__devupContext) => ((__devupAttrs) => ({
		...__devupContext,
		...__devupAttrs,
		className: [__devupContext.className, __devupAttrs.className].filter(Boolean).join(" ") || undefined,
		style: {
			...__devupContext.style,
			...__devupAttrs.style
		}
	}))({
		...{},
		...((props) => ({
			"data-tone": props.$tone,
			"$fromAttrs": "attrs",
			id: "base"
		}))(__devupContext)
	}))({
		...__devupProps,
		className: undefined
	})));
	return {
		Ext,
		count: () => count
	};
}

/*! devup-ui v1.0.83, | Apache License 2.0 | https://devup-ui.com */.a{color:blue}.b{color:red}.c{padding:4px}
[
  {
    "extension": "jsx",
    "on": false,
    "override": false,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "red"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<section id=\"base\" title=\"kept\" data-tone=\"gold\" as=\"nav\" class=\"c b caller\">child</section>",
    "warnings": []
  },
  {
    "extension": "jsx",
    "on": false,
    "override": true,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "red"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<aside id=\"base\" title=\"kept\" data-tone=\"gold\" as=\"aside\" class=\"c b caller\">child</aside>",
    "warnings": []
  },
  {
    "extension": "jsx",
    "on": true,
    "override": false,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "blue"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<section id=\"base\" title=\"kept\" data-tone=\"gold\" as=\"nav\" class=\"c a caller\">child</section>",
    "warnings": []
  },
  {
    "extension": "jsx",
    "on": true,
    "override": true,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "blue"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<aside id=\"base\" title=\"kept\" data-tone=\"gold\" as=\"aside\" class=\"c a caller\">child</aside>",
    "warnings": []
  },
  {
    "extension": "ts",
    "on": false,
    "override": false,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "red"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<section id=\"base\" title=\"kept\" data-tone=\"gold\" class=\"c b caller\" as=\"nav\">child</section>",
    "warnings": []
  },
  {
    "extension": "ts",
    "on": false,
    "override": true,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "red"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<aside id=\"base\" title=\"kept\" data-tone=\"gold\" class=\"c b caller\" as=\"aside\">child</aside>",
    "warnings": []
  },
  {
    "extension": "ts",
    "on": true,
    "override": false,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "blue"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<section id=\"base\" title=\"kept\" data-tone=\"gold\" class=\"c a caller\" as=\"nav\">child</section>",
    "warnings": []
  },
  {
    "extension": "ts",
    "on": true,
    "override": true,
    "transientKeys": [],
    "attrsTone": "gold",
    "captureReadsPerRender": 1,
    "selected": [
      [
        "color",
        "blue"
      ],
      [
        "padding",
        "4px"
      ]
    ],
    "html": "<aside id=\"base\" title=\"kept\" data-tone=\"gold\" class=\"c a caller\" as=\"aside\">child</aside>",
    "warnings": []
  }
]

2026-10-16 CI 확인: head 0b13225d5f0511c57d621c7314ebfdae851bde8f의 run 38855620475은 모든 job 및 Codecov가 통과했습니다. publish job 116826297860 실제 로그에서 Linux Rust 100.00% coverage, 13134/13134 lines covered, Bun 5914개/실패 0개/전체 커버리지 100%를 확인했습니다. 확정 main a6ecc12 포함 및 현재 merge 상태는 최신 PR audit로 재확인했습니다.

2026-10-18 main 동기화 (W60k)

main a26bb57928a04091e229d0972048eee39f09eb53을 실제 두 부모 병합 042ed9f6021330eca914a3741538f5bac47b4fc9으로 반영했습니다. lib.rs, styled_source_tests/compatibility.rs 충돌은 main의 forwardRef/ref 전달과 이 PR의 $ 접두사 전달 필터를 모두 유지했습니다. 자동 병합된 렌더 경로의 두 의도를 확인했고, 실행 테스트 16개는 단언·입력·깊이를 유지한 채 forwardRef.render(props, ref) 관찰 어댑터로 맞췄습니다.

스냅샷 58개는 실제 테스트로 재생성한 뒤 전부 검사했습니다. main 출력에 $ 전달 필터 100곳을 더한 정확한 결과이며, 다른 출력 변경은 없습니다. 새로 생기는 오류: 이번 동기화로 추가되는 오류 없음. 남는 한계: 기존 설명 유지, 새 동작 한계 없음; LSP는 시간 초과/미설치로 검증 불가입니다.

최신 공유 Rust stable 1.99.1, 기본 스택, jobs=2에서 fmt/Clippy/전체 Rust 4,874 통과·0 실패, Bun 6,037 통과·0 실패 및 함수/라인 100%, lint 0 오류·기존 20 경고입니다. 전체 훅은 12 GiB 제한에서 654.495초·최대 3.431 GiB로 통과했고, 훅 뒤 WASM/루트 재빌드 및 실제 공개 API/SSR 34개도 다시 통과했습니다. 실제 예: <section id="base" title="kept" data-tone="gold" as="nav" class="c b caller">child</section>; 알 수 없는 $ 키는 전달되지 않고 attrs 입력·ref·조건 평가 1회는 유지됩니다. 로컬 Windows Rust 97.87%는 Linux 100% 증거가 아니며, 새 head CI는 push 직후 아직 생성 대기 중입니다. 원격 Linux 100%와 전체 job 결과는 이어서 확인합니다.

2026-10-18 추가 main 동기화 (W60k-r2)

사용자 병합 후 새 main 4baa1d7458e14aba30c5fa0c005b1011310fc365(#782/#751/#786/#773)을 실제 두 부모 병합 603c82a253c9c451455d04af1d26c9f712795ec7으로 반영했습니다. 유일한 충돌 libs/extractor/src/lib.rs의 테스트 모듈 삽입 위치는 기존 styled_transient_tests와 main의 exact_literal_coercion을 둘 다 그대로 유지했습니다. 프로덕션 코드는 자동 병합되었고, 기존 단언·입력·깊이·스냅샷을 줄이거나 바꾸지 않았습니다. 이번 추가 병합의 스냅샷 재생성은 0개입니다.

새로 생기는 오류: 충돌 해결 자체의 추가 오류는 없습니다. main #773의 imported native css()에 대한 비상수·순환·누락 정의의 위치 포함 오류는 main과 동일하게 유지됩니다. 남는 한계: 기존 설명 유지, 새 동작 한계 없음; LSP 시간 초과/미설치로 검증 불가입니다.

최신 공유 stable 1.99.1/기본 스택/jobs=2에서 fmt/Clippy/Rust 5,154 통과·0 실패·0 무시, Bun 6,324 통과·0 실패·함수/라인 100%, lint 0 오류·기존 20 경고입니다. 전체 훅은 12 GiB 제한에서 1,023.719초·최대 3.419 GiB로 통과했고, 훅 뒤 새 WASM/루트 빌드와 실제 공개 API/SSR 34개도 다시 통과했습니다. 로컬 Windows Rust 97.93%는 Linux 100% 증거가 아닙니다. 이전 head 042ed9f6의 CI 39123346351은 Linux 100%(13,571/13,571), Bun 100%, 5개 검증 job 및 Codecov까지 통과했지만, 이 새 head의 검증으로 재사용하지 않습니다. 새 head CI는 push 직후 생성 대기였으며 실제 결과를 이어서 확인합니다.

2026-10-18 최종 확인: 새 head 603c82a253c9c451455d04af1d26c9f712795ec7의 CI 39128538303 전체 성공입니다. Linux Rust 100.00%(13,941/13,941), Bun 6,324 통과·0 실패·함수/라인 100%, preload 회귀 9 통과·0 실패, publish/benchmark/landing-next-e2e/vinext-rsc-css-e2e/react-18 및 Codecov patch 모두 통과했습니다. 배포·release는 PR 실행의 정상 skip이며, 현재 MERGEABLE/CLEAN, main 4baa1d74 포함을 확인했습니다. 이전 head 결과나 외부 장애 예외를 재사용하지 않았습니다.

owjs3901 and others added 2 commits October 12, 2026 20:34
Refs #689, #682

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

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

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

Copy link
Copy Markdown
Contributor

Changepacks

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

Patch

  • Rewriting styled-components, Emotion and vanilla-extract imports keeps type-only imports type-only: import { type DefaultTheme, ThemeProvider } now leaves import type { DefaultTheme } on the package instead of a value import that made it a runtime dependency, and an import type declaration is left as written. spin.getName() on a keyframes() binding becomes the generated keyframes name instead of a call on a string that threw at runtime
  • The styled-components and Emotion compat ThemeProvider now gives its theme through React context: useTheme, withTheme and the new ThemeConsumer return the real theme object (an object merged over the outer theme, or what a theme function makes of it) instead of an empty Proxy of var() references, withTheme forwards refs, lets a caller's theme win and sets a displayName. Styles keep reading the CSS variables the provider declares. styled-components v6.4 createTheme is available from the compat entry, and ThemeConsumer and createTheme imports are redirected there instead of staying a runtime dependency on styled-components
  • 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
  • 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.
  • Preserve unitless legacy vendor numbers, classify vendor time values as milliseconds and emit the Opera vendor prefix correctly; reject nested responsive arrays and known dead unprefixed CSS property names with source-located build errors, while preserving supported experimental CSS, unknown properties and documented shorthands.
  • Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale
  • Typography documents the emitted default breakpoints, and source-backed integration tests keep the public breakpoint lists and range bounds synchronized with the production defaults. No runtime changes or new build errors.
  • @emotion/css is aliased by default: css and keyframes compile like Devup UI's own (numbers in px, as Emotion reads them), injectGlobal declares global CSS. cx takes class names, as in Emotion: strings and runtime values are classes kept as written (falsy values are skipped), an object is a { name: condition } class map, arrays nest, and inline css() calls and css() classes compose with the rules css(a, b) uses. merge(className) splits what the build can read (string tokens, template literal parts) and composes the css() classes in it, keeping a runtime string it cannot split as it is. Namespace members, constant-string bracket access, const destructuring and const alias chains compile through these same named APIs with scope-aware binding resolution; numeric values retain Emotion units after constant inlining and build-time evaluation. The Emotion import disappears when no runtime-only member is used. Ambient declarations type the styling imports (cx(...args: ClassNamesArg[]), merge(className: string)). New located build errors: cx() cannot use a spread, a getter, a setter or a method in a class map, or a template literal call; merge() takes exactly one class string, not a spread or a template literal call; the root module has no default export, namespace member keys must be constant strings naming supported exports, and styling functions cannot be passed, mutated or re-exported. Optional styling reads/calls, namespace rest destructuring, styling defaults or nested patterns, non-const aliases, exported styling bindings and reads whose initialization order cannot be established are build errors instead of runtime dependencies. Styling loaded through require, import-equals or dynamic import must use static ES module imports; an unawaited dynamic namespace that the build cannot follow is also a located error. Its runtime-only APIs (cache, flush, hydrate, sheet, getRegisteredStyles) stay on @emotion/css, including their nested/default destructuring, optional reads and recognized runtime-loader access
  • Emotion's CacheProvider is available from the compat entry as a no-op that renders its children, and import { CacheProvider } from '@emotion/react' is redirected there instead of keeping @emotion/react as a runtime dependency; the cache it configures holds styles Emotion inserts at runtime, which Devup UI writes at build time
  • In rule objects written against Emotion, styled-components or vanilla-extract, a property whose value is an array of literals (background: ['red', 'linear-gradient(...)']) declares each value in order as a browser fallback, as those libraries do, instead of being read as Devup UI breakpoints; numbers get px as the library gives them. Arrays holding runtime values stay as written
  • 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.
  • The extractor guide shows reproducible native-element output, generated CSS-variable bindings, and actual atomic CSS rather than obsolete class names. Documentation only: no new build errors.
  • 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")
  • 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
  • stylex.attrs() gives the dynamic CSS variables it sets as style attribute text ("--a:" + value), as StyleX does for spreading onto non-React targets, instead of a style object; stylex.props() keeps the object React takes. The attrs() return type declares style as a string
  • 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
  • Order standalone nonresponsive hover-capability guards within their selector group so active, focus and disabled styles can override hover. Keep responsive Tailwind hover and Devup selector prop ordering unchanged. No new build errors.
  • More of Tailwind CSS v4 compiles: container queries (@sm:, @max-md:, @min-[400px]:, @sm/main:) and @container utilities, mask-* (all but the gradient masks), text-shadow-, inset-shadow-, font-variant-numeric utilities (tabular-nums and the like), 3D transforms (rotate-x/y/z, translate-z, scale-z, translate-3d, scale-3d, transform-3d, backface-, perspective-, perspective-origin-), contain- and outline-hidden (with its forced-colors fallback). The project's Tailwind CSS is read further: functional @Utility with --value() and --modifier(), @apply in @Utility, and theme(--name) in arbitrary values; classes of @plugin entries stay as written. A className now compiles the classes of cn()/twMerge() calls whose arguments are all static (merged as tailwind-merge does, the later class wins), of consts bound to class strings, and of cva() calls (base, variants, compound variants). The Rsbuild plugin reads the Tailwind CSS again and compiles every module again when it changes, and the Bun plugin reads it again. No new build errors
  • Tailwind classes in a className follow Tailwind CSS v4: colors are the v4 default palette (oklch) and the colors and shadows of devup.json theme are usable as Tailwind theme values (bg-primary, text-text, shadow-card, as var(--token)); the opacity modifier (bg-red-500/50, /[71.37%], /(--alpha)) mixes with color-mix as v4 does; a trailing or leading ! makes every declaration important; blur, brightness, contrast, grayscale, hue-rotate, invert, saturate, sepia and their backdrop-* forms compose through Tailwind's custom properties instead of overwriting filter and backdrop-filter; gradients use bg-linear-, bg-conic- and bg-radial-* with from-, via- and to-* composing through the gradient variables (bg-gradient-to-* is not defined by v4 and stays as written); animate-* brings the @Keyframes it names and transition, duration, delay and ease follow v4; space-* and divide-* apply to the children like v4 (:where(& > :not(:last-child))); @Property rules are emitted only for the custom properties a class uses. Anything the build cannot reproduce exactly (drop-shadow-*) stays as written in the className. No new build errors
  • Tailwind classes in a className read the Tailwind CSS of the project: the file that imports tailwindcss (the tailwind.css option of devup.json, else a conventional path) gives @theme variables (colors, spacing, shadows, blurs, radii, easings, animations with their @Keyframes, default transitions, breakpoints), @Utility utilities and @custom-variant variants, and the Vite, webpack and Next.js plugins read it again when it or a CSS file it imports changes. Anything that cannot be read exactly, and the classes of namespaces the build does not read, stay as written. More of Tailwind CSS v4 compiles: ring-, inset-ring-, ring-offset-* and drop-shadow-* compose with shadows and filters, borders and outlines draw in --tw-border-style and --tw-outline-style (a border now shows its style), line-clamp, text decoration, border-spacing, logical and scroll spacing, tables, perspective, font-stretch and more; the arguments of clsx(), classnames() and classNames() and [...].join(' ') compile in place. Radii follow v4 (rounded-sm is 0.25rem, rounded-full is calc(infinity * 1px)). No new build errors
  • A styled-components or Emotion style function reading the theme by literal keys, such as p => p.theme.space[2] or ({ theme }) => theme.colors['brand'], now reads the CSS variable ThemeProvider declares (var(--space-2)). A theme read the build cannot turn into a CSS variable, such as p => p.theme, p => p.theme.brand() or theme.space[i], is a build error naming the file, line, column and code, where it used to read a theme object that does not exist at runtime and give undefined or throw
  • Styled components now remove every runtime prop whose name starts with $ before forwarding a rest spread to a tag or component, including props supplied by callers or attrs. Style and attrs functions can still read transient props, and all other build-time forwarding policies remain unchanged. The styled-components migration guide documents transient prop support. No new build errors
  • Generate React JSX-runtime calls for styled components in non-JSX source modules while preserving JSX and TSX output. No new build errors.
  • While @vanilla-extract/css is aliased, importing @vanilla-extract/recipes or @vanilla-extract/sprinkles is a build error naming the file, line and column: those packages generate CSS Devup UI does not compile, so their rules used to be silently missing from the stylesheet. The error says to write the styles with css(), or to disable the alias and build with the vanilla-extract plugin. Type-only imports are allowed
  • Skip source-map generation in the codeExtractWithoutSourceMap WASM export while preserving extracted code, CSS and dependencies. Add real WASM and Next loader regressions for map-enabled and map-disabled extraction. No new build errors.

@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
  • Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale
  • More of Tailwind CSS v4 compiles: container queries (@sm:, @max-md:, @min-[400px]:, @sm/main:) and @container utilities, mask-* (all but the gradient masks), text-shadow-, inset-shadow-, font-variant-numeric utilities (tabular-nums and the like), 3D transforms (rotate-x/y/z, translate-z, scale-z, translate-3d, scale-3d, transform-3d, backface-, perspective-, perspective-origin-), contain- and outline-hidden (with its forced-colors fallback). The project's Tailwind CSS is read further: functional @Utility with --value() and --modifier(), @apply in @Utility, and theme(--name) in arbitrary values; classes of @plugin entries stay as written. A className now compiles the classes of cn()/twMerge() calls whose arguments are all static (merged as tailwind-merge does, the later class wins), of consts bound to class strings, and of cva() calls (base, variants, compound variants). The Rsbuild plugin reads the Tailwind CSS again and compiles every module again when it changes, and the Bun plugin reads it again. No new build errors
  • Tailwind classes in a className read the Tailwind CSS of the project: the file that imports tailwindcss (the tailwind.css option of devup.json, else a conventional path) gives @theme variables (colors, spacing, shadows, blurs, radii, easings, animations with their @Keyframes, default transitions, breakpoints), @Utility utilities and @custom-variant variants, and the Vite, webpack and Next.js plugins read it again when it or a CSS file it imports changes. Anything that cannot be read exactly, and the classes of namespaces the build does not read, stay as written. More of Tailwind CSS v4 compiles: ring-, inset-ring-, ring-offset-* and drop-shadow-* compose with shadows and filters, borders and outlines draw in --tw-border-style and --tw-outline-style (a border now shows its style), line-clamp, text decoration, border-spacing, logical and scroll spacing, tables, perspective, font-stretch and more; the arguments of clsx(), classnames() and classNames() and [...].join(' ') compile in place. Radii follow v4 (rounded-sm is 0.25rem, rounded-full is calc(infinity * 1px)). No new build errors

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

Patch

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

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

Patch

  • The @devup-ui/react README and the css-utils-literal-only rule docs link their mention of build errors to the new Build Errors reference (https://devup-ui.com/docs/build-errors), which lists the build errors grouped by API; the docs sidebar lists the page after Supported Syntax & Limitations, whose Build errors section links to it. Documentation only: no new build errors
  • ESLint rules now check exactly what the build reads. The array, typography, media and styleOrder rules and css-utils-literal-only follow the import aliases the build compiles by default (@emotion/react, @emotion/styled, styled-components, @vanilla-extract/css) and the rules of styled (styled.div({...}), styled("div", {...}), styled(Base)({...}, {...}); not .attrs()), createGlobalStyle({...}) and the styles of Emotion Global, and check and fix a module-level const object that only styles read. Vanilla-extract stylesheets (.css.ts/.css.js) are no longer read as Devup UI. New rule no-runtime-read reports css/keyframes/styled/Box read as a value ("X is read at runtime, where it does not exist"), and css-utils-literal-only reports globals that do not exist where a .css.ts is evaluated (window, document, process, ...). No new build errors.
  • 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

  • Isolate the coordinator port file per owning process when another live next dev/build holds it, record the owner pid, and report port, pid and recovery when the coordinator is missing or unreachable
  • Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale
  • Tailwind classes in a className read the Tailwind CSS of the project: the file that imports tailwindcss (the tailwind.css option of devup.json, else a conventional path) gives @theme variables (colors, spacing, shadows, blurs, radii, easings, animations with their @Keyframes, default transitions, breakpoints), @Utility utilities and @custom-variant variants, and the Vite, webpack and Next.js plugins read it again when it or a CSS file it imports changes. Anything that cannot be read exactly, and the classes of namespaces the build does not read, stay as written. More of Tailwind CSS v4 compiles: ring-, inset-ring-, ring-offset-* and drop-shadow-* compose with shadows and filters, borders and outlines draw in --tw-border-style and --tw-outline-style (a border now shows its style), line-clamp, text decoration, border-spacing, logical and scroll spacing, tables, perspective, font-stretch and more; the arguments of clsx(), classnames() and classNames() and [...].join(' ') compile in place. Radii follow v4 (rounded-sm is 0.25rem, rounded-full is calc(infinity * 1px)). No new build errors
  • Skip source-map generation in the codeExtractWithoutSourceMap WASM export while preserving extracted code, CSS and dependencies. Add real WASM and Next loader regressions for map-enabled and map-disabled extraction. No new build errors.

@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
  • Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale
  • @emotion/css is aliased by default: css and keyframes compile like Devup UI's own (numbers in px, as Emotion reads them), injectGlobal declares global CSS. cx takes class names, as in Emotion: strings and runtime values are classes kept as written (falsy values are skipped), an object is a { name: condition } class map, arrays nest, and inline css() calls and css() classes compose with the rules css(a, b) uses. merge(className) splits what the build can read (string tokens, template literal parts) and composes the css() classes in it, keeping a runtime string it cannot split as it is. Namespace members, constant-string bracket access, const destructuring and const alias chains compile through these same named APIs with scope-aware binding resolution; numeric values retain Emotion units after constant inlining and build-time evaluation. The Emotion import disappears when no runtime-only member is used. Ambient declarations type the styling imports (cx(...args: ClassNamesArg[]), merge(className: string)). New located build errors: cx() cannot use a spread, a getter, a setter or a method in a class map, or a template literal call; merge() takes exactly one class string, not a spread or a template literal call; the root module has no default export, namespace member keys must be constant strings naming supported exports, and styling functions cannot be passed, mutated or re-exported. Optional styling reads/calls, namespace rest destructuring, styling defaults or nested patterns, non-const aliases, exported styling bindings and reads whose initialization order cannot be established are build errors instead of runtime dependencies. Styling loaded through require, import-equals or dynamic import must use static ES module imports; an unawaited dynamic namespace that the build cannot follow is also a located error. Its runtime-only APIs (cache, flush, hydrate, sheet, getRegisteredStyles) stay on @emotion/css, including their nested/default destructuring, optional reads and recognized runtime-loader access
  • Tailwind classes in a className read the Tailwind CSS of the project: the file that imports tailwindcss (the tailwind.css option of devup.json, else a conventional path) gives @theme variables (colors, spacing, shadows, blurs, radii, easings, animations with their @Keyframes, default transitions, breakpoints), @Utility utilities and @custom-variant variants, and the Vite, webpack and Next.js plugins read it again when it or a CSS file it imports changes. Anything that cannot be read exactly, and the classes of namespaces the build does not read, stay as written. More of Tailwind CSS v4 compiles: ring-, inset-ring-, ring-offset-* and drop-shadow-* compose with shadows and filters, borders and outlines draw in --tw-border-style and --tw-outline-style (a border now shows its style), line-clamp, text decoration, border-spacing, logical and scroll spacing, tables, perspective, font-stretch and more; the arguments of clsx(), classnames() and classNames() and [...].join(' ') compile in place. Radii follow v4 (rounded-sm is 0.25rem, rounded-full is calc(infinity * 1px)). No new build errors

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

Patch

  • The @devup-ui/react README and the css-utils-literal-only rule docs link their mention of build errors to the new Build Errors reference (https://devup-ui.com/docs/build-errors), which lists the build errors grouped by API; the docs sidebar lists the page after Supported Syntax & Limitations, whose Build errors section links to it. Documentation only: no new build errors
  • The styled-components and Emotion compat ThemeProvider now gives its theme through React context: useTheme, withTheme and the new ThemeConsumer return the real theme object (an object merged over the outer theme, or what a theme function makes of it) instead of an empty Proxy of var() references, withTheme forwards refs, lets a caller's theme win and sets a displayName. Styles keep reading the CSS variables the provider declares. styled-components v6.4 createTheme is available from the compat entry, and ThemeConsumer and createTheme imports are redirected there instead of staying a runtime dependency on styled-components
  • @emotion/css is aliased by default: css and keyframes compile like Devup UI's own (numbers in px, as Emotion reads them), injectGlobal declares global CSS. cx takes class names, as in Emotion: strings and runtime values are classes kept as written (falsy values are skipped), an object is a { name: condition } class map, arrays nest, and inline css() calls and css() classes compose with the rules css(a, b) uses. merge(className) splits what the build can read (string tokens, template literal parts) and composes the css() classes in it, keeping a runtime string it cannot split as it is. Namespace members, constant-string bracket access, const destructuring and const alias chains compile through these same named APIs with scope-aware binding resolution; numeric values retain Emotion units after constant inlining and build-time evaluation. The Emotion import disappears when no runtime-only member is used. Ambient declarations type the styling imports (cx(...args: ClassNamesArg[]), merge(className: string)). New located build errors: cx() cannot use a spread, a getter, a setter or a method in a class map, or a template literal call; merge() takes exactly one class string, not a spread or a template literal call; the root module has no default export, namespace member keys must be constant strings naming supported exports, and styling functions cannot be passed, mutated or re-exported. Optional styling reads/calls, namespace rest destructuring, styling defaults or nested patterns, non-const aliases, exported styling bindings and reads whose initialization order cannot be established are build errors instead of runtime dependencies. Styling loaded through require, import-equals or dynamic import must use static ES module imports; an unawaited dynamic namespace that the build cannot follow is also a located error. Its runtime-only APIs (cache, flush, hydrate, sheet, getRegisteredStyles) stay on @emotion/css, including their nested/default destructuring, optional reads and recognized runtime-loader access
  • Emotion's CacheProvider is available from the compat entry as a no-op that renders its children, and import { CacheProvider } from '@emotion/react' is redirected there instead of keeping @emotion/react as a runtime dependency; the cache it configures holds styles Emotion inserts at runtime, which Devup UI writes at build time
  • The README no longer claims every CSS-in-JS pattern compiles: it describes what the build compiles and that anything it cannot know is a CSS variable or a located build error, and links the new Supported Syntax & Limitations page, which lists what compiles, the style override rules (where a later part wins and where the CSS cascade decides), Baseline 2024 browser support, that a component taking the css prop must pass className and style on, the runtime-only APIs and how each is handled, and the known limitations
  • stylex.attrs() gives the dynamic CSS variables it sets as style attribute text ("--a:" + value), as StyleX does for spreading onto non-React targets, instead of a style object; stylex.props() keeps the object React takes. The attrs() return type declares style as a string
  • Styled components now remove every runtime prop whose name starts with $ before forwarding a rest spread to a tag or component, including props supplied by callers or attrs. Style and attrs functions can still read transient props, and all other build-time forwarding policies remain unchanged. The styled-components migration guide documents transient prop support. No new build errors

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

Patch

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

@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
  • Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale
  • More of Tailwind CSS v4 compiles: container queries (@sm:, @max-md:, @min-[400px]:, @sm/main:) and @container utilities, mask-* (all but the gradient masks), text-shadow-, inset-shadow-, font-variant-numeric utilities (tabular-nums and the like), 3D transforms (rotate-x/y/z, translate-z, scale-z, translate-3d, scale-3d, transform-3d, backface-, perspective-, perspective-origin-), contain- and outline-hidden (with its forced-colors fallback). The project's Tailwind CSS is read further: functional @Utility with --value() and --modifier(), @apply in @Utility, and theme(--name) in arbitrary values; classes of @plugin entries stay as written. A className now compiles the classes of cn()/twMerge() calls whose arguments are all static (merged as tailwind-merge does, the later class wins), of consts bound to class strings, and of cva() calls (base, variants, compound variants). The Rsbuild plugin reads the Tailwind CSS again and compiles every module again when it changes, and the Bun plugin reads it again. No new build errors
  • Tailwind classes in a className read the Tailwind CSS of the project: the file that imports tailwindcss (the tailwind.css option of devup.json, else a conventional path) gives @theme variables (colors, spacing, shadows, blurs, radii, easings, animations with their @Keyframes, default transitions, breakpoints), @Utility utilities and @custom-variant variants, and the Vite, webpack and Next.js plugins read it again when it or a CSS file it imports changes. Anything that cannot be read exactly, and the classes of namespaces the build does not read, stay as written. More of Tailwind CSS v4 compiles: ring-, inset-ring-, ring-offset-* and drop-shadow-* compose with shadows and filters, borders and outlines draw in --tw-border-style and --tw-outline-style (a border now shows its style), line-clamp, text decoration, border-spacing, logical and scroll spacing, tables, perspective, font-stretch and more; the arguments of clsx(), classnames() and classNames() and [...].join(' ') compile in place. Radii follow v4 (rounded-sm is 0.25rem, rounded-full is calc(infinity * 1px)). No new build errors

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

Patch

  • Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale
  • Tailwind classes in a className read the Tailwind CSS of the project: the file that imports tailwindcss (the tailwind.css option of devup.json, else a conventional path) gives @theme variables (colors, spacing, shadows, blurs, radii, easings, animations with their @Keyframes, default transitions, breakpoints), @Utility utilities and @custom-variant variants, and the Vite, webpack and Next.js plugins read it again when it or a CSS file it imports changes. Anything that cannot be read exactly, and the classes of namespaces the build does not read, stay as written. More of Tailwind CSS v4 compiles: ring-, inset-ring-, ring-offset-* and drop-shadow-* compose with shadows and filters, borders and outlines draw in --tw-border-style and --tw-outline-style (a border now shows its style), line-clamp, text decoration, border-spacing, logical and scroll spacing, tables, perspective, font-stretch and more; the arguments of clsx(), classnames() and classNames() and [...].join(' ') compile in place. Radii follow v4 (rounded-sm is 0.25rem, rounded-full is calc(infinity * 1px)). No new build errors

@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
  • Class names and file numbers no longer depend on the order files are processed in. Every plugin walks the files it can extract (the project's source and the included packages) before the first transform and numbers them in path order (a file collapsed into a bucket is numbered as the bucket, and only files that mention the package, StyleX or an aliased package take numbers) through the new seedFileMap, so identical builds give identical class prefixes, CSS file names and output hashes; in development, files that appear later are numbered after the existing ones. Styles are given class names in a fixed order, not the order of a hash set. The exported class, file and canonical maps are canonical JSON (sorted keys). Stylesheet evaluation (vanilla-extract) refuses Math.random(), Date.now(), new Date() and Date(), which differ between builds, with a build error; fixed dates such as new Date(0) are unchanged. resetBuildState starts a build from its own options: the prefix, atom hoisting, routes, buckets, names, numbers and styles an earlier build in the process left are cleared, and the prefix is set on every setup even when it is absent. Engine snapshots from concurrent transforms are written one after another per file, so an older snapshot is never written last. File lists are ordered by code point rather than the locale
  • Tailwind classes in a className read the Tailwind CSS of the project: the file that imports tailwindcss (the tailwind.css option of devup.json, else a conventional path) gives @theme variables (colors, spacing, shadows, blurs, radii, easings, animations with their @Keyframes, default transitions, breakpoints), @Utility utilities and @custom-variant variants, and the Vite, webpack and Next.js plugins read it again when it or a CSS file it imports changes. Anything that cannot be read exactly, and the classes of namespaces the build does not read, stay as written. More of Tailwind CSS v4 compiles: ring-, inset-ring-, ring-offset-* and drop-shadow-* compose with shadows and filters, borders and outlines draw in --tw-border-style and --tw-outline-style (a border now shows its style), line-clamp, text decoration, border-spacing, logical and scroll spacing, tables, perspective, font-stretch and more; the arguments of clsx(), classnames() and classNames() and [...].join(' ') compile in place. Radii follow v4 (rounded-sm is 0.25rem, rounded-full is calc(infinity * 1px)). No new build errors

@codecov

codecov Bot commented Oct 12, 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% <ø> (ø)
libs/extractor/src/extractor/styled_transient.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 12, 2026 12:23
owjs3901 and others added 4 commits October 15, 2026 05:28
Refs #689 #695

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>
Refs #682 #695 #688 #689

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