1use 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 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 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 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 if let Expr::Object(css_obj) = &mut css {
203 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 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 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 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 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 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 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}