eslint-plugin-react-jsx 0.0.0 → 4.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -7
- package/dist/index.d.ts +9 -0
- package/dist/index.js +505 -0
- package/package.json +70 -4
- package/bun.lock +0 -26
- package/index.ts +0 -1
- package/tsconfig.json +0 -29
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Rel1cx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,15 +1,38 @@
|
|
|
1
1
|
# eslint-plugin-react-jsx
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React Flavored JSX rules for React.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
# npm
|
|
9
|
+
npm install --save-dev eslint-plugin-react-jsx
|
|
7
10
|
```
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import js from "@eslint/js";
|
|
16
|
+
import reactJsx from "eslint-plugin-react-jsx";
|
|
17
|
+
import { defineConfig } from "eslint/config";
|
|
18
|
+
import tseslint from "typescript-eslint";
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
export default defineConfig(
|
|
21
|
+
{
|
|
22
|
+
files: ["**/*.{ts,tsx}"],
|
|
23
|
+
extends: [
|
|
24
|
+
js.configs.recommended,
|
|
25
|
+
tseslint.configs.recommended,
|
|
26
|
+
// Add configs from eslint-plugin-react-jsx
|
|
27
|
+
reactJsx.configs.recommended,
|
|
28
|
+
],
|
|
29
|
+
rules: {
|
|
30
|
+
// Put rules you want to override here
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
);
|
|
13
34
|
```
|
|
14
35
|
|
|
15
|
-
|
|
36
|
+
## Rules
|
|
37
|
+
|
|
38
|
+
<https://eslint-react.xyz/docs/rules/overview#jsx-rules>
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ESLint, Linter } from "eslint";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
type ConfigName = "recommended" | "strict";
|
|
5
|
+
declare const finalPlugin: ESLint.Plugin & {
|
|
6
|
+
configs: Record<ConfigName, Linter.Config>;
|
|
7
|
+
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { finalPlugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { DEFAULT_ESLINT_REACT_SETTINGS, WEBSITE_URL, defineRuleListener } from "@eslint-react/shared";
|
|
2
|
+
import { JsxEmit, findAttribute, getChildren, getElementFullType, getJsxConfig, hasAnyAttribute, hasChildren, isFragmentElement, isHostElement, isWhitespaceText } from "@eslint-react/jsx";
|
|
3
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
4
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
5
|
+
import * as ast from "@eslint-react/ast";
|
|
6
|
+
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __exportAll = (all, no_symbols) => {
|
|
10
|
+
let target = {};
|
|
11
|
+
for (var name in all) {
|
|
12
|
+
__defProp(target, name, {
|
|
13
|
+
get: all[name],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (!no_symbols) {
|
|
18
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
19
|
+
}
|
|
20
|
+
return target;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region package.json
|
|
25
|
+
var name$2 = "eslint-plugin-react-jsx";
|
|
26
|
+
var version = "4.0.0-beta.1";
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/utils/create-rule.ts
|
|
30
|
+
function getDocsUrl(ruleName) {
|
|
31
|
+
return `${WEBSITE_URL}/docs/rules/${ruleName}`;
|
|
32
|
+
}
|
|
33
|
+
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/utils/jsx.ts
|
|
37
|
+
/**
|
|
38
|
+
* Trim leading / trailing whitespace the same way React does when rendering
|
|
39
|
+
* JSX text. Whitespace that contains a newline is stripped entirely;
|
|
40
|
+
* whitespace that stays on the same line is preserved.
|
|
41
|
+
* @param text
|
|
42
|
+
*/
|
|
43
|
+
function trimLikeReact(text) {
|
|
44
|
+
const leadingSpaces = /^\s*/.exec(text)?.[0] ?? "";
|
|
45
|
+
const trailingSpaces = /\s*$/.exec(text)?.[0] ?? "";
|
|
46
|
+
const start = leadingSpaces.includes("\n") ? leadingSpaces.length : 0;
|
|
47
|
+
const end = trailingSpaces.includes("\n") ? text.length - trailingSpaces.length : text.length;
|
|
48
|
+
return text.slice(start, end);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Compute the removal range for a JSX attribute, consuming any leading
|
|
52
|
+
* whitespace (spaces, tabs, newlines) so the resulting markup stays clean.
|
|
53
|
+
* @param context
|
|
54
|
+
* @param prop
|
|
55
|
+
*/
|
|
56
|
+
function getPropRemovalRange(context, prop) {
|
|
57
|
+
const { sourceCode } = context;
|
|
58
|
+
let start = prop.range[0];
|
|
59
|
+
const end = prop.range[1];
|
|
60
|
+
while (start > 0 && /\s/.test(sourceCode.text[start - 1])) start--;
|
|
61
|
+
return [start, end];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extract the text to use as JSX children content from a `children` prop.
|
|
65
|
+
*
|
|
66
|
+
* - `children="text"` -> `text` (raw string, no quotes)
|
|
67
|
+
* - `children={<div />}` -> `<div />` (JSX element, no braces)
|
|
68
|
+
* - `children={<>…</>}` -> `<>…</>` (JSX fragment, no braces)
|
|
69
|
+
* - `children={expression}` -> `{expression}` (wrapped in braces)
|
|
70
|
+
* - `children` -> `null` (boolean shorthand, cannot extract)
|
|
71
|
+
* @param context
|
|
72
|
+
* @param prop
|
|
73
|
+
*/
|
|
74
|
+
function getChildrenPropText(context, prop) {
|
|
75
|
+
const { sourceCode } = context;
|
|
76
|
+
const { value } = prop;
|
|
77
|
+
if (value == null) return null;
|
|
78
|
+
if (value.type === AST_NODE_TYPES.Literal) return String(value.value);
|
|
79
|
+
if (value.type === AST_NODE_TYPES.JSXExpressionContainer) {
|
|
80
|
+
const { expression } = value;
|
|
81
|
+
if (expression.type === AST_NODE_TYPES.JSXEmptyExpression) return null;
|
|
82
|
+
const exprText = sourceCode.getText(expression);
|
|
83
|
+
if (ast.isJSXElementLike(expression)) return exprText;
|
|
84
|
+
return `{${exprText}}`;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Compute the range covering **all** children content of a JSX element or
|
|
90
|
+
* fragment (from the start of the first child to the end of the last child).
|
|
91
|
+
*
|
|
92
|
+
* Returns `null` when there are no children at all.
|
|
93
|
+
* @param node
|
|
94
|
+
*/
|
|
95
|
+
function getChildrenContentRange(node) {
|
|
96
|
+
if (node.children.length === 0) return null;
|
|
97
|
+
const first = node.children[0];
|
|
98
|
+
const last = node.children[node.children.length - 1];
|
|
99
|
+
return [first.range[0], last.range[1]];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Extract the raw source text of an element's / fragment's children
|
|
103
|
+
* (everything between the opening and closing tags).
|
|
104
|
+
*
|
|
105
|
+
* Returns `""` for self-closing elements like `<Fragment />`.
|
|
106
|
+
* @param context
|
|
107
|
+
* @param node
|
|
108
|
+
*/
|
|
109
|
+
function getChildrenSourceText(context, node) {
|
|
110
|
+
const { sourceCode } = context;
|
|
111
|
+
const opener = node.type === AST_NODE_TYPES.JSXFragment ? node.openingFragment : node.openingElement;
|
|
112
|
+
const closer = node.type === AST_NODE_TYPES.JSXFragment ? node.closingFragment : node.closingElement;
|
|
113
|
+
if (opener.type === AST_NODE_TYPES.JSXOpeningElement && opener.selfClosing) return "";
|
|
114
|
+
return sourceCode.text.slice(opener.range[1], closer?.range[0]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/rules/no-children-prop-with-children/no-children-prop-with-children.ts
|
|
119
|
+
const RULE_NAME$5 = "no-children-prop-with-children";
|
|
120
|
+
var no_children_prop_with_children_default = createRule({
|
|
121
|
+
meta: {
|
|
122
|
+
type: "problem",
|
|
123
|
+
docs: { description: "Disallows passing 'children' as a prop when children are also passed as nested content." },
|
|
124
|
+
fixable: "code",
|
|
125
|
+
hasSuggestions: true,
|
|
126
|
+
messages: {
|
|
127
|
+
default: "Do not pass 'children' as a prop when the element already has children content.",
|
|
128
|
+
removeChildrenContent: "Remove the nested children content.",
|
|
129
|
+
removeChildrenProp: "Remove the 'children' prop."
|
|
130
|
+
},
|
|
131
|
+
schema: []
|
|
132
|
+
},
|
|
133
|
+
name: RULE_NAME$5,
|
|
134
|
+
create: create$5,
|
|
135
|
+
defaultOptions: []
|
|
136
|
+
});
|
|
137
|
+
function create$5(context) {
|
|
138
|
+
return defineRuleListener({ JSXElement(node) {
|
|
139
|
+
const childrenProp = findAttribute(context, node, "children");
|
|
140
|
+
if (childrenProp == null) return;
|
|
141
|
+
if (!hasChildren(node)) return;
|
|
142
|
+
if (childrenProp.type !== AST_NODE_TYPES.JSXAttribute) {
|
|
143
|
+
context.report({
|
|
144
|
+
messageId: "default",
|
|
145
|
+
node: childrenProp
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
context.report({
|
|
150
|
+
messageId: "default",
|
|
151
|
+
node: childrenProp,
|
|
152
|
+
suggest: [{
|
|
153
|
+
fix(fixer) {
|
|
154
|
+
const [start, end] = getPropRemovalRange(context, childrenProp);
|
|
155
|
+
return fixer.removeRange([start, end]);
|
|
156
|
+
},
|
|
157
|
+
messageId: "removeChildrenProp"
|
|
158
|
+
}, {
|
|
159
|
+
fix(fixer) {
|
|
160
|
+
const range = getChildrenContentRange(node);
|
|
161
|
+
if (range == null) return [];
|
|
162
|
+
return fixer.removeRange(range);
|
|
163
|
+
},
|
|
164
|
+
messageId: "removeChildrenContent"
|
|
165
|
+
}]
|
|
166
|
+
});
|
|
167
|
+
} });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/rules/no-children-prop/no-children-prop.ts
|
|
172
|
+
const RULE_NAME$4 = "no-children-prop";
|
|
173
|
+
var no_children_prop_default = createRule({
|
|
174
|
+
meta: {
|
|
175
|
+
type: "suggestion",
|
|
176
|
+
docs: { description: "Disallows passing 'children' as a prop." },
|
|
177
|
+
fixable: "code",
|
|
178
|
+
hasSuggestions: true,
|
|
179
|
+
messages: {
|
|
180
|
+
default: "Do not pass 'children' as props.",
|
|
181
|
+
moveChildrenToContent: "Move 'children' to element content."
|
|
182
|
+
},
|
|
183
|
+
schema: []
|
|
184
|
+
},
|
|
185
|
+
name: RULE_NAME$4,
|
|
186
|
+
create: create$4,
|
|
187
|
+
defaultOptions: []
|
|
188
|
+
});
|
|
189
|
+
function create$4(context) {
|
|
190
|
+
return defineRuleListener({ JSXElement(node) {
|
|
191
|
+
const childrenProp = findAttribute(context, node, "children");
|
|
192
|
+
if (childrenProp == null) return;
|
|
193
|
+
if (childrenProp.type !== AST_NODE_TYPES.JSXAttribute) {
|
|
194
|
+
context.report({
|
|
195
|
+
messageId: "default",
|
|
196
|
+
node: childrenProp
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const childrenText = getChildrenPropText(context, childrenProp);
|
|
201
|
+
if (childrenText == null) {
|
|
202
|
+
context.report({
|
|
203
|
+
messageId: "default",
|
|
204
|
+
node: childrenProp
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
context.report({
|
|
209
|
+
messageId: "default",
|
|
210
|
+
node: childrenProp,
|
|
211
|
+
suggest: [{
|
|
212
|
+
fix(fixer) {
|
|
213
|
+
const sourceCode = context.sourceCode;
|
|
214
|
+
const { openingElement } = node;
|
|
215
|
+
const [removeStart, removeEnd] = getPropRemovalRange(context, childrenProp);
|
|
216
|
+
if (openingElement.selfClosing) {
|
|
217
|
+
const tagName = sourceCode.getText(openingElement.name);
|
|
218
|
+
const selfCloseOffset = sourceCode.getText(openingElement).lastIndexOf("/>");
|
|
219
|
+
let wsStart = openingElement.range[0] + selfCloseOffset;
|
|
220
|
+
while (wsStart > removeEnd && /\s/.test(sourceCode.text[wsStart - 1])) wsStart--;
|
|
221
|
+
return [fixer.removeRange([removeStart, removeEnd]), fixer.replaceTextRange([wsStart, openingElement.range[1]], `>${childrenText}</${tagName}>`)];
|
|
222
|
+
}
|
|
223
|
+
const fixes = [fixer.removeRange([removeStart, removeEnd])];
|
|
224
|
+
if (node.closingElement != null) fixes.push(fixer.insertTextBefore(node.closingElement, childrenText));
|
|
225
|
+
return fixes;
|
|
226
|
+
},
|
|
227
|
+
messageId: "moveChildrenToContent"
|
|
228
|
+
}]
|
|
229
|
+
});
|
|
230
|
+
} });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/rules/no-comment-textnodes/no-comment-textnodes.ts
|
|
235
|
+
const RULE_NAME$3 = "no-comment-textnodes";
|
|
236
|
+
var no_comment_textnodes_default = createRule({
|
|
237
|
+
meta: {
|
|
238
|
+
type: "problem",
|
|
239
|
+
docs: { description: "Prevents comment strings (ex: beginning with '//' or '/*') from being accidentally inserted into a JSX element's text nodes." },
|
|
240
|
+
messages: { default: "Possible misused comment in text node. Comments inside children section of tag should be placed inside braces." },
|
|
241
|
+
schema: []
|
|
242
|
+
},
|
|
243
|
+
name: RULE_NAME$3,
|
|
244
|
+
create: create$3,
|
|
245
|
+
defaultOptions: []
|
|
246
|
+
});
|
|
247
|
+
function create$3(context) {
|
|
248
|
+
function hasCommentLike(node) {
|
|
249
|
+
if (ast.isOneOf([AST_NODE_TYPES.JSXAttribute, AST_NODE_TYPES.JSXExpressionContainer])(node.parent)) return false;
|
|
250
|
+
return /^\s*\/(?:\/|\*)/mu.test(context.sourceCode.getText(node));
|
|
251
|
+
}
|
|
252
|
+
const visitorFunction = (node) => {
|
|
253
|
+
if (!ast.isJSXElementLike(node.parent)) return;
|
|
254
|
+
if (!hasCommentLike(node)) return;
|
|
255
|
+
context.report({
|
|
256
|
+
messageId: "default",
|
|
257
|
+
node
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
return defineRuleListener({
|
|
261
|
+
JSXText: visitorFunction,
|
|
262
|
+
Literal: visitorFunction
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/rules/no-deoptimization/no-deoptimization.ts
|
|
268
|
+
const RULE_NAME$2 = "no-deoptimization";
|
|
269
|
+
var no_deoptimization_default = createRule({
|
|
270
|
+
meta: {
|
|
271
|
+
type: "problem",
|
|
272
|
+
docs: { description: "Prevent patterns that cause deoptimization when using the automatic JSX runtime." },
|
|
273
|
+
messages: { noKeyAfterSpread: "Placing 'key' after spread props causes deoptimization when using the automatic JSX runtime. Put 'key' before any spread props." },
|
|
274
|
+
schema: []
|
|
275
|
+
},
|
|
276
|
+
name: RULE_NAME$2,
|
|
277
|
+
create: create$2,
|
|
278
|
+
defaultOptions: []
|
|
279
|
+
});
|
|
280
|
+
function create$2(context) {
|
|
281
|
+
const { jsx } = getJsxConfig(context);
|
|
282
|
+
if (!(jsx === JsxEmit.ReactJSX || jsx === JsxEmit.ReactJSXDev)) return {};
|
|
283
|
+
return defineRuleListener({ JSXOpeningElement(node) {
|
|
284
|
+
let firstSpreadPropIndex = null;
|
|
285
|
+
for (const [index, prop] of node.attributes.entries()) {
|
|
286
|
+
if (prop.type === AST_NODE_TYPES.JSXSpreadAttribute) {
|
|
287
|
+
firstSpreadPropIndex ??= index;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (firstSpreadPropIndex == null) continue;
|
|
291
|
+
if (prop.name.name === "key" && index > firstSpreadPropIndex) context.report({
|
|
292
|
+
messageId: "noKeyAfterSpread",
|
|
293
|
+
node: prop
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
} });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/rules/no-namespace/no-namespace.ts
|
|
301
|
+
const RULE_NAME$1 = "no-namespace";
|
|
302
|
+
var no_namespace_default = createRule({
|
|
303
|
+
meta: {
|
|
304
|
+
type: "problem",
|
|
305
|
+
docs: { description: "Disallow JSX namespace syntax, as React does not support them." },
|
|
306
|
+
messages: { noNamespace: "A React component '{{name}}' must not be in a namespace, as React does not support them." },
|
|
307
|
+
schema: []
|
|
308
|
+
},
|
|
309
|
+
name: RULE_NAME$1,
|
|
310
|
+
create: create$1,
|
|
311
|
+
defaultOptions: []
|
|
312
|
+
});
|
|
313
|
+
function create$1(context) {
|
|
314
|
+
return defineRuleListener({ JSXElement(node) {
|
|
315
|
+
const name = getElementFullType(node);
|
|
316
|
+
if (typeof name !== "string" || !name.includes(":")) return;
|
|
317
|
+
context.report({
|
|
318
|
+
data: { name },
|
|
319
|
+
messageId: "noNamespace",
|
|
320
|
+
node: node.openingElement.name
|
|
321
|
+
});
|
|
322
|
+
} });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/rules/no-useless-fragment/no-useless-fragment.ts
|
|
327
|
+
const RULE_NAME = "no-useless-fragment";
|
|
328
|
+
const defaultOptions = [{
|
|
329
|
+
allowEmptyFragment: false,
|
|
330
|
+
allowExpressions: true
|
|
331
|
+
}];
|
|
332
|
+
const schema = [{
|
|
333
|
+
type: "object",
|
|
334
|
+
additionalProperties: false,
|
|
335
|
+
properties: {
|
|
336
|
+
allowEmptyFragment: {
|
|
337
|
+
type: "boolean",
|
|
338
|
+
description: "Allow empty fragments"
|
|
339
|
+
},
|
|
340
|
+
allowExpressions: {
|
|
341
|
+
type: "boolean",
|
|
342
|
+
description: "Allow fragments with a single expression child"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}];
|
|
346
|
+
var no_useless_fragment_default = createRule({
|
|
347
|
+
meta: {
|
|
348
|
+
type: "suggestion",
|
|
349
|
+
defaultOptions: [...defaultOptions],
|
|
350
|
+
docs: { description: "Disallows useless fragment elements." },
|
|
351
|
+
fixable: "code",
|
|
352
|
+
messages: { default: "A fragment {{reason}} is useless." },
|
|
353
|
+
schema
|
|
354
|
+
},
|
|
355
|
+
name: RULE_NAME,
|
|
356
|
+
create,
|
|
357
|
+
defaultOptions
|
|
358
|
+
});
|
|
359
|
+
function create(context, [option]) {
|
|
360
|
+
const { allowEmptyFragment = false, allowExpressions = true } = option;
|
|
361
|
+
const jsxConfig = getJsxConfig(context);
|
|
362
|
+
/**
|
|
363
|
+
* Whether the fragment has too few meaningful children to justify its
|
|
364
|
+
* existence (the "contains less than two children" reason).
|
|
365
|
+
* @param node
|
|
366
|
+
*/
|
|
367
|
+
function isContentUseless(node) {
|
|
368
|
+
if (node.children.length === 0) return !allowEmptyFragment;
|
|
369
|
+
const insideJsx = ast.isJSXElementLike(node.parent);
|
|
370
|
+
if (!allowExpressions) {
|
|
371
|
+
if (insideJsx) return true;
|
|
372
|
+
if (node.children.length === 1) return true;
|
|
373
|
+
}
|
|
374
|
+
if (allowExpressions && !insideJsx && node.children.length === 1) {
|
|
375
|
+
const child = node.children[0];
|
|
376
|
+
if (child != null && child.type === AST_NODE_TYPES.JSXText) return false;
|
|
377
|
+
}
|
|
378
|
+
const meaningful = getChildren(node);
|
|
379
|
+
if (meaningful.length === 0) return true;
|
|
380
|
+
if (meaningful.length === 1 && meaningful[0].type !== AST_NODE_TYPES.JSXExpressionContainer) return true;
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Whether it is safe to auto-fix the fragment by unwrapping it.
|
|
385
|
+
* @param node
|
|
386
|
+
*/
|
|
387
|
+
function isSafeToFix(node) {
|
|
388
|
+
if (ast.isJSXElementLike(node.parent)) return isHostElement(node.parent);
|
|
389
|
+
if (node.children.length === 0) return false;
|
|
390
|
+
return !node.children.some((child) => {
|
|
391
|
+
if (child.type === AST_NODE_TYPES.JSXExpressionContainer) return true;
|
|
392
|
+
if (child.type === AST_NODE_TYPES.JSXText) return !isWhitespaceText(child);
|
|
393
|
+
return false;
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Build an autofix that unwraps the fragment, replacing it with its
|
|
398
|
+
* trimmed children text. Returns `null` when the fix is unsafe.
|
|
399
|
+
* @param node
|
|
400
|
+
*/
|
|
401
|
+
function buildFix(node) {
|
|
402
|
+
if (!isSafeToFix(node)) return null;
|
|
403
|
+
return (fixer) => {
|
|
404
|
+
const childrenText = getChildrenSourceText(context, node);
|
|
405
|
+
return fixer.replaceText(node, trimLikeReact(childrenText));
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Inspect a fragment node and report if it is useless.
|
|
410
|
+
*
|
|
411
|
+
* A fragment may be reported for **two independent reasons** on the same
|
|
412
|
+
* node (e.g. `<p><>foo</></p>` is both "placed inside a host component"
|
|
413
|
+
* and* "contains less than two children").
|
|
414
|
+
* @param node
|
|
415
|
+
*/
|
|
416
|
+
function checkNode(node) {
|
|
417
|
+
if (isHostElement(node.parent)) context.report({
|
|
418
|
+
data: { reason: "placed inside a host component" },
|
|
419
|
+
fix: buildFix(node),
|
|
420
|
+
messageId: "default",
|
|
421
|
+
node
|
|
422
|
+
});
|
|
423
|
+
if (isContentUseless(node)) context.report({
|
|
424
|
+
data: { reason: "contains less than two children" },
|
|
425
|
+
fix: buildFix(node),
|
|
426
|
+
messageId: "default",
|
|
427
|
+
node
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return defineRuleListener({
|
|
431
|
+
JSXElement(node) {
|
|
432
|
+
if (!isFragmentElement(node, jsxConfig.jsxFragmentFactory)) return;
|
|
433
|
+
if (hasAnyAttribute(context, node, ["key", "ref"])) return;
|
|
434
|
+
checkNode(node);
|
|
435
|
+
},
|
|
436
|
+
JSXFragment(node) {
|
|
437
|
+
checkNode(node);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/plugin.ts
|
|
444
|
+
const plugin = {
|
|
445
|
+
meta: {
|
|
446
|
+
name: name$2,
|
|
447
|
+
version
|
|
448
|
+
},
|
|
449
|
+
rules: {
|
|
450
|
+
"no-children-prop": no_children_prop_default,
|
|
451
|
+
"no-children-prop-with-children": no_children_prop_with_children_default,
|
|
452
|
+
"no-comment-textnodes": no_comment_textnodes_default,
|
|
453
|
+
"no-deoptimization": no_deoptimization_default,
|
|
454
|
+
"no-namespace": no_namespace_default,
|
|
455
|
+
"no-useless-fragment": no_useless_fragment_default
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/configs/recommended.ts
|
|
461
|
+
var recommended_exports = /* @__PURE__ */ __exportAll({
|
|
462
|
+
name: () => name$1,
|
|
463
|
+
plugins: () => plugins$1,
|
|
464
|
+
rules: () => rules$1,
|
|
465
|
+
settings: () => settings$1
|
|
466
|
+
});
|
|
467
|
+
const name$1 = "react-jsx/recommended";
|
|
468
|
+
const rules$1 = {
|
|
469
|
+
"react-jsx/no-children-prop": "warn",
|
|
470
|
+
"react-jsx/no-comment-textnodes": "warn",
|
|
471
|
+
"react-jsx/no-deoptimization": "error",
|
|
472
|
+
"react-jsx/no-namespace": "error"
|
|
473
|
+
};
|
|
474
|
+
const plugins$1 = { "react-jsx": plugin };
|
|
475
|
+
const settings$1 = { "react-x": DEFAULT_ESLINT_REACT_SETTINGS };
|
|
476
|
+
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/configs/strict.ts
|
|
479
|
+
var strict_exports = /* @__PURE__ */ __exportAll({
|
|
480
|
+
name: () => name,
|
|
481
|
+
plugins: () => plugins,
|
|
482
|
+
rules: () => rules,
|
|
483
|
+
settings: () => settings
|
|
484
|
+
});
|
|
485
|
+
const name = "react-jsx/strict";
|
|
486
|
+
const rules = {
|
|
487
|
+
...rules$1,
|
|
488
|
+
"react-jsx/no-children-prop": "error",
|
|
489
|
+
"react-jsx/no-useless-fragment": "warn"
|
|
490
|
+
};
|
|
491
|
+
const plugins = { ...plugins$1 };
|
|
492
|
+
const settings = { ...settings$1 };
|
|
493
|
+
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region src/index.ts
|
|
496
|
+
const finalPlugin = {
|
|
497
|
+
...plugin,
|
|
498
|
+
configs: {
|
|
499
|
+
["recommended"]: recommended_exports,
|
|
500
|
+
["strict"]: strict_exports
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
//#endregion
|
|
505
|
+
export { finalPlugin as default };
|
package/package.json
CHANGED
|
@@ -1,12 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-react-jsx",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"version": "4.0.0-beta.1",
|
|
4
|
+
"description": "ESLint React's ESLint plugin for React Flavored JSX rules.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"jsx",
|
|
8
|
+
"eslint",
|
|
9
|
+
"eslint-react",
|
|
10
|
+
"eslint-plugin",
|
|
11
|
+
"eslint-plugin-react-jsx"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/Rel1cx/eslint-react",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/Rel1cx/eslint-react/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Rel1cx/eslint-react.git",
|
|
20
|
+
"directory": "packages/plugins/eslint-plugin-react-jsx"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "Rel1cx",
|
|
24
|
+
"sideEffects": false,
|
|
5
25
|
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"./package.json"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@typescript-eslint/scope-manager": "^8.57.2",
|
|
42
|
+
"@typescript-eslint/types": "^8.57.2",
|
|
43
|
+
"@typescript-eslint/utils": "^8.57.2",
|
|
44
|
+
"compare-versions": "^6.1.1",
|
|
45
|
+
"ts-pattern": "^5.9.0",
|
|
46
|
+
"@eslint-react/ast": "4.0.0-beta.1",
|
|
47
|
+
"@eslint-react/core": "4.0.0-beta.1",
|
|
48
|
+
"@eslint-react/jsx": "4.0.0-beta.1",
|
|
49
|
+
"@eslint-react/shared": "4.0.0-beta.1",
|
|
50
|
+
"@eslint-react/var": "4.0.0-beta.1"
|
|
51
|
+
},
|
|
6
52
|
"devDependencies": {
|
|
7
|
-
"@types/
|
|
53
|
+
"@types/react": "^19.2.14",
|
|
54
|
+
"@types/react-dom": "^19.2.3",
|
|
55
|
+
"eslint": "^10.1.0",
|
|
56
|
+
"tsdown": "^0.21.4",
|
|
57
|
+
"@local/configs": "0.0.0",
|
|
58
|
+
"@local/eff": "3.0.0-beta.72"
|
|
8
59
|
},
|
|
9
60
|
"peerDependencies": {
|
|
10
|
-
"
|
|
61
|
+
"eslint": "^10.0.0",
|
|
62
|
+
"typescript": "*"
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"node": ">=22.0.0"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"inlinedDependencies": {
|
|
71
|
+
"@local/eff": "workspace:*"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsdown",
|
|
75
|
+
"lint:publish": "publint",
|
|
76
|
+
"lint:ts": "tsl"
|
|
11
77
|
}
|
|
12
78
|
}
|
package/bun.lock
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"lockfileVersion": 1,
|
|
3
|
-
"configVersion": 1,
|
|
4
|
-
"workspaces": {
|
|
5
|
-
"": {
|
|
6
|
-
"name": "eslint-plugin-react-jsx",
|
|
7
|
-
"devDependencies": {
|
|
8
|
-
"@types/bun": "latest",
|
|
9
|
-
},
|
|
10
|
-
"peerDependencies": {
|
|
11
|
-
"typescript": "^5",
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
},
|
|
15
|
-
"packages": {
|
|
16
|
-
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
17
|
-
|
|
18
|
-
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
|
19
|
-
|
|
20
|
-
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
21
|
-
|
|
22
|
-
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
23
|
-
|
|
24
|
-
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
|
25
|
-
}
|
|
26
|
-
}
|
package/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("Hello via Bun!");
|
package/tsconfig.json
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
// Environment setup & latest features
|
|
4
|
-
"lib": ["ESNext"],
|
|
5
|
-
"target": "ESNext",
|
|
6
|
-
"module": "Preserve",
|
|
7
|
-
"moduleDetection": "force",
|
|
8
|
-
"jsx": "react-jsx",
|
|
9
|
-
"allowJs": true,
|
|
10
|
-
|
|
11
|
-
// Bundler mode
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"allowImportingTsExtensions": true,
|
|
14
|
-
"verbatimModuleSyntax": true,
|
|
15
|
-
"noEmit": true,
|
|
16
|
-
|
|
17
|
-
// Best practices
|
|
18
|
-
"strict": true,
|
|
19
|
-
"skipLibCheck": true,
|
|
20
|
-
"noFallthroughCasesInSwitch": true,
|
|
21
|
-
"noUncheckedIndexedAccess": true,
|
|
22
|
-
"noImplicitOverride": true,
|
|
23
|
-
|
|
24
|
-
// Some stricter flags (disabled by default)
|
|
25
|
-
"noUnusedLocals": false,
|
|
26
|
-
"noUnusedParameters": false,
|
|
27
|
-
"noPropertyAccessFromIndexSignature": false
|
|
28
|
-
}
|
|
29
|
-
}
|