eslint-config-agent 1.2.3 → 1.3.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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. See [Conven
4
4
 
5
5
 
6
6
 
7
+ ## [1.3.1](https://github.com/tupe12334/eslint-config/compare/v1.3.0...v1.3.1) (2025-09-20)
8
+
9
+ ### Bug Fixes
10
+
11
+ * update eslint-plugin-single-export version to 1.1.1 and adjust dependencies ([d2a82b5](https://github.com/tupe12334/eslint-config/commit/d2a82b51fc302ffb0589fb58a6a26452833d6639))
12
+
13
+ ## [1.3.0](https://github.com/tupe12334/eslint-config/compare/v1.2.3...v1.3.0) (2025-09-20)
14
+
15
+ ### Features
16
+
17
+ * add eslint-plugin-single-export to enforce single export rules ([9cfb800](https://github.com/tupe12334/eslint-config/commit/9cfb80073cf9cdcefd31af5cc2c9a1c83dcd6926))
18
+ * add no-default-class-export rule to enforce named exports for classes ([03d5273](https://github.com/tupe12334/eslint-config/commit/03d52731ca190ebf3d76d4ded7c6ace7fe5fc75f))
19
+
20
+ ### Bug Fixes
21
+
22
+ * disable group-exports rule to prevent enforcing single statement exports ([0cfd699](https://github.com/tupe12334/eslint-config/commit/0cfd6994a0978d8a15e7ecae83b0765e5cfcf747))
23
+ * improve null check for class names in no-default-class-export rule ([bf090e3](https://github.com/tupe12334/eslint-config/commit/bf090e3490f08f6e9b07a16227ab22aba702f31c))
24
+
7
25
  ## [1.2.3](https://github.com/tupe12334/eslint-config/compare/v1.2.2...v1.2.3) (2025-09-14)
8
26
 
9
27
  ### Features
package/index.js CHANGED
@@ -7,9 +7,11 @@ import importPlugin from "eslint-plugin-import";
7
7
  import securityPlugin from "eslint-plugin-security";
8
8
  import nPlugin from "eslint-plugin-n";
9
9
  import classExportPlugin from "eslint-plugin-class-export";
10
+ import singleExportPlugin from "eslint-plugin-single-export";
10
11
  import storybookPlugin from "eslint-plugin-storybook";
11
12
  import globals from "globals";
12
13
  import allRules from "./rules/index.js";
14
+ import { noDefaultClassExportRule } from "./rules/no-default-class-export/index.js";
13
15
 
14
16
  // Conditionally import preact plugin if available
15
17
  let preactPlugin = null;
@@ -95,6 +97,7 @@ const sharedRestrictedSyntax = [
95
97
  },
96
98
  allRules.noProcessEnvPropertiesConfig,
97
99
  allRules.noExportSpecifiersConfig,
100
+ ...allRules.noDefaultClassExportRules,
98
101
  ];
99
102
 
100
103
  // Required export rules (always errors)
@@ -198,6 +201,12 @@ const config = [
198
201
  plugins: {
199
202
  n: nPlugin,
200
203
  "class-export": classExportPlugin,
204
+ "single-export": singleExportPlugin,
205
+ "custom": {
206
+ rules: {
207
+ "no-default-class-export": noDefaultClassExportRule,
208
+ },
209
+ },
201
210
  },
202
211
  },
203
212
  reactHooks.configs["recommended-latest"],
@@ -245,6 +254,8 @@ const config = [
245
254
  ...sharedRules,
246
255
  ...allRules.typescriptEslintRules,
247
256
  "no-undef": "off", // TypeScript handles this
257
+ "custom/no-default-class-export": "error",
258
+ "single-export/single-export": "error",
248
259
  "no-restricted-syntax": [
249
260
  "error",
250
261
  ...sharedRestrictedSyntax,
@@ -261,6 +272,7 @@ const config = [
261
272
  rules: {
262
273
  // Include all shared rules (like max-lines-per-function)
263
274
  ...sharedRules,
275
+ "single-export/single-export": "off", // TSX files have their own specific export rules
264
276
  "no-restricted-syntax": [
265
277
  "error",
266
278
  // Switch case rules as errors
@@ -337,6 +349,7 @@ const config = [
337
349
  files: ["**/*.tsx"],
338
350
  ignores: ["**/*.stories.{js,jsx,ts,tsx}"],
339
351
  rules: {
352
+ "single-export/single-export": "off", // TSX files have their own specific export rules
340
353
  "no-restricted-syntax": [
341
354
  "warn",
342
355
  // Include shared rules but remove the multiple exports restriction and switch case rules for TSX
@@ -446,6 +459,7 @@ const config = [
446
459
  security: securityPlugin,
447
460
  n: nPlugin,
448
461
  "class-export": classExportPlugin,
462
+ "single-export": singleExportPlugin,
449
463
  },
450
464
  settings: {
451
465
  react: {
@@ -454,6 +468,7 @@ const config = [
454
468
  },
455
469
  rules: {
456
470
  ...sharedRules,
471
+ "single-export/single-export": "error",
457
472
  "no-restricted-syntax": [
458
473
  "error",
459
474
  ...sharedRestrictedSyntax,
@@ -513,6 +528,7 @@ const config = [
513
528
  security: securityPlugin,
514
529
  n: nPlugin,
515
530
  "class-export": classExportPlugin,
531
+ "single-export": singleExportPlugin,
516
532
  ...(preactPlugin && { preact: preactPlugin }),
517
533
  },
518
534
  settings: {
@@ -523,6 +539,7 @@ const config = [
523
539
  rules: {
524
540
  ...sharedRules,
525
541
  "no-undef": "off", // TypeScript handles this
542
+ "single-export/single-export": "off", // JSX files have their own specific export rules
526
543
  "no-restricted-syntax": [
527
544
  "error",
528
545
  // Switch case rules as errors
@@ -632,6 +649,7 @@ const config = [
632
649
  security: securityPlugin,
633
650
  n: nPlugin,
634
651
  "class-export": classExportPlugin,
652
+ "single-export": singleExportPlugin,
635
653
  ...(preactPlugin && { preact: preactPlugin }),
636
654
  },
637
655
  settings: {
@@ -641,6 +659,7 @@ const config = [
641
659
  },
642
660
  rules: {
643
661
  "no-undef": "off", // TypeScript handles this
662
+ "single-export/single-export": "off", // JSX files have their own specific export rules
644
663
  "no-restricted-syntax": [
645
664
  "warn",
646
665
  // Include shared rules but remove the multiple exports restriction and switch case rules for JSX
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-config-agent",
3
- "version": "1.2.3",
3
+ "version": "1.3.1",
4
4
  "description": "ESLint configuration package with TypeScript support",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -100,6 +100,7 @@
100
100
  "eslint-plugin-react": "^7.37.5",
101
101
  "eslint-plugin-react-hooks": "^5.2.0",
102
102
  "eslint-plugin-security": "^3.0.1",
103
+ "eslint-plugin-single-export": "^1.1.1",
103
104
  "eslint-plugin-storybook": "^9.1.5",
104
105
  "globals": "^16.3.0",
105
106
  "release-it": "^19.0.4",
package/rules/index.js CHANGED
@@ -15,6 +15,7 @@ import { noProcessEnvPropertiesConfig } from "./no-process-env-properties/index.
15
15
  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
+ import { noDefaultClassExportRules } from "./no-default-class-export/index.js";
18
19
 
19
20
  // Plugin rule configurations
20
21
  import { pluginRules } from "./plugin/index.js";
@@ -34,6 +35,7 @@ const allRules = {
34
35
  noTypeAssertionsConfig,
35
36
  noExportSpecifiersConfig,
36
37
  noClassPropertyDefaultsConfig,
38
+ noDefaultClassExportRules,
37
39
 
38
40
  // Plugin rule configurations
39
41
  pluginRules,
@@ -56,6 +58,7 @@ export {
56
58
  noTypeAssertionsConfig,
57
59
  noExportSpecifiersConfig,
58
60
  noClassPropertyDefaultsConfig,
61
+ noDefaultClassExportRules,
59
62
 
60
63
  // Plugin rule configurations
61
64
  pluginRules,
@@ -0,0 +1,10 @@
1
+ // Invalid: Default abstract class export
2
+ export default abstract class BaseHandler {
3
+ protected active = false;
4
+
5
+ abstract handle(): void;
6
+
7
+ isActive(): boolean {
8
+ return this.active;
9
+ }
10
+ }
@@ -0,0 +1,12 @@
1
+ // Invalid: Default class export (class declaration)
2
+ export default class UserService {
3
+ private users: string[] = [];
4
+
5
+ addUser(name: string): void {
6
+ this.users.push(name);
7
+ }
8
+
9
+ getUsers(): string[] {
10
+ return this.users;
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ // Invalid: Default class export (class expression)
2
+ export default class {
3
+ private value: number = 0;
4
+
5
+ increment(): void {
6
+ this.value++;
7
+ }
8
+
9
+ getValue(): number {
10
+ return this.value;
11
+ }
12
+ }
@@ -0,0 +1,12 @@
1
+ // Invalid: Default generic class export
2
+ export default class Repository<T> {
3
+ private items: T[] = [];
4
+
5
+ save(item: T): void {
6
+ this.items.push(item);
7
+ }
8
+
9
+ findAll(): T[] {
10
+ return [...this.items];
11
+ }
12
+ }
@@ -0,0 +1,17 @@
1
+ // Valid: Named abstract class export
2
+ export abstract class BaseService {
3
+ protected initialized = false;
4
+
5
+ abstract initialize(): Promise<void>;
6
+
7
+ isInitialized(): boolean {
8
+ return this.initialized;
9
+ }
10
+ }
11
+
12
+ export class ConcreteService extends BaseService {
13
+ async initialize(): Promise<void> {
14
+ // Implementation
15
+ this.initialized = true;
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ // Valid: Named generic class export
2
+ export class Container<T> {
3
+ private items: T[] = [];
4
+
5
+ add(item: T): void {
6
+ this.items.push(item);
7
+ }
8
+
9
+ get(index: number): T | undefined {
10
+ return this.items[index];
11
+ }
12
+
13
+ getAll(): T[] {
14
+ return [...this.items];
15
+ }
16
+ }
@@ -0,0 +1,22 @@
1
+ // Valid: Multiple named exports including classes
2
+ export interface UserData {
3
+ id: number;
4
+ name: string;
5
+ }
6
+
7
+ export class UserRepository {
8
+ private data: UserData[] = [];
9
+
10
+ save(user: UserData): void {
11
+ this.data.push(user);
12
+ }
13
+
14
+ findById(id: number): UserData | undefined {
15
+ return this.data.find(user => user.id === id);
16
+ }
17
+ }
18
+
19
+ export const DEFAULT_USER: UserData = {
20
+ id: 0,
21
+ name: 'Anonymous'
22
+ };
@@ -0,0 +1,12 @@
1
+ // Valid: Named class export
2
+ export class UserService {
3
+ private users: string[] = [];
4
+
5
+ addUser(name: string): void {
6
+ this.users.push(name);
7
+ }
8
+
9
+ getUsers(): string[] {
10
+ return this.users;
11
+ }
12
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Rule configuration for preventing default class exports
3
+ *
4
+ * This rule enforces named exports for classes instead of default exports
5
+ * to improve code organization, import consistency, and tree-shaking.
6
+ *
7
+ * Default class exports can make it harder to:
8
+ * - Track class usage across the codebase
9
+ * - Perform refactoring and renaming operations
10
+ * - Enable proper tree-shaking optimizations
11
+ * - Maintain consistent import naming conventions
12
+ */
13
+
14
+ /**
15
+ * Custom ESLint rule that prevents default class exports and provides auto-fix
16
+ */
17
+ export const noDefaultClassExportRule = {
18
+ meta: {
19
+ type: "problem",
20
+ docs: {
21
+ description: "Disallow default class exports in favor of named exports",
22
+ category: "Best Practices",
23
+ },
24
+ fixable: "code",
25
+ messages: {
26
+ noDefaultClassDeclaration: "Default class exports are not allowed. Use named exports instead: 'export class {{className}} {}'.",
27
+ noDefaultClassExpression: "Default class expressions are not allowed. Use named class declarations instead: 'export class ClassName {}'.",
28
+ },
29
+ schema: [],
30
+ },
31
+ create(context) {
32
+ const sourceCode = context.getSourceCode();
33
+
34
+ return {
35
+ ExportDefaultDeclaration(node) {
36
+ // Check if the default export is a class declaration
37
+ if (node.declaration && node.declaration.type === "ClassDeclaration") {
38
+ const classDeclaration = node.declaration;
39
+ const className = (classDeclaration.id && classDeclaration.id.name) || "ClassName";
40
+
41
+ context.report({
42
+ node: node,
43
+ messageId: "noDefaultClassDeclaration",
44
+ data: {
45
+ className: className,
46
+ },
47
+ fix(fixer) {
48
+ // Handle anonymous classes
49
+ if (!classDeclaration.id) {
50
+ // Get class body and other parts
51
+ const body = sourceCode.getText(classDeclaration.body);
52
+ const superClass = classDeclaration.superClass
53
+ ? ` extends ${sourceCode.getText(classDeclaration.superClass)}`
54
+ : "";
55
+
56
+ return fixer.replaceText(node, `export class ${className}${superClass} ${body}`);
57
+ }
58
+
59
+ // For named classes, just replace default with named export
60
+ const classText = sourceCode.getText(classDeclaration);
61
+ return fixer.replaceText(node, `export ${classText}`);
62
+ },
63
+ });
64
+ }
65
+ // Check if the default export is a class expression
66
+ else if (node.declaration && node.declaration.type === "ClassExpression") {
67
+ const classExpression = node.declaration;
68
+ const className = (classExpression.id && classExpression.id.name) || "ClassName";
69
+
70
+ context.report({
71
+ node: node,
72
+ messageId: "noDefaultClassExpression",
73
+ fix(fixer) {
74
+ // Build the fixed class declaration
75
+ const body = sourceCode.getText(classExpression.body);
76
+ const superClass = classExpression.superClass
77
+ ? ` extends ${sourceCode.getText(classExpression.superClass)}`
78
+ : "";
79
+
80
+ const newCode = `export class ${className}${superClass} ${body}`;
81
+ return fixer.replaceText(node, newCode);
82
+ },
83
+ });
84
+ }
85
+ },
86
+ };
87
+ },
88
+ };
89
+
90
+ /**
91
+ * Backward compatibility: no-restricted-syntax configurations
92
+ * (for use in existing rule arrays that don't support custom rules)
93
+ */
94
+ export const noDefaultClassExportConfig = {
95
+ selector: "ExportDefaultDeclaration > ClassDeclaration",
96
+ message: "Default class exports are not allowed. Use named exports instead: 'export class ClassName {}'."
97
+ };
98
+
99
+ export const noDefaultClassExpressionConfig = {
100
+ selector: "ExportDefaultDeclaration > ClassExpression",
101
+ message: "Default class expressions are not allowed. Use named class declarations instead: 'export class ClassName {}'."
102
+ };
103
+
104
+ /**
105
+ * Combined rule configurations for comprehensive default class export prevention
106
+ */
107
+ export const noDefaultClassExportRules = [
108
+ noDefaultClassExportConfig,
109
+ noDefaultClassExpressionConfig
110
+ ];
111
+
112
+ // Default export for backward compatibility
113
+ export default noDefaultClassExportConfig;
114
+
115
+ // Named exports for specific use cases
116
+ export {
117
+ noDefaultClassExportConfig as noDefaultClassExport,
118
+ noDefaultClassExpressionConfig as noDefaultClassExpression,
119
+ noDefaultClassExportRules as allNoDefaultClassExportRules,
120
+ };
@@ -0,0 +1,301 @@
1
+ import { RuleTester } from "eslint";
2
+ import { noDefaultClassExportRule } from "./index.js";
3
+
4
+ /**
5
+ * Test suite for no-default-class-export rule
6
+ *
7
+ * This tests the custom ESLint rule that prevents default class exports
8
+ * and provides auto-fix functionality to convert them to named exports.
9
+ */
10
+
11
+ const ruleTester = new RuleTester({
12
+ languageOptions: {
13
+ ecmaVersion: 2022,
14
+ sourceType: "module",
15
+ parser: (await import("@typescript-eslint/parser")).default,
16
+ parserOptions: {
17
+ ecmaFeatures: {
18
+ jsx: true,
19
+ },
20
+ },
21
+ },
22
+ });
23
+
24
+ ruleTester.run("no-default-class-export", noDefaultClassExportRule, {
25
+ valid: [
26
+ // Valid: Named class export
27
+ {
28
+ code: `
29
+ export class UserService {
30
+ constructor() {}
31
+ }
32
+ `,
33
+ },
34
+ // Valid: Multiple named exports including class
35
+ {
36
+ code: `
37
+ export interface UserData {
38
+ id: number;
39
+ name: string;
40
+ }
41
+
42
+ export class UserRepository {
43
+ save(user: UserData): void {}
44
+ }
45
+
46
+ export const DEFAULT_USER: UserData = {
47
+ id: 0,
48
+ name: 'Anonymous'
49
+ };
50
+ `,
51
+ },
52
+ // Valid: Abstract class with named export
53
+ {
54
+ code: `
55
+ export abstract class BaseService {
56
+ abstract initialize(): void;
57
+ }
58
+ `,
59
+ },
60
+ // Valid: Generic class with named export
61
+ {
62
+ code: `
63
+ export class Container<T> {
64
+ private items: T[] = [];
65
+ add(item: T): void {
66
+ this.items.push(item);
67
+ }
68
+ }
69
+ `,
70
+ },
71
+ // Valid: Class with decorators (named export)
72
+ {
73
+ code: `
74
+ @Injectable()
75
+ export class ApiService {
76
+ constructor() {}
77
+ }
78
+ `,
79
+ },
80
+ // Valid: Default export of non-class
81
+ {
82
+ code: `
83
+ export default function createUser() {
84
+ return { id: 1, name: 'Test' };
85
+ }
86
+ `,
87
+ },
88
+ // Valid: Default export of constant
89
+ {
90
+ code: `
91
+ const config = { apiUrl: 'http://localhost' };
92
+ export default config;
93
+ `,
94
+ },
95
+ // Valid: Default export of object
96
+ {
97
+ code: `
98
+ export default {
99
+ users: [],
100
+ addUser(name: string) {
101
+ this.users.push(name);
102
+ }
103
+ };
104
+ `,
105
+ },
106
+ ],
107
+
108
+ invalid: [
109
+ // Invalid: Default class declaration export
110
+ {
111
+ code: `
112
+ export default class UserService {
113
+ constructor() {}
114
+ }
115
+ `,
116
+ output: `
117
+ export class UserService {
118
+ constructor() {}
119
+ }
120
+ `,
121
+ errors: [
122
+ {
123
+ messageId: "noDefaultClassDeclaration",
124
+ },
125
+ ],
126
+ },
127
+ // Invalid: Default anonymous class (treated as declaration)
128
+ {
129
+ code: `
130
+ export default class {
131
+ getValue() {
132
+ return 42;
133
+ }
134
+ }
135
+ `,
136
+ output: `
137
+ export class ClassName {
138
+ getValue() {
139
+ return 42;
140
+ }
141
+ }
142
+ `,
143
+ errors: [
144
+ {
145
+ messageId: "noDefaultClassDeclaration",
146
+ },
147
+ ],
148
+ },
149
+ // Invalid: Default abstract class
150
+ {
151
+ code: `
152
+ export default abstract class BaseHandler {
153
+ abstract handle(): void;
154
+ }
155
+ `,
156
+ output: `
157
+ export abstract class BaseHandler {
158
+ abstract handle(): void;
159
+ }
160
+ `,
161
+ errors: [
162
+ {
163
+ messageId: "noDefaultClassDeclaration",
164
+ },
165
+ ],
166
+ },
167
+ // Invalid: Default generic class
168
+ {
169
+ code: `
170
+ export default class Repository<T> {
171
+ private items: T[] = [];
172
+ save(item: T): void {
173
+ this.items.push(item);
174
+ }
175
+ }
176
+ `,
177
+ output: `
178
+ export class Repository<T> {
179
+ private items: T[] = [];
180
+ save(item: T): void {
181
+ this.items.push(item);
182
+ }
183
+ }
184
+ `,
185
+ errors: [
186
+ {
187
+ messageId: "noDefaultClassDeclaration",
188
+ },
189
+ ],
190
+ },
191
+ // Invalid: Default class with decorators
192
+ {
193
+ code: `
194
+ @Injectable()
195
+ export default class ApiService {
196
+ constructor() {}
197
+ }
198
+ `,
199
+ output: `
200
+ @Injectable()
201
+ export class ApiService {
202
+ constructor() {}
203
+ }
204
+ `,
205
+ errors: [
206
+ {
207
+ messageId: "noDefaultClassDeclaration",
208
+ },
209
+ ],
210
+ },
211
+ // Invalid: Default class extending another class
212
+ {
213
+ code: `
214
+ export default class ConcreteService extends BaseService {
215
+ initialize(): void {}
216
+ }
217
+ `,
218
+ output: `
219
+ export class ConcreteService extends BaseService {
220
+ initialize(): void {}
221
+ }
222
+ `,
223
+ errors: [
224
+ {
225
+ messageId: "noDefaultClassDeclaration",
226
+ },
227
+ ],
228
+ },
229
+ // Invalid: Default class implementing interface
230
+ {
231
+ code: `
232
+ export default class UserService implements IUserService {
233
+ getUsers(): User[] {
234
+ return [];
235
+ }
236
+ }
237
+ `,
238
+ output: `
239
+ export class UserService implements IUserService {
240
+ getUsers(): User[] {
241
+ return [];
242
+ }
243
+ }
244
+ `,
245
+ errors: [
246
+ {
247
+ messageId: "noDefaultClassDeclaration",
248
+ },
249
+ ],
250
+ },
251
+ // Invalid: Default class with static methods
252
+ {
253
+ code: `
254
+ export default class Utils {
255
+ static format(text: string): string {
256
+ return text.trim();
257
+ }
258
+ }
259
+ `,
260
+ output: `
261
+ export class Utils {
262
+ static format(text: string): string {
263
+ return text.trim();
264
+ }
265
+ }
266
+ `,
267
+ errors: [
268
+ {
269
+ messageId: "noDefaultClassDeclaration",
270
+ },
271
+ ],
272
+ },
273
+ // Invalid: Named class expression (edge case)
274
+ {
275
+ code: `
276
+ export default class NamedClass {
277
+ test() {
278
+ return 'test';
279
+ }
280
+ }
281
+ `,
282
+ output: `
283
+ export class NamedClass {
284
+ test() {
285
+ return 'test';
286
+ }
287
+ }
288
+ `,
289
+ errors: [
290
+ {
291
+ messageId: "noDefaultClassDeclaration",
292
+ },
293
+ ],
294
+ },
295
+ ],
296
+ });
297
+
298
+ console.log("✅ All no-default-class-export rule tests passed!");
299
+
300
+ // Export for potential use in other test files
301
+ export { noDefaultClassExportRule };
@@ -1,3 +1,3 @@
1
1
  export const groupExportsConfig = {
2
- "import/group-exports": "error", // Enforce consolidating exports into single statements
3
- };
2
+ "import/group-exports": "off",
3
+ };