babel-plugin-formatjs 10.3.25 → 10.3.26
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 +90 -0
- package/CHANGELOG.md +1139 -0
- package/LICENSE.md +0 -0
- package/README.md +0 -0
- package/global.d.ts +0 -0
- package/index.ts +88 -0
- package/integration-tests/BUILD +21 -0
- package/integration-tests/package.json +5 -0
- package/integration-tests/vue/fixtures/App.vue +19 -0
- package/integration-tests/vue/fixtures/app.js +6 -0
- package/integration-tests/vue/integration.test.ts +62 -0
- package/package.json +5 -4
- package/tests/__snapshots__/index.test.ts.snap +1246 -0
- package/tests/fixtures/2663.js +3 -0
- package/tests/fixtures/FormattedMessage.js +14 -0
- package/tests/fixtures/additionalComponentNames.js +15 -0
- package/tests/fixtures/additionalFunctionNames.js +23 -0
- package/tests/fixtures/ast.js +45 -0
- package/tests/fixtures/defineMessage.js +57 -0
- package/tests/fixtures/defineMessages.js +49 -0
- package/tests/fixtures/descriptionsAsObjects.js +18 -0
- package/tests/fixtures/empty.js +8 -0
- package/tests/fixtures/extractFromFormatMessageCall.js +47 -0
- package/tests/fixtures/extractFromFormatMessageCallStateless.js +46 -0
- package/tests/fixtures/extractSourceLocation.js +8 -0
- package/tests/fixtures/formatMessageCall.js +38 -0
- package/tests/fixtures/icuSyntax.js +18 -0
- package/tests/fixtures/idInterpolationPattern.js +40 -0
- package/tests/fixtures/inline.js +26 -0
- package/tests/fixtures/overrideIdFn.js +48 -0
- package/tests/fixtures/preserveWhitespace.js +79 -0
- package/tests/fixtures/removeDefaultMessage.js +36 -0
- package/tests/fixtures/skipExtractionFormattedMessage.js +12 -0
- package/tests/fixtures/templateLiteral.js +21 -0
- package/tests/index.test.ts +221 -0
- package/tsconfig.json +5 -0
- package/types.ts +46 -0
- package/utils.ts +226 -0
- package/visitors/call-expression.ts +208 -0
- package/visitors/jsx-opening-element.ts +147 -0
- package/index.d.ts +0 -10
- package/index.d.ts.map +0 -1
- package/index.js +0 -74
- package/types.d.ts +0 -31
- package/types.d.ts.map +0 -1
- package/types.js +0 -2
- package/utils.d.ts +0 -34
- package/utils.d.ts.map +0 -1
- package/utils.js +0 -155
- package/visitors/call-expression.d.ts +0 -6
- package/visitors/call-expression.d.ts.map +0 -1
- package/visitors/call-expression.js +0 -127
- package/visitors/jsx-opening-element.d.ts +0 -6
- package/visitors/jsx-opening-element.d.ts.map +0 -1
- package/visitors/jsx-opening-element.js +0 -89
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as path from 'path'
|
|
2
|
+
|
|
3
|
+
import {transformFileSync} from '@babel/core'
|
|
4
|
+
import plugin from '../'
|
|
5
|
+
import {Options, ExtractedMessageDescriptor} from '../types'
|
|
6
|
+
|
|
7
|
+
function transformAndCheck(fn: string, opts: Options = {}) {
|
|
8
|
+
const filePath = path.join(__dirname, 'fixtures', `${fn}.js`)
|
|
9
|
+
const messages: ExtractedMessageDescriptor[] = []
|
|
10
|
+
const meta = {}
|
|
11
|
+
const {code} = transform(filePath, {
|
|
12
|
+
pragma: '@react-intl',
|
|
13
|
+
...opts,
|
|
14
|
+
onMsgExtracted(_, msgs) {
|
|
15
|
+
messages.push(...msgs)
|
|
16
|
+
},
|
|
17
|
+
onMetaExtracted(_, m) {
|
|
18
|
+
Object.assign(meta, m)
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
expect({
|
|
22
|
+
data: {messages, meta},
|
|
23
|
+
code: code?.trim(),
|
|
24
|
+
}).toMatchSnapshot()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('additionalComponentNames', function () {
|
|
28
|
+
transformAndCheck('additionalComponentNames', {
|
|
29
|
+
additionalComponentNames: ['CustomMessage'],
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('additionalFunctionNames', function () {
|
|
34
|
+
transformAndCheck('additionalFunctionNames', {
|
|
35
|
+
additionalFunctionNames: ['t'],
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('ast', function () {
|
|
40
|
+
transformAndCheck('ast', {
|
|
41
|
+
ast: true,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('defineMessage', function () {
|
|
46
|
+
transformAndCheck('defineMessage')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('descriptionsAsObjects', function () {
|
|
50
|
+
transformAndCheck('descriptionsAsObjects')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('defineMessages', function () {
|
|
54
|
+
transformAndCheck('defineMessages')
|
|
55
|
+
})
|
|
56
|
+
test('empty', function () {
|
|
57
|
+
transformAndCheck('empty')
|
|
58
|
+
})
|
|
59
|
+
test('extractFromFormatMessageCall', function () {
|
|
60
|
+
transformAndCheck('extractFromFormatMessageCall')
|
|
61
|
+
})
|
|
62
|
+
test('extractFromFormatMessageCallStateless', function () {
|
|
63
|
+
transformAndCheck('extractFromFormatMessageCallStateless')
|
|
64
|
+
})
|
|
65
|
+
test('formatMessageCall', function () {
|
|
66
|
+
transformAndCheck('formatMessageCall')
|
|
67
|
+
})
|
|
68
|
+
test('FormattedMessage', function () {
|
|
69
|
+
transformAndCheck('FormattedMessage')
|
|
70
|
+
})
|
|
71
|
+
test('inline', function () {
|
|
72
|
+
transformAndCheck('inline')
|
|
73
|
+
})
|
|
74
|
+
test('templateLiteral', function () {
|
|
75
|
+
transformAndCheck('templateLiteral')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('idInterpolationPattern', function () {
|
|
79
|
+
transformAndCheck('idInterpolationPattern', {
|
|
80
|
+
idInterpolationPattern: '[folder].[name].[sha512:contenthash:hex:6]',
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('idInterpolationPattern default', function () {
|
|
85
|
+
transformAndCheck('idInterpolationPattern')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('GH #2663', function () {
|
|
89
|
+
const filePath = path.join(__dirname, 'fixtures', `2663.js`)
|
|
90
|
+
const messages: ExtractedMessageDescriptor[] = []
|
|
91
|
+
const meta = {}
|
|
92
|
+
|
|
93
|
+
const {code} = transformFileSync(filePath, {
|
|
94
|
+
presets: ['@babel/preset-env', '@babel/preset-react'],
|
|
95
|
+
plugins: [
|
|
96
|
+
[
|
|
97
|
+
plugin,
|
|
98
|
+
{
|
|
99
|
+
pragma: '@react-intl',
|
|
100
|
+
onMsgExtracted(_, msgs) {
|
|
101
|
+
messages.push(...msgs)
|
|
102
|
+
},
|
|
103
|
+
onMetaExtracted(_, m) {
|
|
104
|
+
Object.assign(meta, m)
|
|
105
|
+
},
|
|
106
|
+
} as Options,
|
|
107
|
+
Date.now() + '' + ++cacheBust,
|
|
108
|
+
],
|
|
109
|
+
],
|
|
110
|
+
})!
|
|
111
|
+
|
|
112
|
+
expect({
|
|
113
|
+
data: {messages, meta},
|
|
114
|
+
code: code?.trim(),
|
|
115
|
+
}).toMatchSnapshot()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('overrideIdFn', function () {
|
|
119
|
+
transformAndCheck('overrideIdFn', {
|
|
120
|
+
overrideIdFn: (
|
|
121
|
+
id?: string,
|
|
122
|
+
defaultMessage?: string,
|
|
123
|
+
description?: string,
|
|
124
|
+
filePath?: string
|
|
125
|
+
) => {
|
|
126
|
+
const filename = path.basename(filePath!)
|
|
127
|
+
return `${filename}.${id}.${defaultMessage!.length}.${typeof description}`
|
|
128
|
+
},
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
test('removeDefaultMessage', function () {
|
|
132
|
+
transformAndCheck('removeDefaultMessage', {
|
|
133
|
+
removeDefaultMessage: true,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
test('removeDefaultMessage + overrideIdFn', function () {
|
|
137
|
+
transformAndCheck('removeDefaultMessage', {
|
|
138
|
+
removeDefaultMessage: true,
|
|
139
|
+
overrideIdFn: (
|
|
140
|
+
id?: string,
|
|
141
|
+
defaultMessage?: string,
|
|
142
|
+
description?: string,
|
|
143
|
+
filePath?: string
|
|
144
|
+
) => {
|
|
145
|
+
const filename = path.basename(filePath!)
|
|
146
|
+
return `${filename}.${id}.${defaultMessage!.length}.${typeof description}`
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
test('preserveWhitespace', function () {
|
|
151
|
+
transformAndCheck('preserveWhitespace', {
|
|
152
|
+
preserveWhitespace: true,
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('extractSourceLocation', function () {
|
|
157
|
+
const filePath = path.join(__dirname, 'fixtures', 'extractSourceLocation.js')
|
|
158
|
+
const messages: ExtractedMessageDescriptor[] = []
|
|
159
|
+
const meta = {}
|
|
160
|
+
|
|
161
|
+
const {code} = transform(filePath, {
|
|
162
|
+
pragma: '@react-intl',
|
|
163
|
+
extractSourceLocation: true,
|
|
164
|
+
onMsgExtracted(_, msgs) {
|
|
165
|
+
messages.push(...msgs)
|
|
166
|
+
},
|
|
167
|
+
onMetaExtracted(_, m) {
|
|
168
|
+
Object.assign(meta, m)
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
expect(code?.trim()).toMatchSnapshot()
|
|
172
|
+
expect(messages).toMatchSnapshot([
|
|
173
|
+
{
|
|
174
|
+
file: expect.any(String),
|
|
175
|
+
},
|
|
176
|
+
])
|
|
177
|
+
expect(meta).toMatchSnapshot()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test('Properly throws parse errors', () => {
|
|
181
|
+
expect(() =>
|
|
182
|
+
transform(path.join(__dirname, 'fixtures', 'icuSyntax.js'))
|
|
183
|
+
).toThrow('SyntaxError: MALFORMED_ARGUMENT')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('skipExtractionFormattedMessage', function () {
|
|
187
|
+
transformAndCheck('skipExtractionFormattedMessage')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
let cacheBust = 1
|
|
191
|
+
|
|
192
|
+
function transform(
|
|
193
|
+
filePath: string,
|
|
194
|
+
options: Options = {},
|
|
195
|
+
{multiplePasses = false} = {}
|
|
196
|
+
) {
|
|
197
|
+
function getPluginConfig() {
|
|
198
|
+
return [plugin, options, Date.now() + '' + ++cacheBust]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return transformFileSync(filePath, {
|
|
202
|
+
presets: [
|
|
203
|
+
[
|
|
204
|
+
'@babel/preset-env',
|
|
205
|
+
{
|
|
206
|
+
targets: {
|
|
207
|
+
node: '14',
|
|
208
|
+
esmodules: true,
|
|
209
|
+
},
|
|
210
|
+
modules: false,
|
|
211
|
+
useBuiltIns: false,
|
|
212
|
+
ignoreBrowserslistConfig: true,
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
'@babel/preset-react',
|
|
216
|
+
],
|
|
217
|
+
plugins: multiplePasses
|
|
218
|
+
? [getPluginConfig(), getPluginConfig()]
|
|
219
|
+
: [getPluginConfig()],
|
|
220
|
+
})!
|
|
221
|
+
}
|
package/tsconfig.json
ADDED
package/types.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {NodePath} from '@babel/core'
|
|
2
|
+
import {
|
|
3
|
+
JSXExpressionContainer,
|
|
4
|
+
SourceLocation,
|
|
5
|
+
StringLiteral,
|
|
6
|
+
} from '@babel/types'
|
|
7
|
+
|
|
8
|
+
export interface MessageDescriptor {
|
|
9
|
+
id: string
|
|
10
|
+
defaultMessage?: string
|
|
11
|
+
description?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface State {
|
|
15
|
+
messages: ExtractedMessageDescriptor[]
|
|
16
|
+
meta: Record<string, string>
|
|
17
|
+
componentNames: string[]
|
|
18
|
+
functionNames: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ExtractedMessageDescriptor = MessageDescriptor &
|
|
22
|
+
Partial<SourceLocation> & {file?: string}
|
|
23
|
+
|
|
24
|
+
export type MessageDescriptorPath = Record<
|
|
25
|
+
keyof MessageDescriptor,
|
|
26
|
+
NodePath<StringLiteral> | NodePath<JSXExpressionContainer> | undefined
|
|
27
|
+
>
|
|
28
|
+
|
|
29
|
+
export interface Options {
|
|
30
|
+
overrideIdFn?: (
|
|
31
|
+
id?: string,
|
|
32
|
+
defaultMessage?: string,
|
|
33
|
+
description?: string,
|
|
34
|
+
filePath?: string
|
|
35
|
+
) => string
|
|
36
|
+
onMsgExtracted?: (filePath: string, msgs: MessageDescriptor[]) => void
|
|
37
|
+
onMetaExtracted?: (filePath: string, meta: Record<string, string>) => void
|
|
38
|
+
idInterpolationPattern?: string
|
|
39
|
+
removeDefaultMessage?: boolean
|
|
40
|
+
additionalComponentNames?: string[]
|
|
41
|
+
additionalFunctionNames?: string[]
|
|
42
|
+
pragma?: string
|
|
43
|
+
extractSourceLocation?: boolean
|
|
44
|
+
ast?: boolean
|
|
45
|
+
preserveWhitespace?: boolean
|
|
46
|
+
}
|
package/utils.ts
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as t from '@babel/types'
|
|
2
|
+
import {parse} from '@formatjs/icu-messageformat-parser'
|
|
3
|
+
import {interpolateName} from '@formatjs/ts-transformer'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Options,
|
|
7
|
+
ExtractedMessageDescriptor,
|
|
8
|
+
MessageDescriptor,
|
|
9
|
+
MessageDescriptorPath,
|
|
10
|
+
} from './types'
|
|
11
|
+
import {NodePath} from '@babel/core'
|
|
12
|
+
|
|
13
|
+
const DESCRIPTOR_PROPS = new Set<keyof MessageDescriptorPath>([
|
|
14
|
+
'id',
|
|
15
|
+
'description',
|
|
16
|
+
'defaultMessage',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
function evaluatePath(path: NodePath<any>): string {
|
|
20
|
+
const evaluated = path.evaluate()
|
|
21
|
+
if (evaluated.confident) {
|
|
22
|
+
return evaluated.value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
throw path.buildCodeFrameError(
|
|
26
|
+
'[React Intl] Messages must be statically evaluate-able for extraction.'
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getMessageDescriptorKey(path: NodePath<any>) {
|
|
31
|
+
if (path.isIdentifier() || path.isJSXIdentifier()) {
|
|
32
|
+
return path.node.name
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return evaluatePath(path)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getMessageDescriptorValue(
|
|
39
|
+
path?:
|
|
40
|
+
| NodePath<t.StringLiteral>
|
|
41
|
+
| NodePath<t.JSXExpressionContainer>
|
|
42
|
+
| NodePath<t.TemplateLiteral>,
|
|
43
|
+
isMessageNode?: boolean
|
|
44
|
+
) {
|
|
45
|
+
if (!path) {
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
if (path.isJSXExpressionContainer()) {
|
|
49
|
+
// If this is already compiled, no need to recompiled it
|
|
50
|
+
if (isMessageNode && path.get('expression').isArrayExpression()) {
|
|
51
|
+
return ''
|
|
52
|
+
}
|
|
53
|
+
path = path.get('expression') as NodePath<t.StringLiteral>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Always trim the Message Descriptor values.
|
|
57
|
+
const descriptorValue = evaluatePath(path)
|
|
58
|
+
|
|
59
|
+
return descriptorValue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createMessageDescriptor(
|
|
63
|
+
propPaths: [
|
|
64
|
+
NodePath<t.JSXIdentifier> | NodePath<t.Identifier>,
|
|
65
|
+
NodePath<t.StringLiteral> | NodePath<t.JSXExpressionContainer>
|
|
66
|
+
][]
|
|
67
|
+
): MessageDescriptorPath {
|
|
68
|
+
return propPaths.reduce(
|
|
69
|
+
(hash: MessageDescriptorPath, [keyPath, valuePath]) => {
|
|
70
|
+
const key = getMessageDescriptorKey(
|
|
71
|
+
keyPath
|
|
72
|
+
) as keyof MessageDescriptorPath
|
|
73
|
+
|
|
74
|
+
if (DESCRIPTOR_PROPS.has(key)) {
|
|
75
|
+
hash[key] = valuePath
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return hash
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: undefined,
|
|
82
|
+
defaultMessage: undefined,
|
|
83
|
+
description: undefined,
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function evaluateMessageDescriptor(
|
|
89
|
+
descriptorPath: MessageDescriptorPath,
|
|
90
|
+
isJSXSource = false,
|
|
91
|
+
filename: string | undefined,
|
|
92
|
+
idInterpolationPattern?: string,
|
|
93
|
+
overrideIdFn?: Options['overrideIdFn'],
|
|
94
|
+
preserveWhitespace?: Options['preserveWhitespace']
|
|
95
|
+
) {
|
|
96
|
+
let id = getMessageDescriptorValue(descriptorPath.id)
|
|
97
|
+
const defaultMessage = getICUMessageValue(
|
|
98
|
+
descriptorPath.defaultMessage,
|
|
99
|
+
{
|
|
100
|
+
isJSXSource,
|
|
101
|
+
},
|
|
102
|
+
preserveWhitespace
|
|
103
|
+
)
|
|
104
|
+
const description = getMessageDescriptorValue(descriptorPath.description)
|
|
105
|
+
|
|
106
|
+
if (overrideIdFn) {
|
|
107
|
+
id = overrideIdFn(id, defaultMessage, description, filename)
|
|
108
|
+
} else if (!id && idInterpolationPattern && defaultMessage) {
|
|
109
|
+
id = interpolateName(
|
|
110
|
+
{resourcePath: filename} as any,
|
|
111
|
+
idInterpolationPattern,
|
|
112
|
+
{
|
|
113
|
+
content: description
|
|
114
|
+
? `${defaultMessage}#${description}`
|
|
115
|
+
: defaultMessage,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
const descriptor: MessageDescriptor = {
|
|
120
|
+
id,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (description) {
|
|
124
|
+
descriptor.description = description
|
|
125
|
+
}
|
|
126
|
+
if (defaultMessage) {
|
|
127
|
+
descriptor.defaultMessage = defaultMessage
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return descriptor
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getICUMessageValue(
|
|
134
|
+
messagePath?:
|
|
135
|
+
| NodePath<t.StringLiteral>
|
|
136
|
+
| NodePath<t.TemplateLiteral>
|
|
137
|
+
| NodePath<t.JSXExpressionContainer>,
|
|
138
|
+
{isJSXSource = false} = {},
|
|
139
|
+
preserveWhitespace?: Options['preserveWhitespace']
|
|
140
|
+
) {
|
|
141
|
+
if (!messagePath) {
|
|
142
|
+
return ''
|
|
143
|
+
}
|
|
144
|
+
let message = getMessageDescriptorValue(messagePath, true)
|
|
145
|
+
|
|
146
|
+
if (!preserveWhitespace) {
|
|
147
|
+
message = message.trim().replace(/\s+/gm, ' ')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
parse(message)
|
|
152
|
+
} catch (parseError) {
|
|
153
|
+
if (
|
|
154
|
+
isJSXSource &&
|
|
155
|
+
messagePath.isLiteral() &&
|
|
156
|
+
message.indexOf('\\\\') >= 0
|
|
157
|
+
) {
|
|
158
|
+
throw messagePath.buildCodeFrameError(
|
|
159
|
+
'[React Intl] Message failed to parse. ' +
|
|
160
|
+
'It looks like `\\`s were used for escaping, ' +
|
|
161
|
+
"this won't work with JSX string literals. " +
|
|
162
|
+
'Wrap with `{}`. ' +
|
|
163
|
+
'See: http://facebook.github.io/react/docs/jsx-gotchas.html'
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
throw messagePath.buildCodeFrameError(
|
|
168
|
+
'[React Intl] Message failed to parse. ' +
|
|
169
|
+
'See: https://formatjs.io/docs/core-concepts/icu-syntax' +
|
|
170
|
+
`\n${parseError}`
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
return message
|
|
174
|
+
}
|
|
175
|
+
const EXTRACTED = Symbol('FormatJSExtracted')
|
|
176
|
+
/**
|
|
177
|
+
* Tag a node as extracted
|
|
178
|
+
* Store this in the node itself so that multiple passes work. Specifically
|
|
179
|
+
* if we remove `description` in the 1st pass, 2nd pass will fail since
|
|
180
|
+
* it expect `description` to be there.
|
|
181
|
+
* HACK: We store this in the node instance since this persists across
|
|
182
|
+
* multiple plugin runs
|
|
183
|
+
* @param path
|
|
184
|
+
*/
|
|
185
|
+
export function tagAsExtracted(path: NodePath<any>) {
|
|
186
|
+
path.node[EXTRACTED] = true
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check if a node was extracted
|
|
190
|
+
* @param path
|
|
191
|
+
*/
|
|
192
|
+
export function wasExtracted(path: NodePath<any>) {
|
|
193
|
+
return !!path.node[EXTRACTED]
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Store a message in our global messages
|
|
198
|
+
* @param messageDescriptor
|
|
199
|
+
* @param path
|
|
200
|
+
* @param opts
|
|
201
|
+
* @param filename
|
|
202
|
+
* @param messages
|
|
203
|
+
*/
|
|
204
|
+
export function storeMessage(
|
|
205
|
+
{id, description, defaultMessage}: MessageDescriptor,
|
|
206
|
+
path: NodePath<any>,
|
|
207
|
+
{extractSourceLocation}: Options,
|
|
208
|
+
|
|
209
|
+
filename: string | undefined,
|
|
210
|
+
messages: ExtractedMessageDescriptor[]
|
|
211
|
+
) {
|
|
212
|
+
if (!id && !defaultMessage) {
|
|
213
|
+
throw path.buildCodeFrameError(
|
|
214
|
+
'[React Intl] Message Descriptors require an `id` or `defaultMessage`.'
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let loc = {}
|
|
219
|
+
if (extractSourceLocation) {
|
|
220
|
+
loc = {
|
|
221
|
+
file: filename,
|
|
222
|
+
...path.node.loc,
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
messages.push({id, description, defaultMessage, ...loc})
|
|
226
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {NodePath, PluginPass} from '@babel/core'
|
|
2
|
+
import * as t from '@babel/types'
|
|
3
|
+
import {Options, State} from '../types'
|
|
4
|
+
import {VisitNodeFunction} from '@babel/traverse'
|
|
5
|
+
import {
|
|
6
|
+
createMessageDescriptor,
|
|
7
|
+
evaluateMessageDescriptor,
|
|
8
|
+
wasExtracted,
|
|
9
|
+
storeMessage,
|
|
10
|
+
tagAsExtracted,
|
|
11
|
+
} from '../utils'
|
|
12
|
+
import {parse} from '@formatjs/icu-messageformat-parser'
|
|
13
|
+
|
|
14
|
+
function assertObjectExpression(
|
|
15
|
+
path: NodePath<any>,
|
|
16
|
+
callee: NodePath<t.Expression | t.V8IntrinsicIdentifier>
|
|
17
|
+
): asserts path is NodePath<t.ObjectExpression> {
|
|
18
|
+
if (!path || !path.isObjectExpression()) {
|
|
19
|
+
throw path.buildCodeFrameError(
|
|
20
|
+
`[React Intl] \`${
|
|
21
|
+
(callee.get('property') as NodePath<t.Identifier>).node.name
|
|
22
|
+
}()\` must be called with an object expression with values that are React Intl Message Descriptors, also defined as object expressions.`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isFormatMessageCall(
|
|
28
|
+
callee: NodePath<t.Expression | t.V8IntrinsicIdentifier | t.MemberExpression>,
|
|
29
|
+
functionNames: string[]
|
|
30
|
+
) {
|
|
31
|
+
if (functionNames.find(name => callee.isIdentifier({name}))) {
|
|
32
|
+
return true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (callee.isMemberExpression()) {
|
|
36
|
+
const property = callee.get('property') as NodePath<t.MemberExpression>
|
|
37
|
+
return !!functionNames.find(name => property.isIdentifier({name}))
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getMessagesObjectFromExpression(
|
|
43
|
+
nodePath: NodePath<any>
|
|
44
|
+
): NodePath<any> {
|
|
45
|
+
let currentPath = nodePath
|
|
46
|
+
while (
|
|
47
|
+
t.isTSAsExpression(currentPath.node) ||
|
|
48
|
+
t.isTSTypeAssertion(currentPath.node) ||
|
|
49
|
+
t.isTypeCastExpression(currentPath.node)
|
|
50
|
+
) {
|
|
51
|
+
currentPath = currentPath.get('expression') as NodePath<any>
|
|
52
|
+
}
|
|
53
|
+
return currentPath
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const visitor: VisitNodeFunction<PluginPass & State, t.CallExpression> =
|
|
57
|
+
function (
|
|
58
|
+
path,
|
|
59
|
+
{
|
|
60
|
+
opts,
|
|
61
|
+
file: {
|
|
62
|
+
opts: {filename},
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
) {
|
|
66
|
+
const {
|
|
67
|
+
overrideIdFn,
|
|
68
|
+
idInterpolationPattern,
|
|
69
|
+
removeDefaultMessage,
|
|
70
|
+
ast,
|
|
71
|
+
preserveWhitespace,
|
|
72
|
+
} = opts as Options
|
|
73
|
+
if (wasExtracted(path)) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
const {messages, functionNames} = this
|
|
77
|
+
const callee = path.get('callee')
|
|
78
|
+
const args = path.get('arguments')
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Process MessageDescriptor
|
|
82
|
+
* @param messageDescriptor Message Descriptor
|
|
83
|
+
*/
|
|
84
|
+
function processMessageObject(
|
|
85
|
+
messageDescriptor: NodePath<t.ObjectExpression>
|
|
86
|
+
) {
|
|
87
|
+
assertObjectExpression(messageDescriptor, callee)
|
|
88
|
+
|
|
89
|
+
const properties = messageDescriptor.get(
|
|
90
|
+
'properties'
|
|
91
|
+
) as NodePath<t.ObjectProperty>[]
|
|
92
|
+
|
|
93
|
+
const descriptorPath = createMessageDescriptor(
|
|
94
|
+
properties.map(
|
|
95
|
+
prop =>
|
|
96
|
+
[prop.get('key'), prop.get('value')] as [
|
|
97
|
+
NodePath<t.Identifier>,
|
|
98
|
+
NodePath<t.StringLiteral>
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// If the message is already compiled, don't re-compile it
|
|
104
|
+
if (descriptorPath.defaultMessage?.isArrayExpression()) {
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Evaluate the Message Descriptor values, then store it.
|
|
109
|
+
const descriptor = evaluateMessageDescriptor(
|
|
110
|
+
descriptorPath,
|
|
111
|
+
false,
|
|
112
|
+
filename || undefined,
|
|
113
|
+
idInterpolationPattern,
|
|
114
|
+
overrideIdFn,
|
|
115
|
+
preserveWhitespace
|
|
116
|
+
)
|
|
117
|
+
storeMessage(
|
|
118
|
+
descriptor,
|
|
119
|
+
messageDescriptor,
|
|
120
|
+
opts as Options,
|
|
121
|
+
filename || undefined,
|
|
122
|
+
messages
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const firstProp = properties[0]
|
|
126
|
+
const defaultMessageProp = properties.find(prop => {
|
|
127
|
+
const keyProp = prop.get('key')
|
|
128
|
+
return (
|
|
129
|
+
keyProp.isIdentifier({name: 'defaultMessage'}) ||
|
|
130
|
+
keyProp.isStringLiteral({value: 'defaultMessage'})
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
const idProp = properties.find(prop => {
|
|
134
|
+
const keyProp = prop.get('key')
|
|
135
|
+
return (
|
|
136
|
+
keyProp.isIdentifier({name: 'id'}) ||
|
|
137
|
+
keyProp.isStringLiteral({value: 'id'})
|
|
138
|
+
)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Insert ID potentially 1st before removing nodes
|
|
142
|
+
if (idProp) {
|
|
143
|
+
idProp.get('value').replaceWith(t.stringLiteral(descriptor.id))
|
|
144
|
+
} else {
|
|
145
|
+
firstProp.insertBefore(
|
|
146
|
+
t.objectProperty(t.identifier('id'), t.stringLiteral(descriptor.id))
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Remove description
|
|
151
|
+
properties
|
|
152
|
+
.find(prop => {
|
|
153
|
+
const keyProp = prop.get('key')
|
|
154
|
+
return (
|
|
155
|
+
keyProp.isIdentifier({name: 'description'}) ||
|
|
156
|
+
keyProp.isStringLiteral({value: 'description'})
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
?.remove()
|
|
160
|
+
|
|
161
|
+
// Pre-parse or remove defaultMessage
|
|
162
|
+
if (defaultMessageProp) {
|
|
163
|
+
if (removeDefaultMessage) {
|
|
164
|
+
defaultMessageProp?.remove()
|
|
165
|
+
} else if (descriptor.defaultMessage) {
|
|
166
|
+
const valueProp = defaultMessageProp.get('value')
|
|
167
|
+
if (ast) {
|
|
168
|
+
valueProp.replaceWithSourceString(
|
|
169
|
+
JSON.stringify(parse(descriptor.defaultMessage))
|
|
170
|
+
)
|
|
171
|
+
} else {
|
|
172
|
+
valueProp.replaceWith(t.stringLiteral(descriptor.defaultMessage))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
tagAsExtracted(path)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check that this is `defineMessages` call
|
|
181
|
+
if (
|
|
182
|
+
callee.isIdentifier({name: 'defineMessages'}) ||
|
|
183
|
+
callee.isIdentifier({name: 'defineMessage'})
|
|
184
|
+
) {
|
|
185
|
+
const firstArgument = args[0]
|
|
186
|
+
const messagesObj = getMessagesObjectFromExpression(firstArgument)
|
|
187
|
+
|
|
188
|
+
assertObjectExpression(messagesObj, callee)
|
|
189
|
+
if (callee.isIdentifier({name: 'defineMessage'})) {
|
|
190
|
+
processMessageObject(messagesObj as NodePath<t.ObjectExpression>)
|
|
191
|
+
} else {
|
|
192
|
+
const properties = messagesObj.get('properties')
|
|
193
|
+
if (Array.isArray(properties)) {
|
|
194
|
+
properties
|
|
195
|
+
.map(prop => prop.get('value') as NodePath<t.ObjectExpression>)
|
|
196
|
+
.forEach(processMessageObject)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check that this is `intl.formatMessage` call
|
|
202
|
+
if (isFormatMessageCall(callee, functionNames)) {
|
|
203
|
+
const messageDescriptor = args[0]
|
|
204
|
+
if (messageDescriptor.isObjectExpression()) {
|
|
205
|
+
processMessageObject(messageDescriptor)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|