Skip to content

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

Closed
owjs3901 wants to merge 107 commits into
mainfrom
feat/compat-gaps-and-module-loader
Closed

owjs3901 wants to merge 107 commits into
mainfrom
feat/compat-gaps-and-module-loader

Conversation

@owjs3901

@owjs3901 owjs3901 commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

머지 순서: #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를 갱신했습니다.

owjs3901 and others added 30 commits September 27, 2026 00:39
…cking

Boa's Context::default() reads std::time::Instant, which panics on wasm32-unknown-unknown without the js feature. Every .css.ts extraction crashed in the full WASM; the lite build is unaffected. Add a wasm-bindgen test and a bun regression that runs in CI.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…en-prefix rules

prefer-media-shorthand suggests _motionReduce, _print and the other media shorthands for _media entries and @media keys that spell out the same query. no-typography-token-prefix reports typography=$heading, which names no preset. Both are auto-fixable and part of the recommended config.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Media shorthand queries, at-rule key parsing, query normalization, media query combination (merge, never-match, nest) and the deterministic order at-rule blocks are emitted in. StyleSelector::At now carries its enclosing at-rules and owning file.

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

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

Nest _print, _media and @media keys with selectors in both directions (props, globalCss, template literals, Tailwind variants, StyleX conditions), apply typography conditionally, and keep responsive globalCss values in the base layer. vanilla-extract selectors, values and globalStyle now resolve references to styles, keyframes, fonts and themes, and createVar/fontFace/layer exports no longer shift onto each other.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Rules order by selector group before breakpoint level and at-rules follow every plain rule; globalCss and custom layers keep SELECTOR_ORDER instead of sorting selectors alphabetically. Adjacent rules with the same selector share one block, and a declaration written next to a conditional typography preset wins over it.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add _motionReduce, _motionSafe, _portrait, _landscape, _contrastMore, _contrastLess and _forcedColors, accept @media/@supports/@container keys, allow top-level at-rules in globalCss, and drop the never-matching _speech.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
typography takes the bare preset key, so the examples no longer write typography=$heading.

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>
… 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>
owjs3901 and others added 26 commits September 28, 2026 22:39
…and source maps

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

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

- A runtime value an element holds is written as it is: it was optimized as CSS text, so a string passed to a function lost digits and --i became var(--i)

- await, new, assignments, sequences and tagged templates reach the element instead of being dropped; a function or class is reported

- globalCss() used as a value gives undefined instead of invalid code, and a runtime condition in globalCss(), keyframes() or <Global> is a build error instead of being dropped; a condition the build knows picks its branch

- With the full engine, JSX spreads, selector props and the spreads, selectors and computed keys of styled objects 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 or needs the full engine, and a stylesheet that loops forever fails instead of hanging

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

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

An element spreading or selecting a binding may take styles only the full engine computes, so the engine choice checks those files too.

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

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

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…impure spreads once

- 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, which throws at runtime, in the output; 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

- A literal spread no longer reads a className or style it does not write

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
… cover every read-once path

- css(tokens.nested.x) is computed when the build knows tokens.nested but not its x: a member chain is resolved through the constants the build knows, where only the first member was checked

- Tests cover reading once in children, attribute elements, generators and compiled jsx() calls, the purity of each expression kind, and the conditions globalCss() cannot switch

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

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

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>
- Every build loads the one extraction engine instead of scanning the project at startup to choose between the lite and full engines, so a file added while the dev server runs is extracted like the others

- The static import graph and the prewarm file list are built where they were before the engine choice moved them ahead of loading WASM

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- Boa is a plain dependency of the extractor, sheet and WASM crates instead of the vanilla-extract feature, so every build evaluates stylesheets and computes style values

- @devup-ui/wasm builds one package: the lite build, its ./lite export and hasBuildTimeValues, which only chose between the engines, are gone

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
- The docs, the css-utils-literal-only README and the changepacks no longer mention the lite engine, hasBuildTimeValues or an engine Next.js picks: every build computes style values the module computes

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>
…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>
@owjs3901

Copy link
Copy Markdown
Contributor Author

#681로 합쳤습니다. #676·#677·#678의 커밋을 main 위로 rebase한 종합 PR입니다.

@owjs3901 owjs3901 closed this Sep 29, 2026
owjs3901 added a commit that referenced this pull request Sep 30, 2026
…me values (#676, #677, #678) (#681)

* fix(extractor): give vanilla-extract and StyleX numbers their library 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>

* fix(next-plugin): run every vanilla-extract stylesheet on the full engine

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>

* docs: document library number units and the full vanilla-extract engine

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

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

* chore: add changepack for library number units and full engine

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

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

* fix(extractor): fix vanilla-extract evaluation and Emotion/styled-components 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>

* fix(sheet): write layered rules like the others and let typography yield

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>

* docs: document library composition, vanilla-extract coverage and typography precedence

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

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

* chore: add changepack for compat, layer and typography fixes

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

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

* refactor(extractor): build font faces without a branching loop

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>

* refactor(extractor): build font faces without closures

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>

* refactor(extractor): build the single font-face rule list without vec!

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>

* refactor(extractor): make declaring font faces infallible

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>

* chore(extractor): keep the font-face loop out of tarpaulin's report

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>

* refactor(extractor): declare font-face lists recursively

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>

* refactor(extractor): build font-face rules without touching the collector

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>

* test(extractor): extract font faces through the library build

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>

* fix(extractor): keep custom property values verbatim

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>

* test(extractor): cover a createVar declaration that cannot be read

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

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

* refactor(extractor): fit the createVar signature on one line

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

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

* refactor(extractor): split createVar into name and reference helpers

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

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

* refactor(extractor): move the declared createVar branch into its own function

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

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

* refactor(extractor): keep the declared createVar helper out of line

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

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

* fix(extractor): resolve placeholders through one selector function type

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>

* test(extractor): resolve a style reference outside a selector through the library

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

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

* feat(extractor): support styled-components attrs and withConfig, and 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>

* fix(extractor): keep dynamic values and layered classes in their layer

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>

* fix(sheet): place dynamic rules of a layer inside it

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

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

* feat(extractor): compose conditional style parts per property

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>

* fix(sheet): put conditional typography below direct declarations

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>

* feat(extractor): load the modules a stylesheet imports

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>

* feat(extractor): inline imported constants read by styles

With a module resolver, an import read in a style (props of the package's components, arguments of its functions) is looked up in its module; a string or number declared with const there (a literal, an as-const object's member, an alias, a re-export or a default export) replaces the read, so <Box color={PRIMARY} /> becomes a static class instead of a CSS variable. Only primitives are inlined and references are resolved by scope, so the output behaves the same; the modules read are returned as dependencies.

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

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

* feat(wasm): resolve imports through setModuleResolver

setModuleResolver((specifier, importer) => ({ path, code })) lets codeExtract load the modules a file imports, and Output.dependencies lists the files it read for the bundler to watch.

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

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

* feat(plugin-utils): add createModuleResolver

Resolves the imports of extracted files like the import graph does (relative paths, tsconfig paths, index files, and names like theme.css to theme.css.ts) and reads each module, naming it with the plugin's own toId so a module is extracted under the name the bundler uses.

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

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

* feat(vite-plugin): load imported modules and watch them

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

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

* feat(webpack-plugin): load imported modules and depend on them

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

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

* feat(next-plugin): load imported modules and depend on them

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

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

* feat(rsbuild-plugin): load imported modules and depend on them

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

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

* feat(bun-plugin): load imported modules

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

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

* docs: document module loading, imported constants and the new compatibility

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

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

* chore: add changepack for module loading and compatibility gaps

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

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

* test(extractor): cover every path of imported constant inlining

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

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

* feat(extractor): compose || and ?? style parts and reject what cannot compose

left || right and left ?? right now compose like the other conditions: left as a class while it applies (only when it is a string, as the libraries skip other values), right per property otherwise. styled factories compose classes passed beside rule objects like css() does. Style arguments that cannot be composed at build time (a call, a spread, a computed key or a rule object replacing a value) are now a build error naming them instead of silently producing an empty class.

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

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

* feat(extractor): evaluate import cycles like ES modules

A module is reserved before the modules it imports and exposes live bindings, so a cycle works when neither side reads the other before it is evaluated, and a read too early fails with a clear error naming the binding and module. Imports of a module still evaluating are read where they are used. A stylesheet loaded by another one reports its failure instead of falling back to plain extraction, and modules are parsed with their own source type (so .tsx and .js modules load).

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

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

* feat(extractor): load CommonJS modules

A module without import or export that uses module, exports or require runs with its own module.exports; require of a literal path loads the module like an import, its keys become the exports, and the default follows bundler interop (the .default of a compiled ES module, module.exports otherwise).

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

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

* feat(extractor): inline constants of CommonJS modules

exports.x = ..., module.exports.x = ... and module.exports = { ... } inline like export const, as long as the module assigns the property once (TypeScript's void 0 placeholders do not count), so a value that can change is never inlined. require() bindings are followed, and the default follows bundler interop.

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

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

* feat(plugin-utils): resolve packages in createModuleResolver

Bare specifiers that no tsconfig path matches resolve in node_modules like a bundler: package exports (strings, arrays, conditions import > module > default > require > node, subpath maps with * patterns, null blocks), then module, main and index, including .cjs entries. Resolved files use their real path, as bundlers name symlinked installs.

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

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

* fix(extractor): report an import cycle read too early instead of falling back

A stylesheet whose import cycle is read before it is evaluated fails as it does in ES modules (and in vanilla-extract) rather than falling back to plain extraction, whose output would still call the styling APIs.

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

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

* docs: document packages, CommonJS, import cycles and style composition errors

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

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

* test(extractor): cover lazy default imports and reads without constants

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

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

* fix(extractor): reject runtime values without an element and place template interpolations

css(), globalCss(), keyframes() and stylex.create() now report a value known only at runtime instead of emitting an unset CSS variable or dropping it. Module-level consts inline without a resolver, with templates and arithmetic folded. Template interpolations are written into global and keyframes CSS, mixins compose their class, and unknown selector or property interpolations are build errors.

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

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

* fix(next-plugin): keep the files an extraction read when it is prewarmed

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

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

* fix(eslint-plugin): accept constants the build inlines in css utils

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

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

* docs: document runtime value errors, local constants and template interpolations

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

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

* test(extractor): keep computed stylex keys runtime values and cover number strings

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

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

* test(sheet): keep the important value dynamic now that local consts inline

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

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

* test(sheet): cover a dynamic base style updating the base sheet

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

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

* fix(webpack-plugin): prewarm with the module resolver the loader uses

Without it, the prewarm extracted imported constants as CSS variables while the loader later emitted static classes, so a file merged into another file's CSS bucket could reference classes missing from the CSS webpack had already built.

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

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

* feat(extractor): resolve imported StyleX values and locate every build error

StyleX defineVars, createThemeContract, defineConsts and createTheme values 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 when plain extraction would leave its calls uncompiled. Constant inlining builds scoping only when a style reads a name that may hold a constant. Build errors are reported together, in source order, as file:line:column, mapped back through the import alias rewrite.

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

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

* docs: document imported StyleX values, stylesheet evaluation errors and located build errors

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

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

* feat(extractor): support conditional StyleX variable and theme values

defineVars and createTheme values may be condition objects of default, @media, @supports and @container keys, optionally wrapped in types.*(). Each set of conditions becomes one block after the unconditional values, deeper conditions last so the more specific value wins. Importers resolve such variables to the same names, reading constant condition keys like [DARK] as the defining file does. A value that is not static is a build error instead of a silently missing variable.

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

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

* docs: document conditional StyleX variable and theme values

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

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

* test(extractor): cover a spread in a StyleX variable condition

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

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

* feat(extractor): compute build-time style values and report every StyleX input it cannot read

Math.* calls over constants fold; a style value computed from constants runs through the full engine and becomes a static class. StyleX create/defineVars/defineConsts/createTheme/keyframes/positionTry/viewTransitionClass/include report spreads, computed keys and non-object arguments as located build errors, and props()/attrs() join styles compiled elsewhere at runtime.

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

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

* feat(next-plugin): select the full engine when a file computes style values

The WASM bindings export hasBuildTimeValues, and selectWasmVariant loads the full engine when a candidate file has a style value only running the module computes.

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

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

* docs: document build-time computed values and StyleX build errors

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

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

* test(extractor): cover build-time values and StyleX error branches

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

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

* fix(extractor): read constant objects, arrays and enums as styles and report styles it cannot read

Object, array and TypeScript enum constants, declared in the file or imported, are inlined where styles read them, a later property replacing an earlier one in style objects and among JSX props. A spread of an unknown object, a selector given a non-object, a computed key and a non-object globalCss/keyframes argument are build errors instead of silently missing; a single css()/styled argument composes as a class; a JSX literal spread keeps its non-style props; theme.colors[key] becomes a CSS variable; an index or key a spread may supply is read at runtime. The full engine computes values reached through a const or an import, spreads, shorthand properties, computed keys and conditions, standing in for the style packages and for modules it cannot load. Stylesheet evaluation no longer panics on TypeScript enums.

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

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

* feat(next-plugin): detect computed style values after inlining constants

hasBuildTimeValues takes the import aliases and reads imported constants through the module resolver before deciding, so a file reading only constants keeps the lite engine and one reading a computed constant selects the full engine.

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

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

* docs: document constant style objects and styles the build cannot read

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

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

* test(extractor): cover remaining build-time value and constant branches

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

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

* feat(extractor): compose rules the module computes and map sources to the code as written

- css() and styled() compose a binding holding rules only running the module gives through the full engine; a class computed in its place is no longer read as CSS text

- build errors state what each engine accepts: constants, and with the full engine values computed from them

- source maps point at the code as written through rewritten imports and computed values

- an imported stylesheet runs once per script it evaluates to, and each module is stripped of TypeScript once

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

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

* fix(next-plugin): check styled compositions when choosing the engine

A file composing a binding with styled.div(...) or styled(...)(...) may hold rules only the full engine computes, so the engine choice checks it too.

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

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

* fix(eslint-plugin): accept values the build computes in css utils

css-utils-literal-only follows what the engines read: calls of imported or module-level functions and built-ins over constants, Math folds, arrays, conditions and computed members of constants pass, while Date, Math.random and functions only known at runtime are reported. The README documents the computed values and the engine each needs.

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

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

* docs: document composed computed rules, engine-specific build errors and source maps

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

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

* fix(extractor): keep runtime values as written and compute what elements take

- A runtime value an element holds is written as it is: it was optimized as CSS text, so a string passed to a function lost digits and --i became var(--i)

- await, new, assignments, sequences and tagged templates reach the element instead of being dropped; a function or class is reported

- globalCss() used as a value gives undefined instead of invalid code, and a runtime condition in globalCss(), keyframes() or <Global> is a build error instead of being dropped; a condition the build knows picks its branch

- With the full engine, JSX spreads, selector props and the spreads, selectors and computed keys of styled objects 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 or needs the full engine, and a stylesheet that loops forever fails instead of hanging

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

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

* fix(next-plugin): check JSX spreads and selector props when choosing the engine

An element spreading or selecting a binding may take styles only the full engine computes, so the engine choice checks those files too.

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

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

* docs: document computed element styles, known conditions and runtime values

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

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

* fix(extractor): place a dynamic as where the element stands and read impure spreads once

- 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, which throws at runtime, in the output; 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

- A literal spread no longer reads a className or style it does not write

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

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

* fix(extractor): tell nested members of partly known objects apart and cover every read-once path

- css(tokens.nested.x) is computed when the build knows tokens.nested but not its x: a member chain is resolved through the constants the build knows, where only the first member was checked

- Tests cover reading once in children, attribute elements, generators and compiled jsx() calls, the purity of each expression kind, and the conditions globalCss() cannot switch

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

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

* test(css,extractor): cover value whitespace and calls of partly known objects

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

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

* docs: note the dynamic as and read-once spread fixes in the changepack

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

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

* refactor(next-plugin): always load @devup-ui/wasm

- Every build loads the one extraction engine instead of scanning the project at startup to choose between the lite and full engines, so a file added while the dev server runs is extracted like the others

- The static import graph and the prewarm file list are built where they were before the engine choice moved them ahead of loading WASM

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

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

* refactor(wasm): build one engine and drop @devup-ui/wasm/lite

- Boa is a plain dependency of the extractor, sheet and WASM crates instead of the vanilla-extract feature, so every build evaluates stylesheets and computes style values

- @devup-ui/wasm builds one package: the lite build, its ./lite export and hasBuildTimeValues, which only chose between the engines, are gone

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

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

* docs: describe one extraction engine

- The docs, the css-utils-literal-only README and the changepacks no longer mention the lite engine, hasBuildTimeValues or an engine Next.js picks: every build computes style values the module computes

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

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

* chore: add changepack for removing the lite engine

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

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

* fix(extractor): stop reading changed objects and the build environment 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>

* docs: document changed objects and the build environment

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

* chore: add changepack for changed objects and the build environment

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

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

* test(extractor): cover spread arguments and the first argument Object 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>

* fix(extractor): close the gaps in changed objects and build-environment 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>

* fix(eslint-plugin): report constants the file changes in css-utils-literal-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>

* docs: document what changes objects, the build environment and failed 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>

* chore: add changepack for the runtime value gaps

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

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

* fix(extractor): only run build-time code whose result is certain

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>

* fix(eslint-plugin): follow the strict build-time rules in css-utils-literal-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>

* docs: document the strict build-time rules and time units

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

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

* chore: add changepack for the strict build-time values

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

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

* test(extractor): cover literal conditions and reads of partly known namespaces

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

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

* test(extractor): read a member of a string a partly known namespace holds

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

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

* chore: leave the changepack of #675 as main has it

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>

* test(extractor): read an array member of a partly known namespace

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

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

* fix(extractor): render what a dynamic as 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 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>

* fix(eslint-plugin): check every style argument and StyleX values in css-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>

* docs: document dynamic as, styled values and times in .css.ts files

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>

* chore: add changepack for dynamic as and wider lint checks

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

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

* fix(extractor): read style results as values and never leave removed 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>

* fix(eslint-plugin): read css() and keyframes() results held in a const as static

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

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

* chore: add changepack for style results as values

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

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

* test(components): inline the spinner keyframes name as a static class

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

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

---------

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant