Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions packages/formatjs/__tests__/wasm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <div>Loading...</div>;

function App() {
return (
<React.Suspense fallback={<Loading />}>
<div>Content</div>
</React.Suspense>
);
}
`;

// 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 (
<div>
{isLoading ? <span>Loading...</span> : <span>Done</span>}
</div>
);
}
`;

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 (
<FormattedMessage
defaultMessage="Hello <b>{name}</b>"
values={{ b: (chunks) => <b>{chunks}</b>, name: "World" }}
/>
);
}
`;

const output = await transformCode(input);
expect(output).toMatch(/defaultMessage/);
});
});
25 changes: 20 additions & 5 deletions packages/formatjs/transform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Comment on lines +993 to +999

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Support member-expression message components

visit_mut_jsx_opening_element now returns for every non-Ident tag, which fixes React.Suspense but also skips valid message components written as member expressions, e.g. import * as ReactIntl from 'react-intl'; <ReactIntl.FormattedMessage defaultMessage="..." />. Before this change those nodes still reached create_message_descriptor_from_extractor; with the new early return they never get IDs generated, descriptions removed, or messages extracted. This affects projects that use namespace imports or object-qualified wrapper components.

Useful? React with 👍 / 👎.

}

let mut descriptor = self.create_message_descriptor_from_extractor(&jsx_opening_elem.attrs);
Expand Down
Loading