From 18affda086173a2f51fca040ee07746440b04425 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 22:51:25 +0900 Subject: [PATCH 1/8] Fix typo --- libs/sheet/src/theme.rs | 393 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 383 insertions(+), 10 deletions(-) diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 32058406..295b61a9 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -176,27 +176,171 @@ impl Typography { } #[derive(Serialize, Debug)] -pub struct Typographies(Vec>); +pub struct Typographies(pub Vec>); impl From>> for Typographies { fn from(v: Vec>) -> Self { Self(v) } } + +/// Helper to deserialize a typography property that can be either a single value or an array +fn deserialize_typo_prop(value: &Value) -> Result>, String> { + match value { + Value::Null => Ok(vec![None]), + Value::String(s) => Ok(vec![Some(s.clone())]), + Value::Number(n) => Ok(vec![Some(n.to_string())]), + Value::Array(arr) => { + let mut result = Vec::with_capacity(arr.len()); + for item in arr { + match item { + Value::Null => result.push(None), + Value::String(s) => result.push(Some(s.clone())), + Value::Number(n) => result.push(Some(n.to_string())), + _ => return Err(format!("Invalid typography property value: {:?}", item)), + } + } + Ok(result) + } + _ => Err(format!("Invalid typography property value: {:?}", value)), + } +} + impl<'de> Deserialize<'de> for Typographies { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - #[derive(Deserialize)] - #[serde(untagged)] - enum ArrayOrSingle { - Array(Vec>), - Single(Typography), - } - match ArrayOrSingle::deserialize(deserializer)? { - ArrayOrSingle::Array(v) => Ok(Self(v)), - ArrayOrSingle::Single(v) => Ok(Self(vec![Some(v)])), + use serde::de::Error; + + let value = Value::deserialize(deserializer)?; + + match &value { + // Traditional array format: [{ fontFamily: "Arial", ... }, null, { ... }] + Value::Array(arr) => { + // Check if this looks like a traditional typography array (array of objects/nulls) + // vs a compact format that accidentally starts with an array + let is_traditional = arr.iter().all(|v| v.is_object() || v.is_null()); + if is_traditional { + let mut result = Vec::with_capacity(arr.len()); + for item in arr { + match item { + Value::Null => result.push(None), + Value::Object(_) => { + let typo: Typography = + serde_json::from_value(item.clone()).map_err(D::Error::custom)?; + result.push(Some(typo)); + } + _ => { + return Err(D::Error::custom(format!( + "Typography array must contain objects or null, got: {:?}", + item + ))) + } + } + } + Ok(Self(result)) + } else { + // Top-level array that's not a traditional format is an error + Err(D::Error::custom( + "Typography value cannot start with an array. Use object format with property-level arrays instead.", + )) + } + } + // Compact object format: { fontFamily: "Arial", fontSize: ["16px", null, "20px"], ... } + Value::Object(obj) => { + // Extract each property, which can be a single value or an array + let font_family = obj + .get("fontFamily") + .map(deserialize_typo_prop) + .transpose() + .map_err(D::Error::custom)? + .unwrap_or_else(|| vec![None]); + + let font_size = obj + .get("fontSize") + .map(deserialize_typo_prop) + .transpose() + .map_err(D::Error::custom)? + .unwrap_or_else(|| vec![None]); + + let font_weight = obj + .get("fontWeight") + .map(deserialize_typo_prop) + .transpose() + .map_err(D::Error::custom)? + .unwrap_or_else(|| vec![None]); + + let line_height = obj + .get("lineHeight") + .map(deserialize_typo_prop) + .transpose() + .map_err(D::Error::custom)? + .unwrap_or_else(|| vec![None]); + + let letter_spacing = obj + .get("letterSpacing") + .map(deserialize_typo_prop) + .transpose() + .map_err(D::Error::custom)? + .unwrap_or_else(|| vec![None]); + + let font_style = obj + .get("fontStyle") + .map(deserialize_typo_prop) + .transpose() + .map_err(D::Error::custom)? + .unwrap_or_else(|| vec![None]); + + // Find the maximum length among all properties + let max_len = [ + font_family.len(), + font_size.len(), + font_weight.len(), + line_height.len(), + letter_spacing.len(), + font_style.len(), + ] + .into_iter() + .max() + .unwrap_or(1); + + // Build typography for each breakpoint level + let mut result = Vec::with_capacity(max_len); + for i in 0..max_len { + let ff = font_family.get(i).cloned().unwrap_or(None); + let fs = font_size.get(i).cloned().unwrap_or(None); + let fw = font_weight.get(i).cloned().unwrap_or(None); + let lh = line_height.get(i).cloned().unwrap_or(None); + let ls = letter_spacing.get(i).cloned().unwrap_or(None); + let fst = font_style.get(i).cloned().unwrap_or(None); + + // If all properties are None for this level, push None + if ff.is_none() + && fs.is_none() + && fw.is_none() + && lh.is_none() + && ls.is_none() + && fst.is_none() + { + result.push(None); + } else { + result.push(Some(Typography { + font_family: ff, + font_size: fs, + font_weight: fw, + line_height: lh, + letter_spacing: ls, + })); + } + } + + Ok(Self(result)) + } + _ => Err(D::Error::custom(format!( + "Typography must be an object or array, got: {:?}", + value + ))), } } } @@ -794,4 +938,233 @@ mod tests { // CSS key uses dashes assert!(css_keys.contains(&"a-b-c".to_string())); } + + #[test] + fn test_compact_typography_format() { + // Test new compact format with property-level arrays + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "fontFamily": "Pretendard", + "fontStyle": "normal", + "fontWeight": 800, + "fontSize": ["38px", null, null, null, "52px"], + "lineHeight": 1.3, + "letterSpacing": "-0.03em" + } + } + }"##, + ) + .unwrap(); + + let h1 = theme.typography.get("h1").unwrap(); + assert_eq!(h1.0.len(), 5); + + // First breakpoint + let first = h1.0[0].as_ref().unwrap(); + assert_eq!(first.font_family, Some("Pretendard".to_string())); + assert_eq!(first.font_size, Some("38px".to_string())); + assert_eq!(first.font_weight, Some("800".to_string())); + assert_eq!(first.line_height, Some("1.3".to_string())); + assert_eq!(first.letter_spacing, Some("-0.03em".to_string())); + + // Middle breakpoints should be None (all properties are single values except fontSize) + assert!(h1.0[1].is_none()); + assert!(h1.0[2].is_none()); + assert!(h1.0[3].is_none()); + + // Last breakpoint (only fontSize changes) + let last = h1.0[4].as_ref().unwrap(); + assert_eq!(last.font_size, Some("52px".to_string())); + } + + #[test] + fn test_compact_typography_all_arrays() { + // Test compact format where multiple properties have arrays + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "body": { + "fontFamily": "Pretendard", + "fontSize": ["14px", null, "16px"], + "fontWeight": [500, null, 600], + "lineHeight": [1.3, null, 1.5] + } + } + }"##, + ) + .unwrap(); + + let body = theme.typography.get("body").unwrap(); + assert_eq!(body.0.len(), 3); + + // First breakpoint + let first = body.0[0].as_ref().unwrap(); + assert_eq!(first.font_family, Some("Pretendard".to_string())); + assert_eq!(first.font_size, Some("14px".to_string())); + assert_eq!(first.font_weight, Some("500".to_string())); + assert_eq!(first.line_height, Some("1.3".to_string())); + + // Middle is None + assert!(body.0[1].is_none()); + + // Third breakpoint + let third = body.0[2].as_ref().unwrap(); + assert_eq!(third.font_size, Some("16px".to_string())); + assert_eq!(third.font_weight, Some("600".to_string())); + assert_eq!(third.line_height, Some("1.5".to_string())); + } + + #[test] + fn test_compact_typography_single_value() { + // Test compact format with all single values (no arrays) + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "caption": { + "fontFamily": "Pretendard", + "fontStyle": "normal", + "fontWeight": 500, + "fontSize": "14px", + "lineHeight": 1.4, + "letterSpacing": "-0.03em" + } + } + }"##, + ) + .unwrap(); + + let caption = theme.typography.get("caption").unwrap(); + assert_eq!(caption.0.len(), 1); + + let first = caption.0[0].as_ref().unwrap(); + assert_eq!(first.font_family, Some("Pretendard".to_string())); + assert_eq!(first.font_size, Some("14px".to_string())); + assert_eq!(first.font_weight, Some("500".to_string())); + assert_eq!(first.line_height, Some("1.4".to_string())); + assert_eq!(first.letter_spacing, Some("-0.03em".to_string())); + } + + #[test] + fn test_traditional_typography_array_still_works() { + // Ensure backward compatibility with traditional array format + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": [ + { + "fontFamily": "Pretendard", + "fontWeight": 800, + "fontSize": "38px", + "lineHeight": 1.3 + }, + null, + null, + null, + { + "fontFamily": "Pretendard", + "fontWeight": 800, + "fontSize": "52px", + "lineHeight": 1.3 + } + ] + } + }"##, + ) + .unwrap(); + + let h1 = theme.typography.get("h1").unwrap(); + assert_eq!(h1.0.len(), 5); + + let first = h1.0[0].as_ref().unwrap(); + assert_eq!(first.font_size, Some("38px".to_string())); + + assert!(h1.0[1].is_none()); + assert!(h1.0[2].is_none()); + assert!(h1.0[3].is_none()); + + let last = h1.0[4].as_ref().unwrap(); + assert_eq!(last.font_size, Some("52px".to_string())); + } + + #[test] + fn test_compact_typography_css_output() { + // Verify CSS output is correct for compact format + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "fontFamily": "Pretendard", + "fontSize": ["38px", null, null, null, "52px"], + "fontWeight": 800, + "lineHeight": 1.3 + } + } + }"##, + ) + .unwrap(); + + let css = theme.to_css(); + // Should have base style + assert!(css.contains(".typo-h1{")); + assert!(css.contains("font-family:Pretendard")); + assert!(css.contains("font-size:38px")); + assert!(css.contains("font-weight:800")); + // Should have media query for breakpoint 4 (1280px) + assert!(css.contains("@media(min-width:1280px)")); + assert!(css.contains("font-size:52px")); + } + + #[test] + fn test_invalid_top_level_array_should_fail() { + // Top-level array that's not traditional format should fail + let result: Result = serde_json::from_str( + r##"{ + "typography": { + "h1": ["38px", null, "52px"] + } + }"##, + ); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("cannot start with an array")); + } + + #[test] + fn test_mixed_typography_formats() { + // Test that both formats can coexist in the same theme + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": [ + { "fontFamily": "Pretendard", "fontSize": "38px" }, + null, + { "fontFamily": "Pretendard", "fontSize": "52px" } + ], + "body": { + "fontFamily": "Pretendard", + "fontSize": ["14px", null, "16px"] + } + } + }"##, + ) + .unwrap(); + + // Traditional format + let h1 = theme.typography.get("h1").unwrap(); + assert_eq!(h1.0.len(), 3); + assert_eq!( + h1.0[0].as_ref().unwrap().font_size, + Some("38px".to_string()) + ); + + // Compact format + let body = theme.typography.get("body").unwrap(); + assert_eq!(body.0.len(), 3); + assert_eq!( + body.0[0].as_ref().unwrap().font_size, + Some("14px".to_string()) + ); + } } From 253e68835cb3bf1fc55484a5a4bb8412de90806d Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 22:51:50 +0900 Subject: [PATCH 2/8] Fix responsive --- .changepacks/changepack_log_F-ZmudGLGxayXj02N3gBK.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changepacks/changepack_log_F-ZmudGLGxayXj02N3gBK.json diff --git a/.changepacks/changepack_log_F-ZmudGLGxayXj02N3gBK.json b/.changepacks/changepack_log_F-ZmudGLGxayXj02N3gBK.json new file mode 100644 index 00000000..69ef4f0a --- /dev/null +++ b/.changepacks/changepack_log_F-ZmudGLGxayXj02N3gBK.json @@ -0,0 +1,5 @@ +{ + "changes": { "bindings/devup-ui-wasm/package.json": "Patch" }, + "note": "Support responsive", + "date": "2025-12-26T13:51:45.007993200Z" +} From ab464b0241acc5aab6e5ba35931c35569acccb78 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 22:54:28 +0900 Subject: [PATCH 3/8] Fix responsive --- libs/sheet/src/theme.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 295b61a9..c862c15a 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -227,15 +227,15 @@ impl<'de> Deserialize<'de> for Typographies { match item { Value::Null => result.push(None), Value::Object(_) => { - let typo: Typography = - serde_json::from_value(item.clone()).map_err(D::Error::custom)?; + let typo: Typography = serde_json::from_value(item.clone()) + .map_err(D::Error::custom)?; result.push(Some(typo)); } _ => { return Err(D::Error::custom(format!( "Typography array must contain objects or null, got: {:?}", item - ))) + ))); } } } From 8c8323b5cd460f39926a1c4262a89a27671fdca4 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 23:19:38 +0900 Subject: [PATCH 4/8] Add testcase --- libs/sheet/src/theme.rs | 279 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index c862c15a..8f773be2 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -1167,4 +1167,283 @@ mod tests { Some("14px".to_string()) ); } + + #[test] + fn test_deserialize_typo_prop_null_value() { + // Test compact format with null values in arrays + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "fontFamily": null, + "fontSize": ["14px", null, "16px"] + } + } + }"##, + ) + .unwrap(); + + let h1 = theme.typography.get("h1").unwrap(); + assert_eq!(h1.0.len(), 3); + // fontFamily is null at all levels + assert!(h1.0[0].as_ref().unwrap().font_family.is_none()); + assert_eq!( + h1.0[0].as_ref().unwrap().font_size, + Some("14px".to_string()) + ); + } + + #[test] + fn test_deserialize_typo_prop_invalid_array_value() { + // Test that invalid values in typography arrays fail + let result: Result = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "fontSize": ["14px", {"invalid": "object"}, "16px"] + } + } + }"##, + ); + assert!(result.is_err()); + } + + #[test] + fn test_deserialize_typo_prop_invalid_single_value() { + // Test that invalid single value fails + let result: Result = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "fontSize": true + } + } + }"##, + ); + assert!(result.is_err()); + } + + #[test] + fn test_typography_invalid_type() { + // Test that typography with invalid type (string) fails + let result: Result = serde_json::from_str( + r##"{ + "typography": { + "h1": "invalid string" + } + }"##, + ); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("must be an object or array")); + } + + #[test] + fn test_get_default_theme_priority() { + fn make_color_theme() -> ColorTheme { + let mut ct = ColorTheme::default(); + ct.add_color("primary", "#000"); + ct + } + + // Test "default" theme has highest priority + let mut theme = Theme::default(); + theme.add_color_theme("default", make_color_theme()); + theme.add_color_theme("light", make_color_theme()); + theme.add_color_theme("dark", make_color_theme()); + assert_eq!(theme.get_default_theme(), Some("default".to_string())); + + // Test "light" theme has second priority when "default" is absent + let mut theme = Theme::default(); + theme.add_color_theme("light", make_color_theme()); + theme.add_color_theme("dark", make_color_theme()); + theme.add_color_theme("custom", make_color_theme()); + assert_eq!(theme.get_default_theme(), Some("light".to_string())); + + // Test first theme when neither "default" nor "light" exists + let mut theme = Theme::default(); + theme.add_color_theme("dark", make_color_theme()); + theme.add_color_theme("custom", make_color_theme()); + // BTreeMap returns keys in alphabetical order, so "custom" comes first + assert_eq!(theme.get_default_theme(), Some("custom".to_string())); + + // Test None when no color themes exist + let theme = Theme::default(); + assert_eq!(theme.get_default_theme(), None); + } + + #[test] + fn test_css_entries_iterator() { + let mut color_theme = ColorTheme::default(); + color_theme.add_color("primary", "#000"); + color_theme.add_color("secondary.100", "#111"); + color_theme.add_color("gray.200", "#222"); + + let entries: Vec<_> = color_theme.css_entries().collect(); + assert_eq!(entries.len(), 3); + + // Verify we can find all entries + assert!(entries.iter().any(|(k, v)| *k == "primary" && *v == "#000")); + assert!( + entries + .iter() + .any(|(k, v)| *k == "secondary-100" && *v == "#111") + ); + assert!( + entries + .iter() + .any(|(k, v)| *k == "gray-200" && *v == "#222") + ); + } + + #[test] + fn test_typography_empty_properties_all_none() { + // Test that empty compact format with no properties creates None + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "empty": {} + } + }"##, + ) + .unwrap(); + + let empty = theme.typography.get("empty").unwrap(); + assert_eq!(empty.0.len(), 1); + assert!(empty.0[0].is_none()); + } + + #[test] + fn test_typography_with_only_letter_spacing() { + // Test typography with only letterSpacing property + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "letterSpacing": ["-0.02em", null, "-0.03em"] + } + } + }"##, + ) + .unwrap(); + + let h1 = theme.typography.get("h1").unwrap(); + assert_eq!(h1.0.len(), 3); + assert_eq!( + h1.0[0].as_ref().unwrap().letter_spacing, + Some("-0.02em".to_string()) + ); + assert!(h1.0[1].is_none()); + assert_eq!( + h1.0[2].as_ref().unwrap().letter_spacing, + Some("-0.03em".to_string()) + ); + } + + #[test] + fn test_color_theme_empty() { + let color_theme = ColorTheme::default(); + assert_eq!(color_theme.css_keys().count(), 0); + assert_eq!(color_theme.interface_keys().count(), 0); + assert_eq!(color_theme.css_entries().count(), 0); + assert!(!color_theme.contains_key("any")); + assert!(color_theme.get("any").is_none()); + } + + #[test] + fn test_traditional_typography_with_invalid_item() { + // Test that traditional array with invalid item (not object/null) fails + let result: Result = serde_json::from_str( + r##"{ + "typography": { + "h1": [ + { "fontFamily": "Arial" }, + "invalid string item", + null + ] + } + }"##, + ); + // This should fail because "invalid string item" is not null or object + // But the current implementation detects this as non-traditional and fails differently + assert!(result.is_err()); + } + + #[test] + fn test_compact_typography_different_array_lengths() { + // Test when different properties have different array lengths + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "fontSize": ["14px", "16px"], + "fontWeight": ["400", "500", "600", "700"] + } + } + }"##, + ) + .unwrap(); + + let h1 = theme.typography.get("h1").unwrap(); + // Should use max length (4) + assert_eq!(h1.0.len(), 4); + + // First two should have both properties + assert_eq!( + h1.0[0].as_ref().unwrap().font_size, + Some("14px".to_string()) + ); + assert_eq!( + h1.0[0].as_ref().unwrap().font_weight, + Some("400".to_string()) + ); + + assert_eq!( + h1.0[1].as_ref().unwrap().font_size, + Some("16px".to_string()) + ); + assert_eq!( + h1.0[1].as_ref().unwrap().font_weight, + Some("500".to_string()) + ); + + // Last two should only have fontWeight (fontSize array is shorter) + assert!(h1.0[2].as_ref().unwrap().font_size.is_none()); + assert_eq!( + h1.0[2].as_ref().unwrap().font_weight, + Some("600".to_string()) + ); + + assert!(h1.0[3].as_ref().unwrap().font_size.is_none()); + assert_eq!( + h1.0[3].as_ref().unwrap().font_weight, + Some("700".to_string()) + ); + } + + #[test] + fn test_typography_float_values() { + // Test that float values are properly converted + let theme: Theme = serde_json::from_str( + r##"{ + "typography": { + "h1": { + "lineHeight": [1.2, 1.5, 1.8], + "fontWeight": [400.5, 500, 600] + } + } + }"##, + ) + .unwrap(); + + let h1 = theme.typography.get("h1").unwrap(); + assert_eq!( + h1.0[0].as_ref().unwrap().line_height, + Some("1.2".to_string()) + ); + assert_eq!( + h1.0[0].as_ref().unwrap().font_weight, + Some("400.5".to_string()) + ); + } } From 5a6b9c44b4b474504b6ac4b13d80bf58f049ccee Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 26 Dec 2025 23:44:54 +0900 Subject: [PATCH 5/8] Refactor --- libs/extractor/src/utils.rs | 11 +--------- libs/sheet/src/theme.rs | 40 ++++++++++++++----------------------- 2 files changed, 16 insertions(+), 35 deletions(-) diff --git a/libs/extractor/src/utils.rs b/libs/extractor/src/utils.rs index 8b9a805f..0a38bf2a 100644 --- a/libs/extractor/src/utils.rs +++ b/libs/extractor/src/utils.rs @@ -197,16 +197,7 @@ pub(super) fn wrap_direct_call<'a>( expr: &Expression<'a>, args: &[Expression<'a>], ) -> Expression<'a> { - builder.expression_call::>>>( - SPAN, - expr.clone_in(builder.allocator), - None, - oxc_allocator::Vec::from_iter_in( - args.iter().map(|e| e.clone_in(builder.allocator).into()), - builder.allocator, - ), - false, - ) + builder.expression_call::>>>(SPAN, expr.clone_in(builder.allocator), None, oxc_allocator::Vec::from_iter_in(args.iter().map(|e| e.clone_in(builder.allocator).into()), builder.allocator), false) } /// merge expressions to object expression pub(super) fn merge_object_expressions<'a>( diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 8f773be2..a70c8f18 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -218,34 +218,24 @@ impl<'de> Deserialize<'de> for Typographies { match &value { // Traditional array format: [{ fontFamily: "Arial", ... }, null, { ... }] Value::Array(arr) => { - // Check if this looks like a traditional typography array (array of objects/nulls) - // vs a compact format that accidentally starts with an array - let is_traditional = arr.iter().all(|v| v.is_object() || v.is_null()); - if is_traditional { - let mut result = Vec::with_capacity(arr.len()); - for item in arr { - match item { - Value::Null => result.push(None), - Value::Object(_) => { - let typo: Typography = serde_json::from_value(item.clone()) - .map_err(D::Error::custom)?; - result.push(Some(typo)); - } - _ => { - return Err(D::Error::custom(format!( - "Typography array must contain objects or null, got: {:?}", - item - ))); - } + let mut result = Vec::with_capacity(arr.len()); + for item in arr { + match item { + Value::Null => result.push(None), + Value::Object(_) => { + let typo: Typography = + serde_json::from_value(item.clone()).map_err(D::Error::custom)?; + result.push(Some(typo)); + } + // Non-object/null values mean this is not a valid traditional array format + _ => { + return Err(D::Error::custom( + "Typography value cannot start with an array. Use object format with property-level arrays instead.", + )); } } - Ok(Self(result)) - } else { - // Top-level array that's not a traditional format is an error - Err(D::Error::custom( - "Typography value cannot start with an array. Use object format with property-level arrays instead.", - )) } + Ok(Self(result)) } // Compact object format: { fontFamily: "Arial", fontSize: ["16px", null, "20px"], ... } Value::Object(obj) => { From a7d1bcb2bcbd9e6d40aca22a7648d21d0960e895 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:03:32 +0900 Subject: [PATCH 6/8] Add wrong case --- .../extractor/extract_style_from_styled.rs | 81 +++++++++-------- libs/sheet/src/theme.rs | 88 +++++++++++++++++++ 2 files changed, 132 insertions(+), 37 deletions(-) diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index 7872abfa..6873e140 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -165,14 +165,15 @@ fn create_styled_component<'a>( SPAN, FormalParameterKind::ArrowFormalParameters, oxc_allocator::Vec::from_iter_in( - vec![ast_builder.formal_parameter( - SPAN, - oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), - ast_builder.binding_pattern( - ast_builder.binding_pattern_kind_object_pattern( - SPAN, - oxc_allocator::Vec::from_iter_in( - vec![ + vec![ + ast_builder.formal_parameter( + SPAN, + oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), + ast_builder.binding_pattern( + ast_builder.binding_pattern_kind_object_pattern( + SPAN, + oxc_allocator::Vec::from_iter_in( + vec![ ast_builder.binding_property( SPAN, ast_builder.property_key_static_identifier(SPAN, "style"), @@ -210,27 +211,28 @@ fn create_styled_component<'a>( false, ), ], - ast_builder.allocator, - ), - Some(ast_builder.binding_rest_element( - SPAN, - ast_builder.binding_pattern( - ast_builder.binding_pattern_kind_binding_identifier( - SPAN, - ast_builder.atom("rest"), - ), - None::>>, - false, + ast_builder.allocator, ), - )), + Some(ast_builder.binding_rest_element( + SPAN, + ast_builder.binding_pattern( + ast_builder.binding_pattern_kind_binding_identifier( + SPAN, + ast_builder.atom("rest"), + ), + None::>>, + false, + ), + )), + ), + None::>>, + false, ), - None::>>, + None, + false, false, ), - None, - false, - false, - )], + ], ast_builder.allocator, ), None::>>, @@ -239,16 +241,20 @@ fn create_styled_component<'a>( SPAN, oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), oxc_allocator::Vec::from_iter_in( - vec![ast_builder.statement_expression( - SPAN, - ast_builder.expression_jsx_element( + vec![ + ast_builder.statement_expression( SPAN, - ast_builder.alloc_jsx_opening_element( + ast_builder.expression_jsx_element( SPAN, - ast_builder.jsx_element_name_identifier(SPAN, ast_builder.atom(tag_name)), - None::>>, - oxc_allocator::Vec::from_iter_in( - vec![ + ast_builder.alloc_jsx_opening_element( + SPAN, + ast_builder + .jsx_element_name_identifier(SPAN, ast_builder.atom(tag_name)), + None::< + oxc_allocator::Box>, + >, + oxc_allocator::Vec::from_iter_in( + vec![ ast_builder.jsx_attribute_item_spread_attribute( SPAN, ast_builder @@ -327,13 +333,14 @@ fn create_styled_component<'a>( ), ), ], - ast_builder.allocator, + ast_builder.allocator, + ), ), + oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), + None::>>, ), - oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), - None::>>, ), - )], + ], ast_builder.allocator, ), ); diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index a70c8f18..1684d73b 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -1436,4 +1436,92 @@ mod tests { Some("400.5".to_string()) ); } + + #[test] + fn test_typographies_direct_traditional_array_deserialize() { + // Directly deserialize Typographies to ensure Value::Object branch is covered (line 183) + let typographies: Typographies = serde_json::from_str( + r##"[ + { "fontFamily": "Arial", "fontSize": "16px" }, + null, + { "fontFamily": "Helvetica", "fontSize": "18px" } + ]"##, + ) + .unwrap(); + + assert_eq!(typographies.0.len(), 3); + assert_eq!( + typographies.0[0].as_ref().unwrap().font_family, + Some("Arial".to_string()) + ); + assert!(typographies.0[1].is_none()); + assert_eq!( + typographies.0[2].as_ref().unwrap().font_family, + Some("Helvetica".to_string()) + ); + } + + #[test] + fn test_typographies_direct_invalid_array_item() { + // Directly deserialize Typographies with invalid array item to cover line 188 + let result: Result = serde_json::from_str( + r##"[ + { "fontFamily": "Arial" }, + "invalid string", + null + ]"##, + ); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("cannot start with an array")); + } + + #[test] + fn test_typographies_direct_number_in_array() { + // Test with number in traditional array to ensure error branch is hit + let result: Result = serde_json::from_str( + r##"[ + { "fontFamily": "Arial" }, + 123, + null + ]"##, + ); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("cannot start with an array")); + } + + #[test] + fn test_typographies_direct_bool_in_array() { + // Test with boolean in traditional array + let result: Result = serde_json::from_str( + r##"[ + null, + { "fontFamily": "Arial" }, + true + ]"##, + ); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("cannot start with an array")); + } + + #[test] + fn test_typographies_direct_nested_array_in_array() { + // Test with nested array in traditional array + let result: Result = serde_json::from_str( + r##"[ + { "fontFamily": "Arial" }, + ["nested", "array"], + null + ]"##, + ); + + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("cannot start with an array")); + } } From b2a6632ed6ccfaec41c3c04dce71b5f18f4b9a79 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:18:45 +0900 Subject: [PATCH 7/8] Fix lint --- .../extractor/extract_style_from_styled.rs | 81 +++++++++---------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index 6873e140..7872abfa 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -165,15 +165,14 @@ fn create_styled_component<'a>( SPAN, FormalParameterKind::ArrowFormalParameters, oxc_allocator::Vec::from_iter_in( - vec![ - ast_builder.formal_parameter( - SPAN, - oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), - ast_builder.binding_pattern( - ast_builder.binding_pattern_kind_object_pattern( - SPAN, - oxc_allocator::Vec::from_iter_in( - vec![ + vec![ast_builder.formal_parameter( + SPAN, + oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), + ast_builder.binding_pattern( + ast_builder.binding_pattern_kind_object_pattern( + SPAN, + oxc_allocator::Vec::from_iter_in( + vec![ ast_builder.binding_property( SPAN, ast_builder.property_key_static_identifier(SPAN, "style"), @@ -211,28 +210,27 @@ fn create_styled_component<'a>( false, ), ], - ast_builder.allocator, - ), - Some(ast_builder.binding_rest_element( - SPAN, - ast_builder.binding_pattern( - ast_builder.binding_pattern_kind_binding_identifier( - SPAN, - ast_builder.atom("rest"), - ), - None::>>, - false, - ), - )), + ast_builder.allocator, ), - None::>>, - false, + Some(ast_builder.binding_rest_element( + SPAN, + ast_builder.binding_pattern( + ast_builder.binding_pattern_kind_binding_identifier( + SPAN, + ast_builder.atom("rest"), + ), + None::>>, + false, + ), + )), ), - None, - false, + None::>>, false, ), - ], + None, + false, + false, + )], ast_builder.allocator, ), None::>>, @@ -241,20 +239,16 @@ fn create_styled_component<'a>( SPAN, oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), oxc_allocator::Vec::from_iter_in( - vec![ - ast_builder.statement_expression( + vec![ast_builder.statement_expression( + SPAN, + ast_builder.expression_jsx_element( SPAN, - ast_builder.expression_jsx_element( + ast_builder.alloc_jsx_opening_element( SPAN, - ast_builder.alloc_jsx_opening_element( - SPAN, - ast_builder - .jsx_element_name_identifier(SPAN, ast_builder.atom(tag_name)), - None::< - oxc_allocator::Box>, - >, - oxc_allocator::Vec::from_iter_in( - vec![ + ast_builder.jsx_element_name_identifier(SPAN, ast_builder.atom(tag_name)), + None::>>, + oxc_allocator::Vec::from_iter_in( + vec![ ast_builder.jsx_attribute_item_spread_attribute( SPAN, ast_builder @@ -333,14 +327,13 @@ fn create_styled_component<'a>( ), ), ], - ast_builder.allocator, - ), + ast_builder.allocator, ), - oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), - None::>>, ), + oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), + None::>>, ), - ], + )], ast_builder.allocator, ), ); From ac9c7ccbe216686ccb7e56f252e99a48664cf795 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 27 Dec 2025 00:31:30 +0900 Subject: [PATCH 8/8] Fix case --- libs/sheet/src/theme.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/libs/sheet/src/theme.rs b/libs/sheet/src/theme.rs index 1684d73b..37e34bb8 100644 --- a/libs/sheet/src/theme.rs +++ b/libs/sheet/src/theme.rs @@ -220,19 +220,17 @@ impl<'de> Deserialize<'de> for Typographies { Value::Array(arr) => { let mut result = Vec::with_capacity(arr.len()); for item in arr { - match item { - Value::Null => result.push(None), - Value::Object(_) => { - let typo: Typography = - serde_json::from_value(item.clone()).map_err(D::Error::custom)?; - result.push(Some(typo)); - } + if item.is_null() { + result.push(None); + } else if item.is_object() { + let typo: Typography = + serde_json::from_value(item.clone()).map_err(D::Error::custom)?; + result.push(Some(typo)); + } else { // Non-object/null values mean this is not a valid traditional array format - _ => { - return Err(D::Error::custom( - "Typography value cannot start with an array. Use object format with property-level arrays instead.", - )); - } + return Err(D::Error::custom( + "Typography value cannot start with an array. Use object format with property-level arrays instead.", + )); } } Ok(Self(result))