77import * as p from 'path' ;
88import { writeFileSync } from 'fs' ;
99import { sync as mkdirpSync } from 'mkdirp' ;
10+ import printICUMessage from './print-icu-message' ;
1011
1112const COMPONENT_NAMES = [
1213 'FormattedMessage' ,
1314 'FormattedHTMLMessage' ,
1415] ;
1516
1617const FUNCTION_NAMES = [
17- 'defineMessage ' ,
18+ 'defineMessages ' ,
1819] ;
1920
2021const IMPORTED_NAMES = new Set ( [ ...COMPONENT_NAMES , ...FUNCTION_NAMES ] ) ;
2122const DESCRIPTOR_PROPS = new Set ( [ 'id' , 'description' , 'defaultMessage' ] ) ;
2223
23- export default function ( { Plugin} ) {
24- function getModuleSourceName ( options ) {
25- const reactIntlOptions = options . extra [ 'react-intl' ] || { } ;
26- return reactIntlOptions . moduleSourceName || 'react-intl' ;
27- }
28-
29- function getMessagesDir ( options ) {
30- const reactIntlOptions = options . extra [ 'react-intl' ] || { } ;
31- return reactIntlOptions . messagesDir ;
24+ export default function ( { Plugin, types : t } ) {
25+ function getReactIntlOptions ( options ) {
26+ return options . extra [ 'react-intl' ] || { } ;
3227 }
3328
34- function getMessageDescriptor ( propertiesMap ) {
35- // Force property order on descriptors.
36- let descriptor = [ ...DESCRIPTOR_PROPS ] . reduce ( ( descriptor , key ) => {
37- descriptor [ key ] = undefined ;
38- return descriptor ;
39- } , { } ) ;
40-
41- for ( let [ key , value ] of propertiesMap ) {
42- key = getMessageDescriptorKey ( key ) ;
43-
44- if ( DESCRIPTOR_PROPS . has ( key ) ) {
45- // TODO: Should this be trimming values?
46- descriptor [ key ] = getMessageDescriptorValue ( value ) . trim ( ) ;
47- }
48- }
49-
50- return descriptor ;
29+ function getModuleSourceName ( options ) {
30+ return getReactIntlOptions ( options ) . moduleSourceName || 'react-intl' ;
5131 }
5232
5333 function getMessageDescriptorKey ( path ) {
@@ -84,9 +64,38 @@ export default function ({Plugin}) {
8464 ) ;
8565 }
8666
87- function storeMessage ( descriptor , node , file ) {
88- const { id} = descriptor ;
89- const { messages} = file . get ( 'react-intl' ) ;
67+ function createMessageDescriptor ( propPaths ) {
68+ return propPaths . reduce ( ( hash , [ keyPath , valuePath ] ) => {
69+ let key = getMessageDescriptorKey ( keyPath ) ;
70+
71+ if ( DESCRIPTOR_PROPS . has ( key ) ) {
72+ let value = getMessageDescriptorValue ( valuePath ) . trim ( ) ;
73+
74+ if ( key === 'defaultMessage' ) {
75+ try {
76+ hash [ key ] = printICUMessage ( value ) ;
77+ } catch ( e ) {
78+ throw valuePath . errorWithNode (
79+ `[React Intl] Message failed to parse: ${ e } ` +
80+ 'See: http://formatjs.io/guides/message-syntax/'
81+ ) ;
82+ }
83+ } else {
84+ hash [ key ] = value ;
85+ }
86+ }
87+
88+ return hash ;
89+ } , { } ) ;
90+ }
91+
92+ function createPropNode ( key , value ) {
93+ return t . property ( 'init' , t . literal ( key ) , t . literal ( value ) ) ;
94+ }
95+
96+ function storeMessage ( { id, description, defaultMessage} , node , file ) {
97+ const { enforceDescriptions} = getReactIntlOptions ( file . opts ) ;
98+ const { messages} = file . get ( 'react-intl' ) ;
9099
91100 if ( ! id ) {
92101 throw file . errorWithNode ( node ,
@@ -95,22 +104,35 @@ export default function ({Plugin}) {
95104 }
96105
97106 if ( messages . has ( id ) ) {
98- throw file . errorWithNode ( node ,
99- `[React Intl] Duplicate message id: "${ id } "`
100- ) ;
107+ let existing = messages . get ( id ) ;
108+
109+ if ( description !== existing . description ||
110+ defaultMessage !== existing . defaultMessage ) {
111+
112+ throw file . errorWithNode ( node ,
113+ `[React Intl] Duplicate message id: "${ id } ", ` +
114+ 'but the `description` and/or `defaultMessage` are different.'
115+ ) ;
116+ }
101117 }
102118
103- if ( ! descriptor . defaultMessage ) {
119+ if ( ! defaultMessage ) {
104120 let { loc} = node ;
105121 file . log . warn (
106122 `[React Intl] Line ${ loc . start . line } : ` +
107- `Message "${ id } " is missing a \`defaultMessage\` ` +
108- `and will not be extracted.`
123+ 'Message is missing a `defaultMessage` and will not be extracted.'
109124 ) ;
125+
110126 return ;
111127 }
112128
113- messages . set ( id , descriptor ) ;
129+ if ( enforceDescriptions && ! description ) {
130+ throw file . errorWithNode ( node ,
131+ '[React Intl] Message must have a `description`.'
132+ ) ;
133+ }
134+
135+ messages . set ( id , { id, description, defaultMessage} ) ;
114136 }
115137
116138 function referencesImport ( path , mod , importedNames ) {
@@ -147,7 +169,7 @@ export default function ({Plugin}) {
147169
148170 exit ( node , parent , scope , file ) {
149171 const { messages} = file . get ( 'react-intl' ) ;
150- const messagesDir = getMessagesDir ( file . opts ) ;
172+ const { messagesDir} = getReactIntlOptions ( file . opts ) ;
151173 const { basename, filename} = file . opts ;
152174
153175 let descriptors = [ ...messages . values ( ) ] ;
@@ -170,46 +192,79 @@ export default function ({Plugin}) {
170192
171193 JSXOpeningElement ( node , parent , scope , file ) {
172194 const moduleSourceName = getModuleSourceName ( file . opts ) ;
195+
173196 let name = this . get ( 'name' ) ;
174197
175198 if ( referencesImport ( name , moduleSourceName , COMPONENT_NAMES ) ) {
176199 let attributes = this . get ( 'attributes' )
177- . filter ( ( attr ) => attr . isJSXAttribute ( ) )
178- . map ( ( attr ) => [ attr . get ( 'name' ) , attr . get ( 'value' ) ] ) ;
200+ . filter ( ( attr ) => attr . isJSXAttribute ( ) ) ;
179201
180- let descriptor = getMessageDescriptor ( new Map ( attributes ) ) ;
202+ let descriptor = createMessageDescriptor (
203+ attributes . map ( ( attr ) => [
204+ attr . get ( 'name' ) ,
205+ attr . get ( 'value' ) ,
206+ ] )
207+ ) ;
181208
182209 // In order for a default message to be extracted when
183210 // declaring a JSX element, it must be done with standard
184211 // `key=value` attributes. But it's completely valid to
185212 // write `<FormattedMessage {...descriptor} />`, because it
186- // will be skipped here and extracted elsewhere.
187- if ( descriptor . id ) {
213+ // will be skipped here and extracted elsewhere. When
214+ // _either_ an `id` or `defaultMessage` prop exists, the
215+ // descriptor will be checked; this way mixing an object
216+ // spread with props will fail.
217+ if ( descriptor . id || descriptor . defaultMessage ) {
188218 storeMessage ( descriptor , node , file ) ;
219+
220+ attributes
221+ . filter ( ( attr ) => {
222+ let keyPath = attr . get ( 'name' ) ;
223+ let key = getMessageDescriptorKey ( keyPath ) ;
224+ return key === 'description' ;
225+ } )
226+ . forEach ( ( attr ) => attr . dangerouslyRemove ( ) ) ;
189227 }
190228 }
191229 } ,
192230
193231 CallExpression ( node , parent , scope , file ) {
194232 const moduleSourceName = getModuleSourceName ( file . opts ) ;
195233
196- let callee = this . get ( 'callee' ) ;
197-
198- if ( referencesImport ( callee , moduleSourceName , FUNCTION_NAMES ) ) {
199- let messageArg = this . get ( 'arguments' ) [ 0 ] ;
200- if ( ! ( messageArg && messageArg . isObjectExpression ( ) ) ) {
234+ function processMessageObject ( messageObj ) {
235+ if ( ! ( messageObj && messageObj . isObjectExpression ( ) ) ) {
201236 throw file . errorWithNode ( node ,
202237 `[React Intl] \`${ callee . node . name } ()\` must be ` +
203238 `called with message descriptor defined via an ` +
204239 `object expression.`
205240 ) ;
206241 }
207242
208- let properties = messageArg . get ( 'properties' )
209- . map ( ( prop ) => [ prop . get ( 'key' ) , prop . get ( 'value' ) ] ) ;
243+ let properties = messageObj . get ( 'properties' ) ;
244+
245+ let descriptor = createMessageDescriptor (
246+ properties . map ( ( prop ) => [
247+ prop . get ( 'key' ) ,
248+ prop . get ( 'value' ) ,
249+ ] )
250+ ) ;
210251
211- let descriptor = getMessageDescriptor ( new Map ( properties ) ) ;
212252 storeMessage ( descriptor , node , file ) ;
253+
254+ messageObj . replaceWith ( t . objectExpression ( [
255+ createPropNode ( 'id' , descriptor . id ) ,
256+ createPropNode ( 'defaultMessage' , descriptor . defaultMessage ) ,
257+ ] ) ) ;
258+ }
259+
260+ let callee = this . get ( 'callee' ) ;
261+
262+ if ( referencesImport ( callee , moduleSourceName , FUNCTION_NAMES ) ) {
263+ let firstArg = this . get ( 'arguments' ) [ 0 ] ;
264+
265+ firstArg . get ( 'properties' )
266+ . map ( ( prop ) => prop . get ( 'value' ) )
267+ . forEach ( processMessageObject ) ;
213268 }
214269 } ,
215270 } ,
0 commit comments