Skip to main content

styled_components/visitors/transpile_css_prop/
transpile.rs

1//! Port of https://github.com/styled-components/babel-plugin-styled-components/blob/a20c3033508677695953e7a434de4746168eeb4e/src/visitors/transpileCssProp.js
2
3use std::{borrow::Cow, collections::HashMap};
4
5use inflector::Inflector;
6use once_cell::sync::Lazy;
7use regex::Regex;
8use rustc_hash::{FxHashMap, FxHashSet};
9use swc_atoms::Atom;
10use swc_common::{util::take::Take, Spanned, DUMMY_SP};
11use swc_ecma_ast::*;
12use swc_ecma_utils::{prepend_stmt, private_ident, quote_ident, ExprFactory};
13use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
14
15use super::top_level_binding_collector::collect_top_level_decls;
16use crate::{
17    utils::{get_prop_key_as_expr, get_prop_name, get_prop_name2},
18    State,
19};
20
21static TAG_NAME_REGEX: Lazy<Regex> =
22    Lazy::new(|| Regex::new("^[a-z][a-z\\d]*(\\-[a-z][a-z\\d]*)?$").unwrap());
23
24pub fn transpile_css_prop(state: &mut State) -> impl '_ + Pass {
25    visit_mut_pass(TranspileCssProp {
26        state,
27        import_name: Default::default(),
28        injected_nodes: Default::default(),
29        interleaved_injections: Default::default(),
30        identifier_idx: Default::default(),
31        styled_idx: Default::default(),
32        top_level_decls: Default::default(),
33    })
34}
35
36struct TranspileCssProp<'a> {
37    state: &'a mut State,
38
39    import_name: Option<Ident>,
40    injected_nodes: Vec<Stmt>,
41    interleaved_injections: FxHashMap<Id, Vec<Stmt>>,
42
43    identifier_idx: usize,
44    styled_idx: HashMap<Atom, usize>,
45    top_level_decls: Option<FxHashSet<Id>>,
46}
47
48impl TranspileCssProp<'_> {
49    fn next_styled_idx(&mut self, key: Atom) -> usize {
50        let idx = self.styled_idx.entry(key).or_insert(0);
51        *idx += 1;
52        *idx
53    }
54
55    #[allow(clippy::wrong_self_convention)]
56    fn is_top_level_ident(&self, ident: &Ident) -> bool {
57        self.top_level_decls
58            .as_ref()
59            .map(|decls| decls.contains(&ident.to_id()))
60            .unwrap_or(false)
61    }
62}
63
64impl VisitMut for TranspileCssProp<'_> {
65    noop_visit_mut_type!(fail);
66
67    fn visit_mut_jsx_element(&mut self, elem: &mut JSXElement) {
68        elem.visit_mut_children_with(self);
69
70        let mut extra_attrs = vec![];
71
72        for attr in elem.opening.attrs.iter_mut() {
73            match &mut *attr {
74                JSXAttrOrSpread::JSXAttr(attr) => {
75                    if !matches!(&attr.name, JSXAttrName::Ident(i) if &*i.sym == "css") {
76                        continue;
77                    }
78
79                    let import_name = if let Some(ident) = self
80                        .state
81                        .import_local_name("default", None)
82                        .map(Ident::from)
83                    {
84                        ident
85                    } else {
86                        self.import_name
87                            .get_or_insert_with(|| private_ident!("_styled"))
88                            .clone()
89                    };
90
91                    let name = get_name_ident(&elem.opening.name);
92                    let id_sym = name.sym.to_pascal_case();
93
94                    // Match the original plugin's behavior.
95                    let id_sym = id_sym.trim_end_matches(char::is_numeric);
96
97                    let id_sym = Atom::from(id_sym);
98                    let styled_idx = self.next_styled_idx(id_sym.clone());
99                    let id = private_ident!(
100                        elem.opening.name.span(),
101                        append_if_gt_one(&format!("_Styled{id_sym}"), styled_idx)
102                    );
103
104                    let (styled, inject_after) = if TAG_NAME_REGEX.is_match(&name.sym) {
105                        (
106                            (Expr::Call(CallExpr {
107                                span: DUMMY_SP,
108                                callee: import_name.as_callee(),
109                                args: vec![Lit::Str(Str {
110                                    span: DUMMY_SP,
111                                    value: name.sym.into(),
112                                    raw: None,
113                                })
114                                .as_arg()],
115                                ..Default::default()
116                            })),
117                            None::<Ident>,
118                        )
119                    } else {
120                        let name_expr = get_name_expr(&elem.opening.name);
121
122                        (
123                            Expr::Call(CallExpr {
124                                span: DUMMY_SP,
125                                callee: import_name.as_callee(),
126                                args: vec![name_expr.as_arg()],
127                                ..Default::default()
128                            }),
129                            if self.is_top_level_ident(&name) {
130                                Some(name)
131                            } else {
132                                None
133                            },
134                        )
135                    };
136
137                    let mut css = match &mut attr.value {
138                        Some(css) => {
139                            //
140
141                            match css {
142                                JSXAttrValue::Str(v) => Expr::Tpl(Tpl {
143                                    span: DUMMY_SP,
144                                    exprs: Default::default(),
145                                    quasis: vec![TplElement {
146                                        span: DUMMY_SP,
147                                        tail: true,
148                                        cooked: None,
149                                        raw: v.value.to_atom_lossy().into_owned(),
150                                    }],
151                                }),
152                                JSXAttrValue::JSXExprContainer(JSXExprContainer {
153                                    expr: JSXExpr::Expr(v),
154                                    ..
155                                }) => match &mut **v {
156                                    Expr::Tpl(..) => *v.take(),
157                                    Expr::TaggedTpl(v)
158                                        if match &*v.tag {
159                                            Expr::Ident(i) => &*i.sym == "css",
160                                            _ => false,
161                                        } =>
162                                    {
163                                        Expr::Tpl(*v.tpl.take())
164                                    }
165                                    Expr::Object(..) => *v.take(),
166                                    _ => Expr::Tpl(Tpl {
167                                        span: DUMMY_SP,
168                                        exprs: vec![v.take()],
169                                        quasis: vec![
170                                            TplElement {
171                                                span: DUMMY_SP,
172                                                tail: false,
173                                                cooked: None,
174                                                raw: "".into(),
175                                            },
176                                            TplElement {
177                                                span: DUMMY_SP,
178                                                tail: true,
179                                                cooked: None,
180                                                raw: "".into(),
181                                            },
182                                        ],
183                                    }),
184                                },
185
186                                _ => continue,
187                            }
188                        }
189                        None => continue,
190                    };
191
192                    // Remove this attribute
193                    attr.name = JSXAttrName::Ident(Take::dummy());
194
195                    elem.opening.name = JSXElementName::Ident(id.clone());
196
197                    if let Some(closing) = &mut elem.closing {
198                        closing.name = JSXElementName::Ident(id.clone());
199                    }
200
201                    // object syntax
202                    if let Expr::Object(css_obj) = &mut css {
203                        // Original plugin says
204                        //
205                        //
206                        // for objects as CSS props, we have to recurse through the object and
207                        // replace any object key/value scope references with generated props
208                        // similar to how the template literal transform above creates dynamic
209                        // interpolations
210                        let p = quote_ident!("p");
211
212                        let mut reducer = PropertyReducer {
213                            p: p.clone().into(),
214                            replace_object_with_prop_function: false,
215                            extra_attrs: Default::default(),
216                            identifier_idx: &mut self.identifier_idx,
217                        };
218
219                        css_obj.props = css_obj
220                            .props
221                            .take()
222                            .into_iter()
223                            .fold(vec![], |acc, property| {
224                                reducer.reduce_object_properties(acc, property)
225                            });
226
227                        extra_attrs.extend(reducer.extra_attrs);
228
229                        if reducer.replace_object_with_prop_function {
230                            css = Expr::Arrow(ArrowExpr {
231                                params: vec![Pat::Ident(p.clone().into())],
232                                body: Box::new(BlockStmtOrExpr::Expr(Box::new(css.take()))),
233                                is_async: false,
234                                is_generator: false,
235                                ..Default::default()
236                            });
237                        }
238                    } else {
239                        // tagged template literal
240                        let mut tpl = css.expect_tpl();
241
242                        tpl.exprs =
243                            tpl.exprs
244                                .take()
245                                .into_iter()
246                                .fold(vec![], |mut acc, mut expr| {
247                                    if expr.is_fn_expr()
248                                        || expr.is_arrow()
249                                        || is_direct_access(&expr, &|id| {
250                                            self.is_top_level_ident(id)
251                                        })
252                                    {
253                                        acc.push(expr);
254                                        return acc;
255                                    }
256
257                                    let identifier =
258                                        get_local_identifier(&mut self.identifier_idx, &expr);
259                                    let p = quote_ident!("p");
260                                    extra_attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
261                                        span: DUMMY_SP,
262                                        name: JSXAttrName::Ident(identifier.clone()),
263                                        value: Some(JSXAttrValue::JSXExprContainer(
264                                            JSXExprContainer {
265                                                span: DUMMY_SP,
266                                                expr: JSXExpr::Expr(expr.take()),
267                                            },
268                                        )),
269                                    }));
270
271                                    acc.push(Box::new(Expr::Arrow(ArrowExpr {
272                                        params: vec![Pat::Ident(p.clone().into())],
273                                        body: Box::new(BlockStmtOrExpr::Expr(
274                                            p.make_member(identifier).into(),
275                                        )),
276                                        is_async: false,
277                                        is_generator: false,
278                                        ..Default::default()
279                                    })));
280
281                                    acc
282                                });
283
284                        css = Expr::Tpl(tpl);
285                    }
286
287                    let var = VarDeclarator {
288                        span: DUMMY_SP,
289                        name: Pat::Ident(id.clone().into()),
290                        init: Some(match css {
291                            Expr::Object(..) | Expr::Arrow(..) => Box::new(Expr::Call(CallExpr {
292                                span: DUMMY_SP,
293                                callee: styled.as_callee(),
294                                args: vec![css.as_arg()],
295                                ..Default::default()
296                            })),
297                            _ => Box::new(Expr::TaggedTpl(TaggedTpl {
298                                span: DUMMY_SP,
299                                tag: Box::new(styled),
300                                tpl: Box::new(css.expect_tpl()),
301                                ..Default::default()
302                            })),
303                        }),
304                        definite: false,
305                    };
306                    let stmt = Stmt::Decl(Decl::Var(Box::new(VarDecl {
307                        kind: VarDeclKind::Var,
308                        declare: false,
309                        decls: vec![var],
310                        ..Default::default()
311                    })));
312                    match inject_after {
313                        Some(injector) => {
314                            let id = injector.to_id();
315                            self.interleaved_injections
316                                .entry(id)
317                                .or_default()
318                                .push(stmt);
319                        }
320                        None => {
321                            self.injected_nodes.push(stmt);
322                        }
323                    }
324                }
325                JSXAttrOrSpread::SpreadElement(_) => {}
326                #[cfg(swc_ast_unknown)]
327                _ => panic!("unknown node"),
328            }
329        }
330
331        elem.opening.attrs.retain(|attr| {
332            match attr {
333                JSXAttrOrSpread::JSXAttr(attr) => {
334                    if match &attr.name {
335                        JSXAttrName::Ident(IdentName { sym, .. }) => sym.is_empty(),
336                        _ => false,
337                    } {
338                        return false;
339                    }
340                }
341                JSXAttrOrSpread::SpreadElement(_) => {}
342                #[cfg(swc_ast_unknown)]
343                _ => panic!("unknown node"),
344            }
345            true
346        });
347
348        elem.opening.attrs.extend(extra_attrs);
349    }
350
351    fn visit_mut_module(&mut self, n: &mut Module) {
352        // TODO: Skip if there are no css prop usage
353        self.top_level_decls = Some(collect_top_level_decls(n));
354        n.visit_mut_children_with(self);
355        self.top_level_decls = None;
356
357        if let Some(import_name) = self.import_name.take() {
358            self.state.set_import_name(import_name.to_id());
359            let specifier = ImportSpecifier::Default(ImportDefaultSpecifier {
360                span: DUMMY_SP,
361                local: import_name,
362            });
363            prepend_stmt(
364                &mut n.body,
365                ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
366                    span: DUMMY_SP,
367                    specifiers: vec![specifier],
368                    src: Box::new(Str {
369                        span: DUMMY_SP,
370                        value: "styled-components".into(),
371                        raw: None,
372                    }),
373                    type_only: Default::default(),
374                    with: Default::default(),
375                    phase: Default::default(),
376                })),
377            );
378        }
379
380        if !self.state.need_work() {
381            return;
382        }
383
384        let mut serialized_body: Vec<ModuleItem> = vec![];
385        let body = std::mem::take(&mut n.body);
386        for item in body {
387            serialized_body.push(item.clone());
388            if let ModuleItem::Stmt(Stmt::Decl(Decl::Var(vd))) = &item {
389                for decl in &vd.decls {
390                    if let Pat::Ident(ident) = &decl.name {
391                        let id = ident.to_id();
392                        let stmts = self.interleaved_injections.remove(&id);
393                        if let Some(stmts) = stmts {
394                            serialized_body.extend(stmts.into_iter().rev().map(ModuleItem::Stmt));
395                        }
396                    }
397                }
398            }
399        }
400        n.body = serialized_body;
401
402        let mut remaining = std::mem::take(&mut self.interleaved_injections)
403            .into_iter()
404            .collect::<Vec<_>>();
405        remaining.sort_by_key(|x| x.0.clone());
406
407        remaining
408            .into_iter()
409            .for_each(|(_, stmts)| n.body.extend(stmts.into_iter().map(ModuleItem::Stmt)));
410
411        n.body
412            .extend(self.injected_nodes.take().into_iter().map(ModuleItem::Stmt));
413    }
414}
415
416fn get_name_expr(name: &JSXElementName) -> Box<Expr> {
417    fn get_name_expr_jsx_object(name: &JSXObject) -> Box<Expr> {
418        match name {
419            JSXObject::Ident(n) => Box::new(Expr::Ident(n.clone())),
420            JSXObject::JSXMemberExpr(n) => Box::new(Expr::Member(MemberExpr {
421                span: DUMMY_SP,
422                obj: get_name_expr_jsx_object(&n.obj),
423                prop: MemberProp::Ident(n.prop.clone()),
424            })),
425            #[cfg(swc_ast_unknown)]
426            _ => panic!("unknown node"),
427        }
428    }
429    match name {
430        JSXElementName::Ident(n) => Box::new(Expr::Ident(n.clone())),
431        JSXElementName::JSXMemberExpr(n) => Box::new(Expr::Member(MemberExpr {
432            span: DUMMY_SP,
433            obj: get_name_expr_jsx_object(&n.obj),
434            prop: MemberProp::Ident(n.prop.clone()),
435        })),
436        JSXElementName::JSXNamespacedName(..) => {
437            unimplemented!("get_name_expr for JSXNamespacedName")
438        }
439        #[cfg(swc_ast_unknown)]
440        _ => panic!("unknown node"),
441    }
442}
443
444struct PropertyReducer<'a> {
445    p: Ident,
446    replace_object_with_prop_function: bool,
447    extra_attrs: Vec<JSXAttrOrSpread>,
448
449    identifier_idx: &'a mut usize,
450}
451
452impl PropertyReducer<'_> {
453    fn reduce_object_properties(
454        &mut self,
455        mut acc: Vec<PropOrSpread>,
456        mut property: PropOrSpread,
457    ) -> Vec<PropOrSpread> {
458        match property {
459            PropOrSpread::Spread(ref mut prop) => {
460                // handle spread variables and such
461
462                if let Expr::Object(arg) = &mut *prop.expr {
463                    arg.props = arg
464                        .props
465                        .take()
466                        .into_iter()
467                        .fold(vec![], |acc, p| self.reduce_object_properties(acc, p));
468                } else {
469                    self.replace_object_with_prop_function = true;
470
471                    let identifier = get_local_identifier(self.identifier_idx, &prop.expr);
472
473                    self.extra_attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
474                        span: DUMMY_SP,
475                        name: JSXAttrName::Ident(identifier.clone()),
476                        value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
477                            span: DUMMY_SP,
478                            expr: JSXExpr::Expr(prop.expr.take()),
479                        })),
480                    }));
481
482                    prop.expr = self.p.clone().make_member(identifier).into();
483                }
484
485                acc.push(property);
486            }
487            PropOrSpread::Prop(ref mut prop) => {
488                let key = get_prop_key_as_expr(prop);
489                let key_pn = get_prop_name(prop);
490
491                if key.is_member()
492                    || key.is_call()
493                    || (key.is_ident()
494                        && key_pn.is_some()
495                        && key_pn.unwrap().is_computed()
496                        && !matches!(&**prop, Prop::Shorthand(..)))
497                {
498                    self.replace_object_with_prop_function = true;
499
500                    let identifier = get_local_identifier(self.identifier_idx, &key);
501
502                    self.extra_attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
503                        span: DUMMY_SP,
504                        name: identifier.clone().into(),
505                        value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
506                            span: DUMMY_SP,
507                            // TODO: Perf
508                            expr: JSXExpr::Expr(Box::new(key.clone().into_owned())),
509                        })),
510                    }));
511
512                    set_key_of_prop(prop, self.p.clone().make_member(identifier).into());
513                }
514
515                let mut value = take_prop_value(prop);
516
517                if let Expr::Object(value_obj) = &mut *value {
518                    value_obj.props = value_obj
519                        .props
520                        .take()
521                        .into_iter()
522                        .fold(vec![], |acc, p| self.reduce_object_properties(acc, p));
523
524                    set_value_of_prop(prop, value);
525                    acc.push(property);
526                } else if !matches!(&*value, Expr::Lit(..)) {
527                    // if a non-primitive value we have to interpolate it
528
529                    self.replace_object_with_prop_function = true;
530
531                    let identifier = get_local_identifier(self.identifier_idx, &value);
532
533                    self.extra_attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
534                        span: DUMMY_SP,
535                        name: JSXAttrName::Ident(identifier.clone()),
536                        value: Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
537                            span: DUMMY_SP,
538                            expr: JSXExpr::Expr(value.take()),
539                        })),
540                    }));
541
542                    let key = get_prop_name2(prop);
543
544                    acc.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
545                        key,
546                        value: self.p.clone().make_member(identifier).into(),
547                    }))));
548                } else {
549                    set_value_of_prop(prop, value);
550                    acc.push(property);
551                }
552            }
553            #[cfg(swc_ast_unknown)]
554            _ => panic!("unknown node"),
555        }
556
557        acc
558    }
559}
560
561fn set_value_of_prop(prop: &mut Prop, value: Box<Expr>) {
562    match prop {
563        Prop::Shorthand(p) => {
564            *prop = Prop::KeyValue(KeyValueProp {
565                key: PropName::Ident(p.clone().into()),
566                value,
567            });
568        }
569        Prop::KeyValue(p) => {
570            p.value = value;
571        }
572        Prop::Assign(..) => unreachable!("assign property is not allowed for object literals"),
573        Prop::Getter(_p) => todo!(),
574        Prop::Setter(_p) => todo!(),
575        Prop::Method(_p) => todo!(),
576        #[cfg(swc_ast_unknown)]
577        _ => panic!("unknown node"),
578    }
579}
580
581fn take_prop_value(prop: &mut Prop) -> Box<Expr> {
582    match prop {
583        Prop::Shorthand(p) => Box::new(Expr::Ident(p.clone())),
584        Prop::KeyValue(p) => p.value.take(),
585        Prop::Assign(..) => unreachable!("assign property is not allowed for object literals"),
586        Prop::Getter(_p) => todo!(),
587        Prop::Setter(_p) => todo!(),
588        Prop::Method(_p) => todo!(),
589        #[cfg(swc_ast_unknown)]
590        _ => panic!("unknown node"),
591    }
592}
593
594fn set_key_of_prop(prop: &mut Prop, key: Box<Expr>) {
595    let value = take_prop_value(prop);
596
597    *prop = Prop::KeyValue(KeyValueProp {
598        key: PropName::Computed(ComputedPropName {
599            span: DUMMY_SP,
600            expr: key,
601        }),
602        value,
603    });
604}
605
606fn get_local_identifier(idx: &mut usize, expr: &Expr) -> IdentName {
607    *idx += 1;
608
609    let identifier = IdentName::new(append_if_gt_one("$_css", *idx).into(), expr.span());
610
611    // TODO: Unique identifier
612
613    identifier
614}
615
616fn append_if_gt_one(s: &str, suffix: usize) -> Cow<str> {
617    if suffix > 1 {
618        Cow::Owned(format!("{s}{suffix}"))
619    } else {
620        Cow::Borrowed(s)
621    }
622}
623
624fn get_name_ident(el: &JSXElementName) -> Ident {
625    match el {
626        JSXElementName::Ident(v) => v.clone(),
627        JSXElementName::JSXMemberExpr(e) => Ident {
628            sym: format!("{}_{}", get_name_of_jsx_obj(&e.obj), e.prop.sym).into(),
629            span: e.prop.span,
630            ..Default::default()
631        },
632        _ => {
633            unimplemented!("get_name_ident for namespaced jsx element")
634        }
635    }
636}
637
638fn get_name_of_jsx_obj(el: &JSXObject) -> Atom {
639    match el {
640        JSXObject::Ident(v) => v.sym.clone(),
641        JSXObject::JSXMemberExpr(e) => {
642            format!("{}{}", get_name_of_jsx_obj(&e.obj), e.prop.sym).into()
643        }
644        #[cfg(swc_ast_unknown)]
645        _ => panic!("unknown node"),
646    }
647}
648
649fn trace_root_value(e: &Expr) -> Option<&Expr> {
650    match e {
651        Expr::Member(e) => trace_root_value(&e.obj),
652        Expr::Call(e) => match &e.callee {
653            Callee::Expr(e) => trace_root_value(e),
654            _ => None,
655        },
656        Expr::Ident(_) => Some(e),
657        Expr::Lit(_) => Some(e),
658        _ => None,
659    }
660}
661
662fn is_direct_access<F>(expr: &Expr, is_top_level_ident: &F) -> bool
663where
664    F: Fn(&Ident) -> bool,
665{
666    if let Some(root) = trace_root_value(expr) {
667        match root {
668            Expr::Lit(_) => true,
669            Expr::Ident(id) if is_top_level_ident(id) => match expr {
670                Expr::Call(CallExpr { args, .. }) => args
671                    .iter()
672                    .all(|arg| -> bool { is_direct_access(&arg.expr, is_top_level_ident) }),
673                Expr::Member(MemberExpr {
674                    prop: MemberProp::Computed(ComputedPropName { expr, .. }),
675                    ..
676                }) => is_direct_access(expr, is_top_level_ident),
677                _ => true,
678            },
679            _ => false,
680        }
681    } else {
682        false
683    }
684}