Skip to content

feat: number units, compat layers, module loading and strict build-time values (#676, #677, #678) - #681

Merged
owjs3901 merged 109 commits into
mainfrom
feat/compat-and-build-time-values
Sep 30, 2026
Merged

owjs3901 merged 109 commits into
mainfrom
feat/compat-and-build-time-values

Conversation

@owjs3901

@owjs3901 owjs3901 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

종합 PR

#676, #677, #678을 하나로 합쳤습니다. 이 브랜치는 main에 바로 병합할 수 있습니다.

세 PR은 #675 브랜치 위에 쌓여 있었는데, #675가 main에 squash로 병합되면서 모두 충돌했습니다. 이 브랜치는 세 PR의 커밋 99개를 main 위로 rebase했습니다. #675와 #679는 이미 main에 있으므로 이 PR의 변경에는 들어 있지 않습니다.

아래는 세 PR의 원래 본문입니다. 제목만 한 단계씩 내렸습니다.

#676: fix: vanilla-extract/StyleX number units and full engine for .css.ts

#675에 의존합니다. #675를 먼저 머지해 주세요. CI가 main 대상 PR에서만 돌기 때문에 base를 main으로 열었고, diff에는 #675 커밋 8개가 함께 보입니다. 이 PR의 변경은 마지막 4개 커밋입니다.

문제

  1. vanilla-extract·StyleX의 숫자가 Devup UI 스페이싱 스케일(×4)로 해석됨
    • VE style({ padding: 8 }) → padding: 32px (VE 원본은 8px)
    • StyleX fontSize: 16 → 64px, transitionDuration: 300 → 1200px
    • .css.ts가 아닌 일반 모듈에서 쓴 VE style() / globalStyle() / keyframes()도 똑같이 ×4가 적용됨
    • Devup 자체 unitless 목록이 좁아서 strokeWidth={2}가 8px, columns, zoom, orphans, column-count 등도 px로 바뀜
  2. Next 플러그인 .css.ts 정적 fast path의 비결정성
    • 프로덕션에서만 lite 엔진과 TS 정적 변환을 썼고, 개발 모드는 full(Boa)을 썼습니다. 같은 파일인데 클래스명과 export 순서가 달랐습니다.
    • 정적 변환은 리터럴만 쓴 style({...})만 처리할 수 있었습니다. vars, 템플릿 리터럴, createVar, 계산값은 결국 Boa가 필요합니다.

수정

  • extractor
    • VE: JSON.stringify에 native replacer(pixelify)를 넘깁니다.
      • VE 규칙대로 0이 아닌 숫자에만 px를 붙입니다.
      • unitless 속성, --*/var( 키, 배열 인덱스, vars는 그대로 둡니다.
    • 일반 모듈의 VE style() / globalStyle() / keyframes()는 import 별칭 단계에서 숫자 리터럴을 "8px"로 치환합니다.
    • StyleX: getNumberSuffix를 동일하게 옮겼습니다.
      • unitless는 그대로, 시간 속성은 ms, 나머지는 px입니다.
      • 반올림은 Math.round(x*10000)/10000입니다.
      • 동적 인자는 길이·시간 속성에서만 ((v) => typeof v === "number" ? v + "px" : v)(arg)로 감쌉니다.
    • MAINTAIN_VALUE_PROPERTIES를 vanilla-extract의 unitless 목록으로 확장했습니다.
      • 추가: strokeWidth, columns, columnCount, zoom, orphans, widows, gridArea, borderImage*, maskBorder*, SVG opacity 계열 등
      • Devup 네이티브 prop에도 적용되는 동작 변경입니다. 예: strokeWidth={2}가 8px에서 2가 됩니다.
  • next-plugin
    • static-vanilla.ts와 관련 옵션을 제거했습니다. .css.ts가 있으면 항상 full 엔진을 씁니다(없으면 기존처럼 lite).
    • 측정: 첫 .css.ts에 약 +20ms, 이후 파일당 1ms 미만입니다.
  • docs
    • README 10곳의 lite fast path 설명을 교체했습니다.
    • VE/StyleX 마이그레이션 문서에 숫자 규칙을 추가했습니다.
    • API 문서(Box/Button/Center/Grid/VStack)의 "×4 예외" 목록과 SKILL.md를 갱신했습니다.
  • changepack: @devup-ui/wasm, @devup-ui/next-plugin Patch

테스트

  • 단위 테스트
    • test_vanilla_extract_numbers_follow_vanilla_extract_units
    • test_stylex_numbers_follow_stylex_units
    • test_style_to_json_adds_vanilla_extract_units
    • test_vanilla_extract_numbers_become_px_outside_stylesheets
    • test_js_number_literal
  • 스냅샷 28개를 갱신했습니다(VE는 px, StyleX는 px/ms, 동적 값은 IIFE).
  • cargo fmt / clippy -D warnings(기본 + lite wasm 타깃), cargo test --workspace 전부 통과
  • bun test 5231 pass, 100% 커버리지 / 회귀 테스트 6 pass / next-plugin tsc·eslint 통과
  • 재빌드한 wasm(full·lite)으로 직접 확인:
    • VE .css.ts: font-size:16px, padding:8px, line-height:1.5, column-count:3
    • 일반 모듈의 VE keyframes: width:10px
    • StyleX: transition-duration:300ms, 동적 height에 px IIFE 적용

참고

  • 미검증 후보(별도 이슈 후보): Emotion·styled-components 객체 스타일의 숫자, VE createVar/createContainer 이름의 파일 간 충돌 가능성

#677: fix: vanilla-extract/Emotion/styled-components compat, global layers and typography precedence

#675 → #676 → 이 PR 순서로 머지해 주세요. CI가 main 대상 PR에서만 돌기 때문에 base를 main으로 열었습니다. diff에는 앞 PR 커밋이 함께 보이고, 이 PR의 변경은 마지막 4개 커밋입니다.

문제

미검증 후보로 남겨 둔 버그를 모두 재현해서 확인했습니다. 확인하는 과정에서 같은 계열 버그도 함께 찾았습니다.

  1. Emotion·styled-components 객체 스타일의 숫자가 ×4 스페이싱으로 해석됨
    • 예: css({ padding: 8 })가 32px로 나옴(원래 8px)
    • 같은 계열 버그:
      • styled('div', { shouldForwardProp })(...)가 컴포넌트 대신 React 요소를 export함(런타임 크래시)
      • styled.div(a, b)는 변환되지 않음
      • styled.div([a, b])는 스타일이 사라짐
      • css(a, b)는 ""가 됨
  2. VE 이름이 파일 간에 충돌
    • createVar는 --var-0, createContainer는 __container_0__로 고정되어 파일마다 같은 이름이 나옴
    • layer()의 값이 "undefined"가 됨
    • 테마 변수 이름도 파일 간에 충돌함. 예: 두 파일의 createGlobalTheme이 모두 --color-brand-global_theme_0-0을 만들어 서로 덮어씀
  3. VE 추가 버그
    • styleVariants: 같은 파일 안에서 tone.primary가 undefined라서 조합과 셀렉터가 깨짐. map 함수 형태를 무시함
    • 이름을 호출 순서로 매칭함. 그래서 { sm: style(...) }처럼 중첩된 호출이 있으면 그 뒤의 스타일이 전부 엉뚱한 이름에 붙음
    • createThemeContract export는 ReferenceError가 남
    • createVar()가 var()로 감싸지 않은 이름을 반환해서 `calc(${v} * 2)`가 깨짐
    • createTheme(tokens, debugId), 인자 3개짜리 createGlobalTheme, 대입하지 않은 createGlobalTheme이 무시됨
    • 조합이 한 단계만 이어짐
    • '@layer': {...}를 무시함
    • 레이어 선언 순서가 알파벳순으로 정렬됨
    • 가져온 쪽이 모두 사용되지 않는 .css.ts에서 TypeScript 변환이 export {};를 남겨 실행이 실패함
  4. 전역 @layer와 반응형을 함께 쓰면 레이어가 사라짐
    • 반응형 값에서는 @layer가 통째로 사라짐
    • 시트에서는 레이어 안 선언의 breakpoint 레벨이 누락됨
  5. typography
    • globalCss({ h1: { typography, fontSize } })에서 프리셋이 명시한 값을 모든 breakpoint에서 이김
    • 반응형 typography와 셀렉터 안 typography는 더 넓은 breakpoint에서 명시값을 이김

수정

  • vanilla-extract(vanilla_extract.rs 재작성)
    • 실행한 뒤 각 top-level 변수가 실제로 가진 값을 보고 스타일 이름을 붙입니다. 중첩 호출은 _veN으로 이름 붙이고, 일반 데이터 export는 값을 그대로 코드로 냅니다.
    • 생성되는 모든 이름을 {debugId}-{파일번호}-{n} 형식으로 파일마다 고유하게, 결정론적으로 만듭니다.
    • createVar/fallbackVar/createTheme/createGlobalTheme/contract/layer 동작을 VE와 똑같이 맞췄습니다.
    • globalLayer, createGlobalThemeContract, assignVars, globalFontFace, @property 선언을 추가했습니다.
    • variant를 일반 스타일로 처리하고 조합을 여러 단계까지 이어서 적용합니다.
  • Emotion·styled-components
    • 객체 스타일의 숫자를 px로 바꿉니다. 단, Devup 단축 prop(p, bg 등)은 스케일을 유지합니다.
    • 여러 인자와 배열을 병합합니다. 뒤 값이 이기고, 중첩 규칙은 합칩니다.
    • styled(tag, options)를 팩토리로 인식합니다.
    • css()가 클래스 참조와 규칙 객체를 함께 조합합니다.
  • layer
    • '@layer': { [name]: rules } 레코드 형태를 지원합니다.
    • 전역 @layer가 중첩·반응형 선언까지 적용됩니다.
    • 시트는 레이어 안의 선언도 일반 경로와 같은 writer로 출력합니다(레벨, 셀렉터, at-rule 유지).
  • 커스텀 속성
    • --* 값의 숫자를 스페이싱 스케일로 바꾸지 않고 그대로 둡니다(예: --columns: 4가 16px이 되던 문제).
  • typography
    • 같은 셀렉터에 명시한 속성이 있으면, 그 속성이 시작하는 breakpoint부터 프리셋 선언을 건너뜁니다.

테스트

  • 새 테스트
    • VE 단위 테스트 14개
    • layer·typography 파이프라인 테스트
    • Emotion·styled-components 스냅샷과 단위 테스트
  • 변경된 스냅샷은 모두 직접 검토했습니다.
  • 검증 결과
    • cargo fmt, clippy -D warnings(기본 빌드와 lite wasm) 통과
    • cargo test --workspace 통과
    • bun test 5231 pass(커버리지 100%), 회귀 테스트 6 pass
  • 재빌드한 wasm으로 위 버그를 모두 다시 재현해서 고쳐진 것을 확인했습니다.

참고 (아직 남은 한계)

  • .css.ts끼리 import하는 경우는 여전히 지원하지 않습니다. 모듈 로더가 필요해서 별도로 설계해야 합니다.
  • styled-components의 .attrs()/.withConfig()와, cond && style 같은 조건부 조합 조각은 지원하지 않습니다.
  • 파일 하나에 styled import가 여러 개 있으면 마지막 것만 인식합니다(기존 한계).

커버리지

  • libs/extractor/tests/vanilla_extract.rs 통합 테스트로 VE API 전체를 라이브러리 빌드를 거쳐 실행합니다.
  • CollectedStyles::resolve의 selector 인자를 impl Fn에서 fn 포인터로 바꿔 단형화된 복사본이 하나만 생기게 했습니다. CI는 rustfmt max_width = 100000으로 다시 포맷한 뒤 tarpaulin을 돌리기 때문에, 보고된 줄 번호(801)가 로컬 소스가 아니라 이 분기를 가리키고 있었습니다.

#678: feat: module loading for stylesheets, imported constants, and remaining compat gaps

머지 순서: #675 → #676 → #677 → 이 PR. CI가 main 대상 PR에서만 돌기 때문에 base를 main으로 열었습니다. 이전 PR들의 커밋이 포함되어 있으며, 이 PR의 변경은 feat/compat-gaps-and-module-loader 브랜치에서 #677 이후 커밋들입니다.

요약

#677에서 남은 한계로 적었던 항목을 모두 해결하고, import한 상수를 정적 값으로 처리하는 최적화를 추가했습니다.
이후 라운드에서 StyleX 조건부 값, 빌드 타임 계산값(full 엔진), 상수 스타일 객체를 지원하고, 빌드가 읽을 수 없는 스타일은 조용히 버리지 않고 위치가 붙은 빌드 에러로 보고합니다.

모듈 로더 (.css.ts가 다른 모듈을 import)

  • 번들러가 쓰는 방식으로 import를 해석하는 resolver를 받습니다: setModuleResolver((specifier, importer) => ({ path, code }))
    • Rust API: extract_with_modules
    • 각 플러그인은 createModuleResolver로 이를 등록합니다. 해석 대상은 상대 경로, tsconfig paths, theme.css→theme.css.ts 보완입니다.
  • import된 스타일시트는 번들러와 같은 파이프라인으로 추출된 뒤, 그 출력을 평가합니다.
    • 그래서 export하는 클래스/변수 이름이 그 파일이 직접 내보내는 CSS와 정확히 일치합니다.
    • importer는 import "./theme.css"를 유지해서 해당 CSS가 번들에 포함되게 합니다.
  • 일반 모듈도 지원합니다. 지원하는 export 형태: named, default, export { a as b }, export … from, export *, export * as ns
  • 순환 import는 오류로 처리되고, 해당 스타일시트는 기존처럼 plain 추출로 폴백합니다.
  • 읽은 파일은 Output.dependencies로 반환되고, 각 플러그인이 watch 대상으로 등록합니다.
    • vite addWatchFile, webpack/next/rsbuild addDependency, Turbopack coordinator는 응답에 포함합니다.
  • .css.ts의 side-effect import(import './global.css')가 사라지던 기존 버그도 고쳤습니다.

import한 상수 인라인 (제안된 최적화)

  • <Box color={PRIMARY} />에서 PRIMARY가 다른 모듈의 const 문자열/숫자이면, CSS 변수 대신 정적 클래스로 처리합니다.
    • 지원하는 형태: 리터럴, as const 객체의 멤버 접근, alias, re-export, default
  • 스타일 위치(패키지 컴포넌트의 prop, 패키지 함수의 인자)에서 읽는 import만 resolver로 조회합니다.
  • 원시값만 치환하고 scope를 고려해서 해석하므로(shadowing 안전) 런타임 동작은 같습니다.
  • lite 엔진에서도 동작합니다.

호환성

  • styled-components .attrs():
    • 객체와 함수 모두 지원하고, 순서대로 props 위에 병합합니다.
    • 함수 attrs는 사용자 코드일 뿐이라 스타일은 여전히 빌드 타임에 추출됩니다. 그래서 에러 대신 지원했습니다.
  • .withConfig(): 제거합니다(런타임 옵션뿐입니다).
  • 한 파일에 여러 라이브러리의 styled import가 있어도 모두 추출합니다.
  • 조건부 조합(css(base, cond && {...}), 삼항, styled.div(...))은 속성별 삼항으로 병합합니다.
    • atomic 클래스 두 개의 우선순위는 스타일시트 순서로 정해지므로, 분기마다 클래스를 만드는 방식은 틀립니다.
    • 조건부 클래스는 cond ? cls : ''로 합칩니다. 기존에는 "false"가 출력되거나 스타일이 통째로 사라졌습니다.
  • Emotion의 숫자→px 변환이 조건 분기 안까지 적용됩니다.

레이어 · 타이포그래피 (cascade)

  • @layer 안의 동적 값도 레이어 안에 둡니다.
  • 레이어가 클래스/변수 이름 키에 포함됩니다.
    • 기존에는 같은 값의 layered/unlayered 선언이 한 클래스를 공유해서 레이어가 무효화되는 버그가 있었습니다.
  • 조건부 typography 프리셋 선언을 각 origin 안의 typography 레이어에 둡니다(t / oN.t / b.t).
    • 그래서 직접 쓴 선언이 조건부로만 적용되더라도 항상 프리셋을 이깁니다.

검증

  • cargo fmt, clippy(full·lite·wasm32) 통과
  • cargo test --workspace 통과
  • bun test 전체 통과, 변경 파일 커버리지 100%
  • bun run build, verify:dist 통과
  • bun-plugin 회귀 테스트 통과
  • 빌드된 wasm과 createModuleResolver로 실제 end-to-end 확인:
    • theme/button 스타일시트 간 클래스·변수 일치
    • 상수 인라인, 의존성 반환

추가: 남은 동작 해결

  • 패키지 로딩: createModuleResolver가 bare specifier를 번들러처럼 node_modules에서 해석합니다.
    • exports: 문자열, 배열, 조건(import > module > default > require > node), * 패턴, null 차단
    • 그다음 module → main → index 순서, .cjs 포함
    • realpath로 해석해서 심볼릭 링크로 설치된 경우에도 번들러와 이름이 같습니다.
  • CommonJS:
    • 로더가 module/exports/require 모듈을 평가합니다. default는 __esModule interop를 따릅니다.
    • 상수 인라인은 한 번만 할당된 export만 대상으로 합니다(TS의 void 0 placeholder는 제외). require 바인딩도 추적합니다.
  • import 순환: ES 모듈 의미론으로 처리합니다.
    • 모듈 객체를 먼저 예약하고, live binding(getter)과 지연 바인딩을 씁니다.
    • 평가 전에 읽지 않는 순환은 동작합니다.
    • 너무 일찍 읽으면 binding과 모듈 이름을 담은 빌드 에러가 납니다. 이 경우 폴백하지 않습니다(vanilla-extract와 동일).
    • .tsx/.js 모듈은 각자의 source type으로 파싱합니다.
  • 스타일 조합:
    • value || {...}, value ?? {...}를 지원합니다.
    • styled도 css()처럼 클래스를 조합합니다.
    • 빌드 타임에 알 수 없는 인자(호출, spread, 계산된 키)는 빈 클래스로 조용히 사라지지 않고, 인자를 명시한 빌드 에러가 됩니다.
  • 검증:
    • 저장소의 apps·components·benchmark 파일 168개를 빌드된 wasm으로 추출해 봤고 실패는 0건입니다.
    • 임시 프로젝트로 end-to-end 확인: CJS 패키지 토큰 인라인, 지연 순환, 조기 읽기 에러, 조합 불가 에러

감사 라운드: 찾아서 고친 누락

  • css(), globalCss(), keyframes(), stylex.create()는 런타임에만 알 수 있는 값을 빌드 에러로 보고합니다. 기존에는 아무도 설정하지 않는 CSS 변수를 출력하거나, 빈 선언(color:)을 남기거나, 선언을 버렸습니다.
  • 파일 최상위의 문자열/숫자 const는 resolver 없이도 인라인하고, 상수에 대한 템플릿과 산술 연산을 접습니다(${SIZE * 2}px). StyleX 파일에도 적용됩니다.
  • globalCss / createGlobalStyle / keyframes 템플릿의 보간(리터럴, 상수, 테마 읽기)을 CSS에 씁니다.
  • 템플릿 mixin(${mixin}, ${p => cond && mixin})은 그 클래스를 조합합니다. 기존에는 mixin 자신과 다음 선언까지 조용히 사라졌습니다.
  • 셀렉터나 속성 이름 자리의 알 수 없는 보간은 빌드 에러입니다.
  • Next.js: prewarm한 추출 결과도 dependencies를 유지하므로, import한 모듈이 바뀌면 다시 빌드합니다.
  • css-utils-literal-only 규칙이 빌드가 인라인하는 상수를 허용합니다.
  • 변경 후 저장소 스캔: 파일 168개 추출, 실패 0건

개선 라운드

  • webpack(및 Next.js webpack 모드): prewarm이 loader의 module resolver를 등록합니다. 이것이 없으면 다른 파일의 CSS 버킷에 합쳐진 파일이, webpack이 이미 만든 CSS에 없는 클래스를 쓸 수 있었습니다.
  • StyleX: 다른 모듈(.stylex.ts 관례)에서 import한 defineVars / createThemeContract / defineConsts / createTheme 값을 그 모듈이 생성하는 이름으로 해석합니다. StyleX namespace/default import를 모두 인식합니다.
  • vanilla-extract: 평가 중 예외를 던지는 .css.ts는 그 예외를 보고합니다. 기존에는 런타임에 여전히 style()을 호출하는 출력으로 폴백했습니다.
  • 성능: 상수 인라인은 스타일이 상수일 수 있는 이름을 읽을 때만 scope를 분석합니다. 파일당 오버헤드 0.036 ms → 0.005 ms
  • DX: 빌드 에러를 소스 순서대로 모아 file:line:column: message 형식으로 보고합니다. 위치는 import alias 변환 전의 원본 위치로 매핑합니다.
  • 저장소 스캔: 파일 168개와 .css.ts 스타일시트 2개 모두 실패 없이 추출합니다.

StyleX 조건부 값

  • defineVars / createTheme 값에 조건 객체를 쓸 수 있습니다.
    • 키: default, @media, @supports, @container([DARK] 같은 상수 키 포함)
    • types.*() 래퍼는 있어도 되고 없어도 됩니다.
  • 조건 조합마다 조건 없는 값 뒤에 블록을 하나씩 만들고, 더 깊은 조건을 뒤에 두어 더 구체적인 값이 이기게 합니다.
  • 정의한 파일과 그 파일을 import하는 파일 모두에서 동작합니다.
  • 정적이지 않은 변수/테마 값은 변수가 조용히 빠지는 대신 빌드 에러가 됩니다.

빌드 타임 계산값과 StyleX 빌드 에러

  • 상수에 대한 Math.* 호출을 빌드 타임에 접습니다(Math.max(SIZE, 20), Math.round, Math.PI). 반올림은 JavaScript 규칙을 따르고, 다른 값으로 가려진 Math는 건드리지 않습니다.
  • css(), globalCss(), keyframes(), StyleX가 모듈 실행 시 상수로 계산하는 값(darken(0.1, PRIMARY), helper 호출)은 full 엔진(Boa)에서 계산해 정적 클래스로 만듭니다.
    • 그 값이 읽는 코드만 실행합니다.
    • 빌드가 결정론적이도록 Date와 Math.random은 쓸 수 없습니다.
    • JSX prop과 styled는 기존처럼 런타임 CSS 변수를 씁니다.
  • 두 WASM 엔진 모두 hasBuildTimeValues를 export합니다. Next.js는 후보 파일에 계산이 필요하면 full 엔진을 선택하고, lite 엔진의 에러는 full 엔진을 안내합니다.
  • StyleX create, defineVars, defineConsts, createTheme, keyframes, positionTry, viewTransitionClass, include는 읽을 수 없는 입력(spread, 계산된 키, 객체가 아닌 인자, 없는 테마 키, 구조 분해한 create())을 그대로 출력하거나 버리지 않고 위치가 붙은 빌드 에러로 보고합니다.
  • stylex.props() / attrs()는 다른 곳에서 컴파일된 스타일(style prop, spread, 다른 모듈의 namespace)을 런타임에 합칩니다. 같은 파일의 namespace는 정적으로 해석합니다.

상수 스타일 객체와 빌드가 읽을 수 없는 스타일

  • 파일에 선언했거나 import한 객체·배열·TypeScript enum 상수를, 스타일을 읽는 모든 위치에서 그 자리에 직접 쓴 것처럼 읽습니다: css(base), { ...base, color: 'red' }, _hover: hover, space[2], Size.M, <Box {...base} _hover={hover} />
    • 스타일 객체와 JSX prop 모두 JavaScript처럼 뒤 속성이 앞 속성을 덮습니다. 기존에는 두 클래스를 모두 출력해서 스타일시트 순서가 결과를 정했습니다.
    • 객체는 빌드가 스타일을 읽는 위치에서만 인라인하므로 props={...} 같은 런타임 prop은 원래 객체를 그대로 받습니다.
  • 조용히 사라지던 경우는 위치가 붙은 빌드 에러가 됩니다: 알 수 없는 객체의 spread, 셀렉터(_hover)에 준 객체가 아닌 값, 계산된 키, 객체가 아닌 globalCss/keyframes 인자, styled 객체 형식 안의 props 함수. 알 수 없는 객체의 JSX spread는 기존처럼 런타임에 그대로 전달합니다.
  • 조용히 틀리던 경우도 고쳤습니다.
    • 객체가 아닌 인자 하나를 받은 css(x) / styled.div(x)는 그 값을 클래스로 조합합니다(기존에는 버려졌습니다).
    • JSX의 객체 리터럴 spread가 스타일이 아닌 prop을 유지합니다(기존에는 onClick이 사라졌습니다).
    • bg={theme.colors[key]}는 CSS 변수가 됩니다(기존에는 사라졌습니다).
    • spread가 채울 수 있는 배열 인덱스나 객체 키는 런타임에 리터럴 전체에서 읽습니다(기존에는 잘못된 인덱스를 읽었습니다).
  • full 엔진은 const나 import를 거친 값(토큰 파일의 const DARK = darken(...)), spread, shorthand 속성, 계산된 키, 런타임 조건도 계산합니다.
    • import는 필요할 때 로드하고, 스타일 패키지와 로드할 수 없는 모듈은 stub으로 대체합니다.
    • 예외를 던지는 모듈이나 선언은 그것에 의존하는 값만 잃습니다.
  • hasBuildTimeValues(filename, code, package, importAliases)는 먼저 module resolver로 상수를 인라인하므로, 정말 평가가 필요한 값이 없으면 Next.js는 lite 엔진을 유지합니다(이 저장소에서 파일당 약 0.4 ms).
  • 스타일시트 평가가 TypeScript enum에서 panic하지 않습니다.

남은 개선 항목

  • 계산된 규칙 조합: full 엔진에서 css()/styled()가 조합하는 바인딩이 모듈이 계산하는 규칙을 담고 있으면 그 규칙을 적용합니다.
    • 예: const base = makeStyles(); css(base), styled.div(base)
    • 이전에는 클래스로 조합되어 런타임에 조용히 사라졌습니다.
    • 클래스를 담은 바인딩(css() 결과, 스타일시트에서 import한 클래스)은 그대로 클래스로 조합합니다.
    • 버그 수정: 다른 계산값 때문에 평가가 실행되면, 조합 위치에서 계산된 클래스 문자열이 CSS 텍스트로 읽히던 문제를 고쳤습니다. 조합 위치에는 객체·배열 결과만 씁니다.
    • Next.js 엔진 선택이 styled.div(...), styled(...)(...) 조합도 확인합니다.
  • 에러 문구: 엔진별로 실제로 허용하는 범위를 말합니다.
    • full 엔진: "its values must be literals, theme tokens or constants, or be computed from them"
    • lite 엔진과 JSX 요소: "its values must be literals, theme tokens or constants"
    • 파일 안의 상수도 인라인되므로 "imported"를 뺐습니다.
  • source map: import alias 변환이나 계산값 치환 뒤에도 원본 코드의 줄·열을 가리키고, sourcesContent는 원본 코드입니다. 이전에는 치환된 코드를 기준으로 해서, 여러 줄 계산값 뒤의 줄이 어긋났습니다.
  • 평가 캐시: 다른 평가가 import한 스타일시트는 같은 스크립트면 한 번만 실행하고, 모듈의 TypeScript 제거 결과는 소스가 같으면 재사용합니다.
    • 파일 번호, 소스, 실행 스크립트를 모두 비교하므로 오래된 결과를 쓰지 않습니다.
    • 측정(release, 파일당): 헬퍼를 import하는 테마를 import하는 스타일시트 5.68 ms → 1.67 ms, 테마와 헬퍼를 import하는 스타일시트 6.48 ms → 5.18 ms, 계산값 5.12 ms → 5.01 ms
    • Boa가 import한 모듈을 평가마다 다시 컴파일하는 비용은 남습니다. 평가마다 새 컨텍스트를 써야 평가 사이에 상태가 섞이지 않아 결과가 결정론적이기 때문입니다.
  • eslint css-utils-literal-only: 엔진이 읽는 범위에 맞췄습니다.
    • import하거나 모듈에 선언한 함수와 내장 함수(Math(단 Math.random 제외), String 등)로 상수에서 계산한 값은 허용합니다.
    • Date, Math.random, 매개변수처럼 런타임에만 아는 함수는 보고합니다.
    • README에 계산값과 필요한 엔진을 설명했습니다.
  • 검증:
    • extractor 테스트 1382개, cargo test --workspace, clippy(full·lite·wasm32) 통과
    • bun 테스트 5267개 통과
    • 저장소 스캔: 176개 추출, 실패 0, 엔진 감지 68.1 ms

추가 점검에서 찾은 버그

  • 동적 as가 JSX 자식 위치에서 코드를 망가뜨리던 버그 (main에도 있음): as={Link}, as={cond ? 'a' : 'b'}처럼 리터럴이 아닌 as를 JSX 자식으로 쓰면 결과가 틀렸습니다.
    • 부모 요소 전체가 대체되거나(<section>이 사라짐), 뒤의 다른 식({on}, 형제의 title)이 대체됐습니다.
    • <Box>가 출력에 남아 런타임에 "Cannot run on the runtime" 에러가 났습니다.
    • 목록(items.map(... <Box key={id} as={C} />))에서는 key 없는 fragment로 감싸져 React key 경고가 났습니다.
    • 이제 요소가 있던 자리(식, 자식, 속성 값)에 바로 대체합니다.
  • 런타임 값을 CSS 텍스트로 최적화하던 버그 (main에도 있음): 요소가 런타임에 받는 값의 코드에 CSS 최적화가 적용됐습니다.
    • 함수에 넘기는 문자열의 숫자가 바뀌었고(f('#336699') → f('#369'), '10px 0px' → '10px 0'), --i가 var(--i)가 됐습니다.
    • 이제 코드를 그대로 씁니다.
  • 런타임 값 누락: await, new, 대입, 쉼표 식, tagged template 값이 조용히 사라졌습니다. 이제 CSS 변수로 전달하고, 함수·클래스는 에러로 보고합니다.
  • spread 반복 평가 (main에도 있음): <Box {...register('email')} />처럼 다시 읽으면 값이 달라질 수 있는 spread를 spread, className, style에서 3번 평가했습니다. 훅이면 상태가 3개로 갈라집니다.
    • 이제 한 번만 평가합니다.
    • 앞에 쓴 props와 자식도 쓴 순서대로 평가하고, await/yield도 지원합니다.
    • 컴파일된 jsx() 호출도 같습니다.
  • globalCss()를 값으로 쓰면 잘못된 코드가 나오던 버그 (main에도 있음): const a = globalCss(...)가 const a = ;가 됐습니다. 이제 void 0이 됩니다.
  • 클래스를 바꿀 수 없는 곳의 런타임 조건: globalCss(), keyframes(), <Global>의 런타임 조건은 조용히 사라졌습니다. 이제 빌드 에러입니다.
    • 빌드가 아는 조건(const DARK = true; DARK ? 'white' : 'black')은 해당 분기로 접습니다.

계산값 확장

  • JSX spread, 셀렉터 prop, styled 객체의 spread·셀렉터·계산된 키, css(...parts), css()의 템플릿 인자를 full 엔진이 계산합니다.
  • 알려진 클래스만 담은 namespace/객체(css(styles.card))는 더 이상 모듈을 실행하거나 full 엔진을 선택하게 만들지 않습니다.
  • 일부만 아는 객체의 중첩 멤버(css(tokens.nested.x))도 상수 경로로 판별합니다.
  • Next.js 엔진 선택이 JSX spread와 셀렉터 prop도 확인합니다.
  • 끝나지 않는 스타일시트는 빌드를 멈추는 대신 반복 한도에서 실패합니다.
  • 에러 문구의 요구 조건을 엔진별로 정확히 씁니다. Cannot compose 에러는 spread 인자 이름(...rest)을 보여 줍니다.
  • eslint css-utils-literal-only는 값 안에 쓴 콜백의 매개변수([1, 2].map((n) => n * 4))를 허용합니다.

추가 점검 검증

  • extractor 테스트 1,390개와 cargo test --workspace가 통과하고, clippy(full·lite·wasm32)는 경고가 없습니다.
  • bun 테스트 5,269개가 통과합니다.
  • 저장소 스캔: 176개 추출, 실패 0
  • 빌드한 WASM으로 동적 as, spread 1회 평가, 계산값을 직접 확인했습니다.

lite 엔진 제거

사용하는 곳이 우리 플러그인뿐이어서 엔진을 Boa가 들어간 하나로 합쳤습니다.

  • @devup-ui/wasm은 빌드를 하나만 만듭니다. @devup-ui/wasm/lite export와 lite 빌드, 엔진을 고르는 데만 쓰던 hasBuildTimeValues를 없앴습니다.
  • Rust vanilla-extract feature를 없애고 Boa를 extractor·sheet·wasm crate의 일반 의존성으로 바꿨습니다.
  • Next 플러그인은 시작할 때 프로젝트 파일을 훑어 엔진을 고르지 않고 항상 @devup-ui/wasm을 불러옵니다. 그래서 dev 서버 실행 중에 추가한 파일도 다른 파일과 똑같이 처리됩니다.
  • 엔진 선택 때문에 WASM 로드 앞으로 옮겼던 import 그래프·prewarm 파일 수집은 원래 자리로 돌렸습니다.
  • 문서, css-utils-literal-only README, changepack에서 lite와 엔진 선택 설명을 지웠습니다.

런타임에 바뀌는 값은 빌드 때 굳히지 않음

빌드 때 미리 계산한 값이 런타임에 계산한 값과 다를 수 있는 두 경우를 막았습니다.

1. 코드가 바꾸는 객체·배열·enum 상수

빌드가 볼 수 있는 코드에서 바꾸는 상수는 선언한 값으로 읽지 않습니다. 확인하는 파일은 스타일을 쓰는 파일, 상수를 선언한 모듈, 중간에 re-export하는 모듈입니다.

  • 바꾸는 코드로 보는 것:
    • 멤버 대입, delete, ++
    • push·sort 같은 메서드, Object.assign
    • 빌드가 모르는 함수에 넘기기
    • 바뀌는 다른 객체에 담기
  • 바꾸는 코드로 보지 않는 것:
    • 담긴 문자열·숫자를 넘기기. 예: darken(colors.primary)
    • 아무도 바꾸지 않는 top-level const에 담기. 예: const theme = { colors }
  • 이런 상수를 쓰면 이렇게 처리합니다.
    • 요소 prop에서 멤버를 읽으면 런타임 CSS 변수가 됩니다.
    • css(obj), styled.div(obj), <Box {...obj} />처럼 객체 전체를 받으면 빌드 에러가 납니다. 에러에는 바꾸는 위치(src/tokens.ts:10:26)를 적습니다.
    • Boa로 계산하는 값도 이 상수를 읽지 않습니다.
  • 빌드가 볼 수 없는 모듈에서 바꾸는 경우는 감지할 수 없습니다. 그래서 "스타일에 쓰는 객체는 런타임에 바꾸지 말고, 바뀌는 값은 테마 토큰이나 prop으로 쓴다"는 규칙을 문서에 적었습니다.

2. 실행 환경에 따라 달라지는 코드

Boa로 계산할 때 아래 값을 읽으면 에러가 납니다. 그래서 그 값에 의존하는 계산은 빌드 때 하지 않습니다.

  • 읽을 수 없는 값: window, document, navigator, location, process, globalThis에서 정의되지 않은 속성
  • 예: typeof window === 'undefined' ? ..., process.env.NODE_ENV, globalThis.BRAND ?? 'red'
  • 이런 값은 빌드 환경에서 나온 분기가 CSS에 박히는 대신, css()에서 빌드 에러가 납니다.

lite 제거·런타임 값 검증

  • CI green: 5db2b827(lite 제거), 8866988a(런타임 값). bfd18e60에서 커버리지가 두 줄 모자라 테스트를 보강했습니다.
  • 로컬 확인
    • cargo: fmt, clippy -D warnings(host·wasm32), test --workspace
    • bun: 5264개 테스트 통과
    • tarpaulin: CI와 같은 포맷으로 돌려, 새로 바뀐 줄 중 커버되지 않은 줄이 없음을 확인
  • 새 테스트
    • mutations::tests: 바꾸는 코드, 넘기기, 읽기, 전역 이름을 가린 경우를 분류하는지 확인
    • test_changed_constants 스냅샷 21개 케이스
      • 로컬·import·re-export 상수가 바뀌는 경우
      • 문자열만 넘기는 경우, 다른 const에 담는 경우, 순환 참조
      • Boa 입력, window·process·globalThis

런타임 값 보호 보완

앞 섹션의 보호를 점검해 찾은 빈틈을 모두 고쳤습니다.

에러 없이 틀린 결과가 나오던 경우

  • namespace import(tokens.base)나 다른 객체(const theme = { base })에 든 바뀌는 객체를 css()·spread로 통째로 받으면, 클래스 조합으로 넘어가 스타일이 사라졌습니다. 이제 빌드 에러입니다.
  • CommonJS 모듈에서 바꾸는 객체가 초기값으로 인라인됐습니다. 이제 CommonJS 모듈도 바뀌는지 확인합니다.
  • 아래처럼 바꾸는 경우를 놓쳤습니다. 이제 모두 바꾸는 코드로 봅니다.
    • for...of 안에서 바꾸기
    • forEach·map 콜백에서 바꾸기
    • this를 쓰는 메서드 호출

정상 코드가 상수로 읽히지 않던 경우

  • 아래는 읽기만 하는 것으로 봐서 정적 클래스로 유지합니다.
    • export default { colors }, module.exports = { colors }
    • Object.freeze(...), Object.keys, Array.from
  • 컴포넌트 prop으로 넘기는 것도 읽기로 봅니다.
    • React 규칙상 컴포넌트는 props를 바꾸지 않습니다.
    • 그래서 <ThemeProvider theme={theme}>가 있어도 theme는 상수입니다.
    • 예외: React가 값을 대입하는 ref
  • 에러 문구는 "바꾸는 위치"와 "바꿀 수 있는 코드에 넘긴 위치"를 구분하고, 그 모듈에서 쓰는 이름으로 표시합니다.

결정론

  • 로케일에 따라 달라지는 값은 빌드 때 계산하지 않습니다. 변수나 import를 거쳐 읽는 경우도 마찬가지입니다.
    • 대상: toLocaleString, toLocaleUpperCase, localeCompare, Intl
    • 정적 분석과 Boa 실행 두 단계에서 모두 막습니다.
  • 실패한 값에서 분기하던 문제를 고쳤습니다.
    • 전에는 예외가 난 선언이나 불러오지 못한 모듈을 truthy한 대체 객체로 두었습니다. 그래서 !isBrowser ? 'white' : 'black' 같은 분기가 빌드 때 계산됐습니다.
    • 이제 그런 값에 의존하는 계산은 하지 않습니다.
  • 환경을 확인하는 모듈의 나머지 export를 쓸 수 있게 했습니다.
    • 예: top-level에 const isBrowser = typeof window !== 'undefined'가 있는 모듈
    • 환경을 읽는 문장과 그것을 즉시 읽는 문장을 모듈 끝으로 옮겨 실행합니다.
  • top-level console 호출 때문에 모듈이나 .css.ts 전체가 실패하지 않습니다.

성능과 기타

  • 계산 파일마다 import한 모듈을 전부 읽던 것을, 계산에 쓰는 모듈만 읽도록 줄였습니다.
    • apps/landing 실측: resolver 호출 20회 → 0회, 2.39ms → 1.17ms
  • content, font-size-adjust, math-depth의 숫자는 px로 바꾸지 않습니다. 예: content: '1234.5'는 전에 4938px로 바뀌었습니다.
  • ESLint css-utils-literal-only가 파일 안에서 바꾸는 상수·import를 보고합니다.

검증

  • CI green: c2f61a40(publish·e2e·benchmark)
  • 로컬
    • cargo: clippy -D warnings(host·wasm32), test --workspace
    • bun: 5295개 테스트 통과
    • ESLint 규칙: 78개 테스트, 파일 커버리지 100%
  • 새 테스트
    • mutations::tests 스냅샷 5개: 바꾸기, 넘기기, 읽기, this, 전역 이름을 가린 경우
    • test_values_known_only_at_runtime 21개 케이스: 위 경우 전부와 .css.ts의 console

확실한 코드만 빌드 때 실행

원칙: 결과가 확실하고 정적인 코드만 Boa가 실행합니다. 조금이라도 애매하면 요소에서는 CSS 변수로 남기고, css()·globalCss()·keyframes()·styled()에서는 빌드 에러로 알립니다.

  • 실행하는 것: 이 파일의 const·함수·enum이 리터럴·상수에서 정확한 내장 기능(String, Number, JSON, 문자열·배열 메서드, Math.abs/ceil/floor/round/trunc/sign/max/min/sqrt/fround/imul/clz32와 Math 상수)으로 계산하는 값
  • import: 모듈이 선언한 리터럴·객체·배열을 값으로만 주입합니다. 다른 모듈의 코드는 실행하지 않습니다(ModuleLoader 미사용).
  • 실행하지 않는 것: 목록 밖 전역(window, Date, Intl, typeof IntersectionObserver 등), Math.random과 근사 Math 함수(sin, pow 등), **, 인자가 있는 toString, toLocale*/localeCompare/normalize, this, new, class, 정규식, try, getter/setter, async/generator, JSX, obj[key](), 모듈 top-level 바인딩을 쓰는 함수. PRELUDE가 백스톱으로 남아 있습니다.
  • 조건 접기: 상수 조건과 &&/||/??는 스타일 인자·스타일 prop 안에서만 접습니다. 앱 코드는 바인딩을 그대로 읽습니다.
  • 변경 판정 오탐 수정: bool·undefined·함수 멤버를 가진 객체를 더 이상 "바뀜"으로 보지 않습니다.
  • css()/styled()가 합성하는 모르는 바인딩: 계산하지 못하면 STYLE_OBJECT 빌드 에러로 알립니다.
  • 단위: transition/animation의 duration·delay 숫자는 ×4 없이 ms로 해석하고(300 → 300ms), counter 속성은 숫자를 그대로 둡니다.
  • ESLint css-utils-literal-only: 같은 규칙을 적용합니다. import 함수 호출, 빌드가 실행하지 않는 함수, 부정확한 내장 기능을 보고합니다. 파일 안의 변경 판정(for...of·forEach/map 요소 변경, this 메서드, 모르는 함수에 넘기기)도 빌드와 같습니다. 다른 모듈에서 바꾸는 경우는 lint로 볼 수 없어 문서에 적었습니다.
  • 문서: features, style-props, stylex·vanilla-extract 마이그레이션, SKILL.md, 규칙 README를 갱신했습니다.

동적 as 수정과 lint 검사 범위 확장

  • 동적 as: as에 들어온 값대로 렌더합니다.
    • undefined·null·false는 기본 요소가 됩니다(Box는 div, Text는 span). 이전에는 아무것도 렌더하지 않았습니다.
    • 컴포넌트와 motion.div 같은 점 접근은 그 이름으로 렌더합니다. 이전에는 <motion />.div가 되어 아무것도 렌더하지 않았습니다.
    • 이름 사이를 고르는 삼항은 분기마다 요소를 만듭니다. React Fragment로 감싸지 않고, key는 각 요소에 붙습니다.
    • 그 밖의 값(소문자 변수, `h${level}`, 호출, 조회)은 런타임에 한 번만 읽고, 비어 있으면 기본 요소가 됩니다: ((DevupAs0) => <DevupAs0 />)(expr || "div"). 이전에는 템플릿이 첫 조각(<h>)만 쓰고, 소문자 변수가 그대로 태그가 되고, 조회는 모든 요소를 만들어 props와 children을 항목 수만큼 실행했습니다.
  • Math·toString 우회 차단: 빌드 타임 계산은 Math를 키가 정해진 멤버로만, toString은 인자 없이 바로 호출할 때만 읽습니다. const { sin } = Math 같은 별칭으로 근사 함수가 계산되던 틈을 막았습니다.
  • css-utils-literal-only: css·globalCss·keyframes·createGlobalStyle의 모든 인자에 있는 규칙 객체와 CSS 텍스트(tagged template 포함), devup-ui가 컴파일하는 StyleX 함수의 값까지 검사합니다. css()가 클래스로 합성하는 부분과 styled()의 값(CSS 변수가 됨)은 검사하지 않습니다. Math·toString 규칙도 빌드와 같습니다.
  • 문서 정정: styled()는 불확실한 값을 요소처럼 CSS 변수로 두고, 스타일 객체 전체를 계산할 수 없을 때만 빌드 에러입니다. .css.ts에서 시간 숫자는 vanilla-extract처럼 px가 붙으므로 '300ms'처럼 단위를 적어야 합니다.

스타일 결과를 값으로 사용, 제거된 import 참조 검사

같은 파일의 const에 담긴 keyframes()/css() 결과를 쓰지 못하던 한계를 고쳤고, 그 과정에서 찾은 버그도 함께 고쳤습니다.

  • 스타일 결과를 값으로 사용: const fadeIn = keyframes(...)와 const card = css(...)를 css(), globalCss(), keyframes(), styled(), <Global>, 스타일 props에서 빌드 타임 값으로 읽습니다. 규칙 객체 안과 계산된 selector 키도 포함합니다. CSS 텍스트 안의 class는 지금처럼 mixin으로 합성합니다. 섀도잉된 이름, let, 구조분해 바인딩은 치환하지 않습니다.
  • 패키지 통째 import: import * as Devup와 default import에서 Devup.css, Devup.keyframes, Devup.createGlobalStyle, Devup.styled, <Devup.Global>을 컴파일합니다.
  • styled() base: styled(motion.div)는 멤버 이름 그대로 렌더합니다. forwardRef(...)나 변수 같은 그 밖의 값은 선언 위치에서 한 번 읽습니다. null, 숫자, boolean, undefined base와 컴파일되지 않는 styled는 빌드 에러입니다.
  • 버그: 제거된 import 참조: 빌드가 import를 지운 뒤에도 참조가 남아 런타임 ReferenceError가 나던 문제를 고쳤습니다. 예: const myCss = css, export const C = Box, [Box], 단독 styled('div'). top-level alias(const myCss = css, const Row = Flex)는 원본처럼 컴파일하고 선언을 지웁니다. 남는 런타임 참조는 모두 위치를 가리키는 빌드 에러가 됩니다. 섀도잉된 이름과 타입 위치는 제외합니다.
  • 버그: selector 규칙 누락: 런타임에만 알 수 있는 selectors 키가 조용히 버려지던 것을 빌드 에러로 바꿨습니다. 상수로 계산되는 키(['.' + card + ':hover &'])는 읽습니다.
  • lint 오탐: css-utils-literal-only가 module-level const에 담긴 devup css()/keyframes() 결과를 정적 값으로 봅니다. named import와 namespace import 모두 해당합니다.
  • 남은 한계
    • 다른 모듈에서 import한 keyframes/class 이름은 값으로 읽지 않습니다. 그 이름은 해당 파일이 빌드되어야 정해지기 때문입니다.
    • 사용 위치보다 뒤에 선언된 const도 읽지 않습니다.
    • styled['div']는 빌드 에러입니다.
  • 문서(features, migration/styled-components, SKILL.md, lint README)를 갱신했고 Patch changepack을 추가했습니다. @devup-ui/components Button 스냅샷은 spinner keyframes가 CSS 변수 대신 정적 class로 바뀐 결과를 반영했습니다.
  • CI: 모든 job 성공, Rust 커버리지 100%(9780/9780), bun 커버리지 100%

owjs3901 and others added 30 commits September 30, 2026 00:08
… units

vanilla-extract styles add px to numbers (in .css.ts files and in style/globalStyle/keyframes calls of ordinary modules) and StyleX adds px/ms to static and dynamic numbers, instead of Devup UI's spacing scale. The unitless property list now matches vanilla-extract's, so strokeWidth, columns, zoom, orphans and similar keep bare numbers everywhere.

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

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

Remove the static .css.ts fast path so development and production builds evaluate stylesheets the same way and produce the same class names and CSS.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ponents object styles

vanilla-extract: name styles after the variable holding them instead of by call order, generate file-unique names for vars, containers, layers, fonts and theme variables, follow vanilla-extract's createVar/fallbackVar/createTheme/createGlobalTheme/contract/layer semantics, resolve styleVariants variants and transitive composition, and export plain values as data. Emotion and styled-components: numbers in object styles mean pixels, several style arguments or arrays merge, styled(tag, options) is a factory, and css() composes class names passed with rule objects. '@layer' records and a global '@layer' cover nested and responsive declarations, and typography presets yield to declarations written beside them.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Layered properties of any kind are written with the regular writer, one layer at a time, so a layer keeps breakpoints, selectors and at-rules. A typography atom skips the declarations its value lists as written beside it from the breakpoint they start at.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…graphy precedence

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The loop left a region tarpaulin maps past the function on Linux; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Tarpaulin's Linux report still attributed a line past the function to its closures; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The vec! expansion is the region tarpaulin maps past the function on Linux; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Reading a rule list cannot fail, so the error path tarpaulin kept reporting as uncovered past the function is gone; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Its output is asserted by test_font_faces; tarpaulin's Linux report maps the loop exit onto a line it never counts however the loop is written.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Shaped like compose so tarpaulin's Linux report maps its loop, and the coverage exclusion is gone again; the output is unchanged.

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

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

The callers store the rules, so the helper ends in its value; the output is unchanged.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Tarpaulin merges the unit-test build and the library build that other targets link; the latter never ran the font-face helper, which left one region at zero. The helper is split into single-rule and list parts.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
A number on a custom property was read as the spacing scale (--columns: 4 became 16px), which broke theme tokens and var() math. The vanilla-extract API now also runs through the library build in an integration test.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

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

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
resolve took impl Fn, so the always-selector caller got its own copy whose plain class-name branch never runs; CI coverage reported that branch as uncovered. A fn pointer keeps one copy. createVar returns to its original shape.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… the library

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…every styled import

styled.x.attrs(a).withConfig(c) now extracts like styled.x: withConfig is dropped and the attrs are merged over the props in order, objects spread and functions called with the props. A file importing styled from several libraries extracts all of them instead of only the last.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
A dynamic value inside '@layer' now carries the layer, and the layer is part of the class and variable name key, so a layered declaration no longer shares a class (and loses its layer) with the same unlayered one.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
css(base, cond && over) and ternaries of rule objects now merge into per-property ternaries (color: cond ? 'blue' : 'red'), since the order of two atomic classes is decided by the stylesheet, not by composition. Conditional class parts join as cond ? cls : '' so a falsy part no longer prints 'false', and Emotion's number-to-px conversion reaches conditional branches.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The declarations of a conditional typography preset now sit in the typography layer nested in their own origin (t, oN.t, b.t), like the theme's typography classes. A declaration written directly therefore wins whenever it applies, including one that only applies on a condition, which yielding alone could not decide.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
A .css.ts file can now import other stylesheets and plain modules. Imports are resolved through a resolver the bundler supplies (extract_with_modules). A stylesheet is extracted the way the bundler extracts it and its output evaluated, so the names it exports are the ones its own CSS uses; other modules are run as they are. Every export form is supported, cycles are reported, the output keeps importing the stylesheets and side-effect imports it had, and the files read are returned as dependencies to watch.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
owjs3901 and others added 17 commits September 30, 2026 00:09
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…t as constants

- An object, array or enum constant that code the build sees changes (a member assignment, delete, ++, push, sort, Object.assign, or handing it to a function the build does not know) is read at runtime: elements read its members as CSS variables, and css(), styled() and element spreads taking it whole are build errors naming where it is changed. Handing on a string or number it holds, or putting it in a top-level const nothing changes, keeps it constant

- The module declaring an imported constant and the modules re-exporting it are checked too, and values computed at build time do not read a changed binding

- Values computed at build time cannot read window, document, navigator, location, process or globalThis properties the build does not define, so the CSS never holds the build environment's answer

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Objects, arrays and enums that visible code changes are read at runtime, taking one whole is a build error naming where it changes, and values computed at build time cannot read window, document, navigator, location, process or unknown globalThis properties

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… functions change

- A spread argument of css() reading a changed binding is reported like any other argument, and the check for the argument Object.assign and friends change no longer hides in a match guard coverage cannot count

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…nt values

- A changed object a namespace import or another object holds is a build error where css(), styled() or a spread takes it whole, where it was composed as a class; a value read from a changed object is never read as its declared value; CommonJS modules, element changes in for...of loops and forEach/map callbacks, and methods using this count as changes

- export default, module.exports, Object.freeze, Array.from, Object.keys and element props other than ref only read an object, so token objects stay static classes; a namespace import itself is never changed; notes tell a change from handing an object on and name the binding where it is changed

- Values computed at build time never read Intl or locale methods, directly or through variables and imports; a value reading a statement that threw or a module the build cannot load is not computed instead of taking a branch on a stand-in; a module checking the environment at its top still gives its other exports; console calls no longer stop a module or a .css.ts stylesheet; only the modules a computed value reads are loaded

- content, font-size-adjust and math-depth keep their numbers instead of scaling them to px

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…teral-only

- A constant or an import the file changes through a member assignment, delete, ++, a destructuring target, a changing method or Object.assign is reported, as the build no longer reads it as written

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

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

- Iteration, methods using this, CommonJS modules and namespace imports can change an object; export default, module.exports, Object.freeze and element props only read it; locale methods and Intl are unavailable, and nothing is computed from a failed statement or an unloadable module

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Values are computed from the file's own const, function and enum declarations with exact built-ins; imports are injected as the literals their modules declare and no other module runs. Uncertain code stays a CSS variable on elements and is a build error in css(), globalCss(), keyframes() and styled(). Constant conditions fold only where styles read them, objects holding booleans, undefined or functions are no longer taken as changed, times are milliseconds and counters keep their numbers.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…iteral-only

Report calls of imported functions and of functions the build does not run, uncertain built-ins and members, changing elements in for...of loops and forEach/map callbacks, methods using this, and objects handed to unknown code, as the build decides them.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

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

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

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

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
The Update Versions PR removes it once released, so a change here would conflict with that PR.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
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 Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Changepacks

@devup-ui/wasm@1.0.81 → 1.0.82 - bindings/devup-ui-wasm/package.json

Patch

  • Fix at-rule composition and cascade: _print/_media/@media … nest correctly with selectors in both directions and inside globalCss, nested media queries merge (print and (…)), breakpoints put media types first, distribute query lists, and nest not … queries, and at-rules are emitted after all breakpoint values while pseudo selectors keep SELECTOR_ORDER across breakpoints. Add _motionReduce, _motionSafe, _portrait, _landscape, _contrastMore, _contrastLess and _forcedColors, remove the never-matching _speech, normalize query whitespace, support Emotion-style '@media …'/'&:hover' object keys, fix template literals losing declarations before @media or selectors around nested at-rules, apply typography conditionally under selectors, and drop stale global at-rules on re-extraction
  • An object, array or enum constant that code the build sees changes (a member assignment, delete, ++, push, sort, Object.assign, or handing it to a function the build does not know) is no longer read as its declared value: elements read its members at runtime, css(), styled() and element spreads taking it whole are build errors naming where it is changed, and values computed at build time do not read it. Values computed at build time cannot read window, document, navigator, location, process or globalThis properties the build does not define, so the CSS never holds the build environment's answer
  • Fix vanilla-extract stylesheets: styles are named after the variable holding them, so style calls nested in objects no longer shift every later name onto the wrong styles and such objects export their classes; createVar, createContainer, layer, fontFace and theme variables get names unique to each stylesheet instead of colliding across files, createVar returns var() so template literals and fallbackVar work, createTheme(tokens, debugId), the three-argument createGlobalTheme and unassigned createGlobalTheme calls produce their variables, createThemeContract exports, styleVariants variants resolve in selectors and compositions and accept a map function, composition keeps transitive bases, '@layer' records, globalLayer, createGlobalThemeContract, assignVars and globalFontFace work, and layers keep their declaration order. Emotion and styled-components object styles read numbers as pixels (Devup UI shorthands keep the spacing scale), compose several style arguments or arrays, and accept Emotion's styled(tag, options) factory; css() composes class names passed with rule objects. A global '@layer' now covers responsive, selector and at-rule declarations, and layered declarations keep their breakpoints. Custom properties keep their value instead of reading a number as the spacing scale. A typography preset no longer overrides a property written beside it at a wider breakpoint, in components and globalCss
  • Fix the full WASM build panicking on every vanilla-extract .css.ts file (Boa read the clock without its js feature), resolve vanilla-extract references to other styles, keyframes, fonts and themes in selectors, values and globalStyle, keep globalCss pseudo selectors in SELECTOR_ORDER across breakpoints, merge adjacent rules with the same selector, and let declarations written next to a conditional typography preset win over it. Add the prefer-media-shorthand and no-typography-token-prefix ESLint rules
  • A dynamic as renders what it gives: undefined, null and false give the default element, a component or a member such as motion.div renders as named, a condition between names becomes an element for each, and any other value (a lowercase variable, a template, a call, a lookup) is read at runtime, once, with the default element when it is empty, where member expressions rendered nothing, templates took their first part and lookups built every element. Build-time values read Math only through members with a known key, and toString only called at once without a radix. css-utils-literal-only checks every argument and the CSS text of css, globalCss, keyframes and createGlobalStyle, tagged templates included, and the StyleX functions devup-ui compiles, and reports aliases of Math and toString read other than called
  • Numbers in vanilla-extract and StyleX styles keep their library's meaning instead of Devup UI's spacing scale: vanilla-extract styles add px in .css.ts files and in style(), globalStyle() and keyframes() calls inside ordinary modules, StyleX adds px/ms to static values and to numbers passed to dynamic styles, and unitless properties such as columnCount, strokeWidth, orphans or zoom keep a bare number everywhere. The Next.js plugin now evaluates every .css.ts module instead of taking a separate static fast path, so development and production builds produce the same output
  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.
  • Remove the lite engine: @devup-ui/wasm ships one build that evaluates stylesheets and computed style values, the @devup-ui/wasm/lite export is gone, and the Next.js plugin always loads @devup-ui/wasm instead of choosing an engine from the project's files
  • Close the gaps left in reading changed objects and the build environment: an object a namespace import or another object holds that code changes is a build error where taken whole instead of silently composed as a class; CommonJS modules, changes to elements in for...of loops and forEach/map callbacks, and methods using this count as changes; export default, module.exports, Object.freeze, Array.from, Object.keys and element props (except ref) only read what they are given, so token objects stay static classes; notes tell a change apart from handing an object on. Values computed at build time never read Intl or locale methods (toLocaleString, toLocaleUpperCase, localeCompare), directly or through a variable or an import, and a value reading a statement that threw or a module the build cannot load is not computed instead of taking a branch on a stand-in. A module checking the environment at its top still gives its other exports, a top-level console call no longer stops a module or a .css.ts stylesheet, and a computed value only reads the modules its code reads. content, font-size-adjust and math-depth keep their numbers instead of scaling them to px. css-utils-literal-only reports a constant or an import the file changes
  • Only run build-time code whose result is certain: this file's const, function and enum declarations computing from literals and constants with exact built-ins. Imports are read as the literal, object or array their module declares and another module's code never runs. Other globals, Math.random and approximate Math functions, **, toString with a radix, locale and Unicode methods, this, new, classes, regular expressions, try, getters, async code, generators, JSX, methods chosen at runtime and functions writing module-level bindings are not run: elements and styled() keep such values as CSS variables, css(), globalCss() and keyframes() report a build error, and so do css() and styled() for a whole style computed that way. Constant conditions and &&, || and ?? choose their side in style arguments and props only, so application code keeps reading its bindings. Objects holding booleans, undefined or functions are no longer taken as changed. transition and animation durations and delays are milliseconds instead of spacing (a .css.ts file keeps vanilla-extract's px), and counters keep their numbers. css-utils-literal-only follows the same rules: it reports calls of imported functions and of functions the build does not run, changing elements in for...of loops and forEach/map callbacks, methods using this, and objects handed to unknown code
  • A const holding what keyframes() or css() gives is a build-time value, so css(), globalCss(), keyframes(), styled(), Global and style props read it, in rule objects and computed selector keys; in CSS text a class stays a mixin. The package imported whole (import * as Devup) compiles Devup.css, Devup.keyframes, Devup.createGlobalStyle, Devup.styled and Devup.Global. styled() renders any base: a member such as motion.div as named, any other value (forwardRef(...), a variable) read once; null, numbers, booleans and undefined, and a styled left uncompiled, are build errors. A top-level alias of a compiled import (const myCss = css, const Row = Flex) compiles and is removed, and any other runtime read of a removed import is a build error instead of a ReferenceError. A selectors key only the runtime knows is a build error instead of a dropped rule. css-utils-literal-only reads css() and keyframes() results held in a module-level const as static

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

Patch

  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.

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

Patch

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

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

Patch

  • Fix the full WASM build panicking on every vanilla-extract .css.ts file (Boa read the clock without its js feature), resolve vanilla-extract references to other styles, keyframes, fonts and themes in selectors, values and globalStyle, keep globalCss pseudo selectors in SELECTOR_ORDER across breakpoints, merge adjacent rules with the same selector, and let declarations written next to a conditional typography preset win over it. Add the prefer-media-shorthand and no-typography-token-prefix ESLint rules
  • A dynamic as renders what it gives: undefined, null and false give the default element, a component or a member such as motion.div renders as named, a condition between names becomes an element for each, and any other value (a lowercase variable, a template, a call, a lookup) is read at runtime, once, with the default element when it is empty, where member expressions rendered nothing, templates took their first part and lookups built every element. Build-time values read Math only through members with a known key, and toString only called at once without a radix. css-utils-literal-only checks every argument and the CSS text of css, globalCss, keyframes and createGlobalStyle, tagged templates included, and the StyleX functions devup-ui compiles, and reports aliases of Math and toString read other than called
  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.
  • Close the gaps left in reading changed objects and the build environment: an object a namespace import or another object holds that code changes is a build error where taken whole instead of silently composed as a class; CommonJS modules, changes to elements in for...of loops and forEach/map callbacks, and methods using this count as changes; export default, module.exports, Object.freeze, Array.from, Object.keys and element props (except ref) only read what they are given, so token objects stay static classes; notes tell a change apart from handing an object on. Values computed at build time never read Intl or locale methods (toLocaleString, toLocaleUpperCase, localeCompare), directly or through a variable or an import, and a value reading a statement that threw or a module the build cannot load is not computed instead of taking a branch on a stand-in. A module checking the environment at its top still gives its other exports, a top-level console call no longer stops a module or a .css.ts stylesheet, and a computed value only reads the modules its code reads. content, font-size-adjust and math-depth keep their numbers instead of scaling them to px. css-utils-literal-only reports a constant or an import the file changes
  • Only run build-time code whose result is certain: this file's const, function and enum declarations computing from literals and constants with exact built-ins. Imports are read as the literal, object or array their module declares and another module's code never runs. Other globals, Math.random and approximate Math functions, **, toString with a radix, locale and Unicode methods, this, new, classes, regular expressions, try, getters, async code, generators, JSX, methods chosen at runtime and functions writing module-level bindings are not run: elements and styled() keep such values as CSS variables, css(), globalCss() and keyframes() report a build error, and so do css() and styled() for a whole style computed that way. Constant conditions and &&, || and ?? choose their side in style arguments and props only, so application code keeps reading its bindings. Objects holding booleans, undefined or functions are no longer taken as changed. transition and animation durations and delays are milliseconds instead of spacing (a .css.ts file keeps vanilla-extract's px), and counters keep their numbers. css-utils-literal-only follows the same rules: it reports calls of imported functions and of functions the build does not run, changing elements in for...of loops and forEach/map callbacks, methods using this, and objects handed to unknown code
  • A const holding what keyframes() or css() gives is a build-time value, so css(), globalCss(), keyframes(), styled(), Global and style props read it, in rule objects and computed selector keys; in CSS text a class stays a mixin. The package imported whole (import * as Devup) compiles Devup.css, Devup.keyframes, Devup.createGlobalStyle, Devup.styled and Devup.Global. styled() renders any base: a member such as motion.div as named, any other value (forwardRef(...), a variable) read once; null, numbers, booleans and undefined, and a styled left uncompiled, are build errors. A top-level alias of a compiled import (const myCss = css, const Row = Flex) compiles and is removed, and any other runtime read of a removed import is a build error instead of a ReferenceError. A selectors key only the runtime knows is a build error instead of a dropped rule. css-utils-literal-only reads css() and keyframes() results held in a module-level const as static

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

Patch

  • Numbers in vanilla-extract and StyleX styles keep their library's meaning instead of Devup UI's spacing scale: vanilla-extract styles add px in .css.ts files and in style(), globalStyle() and keyframes() calls inside ordinary modules, StyleX adds px/ms to static values and to numbers passed to dynamic styles, and unitless properties such as columnCount, strokeWidth, orphans or zoom keep a bare number everywhere. The Next.js plugin now evaluates every .css.ts module instead of taking a separate static fast path, so development and production builds produce the same output
  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.
  • Remove the lite engine: @devup-ui/wasm ships one build that evaluates stylesheets and computed style values, the @devup-ui/wasm/lite export is gone, and the Next.js plugin always loads @devup-ui/wasm instead of choosing an engine from the project's files

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

Patch

  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.

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

Patch

  • Fix at-rule composition and cascade: _print/_media/@media … nest correctly with selectors in both directions and inside globalCss, nested media queries merge (print and (…)), breakpoints put media types first, distribute query lists, and nest not … queries, and at-rules are emitted after all breakpoint values while pseudo selectors keep SELECTOR_ORDER across breakpoints. Add _motionReduce, _motionSafe, _portrait, _landscape, _contrastMore, _contrastLess and _forcedColors, remove the never-matching _speech, normalize query whitespace, support Emotion-style '@media …'/'&:hover' object keys, fix template literals losing declarations before @media or selectors around nested at-rules, apply typography conditionally under selectors, and drop stale global at-rules on re-extraction

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

Patch

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

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

Patch

  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.

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

Patch

  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.
  • Stop the dev server from reloading in a loop with vinext and other server environments: write a devup-ui CSS file only when its CSS changed, and keep those changes out of the server environments so a render is never reset midway

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

Patch

  • Load the modules a file imports: a .css.ts stylesheet can import other stylesheets and ordinary modules (an imported stylesheet exports the names its own CSS uses and stays imported), and a string or number declared with const in an imported module stays a static class instead of a CSS variable. The plugins resolve imports like the bundler (relative paths, tsconfig paths) through setModuleResolver and watch the files read (Output.dependencies). styled-components .attrs() (objects and functions) and .withConfig() compile, a file may import styled from several libraries, and conditional style arguments (cond && { ... }, ternaries) merge per property in css() and styled. Dynamic values inside '@layer' stay in the layer, and a layered declaration no longer shares a class with the same unlayered one. A declaration written only on a condition now wins over a typography preset, which sits in a cascade layer below direct declarations. Emotion's number-to-px conversion reaches conditional branches, and a side-effect import in a .css.ts file is kept. Imports also resolve to packages (exports conditions, module, main) and CommonJS modules, import cycles behave as in ES modules, value || { ... } and value ?? { ... } compose, styled composes classes like css(), and a style argument that cannot be known at build time is a build error instead of a silently missing style. A string or number const at the top of the file itself is inlined too, with templates and arithmetic over constants folded; css(), globalCss(), keyframes() and stylex.create() report a value known only at runtime as a build error instead of emitting an unset CSS variable; template interpolations of globalCss, createGlobalStyle and keyframes are written into the CSS, a template mixin (${mixin}, ${p => cond && mixin}) composes its class, and an unknown interpolation in a selector or property name is a build error; Next.js keeps the files a prewarmed extraction read so their changes rebuild; the css-utils-literal-only lint rule accepts the constants the build inlines; the webpack plugin prewarms with the module resolver, so a file merged into another file's CSS keeps the classes its loader emits; StyleX variables, constants and themes imported from another module resolve to the names that module generates, and every namespace or default StyleX import is recognized; a .css.ts file that throws while evaluated reports the exception instead of emitting runtime style() calls; constant inlining skips scope analysis when no style reads a constant; build errors carry file:line:column and all of them are reported; StyleX defineVars and createTheme values may depend on @media, @supports and @container conditions (optionally wrapped in types.()), emitted as one block per set of conditions after the unconditional values with nested conditions last, in the defining file and in the files importing it; a StyleX variable or theme value that is not static is a build error; Math. calls over constants fold at build time; a style value computed from constants when the module runs (darken(0.1, PRIMARY), a helper call) is computed at build time and becomes a static class; StyleX create(), defineVars(), defineConsts(), createTheme(), keyframes(), positionTry(), viewTransitionClass() and include() report what they cannot read (spreads, computed keys, non-object arguments) as located build errors instead of printing or dropping it, and stylex.props()/attrs() join styles compiled elsewhere (a style prop, a spread) at runtime; object, array and TypeScript enum constants (declared in the file or imported) read as if written in place wherever styles are read (css(base), {...base}, _hover: hover, space[2], Size.M, <Box {...base} />), a later property replacing an earlier one as in JavaScript, both in style objects and among JSX props; styles the build cannot read where an object is expected (a spread of an unknown object, a selector given a non-object, a computed key, a non-object globalCss/keyframes argument) are build errors instead of silently missing, a single css()/styled argument that is not a rule object composes as a class, a JSX spread of an object literal keeps its non-style props, a member read such as theme.colors[key] becomes a CSS variable instead of vanishing, and an index or key a spread may supply is read at runtime; values computed from constants through a const or an import, spreads, shorthand properties, computed keys and conditions are computed at build time, with stand-ins for the style packages and for modules the build cannot load; a TypeScript enum no longer crashes stylesheet evaluation; a binding css() or styled() composes that holds rules the module computes (const base = makeStyles(); css(base)) applies those rules, and a class computed in its place is never read as CSS text; build errors name what the build accepts (constants, and values computed from them); source maps point at the code as written when imports were rewritten or values computed in its place; an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once instead of on every evaluation importing it; css-utils-literal-only accepts what module functions, imports and built-ins compute from constants, and reports Date, Math.random and functions only known at runtime. A runtime value an element holds is written as it is, where it was optimized as CSS text (a string passed to a function lost digits, --i became var(--i)), and every value an element can hold reaches it: await, new, assignments, sequences and tagged templates were dropped. globalCss() used as a value gives undefined instead of invalid code, a condition known only at runtime is a build error in globalCss(), keyframes() and (it was dropped), and a condition the build knows picks its branch. A JSX spread, a selector prop and a spread, selector or computed key in a styled object compute what the module computes, as do css(...parts) and a template argument of css(); a namespace or object of known classes no longer runs the module; a stylesheet that loops forever fails instead of hanging the build; and css-utils-literal-only accepts the parameters of a callback written in the value. A component with a non-literal as (as={Link}, as={cond ? "a" : "b"}) is replaced where it stands: in JSX children it replaced its parent element or a later expression and left the component in the output, and in lists it lost its key inside a fragment. A spread whose value may change when read again (<Box {...register("email")} />, a hook) is evaluated once, with the props and children written before it in order, where it was evaluated for the spread, the className and the style; and a literal spread no longer reads className or style it does not write.

@codecov

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
bindings/devup-ui-wasm/src/lib.rs 100.00% <100.00%> (ø)
libs/css/src/optimize_value.rs 100.00% <ø> (ø)
libs/extractor/benches/extract_benchmark.rs 100.00% <ø> (ø)
libs/extractor/src/as_visit.rs 100.00% <100.00%> (ø)
libs/extractor/src/build_time_values.rs 100.00% <100.00%> (ø)
libs/extractor/src/css_utils.rs 100.00% <100.00%> (ø)
...tractor/src/extract_style/extract_dynamic_style.rs 100.00% <100.00%> (ø)
...xtractor/src/extract_style/extract_static_style.rs 100.00% <100.00%> (ø)
libs/extractor/src/extract_style/mod.rs 100.00% <100.00%> (ø)
.../extractor/extract_global_style_from_expression.rs 100.00% <ø> (ø)
... and 31 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

owjs3901 and others added 8 commits September 30, 2026 01:56
undefined, null and false give the default element, a component or a member such as motion.div renders as named, a condition between names becomes an element for each, and any other value is read at runtime once, with the default element when it is empty. Member expressions rendered nothing, templates took their first part, lowercase variables became tags and lookups built every element. Build-time values read Math only through members with a known key and toString only called at once without a radix, so aliases no longer reach approximate functions.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ss-utils-literal-only

Values in every rule object and in the CSS text of css, globalCss, keyframes and createGlobalStyle are checked, tagged templates included, as are the StyleX functions devup-ui compiles; parts css() composes as classes stay unchecked, and styled() is left out as it keeps values as CSS variables. Aliases of Math and toString read other than called at once are reported, as the build reads them.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
A dynamic as renders what it gives, styled() keeps uncertain values as CSS variables like elements, and a number for a time in a .css.ts file gets px as in vanilla-extract.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…imports read

A const holding what keyframes() or css() gives is read by the other style APIs and computed selector keys; the package imported whole compiles; styled() renders any base; top-level aliases of compiled imports compile and are removed, other runtime reads of removed imports and unknown selectors keys are build errors.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…t as static

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@owjs3901
owjs3901 merged commit cebcfb2 into main Sep 30, 2026
4 checks passed
@owjs3901
owjs3901 deleted the feat/compat-and-build-time-values branch September 30, 2026 02:49
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