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);