Skip to content

Commit 74efbf7

Browse files
authored
jsx-fragments: allow Fragment syntax option
Right now one can specify either `syntax` or `element`, however `element` automatically falls back to using `React.Fragment` which is not supported in the case the variable is not exposed as an UMD global - the case when using es modules. This option allows to use modules instead and import the `Fragment` component from the react library
1 parent a2306e7 commit 74efbf7

File tree

1 file changed

+58
-7
lines changed

1 file changed

+58
-7
lines changed

lib/rules/jsx-fragments.js

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const messages = {
2525
fragmentsNotSupported: 'Fragments are only supported starting from React v16.2. '
2626
+ 'Please disable the `react/jsx-fragments` rule in `eslint` settings or upgrade your version of React.',
2727
preferPragma: 'Prefer {{react}}.{{fragment}} over fragment shorthand',
28+
preferPragmaShort: 'Prefer {{fragment}} over fragment shorthand',
2829
preferFragment: 'Prefer fragment shorthand over {{react}}.{{fragment}}',
2930
};
3031

@@ -41,7 +42,7 @@ module.exports = {
4142
messages,
4243

4344
schema: [{
44-
enum: ['syntax', 'element'],
45+
enum: ['syntax', 'element', 'elementShort'],
4546
}],
4647
},
4748

@@ -53,6 +54,11 @@ module.exports = {
5354
const closeFragShort = '</>';
5455
const openFragLong = `<${reactPragma}.${fragmentPragma}>`;
5556
const closeFragLong = `</${reactPragma}.${fragmentPragma}>`;
57+
const openFragMedium = `<${fragmentPragma}>`;
58+
const closeFragMedium = `</${fragmentPragma}>`;
59+
60+
let reactImportFound = false;
61+
const reactImports = [];
5662

5763
function reportOnReactVersion(node) {
5864
if (!testReactVersion(context, '>= 16.2.0')) {
@@ -65,20 +71,50 @@ module.exports = {
6571
return false;
6672
}
6773

68-
function getFixerToLong(jsxFragment) {
74+
function getFixerToLong(jsxFragment, withoutReactPragma) {
6975
if (!jsxFragment.closingFragment || !jsxFragment.openingFragment) {
7076
// the old TS parser crashes here
7177
// TODO: FIXME: can we fake these two descriptors?
7278
return null;
7379
}
7480
return function fix(fixer) {
81+
const closeFrag = withoutReactPragma ? closeFragMedium : closeFragLong;
82+
const openFrag = withoutReactPragma ? openFragMedium : openFragLong;
7583
let source = getText(context);
76-
source = replaceNode(source, jsxFragment.closingFragment, closeFragLong);
77-
source = replaceNode(source, jsxFragment.openingFragment, openFragLong);
78-
const lengthDiff = openFragLong.length - getText(context, jsxFragment.openingFragment).length
79-
+ closeFragLong.length - getText(context, jsxFragment.closingFragment).length;
84+
source = replaceNode(source, jsxFragment.closingFragment, closeFrag);
85+
source = replaceNode(source, jsxFragment.openingFragment, openFrag);
86+
const lengthDiff = openFrag.length - getText(context, jsxFragment.openingFragment).length
87+
+ closeFrag.length - getText(context, jsxFragment.closingFragment).length;
8088
const range = jsxFragment.range;
81-
return fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff));
89+
90+
const fixes = [];
91+
92+
// Insert the import statement at the top of the file if `withoutReactPragma` is true
93+
if (withoutReactPragma) {
94+
const ancestors = context.getAncestors();
95+
const rootNode = ancestors.length > 0 ? ancestors[0] : jsxFragment;
96+
const reactImport = reactImports.find(importNode =>
97+
importNode.specifiers.some(spec => spec.imported && spec.imported.name === fragmentPragma)
98+
);
99+
100+
if (!reactImport) {
101+
// No `Fragment` import found, so add it
102+
const existingReactImport = reactImports.find(importNode => importNode.source.value === 'react');
103+
104+
if (existingReactImport) {
105+
// If there's already an import from 'react', add `Fragment` to the existing specifiers
106+
const lastSpecifier = existingReactImport.specifiers[existingReactImport.specifiers.length - 1];
107+
fixes.push(fixer.insertTextAfter(lastSpecifier, `, ${fragmentPragma}`));
108+
} else {
109+
// Otherwise, add a new import statement at the top
110+
fixes.push(fixer.insertTextBefore(rootNode.body[0], `import { Fragment } from 'react';\n\n`));
111+
}
112+
}
113+
}
114+
115+
fixes.push(fixer.replaceTextRange(range, source.slice(range[0], range[1] + lengthDiff)))
116+
117+
return fixes;
82118
};
83119
}
84120

@@ -165,12 +201,27 @@ module.exports = {
165201
fix: getFixerToLong(node),
166202
});
167203
}
204+
205+
if (configuration === 'elementShort') {
206+
report(context, messages.preferPragmaShort, 'preferPragmaShort', {
207+
node,
208+
data: {
209+
react: reactPragma,
210+
fragment: fragmentPragma,
211+
},
212+
fix: getFixerToLong(node, true),
213+
});
214+
}
168215
},
169216

170217
ImportDeclaration(node) {
171218
if (node.source && node.source.value === 'react') {
219+
reactImports.push(node);
220+
let hasFragment = false;
221+
172222
node.specifiers.forEach((spec) => {
173223
if (spec.imported && spec.imported.name === fragmentPragma) {
224+
hasFragment = true;
174225
if (spec.local) {
175226
fragmentNames.add(spec.local.name);
176227
}

0 commit comments

Comments
 (0)