@formatjs/ts-transformer 3.9.9 → 3.9.10
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/BUILD +82 -0
- package/CHANGELOG.md +713 -0
- package/LICENSE.md +0 -0
- package/README.md +0 -0
- package/examples/compile.ts +50 -0
- package/index.ts +3 -0
- package/integration-tests/BUILD +37 -0
- package/integration-tests/integration/comp.tsx +40 -0
- package/integration-tests/integration/jest.config.js +25 -0
- package/integration-tests/integration/ts-jest.test.tsx +32 -0
- package/integration-tests/package.json +5 -0
- package/integration-tests/vue/fixtures/index.vue +30 -0
- package/integration-tests/vue/fixtures/main.ts +4 -0
- package/integration-tests/vue/integration.test.ts +75 -0
- package/package.json +3 -3
- package/src/console_utils.ts +32 -0
- package/src/interpolate-name.ts +147 -0
- package/src/transform.ts +764 -0
- package/src/types.ts +12 -0
- package/tests/__snapshots__/index.test.ts.snap +908 -0
- package/tests/fixtures/FormattedMessage.tsx +35 -0
- package/tests/fixtures/additionalComponentNames.tsx +16 -0
- package/tests/fixtures/additionalFunctionNames.tsx +20 -0
- package/tests/fixtures/ast.tsx +83 -0
- package/tests/fixtures/defineMessages.tsx +67 -0
- package/tests/fixtures/defineMessagesPreserveWhitespace.tsx +87 -0
- package/tests/fixtures/descriptionsAsObjects.tsx +17 -0
- package/tests/fixtures/extractFromFormatMessage.tsx +45 -0
- package/tests/fixtures/extractFromFormatMessageStateless.tsx +46 -0
- package/tests/fixtures/extractSourceLocation.tsx +8 -0
- package/tests/fixtures/formatMessageCall.tsx +44 -0
- package/tests/fixtures/inline.tsx +26 -0
- package/tests/fixtures/nested.tsx +10 -0
- package/tests/fixtures/noImport.tsx +52 -0
- package/tests/fixtures/overrideIdFn.tsx +70 -0
- package/tests/fixtures/removeDefaultMessage.tsx +22 -0
- package/tests/fixtures/removeDescription.tsx +22 -0
- package/tests/fixtures/resourcePath.tsx +23 -0
- package/tests/fixtures/stringConcat.tsx +26 -0
- package/tests/fixtures/templateLiteral.tsx +21 -0
- package/tests/index.test.ts +127 -0
- package/tests/interpolate-name.test.ts +14 -0
- package/ts-jest-integration.ts +9 -0
- package/tsconfig.json +5 -0
- package/index.d.ts +0 -4
- package/index.d.ts.map +0 -1
- package/index.js +0 -6
- package/src/console_utils.d.ts +0 -4
- package/src/console_utils.d.ts.map +0 -1
- package/src/console_utils.js +0 -49
- package/src/interpolate-name.d.ts +0 -15
- package/src/interpolate-name.d.ts.map +0 -1
- package/src/interpolate-name.js +0 -95
- package/src/transform.d.ts +0 -78
- package/src/transform.d.ts.map +0 -1
- package/src/transform.js +0 -483
- package/src/types.d.ts +0 -12
- package/src/types.d.ts.map +0 -1
- package/src/types.js +0 -2
- package/ts-jest-integration.d.ts +0 -6
- package/ts-jest-integration.d.ts.map +0 -1
- package/ts-jest-integration.js +0 -10
package/src/transform.ts
ADDED
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
import * as typescript from 'typescript'
|
|
2
|
+
import {MessageDescriptor} from './types'
|
|
3
|
+
import {interpolateName} from './interpolate-name'
|
|
4
|
+
import {parse, MessageFormatElement} from '@formatjs/icu-messageformat-parser'
|
|
5
|
+
import {debug} from './console_utils'
|
|
6
|
+
import stringify from 'json-stable-stringify'
|
|
7
|
+
export type Extractor = (filePath: string, msgs: MessageDescriptor[]) => void
|
|
8
|
+
export type MetaExtractor = (
|
|
9
|
+
filePath: string,
|
|
10
|
+
meta: Record<string, string>
|
|
11
|
+
) => void
|
|
12
|
+
|
|
13
|
+
export type InterpolateNameFn = (
|
|
14
|
+
id?: MessageDescriptor['id'],
|
|
15
|
+
defaultMessage?: MessageDescriptor['defaultMessage'],
|
|
16
|
+
description?: MessageDescriptor['description'],
|
|
17
|
+
filePath?: string
|
|
18
|
+
) => string
|
|
19
|
+
|
|
20
|
+
const MESSAGE_DESC_KEYS: Array<keyof MessageDescriptor> = [
|
|
21
|
+
'id',
|
|
22
|
+
'defaultMessage',
|
|
23
|
+
'description',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
type TypeScript = typeof typescript
|
|
27
|
+
|
|
28
|
+
function primitiveToTSNode(
|
|
29
|
+
factory: typescript.NodeFactory,
|
|
30
|
+
v: string | number | boolean
|
|
31
|
+
) {
|
|
32
|
+
return typeof v === 'string'
|
|
33
|
+
? factory.createStringLiteral(v)
|
|
34
|
+
: typeof v === 'number'
|
|
35
|
+
? factory.createNumericLiteral(v + '')
|
|
36
|
+
: typeof v === 'boolean'
|
|
37
|
+
? v
|
|
38
|
+
? factory.createTrue()
|
|
39
|
+
: factory.createFalse()
|
|
40
|
+
: undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isValidIdentifier(k: string): boolean {
|
|
44
|
+
try {
|
|
45
|
+
new Function(`return {${k}:1}`)
|
|
46
|
+
return true
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function objToTSNode(factory: typescript.NodeFactory, obj: object) {
|
|
53
|
+
if (typeof obj === 'object' && !obj) {
|
|
54
|
+
return factory.createNull()
|
|
55
|
+
}
|
|
56
|
+
const props: typescript.PropertyAssignment[] = Object.entries(obj)
|
|
57
|
+
.filter(([_, v]) => typeof v !== 'undefined')
|
|
58
|
+
.map(([k, v]) =>
|
|
59
|
+
factory.createPropertyAssignment(
|
|
60
|
+
isValidIdentifier(k) ? k : factory.createStringLiteral(k),
|
|
61
|
+
primitiveToTSNode(factory, v) ||
|
|
62
|
+
(Array.isArray(v)
|
|
63
|
+
? factory.createArrayLiteralExpression(
|
|
64
|
+
v
|
|
65
|
+
.filter(n => typeof n !== 'undefined')
|
|
66
|
+
.map(n => objToTSNode(factory, n))
|
|
67
|
+
)
|
|
68
|
+
: objToTSNode(factory, v))
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
return factory.createObjectLiteralExpression(props)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function messageASTToTSNode(
|
|
75
|
+
factory: typescript.NodeFactory,
|
|
76
|
+
ast: MessageFormatElement[]
|
|
77
|
+
) {
|
|
78
|
+
return factory.createArrayLiteralExpression(
|
|
79
|
+
ast.map(el => objToTSNode(factory, el))
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function literalToObj(ts: TypeScript, n: typescript.Node) {
|
|
84
|
+
if (ts.isNumericLiteral(n)) {
|
|
85
|
+
return +n.text
|
|
86
|
+
}
|
|
87
|
+
if (ts.isStringLiteral(n)) {
|
|
88
|
+
return n.text
|
|
89
|
+
}
|
|
90
|
+
if (n.kind === ts.SyntaxKind.TrueKeyword) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
if (n.kind === ts.SyntaxKind.FalseKeyword) {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function objectLiteralExpressionToObj(
|
|
99
|
+
ts: TypeScript,
|
|
100
|
+
obj: typescript.ObjectLiteralExpression
|
|
101
|
+
): object {
|
|
102
|
+
return obj.properties.reduce((all: Record<string, any>, prop) => {
|
|
103
|
+
if (ts.isPropertyAssignment(prop) && prop.name) {
|
|
104
|
+
if (ts.isIdentifier(prop.name)) {
|
|
105
|
+
all[prop.name.escapedText.toString()] = literalToObj(
|
|
106
|
+
ts,
|
|
107
|
+
prop.initializer
|
|
108
|
+
)
|
|
109
|
+
} else if (ts.isStringLiteral(prop.name)) {
|
|
110
|
+
all[prop.name.text] = literalToObj(ts, prop.initializer)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return all
|
|
114
|
+
}, {})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface Opts {
|
|
118
|
+
/**
|
|
119
|
+
* Parse specific additional custom pragma.
|
|
120
|
+
* This allows you to tag certain file with metadata such as `project`.
|
|
121
|
+
* For example with this file:
|
|
122
|
+
* ```tsx
|
|
123
|
+
* // @intl-meta project:my-custom-project
|
|
124
|
+
* import {FormattedMessage} from 'react-intl';
|
|
125
|
+
* <FormattedMessage defaultMessage="foo" id="bar" />;
|
|
126
|
+
* ```
|
|
127
|
+
* and with option `{pragma: "@intl-meta"}`,
|
|
128
|
+
* we'll parse out `// @intl-meta project:my-custom-project`
|
|
129
|
+
* into `{project: 'my-custom-project'}` in the result file.
|
|
130
|
+
*/
|
|
131
|
+
pragma?: string
|
|
132
|
+
/**
|
|
133
|
+
* Whether the metadata about the location of the message in the source file
|
|
134
|
+
* should be extracted. If `true`, then `file`, `start`, and `end`
|
|
135
|
+
* fields will exist for each extracted message descriptors.
|
|
136
|
+
* Defaults to `false`.
|
|
137
|
+
*/
|
|
138
|
+
extractSourceLocation?: boolean
|
|
139
|
+
/**
|
|
140
|
+
* Remove `defaultMessage` field in generated js after extraction.
|
|
141
|
+
*/
|
|
142
|
+
removeDefaultMessage?: boolean
|
|
143
|
+
/**
|
|
144
|
+
* Additional component names to extract messages from,
|
|
145
|
+
* e.g: `['FormattedFooBarMessage']`.
|
|
146
|
+
*/
|
|
147
|
+
additionalComponentNames?: string[]
|
|
148
|
+
/**
|
|
149
|
+
* Additional function names to extract messages from,
|
|
150
|
+
* e.g: `['formatMessage', '$t']`
|
|
151
|
+
* Default to `['formatMessage']`
|
|
152
|
+
*/
|
|
153
|
+
additionalFunctionNames?: string[]
|
|
154
|
+
/**
|
|
155
|
+
* Callback function that gets called everytime we encountered something
|
|
156
|
+
* that looks like a MessageDescriptor
|
|
157
|
+
*
|
|
158
|
+
* @type {Extractor}
|
|
159
|
+
* @memberof Opts
|
|
160
|
+
*/
|
|
161
|
+
onMsgExtracted?: Extractor
|
|
162
|
+
/**
|
|
163
|
+
* Callback function that gets called when we successfully parsed meta
|
|
164
|
+
* declared in pragma
|
|
165
|
+
*/
|
|
166
|
+
onMetaExtracted?: MetaExtractor
|
|
167
|
+
/**
|
|
168
|
+
* webpack-style name interpolation.
|
|
169
|
+
* Can also be a string like '[sha512:contenthash:hex:6]'
|
|
170
|
+
*
|
|
171
|
+
* @type {(InterpolateNameFn | string)}
|
|
172
|
+
* @memberof Opts
|
|
173
|
+
*/
|
|
174
|
+
overrideIdFn?: InterpolateNameFn | string
|
|
175
|
+
/**
|
|
176
|
+
* Whether to compile `defaultMessage` to AST.
|
|
177
|
+
* This is no-op if `removeDefaultMessage` is `true`
|
|
178
|
+
*/
|
|
179
|
+
ast?: boolean
|
|
180
|
+
/**
|
|
181
|
+
* Whether to preserve whitespace and newlines.
|
|
182
|
+
*/
|
|
183
|
+
preserveWhitespace?: boolean
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const DEFAULT_OPTS: Omit<Opts, 'program'> = {
|
|
187
|
+
onMsgExtracted: () => undefined,
|
|
188
|
+
onMetaExtracted: () => undefined,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isMultipleMessageDecl(
|
|
192
|
+
ts: TypeScript,
|
|
193
|
+
node: typescript.CallExpression
|
|
194
|
+
) {
|
|
195
|
+
return (
|
|
196
|
+
ts.isIdentifier(node.expression) &&
|
|
197
|
+
node.expression.text === 'defineMessages'
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isSingularMessageDecl(
|
|
202
|
+
ts: TypeScript,
|
|
203
|
+
node:
|
|
204
|
+
| typescript.CallExpression
|
|
205
|
+
| typescript.JsxOpeningElement
|
|
206
|
+
| typescript.JsxSelfClosingElement,
|
|
207
|
+
additionalComponentNames: string[]
|
|
208
|
+
) {
|
|
209
|
+
const compNames = new Set([
|
|
210
|
+
'FormattedMessage',
|
|
211
|
+
'defineMessage',
|
|
212
|
+
'formatMessage',
|
|
213
|
+
'$formatMessage',
|
|
214
|
+
...additionalComponentNames,
|
|
215
|
+
])
|
|
216
|
+
let fnName = ''
|
|
217
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
|
218
|
+
fnName = node.expression.text
|
|
219
|
+
} else if (ts.isJsxOpeningElement(node) && ts.isIdentifier(node.tagName)) {
|
|
220
|
+
fnName = node.tagName.text
|
|
221
|
+
} else if (
|
|
222
|
+
ts.isJsxSelfClosingElement(node) &&
|
|
223
|
+
ts.isIdentifier(node.tagName)
|
|
224
|
+
) {
|
|
225
|
+
fnName = node.tagName.text
|
|
226
|
+
}
|
|
227
|
+
return compNames.has(fnName)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function evaluateStringConcat(
|
|
231
|
+
ts: TypeScript,
|
|
232
|
+
node: typescript.BinaryExpression
|
|
233
|
+
): [result: string, isStaticallyEvaluatable: boolean] {
|
|
234
|
+
const {right, left} = node
|
|
235
|
+
if (!ts.isStringLiteral(right)) {
|
|
236
|
+
return ['', false]
|
|
237
|
+
}
|
|
238
|
+
if (ts.isStringLiteral(left)) {
|
|
239
|
+
return [left.text + right.text, true]
|
|
240
|
+
}
|
|
241
|
+
if (ts.isBinaryExpression(left)) {
|
|
242
|
+
const [result, isStatic] = evaluateStringConcat(ts, left)
|
|
243
|
+
return [result + right.text, isStatic]
|
|
244
|
+
}
|
|
245
|
+
return ['', false]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function extractMessageDescriptor(
|
|
249
|
+
ts: TypeScript,
|
|
250
|
+
node:
|
|
251
|
+
| typescript.ObjectLiteralExpression
|
|
252
|
+
| typescript.JsxOpeningElement
|
|
253
|
+
| typescript.JsxSelfClosingElement,
|
|
254
|
+
{overrideIdFn, extractSourceLocation, preserveWhitespace}: Opts,
|
|
255
|
+
sf: typescript.SourceFile
|
|
256
|
+
): MessageDescriptor | undefined {
|
|
257
|
+
let properties:
|
|
258
|
+
| typescript.NodeArray<typescript.ObjectLiteralElement>
|
|
259
|
+
| undefined = undefined
|
|
260
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
261
|
+
properties = node.properties
|
|
262
|
+
} else if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
263
|
+
properties = node.attributes.properties
|
|
264
|
+
}
|
|
265
|
+
const msg: MessageDescriptor = {id: ''}
|
|
266
|
+
if (!properties) {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
properties.forEach(prop => {
|
|
271
|
+
const {name} = prop
|
|
272
|
+
const initializer:
|
|
273
|
+
| typescript.Expression
|
|
274
|
+
| typescript.JsxExpression
|
|
275
|
+
| undefined =
|
|
276
|
+
ts.isPropertyAssignment(prop) || ts.isJsxAttribute(prop)
|
|
277
|
+
? prop.initializer
|
|
278
|
+
: undefined
|
|
279
|
+
|
|
280
|
+
if (name && ts.isIdentifier(name) && initializer) {
|
|
281
|
+
// {id: 'id'}
|
|
282
|
+
if (ts.isStringLiteral(initializer)) {
|
|
283
|
+
switch (name.text) {
|
|
284
|
+
case 'id':
|
|
285
|
+
msg.id = initializer.text
|
|
286
|
+
break
|
|
287
|
+
case 'defaultMessage':
|
|
288
|
+
msg.defaultMessage = initializer.text
|
|
289
|
+
break
|
|
290
|
+
case 'description':
|
|
291
|
+
msg.description = initializer.text
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// {id: `id`}
|
|
296
|
+
else if (ts.isNoSubstitutionTemplateLiteral(initializer)) {
|
|
297
|
+
switch (name.text) {
|
|
298
|
+
case 'id':
|
|
299
|
+
msg.id = initializer.text
|
|
300
|
+
break
|
|
301
|
+
case 'defaultMessage':
|
|
302
|
+
msg.defaultMessage = initializer.text
|
|
303
|
+
break
|
|
304
|
+
case 'description':
|
|
305
|
+
msg.description = initializer.text
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
} else if (ts.isJsxExpression(initializer) && initializer.expression) {
|
|
309
|
+
// <FormattedMessage foo={'barbaz'} />
|
|
310
|
+
if (ts.isStringLiteral(initializer.expression)) {
|
|
311
|
+
switch (name.text) {
|
|
312
|
+
case 'id':
|
|
313
|
+
msg.id = initializer.expression.text
|
|
314
|
+
break
|
|
315
|
+
case 'defaultMessage':
|
|
316
|
+
msg.defaultMessage = initializer.expression.text
|
|
317
|
+
break
|
|
318
|
+
case 'description':
|
|
319
|
+
msg.description = initializer.expression.text
|
|
320
|
+
break
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// description={{custom: 1}}
|
|
324
|
+
else if (
|
|
325
|
+
ts.isObjectLiteralExpression(initializer.expression) &&
|
|
326
|
+
name.text === 'description'
|
|
327
|
+
) {
|
|
328
|
+
msg.description = objectLiteralExpressionToObj(
|
|
329
|
+
ts,
|
|
330
|
+
initializer.expression
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
// <FormattedMessage foo={`bar`} />
|
|
334
|
+
else if (ts.isNoSubstitutionTemplateLiteral(initializer.expression)) {
|
|
335
|
+
const {expression} = initializer
|
|
336
|
+
switch (name.text) {
|
|
337
|
+
case 'id':
|
|
338
|
+
msg.id = expression.text
|
|
339
|
+
break
|
|
340
|
+
case 'defaultMessage':
|
|
341
|
+
msg.defaultMessage = expression.text
|
|
342
|
+
break
|
|
343
|
+
case 'description':
|
|
344
|
+
msg.description = expression.text
|
|
345
|
+
break
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// <FormattedMessage foo={'bar' + 'baz'} />
|
|
349
|
+
else if (ts.isBinaryExpression(initializer.expression)) {
|
|
350
|
+
const {expression} = initializer
|
|
351
|
+
const [result, isStatic] = evaluateStringConcat(ts, expression)
|
|
352
|
+
if (isStatic) {
|
|
353
|
+
switch (name.text) {
|
|
354
|
+
case 'id':
|
|
355
|
+
msg.id = result
|
|
356
|
+
break
|
|
357
|
+
case 'defaultMessage':
|
|
358
|
+
msg.defaultMessage = result
|
|
359
|
+
break
|
|
360
|
+
case 'description':
|
|
361
|
+
msg.description = result
|
|
362
|
+
break
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// {defaultMessage: 'asd' + bar'}
|
|
368
|
+
else if (ts.isBinaryExpression(initializer)) {
|
|
369
|
+
const [result, isStatic] = evaluateStringConcat(ts, initializer)
|
|
370
|
+
if (isStatic) {
|
|
371
|
+
switch (name.text) {
|
|
372
|
+
case 'id':
|
|
373
|
+
msg.id = result
|
|
374
|
+
break
|
|
375
|
+
case 'defaultMessage':
|
|
376
|
+
msg.defaultMessage = result
|
|
377
|
+
break
|
|
378
|
+
case 'description':
|
|
379
|
+
msg.description = result
|
|
380
|
+
break
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// description: {custom: 1}
|
|
385
|
+
else if (
|
|
386
|
+
ts.isObjectLiteralExpression(initializer) &&
|
|
387
|
+
name.text === 'description'
|
|
388
|
+
) {
|
|
389
|
+
msg.description = objectLiteralExpressionToObj(ts, initializer)
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
// We extracted nothing
|
|
394
|
+
if (!msg.defaultMessage && !msg.id) {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (msg.defaultMessage && !preserveWhitespace) {
|
|
399
|
+
msg.defaultMessage = msg.defaultMessage.trim().replace(/\s+/gm, ' ')
|
|
400
|
+
}
|
|
401
|
+
if (msg.defaultMessage && overrideIdFn) {
|
|
402
|
+
switch (typeof overrideIdFn) {
|
|
403
|
+
case 'string':
|
|
404
|
+
if (!msg.id) {
|
|
405
|
+
msg.id = interpolateName(
|
|
406
|
+
{resourcePath: sf.fileName} as any,
|
|
407
|
+
overrideIdFn,
|
|
408
|
+
{
|
|
409
|
+
content: msg.description
|
|
410
|
+
? `${msg.defaultMessage}#${
|
|
411
|
+
typeof msg.description === 'string'
|
|
412
|
+
? msg.description
|
|
413
|
+
: stringify(msg.description)
|
|
414
|
+
}`
|
|
415
|
+
: msg.defaultMessage,
|
|
416
|
+
}
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
break
|
|
420
|
+
case 'function':
|
|
421
|
+
msg.id = overrideIdFn(
|
|
422
|
+
msg.id,
|
|
423
|
+
msg.defaultMessage,
|
|
424
|
+
msg.description,
|
|
425
|
+
sf.fileName
|
|
426
|
+
)
|
|
427
|
+
break
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (extractSourceLocation) {
|
|
431
|
+
return {
|
|
432
|
+
...msg,
|
|
433
|
+
file: sf.fileName,
|
|
434
|
+
start: node.pos,
|
|
435
|
+
end: node.end,
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return msg
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Check if node is `foo.bar.formatMessage` node
|
|
443
|
+
* @param node
|
|
444
|
+
* @param sf
|
|
445
|
+
*/
|
|
446
|
+
function isMemberMethodFormatMessageCall(
|
|
447
|
+
ts: TypeScript,
|
|
448
|
+
node: typescript.CallExpression,
|
|
449
|
+
additionalFunctionNames: string[]
|
|
450
|
+
) {
|
|
451
|
+
const fnNames = new Set([
|
|
452
|
+
'formatMessage',
|
|
453
|
+
'$formatMessage',
|
|
454
|
+
...additionalFunctionNames,
|
|
455
|
+
])
|
|
456
|
+
const method = node.expression
|
|
457
|
+
|
|
458
|
+
// Handle foo.formatMessage()
|
|
459
|
+
if (ts.isPropertyAccessExpression(method)) {
|
|
460
|
+
return fnNames.has(method.name.text)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Handle formatMessage()
|
|
464
|
+
return ts.isIdentifier(method) && fnNames.has(method.text)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function extractMessageFromJsxComponent(
|
|
468
|
+
ts: TypeScript,
|
|
469
|
+
factory: typescript.NodeFactory,
|
|
470
|
+
node: typescript.JsxOpeningElement | typescript.JsxSelfClosingElement,
|
|
471
|
+
opts: Opts,
|
|
472
|
+
sf: typescript.SourceFile
|
|
473
|
+
): typeof node {
|
|
474
|
+
const {onMsgExtracted} = opts
|
|
475
|
+
if (!isSingularMessageDecl(ts, node, opts.additionalComponentNames || [])) {
|
|
476
|
+
return node
|
|
477
|
+
}
|
|
478
|
+
const msg = extractMessageDescriptor(ts, node, opts, sf)
|
|
479
|
+
if (!msg) {
|
|
480
|
+
return node
|
|
481
|
+
}
|
|
482
|
+
if (typeof onMsgExtracted === 'function') {
|
|
483
|
+
onMsgExtracted(sf.fileName, [msg])
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const newProps = generateNewProperties(
|
|
487
|
+
ts,
|
|
488
|
+
factory,
|
|
489
|
+
node.attributes,
|
|
490
|
+
{
|
|
491
|
+
defaultMessage: opts.removeDefaultMessage
|
|
492
|
+
? undefined
|
|
493
|
+
: msg.defaultMessage,
|
|
494
|
+
id: msg.id,
|
|
495
|
+
},
|
|
496
|
+
opts.ast
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
if (ts.isJsxOpeningElement(node)) {
|
|
500
|
+
return factory.updateJsxOpeningElement(
|
|
501
|
+
node,
|
|
502
|
+
node.tagName,
|
|
503
|
+
node.typeArguments,
|
|
504
|
+
factory.createJsxAttributes(newProps)
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return factory.updateJsxSelfClosingElement(
|
|
509
|
+
node,
|
|
510
|
+
node.tagName,
|
|
511
|
+
node.typeArguments,
|
|
512
|
+
factory.createJsxAttributes(newProps)
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function setAttributesInObject(
|
|
517
|
+
ts: TypeScript,
|
|
518
|
+
factory: typescript.NodeFactory,
|
|
519
|
+
node: typescript.ObjectLiteralExpression,
|
|
520
|
+
msg: MessageDescriptor,
|
|
521
|
+
ast?: boolean
|
|
522
|
+
) {
|
|
523
|
+
const newProps = [
|
|
524
|
+
factory.createPropertyAssignment('id', factory.createStringLiteral(msg.id)),
|
|
525
|
+
...(msg.defaultMessage
|
|
526
|
+
? [
|
|
527
|
+
factory.createPropertyAssignment(
|
|
528
|
+
'defaultMessage',
|
|
529
|
+
ast
|
|
530
|
+
? messageASTToTSNode(factory, parse(msg.defaultMessage))
|
|
531
|
+
: factory.createStringLiteral(msg.defaultMessage)
|
|
532
|
+
),
|
|
533
|
+
]
|
|
534
|
+
: []),
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
for (const prop of node.properties) {
|
|
538
|
+
if (
|
|
539
|
+
ts.isPropertyAssignment(prop) &&
|
|
540
|
+
ts.isIdentifier(prop.name) &&
|
|
541
|
+
MESSAGE_DESC_KEYS.includes(prop.name.text as keyof MessageDescriptor)
|
|
542
|
+
) {
|
|
543
|
+
continue
|
|
544
|
+
}
|
|
545
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
546
|
+
newProps.push(prop)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return factory.createObjectLiteralExpression(
|
|
550
|
+
factory.createNodeArray(newProps)
|
|
551
|
+
)
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function generateNewProperties(
|
|
555
|
+
ts: TypeScript,
|
|
556
|
+
factory: typescript.NodeFactory,
|
|
557
|
+
node: typescript.JsxAttributes,
|
|
558
|
+
msg: MessageDescriptor,
|
|
559
|
+
ast?: boolean
|
|
560
|
+
) {
|
|
561
|
+
const newProps = [
|
|
562
|
+
factory.createJsxAttribute(
|
|
563
|
+
factory.createIdentifier('id'),
|
|
564
|
+
factory.createStringLiteral(msg.id)
|
|
565
|
+
),
|
|
566
|
+
...(msg.defaultMessage
|
|
567
|
+
? [
|
|
568
|
+
factory.createJsxAttribute(
|
|
569
|
+
factory.createIdentifier('defaultMessage'),
|
|
570
|
+
ast
|
|
571
|
+
? factory.createJsxExpression(
|
|
572
|
+
undefined,
|
|
573
|
+
messageASTToTSNode(factory, parse(msg.defaultMessage))
|
|
574
|
+
)
|
|
575
|
+
: factory.createStringLiteral(msg.defaultMessage)
|
|
576
|
+
),
|
|
577
|
+
]
|
|
578
|
+
: []),
|
|
579
|
+
]
|
|
580
|
+
for (const prop of node.properties) {
|
|
581
|
+
if (
|
|
582
|
+
ts.isJsxAttribute(prop) &&
|
|
583
|
+
ts.isIdentifier(prop.name) &&
|
|
584
|
+
MESSAGE_DESC_KEYS.includes(prop.name.text as keyof MessageDescriptor)
|
|
585
|
+
) {
|
|
586
|
+
continue
|
|
587
|
+
}
|
|
588
|
+
if (ts.isJsxAttribute(prop)) {
|
|
589
|
+
newProps.push(prop)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return newProps
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function extractMessagesFromCallExpression(
|
|
596
|
+
ts: TypeScript,
|
|
597
|
+
factory: typescript.NodeFactory,
|
|
598
|
+
node: typescript.CallExpression,
|
|
599
|
+
opts: Opts,
|
|
600
|
+
sf: typescript.SourceFile
|
|
601
|
+
): typeof node {
|
|
602
|
+
const {onMsgExtracted, additionalFunctionNames} = opts
|
|
603
|
+
if (isMultipleMessageDecl(ts, node)) {
|
|
604
|
+
const [arg, ...restArgs] = node.arguments
|
|
605
|
+
let descriptorsObj: typescript.ObjectLiteralExpression | undefined
|
|
606
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
607
|
+
descriptorsObj = arg
|
|
608
|
+
} else if (
|
|
609
|
+
ts.isAsExpression(arg) &&
|
|
610
|
+
ts.isObjectLiteralExpression(arg.expression)
|
|
611
|
+
) {
|
|
612
|
+
descriptorsObj = arg.expression
|
|
613
|
+
}
|
|
614
|
+
if (descriptorsObj) {
|
|
615
|
+
const properties = descriptorsObj.properties
|
|
616
|
+
const msgs = properties
|
|
617
|
+
.filter<typescript.PropertyAssignment>(
|
|
618
|
+
(prop): prop is typescript.PropertyAssignment =>
|
|
619
|
+
ts.isPropertyAssignment(prop)
|
|
620
|
+
)
|
|
621
|
+
.map(
|
|
622
|
+
prop =>
|
|
623
|
+
ts.isObjectLiteralExpression(prop.initializer) &&
|
|
624
|
+
extractMessageDescriptor(ts, prop.initializer, opts, sf)
|
|
625
|
+
)
|
|
626
|
+
.filter((msg): msg is MessageDescriptor => !!msg)
|
|
627
|
+
if (!msgs.length) {
|
|
628
|
+
return node
|
|
629
|
+
}
|
|
630
|
+
debug('Multiple messages extracted from "%s": %s', sf.fileName, msgs)
|
|
631
|
+
if (typeof onMsgExtracted === 'function') {
|
|
632
|
+
onMsgExtracted(sf.fileName, msgs)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const clonedProperties = factory.createNodeArray(
|
|
636
|
+
properties.map((prop, i) => {
|
|
637
|
+
if (
|
|
638
|
+
!ts.isPropertyAssignment(prop) ||
|
|
639
|
+
!ts.isObjectLiteralExpression(prop.initializer)
|
|
640
|
+
) {
|
|
641
|
+
return prop
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return factory.createPropertyAssignment(
|
|
645
|
+
prop.name,
|
|
646
|
+
setAttributesInObject(
|
|
647
|
+
ts,
|
|
648
|
+
factory,
|
|
649
|
+
prop.initializer,
|
|
650
|
+
{
|
|
651
|
+
defaultMessage: opts.removeDefaultMessage
|
|
652
|
+
? undefined
|
|
653
|
+
: msgs[i].defaultMessage,
|
|
654
|
+
id: msgs[i] ? msgs[i].id : '',
|
|
655
|
+
},
|
|
656
|
+
opts.ast
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
})
|
|
660
|
+
)
|
|
661
|
+
const clonedDescriptorsObj =
|
|
662
|
+
factory.createObjectLiteralExpression(clonedProperties)
|
|
663
|
+
return factory.updateCallExpression(
|
|
664
|
+
node,
|
|
665
|
+
node.expression,
|
|
666
|
+
node.typeArguments,
|
|
667
|
+
[clonedDescriptorsObj, ...restArgs]
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
} else if (
|
|
671
|
+
isSingularMessageDecl(ts, node, opts.additionalComponentNames || []) ||
|
|
672
|
+
isMemberMethodFormatMessageCall(ts, node, additionalFunctionNames || [])
|
|
673
|
+
) {
|
|
674
|
+
const [descriptorsObj, ...restArgs] = node.arguments
|
|
675
|
+
if (ts.isObjectLiteralExpression(descriptorsObj)) {
|
|
676
|
+
const msg = extractMessageDescriptor(ts, descriptorsObj, opts, sf)
|
|
677
|
+
if (!msg) {
|
|
678
|
+
return node
|
|
679
|
+
}
|
|
680
|
+
debug('Message extracted from "%s": %s', sf.fileName, msg)
|
|
681
|
+
if (typeof onMsgExtracted === 'function') {
|
|
682
|
+
onMsgExtracted(sf.fileName, [msg])
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return factory.updateCallExpression(
|
|
686
|
+
node,
|
|
687
|
+
node.expression,
|
|
688
|
+
node.typeArguments,
|
|
689
|
+
[
|
|
690
|
+
setAttributesInObject(
|
|
691
|
+
ts,
|
|
692
|
+
factory,
|
|
693
|
+
descriptorsObj,
|
|
694
|
+
{
|
|
695
|
+
defaultMessage: opts.removeDefaultMessage
|
|
696
|
+
? undefined
|
|
697
|
+
: msg.defaultMessage,
|
|
698
|
+
id: msg.id,
|
|
699
|
+
},
|
|
700
|
+
opts.ast
|
|
701
|
+
),
|
|
702
|
+
...restArgs,
|
|
703
|
+
]
|
|
704
|
+
)
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return node
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const PRAGMA_REGEX = /^\/\/ @([^\s]*) (.*)$/m
|
|
711
|
+
|
|
712
|
+
function getVisitor(
|
|
713
|
+
ts: TypeScript,
|
|
714
|
+
ctx: typescript.TransformationContext,
|
|
715
|
+
sf: typescript.SourceFile,
|
|
716
|
+
opts: Opts
|
|
717
|
+
) {
|
|
718
|
+
const visitor: typescript.Visitor = (
|
|
719
|
+
node: typescript.Node
|
|
720
|
+
): typescript.Node => {
|
|
721
|
+
const newNode = ts.isCallExpression(node)
|
|
722
|
+
? extractMessagesFromCallExpression(ts, ctx.factory, node, opts, sf)
|
|
723
|
+
: ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)
|
|
724
|
+
? extractMessageFromJsxComponent(ts, ctx.factory, node, opts, sf)
|
|
725
|
+
: node
|
|
726
|
+
return ts.visitEachChild(newNode, visitor, ctx)
|
|
727
|
+
}
|
|
728
|
+
return visitor
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function transformWithTs(ts: TypeScript, opts: Opts) {
|
|
732
|
+
opts = {...DEFAULT_OPTS, ...opts}
|
|
733
|
+
debug('Transforming options', opts)
|
|
734
|
+
const transformFn: typescript.TransformerFactory<
|
|
735
|
+
typescript.SourceFile
|
|
736
|
+
> = ctx => {
|
|
737
|
+
return (sf: typescript.SourceFile) => {
|
|
738
|
+
const pragmaResult = PRAGMA_REGEX.exec(sf.text)
|
|
739
|
+
if (pragmaResult) {
|
|
740
|
+
debug('Pragma found', pragmaResult)
|
|
741
|
+
const [, pragma, kvString] = pragmaResult
|
|
742
|
+
if (pragma === opts.pragma) {
|
|
743
|
+
const kvs = kvString.split(' ')
|
|
744
|
+
const result: Record<string, string> = {}
|
|
745
|
+
for (const kv of kvs) {
|
|
746
|
+
const [k, v] = kv.split(':')
|
|
747
|
+
result[k] = v
|
|
748
|
+
}
|
|
749
|
+
debug('Pragma extracted', result)
|
|
750
|
+
if (typeof opts.onMetaExtracted === 'function') {
|
|
751
|
+
opts.onMetaExtracted(sf.fileName, result)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return ts.visitNode(sf, getVisitor(ts, ctx, sf, opts))
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return transformFn
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function transform(opts: Opts) {
|
|
763
|
+
return transformWithTs(typescript, opts)
|
|
764
|
+
}
|