1- import * as path from 'path' ;
2- import { readFileSync , writeFileSync } from 'fs' ;
1+ import * as p from 'path' ;
2+ import { writeFileSync } from 'fs' ;
33import { sync as mkdirpSync } from 'mkdirp' ;
44
55const COMPONENT_NAMES = [
@@ -12,111 +12,195 @@ const FUNCTION_NAMES = [
1212 'formatHTMLMessage' ,
1313] ;
1414
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' ] ) ;
1717
1818export 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 ;
2251 }
2352
24- return importedNames . some ( ( name ) => node . referencesImport ( mod , name ) ) ;
53+ let evaluated = path . evaluate ( ) ;
54+ if ( evaluated . confident ) {
55+ return evaluated . value ;
56+ }
2557 }
2658
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 ) {
2987 throw file . errorWithNode ( node ,
30- 'React Intl message is missing an `id`.'
88+ '[ React Intl] Message is missing an `id`.'
3189 ) ;
3290 }
3391
34- if ( messages . hasOwnProperty ( message . id ) ) {
92+ if ( messages . has ( id ) ) {
3593 throw file . errorWithNode ( node ,
36- `Duplicate React Intl message id: "${ message . id } "`
94+ `[ React Intl] Duplicate message id: "${ id } "`
3795 ) ;
3896 }
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 ) ) ;
39117 }
40118
41119 return new Plugin ( 'react-intl' , {
42120 visitor : {
43121 Program : {
44122 enter ( node , parent , scope , file ) {
45- const { moduleSourceName} = file . opts . extra . reactIntl ;
123+ const moduleSourceName = getModuleSourceName ( file . opts ) ;
46124 const { imports} = file . metadata . modules ;
47125
48- let hasReactIntlMessages = imports . some ( ( mod ) => {
126+ let mightHaveReactIntlMessages = imports . some ( ( mod ) => {
49127 if ( mod . source === moduleSourceName ) {
50128 return mod . imported . some ( ( name ) => {
51129 return IMPORTED_NAMES . has ( name ) ;
52130 } ) ;
53131 }
54132 } ) ;
55133
56- if ( hasReactIntlMessages ) {
57- file . reactIntl = {
58- messages : { }
59- } ;
134+ if ( mightHaveReactIntlMessages ) {
135+ file . set ( 'react-intl' , {
136+ messages : new Map ( ) ,
137+ } ) ;
60138 } else {
61139 this . skip ( ) ;
62140 }
63141 } ,
64142
65143 exit ( node , parent , scope , file ) {
144+ const { messages} = file . get ( 'react-intl' ) ;
145+ const messagesDir = getMessagesDir ( file . opts ) ;
66146 const { basename, filename} = file . opts ;
67- const { messagesDir} = file . opts . extra . reactIntl ;
68147
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'
71152 ) ;
72153
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 ) ;
75156
76- mkdirpSync ( path . dirname ( messagesFilename ) ) ;
157+ mkdirpSync ( p . dirname ( messagesFilename ) ) ;
77158 writeFileSync ( messagesFilename , messagesFile ) ;
78159 }
79160 } ,
80161
81162 JSXOpeningElement ( node , parent , scope , file ) {
82- const { moduleSourceName} = file . opts . extra . reactIntl ;
163+ const moduleSourceName = getModuleSourceName ( file . opts ) ;
83164 let name = this . get ( 'name' ) ;
84165
85166 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' ) ] ) ;
94169
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 ) ;
97172 }
98173 } ,
99174
100175 CallExpression ( node , parent , scope , file ) {
101- const { moduleSourceName} = file . opts . extra . reactIntl ;
176+ const moduleSourceName = getModuleSourceName ( file . opts ) ;
102177
103178 let callee = this . get ( 'callee' ) ;
104- let messageArg = node . arguments [ 1 ] ;
105179
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+ }
108188
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+ }
115198
116- let { messages} = file . reactIntl ;
199+ let properties = messageArg . get ( 'properties' )
200+ . map ( ( prop ) => [ prop . get ( 'key' ) , prop . get ( 'value' ) ] ) ;
117201
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 ) ;
120204 }
121205 }
122206 }
0 commit comments