eslint-config-setup 0.3.3 → 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,591 +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
- // Prefer template literals over concatenation — more readable
46
- // https://eslint.org/docs/latest/rules/prefer-template
47
- "prefer-template": "error",
48
- // Require shorthand properties in objects — concise, skip quoted keys
49
- // https://eslint.org/docs/latest/rules/object-shorthand
50
- "object-shorthand": ["error", "always", { avoidQuotes: true }],
51
- // Prefer concise arrow body: `() => expr` over `() => { return expr }`
52
- // https://eslint.org/docs/latest/rules/arrow-body-style
53
- "arrow-body-style": "error",
54
- // Prefer arrow functions for callbacks — lexical `this`
55
- // https://eslint.org/docs/latest/rules/prefer-arrow-callback
56
- "prefer-arrow-callback": ["error", { allowNamedFunctions: true }],
57
- // Prefer `x ??= y` over `x = x ?? y` — concise null-coalescing assignment
58
- // https://eslint.org/docs/latest/rules/logical-assignment-operators
59
- "logical-assignment-operators": [
60
- "error",
61
- "always",
62
- { enforceForIfStatements: true }
63
- ],
64
- // No assignment in return statements — separate mutation from return
65
- // https://eslint.org/docs/latest/rules/no-return-assign
66
- "no-return-assign": ["error", "always"],
67
- // One statement per line — scannable, diff-friendly
68
- // https://eslint.org/docs/latest/rules/max-statements-per-line
69
- "max-statements-per-line": ["error", { max: 1 }],
70
- // Prefer `x ** 2` over `Math.pow(x, 2)` — modern operator syntax
71
- // https://eslint.org/docs/latest/rules/prefer-exponentiation-operator
72
- "prefer-exponentiation-operator": "error",
73
- // Require named capture groups in regex — self-documenting patterns
74
- // https://eslint.org/docs/latest/rules/prefer-named-capture-group
75
- "prefer-named-capture-group": "error",
76
- // Require Unicode-aware regex (`u` or `v` flag) — correct string handling
77
- // https://eslint.org/docs/latest/rules/require-unicode-regexp
78
- "require-unicode-regexp": "error",
79
- // ── B. Magic numbers & constants — no unexplained code ────────
80
- // No magic numbers — extract to named constants (allows -1, 0, 1, 2)
81
- // https://typescript-eslint.io/rules/no-magic-numbers
82
- "@typescript-eslint/no-magic-numbers": [
83
- "error",
84
- {
85
- ignore: [-1, 0, 1, 2],
86
- ignoreArrayIndexes: true,
87
- ignoreDefaultValues: true,
88
- enforceConst: true,
89
- ignoreClassFieldInitialValues: true,
90
- ignoreEnums: true,
91
- ignoreNumericLiteralTypes: true,
92
- ignoreReadonlyClassProperties: true,
93
- ignoreTypeIndexes: true
94
- }
95
- ],
96
- // No duplicate strings (threshold 3) — extract to constant
97
- // https://sonarsource.github.io/rspec/#/rspec/S1192/javascript
98
- "sonarjs/no-duplicate-string": ["error", { threshold: 3 }],
99
- // Flag TODO/FIXME/HACK comments — technical debt tracker
100
- // https://eslint.org/docs/latest/rules/no-warning-comments
101
- "no-warning-comments": "warn",
102
- // ── F. Async/Promise hygiene ──────────────────────────────────
103
- // No await inside loops — use Promise.all() for parallel execution
104
- // https://eslint.org/docs/latest/rules/no-await-in-loop
105
- "no-await-in-loop": "error",
106
- // Disallow returning values from Promise executors — use resolve/reject
107
- // https://eslint.org/docs/latest/rules/no-promise-executor-return
108
- "no-promise-executor-return": "error",
109
- // Every Promise must be awaited, returned, or voided — prevents silent failures
110
- // https://typescript-eslint.io/rules/no-floating-promises
111
- "@typescript-eslint/no-floating-promises": [
112
- "error",
113
- { checkThenables: true, ignoreVoid: true }
114
- ]
115
- }
116
- },
117
- {
118
- name: "eslint-config-setup/ai-typescript",
119
- rules: {
120
- // ── C. TypeScript strictness — explicit types, safe patterns ──
121
- // Require explicit return types — self-documenting function signatures
122
- // https://typescript-eslint.io/rules/explicit-function-return-type
123
- "@typescript-eslint/explicit-function-return-type": [
124
- "error",
125
- {
126
- allowExpressions: true,
127
- allowTypedFunctionExpressions: true,
128
- allowHigherOrderFunctions: true,
129
- allowIIFEs: true
130
- }
131
- ],
132
- // Enforce consistent naming: camelCase for values, PascalCase for types,
133
- // is/has/can/should/will/did prefix for boolean variables
134
- // https://typescript-eslint.io/rules/naming-convention
135
- "@typescript-eslint/naming-convention": [
136
- "error",
137
- {
138
- selector: "variable",
139
- format: isReact ? ["strictCamelCase", "UPPER_CASE", "StrictPascalCase"] : ["strictCamelCase", "UPPER_CASE"],
140
- leadingUnderscore: "allowSingleOrDouble",
141
- trailingUnderscore: "allow",
142
- filter: { regex: "[- ]", match: false }
143
- },
144
- {
145
- selector: "function",
146
- format: isReact ? ["strictCamelCase", "StrictPascalCase"] : ["strictCamelCase"]
147
- },
148
- {
149
- selector: "parameter",
150
- format: ["strictCamelCase"],
151
- leadingUnderscore: "allow"
152
- },
153
- {
154
- selector: "import",
155
- format: ["strictCamelCase", "StrictPascalCase", "UPPER_CASE"]
156
- },
157
- {
158
- selector: [
159
- "classProperty",
160
- "parameterProperty",
161
- "classMethod",
162
- "objectLiteralMethod",
163
- "typeMethod",
164
- "accessor"
165
- ],
166
- format: ["strictCamelCase"],
167
- leadingUnderscore: "allowSingleOrDouble",
168
- trailingUnderscore: "allow",
169
- filter: { regex: "[- ]", match: false }
170
- },
171
- {
172
- selector: ["objectLiteralProperty", "typeProperty"],
173
- format: null
174
- },
175
- {
176
- selector: "typeLike",
177
- format: ["StrictPascalCase"]
178
- },
179
- {
180
- // No "I" prefix on interfaces — use descriptive names (XO convention)
181
- selector: "interface",
182
- format: ["StrictPascalCase"],
183
- custom: { regex: "^I[A-Z]", match: false }
184
- },
185
- {
186
- // Type parameters: single uppercase letter (T) or PascalCase (TResult)
187
- selector: "typeParameter",
188
- format: ["PascalCase"],
189
- custom: { regex: "^(T([A-Z][a-zA-Z]*)?|[A-Z])$", match: true }
190
- },
191
- {
192
- selector: "variable",
193
- types: ["boolean"],
194
- format: ["StrictPascalCase"],
195
- prefix: ["is", "has", "can", "should", "will", "did"]
196
- },
197
- {
198
- selector: ["classProperty", "objectLiteralProperty"],
199
- format: null,
200
- modifiers: ["requiresQuotes"]
201
- }
202
- ],
203
- // Enforce `import type { T }` — types are erased at compile time
204
- // https://typescript-eslint.io/rules/consistent-type-imports
205
- "@typescript-eslint/consistent-type-imports": [
206
- "error",
207
- { fixStyle: "inline-type-imports" }
208
- ],
209
- // Enforce `export type { T }` — matches import convention
210
- // https://typescript-eslint.io/rules/consistent-type-exports
211
- "@typescript-eslint/consistent-type-exports": [
212
- "error",
213
- { fixMixedExportsWithInlineTypeSpecifier: true }
214
- ],
215
- // Disallow `any` type — auto-fix to `unknown` for type safety
216
- // https://typescript-eslint.io/rules/no-explicit-any
217
- "@typescript-eslint/no-explicit-any": ["error", { fixToUnknown: true }],
218
- // Prefer readonly for unmodified class properties — signals immutability
219
- // https://typescript-eslint.io/rules/prefer-readonly
220
- "@typescript-eslint/prefer-readonly": "error",
221
- // Functions returning promises must be async — consistent async patterns
222
- // https://typescript-eslint.io/rules/promise-function-async
223
- "@typescript-eslint/promise-function-async": "error",
224
- // Exhaustive switch statements — no missing cases, no unnecessary defaults
225
- // https://typescript-eslint.io/rules/switch-exhaustiveness-check
226
- "@typescript-eslint/switch-exhaustiveness-check": [
227
- "error",
228
- {
229
- allowDefaultCaseForExhaustiveSwitch: false,
230
- requireDefaultForNonUnion: true
231
- }
232
- ],
233
- // Disallow unsafe type assertions (as Type) — use type guards instead
234
- // https://typescript-eslint.io/rules/no-unsafe-type-assertion
235
- "@typescript-eslint/no-unsafe-type-assertion": "error",
236
- // Require comparator for Array.sort() — prevents locale-dependent string sort
237
- // https://typescript-eslint.io/rules/require-array-sort-compare
238
- "@typescript-eslint/require-array-sort-compare": [
239
- "error",
240
- { ignoreStringArrays: true }
241
- ],
242
- // Require explicit `public`/`private`/`protected` on class members
243
- // https://typescript-eslint.io/rules/explicit-member-accessibility
244
- "@typescript-eslint/explicit-member-accessibility": "error",
245
- // Enforce property style for method signatures — prevents bivariance issues
246
- // https://typescript-eslint.io/rules/method-signature-style
247
- "@typescript-eslint/method-signature-style": ["error", "property"],
248
- // Require explicit values for enum members — prevents accidental shifts on reorder
249
- // https://typescript-eslint.io/rules/prefer-enum-initializers
250
- "@typescript-eslint/prefer-enum-initializers": "error",
251
- // Prefer `type` over `interface` — consistent, supports unions/intersections
252
- // https://typescript-eslint.io/rules/consistent-type-definitions
253
- "@typescript-eslint/consistent-type-definitions": ["error", "type"],
254
- // Enforce consistent member ordering in classes and interfaces
255
- // Static → fields by visibility → constructors → methods by visibility (XO convention)
256
- // https://typescript-eslint.io/rules/member-ordering
257
- "@typescript-eslint/member-ordering": [
258
- "warn",
259
- {
260
- default: [
261
- // Index signature
262
- "signature",
263
- "call-signature",
264
- // Static
265
- "public-static-field",
266
- "protected-static-field",
267
- "private-static-field",
268
- "#private-static-field",
269
- "static-field",
270
- "public-static-method",
271
- "protected-static-method",
272
- "private-static-method",
273
- "#private-static-method",
274
- "static-method",
275
- // Fields
276
- "public-decorated-field",
277
- "protected-decorated-field",
278
- "private-decorated-field",
279
- "public-instance-field",
280
- "protected-instance-field",
281
- "private-instance-field",
282
- "#private-instance-field",
283
- "public-abstract-field",
284
- "protected-abstract-field",
285
- "field",
286
- // Constructors
287
- "public-constructor",
288
- "protected-constructor",
289
- "private-constructor",
290
- "constructor",
291
- // Getters/Setters
292
- ["public-get", "public-set"],
293
- ["protected-get", "protected-set"],
294
- ["private-get", "private-set"],
295
- ["#private-get", "#private-set"],
296
- // Methods
297
- "public-decorated-method",
298
- "protected-decorated-method",
299
- "private-decorated-method",
300
- "public-instance-method",
301
- "protected-instance-method",
302
- "private-instance-method",
303
- "#private-instance-method",
304
- "public-abstract-method",
305
- "protected-abstract-method",
306
- "method"
307
- ]
308
- }
309
- ]
310
- }
311
- },
312
- {
313
- name: "eslint-config-setup/ai-unicorn",
314
- rules: {
315
- // ── D. Unicorn — modern, idiomatic patterns ───────────────────
316
- // Move functions to the smallest scope where they're used
317
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-function-scoping.md
318
- "unicorn/consistent-function-scoping": "error",
319
- // Forbid blanket `/* eslint-disable */` — must specify rules
320
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-abusive-eslint-disable.md
321
- "unicorn/no-abusive-eslint-disable": "error",
322
- // No .forEach() — use for-of loop (breakable, async-safe)
323
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-for-each.md
324
- "unicorn/no-array-for-each": "error",
325
- // No .reduce() — explicit loops are more readable
326
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-reduce.md
327
- "unicorn/no-array-reduce": "error",
328
- // Prefer simple ternary over if/else for single-line assignments
329
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-ternary.md
330
- "unicorn/prefer-ternary": ["error", "only-single-line"],
331
- // Prefer switch for 3+ conditions on same variable — structured
332
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-switch.md
333
- "unicorn/prefer-switch": ["error", { minimumCases: 3 }],
334
- // Enforce camelCase or PascalCase filenames — consistent project structure
335
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
336
- "unicorn/filename-case": [
337
- "error",
338
- { cases: { camelCase: true, pascalCase: true } }
339
- ],
340
- // Prevent abbreviated variable names (e → error, btn → button) — readable
341
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prevent-abbreviations.md
342
- "unicorn/prevent-abbreviations": "error",
343
- // Detect useless switch cases that fall through to the next case
344
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-switch-case.md
345
- "unicorn/no-useless-switch-case": "error",
346
- // Enforce correct Error subclassing (name, constructor pattern)
347
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/custom-error-definition.md
348
- "unicorn/custom-error-definition": "error",
349
- // Prefer default parameters over manual reassignment
350
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-default-parameters.md
351
- "unicorn/prefer-default-parameters": "error",
352
- // Prefer `a || b` over `a ? a : b` — simpler when equivalent
353
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-logical-operator-over-ternary.md
354
- "unicorn/prefer-logical-operator-over-ternary": "error",
355
- // Prefer Math.min()/Math.max() over ternaries for clamping
356
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-math-min-max.md
357
- "unicorn/prefer-math-min-max": "error",
358
- // Prefer Set#size over converting to array — direct and correct
359
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-set-size.md
360
- "unicorn/prefer-set-size": "error",
361
- // Enforce explicit `.length > 0` / `.length === 0` checks
362
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/explicit-length-check.md
363
- "unicorn/explicit-length-check": "error",
364
- // Prefer for-of over C-style for loops — no off-by-one risk
365
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-for-loop.md
366
- "unicorn/no-for-loop": "error",
367
- // Enforce braces in switch cases — prevents scope leakage
368
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/switch-case-braces.md
369
- "unicorn/switch-case-braces": "error",
370
- // Combine multiple .push() calls into one — cleaner
371
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-push-push.md
372
- "unicorn/no-array-push-push": "error"
373
- }
374
- },
375
- {
376
- name: "eslint-config-setup/ai-sonarjs",
377
- rules: {
378
- // ── E. SonarJS — code quality and duplicates ──────────────────
379
- // Detect copy-pasted functions — extract to shared helper
380
- // https://sonarsource.github.io/rspec/#/rspec/S4144/javascript
381
- "sonarjs/no-identical-functions": "error",
382
- // Merge nested if-statements that can be combined — reduce nesting
383
- // https://sonarsource.github.io/rspec/#/rspec/S1066/javascript
384
- "sonarjs/no-collapsible-if": "error",
385
- // Simplify redundant boolean expressions
386
- // https://sonarsource.github.io/rspec/#/rspec/S1125/javascript
387
- "sonarjs/no-redundant-boolean": "error",
388
- // Detect collections that are populated but never read — dead code
389
- // https://sonarsource.github.io/rspec/#/rspec/S4030/javascript
390
- "sonarjs/no-unused-collection": "error",
391
- // Return value directly instead of storing in temp variable
392
- // https://sonarsource.github.io/rspec/#/rspec/S1488/javascript
393
- "sonarjs/prefer-immediate-return": "error",
394
- // Simplify boolean return patterns
395
- // https://sonarsource.github.io/rspec/#/rspec/S1126/javascript
396
- "sonarjs/prefer-single-boolean-return": "error",
397
- // Detect identical sub-expressions on both sides of operator
398
- // https://sonarsource.github.io/rspec/#/rspec/S1764/javascript
399
- "sonarjs/no-identical-expressions": "error",
400
- // Simplify negated boolean checks — `!a !== b` → `a === b`
401
- // https://sonarsource.github.io/rspec/#/rspec/S1940/javascript
402
- "sonarjs/no-inverted-boolean-check": "error",
403
- // Disallow nested switch statements — extract to function
404
- // https://sonarsource.github.io/rspec/#/rspec/S1821/javascript
405
- "sonarjs/no-nested-switch": "error",
406
- // Disallow nested template literals — unreadable
407
- // https://sonarsource.github.io/rspec/#/rspec/S4624/javascript
408
- "sonarjs/no-nested-template-literals": "error",
409
- // Limit union type size — too many members signals missing abstraction
410
- // https://sonarsource.github.io/rspec/#/rspec/S4622/javascript
411
- "sonarjs/max-union-size": ["error", { threshold: 5 }],
412
- // Prefer type predicates for type narrowing — safer than assertions
413
- // https://sonarsource.github.io/rspec/#/rspec/S4322/javascript
414
- "sonarjs/prefer-type-guard": "error",
415
- // Public static fields should be readonly — prevents accidental mutation
416
- // https://sonarsource.github.io/rspec/#/rspec/S1444/javascript
417
- "sonarjs/public-static-readonly": "error"
418
- }
419
- },
420
- {
421
- name: "eslint-config-setup/ai-regexp",
422
- rules: {
423
- // Prefer lookarounds over capturing groups used only for context
424
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-lookaround.html
425
- "regexp/prefer-lookaround": "error",
426
- // Prefer named backreferences — \k<quote> over \1
427
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-backreference.html
428
- "regexp/prefer-named-backreference": "error",
429
- // Prefer named replacement — $<name> over $1
430
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-named-replacement.html
431
- "regexp/prefer-named-replacement": "error",
432
- // Prefer quantifier shorthand — a{3} over aaa
433
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-quantifier.html
434
- "regexp/prefer-quantifier": "error",
435
- // Prefer match.groups.name over match[1] — consistent named groups usage
436
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-result-array-groups.html
437
- "regexp/prefer-result-array-groups": "error",
438
- // Require v flag (unicode sets) over u flag — stricter ES2024 superset
439
- // https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-sets-regexp.html
440
- "regexp/require-unicode-sets-regexp": "error"
441
- }
442
- },
443
- {
444
- name: "eslint-config-setup/ai-jsdoc",
445
- rules: {
446
- // Prevent comments that just repeat the name — `/** The name */ name: string`
447
- // https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/informative-docs.md
448
- "jsdoc/informative-docs": "error",
449
- // AI should document parameters and return values completely
450
- "jsdoc/require-param": "error",
451
- "jsdoc/require-returns": "error"
452
- }
453
- },
454
- {
455
- name: "eslint-config-setup/ai-node",
456
- rules: {
457
- // Warn when using Node.js builtins not available in the target version
458
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unsupported-features/node-builtins.md
459
- "node/no-unsupported-features/node-builtins": "error"
460
- }
461
- },
462
- {
463
- name: "eslint-config-setup/ai-react",
464
- rules: {
465
- // No click handlers on static elements without role — use semantic HTML
466
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md
467
- "jsx-a11y/no-static-element-interactions": "error",
468
- // No event handlers on non-interactive elements — use button/link instead
469
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-noninteractive-element-interactions.md
470
- "jsx-a11y/no-noninteractive-element-interactions": "error",
471
- // Interactive elements (role="button") must be focusable
472
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/interactive-supports-focus.md
473
- "jsx-a11y/interactive-supports-focus": "error"
474
- }
475
- },
476
- {
477
- name: "eslint-config-setup/ai-complexity",
478
- rules: {
479
- // Cyclomatic complexity limit — max branches per function
480
- // https://eslint.org/docs/latest/rules/complexity
481
- complexity: ["error", 10],
482
- // Max nesting depth — deep nesting signals need for extraction
483
- // https://eslint.org/docs/latest/rules/max-depth
484
- "max-depth": ["error", 3],
485
- // Max nested callbacks — prevents callback hell
486
- // https://eslint.org/docs/latest/rules/max-nested-callbacks
487
- "max-nested-callbacks": ["error", 2],
488
- // Max function parameters — many params suggest a config object
489
- // https://eslint.org/docs/latest/rules/max-params
490
- "max-params": ["error", 3],
491
- // Max statements per function — keeps functions focused
492
- // https://eslint.org/docs/latest/rules/max-statements
493
- "max-statements": ["error", 15],
494
- // Max lines per function — encourages extraction of helpers
495
- // https://eslint.org/docs/latest/rules/max-lines-per-function
496
- "max-lines-per-function": [
497
- "error",
498
- {
499
- max: 50,
500
- skipBlankLines: true,
501
- skipComments: true
502
- }
503
- ],
504
- // Max lines per file — encourages modular file organization
505
- // https://eslint.org/docs/latest/rules/max-lines
506
- "max-lines": [
507
- "error",
508
- {
509
- max: 300,
510
- skipBlankLines: true,
511
- skipComments: true
512
- }
513
- ],
514
- // Cognitive complexity — measures how hard a function is to understand
515
- // https://sonarsource.github.io/rspec/#/rspec/S3776/javascript
516
- "sonarjs/cognitive-complexity": ["error", 10]
517
- }
518
- },
519
- {
520
- name: "eslint-config-setup/ai-tests-strict",
521
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
522
- rules: {
523
- // Every test must be inside a describe block — organized test suites
524
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/require-top-level-describe.md
525
- "vitest/require-top-level-describe": "error",
526
- // Hooks (beforeEach, afterEach) must be at the top of describe — predictable setup
527
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-hooks-on-top.md
528
- "vitest/prefer-hooks-on-top": "error"
529
- }
530
- },
531
- {
532
- name: "eslint-config-setup/ai-tests-relaxed",
533
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
534
- rules: {
535
- "max-lines": "off",
536
- "max-lines-per-function": "off",
537
- "max-statements": "off",
538
- "max-nested-callbacks": "off",
539
- "@typescript-eslint/no-magic-numbers": "off",
540
- "sonarjs/no-duplicate-string": "off",
541
- "@typescript-eslint/explicit-function-return-type": "off",
542
- "@typescript-eslint/naming-convention": "off",
543
- "unicorn/prevent-abbreviations": "off"
544
- }
545
- },
546
- {
547
- name: "eslint-config-setup/ai-e2e-relaxed",
548
- files: ["**/*.spec.ts"],
549
- rules: {
550
- "max-lines": "off",
551
- "max-lines-per-function": "off",
552
- "max-statements": "off",
553
- "@typescript-eslint/no-magic-numbers": "off",
554
- "@typescript-eslint/explicit-function-return-type": "off"
555
- }
556
- },
557
- {
558
- name: "eslint-config-setup/ai-config-relaxed",
559
- files: [
560
- "**/*.config.{ts,mts,cts,js,mjs,cjs}",
561
- "**/vite.config.*",
562
- "**/vitest.config.*",
563
- "**/next.config.*"
564
- ],
565
- rules: {
566
- complexity: "off",
567
- "max-lines": "off",
568
- "max-lines-per-function": "off",
569
- "max-statements": "off",
570
- "@typescript-eslint/no-magic-numbers": "off",
571
- "@typescript-eslint/explicit-function-return-type": "off",
572
- "@typescript-eslint/naming-convention": "off"
573
- }
574
- },
575
- {
576
- name: "eslint-config-setup/ai-declarations-relaxed",
577
- files: ["**/*.d.ts"],
578
- rules: {
579
- "@typescript-eslint/explicit-function-return-type": "off",
580
- "@typescript-eslint/naming-convention": "off",
581
- "@typescript-eslint/no-explicit-any": "off",
582
- "@typescript-eslint/no-magic-numbers": "off",
583
- "unicorn/prevent-abbreviations": "off",
584
- "unicorn/filename-case": "off"
585
- }
586
- }
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
587
78
  ];
588
- return configs;
589
79
  }
590
80
 
591
81
  // src/configs/base.ts
@@ -593,7 +83,7 @@ import eslint from "@eslint/js";
593
83
 
594
84
  // src/build/config-builder.ts
595
85
  function createConfig(options) {
596
- const presetRules = /* @__PURE__ */ new Map();
86
+ const presetRules2 = /* @__PURE__ */ new Map();
597
87
  const presetPlugins = {};
598
88
  if (options.presets) {
599
89
  for (const preset of options.presets) {
@@ -603,7 +93,7 @@ function createConfig(options) {
603
93
  if (preset.rules) {
604
94
  for (const [name, value] of Object.entries(preset.rules)) {
605
95
  if (value !== void 0) {
606
- presetRules.set(name, value);
96
+ presetRules2.set(name, value);
607
97
  }
608
98
  }
609
99
  }
@@ -616,7 +106,7 @@ function createConfig(options) {
616
106
  const fileOverrides = [];
617
107
  const builder = {
618
108
  overrideRule(name, value) {
619
- if (!presetRules.has(name)) {
109
+ if (!presetRules2.has(name)) {
620
110
  throw new Error(
621
111
  `overrideRule("${name}"): rule not found in preset. Cannot override a rule that doesn't exist in the preset.`
622
112
  );
@@ -624,8 +114,34 @@ function createConfig(options) {
624
114
  overrides.set(name, value);
625
115
  return builder;
626
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
+ },
627
143
  addRule(name, value) {
628
- if (presetRules.has(name)) {
144
+ if (presetRules2.has(name)) {
629
145
  throw new Error(
630
146
  `addRule("${name}"): rule already exists in preset. Use overrideRule() to change its value, or removeRule() to drop it.`
631
147
  );
@@ -639,7 +155,7 @@ function createConfig(options) {
639
155
  return builder;
640
156
  },
641
157
  disableRule(name) {
642
- if (!presetRules.has(name) && !additions.has(name)) {
158
+ if (!presetRules2.has(name) && !additions.has(name)) {
643
159
  throw new Error(
644
160
  `disableRule("${name}"): rule not found in preset or additions. Cannot disable a rule that doesn't exist.`
645
161
  );
@@ -648,7 +164,7 @@ function createConfig(options) {
648
164
  return builder;
649
165
  },
650
166
  removeRule(name) {
651
- if (!presetRules.has(name) && !additions.has(name)) {
167
+ if (!presetRules2.has(name) && !additions.has(name)) {
652
168
  throw new Error(
653
169
  `removeRule("${name}"): rule not found in preset or additions. Cannot remove a rule that doesn't exist.`
654
170
  );
@@ -662,7 +178,7 @@ function createConfig(options) {
662
178
  },
663
179
  build() {
664
180
  const rules = {};
665
- for (const [name, value] of presetRules) {
181
+ for (const [name, value] of presetRules2) {
666
182
  if (!removed.has(name)) {
667
183
  rules[name] = value;
668
184
  }
@@ -721,14 +237,147 @@ function createConfig(options) {
721
237
  }
722
238
 
723
239
  // src/configs/base.ts
724
- function baseConfig() {
725
- 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({
726
350
  name: "eslint-config-setup/base",
727
351
  presets: [eslint.configs.recommended]
728
- }).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", [
729
- "error",
730
- { disallowRedundantWrapping: true }
731
- ]).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
+ ];
732
381
  }
733
382
 
734
383
  // src/configs/cspell.ts
@@ -788,6 +437,7 @@ function importsConfig() {
788
437
  {
789
438
  name: "eslint-config-setup/imports",
790
439
  plugins: {
440
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Plugin type is compatible
791
441
  "import": importXPlugin,
792
442
  "unused-imports": unusedImportsPlugin
793
443
  },
@@ -795,7 +445,7 @@ function importsConfig() {
795
445
  // ── Validation (import-x) ────────────────────────────────────
796
446
  // Merge duplicate import paths into one statement — reduces noise
797
447
  // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-duplicates.md
798
- "import/no-duplicates": ["error", { "prefer-inline": true }],
448
+ "import/no-duplicates": "error",
799
449
  // Forbid a module from importing itself — always a bug
800
450
  // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-self-import.md
801
451
  "import/no-self-import": "error",
@@ -826,6 +476,30 @@ function importsConfig() {
826
476
  // Forbid absolute file paths in imports — not portable across machines
827
477
  // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-absolute-path.md
828
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",
829
503
  // ── Disabled: ordering handled by perfectionist ────────────────
830
504
  // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/order.md
831
505
  "import/order": "off",
@@ -842,11 +516,18 @@ function importsConfig() {
842
516
 
843
517
  // src/configs/jsdoc.ts
844
518
  import jsdocPlugin from "eslint-plugin-jsdoc";
845
- function jsdocConfig() {
846
- return createConfig({
519
+ function jsdocConfig(opts) {
520
+ const isAi = opts?.ai ?? false;
521
+ const builder = createConfig({
847
522
  name: "eslint-config-setup/jsdoc",
848
523
  presets: [jsdocPlugin.configs["flat/recommended-typescript-error"]]
849
- }).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();
850
531
  }
851
532
 
852
533
  // src/configs/json.ts
@@ -910,6 +591,8 @@ function jsonConfig() {
910
591
  // src/configs/markdown.ts
911
592
  import * as mdxPlugin from "eslint-plugin-mdx";
912
593
  function markdownConfig() {
594
+ const codeBlockLanguageOptions = mdxPlugin.flatCodeBlocks.languageOptions ?? {};
595
+ const codeBlockParserOptions = codeBlockLanguageOptions.parserOptions ?? {};
913
596
  return [
914
597
  // ── MDX / Markdown parsing ─────────────────────────────────────
915
598
  {
@@ -924,6 +607,13 @@ function markdownConfig() {
924
607
  // Relaxes rules that don't apply to incomplete code snippets.
925
608
  {
926
609
  ...mdxPlugin.flatCodeBlocks,
610
+ languageOptions: {
611
+ ...codeBlockLanguageOptions,
612
+ parserOptions: {
613
+ ...codeBlockParserOptions,
614
+ projectService: false
615
+ }
616
+ },
927
617
  rules: {
928
618
  ...mdxPlugin.flatCodeBlocks.rules,
929
619
  // Snippets don't need trailing newlines
@@ -948,69 +638,76 @@ function markdownConfig() {
948
638
  // src/configs/node.ts
949
639
  import nodePlugin from "eslint-plugin-n";
950
640
  import globals from "globals";
951
- function nodeConfig() {
952
- return [
953
- {
954
- name: "eslint-config-setup/node",
955
- languageOptions: {
956
- globals: {
957
- ...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"
958
701
  }
959
- },
960
- plugins: {
961
- node: nodePlugin
962
- },
963
- rules: {
964
- // Detect usage of deprecated Node.js APIs (fs.exists, url.parse, etc.)
965
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-deprecated-api.md
966
- "node/no-deprecated-api": "error",
967
- // Prevent `module.exports = ...` assignment in ES modules
968
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-exports-assign.md
969
- "node/no-exports-assign": "error",
970
- // OFF: TypeScript resolves imports — this rule has false positives
971
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-import.md
972
- "node/no-missing-import": "off",
973
- // OFF: TypeScript resolves requires — this rule has false positives
974
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-missing-require.md
975
- "node/no-missing-require": "off",
976
- // Warn on process.exit() — prefer throwing errors for clean shutdown
977
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-process-exit.md
978
- "node/no-process-exit": "warn",
979
- // OFF: Too many false positives with monorepos and devDependencies
980
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-unpublished-import.md
981
- "node/no-unpublished-import": "off",
982
- // Validate hashbang lines — correct syntax, Unix linebreaks, only in entry files
983
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/hashbang.md
984
- "node/hashbang": "error",
985
- // Detect `__dirname + '/foo'` — use path.join() instead (breaks on Windows)
986
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-path-concat.md
987
- "node/no-path-concat": "error",
988
- // ── Prefer global builtins ────────────────────────────────────
989
- // Use global Buffer instead of require('buffer').Buffer
990
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/buffer.md
991
- "node/prefer-global/buffer": ["error", "always"],
992
- // Use global console — always available in Node.js
993
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/console.md
994
- "node/prefer-global/console": ["error", "always"],
995
- // Use global process — always available in Node.js
996
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/process.md
997
- "node/prefer-global/process": ["error", "always"],
998
- // Use global URL — available since Node.js 10
999
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/url.md
1000
- "node/prefer-global/url": ["error", "always"],
1001
- // Use global URLSearchParams — available since Node.js 10
1002
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-global/url-search-params.md
1003
- "node/prefer-global/url-search-params": ["error", "always"],
1004
- // ── Prefer promise-based APIs ─────────────────────────────────
1005
- // Use dns.promises instead of callback-based dns — modern async
1006
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-promises/dns.md
1007
- "node/prefer-promises/dns": "error",
1008
- // Use fs.promises instead of callback-based fs — modern async
1009
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/prefer-promises/fs.md
1010
- "node/prefer-promises/fs": "error"
1011
702
  }
1012
- }
1013
- ];
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();
1014
711
  }
1015
712
 
1016
713
  // src/configs/package-json.ts
@@ -1137,235 +834,326 @@ function prettierCompatConfig() {
1137
834
  }
1138
835
 
1139
836
  // src/configs/react.ts
1140
- import eslintReactPlugin from "@eslint-react/eslint-plugin";
837
+ import eslintReactPlugin2 from "@eslint-react/eslint-plugin";
1141
838
  import stylisticPlugin from "@stylistic/eslint-plugin";
1142
839
  import jsxA11yPlugin from "eslint-plugin-jsx-a11y";
1143
- import reactHooksPlugin from "eslint-plugin-react-hooks";
1144
840
  import reactRefreshPlugin from "eslint-plugin-react-refresh";
1145
841
  import globals2 from "globals";
1146
- function reactConfig() {
1147
- const reactDomPlugin = eslintReactPlugin.configs.dom.plugins;
1148
- const reactWebApiPlugin = eslintReactPlugin.configs["web-api"].plugins;
1149
- 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],
1150
1116
  {
1151
- name: "eslint-config-setup/react",
1152
- languageOptions: {
1153
- globals: {
1154
- ...globals2.browser
1155
- },
1156
- parserOptions: {
1157
- ecmaFeatures: {
1158
- jsx: true
1159
- }
1160
- }
1161
- },
1162
- plugins: {
1163
- // @eslint-react core registered as "react" (like import-x → "import")
1164
- react: eslintReactPlugin,
1165
- // @eslint-react/dom → registered as "react-dom"
1166
- "react-dom": reactDomPlugin["@eslint-react/dom"],
1167
- // @eslint-react/web-api → registered as "react-web-api"
1168
- "react-web-api": reactWebApiPlugin["@eslint-react/web-api"],
1169
- // eslint-plugin-react-hooks stays as-is
1170
- "react-hooks": reactHooksPlugin,
1171
- "@stylistic": stylisticPlugin,
1172
- "react-refresh": reactRefreshPlugin,
1173
- "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
1174
1140
  },
1175
- rules: {
1176
- // ── React core (react/) ─────────────────────────────────────────
1177
- // Prevent passing children as a prop — use JSX children syntax instead
1178
- // https://eslint-react.xyz/docs/rules/no-children-prop
1179
- "react/no-children-prop": "error",
1180
- // Prevent direct mutation of this.state — use setState instead
1181
- // https://eslint-react.xyz/docs/rules/no-direct-mutation-state
1182
- "react/no-direct-mutation-state": "error",
1183
- // Prevent unstable nested component definitions — causes remounts
1184
- // https://eslint-react.xyz/docs/rules/no-nested-component-definitions
1185
- "react/no-nested-component-definitions": "error",
1186
- // Require key prop in iterators — prevent reconciliation bugs
1187
- // https://eslint-react.xyz/docs/rules/no-missing-key
1188
- "react/no-missing-key": "error",
1189
- // Prevent duplicate key props in iterators
1190
- // https://eslint-react.xyz/docs/rules/no-duplicate-key
1191
- "react/no-duplicate-key": "error",
1192
- // Prevent comments from being inserted as text nodes in JSX
1193
- // https://eslint-react.xyz/docs/rules/jsx-no-comment-textnodes
1194
- "react/jsx-no-comment-textnodes": "error",
1195
- // Remove unnecessary JSX fragments — <>{x}</> → {x}
1196
- // https://eslint-react.xyz/docs/rules/no-useless-fragment
1197
- "react/no-useless-fragment": "error",
1198
- // Prefer <Foo active /> over <Foo active={true} /> — concise
1199
- // https://eslint-react.xyz/docs/rules/jsx-shorthand-boolean
1200
- "react/jsx-shorthand-boolean": "error",
1201
- // Prevent using array index as key — breaks reconciliation on reorder
1202
- // https://eslint-react.xyz/docs/rules/no-array-index-key
1203
- "react/no-array-index-key": "error",
1204
- // Prevent object/array literals as default props — creates new reference every render
1205
- // https://eslint-react.xyz/docs/rules/no-unstable-default-props
1206
- "react/no-unstable-default-props": "error",
1207
- // Prevent `{count && <Foo />}` — renders "0" when count is 0
1208
- // https://eslint-react.xyz/docs/rules/no-leaked-conditional-rendering
1209
- "react/no-leaked-conditional-rendering": "error",
1210
- // Prevent inline object creation in context providers — causes re-renders
1211
- // https://eslint-react.xyz/docs/rules/no-unstable-context-value
1212
- "react/no-unstable-context-value": "error",
1213
- // Prevent `this.setState({ count: this.state.count + 1 })` — race condition
1214
- // https://eslint-react.xyz/docs/rules/no-access-state-in-setstate
1215
- "react/no-access-state-in-setstate": "error",
1216
- // Detect state properties that are set but never read — dead code
1217
- // https://eslint-react.xyz/docs/rules/no-unused-state
1218
- "react/no-unused-state": "error",
1219
- // ── React 19 migration rules ────────────────────────────────────
1220
- // React 19: Use <Context> instead of <Context.Provider>
1221
- // https://eslint-react.xyz/docs/rules/no-context-provider
1222
- "react/no-context-provider": "error",
1223
- // React 19: Use ref as prop instead of forwardRef
1224
- // https://eslint-react.xyz/docs/rules/no-forward-ref
1225
- "react/no-forward-ref": "error",
1226
- // React 19: Use use() instead of useContext()
1227
- // https://eslint-react.xyz/docs/rules/no-use-context
1228
- "react/no-use-context": "error",
1229
- // ── React DOM (react-dom/) ──────────────────────────────────────
1230
- // Prevent unsafe target="_blank" links — requires rel="noreferrer"
1231
- // https://eslint-react.xyz/docs/rules/dom-no-unsafe-target-blank
1232
- "react-dom/no-unsafe-target-blank": "error",
1233
- // Prevent unknown DOM properties (e.g., class → className)
1234
- // https://eslint-react.xyz/docs/rules/dom-no-unknown-property
1235
- "react-dom/no-unknown-property": "error",
1236
- // Prevent void DOM elements (br, img, hr) from having children
1237
- // https://eslint-react.xyz/docs/rules/dom-no-void-elements-with-children
1238
- "react-dom/no-void-elements-with-children": "error",
1239
- // Require sandbox attribute on iframes — security best practice
1240
- // https://eslint-react.xyz/docs/rules/dom-no-missing-iframe-sandbox
1241
- "react-dom/no-missing-iframe-sandbox": "error",
1242
- // Prevent `style="color: red"` — must be an object in React
1243
- // https://eslint-react.xyz/docs/rules/dom-no-string-style-prop
1244
- "react-dom/no-string-style-prop": "error",
1245
- // Require explicit type on <button> — prevents unintended form submits
1246
- // https://eslint-react.xyz/docs/rules/dom-no-missing-button-type
1247
- "react-dom/no-missing-button-type": "error",
1248
- // Prevent dangerouslySetInnerHTML + children at the same time — conflict
1249
- // https://eslint-react.xyz/docs/rules/dom-no-dangerously-set-innerhtml-with-children
1250
- "react-dom/no-dangerously-set-innerhtml-with-children": "error",
1251
- // Warn on dangerouslySetInnerHTML — XSS risk, should be reviewed
1252
- // https://eslint-react.xyz/docs/rules/dom-no-dangerously-set-innerhtml
1253
- "react-dom/no-dangerously-set-innerhtml": "warn",
1254
- // ── React Web API (react-web-api/) — cleanup leak detection ─────
1255
- // Require cleanup for addEventListener in effects
1256
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-event-listener
1257
- "react-web-api/no-leaked-event-listener": "error",
1258
- // Require cleanup for setInterval in effects
1259
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-interval
1260
- "react-web-api/no-leaked-interval": "error",
1261
- // Require cleanup for setTimeout in effects
1262
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-timeout
1263
- "react-web-api/no-leaked-timeout": "error",
1264
- // Require cleanup for ResizeObserver in effects
1265
- // https://eslint-react.xyz/docs/rules/web-api-no-leaked-resize-observer
1266
- "react-web-api/no-leaked-resize-observer": "error",
1267
- // ── React Hooks (react-hooks/) — eslint-plugin-react-hooks ──────
1268
- // Enforce Rules of Hooks — hooks must be called at the top level
1269
- // https://react.dev/reference/rules/rules-of-hooks
1270
- "react-hooks/rules-of-hooks": "error",
1271
- // Verify dependency arrays in useEffect/useMemo/useCallback
1272
- // https://react.dev/reference/react/useEffect#specifying-reactive-dependencies
1273
- "react-hooks/exhaustive-deps": "error",
1274
- // ── @stylistic — JSX formatting rules ───────────────────────────
1275
- // Enforce self-closing tags for components without children — <Foo />
1276
- // https://eslint.style/rules/jsx/jsx-self-closing-comp
1277
- "@stylistic/jsx-self-closing-comp": "error",
1278
- // Prevent unnecessary string curly braces: title={"foo"} → title="foo"
1279
- // https://eslint.style/rules/jsx/jsx-curly-brace-presence
1280
- "@stylistic/jsx-curly-brace-presence": [
1281
- "error",
1282
- { props: "never", children: "never" }
1283
- ],
1284
- // ── React Refresh (Fast Refresh / HMR) ────────────────────────
1285
- // Ensure components are exported in a way that supports Fast Refresh.
1286
- // Mixed exports (component + constants) break HMR state preservation.
1287
- // allowConstantExport: Vite handles constant exports without breaking HMR.
1288
- // https://github.com/ArnaudBarre/eslint-plugin-react-refresh
1289
- "react-refresh/only-export-components": [
1290
- "warn",
1291
- { allowConstantExport: true }
1292
- ],
1293
- // ── JSX Accessibility (a11y) ──────────────────────────────────
1294
- // Require alt text on img, area, input[type="image"], object
1295
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/alt-text.md
1296
- "jsx-a11y/alt-text": "error",
1297
- // Require anchor content — empty links are inaccessible
1298
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-has-content.md
1299
- "jsx-a11y/anchor-has-content": "error",
1300
- // Require valid href on anchors — no `#` or `javascript:` void
1301
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/anchor-is-valid.md
1302
- "jsx-a11y/anchor-is-valid": "error",
1303
- // Active descendant elements must be focusable (tabindex)
1304
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-activedescendant-has-tabindex.md
1305
- "jsx-a11y/aria-activedescendant-has-tabindex": "error",
1306
- // Validate aria-* attributes exist — catches typos in ARIA props
1307
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-props.md
1308
- "jsx-a11y/aria-props": "error",
1309
- // Validate aria-* values match their type (boolean, token, etc.)
1310
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-proptypes.md
1311
- "jsx-a11y/aria-proptypes": "error",
1312
- // Validate role attribute values — catches invalid role names
1313
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-role.md
1314
- "jsx-a11y/aria-role": "error",
1315
- // Prevent ARIA on elements that don't support it (meta, script, style)
1316
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/aria-unsupported-elements.md
1317
- "jsx-a11y/aria-unsupported-elements": "error",
1318
- // Click handlers must have keyboard equivalent — keyboard accessibility
1319
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/click-events-have-key-events.md
1320
- "jsx-a11y/click-events-have-key-events": "error",
1321
- // Heading elements must have content — screen readers need it
1322
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/heading-has-content.md
1323
- "jsx-a11y/heading-has-content": "error",
1324
- // Require lang attribute on <html> — language identification
1325
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/html-has-lang.md
1326
- "jsx-a11y/html-has-lang": "error",
1327
- // Prevent redundant alt text like "image of..." — screen readers add this
1328
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/img-redundant-alt.md
1329
- "jsx-a11y/img-redundant-alt": "error",
1330
- // Every form label must be associated with a control
1331
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/label-has-associated-control.md
1332
- "jsx-a11y/label-has-associated-control": "error",
1333
- // Mouse event handlers must have keyboard equivalents
1334
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/mouse-events-have-key-events.md
1335
- "jsx-a11y/mouse-events-have-key-events": "error",
1336
- // No accessKey attribute — inconsistent shortcuts confuse users
1337
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-access-key.md
1338
- "jsx-a11y/no-access-key": "error",
1339
- // No autofocus attribute (except non-DOM) — disorienting for screen readers
1340
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-autofocus.md
1341
- "jsx-a11y/no-autofocus": ["error", { ignoreNonDOM: true }],
1342
- // No <marquee>/<blink> — deprecated distracting elements
1343
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-distracting-elements.md
1344
- "jsx-a11y/no-distracting-elements": "error",
1345
- // No redundant ARIA roles (e.g., <button role="button">)
1346
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-redundant-roles.md
1347
- "jsx-a11y/no-redundant-roles": "error",
1348
- // Role elements must have all required ARIA props
1349
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/role-has-required-aria-props.md
1350
- "jsx-a11y/role-has-required-aria-props": "error",
1351
- // Don't use unsupported ARIA attributes for a given role
1352
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/role-supports-aria-props.md
1353
- "jsx-a11y/role-supports-aria-props": "error",
1354
- // scope attribute only on <th> elements — HTML specification
1355
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/scope.md
1356
- "jsx-a11y/scope": "error",
1357
- // No positive tabIndex values — disrupts natural tab order
1358
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/tabindex-no-positive.md
1359
- "jsx-a11y/tabindex-no-positive": "error",
1360
- // Validate lang attribute values — e.g. "de" ok, "deutsch" not
1361
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/lang.md
1362
- "jsx-a11y/lang": "error",
1363
- // Validate autocomplete attribute values on form elements
1364
- // https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/autocomplete-valid.md
1365
- "jsx-a11y/autocomplete-valid": "error"
1141
+ parserOptions: {
1142
+ ecmaFeatures: {
1143
+ jsx: true
1144
+ }
1366
1145
  }
1367
1146
  }
1368
- ];
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();
1369
1157
  }
1370
1158
 
1371
1159
  // src/configs/react-effect.ts
@@ -1412,11 +1200,21 @@ function reactEffectConfig() {
1412
1200
 
1413
1201
  // src/configs/regexp.ts
1414
1202
  import { configs as regexpConfigs } from "eslint-plugin-regexp";
1415
- function regexpConfig() {
1416
- return createConfig({
1203
+ function regexpConfig(opts) {
1204
+ const isAi = opts?.ai ?? false;
1205
+ const builder = createConfig({
1417
1206
  name: "eslint-config-setup/regexp",
1418
1207
  presets: [regexpConfigs["flat/recommended"]]
1419
- }).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();
1420
1218
  }
1421
1219
 
1422
1220
  // src/configs/security.ts
@@ -1482,13 +1280,12 @@ function securityConfig() {
1482
1280
 
1483
1281
  // src/configs/sonarjs.ts
1484
1282
  import sonarjsPlugin from "eslint-plugin-sonarjs";
1485
- function sonarjsConfig() {
1486
- return [
1487
- {
1488
- name: "eslint-config-setup/sonarjs",
1489
- plugins: {
1490
- sonarjs: sonarjsPlugin
1491
- },
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 },
1492
1289
  rules: {
1493
1290
  // Detect copy-pasted functions — extract to shared helper instead
1494
1291
  // https://sonarsource.github.io/rspec/#/rspec/S4144/javascript
@@ -1551,13 +1348,24 @@ function sonarjsConfig() {
1551
1348
  // https://sonarsource.github.io/rspec/#/rspec/S6418/javascript
1552
1349
  "sonarjs/no-hardcoded-secrets": "warn"
1553
1350
  }
1554
- }
1555
- ];
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();
1556
1362
  }
1557
1363
 
1558
1364
  // src/configs/typescript.ts
1559
1365
  import tseslint from "typescript-eslint";
1560
- function typescriptConfig() {
1366
+ function typescriptConfig(opts) {
1367
+ const isAi = opts?.ai ?? false;
1368
+ const isReact = opts?.react ?? false;
1561
1369
  const typeChecked = tseslint.configs.strictTypeChecked;
1562
1370
  const stylistic = tseslint.configs.stylisticTypeChecked;
1563
1371
  const structuralBlocks = typeChecked.slice(0, 2);
@@ -1566,6 +1374,8 @@ function typescriptConfig() {
1566
1374
  name: "eslint-config-setup/typescript",
1567
1375
  passthrough: structuralBlocks,
1568
1376
  presets: ruleBlocks,
1377
+ files: [...TYPESCRIPT_SOURCE_FILES],
1378
+ ignores: [...MARKDOWN_CODE_BLOCK_FILES],
1569
1379
  languageOptions: {
1570
1380
  parserOptions: {
1571
1381
  // Use project service for automatic tsconfig resolution
@@ -1594,10 +1404,25 @@ function typescriptConfig() {
1594
1404
  "error",
1595
1405
  "in-try-catch"
1596
1406
  ]);
1597
- 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
+ });
1598
1423
  builder.addRule("@typescript-eslint/consistent-type-imports", [
1599
1424
  "error",
1600
- { fixStyle: "inline-type-imports" }
1425
+ { fixStyle: isAi ? "inline-type-imports" : "separate-type-imports" }
1601
1426
  ]);
1602
1427
  builder.addRule("@typescript-eslint/consistent-type-exports", [
1603
1428
  "error",
@@ -1611,6 +1436,24 @@ function typescriptConfig() {
1611
1436
  "error"
1612
1437
  );
1613
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
+ ]);
1614
1457
  builder.addRule("no-shadow", "off");
1615
1458
  builder.addRule("@typescript-eslint/no-shadow", [
1616
1459
  "error",
@@ -1621,10 +1464,7 @@ function typescriptConfig() {
1621
1464
  ignoreFunctionTypeParameterNameValueShadow: true
1622
1465
  }
1623
1466
  ]);
1624
- builder.overrideRule("@typescript-eslint/restrict-template-expressions", [
1625
- "error",
1626
- { allowNumber: true }
1627
- ]);
1467
+ builder.overrideOptions("@typescript-eslint/restrict-template-expressions", { allowNumber: true });
1628
1468
  builder.addRule("@typescript-eslint/strict-boolean-expressions", [
1629
1469
  "error",
1630
1470
  { allowNullableBoolean: true, allowNullableObject: true }
@@ -1634,18 +1474,168 @@ function typescriptConfig() {
1634
1474
  ["**/*.{js,mjs,cjs}"],
1635
1475
  tseslint.configs.disableTypeChecked.rules ?? {}
1636
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
+ }
1637
1628
  return builder.build();
1638
1629
  }
1639
1630
 
1640
1631
  // src/configs/unicorn.ts
1641
1632
  import unicornPlugin from "eslint-plugin-unicorn";
1642
- function unicornConfig() {
1643
- return [
1644
- {
1645
- name: "eslint-config-setup/unicorn",
1646
- plugins: {
1647
- unicorn: unicornPlugin
1648
- },
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 },
1649
1639
  rules: {
1650
1640
  // ── Error prevention ──────────────────────────────────────────
1651
1641
  // Forbid `/* eslint-disable */` without specific rule — too broad
@@ -1675,9 +1665,11 @@ function unicornConfig() {
1675
1665
  // Remove useless Promise.resolve/reject wrappers — simplify async code
1676
1666
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-promise-resolve-reject.md
1677
1667
  "unicorn/no-useless-promise-resolve-reject": "error",
1678
- // 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`.
1679
1671
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-undefined.md
1680
- "unicorn/no-useless-undefined": "error",
1672
+ "unicorn/no-useless-undefined": ["warn", { checkArguments: false }],
1681
1673
  // Disallow `await` in Promise.all/race arguments — already a promise
1682
1674
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-await-in-promise-methods.md
1683
1675
  "unicorn/no-await-in-promise-methods": "error",
@@ -1733,6 +1725,28 @@ function unicornConfig() {
1733
1725
  // Enforce consistent style for indexOf/findIndex existence checks
1734
1726
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-existence-index-check.md
1735
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",
1736
1750
  // ── Modern API preferences ────────────────────────────────────
1737
1751
  // Prefer .flatMap() over .map().flat() — single pass, more readable
1738
1752
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-flat-map.md
@@ -1758,6 +1772,9 @@ function unicornConfig() {
1758
1772
  // Prefer .before()/.after()/.replaceWith() over parent.insertBefore()
1759
1773
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-modern-dom-apis.md
1760
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",
1761
1778
  // Prefer Math.log10/Math.hypot over manual math — accurate and readable
1762
1779
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-modern-math-apis.md
1763
1780
  "unicorn/prefer-modern-math-apis": "error",
@@ -1863,8 +1880,27 @@ function unicornConfig() {
1863
1880
  // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-single-call.md
1864
1881
  "unicorn/prefer-single-call": "off"
1865
1882
  }
1866
- }
1867
- ];
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();
1868
1904
  }
1869
1905
 
1870
1906
  // src/overrides/config-files.ts
@@ -2097,136 +2133,165 @@ function storiesOverride() {
2097
2133
  // src/overrides/tests.ts
2098
2134
  import vitestPlugin from "@vitest/eslint-plugin";
2099
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
+ };
2100
2250
  function testsOverride() {
2101
- const configs = [
2251
+ return [
2102
2252
  {
2103
2253
  name: "eslint-config-setup/tests",
2104
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
2254
+ files: TEST_FILES2,
2105
2255
  plugins: {
2106
2256
  vitest: vitestPlugin
2107
2257
  },
2108
- rules: {
2109
- // ── Vitest rules ──────────────────────────────────────────────
2110
- // Every test must contain at least one assertion
2111
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/expect-expect.md
2112
- "vitest/expect-expect": "error",
2113
- // Prevent duplicate test titles within a describe block
2114
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-identical-title.md
2115
- "vitest/no-identical-title": "error",
2116
- // No it.only / describe.only — prevents accidentally skipping tests in CI
2117
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-focused-tests.md
2118
- "vitest/no-focused-tests": "error",
2119
- // Warn on it.skip / describe.skip — track disabled tests
2120
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-disabled-tests.md
2121
- "vitest/no-disabled-tests": "warn",
2122
- // No duplicate beforeEach/afterEach hooks — merge them
2123
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-duplicate-hooks.md
2124
- "vitest/no-duplicate-hooks": "error",
2125
- // Prefer .toBe() over .toEqual() for primitives — clearer intent
2126
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-be.md
2127
- "vitest/prefer-to-be": "error",
2128
- // Prefer .toHaveLength() over .toBe(arr.length) — better errors
2129
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-have-length.md
2130
- "vitest/prefer-to-have-length": "error",
2131
- // Validate expect() usage — no dangling expect without assertion
2132
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-expect.md
2133
- "vitest/valid-expect": "error",
2134
- // Validate test/describe title format — no empty or invalid titles
2135
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-title.md
2136
- "vitest/valid-title": "error",
2137
- // No conditional logic (if/else) inside tests — split into separate tests
2138
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-in-test.md
2139
- "vitest/no-conditional-in-test": "error",
2140
- // No expect() inside conditional blocks — always assert unconditionally
2141
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md
2142
- "vitest/no-conditional-expect": "error",
2143
- // No standalone expect() outside test blocks — always wrap in it/test
2144
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-standalone-expect.md
2145
- "vitest/no-standalone-expect": "error",
2146
- // Prefer .toStrictEqual() over .toEqual() — catches undefined vs missing
2147
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-strict-equal.md
2148
- "vitest/prefer-strict-equal": "error",
2149
- // Prefer vi.spyOn() over vi.fn() for method mocks — preserves original
2150
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-spy-on.md
2151
- "vitest/prefer-spy-on": "error",
2152
- // Require message argument in toThrow/toThrowError — verify correct error
2153
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/require-to-throw-message.md
2154
- "vitest/require-to-throw-message": "error",
2155
- // ── Relaxed rules for tests ───────────────────────────────────
2156
- // Tests are naturally verbose and use patterns banned in prod code
2157
- // Tests can be long (setup + many assertions)
2158
- "max-lines": "off",
2159
- "max-lines-per-function": "off",
2160
- "max-statements": "off",
2161
- // Tests often need `any` for mocking and type assertions
2162
- "@typescript-eslint/no-explicit-any": "off",
2163
- "@typescript-eslint/no-non-null-assertion": "off",
2164
- "@typescript-eslint/no-unsafe-assignment": "off",
2165
- "@typescript-eslint/no-unsafe-member-access": "off"
2166
- }
2258
+ rules: VITEST_RULES
2167
2259
  },
2168
2260
  {
2169
2261
  name: "eslint-config-setup/tests-testing-library",
2170
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"],
2262
+ files: TEST_FILES2,
2171
2263
  plugins: {
2172
2264
  "testing-library": testingLibraryPlugin
2173
2265
  },
2174
- rules: {
2175
- // ── Testing Library rules ───────────────────────────────────
2176
- // Await async events (userEvent.click, etc.) — prevents race conditions
2177
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-events.md
2178
- "testing-library/await-async-events": "error",
2179
- // Await async queries (findBy*) — they return promises
2180
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-queries.md
2181
- "testing-library/await-async-queries": "error",
2182
- // Await async utilities (waitFor, waitForElementToBeRemoved)
2183
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-utils.md
2184
- "testing-library/await-async-utils": "error",
2185
- // Don't await synchronous events — misleading
2186
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-events.md
2187
- "testing-library/no-await-sync-events": "error",
2188
- // Don't await synchronous queries (getBy*, queryBy*) — not promises
2189
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-queries.md
2190
- "testing-library/no-await-sync-queries": "error",
2191
- // Don't use container.querySelector — use queries instead
2192
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-container.md
2193
- "testing-library/no-container": "error",
2194
- // Warn on debug()/prettyDOM() left in tests — debugging artifacts
2195
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-debugging-utils.md
2196
- "testing-library/no-debugging-utils": "warn",
2197
- // Don't access DOM nodes directly — use queries
2198
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-node-access.md
2199
- "testing-library/no-node-access": "error",
2200
- // Don't call render in beforeEach — call in each test
2201
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-render-in-lifecycle.md
2202
- "testing-library/no-render-in-lifecycle": "error",
2203
- // No unnecessary act() wrappers — TL handles this internally
2204
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-unnecessary-act.md
2205
- "testing-library/no-unnecessary-act": "error",
2206
- // One assertion per waitFor — multiple can mask failures
2207
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-multiple-assertions.md
2208
- "testing-library/no-wait-for-multiple-assertions": "error",
2209
- // No side effects in waitFor — only assertions
2210
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-side-effects.md
2211
- "testing-library/no-wait-for-side-effects": "error",
2212
- // Prefer findBy* over waitFor + getBy* — built-in combination
2213
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-find-by.md
2214
- "testing-library/prefer-find-by": "error",
2215
- // Use getBy* (throws) for present elements, queryBy* for absent
2216
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-presence-queries.md
2217
- "testing-library/prefer-presence-queries": "error",
2218
- // Use queryBy* for disappearance checks — returns null when gone
2219
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-query-by-disappearance.md
2220
- "testing-library/prefer-query-by-disappearance": "error",
2221
- // Use screen.getBy* over destructured render result — consistent
2222
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md
2223
- "testing-library/prefer-screen-queries": "error",
2224
- // Name render result consistently (e.g., `const { getByText } = render(...)`)
2225
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/render-result-naming-convention.md
2226
- "testing-library/render-result-naming-convention": "error"
2227
- }
2266
+ rules: TESTING_LIBRARY_RULES
2228
2267
  }
2229
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");
2230
2295
  return configs;
2231
2296
  }
2232
2297
 
@@ -2281,65 +2346,24 @@ function standardComplexity() {
2281
2346
  ];
2282
2347
  }
2283
2348
 
2284
- // src/oxlint/integration.ts
2285
- import oxlintPlugin from "eslint-plugin-oxlint";
2286
- function oxlintIntegration(opts) {
2287
- const typedPlugin = oxlintPlugin;
2288
- const configs = [];
2289
- const add = (configName, blockName) => {
2290
- const raw = typedPlugin.configs[configName];
2291
- const items = Array.isArray(raw) ? raw : [raw];
2292
- for (const item of items) {
2293
- configs.push({ name: `eslint-config-setup/${blockName}`, ...item });
2294
- }
2295
- };
2296
- add("flat/recommended", "oxlint");
2297
- if (opts.react) {
2298
- add("flat/react", "oxlint-react");
2299
- add("flat/jsx-a11y", "oxlint-jsx-a11y");
2300
- }
2301
- if (opts.node) {
2302
- add("flat/node", "oxlint-node");
2303
- }
2304
- add("flat/typescript", "oxlint-typescript");
2305
- add("flat/unicorn", "oxlint-unicorn");
2306
- add("flat/import", "oxlint-import");
2307
- add("flat/jsdoc", "oxlint-jsdoc");
2308
- return configs;
2309
- }
2310
-
2311
- // src/configs/compat.ts
2312
- import compatPlugin from "eslint-plugin-compat";
2313
- function compatConfig() {
2314
- return [
2315
- {
2316
- name: "eslint-config-setup/compat",
2317
- plugins: {
2318
- compat: compatPlugin
2319
- },
2320
- rules: {
2321
- // Warn when using browser APIs not supported in browserslist targets
2322
- // https://github.com/amilajack/eslint-plugin-compat#usage
2323
- "compat/compat": "warn"
2324
- }
2325
- }
2326
- ];
2327
- }
2328
-
2329
2349
  // src/build/compose.ts
2330
- function composeConfig(opts) {
2331
- const config = [];
2332
- config.push(...baseConfig());
2333
- config.push(...typescriptConfig());
2334
- config.push(...importsConfig());
2335
- config.push(...perfectionistConfig());
2336
- config.push(...unicornConfig());
2337
- config.push(...regexpConfig());
2338
- config.push(...jsdocConfig());
2339
- config.push(...cspellConfig());
2340
- config.push(...sonarjsConfig());
2341
- config.push(...securityConfig());
2342
- 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) {
2343
2367
  if (!opts.node) {
2344
2368
  config.push(...compatConfig());
2345
2369
  }
@@ -2347,30 +2371,40 @@ function composeConfig(opts) {
2347
2371
  config.push(...standardComplexity());
2348
2372
  }
2349
2373
  if (opts.node) {
2350
- config.push(...nodeConfig());
2374
+ config.push(...nodeConfig({ ai: opts.ai }));
2351
2375
  }
2352
2376
  if (opts.react) {
2353
- config.push(...reactConfig());
2377
+ config.push(...reactConfig({ ai: opts.ai }));
2354
2378
  config.push(...reactEffectConfig());
2355
2379
  }
2356
2380
  if (opts.ai) {
2357
- config.push(...aiConfig({ react: opts.react }));
2381
+ config.push(...aiConfig());
2358
2382
  config.push(...perfectionistAiConfig());
2359
2383
  config.push(...packageJsonAiConfig());
2360
2384
  }
2361
- config.push(...testsOverride());
2362
- config.push(...e2eOverride());
2363
- config.push(...storiesOverride());
2364
- config.push(...configFilesOverride());
2365
- config.push(...declarationsOverride());
2366
- config.push(...scriptsOverride());
2367
- config.push(...jsonConfig());
2368
- config.push(...packageJsonConfig());
2369
- config.push(...markdownConfig());
2370
- 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());
2371
2398
  if (opts.oxlint) {
2372
2399
  config.push(...oxlintIntegration(opts));
2373
2400
  }
2401
+ }
2402
+ function composeConfig(opts) {
2403
+ const config = [];
2404
+ addCoreConfigs(config, opts);
2405
+ addConditionalConfigs(config, opts);
2406
+ addFileTypeOverrides(config);
2407
+ addFinalConfigs(config, opts);
2374
2408
  return config;
2375
2409
  }
2376
2410
  export {