From 48d495fb1c33ca4b17d2d06522abb362721c8db7 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Tue, 14 Oct 2025 14:19:16 -0700 Subject: [PATCH 1/5] improved styled component detection --- src/lib.rs | 156 ++++++++++++++++++ .../react_inline_styled_component/input.jsx | 36 ++++ .../react_inline_styled_component/output.jsx | 26 +++ 3 files changed, 218 insertions(+) create mode 100644 tests/fixture/react_inline_styled_component/input.jsx create mode 100644 tests/fixture/react_inline_styled_component/output.jsx diff --git a/src/lib.rs b/src/lib.rs index d526d89..6496a17 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,8 @@ pub struct ReactComponentAnnotateVisitor { current_component_name: Option, ignored_elements: FxHashSet<&'static str>, ignored_components_set: FxHashSet, + /// Track the local identifier name for `styled` from @emotion/styled + styled_import: Option, } impl ReactComponentAnnotateVisitor { @@ -44,6 +46,7 @@ impl ReactComponentAnnotateVisitor { current_component_name: None, ignored_elements: constants::default_ignored_elements(), ignored_components_set, + styled_import: None, } } @@ -210,11 +213,155 @@ impl ReactComponentAnnotateVisitor { _ => {} } } + + /// Check if a call expression matches styled(ComponentRef) pattern + fn is_styled_call_with_component_ref(&self, call_expr: &CallExpr) -> Option { + // Check if we have a tracked styled import + let styled_name = self.styled_import.as_ref()?; + + // Check if the callee is the styled identifier + let callee_name = match call_expr.callee.as_expr() { + Some(expr) => match expr.as_ref() { + Expr::Ident(ident) => ident.sym.as_ref(), + _ => return None, + }, + _ => return None, + }; + + if callee_name != styled_name { + return None; + } + + // Check if the first argument is an identifier (component reference) + if let Some(ExprOrSpread { spread: None, expr }) = call_expr.args.first() { + if let Expr::Ident(ident) = expr.as_ref() { + return Some(ident.sym.to_string()); + } + } + + None + } + + /// Transform styled(ComponentRef) to styled(props => ) + fn transform_styled_call(&self, call_expr: &mut CallExpr, component_name: String) { + use swc_core::common::{SyntaxContext, DUMMY_SP}; + + // Create the props parameter: props + let props_param = Pat::Ident(BindingIdent { + id: Ident::new("props".into(), DUMMY_SP, SyntaxContext::empty()), + type_ann: None, + }); + + // Build attributes in order: data attributes first, then spread + let mut attrs = vec![]; + + // Add data-element attribute + attrs.push(create_jsx_attr( + self.config.element_attr_name(), + &component_name, + )); + + // Add data-source-file attribute + if let Some(ref source_file) = self.source_file_name { + attrs.push(create_jsx_attr( + self.config.source_file_attr_name(), + source_file, + )); + } + + // Add data-source-path attribute (only if explicitly configured) + if self.config.source_path_attr.is_some() { + if let Some(ref source_path) = self.source_file_path { + attrs.push(create_jsx_attr( + self.config.source_path_attr_name(), + source_path, + )); + } + } + + // Add spread attribute AFTER data attributes: {...props} + attrs.push(JSXAttrOrSpread::SpreadElement(SpreadElement { + dot3_token: DUMMY_SP, + expr: Box::new(Expr::Ident(Ident::new( + "props".into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + })); + + // Create JSX element: + let jsx_element = JSXElement { + span: DUMMY_SP, + opening: JSXOpeningElement { + name: JSXElementName::Ident(Ident::new( + component_name.into(), + DUMMY_SP, + SyntaxContext::empty(), + )), + span: DUMMY_SP, + attrs, + self_closing: true, + type_args: None, + }, + children: vec![], + closing: None, + }; + + // Create arrow function: props => + let arrow_func = ArrowExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + params: vec![props_param], + body: Box::new(BlockStmtOrExpr::Expr(Box::new(Expr::JSXElement(Box::new( + jsx_element, + ))))), + is_async: false, + is_generator: false, + type_params: None, + return_type: None, + }; + + // Replace the first argument with the arrow function + call_expr.args[0] = ExprOrSpread { + spread: None, + expr: Box::new(Expr::Arrow(arrow_func)), + }; + } } impl VisitMut for ReactComponentAnnotateVisitor { noop_visit_mut_type!(); + fn visit_mut_import_decl(&mut self, import_decl: &mut ImportDecl) { + // Track imports from @emotion/styled + if import_decl.src.value.as_ref() == "@emotion/styled" { + for specifier in &import_decl.specifiers { + match specifier { + // Default import: import styled from '@emotion/styled' + ImportSpecifier::Default(default_import) => { + self.styled_import = Some(default_import.local.sym.to_string()); + } + // Named import: import { styled } from '@emotion/styled' + ImportSpecifier::Named(named_import) => { + // Check if the imported name is 'default' or 'styled' + let imported_name = match &named_import.imported { + Some(ModuleExportName::Ident(ident)) => ident.sym.as_ref(), + None => named_import.local.sym.as_ref(), + _ => continue, + }; + + if imported_name == "default" || imported_name == "styled" { + self.styled_import = Some(named_import.local.sym.to_string()); + } + } + _ => {} + } + } + } + + import_decl.visit_mut_children_with(self); + } + fn visit_mut_fn_decl(&mut self, func_decl: &mut FnDecl) { let component_name = func_decl.ident.sym.to_string(); self.find_jsx_in_function_body(&mut func_decl.function, component_name); @@ -228,6 +375,15 @@ impl VisitMut for ReactComponentAnnotateVisitor { if let Some(init) = &mut var_declarator.init { match init.as_mut() { + Expr::Call(call_expr) => { + // Check if this is a styled(ComponentRef) pattern + if let Some(ref_component_name) = + self.is_styled_call_with_component_ref(call_expr) + { + // Transform styled(ComponentRef) to styled(props => ) + self.transform_styled_call(call_expr, ref_component_name); + } + } Expr::Arrow(arrow_func) => { self.current_component_name = Some(component_name); diff --git a/tests/fixture/react_inline_styled_component/input.jsx b/tests/fixture/react_inline_styled_component/input.jsx new file mode 100644 index 0000000..aab5ec6 --- /dev/null +++ b/tests/fixture/react_inline_styled_component/input.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import styled from '@emotion/styled'; + +// A regular React component +const Button = ({ children, ...props }) => { + return ; +}; + +// Styled component using component reference +const StyledButton = styled(Button); + +// Another regular component +const Card = (props) => { + return ( +
+

{props.title}

+

{props.content}

+
+ ); +}; + +// Styled component using component reference +const StyledCard = styled(Card); + +// Component that uses the styled components +const MyComponent = () => { + return ( +
+

Styled Components Example

+ Click me + +
+ ); +}; + +export default MyComponent; diff --git a/tests/fixture/react_inline_styled_component/output.jsx b/tests/fixture/react_inline_styled_component/output.jsx new file mode 100644 index 0000000..c62d279 --- /dev/null +++ b/tests/fixture/react_inline_styled_component/output.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import styled from '@emotion/styled'; +// A regular React component +const Button = ({ children, ...props })=>{ + return ; +}; +// Styled component using component reference +const StyledButton = styled((props)=>; }; // Styled component using component reference -const StyledButton = styled((props)=>