Skip to main content

swc_formatjs_transform/
lib.rs

1use std::{
2    collections::{HashMap, HashSet},
3    ffi::OsStr,
4    path::Path,
5};
6
7use base64ct::{Base64, Base64UrlUnpadded, Encoding};
8use digest::DynDigest;
9use md5::Md5;
10use once_cell::sync::Lazy;
11use regex::{Captures, Regex as Regexp};
12use serde::{ser::SerializeMap, Deserialize, Serialize};
13use sha1::Sha1;
14use sha2::{Digest, Sha512};
15use swc_core::{
16    common::{
17        comments::{Comment, CommentKind, Comments},
18        source_map::SmallPos,
19        BytePos, Loc, SourceMapper, Span, Spanned, DUMMY_SP,
20    },
21    ecma::{
22        ast::{
23            ArrayLit, AssignExpr, AssignTarget, Bool, CallExpr, Callee, Expr, ExprOrSpread, Ident,
24            IdentName, JSXAttr, JSXAttrName, JSXAttrOrSpread, JSXAttrValue, JSXElementName,
25            JSXExpr, JSXExprContainer, JSXNamespacedName, JSXOpeningElement, KeyValueProp, Lit,
26            MemberProp, ModuleItem, Number, ObjectLit, Prop, PropName, PropOrSpread,
27            SimpleAssignTarget, Str,
28        },
29        visit::{noop_visit_mut_type, VisitMut, VisitMutWith},
30    },
31};
32use swc_ecma_minifier::eval::{EvalResult, Evaluator};
33use swc_icu_messageformat_parser::{Parser, ParserOptions};
34
35pub static WHITESPACE_REGEX: Lazy<Regexp> = Lazy::new(|| Regexp::new(r"\s+").unwrap());
36
37#[derive(Debug, Clone, Default, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase", default)]
39pub struct FormatJSPluginOptions {
40    pub pragma: Option<String>,
41    pub remove_default_message: bool,
42    pub id_interpolation_pattern: Option<String>,
43    pub ast: bool,
44    pub extract_source_location: bool,
45    pub preserve_whitespace: bool,
46    pub __debug_extracted_messages_comment: bool,
47    pub additional_function_names: Vec<String>,
48    pub additional_component_names: Vec<String>,
49}
50
51fn evaluate_expr(expr: &Expr, evaluator: &mut Evaluator) -> Option<String> {
52    let result = match expr {
53        Expr::Tpl(tpl) => evaluator.eval_tpl(tpl),
54        _ => evaluator.eval(expr),
55    };
56
57    match result {
58        Some(EvalResult::Lit(Lit::Str(s))) => {
59            Some(s.value.as_str().expect("non-utf8 string").to_string())
60        }
61        _ => {
62            emit_non_evaluable_error(expr.span());
63            None
64        }
65    }
66}
67
68trait MessageDescriptorExtractor {
69    fn get_key_value_with_visitor(
70        &self,
71        _visitor: &mut FormatJSVisitor<impl Clone + Comments, impl SourceMapper>,
72    ) -> Option<(String, MessageDescriptionValue)> {
73        None
74    }
75    fn is_jsx(&self) -> bool {
76        false
77    }
78}
79
80impl MessageDescriptorExtractor for JSXAttrOrSpread {
81    fn get_key_value_with_visitor(
82        &self,
83        visitor: &mut FormatJSVisitor<impl Clone + Comments, impl SourceMapper>,
84    ) -> Option<(String, MessageDescriptionValue)> {
85        if let JSXAttrOrSpread::JSXAttr(JSXAttr {
86            name,
87            value: Some(value),
88            ..
89        }) = self
90        {
91            let key = match name {
92                JSXAttrName::Ident(name)
93                | JSXAttrName::JSXNamespacedName(JSXNamespacedName { name, .. }) => {
94                    Some(name.sym.to_string())
95                }
96            };
97            let value = match value {
98                JSXAttrValue::Str(s) => Some(MessageDescriptionValue::Str(
99                    s.value.as_str().expect("non-utf8 string").to_string(),
100                )),
101                JSXAttrValue::JSXExprContainer(container) => {
102                    if let JSXExpr::Expr(expr) = &container.expr {
103                        match &**expr {
104                            Expr::Ident(ident) => {
105                                let resolved = visitor.resolve_identifier(ident);
106                                if let Some(resolved_expr) = resolved {
107                                    match resolved_expr {
108                                        Expr::Object(object_lit) => {
109                                            Some(MessageDescriptionValue::Obj(object_lit))
110                                        }
111                                        expr => evaluate_expr(&expr, visitor.evaluator)
112                                            .map(MessageDescriptionValue::Str),
113                                    }
114                                } else {
115                                    None
116                                }
117                            }
118                            Expr::Object(obj) => Some(MessageDescriptionValue::Obj(obj.clone())),
119                            expr => evaluate_expr(expr, visitor.evaluator)
120                                .map(MessageDescriptionValue::Str),
121                        }
122                    } else {
123                        None
124                    }
125                }
126                _ => None,
127            };
128
129            if let (Some(key), Some(value)) = (key, value) {
130                Some((key, value))
131            } else {
132                None
133            }
134        } else {
135            None
136        }
137    }
138
139    fn is_jsx(&self) -> bool {
140        true
141    }
142}
143
144impl MessageDescriptorExtractor for PropOrSpread {
145    fn get_key_value_with_visitor(
146        &self,
147        visitor: &mut FormatJSVisitor<impl Clone + Comments, impl SourceMapper>,
148    ) -> Option<(String, MessageDescriptionValue)> {
149        if let PropOrSpread::Prop(prop) = self {
150            if let Prop::KeyValue(key_value) = &**prop {
151                let key = match &key_value.key {
152                    PropName::Computed(prop_name) => {
153                        evaluate_expr(&prop_name.expr, visitor.evaluator)
154                    }
155                    PropName::Ident(ident) => Some(ident.sym.to_string()),
156                    PropName::Str(s) => {
157                        Some(s.value.as_str().expect("non-utf8 string").to_string())
158                    }
159                    prop_name => {
160                        emit_non_evaluable_error(prop_name.span());
161                        None
162                    }
163                };
164                let value = match &*key_value.value {
165                    Expr::Object(obj) => Some(MessageDescriptionValue::Obj(obj.clone())),
166                    expr => {
167                        evaluate_expr(expr, visitor.evaluator).map(MessageDescriptionValue::Str)
168                    }
169                };
170                if let (Some(key), Some(value)) = (key, value) {
171                    Some((key, value))
172                } else {
173                    None
174                }
175            } else {
176                None
177            }
178        } else {
179            None
180        }
181    }
182
183    fn is_jsx(&self) -> bool {
184        false
185    }
186}
187
188#[derive(Debug, Clone, Default)]
189pub struct MessageDescriptor {
190    id: Option<String>,
191    default_message: Option<String>,
192    description: Option<MessageDescriptionValue>,
193    ast: Option<Box<Expr>>,
194}
195
196fn parse(source: &str) -> Result<Box<Expr>, swc_icu_messageformat_parser::Error> {
197    let options = ParserOptions {
198        should_parse_skeletons: true,
199        requires_other_clause: true,
200        ..ParserOptions::default()
201    };
202    let mut parser = Parser::new(source, &options);
203    match parser.parse() {
204        Ok(parsed) => {
205            let v = serde_json::to_value(&parsed).unwrap();
206            Ok(json_value_to_expr(&v))
207        }
208        Err(e) => Err(e),
209    }
210}
211
212// TODO: consolidate with get_message_descriptor_key_from_call_expr?
213fn get_message_descriptor_key_from_jsx(name: &JSXAttrName) -> &str {
214    match name {
215        JSXAttrName::Ident(name)
216        | JSXAttrName::JSXNamespacedName(JSXNamespacedName { name, .. }) => &name.sym,
217        #[cfg(swc_ast_unknown)]
218        _ => panic!("unknown node"),
219    }
220
221    // NOTE: Do not support evaluatePath()
222}
223
224fn get_message_descriptor_key_from_call_expr(name: &PropName) -> Option<&str> {
225    match name {
226        PropName::Ident(name) => Some(&*name.sym),
227        PropName::Str(name) => Some(name.value.as_str().expect("non-utf8 prop name")),
228        _ => None,
229    }
230
231    // NOTE: Do not support evaluatePath()
232}
233
234#[derive(Debug, Clone, Deserialize)]
235pub enum MessageDescriptionValue {
236    Str(String),
237    Obj(ObjectLit),
238}
239
240impl Serialize for MessageDescriptionValue {
241    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
242    where
243        S: serde::Serializer,
244    {
245        match self {
246            MessageDescriptionValue::Str(s) => serializer.serialize_str(s),
247            // NOTE: this is good enough to barely pass key-value object serialization. Not a
248            // complete implementation.
249            MessageDescriptionValue::Obj(obj) => {
250                let mut state = serializer.serialize_map(Some(obj.props.len()))?;
251                for prop in &obj.props {
252                    match prop {
253                        PropOrSpread::Prop(prop) => {
254                            match &**prop {
255                                Prop::KeyValue(key_value) => {
256                                    let key = match &key_value.key {
257                                        PropName::Ident(ident) => ident.sym.to_string(),
258                                        PropName::Str(s) => s.value.to_atom_lossy().to_string(),
259                                        _ => {
260                                            //unexpected
261                                            continue;
262                                        }
263                                    };
264                                    let value = match &*key_value.value {
265                                        Expr::Lit(Lit::Str(s)) => {
266                                            s.value.to_atom_lossy().to_string()
267                                        }
268                                        _ => {
269                                            //unexpected
270                                            continue;
271                                        }
272                                    };
273                                    state.serialize_entry(&key, &value)?;
274                                }
275                                _ => {
276                                    //unexpected
277                                    continue;
278                                }
279                            }
280                        }
281                        _ => {
282                            //unexpected
283                            continue;
284                        }
285                    }
286                }
287                state.end()
288            }
289        }
290    }
291}
292
293fn interpolate_name(filename: &str, interpolate_pattern: &str, content: &str) -> Option<String> {
294    let mut resource_path = filename.to_string();
295    let mut basename = "file";
296
297    let path = Path::new(filename);
298    let parent = path.parent();
299    if let Some(parent) = parent {
300        let parent_str = parent.to_str().unwrap();
301        if !parent_str.is_empty() {
302            basename = path.file_stem()?.to_str().unwrap();
303            resource_path = format!("{parent_str}/");
304        }
305    }
306
307    let mut directory: String;
308    directory = resource_path.replace("\\", "/").to_owned();
309    directory = Regexp::new(r#"\.\.(/)?"#)
310        .unwrap()
311        .replace(directory.as_str(), "_$1")
312        .to_string();
313
314    let folder = match directory.len() {
315        0 | 1 => {
316            directory = "".to_string();
317            ""
318        }
319        _ => Path::new(&directory)
320            .file_name()
321            .and_then(OsStr::to_str)
322            .unwrap_or(""),
323    };
324
325    let mut url = interpolate_pattern.to_string();
326    let r =
327        Regexp::new(r#"\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z][a-z0-9]*))?(?::(\d+))?\]"#)
328            .unwrap();
329
330    url = r
331        .replace(url.as_str(), |cap: &Captures| {
332            let hash_type = cap.get(1);
333            let digest_encoding_type = cap.get(2);
334            let max_length = cap.get(3);
335
336            // TODO: support more hash_types than md5, sha1 and sha512
337            let mut hasher: Box<dyn DynDigest> = match hash_type {
338                Some(hash_type) if hash_type.as_str() == "md5" => Box::new(Md5::new()),
339                Some(hash_type) if hash_type.as_str() == "sha1" => Box::new(Sha1::new()),
340                _ => Box::new(Sha512::new()),
341            };
342            hasher.update(content.as_bytes());
343            let hash = hasher.finalize();
344            let encoded_hash = match digest_encoding_type.map(|m| m.as_str()) {
345                Some("base64") => Base64::encode_string(&hash),
346                Some("base64url") => Base64UrlUnpadded::encode_string(&hash),
347                Some("hex") | None => hex::encode(&hash),
348                Some(other) => {
349                    swc_core::plugin::errors::HANDLER.with(|handler| {
350                        handler.warn(&format!(
351                            "[React Intl] Unsupported encoding type `{other}` in \
352                             `idInterpolationPattern`, must be one of `hex`, `base64`, or \
353                             `base64url`."
354                        ))
355                    });
356
357                    hex::encode(&hash)
358                }
359            };
360
361            if let Some(max_length) = max_length {
362                encoded_hash[0..max_length.as_str().parse::<usize>().unwrap()].to_string()
363            } else {
364                encoded_hash
365            }
366        })
367        .to_string();
368
369    url = Regexp::new(r#"\[(ext|name|path|folder|query)\]"#)
370        .unwrap()
371        .replace_all(url.as_str(), |cap: &Captures| {
372            if let Some(placeholder) = cap.get(1) {
373                match placeholder.as_str() {
374                    "ext" => {
375                        if let Some(extension) = path.extension() {
376                            extension.to_str().unwrap()
377                        } else {
378                            "bin"
379                        }
380                    }
381                    "name" => basename,
382                    "path" => directory.as_str(),
383                    "folder" => folder,
384                    "query" => "",
385                    _ => panic!("unreachable"),
386                }
387            } else {
388                ""
389            }
390        })
391        .to_string();
392
393    Some(url)
394}
395
396fn store_message(
397    messages: &mut Vec<ExtractedMessage>,
398    descriptor: &MessageDescriptor,
399    filename: &str,
400    location: Option<(Loc, Loc)>,
401) {
402    if descriptor.id.is_none() && descriptor.default_message.is_none() {
403        let handler = &swc_core::plugin::errors::HANDLER;
404
405        handler.with(|handler| {
406            handler
407                .struct_err("[React Intl] Message Descriptors require an `id` or `defaultMessage`.")
408                .emit()
409        });
410    }
411
412    let source_location = if let Some(location) = location {
413        let (start, end) = location;
414
415        // NOTE: this is not fully identical to babel's test snapshot output
416        Some(SourceLocation {
417            file: filename.to_string(),
418            start: Location {
419                line: start.line,
420                col: start.col.to_usize(),
421            },
422            end: Location {
423                line: end.line,
424                col: end.col.to_usize(),
425            },
426        })
427    } else {
428        None
429    };
430
431    messages.push(ExtractedMessage {
432        id: descriptor
433            .id
434            .as_ref()
435            .unwrap_or(&"".to_string())
436            .to_string(),
437        default_message: descriptor
438            .default_message
439            .as_ref()
440            .expect("Should be available")
441            .clone(),
442        description: descriptor.description.clone(),
443        loc: source_location,
444    });
445}
446
447fn get_message_object_from_expression(expr: Option<&mut ExprOrSpread>) -> Option<&mut Expr> {
448    if let Some(expr) = expr {
449        let expr = &mut *expr.expr;
450        Some(expr)
451    } else {
452        None
453    }
454}
455
456fn assert_object_expression(expr: &Option<&mut Expr>, callee: &Callee) {
457    let assert_fail = match expr {
458        Some(expr) => !expr.is_object(),
459        _ => true,
460    };
461
462    if assert_fail {
463        let prop = if let Callee::Expr(expr) = callee {
464            if let Expr::Ident(ident) = &**expr {
465                Some(ident.sym.to_string())
466            } else {
467                None
468            }
469        } else {
470            None
471        };
472
473        let handler = &swc_core::plugin::errors::HANDLER;
474
475        handler.with(|handler| {
476            handler
477                .struct_err(
478                    &(format!(
479                        r#"[React Intl] `{}` must be called with an object expression
480                        with values that are React Intl Message Descriptors,
481                        also defined as object expressions."#,
482                        prop.unwrap_or_default()
483                    )),
484                )
485                .emit()
486        });
487    }
488}
489
490#[derive(Debug, Clone, Default, Serialize, Deserialize)]
491#[serde(rename_all = "camelCase", default)]
492pub struct ExtractedMessage {
493    pub id: String,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub description: Option<MessageDescriptionValue>,
496    pub default_message: String,
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub loc: Option<SourceLocation>,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
502#[serde(rename_all = "camelCase")]
503pub struct SourceLocation {
504    pub file: String,
505    pub start: Location,
506    pub end: Location,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase")]
511pub struct Location {
512    pub line: usize,
513    pub col: usize,
514}
515
516pub struct FormatJSVisitor<'a, C: Clone + Comments, S: SourceMapper> {
517    // We may not need Arc in the plugin context - this is only to preserve isomorphic interface
518    // between plugin & custom transform pass.
519    source_map: std::sync::Arc<S>,
520    comments: C,
521    options: FormatJSPluginOptions,
522    filename: String,
523    messages: Vec<ExtractedMessage>,
524    meta: HashMap<String, String>,
525    component_names: HashSet<String>,
526    function_names: HashSet<String>,
527    evaluator: &'a mut Evaluator,
528}
529
530impl<'a, C: Clone + Comments, S: SourceMapper> FormatJSVisitor<'a, C, S> {
531    fn new(
532        source_map: std::sync::Arc<S>,
533        comments: C,
534        plugin_options: FormatJSPluginOptions,
535        filename: &str,
536        evaluator: &'a mut Evaluator,
537    ) -> Self {
538        let mut function_names: HashSet<String> = Default::default();
539        plugin_options
540            .additional_function_names
541            .iter()
542            .for_each(|name| {
543                function_names.insert(name.to_string());
544            });
545        function_names.insert("formatMessage".to_string());
546        function_names.insert("$formatMessage".to_string());
547
548        let mut component_names: HashSet<String> = Default::default();
549        component_names.insert("FormattedMessage".to_string());
550        plugin_options
551            .additional_component_names
552            .iter()
553            .for_each(|name| {
554                component_names.insert(name.to_string());
555            });
556
557        FormatJSVisitor {
558            source_map,
559            comments,
560            options: plugin_options,
561            filename: filename.to_string(),
562            messages: Default::default(),
563            meta: Default::default(),
564            component_names,
565            function_names,
566            evaluator,
567        }
568    }
569
570    fn read_pragma(&mut self, span_lo: BytePos, span_hi: BytePos) {
571        if let Some(pragma) = &self.options.pragma {
572            let mut comments = self.comments.get_leading(span_lo).unwrap_or_default();
573            comments.append(&mut self.comments.get_leading(span_hi).unwrap_or_default());
574
575            let pragma = pragma.as_str();
576
577            for comment in comments {
578                let comment_text = &*comment.text;
579                if comment_text.contains(pragma) {
580                    let value = comment_text.split(pragma).nth(1);
581                    if let Some(value) = value {
582                        let value = WHITESPACE_REGEX.split(value.trim());
583                        for kv in value {
584                            let mut kv = kv.split(":");
585                            if let Some(k) = kv.next() {
586                                if let Some(v) = kv.next() {
587                                    self.meta.insert(k.to_string(), v.to_string());
588                                }
589                            }
590                        }
591                    }
592                }
593            }
594        }
595    }
596
597    fn resolve_identifier(&mut self, ident: &Ident) -> Option<Expr> {
598        self.evaluator.resolve_identifier(ident).as_deref().cloned()
599    }
600
601    fn create_message_descriptor_from_extractor<T: MessageDescriptorExtractor>(
602        &mut self,
603        nodes: &Vec<T>,
604    ) -> MessageDescriptor {
605        let mut ret = MessageDescriptor::default();
606        for node in nodes {
607            let Some((key, value)) = node.get_key_value_with_visitor(self) else {
608                continue;
609            };
610
611            match key.as_str() {
612                "id" => {
613                    if let MessageDescriptionValue::Str(s) = value {
614                        ret.id = Some(s)
615                    }
616                }
617                "defaultMessage" => {
618                    if let MessageDescriptionValue::Str(s) = value {
619                        ret.default_message = Some(s)
620                    }
621                }
622                "description" => {
623                    ret.description = match value {
624                        MessageDescriptionValue::Str(s) => Some(MessageDescriptionValue::Str(s)),
625                        MessageDescriptionValue::Obj(obj) => {
626                            // When description is an object, we need to resolve it
627                            Some(MessageDescriptionValue::Obj(obj))
628                        }
629                    };
630                }
631                _ => {
632                    // ignore other keys
633                }
634            }
635        }
636
637        let message = ret.default_message.as_deref().unwrap_or("");
638
639        let message = if !self.options.preserve_whitespace {
640            let replaced = WHITESPACE_REGEX.replace_all(message, " ");
641            replaced.trim().to_string()
642        } else {
643            message.to_string()
644        };
645
646        match parse(message.as_str()) {
647            Err(e) => {
648                let is_literal_err = if nodes[0].is_jsx() {
649                    message.contains("\\\\")
650                } else {
651                    false
652                };
653
654                let handler = &swc_core::plugin::errors::HANDLER;
655
656                if is_literal_err {
657                    {
658                        handler.with(|handler| {
659                            handler
660                                .struct_err(
661                                    r#"
662                        [React Intl] Message failed to parse.
663                        It looks like `\\`s were used for escaping,
664                        this won't work with JSX string literals.
665                        Wrap with `{{}}`.
666                        See: http://facebook.github.io/react/docs/jsx-gotchas.html
667                        "#,
668                                )
669                                .emit()
670                        });
671                    }
672                } else {
673                    {
674                        handler.with(|handler| {
675                            handler
676                                .struct_warn(
677                                    r#"
678                        [React Intl] Message failed to parse.
679                        See: https://formatjs.io/docs/core-concepts/icu-syntax
680                        \n {:#?}
681                        "#,
682                                )
683                                .emit();
684                            handler
685                                .struct_err(&format!("SyntaxError: {}", e.kind))
686                                .emit()
687                        });
688                    }
689                }
690            }
691            Ok(ast) => {
692                ret.ast = Some(ast);
693            }
694        }
695
696        ret.default_message = Some(message);
697
698        ret
699    }
700
701    fn evaluate_message_descriptor(&mut self, descriptor: &mut MessageDescriptor) {
702        let id = &descriptor.id;
703        let default_message = descriptor.default_message.clone().unwrap_or_default();
704
705        let description = descriptor.description.clone();
706
707        // Note: do not support override fn
708        let id = if id.is_none() && !default_message.is_empty() {
709            let interpolate_pattern = self
710                .options
711                .id_interpolation_pattern
712                .clone()
713                .unwrap_or("[sha512:contenthash:base64:6]".to_string());
714
715            let content = match &description {
716                Some(MessageDescriptionValue::Str(description)) => {
717                    format!("{default_message}#{description}")
718                }
719                Some(MessageDescriptionValue::Obj(obj)) => {
720                    // When description is an object, stringify it for the hash calculation
721                    let mut map = std::collections::BTreeMap::new();
722                    // Extract and convert properties in one pass
723                    for prop in &obj.props {
724                        if let PropOrSpread::Prop(prop) = prop {
725                            if let Prop::KeyValue(key_value) = &**prop {
726                                let key_str = match &key_value.key {
727                                    PropName::Ident(ident) => ident.sym.to_string(),
728                                    PropName::Str(s) => s.value.to_atom_lossy().to_string(),
729                                    _ => continue,
730                                };
731                                let value = match &*key_value.value {
732                                    Expr::Ident(ident) => {
733                                        // If this is a variable reference, resolve it
734                                        if let Some(resolved_expr) = self.resolve_identifier(ident)
735                                        {
736                                            match resolved_expr {
737                                                Expr::Lit(Lit::Str(s)) => {
738                                                    serde_json::Value::String(
739                                                        s.value
740                                                            .as_str()
741                                                            .expect("non-utf8 string")
742                                                            .to_string(),
743                                                    )
744                                                }
745                                                Expr::Lit(Lit::Num(n)) => {
746                                                    serde_json::Number::from_f64(n.value)
747                                                        .map(serde_json::Value::Number)
748                                                        .unwrap_or(serde_json::Value::Null)
749                                                }
750                                                Expr::Lit(Lit::Bool(b)) => {
751                                                    serde_json::Value::Bool(b.value)
752                                                }
753                                                _ => continue,
754                                            }
755                                        } else {
756                                            continue;
757                                        }
758                                    }
759                                    Expr::Lit(Lit::Str(s)) => serde_json::Value::String(
760                                        s.value.to_atom_lossy().to_string(),
761                                    ),
762                                    Expr::Lit(Lit::Num(n)) => serde_json::Number::from_f64(n.value)
763                                        .map(serde_json::Value::Number)
764                                        .unwrap_or(serde_json::Value::Null),
765                                    Expr::Lit(Lit::Bool(b)) => serde_json::Value::Bool(b.value),
766                                    _ => continue,
767                                };
768
769                                map.insert(key_str, value);
770                            }
771                        }
772                    }
773
774                    // Convert BTreeMap to JSON object with keys already sorted
775                    let json_obj = map
776                        .into_iter()
777                        .collect::<serde_json::Map<String, serde_json::Value>>();
778                    let obj_value = serde_json::Value::Object(json_obj);
779                    let desc_json = serde_json::to_string(&obj_value).unwrap_or_default();
780                    format!("{default_message}#{desc_json}")
781                }
782                _ => default_message.clone(),
783            };
784
785            interpolate_name(&self.filename, &interpolate_pattern, &content)
786        } else {
787            id.clone()
788        };
789
790        descriptor.id = id;
791    }
792
793    fn process_message_object(&mut self, message_descriptor: &mut Option<&mut Expr>) {
794        let Some(message_obj) = &mut *message_descriptor else {
795            return;
796        };
797        let (lo, hi) = (message_obj.span().lo, message_obj.span().hi);
798        let Expr::Object(obj) = *message_obj else {
799            return;
800        };
801
802        let properties = &obj.props;
803
804        let mut descriptor = self.create_message_descriptor_from_extractor(properties);
805
806        // If the message is already compiled, don't re-compile it
807        if descriptor.default_message.is_none() {
808            return;
809        }
810
811        self.evaluate_message_descriptor(&mut descriptor);
812
813        let source_location = if self.options.extract_source_location {
814            Some((
815                self.source_map.lookup_char_pos(lo),
816                self.source_map.lookup_char_pos(hi),
817            ))
818        } else {
819            None
820        };
821
822        store_message(
823            &mut self.messages,
824            &descriptor,
825            &self.filename,
826            source_location,
827        );
828
829        // let first_prop = properties.first().is_some();
830
831        // Insert ID potentially 1st before removing nodes
832        let id_prop = obj.props.iter().find(|prop| {
833            if let PropOrSpread::Prop(prop) = prop {
834                if let Prop::KeyValue(kv) = &**prop {
835                    return match &kv.key {
836                        PropName::Ident(ident) => &*ident.sym == "id",
837                        PropName::Str(str_) => &*str_.value == "id",
838                        _ => false,
839                    };
840                }
841            }
842            false
843        });
844
845        if let Some(descriptor_id) = descriptor.id {
846            if let Some(id_prop) = id_prop {
847                let prop = id_prop.as_prop().unwrap();
848                let kv = &mut prop.as_key_value().unwrap();
849                kv.to_owned().value = Box::new(Expr::Lit(Lit::Str(Str {
850                    span: DUMMY_SP,
851                    value: descriptor_id.into(),
852                    raw: None,
853                })));
854            } else {
855                obj.props.insert(
856                    0,
857                    PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
858                        key: PropName::Ident(IdentName::new("id".into(), DUMMY_SP)),
859                        value: Box::new(Expr::Lit(Lit::Str(Str {
860                            span: DUMMY_SP,
861                            value: descriptor_id.into(),
862                            raw: None,
863                        }))),
864                    }))),
865                )
866            }
867        }
868
869        let mut props = vec![];
870        for prop in obj.props.drain(..) {
871            match prop {
872                PropOrSpread::Prop(mut prop) => {
873                    if let Prop::KeyValue(keyvalue) = &mut *prop {
874                        let key = get_message_descriptor_key_from_call_expr(&keyvalue.key);
875                        if let Some(key) = key {
876                            match key {
877                                "description" => {
878                                    // remove description
879                                    if descriptor.description.is_some() {
880                                        self.comments.take_leading(prop.span().lo);
881                                    } else {
882                                        props.push(PropOrSpread::Prop(prop));
883                                    }
884                                }
885                                // Pre-parse or remove defaultMessage
886                                "defaultMessage" => {
887                                    if self.options.remove_default_message {
888                                        // remove defaultMessage
889                                    } else {
890                                        if let Some(descriptor_default_message) =
891                                            descriptor.default_message.as_ref()
892                                        {
893                                            if self.options.ast {
894                                                if let Some(ref parsed_expr) = descriptor.ast {
895                                                    keyvalue.value = parsed_expr.clone();
896                                                }
897                                            } else {
898                                                keyvalue.value =
899                                                    Box::new(Expr::Lit(Lit::Str(Str {
900                                                        span: DUMMY_SP,
901                                                        value: descriptor_default_message
902                                                            .as_str()
903                                                            .into(),
904                                                        raw: None,
905                                                    })));
906                                            }
907                                        }
908
909                                        props.push(PropOrSpread::Prop(prop));
910                                    }
911                                }
912                                _ => props.push(PropOrSpread::Prop(prop)),
913                            }
914                        } else {
915                            props.push(PropOrSpread::Prop(prop));
916                        }
917                    } else {
918                        props.push(PropOrSpread::Prop(prop));
919                    }
920                }
921                _ => props.push(prop),
922            }
923        }
924
925        obj.props = props;
926    }
927}
928
929fn emit_non_evaluable_error(span: Span) {
930    let handler = &swc_core::plugin::errors::HANDLER;
931
932    handler.with(|handler| {
933        handler
934            .struct_span_err(
935                span,
936                "[React Intl] Messages must be statically evaluate-able for extraction.",
937            )
938            .emit()
939    });
940}
941
942impl<'a, C: Clone + Comments, S: SourceMapper> VisitMut for FormatJSVisitor<'a, C, S> {
943    noop_visit_mut_type!(fail);
944
945    fn visit_mut_assign_expr(&mut self, assign_expr: &mut AssignExpr) {
946        assign_expr.visit_mut_children_with(self);
947
948        // Track assignment expressions for React Compiler optimizations
949        // Handle patterns like: t1 = { ... }
950        if let AssignTarget::Simple(SimpleAssignTarget::Ident(ident)) = &assign_expr.left {
951            let variable_id = ident.id.to_id();
952
953            // Check if we already have a binding for this variable
954            let should_update = match self.resolve_identifier(ident) {
955                Some(existing_expr) => {
956                    // Only overwrite if the new expression is an object literal
957                    // and the existing one is not, or if both are object literals
958                    match (existing_expr, &*assign_expr.right) {
959                        (Expr::Object(_), Expr::Object(_)) => true, // Both objects, update
960                        (_, Expr::Object(_)) => true,               /* New is object, existing */
961                        // is not, update
962                        (Expr::Object(_), _) => false, /* Existing is object, new is not, don't */
963                        // update
964                        _ => true, // Neither is object, update
965                    }
966                }
967                None => true, // No existing binding, always update
968            };
969
970            if should_update {
971                self.evaluator.store(variable_id, &assign_expr.right);
972            }
973        }
974    }
975
976    fn visit_mut_jsx_opening_element(&mut self, jsx_opening_elem: &mut JSXOpeningElement) {
977        jsx_opening_elem.visit_mut_children_with(self);
978
979        let name = &jsx_opening_elem.name;
980
981        if let JSXElementName::Ident(ident) = name {
982            if !self.component_names.contains(&*ident.sym) {
983                return;
984            }
985        }
986
987        let mut descriptor = self.create_message_descriptor_from_extractor(&jsx_opening_elem.attrs);
988
989        // In order for a default message to be extracted when
990        // declaring a JSX element, it must be done with standard
991        // `key=value` attributes. But it's completely valid to
992        // write `<FormattedMessage {...descriptor} />`, because it will be
993        // skipped here and extracted elsewhere. The descriptor will
994        // be extracted only (storeMessage) if a `defaultMessage` prop.
995        if descriptor.default_message.is_none() {
996            return;
997        }
998
999        // Evaluate the Message Descriptor values in a JSX
1000        // context, then store it.
1001        self.evaluate_message_descriptor(&mut descriptor);
1002
1003        let source_location = if self.options.extract_source_location {
1004            Some((
1005                self.source_map.lookup_char_pos(jsx_opening_elem.span().lo),
1006                self.source_map.lookup_char_pos(jsx_opening_elem.span().hi),
1007            ))
1008        } else {
1009            None
1010        };
1011
1012        store_message(
1013            &mut self.messages,
1014            &descriptor,
1015            &self.filename,
1016            source_location,
1017        );
1018
1019        let id_attr = jsx_opening_elem.attrs.iter().find(|attr| match attr {
1020            JSXAttrOrSpread::JSXAttr(attr) => {
1021                if let JSXAttrName::Ident(ident) = &attr.name {
1022                    &*ident.sym == "id"
1023                } else {
1024                    false
1025                }
1026            }
1027            _ => false,
1028        });
1029
1030        let first_attr = !jsx_opening_elem.attrs.is_empty();
1031
1032        // Do not support overrideIdFn, only support idInterpolatePattern
1033        if descriptor.id.is_some() {
1034            if let Some(id_attr) = id_attr {
1035                if let JSXAttrOrSpread::JSXAttr(attr) = id_attr {
1036                    attr.to_owned().value =
1037                        Some(JSXAttrValue::Str(Str::from(descriptor.id.unwrap().clone())));
1038                }
1039            } else if first_attr {
1040                jsx_opening_elem.attrs.insert(
1041                    0,
1042                    JSXAttrOrSpread::JSXAttr(JSXAttr {
1043                        span: DUMMY_SP,
1044                        name: JSXAttrName::Ident(IdentName::new("id".into(), DUMMY_SP)),
1045                        value: Some(JSXAttrValue::Str(Str::from(descriptor.id.unwrap()))),
1046                    }),
1047                )
1048            }
1049        }
1050
1051        let mut attrs = vec![];
1052        for attr in jsx_opening_elem.attrs.drain(..) {
1053            match attr {
1054                JSXAttrOrSpread::JSXAttr(attr) => {
1055                    let key = get_message_descriptor_key_from_jsx(&attr.name);
1056                    match key {
1057                        "description" => {
1058                            // remove description
1059                            if descriptor.description.is_some() {
1060                                self.comments.take_leading(attr.span.lo);
1061                            } else {
1062                                attrs.push(JSXAttrOrSpread::JSXAttr(attr));
1063                            }
1064                        }
1065                        "defaultMessage" => {
1066                            if self.options.remove_default_message {
1067                                // remove defaultMessage
1068                            } else {
1069                                let mut attr = attr.to_owned();
1070                                if let Some(descriptor_default_message) =
1071                                    descriptor.default_message.as_ref()
1072                                {
1073                                    if self.options.ast {
1074                                        if let Some(ref parsed_expr) = descriptor.ast {
1075                                            attr.value = Some(JSXAttrValue::JSXExprContainer(
1076                                                JSXExprContainer {
1077                                                    span: DUMMY_SP,
1078                                                    expr: JSXExpr::Expr(parsed_expr.clone()),
1079                                                },
1080                                            ));
1081                                        }
1082                                    } else {
1083                                        // Only update the defaultMessage value with the evaluated
1084                                        // string
1085                                        // if the original value was a binary expression
1086                                        // (concatenation)
1087                                        // Otherwise, keep the original to preserve formatting
1088                                        let should_update = if let Some(
1089                                            JSXAttrValue::JSXExprContainer(container),
1090                                        ) = &attr.value
1091                                        {
1092                                            if let JSXExpr::Expr(expr) = &container.expr {
1093                                                matches!(&**expr, Expr::Bin(_))
1094                                            } else {
1095                                                false
1096                                            }
1097                                        } else {
1098                                            false
1099                                        };
1100
1101                                        if should_update {
1102                                            attr.value = Some(JSXAttrValue::Str(Str::from(
1103                                                descriptor_default_message.clone(),
1104                                            )));
1105                                        }
1106                                    }
1107                                }
1108                                attrs.push(JSXAttrOrSpread::JSXAttr(attr))
1109                            }
1110                        }
1111                        _ => attrs.push(JSXAttrOrSpread::JSXAttr(attr)),
1112                    }
1113                }
1114                _ => attrs.push(attr),
1115            }
1116        }
1117
1118        jsx_opening_elem.attrs = attrs.to_vec();
1119
1120        // tag_as_extracted();
1121    }
1122
1123    fn visit_mut_call_expr(&mut self, call_expr: &mut CallExpr) {
1124        call_expr.visit_mut_children_with(self);
1125
1126        let callee = &call_expr.callee;
1127        let args = &mut call_expr.args;
1128
1129        if let Callee::Expr(callee_expr) = callee {
1130            if let Expr::Ident(ident) = &**callee_expr {
1131                if &*ident.sym == "defineMessage" || &*ident.sym == "defineMessages" {
1132                    let first_arg = args.get_mut(0);
1133                    let mut message_obj = get_message_object_from_expression(first_arg);
1134
1135                    assert_object_expression(&message_obj, callee);
1136
1137                    if &*ident.sym == "defineMessage" {
1138                        self.process_message_object(&mut message_obj);
1139                    } else if let Some(Expr::Object(obj)) = message_obj {
1140                        for prop in obj.props.iter_mut() {
1141                            if let PropOrSpread::Prop(prop) = &mut *prop {
1142                                if let Prop::KeyValue(kv) = &mut **prop {
1143                                    self.process_message_object(&mut Some(&mut *kv.value));
1144                                }
1145                            }
1146                        }
1147                    }
1148                }
1149            }
1150        }
1151
1152        // Check that this is `intl.formatMessage` call
1153        if let Callee::Expr(expr) = &callee {
1154            let is_format_message_call = match &**expr {
1155                Expr::Ident(ident) if self.function_names.contains(&*ident.sym) => true,
1156                Expr::Member(member_expr) => {
1157                    if let MemberProp::Ident(ident) = &member_expr.prop {
1158                        self.function_names.contains(&*ident.sym)
1159                    } else {
1160                        false
1161                    }
1162                }
1163                _ => false,
1164            };
1165
1166            if is_format_message_call {
1167                let message_descriptor = args.get_mut(0);
1168                if let Some(message_descriptor) = message_descriptor {
1169                    if message_descriptor.expr.is_object() {
1170                        self.process_message_object(&mut Some(message_descriptor.expr.as_mut()));
1171                    }
1172                }
1173            }
1174        }
1175    }
1176
1177    fn visit_mut_module_items(&mut self, items: &mut Vec<ModuleItem>) {
1178        /*
1179        if self.is_instrumented_already() {
1180            return;
1181        }
1182        */
1183
1184        for item in items {
1185            self.read_pragma(item.span().lo, item.span().hi);
1186            item.visit_mut_children_with(self);
1187        }
1188
1189        if self.options.__debug_extracted_messages_comment {
1190            let messages_json_str =
1191                serde_json::to_string(&self.messages).expect("Should be serializable");
1192            let meta_json_str = serde_json::to_string(&self.meta).expect("Should be serializable");
1193
1194            // Append extracted messages to the end of the file as stringified JSON
1195            // comments. SWC's plugin does not support to return aribitary data
1196            // other than transformed codes, There's no way to pass extracted
1197            // messages after transform. This is not a public interface;
1198            // currently for debugging / testing purpose only.
1199            self.comments.add_trailing(
1200                Span::dummy_with_cmt().hi,
1201                Comment {
1202                    kind: CommentKind::Block,
1203                    span: Span::dummy_with_cmt(),
1204                    text: format!(
1205                        "__formatjs__messages_extracted__::{{\"messages\":{messages_json_str}, \
1206                         \"meta\":{meta_json_str}}}"
1207                    )
1208                    .into(),
1209                },
1210            );
1211        }
1212    }
1213}
1214
1215fn json_value_to_expr(json_value: &serde_json::Value) -> Box<Expr> {
1216    Box::new(match json_value {
1217        serde_json::Value::Null => {
1218            Expr::Lit(Lit::Null(swc_core::ecma::ast::Null { span: DUMMY_SP }))
1219        }
1220        serde_json::Value::Bool(v) => Expr::Lit(Lit::Bool(Bool {
1221            span: DUMMY_SP,
1222            value: *v,
1223        })),
1224        serde_json::Value::Number(v) => Expr::Lit(Lit::Num(Number {
1225            span: DUMMY_SP,
1226            raw: None,
1227            value: v.as_f64().unwrap(),
1228        })),
1229        serde_json::Value::String(v) => Expr::Lit(Lit::Str(Str {
1230            span: DUMMY_SP,
1231            raw: None,
1232            value: v.as_str().into(),
1233        })),
1234        serde_json::Value::Array(v) => Expr::Array(ArrayLit {
1235            span: DUMMY_SP,
1236            elems: v
1237                .iter()
1238                .map(|elem| {
1239                    Some(ExprOrSpread {
1240                        spread: None,
1241                        expr: json_value_to_expr(elem),
1242                    })
1243                })
1244                .collect(),
1245        }),
1246        serde_json::Value::Object(v) => Expr::Object(ObjectLit {
1247            span: DUMMY_SP,
1248            props: v
1249                .iter()
1250                .map(|(key, value)| {
1251                    PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
1252                        key: PropName::Str(Str::from(key.clone())),
1253                        value: json_value_to_expr(value),
1254                    })))
1255                })
1256                .collect(),
1257        }),
1258    })
1259}
1260
1261pub fn create_formatjs_visitor<'a, C: Clone + Comments, S: SourceMapper>(
1262    source_map: std::sync::Arc<S>,
1263    comments: C,
1264    plugin_options: FormatJSPluginOptions,
1265    filename: &str,
1266    evaluator: &'a mut Evaluator,
1267) -> FormatJSVisitor<'a, C, S> {
1268    FormatJSVisitor::new(source_map, comments, plugin_options, filename, evaluator)
1269}