From 30b45d63ce9fb05084269941b6aefb6025961ebc Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 10 Apr 2026 17:51:19 +0900 Subject: [PATCH] Fix dynamic important issue --- .../changepack_log_MSQAJ5U_6Jnh_f3pbZxur.json | 1 + .../extract_style/extract_dynamic_style.rs | 93 ++++++++++++++++++- libs/extractor/src/lib.rs | 26 ++++++ ...__tests__dynamic_value_with_important.snap | 19 ++++ libs/sheet/src/lib.rs | 69 +++++++++++++- 5 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 .changepacks/changepack_log_MSQAJ5U_6Jnh_f3pbZxur.json create mode 100644 libs/extractor/src/snapshots/extractor__tests__dynamic_value_with_important.snap diff --git a/.changepacks/changepack_log_MSQAJ5U_6Jnh_f3pbZxur.json b/.changepacks/changepack_log_MSQAJ5U_6Jnh_f3pbZxur.json new file mode 100644 index 00000000..5287a828 --- /dev/null +++ b/.changepacks/changepack_log_MSQAJ5U_6Jnh_f3pbZxur.json @@ -0,0 +1 @@ +{"changes":{"bindings/devup-ui-wasm/package.json":"Patch"},"note":"Fix dynamic important issue","date":"2026-04-10T08:41:58.099091600Z"} \ No newline at end of file diff --git a/libs/extractor/src/extract_style/extract_dynamic_style.rs b/libs/extractor/src/extract_style/extract_dynamic_style.rs index bb896e4f..4fa2a656 100644 --- a/libs/extractor/src/extract_style/extract_dynamic_style.rs +++ b/libs/extractor/src/extract_style/extract_dynamic_style.rs @@ -1,3 +1,5 @@ +use std::fmt::{Debug, Formatter}; + use css::{ optimize_value::optimize_value, sheet_to_classname, sheet_to_variable_name, @@ -6,7 +8,7 @@ use css::{ use crate::extract_style::{ExtractStyleProperty, style_property::StyleProperty}; -#[derive(Debug, PartialEq, Clone, Eq, Hash, Ord, PartialOrd)] +#[derive(PartialEq, Clone, Eq, Hash, Ord, PartialOrd)] pub struct ExtractDynamicStyle { /// property property: String, @@ -18,6 +20,41 @@ pub struct ExtractDynamicStyle { selector: Option, pub(super) style_order: Option, + + /// Whether the value had `!important` that was stripped from the identifier + important: bool, +} + +impl Debug for ExtractDynamicStyle { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut s = f.debug_struct("ExtractDynamicStyle"); + s.field("property", &self.property) + .field("level", &self.level) + .field("identifier", &self.identifier) + .field("selector", &self.selector) + .field("style_order", &self.style_order); + if self.important { + s.field("important", &self.important); + } + s.finish() + } +} + +/// Strip ` !important` from a dynamic style identifier, returning the cleaned +/// identifier and whether `!important` was found. +/// +/// Handles JS expression code produced by `expression_to_code`, where the +/// `!important` text appears before a closing delimiter (backtick, quote) or +/// at the very end of the string. +fn strip_important(identifier: &str) -> (String, bool) { + for str_symbol in ["", "`", "\"", "'"] { + let suffix = format!(" !important{str_symbol}"); + if identifier.ends_with(&suffix) { + let base = &identifier[..identifier.len() - suffix.len()]; + return (format!("{base}{str_symbol}"), true); + } + } + (identifier.to_string(), false) } impl ExtractDynamicStyle { @@ -28,12 +65,15 @@ impl ExtractDynamicStyle { identifier: &str, selector: Option, ) -> Self { + let optimized = optimize_value(identifier); + let (identifier, important) = strip_important(&optimized); Self { property: property.to_string(), level, - identifier: optimize_value(identifier), + identifier, selector: selector.map(optimize_selector), style_order: None, + important, } } @@ -56,6 +96,10 @@ impl ExtractDynamicStyle { pub fn style_order(&self) -> Option { self.style_order } + + pub fn important(&self) -> bool { + self.important + } } impl ExtractStyleProperty for ExtractDynamicStyle { @@ -92,5 +136,50 @@ mod tests { assert_eq!(style.selector(), None); assert_eq!(style.identifier(), "primary"); assert_eq!(style.style_order(), None); + assert!(!style.important()); + } + + #[test] + fn test_strip_important_plain() { + let (id, important) = strip_important("color"); + assert_eq!(id, "color"); + assert!(!important); + } + + #[test] + fn test_strip_important_template_literal() { + // Template literal: `${color} !important` + let (id, important) = strip_important("`${color} !important`"); + assert_eq!(id, "`${color}`"); + assert!(important); + } + + #[test] + fn test_strip_important_double_quote() { + let (id, important) = strip_important("\"red !important\""); + assert_eq!(id, "\"red\""); + assert!(important); + } + + #[test] + fn test_strip_important_single_quote() { + let (id, important) = strip_important("'red !important'"); + assert_eq!(id, "'red'"); + assert!(important); + } + + #[test] + fn test_strip_important_bare() { + let (id, important) = strip_important("something !important"); + assert_eq!(id, "something"); + assert!(important); + } + + #[test] + fn test_dynamic_style_with_important() { + let style = ExtractDynamicStyle::new("background", 0, "`${color} !important`", None); + assert_eq!(style.property(), "background"); + assert_eq!(style.identifier(), "`${color}`"); + assert!(style.important()); } } diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index a584373f..3113e37a 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -10697,6 +10697,32 @@ const margin = 5; )); } + #[test] + #[serial] + fn test_dynamic_value_with_important() { + reset_class_map(); + reset_file_map(); + // !important in a template literal should be stripped from the runtime + // value and placed on the CSS property declaration instead. + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' +const color = "red"; + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false, + import_aliases: HashMap::new() + } + ) + .unwrap() + )); + } + #[test] #[serial] fn test_media_query_selectors() { diff --git a/libs/extractor/src/snapshots/extractor__tests__dynamic_value_with_important.snap b/libs/extractor/src/snapshots/extractor__tests__dynamic_value_with_important.snap new file mode 100644 index 00000000..a1cf67ae --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__dynamic_value_with_important.snap @@ -0,0 +1,19 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {Box} from '@devup-ui/core'\nconst color = \"red\";\n\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())" +--- +ToBTreeSet { + styles: { + Dynamic( + ExtractDynamicStyle { + property: "background", + level: 0, + identifier: "`${color}`", + selector: None, + style_order: None, + important: true, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst color = \"red\";\n
;\n", +} diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index ae42d806..746ed990 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -412,7 +412,11 @@ impl StyleSheet { &class_name, dy.property(), dy.level(), - &format!("var({})", variable_name), + &if dy.important() { + format!("var({}) !important", variable_name) + } else { + format!("var({})", variable_name) + }, dy.selector(), dy.style_order(), if !single_css { Some(filename) } else { None }, @@ -2413,4 +2417,67 @@ mod tests { let css = sheet.create_css(None, false); assert!(css.contains("box-shadow:0 1px 2px #0003")); } + + #[test] + fn test_important_in_css_via_add_property() { + // Verify that !important in the value is preserved in the final CSS output + let mut sheet = StyleSheet::default(); + sheet.add_property( + "test", + "background", + 0, + "var(--a) !important", + None, + None, + None, + ); + let css = sheet.create_css(None, false); + let css_body = css.split("*/").nth(1).unwrap(); + assert!( + css_body.contains("background:var(--a) !important"), + "CSS should contain !important. Got: {css_body}", + ); + } + + #[test] + #[serial] + fn test_dynamic_style_important_full_pipeline() { + // Full pipeline: extract JSX with `${color} !important` → sheet → CSS + // Verifies !important ends up on the CSS property, not in the style attribute + reset_class_map(); + reset_file_map(); + let mut sheet = StyleSheet::default(); + + let output = extract( + "test.tsx", + r#"import {Box} from '@devup-ui/core' +const color = "red"; + +"#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: true, + import_main_css: false, + import_aliases: std::collections::HashMap::new(), + }, + ) + .unwrap(); + + let (collected, _) = sheet.update_styles(&output.styles, "test.tsx", true); + assert!(collected); + + let css = sheet.create_css(None, false); + let css_body = css.split("*/").nth(1).unwrap(); + assert!( + css_body.contains("!important"), + "CSS output should contain !important for dynamic styles. Got: {css_body}", + ); + // Verify the code has clean style value (no !important in the variable) + assert!( + !output.code.contains("!important"), + "Generated code should NOT contain !important in style vars. Got: {}", + output.code, + ); + } }