eslint-config-setup 0.3.2 → 0.4.0

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/dist/modules.js CHANGED
@@ -1,594 +1,81 @@
1
1
  // src/configs/ai.ts
2
- function aiConfig(opts) {
3
- const isReact = opts?.react ?? false;
4
- const configs = [
5
- {
6
- name: "eslint-config-setup/ai-structural",
7
- rules: {
8
- // ── A. Structural clarity — always explicit, never ambiguous ──
9
- // Require braces for ALL control flow — no ambiguous one-liners
10
- // https://eslint.org/docs/latest/rules/curly
11
- curly: ["error", "all"],
12
- // Disallow else after return — flattens control flow
13
- // https://eslint.org/docs/latest/rules/no-else-return
14
- "no-else-return": ["error", { allowElseIf: false }],
15
- // No nested ternaries — unreadable, use if/else or early return
16
- // https://eslint.org/docs/latest/rules/no-nested-ternary
17
- "no-nested-ternary": "error",
18
- // No ternary when simpler alternatives exist (e.g., || or ??)
19
- // https://eslint.org/docs/latest/rules/no-unneeded-ternary
20
- "no-unneeded-ternary": "error",
21
- // No negated conditions in if/else — flip the branches instead
22
- // https://eslint.org/docs/latest/rules/no-negated-condition
23
- "no-negated-condition": "error",
24
- // No standalone if in else — use else-if instead
25
- // https://eslint.org/docs/latest/rules/no-lonely-if
26
- "no-lonely-if": "error",
27
- // No parameter reassignment — prevents subtle mutation bugs
28
- // https://eslint.org/docs/latest/rules/no-param-reassign
29
- "no-param-reassign": ["error", { props: true }],
30
- // One assignment per statement — prevents `a = b = c` chains
31
- // https://eslint.org/docs/latest/rules/no-multi-assign
32
- "no-multi-assign": "error",
33
- // One variable per declaration — clear and grep-friendly
34
- // https://eslint.org/docs/latest/rules/one-var
35
- "one-var": ["error", "never"],
36
- // No implicit type coercion (!!x, +x, "" + x) — use explicit Boolean/Number/String
37
- // https://eslint.org/docs/latest/rules/no-implicit-coercion
38
- "no-implicit-coercion": "error",
39
- // Prefer const for all bindings in destructuring when possible
40
- // https://eslint.org/docs/latest/rules/prefer-const
41
- "prefer-const": ["error", { destructuring: "all" }],
42
- // Disallow var — block-scoped let/const only
43
- // https://eslint.org/docs/latest/rules/no-var
44
- "no-var": "error",
45
- // Require strict equality (===) — no type coercion
46
- // https://eslint.org/docs/latest/rules/eqeqeq
47
- eqeqeq: "error",
48
- // Prefer template literals over concatenation — more readable
49
- // https://eslint.org/docs/latest/rules/prefer-template
50
- "prefer-template": "error",
51
- // Require shorthand properties in objects — concise, skip quoted keys
52
- // https://eslint.org/docs/latest/rules/object-shorthand
53
- "object-shorthand": ["error", "always", { avoidQuotes: true }],
54
- // Prefer concise arrow body: `() => expr` over `() => { return expr }`
55
- // https://eslint.org/docs/latest/rules/arrow-body-style
56
- "arrow-body-style": "error",
57
- // Prefer arrow functions for callbacks — lexical `this`
58
- // https://eslint.org/docs/latest/rules/prefer-arrow-callback
59
- "prefer-arrow-callback": ["error", { allowNamedFunctions: true }],
60
- // Prefer `x ??= y` over `x = x ?? y` — concise null-coalescing assignment
61
- // https://eslint.org/docs/latest/rules/logical-assignment-operators
62
- "logical-assignment-operators": [
63
- "error",
64
- "always",
65
- { enforceForIfStatements: true }
66
- ],
67
- // No assignment in return statements — separate mutation from return
68
- // https://eslint.org/docs/latest/rules/no-return-assign
69
- "no-return-assign": ["error", "always"],
70
- // One statement per line — scannable, diff-friendly
71
- // https://eslint.org/docs/latest/rules/max-statements-per-line
72
- "max-statements-per-line": ["error", { max: 1 }],
73
- // Prefer `x ** 2` over `Math.pow(x, 2)` — modern operator syntax
74
- // https://eslint.org/docs/latest/rules/prefer-exponentiation-operator
75
- "prefer-exponentiation-operator": "error",
76
- // Require named capture groups in regex — self-documenting patterns
77
- // https://eslint.org/docs/latest/rules/prefer-named-capture-group
78
- "prefer-named-capture-group": "error",
79
- // Require Unicode-aware regex (`u` or `v` flag) — correct string handling
80
- // https://eslint.org/docs/latest/rules/require-unicode-regexp
81
- "require-unicode-regexp": "error",
82
- // ── B. Magic numbers & constants — no unexplained code ────────
83
- // No magic numbers — extract to named constants (allows -1, 0, 1, 2)
84
- // https://typescript-eslint.io/rules/no-magic-numbers
85
- "@typescript-eslint/no-magic-numbers": [
86
- "error",
87
- {
88
- ignore: [-1, 0, 1, 2],
89
- ignoreArrayIndexes: true,
90
- ignoreDefaultValues: true,
91
- enforceConst: true,
92
- ignoreClassFieldInitialValues: true,
93
- ignoreEnums: true,
94
- ignoreNumericLiteralTypes: true,
95
- ignoreReadonlyClassProperties: true,
96
- ignoreTypeIndexes: true
97
- }
98
- ],
99
- // No duplicate strings (threshold 3) — extract to constant
100
- // https://sonarsource.github.io/rspec/#/rspec/S1192/javascript
101
- "sonarjs/no-duplicate-string": ["error", { threshold: 3 }],
102
- // Flag TODO/FIXME/HACK comments — technical debt tracker
103
- // https://eslint.org/docs/latest/rules/no-warning-comments
104
- "no-warning-comments": "warn",
105
- // ── F. Async/Promise hygiene ──────────────────────────────────
106
- // No await inside loops — use Promise.all() for parallel execution
107
- // https://eslint.org/docs/latest/rules/no-await-in-loop
108
- "no-await-in-loop": "error",
109
- // Disallow returning values from Promise executors — use resolve/reject
110
- // https://eslint.org/docs/latest/rules/no-promise-executor-return
111
- "no-promise-executor-return": "error",
112
- // Every Promise must be awaited, returned, or voided — prevents silent failures
113
- // https://typescript-eslint.io/rules/no-floating-promises
114
- "@typescript-eslint/no-floating-promises": [
115
- "error",
116
- { checkThenables: true, ignoreVoid: true }
117
- ]
118
- }
119
- },
120
- {
121
- name: "eslint-config-setup/ai-typescript",
122
- rules: {
123
- // ── C. TypeScript strictness — explicit types, safe patterns ──
124
- // Require explicit return types — self-documenting function signatures
125
- // https://typescript-eslint.io/rules/explicit-function-return-type
126
- "@typescript-eslint/explicit-function-return-type": [
127
- "error",
128
- {
129
- allowExpressions: true,
130
- allowTypedFunctionExpressions: true,
131
- allowHigherOrderFunctions: true,
132
- allowIIFEs: true
133
- }
134
- ],
135
- // Enforce consistent naming: camelCase for values, PascalCase for types,
136
- // is/has/can/should/will/did prefix for boolean variables
137
- // https://typescript-eslint.io/rules/naming-convention
138
- "@typescript-eslint/naming-convention": [
139
- "error",
140
- {
141
- selector: "variable",
142
- format: isReact ? ["strictCamelCase", "UPPER_CASE", "StrictPascalCase"] : ["strictCamelCase", "UPPER_CASE"],
143
- leadingUnderscore: "allowSingleOrDouble",
144
- trailingUnderscore: "allow",
145
- filter: { regex: "[- ]", match: false }
146
- },
147
- {
148
- selector: "function",
149
- format: isReact ? ["strictCamelCase", "StrictPascalCase"] : ["strictCamelCase"]
150
- },
151
- {
152
- selector: "parameter",
153
- format: ["strictCamelCase"],
154
- leadingUnderscore: "allow"
155
- },
156
- {
157
- selector: "import",
158
- format: ["strictCamelCase", "StrictPascalCase", "UPPER_CASE"]
159
- },
160
- {
161
- selector: [
162
- "classProperty",
163
- "parameterProperty",
164
- "classMethod",
165
- "objectLiteralMethod",
166
- "typeMethod",
167
- "accessor"
168
- ],
169
- format: ["strictCamelCase"],
170
- leadingUnderscore: "allowSingleOrDouble",
171
- trailingUnderscore: "allow",
172
- filter: { regex: "[- ]", match: false }
173
- },
174
- {
175
- selector: ["objectLiteralProperty", "typeProperty"],
176
- format: null
177
- },
178
- {
179
- selector: "typeLike",
180
- format: ["StrictPascalCase"]
181
- },
182
- {
183
- // No "I" prefix on interfaces — use descriptive names (XO convention)
184
- selector: "interface",
185
- format: ["StrictPascalCase"],
186
- custom: { regex: "^I[A-Z]", match: false }
187
- },
188
- {
189
- // Type parameters: single uppercase letter (T) or PascalCase (TResult)
190
- selector: "typeParameter",
191
- format: ["PascalCase"],
192
- custom: { regex: "^(T([A-Z][a-zA-Z]*)?|[A-Z])$", match: true }
193
- },
194
- {
195
- selector: "variable",
196
- types: ["boolean"],
197
- format: ["StrictPascalCase"],
198
- prefix: ["is", "has", "can", "should", "will", "did"]
199
- },
200
- {
201
- selector: ["classProperty", "objectLiteralProperty"],
202
- format: null,
203
- modifiers: ["requiresQuotes"]
204
- }
205
- ],
206
- // Enforce `import type { T }` — types are erased at compile time
207
- // https://typescript-eslint.io/rules/consistent-type-imports
208
- "@typescript-eslint/consistent-type-imports": [
209
- "error",
210
- { fixStyle: "inline-type-imports" }
211
- ],
212
- // Enforce `export type { T }` — matches import convention
213
- // https://typescript-eslint.io/rules/consistent-type-exports
214
- "@typescript-eslint/consistent-type-exports": [
215
- "error",
216
- { fixMixedExportsWithInlineTypeSpecifier: true }
217
- ],
218
- // Disallow `any` type — auto-fix to `unknown` for type safety
219
- // https://typescript-eslint.io/rules/no-explicit-any
220
- "@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
221
- // Prefer readonly for unmodified class properties — signals immutability
222
- // https://typescript-eslint.io/rules/prefer-readonly
223
- "@typescript-eslint/prefer-readonly": "error",
224
- // Functions returning promises must be async — consistent async patterns
225
- // https://typescript-eslint.io/rules/promise-function-async
226
- "@typescript-eslint/promise-function-async": "error",
227
- // Exhaustive switch statements — no missing cases, no unnecessary defaults
228
- // https://typescript-eslint.io/rules/switch-exhaustiveness-check
229
- "@typescript-eslint/switch-exhaustiveness-check": [
230
- "error",
231
- {
232
- allowDefaultCaseForExhaustiveSwitch: false,
233
- requireDefaultForNonUnion: true
234
- }
235
- ],
236
- // Disallow unsafe type assertions (as Type) — use type guards instead
237
- // https://typescript-eslint.io/rules/no-unsafe-type-assertion
238
- "@typescript-eslint/no-unsafe-type-assertion": "error",
239
- // Require comparator for Array.sort() — prevents locale-dependent string sort
240
- // https://typescript-eslint.io/rules/require-array-sort-compare
241
- "@typescript-eslint/require-array-sort-compare": [
242
- "error",
243
- { ignoreStringArrays: true }
244
- ],
245
- // Require explicit `public`/`private`/`protected` on class members
246
- // https://typescript-eslint.io/rules/explicit-member-accessibility
247
- "@typescript-eslint/explicit-member-accessibility": "error",
248
- // Enforce property style for method signatures — prevents bivariance issues
249
- // https://typescript-eslint.io/rules/method-signature-style
250
- "@typescript-eslint/method-signature-style": ["error", "property"],
251
- // Require explicit values for enum members — prevents accidental shifts on reorder
252
- // https://typescript-eslint.io/rules/prefer-enum-initializers
253
- "@typescript-eslint/prefer-enum-initializers": "error",
254
- // Prefer `type` over `interface` — consistent, supports unions/intersections
255
- // https://typescript-eslint.io/rules/consistent-type-definitions
256
- "@typescript-eslint/consistent-type-definitions": ["error", "type"],
257
- // Enforce consistent member ordering in classes and interfaces
258
- // Static → fields by visibility → constructors → methods by visibility (XO convention)
259
- // https://typescript-eslint.io/rules/member-ordering
260
- "@typescript-eslint/member-ordering": [
261
- "warn",
262
- {
263
- default: [
264
- // Index signature
265
- "signature",
266
- "call-signature",
267
- // Static
268
- "public-static-field",
269
- "protected-static-field",
270
- "private-static-field",
271
- "#private-static-field",
272
- "static-field",
273
- "public-static-method",
274
- "protected-static-method",
275
- "private-static-method",
276
- "#private-static-method",
277
- "static-method",
278
- // Fields
279
- "public-decorated-field",
280
- "protected-decorated-field",
281
- "private-decorated-field",
282
- "public-instance-field",
283
- "protected-instance-field",
284
- "private-instance-field",
285
- "#private-instance-field",
286
- "public-abstract-field",
287
- "protected-abstract-field",
288
- "field",
289
- // Constructors
290
- "public-constructor",
291
- "protected-constructor",
292
- "private-constructor",
293
- "constructor",
294
- // Getters/Setters
295
- ["public-get", "public-set"],
296
- ["protected-get", "protected-set"],
297
- ["private-get", "private-set"],
298
- ["#private-get", "#private-set"],
299
- // Methods
300
- "public-decorated-method",
301
- "protected-decorated-method",
302
- "private-decorated-method",
303
- "public-instance-method",
304
- "protected-instance-method",
305
- "private-instance-method",
306
- "#private-instance-method",
307
- "public-abstract-method",
308
- "protected-abstract-method",
309
- "method"
310
- ]
311
- }
312
- ]
313
- }
314
- },
315
- {
316
- name: "eslint-config-setup/ai-unicorn",
317
- rules: {
318
- // ── D. Unicorn — modern, idiomatic patterns ───────────────────
319
- // Move functions to the smallest scope where they're used
320
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-function-scoping.md
321
- "unicorn/consistent-function-scoping": "error",
322
- // Forbid blanket `/* eslint-disable */` — must specify rules
323
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-abusive-eslint-disable.md
324
- "unicorn/no-abusive-eslint-disable": "error",
325
- // No .forEach() — use for-of loop (breakable, async-safe)
326
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-for-each.md
327
- "unicorn/no-array-for-each": "error",
328
- // No .reduce() — explicit loops are more readable
329
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-reduce.md
330
- "unicorn/no-array-reduce": "error",
331
- // Prefer simple ternary over if/else for single-line assignments
332
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-ternary.md
333
- "unicorn/prefer-ternary": ["error", "only-single-line"],
334
- // Prefer switch for 3+ conditions on same variable — structured
335
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-switch.md
336
- "unicorn/prefer-switch": ["error", { minimumCases: 3 }],
337
- // Enforce camelCase or PascalCase filenames — consistent project structure
338
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
339
- "unicorn/filename-case": [
340
- "error",
341
- { cases: { camelCase: true, pascalCase: true } }
342
- ],
343
- // Prevent abbreviated variable names (e → error, btn → button) — readable
344
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prevent-abbreviations.md
345
- "unicorn/prevent-abbreviations": "error",
346
- // Detect useless switch cases that fall through to the next case
347
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-switch-case.md
348
- "unicorn/no-useless-switch-case": "error",
349
- // Enforce correct Error subclassing (name, constructor pattern)
350
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/custom-error-definition.md
351
- "unicorn/custom-error-definition": "error",
352
- // Prefer default parameters over manual reassignment
353
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-default-parameters.md
354
- "unicorn/prefer-default-parameters": "error",
355
- // Prefer `a || b` over `a ? a : b` — simpler when equivalent
356
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-logical-operator-over-ternary.md
357
- "unicorn/prefer-logical-operator-over-ternary": "error",
358
- // Prefer Math.min()/Math.max() over ternaries for clamping
359
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-math-min-max.md
360
- "unicorn/prefer-math-min-max": "error",
361
- // Prefer Set#size over converting to array — direct and correct
362
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-set-size.md
363
- "unicorn/prefer-set-size": "error",
364
- // Enforce explicit `.length > 0` / `.length === 0` checks
365
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/explicit-length-check.md
366
- "unicorn/explicit-length-check": "error",
367
- // Prefer for-of over C-style for loops — no off-by-one risk
368
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-for-loop.md
369
- "unicorn/no-for-loop": "error",
370
- // Enforce braces in switch cases — prevents scope leakage
371
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/switch-case-braces.md
372
- "unicorn/switch-case-braces": "error",
373
- // Combine multiple .push() calls into one — cleaner
374
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-push-push.md
375
- "unicorn/no-array-push-push": "error"
376
- }
377
- },
378
- {
379
- name: "eslint-config-setup/ai-sonarjs",
380
- rules: {
381
- // ── E. SonarJS — code quality and duplicates ──────────────────
382
- // Detect copy-pasted functions — extract to shared helper
383
- // https://sonarsource.github.io/rspec/#/rspec/S4144/javascript
384
- "sonarjs/no-identical-functions": "error",
385
- // Merge nested if-statements that can be combined — reduce nesting
386
- // https://sonarsource.github.io/rspec/#/rspec/S1066/javascript
387
- "sonarjs/no-collapsible-if": "error",
388
- // Simplify redundant boolean expressions
389
- // https://sonarsource.github.io/rspec/#/rspec/S1125/javascript
390
- "sonarjs/no-redundant-boolean": "error",
391
- // Detect collections that are populated but never read — dead code
392
- // https://sonarsource.github.io/rspec/#/rspec/S4030/javascript
393
- "sonarjs/no-unused-collection": "error",
394
- // Return value directly instead of storing in temp variable
395
- // https://sonarsource.github.io/rspec/#/rspec/S1488/javascript
396
- "sonarjs/prefer-immediate-return": "error",
397
- // Simplify boolean return patterns
398
- // https://sonarsource.github.io/rspec/#/rspec/S1126/javascript
399
- "sonarjs/prefer-single-boolean-return": "error",
400
- // Detect identical sub-expressions on both sides of operator
401
- // https://sonarsource.github.io/rspec/#/rspec/S1764/javascript
402
- "sonarjs/no-identical-expressions": "error",
403
- // Simplify negated boolean checks — `!a !== b` → `a === b`
404
- // https://sonarsource.github.io/rspec/#/rspec/S1940/javascript
405
- "sonarjs/no-inverted-boolean-check": "error",
406
- // Disallow nested switch statements — extract to function
407
- // https://sonarsource.github.io/rspec/#/rspec/S1821/javascript
408
- "sonarjs/no-nested-switch": "error",
409
- // Disallow nested template literals — unreadable
410
- // https://sonarsource.github.io/rspec/#/rspec/S4624/javascript
411
- "sonarjs/no-nested-template-literals": "error",
412
- // Limit union type size — too many members signals missing abstraction
413
- // https://sonarsource.github.io/rspec/#/rspec/S4622/javascript
414
- "sonarjs/max-union-size": ["error", { threshold: 5 }],
415
- // Prefer type predicates for type narrowing — safer than assertions
416
- // https://sonarsource.github.io/rspec/#/rspec/S4322/javascript
417
- "sonarjs/prefer-type-guard": "error",
418
- // Public static fields should be readonly — prevents accidental mutation
419
- // https://sonarsource.github.io/rspec/#/rspec/S1444/javascript
420
- "sonarjs/public-static-readonly": "error"
421
- }
422
- },
423
- {
424
- name: "eslint-config-setup/ai-regexp",
425
- rules: {
426
- // Prefer lookarounds over capturing groups used only for context
427
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-lookaround.html
428
- "regexp/prefer-lookaround": "error",
429
- // Prefer named backreferences — \k<quote> over \1
430
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-backreference.html
431
- "regexp/prefer-named-backreference": "error",
432
- // Prefer named replacement — $<name> over $1
433
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-replacement.html
434
- "regexp/prefer-named-replacement": "error",
435
- // Prefer quantifier shorthand — a{3} over aaa
436
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-quantifier.html
437
- "regexp/prefer-quantifier": "error",
438
- // Prefer match.groups.name over match[1] — consistent named groups usage
439
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-result-array-groups.html
440
- "regexp/prefer-result-array-groups": "error",
441
- // Require v flag (unicode sets) over u flag — stricter ES2024 superset
442
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-sets-regexp.html
443
- "regexp/require-unicode-sets-regexp": "error"
444
- }
445
- },
446
- {
447
- name: "eslint-config-setup/ai-jsdoc",
448
- rules: {
449
- // Prevent comments that just repeat the name — `/** The name */ name: string`
450
- // https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/informative-docs.md
451
- "jsdoc/informative-docs": "error",
452
- // AI should document parameters and return values completely
453
- "jsdoc/require-param": "error",
454
- "jsdoc/require-returns": "error"
455
- }
456
- },
457
- {
458
- name: "eslint-config-setup/ai-node",
459
- rules: {
460
- // Warn when using Node.js builtins not available in the target version
461
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unsupported-features/node-builtins.md
462
- "node/no-unsupported-features/node-builtins": "error"
463
- }
464
- },
465
- {
466
- name: "eslint-config-setup/ai-react",
467
- rules: {
468
- // No click handlers on static elements without role — use semantic HTML
469
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md
470
- "jsx-a11y/no-static-element-interactions": "error",
471
- // No event handlers on non-interactive elements — use button/link instead
472
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-noninteractive-element-interactions.md
473
- "jsx-a11y/no-noninteractive-element-interactions": "error",
474
- // Interactive elements (role="button") must be focusable
475
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/interactive-supports-focus.md
476
- "jsx-a11y/interactive-supports-focus": "error"
477
- }
478
- },
479
- {
480
- name: "eslint-config-setup/ai-complexity",
481
- rules: {
482
- // Cyclomatic complexity limit — max branches per function
483
- // https://eslint.org/docs/latest/rules/complexity
484
- complexity: ["error", 10],
485
- // Max nesting depth — deep nesting signals need for extraction
486
- // https://eslint.org/docs/latest/rules/max-depth
487
- "max-depth": ["error", 3],
488
- // Max nested callbacks — prevents callback hell
489
- // https://eslint.org/docs/latest/rules/max-nested-callbacks
490
- "max-nested-callbacks": ["error", 2],
491
- // Max function parameters — many params suggest a config object
492
- // https://eslint.org/docs/latest/rules/max-params
493
- "max-params": ["error", 3],
494
- // Max statements per function — keeps functions focused
495
- // https://eslint.org/docs/latest/rules/max-statements
496
- "max-statements": ["error", 15],
497
- // Max lines per function — encourages extraction of helpers
498
- // https://eslint.org/docs/latest/rules/max-lines-per-function
499
- "max-lines-per-function": [
500
- "error",
501
- {
502
- max: 50,
503
- skipBlankLines: true,
504
- skipComments: true
505
- }
506
- ],
507
- // Max lines per file — encourages modular file organization
508
- // https://eslint.org/docs/latest/rules/max-lines
509
- "max-lines": [
510
- "error",
511
- {
512
- max: 300,
513
- skipBlankLines: true,
514
- skipComments: true
515
- }
516
- ],
517
- // Cognitive complexity — measures how hard a function is to understand
518
- // https://sonarsource.github.io/rspec/#/rspec/S3776/javascript
519
- "sonarjs/cognitive-complexity": ["error", 10]
520
- }
521
- },
522
- {
523
- name: "eslint-config-setup/ai-tests-strict",
524
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
525
- rules: {
526
- // Every test must be inside a describe block — organized test suites
527
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/require-top-level-describe.md
528
- "vitest/require-top-level-describe": "error",
529
- // Hooks (beforeEach, afterEach) must be at the top of describe — predictable setup
530
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-hooks-on-top.md
531
- "vitest/prefer-hooks-on-top": "error"
532
- }
533
- },
534
- {
535
- name: "eslint-config-setup/ai-tests-relaxed",
536
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
537
- rules: {
538
- "max-lines": "off",
539
- "max-lines-per-function": "off",
540
- "max-statements": "off",
541
- "max-nested-callbacks": "off",
542
- "@typescript-eslint/no-magic-numbers": "off",
543
- "sonarjs/no-duplicate-string": "off",
544
- "@typescript-eslint/explicit-function-return-type": "off",
545
- "@typescript-eslint/naming-convention": "off",
546
- "unicorn/prevent-abbreviations": "off"
547
- }
548
- },
549
- {
550
- name: "eslint-config-setup/ai-e2e-relaxed",
551
- files: ["**/*.spec.ts"],
552
- rules: {
553
- "max-lines": "off",
554
- "max-lines-per-function": "off",
555
- "max-statements": "off",
556
- "@typescript-eslint/no-magic-numbers": "off",
557
- "@typescript-eslint/explicit-function-return-type": "off"
558
- }
559
- },
560
- {
561
- name: "eslint-config-setup/ai-config-relaxed",
562
- files: [
563
- "**/*.config.{ts,mts,cts,js,mjs,cjs}",
564
- "**/vite.config.*",
565
- "**/vitest.config.*",
566
- "**/next.config.*"
567
- ],
568
- rules: {
569
- complexity: "off",
570
- "max-lines": "off",
571
- "max-lines-per-function": "off",
572
- "max-statements": "off",
573
- "@typescript-eslint/no-magic-numbers": "off",
574
- "@typescript-eslint/explicit-function-return-type": "off",
575
- "@typescript-eslint/naming-convention": "off"
576
- }
577
- },
578
- {
579
- name: "eslint-config-setup/ai-declarations-relaxed",
580
- files: ["**/*.d.ts"],
581
- rules: {
582
- "@typescript-eslint/explicit-function-return-type": "off",
583
- "@typescript-eslint/naming-convention": "off",
584
- "@typescript-eslint/no-explicit-any": "off",
585
- "@typescript-eslint/no-magic-numbers": "off",
586
- "unicorn/prevent-abbreviations": "off",
587
- "unicorn/filename-case": "off"
588
- }
589
- }
2
+ var TEST_FILES = ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"];
3
+ var E2E_FILES = ["**/*.spec.ts"];
4
+ var CONFIG_FILES = [
5
+ "**/*.config.{ts,mts,cts,js,mjs,cjs}",
6
+ "**/vite.config.*",
7
+ "**/vitest.config.*",
8
+ "**/next.config.*"
9
+ ];
10
+ var AI_TESTS_STRICT_CONFIG = {
11
+ name: "eslint-config-setup/ai-tests-strict",
12
+ files: TEST_FILES,
13
+ rules: {
14
+ // Every test must be inside a describe block — organized test suites
15
+ "vitest/require-top-level-describe": "error",
16
+ // Hooks (beforeEach, afterEach) must be at the top of describe — predictable setup
17
+ "vitest/prefer-hooks-on-top": "error"
18
+ }
19
+ };
20
+ var AI_TESTS_RELAXED_CONFIG = {
21
+ name: "eslint-config-setup/ai-tests-relaxed",
22
+ files: TEST_FILES,
23
+ rules: {
24
+ "max-lines": "off",
25
+ "max-lines-per-function": "off",
26
+ "max-statements": "off",
27
+ "max-nested-callbacks": "off",
28
+ "@typescript-eslint/no-magic-numbers": "off",
29
+ "sonarjs/no-duplicate-string": "off",
30
+ "@typescript-eslint/explicit-function-return-type": "off",
31
+ "@typescript-eslint/naming-convention": "off",
32
+ "unicorn/prevent-abbreviations": "off"
33
+ }
34
+ };
35
+ var AI_E2E_RELAXED_CONFIG = {
36
+ name: "eslint-config-setup/ai-e2e-relaxed",
37
+ files: E2E_FILES,
38
+ rules: {
39
+ "max-lines": "off",
40
+ "max-lines-per-function": "off",
41
+ "max-statements": "off",
42
+ "@typescript-eslint/no-magic-numbers": "off",
43
+ "@typescript-eslint/explicit-function-return-type": "off"
44
+ }
45
+ };
46
+ var AI_CONFIG_RELAXED_CONFIG = {
47
+ name: "eslint-config-setup/ai-config-relaxed",
48
+ files: CONFIG_FILES,
49
+ rules: {
50
+ complexity: "off",
51
+ "max-lines": "off",
52
+ "max-lines-per-function": "off",
53
+ "max-statements": "off",
54
+ "@typescript-eslint/no-magic-numbers": "off",
55
+ "@typescript-eslint/explicit-function-return-type": "off",
56
+ "@typescript-eslint/naming-convention": "off"
57
+ }
58
+ };
59
+ var AI_DECLARATIONS_RELAXED_CONFIG = {
60
+ name: "eslint-config-setup/ai-declarations-relaxed",
61
+ files: ["**/*.d.ts"],
62
+ rules: {
63
+ "@typescript-eslint/explicit-function-return-type": "off",
64
+ "@typescript-eslint/naming-convention": "off",
65
+ "@typescript-eslint/no-explicit-any": "off",
66
+ "@typescript-eslint/no-magic-numbers": "off",
67
+ "unicorn/prevent-abbreviations": "off",
68
+ "unicorn/filename-case": "off"
69
+ }
70
+ };
71
+ function aiConfig() {
72
+ return [
73
+ AI_TESTS_STRICT_CONFIG,
74
+ AI_TESTS_RELAXED_CONFIG,
75
+ AI_E2E_RELAXED_CONFIG,
76
+ AI_CONFIG_RELAXED_CONFIG,
77
+ AI_DECLARATIONS_RELAXED_CONFIG
590
78
  ];
591
- return configs;
592
79
  }
593
80
 
594
81
  // src/configs/base.ts
@@ -596,7 +83,7 @@ import eslint from "@eslint/js";
596
83
 
597
84
  // src/build/config-builder.ts
598
85
  function createConfig(options) {
599
- const presetRules = /* @__PURE__ */ new Map();
86
+ const presetRules2 = /* @__PURE__ */ new Map();
600
87
  const presetPlugins = {};
601
88
  if (options.presets) {
602
89
  for (const preset of options.presets) {
@@ -606,7 +93,7 @@ function createConfig(options) {
606
93
  if (preset.rules) {
607
94
  for (const [name, value] of Object.entries(preset.rules)) {
608
95
  if (value !== void 0) {
609
- presetRules.set(name, value);
96
+ presetRules2.set(name, value);
610
97
  }
611
98
  }
612
99
  }
@@ -619,7 +106,7 @@ function createConfig(options) {
619
106
  const fileOverrides = [];
620
107
  const builder = {
621
108
  overrideRule(name, value) {
622
- if (!presetRules.has(name)) {
109
+ if (!presetRules2.has(name)) {
623
110
  throw new Error(
624
111
  `overrideRule("${name}"): rule not found in preset. Cannot override a rule that doesn't exist in the preset.`
625
112
  );
@@ -627,8 +114,34 @@ function createConfig(options) {
627
114
  overrides.set(name, value);
628
115
  return builder;
629
116
  },
117
+ overrideSeverity(name, severity) {
118
+ if (!presetRules2.has(name)) {
119
+ throw new Error(
120
+ `overrideSeverity("${name}"): rule not found in preset. Cannot override severity of a rule that doesn't exist in the preset.`
121
+ );
122
+ }
123
+ const existing = presetRules2.get(name);
124
+ if (Array.isArray(existing)) {
125
+ const [, ...restOptions] = existing;
126
+ overrides.set(name, [severity, ...restOptions]);
127
+ } else {
128
+ overrides.set(name, severity);
129
+ }
130
+ return builder;
131
+ },
132
+ overrideOptions(name, ...ruleOptions) {
133
+ if (!presetRules2.has(name)) {
134
+ throw new Error(
135
+ `overrideOptions("${name}"): rule not found in preset. Cannot override options of a rule that doesn't exist in the preset.`
136
+ );
137
+ }
138
+ const existing = presetRules2.get(name);
139
+ const severity = Array.isArray(existing) ? existing[0] : existing;
140
+ overrides.set(name, [severity, ...ruleOptions]);
141
+ return builder;
142
+ },
630
143
  addRule(name, value) {
631
- if (presetRules.has(name)) {
144
+ if (presetRules2.has(name)) {
632
145
  throw new Error(
633
146
  `addRule("${name}"): rule already exists in preset. Use overrideRule() to change its value, or removeRule() to drop it.`
634
147
  );
@@ -642,7 +155,7 @@ function createConfig(options) {
642
155
  return builder;
643
156
  },
644
157
  disableRule(name) {
645
- if (!presetRules.has(name) && !additions.has(name)) {
158
+ if (!presetRules2.has(name) && !additions.has(name)) {
646
159
  throw new Error(
647
160
  `disableRule("${name}"): rule not found in preset or additions. Cannot disable a rule that doesn't exist.`
648
161
  );
@@ -651,7 +164,7 @@ function createConfig(options) {
651
164
  return builder;
652
165
  },
653
166
  removeRule(name) {
654
- if (!presetRules.has(name) && !additions.has(name)) {
167
+ if (!presetRules2.has(name) && !additions.has(name)) {
655
168
  throw new Error(
656
169
  `removeRule("${name}"): rule not found in preset or additions. Cannot remove a rule that doesn't exist.`
657
170
  );
@@ -665,7 +178,7 @@ function createConfig(options) {
665
178
  },
666
179
  build() {
667
180
  const rules = {};
668
- for (const [name, value] of presetRules) {
181
+ for (const [name, value] of presetRules2) {
669
182
  if (!removed.has(name)) {
670
183
  rules[name] = value;
671
184
  }
@@ -724,14 +237,147 @@ function createConfig(options) {
724
237
  }
725
238
 
726
239
  // src/configs/base.ts
727
- function baseConfig() {
728
- return createConfig({
240
+ var ERROR_PREVENTION_RULES = {
241
+ "accessor-pairs": ["error", { enforceForClassMembers: true }],
242
+ "array-callback-return": ["error", { allowImplicit: true }],
243
+ "no-constructor-return": "error",
244
+ "no-promise-executor-return": "error",
245
+ "no-self-compare": "error",
246
+ "no-template-curly-in-string": "error",
247
+ "no-unreachable-loop": "error",
248
+ "require-atomic-updates": "error",
249
+ "no-unmodified-loop-condition": "error",
250
+ "grouped-accessor-pairs": ["error", "getBeforeSet"],
251
+ "no-useless-rename": "error",
252
+ "no-useless-computed-key": ["error", { enforceForClassMembers: true }]
253
+ };
254
+ var DANGEROUS_PATTERN_RULES = {
255
+ "no-eval": "error",
256
+ "no-alert": "error",
257
+ "no-caller": "error",
258
+ "no-extend-native": "error",
259
+ "no-new-func": "error",
260
+ "no-new-wrappers": "error",
261
+ "no-object-constructor": "error",
262
+ "no-proto": "error",
263
+ "no-iterator": "error",
264
+ "no-script-url": "error",
265
+ "no-octal-escape": "error",
266
+ "no-implicit-globals": "error"
267
+ };
268
+ var CODE_QUALITY_RULES = {
269
+ eqeqeq: ["error", "smart"],
270
+ "guard-for-in": "error",
271
+ "default-case-last": "error",
272
+ radix: "error",
273
+ yoda: "error",
274
+ "no-sequences": ["error", { allowInParentheses: false }],
275
+ "no-new": "error",
276
+ "no-labels": "error",
277
+ "no-extra-bind": "error",
278
+ "no-lone-blocks": "error",
279
+ "no-useless-call": "error",
280
+ "no-useless-concat": "error",
281
+ "no-useless-return": "error",
282
+ "no-return-assign": ["error", "always"],
283
+ "no-multi-str": "error",
284
+ "prefer-regex-literals": ["error", { disallowRedundantWrapping: true }]
285
+ };
286
+ var MODERN_STYLE_RULES = {
287
+ "no-var": "error",
288
+ "prefer-const": ["error", { destructuring: "all" }],
289
+ "prefer-object-has-own": "error",
290
+ "prefer-object-spread": "error",
291
+ "prefer-rest-params": "error",
292
+ "prefer-spread": "error",
293
+ "symbol-description": "error",
294
+ "prefer-numeric-literals": "error",
295
+ "object-shorthand": ["error", "always", { avoidExplicitReturnArrows: true, avoidQuotes: true }]
296
+ };
297
+ var AI_STRUCTURAL_RULES = {
298
+ curly: ["error", "all"],
299
+ "no-else-return": ["error", { allowElseIf: false }],
300
+ "no-nested-ternary": "error",
301
+ "no-unneeded-ternary": "error",
302
+ "no-negated-condition": "error",
303
+ "no-lonely-if": "error",
304
+ "no-param-reassign": ["error", { props: true }],
305
+ "no-multi-assign": "error",
306
+ "one-var": ["error", "never"],
307
+ "no-implicit-coercion": "error",
308
+ "arrow-body-style": "error",
309
+ "prefer-arrow-callback": ["error", { allowNamedFunctions: true }],
310
+ "logical-assignment-operators": ["error", "always", { enforceForIfStatements: true }],
311
+ "max-statements-per-line": ["error", { max: 1 }],
312
+ "prefer-exponentiation-operator": "error",
313
+ "prefer-named-capture-group": "error",
314
+ "require-unicode-regexp": "error",
315
+ "no-warning-comments": "warn",
316
+ "no-await-in-loop": "error"
317
+ };
318
+ function addRules(builder, rules) {
319
+ for (const [ruleName, value] of Object.entries(rules)) {
320
+ builder.addRule(ruleName, value);
321
+ }
322
+ }
323
+ function addBaseOverrides(builder) {
324
+ builder.overrideRule("use-isnan", ["error", { enforceForIndexOf: true, enforceForSwitchCase: true }]);
325
+ builder.overrideRule("valid-typeof", ["error", { requireStringLiterals: true }]);
326
+ }
327
+ function addComplexityRules(builder, isAi) {
328
+ builder.addRule("complexity", ["error", isAi ? 10 : 20]);
329
+ builder.addRule("max-depth", ["error", isAi ? 3 : 5]);
330
+ builder.addRule("max-nested-callbacks", ["error", isAi ? 2 : 4]);
331
+ builder.addRule("max-params", ["error", isAi ? 3 : 5]);
332
+ builder.addRule("max-statements", ["error", isAi ? 15 : 25]);
333
+ builder.addRule("max-lines-per-function", [
334
+ "error",
335
+ { max: isAi ? 100 : 200, skipBlankLines: true, skipComments: true }
336
+ ]);
337
+ builder.addRule("max-lines", [
338
+ "error",
339
+ { max: isAi ? 300 : 500, skipBlankLines: true, skipComments: true }
340
+ ]);
341
+ builder.addRule("sonarjs/cognitive-complexity", ["error", isAi ? 10 : 20]);
342
+ }
343
+ function addModernStyleRules(builder, isAi) {
344
+ addRules(builder, MODERN_STYLE_RULES);
345
+ builder.addRule("prefer-template", isAi ? "error" : "warn");
346
+ }
347
+ function baseConfig(opts) {
348
+ const isAi = opts?.ai ?? false;
349
+ const builder = createConfig({
729
350
  name: "eslint-config-setup/base",
730
351
  presets: [eslint.configs.recommended]
731
- }).addRule("accessor-pairs", ["error", { enforceForClassMembers: true }]).addRule("array-callback-return", ["error", { allowImplicit: true }]).addRule("no-constructor-return", "error").addRule("no-promise-executor-return", "error").addRule("no-self-compare", "error").addRule("no-template-curly-in-string", "error").addRule("no-unreachable-loop", "error").addRule("require-atomic-updates", "error").addRule("no-unmodified-loop-condition", "error").addRule("grouped-accessor-pairs", ["error", "getBeforeSet"]).addRule("no-useless-rename", "error").addRule("no-useless-computed-key", "error").addRule("no-eval", "error").addRule("no-alert", "error").addRule("no-caller", "error").addRule("no-extend-native", "error").addRule("no-new-func", "error").addRule("no-new-wrappers", "error").addRule("no-object-constructor", "error").addRule("no-proto", "error").addRule("no-iterator", "error").addRule("no-script-url", "error").addRule("no-octal-escape", "error").addRule("eqeqeq", ["error", "smart"]).addRule("guard-for-in", "error").addRule("default-case-last", "error").addRule("radix", "error").addRule("yoda", "error").addRule("no-sequences", "error").addRule("no-new", "error").addRule("no-labels", "error").addRule("no-extra-bind", "error").addRule("no-lone-blocks", "error").addRule("no-useless-call", "error").addRule("no-useless-concat", "error").addRule("no-useless-return", "error").addRule("no-multi-str", "error").addRule("prefer-regex-literals", [
732
- "error",
733
- { disallowRedundantWrapping: true }
734
- ]).addRule("no-var", "error").addRule("prefer-const", ["error", { destructuring: "all" }]).addRule("prefer-object-has-own", "error").addRule("prefer-object-spread", "error").addRule("prefer-rest-params", "error").addRule("prefer-spread", "error").addRule("prefer-template", "error").addRule("symbol-description", "error").build();
352
+ });
353
+ addRules(builder, ERROR_PREVENTION_RULES);
354
+ addBaseOverrides(builder);
355
+ addRules(builder, DANGEROUS_PATTERN_RULES);
356
+ addRules(builder, CODE_QUALITY_RULES);
357
+ addComplexityRules(builder, isAi);
358
+ addModernStyleRules(builder, isAi);
359
+ if (isAi) {
360
+ addRules(builder, AI_STRUCTURAL_RULES);
361
+ }
362
+ return builder.build();
363
+ }
364
+
365
+ // src/configs/compat.ts
366
+ import compatPlugin from "eslint-plugin-compat";
367
+ function compatConfig() {
368
+ return [
369
+ {
370
+ name: "eslint-config-setup/compat",
371
+ plugins: {
372
+ compat: compatPlugin
373
+ },
374
+ rules: {
375
+ // Warn when using browser APIs not supported in browserslist targets
376
+ // https://github.com/amilajack/eslint-plugin-compat#usage
377
+ "compat/compat": "warn"
378
+ }
379
+ }
380
+ ];
735
381
  }
736
382
 
737
383
  // src/configs/cspell.ts
@@ -791,6 +437,7 @@ function importsConfig() {
791
437
  {
792
438
  name: "eslint-config-setup/imports",
793
439
  plugins: {
440
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Plugin type is compatible
794
441
  "import": importXPlugin,
795
442
  "unused-imports": unusedImportsPlugin
796
443
  },
@@ -798,7 +445,7 @@ function importsConfig() {
798
445
  // ── Validation (import-x) ────────────────────────────────────
799
446
  // Merge duplicate import paths into one statement — reduces noise
800
447
  // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-duplicates.md
801
- "import/no-duplicates": ["error", { "prefer-inline": true }],
448
+ "import/no-duplicates": "error",
802
449
  // Forbid a module from importing itself — always a bug
803
450
  // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-self-import.md
804
451
  "import/no-self-import": "error",
@@ -829,6 +476,30 @@ function importsConfig() {
829
476
  // Forbid absolute file paths in imports — not portable across machines
830
477
  // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-absolute-path.md
831
478
  "import/no-absolute-path": "error",
479
+ // Forbid imports of packages not listed in package.json — catches phantom deps
480
+ // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-extraneous-dependencies.md
481
+ "import/no-extraneous-dependencies": ["error", { includeTypes: true }],
482
+ // Forbid side-effect-only imports — make dependencies explicit
483
+ // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-unassigned-import.md
484
+ "import/no-unassigned-import": [
485
+ "error",
486
+ {
487
+ allow: [
488
+ "@babel/polyfill",
489
+ "**/register",
490
+ "**/register.*",
491
+ "**/register/**",
492
+ "**/register/**.*",
493
+ "**/*.css",
494
+ "**/*.scss",
495
+ "**/*.sass",
496
+ "**/*.less"
497
+ ]
498
+ }
499
+ ],
500
+ // Forbid `import { default as Foo }` — use `import Foo` instead
501
+ // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-named-default.md
502
+ "import/no-named-default": "error",
832
503
  // ── Disabled: ordering handled by perfectionist ────────────────
833
504
  // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/order.md
834
505
  "import/order": "off",
@@ -845,11 +516,18 @@ function importsConfig() {
845
516
 
846
517
  // src/configs/jsdoc.ts
847
518
  import jsdocPlugin from "eslint-plugin-jsdoc";
848
- function jsdocConfig() {
849
- return createConfig({
519
+ function jsdocConfig(opts) {
520
+ const isAi = opts?.ai ?? false;
521
+ const builder = createConfig({
850
522
  name: "eslint-config-setup/jsdoc",
851
523
  presets: [jsdocPlugin.configs["flat/recommended-typescript-error"]]
852
- }).overrideRule("jsdoc/require-jsdoc", "off").overrideRule("jsdoc/require-param", "off").overrideRule("jsdoc/require-returns", "off").overrideRule("jsdoc/require-yields", "off").overrideRule("jsdoc/require-param-description", "warn").overrideRule("jsdoc/require-returns-description", "warn").overrideRule("jsdoc/check-tag-names", "error").overrideRule("jsdoc/no-undefined-types", "error").overrideRule("jsdoc/tag-lines", "off").build();
524
+ }).overrideRule("jsdoc/require-jsdoc", "off").overrideRule("jsdoc/require-param", "off").overrideRule("jsdoc/require-returns", "off").overrideRule("jsdoc/require-yields", "off").overrideRule("jsdoc/require-param-description", "warn").overrideRule("jsdoc/require-returns-description", "warn").overrideRule("jsdoc/check-tag-names", "error").overrideRule("jsdoc/no-undefined-types", "error").overrideRule("jsdoc/tag-lines", "off");
525
+ if (isAi) {
526
+ builder.overrideRule("jsdoc/require-param", "error");
527
+ builder.overrideRule("jsdoc/require-returns", "error");
528
+ builder.overrideRule("jsdoc/informative-docs", "error");
529
+ }
530
+ return builder.build();
853
531
  }
854
532
 
855
533
  // src/configs/json.ts
@@ -913,6 +591,8 @@ function jsonConfig() {
913
591
  // src/configs/markdown.ts
914
592
  import * as mdxPlugin from "eslint-plugin-mdx";
915
593
  function markdownConfig() {
594
+ const codeBlockLanguageOptions = mdxPlugin.flatCodeBlocks.languageOptions ?? {};
595
+ const codeBlockParserOptions = codeBlockLanguageOptions.parserOptions ?? {};
916
596
  return [
917
597
  // ── MDX / Markdown parsing ─────────────────────────────────────
918
598
  {
@@ -927,6 +607,13 @@ function markdownConfig() {
927
607
  // Relaxes rules that don't apply to incomplete code snippets.
928
608
  {
929
609
  ...mdxPlugin.flatCodeBlocks,
610
+ languageOptions: {
611
+ ...codeBlockLanguageOptions,
612
+ parserOptions: {
613
+ ...codeBlockParserOptions,
614
+ projectService: false
615
+ }
616
+ },
930
617
  rules: {
931
618
  ...mdxPlugin.flatCodeBlocks.rules,
932
619
  // Snippets don't need trailing newlines
@@ -951,69 +638,76 @@ function markdownConfig() {
951
638
  // src/configs/node.ts
952
639
  import nodePlugin from "eslint-plugin-n";
953
640
  import globals from "globals";
954
- function nodeConfig() {
955
- return [
956
- {
957
- name: "eslint-config-setup/node",
958
- languageOptions: {
959
- globals: {
960
- ...globals.node
641
+ function nodeConfig(opts) {
642
+ const isAi = opts?.ai ?? false;
643
+ const builder = createConfig({
644
+ name: "eslint-config-setup/node",
645
+ presets: [
646
+ {
647
+ rules: {
648
+ // Detect usage of deprecated Node.js APIs (fs.exists, url.parse, etc.)
649
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-deprecated-api.md
650
+ "node/no-deprecated-api": "error",
651
+ // Prevent `module.exports = ...` assignment in ES modules
652
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-exports-assign.md
653
+ "node/no-exports-assign": "error",
654
+ // OFF: TypeScript resolves imports — this rule has false positives
655
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-import.md
656
+ "node/no-missing-import": "off",
657
+ // OFF: TypeScript resolves requires — this rule has false positives
658
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-require.md
659
+ "node/no-missing-require": "off",
660
+ // Warn on process.exit() — prefer throwing errors for clean shutdown
661
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-process-exit.md
662
+ "node/no-process-exit": "warn",
663
+ // OFF: Too many false positives with monorepos and devDependencies
664
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unpublished-import.md
665
+ "node/no-unpublished-import": "off",
666
+ // Validate hashbang lines — correct syntax, Unix linebreaks, only in entry files
667
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/hashbang.md
668
+ "node/hashbang": "error",
669
+ // Detect `__dirname + '/foo'` — use path.join() instead (breaks on Windows)
670
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-path-concat.md
671
+ "node/no-path-concat": "error",
672
+ // Treat process.exit() as throw — prevents false positives in unreachable code analysis
673
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/process-exit-as-throw.md
674
+ "node/process-exit-as-throw": "error",
675
+ // Ensure error parameters in callbacks are handled — don't silently swallow errors
676
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/handle-callback-err.md
677
+ "node/handle-callback-err": "error",
678
+ // ── Prefer global builtins ────────────────────────────────────
679
+ // Use global Buffer instead of require('buffer').Buffer
680
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/buffer.md
681
+ "node/prefer-global/buffer": ["error", "always"],
682
+ // Use global console — always available in Node.js
683
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/console.md
684
+ "node/prefer-global/console": ["error", "always"],
685
+ // Use global process — always available in Node.js
686
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/process.md
687
+ "node/prefer-global/process": ["error", "always"],
688
+ // Use global URL — available since Node.js 10
689
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/url.md
690
+ "node/prefer-global/url": ["error", "always"],
691
+ // Use global URLSearchParams — available since Node.js 10
692
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/url-search-params.md
693
+ "node/prefer-global/url-search-params": ["error", "always"],
694
+ // ── Prefer promise-based APIs ─────────────────────────────────
695
+ // Use dns.promises instead of callback-based dns — modern async
696
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-promises/dns.md
697
+ "node/prefer-promises/dns": "error",
698
+ // Use fs.promises instead of callback-based fs — modern async
699
+ // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-promises/fs.md
700
+ "node/prefer-promises/fs": "error"
961
701
  }
962
- },
963
- plugins: {
964
- node: nodePlugin
965
- },
966
- rules: {
967
- // Detect usage of deprecated Node.js APIs (fs.exists, url.parse, etc.)
968
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-deprecated-api.md
969
- "node/no-deprecated-api": "error",
970
- // Prevent `module.exports = ...` assignment in ES modules
971
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-exports-assign.md
972
- "node/no-exports-assign": "error",
973
- // OFF: TypeScript resolves imports — this rule has false positives
974
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-import.md
975
- "node/no-missing-import": "off",
976
- // OFF: TypeScript resolves requires — this rule has false positives
977
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-require.md
978
- "node/no-missing-require": "off",
979
- // Warn on process.exit() — prefer throwing errors for clean shutdown
980
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-process-exit.md
981
- "node/no-process-exit": "warn",
982
- // OFF: Too many false positives with monorepos and devDependencies
983
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unpublished-import.md
984
- "node/no-unpublished-import": "off",
985
- // Validate hashbang lines — correct syntax, Unix linebreaks, only in entry files
986
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/hashbang.md
987
- "node/hashbang": "error",
988
- // Detect `__dirname + '/foo'` — use path.join() instead (breaks on Windows)
989
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-path-concat.md
990
- "node/no-path-concat": "error",
991
- // ── Prefer global builtins ────────────────────────────────────
992
- // Use global Buffer instead of require('buffer').Buffer
993
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/buffer.md
994
- "node/prefer-global/buffer": ["error", "always"],
995
- // Use global console — always available in Node.js
996
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/console.md
997
- "node/prefer-global/console": ["error", "always"],
998
- // Use global process — always available in Node.js
999
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/process.md
1000
- "node/prefer-global/process": ["error", "always"],
1001
- // Use global URL — available since Node.js 10
1002
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/url.md
1003
- "node/prefer-global/url": ["error", "always"],
1004
- // Use global URLSearchParams — available since Node.js 10
1005
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/url-search-params.md
1006
- "node/prefer-global/url-search-params": ["error", "always"],
1007
- // ── Prefer promise-based APIs ─────────────────────────────────
1008
- // Use dns.promises instead of callback-based dns — modern async
1009
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-promises/dns.md
1010
- "node/prefer-promises/dns": "error",
1011
- // Use fs.promises instead of callback-based fs — modern async
1012
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-promises/fs.md
1013
- "node/prefer-promises/fs": "error"
1014
702
  }
1015
- }
1016
- ];
703
+ ],
704
+ plugins: { node: nodePlugin },
705
+ languageOptions: { globals: { ...globals.node } }
706
+ });
707
+ if (isAi) {
708
+ builder.addRule("node/no-unsupported-features/node-builtins", "error");
709
+ }
710
+ return builder.build();
1017
711
  }
1018
712
 
1019
713
  // src/configs/package-json.ts
@@ -1140,235 +834,326 @@ function prettierCompatConfig() {
1140
834
  }
1141
835
 
1142
836
  // src/configs/react.ts
1143
- import eslintReactPlugin from "@eslint-react/eslint-plugin";
837
+ import eslintReactPlugin2 from "@eslint-react/eslint-plugin";
1144
838
  import stylisticPlugin from "@stylistic/eslint-plugin";
1145
839
  import jsxA11yPlugin from "eslint-plugin-jsx-a11y";
1146
- import reactHooksPlugin from "eslint-plugin-react-hooks";
1147
840
  import reactRefreshPlugin from "eslint-plugin-react-refresh";
1148
841
  import globals2 from "globals";
1149
- function reactConfig() {
1150
- const reactDomPlugin = eslintReactPlugin.configs.dom.plugins;
1151
- const reactWebApiPlugin = eslintReactPlugin.configs["web-api"].plugins;
1152
- return [
842
+
843
+ // src/file-patterns.ts
844
+ var TYPESCRIPT_SOURCE_FILES = [
845
+ "**/*.ts",
846
+ "**/*.tsx",
847
+ "**/*.mts",
848
+ "**/*.cts"
849
+ ];
850
+ var MARKDOWN_CODE_BLOCK_FILES = ["**/*.{md,mdx}/**"];
851
+
852
+ // src/plugins/react-compat.ts
853
+ import eslintReactPlugin from "@eslint-react/eslint-plugin";
854
+ var coreRules = eslintReactPlugin.rules;
855
+ function selectPrefixedRules(prefix) {
856
+ const result = {};
857
+ for (const [name, rule] of Object.entries(coreRules)) {
858
+ if (name.startsWith(prefix)) {
859
+ result[name.slice(prefix.length)] = rule;
860
+ }
861
+ }
862
+ return result;
863
+ }
864
+ var domRules = selectPrefixedRules("dom-");
865
+ var jsxRules = selectPrefixedRules("jsx-");
866
+ var namingRules = selectPrefixedRules("naming-convention-");
867
+ var rscRules = selectPrefixedRules("rsc-");
868
+ var webApiRules = selectPrefixedRules("web-api-");
869
+ var LEGACY_ALIASES = {
870
+ // ── Core: identical names ──────────────────────────────────────────
871
+ "no-access-state-in-setstate": [coreRules, "no-access-state-in-setstate"],
872
+ "no-array-index-key": [coreRules, "no-array-index-key"],
873
+ "no-children-prop": [jsxRules, "no-children-prop"],
874
+ "no-direct-mutation-state": [coreRules, "no-direct-mutation-state"],
875
+ "no-redundant-should-component-update": [
876
+ coreRules,
877
+ "no-redundant-should-component-update"
878
+ ],
879
+ "no-unused-class-component-members": [
880
+ coreRules,
881
+ "no-unused-class-component-members"
882
+ ],
883
+ "no-unused-state": [coreRules, "no-unused-state"],
884
+ "jsx-no-comment-textnodes": [jsxRules, "no-comment-textnodes"],
885
+ // ── Core: renamed 1:1 ──────────────────────────────────────────────
886
+ "jsx-key": [coreRules, "no-missing-key"],
887
+ "jsx-key-before-spread": [jsxRules, "no-key-after-spread"],
888
+ "jsx-no-constructed-context-values": [
889
+ coreRules,
890
+ "no-unstable-context-value"
891
+ ],
892
+ "jsx-no-leaked-render": [coreRules, "no-leaked-conditional-rendering"],
893
+ "jsx-no-useless-fragment": [jsxRules, "no-useless-fragment"],
894
+ "no-object-type-as-default-prop": [coreRules, "no-unstable-default-props"],
895
+ "no-unstable-nested-components": [
896
+ coreRules,
897
+ "no-nested-component-definitions"
898
+ ],
899
+ "display-name": [coreRules, "no-missing-component-display-name"],
900
+ "forward-ref-uses-ref": [coreRules, "no-forward-ref"],
901
+ "destructuring-assignment": [coreRules, "prefer-destructuring-assignment"],
902
+ "no-did-mount-set-state": [
903
+ coreRules,
904
+ "no-set-state-in-component-did-mount"
905
+ ],
906
+ "no-did-update-set-state": [
907
+ coreRules,
908
+ "no-set-state-in-component-did-update"
909
+ ],
910
+ "no-will-update-set-state": [
911
+ coreRules,
912
+ "no-set-state-in-component-will-update"
913
+ ],
914
+ // ── Naming convention → legacy names ───────────────────────────────
915
+ "hook-use-state": [coreRules, "use-state"],
916
+ // ── DOM → legacy react/ names (not react-dom/) ─────────────────────
917
+ "no-danger": [domRules, "no-dangerously-set-innerhtml"],
918
+ "no-danger-with-children": [
919
+ domRules,
920
+ "no-dangerously-set-innerhtml-with-children"
921
+ ],
922
+ "no-find-dom-node": [domRules, "no-find-dom-node"],
923
+ "no-namespace": [jsxRules, "no-namespace"],
924
+ "no-render-return-value": [domRules, "no-render-return-value"],
925
+ "jsx-no-script-url": [domRules, "no-script-url"],
926
+ "jsx-no-target-blank": [domRules, "no-unsafe-target-blank"],
927
+ "no-unknown-property": [domRules, "no-unknown-property"],
928
+ "void-dom-elements-no-children": [
929
+ domRules,
930
+ "no-void-elements-with-children"
931
+ ],
932
+ "button-has-type": [domRules, "no-missing-button-type"],
933
+ "iframe-missing-sandbox": [domRules, "no-missing-iframe-sandbox"],
934
+ "style-prop-object": [domRules, "no-string-style-prop"]
935
+ };
936
+ var aliasedOriginals = /* @__PURE__ */ new Set();
937
+ for (const [source, originalName] of Object.values(LEGACY_ALIASES)) {
938
+ if (source === coreRules) {
939
+ aliasedOriginals.add(originalName);
940
+ } else if (source === domRules) {
941
+ aliasedOriginals.add(`dom-${originalName}`);
942
+ } else if (source === jsxRules) {
943
+ aliasedOriginals.add(`jsx-${originalName}`);
944
+ } else if (source === namingRules) {
945
+ aliasedOriginals.add(`naming-convention-${originalName}`);
946
+ } else if (source === rscRules) {
947
+ aliasedOriginals.add(`rsc-${originalName}`);
948
+ } else if (source === webApiRules) {
949
+ aliasedOriginals.add(`web-api-${originalName}`);
950
+ }
951
+ }
952
+ var mergedRules = {};
953
+ for (const [name, rule] of Object.entries(coreRules)) {
954
+ if (!aliasedOriginals.has(name)) {
955
+ mergedRules[name] = rule;
956
+ }
957
+ }
958
+ var subPlugins = [
959
+ [domRules, /* @__PURE__ */ new Set(["prefer-namespace-import"])],
960
+ [jsxRules, /* @__PURE__ */ new Set()],
961
+ [webApiRules, /* @__PURE__ */ new Set()],
962
+ [namingRules, /* @__PURE__ */ new Set()],
963
+ [rscRules, /* @__PURE__ */ new Set()]
964
+ ];
965
+ for (const [rules, skip] of subPlugins) {
966
+ for (const [name, rule] of Object.entries(rules)) {
967
+ if (skip.has(name)) continue;
968
+ const isAliased = Object.values(LEGACY_ALIASES).some(
969
+ ([source, originalName]) => source === rules && originalName === name
970
+ );
971
+ if (!isAliased) {
972
+ mergedRules[name] = rule;
973
+ }
974
+ }
975
+ }
976
+ for (const [legacyName, [source, originalName]] of Object.entries(
977
+ LEGACY_ALIASES
978
+ )) {
979
+ mergedRules[legacyName] = source[originalName];
980
+ }
981
+ var originalToCompat = /* @__PURE__ */ new Map();
982
+ for (const [legacyName, [source, originalName]] of Object.entries(LEGACY_ALIASES)) {
983
+ originalToCompat.set(originalName, legacyName);
984
+ if (source === domRules) {
985
+ originalToCompat.set(`dom-${originalName}`, legacyName);
986
+ } else if (source === jsxRules) {
987
+ originalToCompat.set(`jsx-${originalName}`, legacyName);
988
+ } else if (source === namingRules) {
989
+ originalToCompat.set(`naming-convention-${originalName}`, legacyName);
990
+ } else if (source === rscRules) {
991
+ originalToCompat.set(`rsc-${originalName}`, legacyName);
992
+ } else if (source === webApiRules) {
993
+ originalToCompat.set(`web-api-${originalName}`, legacyName);
994
+ }
995
+ }
996
+ function translatePresetRules(presetRules2) {
997
+ const result = {};
998
+ for (const [key, value] of Object.entries(presetRules2)) {
999
+ const shortName = key.replace(
1000
+ /^@eslint-react\/(?:dom|web-api|naming-convention|rsc)\//,
1001
+ ""
1002
+ ).replace(/^@eslint-react\//, "").replace(/^(?:dom|web-api|naming-convention|rsc)-/, "");
1003
+ const compatName = originalToCompat.get(shortName) ?? shortName;
1004
+ if (compatName in mergedRules) {
1005
+ result[`react/${compatName}`] = value;
1006
+ }
1007
+ }
1008
+ return result;
1009
+ }
1010
+ var reactCompatPlugin = {
1011
+ meta: {
1012
+ name: "react-compat",
1013
+ version: "1.0.0"
1014
+ },
1015
+ rules: mergedRules
1016
+ };
1017
+
1018
+ // src/configs/react.ts
1019
+ var typedConfigs = eslintReactPlugin2.configs;
1020
+ var recommendedRules = typedConfigs.recommended.rules;
1021
+ var strictRules = typedConfigs.strict.rules;
1022
+ var presetRules = translatePresetRules({ ...recommendedRules, ...strictRules });
1023
+ var EXTRA_REACT_RULES = {
1024
+ "react/jsx-no-leaked-render": "error",
1025
+ "react/no-duplicate-key": "error",
1026
+ "react/no-implicit-key": "error",
1027
+ "react/no-unused-props": "warn",
1028
+ "react/no-unknown-property": "error",
1029
+ "react/style-prop-object": "error"
1030
+ };
1031
+ var JSX_A11Y_ERROR_RULES = {
1032
+ "jsx-a11y/alt-text": "error",
1033
+ "jsx-a11y/anchor-has-content": "error",
1034
+ "jsx-a11y/anchor-is-valid": "error",
1035
+ "jsx-a11y/aria-activedescendant-has-tabindex": "error",
1036
+ "jsx-a11y/aria-props": "error",
1037
+ "jsx-a11y/aria-proptypes": "error",
1038
+ "jsx-a11y/aria-role": "error",
1039
+ "jsx-a11y/aria-unsupported-elements": "error",
1040
+ "jsx-a11y/click-events-have-key-events": "error",
1041
+ "jsx-a11y/heading-has-content": "error",
1042
+ "jsx-a11y/html-has-lang": "error",
1043
+ "jsx-a11y/img-redundant-alt": "error",
1044
+ "jsx-a11y/label-has-associated-control": "error",
1045
+ "jsx-a11y/mouse-events-have-key-events": "error",
1046
+ "jsx-a11y/no-access-key": "error",
1047
+ "jsx-a11y/no-distracting-elements": "error",
1048
+ "jsx-a11y/no-redundant-roles": "error",
1049
+ "jsx-a11y/role-has-required-aria-props": "error",
1050
+ "jsx-a11y/role-supports-aria-props": "error",
1051
+ "jsx-a11y/scope": "error",
1052
+ "jsx-a11y/tabindex-no-positive": "error",
1053
+ "jsx-a11y/lang": "error",
1054
+ "jsx-a11y/autocomplete-valid": "error"
1055
+ };
1056
+ var AI_PROMOTED_REACT_RULES = [
1057
+ "react/jsx-no-comment-textnodes",
1058
+ "react/jsx-no-useless-fragment",
1059
+ "react/jsx-no-constructed-context-values",
1060
+ "react/no-array-index-key",
1061
+ "react/no-object-type-as-default-prop",
1062
+ "react/no-unused-state",
1063
+ "react/jsx-no-target-blank",
1064
+ "react/button-has-type",
1065
+ "react/iframe-missing-sandbox",
1066
+ "react/forward-ref-uses-ref",
1067
+ "react/no-context-provider",
1068
+ "react/no-use-context",
1069
+ "react/no-leaked-event-listener",
1070
+ "react/no-leaked-interval",
1071
+ "react/no-leaked-timeout",
1072
+ "react/no-leaked-resize-observer"
1073
+ ];
1074
+ var AI_A11Y_RULES = [
1075
+ "jsx-a11y/no-static-element-interactions",
1076
+ "jsx-a11y/no-noninteractive-element-interactions",
1077
+ "jsx-a11y/interactive-supports-focus"
1078
+ ];
1079
+ function addRules2(builder, rules) {
1080
+ for (const [ruleName, value] of Object.entries(rules)) {
1081
+ builder.addRule(ruleName, value);
1082
+ }
1083
+ }
1084
+ function addExtraReactRules(builder) {
1085
+ addRules2(builder, EXTRA_REACT_RULES);
1086
+ }
1087
+ function addStylisticReactRules(builder) {
1088
+ builder.addRule("@stylistic/jsx-self-closing-comp", "error");
1089
+ builder.addRule("@stylistic/jsx-curly-brace-presence", [
1090
+ "error",
1091
+ { props: "never", children: "never" }
1092
+ ]);
1093
+ }
1094
+ function addReactRefreshRules(builder) {
1095
+ builder.addRule("react-refresh/only-export-components", [
1096
+ "warn",
1097
+ { allowConstantExport: true }
1098
+ ]);
1099
+ }
1100
+ function addAccessibilityRules(builder) {
1101
+ addRules2(builder, JSX_A11Y_ERROR_RULES);
1102
+ builder.addRule("jsx-a11y/no-autofocus", ["error", { ignoreNonDOM: true }]);
1103
+ }
1104
+ function applyAiReactRules(builder) {
1105
+ for (const ruleName of AI_PROMOTED_REACT_RULES) {
1106
+ builder.overrideSeverity(ruleName, "error");
1107
+ }
1108
+ for (const ruleName of AI_A11Y_RULES) {
1109
+ builder.addRule(ruleName, "error");
1110
+ }
1111
+ }
1112
+ function addReactFileOverrides(builder) {
1113
+ builder.addFileOverride(
1114
+ "eslint-config-setup/react-typescript",
1115
+ [...TYPESCRIPT_SOURCE_FILES],
1153
1116
  {
1154
- name: "eslint-config-setup/react",
1155
- languageOptions: {
1156
- globals: {
1157
- ...globals2.browser
1158
- },
1159
- parserOptions: {
1160
- ecmaFeatures: {
1161
- jsx: true
1162
- }
1163
- }
1164
- },
1165
- plugins: {
1166
- // @eslint-react core registered as "react" (like import-x → "import")
1167
- react: eslintReactPlugin,
1168
- // @eslint-react/dom → registered as "react-dom"
1169
- "react-dom": reactDomPlugin["@eslint-react/dom"],
1170
- // @eslint-react/web-api → registered as "react-web-api"
1171
- "react-web-api": reactWebApiPlugin["@eslint-react/web-api"],
1172
- // eslint-plugin-react-hooks stays as-is
1173
- "react-hooks": reactHooksPlugin,
1174
- "@stylistic": stylisticPlugin,
1175
- "react-refresh": reactRefreshPlugin,
1176
- "jsx-a11y": jsxA11yPlugin
1117
+ // Allow async event handlers — onClick={async () => {...}} is idiomatic React
1118
+ "@typescript-eslint/no-misused-promises": [
1119
+ "error",
1120
+ { checksVoidReturn: false }
1121
+ ]
1122
+ }
1123
+ );
1124
+ }
1125
+ function reactConfig(opts) {
1126
+ const isAi = opts?.ai ?? false;
1127
+ const builder = createConfig({
1128
+ name: "eslint-config-setup/react",
1129
+ presets: [{ rules: presetRules }],
1130
+ plugins: {
1131
+ react: reactCompatPlugin,
1132
+ "@stylistic": stylisticPlugin,
1133
+ "react-refresh": reactRefreshPlugin,
1134
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Plugin from untyped module
1135
+ "jsx-a11y": jsxA11yPlugin
1136
+ },
1137
+ languageOptions: {
1138
+ globals: {
1139
+ ...globals2.browser
1177
1140
  },
1178
- rules: {
1179
- // ── React core (react/) ─────────────────────────────────────────
1180
- // Prevent passing children as a prop — use JSX children syntax instead
1181
- // https://eslint-react.xyz/docs/rules/no-children-prop
1182
- "react/no-children-prop": "error",
1183
- // Prevent direct mutation of this.state — use setState instead
1184
- // https://eslint-react.xyz/docs/rules/no-direct-mutation-state
1185
- "react/no-direct-mutation-state": "error",
1186
- // Prevent unstable nested component definitions — causes remounts
1187
- // https://eslint-react.xyz/docs/rules/no-nested-component-definitions
1188
- "react/no-nested-component-definitions": "error",
1189
- // Require key prop in iterators — prevent reconciliation bugs
1190
- // https://eslint-react.xyz/docs/rules/no-missing-key
1191
- "react/no-missing-key": "error",
1192
- // Prevent duplicate key props in iterators
1193
- // https://eslint-react.xyz/docs/rules/no-duplicate-key
1194
- "react/no-duplicate-key": "error",
1195
- // Prevent comments from being inserted as text nodes in JSX
1196
- // https://eslint-react.xyz/docs/rules/jsx-no-comment-textnodes
1197
- "react/jsx-no-comment-textnodes": "error",
1198
- // Remove unnecessary JSX fragments — <>{x}</> → {x}
1199
- // https://eslint-react.xyz/docs/rules/no-useless-fragment
1200
- "react/no-useless-fragment": "error",
1201
- // Prefer <Foo active /> over <Foo active={true} /> — concise
1202
- // https://eslint-react.xyz/docs/rules/jsx-shorthand-boolean
1203
- "react/jsx-shorthand-boolean": "error",
1204
- // Prevent using array index as key — breaks reconciliation on reorder
1205
- // https://eslint-react.xyz/docs/rules/no-array-index-key
1206
- "react/no-array-index-key": "error",
1207
- // Prevent object/array literals as default props — creates new reference every render
1208
- // https://eslint-react.xyz/docs/rules/no-unstable-default-props
1209
- "react/no-unstable-default-props": "error",
1210
- // Prevent `{count && <Foo />}` — renders "0" when count is 0
1211
- // https://eslint-react.xyz/docs/rules/no-leaked-conditional-rendering
1212
- "react/no-leaked-conditional-rendering": "error",
1213
- // Prevent inline object creation in context providers — causes re-renders
1214
- // https://eslint-react.xyz/docs/rules/no-unstable-context-value
1215
- "react/no-unstable-context-value": "error",
1216
- // Prevent `this.setState({ count: this.state.count + 1 })` — race condition
1217
- // https://eslint-react.xyz/docs/rules/no-access-state-in-setstate
1218
- "react/no-access-state-in-setstate": "error",
1219
- // Detect state properties that are set but never read — dead code
1220
- // https://eslint-react.xyz/docs/rules/no-unused-state
1221
- "react/no-unused-state": "error",
1222
- // ── React 19 migration rules ────────────────────────────────────
1223
- // React 19: Use <Context> instead of <Context.Provider>
1224
- // https://eslint-react.xyz/docs/rules/no-context-provider
1225
- "react/no-context-provider": "error",
1226
- // React 19: Use ref as prop instead of forwardRef
1227
- // https://eslint-react.xyz/docs/rules/no-forward-ref
1228
- "react/no-forward-ref": "error",
1229
- // React 19: Use use() instead of useContext()
1230
- // https://eslint-react.xyz/docs/rules/no-use-context
1231
- "react/no-use-context": "error",
1232
- // ── React DOM (react-dom/) ──────────────────────────────────────
1233
- // Prevent unsafe target="_blank" links — requires rel="noreferrer"
1234
- // https://eslint-react.xyz/docs/rules/dom-no-unsafe-target-blank
1235
- "react-dom/no-unsafe-target-blank": "error",
1236
- // Prevent unknown DOM properties (e.g., class → className)
1237
- // https://eslint-react.xyz/docs/rules/dom-no-unknown-property
1238
- "react-dom/no-unknown-property": "error",
1239
- // Prevent void DOM elements (br, img, hr) from having children
1240
- // https://eslint-react.xyz/docs/rules/dom-no-void-elements-with-children
1241
- "react-dom/no-void-elements-with-children": "error",
1242
- // Require sandbox attribute on iframes — security best practice
1243
- // https://eslint-react.xyz/docs/rules/dom-no-missing-iframe-sandbox
1244
- "react-dom/no-missing-iframe-sandbox": "error",
1245
- // Prevent `style="color: red"` — must be an object in React
1246
- // https://eslint-react.xyz/docs/rules/dom-no-string-style-prop
1247
- "react-dom/no-string-style-prop": "error",
1248
- // Require explicit type on <button> — prevents unintended form submits
1249
- // https://eslint-react.xyz/docs/rules/dom-no-missing-button-type
1250
- "react-dom/no-missing-button-type": "error",
1251
- // Prevent dangerouslySetInnerHTML + children at the same time — conflict
1252
- // https://eslint-react.xyz/docs/rules/dom-no-dangerously-set-innerhtml-with-children
1253
- "react-dom/no-dangerously-set-innerhtml-with-children": "error",
1254
- // Warn on dangerouslySetInnerHTML — XSS risk, should be reviewed
1255
- // https://eslint-react.xyz/docs/rules/dom-no-dangerously-set-innerhtml
1256
- "react-dom/no-dangerously-set-innerhtml": "warn",
1257
- // ── React Web API (react-web-api/) — cleanup leak detection ─────
1258
- // Require cleanup for addEventListener in effects
1259
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-event-listener
1260
- "react-web-api/no-leaked-event-listener": "error",
1261
- // Require cleanup for setInterval in effects
1262
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-interval
1263
- "react-web-api/no-leaked-interval": "error",
1264
- // Require cleanup for setTimeout in effects
1265
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-timeout
1266
- "react-web-api/no-leaked-timeout": "error",
1267
- // Require cleanup for ResizeObserver in effects
1268
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-resize-observer
1269
- "react-web-api/no-leaked-resize-observer": "error",
1270
- // ── React Hooks (react-hooks/) — eslint-plugin-react-hooks ──────
1271
- // Enforce Rules of Hooks — hooks must be called at the top level
1272
- // https://react.dev/reference/rules/rules-of-hooks
1273
- "react-hooks/rules-of-hooks": "error",
1274
- // Verify dependency arrays in useEffect/useMemo/useCallback
1275
- // https://react.dev/reference/react/useEffect#specifying-reactive-dependencies
1276
- "react-hooks/exhaustive-deps": "error",
1277
- // ── @stylistic — JSX formatting rules ───────────────────────────
1278
- // Enforce self-closing tags for components without children — <Foo />
1279
- // https://eslint.style/rules/jsx/jsx-self-closing-comp
1280
- "@stylistic/jsx-self-closing-comp": "error",
1281
- // Prevent unnecessary string curly braces: title={"foo"} → title="foo"
1282
- // https://eslint.style/rules/jsx/jsx-curly-brace-presence
1283
- "@stylistic/jsx-curly-brace-presence": [
1284
- "error",
1285
- { props: "never", children: "never" }
1286
- ],
1287
- // ── React Refresh (Fast Refresh / HMR) ────────────────────────
1288
- // Ensure components are exported in a way that supports Fast Refresh.
1289
- // Mixed exports (component + constants) break HMR state preservation.
1290
- // allowConstantExport: Vite handles constant exports without breaking HMR.
1291
- // https://github.com/ArnaudBarre/eslint-plugin-react-refresh
1292
- "react-refresh/only-export-components": [
1293
- "warn",
1294
- { allowConstantExport: true }
1295
- ],
1296
- // ── JSX Accessibility (a11y) ──────────────────────────────────
1297
- // Require alt text on img, area, input[type="image"], object
1298
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/alt-text.md
1299
- "jsx-a11y/alt-text": "error",
1300
- // Require anchor content — empty links are inaccessible
1301
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-has-content.md
1302
- "jsx-a11y/anchor-has-content": "error",
1303
- // Require valid href on anchors — no `#` or `javascript:` void
1304
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-is-valid.md
1305
- "jsx-a11y/anchor-is-valid": "error",
1306
- // Active descendant elements must be focusable (tabindex)
1307
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-activedescendant-has-tabindex.md
1308
- "jsx-a11y/aria-activedescendant-has-tabindex": "error",
1309
- // Validate aria-* attributes exist — catches typos in ARIA props
1310
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-props.md
1311
- "jsx-a11y/aria-props": "error",
1312
- // Validate aria-* values match their type (boolean, token, etc.)
1313
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-proptypes.md
1314
- "jsx-a11y/aria-proptypes": "error",
1315
- // Validate role attribute values — catches invalid role names
1316
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-role.md
1317
- "jsx-a11y/aria-role": "error",
1318
- // Prevent ARIA on elements that don't support it (meta, script, style)
1319
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-unsupported-elements.md
1320
- "jsx-a11y/aria-unsupported-elements": "error",
1321
- // Click handlers must have keyboard equivalent — keyboard accessibility
1322
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/click-events-have-key-events.md
1323
- "jsx-a11y/click-events-have-key-events": "error",
1324
- // Heading elements must have content — screen readers need it
1325
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/heading-has-content.md
1326
- "jsx-a11y/heading-has-content": "error",
1327
- // Require lang attribute on <html> — language identification
1328
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/html-has-lang.md
1329
- "jsx-a11y/html-has-lang": "error",
1330
- // Prevent redundant alt text like "image of..." — screen readers add this
1331
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/img-redundant-alt.md
1332
- "jsx-a11y/img-redundant-alt": "error",
1333
- // Every form label must be associated with a control
1334
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/label-has-associated-control.md
1335
- "jsx-a11y/label-has-associated-control": "error",
1336
- // Mouse event handlers must have keyboard equivalents
1337
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/mouse-events-have-key-events.md
1338
- "jsx-a11y/mouse-events-have-key-events": "error",
1339
- // No accessKey attribute — inconsistent shortcuts confuse users
1340
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-access-key.md
1341
- "jsx-a11y/no-access-key": "error",
1342
- // No autofocus attribute (except non-DOM) — disorienting for screen readers
1343
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-autofocus.md
1344
- "jsx-a11y/no-autofocus": ["error", { ignoreNonDOM: true }],
1345
- // No <marquee>/<blink> — deprecated distracting elements
1346
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-distracting-elements.md
1347
- "jsx-a11y/no-distracting-elements": "error",
1348
- // No redundant ARIA roles (e.g., <button role="button">)
1349
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-redundant-roles.md
1350
- "jsx-a11y/no-redundant-roles": "error",
1351
- // Role elements must have all required ARIA props
1352
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/role-has-required-aria-props.md
1353
- "jsx-a11y/role-has-required-aria-props": "error",
1354
- // Don't use unsupported ARIA attributes for a given role
1355
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/role-supports-aria-props.md
1356
- "jsx-a11y/role-supports-aria-props": "error",
1357
- // scope attribute only on <th> elements — HTML specification
1358
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/scope.md
1359
- "jsx-a11y/scope": "error",
1360
- // No positive tabIndex values — disrupts natural tab order
1361
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/tabindex-no-positive.md
1362
- "jsx-a11y/tabindex-no-positive": "error",
1363
- // Validate lang attribute values — e.g. "de" ok, "deutsch" not
1364
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/lang.md
1365
- "jsx-a11y/lang": "error",
1366
- // Validate autocomplete attribute values on form elements
1367
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/autocomplete-valid.md
1368
- "jsx-a11y/autocomplete-valid": "error"
1141
+ parserOptions: {
1142
+ ecmaFeatures: {
1143
+ jsx: true
1144
+ }
1369
1145
  }
1370
1146
  }
1371
- ];
1147
+ });
1148
+ addExtraReactRules(builder);
1149
+ addStylisticReactRules(builder);
1150
+ addReactRefreshRules(builder);
1151
+ addAccessibilityRules(builder);
1152
+ if (isAi) {
1153
+ applyAiReactRules(builder);
1154
+ }
1155
+ addReactFileOverrides(builder);
1156
+ return builder.build();
1372
1157
  }
1373
1158
 
1374
1159
  // src/configs/react-effect.ts
@@ -1415,11 +1200,21 @@ function reactEffectConfig() {
1415
1200
 
1416
1201
  // src/configs/regexp.ts
1417
1202
  import { configs as regexpConfigs } from "eslint-plugin-regexp";
1418
- function regexpConfig() {
1419
- return createConfig({
1203
+ function regexpConfig(opts) {
1204
+ const isAi = opts?.ai ?? false;
1205
+ const builder = createConfig({
1420
1206
  name: "eslint-config-setup/regexp",
1421
1207
  presets: [regexpConfigs["flat/recommended"]]
1422
- }).addRule("regexp/no-control-character", "error").addRule("regexp/no-octal", "error").addRule("regexp/no-standalone-backslash", "error").addRule("regexp/no-super-linear-move", "error").addRule("regexp/prefer-escape-replacement-dollar-char", "error").build();
1208
+ }).addRule("regexp/no-control-character", "error").addRule("regexp/no-octal", "error").addRule("regexp/no-standalone-backslash", "error").addRule("regexp/no-super-linear-move", "error").addRule("regexp/prefer-escape-replacement-dollar-char", "error");
1209
+ if (isAi) {
1210
+ builder.addRule("regexp/prefer-lookaround", "error");
1211
+ builder.addRule("regexp/prefer-named-backreference", "error");
1212
+ builder.addRule("regexp/prefer-named-replacement", "error");
1213
+ builder.addRule("regexp/prefer-quantifier", "error");
1214
+ builder.addRule("regexp/prefer-result-array-groups", "error");
1215
+ builder.addRule("regexp/require-unicode-sets-regexp", "error");
1216
+ }
1217
+ return builder.build();
1423
1218
  }
1424
1219
 
1425
1220
  // src/configs/security.ts
@@ -1485,13 +1280,12 @@ function securityConfig() {
1485
1280
 
1486
1281
  // src/configs/sonarjs.ts
1487
1282
  import sonarjsPlugin from "eslint-plugin-sonarjs";
1488
- function sonarjsConfig() {
1489
- return [
1490
- {
1491
- name: "eslint-config-setup/sonarjs",
1492
- plugins: {
1493
- sonarjs: sonarjsPlugin
1494
- },
1283
+ function sonarjsConfig(opts) {
1284
+ const isAi = opts?.ai === true;
1285
+ const builder = createConfig({
1286
+ name: "eslint-config-setup/sonarjs",
1287
+ presets: [{
1288
+ plugins: { sonarjs: sonarjsPlugin },
1495
1289
  rules: {
1496
1290
  // Detect copy-pasted functions — extract to shared helper instead
1497
1291
  // https://sonarsource.github.io/rspec/#/rspec/S4144/javascript
@@ -1554,13 +1348,24 @@ function sonarjsConfig() {
1554
1348
  // https://sonarsource.github.io/rspec/#/rspec/S6418/javascript
1555
1349
  "sonarjs/no-hardcoded-secrets": "warn"
1556
1350
  }
1557
- }
1558
- ];
1351
+ }]
1352
+ });
1353
+ if (isAi) {
1354
+ builder.addRule("sonarjs/no-nested-switch", "error");
1355
+ builder.addRule("sonarjs/no-nested-template-literals", "error");
1356
+ builder.addRule("sonarjs/max-union-size", ["error", { threshold: 5 }]);
1357
+ builder.addRule("sonarjs/prefer-type-guard", "error");
1358
+ builder.addRule("sonarjs/public-static-readonly", "error");
1359
+ builder.addRule("sonarjs/no-duplicate-string", ["error", { threshold: 3 }]);
1360
+ }
1361
+ return builder.build();
1559
1362
  }
1560
1363
 
1561
1364
  // src/configs/typescript.ts
1562
1365
  import tseslint from "typescript-eslint";
1563
- function typescriptConfig() {
1366
+ function typescriptConfig(opts) {
1367
+ const isAi = opts?.ai ?? false;
1368
+ const isReact = opts?.react ?? false;
1564
1369
  const typeChecked = tseslint.configs.strictTypeChecked;
1565
1370
  const stylistic = tseslint.configs.stylisticTypeChecked;
1566
1371
  const structuralBlocks = typeChecked.slice(0, 2);
@@ -1569,6 +1374,8 @@ function typescriptConfig() {
1569
1374
  name: "eslint-config-setup/typescript",
1570
1375
  passthrough: structuralBlocks,
1571
1376
  presets: ruleBlocks,
1377
+ files: [...TYPESCRIPT_SOURCE_FILES],
1378
+ ignores: [...MARKDOWN_CODE_BLOCK_FILES],
1572
1379
  languageOptions: {
1573
1380
  parserOptions: {
1574
1381
  // Use project service for automatic tsconfig resolution
@@ -1597,10 +1404,25 @@ function typescriptConfig() {
1597
1404
  "error",
1598
1405
  "in-try-catch"
1599
1406
  ]);
1600
- builder.overrideRule("@typescript-eslint/no-deprecated", "warn");
1407
+ builder.overrideSeverity("@typescript-eslint/no-deprecated", "warn");
1408
+ builder.overrideRule("@typescript-eslint/no-explicit-any", [
1409
+ "warn",
1410
+ { fixToUnknown: true }
1411
+ ]);
1412
+ builder.overrideRule("@typescript-eslint/consistent-type-definitions", [
1413
+ "error",
1414
+ "type"
1415
+ ]);
1416
+ builder.overrideOptions("@typescript-eslint/ban-ts-comment", {
1417
+ "ts-expect-error": "allow-with-description"
1418
+ });
1419
+ builder.overrideOptions("@typescript-eslint/no-floating-promises", {
1420
+ checkThenables: true,
1421
+ ignoreIIFE: true
1422
+ });
1601
1423
  builder.addRule("@typescript-eslint/consistent-type-imports", [
1602
1424
  "error",
1603
- { fixStyle: "inline-type-imports" }
1425
+ { fixStyle: isAi ? "inline-type-imports" : "separate-type-imports" }
1604
1426
  ]);
1605
1427
  builder.addRule("@typescript-eslint/consistent-type-exports", [
1606
1428
  "error",
@@ -1614,6 +1436,24 @@ function typescriptConfig() {
1614
1436
  "error"
1615
1437
  );
1616
1438
  builder.addRule("@typescript-eslint/strict-void-return", "error");
1439
+ builder.addRule("@typescript-eslint/prefer-readonly", isAi ? "error" : "warn");
1440
+ builder.addRule("@typescript-eslint/require-array-sort-compare", [
1441
+ "error",
1442
+ { ignoreStringArrays: true }
1443
+ ]);
1444
+ builder.addRule("@typescript-eslint/no-unsafe-type-assertion", "error");
1445
+ builder.addRule("@typescript-eslint/switch-exhaustiveness-check", [
1446
+ "error",
1447
+ {
1448
+ allowDefaultCaseForExhaustiveSwitch: false,
1449
+ requireDefaultForNonUnion: true
1450
+ }
1451
+ ]);
1452
+ builder.addRule("@typescript-eslint/promise-function-async", "error");
1453
+ builder.addRule("@typescript-eslint/method-signature-style", [
1454
+ "error",
1455
+ "property"
1456
+ ]);
1617
1457
  builder.addRule("no-shadow", "off");
1618
1458
  builder.addRule("@typescript-eslint/no-shadow", [
1619
1459
  "error",
@@ -1624,10 +1464,7 @@ function typescriptConfig() {
1624
1464
  ignoreFunctionTypeParameterNameValueShadow: true
1625
1465
  }
1626
1466
  ]);
1627
- builder.overrideRule("@typescript-eslint/restrict-template-expressions", [
1628
- "error",
1629
- { allowNumber: true }
1630
- ]);
1467
+ builder.overrideOptions("@typescript-eslint/restrict-template-expressions", { allowNumber: true });
1631
1468
  builder.addRule("@typescript-eslint/strict-boolean-expressions", [
1632
1469
  "error",
1633
1470
  { allowNullableBoolean: true, allowNullableObject: true }
@@ -1637,18 +1474,168 @@ function typescriptConfig() {
1637
1474
  ["**/*.{js,mjs,cjs}"],
1638
1475
  tseslint.configs.disableTypeChecked.rules ?? {}
1639
1476
  );
1477
+ if (isAi) {
1478
+ builder.overrideSeverity("@typescript-eslint/no-explicit-any", "error");
1479
+ builder.overrideRule("@typescript-eslint/no-floating-promises", [
1480
+ "error",
1481
+ { checkThenables: true, ignoreVoid: true }
1482
+ ]);
1483
+ builder.addRule("@typescript-eslint/no-magic-numbers", [
1484
+ "error",
1485
+ {
1486
+ ignore: [-1, 0, 1, 2],
1487
+ ignoreArrayIndexes: true,
1488
+ ignoreDefaultValues: true,
1489
+ enforceConst: true,
1490
+ ignoreClassFieldInitialValues: true,
1491
+ ignoreEnums: true,
1492
+ ignoreNumericLiteralTypes: true,
1493
+ ignoreReadonlyClassProperties: true,
1494
+ ignoreTypeIndexes: true
1495
+ }
1496
+ ]);
1497
+ builder.addRule("@typescript-eslint/explicit-function-return-type", [
1498
+ "error",
1499
+ {
1500
+ allowExpressions: true,
1501
+ allowTypedFunctionExpressions: true,
1502
+ allowHigherOrderFunctions: true,
1503
+ allowIIFEs: true
1504
+ }
1505
+ ]);
1506
+ builder.addRule(
1507
+ "@typescript-eslint/explicit-member-accessibility",
1508
+ "error"
1509
+ );
1510
+ builder.addRule(
1511
+ "@typescript-eslint/prefer-enum-initializers",
1512
+ "error"
1513
+ );
1514
+ builder.addRule("@typescript-eslint/naming-convention", [
1515
+ "error",
1516
+ {
1517
+ selector: "variable",
1518
+ format: isReact ? ["strictCamelCase", "UPPER_CASE", "StrictPascalCase"] : ["strictCamelCase", "UPPER_CASE"],
1519
+ leadingUnderscore: "allowSingleOrDouble",
1520
+ trailingUnderscore: "allow",
1521
+ filter: { regex: "[- ]", match: false }
1522
+ },
1523
+ {
1524
+ selector: "function",
1525
+ format: isReact ? ["strictCamelCase", "StrictPascalCase"] : ["strictCamelCase"]
1526
+ },
1527
+ {
1528
+ selector: "parameter",
1529
+ format: ["strictCamelCase"],
1530
+ leadingUnderscore: "allow"
1531
+ },
1532
+ {
1533
+ selector: "import",
1534
+ format: ["strictCamelCase", "StrictPascalCase", "UPPER_CASE"]
1535
+ },
1536
+ {
1537
+ selector: [
1538
+ "classProperty",
1539
+ "parameterProperty",
1540
+ "classMethod",
1541
+ "objectLiteralMethod",
1542
+ "typeMethod",
1543
+ "accessor"
1544
+ ],
1545
+ format: ["strictCamelCase"],
1546
+ leadingUnderscore: "allowSingleOrDouble",
1547
+ trailingUnderscore: "allow",
1548
+ filter: { regex: "[- ]", match: false }
1549
+ },
1550
+ {
1551
+ selector: ["objectLiteralProperty", "typeProperty"],
1552
+ format: null
1553
+ },
1554
+ { selector: "typeLike", format: ["StrictPascalCase"] },
1555
+ {
1556
+ selector: "interface",
1557
+ format: ["StrictPascalCase"],
1558
+ custom: { regex: "^I[A-Z]", match: false }
1559
+ },
1560
+ {
1561
+ selector: "typeParameter",
1562
+ format: ["PascalCase"],
1563
+ custom: {
1564
+ regex: "^(T([A-Z][a-zA-Z]*)?|[A-Z])$",
1565
+ match: true
1566
+ }
1567
+ },
1568
+ {
1569
+ selector: "variable",
1570
+ types: ["boolean"],
1571
+ format: ["StrictPascalCase"],
1572
+ prefix: ["is", "has", "can", "should", "will", "did"]
1573
+ },
1574
+ {
1575
+ selector: ["classProperty", "objectLiteralProperty"],
1576
+ format: null,
1577
+ modifiers: ["requiresQuotes"]
1578
+ }
1579
+ ]);
1580
+ builder.addRule("@typescript-eslint/member-ordering", [
1581
+ "warn",
1582
+ {
1583
+ default: [
1584
+ "signature",
1585
+ "call-signature",
1586
+ "public-static-field",
1587
+ "protected-static-field",
1588
+ "private-static-field",
1589
+ "#private-static-field",
1590
+ "static-field",
1591
+ "public-static-method",
1592
+ "protected-static-method",
1593
+ "private-static-method",
1594
+ "#private-static-method",
1595
+ "static-method",
1596
+ "public-decorated-field",
1597
+ "protected-decorated-field",
1598
+ "private-decorated-field",
1599
+ "public-instance-field",
1600
+ "protected-instance-field",
1601
+ "private-instance-field",
1602
+ "#private-instance-field",
1603
+ "public-abstract-field",
1604
+ "protected-abstract-field",
1605
+ "field",
1606
+ "public-constructor",
1607
+ "protected-constructor",
1608
+ "private-constructor",
1609
+ "constructor",
1610
+ ["public-get", "public-set"],
1611
+ ["protected-get", "protected-set"],
1612
+ ["private-get", "private-set"],
1613
+ ["#private-get", "#private-set"],
1614
+ "public-decorated-method",
1615
+ "protected-decorated-method",
1616
+ "private-decorated-method",
1617
+ "public-instance-method",
1618
+ "protected-instance-method",
1619
+ "private-instance-method",
1620
+ "#private-instance-method",
1621
+ "public-abstract-method",
1622
+ "protected-abstract-method",
1623
+ "method"
1624
+ ]
1625
+ }
1626
+ ]);
1627
+ }
1640
1628
  return builder.build();
1641
1629
  }
1642
1630
 
1643
1631
  // src/configs/unicorn.ts
1644
1632
  import unicornPlugin from "eslint-plugin-unicorn";
1645
- function unicornConfig() {
1646
- return [
1647
- {
1648
- name: "eslint-config-setup/unicorn",
1649
- plugins: {
1650
- unicorn: unicornPlugin
1651
- },
1633
+ function unicornConfig(opts) {
1634
+ const isAi = opts?.ai === true;
1635
+ const builder = createConfig({
1636
+ name: "eslint-config-setup/unicorn",
1637
+ presets: [{
1638
+ plugins: { unicorn: unicornPlugin },
1652
1639
  rules: {
1653
1640
  // ── Error prevention ──────────────────────────────────────────
1654
1641
  // Forbid `/* eslint-disable */` without specific rule — too broad
@@ -1678,9 +1665,11 @@ function unicornConfig() {
1678
1665
  // Remove useless Promise.resolve/reject wrappers — simplify async code
1679
1666
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-promise-resolve-reject.md
1680
1667
  "unicorn/no-useless-promise-resolve-reject": "error",
1681
- // Disallow `return undefined` implicit undefined is cleaner
1668
+ // Disallow redundant undefined where omission preserves semantics.
1669
+ // Keep call arguments unchecked: TypeScript often needs explicit
1670
+ // `undefined` for required parameters typed as `T | undefined`.
1682
1671
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-undefined.md
1683
- "unicorn/no-useless-undefined": "error",
1672
+ "unicorn/no-useless-undefined": ["warn", { checkArguments: false }],
1684
1673
  // Disallow `await` in Promise.all/race arguments — already a promise
1685
1674
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-await-in-promise-methods.md
1686
1675
  "unicorn/no-await-in-promise-methods": "error",
@@ -1736,6 +1725,28 @@ function unicornConfig() {
1736
1725
  // Enforce consistent style for indexOf/findIndex existence checks
1737
1726
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-existence-index-check.md
1738
1727
  "unicorn/consistent-existence-index-check": "error",
1728
+ // Move functions to the smallest possible scope — avoids re-creation on every call,
1729
+ // improves testability, and makes dependencies explicit
1730
+ // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-function-scoping.md
1731
+ "unicorn/consistent-function-scoping": "warn",
1732
+ // Enforce consistent filename casing — camelCase, PascalCase, or kebab-case (no snake_case)
1733
+ // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
1734
+ "unicorn/filename-case": [
1735
+ "error",
1736
+ {
1737
+ cases: {
1738
+ camelCase: true,
1739
+ pascalCase: true,
1740
+ kebabCase: true
1741
+ }
1742
+ }
1743
+ ],
1744
+ // Discourage Array.reduce() — hard to read for many developers, prefer for...of or other methods
1745
+ // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-reduce.md
1746
+ "unicorn/no-array-reduce": "warn",
1747
+ // Prefer for...of over C-style for loops — more readable when index isn't needed
1748
+ // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-for-loop.md
1749
+ "unicorn/no-for-loop": "warn",
1739
1750
  // ── Modern API preferences ────────────────────────────────────
1740
1751
  // Prefer .flatMap() over .map().flat() — single pass, more readable
1741
1752
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-flat-map.md
@@ -1761,6 +1772,9 @@ function unicornConfig() {
1761
1772
  // Prefer .before()/.after()/.replaceWith() over parent.insertBefore()
1762
1773
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-modern-dom-apis.md
1763
1774
  "unicorn/prefer-modern-dom-apis": "error",
1775
+ // Prefer element.dataset.foo over setAttribute('data-foo') — modern, readable
1776
+ // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-dom-node-dataset.md
1777
+ "unicorn/prefer-dom-node-dataset": "error",
1764
1778
  // Prefer Math.log10/Math.hypot over manual math — accurate and readable
1765
1779
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-modern-math-apis.md
1766
1780
  "unicorn/prefer-modern-math-apis": "error",
@@ -1866,8 +1880,27 @@ function unicornConfig() {
1866
1880
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-single-call.md
1867
1881
  "unicorn/prefer-single-call": "off"
1868
1882
  }
1869
- }
1870
- ];
1883
+ }]
1884
+ });
1885
+ if (isAi) {
1886
+ builder.overrideSeverity("unicorn/consistent-function-scoping", "error");
1887
+ builder.overrideSeverity("unicorn/no-array-reduce", "error");
1888
+ builder.overrideSeverity("unicorn/no-for-loop", "error");
1889
+ builder.addRule("unicorn/no-array-for-each", "error");
1890
+ builder.addRule("unicorn/prefer-ternary", ["error", "only-single-line"]);
1891
+ builder.addRule("unicorn/prefer-switch", ["error", { minimumCases: 3 }]);
1892
+ builder.addRule("unicorn/prevent-abbreviations", "error");
1893
+ builder.addRule("unicorn/no-useless-switch-case", "error");
1894
+ builder.addRule("unicorn/custom-error-definition", "error");
1895
+ builder.addRule("unicorn/prefer-default-parameters", "error");
1896
+ builder.addRule("unicorn/prefer-logical-operator-over-ternary", "error");
1897
+ builder.addRule("unicorn/prefer-math-min-max", "error");
1898
+ builder.addRule("unicorn/prefer-set-size", "error");
1899
+ builder.addRule("unicorn/explicit-length-check", "error");
1900
+ builder.addRule("unicorn/switch-case-braces", "error");
1901
+ builder.addRule("unicorn/no-array-push-push", "error");
1902
+ }
1903
+ return builder.build();
1871
1904
  }
1872
1905
 
1873
1906
  // src/overrides/config-files.ts
@@ -2100,136 +2133,165 @@ function storiesOverride() {
2100
2133
  // src/overrides/tests.ts
2101
2134
  import vitestPlugin from "@vitest/eslint-plugin";
2102
2135
  import testingLibraryPlugin from "eslint-plugin-testing-library";
2136
+ var TEST_FILES2 = ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"];
2137
+ var VITEST_RULES = {
2138
+ // ── Vitest rules ──────────────────────────────────────────────
2139
+ // Every test must contain at least one assertion
2140
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/expect-expect.md
2141
+ "vitest/expect-expect": "error",
2142
+ // Prevent duplicate test titles within a describe block
2143
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-identical-title.md
2144
+ "vitest/no-identical-title": "error",
2145
+ // No it.only / describe.only — prevents accidentally skipping tests in CI
2146
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-focused-tests.md
2147
+ "vitest/no-focused-tests": "error",
2148
+ // Warn on it.skip / describe.skip — track disabled tests
2149
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-disabled-tests.md
2150
+ "vitest/no-disabled-tests": "warn",
2151
+ // No duplicate beforeEach/afterEach hooks — merge them
2152
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-duplicate-hooks.md
2153
+ "vitest/no-duplicate-hooks": "error",
2154
+ // Prefer .toBe() over .toEqual() for primitives — clearer intent
2155
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-be.md
2156
+ "vitest/prefer-to-be": "error",
2157
+ // Prefer .toHaveLength() over .toBe(arr.length) — better errors
2158
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-have-length.md
2159
+ "vitest/prefer-to-have-length": "error",
2160
+ // Validate expect() usage — no dangling expect without assertion
2161
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-expect.md
2162
+ "vitest/valid-expect": "error",
2163
+ // Validate test/describe title format — no empty or invalid titles
2164
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-title.md
2165
+ "vitest/valid-title": "error",
2166
+ // No conditional logic (if/else) inside tests — split into separate tests
2167
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-in-test.md
2168
+ "vitest/no-conditional-in-test": "error",
2169
+ // No expect() inside conditional blocks — always assert unconditionally
2170
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md
2171
+ "vitest/no-conditional-expect": "error",
2172
+ // No standalone expect() outside test blocks — always wrap in it/test
2173
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-standalone-expect.md
2174
+ "vitest/no-standalone-expect": "error",
2175
+ // Prefer .toStrictEqual() over .toEqual() — catches undefined vs missing
2176
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-strict-equal.md
2177
+ "vitest/prefer-strict-equal": "error",
2178
+ // Prefer vi.spyOn() over vi.fn() for method mocks — preserves original
2179
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-spy-on.md
2180
+ "vitest/prefer-spy-on": "error",
2181
+ // Require message argument in toThrow/toThrowError — verify correct error
2182
+ // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/require-to-throw-message.md
2183
+ "vitest/require-to-throw-message": "error",
2184
+ // ── Relaxed rules for tests ───────────────────────────────────
2185
+ // Tests are naturally verbose and use patterns banned in prod code
2186
+ // Tests can be long (setup + many assertions)
2187
+ "max-lines": "off",
2188
+ "max-lines-per-function": "off",
2189
+ "max-statements": "off",
2190
+ // Tests often need `any` for mocking and type assertions
2191
+ "@typescript-eslint/no-explicit-any": "off",
2192
+ "@typescript-eslint/no-non-null-assertion": "off",
2193
+ "@typescript-eslint/no-unsafe-assignment": "off",
2194
+ "@typescript-eslint/no-unsafe-member-access": "off"
2195
+ };
2196
+ var TESTING_LIBRARY_RULES = {
2197
+ // ── Testing Library rules ───────────────────────────────────
2198
+ // Await async events (userEvent.click, etc.) — prevents race conditions
2199
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-events.md
2200
+ "testing-library/await-async-events": "error",
2201
+ // Await async queries (findBy*) — they return promises
2202
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-queries.md
2203
+ "testing-library/await-async-queries": "error",
2204
+ // Await async utilities (waitFor, waitForElementToBeRemoved)
2205
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-utils.md
2206
+ "testing-library/await-async-utils": "error",
2207
+ // Don't await synchronous events — misleading
2208
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-events.md
2209
+ "testing-library/no-await-sync-events": "error",
2210
+ // Don't await synchronous queries (getBy*, queryBy*) — not promises
2211
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-queries.md
2212
+ "testing-library/no-await-sync-queries": "error",
2213
+ // Don't use container.querySelector — use queries instead
2214
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-container.md
2215
+ "testing-library/no-container": "error",
2216
+ // Warn on debug()/prettyDOM() left in tests — debugging artifacts
2217
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-debugging-utils.md
2218
+ "testing-library/no-debugging-utils": "warn",
2219
+ // Don't access DOM nodes directly — use queries
2220
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-node-access.md
2221
+ "testing-library/no-node-access": "error",
2222
+ // Don't call render in beforeEach — call in each test
2223
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-render-in-lifecycle.md
2224
+ "testing-library/no-render-in-lifecycle": "error",
2225
+ // No unnecessary act() wrappers — TL handles this internally
2226
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-unnecessary-act.md
2227
+ "testing-library/no-unnecessary-act": "error",
2228
+ // One assertion per waitFor — multiple can mask failures
2229
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-multiple-assertions.md
2230
+ "testing-library/no-wait-for-multiple-assertions": "error",
2231
+ // No side effects in waitFor — only assertions
2232
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-side-effects.md
2233
+ "testing-library/no-wait-for-side-effects": "error",
2234
+ // Prefer findBy* over waitFor + getBy* — built-in combination
2235
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-find-by.md
2236
+ "testing-library/prefer-find-by": "error",
2237
+ // Use getBy* (throws) for present elements, queryBy* for absent
2238
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-presence-queries.md
2239
+ "testing-library/prefer-presence-queries": "error",
2240
+ // Use queryBy* for disappearance checks — returns null when gone
2241
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-query-by-disappearance.md
2242
+ "testing-library/prefer-query-by-disappearance": "error",
2243
+ // Use screen.getBy* over destructured render result — consistent
2244
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md
2245
+ "testing-library/prefer-screen-queries": "error",
2246
+ // Name render result consistently (e.g., `const { getByText } = render(...)`)
2247
+ // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/render-result-naming-convention.md
2248
+ "testing-library/render-result-naming-convention": "error"
2249
+ };
2103
2250
  function testsOverride() {
2104
- const configs = [
2251
+ return [
2105
2252
  {
2106
2253
  name: "eslint-config-setup/tests",
2107
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
2254
+ files: TEST_FILES2,
2108
2255
  plugins: {
2109
2256
  vitest: vitestPlugin
2110
2257
  },
2111
- rules: {
2112
- // ── Vitest rules ──────────────────────────────────────────────
2113
- // Every test must contain at least one assertion
2114
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/expect-expect.md
2115
- "vitest/expect-expect": "error",
2116
- // Prevent duplicate test titles within a describe block
2117
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-identical-title.md
2118
- "vitest/no-identical-title": "error",
2119
- // No it.only / describe.only — prevents accidentally skipping tests in CI
2120
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-focused-tests.md
2121
- "vitest/no-focused-tests": "error",
2122
- // Warn on it.skip / describe.skip — track disabled tests
2123
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-disabled-tests.md
2124
- "vitest/no-disabled-tests": "warn",
2125
- // No duplicate beforeEach/afterEach hooks — merge them
2126
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-duplicate-hooks.md
2127
- "vitest/no-duplicate-hooks": "error",
2128
- // Prefer .toBe() over .toEqual() for primitives — clearer intent
2129
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-be.md
2130
- "vitest/prefer-to-be": "error",
2131
- // Prefer .toHaveLength() over .toBe(arr.length) — better errors
2132
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-have-length.md
2133
- "vitest/prefer-to-have-length": "error",
2134
- // Validate expect() usage — no dangling expect without assertion
2135
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-expect.md
2136
- "vitest/valid-expect": "error",
2137
- // Validate test/describe title format — no empty or invalid titles
2138
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-title.md
2139
- "vitest/valid-title": "error",
2140
- // No conditional logic (if/else) inside tests — split into separate tests
2141
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-in-test.md
2142
- "vitest/no-conditional-in-test": "error",
2143
- // No expect() inside conditional blocks — always assert unconditionally
2144
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md
2145
- "vitest/no-conditional-expect": "error",
2146
- // No standalone expect() outside test blocks — always wrap in it/test
2147
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-standalone-expect.md
2148
- "vitest/no-standalone-expect": "error",
2149
- // Prefer .toStrictEqual() over .toEqual() — catches undefined vs missing
2150
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-strict-equal.md
2151
- "vitest/prefer-strict-equal": "error",
2152
- // Prefer vi.spyOn() over vi.fn() for method mocks — preserves original
2153
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-spy-on.md
2154
- "vitest/prefer-spy-on": "error",
2155
- // Require message argument in toThrow/toThrowError — verify correct error
2156
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/require-to-throw-message.md
2157
- "vitest/require-to-throw-message": "error",
2158
- // ── Relaxed rules for tests ───────────────────────────────────
2159
- // Tests are naturally verbose and use patterns banned in prod code
2160
- // Tests can be long (setup + many assertions)
2161
- "max-lines": "off",
2162
- "max-lines-per-function": "off",
2163
- "max-statements": "off",
2164
- // Tests often need `any` for mocking and type assertions
2165
- "@typescript-eslint/no-explicit-any": "off",
2166
- "@typescript-eslint/no-non-null-assertion": "off",
2167
- "@typescript-eslint/no-unsafe-assignment": "off",
2168
- "@typescript-eslint/no-unsafe-member-access": "off"
2169
- }
2258
+ rules: VITEST_RULES
2170
2259
  },
2171
2260
  {
2172
2261
  name: "eslint-config-setup/tests-testing-library",
2173
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
2262
+ files: TEST_FILES2,
2174
2263
  plugins: {
2175
2264
  "testing-library": testingLibraryPlugin
2176
2265
  },
2177
- rules: {
2178
- // ── Testing Library rules ───────────────────────────────────
2179
- // Await async events (userEvent.click, etc.) — prevents race conditions
2180
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-events.md
2181
- "testing-library/await-async-events": "error",
2182
- // Await async queries (findBy*) — they return promises
2183
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-queries.md
2184
- "testing-library/await-async-queries": "error",
2185
- // Await async utilities (waitFor, waitForElementToBeRemoved)
2186
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-utils.md
2187
- "testing-library/await-async-utils": "error",
2188
- // Don't await synchronous events — misleading
2189
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-events.md
2190
- "testing-library/no-await-sync-events": "error",
2191
- // Don't await synchronous queries (getBy*, queryBy*) — not promises
2192
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-queries.md
2193
- "testing-library/no-await-sync-queries": "error",
2194
- // Don't use container.querySelector — use queries instead
2195
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-container.md
2196
- "testing-library/no-container": "error",
2197
- // Warn on debug()/prettyDOM() left in tests — debugging artifacts
2198
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-debugging-utils.md
2199
- "testing-library/no-debugging-utils": "warn",
2200
- // Don't access DOM nodes directly — use queries
2201
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-node-access.md
2202
- "testing-library/no-node-access": "error",
2203
- // Don't call render in beforeEach — call in each test
2204
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-render-in-lifecycle.md
2205
- "testing-library/no-render-in-lifecycle": "error",
2206
- // No unnecessary act() wrappers — TL handles this internally
2207
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-unnecessary-act.md
2208
- "testing-library/no-unnecessary-act": "error",
2209
- // One assertion per waitFor — multiple can mask failures
2210
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-multiple-assertions.md
2211
- "testing-library/no-wait-for-multiple-assertions": "error",
2212
- // No side effects in waitFor — only assertions
2213
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-side-effects.md
2214
- "testing-library/no-wait-for-side-effects": "error",
2215
- // Prefer findBy* over waitFor + getBy* — built-in combination
2216
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-find-by.md
2217
- "testing-library/prefer-find-by": "error",
2218
- // Use getBy* (throws) for present elements, queryBy* for absent
2219
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-presence-queries.md
2220
- "testing-library/prefer-presence-queries": "error",
2221
- // Use queryBy* for disappearance checks — returns null when gone
2222
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-query-by-disappearance.md
2223
- "testing-library/prefer-query-by-disappearance": "error",
2224
- // Use screen.getBy* over destructured render result — consistent
2225
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md
2226
- "testing-library/prefer-screen-queries": "error",
2227
- // Name render result consistently (e.g., `const { getByText } = render(...)`)
2228
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/render-result-naming-convention.md
2229
- "testing-library/render-result-naming-convention": "error"
2230
- }
2266
+ rules: TESTING_LIBRARY_RULES
2231
2267
  }
2232
2268
  ];
2269
+ }
2270
+
2271
+ // src/oxlint/integration.ts
2272
+ import oxlintPlugin from "eslint-plugin-oxlint";
2273
+ function oxlintIntegration(opts) {
2274
+ const typedPlugin = oxlintPlugin;
2275
+ const configs = [];
2276
+ const add = (configName, blockName) => {
2277
+ const raw = typedPlugin.configs[configName];
2278
+ const items = Array.isArray(raw) ? raw : [raw];
2279
+ for (const item of items) {
2280
+ configs.push({ name: `eslint-config-setup/${blockName}`, ...item });
2281
+ }
2282
+ };
2283
+ add("flat/recommended", "oxlint");
2284
+ if (opts.react) {
2285
+ add("flat/react", "oxlint-react");
2286
+ add("flat/jsx-a11y", "oxlint-jsx-a11y");
2287
+ }
2288
+ if (opts.node) {
2289
+ add("flat/node", "oxlint-node");
2290
+ }
2291
+ add("flat/typescript", "oxlint-typescript");
2292
+ add("flat/unicorn", "oxlint-unicorn");
2293
+ add("flat/import", "oxlint-import");
2294
+ add("flat/jsdoc", "oxlint-jsdoc");
2233
2295
  return configs;
2234
2296
  }
2235
2297
 
@@ -2284,65 +2346,24 @@ function standardComplexity() {
2284
2346
  ];
2285
2347
  }
2286
2348
 
2287
- // src/oxlint/integration.ts
2288
- import oxlintPlugin from "eslint-plugin-oxlint";
2289
- function oxlintIntegration(opts) {
2290
- const typedPlugin = oxlintPlugin;
2291
- const configs = [];
2292
- const add = (configName, blockName) => {
2293
- const raw = typedPlugin.configs[configName];
2294
- const items = Array.isArray(raw) ? raw : [raw];
2295
- for (const item of items) {
2296
- configs.push({ name: `eslint-config-setup/${blockName}`, ...item });
2297
- }
2298
- };
2299
- add("flat/recommended", "oxlint");
2300
- if (opts.react) {
2301
- add("flat/react", "oxlint-react");
2302
- add("flat/jsx-a11y", "oxlint-jsx-a11y");
2303
- }
2304
- if (opts.node) {
2305
- add("flat/node", "oxlint-node");
2306
- }
2307
- add("flat/typescript", "oxlint-typescript");
2308
- add("flat/unicorn", "oxlint-unicorn");
2309
- add("flat/import", "oxlint-import");
2310
- add("flat/jsdoc", "oxlint-jsdoc");
2311
- return configs;
2312
- }
2313
-
2314
- // src/configs/compat.ts
2315
- import compatPlugin from "eslint-plugin-compat";
2316
- function compatConfig() {
2317
- return [
2318
- {
2319
- name: "eslint-config-setup/compat",
2320
- plugins: {
2321
- compat: compatPlugin
2322
- },
2323
- rules: {
2324
- // Warn when using browser APIs not supported in browserslist targets
2325
- // https://github.com/amilajack/eslint-plugin-compat#usage
2326
- "compat/compat": "warn"
2327
- }
2328
- }
2329
- ];
2330
- }
2331
-
2332
2349
  // src/build/compose.ts
2333
- function composeConfig(opts) {
2334
- const config = [];
2335
- config.push(...baseConfig());
2336
- config.push(...typescriptConfig());
2337
- config.push(...importsConfig());
2338
- config.push(...perfectionistConfig());
2339
- config.push(...unicornConfig());
2340
- config.push(...regexpConfig());
2341
- config.push(...jsdocConfig());
2342
- config.push(...cspellConfig());
2343
- config.push(...sonarjsConfig());
2344
- config.push(...securityConfig());
2345
- config.push(...deMorganConfig());
2350
+ function addCoreConfigs(config, opts) {
2351
+ const ai = opts.ai;
2352
+ config.push(
2353
+ ...baseConfig({ ai }),
2354
+ ...typescriptConfig({ ai, react: opts.react }),
2355
+ ...importsConfig(),
2356
+ ...perfectionistConfig(),
2357
+ ...unicornConfig({ ai }),
2358
+ ...regexpConfig({ ai }),
2359
+ ...jsdocConfig({ ai }),
2360
+ ...cspellConfig(),
2361
+ ...sonarjsConfig({ ai }),
2362
+ ...securityConfig(),
2363
+ ...deMorganConfig()
2364
+ );
2365
+ }
2366
+ function addConditionalConfigs(config, opts) {
2346
2367
  if (!opts.node) {
2347
2368
  config.push(...compatConfig());
2348
2369
  }
@@ -2350,30 +2371,40 @@ function composeConfig(opts) {
2350
2371
  config.push(...standardComplexity());
2351
2372
  }
2352
2373
  if (opts.node) {
2353
- config.push(...nodeConfig());
2374
+ config.push(...nodeConfig({ ai: opts.ai }));
2354
2375
  }
2355
2376
  if (opts.react) {
2356
- config.push(...reactConfig());
2377
+ config.push(...reactConfig({ ai: opts.ai }));
2357
2378
  config.push(...reactEffectConfig());
2358
2379
  }
2359
2380
  if (opts.ai) {
2360
- config.push(...aiConfig({ react: opts.react }));
2381
+ config.push(...aiConfig());
2361
2382
  config.push(...perfectionistAiConfig());
2362
2383
  config.push(...packageJsonAiConfig());
2363
2384
  }
2364
- config.push(...testsOverride());
2365
- config.push(...e2eOverride());
2366
- config.push(...storiesOverride());
2367
- config.push(...configFilesOverride());
2368
- config.push(...declarationsOverride());
2369
- config.push(...scriptsOverride());
2370
- config.push(...jsonConfig());
2371
- config.push(...packageJsonConfig());
2372
- config.push(...markdownConfig());
2373
- config.push(...prettierCompatConfig());
2385
+ }
2386
+ function addFileTypeOverrides(config) {
2387
+ config.push(
2388
+ ...testsOverride(),
2389
+ ...e2eOverride(),
2390
+ ...storiesOverride(),
2391
+ ...configFilesOverride(),
2392
+ ...declarationsOverride(),
2393
+ ...scriptsOverride()
2394
+ );
2395
+ }
2396
+ function addFinalConfigs(config, opts) {
2397
+ config.push(...jsonConfig(), ...packageJsonConfig(), ...markdownConfig(), ...prettierCompatConfig());
2374
2398
  if (opts.oxlint) {
2375
2399
  config.push(...oxlintIntegration(opts));
2376
2400
  }
2401
+ }
2402
+ function composeConfig(opts) {
2403
+ const config = [];
2404
+ addCoreConfigs(config, opts);
2405
+ addConditionalConfigs(config, opts);
2406
+ addFileTypeOverrides(config);
2407
+ addFinalConfigs(config, opts);
2377
2408
  return config;
2378
2409
  }
2379
2410
  export {