Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9e29139
fix(extractor): let a later css() part replace an earlier one's decla…
owjs3901 Oct 1, 2026
337ddeb
test(extractor): cover known style composition branches
owjs3901 Oct 1, 2026
024a24c
chore: satisfy clippy assert_is_empty lint on Rust 1.99
owjs3901 Oct 1, 2026
71585a9
test(extractor): cover false ?? composition branch
owjs3901 Oct 1, 2026
52f4518
refactor(extractor): split coalesce check so composition lines are me…
owjs3901 Oct 1, 2026
1cedf9f
fix(extractor): compose styled extensions, merge attrs and let later …
owjs3901 Oct 1, 2026
01adab9
test(extractor): restore runtime-value composition test and cover sty…
owjs3901 Oct 1, 2026
53f740b
fix(extractor): render styled as targets and compile withComponent
owjs3901 Oct 1, 2026
b4b42cf
fix(extractor): keep read, $ and theme props off styled tags and eval…
owjs3901 Oct 1, 2026
e2cfa67
test(extractor): cover shouldForwardProp forms and element prop filte…
owjs3901 Oct 1, 2026
ce9f390
fix(extractor): forward refs from generated styled components
owjs3901 Oct 2, 2026
95d35c8
fix(extractor): compile Emotion's css prop at build time
owjs3901 Oct 2, 2026
e017367
test(extractor): cover css props on styled components with responsive…
owjs3901 Oct 2, 2026
0faa84e
fix(plugin-utils): compile the css prop where tsconfig builds JSX wit…
owjs3901 Oct 2, 2026
1a6a31e
fix(extractor): compile Emotion's ClassNames at build time
owjs3901 Oct 2, 2026
4f88bd4
test(extractor): cover ClassNames child functions taking rest or nest…
owjs3901 Oct 2, 2026
d44950f
fix(extractor): compile Emotion component selectors at build time
owjs3901 Oct 3, 2026
050c686
fix(extractor): read constants in Emotion rules as px and compose mix…
owjs3901 Oct 3, 2026
0edb211
fix(extractor): compile or reject function-local style objects, keep …
owjs3901 Oct 3, 2026
734fd16
test(extractor): cover parenthesized function-local style objects
owjs3901 Oct 3, 2026
af1177d
merge: update PR #706 with main ESLint checks
owjs3901 Oct 8, 2026
944e723
merge: update PR #706 with 9a953607
owjs3901 Oct 8, 2026
5a70d2a
merge: update PR #706 with db631c6c
owjs3901 Oct 8, 2026
30247ba
merge: update PR #706 with main
owjs3901 Oct 8, 2026
c7ea90a
merge: update PR #707 with main
owjs3901 Oct 8, 2026
4669b17
merge: update PR #710 with PR #707
owjs3901 Oct 8, 2026
9e4d28a
merge: update PR #711 with PR #710
owjs3901 Oct 8, 2026
ae283a7
merge: update PR #722 with PR #711
owjs3901 Oct 8, 2026
12328b9
merge: update PR #727 with PR #722
owjs3901 Oct 8, 2026
9f94748
merge: update PR #728 with main
owjs3901 Oct 8, 2026
7a8d96b
merge: update PR #729 with PR #728
owjs3901 Oct 8, 2026
a355130
merge: update PR #730 with PR #729
owjs3901 Oct 8, 2026
b3f0ac4
merge: update PR #740 with PR #730
owjs3901 Oct 8, 2026
d846365
merge: update PR #745 with PR #740
owjs3901 Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_known_styles.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "css(a, b) composing classes whose styles the build knows (a const bound to css() in the file, or a css() result another module exports with a rule object every value of which is known) merges their styles: a later part's declaration replaces an earlier one's for the same property, selector, breakpoint and layer, also under conditions (css(base, cond && danger)) and for ||/?? parts, where the classes used to be joined and the stylesheet order picked the winner. vanilla-extract style([a, b]) passes each composed style as its own argument, so the later one wins, and a style composed again later (style([a, b, a])) is kept. Classes the build does not know (strings, props, CSS Modules) are kept as they are",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_compose_styled.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "styled(Base) extending a styled component the file binds to a const renders Base's tag directly with Base's styles composed under its own, so the extension's declarations replace Base's for the same property, selector, breakpoint and layer instead of losing to them by stylesheet order; Base's attrs apply before the extension's. attrs merge as styled-components merges them: className is joined with the caller's, style is merged and other props replace earlier ones. On an element, a className or style spread after the explicit prop now wins, as React merges props in the order they are written",
"date": "2026-10-01T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_emotion_class_names.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/react/package.json": "Patch"
},
"note": "Emotion's <ClassNames> now compiles at build time with no runtime: the element becomes what its child function ({ css, cx, theme }) => ... renders, each css and cx call becomes the classes it composes (later styles replace earlier ones; css reads strings as CSS text and its tagged templates split around mixins; cx keeps unknown classes, reads { name: condition } objects as conditional classes and composes nested css and cx calls), and theme.a.b in their values becomes var(--a-b). Numbers in css rules are px lengths, as Emotion reads them, and top-level constants are inlined. New build errors, each at file:line:col: a <ClassNames> without exactly one child function taking { css, cx, theme } that gives what it renders at once (attributes, other children, rest or default parameters, async or generator functions, several statements); reading the theme other than as theme.a.b in a css value; using css or cx other than by calling them; a cx object with a spread, getter or method; parts css and cx cannot compose (runtime calls, spreads, a condition choosing between styles and classes). @devup-ui/react/compat exports ClassNames with the ClassNamesContent and ClassNamesArg types, and the @emotion/react declarations include them",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_emotion_component_selectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Emotion and styled-components component selectors now compile at build time: a styled component the file defines that another style selects (${Child} { ... } in CSS text, [Child] or [`&:hover ${Child}`] as a rule key) gets a short marker class, which only such components get, and the selector reads it as .marker; a component extending it, or an element inlining it for its css prop, keeps the marker. Selectors written without & now nest under the component as stylis nests them (ul { ... } is & ul, a selector list nests each selector, :hover applies to the component itself) instead of becoming global rules, and rule-object keys starting with ., #, [, *, >, + or ~ are read as nested selectors instead of properties",
"date": "2026-10-01T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_emotion_css_prop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/react/package.json": "Patch"
},
"note": "Emotion's css prop compiles at build time while @emotion/react is aliased: on tags and Devup UI components always, and on every element once the file imports @emotion/react or @emotion/styled or names @emotion/react in a @jsxImportSource pragma; jsx, jsxs and jsxDEV from @emotion/react/jsx-runtime (or jsx-dev-runtime) and jsx from @emotion/react compile it too, so libraries built with Emotion's JSX runtime do as well. The prop becomes the element's className, with CSS variables in its style for values only the runtime gives: arrays and conditions compose with a later part replacing what an earlier one sets, css() classes the file knows compose by their styles, strings and templates are CSS text, css`` mixins split the text around them, a function of the theme reads theme.a.b as var(--a-b), unitless numbers are px as in Emotion (constants included), and a className holding known css() classes overrides the prop as Emotion's registered classes do. A styled component the file defines renders its tag in the element's place when the prop overrides its styles and it renders a tag with no attrs or props read and the element has no spread, as or forwardedAs. Emotion's JSX runtime imports become react/jsx-runtime, the pragma names react, and jsx comes from @devup-ui/react/compat (React's createElement); @devup-ui/react/compat/css-prop types the prop on React.Attributes. New build errors, each with file:line:column and the code: a css prop part the build cannot read (a call, an element), a style object declared inside a function or with let, a binding only running the module gives or code changes, a theme function that does more than return rules or reads the theme other than as theme.a.b in a value, an interpolation CSS text cannot place or a mixin inside a nested rule, and a css prop overriding a styled component's styles where its tag cannot be rendered in place. Composing css() with a part that reads a known binding (such as a keyframes name) now reads its value",
"date": "2026-10-01T00:00:00.000Z"
}
14 changes: 14 additions & 0 deletions .changepacks/changepack_log_emotion_jsx_import_source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch",
"packages/react/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/next-plugin/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/bun-plugin/package.json": "Patch"
},
"note": "A project whose tsconfig.json (or jsconfig.json) builds JSX with Emotion (compilerOptions.jsxImportSource of @emotion/react, through extends and project references too) now compiles the css prop on every element of every file, and its .tsx and .jsx files are built with React's JSX runtime through a /** @jsxImportSource react */ pragma, which a file's own JSX pragma overrides, so the bundler no longer imports Emotion's runtime. The plugins read the setting into a new @emotion/react/jsx-runtime alias (importAliases can turn it off with false, and it is left out when @emotion/react is not aliased); @devup-ui/react/compat/emotion-jsx-runtime declares Emotion's JSX runtimes as React's so the project type-checks without Emotion installed. plugin-utils exports readJsxImportSource, and mergeImportAliases takes the JSX import source",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_emotion_rule_numbers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Numbers in rules written against Emotion, styled-components or vanilla-extract now read as px lengths when a constant stands in for the literal, as they do for a literal written in place: css({ padding: gap }) with const gap = 8, styled.div({ margin: SIZE }), <Global styles={{ body: { margin: GAP } }} /> and the same with imported constants and constant style objects give 8px, while unitless properties keep bare numbers, 0 stays 0 and Devup shorthands such as p keep the spacing scale. css`${mixin}; color: red;` outside the css prop composes the mixin by styles, so a later declaration replaces the mixin's, and a css`...` template is a known style a later css() composes. Inline css() calls inside css() arguments (css(css({ ... }), { ... }), css(cond && css({ ... }), base)) are composed with the other parts, the later winning, instead of being compiled to a class of their own. No new build errors",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_local_style_objects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "A style object declared with const inside a function and passed to css(), styled(), globalCss(), keyframes(), a style prop or a spread (also through the Emotion, styled-components and vanilla-extract aliases) now compiles when every entry is written out and the object is only read as styles, instead of leaving the object where the class belongs; the build errors, at the binding, when the object is also read elsewhere (where code may change it), and at the call when it holds values only the runtime gives. Emotion's ClassNames cx keeps a function call's result as a class that stays as written, as @emotion/css cx does. css() and styled() compose an array of rule objects under a condition. A style function of the props (styled.div((props) => ({ ... })), and as a later argument) compiles: each value reading the props becomes a CSS variable the component sets (a number is a px length unless the property is unitless), theme reads (including theme.a[2]) become var(--a-2), and a condition chooses per value. New build errors: styled(...).attrs(...) chained after the styles (chain .attrs before them), a style function of the props that does not give one rule object at once with every entry written out, and a style object declared in a function that is also read elsewhere",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_forward_ref.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components the build generates are wrapped in React's forwardRef, so a ref passed to them reaches the element they render on React 18 as well as React 19; the file imports forwardRef from react when it defines one",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_props.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components no longer pass every prop to the tag they render: `$` props, `theme` and the props their style functions or attrs read are kept away from a tag unless it takes them as attributes, and `shouldForwardProp` (Emotion options or styled-components withConfig) is evaluated at build time. A `shouldForwardProp` the build cannot evaluate is a build error naming the file, line and code, with the forms it accepts. An element using a styled component the file defines drops props the component neither reads nor passes on; spread props are passed as written",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_styled_targets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Styled components render what their `as` prop names, defaulting to the tag or component they were defined with, and pass `forwardedAs` on as `as`, as styled-components and Emotion do; `as` used to reach the rendered tag as an attribute. `Component.withComponent(target)` on a styled component the file binds to a const builds a component rendering the same styles and attrs as `target` (a tag name or a component JSX can name), where it used to call a method the generated function does not have and throw",
"date": "2026-10-01T00:00:00.000Z"
}
38 changes: 38 additions & 0 deletions libs/css/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,25 @@ pub fn keyframes_to_keyframes_name(keyframes: &str, filename: Option<&str>) -> S
})
}

/// The class marking the component `name` defined in `filename`, which other
/// styles select it by
#[must_use]
pub fn component_marker(name: &str, filename: &str) -> String {
with_prefix(|prefix| {
if is_debug() {
format!("{prefix}c-{name}")
} else {
let class_num = class_num_for_key("", |key| {
key.push_str("c-");
key.push_str(filename);
key.push('-');
key.push_str(name);
});
format!("{prefix}{class_num}")
}
})
}

/// ASCII lookup table for selector encoding. `None` means pass through (alphanumeric, `-`, `_`)
/// or fall through to the Unicode escape path.
const SELECTOR_ENCODE: [Option<&str>; 128] = {
Expand Down Expand Up @@ -753,6 +772,25 @@ mod tests {
);
}

#[test]
#[serial]
fn test_component_marker() {
set_debug(false);
reset_class_map();
let child = component_marker("Child", "a.tsx");
assert_eq!(child, "a");
assert_eq!(component_marker("Child", "a.tsx"), child);
assert_eq!(component_marker("Child", "b.tsx"), "b");
assert_eq!(component_marker("Other", "a.tsx"), "c");
assert_eq!(
sheet_to_classname("color", 0, Some("red"), None, None, None),
"d"
);
set_debug(true);
assert_eq!(component_marker("Child", "a.tsx"), "c-Child");
set_debug(false);
}

#[test]
#[serial]
fn test_sheet_to_classname() {
Expand Down
6 changes: 4 additions & 2 deletions libs/extractor/src/build_time_values.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ pub(crate) fn has_build_time_values(
option: &ExtractOption,
resolver: Option<&ModuleResolver>,
) -> bool {
let (code, _) = crate::import_alias_visit::transform_import_aliases_with_edits(
let aliased = crate::import_alias_visit::transform_import_aliases_with_edits(
code,
filename,
&option.package,
&option.import_aliases,
);
let allocator = Allocator::default();
let Some(mut program) = parse(&allocator, filename, &code) else {
let Some(mut program) = parse(&allocator, filename, &aliased.code) else {
return false;
};
let inlined = crate::imported_constants::inline_constants(
Expand All @@ -73,6 +73,8 @@ pub(crate) fn has_build_time_values(
filename,
option,
resolver,
aliased.css_prop,
&aliased.px,
);
let changes = crate::imported_constants::ChangeCheck::new(&program, filename, option, resolver);
!find(
Expand Down
Loading
Loading