Skip to content
Merged
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_nested_css_results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch"
},
"note": "Statically known css() results used as nested rule values expand into their source rules under each top-level comma-separated selector, preserving responsive values, nested selectors, layers and style order without runtime styling. Unknown rule sources retain a located build error naming the value and its static-value requirement.",
"date": "2026-10-01T00:00:00.000Z"
}
21 changes: 21 additions & 0 deletions libs/css/src/style_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,25 @@ pub fn split_selector_list(selector: &str) -> Vec<&str> {
let mut parts = vec![];
let mut depth = 0usize;
let mut start = 0;
let mut quote = None;
let mut escaped = false;
for (index, c) in selector.char_indices() {
if escaped {
escaped = false;
continue;
}
if c == '\\' {
escaped = true;
continue;
}
if let Some(delimiter) = quote {
if c == delimiter {
quote = None;
}
continue;
}
match c {
'\'' | '"' => quote = Some(c),
'(' | '[' => depth += 1,
')' | ']' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
Expand Down Expand Up @@ -789,6 +806,10 @@ mod tests {
#[case("&:hover", vec!["&:hover"])]
#[case(" a > b , i ", vec!["a > b", "i"])]
#[case(":is(a, b), [title=\"a,b\"]", vec![":is(a, b)", "[title=\"a,b\"]"])]
#[case(r#"&[title="a,]b"], &:focus"#, vec![r#"&[title="a,]b"]"#, "&:focus"])]
#[case(r"&.a\,b, &:focus", vec![r"&.a\,b", "&:focus"])]
#[case(r"&.a\(, &:focus", vec![r"&.a\(", "&:focus"])]
#[case(r#"&[title="a\",]b"], &:focus"#, vec![r#"&[title="a\",]b"]"#, "&:focus"])]
fn test_split_selector_list(#[case] selector: &str, #[case] expected: Vec<&str>) {
assert_eq!(split_selector_list(selector), expected);
}
Expand Down
4 changes: 2 additions & 2 deletions libs/extractor/src/extractor/extract_style_from_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ pub fn extract_style_from_expression<'a>(
ast_builder,
part,
offset,
expression,
&mut expression.clone_in(ast_builder.allocator()),
level,
selector,
literal_handling,
Expand Down Expand Up @@ -437,7 +437,7 @@ pub fn extract_style_from_expression<'a>(
ast_builder,
part,
offset,
&mut o.value,
&mut o.value.clone_in(ast_builder.allocator()),
level,
selector,
literal_handling,
Expand Down
221 changes: 221 additions & 0 deletions libs/extractor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11416,6 +11416,227 @@ export const e = spin.toString()",
assert!(extract("a.css.ts", code, option(false)).is_ok());
}

#[rstest]
#[case::user_example(
"{ color: 'hotpink' }",
"'&:hover,&:focus': hotpink",
"'&:hover': { ...hotpinkRules }, '&:focus': { ...hotpinkRules }"
)]
#[case::inner_comma(
"{ color: 'hotpink' }",
"'&:is(a,b), &:not(.x,.y)': hotpink",
"'&:is(a,b)': { ...hotpinkRules }, '&:not(.x,.y)': { ...hotpinkRules }"
)]
#[case::three_selectors(
"{ color: 'hotpink' }",
"' &:hover, &:focus, &:active ': hotpink",
"'&:hover': { ...hotpinkRules }, '&:focus': { ...hotpinkRules }, '&:active': { ...hotpinkRules }"
)]
#[case::responsive(
"{ color: ['hotpink', null, 'blue'], p: [1, 2] }",
"'&:hover,&:focus': hotpink",
"'&:hover': { ...hotpinkRules }, '&:focus': { ...hotpinkRules }"
)]
#[case::nested_layer_order(
"{ '@layer': { base: { color: 'hotpink', _active: { color: ['red', 'blue'] } } } }",
"styleOrder: 3, '&:hover,&:focus': hotpink, '&:hover': { color: 'green' }",
"styleOrder: 3, '&:hover': { ...hotpinkRules }, '&:focus': { ...hotpinkRules }, '&:hover': { color: 'green' }"
)]
#[case::quoted_commas(
"{ color: 'hotpink' }",
r#"'&[data-x=a,b], &[title="a,]b"]': hotpink"#,
r#"'&[data-x=a,b]': { ...hotpinkRules }, '&[title="a,]b"]': { ...hotpinkRules }"#
)]
#[serial]
fn test_css_nested_static_result(
#[case] rules: &str,
#[case] nested: &str,
#[case] expanded: &str,
#[values("css(hotpinkRules)", "css({}, hotpinkRules)")] source: &str,
) {
let compile = |body| {
reset_class_map();
reset_file_map();
extract(
"test.tsx",
&format!("import {{ css }} from '@devup-ui/react'; const hotpinkRules = {rules}; const hotpink = {source}; export const hover = css({{ {body} }});"),
ExtractOption::default(),
)
.unwrap()
};
let expected = compile(expanded);
let actual = compile(nested);
assert_eq!(actual.styles, expected.styles);
assert_eq!(actual.code, expected.code);
}

#[rstest]
#[case::unknown("")]
#[case::external_class("const hotpink = css('external', { color: 'red' });")]
#[serial]
fn test_css_nested_static_result_unknown_error(#[case] binding: &str) {
reset_class_map();
reset_file_map();
let error = extract(
"test.tsx",
&format!("import {{ css }} from '@devup-ui/react'\n{binding}\nexport const hover = css({{ '&:hover,&:focus': hotpink }})"),
ExtractOption {
package: "@devup-ui/react".to_string(),
css_dir: "@devup-ui/react".to_string(),
single_css: true,
import_main_css: false,
import_aliases: HashMap::new(),
},
)
.unwrap_err()
.to_string();
assert!(
error.starts_with("test.tsx:3:47: `css()` cannot use `hotpink` at build time:")
&& error.contains("its values must be literals, theme tokens or constants"),
"{error}"
);
}

#[rstest]
#[case::composed(
"const hotpink = css(base, { p: 2 });",
"hotpink",
"{ color: 'hotpink', p: 2 }"
)]
#[case::chained(
"const hotpink = css({ _active: base });",
"hotpink",
"{ _active: { color: 'hotpink' } }"
)]
#[case::keyframes(
"const hotpink = css({ animationName: spin });",
"hotpink",
"{ animationName: spin }"
)]
#[case::alias("const hotpink = base;", "hotpink", "{ color: 'hotpink' }")]
#[case::inline("", "css({ color: 'hotpink' })", "{ color: 'hotpink' }")]
#[case::inline_known_composition(
"const hotpink = css(base, { color: 'blue', _active: base });",
"css(base, { color: 'blue', _active: base })",
"{ color: 'blue', _active: { color: 'hotpink' } }"
)]
#[case::composed_later_wins(
"const hotpink = css(base, { color: 'blue', p: 2 });",
"hotpink",
"{ color: 'blue', p: 2 }"
)]
#[case::alias_chain(
"const warm = base; const hotpink = warm;",
"hotpink",
"{ color: 'hotpink' }"
)]
#[case::selector_record(
"const hotpink = css({ selectors: { '&:active': base } });",
"hotpink",
"{ selectors: { '&:active': { color: 'hotpink' } } }"
)]
#[case::declaration_string(
"const hotpink = css({ content: base });",
"hotpink",
"{ content: `${base}` }"
)]
#[case::spread_composed(
"const hotpink = css(...[base, { p: 2 }]);",
"hotpink",
"{ color: 'hotpink', p: 2 }"
)]
#[case::adjacent_spreads(
"const hotpink = css(...[base], ...[{ color: 'blue', p: 2 }]);",
"hotpink",
"{ color: 'blue', p: 2 }"
)]
#[case::nested_array_spread(
"const hotpink = css(...[...[base], { p: 2 }]);",
"hotpink",
"{ color: 'hotpink', p: 2 }"
)]
#[case::direct_spread_source(
"const hotpink = css(...{ _active: base });",
"hotpink",
"{ _active: { color: 'hotpink' } }"
)]
#[case::spread_declaration_string(
"const hotpink = css(...[{ content: base }, { p: 2 }]);",
"hotpink",
"{ content: `${base}`, p: 2 }"
)]
#[case::layer_record(
"",
"{ '@layer': { colors: base } }",
"{ '@layer': { colors: { color: 'hotpink' } } }"
)]
#[case::media_record(
"",
"{ '@media': { print: base } }",
"{ '@media': { print: { color: 'hotpink' } } }"
)]
#[case::supports_record(
"",
"{ '@supports': { '(display: grid)': base } }",
"{ '@supports': { '(display: grid)': { color: 'hotpink' } } }"
)]
#[case::container_record(
"",
"{ '@container': { '(min-width: 10px)': base } }",
"{ '@container': { '(min-width: 10px)': { color: 'hotpink' } } }"
)]
#[case::underscore_media_record(
"",
"{ _media: { print: base } }",
"{ _media: { print: { color: 'hotpink' } } }"
)]
#[case::record_declaration_string(
"",
"{ '@layer': { colors: { content: base } } }",
"{ '@layer': { colors: { content: `${base}` } } }"
)]
#[serial]
fn test_css_nested_static_source_references(
#[case] binding: &str,
#[case] value: &str,
#[case] expanded: &str,
) {
let compile = |nested: &str| {
reset_class_map();
reset_file_map();
extract(
"test.tsx",
&format!("import {{ css, keyframes }} from '@devup-ui/react'; const spin = keyframes({{ from: {{ opacity: 0 }}, to: {{ opacity: 1 }} }}); const base = css({{ color: 'hotpink' }}); {binding} export const hover = css({{ styleOrder: 3, '@layer': {{ base: {{ '&:hover,&:focus': {nested} }} }} }});"),
ExtractOption::default(),
).unwrap()
};
let expected = compile(expanded);
let actual = compile(value);
assert_eq!(actual.styles, expected.styles);
assert_eq!(actual.code, expected.code);
}

#[test]
#[serial]
fn test_css_nested_inline_without_binding() {
let compile = |source| {
reset_class_map();
reset_file_map();
extract("test.tsx", source, ExtractOption::default()).unwrap()
};
let expected = compile(
"import { css } from '@devup-ui/react'; css({ color: 'hotpink' }); export const hover = css({ '&:hover': { color: 'hotpink' }, '&:focus': { color: 'hotpink' } });",
);
let actual = compile(
"import { css } from '@devup-ui/react'; export const hover = css({ '&:hover,&:focus': css({ color: 'hotpink' }) });",
);
assert_eq!(actual.styles, expected.styles);
assert_eq!(
actual.code.split("export const").last(),
expected.code.split("export const").last()
);
}

#[test]
#[serial]
fn test_keyframes_no_args() {
Expand Down
Loading
Loading