24
'use strict';
25
26
const esutils = require('esutils');
27
+const {
28
+ isModule,
29
+ addNamespace,
30
+ addNamed,
31
+ addDefault,
32
+} = require('@babel/helper-module-imports');
33
+
34
+// These are all the valid auto import types (under the config autoImport)
35
+// that a user can specific
36
+const IMPORT_TYPES = {
37
+ none: 'none', // default option. Will not import anything
38
+ require: 'require', // var _react = require("react");
39
+ namespace: 'namespace', // import * as _react from "react";
40
+ defaultExport: 'defaultExport', // import _default from "react";
41
+ namedExports: 'namedExports', // import { jsx } from "react";
42
+};
43
+
44
+const JSX_AUTO_IMPORT_ANNOTATION_REGEX = /\*?\s*@jsxAutoImport\s+([^\s]+)/;
45
+const JSX_IMPORT_SOURCE_ANNOTATION_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/;
46
+
47
+// We want to use React.createElement, even in the case of
48
+// jsx, for <div {...props} key={key} /> to distinguish it
49
+// from <div key={key} {...props} />. This is an intermediary
50
+// step while we deprecate key spread from props. Afterwards,
51
+// we will remove createElement entirely
52
+function shouldUseCreateElement(path, types) {
53
+ const openingPath = path.get('openingElement');
54
+ const attributes = openingPath.node.attributes;
55
+
56
+ let seenPropsSpread = false;
57
+ for (let i = 0; i < attributes.length; i++) {
58
+ const attr = attributes[i];
59
+ if (
60
+ seenPropsSpread &&
61
+ types.isJSXAttribute(attr) &&
62
+ attr.name.name === 'key'
63
+ ) {
64
+ return true;
65
+ } else if (types.isJSXSpreadAttribute(attr)) {
66
+ seenPropsSpread = true;
67
+ }
68
+ }
69
+ return false;
70
+}
71
72
function helper(babel, opts) {
73
const {types: t} = babel;
96
visitor.JSXElement = {
97
exit(path, file) {
98
let callExpr;
55
- if (file.opts.useCreateElement || shouldUseCreateElement(path)) {
99
+ if (shouldUseCreateElement(path, t)) {
100
callExpr = buildCreateElementCall(path, file);
101
} else {
102
callExpr = buildJSXElementCall(path, file);
115
'Fragment tags are only supported in React 16 and up.',
116
);
117
}
74
- let callExpr;
75
- if (file.opts.useCreateElement) {
76
- callExpr = buildCreateElementFragmentCall(path, file);
77
- } else {
78
- callExpr = buildJSXFragmentCall(path, file);
79
- }
118
+ let callExpr = buildJSXFragmentCall(path, file);
119
120
if (callExpr) {
121
path.replaceWith(t.inherits(callExpr, path.node));
186
return t.inherits(t.objectProperty(node.name, value), node);
187
}
188
150
- // We want to use React.createElement, even in the case of
151
- // jsx, for <div {...props} key={key} /> to distinguish it
152
- // from <div key={key} {...props} />. This is an intermediary
153
- // step while we deprecate key spread from props. Afterwards,
154
- // we will remove createElement entirely
155
- function shouldUseCreateElement(path) {
156
- const openingPath = path.get('openingElement');
157
- const attributes = openingPath.node.attributes;
158
-
159
- let seenPropsSpread = false;
160
- for (let i = 0; i < attributes.length; i++) {
161
- const attr = attributes[i];
162
- if (
163
- seenPropsSpread &&
164
- t.isJSXAttribute(attr) &&
165
- attr.name.name === 'key'
166
- ) {
167
- return true;
168
- } else if (t.isJSXSpreadAttribute(attr)) {
169
- seenPropsSpread = true;
170
- }
171
- }
172
- return false;
173
- }
174
-
189
// Builds JSX into:
190
// Production: React.jsx(type, arguments, key)
191
// Development: React.jsxDEV(type, arguments, key, isStaticChildren, source, self)
276
if (opts.post) {
277
opts.post(state, file);
278
}
279
+
280
return (
281
state.call ||
282
t.callExpression(
576
577
return attribs;
578
}
564
-
565
- function buildCreateElementFragmentCall(path, file) {
566
- if (opts.filter && !opts.filter(path.node, file)) {
567
- return;
568
- }
569
-
570
- const openingPath = path.get('openingElement');
571
- openingPath.parent.children = t.react.buildChildren(openingPath.parent);
572
-
573
- const args = [];
574
- const tagName = null;
575
- const tagExpr = file.get('jsxFragIdentifier')();
576
-
577
- const state = {
578
- tagExpr: tagExpr,
579
- tagName: tagName,
580
- args: args,
581
- };
582
-
583
- if (opts.pre) {
584
- opts.pre(state, file);
585
- }
586
-
587
- // no attributes are allowed with <> syntax
588
- args.push(t.nullLiteral(), ...path.node.children);
589
-
590
- if (opts.post) {
591
- opts.post(state, file);
592
- }
593
-
594
- return state.call || t.callExpression(state.oldCallee, args);
595
- }
579
}
580
581
module.exports = function(babel) {
606
},
607
});
608
626
- visitor.Program = {
627
- enter(path, state) {
628
- state.set(
629
- 'oldJSXIdentifier',
630
- createIdentifierParser('React.createElement'),
609
+ const createIdentifierName = (path, autoImport, name, importName) => {
610
+ if (autoImport === IMPORT_TYPES.none) {
611
+ return `React.${name}`;
612
+ } else if (autoImport === IMPORT_TYPES.namedExports) {
613
+ if (importName) {
614
+ const identifierName = `${importName[name]}`;
615
+ return identifierName;
616
+ }
617
+ } else {
618
+ return `${importName}.${name}`;
619
+ }
620
+ };
621
+
622
+ function getImportNames(parentPath, state) {
623
+ const imports = {};
624
+ parentPath.traverse({
625
+ JSXElement(path) {
626
+ if (shouldUseCreateElement(path, t)) {
627
+ imports.createElement = true;
628
+ } else if (path.node.children.length > 1) {
629
+ const importName = state.development ? 'jsxDEV' : 'jsxs';
630
+ imports[importName] = true;
631
+ } else {
632
+ const importName = state.development ? 'jsxDEV' : 'jsx';
633
+ imports[importName] = true;
634
+ }
635
+ },
636
+
637
+ JSXFragment(path) {
638
+ imports.Fragment = true;
639
+ },
640
+ });
641
+ return imports;
642
+ }
643
+
644
+ function hasJSX(parentPath) {
645
+ let fileHasJSX = false;
646
+ parentPath.traverse({
647
+ JSXElement(path) {
648
+ fileHasJSX = true;
649
+ path.stop();
650
+ },
651
+
652
+ JSXFragment(path) {
653
+ fileHasJSX = true;
654
+ path.stop();
655
+ },
656
+ });
657
+
658
+ return fileHasJSX;
659
+ }
660
+
661
+ function addAutoImports(path, state) {
662
+ if (state.autoImport === IMPORT_TYPES.none) {
663
+ return;
664
+ }
665
+
666
+ if (IMPORT_TYPES[state.autoImport] === undefined) {
667
+ throw path.buildCodeFrameError(
668
+ 'autoImport must be one of the following: ' +
669
+ Object.keys(IMPORT_TYPES).join(', '),
670
);
632
- state.set(
633
- 'jsxIdentifier',
634
- createIdentifierParser(
635
- state.opts.development ? 'React.jsxDEV' : 'React.jsx',
636
- ),
671
+ }
672
+ if (state.autoImport === IMPORT_TYPES.require && isModule(path)) {
673
+ throw path.buildCodeFrameError(
674
+ 'Babel `sourceType` must be set to `script` for autoImport ' +
675
+ 'to use `require` syntax. See Babel `sourceType` for details.',
676
);
638
- state.set(
639
- 'jsxStaticIdentifier',
640
- createIdentifierParser(
641
- state.opts.development ? 'React.jsxDEV' : 'React.jsxs',
642
- ),
677
+ }
678
+ if (state.autoImport !== IMPORT_TYPES.require && !isModule(path)) {
679
+ throw path.buildCodeFrameError(
680
+ 'Babel `sourceType` must be set to `module` for autoImport to use `' +
681
+ state.autoImport +
682
+ '` syntax. See Babel `sourceType` for details.',
683
);
644
- state.set('jsxFragIdentifier', createIdentifierParser('React.Fragment'));
684
+ }
685
+
686
+ // import {jsx} from "react";
687
+ // import {createElement} from "react";
688
+ if (state.autoImport === IMPORT_TYPES.namedExports) {
689
+ const imports = getImportNames(path, state);
690
+ const importMap = {};
691
+
692
+ Object.keys(imports).forEach(importName => {
693
+ importMap[importName] = addNamed(path, importName, state.source).name;
694
+ });
695
+
696
+ return importMap;
697
+ }
698
+
699
+ // add import to file and get the import name
700
+ let name;
701
+ if (state.autoImport === IMPORT_TYPES.require) {
702
+ // var _react = require("react");
703
+ name = addNamespace(path, state.source, {
704
+ importedInterop: 'uncompiled',
705
+ }).name;
706
+ } else if (state.autoImport === IMPORT_TYPES.namespace) {
707
+ // import * as _react from "react";
708
+ name = addNamespace(path, state.source).name;
709
+ } else if (state.autoImport === IMPORT_TYPES.defaultExport) {
710
+ // import _default from "react";
711
+ name = addDefault(path, state.source).name;
712
+ }
713
+
714
+ return name;
715
+ }
716
+
717
+ visitor.Program = {
718
+ enter(path, state) {
719
+ if (hasJSX(path)) {
720
+ let autoImport = state.opts.autoImport || IMPORT_TYPES.none;
721
+ let source = state.opts.importSource || 'react';
722
+ const {file} = state;
723
+
724
+ if (file.ast.comments) {
725
+ for (let i = 0; i < file.ast.comments.length; i++) {
726
+ const comment = file.ast.comments[i];
727
+ const jsxAutoImportMatches = JSX_AUTO_IMPORT_ANNOTATION_REGEX.exec(
728
+ comment.value,
729
+ );
730
+ if (jsxAutoImportMatches) {
731
+ autoImport = jsxAutoImportMatches[1];
732
+ }
733
+ const jsxImportSourceMatches = JSX_IMPORT_SOURCE_ANNOTATION_REGEX.exec(
734
+ comment.value,
735
+ );
736
+ if (jsxImportSourceMatches) {
737
+ source = jsxImportSourceMatches[1];
738
+ }
739
+ }
740
+ }
741
+
742
+ const importName = addAutoImports(path, {
743
+ ...state.opts,
744
+ autoImport,
745
+ source,
746
+ });
747
+
748
+ state.set(
749
+ 'oldJSXIdentifier',
750
+ createIdentifierParser(
751
+ createIdentifierName(path, autoImport, 'createElement', importName),
752
+ ),
753
+ );
754
+
755
+ state.set(
756
+ 'jsxIdentifier',
757
+ createIdentifierParser(
758
+ createIdentifierName(
759
+ path,
760
+ autoImport,
761
+ state.opts.development ? 'jsxDEV' : 'jsx',
762
+ importName,
763
+ ),
764
+ ),
765
+ );
766
+
767
+ state.set(
768
+ 'jsxStaticIdentifier',
769
+ createIdentifierParser(
770
+ createIdentifierName(
771
+ path,
772
+ autoImport,
773
+ state.opts.development ? 'jsxDEV' : 'jsxs',
774
+ importName,
775
+ ),
776
+ ),
777
+ );
778
+
779
+ state.set(
780
+ 'jsxFragIdentifier',
781
+ createIdentifierParser(
782
+ createIdentifierName(path, autoImport, 'Fragment', importName),
783
+ ),
784
+ );
785
+ }
786
},
787
};
788