UNPKG

19.7 kBJavaScriptView Raw
1"use strict";
2/*
3 * Copyright 2015, Yahoo Inc.
4 * Copyrights licensed under the New BSD License.
5 * See the accompanying LICENSE file for terms.
6 */
7Object.defineProperty(exports, "__esModule", { value: true });
8var tslib_1 = require("tslib");
9var intl_messageformat_parser_1 = require("intl-messageformat-parser");
10var helper_plugin_utils_1 = require("@babel/helper-plugin-utils");
11var core_1 = require("@babel/core");
12var types_1 = require("@babel/types");
13var schema_utils_1 = require("schema-utils");
14var OPTIONS_SCHEMA = tslib_1.__importStar(require("./options.schema.json"));
15var ts_transformer_1 = require("@formatjs/ts-transformer");
16var DEFAULT_COMPONENT_NAMES = ['FormattedMessage'];
17var EXTRACTED = Symbol('ReactIntlExtracted');
18var DESCRIPTOR_PROPS = new Set(['id', 'description', 'defaultMessage']);
19function getICUMessageValue(messagePath, _a) {
20 var _b = _a === void 0 ? {} : _a, _c = _b.isJSXSource, isJSXSource = _c === void 0 ? false : _c;
21 if (!messagePath) {
22 return '';
23 }
24 var message = getMessageDescriptorValue(messagePath)
25 .trim()
26 .replace(/\s+/gm, ' ');
27 try {
28 intl_messageformat_parser_1.parse(message);
29 }
30 catch (parseError) {
31 if (isJSXSource &&
32 messagePath.isLiteral() &&
33 message.indexOf('\\\\') >= 0) {
34 throw messagePath.buildCodeFrameError('[React Intl] Message failed to parse. ' +
35 'It looks like `\\`s were used for escaping, ' +
36 "this won't work with JSX string literals. " +
37 'Wrap with `{}`. ' +
38 'See: http://facebook.github.io/react/docs/jsx-gotchas.html');
39 }
40 throw messagePath.buildCodeFrameError('[React Intl] Message failed to parse. ' +
41 'See: https://formatjs.io/docs/core-concepts/icu-syntax' +
42 ("\n" + parseError));
43 }
44 return message;
45}
46function evaluatePath(path) {
47 var evaluated = path.evaluate();
48 if (evaluated.confident) {
49 return evaluated.value;
50 }
51 throw path.buildCodeFrameError('[React Intl] Messages must be statically evaluate-able for extraction.');
52}
53function getMessageDescriptorKey(path) {
54 if (path.isIdentifier() || path.isJSXIdentifier()) {
55 return path.node.name;
56 }
57 return evaluatePath(path);
58}
59function getMessageDescriptorValue(path) {
60 if (!path) {
61 return '';
62 }
63 if (path.isJSXExpressionContainer()) {
64 path = path.get('expression');
65 }
66 // Always trim the Message Descriptor values.
67 var descriptorValue = evaluatePath(path);
68 return descriptorValue;
69}
70function createMessageDescriptor(propPaths) {
71 return propPaths.reduce(function (hash, _a) {
72 var keyPath = _a[0], valuePath = _a[1];
73 var key = getMessageDescriptorKey(keyPath);
74 if (DESCRIPTOR_PROPS.has(key)) {
75 hash[key] = valuePath;
76 }
77 return hash;
78 }, {
79 id: undefined,
80 defaultMessage: undefined,
81 description: undefined,
82 });
83}
84function evaluateMessageDescriptor(descriptorPath, isJSXSource, filename, idInterpolationPattern, overrideIdFn) {
85 if (isJSXSource === void 0) { isJSXSource = false; }
86 if (idInterpolationPattern === void 0) { idInterpolationPattern = '[contenthash:5]'; }
87 var id = getMessageDescriptorValue(descriptorPath.id);
88 var defaultMessage = getICUMessageValue(descriptorPath.defaultMessage, {
89 isJSXSource: isJSXSource,
90 });
91 var description = getMessageDescriptorValue(descriptorPath.description);
92 if (overrideIdFn) {
93 id = overrideIdFn(id, defaultMessage, description, filename);
94 }
95 else if (!id && idInterpolationPattern && defaultMessage) {
96 id = ts_transformer_1.interpolateName({ resourcePath: filename }, idInterpolationPattern, {
97 content: description
98 ? defaultMessage + "#" + description
99 : defaultMessage,
100 });
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 extractSourceLocation = _b.extractSourceLocation;
116 if (!id && !defaultMessage) {
117 throw path.buildCodeFrameError('[React Intl] Message Descriptors require an `id` or `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 var loc = {};
128 if (extractSourceLocation) {
129 loc = tslib_1.__assign({ file: filename }, path.node.loc);
130 }
131 messages.set(id, tslib_1.__assign({ id: id, description: description, defaultMessage: defaultMessage }, loc));
132}
133function referencesImport(path, mod, importedNames) {
134 if (!(path.isIdentifier() || path.isJSXIdentifier())) {
135 return false;
136 }
137 return importedNames.some(function (name) { return path.referencesImport(mod, name); });
138}
139function isFormatMessageDestructuring(scope) {
140 var binding = scope.getBinding('formatMessage');
141 var block = scope.block;
142 var declNode = binding === null || binding === void 0 ? void 0 : binding.path.node;
143 // things like `const {formatMessage} = intl; formatMessage(...)`
144 if (core_1.types.isVariableDeclarator(declNode)) {
145 // things like `const {formatMessage} = useIntl(); formatMessage(...)`
146 if (core_1.types.isCallExpression(declNode.init)) {
147 if (core_1.types.isIdentifier(declNode.init.callee)) {
148 return declNode.init.callee.name === 'useIntl';
149 }
150 }
151 return (core_1.types.isObjectPattern(declNode.id) &&
152 declNode.id.properties.find(function (value) { return value.key.name === 'intl'; }));
153 }
154 // things like const fn = ({ intl: { formatMessage }}) => { formatMessage(...) }
155 if (core_1.types.isFunctionDeclaration(block) &&
156 block.params.length &&
157 core_1.types.isObjectPattern(block.params[0])) {
158 return block.params[0].properties.find(function (value) { return value.key.name === 'intl'; });
159 }
160 return false;
161}
162function isFormatMessageCall(callee, path) {
163 if (callee.isIdentifier() &&
164 callee.node.name === 'formatMessage' &&
165 isFormatMessageDestructuring(path.scope)) {
166 return true;
167 }
168 if (!callee.isMemberExpression()) {
169 return false;
170 }
171 var object = callee.get('object');
172 var property = callee.get('property');
173 return (property.isIdentifier() &&
174 property.node.name === 'formatMessage' &&
175 !Array.isArray(object) &&
176 // things like `intl.formatMessage`
177 ((object.isIdentifier() && object.node.name === 'intl') ||
178 // things like `this.props.intl.formatMessage`
179 (object.isMemberExpression() &&
180 object.get('property').node.name === 'intl')));
181}
182function assertObjectExpression(path, callee) {
183 if (!path || !path.isObjectExpression()) {
184 throw path.buildCodeFrameError("[React Intl] `" + callee.get('property').node.name + "()` must be " +
185 'called with an object expression with values ' +
186 'that are React Intl Message Descriptors, also ' +
187 'defined as object expressions.');
188 }
189 return true;
190}
191exports.default = helper_plugin_utils_1.declare(function (api, options) {
192 api.assertVersion(7);
193 schema_utils_1.validate(OPTIONS_SCHEMA, options, {
194 name: 'babel-plugin-react-intl',
195 baseDataPath: 'options',
196 });
197 var pragma = options.pragma;
198 /**
199 * Store this in the node itself so that multiple passes work. Specifically
200 * if we remove `description` in the 1st pass, 2nd pass will fail since
201 * it expect `description` to be there.
202 * HACK: We store this in the node instance since this persists across
203 * multiple plugin runs
204 */
205 function tagAsExtracted(path) {
206 path.node[EXTRACTED] = true;
207 }
208 function wasExtracted(path) {
209 return !!path.node[EXTRACTED];
210 }
211 return {
212 pre: function () {
213 if (!this.ReactIntlMessages) {
214 this.ReactIntlMessages = new Map();
215 this.ReactIntlMeta = {};
216 }
217 },
218 post: function (state) {
219 var _a = this, messages = _a.ReactIntlMessages, ReactIntlMeta = _a.ReactIntlMeta;
220 var descriptors = Array.from(messages.values());
221 state.metadata['react-intl'] = {
222 messages: descriptors,
223 meta: ReactIntlMeta,
224 };
225 },
226 visitor: {
227 Program: function (path) {
228 var body = path.node.body;
229 var ReactIntlMeta = this.ReactIntlMeta;
230 if (!pragma) {
231 return;
232 }
233 for (var _i = 0, body_1 = body; _i < body_1.length; _i++) {
234 var leadingComments = body_1[_i].leadingComments;
235 if (!leadingComments) {
236 continue;
237 }
238 var pragmaLineNode = leadingComments.find(function (c) {
239 return c.value.includes(pragma);
240 });
241 if (!pragmaLineNode) {
242 continue;
243 }
244 pragmaLineNode.value
245 .split(pragma)[1]
246 .trim()
247 .split(/\s+/g)
248 .forEach(function (kv) {
249 var _a = kv.split(':'), k = _a[0], v = _a[1];
250 ReactIntlMeta[k] = v;
251 });
252 }
253 },
254 JSXOpeningElement: function (path, _a) {
255 var opts = _a.opts, filename = _a.file.opts.filename;
256 var _b = opts.moduleSourceName, moduleSourceName = _b === void 0 ? 'react-intl' : _b, _c = opts.additionalComponentNames, additionalComponentNames = _c === void 0 ? [] : _c, removeDefaultMessage = opts.removeDefaultMessage, idInterpolationPattern = opts.idInterpolationPattern, overrideIdFn = opts.overrideIdFn, ast = opts.ast;
257 if (wasExtracted(path)) {
258 return;
259 }
260 var name = path.get('name');
261 if (name.referencesImport(moduleSourceName, 'FormattedPlural')) {
262 if (path.node && path.node.loc)
263 console.warn("[React Intl] Line " + path.node.loc.start.line + ": " +
264 'Default messages are not extracted from ' +
265 '<FormattedPlural>, use <FormattedMessage> instead.');
266 return;
267 }
268 if (name.isJSXIdentifier() &&
269 (referencesImport(name, moduleSourceName, DEFAULT_COMPONENT_NAMES) ||
270 additionalComponentNames.includes(name.node.name))) {
271 var attributes = path
272 .get('attributes')
273 .filter(function (attr) { return attr.isJSXAttribute(); });
274 var descriptorPath = createMessageDescriptor(attributes.map(function (attr) { return [
275 attr.get('name'),
276 attr.get('value'),
277 ]; }));
278 // In order for a default message to be extracted when
279 // declaring a JSX element, it must be done with standard
280 // `key=value` attributes. But it's completely valid to
281 // write `<FormattedMessage {...descriptor} />`, because it will be
282 // skipped here and extracted elsewhere. The descriptor will
283 // be extracted only (storeMessage) if a `defaultMessage` prop.
284 if (descriptorPath.id || descriptorPath.defaultMessage) {
285 // Evaluate the Message Descriptor values in a JSX
286 // context, then store it.
287 var descriptor = evaluateMessageDescriptor(descriptorPath, true, filename, idInterpolationPattern, overrideIdFn);
288 storeMessage(descriptor, path, opts, filename, this.ReactIntlMessages);
289 var idAttr = void 0;
290 var descriptionAttr = void 0;
291 var defaultMessageAttr = void 0;
292 for (var _i = 0, attributes_1 = attributes; _i < attributes_1.length; _i++) {
293 var attr = attributes_1[_i];
294 if (!attr.isJSXAttribute()) {
295 continue;
296 }
297 switch (getMessageDescriptorKey(attr.get('name'))) {
298 case 'description':
299 descriptionAttr = attr;
300 break;
301 case 'defaultMessage':
302 defaultMessageAttr = attr;
303 break;
304 case 'id':
305 idAttr = attr;
306 break;
307 }
308 }
309 if (descriptionAttr) {
310 descriptionAttr.remove();
311 }
312 if (!removeDefaultMessage &&
313 ast &&
314 descriptor.defaultMessage &&
315 defaultMessageAttr) {
316 defaultMessageAttr
317 .get('value')
318 .replaceWith(core_1.types.jsxExpressionContainer(core_1.types.stringLiteral('foo')));
319 defaultMessageAttr.get('value')
320 .get('expression')
321 .replaceWithSourceString(JSON.stringify(intl_messageformat_parser_1.parse(descriptor.defaultMessage)));
322 }
323 if (overrideIdFn || (descriptor.id && idInterpolationPattern)) {
324 if (idAttr) {
325 idAttr.get('value').replaceWith(core_1.types.stringLiteral(descriptor.id));
326 }
327 else if (defaultMessageAttr) {
328 defaultMessageAttr.insertBefore(core_1.types.jsxAttribute(core_1.types.jsxIdentifier('id'), core_1.types.stringLiteral(descriptor.id)));
329 }
330 }
331 if (removeDefaultMessage && defaultMessageAttr) {
332 defaultMessageAttr.remove();
333 }
334 // Tag the AST node so we don't try to extract it twice.
335 tagAsExtracted(path);
336 }
337 }
338 },
339 CallExpression: function (path, _a) {
340 var opts = _a.opts, filename = _a.file.opts.filename;
341 var messages = this.ReactIntlMessages;
342 var _b = opts.moduleSourceName, moduleSourceName = _b === void 0 ? 'react-intl' : _b, overrideIdFn = opts.overrideIdFn, idInterpolationPattern = opts.idInterpolationPattern, removeDefaultMessage = opts.removeDefaultMessage, extractFromFormatMessageCall = opts.extractFromFormatMessageCall, ast = opts.ast;
343 var callee = path.get('callee');
344 /**
345 * Process MessageDescriptor
346 * @param messageDescriptor Message Descriptor
347 */
348 function processMessageObject(messageDescriptor) {
349 assertObjectExpression(messageDescriptor, callee);
350 if (wasExtracted(messageDescriptor)) {
351 return;
352 }
353 var properties = messageDescriptor.get('properties');
354 var descriptorPath = createMessageDescriptor(properties.map(function (prop) {
355 return [prop.get('key'), prop.get('value')];
356 }));
357 // Evaluate the Message Descriptor values, then store it.
358 var descriptor = evaluateMessageDescriptor(descriptorPath, false, filename, idInterpolationPattern, overrideIdFn);
359 storeMessage(descriptor, messageDescriptor, opts, filename, messages);
360 // Remove description since it's not used at runtime.
361 messageDescriptor.replaceWithSourceString(JSON.stringify(tslib_1.__assign({ id: descriptor.id }, (!removeDefaultMessage && descriptor.defaultMessage
362 ? {
363 defaultMessage: ast
364 ? intl_messageformat_parser_1.parse(descriptor.defaultMessage)
365 : descriptor.defaultMessage,
366 }
367 : {}))));
368 // Tag the AST node so we don't try to extract it twice.
369 tagAsExtracted(messageDescriptor);
370 }
371 // Check that this is `defineMessages` call
372 if (isMultipleMessagesDeclMacro(callee, moduleSourceName) ||
373 isSingularMessagesDeclMacro(callee, moduleSourceName)) {
374 var firstArgument = path.get('arguments')[0];
375 var messagesObj = getMessagesObjectFromExpression(firstArgument);
376 if (assertObjectExpression(messagesObj, callee)) {
377 if (isSingularMessagesDeclMacro(callee, moduleSourceName)) {
378 processMessageObject(messagesObj);
379 }
380 else {
381 var properties = messagesObj.get('properties');
382 if (Array.isArray(properties)) {
383 properties
384 .map(function (prop) { return prop.get('value'); })
385 .forEach(processMessageObject);
386 }
387 }
388 }
389 }
390 // Check that this is `intl.formatMessage` call
391 if (extractFromFormatMessageCall && isFormatMessageCall(callee, path)) {
392 var messageDescriptor = path.get('arguments')[0];
393 if (messageDescriptor.isObjectExpression()) {
394 processMessageObject(messageDescriptor);
395 }
396 }
397 },
398 },
399 };
400});
401function isMultipleMessagesDeclMacro(callee, moduleSourceName) {
402 return referencesImport(callee, moduleSourceName, ['defineMessages']);
403}
404function isSingularMessagesDeclMacro(callee, moduleSourceName) {
405 return referencesImport(callee, moduleSourceName, ['defineMessage']);
406}
407function getMessagesObjectFromExpression(nodePath) {
408 var currentPath = nodePath;
409 while (types_1.isTSAsExpression(currentPath.node) ||
410 types_1.isTSTypeAssertion(currentPath.node) ||
411 types_1.isTypeCastExpression(currentPath.node)) {
412 currentPath = currentPath.get('expression');
413 }
414 return currentPath;
415}