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
212fn 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 }
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 }
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 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 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 continue;
271 }
272 };
273 state.serialize_entry(&key, &value)?;
274 }
275 _ => {
276 continue;
278 }
279 }
280 }
281 _ => {
282 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 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 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 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 Some(MessageDescriptionValue::Obj(obj))
628 }
629 };
630 }
631 _ => {
632 }
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 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 let mut map = std::collections::BTreeMap::new();
722 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 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 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 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 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 if descriptor.description.is_some() {
880 self.comments.take_leading(prop.span().lo);
881 } else {
882 props.push(PropOrSpread::Prop(prop));
883 }
884 }
885 "defaultMessage" => {
887 if self.options.remove_default_message {
888 } 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 if let AssignTarget::Simple(SimpleAssignTarget::Ident(ident)) = &assign_expr.left {
951 let variable_id = ident.id.to_id();
952
953 let should_update = match self.resolve_identifier(ident) {
955 Some(existing_expr) => {
956 match (existing_expr, &*assign_expr.right) {
959 (Expr::Object(_), Expr::Object(_)) => true, (_, Expr::Object(_)) => true, (Expr::Object(_), _) => false, _ => true, }
966 }
967 None => true, };
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 if descriptor.default_message.is_none() {
996 return;
997 }
998
999 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 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 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 } 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 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 }
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 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 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 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}