Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 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
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_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"
}
8 changes: 4 additions & 4 deletions bindings/devup-ui-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1551,7 +1551,7 @@ mod tests {
);

// Test getters
assert!(!output.code().is_empty());
assert_ne!(output.code(), "");
assert_eq!(output.css_file(), Some("devup-ui-0.css".to_string()));
assert_eq!(output.map(), Some("//# sourceMappingURL=test".to_string()));
assert!(output.css().is_some());
Expand Down Expand Up @@ -1951,7 +1951,7 @@ mod tests {

assert!(result.is_ok());
let output = result.unwrap();
assert!(!output.code().is_empty());
assert_ne!(output.code(), "");
assert!(output.map().is_some());
}

Expand All @@ -1975,7 +1975,7 @@ mod tests {

assert!(result.is_ok());
let output = result.unwrap();
assert!(!output.code().is_empty());
assert_ne!(output.code(), "");
assert!(output.map().is_none());
}

Expand All @@ -2000,7 +2000,7 @@ mod tests {

assert!(result.is_err());
if let Err(error) = result {
assert!(!error.is_empty());
assert_ne!(error, "");
}
}

Expand Down
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
2 changes: 1 addition & 1 deletion libs/css/src/theme_tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ mod tests {
set_typography_keys(vec!["body".to_string(), "title".to_string()]);
assert_eq!(get_typography_keys(), vec!["body", "title"]);
set_typography_keys(vec![]);
assert!(get_typography_keys().is_empty());
assert_eq!(get_typography_keys(), Vec::<String>::new());
}

#[test]
Expand Down
5 changes: 3 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,7 @@ pub(crate) fn has_build_time_values(
filename,
option,
resolver,
aliased.css_prop,
);
let changes = crate::imported_constants::ChangeCheck::new(&program, filename, option, resolver);
!find(
Expand Down
Loading
Loading