1use std::cell::RefCell;
24use std::collections::HashMap;
25
26use serde::Deserialize;
27use swc_core::common::errors::HANDLER;
28use swc_core::common::{Span, Spanned, DUMMY_SP};
29use swc_core::ecma::ast::*;
30use swc_core::ecma::atoms::Atom;
31use swc_core::ecma::visit::{VisitMut, VisitMutWith};
32
33thread_local! {
34 static CURRENT_FILE: RefCell<Option<String>> = const { RefCell::new(None) };
43}
44
45#[cfg(feature = "plugin")]
48fn set_current_file(filename: Option<String>) {
49 CURRENT_FILE.with(|c| *c.borrow_mut() = filename);
50}
51
52pub fn with_current_file<F, R>(filename: Option<String>, f: F) -> R
56where
57 F: FnOnce() -> R,
58{
59 let prev = CURRENT_FILE.with(|c| c.borrow_mut().replace(filename.unwrap_or_default()));
60 let result = f();
61 CURRENT_FILE.with(|c| *c.borrow_mut() = prev);
62 result
63}
64
65mod hash;
66mod whitespace;
67
68use hash::{interpolate_pattern, validate_pattern};
69use whitespace::normalize_whitespace;
70
71pub const DEFAULT_ID_INTERPOLATION_PATTERN: &str = "[sha512:contenthash:base64:6]";
72
73pub(crate) fn fail(span: Span, message: impl AsRef<str>) -> ! {
89 let msg = message.as_ref();
90 let filename =
91 CURRENT_FILE.with(|c| c.borrow().clone()).filter(|s| !s.is_empty());
92 let with_file = match &filename {
93 Some(f) => format!("{} (in {})", msg, f),
94 None => msg.to_string(),
95 };
96 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
97 HANDLER.with(|h| {
98 h.struct_span_err(span, &with_file).emit();
99 });
100 }));
101 panic!("swc-plugin-formatjs: {}", with_file);
102}
103
104#[derive(Debug, Default, Clone, Deserialize)]
110#[serde(rename_all = "camelCase", default)]
111pub struct Config {
112 pub id_interpolation_pattern: Option<String>,
113 pub remove_default_message: bool,
114 pub additional_component_names: Vec<String>,
115 pub additional_function_names: Vec<String>,
116 pub pragma: Option<String>,
117 pub extract_source_location: bool,
118 pub ast: bool,
119 pub preserve_whitespace: bool,
120}
121
122pub struct FormatJsTransform {
123 config: Config,
124 component_names: Vec<String>,
125 function_names: Vec<String>,
126 module_consts: HashMap<Atom, String>,
137}
138
139impl FormatJsTransform {
140 pub fn new(mut config: Config) -> Self {
141 if config.ast {
143 fail(
144 DUMMY_SP,
145 "option `ast: true` is not supported — would require a Rust port \
146 of @formatjs/icu-messageformat-parser",
147 );
148 }
149 if config.extract_source_location {
150 fail(
151 DUMMY_SP,
152 "option `extractSourceLocation: true` is not supported",
153 );
154 }
155 if let Some(p) = &config.pragma {
156 if !p.is_empty() {
157 fail(
158 DUMMY_SP,
159 format!("option `pragma: {:?}` is not supported", p),
160 );
161 }
162 }
163
164 if config.id_interpolation_pattern.is_none() {
165 config.id_interpolation_pattern =
166 Some(DEFAULT_ID_INTERPOLATION_PATTERN.to_string());
167 }
168 validate_pattern(config.id_interpolation_pattern.as_deref().unwrap());
169
170 let mut function_names = config.additional_function_names.clone();
174 for fixed in ["formatMessage", "$t", "$formatMessage"] {
175 if !function_names.iter().any(|n| n == fixed) {
176 function_names.push(fixed.to_string());
177 }
178 }
179 let mut component_names = config.additional_component_names.clone();
180 if !component_names.iter().any(|n| n == "FormattedMessage") {
181 component_names.push("FormattedMessage".to_string());
182 }
183
184 Self {
185 config,
186 component_names,
187 function_names,
188 module_consts: HashMap::new(),
189 }
190 }
191}
192
193pub fn formatjs(config: Config) -> FormatJsTransform {
194 FormatJsTransform::new(config)
195}
196
197pub fn formatjs_pass(config: Config) -> impl swc_core::ecma::ast::Pass {
203 swc_core::ecma::visit::visit_mut_pass(formatjs(config))
204}
205
206impl VisitMut for FormatJsTransform {
207 fn visit_mut_module(&mut self, n: &mut Module) {
208 self.module_consts = collect_module_consts(&n.body);
212 n.visit_mut_children_with(self);
213 }
214
215 fn visit_mut_script(&mut self, n: &mut Script) {
216 self.module_consts = collect_script_consts(&n.body);
217 n.visit_mut_children_with(self);
218 }
219
220 fn visit_mut_call_expr(&mut self, n: &mut CallExpr) {
221 n.visit_mut_children_with(self);
222 self.handle_call_expr(n);
223 }
224
225 fn visit_mut_jsx_opening_element(&mut self, n: &mut JSXOpeningElement) {
226 n.visit_mut_children_with(self);
227 self.handle_jsx_opening(n);
228 }
229}
230
231fn collect_module_consts(items: &[ModuleItem]) -> HashMap<Atom, String> {
246 let mut table = HashMap::new();
247 for item in items {
248 let var_decl = match item {
249 ModuleItem::Stmt(Stmt::Decl(Decl::Var(v))) if v.kind == VarDeclKind::Const => v,
250 ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(e)) => match &e.decl {
251 Decl::Var(v) if v.kind == VarDeclKind::Const => v,
252 _ => continue,
253 },
254 _ => continue,
255 };
256 collect_consts_from_var_decl(var_decl, &mut table);
257 }
258 table
259}
260
261fn collect_script_consts(items: &[Stmt]) -> HashMap<Atom, String> {
262 let mut table = HashMap::new();
263 for item in items {
264 if let Stmt::Decl(Decl::Var(v)) = item {
265 if v.kind == VarDeclKind::Const {
266 collect_consts_from_var_decl(v, &mut table);
267 }
268 }
269 }
270 table
271}
272
273fn collect_consts_from_var_decl(v: &VarDecl, table: &mut HashMap<Atom, String>) {
274 for decl in &v.decls {
275 let Pat::Ident(name) = &decl.name else { continue };
279 let Some(init) = &decl.init else { continue };
280 if let Some(s) = fold_string_with(init, table) {
281 table.insert(name.id.sym.clone(), s);
282 }
283 }
284}
285
286fn fold_string_with(e: &Expr, consts: &HashMap<Atom, String>) -> Option<String> {
292 let e = unwrap_ts(e);
293 match e {
294 Expr::Lit(Lit::Str(s)) => Some(s.value.to_atom_lossy().to_string()),
295
296 Expr::Tpl(t) => fold_template(t, consts),
300
301 Expr::Bin(b) if matches!(b.op, BinaryOp::Add) => {
306 let left = fold_string_with(&b.left, consts)?;
307 let right = fold_string_with(&b.right, consts)?;
308 Some(format!("{}{}", left, right))
309 }
310
311 Expr::Ident(id) => consts.get(&id.sym).cloned(),
313
314 Expr::Paren(p) => fold_string_with(&p.expr, consts),
316
317 _ => None,
318 }
319}
320
321fn fold_template(t: &Tpl, consts: &HashMap<Atom, String>) -> Option<String> {
322 if t.quasis.len() != t.exprs.len() + 1 {
324 return None;
325 }
326 let mut out = String::new();
327 for (i, q) in t.quasis.iter().enumerate() {
328 let cooked = q.cooked.as_ref()?;
329 out.push_str(&cooked.to_atom_lossy().to_string());
330 if i < t.exprs.len() {
331 let folded = fold_string_with(&t.exprs[i], consts)?;
332 out.push_str(&folded);
333 }
334 }
335 Some(out)
336}
337
338impl FormatJsTransform {
342 fn handle_call_expr(&self, n: &mut CallExpr) {
343 let Callee::Expr(callee_box) = &n.callee else { return };
344 let callee = unwrap_ts(callee_box);
345 let Some((name, _is_member)) = callee_ident_name(callee) else { return };
346
347 if name == "defineMessage" {
348 let arg = n
349 .args
350 .get_mut(0)
351 .unwrap_or_else(|| fail(n.span, "defineMessage(...) requires an argument"));
352 match unwrap_ts_mut(&mut arg.expr) {
353 Expr::Object(obj) => self.process_message_object(obj),
354 other => fail(
355 other.span(),
356 format!(
357 "defineMessage(...) argument must be an object literal — got {}",
358 expr_kind(other)
359 ),
360 ),
361 }
362 return;
363 }
364
365 if name == "defineMessages" {
366 let arg = n
367 .args
368 .get_mut(0)
369 .unwrap_or_else(|| fail(n.span, "defineMessages(...) requires an argument"));
370 match unwrap_ts_mut(&mut arg.expr) {
371 Expr::Object(outer) => {
372 for prop in outer.props.iter_mut() {
373 match prop {
374 PropOrSpread::Spread(s) => fail(
375 s.dot3_token,
376 "spread (`...rest`) inside defineMessages({...}) is not supported \
377 — expand to explicit `key: descriptor` entries",
378 ),
379 PropOrSpread::Prop(p) => {
380 let kv = match p.as_mut() {
381 Prop::KeyValue(kv) => kv,
382 Prop::Shorthand(id) => fail(
383 id.span,
384 "shorthand property inside defineMessages({...}) is not \
385 supported — expand to `key: { id, defaultMessage, ... }`",
386 ),
387 Prop::Method(m) => fail(
388 m.function.span,
389 "method property inside defineMessages({...}) is not supported",
390 ),
391 Prop::Getter(_) | Prop::Setter(_) | Prop::Assign(_) => fail(
392 DUMMY_SP,
393 "getter/setter/assign properties inside defineMessages({...}) are not supported",
394 ),
395 };
396 match unwrap_ts_mut(&mut kv.value) {
397 Expr::Object(inner) => self.process_message_object(inner),
398 other => fail(
399 other.span(),
400 format!(
401 "defineMessages bag entry must be an object literal — got {}",
402 expr_kind(other)
403 ),
404 ),
405 }
406 }
407 }
408 }
409 }
410 other => fail(
411 other.span(),
412 format!(
413 "defineMessages(...) argument must be an object literal — got {}",
414 expr_kind(other)
415 ),
416 ),
417 }
418 return;
419 }
420
421 if self.is_format_message_call(&name) {
422 let arg = match n.args.get_mut(0) {
423 None => return,
427 Some(a) => a,
428 };
429 match unwrap_ts_mut(&mut arg.expr) {
430 Expr::Object(obj) => self.process_message_object(obj),
431 _ => {}
435 }
436 }
437 }
438
439 fn is_format_message_call(&self, name: &str) -> bool {
440 self.function_names.iter().any(|n| n == name)
441 }
442
443 fn process_message_object(&self, obj: &mut ObjectLit) {
444 let mut existing_id: Option<String> = None;
445 let mut default_message: Option<String> = None;
446 let mut description: Option<String> = None;
447 let mut has_id_prop = false;
448 let mut has_default_msg_prop = false;
449
450 for prop in &obj.props {
451 match prop {
452 PropOrSpread::Spread(s) => fail(
453 s.dot3_token,
454 "spread (`...rest`) inside a message descriptor is not supported — \
455 expand to explicit `id` / `defaultMessage` / `description` properties",
456 ),
457 PropOrSpread::Prop(p) => match p.as_ref() {
458 Prop::KeyValue(kv) => {
459 let key = require_static_key(&kv.key);
460 if is_descriptor_key(&key) {
461 let val = self.require_static_string(&kv.value, &key);
462 match key.as_str() {
463 "id" => {
464 has_id_prop = true;
465 if !val.is_empty() {
466 existing_id = Some(val);
467 }
468 }
469 "defaultMessage" => {
470 has_default_msg_prop = true;
471 default_message = Some(val);
472 }
473 "description" => {
474 description = Some(val);
475 }
476 _ => unreachable!(),
477 }
478 }
479 }
481 Prop::Shorthand(id) => {
482 if is_descriptor_key(&id.sym) {
483 fail(
484 id.span,
485 format!(
486 "shorthand property `{}` in a message descriptor is not \
487 supported — expand to `{}: <string literal>`",
488 id.sym, id.sym
489 ),
490 );
491 }
492 }
494 Prop::Method(m) => {
495 let name = match &m.key {
496 PropName::Ident(i) => Some(i.sym.to_string()),
497 PropName::Str(s) => Some(s.value.to_atom_lossy().to_string()),
498 _ => None,
499 };
500 if name.as_deref().is_none()
501 || name.as_deref().map(is_descriptor_key).unwrap_or(true)
502 {
503 fail(
504 m.function.span,
505 "method property in a message descriptor is not supported",
506 );
507 }
508 }
509 Prop::Getter(g) => fail(
510 g.span,
511 "getter property in a message descriptor is not supported",
512 ),
513 Prop::Setter(s) => fail(
514 s.span,
515 "setter property in a message descriptor is not supported",
516 ),
517 Prop::Assign(a) => fail(
518 a.span,
519 "assignment property in a message descriptor is not supported",
520 ),
521 },
522 }
523 }
524
525 if !has_id_prop && !has_default_msg_prop {
528 return;
529 }
530
531 if existing_id.is_none()
537 && default_message
538 .as_deref()
539 .map(str::is_empty)
540 .unwrap_or(true)
541 {
542 fail(
543 DUMMY_SP,
544 "message descriptor has neither a non-empty `id` nor a non-empty `defaultMessage` \
545 — cannot generate an id",
546 );
547 }
548
549 let normalized_msg = default_message.as_ref().map(|m| {
550 if self.config.preserve_whitespace {
551 m.clone()
552 } else {
553 normalize_whitespace(m)
554 }
555 });
556
557 let final_id = compute_id(
558 existing_id.as_deref(),
559 normalized_msg.as_deref(),
560 description.as_deref(),
561 self.config
562 .id_interpolation_pattern
563 .as_deref()
564 .unwrap_or(DEFAULT_ID_INTERPOLATION_PATTERN),
565 );
566
567 if has_id_prop {
573 replace_object_value(obj, "id", string_expr(&final_id));
574 } else {
575 obj.props.insert(
576 0,
577 PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
578 key: PropName::Ident(IdentName {
579 span: DUMMY_SP,
580 sym: "id".into(),
581 }),
582 value: Box::new(string_expr(&final_id)),
583 }))),
584 );
585 }
586
587 obj.props.retain(|p| {
588 let PropOrSpread::Prop(prop) = p else { return true };
589 let Prop::KeyValue(kv) = prop.as_ref() else { return true };
590 prop_name_as_str(&kv.key).as_deref() != Some("description")
591 });
592
593 if self.config.remove_default_message {
594 obj.props.retain(|p| {
595 let PropOrSpread::Prop(prop) = p else { return true };
596 let Prop::KeyValue(kv) = prop.as_ref() else { return true };
597 prop_name_as_str(&kv.key).as_deref() != Some("defaultMessage")
598 });
599 } else if let Some(msg) = &normalized_msg {
600 replace_object_value(obj, "defaultMessage", string_expr(msg));
601 }
602 }
603}
604
605impl FormatJsTransform {
609 fn handle_jsx_opening(&self, n: &mut JSXOpeningElement) {
610 let JSXElementName::Ident(name_ident) = &n.name else { return };
611 if !self
612 .component_names
613 .iter()
614 .any(|c| c.as_str() == &*name_ident.sym)
615 {
616 return;
617 }
618
619 let mut existing_id: Option<String> = None;
620 let mut default_message: Option<String> = None;
621 let mut description: Option<String> = None;
622 let mut has_id_attr = false;
623 let mut has_default_message_attr = false;
624
625 for attr in &n.attrs {
626 let JSXAttrOrSpread::JSXAttr(a) = attr else { continue };
631
632 let name_str = match &a.name {
633 JSXAttrName::Ident(i) => i.sym.to_string(),
634 JSXAttrName::JSXNamespacedName(_) => continue,
636 };
637
638 if !is_descriptor_key(&name_str) {
639 continue;
640 }
641
642 let val = match &a.value {
644 None => fail(
645 a.span,
646 format!("`{}` JSX attribute requires a value", name_str),
647 ),
648 Some(JSXAttrValue::Str(s)) => s.value.to_atom_lossy().to_string(),
649 Some(JSXAttrValue::JSXExprContainer(c)) => match &c.expr {
650 JSXExpr::Expr(e) => self.fold_string(e).unwrap_or_else(|| {
651 fail(
652 c.span,
653 format!(
654 "`{}` JSX attribute must be a string literal — got a {}. \
655 Module-level `const NAME = <literal>` references DO fold; \
656 function-local consts, function calls, member access, and \
657 cross-module imports do NOT. Inline the literal or hoist the \
658 const to module scope.",
659 name_str,
660 expr_kind(e)
661 ),
662 )
663 }),
664 JSXExpr::JSXEmptyExpr(_) => fail(
665 c.span,
666 format!("`{}` JSX attribute is empty (`{{}}`)", name_str),
667 ),
668 },
669 Some(JSXAttrValue::JSXElement(e)) => fail(
670 e.span,
671 format!(
672 "`{}` JSX attribute cannot be a JSX element",
673 name_str
674 ),
675 ),
676 Some(JSXAttrValue::JSXFragment(f)) => fail(
677 f.span,
678 format!(
679 "`{}` JSX attribute cannot be a JSX fragment",
680 name_str
681 ),
682 ),
683 };
684
685 match name_str.as_str() {
686 "id" => {
687 has_id_attr = true;
688 if !val.is_empty() {
689 existing_id = Some(val);
690 }
691 }
692 "defaultMessage" => {
693 has_default_message_attr = true;
694 default_message = Some(val);
695 }
696 "description" => {
697 description = Some(val);
698 }
699 _ => unreachable!(),
700 }
701 }
702
703 if !has_default_message_attr {
707 return;
708 }
709
710 if existing_id.is_none() && default_message.as_deref() == Some("") {
712 fail(
713 n.span,
714 "<FormattedMessage> has empty `defaultMessage` and no `id` — cannot generate an id",
715 );
716 }
717
718 let normalized_msg = default_message.as_ref().map(|m| {
719 if self.config.preserve_whitespace {
720 m.clone()
721 } else {
722 normalize_whitespace(m)
723 }
724 });
725
726 let final_id = compute_id(
727 existing_id.as_deref(),
728 normalized_msg.as_deref(),
729 description.as_deref(),
730 self.config
731 .id_interpolation_pattern
732 .as_deref()
733 .unwrap_or(DEFAULT_ID_INTERPOLATION_PATTERN),
734 );
735
736 if !final_id.is_empty() {
737 if has_id_attr {
738 replace_jsx_attr_value(n, "id", string_jsx_value(&final_id));
739 } else {
740 n.attrs.insert(
741 0,
742 JSXAttrOrSpread::JSXAttr(JSXAttr {
743 span: DUMMY_SP,
744 name: JSXAttrName::Ident(IdentName {
745 span: DUMMY_SP,
746 sym: "id".into(),
747 }),
748 value: Some(string_jsx_value(&final_id)),
749 }),
750 );
751 }
752 }
753
754 n.attrs.retain(|attr| {
755 let JSXAttrOrSpread::JSXAttr(a) = attr else { return true };
756 let JSXAttrName::Ident(id) = &a.name else { return true };
757 id.sym != "description"
758 });
759
760 if self.config.remove_default_message {
761 n.attrs.retain(|attr| {
762 let JSXAttrOrSpread::JSXAttr(a) = attr else { return true };
763 let JSXAttrName::Ident(id) = &a.name else { return true };
764 id.sym != "defaultMessage"
765 });
766 }
767 let _ = &normalized_msg;
774 }
775}
776
777fn is_descriptor_key(key: &str) -> bool {
782 matches!(key, "id" | "defaultMessage" | "description")
783}
784
785fn compute_id(
786 existing_id: Option<&str>,
787 default_message: Option<&str>,
788 description: Option<&str>,
789 pattern: &str,
790) -> String {
791 if let Some(id) = existing_id {
792 if !id.is_empty() {
793 return id.to_string();
794 }
795 }
796 let Some(msg) = default_message else { return String::new() };
797 if msg.is_empty() {
798 return String::new();
799 }
800 let content = match description {
801 Some(d) if !d.is_empty() => format!("{}#{}", msg, d),
802 _ => msg.to_string(),
803 };
804 interpolate_pattern(pattern, &content)
805}
806
807fn callee_ident_name(e: &Expr) -> Option<(String, bool)> {
808 match e {
809 Expr::Ident(id) => Some((id.sym.to_string(), false)),
810 Expr::Member(m) => match &m.prop {
811 MemberProp::Ident(id) => Some((id.sym.to_string(), true)),
812 MemberProp::Computed(c) => match &*c.expr {
820 Expr::Ident(id) => Some((id.sym.to_string(), true)),
821 _ => None,
822 },
823 MemberProp::PrivateName(_) => None,
824 },
825 _ => None,
826 }
827}
828
829fn require_static_key(name: &PropName) -> String {
830 match name {
831 PropName::Ident(id) => id.sym.to_string(),
832 PropName::Str(s) => s.value.to_atom_lossy().to_string(),
833 PropName::Computed(c) => fail(
834 c.span,
835 "computed property keys (`[expr]: value`) are not supported inside a message \
836 descriptor — use a literal key (`id` / `defaultMessage` / `description`)",
837 ),
838 PropName::Num(n) => fail(
839 n.span,
840 "numeric property keys are not supported inside a message descriptor",
841 ),
842 PropName::BigInt(b) => fail(
843 b.span,
844 "bigint property keys are not supported inside a message descriptor",
845 ),
846 }
847}
848
849fn prop_name_as_str(name: &PropName) -> Option<String> {
850 match name {
853 PropName::Ident(id) => Some(id.sym.to_string()),
854 PropName::Str(s) => Some(s.value.to_atom_lossy().to_string()),
855 _ => None,
856 }
857}
858
859impl FormatJsTransform {
860 fn fold_string(&self, e: &Expr) -> Option<String> {
864 fold_string_with(e, &self.module_consts)
865 }
866
867 fn require_static_string(&self, e: &Expr, key: &str) -> String {
868 self.fold_string(e).unwrap_or_else(|| {
869 fail(
870 e.span(),
871 format!(
872 "`{}` in a message descriptor must be a string literal — got a {}. \
873 Module-level `const NAME = <literal>` references DO fold (Option 1 \
874 evaluator); function-local consts, function calls, member access, and \
875 cross-module imports do NOT. Inline the literal at the call site or \
876 hoist the const to module scope.",
877 key,
878 expr_kind(e)
879 ),
880 )
881 })
882 }
883}
884
885fn unwrap_ts(e: &Expr) -> &Expr {
886 let mut cur = e;
887 loop {
888 match cur {
889 Expr::TsAs(t) => cur = &t.expr,
890 Expr::TsTypeAssertion(t) => cur = &t.expr,
891 Expr::TsNonNull(t) => cur = &t.expr,
892 Expr::TsConstAssertion(t) => cur = &t.expr,
893 Expr::TsSatisfies(t) => cur = &t.expr,
894 _ => return cur,
895 }
896 }
897}
898
899fn unwrap_ts_mut(mut e: &mut Expr) -> &mut Expr {
900 loop {
901 let is_ts = matches!(
902 e,
903 Expr::TsAs(_)
904 | Expr::TsTypeAssertion(_)
905 | Expr::TsNonNull(_)
906 | Expr::TsConstAssertion(_)
907 | Expr::TsSatisfies(_)
908 );
909 if !is_ts {
910 return e;
911 }
912 e = match e {
913 Expr::TsAs(t) => &mut *t.expr,
914 Expr::TsTypeAssertion(t) => &mut *t.expr,
915 Expr::TsNonNull(t) => &mut *t.expr,
916 Expr::TsConstAssertion(t) => &mut *t.expr,
917 Expr::TsSatisfies(t) => &mut *t.expr,
918 _ => unreachable!(),
919 };
920 }
921}
922
923
924fn expr_kind(e: &Expr) -> &'static str {
927 match e {
928 Expr::Lit(Lit::Str(_)) => "string literal",
929 Expr::Lit(Lit::Num(_)) => "number literal",
930 Expr::Lit(Lit::Bool(_)) => "boolean literal",
931 Expr::Lit(Lit::Null(_)) => "null",
932 Expr::Lit(_) => "non-string literal",
933 Expr::Ident(_) => "identifier reference",
934 Expr::Bin(_) => "binary expression (e.g. `a + b`)",
935 Expr::Tpl(_) => "template literal with `${...}` interpolation",
936 Expr::Call(_) => "function call",
937 Expr::Member(_) => "member access (e.g. `x.y`)",
938 Expr::Cond(_) => "ternary expression",
939 Expr::Object(_) => "object literal",
940 Expr::Array(_) => "array literal",
941 Expr::Paren(_) => "parenthesised expression",
942 _ => "non-literal expression",
943 }
944}
945
946fn string_expr(s: &str) -> Expr {
947 Expr::Lit(Lit::Str(Str {
948 span: DUMMY_SP,
949 value: s.into(),
950 raw: None,
951 }))
952}
953
954fn string_jsx_value(s: &str) -> JSXAttrValue {
955 JSXAttrValue::Str(Str {
956 span: DUMMY_SP,
957 value: s.into(),
958 raw: None,
959 })
960}
961
962fn replace_object_value(obj: &mut ObjectLit, key: &str, new_value: Expr) {
963 for prop in obj.props.iter_mut() {
964 let PropOrSpread::Prop(p) = prop else { continue };
965 let Prop::KeyValue(kv) = p.as_mut() else { continue };
966 if prop_name_as_str(&kv.key).as_deref() == Some(key) {
967 kv.value = Box::new(new_value);
968 return;
969 }
970 }
971}
972
973fn replace_jsx_attr_value(n: &mut JSXOpeningElement, key: &str, new_value: JSXAttrValue) {
974 for attr in n.attrs.iter_mut() {
975 let JSXAttrOrSpread::JSXAttr(a) = attr else { continue };
976 let JSXAttrName::Ident(id) = &a.name else { continue };
977 if id.sym == key {
978 a.value = Some(new_value);
979 return;
980 }
981 }
982}
983
984#[cfg(feature = "plugin")]
988mod plugin_entry {
989 use super::*;
990 use swc_core::plugin::metadata::TransformPluginMetadataContextKind;
991 use swc_core::plugin::{metadata::TransformPluginProgramMetadata, plugin_transform};
992
993 #[plugin_transform]
994 pub fn process_transform(
995 mut program: Program,
996 metadata: TransformPluginProgramMetadata,
997 ) -> Program {
998 let filename = metadata.get_context(&TransformPluginMetadataContextKind::Filename);
1003 set_current_file(filename);
1004
1005 let raw = metadata
1006 .get_transform_plugin_config()
1007 .unwrap_or_else(|| "{}".to_string());
1008 let config: Config = match serde_json::from_str(&raw) {
1009 Ok(c) => c,
1010 Err(e) => fail(
1011 DUMMY_SP,
1012 format!("failed to parse plugin config: {}", e),
1013 ),
1014 };
1015 program.visit_mut_with(&mut FormatJsTransform::new(config));
1016
1017 set_current_file(None);
1021
1022 program
1023 }
1024}
1025
1026#[cfg(not(feature = "plugin"))]
1027#[allow(dead_code)]
1028fn _force_program_used(_: &Program) {}