diff --git a/packages/formatjs/__tests__/wasm.test.ts b/packages/formatjs/__tests__/wasm.test.ts
index 29780795a..87cca20a7 100644
--- a/packages/formatjs/__tests__/wasm.test.ts
+++ b/packages/formatjs/__tests__/wasm.test.ts
@@ -609,4 +609,63 @@ describe("formatjs swc plugin", () => {
expect(code2).toMatch(/id: "Ae\/S0P"/);
expect(code3).toMatch(/id: "Ae\/S0P"/);
});
+
+ it("should not error on valid JSX outside formatjs calls (issue #588)", async () => {
+ // Member expression JSX names like React.Suspense with JSX fallback props
+ // should not trigger the static evaluation error.
+ const input = `
+ import React from 'react';
+
+ const Loading = () =>
Loading...
;
+
+ function App() {
+ return (
+ }>
+ Content
+
+ );
+ }
+ `;
+
+ // Should succeed without throwing "must be statically evaluate-able" error
+ await expect(transformCode(input)).resolves.toBeDefined();
+ });
+
+ it("should not error on conditional JSX outside formatjs calls", async () => {
+ // Conditional JSX expressions unrelated to formatjs should not be evaluated
+ const input = `
+ import React from 'react';
+
+ function App({ isLoading }: { isLoading: boolean }) {
+ return (
+
+ {isLoading ? Loading... : Done}
+
+ );
+ }
+ `;
+
+ await expect(transformCode(input)).resolves.toBeDefined();
+ });
+
+ it("should not error on FormattedMessage with JSX values prop", async () => {
+ // FormattedMessage with a `values` prop containing JSX should not error,
+ // since `values` is not a known formatjs descriptor key.
+ const input = `
+ import React from 'react';
+ import { FormattedMessage } from 'react-intl';
+
+ function App() {
+ return (
+ {chunks}, name: "World" }}
+ />
+ );
+ }
+ `;
+
+ const output = await transformCode(input);
+ expect(output).toMatch(/defaultMessage/);
+ });
});
diff --git a/packages/formatjs/transform/src/lib.rs b/packages/formatjs/transform/src/lib.rs
index 42424290c..4450c2453 100644
--- a/packages/formatjs/transform/src/lib.rs
+++ b/packages/formatjs/transform/src/lib.rs
@@ -91,9 +91,17 @@ impl MessageDescriptorExtractor for JSXAttrOrSpread {
let key = match name {
JSXAttrName::Ident(name)
| JSXAttrName::JSXNamespacedName(JSXNamespacedName { name, .. }) => {
- Some(name.sym.to_string())
+ name.sym.to_string()
}
};
+
+ // Only evaluate values for known formatjs message descriptor keys.
+ // Attributes like `values`, `fallback`, etc. may contain JSX
+ // expressions that cannot (and should not) be statically evaluated.
+ if !matches!(key.as_str(), "id" | "defaultMessage" | "description") {
+ return None;
+ }
+
let value = match value {
JSXAttrValue::Str(s) => Some(MessageDescriptionValue::Str(
s.value.as_str().expect("non-utf8 string").to_string(),
@@ -126,7 +134,7 @@ impl MessageDescriptorExtractor for JSXAttrOrSpread {
_ => None,
};
- if let (Some(key), Some(value)) = (key, value) {
+ if let Some(value) = value {
Some((key, value))
} else {
None
@@ -978,10 +986,17 @@ impl<'a, C: Clone + Comments, S: SourceMapper> VisitMut for FormatJSVisitor<'a,
let name = &jsx_opening_elem.name;
- if let JSXElementName::Ident(ident) = name {
- if !self.component_names.contains(&*ident.sym) {
- return;
+ // Only process known formatjs component names (simple identifiers).
+ // Member expressions (e.g. `React.Suspense`) and namespaced names are
+ // never formatjs components, so return early to avoid evaluating their
+ // attributes.
+ match name {
+ JSXElementName::Ident(ident) => {
+ if !self.component_names.contains(&*ident.sym) {
+ return;
+ }
}
+ _ => return,
}
let mut descriptor = self.create_message_descriptor_from_extractor(&jsx_opening_elem.attrs);