diff --git a/packages/formatjs/__tests__/wasm.test.ts b/packages/formatjs/__tests__/wasm.test.ts index 29780795a..985eb9fc1 100644 --- a/packages/formatjs/__tests__/wasm.test.ts +++ b/packages/formatjs/__tests__/wasm.test.ts @@ -553,6 +553,50 @@ describe("formatjs swc plugin", () => { expect(code).toMatchSnapshot(); }); + it("should not break on JSX outside formatjs calls", async () => { + const input = ` + import React from 'react'; + + const Loading = () =>
Loading...
; + + function App() { + return ( + }> +
Content
+
+ ); + } + `; + + const output = await transformCode(input); + + // Build should succeed; no formatjs ids should be generated + expect(output).toBeTruthy(); + expect(output).not.toMatch(/id:/); + // The original JSX structure should be preserved + expect(output).toMatch(/React\.Suspense/); + expect(output).toMatch(/Loading/); + }); + + it("should not break on conditional JSX rendering outside formatjs calls", async () => { + const input = ` + import React from 'react'; + + function App({ show }) { + return show ?
Hello
: World; + } + `; + + const output = await transformCode(input); + + // Build should succeed; no formatjs ids should be generated + expect(output).toBeTruthy(); + expect(output).not.toMatch(/id:/); + // The original JSX structure should be preserved + expect(output).toMatch(/Hello/); + expect(output).toMatch(/World/); + }); + it("should generate same id even if description is an template literal string", async () => { const input1 = ` import { FormattedMessage } from 'react-intl'; diff --git a/packages/formatjs/transform/src/lib.rs b/packages/formatjs/transform/src/lib.rs index 42424290c..66678792a 100644 --- a/packages/formatjs/transform/src/lib.rs +++ b/packages/formatjs/transform/src/lib.rs @@ -94,6 +94,14 @@ impl MessageDescriptorExtractor for JSXAttrOrSpread { Some(name.sym.to_string()) } }; + // Only evaluate expressions for known formatjs attribute names to avoid + // spurious "must be statically evaluate-able" errors on unrelated attributes. + if !matches!( + key.as_deref(), + Some("id") | Some("defaultMessage") | Some("description") + ) { + return None; + } let value = match value { JSXAttrValue::Str(s) => Some(MessageDescriptionValue::Str( s.value.as_str().expect("non-utf8 string").to_string(), @@ -161,6 +169,14 @@ impl MessageDescriptorExtractor for PropOrSpread { None } }; + // Only evaluate expressions for known formatjs prop names to avoid + // spurious "must be statically evaluate-able" errors on unrelated props. + if !matches!( + key.as_deref(), + Some("id") | Some("defaultMessage") | Some("description") + ) { + return None; + } let value = match &*key_value.value { Expr::Object(obj) => Some(MessageDescriptionValue::Obj(obj.clone())), expr => { @@ -978,10 +994,15 @@ 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; + match name { + JSXElementName::Ident(ident) => { + if !self.component_names.contains(&*ident.sym) { + return; + } } + // Member expressions (e.g. React.Suspense) and namespaced names are never + // formatjs components, so skip processing their attributes entirely. + _ => return, } let mut descriptor = self.create_message_descriptor_from_extractor(&jsx_opening_elem.attrs);