eslint-config-agent 1.3.5 → 1.3.7
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/CHANGELOG.md +14 -0
- package/index.js +102 -125
- package/package.json +4 -1
- package/rules/index.js +3 -0
- package/rules/jsx-classname-required/index.js +68 -0
- package/rules/jsx-classname-required/jsx-classname-required.spec.js +283 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. See [Conven
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
## [1.3.7](https://github.com/tupe12334/eslint-config/compare/v1.3.6...v1.3.7) (2025-09-24)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add error handling rules from eslint-plugin-error ([082701f](https://github.com/tupe12334/eslint-config/commit/082701f8abe8730ed12afe206caf8f10825b4d1c))
|
|
12
|
+
* add jsx-classname-required rule to enforce className attribute on HTML elements in JSX ([3603865](https://github.com/tupe12334/eslint-config/commit/360386582f05e63d6ba1625548725b9216c4f4d6))
|
|
13
|
+
* integrate error handling rules from eslint-plugin-error and update configuration ([7c8b846](https://github.com/tupe12334/eslint-config/commit/7c8b84694550b92ac2a265591fac736d5ddb1180))
|
|
14
|
+
|
|
15
|
+
## [1.3.6](https://github.com/tupe12334/eslint-config/compare/v1.3.5...v1.3.6) (2025-09-21)
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* update TypeScript ESLint integration and remove deprecated parser references ([79ef677](https://github.com/tupe12334/eslint-config/commit/79ef677335cbeb7ed4550da40a0789975dcc68d7))
|
|
20
|
+
|
|
7
21
|
## [1.3.5](https://github.com/tupe12334/eslint-config/compare/v1.3.4...v1.3.5) (2025-09-20)
|
|
8
22
|
|
|
9
23
|
### Bug Fixes
|
package/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import js from "@eslint/js";
|
|
2
|
-
import
|
|
3
|
-
import tsParser from "@typescript-eslint/parser";
|
|
2
|
+
import tseslint from "typescript-eslint";
|
|
4
3
|
import reactHooks from "eslint-plugin-react-hooks";
|
|
5
4
|
import reactPlugin from "eslint-plugin-react";
|
|
6
5
|
import importPlugin from "eslint-plugin-import";
|
|
@@ -13,12 +12,13 @@ import storybookPlugin from "eslint-plugin-storybook";
|
|
|
13
12
|
import globals from "globals";
|
|
14
13
|
import allRules from "./rules/index.js";
|
|
15
14
|
import { noDefaultClassExportRule } from "./rules/no-default-class-export/index.js";
|
|
15
|
+
import errorPlugin from "eslint-plugin-error";
|
|
16
16
|
|
|
17
17
|
// Conditionally import preact plugin if available
|
|
18
18
|
let preactPlugin = null;
|
|
19
19
|
try {
|
|
20
20
|
preactPlugin = (await import("eslint-plugin-preact")).default;
|
|
21
|
-
} catch
|
|
21
|
+
} catch {
|
|
22
22
|
// eslint-plugin-preact is not available
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -31,6 +31,7 @@ const sharedRules = {
|
|
|
31
31
|
"function-paren-newline": "off",
|
|
32
32
|
quotes: "off",
|
|
33
33
|
"no-unused-vars": "off",
|
|
34
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
34
35
|
"max-lines-per-function": allRules.maxFunctionLinesWarning,
|
|
35
36
|
"max-lines": allRules.maxFileLinesWarning,
|
|
36
37
|
semi: "off",
|
|
@@ -40,6 +41,8 @@ const sharedRules = {
|
|
|
40
41
|
"implicit-arrow-linebreak": "off",
|
|
41
42
|
"arrow-body-style": "off",
|
|
42
43
|
"no-continue": "off",
|
|
44
|
+
// Additional built-in error handling rules
|
|
45
|
+
"prefer-promise-reject-errors": "error",
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
// Shared no-restricted-syntax rules for both JS and TS
|
|
@@ -101,7 +104,6 @@ const sharedRestrictedSyntax = [
|
|
|
101
104
|
...allRules.noDefaultClassExportRules,
|
|
102
105
|
];
|
|
103
106
|
|
|
104
|
-
|
|
105
107
|
// TypeScript-specific no-restricted-syntax rules
|
|
106
108
|
const tsOnlyRestrictedSyntax = [
|
|
107
109
|
{
|
|
@@ -189,9 +191,11 @@ const config = [
|
|
|
189
191
|
"class-export": classExportPlugin,
|
|
190
192
|
"single-export": singleExportPlugin,
|
|
191
193
|
"required-exports": requiredExportsPlugin,
|
|
194
|
+
error: errorPlugin,
|
|
192
195
|
custom: {
|
|
193
196
|
rules: {
|
|
194
197
|
"no-default-class-export": noDefaultClassExportRule,
|
|
198
|
+
"jsx-classname-required": allRules.jsxClassNameRequiredRule,
|
|
195
199
|
},
|
|
196
200
|
},
|
|
197
201
|
},
|
|
@@ -199,18 +203,29 @@ const config = [
|
|
|
199
203
|
// Use recommended-latest if available (v5+), otherwise create flat config equivalent of legacy recommended
|
|
200
204
|
...(reactHooks.configs["recommended-latest"]
|
|
201
205
|
? [reactHooks.configs["recommended-latest"]]
|
|
202
|
-
: [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
: [
|
|
207
|
+
{
|
|
208
|
+
name: "react-hooks/recommended-flat",
|
|
209
|
+
plugins: {
|
|
210
|
+
"react-hooks": reactHooks,
|
|
211
|
+
},
|
|
212
|
+
rules: {
|
|
213
|
+
"react-hooks/rules-of-hooks": "error",
|
|
214
|
+
"react-hooks/exhaustive-deps": "warn",
|
|
215
|
+
},
|
|
210
216
|
},
|
|
211
|
-
|
|
212
|
-
),
|
|
217
|
+
]),
|
|
213
218
|
js.configs.recommended,
|
|
219
|
+
...tseslint.configs.recommended,
|
|
220
|
+
// Error handling plugin strict config
|
|
221
|
+
{
|
|
222
|
+
plugins: {
|
|
223
|
+
error: errorPlugin,
|
|
224
|
+
},
|
|
225
|
+
rules: {
|
|
226
|
+
...errorPlugin.configs.strict.rules,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
214
229
|
|
|
215
230
|
// TypeScript and TSX files
|
|
216
231
|
{
|
|
@@ -223,21 +238,12 @@ const config = [
|
|
|
223
238
|
"**/*.stories.{js,jsx,ts,tsx}",
|
|
224
239
|
],
|
|
225
240
|
languageOptions: {
|
|
226
|
-
parser: tsParser,
|
|
227
|
-
parserOptions: {
|
|
228
|
-
ecmaVersion: "latest",
|
|
229
|
-
sourceType: "module",
|
|
230
|
-
ecmaFeatures: {
|
|
231
|
-
jsx: true,
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
241
|
globals: {
|
|
235
242
|
...globals.browser,
|
|
236
243
|
...globals.es2021,
|
|
237
244
|
},
|
|
238
245
|
},
|
|
239
246
|
plugins: {
|
|
240
|
-
"@typescript-eslint": tsPlugin,
|
|
241
247
|
react: reactPlugin,
|
|
242
248
|
import: importPlugin,
|
|
243
249
|
security: securityPlugin,
|
|
@@ -256,15 +262,18 @@ const config = [
|
|
|
256
262
|
"no-undef": "off", // TypeScript handles this
|
|
257
263
|
"custom/no-default-class-export": "error",
|
|
258
264
|
"single-export/single-export": "error",
|
|
259
|
-
"required-exports/required-exports": [
|
|
260
|
-
"
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
"required-exports/required-exports": [
|
|
266
|
+
"error",
|
|
267
|
+
{
|
|
268
|
+
variable: false, // Don't require exporting variables/constants
|
|
269
|
+
function: false, // Don't require exporting functions
|
|
270
|
+
class: true, // Require exporting classes (matches old behavior)
|
|
271
|
+
interface: false, // Don't require exporting interfaces
|
|
272
|
+
type: false, // Don't require exporting types
|
|
273
|
+
enum: true, // Require exporting enums (matches old behavior)
|
|
274
|
+
ignorePrivate: true, // Ignore declarations starting with _
|
|
275
|
+
},
|
|
276
|
+
],
|
|
268
277
|
"no-restricted-syntax": [
|
|
269
278
|
"error",
|
|
270
279
|
...sharedRestrictedSyntax,
|
|
@@ -281,15 +290,18 @@ const config = [
|
|
|
281
290
|
// Include all shared rules (like max-lines-per-function)
|
|
282
291
|
...sharedRules,
|
|
283
292
|
"single-export/single-export": "off", // TSX files have their own specific export rules
|
|
284
|
-
"required-exports/required-exports": [
|
|
285
|
-
"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
"required-exports/required-exports": [
|
|
294
|
+
"error",
|
|
295
|
+
{
|
|
296
|
+
variable: false, // Don't require exporting variables/constants
|
|
297
|
+
function: false, // Don't require exporting functions
|
|
298
|
+
class: true, // Require exporting classes (matches old behavior)
|
|
299
|
+
interface: false, // Don't require exporting interfaces
|
|
300
|
+
type: false, // Don't require exporting types
|
|
301
|
+
enum: true, // Require exporting enums (matches old behavior)
|
|
302
|
+
ignorePrivate: true, // Ignore declarations starting with _
|
|
303
|
+
},
|
|
304
|
+
],
|
|
293
305
|
"no-restricted-syntax": [
|
|
294
306
|
"error",
|
|
295
307
|
// Switch case rules as errors
|
|
@@ -351,12 +363,6 @@ const config = [
|
|
|
351
363
|
message:
|
|
352
364
|
"Function expressions containing switch statements must have explicit return type annotations.",
|
|
353
365
|
},
|
|
354
|
-
// className requirement for HTML elements
|
|
355
|
-
{
|
|
356
|
-
selector:
|
|
357
|
-
'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
|
|
358
|
-
message: "HTML elements must have a className attribute.",
|
|
359
|
-
},
|
|
360
366
|
],
|
|
361
367
|
},
|
|
362
368
|
},
|
|
@@ -482,19 +488,19 @@ const config = [
|
|
|
482
488
|
rules: {
|
|
483
489
|
...sharedRules,
|
|
484
490
|
"single-export/single-export": "error",
|
|
485
|
-
"required-exports/required-exports": [
|
|
486
|
-
"variable": false, // Don't require exporting variables/constants
|
|
487
|
-
"function": false, // Don't require exporting functions
|
|
488
|
-
"class": true, // Require exporting classes (matches old behavior)
|
|
489
|
-
"interface": false, // Don't require exporting interfaces (N/A for JS)
|
|
490
|
-
"type": false, // Don't require exporting types (N/A for JS)
|
|
491
|
-
"enum": false, // Don't require exporting enums (N/A for JS)
|
|
492
|
-
"ignorePrivate": true // Ignore declarations starting with _
|
|
493
|
-
}],
|
|
494
|
-
"no-restricted-syntax": [
|
|
491
|
+
"required-exports/required-exports": [
|
|
495
492
|
"error",
|
|
496
|
-
|
|
493
|
+
{
|
|
494
|
+
variable: false, // Don't require exporting variables/constants
|
|
495
|
+
function: false, // Don't require exporting functions
|
|
496
|
+
class: true, // Require exporting classes (matches old behavior)
|
|
497
|
+
interface: false, // Don't require exporting interfaces (N/A for JS)
|
|
498
|
+
type: false, // Don't require exporting types (N/A for JS)
|
|
499
|
+
enum: false, // Don't require exporting enums (N/A for JS)
|
|
500
|
+
ignorePrivate: true, // Ignore declarations starting with _
|
|
501
|
+
},
|
|
497
502
|
],
|
|
503
|
+
"no-restricted-syntax": ["error", ...sharedRestrictedSyntax],
|
|
498
504
|
},
|
|
499
505
|
},
|
|
500
506
|
|
|
@@ -523,21 +529,12 @@ const config = [
|
|
|
523
529
|
"**/*.stories.{js,jsx,ts,tsx}",
|
|
524
530
|
],
|
|
525
531
|
languageOptions: {
|
|
526
|
-
parser: tsParser,
|
|
527
|
-
parserOptions: {
|
|
528
|
-
ecmaVersion: "latest",
|
|
529
|
-
sourceType: "module",
|
|
530
|
-
ecmaFeatures: {
|
|
531
|
-
jsx: true,
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
532
|
globals: {
|
|
535
533
|
...globals.browser,
|
|
536
534
|
...globals.es2021,
|
|
537
535
|
},
|
|
538
536
|
},
|
|
539
537
|
plugins: {
|
|
540
|
-
"@typescript-eslint": tsPlugin,
|
|
541
538
|
react: reactPlugin,
|
|
542
539
|
import: importPlugin,
|
|
543
540
|
security: securityPlugin,
|
|
@@ -555,15 +552,18 @@ const config = [
|
|
|
555
552
|
...sharedRules,
|
|
556
553
|
"no-undef": "off", // TypeScript handles this
|
|
557
554
|
"single-export/single-export": "off", // JSX files have their own specific export rules
|
|
558
|
-
"required-exports/required-exports": [
|
|
559
|
-
"
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
555
|
+
"required-exports/required-exports": [
|
|
556
|
+
"error",
|
|
557
|
+
{
|
|
558
|
+
variable: false, // Don't require exporting variables/constants
|
|
559
|
+
function: false, // Don't require exporting functions
|
|
560
|
+
class: true, // Require exporting classes (matches old behavior)
|
|
561
|
+
interface: false, // Don't require exporting interfaces (N/A for JSX)
|
|
562
|
+
type: false, // Don't require exporting types (N/A for JSX)
|
|
563
|
+
enum: false, // Don't require exporting enums (N/A for JSX)
|
|
564
|
+
ignorePrivate: true, // Ignore declarations starting with _
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
567
|
"no-restricted-syntax": [
|
|
568
568
|
"error",
|
|
569
569
|
// Switch case rules as errors
|
|
@@ -625,12 +625,6 @@ const config = [
|
|
|
625
625
|
message:
|
|
626
626
|
"Function expressions containing switch statements must have explicit return type annotations.",
|
|
627
627
|
},
|
|
628
|
-
// className requirement for HTML elements
|
|
629
|
-
{
|
|
630
|
-
selector:
|
|
631
|
-
'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
|
|
632
|
-
message: "HTML elements must have a className attribute.",
|
|
633
|
-
},
|
|
634
628
|
// Required export rules as errors (class rule only for JSX)
|
|
635
629
|
{
|
|
636
630
|
selector:
|
|
@@ -653,21 +647,12 @@ const config = [
|
|
|
653
647
|
"**/*.stories.{js,jsx,ts,tsx}",
|
|
654
648
|
],
|
|
655
649
|
languageOptions: {
|
|
656
|
-
parser: tsParser,
|
|
657
|
-
parserOptions: {
|
|
658
|
-
ecmaVersion: "latest",
|
|
659
|
-
sourceType: "module",
|
|
660
|
-
ecmaFeatures: {
|
|
661
|
-
jsx: true,
|
|
662
|
-
},
|
|
663
|
-
},
|
|
664
650
|
globals: {
|
|
665
651
|
...globals.browser,
|
|
666
652
|
...globals.es2021,
|
|
667
653
|
},
|
|
668
654
|
},
|
|
669
655
|
plugins: {
|
|
670
|
-
"@typescript-eslint": tsPlugin,
|
|
671
656
|
react: reactPlugin,
|
|
672
657
|
import: importPlugin,
|
|
673
658
|
security: securityPlugin,
|
|
@@ -684,15 +669,18 @@ const config = [
|
|
|
684
669
|
rules: {
|
|
685
670
|
"no-undef": "off", // TypeScript handles this
|
|
686
671
|
"single-export/single-export": "off", // JSX files have their own specific export rules
|
|
687
|
-
"required-exports/required-exports": [
|
|
688
|
-
"
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
672
|
+
"required-exports/required-exports": [
|
|
673
|
+
"warn",
|
|
674
|
+
{
|
|
675
|
+
variable: false, // Don't require exporting variables/constants
|
|
676
|
+
function: false, // Don't require exporting functions
|
|
677
|
+
class: true, // Require exporting classes (matches old behavior)
|
|
678
|
+
interface: false, // Don't require exporting interfaces (N/A for JSX)
|
|
679
|
+
type: false, // Don't require exporting types (N/A for JSX)
|
|
680
|
+
enum: false, // Don't require exporting enums (N/A for JSX)
|
|
681
|
+
ignorePrivate: true, // Ignore declarations starting with _
|
|
682
|
+
},
|
|
683
|
+
],
|
|
696
684
|
"no-restricted-syntax": [
|
|
697
685
|
"warn",
|
|
698
686
|
// Include shared rules but remove the multiple exports restriction and switch case rules for JSX
|
|
@@ -855,18 +843,22 @@ const config = [
|
|
|
855
843
|
"**/test/**",
|
|
856
844
|
"!**/test/export/**",
|
|
857
845
|
"!**/test/required-exports/**",
|
|
846
|
+
"!**/test/switch-case/**",
|
|
858
847
|
"**/rules/**/index.js",
|
|
859
848
|
],
|
|
860
849
|
rules: {
|
|
861
|
-
"required-exports/required-exports": [
|
|
862
|
-
"
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
850
|
+
"required-exports/required-exports": [
|
|
851
|
+
"error",
|
|
852
|
+
{
|
|
853
|
+
variable: false, // Don't require exporting variables/constants
|
|
854
|
+
function: false, // Don't require exporting functions
|
|
855
|
+
class: true, // Require exporting classes (matches old behavior)
|
|
856
|
+
interface: false, // Don't require exporting interfaces
|
|
857
|
+
type: false, // Don't require exporting types
|
|
858
|
+
enum: true, // Require exporting enums (matches old behavior)
|
|
859
|
+
ignorePrivate: true, // Ignore declarations starting with _
|
|
860
|
+
},
|
|
861
|
+
],
|
|
870
862
|
"no-restricted-syntax": [
|
|
871
863
|
"error",
|
|
872
864
|
// Switch case rules as errors
|
|
@@ -959,14 +951,7 @@ const config = [
|
|
|
959
951
|
files: ["**/*.{tsx,jsx}"],
|
|
960
952
|
ignores: ["**/*.stories.{js,jsx,ts,tsx}"],
|
|
961
953
|
rules: {
|
|
962
|
-
"
|
|
963
|
-
"error",
|
|
964
|
-
{
|
|
965
|
-
selector:
|
|
966
|
-
'JSXOpeningElement:not([name.name=/^[A-Z]/]):not([name.name="Fragment"]):not(:has(JSXAttribute[name.name="className"]))',
|
|
967
|
-
message: "HTML elements must have a className attribute.",
|
|
968
|
-
},
|
|
969
|
-
],
|
|
954
|
+
"custom/jsx-classname-required": "error",
|
|
970
955
|
},
|
|
971
956
|
},
|
|
972
957
|
|
|
@@ -1019,14 +1004,6 @@ const config = [
|
|
|
1019
1004
|
{
|
|
1020
1005
|
files: ["**/*.stories.{js,jsx,ts,tsx}"],
|
|
1021
1006
|
languageOptions: {
|
|
1022
|
-
parser: tsParser,
|
|
1023
|
-
parserOptions: {
|
|
1024
|
-
ecmaVersion: "latest",
|
|
1025
|
-
sourceType: "module",
|
|
1026
|
-
ecmaFeatures: {
|
|
1027
|
-
jsx: true,
|
|
1028
|
-
},
|
|
1029
|
-
},
|
|
1030
1007
|
globals: {
|
|
1031
1008
|
...globals.browser,
|
|
1032
1009
|
...globals.es2021,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-config-agent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"description": "ESLint configuration package with TypeScript support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -106,5 +106,8 @@
|
|
|
106
106
|
"globals": "^16.3.0",
|
|
107
107
|
"release-it": "^19.0.4",
|
|
108
108
|
"typescript-eslint": "^8.40.0"
|
|
109
|
+
},
|
|
110
|
+
"dependencies": {
|
|
111
|
+
"eslint-plugin-error": "^1.1.1"
|
|
109
112
|
}
|
|
110
113
|
}
|
package/rules/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { noTypeAssertionsConfig } from "./no-type-assertions/index.js";
|
|
|
16
16
|
import { noExportSpecifiersConfig } from "./no-empty-exports/index.js";
|
|
17
17
|
import { noClassPropertyDefaultsConfig } from "./no-class-property-defaults/index.js";
|
|
18
18
|
import { noDefaultClassExportRules } from "./no-default-class-export/index.js";
|
|
19
|
+
import { jsxClassNameRequiredRule } from "./jsx-classname-required/index.js";
|
|
19
20
|
|
|
20
21
|
// Plugin rule configurations
|
|
21
22
|
import { pluginRules } from "./plugin/index.js";
|
|
@@ -36,6 +37,7 @@ const allRules = {
|
|
|
36
37
|
noExportSpecifiersConfig,
|
|
37
38
|
noClassPropertyDefaultsConfig,
|
|
38
39
|
noDefaultClassExportRules,
|
|
40
|
+
jsxClassNameRequiredRule,
|
|
39
41
|
|
|
40
42
|
// Plugin rule configurations
|
|
41
43
|
pluginRules,
|
|
@@ -59,6 +61,7 @@ export {
|
|
|
59
61
|
noExportSpecifiersConfig,
|
|
60
62
|
noClassPropertyDefaultsConfig,
|
|
61
63
|
noDefaultClassExportRules,
|
|
64
|
+
jsxClassNameRequiredRule,
|
|
62
65
|
|
|
63
66
|
// Plugin rule configurations
|
|
64
67
|
pluginRules,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule to require className attribute on HTML elements in JSX
|
|
3
|
+
*
|
|
4
|
+
* This rule enforces that all HTML elements in JSX must have a className attribute.
|
|
5
|
+
* It excludes React components (starting with capital letters) and fragments.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - ❌ <div>Content</div>
|
|
9
|
+
* - ✅ <div className="container">Content</div>
|
|
10
|
+
* - ✅ <MyComponent>Content</MyComponent> (ignored - React component)
|
|
11
|
+
* - ✅ <Fragment>Content</Fragment> (ignored - fragment)
|
|
12
|
+
* - ✅ <React.Fragment>Content</React.Fragment> (ignored - React fragment)
|
|
13
|
+
* - ✅ <React.StrictMode>Content</React.StrictMode> (ignored - React component)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const jsxClassNameRequiredRule = {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "layout",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "Require className attribute on HTML elements in JSX",
|
|
21
|
+
category: "Stylistic Issues",
|
|
22
|
+
recommended: false,
|
|
23
|
+
},
|
|
24
|
+
fixable: null,
|
|
25
|
+
schema: [],
|
|
26
|
+
messages: {
|
|
27
|
+
missingClassName: "HTML elements must have a className attribute.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
create(context) {
|
|
32
|
+
return {
|
|
33
|
+
JSXOpeningElement(node) {
|
|
34
|
+
// Skip if this is a React component (starts with capital letter)
|
|
35
|
+
if (node.name.type === 'JSXIdentifier' && /^[A-Z]/.test(node.name.name)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Skip if this is Fragment
|
|
40
|
+
if (node.name.type === 'JSXIdentifier' && node.name.name === 'Fragment') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Skip if this is React.Something (like React.Fragment, React.StrictMode, etc.)
|
|
45
|
+
if (node.name.type === 'JSXMemberExpression' &&
|
|
46
|
+
node.name.object.name === 'React' &&
|
|
47
|
+
/^[A-Z]/.test(node.name.property.name)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check if element has className attribute
|
|
52
|
+
const hasClassName = node.attributes.some(attr =>
|
|
53
|
+
attr.type === 'JSXAttribute' &&
|
|
54
|
+
attr.name.name === 'className'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!hasClassName) {
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: 'missingClassName',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { jsxClassNameRequiredRule };
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { RuleTester } from "eslint";
|
|
2
|
+
import { jsxClassNameRequiredRule } from "./index.js";
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
ecmaVersion: 2022,
|
|
7
|
+
sourceType: "module",
|
|
8
|
+
parser: (await import("typescript-eslint")).parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaFeatures: {
|
|
11
|
+
jsx: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
ruleTester.run('jsx-classname-required', jsxClassNameRequiredRule, {
|
|
18
|
+
valid: [
|
|
19
|
+
// HTML elements with className
|
|
20
|
+
{
|
|
21
|
+
code: '<div className="container">Content</div>',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
code: '<span className="text">Text</span>',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: '<p className="paragraph">Paragraph</p>',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
code: '<button className="btn">Button</button>',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
code: '<input className="input" type="text" />',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
code: '<img className="image" src="test.jpg" alt="test" />',
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// React components (should be ignored)
|
|
40
|
+
{
|
|
41
|
+
code: '<MyComponent>Content</MyComponent>',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
code: '<CustomButton onClick={() => {}}>Click</CustomButton>',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: '<AnotherComponent prop="value" />',
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Fragments (should be ignored)
|
|
51
|
+
{
|
|
52
|
+
code: '<Fragment>Content</Fragment>',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: '<React.Fragment>Content</React.Fragment>',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
code: '<>Content</>',
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// React.* components (should be ignored)
|
|
62
|
+
{
|
|
63
|
+
code: '<React.StrictMode><div className="content">Content</div></React.StrictMode>',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
code: '<React.Suspense fallback={<div className="loading">Loading</div>}><div className="content">Content</div></React.Suspense>',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
code: '<React.Profiler id="test" onRender={() => {}}><div className="content">Content</div></React.Profiler>',
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Mixed cases with valid HTML elements
|
|
73
|
+
{
|
|
74
|
+
code: `
|
|
75
|
+
<div className="container">
|
|
76
|
+
<MyComponent>
|
|
77
|
+
<p className="text">Text</p>
|
|
78
|
+
</MyComponent>
|
|
79
|
+
<React.Fragment>
|
|
80
|
+
<span className="fragment-content">Fragment content</span>
|
|
81
|
+
</React.Fragment>
|
|
82
|
+
</div>
|
|
83
|
+
`,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Nested valid cases
|
|
87
|
+
{
|
|
88
|
+
code: `
|
|
89
|
+
<div className="outer">
|
|
90
|
+
<div className="inner">
|
|
91
|
+
<span className="nested">Nested content</span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
`,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
|
|
98
|
+
invalid: [
|
|
99
|
+
// HTML elements without className
|
|
100
|
+
{
|
|
101
|
+
code: '<div>Content</div>',
|
|
102
|
+
errors: [
|
|
103
|
+
{
|
|
104
|
+
messageId: 'missingClassName',
|
|
105
|
+
type: 'JSXOpeningElement',
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
code: '<span>Text</span>',
|
|
111
|
+
errors: [
|
|
112
|
+
{
|
|
113
|
+
messageId: 'missingClassName',
|
|
114
|
+
type: 'JSXOpeningElement',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
code: '<p>Paragraph</p>',
|
|
120
|
+
errors: [
|
|
121
|
+
{
|
|
122
|
+
messageId: 'missingClassName',
|
|
123
|
+
type: 'JSXOpeningElement',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
code: '<button>Button</button>',
|
|
129
|
+
errors: [
|
|
130
|
+
{
|
|
131
|
+
messageId: 'missingClassName',
|
|
132
|
+
type: 'JSXOpeningElement',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
code: '<input type="text" />',
|
|
138
|
+
errors: [
|
|
139
|
+
{
|
|
140
|
+
messageId: 'missingClassName',
|
|
141
|
+
type: 'JSXOpeningElement',
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
code: '<img src="test.jpg" alt="test" />',
|
|
147
|
+
errors: [
|
|
148
|
+
{
|
|
149
|
+
messageId: 'missingClassName',
|
|
150
|
+
type: 'JSXOpeningElement',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// HTML elements inside React components (should still error)
|
|
156
|
+
{
|
|
157
|
+
code: '<MyComponent><div>Content</div></MyComponent>',
|
|
158
|
+
errors: [
|
|
159
|
+
{
|
|
160
|
+
messageId: 'missingClassName',
|
|
161
|
+
type: 'JSXOpeningElement',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
code: '<React.StrictMode><div>Content</div></React.StrictMode>',
|
|
167
|
+
errors: [
|
|
168
|
+
{
|
|
169
|
+
messageId: 'missingClassName',
|
|
170
|
+
type: 'JSXOpeningElement',
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
code: '<React.Fragment><span>Content</span></React.Fragment>',
|
|
176
|
+
errors: [
|
|
177
|
+
{
|
|
178
|
+
messageId: 'missingClassName',
|
|
179
|
+
type: 'JSXOpeningElement',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
code: '<Fragment><p>Content</p></Fragment>',
|
|
185
|
+
errors: [
|
|
186
|
+
{
|
|
187
|
+
messageId: 'missingClassName',
|
|
188
|
+
type: 'JSXOpeningElement',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
code: '<><div>Content</div></>',
|
|
194
|
+
errors: [
|
|
195
|
+
{
|
|
196
|
+
messageId: 'missingClassName',
|
|
197
|
+
type: 'JSXOpeningElement',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Multiple errors in one file
|
|
203
|
+
{
|
|
204
|
+
code: `
|
|
205
|
+
<div className="container">
|
|
206
|
+
<span>Missing className</span>
|
|
207
|
+
<p>Also missing className</p>
|
|
208
|
+
</div>
|
|
209
|
+
`,
|
|
210
|
+
errors: [
|
|
211
|
+
{
|
|
212
|
+
messageId: 'missingClassName',
|
|
213
|
+
type: 'JSXOpeningElement',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
messageId: 'missingClassName',
|
|
217
|
+
type: 'JSXOpeningElement',
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
// Mixed valid/invalid cases
|
|
223
|
+
{
|
|
224
|
+
code: `
|
|
225
|
+
<div className="container">
|
|
226
|
+
<span className="valid">Valid</span>
|
|
227
|
+
<p>Invalid - no className</p>
|
|
228
|
+
<MyComponent>
|
|
229
|
+
<div>Invalid - no className</div>
|
|
230
|
+
</MyComponent>
|
|
231
|
+
</div>
|
|
232
|
+
`,
|
|
233
|
+
errors: [
|
|
234
|
+
{
|
|
235
|
+
messageId: 'missingClassName',
|
|
236
|
+
type: 'JSXOpeningElement',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
messageId: 'missingClassName',
|
|
240
|
+
type: 'JSXOpeningElement',
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
// Edge cases
|
|
246
|
+
{
|
|
247
|
+
code: '<h1>Heading</h1>',
|
|
248
|
+
errors: [
|
|
249
|
+
{
|
|
250
|
+
messageId: 'missingClassName',
|
|
251
|
+
type: 'JSXOpeningElement',
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
code: '<section>Section</section>',
|
|
257
|
+
errors: [
|
|
258
|
+
{
|
|
259
|
+
messageId: 'missingClassName',
|
|
260
|
+
type: 'JSXOpeningElement',
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
code: '<article>Article</article>',
|
|
266
|
+
errors: [
|
|
267
|
+
{
|
|
268
|
+
messageId: 'missingClassName',
|
|
269
|
+
type: 'JSXOpeningElement',
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
code: '<nav>Navigation</nav>',
|
|
275
|
+
errors: [
|
|
276
|
+
{
|
|
277
|
+
messageId: 'missingClassName',
|
|
278
|
+
type: 'JSXOpeningElement',
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
});
|