1
- import * as path from 'path' ;
2
- import { readFileSync , writeFileSync } from 'fs' ;
1
+ import * as p from 'path' ;
2
+ import { writeFileSync } from 'fs' ;
3
3
import { sync as mkdirpSync } from 'mkdirp' ;
4
4
5
5
const COMPONENT_NAMES = [
@@ -12,111 +12,195 @@ const FUNCTION_NAMES = [
12
12
'formatHTMLMessage' ,
13
13
] ;
14
14
15
- const IMPORTED_NAMES = new Set ( [ ...COMPONENT_NAMES , ...FUNCTION_NAMES ] ) ;
16
- const ATTR_WHITELIST = new Set ( [ 'id' , 'description' , 'defaultMessage' ] ) ;
15
+ const IMPORTED_NAMES = new Set ( [ ...COMPONENT_NAMES , ...FUNCTION_NAMES ] ) ;
16
+ const DESCRIPTOR_PROPS = new Set ( [ 'id' , 'description' , 'defaultMessage' ] ) ;
17
17
18
18
export default function ( { Plugin, types : t } ) {
19
- function referencesImport ( node , mod , importedNames ) {
20
- if ( ! ( t . isIdentifier ( node ) || t . isJSXIdentifier ( node ) ) ) {
21
- return false ;
19
+ function getModuleSourceName ( options ) {
20
+ const reactIntlOptions = options . extra [ 'react-intl' ] || { } ;
21
+ return reactIntlOptions . moduleSourceName || 'react-intl' ;
22
+ }
23
+
24
+ function getMessagesDir ( options ) {
25
+ const reactIntlOptions = options . extra [ 'react-intl' ] || { } ;
26
+ return reactIntlOptions . messagesDir ;
27
+ }
28
+
29
+ function getMessageDescriptor ( propertiesMap ) {
30
+ // Force property order on descriptors.
31
+ let descriptor = [ ...DESCRIPTOR_PROPS ] . reduce ( ( descriptor , key ) => {
32
+ descriptor [ key ] = undefined ;
33
+ return descriptor ;
34
+ } , { } ) ;
35
+
36
+ for ( let [ key , value ] of propertiesMap ) {
37
+ key = getMessageDescriptorKey ( key ) ;
38
+
39
+ if ( DESCRIPTOR_PROPS . has ( key ) ) {
40
+ // TODO: Should this be trimming values?
41
+ descriptor [ key ] = getMessageDescriptorValue ( value ) . trim ( ) ;
42
+ }
43
+ }
44
+
45
+ return descriptor ;
46
+ }
47
+
48
+ function getMessageDescriptorKey ( path ) {
49
+ if ( path . isIdentifier ( ) || path . isJSXIdentifier ( ) ) {
50
+ return path . node . name ;
22
51
}
23
52
24
- return importedNames . some ( ( name ) => node . referencesImport ( mod , name ) ) ;
53
+ let evaluated = path . evaluate ( ) ;
54
+ if ( evaluated . confident ) {
55
+ return evaluated . value ;
56
+ }
25
57
}
26
58
27
- function checkMessageId ( messages , message , node , file ) {
28
- if ( ! message . id ) {
59
+ function getMessageDescriptorValue ( path ) {
60
+ if ( path . isJSXExpressionContainer ( ) ) {
61
+ path = path . get ( 'expression' ) ;
62
+ }
63
+
64
+ let evaluated = path . evaluate ( ) ;
65
+ if ( evaluated . confident ) {
66
+ return evaluated . value ;
67
+ }
68
+
69
+ if ( path . isTemplateLiteral ( ) && path . get ( 'expressions' ) . length === 0 ) {
70
+ let str = path . get ( 'quasis' )
71
+ . map ( ( quasi ) => quasi . node . value . cooked )
72
+ . reduce ( ( str , value ) => str + value ) ;
73
+
74
+ return str ;
75
+ }
76
+
77
+ throw path . errorWithNode (
78
+ '[React Intl] Messages must be statically evaluate-able for extraction.'
79
+ ) ;
80
+ }
81
+
82
+ function storeMessage ( descriptor , node , file ) {
83
+ const { id} = descriptor ;
84
+ const { messages} = file . get ( 'react-intl' ) ;
85
+
86
+ if ( ! id ) {
29
87
throw file . errorWithNode ( node ,
30
- 'React Intl message is missing an `id`.'
88
+ '[ React Intl] Message is missing an `id`.'
31
89
) ;
32
90
}
33
91
34
- if ( messages . hasOwnProperty ( message . id ) ) {
92
+ if ( messages . has ( id ) ) {
35
93
throw file . errorWithNode ( node ,
36
- `Duplicate React Intl message id: "${ message . id } "`
94
+ `[ React Intl] Duplicate message id: "${ id } "`
37
95
) ;
38
96
}
97
+
98
+ if ( ! descriptor . defaultMessage ) {
99
+ let { loc} = node ;
100
+ file . log . warn (
101
+ `[React Intl] Line ${ loc . start . line } : ` +
102
+ `Message "${ id } " is missing a \`defaultMessage\` ` +
103
+ `and will not be extracted.`
104
+ ) ;
105
+ return ;
106
+ }
107
+
108
+ messages . set ( id , descriptor ) ;
109
+ }
110
+
111
+ function referencesImport ( path , mod , importedNames ) {
112
+ if ( ! ( path . isIdentifier ( ) || path . isJSXIdentifier ( ) ) ) {
113
+ return false ;
114
+ }
115
+
116
+ return importedNames . some ( ( name ) => path . referencesImport ( mod , name ) ) ;
39
117
}
40
118
41
119
return new Plugin ( 'react-intl' , {
42
120
visitor : {
43
121
Program : {
44
122
enter ( node , parent , scope , file ) {
45
- const { moduleSourceName} = file . opts . extra . reactIntl ;
123
+ const moduleSourceName = getModuleSourceName ( file . opts ) ;
46
124
const { imports} = file . metadata . modules ;
47
125
48
- let hasReactIntlMessages = imports . some ( ( mod ) => {
126
+ let mightHaveReactIntlMessages = imports . some ( ( mod ) => {
49
127
if ( mod . source === moduleSourceName ) {
50
128
return mod . imported . some ( ( name ) => {
51
129
return IMPORTED_NAMES . has ( name ) ;
52
130
} ) ;
53
131
}
54
132
} ) ;
55
133
56
- if ( hasReactIntlMessages ) {
57
- file . reactIntl = {
58
- messages : { }
59
- } ;
134
+ if ( mightHaveReactIntlMessages ) {
135
+ file . set ( 'react-intl' , {
136
+ messages : new Map ( ) ,
137
+ } ) ;
60
138
} else {
61
139
this . skip ( ) ;
62
140
}
63
141
} ,
64
142
65
143
exit ( node , parent , scope , file ) {
144
+ const { messages} = file . get ( 'react-intl' ) ;
145
+ const messagesDir = getMessagesDir ( file . opts ) ;
66
146
const { basename, filename} = file . opts ;
67
- const { messagesDir} = file . opts . extra . reactIntl ;
68
147
69
- let messagesFilename = path . join (
70
- messagesDir , path . dirname ( filename ) , basename + '.json'
148
+ let messagesFilename = p . join (
149
+ messagesDir ,
150
+ p . dirname ( p . relative ( process . cwd ( ) , filename ) ) ,
151
+ basename + '.json'
71
152
) ;
72
153
73
- let { messages } = file . reactIntl ;
74
- let messagesFile = JSON . stringify ( messages , null , 2 ) ;
154
+ let descriptors = [ ... messages . values ( ) ] ;
155
+ let messagesFile = JSON . stringify ( descriptors , null , 2 ) ;
75
156
76
- mkdirpSync ( path . dirname ( messagesFilename ) ) ;
157
+ mkdirpSync ( p . dirname ( messagesFilename ) ) ;
77
158
writeFileSync ( messagesFilename , messagesFile ) ;
78
159
}
79
160
} ,
80
161
81
162
JSXOpeningElement ( node , parent , scope , file ) {
82
- const { moduleSourceName} = file . opts . extra . reactIntl ;
163
+ const moduleSourceName = getModuleSourceName ( file . opts ) ;
83
164
let name = this . get ( 'name' ) ;
84
165
85
166
if ( referencesImport ( name , moduleSourceName , COMPONENT_NAMES ) ) {
86
- let message = node . attributes
87
- . filter ( ( attr ) => ATTR_WHITELIST . has ( attr . name . name ) )
88
- . reduce ( ( message , attr ) => {
89
- message [ attr . name . name ] = attr . value . value ;
90
- return message ;
91
- } , { } ) ;
92
-
93
- let { messages} = file . reactIntl ;
167
+ let attributes = this . get ( 'attributes' )
168
+ . map ( ( attr ) => [ attr . get ( 'name' ) , attr . get ( 'value' ) ] ) ;
94
169
95
- checkMessageId ( messages , message , node , file ) ;
96
- Object . assign ( messages , { [ message . id ] : message } ) ;
170
+ let descriptor = getMessageDescriptor ( new Map ( attributes ) ) ;
171
+ storeMessage ( descriptor , node , file ) ;
97
172
}
98
173
} ,
99
174
100
175
CallExpression ( node , parent , scope , file ) {
101
- const { moduleSourceName} = file . opts . extra . reactIntl ;
176
+ const moduleSourceName = getModuleSourceName ( file . opts ) ;
102
177
103
178
let callee = this . get ( 'callee' ) ;
104
- let messageArg = node . arguments [ 1 ] ;
105
179
106
- if ( referencesImport ( callee , moduleSourceName , FUNCTION_NAMES ) &&
107
- t . isObjectExpression ( messageArg ) ) {
180
+ if ( referencesImport ( callee , moduleSourceName , FUNCTION_NAMES ) ) {
181
+ let messageArg = this . get ( 'arguments' ) [ 1 ] ;
182
+ if ( ! messageArg ) {
183
+ throw file . errorWithNode ( node ,
184
+ `[React Intl] \`${ callee . node . name } ()\` requires ` +
185
+ `a message descriptor as the second argument.`
186
+ ) ;
187
+ }
108
188
109
- let message = messageArg . properties
110
- . filter ( ( prop ) => ATTR_WHITELIST . has ( prop . key . name ) )
111
- . reduce ( ( message , prop ) => {
112
- message [ prop . key . name ] = prop . value . value ;
113
- return message ;
114
- } , { } ) ;
189
+ if ( ! ( messageArg && messageArg . isObjectExpression ( ) ) ) {
190
+ let { loc} = messageArg . node ;
191
+ file . log . warn (
192
+ `[React Intl] Line ${ loc . start . line } : ` +
193
+ `\`${ callee . node . name } ()\` must use an inline ` +
194
+ `object expression for the message to be extracted.`
195
+ ) ;
196
+ return ;
197
+ }
115
198
116
- let { messages} = file . reactIntl ;
199
+ let properties = messageArg . get ( 'properties' )
200
+ . map ( ( prop ) => [ prop . get ( 'key' ) , prop . get ( 'value' ) ] ) ;
117
201
118
- checkMessageId ( messages , message , node , file ) ;
119
- Object . assign ( messages , { [ message . id ] : message } ) ;
202
+ let descriptor = getMessageDescriptor ( new Map ( properties ) ) ;
203
+ storeMessage ( descriptor , node , file ) ;
120
204
}
121
205
}
122
206
}
0 commit comments