Skip to main content

stylex_transform/transform/fold/
fold_jsx_opening_element_impl.rs

1use stylex_macros::stylex_panic;
2use swc_core::{
3  common::{DUMMY_SP, comments::Comments},
4  ecma::{
5    ast::{
6      Bool, CallExpr, Callee, Expr, ExprOrSpread, JSXAttrName, JSXAttrOrSpread, JSXAttrValue,
7      JSXElementName, JSXExpr, JSXOpeningElement, Lit, MemberExpr, MemberProp, Prop, PropName,
8      PropOrSpread,
9    },
10    visit::FoldWith,
11  },
12};
13
14use crate::StyleXTransform;
15use stylex_ast::ast::factories::{
16  create_arrow_expression, create_ident, create_ident_call_expr, create_ident_name,
17  create_jsx_spread_attr, create_member_call_expr, create_object_lit, create_spread_prop,
18};
19use stylex_constants::constants::common::RUNTIME_JSX_CALL_NAMES;
20use stylex_enums::core::TransformationCycle;
21
22impl<C> StyleXTransform<C>
23where
24  C: Comments,
25{
26  pub(crate) fn fold_jsx_opening_element_impl(
27    &mut self,
28    mut jsx_opening_element: JSXOpeningElement,
29  ) -> JSXOpeningElement {
30    // Only transform during Initializing cycle
31    if self.state.cycle != TransformationCycle::Initializing {
32      return jsx_opening_element.fold_children_with(self);
33    }
34
35    // Check if sxPropName is enabled
36    let sx_prop_name = match self.state.options.sx_prop_name.clone() {
37      Some(name) => name,
38      None => return jsx_opening_element.fold_children_with(self),
39    };
40
41    // Only transform lowercase JSX element names (HTML elements, not React components)
42    let is_lowercase_element = match &jsx_opening_element.name {
43      JSXElementName::Ident(ident) => ident
44        .sym
45        .chars()
46        .next()
47        .map(|c| c.is_lowercase())
48        .unwrap_or(false),
49      _ => false,
50    };
51
52    if !is_lowercase_element {
53      return jsx_opening_element.fold_children_with(self);
54    }
55
56    // Find the sx attribute index
57    let sx_attr_idx = jsx_opening_element.attrs.iter().position(|attr| {
58      if let JSXAttrOrSpread::JSXAttr(jsx_attr) = attr
59        && let JSXAttrName::Ident(name) = &jsx_attr.name
60      {
61        return name.sym.as_str() == sx_prop_name.as_str();
62      }
63      false
64    });
65
66    if let Some(idx) = sx_attr_idx {
67      let replacement = if let JSXAttrOrSpread::JSXAttr(jsx_attr) = &jsx_opening_element.attrs[idx]
68      {
69        if let Some(JSXAttrValue::JSXExprContainer(container)) = &jsx_attr.value {
70          if let JSXExpr::Expr(expr) = &container.expr {
71            let value_expr = *expr.clone();
72            let stylex_ident_name = self.get_stylex_ident_name();
73            let props_ident_name = self.get_props_ident_name();
74
75            // sx={[styles.a, styles.b]} → {...stylex.props(styles.a, styles.b)}
76            let args = sx_value_to_props_args(value_expr);
77            let call = Expr::Call(build_stylex_props_call(
78              stylex_ident_name,
79              props_ident_name,
80              args,
81            ));
82
83            Some(create_jsx_spread_attr(call))
84          } else {
85            None
86          }
87        } else {
88          None
89        }
90      } else {
91        None
92      };
93
94      if let Some(spread) = replacement {
95        jsx_opening_element.attrs[idx] = spread;
96      }
97    }
98
99    jsx_opening_element.fold_children_with(self)
100  }
101
102  /// Transform compiled JSX/VDOM calls with an `sx` prop during the Initializing cycle.
103  ///
104  /// Handles:
105  /// - React: `_jsx("div", { sx: expr })` / `_jsxs("div", { sx: expr })`
106  /// - React classic: `React.createElement("div", { sx: expr })`
107  /// - Vue: `_createElementBlock("div", { sx: expr })` / `_createElementVNode("div", { sx: expr })`
108  ///
109  /// Transforms to: `fn("div", { ...stylex.props(expr), ... })`
110  pub(crate) fn transform_sx_in_compiled_jsx(&self, expr: &Expr) -> Option<Expr> {
111    let sx_prop_name = self.state.options.sx_prop_name.as_deref()?;
112
113    let call = expr.as_call()?;
114
115    if !is_jsx_runtime_call(call) {
116      return None;
117    }
118
119    // First arg must be a lowercase string literal (HTML element)
120    let first_arg = call.args.first()?;
121    let element_name = match first_arg.expr.as_ref() {
122      Expr::Lit(Lit::Str(s)) => s.value.as_str().unwrap_or("").to_string(),
123      _ => return None,
124    };
125    if !element_name
126      .chars()
127      .next()
128      .map(|c: char| c.is_lowercase())
129      .unwrap_or(false)
130    {
131      return None;
132    }
133
134    // Second arg must be an object literal
135    let second_arg = call.args.get(1)?;
136    let obj_lit = match second_arg.expr.as_ref() {
137      Expr::Object(o) => o.clone(),
138      _ => return None,
139    };
140
141    // Find the sx prop index
142    let sx_prop_idx = find_sx_prop_idx(&obj_lit.props, sx_prop_name)?;
143
144    // Extract the sx value and build args
145    let sx_value = extract_prop_value(&obj_lit.props[sx_prop_idx])?;
146    let stylex_ident_name = self.get_stylex_ident_name();
147    let props_ident_name = self.get_props_ident_name();
148    let args = sx_value_to_props_args(sx_value);
149
150    // Replace the sx prop with: ...stylex.props(...args)
151    let mut new_props = obj_lit.props.clone();
152    let call_expr = Expr::Call(build_stylex_props_call(
153      stylex_ident_name,
154      props_ident_name,
155      args,
156    ));
157    new_props[sx_prop_idx] = create_spread_prop(call_expr);
158
159    let mut new_call = call.clone();
160    new_call.args[1] = ExprOrSpread {
161      spread: None,
162      expr: Box::new(Expr::Object(create_object_lit(new_props))),
163    };
164
165    Some(Expr::Call(new_call))
166  }
167
168  /// Transform Solid.js compiled `sx` attribute during the Initializing cycle.
169  ///
170  /// Solid.js compiles `<div sx={styles.main}>` to:
171  /// `_$setAttribute(_el$, "sx", styles.main)`
172  ///
173  /// We transform it to:
174  /// `_$spread(_el$, _$mergeProps(() => stylex.props(styles.main)), false, true)`
175  pub(crate) fn transform_sx_in_solid_set_attribute(&self, expr: &Expr) -> Option<Expr> {
176    let sx_prop_name = self.state.options.sx_prop_name.as_deref()?;
177
178    let call = expr.as_call()?;
179
180    // Check callee is _$setAttribute
181    let is_set_attribute = match &call.callee {
182      Callee::Expr(e) => match e.as_ref() {
183        Expr::Ident(ident) => ident.sym.as_str() == "_$setAttribute",
184        _ => false,
185      },
186      _ => false,
187    };
188    if !is_set_attribute {
189      return None;
190    }
191
192    // Need at least 3 args: (el, "sx", value)
193    if call.args.len() < 3 {
194      return None;
195    }
196
197    // Args[1] must be the string matching sx_prop_name
198    let attr_name = match call.args[1].expr.as_ref() {
199      Expr::Lit(Lit::Str(s)) => s.value.as_str().unwrap_or("").to_string(),
200      _ => return None,
201    };
202    if attr_name != sx_prop_name {
203      return None;
204    }
205
206    let el_arg = call.args[0].clone();
207    let value_expr = *call.args[2].expr.clone();
208
209    let stylex_ident_name = self.get_stylex_ident_name();
210    let props_ident_name = self.get_props_ident_name();
211    let args = sx_value_to_props_args(value_expr);
212
213    // Build: () => stylex.props(args...)
214    let props_call = Expr::Call(build_stylex_props_call(
215      stylex_ident_name,
216      props_ident_name,
217      args,
218    ));
219    let arrow_fn = create_arrow_expression(props_call);
220
221    // Build: _$mergeProps(() => stylex.props(...))
222    let merge_props_call = Expr::Call(create_ident_call_expr(
223      "_$mergeProps",
224      vec![ExprOrSpread {
225        spread: None,
226        expr: Box::new(arrow_fn),
227      }],
228    ));
229
230    // Build: _$spread(el, _$mergeProps(() => stylex.props(...)), false, true)
231    use stylex_ast::ast::factories::create_expr_or_spread;
232    Some(Expr::Call(create_ident_call_expr(
233      "_$spread",
234      vec![
235        el_arg,
236        create_expr_or_spread(merge_props_call),
237        create_expr_or_spread(Expr::Lit(Lit::Bool(Bool {
238          span: DUMMY_SP,
239          value: false,
240        }))),
241        create_expr_or_spread(Expr::Lit(Lit::Bool(Bool {
242          span: DUMMY_SP,
243          value: true,
244        }))),
245      ],
246    )))
247  }
248
249  /// Get the stylex ident name from the import paths.
250  /// Returns the first stylex import name from the import paths.
251  fn get_stylex_ident_name(&self) -> Option<String> {
252    self.state.stylex_import_stringified().into_iter().next()
253  }
254
255  /// Get the props ident name from the import paths.
256  /// Returns the first props import name from the import paths.
257  fn get_props_ident_name(&self) -> Option<String> {
258    self
259      .state
260      .stylex_props_import
261      .iter()
262      .next()
263      .map(|ident| ident.to_string())
264  }
265}
266
267/// Convert an `sx` value expression into args for `stylex.props(...)`.
268/// Array literals are unpacked: `[a, b]` → `[a, b]` as separate args.
269/// Other expressions are wrapped: `expr` → `[expr]`.
270fn sx_value_to_props_args(value_expr: Expr) -> Vec<ExprOrSpread> {
271  match &value_expr {
272    Expr::Array(array_lit) => array_lit
273      .elems
274      .iter()
275      .filter_map(|elem| elem.clone())
276      .collect(),
277    _ => vec![ExprOrSpread {
278      spread: None,
279      expr: Box::new(value_expr),
280    }],
281  }
282}
283
284/// Build a call expression for stylex props:
285/// - `stylex.props(args...)` when `stylex_ident_name` is available
286/// - `<props_ident_name>(args...)` (e.g. `props(args...)` or `sx(args...)`) otherwise
287fn build_stylex_props_call(
288  stylex_ident_name: Option<String>,
289  props_ident_name: Option<String>,
290  args: Vec<ExprOrSpread>,
291) -> CallExpr {
292  if let Some(stylex_ident_name) = stylex_ident_name {
293    let member = MemberExpr {
294      span: DUMMY_SP,
295      obj: Box::new(Expr::Ident(create_ident(&stylex_ident_name))),
296      prop: MemberProp::Ident(create_ident_name("props")),
297    };
298    create_member_call_expr(member, args)
299  } else if let Some(props_ident_name) = props_ident_name {
300    create_ident_call_expr(&props_ident_name, args)
301  } else {
302    stylex_panic!(
303      "Could not resolve StyleX import. Ensure you have imported stylex or the props function."
304    );
305  }
306}
307
308/// Find the index of the prop with key matching `sx_prop_name` in a props list.
309fn find_sx_prop_idx(props: &[PropOrSpread], sx_prop_name: &str) -> Option<usize> {
310  props.iter().position(|prop| {
311    if let PropOrSpread::Prop(p) = prop
312      && let Prop::KeyValue(kv) = p.as_ref()
313    {
314      return match &kv.key {
315        PropName::Ident(ident) => ident.sym.as_str() == sx_prop_name,
316        PropName::Str(s) => s.value.as_str().unwrap_or("") == sx_prop_name,
317        _ => false,
318      };
319    }
320    false
321  })
322}
323
324/// Extract the value from a `KeyValue` prop entry.
325fn extract_prop_value(prop: &PropOrSpread) -> Option<Expr> {
326  if let PropOrSpread::Prop(p) = prop
327    && let Prop::KeyValue(kv) = p.as_ref()
328  {
329    return Some(kv.value.as_ref().clone());
330  }
331  None
332}
333
334/// Check if a `CallExpr` is a JSX/VDOM runtime call that takes `(elementName, props, ...)`.
335fn is_jsx_runtime_call(call: &CallExpr) -> bool {
336  match &call.callee {
337    Callee::Expr(e) => match e.as_ref() {
338      Expr::Ident(ident) => {
339        let name = ident.sym.as_str();
340
341        let name = name.strip_suffix("DEV").unwrap_or(name);
342        let name = name.strip_prefix('_').unwrap_or(name);
343
344        RUNTIME_JSX_CALL_NAMES.contains(&name)
345      },
346      Expr::Member(member) => {
347        if let (Expr::Ident(obj), MemberProp::Ident(prop)) = (member.obj.as_ref(), &member.prop) {
348          obj.sym.as_str() == "React" && prop.sym.as_str() == "createElement"
349        } else {
350          false
351        }
352      },
353      _ => false,
354    },
355    _ => false,
356  }
357}