UNPKG

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