UNPKG

15.6 kBJavaScriptView Raw
1/*
2 * Copyright 2015, Yahoo Inc.
3 * Copyrights licensed under the New BSD License.
4 * See the accompanying LICENSE file for terms.
5 */
6var __assign = (this && this.__assign) || function () {
7 __assign = Object.assign || function(t) {
8 for (var s, i = 1, n = arguments.length; i < n; i++) {
9 s = arguments[i];
10 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
11 t[p] = s[p];
12 }
13 return t;
14 };
15 return __assign.apply(this, arguments);
16};
17import * as p from 'path';
18import { writeFileSync } from 'fs';
19import { mkdirpSync } from 'fs-extra';
20import { printAST, parse } from 'intl-messageformat-parser';
21var declare = require('@babel/helper-plugin-utils').declare;
22import { types as t } from '@babel/core';
23var DEFAULT_COMPONENT_NAMES = ['FormattedMessage', 'FormattedHTMLMessage'];
24var FUNCTION_NAMES = ['defineMessages'];
25var EXTRACTED = Symbol('ReactIntlExtracted');
26var DESCRIPTOR_PROPS = new Set(['id', 'description', 'defaultMessage']);
27function getICUMessageValue(messagePath, _a) {
28 var _b = (_a === void 0 ? {} : _a).isJSXSource, isJSXSource = _b === void 0 ? false : _b;
29 if (!messagePath) {
30 return '';
31 }
32 var message = getMessageDescriptorValue(messagePath);
33 try {
34 return printAST(parse(message));
35 }
36 catch (parseError) {
37 if (isJSXSource &&
38 messagePath.isLiteral() &&
39 message.indexOf('\\\\') >= 0) {
40 throw messagePath.buildCodeFrameError('[React Intl] Message failed to parse. ' +
41 'It looks like `\\`s were used for escaping, ' +
42 "this won't work with JSX string literals. " +
43 'Wrap with `{}`. ' +
44 'See: http://facebook.github.io/react/docs/jsx-gotchas.html');
45 }
46 throw messagePath.buildCodeFrameError('[React Intl] Message failed to parse. ' +
47 'See: http://formatjs.io/guides/message-syntax/' +
48 ("\n" + parseError));
49 }
50}
51function evaluatePath(path) {
52 var evaluated = path.evaluate();
53 if (evaluated.confident) {
54 return evaluated.value;
55 }
56 throw path.buildCodeFrameError('[React Intl] Messages must be statically evaluate-able for extraction.');
57}
58function getMessageDescriptorKey(path) {
59 if (path.isIdentifier() || path.isJSXIdentifier()) {
60 return path.node.name;
61 }
62 return evaluatePath(path);
63}
64function getMessageDescriptorValue(path) {
65 if (!path) {
66 return '';
67 }
68 if (path.isJSXExpressionContainer()) {
69 path = path.get('expression');
70 }
71 // Always trim the Message Descriptor values.
72 var descriptorValue = evaluatePath(path);
73 if (typeof descriptorValue === 'string') {
74 return descriptorValue.trim();
75 }
76 return descriptorValue;
77}
78function createMessageDescriptor(propPaths) {
79 return propPaths.reduce(function (hash, _a) {
80 var keyPath = _a[0], valuePath = _a[1];
81 var key = getMessageDescriptorKey(keyPath);
82 if (DESCRIPTOR_PROPS.has(key)) {
83 hash[key] = valuePath;
84 }
85 return hash;
86 }, {
87 id: undefined,
88 defaultMessage: undefined,
89 description: undefined
90 });
91}
92function evaluateMessageDescriptor(descriptorPath, isJSXSource, overrideIdFn) {
93 if (isJSXSource === void 0) { isJSXSource = false; }
94 var id = getMessageDescriptorValue(descriptorPath.id);
95 var defaultMessage = getICUMessageValue(descriptorPath.defaultMessage, {
96 isJSXSource: isJSXSource
97 });
98 var description = getMessageDescriptorValue(descriptorPath.description);
99 if (overrideIdFn) {
100 id = overrideIdFn(id, defaultMessage, description);
101 }
102 var descriptor = {
103 id: id
104 };
105 if (description) {
106 descriptor.description = description;
107 }
108 if (defaultMessage) {
109 descriptor.defaultMessage = defaultMessage;
110 }
111 return descriptor;
112}
113function storeMessage(_a, path, _b, filename, messages) {
114 var id = _a.id, description = _a.description, defaultMessage = _a.defaultMessage;
115 var enforceDescriptions = _b.enforceDescriptions, _c = _b.enforceDefaultMessage, enforceDefaultMessage = _c === void 0 ? true : _c, extractSourceLocation = _b.extractSourceLocation;
116 if (!id || (enforceDefaultMessage && !defaultMessage)) {
117 throw path.buildCodeFrameError('[React Intl] Message Descriptors require an `id` and `defaultMessage`.');
118 }
119 if (messages.has(id)) {
120 var existing = messages.get(id);
121 if (description !== existing.description ||
122 defaultMessage !== existing.defaultMessage) {
123 throw path.buildCodeFrameError("[React Intl] Duplicate message id: \"" + id + "\", " +
124 'but the `description` and/or `defaultMessage` are different.');
125 }
126 }
127 if (enforceDescriptions) {
128 if (!description ||
129 (typeof description === 'object' && Object.keys(description).length < 1)) {
130 throw path.buildCodeFrameError('[React Intl] Message must have a `description`.');
131 }
132 }
133 var loc = {};
134 if (extractSourceLocation) {
135 loc = __assign({ file: p.relative(process.cwd(), filename) }, path.node.loc);
136 }
137 messages.set(id, __assign({ id: id, description: description, defaultMessage: defaultMessage }, loc));
138}
139function referencesImport(path, mod, importedNames) {
140 if (!(path.isIdentifier() || path.isJSXIdentifier())) {
141 return false;
142 }
143 return importedNames.some(function (name) { return path.referencesImport(mod, name); });
144}
145function isFormatMessageCall(callee) {
146 if (!callee.isMemberExpression()) {
147 return false;
148 }
149 var object = callee.get('object');
150 var property = callee.get('property');
151 return (property.isIdentifier() &&
152 property.node.name === 'formatMessage' &&
153 // things like `intl.formatMessage`
154 ((object.isIdentifier() && object.node.name === 'intl') ||
155 // things like `this.props.intl.formatMessage`
156 (object.isMemberExpression() &&
157 object.get('property').node.name === 'intl')));
158}
159function assertObjectExpression(path, callee) {
160 if (!path || !path.isObjectExpression()) {
161 throw path.buildCodeFrameError("[React Intl] `" + callee.get('property').node.name + "()` must be " +
162 'called with an object expression with values ' +
163 'that are React Intl Message Descriptors, also ' +
164 'defined as object expressions.');
165 }
166 return true;
167}
168export default declare(function (api) {
169 api.assertVersion(7);
170 /**
171 * Store this in the node itself so that multiple passes work. Specifically
172 * if we remove `description` in the 1st pass, 2nd pass will fail since
173 * it expect `description` to be there.
174 * HACK: We store this in the node instance since this persists across
175 * multiple plugin runs
176 */
177 function tagAsExtracted(path) {
178 path.node[EXTRACTED] = true;
179 }
180 function wasExtracted(path) {
181 return !!path.node[EXTRACTED];
182 }
183 return {
184 pre: function () {
185 if (!this.ReactIntlMessages) {
186 this.ReactIntlMessages = new Map();
187 }
188 },
189 post: function (state) {
190 var _a = this, filename = _a.file.opts.filename, messagesDir = _a.opts.messagesDir;
191 var basename = p.basename(filename, p.extname(filename));
192 var messages = this.ReactIntlMessages;
193 var descriptors = Array.from(messages.values());
194 state.metadata['react-intl'] = { messages: descriptors };
195 if (messagesDir && descriptors.length > 0) {
196 // Make sure the relative path is "absolute" before
197 // joining it with the `messagesDir`.
198 var relativePath = p.join(p.sep, p.relative(process.cwd(), filename));
199 // Solve when the window user has symlink on the directory, because
200 // process.cwd on windows returns the symlink root,
201 // and filename (from babel) returns the original root
202 if (process.platform === 'win32') {
203 var name_1 = p.parse(process.cwd()).name;
204 if (relativePath.includes(name_1)) {
205 relativePath = relativePath.slice(relativePath.indexOf(name_1) + name_1.length);
206 }
207 }
208 var messagesFilename = p.join(messagesDir, p.dirname(relativePath), basename + '.json');
209 var messagesFile = JSON.stringify(descriptors, null, 2);
210 mkdirpSync(p.dirname(messagesFilename));
211 writeFileSync(messagesFilename, messagesFile);
212 }
213 },
214 visitor: {
215 JSXOpeningElement: function (path, _a) {
216 var opts = _a.opts, filename = _a.file.opts.filename;
217 var _b = opts.moduleSourceName, moduleSourceName = _b === void 0 ? 'react-intl' : _b, _c = opts.additionalComponentNames, additionalComponentNames = _c === void 0 ? [] : _c, enforceDefaultMessage = opts.enforceDefaultMessage, removeDefaultMessage = opts.removeDefaultMessage, overrideIdFn = opts.overrideIdFn;
218 if (wasExtracted(path)) {
219 return;
220 }
221 var name = path.get('name');
222 if (name.referencesImport(moduleSourceName, 'FormattedPlural')) {
223 if (path.node && path.node.loc)
224 console.warn("[React Intl] Line " + path.node.loc.start.line + ": " +
225 'Default messages are not extracted from ' +
226 '<FormattedPlural>, use <FormattedMessage> instead.');
227 return;
228 }
229 if (name.isJSXIdentifier() &&
230 (referencesImport(name, moduleSourceName, DEFAULT_COMPONENT_NAMES) ||
231 additionalComponentNames.includes(name.node.name))) {
232 var attributes = path
233 .get('attributes')
234 .filter(function (attr) {
235 return attr.isJSXAttribute();
236 });
237 var descriptorPath = createMessageDescriptor(attributes.map(function (attr) { return [
238 attr.get('name'),
239 attr.get('value')
240 ]; }));
241 // In order for a default message to be extracted when
242 // declaring a JSX element, it must be done with standard
243 // `key=value` attributes. But it's completely valid to
244 // write `<FormattedMessage {...descriptor} />` or
245 // `<FormattedMessage id={dynamicId} />`, because it will be
246 // skipped here and extracted elsewhere. The descriptor will
247 // be extracted only if a `defaultMessage` prop exists and
248 // `enforceDefaultMessage` is `true`.
249 if (enforceDefaultMessage === false ||
250 descriptorPath.defaultMessage) {
251 // Evaluate the Message Descriptor values in a JSX
252 // context, then store it.
253 var descriptor_1 = evaluateMessageDescriptor(descriptorPath, true, overrideIdFn);
254 storeMessage(descriptor_1, path, opts, filename, this.ReactIntlMessages);
255 attributes.forEach(function (attr) {
256 var ketPath = attr.get('name');
257 var msgDescriptorKey = getMessageDescriptorKey(ketPath);
258 if (
259 // Remove description since it's not used at runtime.
260 msgDescriptorKey === 'description' ||
261 // Remove defaultMessage if opts says so.
262 (removeDefaultMessage && msgDescriptorKey === 'defaultMessage')) {
263 attr.remove();
264 }
265 else if (overrideIdFn &&
266 getMessageDescriptorKey(ketPath) === 'id') {
267 attr.get('value').replaceWith(t.stringLiteral(descriptor_1.id));
268 }
269 });
270 // Tag the AST node so we don't try to extract it twice.
271 tagAsExtracted(path);
272 }
273 }
274 },
275 CallExpression: function (path, _a) {
276 var opts = _a.opts, filename = _a.file.opts.filename;
277 var messages = this.ReactIntlMessages;
278 var _b = opts.moduleSourceName, moduleSourceName = _b === void 0 ? 'react-intl' : _b, overrideIdFn = opts.overrideIdFn, removeDefaultMessage = opts.removeDefaultMessage, extractFromFormatMessageCall = opts.extractFromFormatMessageCall;
279 var callee = path.get('callee');
280 /**
281 * Process MessageDescriptor
282 * @param messageDescriptor Message Descriptor
283 */
284 function processMessageObject(messageDescriptor) {
285 assertObjectExpression(messageDescriptor, callee);
286 if (wasExtracted(messageDescriptor)) {
287 return;
288 }
289 var properties = messageDescriptor.get('properties');
290 var descriptorPath = createMessageDescriptor(properties.map(function (prop) {
291 return [prop.get('key'), prop.get('value')];
292 }));
293 // Evaluate the Message Descriptor values, then store it.
294 var descriptor = evaluateMessageDescriptor(descriptorPath, false, overrideIdFn);
295 storeMessage(descriptor, messageDescriptor, opts, filename, messages);
296 // Remove description since it's not used at runtime.
297 messageDescriptor.replaceWith(t.objectExpression([
298 t.objectProperty(t.stringLiteral('id'), t.stringLiteral(descriptor.id))
299 ].concat((!removeDefaultMessage && descriptor.defaultMessage
300 ? [
301 t.objectProperty(t.stringLiteral('defaultMessage'), t.stringLiteral(descriptor.defaultMessage))
302 ]
303 : []))));
304 // Tag the AST node so we don't try to extract it twice.
305 tagAsExtracted(messageDescriptor);
306 }
307 // Check that this is `defineMessages` call
308 if (referencesImport(callee, moduleSourceName, FUNCTION_NAMES)) {
309 var messagesObj = path.get('arguments')[0];
310 if (assertObjectExpression(messagesObj, callee)) {
311 messagesObj
312 .get('properties')
313 .map(function (prop) { return prop.get('value'); })
314 .forEach(processMessageObject);
315 }
316 }
317 // Check that this is `intl.formatMessage` call
318 if (extractFromFormatMessageCall && isFormatMessageCall(callee)) {
319 var messageDescriptor = path.get('arguments')[0];
320 if (messageDescriptor.isObjectExpression()) {
321 processMessageObject(messageDescriptor);
322 }
323 }
324 }
325 }
326 };
327});