@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 +2 -2
- package/src/transform.d.ts +4 -0
- package/src/transform.js +59 -1
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
|
|
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.
|
|
15
|
+
"@formatjs/icu-messageformat-parser": "3.2.1"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"ts-jest": "^29"
|
package/src/transform.d.ts
CHANGED
|
@@ -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
|
}
|