@formatjs/ts-transformer 4.0.6 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formatjs/ts-transformer",
3
3
  "description": "TS Compiler transformer for formatjs",
4
- "version": "4.0.6",
4
+ "version": "4.1.0",
5
5
  "license": "MIT",
6
6
  "author": "Long Ho <holevietlong@gmail.com>",
7
7
  "type": "module",
@@ -12,7 +12,7 @@
12
12
  "json-stable-stringify": "^1.3.0",
13
13
  "tslib": "^2.8.0",
14
14
  "typescript": "^5.6.0",
15
- "@formatjs/icu-messageformat-parser": "3.1.1"
15
+ "@formatjs/icu-messageformat-parser": "3.2.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "ts-jest": "^29"
@@ -71,6 +71,10 @@ export interface Opts {
71
71
  * Whether to preserve whitespace and newlines.
72
72
  */
73
73
  preserveWhitespace?: boolean;
74
+ /**
75
+ * Whether to hoist selectors & flatten sentences
76
+ */
77
+ flatten?: boolean;
74
78
  }
75
79
  export declare function transformWithTs(ts: TypeScript, opts: Opts): typescript.TransformerFactory<typescript.SourceFile>;
76
80
  export declare function transform(opts: Opts): typescript.TransformerFactory<typescript.SourceFile>;
package/src/transform.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { parse } from '@formatjs/icu-messageformat-parser';
2
+ import { hoistSelectors } from '@formatjs/icu-messageformat-parser/manipulator.js';
3
+ import { printAST } from '@formatjs/icu-messageformat-parser/printer.js';
2
4
  import * as stringifyNs from 'json-stable-stringify';
3
5
  import * as typescript from 'typescript';
4
6
  import { debug } from './console_utils.js';
@@ -117,7 +119,7 @@ function evaluateStringConcat(ts, node) {
117
119
  }
118
120
  return ['', false];
119
121
  }
120
- function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocation, preserveWhitespace }, sf) {
122
+ function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocation, preserveWhitespace, flatten }, sf) {
121
123
  let properties = undefined;
122
124
  if (ts.isObjectLiteralExpression(node)) {
123
125
  properties = node.properties;
@@ -164,7 +166,15 @@ function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocatio
164
166
  }
165
167
  }
166
168
  // {id: dedent`id`}
169
+ // GH #5069: Only check for substitutions on message-related props
167
170
  else if (ts.isTaggedTemplateExpression(initializer)) {
171
+ const isMessageProp = name.text === 'id' ||
172
+ name.text === 'defaultMessage' ||
173
+ name.text === 'description';
174
+ if (!isMessageProp) {
175
+ // Skip non-message props (like tagName, values, etc.)
176
+ return;
177
+ }
168
178
  const { template } = initializer;
169
179
  if (!ts.isNoSubstitutionTemplateLiteral(template)) {
170
180
  throw new Error('Tagged template expression must be no substitution');
@@ -217,7 +227,15 @@ function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocatio
217
227
  }
218
228
  }
219
229
  // <FormattedMessage foo={dedent`dedent Hello World!`} />
230
+ // GH #5069: Only check for substitutions on message-related props
220
231
  else if (ts.isTaggedTemplateExpression(initializer.expression)) {
232
+ const isMessageProp = name.text === 'id' ||
233
+ name.text === 'defaultMessage' ||
234
+ name.text === 'description';
235
+ if (!isMessageProp) {
236
+ // Skip non-message props (like tagName, values, etc.)
237
+ return;
238
+ }
221
239
  const { expression: { template }, } = initializer;
222
240
  if (!ts.isNoSubstitutionTemplateLiteral(template)) {
223
241
  throw new Error('Tagged template expression must be no substitution');
@@ -251,6 +269,16 @@ function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocatio
251
269
  break;
252
270
  }
253
271
  }
272
+ else if (MESSAGE_DESC_KEYS.includes(name.text) &&
273
+ name.text !== 'description') {
274
+ // Non-static expression for defaultMessage or id
275
+ throw new Error(`[FormatJS] \`${name.text}\` must be a string literal or statically evaluable expression to be extracted.`);
276
+ }
277
+ }
278
+ // Non-static JSX expression for defaultMessage or id
279
+ else if (MESSAGE_DESC_KEYS.includes(name.text) &&
280
+ name.text !== 'description') {
281
+ throw new Error(`[FormatJS] \`${name.text}\` must be a string literal to be extracted.`);
254
282
  }
255
283
  }
256
284
  // {defaultMessage: 'asd' + bar'}
@@ -269,12 +297,22 @@ function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocatio
269
297
  break;
270
298
  }
271
299
  }
300
+ else if (MESSAGE_DESC_KEYS.includes(name.text) &&
301
+ name.text !== 'description') {
302
+ // Non-static expression for defaultMessage or id
303
+ throw new Error(`[FormatJS] \`${name.text}\` must be a string literal or statically evaluable expression to be extracted.`);
304
+ }
272
305
  }
273
306
  // description: {custom: 1}
274
307
  else if (ts.isObjectLiteralExpression(initializer) &&
275
308
  name.text === 'description') {
276
309
  msg.description = objectLiteralExpressionToObj(ts, initializer);
277
310
  }
311
+ // Non-static value for defaultMessage or id
312
+ else if (MESSAGE_DESC_KEYS.includes(name.text) &&
313
+ name.text !== 'description') {
314
+ throw new Error(`[FormatJS] \`${name.text}\` must be a string literal to be extracted.`);
315
+ }
278
316
  }
279
317
  });
280
318
  // We extracted nothing
@@ -284,6 +322,17 @@ function extractMessageDescriptor(ts, node, { overrideIdFn, extractSourceLocatio
284
322
  if (msg.defaultMessage && !preserveWhitespace) {
285
323
  msg.defaultMessage = msg.defaultMessage.trim().replace(/\s+/gm, ' ');
286
324
  }
325
+ // GH #3537: Apply flatten transformation before calling overrideIdFn
326
+ // so that the ID generation sees the same message format as the final output
327
+ if (flatten && msg.defaultMessage) {
328
+ try {
329
+ msg.defaultMessage = printAST(hoistSelectors(parse(msg.defaultMessage)));
330
+ }
331
+ catch (e) {
332
+ // If flatten fails, continue with original message
333
+ // The error will be caught again during validation
334
+ }
335
+ }
287
336
  if (msg.defaultMessage && overrideIdFn) {
288
337
  switch (typeof overrideIdFn) {
289
338
  case 'string':
@@ -328,6 +377,15 @@ function isMemberMethodFormatMessageCall(ts, node, additionalFunctionNames) {
328
377
  if (ts.isPropertyAccessExpression(method)) {
329
378
  return fnNames.has(method.name.text);
330
379
  }
380
+ // GH #4471: Handle foo.formatMessage<T>?.() - when both generics and optional chaining are used
381
+ // TypeScript represents this as ExpressionWithTypeArguments containing a PropertyAccessExpression
382
+ if (ts.isExpressionWithTypeArguments &&
383
+ ts.isExpressionWithTypeArguments(method)) {
384
+ const expr = method.expression;
385
+ if (ts.isPropertyAccessExpression(expr)) {
386
+ return fnNames.has(expr.name.text);
387
+ }
388
+ }
331
389
  // Handle formatMessage()
332
390
  return ts.isIdentifier(method) && fnNames.has(method.text);
333
391
  }