Skip to content

Commit 4f2efe7

Browse files
authored
fix(formatjs): support throws false (#639)
## Summary - add a `throws` option to `@swc/plugin-formatjs`, defaulting to strict behavior - skip non-static FormatJS descriptors when `throws: false` is configured, leaving skipped descriptors unchanged - keep static sibling descriptors transforming normally and document the new option ## Why This aligns the SWC plugin with `babel-plugin-formatjs` behavior for incremental migrations where some runtime-only IDs are intentionally dynamic. Fixes #634. ## Validation - `cargo check -p swc_plugin_formatjs --target wasm32-wasip1` - `PATH=/Users/kdy1/.local/state/fnm_multishells/81424_1782805620723/bin:$PATH pnpm -F @swc/plugin-formatjs test`
1 parent 14bd92c commit 4f2efe7

5 files changed

Lines changed: 260 additions & 89 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@swc/plugin-formatjs": patch
3+
---
4+
5+
Add `throws: false` support to skip non-static FormatJS descriptors without failing the transform.

packages/formatjs/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ Additional function names to extract messages from, e.g: `['']`. Use this if you
7777

7878
Pre-parse `defaultMessage` into AST for faster runtime perf. This flag doesn't do anything when `removeDefaultMessage` is `true`.
7979

80+
### **`throws`**
81+
82+
Whether to throw when a message descriptor cannot be statically evaluated for extraction. Defaults to `true`. Set to `false` to skip descriptors that fail extraction and leave them unchanged.
83+
8084
### **`preserveWhitespace`**
8185

8286
Whether to preserve whitespace and newlines.

packages/formatjs/README.tmpl.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ Additional function names to extract messages from, e.g: `['$formatMessage']`. U
7777

7878
Pre-parse `defaultMessage` into AST for faster runtime perf. This flag doesn't do anything when `removeDefaultMessage` is `true`.
7979

80+
### **`throws`**
81+
82+
Whether to throw when a message descriptor cannot be statically evaluated for extraction. Defaults to `true`. Set to `false` to skip descriptors that fail extraction and leave them unchanged.
83+
8084
### **`preserveWhitespace`**
8185

8286
Whether to preserve whitespace and newlines.

packages/formatjs/__tests__/wasm.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,106 @@ describe("formatjs swc plugin", () => {
213213
);
214214
});
215215

216+
it("should throw by default on non-static descriptor ids", async () => {
217+
const input = `
218+
import { formatMessage } from 'react-intl';
219+
220+
formatMessage({ id: backendProvidedId });
221+
`;
222+
223+
await expect(transformCode(input)).rejects.toThrow(
224+
"[React Intl] Messages must be statically evaluate-able for extraction.",
225+
);
226+
});
227+
228+
it("should skip non-static descriptors when throws is false", async () => {
229+
const input = `
230+
import React from 'react';
231+
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
232+
233+
defineMessages({
234+
staticMessage: {
235+
defaultMessage: 'Static defineMessages message',
236+
description: 'Static defineMessages description',
237+
},
238+
dynamicId: {
239+
id: window.location.hash,
240+
defaultMessage: 'Dynamic defineMessages id',
241+
},
242+
});
243+
244+
export function Example({ status }) {
245+
const intl = useIntl();
246+
247+
return (
248+
<div>
249+
{intl.formatMessage({
250+
defaultMessage: 'Static formatMessage message',
251+
description: 'Static formatMessage description',
252+
})}
253+
{intl.formatMessage({
254+
id: status,
255+
})}
256+
{intl.formatMessage({
257+
defaultMessage: getDynamicMessage(),
258+
})}
259+
{intl.formatMessage({
260+
defaultMessage: intl.formatMessage({
261+
defaultMessage: 'Nested static formatMessage message',
262+
description: 'Nested static formatMessage description',
263+
}),
264+
})}
265+
<FormattedMessage
266+
defaultMessage="Static JSX message"
267+
description="Static JSX description"
268+
/>
269+
<FormattedMessage
270+
id={\`Agent.Details.Status.\${status}\`}
271+
defaultMessage="Dynamic JSX id"
272+
/>
273+
</div>
274+
);
275+
}
276+
`;
277+
278+
const output = await transformCode(input, {
279+
ast: true,
280+
throws: false,
281+
});
282+
283+
expect(output).toContain("id: window.location.hash");
284+
expect(output).toContain("id: status");
285+
expect(output).toContain("defaultMessage: getDynamicMessage()");
286+
expect(output).toContain("Agent.Details.Status.");
287+
expect(output).toContain('defaultMessage: "Dynamic JSX id"');
288+
expect(output.match(/id: "[A-Za-z0-9+/]{6}"/g)).toHaveLength(4);
289+
expect(output.match(/defaultMessage: \[/g)).toHaveLength(4);
290+
});
291+
292+
it("should skip ICU parse errors when throws is false", async () => {
293+
const input = `
294+
import { FormattedMessage } from 'react-intl';
295+
296+
export function Example() {
297+
return (
298+
<>
299+
<FormattedMessage defaultMessage="{count, plural, one {One}}" />
300+
<FormattedMessage defaultMessage="Static after parse error" />
301+
</>
302+
);
303+
}
304+
`;
305+
306+
const output = await transformCode(input, {
307+
ast: true,
308+
throws: false,
309+
});
310+
311+
expect(output).toContain('defaultMessage: "{count, plural, one {One}}"');
312+
expect(output.match(/id: "[A-Za-z0-9+/]{6}"/g)).toHaveLength(1);
313+
expect(output.match(/defaultMessage: \[/g)).toHaveLength(1);
314+
});
315+
216316
it("should transform to ast when enabled", async () => {
217317
const input = `
218318
import { defineMessage, formatMessage, FormattedMessage } from 'react-intl';

0 commit comments

Comments
 (0)