From 2d1f8db467762eab415191ecd37f894bd7cbddeb Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 2 Dec 2025 14:42:45 +0900 Subject: [PATCH 01/32] Implement styled --- .../extractor/extract_style_from_styled.rs | 185 +++++++++ libs/extractor/src/extractor/mod.rs | 1 + libs/extractor/src/lib.rs | 355 ++++++++++++++++++ libs/extractor/src/visit.rs | 60 +++ 4 files changed, 601 insertions(+) create mode 100644 libs/extractor/src/extractor/extract_style_from_styled.rs diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs new file mode 100644 index 00000000..ca9c8058 --- /dev/null +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -0,0 +1,185 @@ +use crate::{ + ExtractStyleProp, css_utils::css_to_style, + extract_style::extract_style_value::ExtractStyleValue, extractor::ExtractResult, + extractor::extract_style_from_expression::extract_style_from_expression, + gen_class_name::gen_class_names, +}; +use oxc_allocator::CloneIn; +use oxc_ast::{ + AstBuilder, + ast::{Argument, Expression}, +}; +use oxc_span::SPAN; + +/// Extract styles from styled function calls +/// Handles patterns like: +/// - styled.div`css` +/// - styled("div")`css` +/// - styled("div")({ bg: "red" }) +/// - styled.div({ bg: "red" }) +/// - styled(Component)({ bg: "red" }) +pub fn extract_style_from_styled<'a>( + ast_builder: &AstBuilder<'a>, + expression: &mut Expression<'a>, + styled_name: &str, + split_filename: Option<&str>, +) -> (ExtractResult<'a>, Expression<'a>) { + match expression { + // Case 1: styled.div`css` or styled("div")`css` + Expression::TaggedTemplateExpression(tag) => { + // Check if tag is styled.div or styled(...) + let (tag_name, is_member) = match &tag.tag { + Expression::StaticMemberExpression(member) => { + if let Expression::Identifier(ident) = &member.object { + if ident.name.as_str() == styled_name { + (Some(member.property.name.to_string()), true) + } else { + (None, false) + } + } else { + (None, false) + } + } + Expression::CallExpression(call) => { + if let Expression::Identifier(ident) = &call.callee { + if ident.name.as_str() == styled_name && call.arguments.len() == 1 { + // styled("div") or styled(Component) + if let Argument::StringLiteral(lit) = &call.arguments[0] { + (Some(lit.value.to_string()), false) + } else { + // Component reference - we'll handle this later + (None, false) + } + } else { + (None, false) + } + } else { + (None, false) + } + } + _ => (None, false), + }; + + if tag_name.is_some() || is_member { + // Extract CSS from template literal + let css_str = tag + .quasi + .quasis + .iter() + .map(|quasi| quasi.value.raw.to_string()) + .collect::(); + + let styles = css_to_style(&css_str, 0, &None); + let mut props_styles: Vec> = styles + .iter() + .map(|ex| ExtractStyleProp::Static(ExtractStyleValue::Static(ex.clone()))) + .collect(); + + let class_name = + gen_class_names(ast_builder, &mut props_styles, None, split_filename); + + let result = ExtractResult { + styles: props_styles, + tag: tag_name.map(|name| { + ast_builder.expression_string_literal(SPAN, ast_builder.atom(&name), None) + }), + style_order: None, + style_vars: None, + props: None, + }; + + let new_expr = if let Some(cls) = class_name { + cls + } else { + ast_builder.expression_string_literal(SPAN, ast_builder.atom(""), None) + }; + + return (result, new_expr); + } + } + // Case 2: styled.div({ bg: "red" }) or styled("div")({ bg: "red" }) + Expression::CallExpression(call) => { + // Check if this is a call to styled.div or styled("div") + let (tag_name, is_member) = match &call.callee { + Expression::StaticMemberExpression(member) => { + if let Expression::Identifier(ident) = &member.object { + if ident.name.as_str() == styled_name { + (Some(member.property.name.to_string()), true) + } else { + (None, false) + } + } else { + (None, false) + } + } + Expression::CallExpression(inner_call) => { + if let Expression::Identifier(ident) = &inner_call.callee { + if ident.name.as_str() == styled_name && inner_call.arguments.len() == 1 { + // styled("div") or styled(Component) + if let Argument::StringLiteral(lit) = &inner_call.arguments[0] { + (Some(lit.value.to_string()), false) + } else { + // Component reference + (None, false) + } + } else { + (None, false) + } + } else { + (None, false) + } + } + _ => (None, false), + }; + + if (tag_name.is_some() || is_member) && call.arguments.len() == 1 { + // Extract styles from object expression + let ExtractResult { + mut styles, + style_order, + style_vars, + props, + .. + } = extract_style_from_expression( + ast_builder, + None, + if let Argument::SpreadElement(spread) = &mut call.arguments[0] { + &mut spread.argument + } else { + call.arguments[0].to_expression_mut() + }, + 0, + &None, + ); + + let class_name = + gen_class_names(ast_builder, &mut styles, style_order, split_filename); + + let result = ExtractResult { + styles, + tag: tag_name.map(|name| { + ast_builder.expression_string_literal(SPAN, ast_builder.atom(&name), None) + }), + style_order, + style_vars, + props, + }; + + let new_expr = if let Some(cls) = class_name { + cls + } else { + ast_builder.expression_string_literal(SPAN, ast_builder.atom(""), None) + }; + + return (result, new_expr); + } + } + _ => {} + } + + // Default: no extraction + ( + ExtractResult::default(), + expression.clone_in(ast_builder.allocator), + ) +} diff --git a/libs/extractor/src/extractor/mod.rs b/libs/extractor/src/extractor/mod.rs index e467d099..e97bb3ab 100644 --- a/libs/extractor/src/extractor/mod.rs +++ b/libs/extractor/src/extractor/mod.rs @@ -7,6 +7,7 @@ pub(super) mod extract_keyframes_from_expression; pub(super) mod extract_style_from_expression; pub(super) mod extract_style_from_jsx; pub(super) mod extract_style_from_member_expression; +pub(super) mod extract_style_from_styled; /** * type diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 4d8a11ee..1588b244 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -7563,4 +7563,359 @@ keyframes({ .unwrap() )); } + + #[test] + #[serial] + fn test_styled() { + // Test 1: styled.div`css` + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled} from '@devup-ui/core' + const StyledDiv = styled.div` + background: red; + color: blue; + ` + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test 2: styled("div")`css` + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled} from '@devup-ui/core' + const StyledDiv = styled("div")` + background: red; + color: blue; + ` + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test 3: styled("div")({ bg: "red" }) + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled} from '@devup-ui/core' + const StyledDiv = styled("div")({ bg: "red" }) + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test 4: styled.div({ bg: "red" }) + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled} from '@devup-ui/core' + const StyledDiv = styled.div({ bg: "red" }) + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + // Test 5: styled(Component)({ bg: "red" }) + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled, Box} from '@devup-ui/core' + const StyledComponent = styled(Box)({ bg: "red" }) + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled} from '@devup-ui/core' + const StyledComponent = styled(CustomComponent)({ bg: "red" }) + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + } + + // #[test] + // #[serial] + // fn test_styled_with_variable() { + // // Test 1: styled.div({ bg: "$text" }) + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const StyledDiv = styled.div({ bg: "$text", color: "$primary" }) + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 2: styled("div")({ color: "$primary" }) + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const StyledDiv = styled("div")({ bg: "$text", fontSize: 16 }) + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 3: styled.div`css` + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const StyledDiv = styled.div` + // background: var(--text); + // color: var(--primary); + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 4: styled(Component)({ bg: "$text" }) + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled, Box} from '@devup-ui/core' + // const StyledComponent = styled(Box)({ bg: "$text", _hover: { bg: "$primary" } }) + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 5: styled("div")`css` + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const StyledDiv = styled("div")` + // background-color: var(--text); + // padding: 16px; + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + // } + + // #[test] + // #[serial] + // fn test_styled_with_variable_like_emotion() { + // // Test 1: styled.div`css with ${variable}` - 템플릿 리터럴에서 외부 변수 사용 + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const color = 'red'; + // const StyledDiv = styled.div` + // color: ${color}; + // background: blue; + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 2: styled("div")`css with ${variable}` - 함수 호출 형태에서 외부 변수 사용 + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const primaryColor = 'blue'; + // const padding = '16px'; + // const StyledDiv = styled("div")` + // color: ${primaryColor}; + // padding: ${padding}; + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 3: styled.div`css with ${props => props.bg}` - props를 사용하는 함수형 변수 + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const StyledDiv = styled.div` + // background: ${props => props.bg}; + // color: red; + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 4: styled(Component)`css with ${variable}` - 컴포넌트에 외부 변수 사용 + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled, Box} from '@devup-ui/core' + // const fontSize = '18px'; + // const StyledComponent = styled(Box)` + // font-size: ${fontSize}; + // color: ${props => props.color || 'black'}; + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 5: styled.div`css with multiple ${variables}` - 여러 변수 조합 + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const margin = '10px'; + // const padding = '20px'; + // const StyledDiv = styled.div` + // margin: ${margin}; + // padding: ${padding}; + // background: ${props => props.bg || 'white'}; + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + + // // Test 6: styled.div`css with ${expression}` - 표현식 사용 + // reset_class_map(); + // assert_debug_snapshot!(ToBTreeSet::from( + // extract( + // "test.tsx", + // r#"import {styled} from '@devup-ui/core' + // const isActive = true; + // const StyledDiv = styled.div` + // color: ${isActive ? 'red' : 'blue'}; + // opacity: ${isActive ? 1 : 0.5}; + // ` + // "#, + // ExtractOption { + // package: "@devup-ui/core".to_string(), + // css_dir: "@devup-ui/core".to_string(), + // single_css: false, + // import_main_css: false + // } + // ) + // .unwrap() + // )); + // } } diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs index aaae4d37..735d5031 100644 --- a/libs/extractor/src/visit.rs +++ b/libs/extractor/src/visit.rs @@ -11,6 +11,7 @@ use crate::extractor::{ extract_global_style_from_expression::extract_global_style_from_expression, extract_style_from_expression::extract_style_from_expression, extract_style_from_jsx::extract_style_from_jsx, + extract_style_from_styled::extract_style_from_styled, }; use crate::gen_class_name::gen_class_names; use crate::prop_modify_utils::{modify_prop_object, modify_props}; @@ -52,6 +53,7 @@ pub struct DevupVisitor<'a> { split_filename: Option, pub css_files: Vec, pub styles: HashSet, + styled_import: Option, } impl<'a> DevupVisitor<'a> { @@ -74,6 +76,7 @@ impl<'a> DevupVisitor<'a> { jsx_object: None, util_imports: HashMap::new(), split_filename, + styled_import: None, } } } @@ -130,6 +133,60 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { fn visit_expression(&mut self, it: &mut Expression<'a>) { walk_expression(self, it); + // Handle styled function calls + if let Some(styled_name) = &self.styled_import { + let is_styled = match it { + Expression::TaggedTemplateExpression(tag) => match &tag.tag { + Expression::StaticMemberExpression(member) => { + if let Expression::Identifier(ident) = &member.object { + ident.name.as_str() == styled_name.as_str() + } else { + false + } + } + Expression::CallExpression(call) => { + if let Expression::Identifier(ident) = &call.callee { + ident.name.as_str() == styled_name.as_str() + } else { + false + } + } + _ => false, + }, + Expression::CallExpression(call) => match &call.callee { + Expression::StaticMemberExpression(member) => { + if let Expression::Identifier(ident) = &member.object { + ident.name.as_str() == styled_name.as_str() + } else { + false + } + } + Expression::CallExpression(inner_call) => { + if let Expression::Identifier(ident) = &inner_call.callee { + ident.name.as_str() == styled_name.as_str() + } else { + false + } + } + _ => false, + }, + _ => false, + }; + + if is_styled { + let (result, new_expr) = extract_style_from_styled( + &self.ast, + it, + styled_name, + self.split_filename.as_deref(), + ); + self.styles + .extend(result.styles.into_iter().flat_map(|ex| ex.extract())); + *it = new_expr; + return; + } + } + if let Expression::CallExpression(call) = it { let util_import_key = if let Expression::Identifier(ident) = &call.callee { Some(ident.name.to_string()) @@ -449,6 +506,9 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> { self.util_imports .insert(import.local.to_string(), Rc::new(kind)); specifiers.remove(i); + } else if import.imported.to_string() == "styled" { + self.styled_import = Some(import.local.to_string()); + specifiers.remove(i); } } ImportDeclarationSpecifier::ImportDefaultSpecifier( From f3c7586269402eed8674de5336f4236b7ff6d9d7 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Tue, 2 Dec 2025 19:54:15 +0900 Subject: [PATCH 02/32] Implement styled --- libs/extractor/src/component.rs | 30 +- .../extractor/extract_style_from_styled.rs | 396 +++++++++++++----- libs/extractor/src/lib.rs | 89 +++- libs/extractor/src/prop_modify_utils.rs | 25 +- .../extractor__tests__styled-10.snap | 27 ++ .../snapshots/extractor__tests__styled-2.snap | 27 ++ .../snapshots/extractor__tests__styled-3.snap | 18 + .../snapshots/extractor__tests__styled-4.snap | 18 + .../snapshots/extractor__tests__styled-5.snap | 18 + .../snapshots/extractor__tests__styled-6.snap | 27 ++ .../snapshots/extractor__tests__styled-7.snap | 40 ++ .../snapshots/extractor__tests__styled-8.snap | 49 +++ .../snapshots/extractor__tests__styled-9.snap | 18 + .../snapshots/extractor__tests__styled.snap | 27 ++ ...or__utils__tests__wrap_array_filter_a.snap | 5 + ...__utils__tests__wrap_array_filter_a_b.snap | 5 + ...filter_className_quoteclass-namequote.snap | 5 + libs/extractor/src/utils.rs | 150 ++++++- libs/extractor/src/visit.rs | 15 +- 19 files changed, 839 insertions(+), 150 deletions(-) create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-10.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-2.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-3.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-4.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-5.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-6.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-7.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-8.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled-9.snap create mode 100644 libs/extractor/src/snapshots/extractor__tests__styled.snap create mode 100644 libs/extractor/src/snapshots/extractor__utils__tests__wrap_array_filter_a.snap create mode 100644 libs/extractor/src/snapshots/extractor__utils__tests__wrap_array_filter_a_b.snap create mode 100644 libs/extractor/src/snapshots/extractor__utils__tests__wrap_array_filter_className_quoteclass-namequote.snap diff --git a/libs/extractor/src/component.rs b/libs/extractor/src/component.rs index 201bc561..eeb8f0d1 100644 --- a/libs/extractor/src/component.rs +++ b/libs/extractor/src/component.rs @@ -21,17 +21,17 @@ pub enum ExportVariableKind { impl ExportVariableKind { /// Convert the kind to a tag - pub fn to_tag(&self) -> Result<&str, &str> { + pub fn to_tag(&self) -> &str { match self { ExportVariableKind::Center | ExportVariableKind::VStack | ExportVariableKind::Grid | ExportVariableKind::Flex - | ExportVariableKind::Box => Ok("div"), - ExportVariableKind::Text => Ok("span"), - ExportVariableKind::Image => Ok("img"), - ExportVariableKind::Button => Ok("button"), - ExportVariableKind::Input => Ok("input"), + | ExportVariableKind::Box => "div", + ExportVariableKind::Text => "span", + ExportVariableKind::Image => "img", + ExportVariableKind::Button => "button", + ExportVariableKind::Input => "input", } } } @@ -151,15 +151,15 @@ mod tests { #[test] fn test_to_tag() { - assert_eq!(ExportVariableKind::Box.to_tag(), Ok("div")); - assert_eq!(ExportVariableKind::Text.to_tag(), Ok("span")); - assert_eq!(ExportVariableKind::Image.to_tag(), Ok("img")); - assert_eq!(ExportVariableKind::Button.to_tag(), Ok("button")); - assert_eq!(ExportVariableKind::Input.to_tag(), Ok("input")); - assert_eq!(ExportVariableKind::Flex.to_tag(), Ok("div")); - assert_eq!(ExportVariableKind::VStack.to_tag(), Ok("div")); - assert_eq!(ExportVariableKind::Center.to_tag(), Ok("div")); - assert_eq!(ExportVariableKind::Grid.to_tag(), Ok("div")); + assert_eq!(ExportVariableKind::Box.to_tag(), "div"); + assert_eq!(ExportVariableKind::Text.to_tag(), "span"); + assert_eq!(ExportVariableKind::Image.to_tag(), "img"); + assert_eq!(ExportVariableKind::Button.to_tag(), "button"); + assert_eq!(ExportVariableKind::Input.to_tag(), "input"); + assert_eq!(ExportVariableKind::Flex.to_tag(), "div"); + assert_eq!(ExportVariableKind::VStack.to_tag(), "div"); + assert_eq!(ExportVariableKind::Center.to_tag(), "div"); + assert_eq!(ExportVariableKind::Grid.to_tag(), "div"); } #[test] diff --git a/libs/extractor/src/extractor/extract_style_from_styled.rs b/libs/extractor/src/extractor/extract_style_from_styled.rs index ca9c8058..577ea433 100644 --- a/libs/extractor/src/extractor/extract_style_from_styled.rs +++ b/libs/extractor/src/extractor/extract_style_from_styled.rs @@ -1,16 +1,68 @@ +use std::collections::HashMap; + use crate::{ - ExtractStyleProp, css_utils::css_to_style, - extract_style::extract_style_value::ExtractStyleValue, extractor::ExtractResult, - extractor::extract_style_from_expression::extract_style_from_expression, + ExtractStyleProp, + component::ExportVariableKind, + css_utils::css_to_style, + extract_style::extract_style_value::ExtractStyleValue, + extractor::{ExtractResult, extract_style_from_expression::extract_style_from_expression}, gen_class_name::gen_class_names, + utils::{merge_object_expressions, wrap_array_filter}, }; use oxc_allocator::CloneIn; use oxc_ast::{ AstBuilder, - ast::{Argument, Expression}, + ast::{Argument, Expression, FormalParameterKind}, }; use oxc_span::SPAN; +fn extract_base_tag_and_class_name<'a>( + input: &Expression<'a>, + styled_name: &str, + imports: &HashMap, +) -> (Option, Option>) { + match input { + Expression::StaticMemberExpression(member) => { + if let Expression::Identifier(ident) = &member.object { + if ident.name.as_str() == styled_name { + (Some(member.property.name.to_string()), None) + } else { + (None, None) + } + } else { + (None, None) + } + } + Expression::CallExpression(call) => { + if let Expression::Identifier(ident) = &call.callee { + if ident.name.as_str() == styled_name && call.arguments.len() == 1 { + // styled("div") or styled(Component) + if let Argument::StringLiteral(lit) = &call.arguments[0] { + (Some(lit.value.to_string()), None) + } else if let Argument::Identifier(ident) = &call.arguments[0] { + if let Some(export_variable_kind) = imports.get(ident.name.as_str()) { + ( + Some(export_variable_kind.to_tag().to_string()), + Some(export_variable_kind.extract()), + ) + } else { + (Some(ident.name.to_string()), None) + } + } else { + // Component reference - we'll handle this later + (None, None) + } + } else { + (None, None) + } + } else { + (None, None) + } + } + _ => (None, None), + } +} + /// Extract styles from styled function calls /// Handles patterns like: /// - styled.div`css` @@ -23,44 +75,17 @@ pub fn extract_style_from_styled<'a>( expression: &mut Expression<'a>, styled_name: &str, split_filename: Option<&str>, + imports: &HashMap, ) -> (ExtractResult<'a>, Expression<'a>) { - match expression { + println!("-----------"); + let (result, new_expr) = match expression { // Case 1: styled.div`css` or styled("div")`css` Expression::TaggedTemplateExpression(tag) => { // Check if tag is styled.div or styled(...) - let (tag_name, is_member) = match &tag.tag { - Expression::StaticMemberExpression(member) => { - if let Expression::Identifier(ident) = &member.object { - if ident.name.as_str() == styled_name { - (Some(member.property.name.to_string()), true) - } else { - (None, false) - } - } else { - (None, false) - } - } - Expression::CallExpression(call) => { - if let Expression::Identifier(ident) = &call.callee { - if ident.name.as_str() == styled_name && call.arguments.len() == 1 { - // styled("div") or styled(Component) - if let Argument::StringLiteral(lit) = &call.arguments[0] { - (Some(lit.value.to_string()), false) - } else { - // Component reference - we'll handle this later - (None, false) - } - } else { - (None, false) - } - } else { - (None, false) - } - } - _ => (None, false), - }; + let (tag_name, default_class_name) = + extract_base_tag_and_class_name(&tag.tag, styled_name, imports); - if tag_name.is_some() || is_member { + if let Some(tag_name) = tag_name { // Extract CSS from template literal let css_str = tag .quasi @@ -75,64 +100,49 @@ pub fn extract_style_from_styled<'a>( .map(|ex| ExtractStyleProp::Static(ExtractStyleValue::Static(ex.clone()))) .collect(); + if let Some(default_class_name) = default_class_name { + props_styles.extend( + default_class_name + .into_iter() + .map(|style| ExtractStyleProp::Static(style)), + ); + } + let class_name = gen_class_names(ast_builder, &mut props_styles, None, split_filename); + let new_expr = create_styled_component(ast_builder, &tag_name, &class_name, &None); let result = ExtractResult { styles: props_styles, - tag: tag_name.map(|name| { - ast_builder.expression_string_literal(SPAN, ast_builder.atom(&name), None) - }), + tag: Some(ast_builder.expression_string_literal( + SPAN, + ast_builder.atom(&tag_name), + None, + )), style_order: None, style_vars: None, props: None, }; - let new_expr = if let Some(cls) = class_name { - cls - } else { - ast_builder.expression_string_literal(SPAN, ast_builder.atom(""), None) - }; - - return (result, new_expr); + (Some(result), Some(new_expr)) + } else { + (None, None) } } // Case 2: styled.div({ bg: "red" }) or styled("div")({ bg: "red" }) Expression::CallExpression(call) => { // Check if this is a call to styled.div or styled("div") - let (tag_name, is_member) = match &call.callee { - Expression::StaticMemberExpression(member) => { - if let Expression::Identifier(ident) = &member.object { - if ident.name.as_str() == styled_name { - (Some(member.property.name.to_string()), true) - } else { - (None, false) - } - } else { - (None, false) - } - } - Expression::CallExpression(inner_call) => { - if let Expression::Identifier(ident) = &inner_call.callee { - if ident.name.as_str() == styled_name && inner_call.arguments.len() == 1 { - // styled("div") or styled(Component) - if let Argument::StringLiteral(lit) = &inner_call.arguments[0] { - (Some(lit.value.to_string()), false) - } else { - // Component reference - (None, false) - } - } else { - (None, false) - } - } else { - (None, false) - } - } - _ => (None, false), - }; + let (tag_name, default_class_name) = + extract_base_tag_and_class_name(&call.callee, styled_name, imports); + + println!( + "tag_name: {:?}, default_class_name: {:?}", + tag_name, default_class_name + ); - if (tag_name.is_some() || is_member) && call.arguments.len() == 1 { + if let Some(tag_name) = tag_name + && call.arguments.len() == 1 + { // Extract styles from object expression let ExtractResult { mut styles, @@ -151,35 +161,229 @@ pub fn extract_style_from_styled<'a>( 0, &None, ); + if let Some(default_class_name) = default_class_name { + styles.extend( + default_class_name + .into_iter() + .map(|style| ExtractStyleProp::Static(style)), + ); + } let class_name = gen_class_names(ast_builder, &mut styles, style_order, split_filename); + let styled_component = + create_styled_component(ast_builder, &tag_name, &class_name, &style_vars); let result = ExtractResult { styles, - tag: tag_name.map(|name| { - ast_builder.expression_string_literal(SPAN, ast_builder.atom(&name), None) - }), + tag: None, style_order, - style_vars, + style_vars: style_vars, props, }; - let new_expr = if let Some(cls) = class_name { - cls - } else { - ast_builder.expression_string_literal(SPAN, ast_builder.atom(""), None) - }; - - return (result, new_expr); + (Some(result), Some(styled_component)) + } else { + (None, None) } } - _ => {} - } - - // Default: no extraction + _ => (None, None), + }; ( - ExtractResult::default(), - expression.clone_in(ast_builder.allocator), + result.unwrap_or(ExtractResult::default()), + new_expr.unwrap_or(expression.clone_in(ast_builder.allocator)), + ) +} + +fn create_styled_component<'a>( + ast_builder: &AstBuilder<'a>, + tag_name: &str, + class_name: &Option>, + style_vars: &Option>, +) -> Expression<'a> { + let params = ast_builder.formal_parameters( + 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![ + ast_builder.binding_property( + SPAN, + ast_builder.property_key_static_identifier(SPAN, "style"), + ast_builder.binding_pattern( + ast_builder.binding_pattern_kind_binding_identifier( + SPAN, "style", + ), + None::< + oxc_allocator::Box< + oxc_ast::ast::TSTypeAnnotation<'a>, + >, + >, + false, + ), + true, + false, + ), + ast_builder.binding_property( + SPAN, + ast_builder + .property_key_static_identifier(SPAN, "className"), + ast_builder.binding_pattern( + ast_builder.binding_pattern_kind_binding_identifier( + SPAN, + "className", + ), + None::< + oxc_allocator::Box< + oxc_ast::ast::TSTypeAnnotation<'a>, + >, + >, + false, + ), + true, + 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, + false, + false, + )], + ast_builder.allocator, + ), + None::>>, + ); + let body = ast_builder.alloc_function_body( + 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( + SPAN, + ast_builder.alloc_jsx_opening_element( + SPAN, + 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 + .expression_identifier(SPAN, ast_builder.atom("rest")), + ), + ast_builder.jsx_attribute_item_attribute( + SPAN, + ast_builder.jsx_attribute_name_identifier( + SPAN, + ast_builder.atom("className"), + ), + Some( + ast_builder.jsx_attribute_value_expression_container( + SPAN, + class_name + .as_ref() + .map(|name| { + wrap_array_filter( + ast_builder, + &[ + name.clone_in( + ast_builder.allocator, + ), + ast_builder.expression_identifier( + SPAN, + ast_builder.atom("className"), + ), + ], + ) + .unwrap() + }) + .unwrap_or_else(|| { + ast_builder.expression_identifier( + SPAN, + ast_builder.atom("className"), + ) + }) + .into(), + ), + ), + ), + ast_builder.jsx_attribute_item_attribute( + SPAN, + ast_builder.jsx_attribute_name_identifier( + SPAN, + ast_builder.atom("style"), + ), + Some( + ast_builder.jsx_attribute_value_expression_container( + SPAN, + style_vars + .as_ref() + .map(|style_vars| { + merge_object_expressions( + ast_builder, + &[ + style_vars.clone_in( + ast_builder.allocator, + ), + ast_builder.expression_identifier( + SPAN, + ast_builder.atom("style"), + ), + ], + ) + .unwrap() + }) + .unwrap_or_else(|| { + ast_builder.expression_identifier( + SPAN, + ast_builder.atom("style"), + ) + }) + .into(), + ), + ), + ), + ], + ast_builder.allocator, + ), + ), + oxc_allocator::Vec::from_iter_in(vec![], ast_builder.allocator), + None::>>, + ), + )], + ast_builder.allocator, + ), + ); + ast_builder.expression_arrow_function( + SPAN, + true, + false, + None::>>, + params, + None::>>, + body, ) } diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 1588b244..ba85155d 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -7573,7 +7573,7 @@ keyframes({ extract( "test.tsx", r#"import {styled} from '@devup-ui/core' - const StyledDiv = styled.div` + const StyledDiv = styled.section` background: red; color: blue; ` @@ -7594,7 +7594,7 @@ keyframes({ extract( "test.tsx", r#"import {styled} from '@devup-ui/core' - const StyledDiv = styled("div")` + const StyledDiv = styled("article")` background: red; color: blue; ` @@ -7615,7 +7615,7 @@ keyframes({ extract( "test.tsx", r#"import {styled} from '@devup-ui/core' - const StyledDiv = styled("div")({ bg: "red" }) + const StyledDiv = styled("footer")({ bg: "red" }) "#, ExtractOption { package: "@devup-ui/core".to_string(), @@ -7633,7 +7633,7 @@ keyframes({ extract( "test.tsx", r#"import {styled} from '@devup-ui/core' - const StyledDiv = styled.div({ bg: "red" }) + const StyledDiv = styled.aside({ bg: "red" }) "#, ExtractOption { package: "@devup-ui/core".to_string(), @@ -7650,8 +7650,65 @@ keyframes({ assert_debug_snapshot!(ToBTreeSet::from( extract( "test.tsx", - r#"import {styled, Box} from '@devup-ui/core' - const StyledComponent = styled(Box)({ bg: "red" }) + r#"import {styled, Text} from '@devup-ui/core' + const StyledComponent = styled(Text)({ bg: "red" }) + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled, Text} from '@devup-ui/core' + const StyledComponent = styled(Text)` + background: red; + color: blue; + ` + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled, VStack} from '@devup-ui/core' + const StyledComponent = styled(VStack)({ bg: "red" }) + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled, VStack} from '@devup-ui/core' + const StyledComponent = styled(VStack)` + background: red; + color: blue; + ` "#, ExtractOption { package: "@devup-ui/core".to_string(), @@ -7679,6 +7736,26 @@ keyframes({ ) .unwrap() )); + + reset_class_map(); + assert_debug_snapshot!(ToBTreeSet::from( + extract( + "test.tsx", + r#"import {styled} from '@devup-ui/core' + const StyledComponent = styled(CustomComponent)` + background: red; + color: blue; + ` + "#, + ExtractOption { + package: "@devup-ui/core".to_string(), + css_dir: "@devup-ui/core".to_string(), + single_css: false, + import_main_css: false + } + ) + .unwrap() + )); } // #[test] diff --git a/libs/extractor/src/prop_modify_utils.rs b/libs/extractor/src/prop_modify_utils.rs index 90b0c977..0431174a 100644 --- a/libs/extractor/src/prop_modify_utils.rs +++ b/libs/extractor/src/prop_modify_utils.rs @@ -1,7 +1,7 @@ use crate::ExtractStyleProp; use crate::gen_class_name::gen_class_names; use crate::gen_style::gen_styles; -use crate::utils::get_string_by_property_key; +use crate::utils::{get_string_by_property_key, merge_object_expressions}; use oxc_allocator::CloneIn; use oxc_ast::AstBuilder; use oxc_ast::ast::JSXAttributeName::Identifier; @@ -375,29 +375,6 @@ fn merge_string_expressions<'a>( ) } -/// merge expressions to object expression -fn merge_object_expressions<'a>( - ast_builder: &AstBuilder<'a>, - expressions: &[Expression<'a>], -) -> Option> { - if expressions.is_empty() { - return None; - } - if expressions.len() == 1 { - return Some(expressions[0].clone_in(ast_builder.allocator)); - } - Some(ast_builder.expression_object( - SPAN, - oxc_allocator::Vec::from_iter_in( - expressions.iter().map(|ex| { - ast_builder - .object_property_kind_spread_property(SPAN, ex.clone_in(ast_builder.allocator)) - }), - ast_builder.allocator, - ), - )) -} - pub fn convert_class_name<'a>( ast_builder: &AstBuilder<'a>, class_name: &Expression<'a>, diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-10.snap b/libs/extractor/src/snapshots/extractor__tests__styled-10.snap new file mode 100644 index 00000000..d017d60e --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__styled-10.snap @@ -0,0 +1,27 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledComponent = styled(CustomComponent)`\n background: red;\n color: blue;\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}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + }, + ), + Static( + ExtractStaticStyle { + property: "color", + value: "blue", + level: 0, + selector: None, + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledComponent = ({ style, className, ...rest }) => ;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-2.snap b/libs/extractor/src/snapshots/extractor__tests__styled-2.snap new file mode 100644 index 00000000..26242318 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__styled-2.snap @@ -0,0 +1,27 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"article\")`\n background: red;\n color: blue;\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}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + }, + ), + Static( + ExtractStaticStyle { + property: "color", + value: "blue", + level: 0, + selector: None, + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) =>
;\n", +} diff --git a/libs/extractor/src/snapshots/extractor__tests__styled-3.snap b/libs/extractor/src/snapshots/extractor__tests__styled-3.snap new file mode 100644 index 00000000..fb62f318 --- /dev/null +++ b/libs/extractor/src/snapshots/extractor__tests__styled-3.snap @@ -0,0 +1,18 @@ +--- +source: libs/extractor/src/lib.rs +expression: "ToBTreeSet::from(extract(\"test.tsx\",\nr#\"import {styled} from '@devup-ui/core'\n const StyledDiv = styled(\"footer\")({ bg: \"red\" })\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}).unwrap())" +--- +ToBTreeSet { + styles: { + Static( + ExtractStaticStyle { + property: "background", + value: "red", + level: 0, + selector: None, + style_order: None, + }, + ), + }, + code: "import \"@devup-ui/core/devup-ui-0.css\";\nconst StyledDiv = ({ style, className, ...rest }) =>