Skip to content

Commit 30b45d6

Browse files
committed
Fix dynamic important issue
1 parent ad90958 commit 30b45d6

File tree

5 files changed

+205
-3
lines changed

5 files changed

+205
-3
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Fix dynamic important issue","date":"2026-04-10T08:41:58.099091600Z"}

libs/extractor/src/extract_style/extract_dynamic_style.rs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt::{Debug, Formatter};
2+
13
use css::{
24
optimize_value::optimize_value,
35
sheet_to_classname, sheet_to_variable_name,
@@ -6,7 +8,7 @@ use css::{
68

79
use crate::extract_style::{ExtractStyleProperty, style_property::StyleProperty};
810

9-
#[derive(Debug, PartialEq, Clone, Eq, Hash, Ord, PartialOrd)]
11+
#[derive(PartialEq, Clone, Eq, Hash, Ord, PartialOrd)]
1012
pub struct ExtractDynamicStyle {
1113
/// property
1214
property: String,
@@ -18,6 +20,41 @@ pub struct ExtractDynamicStyle {
1820
selector: Option<StyleSelector>,
1921

2022
pub(super) style_order: Option<u8>,
23+
24+
/// Whether the value had `!important` that was stripped from the identifier
25+
important: bool,
26+
}
27+
28+
impl Debug for ExtractDynamicStyle {
29+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
30+
let mut s = f.debug_struct("ExtractDynamicStyle");
31+
s.field("property", &self.property)
32+
.field("level", &self.level)
33+
.field("identifier", &self.identifier)
34+
.field("selector", &self.selector)
35+
.field("style_order", &self.style_order);
36+
if self.important {
37+
s.field("important", &self.important);
38+
}
39+
s.finish()
40+
}
41+
}
42+
43+
/// Strip ` !important` from a dynamic style identifier, returning the cleaned
44+
/// identifier and whether `!important` was found.
45+
///
46+
/// Handles JS expression code produced by `expression_to_code`, where the
47+
/// `!important` text appears before a closing delimiter (backtick, quote) or
48+
/// at the very end of the string.
49+
fn strip_important(identifier: &str) -> (String, bool) {
50+
for str_symbol in ["", "`", "\"", "'"] {
51+
let suffix = format!(" !important{str_symbol}");
52+
if identifier.ends_with(&suffix) {
53+
let base = &identifier[..identifier.len() - suffix.len()];
54+
return (format!("{base}{str_symbol}"), true);
55+
}
56+
}
57+
(identifier.to_string(), false)
2158
}
2259

2360
impl ExtractDynamicStyle {
@@ -28,12 +65,15 @@ impl ExtractDynamicStyle {
2865
identifier: &str,
2966
selector: Option<StyleSelector>,
3067
) -> Self {
68+
let optimized = optimize_value(identifier);
69+
let (identifier, important) = strip_important(&optimized);
3170
Self {
3271
property: property.to_string(),
3372
level,
34-
identifier: optimize_value(identifier),
73+
identifier,
3574
selector: selector.map(optimize_selector),
3675
style_order: None,
76+
important,
3777
}
3878
}
3979

@@ -56,6 +96,10 @@ impl ExtractDynamicStyle {
5696
pub fn style_order(&self) -> Option<u8> {
5797
self.style_order
5898
}
99+
100+
pub fn important(&self) -> bool {
101+
self.important
102+
}
59103
}
60104

61105
impl ExtractStyleProperty for ExtractDynamicStyle {
@@ -92,5 +136,50 @@ mod tests {
92136
assert_eq!(style.selector(), None);
93137
assert_eq!(style.identifier(), "primary");
94138
assert_eq!(style.style_order(), None);
139+
assert!(!style.important());
140+
}
141+
142+
#[test]
143+
fn test_strip_important_plain() {
144+
let (id, important) = strip_important("color");
145+
assert_eq!(id, "color");
146+
assert!(!important);
147+
}
148+
149+
#[test]
150+
fn test_strip_important_template_literal() {
151+
// Template literal: `${color} !important`
152+
let (id, important) = strip_important("`${color} !important`");
153+
assert_eq!(id, "`${color}`");
154+
assert!(important);
155+
}
156+
157+
#[test]
158+
fn test_strip_important_double_quote() {
159+
let (id, important) = strip_important("\"red !important\"");
160+
assert_eq!(id, "\"red\"");
161+
assert!(important);
162+
}
163+
164+
#[test]
165+
fn test_strip_important_single_quote() {
166+
let (id, important) = strip_important("'red !important'");
167+
assert_eq!(id, "'red'");
168+
assert!(important);
169+
}
170+
171+
#[test]
172+
fn test_strip_important_bare() {
173+
let (id, important) = strip_important("something !important");
174+
assert_eq!(id, "something");
175+
assert!(important);
176+
}
177+
178+
#[test]
179+
fn test_dynamic_style_with_important() {
180+
let style = ExtractDynamicStyle::new("background", 0, "`${color} !important`", None);
181+
assert_eq!(style.property(), "background");
182+
assert_eq!(style.identifier(), "`${color}`");
183+
assert!(style.important());
95184
}
96185
}

libs/extractor/src/lib.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10697,6 +10697,32 @@ const margin = 5;
1069710697
));
1069810698
}
1069910699

10700+
#[test]
10701+
#[serial]
10702+
fn test_dynamic_value_with_important() {
10703+
reset_class_map();
10704+
reset_file_map();
10705+
// !important in a template literal should be stripped from the runtime
10706+
// value and placed on the CSS property declaration instead.
10707+
assert_debug_snapshot!(ToBTreeSet::from(
10708+
extract(
10709+
"test.tsx",
10710+
r#"import {Box} from '@devup-ui/core'
10711+
const color = "red";
10712+
<Box bg={`${color} !important`} />
10713+
"#,
10714+
ExtractOption {
10715+
package: "@devup-ui/core".to_string(),
10716+
css_dir: "@devup-ui/core".to_string(),
10717+
single_css: false,
10718+
import_main_css: false,
10719+
import_aliases: HashMap::new()
10720+
}
10721+
)
10722+
.unwrap()
10723+
));
10724+
}
10725+
1070010726
#[test]
1070110727
#[serial]
1070210728
fn test_media_query_selectors() {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
source: libs/extractor/src/lib.rs
3+
expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\nconst color = \"red\";\n<Box bg={`${color} !important`} />\n\"#,\nExtractOption\n{\n package: \"@devup-ui/core\".to_string(), css_dir:\n \"@devup-ui/core\".to_string(), single_css: false, import_main_css: false,\n import_aliases: HashMap::new()\n}).unwrap())"
4+
---
5+
ToBTreeSet {
6+
styles: {
7+
Dynamic(
8+
ExtractDynamicStyle {
9+
property: "background",
10+
level: 0,
11+
identifier: "`${color}`",
12+
selector: None,
13+
style_order: None,
14+
important: true,
15+
},
16+
),
17+
},
18+
code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst color = \"red\";\n<div className=\"a-a\" style={{ \"--a\": `${color}` }} />;\n",
19+
}

libs/sheet/src/lib.rs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,11 @@ impl StyleSheet {
412412
&class_name,
413413
dy.property(),
414414
dy.level(),
415-
&format!("var({})", variable_name),
415+
&if dy.important() {
416+
format!("var({}) !important", variable_name)
417+
} else {
418+
format!("var({})", variable_name)
419+
},
416420
dy.selector(),
417421
dy.style_order(),
418422
if !single_css { Some(filename) } else { None },
@@ -2413,4 +2417,67 @@ mod tests {
24132417
let css = sheet.create_css(None, false);
24142418
assert!(css.contains("box-shadow:0 1px 2px #0003"));
24152419
}
2420+
2421+
#[test]
2422+
fn test_important_in_css_via_add_property() {
2423+
// Verify that !important in the value is preserved in the final CSS output
2424+
let mut sheet = StyleSheet::default();
2425+
sheet.add_property(
2426+
"test",
2427+
"background",
2428+
0,
2429+
"var(--a) !important",
2430+
None,
2431+
None,
2432+
None,
2433+
);
2434+
let css = sheet.create_css(None, false);
2435+
let css_body = css.split("*/").nth(1).unwrap();
2436+
assert!(
2437+
css_body.contains("background:var(--a) !important"),
2438+
"CSS should contain !important. Got: {css_body}",
2439+
);
2440+
}
2441+
2442+
#[test]
2443+
#[serial]
2444+
fn test_dynamic_style_important_full_pipeline() {
2445+
// Full pipeline: extract JSX with `${color} !important` → sheet → CSS
2446+
// Verifies !important ends up on the CSS property, not in the style attribute
2447+
reset_class_map();
2448+
reset_file_map();
2449+
let mut sheet = StyleSheet::default();
2450+
2451+
let output = extract(
2452+
"test.tsx",
2453+
r#"import {Box} from '@devup-ui/core'
2454+
const color = "red";
2455+
<Box bg={`${color} !important`} />
2456+
"#,
2457+
ExtractOption {
2458+
package: "@devup-ui/core".to_string(),
2459+
css_dir: "@devup-ui/core".to_string(),
2460+
single_css: true,
2461+
import_main_css: false,
2462+
import_aliases: std::collections::HashMap::new(),
2463+
},
2464+
)
2465+
.unwrap();
2466+
2467+
let (collected, _) = sheet.update_styles(&output.styles, "test.tsx", true);
2468+
assert!(collected);
2469+
2470+
let css = sheet.create_css(None, false);
2471+
let css_body = css.split("*/").nth(1).unwrap();
2472+
assert!(
2473+
css_body.contains("!important"),
2474+
"CSS output should contain !important for dynamic styles. Got: {css_body}",
2475+
);
2476+
// Verify the code has clean style value (no !important in the variable)
2477+
assert!(
2478+
!output.code.contains("!important"),
2479+
"Generated code should NOT contain !important in style vars. Got: {}",
2480+
output.code,
2481+
);
2482+
}
24162483
}

0 commit comments

Comments
 (0)