Skip to content
This repository was archived by the owner on Jun 8, 2019. It is now read-only.

Commit ba71660

Browse files
committed
Make more robust with better error messages
1 parent f883d80 commit ba71660

File tree

1 file changed

+133
-49
lines changed

1 file changed

+133
-49
lines changed

src/index.js

Lines changed: 133 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as path from 'path';
2-
import {readFileSync, writeFileSync} from 'fs';
1+
import * as p from 'path';
2+
import {writeFileSync} from 'fs';
33
import {sync as mkdirpSync} from 'mkdirp';
44

55
const 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

1818
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;
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

Comments
 (0)