@formatjs/ts-transformer 3.9.7 → 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.
Files changed (62) hide show
  1. package/BUILD +82 -0
  2. package/CHANGELOG.md +713 -0
  3. package/LICENSE.md +0 -0
  4. package/README.md +0 -0
  5. package/examples/compile.ts +50 -0
  6. package/index.ts +3 -0
  7. package/integration-tests/BUILD +37 -0
  8. package/integration-tests/integration/comp.tsx +40 -0
  9. package/integration-tests/integration/jest.config.js +25 -0
  10. package/integration-tests/integration/ts-jest.test.tsx +32 -0
  11. package/integration-tests/package.json +5 -0
  12. package/integration-tests/vue/fixtures/index.vue +30 -0
  13. package/integration-tests/vue/fixtures/main.ts +4 -0
  14. package/integration-tests/vue/integration.test.ts +75 -0
  15. package/package.json +4 -4
  16. package/src/console_utils.ts +32 -0
  17. package/src/interpolate-name.ts +147 -0
  18. package/src/transform.ts +764 -0
  19. package/src/types.ts +12 -0
  20. package/tests/__snapshots__/index.test.ts.snap +908 -0
  21. package/tests/fixtures/FormattedMessage.tsx +35 -0
  22. package/tests/fixtures/additionalComponentNames.tsx +16 -0
  23. package/tests/fixtures/additionalFunctionNames.tsx +20 -0
  24. package/tests/fixtures/ast.tsx +83 -0
  25. package/tests/fixtures/defineMessages.tsx +67 -0
  26. package/tests/fixtures/defineMessagesPreserveWhitespace.tsx +87 -0
  27. package/tests/fixtures/descriptionsAsObjects.tsx +17 -0
  28. package/tests/fixtures/extractFromFormatMessage.tsx +45 -0
  29. package/tests/fixtures/extractFromFormatMessageStateless.tsx +46 -0
  30. package/tests/fixtures/extractSourceLocation.tsx +8 -0
  31. package/tests/fixtures/formatMessageCall.tsx +44 -0
  32. package/tests/fixtures/inline.tsx +26 -0
  33. package/tests/fixtures/nested.tsx +10 -0
  34. package/tests/fixtures/noImport.tsx +52 -0
  35. package/tests/fixtures/overrideIdFn.tsx +70 -0
  36. package/tests/fixtures/removeDefaultMessage.tsx +22 -0
  37. package/tests/fixtures/removeDescription.tsx +22 -0
  38. package/tests/fixtures/resourcePath.tsx +23 -0
  39. package/tests/fixtures/stringConcat.tsx +26 -0
  40. package/tests/fixtures/templateLiteral.tsx +21 -0
  41. package/tests/index.test.ts +127 -0
  42. package/tests/interpolate-name.test.ts +14 -0
  43. package/ts-jest-integration.ts +9 -0
  44. package/tsconfig.json +5 -0
  45. package/index.d.ts +0 -4
  46. package/index.d.ts.map +0 -1
  47. package/index.js +0 -6
  48. package/src/console_utils.d.ts +0 -4
  49. package/src/console_utils.d.ts.map +0 -1
  50. package/src/console_utils.js +0 -49
  51. package/src/interpolate-name.d.ts +0 -15
  52. package/src/interpolate-name.d.ts.map +0 -1
  53. package/src/interpolate-name.js +0 -95
  54. package/src/transform.d.ts +0 -78
  55. package/src/transform.d.ts.map +0 -1
  56. package/src/transform.js +0 -483
  57. package/src/types.d.ts +0 -12
  58. package/src/types.d.ts.map +0 -1
  59. package/src/types.js +0 -2
  60. package/ts-jest-integration.d.ts +0 -6
  61. package/ts-jest-integration.d.ts.map +0 -1
  62. package/ts-jest-integration.js +0 -10
@@ -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
+ }