stylex_transform/transform/fold/
fold_jsx_opening_element_impl.rs1use 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 if self.state.cycle != TransformationCycle::Initializing {
32 return jsx_opening_element.fold_children_with(self);
33 }
34
35 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 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 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 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 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 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 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 let sx_prop_idx = find_sx_prop_idx(&obj_lit.props, sx_prop_name)?;
143
144 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 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 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 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 if call.args.len() < 3 {
194 return None;
195 }
196
197 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 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 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 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 fn get_stylex_ident_name(&self) -> Option<String> {
252 self.state.stylex_import_stringified().into_iter().next()
253 }
254
255 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
267fn 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
284fn 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
308fn 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
324fn 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
334fn 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}