1
+// MIT License
2
+
3
+// Copyright (c) 2014-present Sebastian McKenzie and other contributors
4
+
5
+// Permission is hereby granted, free of charge, to any person obtaining
6
+// a copy of this software and associated documentation files (the
7
+// "Software"), to deal in the Software without restriction, including
8
+// without limitation the rights to use, copy, modify, merge, publish,
9
+// distribute, sublicense, and/or sell copies of the Software, and to
10
+// permit persons to whom the Software is furnished to do so, subject to
11
+// the following conditions:
12
+
13
+// The above copyright notice and this permission notice shall be
14
+// included in all copies or substantial portions of the Software.
15
+
16
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+// © 2019 GitHub, Inc.
24
+'use strict';
25
+
26
+const esutils = require('esutils');
27
+
28
+function helper(babel, opts) {
29
+ const {types: t} = babel;
30
+
31
+ const visitor = {};
32
+
33
+ visitor.JSXNamespacedName = function(path, state) {
34
+ const throwIfNamespace =
35
+ state.opts.throwIfNamespace === undefined
36
+ ? true
37
+ : !!state.opts.throwIfNamespace;
38
+ if (throwIfNamespace) {
39
+ throw path.buildCodeFrameError(
40
+ `Namespace tags are not supported by default. React's JSX doesn't support namespace tags. \
41
+You can turn on the 'throwIfNamespace' flag to bypass this warning.`,
42
+ );
43
+ }
44
+ };
45
+
46
+ visitor.JSXSpreadChild = function(path) {
47
+ throw path.buildCodeFrameError(
48
+ 'Spread children are not supported in React.',
49
+ );
50
+ };
51
+
52
+ visitor.JSXElement = {
53
+ exit(path, file) {
54
+ let callExpr;
55
+ if (file.opts.useCreateElement || shouldUseCreateElement(path)) {
56
+ callExpr = buildCreateElementCall(path, file);
57
+ } else {
58
+ callExpr = buildJSXElementCall(path, file);
59
+ }
60
+
61
+ if (callExpr) {
62
+ path.replaceWith(t.inherits(callExpr, path.node));
63
+ }
64
+ },
65
+ };
66
+
67
+ visitor.JSXFragment = {
68
+ exit(path, file) {
69
+ if (opts.compat) {
70
+ throw path.buildCodeFrameError(
71
+ 'Fragment tags are only supported in React 16 and up.',
72
+ );
73
+ }
74
+ let callExpr;
75
+ if (file.opts.useCreateElement) {
76
+ callExpr = buildCreateElementFragmentCall(path, file);
77
+ } else {
78
+ callExpr = buildJSXFragmentCall(path, file);
79
+ }
80
+
81
+ if (callExpr) {
82
+ path.replaceWith(t.inherits(callExpr, path.node));
83
+ }
84
+ },
85
+ };
86
+
87
+ return visitor;
88
+
89
+ function convertJSXIdentifier(node, parent) {
90
+ if (t.isJSXIdentifier(node)) {
91
+ if (node.name === 'this' && t.isReferenced(node, parent)) {
92
+ return t.thisExpression();
93
+ } else if (esutils.keyword.isIdentifierNameES6(node.name)) {
94
+ node.type = 'Identifier';
95
+ } else {
96
+ return t.stringLiteral(node.name);
97
+ }
98
+ } else if (t.isJSXMemberExpression(node)) {
99
+ return t.memberExpression(
100
+ convertJSXIdentifier(node.object, node),
101
+ convertJSXIdentifier(node.property, node),
102
+ );
103
+ } else if (t.isJSXNamespacedName(node)) {
104
+ /**
105
+ * If there is flag "throwIfNamespace"
106
+ * print XMLNamespace like string literal
107
+ */
108
+ return t.stringLiteral(`${node.namespace.name}:${node.name.name}`);
109
+ }
110
+
111
+ return node;
112
+ }
113
+
114
+ function convertAttributeValue(node) {
115
+ if (t.isJSXExpressionContainer(node)) {
116
+ return node.expression;
117
+ } else {
118
+ return node;
119
+ }
120
+ }
121
+
122
+ function convertAttribute(node) {
123
+ const value = convertAttributeValue(node.value || t.booleanLiteral(true));
124
+
125
+ if (t.isStringLiteral(value) && !t.isJSXExpressionContainer(node.value)) {
126
+ value.value = value.value.replace(/\n\s+/g, ' ');
127
+
128
+ // "raw" JSXText should not be used from a StringLiteral because it needs to be escaped.
129
+ if (value.extra && value.extra.raw) {
130
+ delete value.extra.raw;
131
+ }
132
+ }
133
+
134
+ if (t.isJSXNamespacedName(node.name)) {
135
+ node.name = t.stringLiteral(
136
+ node.name.namespace.name + ':' + node.name.name.name,
137
+ );
138
+ } else if (esutils.keyword.isIdentifierNameES6(node.name.name)) {
139
+ node.name.type = 'Identifier';
140
+ } else {
141
+ node.name = t.stringLiteral(node.name.name);
142
+ }
143
+
144
+ return t.inherits(t.objectProperty(node.name, value), node);
145
+ }
146
+
147
+ // We want to use React.createElement, even in the case of
148
+ // jsx, for <div {...props} key={key} /> to distinguish it
149
+ // from <div key={key} {...props} />. This is an intermediary
150
+ // step while we deprecate key spread from props. Afterwards,
151
+ // we will remove createElement entirely
152
+ function shouldUseCreateElement(path) {
153
+ const openingPath = path.get('openingElement');
154
+ const attributes = openingPath.node.attributes;
155
+
156
+ let seenPropsSpread = false;
157
+ for (let i = 0; i < attributes.length; i++) {
158
+ const attr = attributes[i];
159
+ if (
160
+ seenPropsSpread &&
161
+ t.isJSXAttribute(attr) &&
162
+ attr.name.name === 'key'
163
+ ) {
164
+ return true;
165
+ } else if (t.isJSXSpreadAttribute(attr)) {
166
+ seenPropsSpread = true;
167
+ }
168
+ }
169
+ return false;
170
+ }
171
+
172
+ // Builds JSX into:
173
+ // Production: React.jsx(type, arguments, key)
174
+ // Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self)
175
+ function buildJSXElementCall(path, file) {
176
+ if (opts.filter && !opts.filter(path.node, file)) {
177
+ return;
178
+ }
179
+
180
+ const openingPath = path.get('openingElement');
181
+ openingPath.parent.children = t.react.buildChildren(openingPath.parent);
182
+
183
+ const tagExpr = convertJSXIdentifier(
184
+ openingPath.node.name,
185
+ openingPath.node,
186
+ );
187
+ const args = [];
188
+
189
+ let tagName;
190
+ if (t.isIdentifier(tagExpr)) {
191
+ tagName = tagExpr.name;
192
+ } else if (t.isLiteral(tagExpr)) {
193
+ tagName = tagExpr.value;
194
+ }
195
+
196
+ const state = {
197
+ tagExpr: tagExpr,
198
+ tagName: tagName,
199
+ args: args,
200
+ };
201
+
202
+ if (opts.pre) {
203
+ opts.pre(state, file);
204
+ }
205
+
206
+ let attribs = [];
207
+ let key;
208
+ let source;
209
+ let self;
210
+
211
+ // for React.jsx, key, __source (dev), and __self (dev) is passed in as
212
+ // a separate argument rather than in the args object. We go through the
213
+ // props and filter out these three keywords so we can pass them in
214
+ // as separate arguments later
215
+ for (let i = 0; i < openingPath.node.attributes.length; i++) {
216
+ const attr = openingPath.node.attributes[i];
217
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
218
+ if (attr.name.name === 'key') {
219
+ key = convertAttribute(attr).value;
220
+ } else if (attr.name.name === '__source') {
221
+ source = convertAttribute(attr).value;
222
+ } else if (attr.name.name === '__self') {
223
+ self = convertAttribute(attr).value;
224
+ } else {
225
+ attribs.push(attr);
226
+ }
227
+ } else {
228
+ attribs.push(attr);
229
+ }
230
+ }
231
+
232
+ if (attribs.length || path.node.children.length) {
233
+ attribs = buildJSXOpeningElementAttributes(
234
+ attribs,
235
+ file,
236
+ path.node.children,
237
+ );
238
+ } else {
239
+ // attributes should never be null
240
+ attribs = t.objectExpression([]);
241
+ }
242
+
243
+ args.push(attribs);
244
+
245
+ if (!file.opts.development) {
246
+ if (key !== undefined) {
247
+ args.push(key);
248
+ }
249
+ } else {
250
+ // isStaticChildren, __source, and __self are only used in development
251
+ args.push(
252
+ key === undefined ? t.identifier('undefined') : key,
253
+ t.booleanLiteral(path.node.children.length > 1),
254
+ source === undefined ? t.identifier('undefined') : source,
255
+ self === undefined ? t.identifier('undefined') : self,
256
+ );
257
+ }
258
+
259
+ if (opts.post) {
260
+ opts.post(state, file);
261
+ }
262
+ return (
263
+ state.call ||
264
+ t.callExpression(
265
+ path.node.children.length > 1 ? state.staticCallee : state.callee,
266
+ args,
267
+ )
268
+ );
269
+ }
270
+
271
+ // Builds props for React.jsx. This function adds children into the props
272
+ // and ensures that props is always an object
273
+ function buildJSXOpeningElementAttributes(attribs, file, children) {
274
+ let _props = [];
275
+ const objs = [];
276
+
277
+ const useBuiltIns = file.opts.useBuiltIns || false;
278
+ if (typeof useBuiltIns !== 'boolean') {
279
+ throw new Error(
280
+ 'transform-react-jsx currently only accepts a boolean option for ' +
281
+ 'useBuiltIns (defaults to false)',
282
+ );
283
+ }
284
+
285
+ while (attribs.length) {
286
+ const prop = attribs.shift();
287
+ if (t.isJSXSpreadAttribute(prop)) {
288
+ _props = pushProps(_props, objs);
289
+ objs.push(prop.argument);
290
+ } else {
291
+ _props.push(convertAttribute(prop));
292
+ }
293
+ }
294
+
295
+ // In React.JSX, children is no longer a separate argument, but passed in
296
+ // through the argument object
297
+ if (children && children.length > 0) {
298
+ if (children.length === 1) {
299
+ _props.push(t.objectProperty(t.identifier('children'), children[0]));
300
+ } else {
301
+ _props.push(
302
+ t.objectProperty(
303
+ t.identifier('children'),
304
+ t.arrayExpression(children),
305
+ ),
306
+ );
307
+ }
308
+ }
309
+
310
+ pushProps(_props, objs);
311
+
312
+ if (objs.length === 1) {
313
+ // only one object
314
+ if (!t.isObjectExpression(objs[0])) {
315
+ // if the prop object isn't an object, use Object.assign or _extends
316
+ // to ensure that the prop will always be an object (as opposed to a variable
317
+ // that could be null at some point)
318
+ const expressionHelper = useBuiltIns
319
+ ? t.memberExpression(t.identifier('Object'), t.identifier('assign'))
320
+ : file.addHelper('extends');
321
+
322
+ attribs = t.callExpression(expressionHelper, [
323
+ t.objectExpression([]),
324
+ objs[0],
325
+ ]);
326
+ } else {
327
+ attribs = objs[0];
328
+ }
329
+ } else {
330
+ // looks like we have multiple objects
331
+ if (!t.isObjectExpression(objs[0])) {
332
+ objs.unshift(t.objectExpression([]));
333
+ }
334
+
335
+ const expressionHelper = useBuiltIns
336
+ ? t.memberExpression(t.identifier('Object'), t.identifier('assign'))
337
+ : file.addHelper('extends');
338
+
339
+ // spread it
340
+ attribs = t.callExpression(expressionHelper, objs);
341
+ }
342
+
343
+ return attribs;
344
+ }
345
+
346
+ // Builds JSX Fragment <></> into
347
+ // Production: React.jsx(type, arguments)
348
+ // Development: React.jsxDEV(type, { children})
349
+ function buildJSXFragmentCall(path, file) {
350
+ if (opts.filter && !opts.filter(path.node, file)) {
351
+ return;
352
+ }
353
+
354
+ const openingPath = path.get('openingElement');
355
+ openingPath.parent.children = t.react.buildChildren(openingPath.parent);
356
+
357
+ const args = [];
358
+ const tagName = null;
359
+ const tagExpr = file.get('jsxFragIdentifier')();
360
+
361
+ const state = {
362
+ tagExpr: tagExpr,
363
+ tagName: tagName,
364
+ args: args,
365
+ };
366
+
367
+ if (opts.pre) {
368
+ opts.pre(state, file);
369
+ }
370
+
371
+ let childrenNode;
372
+ if (path.node.children.length > 0) {
373
+ if (path.node.children.length === 1) {
374
+ childrenNode = path.node.children[0];
375
+ } else {
376
+ childrenNode = t.arrayExpression(path.node.children);
377
+ }
378
+ }
379
+
380
+ args.push(
381
+ t.objectExpression(
382
+ childrenNode !== undefined
383
+ ? [t.objectProperty(t.identifier('children'), childrenNode)]
384
+ : [],
385
+ ),
386
+ );
387
+
388
+ if (file.opts.development) {
389
+ args.push(
390
+ t.identifier('undefined'),
391
+ t.booleanLiteral(path.node.children.length > 1),
392
+ );
393
+ }
394
+
395
+ if (opts.post) {
396
+ opts.post(state, file);
397
+ }
398
+
399
+ return (
400
+ state.call ||
401
+ t.callExpression(
402
+ path.node.children.length > 1 ? state.staticCallee : state.callee,
403
+ args,
404
+ )
405
+ );
406
+ }
407
+
408
+ // Builds JSX into:
409
+ // Production: React.createElement(type, arguments, children)
410
+ // Development: React.createElement(type, arguments, children, source, self)
411
+ function buildCreateElementCall(path, file) {
412
+ if (opts.filter && !opts.filter(path.node, file)) {
413
+ return;
414
+ }
415
+
416
+ const openingPath = path.get('openingElement');
417
+ openingPath.parent.children = t.react.buildChildren(openingPath.parent);
418
+
419
+ const tagExpr = convertJSXIdentifier(
420
+ openingPath.node.name,
421
+ openingPath.node,
422
+ );
423
+ const args = [];
424
+
425
+ let tagName;
426
+ if (t.isIdentifier(tagExpr)) {
427
+ tagName = tagExpr.name;
428
+ } else if (t.isLiteral(tagExpr)) {
429
+ tagName = tagExpr.value;
430
+ }
431
+
432
+ const state = {
433
+ tagExpr: tagExpr,
434
+ tagName: tagName,
435
+ args: args,
436
+ };
437
+
438
+ if (opts.pre) {
439
+ opts.pre(state, file);
440
+ }
441
+
442
+ let attribs = openingPath.node.attributes;
443
+ if (attribs.length) {
444
+ attribs = buildCreateElementOpeningElementAttributes(attribs, file);
445
+ } else {
446
+ attribs = t.nullLiteral();
447
+ }
448
+
449
+ args.push(attribs, ...path.node.children);
450
+
451
+ if (opts.post) {
452
+ opts.post(state, file);
453
+ }
454
+
455
+ return state.call || t.callExpression(state.oldCallee, args);
456
+ }
457
+
458
+ function pushProps(_props, objs) {
459
+ if (!_props.length) {
460
+ return _props;
461
+ }
462
+
463
+ objs.push(t.objectExpression(_props));
464
+ return [];
465
+ }
466
+
467
+ /**
468
+ * The logic for this is quite terse. It's because we need to
469
+ * support spread elements. We loop over all attributes,
470
+ * breaking on spreads, we then push a new object containing
471
+ * all prior attributes to an array for later processing.
472
+ */
473
+ function buildCreateElementOpeningElementAttributes(attribs, file) {
474
+ let _props = [];
475
+ const objs = [];
476
+
477
+ const useBuiltIns = file.opts.useBuiltIns || false;
478
+ if (typeof useBuiltIns !== 'boolean') {
479
+ throw new Error(
480
+ 'transform-react-jsx currently only accepts a boolean option for ' +
481
+ 'useBuiltIns (defaults to false)',
482
+ );
483
+ }
484
+
485
+ while (attribs.length) {
486
+ const prop = attribs.shift();
487
+ if (t.isJSXSpreadAttribute(prop)) {
488
+ _props = pushProps(_props, objs);
489
+ objs.push(prop.argument);
490
+ } else {
491
+ const attr = convertAttribute(prop);
492
+ _props.push(attr);
493
+ }
494
+ }
495
+
496
+ pushProps(_props, objs);
497
+
498
+ if (objs.length === 1) {
499
+ // only one object
500
+ attribs = objs[0];
501
+ } else {
502
+ // looks like we have multiple objects
503
+ if (!t.isObjectExpression(objs[0])) {
504
+ objs.unshift(t.objectExpression([]));
505
+ }
506
+
507
+ const expressionHelper = useBuiltIns
508
+ ? t.memberExpression(t.identifier('Object'), t.identifier('assign'))
509
+ : file.addHelper('extends');
510
+
511
+ // spread it
512
+ attribs = t.callExpression(expressionHelper, objs);
513
+ }
514
+
515
+ return attribs;
516
+ }
517
+
518
+ function buildCreateElementFragmentCall(path, file) {
519
+ if (opts.filter && !opts.filter(path.node, file)) {
520
+ return;
521
+ }
522
+
523
+ const openingPath = path.get('openingElement');
524
+ openingPath.parent.children = t.react.buildChildren(openingPath.parent);
525
+
526
+ const args = [];
527
+ const tagName = null;
528
+ const tagExpr = file.get('jsxFragIdentifier')();
529
+
530
+ const state = {
531
+ tagExpr: tagExpr,
532
+ tagName: tagName,
533
+ args: args,
534
+ };
535
+
536
+ if (opts.pre) {
537
+ opts.pre(state, file);
538
+ }
539
+
540
+ // no attributes are allowed with <> syntax
541
+ args.push(t.nullLiteral(), ...path.node.children);
542
+
543
+ if (opts.post) {
544
+ opts.post(state, file);
545
+ }
546
+
547
+ return state.call || t.callExpression(state.oldCallee, args);
548
+ }
549
+}
550
+
551
+module.exports = function(babel) {
552
+ const {types: t} = babel;
553
+
554
+ const createIdentifierParser = id => () => {
555
+ return id
556
+ .split('.')
557
+ .map(name => t.identifier(name))
558
+ .reduce((object, property) => t.memberExpression(object, property));
559
+ };
560
+
561
+ const visitor = helper(babel, {
562
+ pre(state) {
563
+ const tagName = state.tagName;
564
+ const args = state.args;
565
+ if (t.react.isCompatTag(tagName)) {
566
+ args.push(t.stringLiteral(tagName));
567
+ } else {
568
+ args.push(state.tagExpr);
569
+ }
570
+ },
571
+
572
+ post(state, pass) {
573
+ state.callee = pass.get('jsxIdentifier')();
574
+ state.staticCallee = pass.get('jsxStaticIdentifier')();
575
+ state.oldCallee = pass.get('oldJSXIdentifier')();
576
+ },
577
+ });
578
+
579
+ visitor.Program = {
580
+ enter(path, state) {
581
+ state.set(
582
+ 'oldJSXIdentifier',
583
+ createIdentifierParser('React.createElement'),
584
+ );
585
+ state.set(
586
+ 'jsxIdentifier',
587
+ createIdentifierParser(
588
+ state.opts.development ? 'React.jsxDEV' : 'React.jsx',
589
+ ),
590
+ );
591
+ state.set(
592
+ 'jsxStaticIdentifier',
593
+ createIdentifierParser(
594
+ state.opts.development ? 'React.jsxDEV' : 'React.jsxs',
595
+ ),
596
+ );
597
+ state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment'));
598
+ },
599
+ };
600
+
601
+ visitor.JSXAttribute = function(path) {
602
+ if (t.isJSXElement(path.node.value)) {
603
+ path.node.value = t.jsxExpressionContainer(path.node.value);
604
+ }
605
+ };
606
+
607
+ return {
608
+ name: 'transform-react-jsx',
609
+ visitor,
610
+ };
611
+};