eslint-config-setup 0.5.1 → 0.5.2

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,2484 +1,1884 @@
1
- // src/configs/ai.ts
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.*"
1
+ import eslint from "@eslint/js";
2
+ import compatPlugin from "eslint-plugin-compat";
3
+ import cspellPlugin from "@cspell/eslint-plugin";
4
+ import deMorganPlugin from "eslint-plugin-de-morgan";
5
+ import importXPlugin from "eslint-plugin-import-x";
6
+ import unusedImportsPlugin from "eslint-plugin-unused-imports";
7
+ import jsdocPlugin from "eslint-plugin-jsdoc";
8
+ import jsonPlugin from "@eslint/json";
9
+ import * as mdxPlugin from "eslint-plugin-mdx";
10
+ import nodePlugin from "eslint-plugin-n";
11
+ import globals from "globals";
12
+ import { configs } from "eslint-plugin-package-json";
13
+ import perfectionistPlugin from "eslint-plugin-perfectionist";
14
+ import prettierConfig from "eslint-config-prettier";
15
+ import eslintReactPlugin from "@eslint-react/eslint-plugin";
16
+ import stylisticPlugin from "@stylistic/eslint-plugin";
17
+ import jsxA11yPlugin from "eslint-plugin-jsx-a11y";
18
+ import reactHooksPlugin from "eslint-plugin-react-hooks";
19
+ import reactPerfPlugin from "eslint-plugin-react-perf";
20
+ import reactRefreshPlugin from "eslint-plugin-react-refresh";
21
+ import reactEffectPlugin from "eslint-plugin-react-you-might-not-need-an-effect";
22
+ import { configs as configs$1 } from "eslint-plugin-regexp";
23
+ import securityPlugin from "eslint-plugin-security";
24
+ import sonarjsPlugin from "eslint-plugin-sonarjs";
25
+ import tseslint from "typescript-eslint";
26
+ import unicornPlugin from "eslint-plugin-unicorn";
27
+ import playwrightPlugin from "eslint-plugin-playwright";
28
+ import storybookPlugin from "eslint-plugin-storybook";
29
+ import vitestPlugin from "@vitest/eslint-plugin";
30
+ import testingLibraryPlugin from "eslint-plugin-testing-library";
31
+ import oxlintPlugin from "eslint-plugin-oxlint";
32
+ //#region src/configs/ai.ts
33
+ /**
34
+ * AI mode — cross-cutting concerns that span multiple domains.
35
+ *
36
+ * Domain-specific AI rules live in their respective configs (base.ts,
37
+ * typescript.ts, react.ts, unicorn.ts, sonarjs.ts, regexp.ts, jsdoc.ts,
38
+ * node.ts) behind an `ai` flag. This file only contains:
39
+ *
40
+ * 1. Test framework rules (vitest) — no dedicated domain config
41
+ * 2. File-scoped relaxations — cross-domain, must come last to override
42
+ *
43
+ * @see ADR-0006: docs/adr/0006-ai-mode-as-dedicated-flag.md
44
+ */
45
+ const TEST_FILES$1 = ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"];
46
+ const E2E_FILES = ["**/*.spec.ts"];
47
+ const CONFIG_FILES = [
48
+ "**/*.config.{ts,mts,cts,js,mjs,cjs}",
49
+ "**/vite.config.*",
50
+ "**/vitest.config.*",
51
+ "**/next.config.*"
9
52
  ];
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
- }
53
+ const AI_TESTS_STRICT_CONFIG = {
54
+ name: "eslint-config-setup/ai-tests-strict",
55
+ files: TEST_FILES$1,
56
+ rules: {
57
+ "vitest/require-top-level-describe": "error",
58
+ "vitest/prefer-hooks-on-top": "error"
59
+ }
19
60
  };
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
- }
61
+ const AI_TESTS_RELAXED_CONFIG = {
62
+ name: "eslint-config-setup/ai-tests-relaxed",
63
+ files: TEST_FILES$1,
64
+ rules: {
65
+ "max-lines": "off",
66
+ "max-lines-per-function": "off",
67
+ "max-statements": "off",
68
+ "max-nested-callbacks": "off",
69
+ "@typescript-eslint/no-magic-numbers": "off",
70
+ "sonarjs/no-duplicate-string": "off",
71
+ "@typescript-eslint/explicit-function-return-type": "off",
72
+ "@typescript-eslint/naming-convention": "off",
73
+ "unicorn/prevent-abbreviations": "off"
74
+ }
34
75
  };
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
- }
76
+ const AI_E2E_RELAXED_CONFIG = {
77
+ name: "eslint-config-setup/ai-e2e-relaxed",
78
+ files: E2E_FILES,
79
+ rules: {
80
+ "max-lines": "off",
81
+ "max-lines-per-function": "off",
82
+ "max-statements": "off",
83
+ "@typescript-eslint/no-magic-numbers": "off",
84
+ "@typescript-eslint/explicit-function-return-type": "off"
85
+ }
45
86
  };
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
- }
87
+ const AI_CONFIG_RELAXED_CONFIG = {
88
+ name: "eslint-config-setup/ai-config-relaxed",
89
+ files: CONFIG_FILES,
90
+ rules: {
91
+ complexity: "off",
92
+ "max-lines": "off",
93
+ "max-lines-per-function": "off",
94
+ "max-statements": "off",
95
+ "@typescript-eslint/no-magic-numbers": "off",
96
+ "@typescript-eslint/explicit-function-return-type": "off",
97
+ "@typescript-eslint/naming-convention": "off"
98
+ }
58
99
  };
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
- }
100
+ const AI_DECLARATIONS_RELAXED_CONFIG = {
101
+ name: "eslint-config-setup/ai-declarations-relaxed",
102
+ files: ["**/*.d.ts"],
103
+ rules: {
104
+ "@typescript-eslint/explicit-function-return-type": "off",
105
+ "@typescript-eslint/naming-convention": "off",
106
+ "@typescript-eslint/no-explicit-any": "off",
107
+ "@typescript-eslint/no-magic-numbers": "off",
108
+ "unicorn/prevent-abbreviations": "off",
109
+ "unicorn/filename-case": "off"
110
+ }
70
111
  };
71
112
  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
78
- ];
113
+ return [
114
+ AI_TESTS_STRICT_CONFIG,
115
+ AI_TESTS_RELAXED_CONFIG,
116
+ AI_E2E_RELAXED_CONFIG,
117
+ AI_CONFIG_RELAXED_CONFIG,
118
+ AI_DECLARATIONS_RELAXED_CONFIG
119
+ ];
79
120
  }
80
-
81
- // src/configs/base.ts
82
- import eslint from "@eslint/js";
83
-
84
- // src/build/config-builder.ts
121
+ //#endregion
122
+ //#region src/build/config-builder.ts
85
123
  function createConfig(options) {
86
- const presetRules2 = /* @__PURE__ */ new Map();
87
- const presetPlugins = {};
88
- if (options.presets) {
89
- for (const preset of options.presets) {
90
- if (preset.plugins) {
91
- Object.assign(presetPlugins, preset.plugins);
92
- }
93
- if (preset.rules) {
94
- for (const [name, value] of Object.entries(preset.rules)) {
95
- if (value !== void 0) {
96
- presetRules2.set(name, value);
97
- }
98
- }
99
- }
100
- }
101
- }
102
- const overrides = /* @__PURE__ */ new Map();
103
- const additions = /* @__PURE__ */ new Map();
104
- const disabled = /* @__PURE__ */ new Set();
105
- const removed = /* @__PURE__ */ new Set();
106
- const fileOverrides = [];
107
- const builder = {
108
- overrideRule(name, value) {
109
- if (!presetRules2.has(name)) {
110
- throw new Error(
111
- `overrideRule("${name}"): rule not found in preset. Cannot override a rule that doesn't exist in the preset.`
112
- );
113
- }
114
- overrides.set(name, value);
115
- return builder;
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
- },
143
- addRule(name, value) {
144
- if (presetRules2.has(name)) {
145
- throw new Error(
146
- `addRule("${name}"): rule already exists in preset. Use overrideRule() to change its value, or removeRule() to drop it.`
147
- );
148
- }
149
- if (additions.has(name)) {
150
- throw new Error(
151
- `addRule("${name}"): rule was already added. Each rule can only be added once.`
152
- );
153
- }
154
- additions.set(name, value);
155
- return builder;
156
- },
157
- disableRule(name) {
158
- if (!presetRules2.has(name) && !additions.has(name)) {
159
- throw new Error(
160
- `disableRule("${name}"): rule not found in preset or additions. Cannot disable a rule that doesn't exist.`
161
- );
162
- }
163
- disabled.add(name);
164
- return builder;
165
- },
166
- removeRule(name) {
167
- if (!presetRules2.has(name) && !additions.has(name)) {
168
- throw new Error(
169
- `removeRule("${name}"): rule not found in preset or additions. Cannot remove a rule that doesn't exist.`
170
- );
171
- }
172
- removed.add(name);
173
- return builder;
174
- },
175
- addFileOverride(name, files, rules) {
176
- fileOverrides.push({ name, files, rules });
177
- return builder;
178
- },
179
- build() {
180
- const rules = {};
181
- for (const [name, value] of presetRules2) {
182
- if (!removed.has(name)) {
183
- rules[name] = value;
184
- }
185
- }
186
- for (const [name, value] of overrides) {
187
- if (!removed.has(name)) {
188
- rules[name] = value;
189
- }
190
- }
191
- for (const [name, value] of additions) {
192
- if (!removed.has(name)) {
193
- rules[name] = value;
194
- }
195
- }
196
- for (const name of disabled) {
197
- if (!removed.has(name)) {
198
- rules[name] = "off";
199
- }
200
- }
201
- const mainBlock = {
202
- name: options.name,
203
- rules
204
- };
205
- const mergedPlugins = { ...presetPlugins, ...options.plugins };
206
- if (Object.keys(mergedPlugins).length > 0) {
207
- mainBlock.plugins = mergedPlugins;
208
- }
209
- if (options.languageOptions) {
210
- mainBlock.languageOptions = options.languageOptions;
211
- }
212
- if (options.settings) {
213
- mainBlock.settings = options.settings;
214
- }
215
- if (options.files) {
216
- mainBlock.files = options.files;
217
- }
218
- if (options.ignores) {
219
- mainBlock.ignores = options.ignores;
220
- }
221
- const result = [];
222
- if (options.passthrough) {
223
- result.push(...options.passthrough);
224
- }
225
- result.push(mainBlock);
226
- for (const fo of fileOverrides) {
227
- result.push({
228
- name: fo.name,
229
- files: fo.files,
230
- rules: fo.rules
231
- });
232
- }
233
- return result;
234
- }
235
- };
236
- return builder;
124
+ const presetRules = /* @__PURE__ */ new Map();
125
+ const presetPlugins = {};
126
+ if (options.presets) for (const preset of options.presets) {
127
+ if (preset.plugins) Object.assign(presetPlugins, preset.plugins);
128
+ if (preset.rules) {
129
+ for (const [name, value] of Object.entries(preset.rules)) if (value !== void 0) presetRules.set(name, value);
130
+ }
131
+ }
132
+ const overrides = /* @__PURE__ */ new Map();
133
+ const additions = /* @__PURE__ */ new Map();
134
+ const disabled = /* @__PURE__ */ new Set();
135
+ const removed = /* @__PURE__ */ new Set();
136
+ const fileOverrides = [];
137
+ const builder = {
138
+ overrideRule(name, value) {
139
+ if (!presetRules.has(name)) throw new Error(`overrideRule("${name}"): rule not found in preset. Cannot override a rule that doesn't exist in the preset.`);
140
+ overrides.set(name, value);
141
+ return builder;
142
+ },
143
+ overrideSeverity(name, severity) {
144
+ if (!presetRules.has(name)) throw new Error(`overrideSeverity("${name}"): rule not found in preset. Cannot override severity of a rule that doesn't exist in the preset.`);
145
+ const existing = presetRules.get(name);
146
+ if (Array.isArray(existing)) {
147
+ const [, ...restOptions] = existing;
148
+ overrides.set(name, [severity, ...restOptions]);
149
+ } else overrides.set(name, severity);
150
+ return builder;
151
+ },
152
+ overrideOptions(name, ...ruleOptions) {
153
+ if (!presetRules.has(name)) throw new Error(`overrideOptions("${name}"): rule not found in preset. Cannot override options of a rule that doesn't exist in the preset.`);
154
+ const existing = presetRules.get(name);
155
+ const severity = Array.isArray(existing) ? existing[0] : existing;
156
+ overrides.set(name, [severity, ...ruleOptions]);
157
+ return builder;
158
+ },
159
+ addRule(name, value) {
160
+ if (presetRules.has(name)) throw new Error(`addRule("${name}"): rule already exists in preset. Use overrideRule() to change its value, or removeRule() to drop it.`);
161
+ if (additions.has(name)) throw new Error(`addRule("${name}"): rule was already added. Each rule can only be added once.`);
162
+ additions.set(name, value);
163
+ return builder;
164
+ },
165
+ disableRule(name) {
166
+ if (!presetRules.has(name) && !additions.has(name)) throw new Error(`disableRule("${name}"): rule not found in preset or additions. Cannot disable a rule that doesn't exist.`);
167
+ disabled.add(name);
168
+ return builder;
169
+ },
170
+ removeRule(name) {
171
+ if (!presetRules.has(name) && !additions.has(name)) throw new Error(`removeRule("${name}"): rule not found in preset or additions. Cannot remove a rule that doesn't exist.`);
172
+ removed.add(name);
173
+ return builder;
174
+ },
175
+ addFileOverride(name, files, rules) {
176
+ fileOverrides.push({
177
+ name,
178
+ files,
179
+ rules
180
+ });
181
+ return builder;
182
+ },
183
+ build() {
184
+ const rules = {};
185
+ for (const [name, value] of presetRules) if (!removed.has(name)) rules[name] = value;
186
+ for (const [name, value] of overrides) if (!removed.has(name)) rules[name] = value;
187
+ for (const [name, value] of additions) if (!removed.has(name)) rules[name] = value;
188
+ for (const name of disabled) if (!removed.has(name)) rules[name] = "off";
189
+ const mainBlock = {
190
+ name: options.name,
191
+ rules
192
+ };
193
+ const mergedPlugins = {
194
+ ...presetPlugins,
195
+ ...options.plugins
196
+ };
197
+ if (Object.keys(mergedPlugins).length > 0) mainBlock.plugins = mergedPlugins;
198
+ if (options.languageOptions) mainBlock.languageOptions = options.languageOptions;
199
+ if (options.settings) mainBlock.settings = options.settings;
200
+ if (options.files) mainBlock.files = options.files;
201
+ if (options.ignores) mainBlock.ignores = options.ignores;
202
+ const result = [];
203
+ if (options.passthrough) result.push(...options.passthrough);
204
+ result.push(mainBlock);
205
+ for (const fo of fileOverrides) result.push({
206
+ name: fo.name,
207
+ files: fo.files,
208
+ rules: fo.rules
209
+ });
210
+ return result;
211
+ }
212
+ };
213
+ return builder;
237
214
  }
238
-
239
- // src/configs/base.ts
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 }]
215
+ //#endregion
216
+ //#region src/configs/base.ts
217
+ const ERROR_PREVENTION_RULES = {
218
+ "accessor-pairs": ["error", { enforceForClassMembers: true }],
219
+ "array-callback-return": ["error", { allowImplicit: true }],
220
+ "no-constructor-return": "error",
221
+ "no-promise-executor-return": "error",
222
+ "no-self-compare": "error",
223
+ "no-template-curly-in-string": "error",
224
+ "no-unreachable-loop": "error",
225
+ "require-atomic-updates": "error",
226
+ "no-unmodified-loop-condition": "error",
227
+ "grouped-accessor-pairs": ["error", "getBeforeSet"],
228
+ "no-useless-rename": "error",
229
+ "no-useless-computed-key": ["error", { enforceForClassMembers: true }]
253
230
  };
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"
231
+ const DANGEROUS_PATTERN_RULES = {
232
+ "no-eval": "error",
233
+ "no-alert": "error",
234
+ "no-caller": "error",
235
+ "no-extend-native": "error",
236
+ "no-new-func": "error",
237
+ "no-new-wrappers": "error",
238
+ "no-object-constructor": "error",
239
+ "no-proto": "error",
240
+ "no-iterator": "error",
241
+ "no-script-url": "error",
242
+ "no-octal-escape": "error",
243
+ "no-implicit-globals": "error"
267
244
  };
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 }]
245
+ const CODE_QUALITY_RULES = {
246
+ eqeqeq: ["error", "smart"],
247
+ "guard-for-in": "error",
248
+ "default-case-last": "error",
249
+ radix: "error",
250
+ yoda: "error",
251
+ "no-sequences": ["error", { allowInParentheses: false }],
252
+ "no-new": "error",
253
+ "no-labels": "error",
254
+ "no-extra-bind": "error",
255
+ "no-lone-blocks": "error",
256
+ "no-useless-call": "error",
257
+ "no-useless-concat": "error",
258
+ "no-useless-return": "error",
259
+ "no-return-assign": ["error", "always"],
260
+ "no-multi-str": "error",
261
+ "prefer-regex-literals": ["error", { disallowRedundantWrapping: true }]
285
262
  };
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 }]
263
+ const MODERN_STYLE_RULES = {
264
+ "no-var": "error",
265
+ "prefer-const": ["error", { destructuring: "all" }],
266
+ "prefer-object-has-own": "error",
267
+ "prefer-object-spread": "error",
268
+ "prefer-rest-params": "error",
269
+ "prefer-spread": "error",
270
+ "symbol-description": "error",
271
+ "prefer-numeric-literals": "error",
272
+ "object-shorthand": [
273
+ "error",
274
+ "always",
275
+ {
276
+ avoidExplicitReturnArrows: true,
277
+ avoidQuotes: true
278
+ }
279
+ ]
296
280
  };
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"
281
+ const AI_STRUCTURAL_RULES = {
282
+ curly: ["error", "all"],
283
+ "no-else-return": ["error", { allowElseIf: false }],
284
+ "no-nested-ternary": "error",
285
+ "no-unneeded-ternary": "error",
286
+ "no-negated-condition": "error",
287
+ "no-lonely-if": "error",
288
+ "no-param-reassign": ["error", { props: true }],
289
+ "no-multi-assign": "error",
290
+ "one-var": ["error", "never"],
291
+ "no-implicit-coercion": "error",
292
+ "arrow-body-style": "error",
293
+ "prefer-arrow-callback": ["error", { allowNamedFunctions: true }],
294
+ "logical-assignment-operators": [
295
+ "error",
296
+ "always",
297
+ { enforceForIfStatements: true }
298
+ ],
299
+ "max-statements-per-line": ["error", { max: 1 }],
300
+ "prefer-exponentiation-operator": "error",
301
+ "prefer-named-capture-group": "error",
302
+ "require-unicode-regexp": "error",
303
+ "no-warning-comments": "warn",
304
+ "no-await-in-loop": "error"
317
305
  };
318
- function addRules(builder, rules) {
319
- for (const [ruleName, value] of Object.entries(rules)) {
320
- builder.addRule(ruleName, value);
321
- }
306
+ function addRules$1(builder, rules) {
307
+ for (const [ruleName, value] of Object.entries(rules)) builder.addRule(ruleName, value);
322
308
  }
323
309
  function addBaseOverrides(builder) {
324
- builder.overrideRule("use-isnan", ["error", { enforceForIndexOf: true, enforceForSwitchCase: true }]);
325
- builder.overrideRule("valid-typeof", ["error", { requireStringLiterals: true }]);
310
+ builder.overrideRule("use-isnan", ["error", {
311
+ enforceForIndexOf: true,
312
+ enforceForSwitchCase: true
313
+ }]);
314
+ builder.overrideRule("valid-typeof", ["error", { requireStringLiterals: true }]);
326
315
  }
327
316
  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]);
317
+ builder.addRule("complexity", ["error", isAi ? 10 : 20]);
318
+ builder.addRule("max-depth", ["error", isAi ? 3 : 5]);
319
+ builder.addRule("max-nested-callbacks", ["error", isAi ? 2 : 4]);
320
+ builder.addRule("max-params", ["error", isAi ? 3 : 5]);
321
+ builder.addRule("max-statements", ["error", isAi ? 15 : 25]);
322
+ builder.addRule("max-lines-per-function", ["error", {
323
+ max: isAi ? 100 : 200,
324
+ skipBlankLines: true,
325
+ skipComments: true
326
+ }]);
327
+ builder.addRule("max-lines", ["error", {
328
+ max: isAi ? 300 : 500,
329
+ skipBlankLines: true,
330
+ skipComments: true
331
+ }]);
332
+ builder.addRule("sonarjs/cognitive-complexity", ["error", isAi ? 10 : 20]);
342
333
  }
343
334
  function addModernStyleRules(builder, isAi) {
344
- addRules(builder, MODERN_STYLE_RULES);
345
- builder.addRule("prefer-template", isAi ? "error" : "warn");
335
+ addRules$1(builder, MODERN_STYLE_RULES);
336
+ builder.addRule("prefer-template", isAi ? "error" : "warn");
346
337
  }
338
+ /**
339
+ * Base ESLint config — extends `eslint.configs.recommended` with additional
340
+ * best-practice rules for error prevention and modern JS style.
341
+ *
342
+ * Rules with TypeScript equivalents (no-implied-eval, dot-notation, etc.)
343
+ * are NOT included here — they are handled by the typescript-eslint presets.
344
+ *
345
+ * Preset: `@eslint/js` recommended
346
+ * @see https://eslint.org/docs/latest/rules/
347
+ */
347
348
  function baseConfig(opts) {
348
- const isAi = opts?.ai ?? false;
349
- const builder = createConfig({
350
- name: "eslint-config-setup/base",
351
- presets: [eslint.configs.recommended]
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();
349
+ const isAi = opts?.ai ?? false;
350
+ const builder = createConfig({
351
+ name: "eslint-config-setup/base",
352
+ presets: [eslint.configs.recommended]
353
+ });
354
+ addRules$1(builder, ERROR_PREVENTION_RULES);
355
+ addBaseOverrides(builder);
356
+ addRules$1(builder, DANGEROUS_PATTERN_RULES);
357
+ addRules$1(builder, CODE_QUALITY_RULES);
358
+ addComplexityRules(builder, isAi);
359
+ addModernStyleRules(builder, isAi);
360
+ if (isAi) addRules$1(builder, AI_STRUCTURAL_RULES);
361
+ return builder.build();
363
362
  }
364
-
365
- // src/configs/compat.ts
366
- import compatPlugin from "eslint-plugin-compat";
363
+ //#endregion
364
+ //#region src/configs/compat.ts
365
+ /**
366
+ * Browser compatibility config — checks that browser APIs are available
367
+ * in the project's browserslist targets. Uses MDN compatibility data.
368
+ *
369
+ * Activated automatically for non-Node projects (when `node: false`).
370
+ * If no browserslist config exists, the default applies:
371
+ * `> 0.5%, last 2 versions, Firefox ESR, not dead`
372
+ *
373
+ * @see https://github.com/amilajack/eslint-plugin-compat
374
+ * @see https://browsersl.ist/
375
+ */
367
376
  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
- ];
377
+ return [{
378
+ name: "eslint-config-setup/compat",
379
+ plugins: { compat: compatPlugin },
380
+ rules: { "compat/compat": "warn" }
381
+ }];
381
382
  }
382
-
383
- // src/configs/cspell.ts
384
- import cspellPlugin from "@cspell/eslint-plugin";
383
+ //#endregion
384
+ //#region src/configs/cspell.ts
385
+ /**
386
+ * CSpell config — spell checking for identifiers and comments.
387
+ * Catches typos in variable names, function names, and documentation.
388
+ * Severity is "warn" because dictionary misses are common for domain terms.
389
+ *
390
+ * @see https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell-eslint-plugin#readme
391
+ */
385
392
  function cspellConfig() {
386
- return [
387
- {
388
- name: "eslint-config-setup/cspell",
389
- plugins: {
390
- "@cspell": cspellPlugin
391
- },
392
- rules: {
393
- // Check spelling in identifiers and comments — catches typos in API names
394
- // Strings are excluded (may contain user-facing text, URLs, etc.)
395
- // Auto-fix disabled — spelling corrections need human review
396
- // https://github.com/streetsidesoftware/cspell/tree/main/packages/cspell-eslint-plugin#rules
397
- "@cspell/spellchecker": [
398
- "warn",
399
- {
400
- checkComments: true,
401
- checkIdentifiers: true,
402
- checkStrings: false,
403
- autoFix: false
404
- }
405
- ]
406
- }
407
- }
408
- ];
393
+ return [{
394
+ name: "eslint-config-setup/cspell",
395
+ plugins: { "@cspell": cspellPlugin },
396
+ rules: { "@cspell/spellchecker": ["warn", {
397
+ checkComments: true,
398
+ checkIdentifiers: true,
399
+ checkStrings: false,
400
+ autoFix: false
401
+ }] }
402
+ }];
409
403
  }
410
-
411
- // src/configs/de-morgan.ts
412
- import deMorganPlugin from "eslint-plugin-de-morgan";
404
+ //#endregion
405
+ //#region src/configs/de-morgan.ts
406
+ /**
407
+ * De Morgan config — enforces De Morgan's laws on negated boolean expressions.
408
+ * Both rules are auto-fixable: !(A && B) → !A || !B and !(A || B) → !A && !B.
409
+ *
410
+ * Complements sonarjs/no-inverted-boolean-check (which handles simple !(a === b) → a !== b)
411
+ * by targeting compound expressions with && and ||.
412
+ *
413
+ * @see https://github.com/azat-io/eslint-plugin-de-morgan
414
+ */
413
415
  function deMorganConfig() {
414
- return [
415
- {
416
- name: "eslint-config-setup/de-morgan",
417
- plugins: {
418
- "de-morgan": deMorganPlugin
419
- },
420
- rules: {
421
- // Transform !(A && B) → !A || !B — more readable negated conjunction
422
- // https://github.com/azat-io/eslint-plugin-de-morgan/blob/main/docs/no-negated-conjunction.md
423
- "de-morgan/no-negated-conjunction": "error",
424
- // Transform !(A || B) → !A && !B — more readable negated disjunction
425
- // https://github.com/azat-io/eslint-plugin-de-morgan/blob/main/docs/no-negated-disjunction.md
426
- "de-morgan/no-negated-disjunction": "error"
427
- }
428
- }
429
- ];
416
+ return [{
417
+ name: "eslint-config-setup/de-morgan",
418
+ plugins: { "de-morgan": deMorganPlugin },
419
+ rules: {
420
+ "de-morgan/no-negated-conjunction": "error",
421
+ "de-morgan/no-negated-disjunction": "error"
422
+ }
423
+ }];
430
424
  }
431
-
432
- // src/configs/imports.ts
433
- import importXPlugin from "eslint-plugin-import-x";
434
- import unusedImportsPlugin from "eslint-plugin-unused-imports";
425
+ //#endregion
426
+ //#region src/configs/imports.ts
427
+ /**
428
+ * Import/export config — two plugins with clear separation of concerns:
429
+ * - `import-x` handles import **validation** (cycles, duplicates, etc.)
430
+ * - `unused-imports` handles **removal** of unused imports (auto-fixable)
431
+ *
432
+ * Import/export **ordering** is handled by perfectionist (see perfectionist.ts).
433
+ *
434
+ * @see https://github.com/un-ts/eslint-plugin-import-x
435
+ * @see https://github.com/sweepline/eslint-plugin-unused-imports
436
+ */
435
437
  function importsConfig() {
436
- return [
437
- {
438
- name: "eslint-config-setup/imports",
439
- plugins: {
440
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Plugin type is compatible
441
- "import": importXPlugin,
442
- "unused-imports": unusedImportsPlugin
443
- },
444
- rules: {
445
- // ── Validation (import-x) ────────────────────────────────────
446
- // Merge duplicate import paths into one statement — reduces noise
447
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-duplicates.md
448
- "import/no-duplicates": "error",
449
- // Forbid a module from importing itself — always a bug
450
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-self-import.md
451
- "import/no-self-import": "error",
452
- // Detect circular dependencies — limited to depth 3 for performance
453
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-cycle.md
454
- "import/no-cycle": ["error", { maxDepth: 3 }],
455
- // Remove unnecessary path segments (e.g., ./foo/../foo → ./foo)
456
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-useless-path-segments.md
457
- "import/no-useless-path-segments": "error",
458
- // Forbid mutable export bindings — prevents shared mutable state
459
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/no-mutable-exports.md
460
- "import/no-mutable-exports": "error",
461
- // Imports must come before other statements — consistent module structure
462
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/first.md
463
- "import/first": "error",
464
- // Require blank line after import block — visual separation
465
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/newline-after-import.md
466
- "import/newline-after-import": "error",
467
- // Detect `import Foo from './Foo'` when Foo is also a named export — likely wrong
468
- // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-named-as-default.md
469
- "import/no-named-as-default": "error",
470
- // Detect `Foo.bar` when `bar` is a named export — use `import { bar }` instead
471
- // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-named-as-default-member.md
472
- "import/no-named-as-default-member": "error",
473
- // Detect empty `import {} from 'foo'` — leftover after refactoring
474
- // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-empty-named-blocks.md
475
- "import/no-empty-named-blocks": "error",
476
- // Forbid absolute file paths in imports — not portable across machines
477
- // https://github.com/un-ts/eslint-plugin-import-x/blob/master/docs/rules/no-absolute-path.md
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",
503
- // ── Disabled: ordering handled by perfectionist ────────────────
504
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/order.md
505
- "import/order": "off",
506
- // https://github.com/un-ts/eslint-plugin-import/blob/master/docs/rules/sort-imports.md
507
- "import/sort-imports": "off",
508
- // ── Unused import removal ─────────────────────────────────────
509
- // Auto-remove unused imports — keeps imports clean (auto-fixable)
510
- // https://github.com/sweepline/eslint-plugin-unused-imports#usage
511
- "unused-imports/no-unused-imports": "error"
512
- }
513
- }
514
- ];
438
+ return [{
439
+ name: "eslint-config-setup/imports",
440
+ plugins: {
441
+ "import": importXPlugin,
442
+ "unused-imports": unusedImportsPlugin
443
+ },
444
+ rules: {
445
+ "import/no-duplicates": "error",
446
+ "import/no-self-import": "error",
447
+ "import/no-cycle": ["error", { maxDepth: 3 }],
448
+ "import/no-useless-path-segments": "error",
449
+ "import/no-mutable-exports": "error",
450
+ "import/first": "error",
451
+ "import/newline-after-import": "error",
452
+ "import/no-named-as-default": "error",
453
+ "import/no-named-as-default-member": "error",
454
+ "import/no-empty-named-blocks": "error",
455
+ "import/no-absolute-path": "error",
456
+ "import/no-extraneous-dependencies": ["error", { includeTypes: true }],
457
+ "import/no-unassigned-import": ["error", { allow: [
458
+ "@babel/polyfill",
459
+ "**/register",
460
+ "**/register.*",
461
+ "**/register/**",
462
+ "**/register/**.*",
463
+ "**/*.css",
464
+ "**/*.scss",
465
+ "**/*.sass",
466
+ "**/*.less"
467
+ ] }],
468
+ "import/no-named-default": "error",
469
+ "import/order": "off",
470
+ "import/sort-imports": "off",
471
+ "unused-imports/no-unused-imports": "error"
472
+ }
473
+ }];
515
474
  }
516
-
517
- // src/configs/jsdoc.ts
518
- import jsdocPlugin from "eslint-plugin-jsdoc";
475
+ //#endregion
476
+ //#region src/configs/jsdoc.ts
477
+ /**
478
+ * JSDoc config — validates existing JSDoc annotations without requiring them.
479
+ *
480
+ * Preset: `flat/recommended-typescript-error` — all recommended JSDoc rules
481
+ * adapted for TypeScript (types are in TS, not JSDoc).
482
+ *
483
+ * Overrides:
484
+ * - `require-jsdoc` OFF — we validate existing JSDoc, we don't mandate it
485
+ * - param/return descriptions downgraded to warn — helpful but not blocking
486
+ * @see https://github.com/gajus/eslint-plugin-jsdoc#rules
487
+ */
519
488
  function jsdocConfig(opts) {
520
- const isAi = opts?.ai ?? false;
521
- const builder = createConfig({
522
- name: "eslint-config-setup/jsdoc",
523
- presets: [jsdocPlugin.configs["flat/recommended-typescript-error"]]
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();
489
+ const isAi = opts?.ai ?? false;
490
+ const builder = createConfig({
491
+ name: "eslint-config-setup/jsdoc",
492
+ presets: [jsdocPlugin.configs["flat/recommended-typescript-error"]]
493
+ }).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");
494
+ if (isAi) {
495
+ builder.overrideRule("jsdoc/require-param", "error");
496
+ builder.overrideRule("jsdoc/require-returns", "error");
497
+ builder.overrideRule("jsdoc/informative-docs", "error");
498
+ }
499
+ return builder.build();
531
500
  }
532
-
533
- // src/configs/json.ts
534
- import jsonPlugin from "@eslint/json";
535
- var plugin = jsonPlugin;
501
+ //#endregion
502
+ //#region src/configs/json.ts
503
+ const plugin$1 = jsonPlugin;
504
+ /**
505
+ * JSON/JSONC config — native JSON linting using the official `@eslint/json` plugin.
506
+ * Two blocks: strict JSON for most files, JSONC (with comments) for tsconfig etc.
507
+ *
508
+ * @see https://github.com/eslint/json#rules
509
+ */
536
510
  function jsonConfig() {
537
- return [
538
- {
539
- name: "eslint-config-setup/json",
540
- files: ["**/*.json"],
541
- ignores: ["**/package-lock.json"],
542
- language: "json/json",
543
- plugins: {
544
- json: plugin
545
- },
546
- rules: {
547
- // Detect duplicate keys in JSON — last-write-wins is confusing
548
- // https://github.com/eslint/json#rules
549
- "json/no-duplicate-keys": "error",
550
- // Detect empty string keys — likely a mistake
551
- // https://github.com/eslint/json#rules
552
- "json/no-empty-keys": "error",
553
- // Detect unsafe values (NaN, Infinity, lone surrogates) — invalid JSON
554
- // https://github.com/eslint/json#rules
555
- "json/no-unsafe-values": "error",
556
- // Detect unnormalized Unicode keys — prevents invisible key mismatches
557
- // https://github.com/eslint/json#rules
558
- "json/no-unnormalized-keys": "error"
559
- }
560
- },
561
- {
562
- name: "eslint-config-setup/jsonc",
563
- files: [
564
- "**/tsconfig.json",
565
- "**/tsconfig.*.json",
566
- "**/.vscode/*.json",
567
- "**/turbo.json"
568
- ],
569
- language: "json/jsonc",
570
- plugins: {
571
- json: plugin
572
- },
573
- rules: {
574
- // Detect duplicate keys in JSONC — same rationale as JSON
575
- // https://github.com/eslint/json#rules
576
- "json/no-duplicate-keys": "error",
577
- // Detect empty string keys — likely a mistake
578
- // https://github.com/eslint/json#rules
579
- "json/no-empty-keys": "error",
580
- // Detect unsafe values (NaN, Infinity, lone surrogates) — invalid JSON
581
- // https://github.com/eslint/json#rules
582
- "json/no-unsafe-values": "error",
583
- // Detect unnormalized Unicode keys — prevents invisible key mismatches
584
- // https://github.com/eslint/json#rules
585
- "json/no-unnormalized-keys": "error"
586
- }
587
- }
588
- ];
511
+ return [{
512
+ name: "eslint-config-setup/json",
513
+ files: ["**/*.json"],
514
+ ignores: ["**/package-lock.json"],
515
+ language: "json/json",
516
+ plugins: { json: plugin$1 },
517
+ rules: {
518
+ "json/no-duplicate-keys": "error",
519
+ "json/no-empty-keys": "error",
520
+ "json/no-unsafe-values": "error",
521
+ "json/no-unnormalized-keys": "error"
522
+ }
523
+ }, {
524
+ name: "eslint-config-setup/jsonc",
525
+ files: [
526
+ "**/tsconfig.json",
527
+ "**/tsconfig.*.json",
528
+ "**/.vscode/*.json",
529
+ "**/turbo.json"
530
+ ],
531
+ language: "json/jsonc",
532
+ plugins: { json: plugin$1 },
533
+ rules: {
534
+ "json/no-duplicate-keys": "error",
535
+ "json/no-empty-keys": "error",
536
+ "json/no-unsafe-values": "error",
537
+ "json/no-unnormalized-keys": "error"
538
+ }
539
+ }];
589
540
  }
590
-
591
- // src/configs/markdown.ts
592
- import * as mdxPlugin from "eslint-plugin-mdx";
541
+ //#endregion
542
+ //#region src/configs/markdown.ts
543
+ /**
544
+ * Markdown & MDX config — lints code blocks inside Markdown and MDX files
545
+ * using eslint-plugin-mdx. Code examples in docs get the same ESLint rules
546
+ * as your source code. Covers both `.md` and `.mdx` files.
547
+ *
548
+ * Markdown structure linting (headings, links, etc.) is intentionally left
549
+ * to dedicated tools like markdownlint.
550
+ *
551
+ * @see https://github.com/mdx-js/eslint-mdx
552
+ * @see https://github.com/DavidAnson/markdownlint-cli2 (recommended for Markdown structure linting)
553
+ */
593
554
  function markdownConfig() {
594
- const codeBlockLanguageOptions = mdxPlugin.flatCodeBlocks.languageOptions ?? {};
595
- const codeBlockParserOptions = codeBlockLanguageOptions.parserOptions ?? {};
596
- return [
597
- // ── MDX / Markdown parsing ─────────────────────────────────────
598
- {
599
- ...mdxPlugin.flat,
600
- processor: mdxPlugin.createRemarkProcessor({
601
- lintCodeBlocks: true,
602
- languageMapper: {}
603
- })
604
- },
605
- // ── Code block linting ─────────────────────────────────────────
606
- // Applies ESLint rules to fenced code blocks extracted from .md/.mdx.
607
- // Relaxes rules that don't apply to incomplete code snippets.
608
- {
609
- ...mdxPlugin.flatCodeBlocks,
610
- languageOptions: {
611
- ...codeBlockLanguageOptions,
612
- parserOptions: {
613
- ...codeBlockParserOptions,
614
- projectService: false
615
- }
616
- },
617
- rules: {
618
- ...mdxPlugin.flatCodeBlocks.rules,
619
- // Snippets don't need trailing newlines
620
- "eol-last": "off",
621
- // Variables may be defined elsewhere
622
- "no-undef": "off",
623
- // Snippets often show standalone expressions
624
- "no-unused-expressions": "off",
625
- // Variables are often declared for demonstration
626
- "no-unused-vars": "off",
627
- // Padding is irrelevant in snippets
628
- "padded-blocks": "off",
629
- // Strict mode is irrelevant in snippets
630
- strict: "off",
631
- // BOM is irrelevant in snippets
632
- "unicode-bom": "off"
633
- }
634
- }
635
- ];
555
+ const codeBlockLanguageOptions = mdxPlugin.flatCodeBlocks.languageOptions ?? {};
556
+ const codeBlockParserOptions = codeBlockLanguageOptions.parserOptions ?? {};
557
+ return [{
558
+ ...mdxPlugin.flat,
559
+ processor: mdxPlugin.createRemarkProcessor({
560
+ lintCodeBlocks: true,
561
+ languageMapper: {}
562
+ })
563
+ }, {
564
+ ...mdxPlugin.flatCodeBlocks,
565
+ languageOptions: {
566
+ ...codeBlockLanguageOptions,
567
+ parserOptions: {
568
+ ...codeBlockParserOptions,
569
+ projectService: false
570
+ }
571
+ },
572
+ rules: {
573
+ ...mdxPlugin.flatCodeBlocks.rules,
574
+ "eol-last": "off",
575
+ "no-undef": "off",
576
+ "no-unused-expressions": "off",
577
+ "no-unused-vars": "off",
578
+ "padded-blocks": "off",
579
+ strict: "off",
580
+ "unicode-bom": "off"
581
+ }
582
+ }];
636
583
  }
637
-
638
- // src/configs/node.ts
639
- import nodePlugin from "eslint-plugin-n";
640
- import globals from "globals";
584
+ //#endregion
585
+ //#region src/configs/node.ts
586
+ /**
587
+ * Node.js config — rules for server-side JavaScript/TypeScript.
588
+ * Hand-picked subset of eslint-plugin-n. We don't use the full preset because
589
+ * module resolution rules (`no-missing-import`, `no-unpublished-import`) are
590
+ * handled better by TypeScript.
591
+ *
592
+ * @see https://github.com/eslint-community/eslint-plugin-n#-rules
593
+ */
641
594
  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"
701
- }
702
- }
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();
595
+ const isAi = opts?.ai ?? false;
596
+ const builder = createConfig({
597
+ name: "eslint-config-setup/node",
598
+ presets: [{ rules: {
599
+ "node/no-deprecated-api": "error",
600
+ "node/no-exports-assign": "error",
601
+ "node/no-missing-import": "off",
602
+ "node/no-missing-require": "off",
603
+ "node/no-process-exit": "warn",
604
+ "node/no-unpublished-import": "off",
605
+ "node/hashbang": "error",
606
+ "node/no-path-concat": "error",
607
+ "node/process-exit-as-throw": "error",
608
+ "node/handle-callback-err": "error",
609
+ "node/prefer-global/buffer": ["error", "always"],
610
+ "node/prefer-global/console": ["error", "always"],
611
+ "node/prefer-global/process": ["error", "always"],
612
+ "node/prefer-global/url": ["error", "always"],
613
+ "node/prefer-global/url-search-params": ["error", "always"],
614
+ "node/prefer-promises/dns": "error",
615
+ "node/prefer-promises/fs": "error"
616
+ } }],
617
+ plugins: { node: nodePlugin },
618
+ languageOptions: { globals: { ...globals.node } }
619
+ });
620
+ if (isAi) builder.addRule("node/no-unsupported-features/node-builtins", "error");
621
+ return builder.build();
711
622
  }
712
-
713
- // src/configs/package-json.ts
714
- import { configs as packageJsonConfigs } from "eslint-plugin-package-json";
623
+ //#endregion
624
+ //#region src/configs/package-json.ts
625
+ /**
626
+ * Package.json config — semantic validation of package.json files.
627
+ *
628
+ * Uses the recommended preset from eslint-plugin-package-json which includes:
629
+ * - All valid-* rules (only fire on malformed existing fields, never on missing)
630
+ * - Conservative require-* rules with ignorePrivate: true by default
631
+ * - Quality rules (no-empty-fields, unique-dependencies, etc.)
632
+ *
633
+ * Sorting/ordering rules are off by default — enabled only in AI mode.
634
+ *
635
+ * @see https://github.com/JoshuaKGoldberg/eslint-plugin-package-json
636
+ */
715
637
  function packageJsonConfig() {
716
- const recommended = packageJsonConfigs.recommended;
717
- return [
718
- {
719
- ...recommended,
720
- name: "eslint-config-setup/package-json"
721
- }
722
- ];
638
+ return [{
639
+ ...configs.recommended,
640
+ name: "eslint-config-setup/package-json"
641
+ }];
723
642
  }
643
+ /**
644
+ * Package.json AI config — enables ordering and sorting rules.
645
+ * Only active in AI mode where deterministic structure is enforced.
646
+ */
724
647
  function packageJsonAiConfig() {
725
- return [
726
- {
727
- name: "eslint-config-setup/package-json-ai",
728
- files: ["**/package.json"],
729
- rules: {
730
- // Enforce consistent property ordering in package.json
731
- // https://github.com/JoshuaKGoldberg/eslint-plugin-package-json
732
- "package-json/order-properties": "error",
733
- // Sort dependencies, scripts, exports, etc. alphabetically
734
- // https://github.com/JoshuaKGoldberg/eslint-plugin-package-json
735
- "package-json/sort-collections": "error"
736
- }
737
- }
738
- ];
648
+ return [{
649
+ name: "eslint-config-setup/package-json-ai",
650
+ files: ["**/package.json"],
651
+ rules: {
652
+ "package-json/order-properties": "error",
653
+ "package-json/sort-collections": "error"
654
+ }
655
+ }];
739
656
  }
740
-
741
- // src/configs/perfectionist.ts
742
- import perfectionistPlugin from "eslint-plugin-perfectionist";
657
+ //#endregion
658
+ //#region src/configs/perfectionist.ts
659
+ /**
660
+ * Perfectionist config — deterministic sorting of code elements.
661
+ *
662
+ * Sorts everything Prettier doesn't: imports, exports, union types,
663
+ * object keys, interface members, etc. Especially valuable for
664
+ * AI-generated code where ordering is arbitrary.
665
+ *
666
+ * Replaces `simple-import-sort` — perfectionist handles everything
667
+ * simple-import-sort does, plus TypeScript path alias recognition
668
+ * and named import sorting.
669
+ *
670
+ * Core philosophy: `partitionByNewLine: true` globally — semantic
671
+ * grouping via blank lines is preserved, within groups natural sort.
672
+ *
673
+ * @see https://perfectionist.dev
674
+ */
675
+ /**
676
+ * Mechanical sorting rules — always active.
677
+ * These have no semantic dimension: the order of imports, named exports,
678
+ * or union type members carries no meaning.
679
+ */
743
680
  function perfectionistConfig() {
744
- return [
745
- {
746
- name: "eslint-config-setup/perfectionist",
747
- plugins: {
748
- perfectionist: perfectionistPlugin
749
- },
750
- settings: {
751
- perfectionist: {
752
- type: "natural",
753
- order: "asc",
754
- partitionByNewLine: true
755
- }
756
- },
757
- rules: {
758
- // Auto-sort import statements — deterministic ordering regardless of input
759
- // Replaces simple-import-sort/imports
760
- // https://perfectionist.dev/rules/sort-imports
761
- "perfectionist/sort-imports": [
762
- "error",
763
- {
764
- partitionByNewLine: false
765
- }
766
- ],
767
- // Sort specifiers inside `import { a, b, c }` — deterministic
768
- // https://perfectionist.dev/rules/sort-named-imports
769
- "perfectionist/sort-named-imports": "error",
770
- // Sort specifiers inside `export { a, b, c }` — deterministic
771
- // https://perfectionist.dev/rules/sort-named-exports
772
- "perfectionist/sort-named-exports": "error",
773
- // Auto-sort export statements — deterministic ordering
774
- // Replaces simple-import-sort/exports
775
- // https://perfectionist.dev/rules/sort-exports
776
- "perfectionist/sort-exports": "error",
777
- // Sort union type members — `number | string` has no semantic order
778
- // https://perfectionist.dev/rules/sort-union-types
779
- "perfectionist/sort-union-types": "error",
780
- // Sort intersection type members — `A & B` has no semantic order
781
- // https://perfectionist.dev/rules/sort-intersection-types
782
- "perfectionist/sort-intersection-types": "error"
783
- }
784
- }
785
- ];
681
+ return [{
682
+ name: "eslint-config-setup/perfectionist",
683
+ plugins: { perfectionist: perfectionistPlugin },
684
+ settings: { perfectionist: {
685
+ type: "natural",
686
+ order: "asc",
687
+ partitionByNewLine: true
688
+ } },
689
+ rules: {
690
+ "perfectionist/sort-imports": ["error", { partitionByNewLine: false }],
691
+ "perfectionist/sort-named-imports": "error",
692
+ "perfectionist/sort-named-exports": "error",
693
+ "perfectionist/sort-exports": "error",
694
+ "perfectionist/sort-union-types": "error",
695
+ "perfectionist/sort-intersection-types": "error"
696
+ }
697
+ }];
786
698
  }
699
+ /**
700
+ * Structural sorting rules — AI mode only.
701
+ * These sort code elements that *could* have semantic ordering
702
+ * (e.g., object keys grouped by concern). Enforcing alphabetical
703
+ * order here is a trade-off: consistency over intent. Worth it
704
+ * for AI-generated code where "intent" is often random.
705
+ */
787
706
  function perfectionistAiConfig() {
788
- return [
789
- {
790
- name: "eslint-config-setup/perfectionist-ai",
791
- rules: {
792
- // Sort interface members — consistent shape definitions
793
- // https://perfectionist.dev/rules/sort-interfaces
794
- "perfectionist/sort-interfaces": "error",
795
- // Sort object type members — consistent type definitions
796
- // https://perfectionist.dev/rules/sort-object-types
797
- "perfectionist/sort-object-types": "error",
798
- // Sort enum members — consistent enum definitions
799
- // https://perfectionist.dev/rules/sort-enums
800
- "perfectionist/sort-enums": "error",
801
- // Sort JSX props — consistent component usage
802
- // https://perfectionist.dev/rules/sort-jsx-props
803
- "perfectionist/sort-jsx-props": "error",
804
- // Sort object keys — consistent object literals
805
- // https://perfectionist.dev/rules/sort-objects
806
- "perfectionist/sort-objects": "error",
807
- // Sort class members — consistent class structure
808
- // https://perfectionist.dev/rules/sort-classes
809
- "perfectionist/sort-classes": "error",
810
- // Sort switch cases — consistent case ordering
811
- // https://perfectionist.dev/rules/sort-switch-case
812
- "perfectionist/sort-switch-case": "error",
813
- // Sort Map entries — consistent Map initialization
814
- // https://perfectionist.dev/rules/sort-maps
815
- "perfectionist/sort-maps": "error",
816
- // Sort Set entries — consistent Set initialization
817
- // https://perfectionist.dev/rules/sort-sets
818
- "perfectionist/sort-sets": "error",
819
- // Sort array.includes() members — consistent membership checks
820
- // https://perfectionist.dev/rules/sort-array-includes
821
- "perfectionist/sort-array-includes": "error"
822
- }
823
- }
824
- ];
707
+ return [{
708
+ name: "eslint-config-setup/perfectionist-ai",
709
+ rules: {
710
+ "perfectionist/sort-interfaces": "error",
711
+ "perfectionist/sort-object-types": "error",
712
+ "perfectionist/sort-enums": "error",
713
+ "perfectionist/sort-jsx-props": "error",
714
+ "perfectionist/sort-objects": "error",
715
+ "perfectionist/sort-classes": "error",
716
+ "perfectionist/sort-switch-case": "error",
717
+ "perfectionist/sort-maps": "error",
718
+ "perfectionist/sort-sets": "error",
719
+ "perfectionist/sort-array-includes": "error"
720
+ }
721
+ }];
825
722
  }
826
-
827
- // src/configs/prettier.ts
828
- import prettierConfig from "eslint-config-prettier";
723
+ //#endregion
724
+ //#region src/configs/prettier.ts
725
+ /**
726
+ * Prettier compat config — disables all ESLint rules that conflict with Prettier.
727
+ * Must be the last config block (before OxLint if enabled).
728
+ *
729
+ * Preset: eslint-config-prettier (turns off ~30 formatting rules)
730
+ * @see https://github.com/prettier/eslint-config-prettier#readme
731
+ */
829
732
  function prettierCompatConfig() {
830
- return createConfig({
831
- name: "eslint-config-setup/prettier",
832
- presets: [prettierConfig]
833
- }).build();
733
+ return createConfig({
734
+ name: "eslint-config-setup/prettier",
735
+ presets: [prettierConfig]
736
+ }).build();
834
737
  }
835
-
836
- // src/configs/react.ts
837
- import eslintReactPlugin2 from "@eslint-react/eslint-plugin";
838
- import stylisticPlugin from "@stylistic/eslint-plugin";
839
- import jsxA11yPlugin from "eslint-plugin-jsx-a11y";
840
- import reactHooksPlugin from "eslint-plugin-react-hooks";
841
- import reactPerfPlugin from "eslint-plugin-react-perf";
842
- import reactRefreshPlugin from "eslint-plugin-react-refresh";
843
- import globals2 from "globals";
844
-
845
- // src/file-patterns.ts
846
- var TYPESCRIPT_SOURCE_FILES = [
847
- "**/*.ts",
848
- "**/*.tsx",
849
- "**/*.mts",
850
- "**/*.cts"
738
+ //#endregion
739
+ //#region src/file-patterns.ts
740
+ const TYPESCRIPT_SOURCE_FILES = [
741
+ "**/*.ts",
742
+ "**/*.tsx",
743
+ "**/*.mts",
744
+ "**/*.cts"
851
745
  ];
852
- var MARKDOWN_CODE_BLOCK_FILES = ["**/*.{md,mdx}/**"];
853
-
854
- // src/plugins/react-compat.ts
855
- import eslintReactPlugin from "@eslint-react/eslint-plugin";
856
- var coreRules = eslintReactPlugin.rules;
746
+ const MARKDOWN_CODE_BLOCK_FILES = ["**/*.{md,mdx}/**"];
747
+ //#endregion
748
+ //#region src/plugins/react-compat.ts
749
+ /**
750
+ * React Compat Plugin — merges all `@eslint-react` sub-plugins into a single
751
+ * `react` namespace and re-exports rules under legacy `eslint-plugin-react`
752
+ * names where a 1:1 equivalent exists.
753
+ *
754
+ * **Why?** OxLint implements many React rules under the classic `react/` prefix
755
+ * with legacy names (e.g. `react/jsx-key`, `react/no-danger`). By registering
756
+ * the modern `@eslint-react` rule implementations under those legacy names, we
757
+ * get automatic OxLint coverage — OxLint runs the fast Rust check and ESLint
758
+ * skips the JS version via `eslint-plugin-oxlint`'s `flat/react` config.
759
+ *
760
+ * Rules without a legacy equivalent keep their `@eslint-react` short name
761
+ * (e.g. `react/no-context-provider`, `react/no-leaked-event-listener`).
762
+ * As OxLint expands React support, more rules will automatically be covered.
763
+ *
764
+ * @see https://eslint-react.xyz/docs/rules/overview - `@eslint-react` rules overview
765
+ * @see https://oxc.rs/docs/guide/usage/linter/rules.html - OxLint rules
766
+ */
767
+ const coreRules = eslintReactPlugin.rules;
857
768
  function selectPrefixedRules(prefix) {
858
- const result = {};
859
- for (const [name, rule] of Object.entries(coreRules)) {
860
- if (name.startsWith(prefix)) {
861
- result[name.slice(prefix.length)] = rule;
862
- }
863
- }
864
- return result;
769
+ const result = {};
770
+ for (const [name, rule] of Object.entries(coreRules)) if (name.startsWith(prefix)) result[name.slice(prefix.length)] = rule;
771
+ return result;
865
772
  }
866
- var domRules = selectPrefixedRules("dom-");
867
- var jsxRules = selectPrefixedRules("jsx-");
868
- var namingRules = selectPrefixedRules("naming-convention-");
869
- var rscRules = selectPrefixedRules("rsc-");
870
- var webApiRules = selectPrefixedRules("web-api-");
871
- var LEGACY_ALIASES = {
872
- // ── Core: identical names ──────────────────────────────────────────
873
- "no-access-state-in-setstate": [coreRules, "no-access-state-in-setstate"],
874
- "no-array-index-key": [coreRules, "no-array-index-key"],
875
- "no-children-prop": [jsxRules, "no-children-prop"],
876
- "no-direct-mutation-state": [coreRules, "no-direct-mutation-state"],
877
- "no-unused-class-component-members": [
878
- coreRules,
879
- "no-unused-class-component-members"
880
- ],
881
- "no-unused-state": [coreRules, "no-unused-state"],
882
- "jsx-no-comment-textnodes": [jsxRules, "no-comment-textnodes"],
883
- // ── Core: renamed 1:1 ──────────────────────────────────────────────
884
- "jsx-key": [coreRules, "no-missing-key"],
885
- "jsx-key-before-spread": [jsxRules, "no-key-after-spread"],
886
- "jsx-no-constructed-context-values": [
887
- coreRules,
888
- "no-unstable-context-value"
889
- ],
890
- "jsx-no-leaked-render": [coreRules, "no-leaked-conditional-rendering"],
891
- "jsx-no-useless-fragment": [jsxRules, "no-useless-fragment"],
892
- "no-object-type-as-default-prop": [coreRules, "no-unstable-default-props"],
893
- "no-unstable-nested-components": [
894
- coreRules,
895
- "no-nested-component-definitions"
896
- ],
897
- "display-name": [coreRules, "no-missing-component-display-name"],
898
- "forward-ref-uses-ref": [coreRules, "no-forward-ref"],
899
- "no-did-mount-set-state": [
900
- coreRules,
901
- "no-set-state-in-component-did-mount"
902
- ],
903
- "no-did-update-set-state": [
904
- coreRules,
905
- "no-set-state-in-component-did-update"
906
- ],
907
- "no-will-update-set-state": [
908
- coreRules,
909
- "no-set-state-in-component-will-update"
910
- ],
911
- // ── Naming convention → legacy names ───────────────────────────────
912
- "hook-use-state": [coreRules, "use-state"],
913
- // ── DOM → legacy react/ names (not react-dom/) ─────────────────────
914
- "no-danger": [domRules, "no-dangerously-set-innerhtml"],
915
- "no-danger-with-children": [
916
- domRules,
917
- "no-dangerously-set-innerhtml-with-children"
918
- ],
919
- "no-find-dom-node": [domRules, "no-find-dom-node"],
920
- "no-namespace": [jsxRules, "no-namespace"],
921
- "no-render-return-value": [domRules, "no-render-return-value"],
922
- "jsx-no-script-url": [domRules, "no-script-url"],
923
- "jsx-no-target-blank": [domRules, "no-unsafe-target-blank"],
924
- "no-unknown-property": [domRules, "no-unknown-property"],
925
- "void-dom-elements-no-children": [
926
- domRules,
927
- "no-void-elements-with-children"
928
- ],
929
- "button-has-type": [domRules, "no-missing-button-type"],
930
- "iframe-missing-sandbox": [domRules, "no-missing-iframe-sandbox"],
931
- "style-prop-object": [domRules, "no-string-style-prop"]
773
+ const domRules = selectPrefixedRules("dom-");
774
+ const jsxRules = selectPrefixedRules("jsx-");
775
+ const namingRules = selectPrefixedRules("naming-convention-");
776
+ const rscRules = selectPrefixedRules("rsc-");
777
+ const webApiRules = selectPrefixedRules("web-api-");
778
+ const LEGACY_ALIASES = {
779
+ "no-access-state-in-setstate": [coreRules, "no-access-state-in-setstate"],
780
+ "no-array-index-key": [coreRules, "no-array-index-key"],
781
+ "no-children-prop": [jsxRules, "no-children-prop"],
782
+ "no-direct-mutation-state": [coreRules, "no-direct-mutation-state"],
783
+ "no-unused-class-component-members": [coreRules, "no-unused-class-component-members"],
784
+ "no-unused-state": [coreRules, "no-unused-state"],
785
+ "jsx-no-comment-textnodes": [jsxRules, "no-comment-textnodes"],
786
+ "jsx-key": [coreRules, "no-missing-key"],
787
+ "jsx-key-before-spread": [jsxRules, "no-key-after-spread"],
788
+ "jsx-no-constructed-context-values": [coreRules, "no-unstable-context-value"],
789
+ "jsx-no-leaked-render": [coreRules, "no-leaked-conditional-rendering"],
790
+ "jsx-no-useless-fragment": [jsxRules, "no-useless-fragment"],
791
+ "no-object-type-as-default-prop": [coreRules, "no-unstable-default-props"],
792
+ "no-unstable-nested-components": [coreRules, "no-nested-component-definitions"],
793
+ "display-name": [coreRules, "no-missing-component-display-name"],
794
+ "forward-ref-uses-ref": [coreRules, "no-forward-ref"],
795
+ "no-did-mount-set-state": [coreRules, "no-set-state-in-component-did-mount"],
796
+ "no-did-update-set-state": [coreRules, "no-set-state-in-component-did-update"],
797
+ "no-will-update-set-state": [coreRules, "no-set-state-in-component-will-update"],
798
+ "hook-use-state": [coreRules, "use-state"],
799
+ "no-danger": [domRules, "no-dangerously-set-innerhtml"],
800
+ "no-danger-with-children": [domRules, "no-dangerously-set-innerhtml-with-children"],
801
+ "no-find-dom-node": [domRules, "no-find-dom-node"],
802
+ "no-namespace": [jsxRules, "no-namespace"],
803
+ "no-render-return-value": [domRules, "no-render-return-value"],
804
+ "jsx-no-script-url": [domRules, "no-script-url"],
805
+ "jsx-no-target-blank": [domRules, "no-unsafe-target-blank"],
806
+ "no-unknown-property": [domRules, "no-unknown-property"],
807
+ "void-dom-elements-no-children": [domRules, "no-void-elements-with-children"],
808
+ "button-has-type": [domRules, "no-missing-button-type"],
809
+ "iframe-missing-sandbox": [domRules, "no-missing-iframe-sandbox"],
810
+ "style-prop-object": [domRules, "no-string-style-prop"]
932
811
  };
933
- var aliasedOriginals = /* @__PURE__ */ new Set();
934
- for (const [source, originalName] of Object.values(LEGACY_ALIASES)) {
935
- if (source === coreRules) {
936
- aliasedOriginals.add(originalName);
937
- } else if (source === domRules) {
938
- aliasedOriginals.add(`dom-${originalName}`);
939
- } else if (source === jsxRules) {
940
- aliasedOriginals.add(`jsx-${originalName}`);
941
- } else if (source === namingRules) {
942
- aliasedOriginals.add(`naming-convention-${originalName}`);
943
- } else if (source === rscRules) {
944
- aliasedOriginals.add(`rsc-${originalName}`);
945
- } else if (source === webApiRules) {
946
- aliasedOriginals.add(`web-api-${originalName}`);
947
- }
948
- }
949
- var mergedRules = {};
950
- for (const [name, rule] of Object.entries(coreRules)) {
951
- if (!aliasedOriginals.has(name)) {
952
- mergedRules[name] = rule;
953
- }
954
- }
955
- var subPlugins = [
956
- [domRules, /* @__PURE__ */ new Set(["prefer-namespace-import"])],
957
- [jsxRules, /* @__PURE__ */ new Set()],
958
- [webApiRules, /* @__PURE__ */ new Set()],
959
- [namingRules, /* @__PURE__ */ new Set()],
960
- [rscRules, /* @__PURE__ */ new Set()]
812
+ const aliasedOriginals = /* @__PURE__ */ new Set();
813
+ for (const [source, originalName] of Object.values(LEGACY_ALIASES)) if (source === coreRules) aliasedOriginals.add(originalName);
814
+ else if (source === domRules) aliasedOriginals.add(`dom-${originalName}`);
815
+ else if (source === jsxRules) aliasedOriginals.add(`jsx-${originalName}`);
816
+ else if (source === namingRules) aliasedOriginals.add(`naming-convention-${originalName}`);
817
+ else if (source === rscRules) aliasedOriginals.add(`rsc-${originalName}`);
818
+ else if (source === webApiRules) aliasedOriginals.add(`web-api-${originalName}`);
819
+ const mergedRules = {};
820
+ for (const [name, rule] of Object.entries(coreRules)) if (!aliasedOriginals.has(name)) mergedRules[name] = rule;
821
+ const subPlugins = [
822
+ [domRules, /* @__PURE__ */ new Set(["prefer-namespace-import"])],
823
+ [jsxRules, /* @__PURE__ */ new Set()],
824
+ [webApiRules, /* @__PURE__ */ new Set()],
825
+ [namingRules, /* @__PURE__ */ new Set()],
826
+ [rscRules, /* @__PURE__ */ new Set()]
961
827
  ];
962
- for (const [rules, skip] of subPlugins) {
963
- for (const [name, rule] of Object.entries(rules)) {
964
- if (skip.has(name)) continue;
965
- const isAliased = Object.values(LEGACY_ALIASES).some(
966
- ([source, originalName]) => source === rules && originalName === name
967
- );
968
- if (!isAliased) {
969
- mergedRules[name] = rule;
970
- }
971
- }
972
- }
973
- for (const [legacyName, [source, originalName]] of Object.entries(
974
- LEGACY_ALIASES
975
- )) {
976
- mergedRules[legacyName] = source[originalName];
828
+ for (const [rules, skip] of subPlugins) for (const [name, rule] of Object.entries(rules)) {
829
+ if (skip.has(name)) continue;
830
+ if (!Object.values(LEGACY_ALIASES).some(([source, originalName]) => source === rules && originalName === name)) mergedRules[name] = rule;
977
831
  }
978
- var originalToCompat = /* @__PURE__ */ new Map();
832
+ for (const [legacyName, [source, originalName]] of Object.entries(LEGACY_ALIASES)) mergedRules[legacyName] = source[originalName];
833
+ const originalToCompat = /* @__PURE__ */ new Map();
979
834
  for (const [legacyName, [source, originalName]] of Object.entries(LEGACY_ALIASES)) {
980
- originalToCompat.set(originalName, legacyName);
981
- if (source === domRules) {
982
- originalToCompat.set(`dom-${originalName}`, legacyName);
983
- } else if (source === jsxRules) {
984
- originalToCompat.set(`jsx-${originalName}`, legacyName);
985
- } else if (source === namingRules) {
986
- originalToCompat.set(`naming-convention-${originalName}`, legacyName);
987
- } else if (source === rscRules) {
988
- originalToCompat.set(`rsc-${originalName}`, legacyName);
989
- } else if (source === webApiRules) {
990
- originalToCompat.set(`web-api-${originalName}`, legacyName);
991
- }
835
+ originalToCompat.set(originalName, legacyName);
836
+ if (source === domRules) originalToCompat.set(`dom-${originalName}`, legacyName);
837
+ else if (source === jsxRules) originalToCompat.set(`jsx-${originalName}`, legacyName);
838
+ else if (source === namingRules) originalToCompat.set(`naming-convention-${originalName}`, legacyName);
839
+ else if (source === rscRules) originalToCompat.set(`rsc-${originalName}`, legacyName);
840
+ else if (source === webApiRules) originalToCompat.set(`web-api-${originalName}`, legacyName);
992
841
  }
993
- function translatePresetRules(presetRules2) {
994
- const result = {};
995
- for (const [key, value] of Object.entries(presetRules2)) {
996
- const shortName = key.replace(
997
- /^@eslint-react\/(?:dom|web-api|naming-convention|rsc)\//,
998
- ""
999
- ).replace(/^@eslint-react\//, "").replace(/^(?:dom|web-api|naming-convention|rsc)-/, "");
1000
- const compatName = originalToCompat.get(shortName) ?? shortName;
1001
- if (compatName in mergedRules) {
1002
- result[`react/${compatName}`] = value;
1003
- }
1004
- }
1005
- return result;
842
+ /**
843
+ * Translates rules from an `@eslint-react` preset (e.g. `recommended`, `strict`)
844
+ * into `react/` compat names that match our unified plugin namespace.
845
+ *
846
+ * Example: `"@eslint-react/no-missing-key": "error"` → `"react/jsx-key": "error"`
847
+ */
848
+ function translatePresetRules(presetRules) {
849
+ const result = {};
850
+ for (const [key, value] of Object.entries(presetRules)) {
851
+ const shortName = key.replace(/^@eslint-react\/(?:dom|web-api|naming-convention|rsc)\//, "").replace(/^@eslint-react\//, "").replace(/^(?:dom|web-api|naming-convention|rsc)-/, "");
852
+ const compatName = originalToCompat.get(shortName) ?? shortName;
853
+ if (compatName in mergedRules) result[`react/${compatName}`] = value;
854
+ }
855
+ return result;
1006
856
  }
1007
- var reactCompatPlugin = {
1008
- meta: {
1009
- name: "react-compat",
1010
- version: "1.0.0"
1011
- },
1012
- rules: mergedRules
857
+ /**
858
+ * Unified React plugin registered as `react` in ESLint flat config.
859
+ * Contains all `@eslint-react` rule implementations under OxLint-compatible names.
860
+ */
861
+ const reactCompatPlugin = {
862
+ meta: {
863
+ name: "react-compat",
864
+ version: "1.0.0"
865
+ },
866
+ rules: mergedRules
1013
867
  };
1014
-
1015
- // src/configs/react.ts
1016
- var typedConfigs = eslintReactPlugin2.configs;
1017
- var strictTypeCheckedRules = typedConfigs["strict-type-checked"].rules;
1018
- var REACT_HOOKS_REPLACED_ESLINT_REACT_RULES = [
1019
- "error-boundaries",
1020
- "exhaustive-deps",
1021
- "purity",
1022
- "rules-of-hooks",
1023
- "set-state-in-effect",
1024
- "set-state-in-render",
1025
- "static-components",
1026
- "unsupported-syntax",
1027
- "use-memo"
1028
- ];
1029
- var reactHooksReplacedRules = new Set(
1030
- REACT_HOOKS_REPLACED_ESLINT_REACT_RULES
1031
- );
868
+ //#endregion
869
+ //#region src/configs/react.ts
870
+ const strictTypeCheckedRules = eslintReactPlugin.configs["strict-type-checked"].rules;
871
+ const reactHooksReplacedRules = /* @__PURE__ */ new Set([
872
+ "error-boundaries",
873
+ "exhaustive-deps",
874
+ "purity",
875
+ "rules-of-hooks",
876
+ "set-state-in-effect",
877
+ "set-state-in-render",
878
+ "static-components",
879
+ "unsupported-syntax",
880
+ "use-memo"
881
+ ]);
1032
882
  function filterReactHooksRules(rules) {
1033
- const filtered = {};
1034
- for (const [ruleName, value] of Object.entries(rules)) {
1035
- const shortName = ruleName.replace(/^@eslint-react\//, "");
1036
- if (!reactHooksReplacedRules.has(shortName)) {
1037
- filtered[ruleName] = value;
1038
- }
1039
- }
1040
- return filtered;
883
+ const filtered = {};
884
+ for (const [ruleName, value] of Object.entries(rules)) {
885
+ const shortName = ruleName.replace(/^@eslint-react\//, "");
886
+ if (!reactHooksReplacedRules.has(shortName)) filtered[ruleName] = value;
887
+ }
888
+ return filtered;
1041
889
  }
1042
- var presetRules = translatePresetRules(filterReactHooksRules(strictTypeCheckedRules));
1043
- var reactHooksRecommendedLatest = reactHooksPlugin.configs.flat["recommended-latest"];
1044
- var reactHooksPluginForEslint = reactHooksPlugin;
1045
- var EXTRA_REACT_RULES = {
1046
- "react/no-duplicate-key": "error",
1047
- "react/no-implicit-key": "error",
1048
- "react/no-unknown-property": "error",
1049
- "react/style-prop-object": "error"
1050
- };
1051
- var AI_ADDED_REACT_RULES = {
1052
- "react/no-unused-state": "error"
890
+ const presetRules = translatePresetRules(filterReactHooksRules(strictTypeCheckedRules));
891
+ const reactHooksRecommendedLatest = reactHooksPlugin.configs.flat["recommended-latest"];
892
+ const reactHooksPluginForEslint = reactHooksPlugin;
893
+ const EXTRA_REACT_RULES = {
894
+ "react/no-duplicate-key": "error",
895
+ "react/no-implicit-key": "error",
896
+ "react/no-unknown-property": "error",
897
+ "react/style-prop-object": "error"
1053
898
  };
1054
- var AI_REACT_PERF_RULES = {
1055
- "react-perf/jsx-no-jsx-as-prop": "error",
1056
- "react-perf/jsx-no-new-array-as-prop": "error",
1057
- "react-perf/jsx-no-new-function-as-prop": "error",
1058
- "react-perf/jsx-no-new-object-as-prop": "error"
899
+ const AI_ADDED_REACT_RULES = { "react/no-unused-state": "error" };
900
+ const AI_REACT_PERF_RULES = {
901
+ "react-perf/jsx-no-jsx-as-prop": "error",
902
+ "react-perf/jsx-no-new-array-as-prop": "error",
903
+ "react-perf/jsx-no-new-function-as-prop": "error",
904
+ "react-perf/jsx-no-new-object-as-prop": "error"
1059
905
  };
1060
- var JSX_A11Y_ERROR_RULES = {
1061
- "jsx-a11y/alt-text": "error",
1062
- "jsx-a11y/anchor-has-content": "error",
1063
- "jsx-a11y/anchor-is-valid": "error",
1064
- "jsx-a11y/aria-activedescendant-has-tabindex": "error",
1065
- "jsx-a11y/aria-props": "error",
1066
- "jsx-a11y/aria-proptypes": "error",
1067
- "jsx-a11y/aria-role": "error",
1068
- "jsx-a11y/aria-unsupported-elements": "error",
1069
- "jsx-a11y/click-events-have-key-events": "error",
1070
- "jsx-a11y/heading-has-content": "error",
1071
- "jsx-a11y/html-has-lang": "error",
1072
- "jsx-a11y/img-redundant-alt": "error",
1073
- "jsx-a11y/label-has-associated-control": "error",
1074
- "jsx-a11y/mouse-events-have-key-events": "error",
1075
- "jsx-a11y/no-access-key": "error",
1076
- "jsx-a11y/no-distracting-elements": "error",
1077
- "jsx-a11y/no-redundant-roles": "error",
1078
- "jsx-a11y/role-has-required-aria-props": "error",
1079
- "jsx-a11y/role-supports-aria-props": "error",
1080
- "jsx-a11y/scope": "error",
1081
- "jsx-a11y/tabindex-no-positive": "error",
1082
- "jsx-a11y/lang": "error",
1083
- "jsx-a11y/autocomplete-valid": "error"
906
+ const JSX_A11Y_ERROR_RULES = {
907
+ "jsx-a11y/alt-text": "error",
908
+ "jsx-a11y/anchor-has-content": "error",
909
+ "jsx-a11y/anchor-is-valid": "error",
910
+ "jsx-a11y/aria-activedescendant-has-tabindex": "error",
911
+ "jsx-a11y/aria-props": "error",
912
+ "jsx-a11y/aria-proptypes": "error",
913
+ "jsx-a11y/aria-role": "error",
914
+ "jsx-a11y/aria-unsupported-elements": "error",
915
+ "jsx-a11y/click-events-have-key-events": "error",
916
+ "jsx-a11y/heading-has-content": "error",
917
+ "jsx-a11y/html-has-lang": "error",
918
+ "jsx-a11y/img-redundant-alt": "error",
919
+ "jsx-a11y/label-has-associated-control": "error",
920
+ "jsx-a11y/mouse-events-have-key-events": "error",
921
+ "jsx-a11y/no-access-key": "error",
922
+ "jsx-a11y/no-distracting-elements": "error",
923
+ "jsx-a11y/no-redundant-roles": "error",
924
+ "jsx-a11y/role-has-required-aria-props": "error",
925
+ "jsx-a11y/role-supports-aria-props": "error",
926
+ "jsx-a11y/scope": "error",
927
+ "jsx-a11y/tabindex-no-positive": "error",
928
+ "jsx-a11y/lang": "error",
929
+ "jsx-a11y/autocomplete-valid": "error"
1084
930
  };
1085
- var AI_PROMOTED_REACT_RULES = [
1086
- "react/jsx-no-comment-textnodes",
1087
- "react/jsx-no-useless-fragment",
1088
- "react/jsx-no-constructed-context-values",
1089
- "react/no-array-index-key",
1090
- "react/no-object-type-as-default-prop",
1091
- "react/jsx-no-target-blank",
1092
- "react/button-has-type",
1093
- "react/iframe-missing-sandbox",
1094
- "react/forward-ref-uses-ref",
1095
- "react/no-context-provider",
1096
- "react/no-use-context",
1097
- "react/no-leaked-event-listener",
1098
- "react/no-leaked-interval",
1099
- "react/no-leaked-timeout",
1100
- "react/no-leaked-resize-observer"
931
+ const AI_PROMOTED_REACT_RULES = [
932
+ "react/jsx-no-comment-textnodes",
933
+ "react/jsx-no-useless-fragment",
934
+ "react/jsx-no-constructed-context-values",
935
+ "react/no-array-index-key",
936
+ "react/no-object-type-as-default-prop",
937
+ "react/jsx-no-target-blank",
938
+ "react/button-has-type",
939
+ "react/iframe-missing-sandbox",
940
+ "react/forward-ref-uses-ref",
941
+ "react/no-context-provider",
942
+ "react/no-use-context",
943
+ "react/no-leaked-event-listener",
944
+ "react/no-leaked-interval",
945
+ "react/no-leaked-timeout",
946
+ "react/no-leaked-resize-observer"
1101
947
  ];
1102
- var AI_A11Y_RULES = [
1103
- "jsx-a11y/no-static-element-interactions",
1104
- "jsx-a11y/no-noninteractive-element-interactions",
1105
- "jsx-a11y/interactive-supports-focus"
948
+ const AI_A11Y_RULES = [
949
+ "jsx-a11y/no-static-element-interactions",
950
+ "jsx-a11y/no-noninteractive-element-interactions",
951
+ "jsx-a11y/interactive-supports-focus"
1106
952
  ];
1107
- function addRules2(builder, rules) {
1108
- for (const [ruleName, value] of Object.entries(rules)) {
1109
- builder.addRule(ruleName, value);
1110
- }
953
+ function addRules(builder, rules) {
954
+ for (const [ruleName, value] of Object.entries(rules)) builder.addRule(ruleName, value);
1111
955
  }
1112
956
  function addExtraReactRules(builder) {
1113
- addRules2(builder, EXTRA_REACT_RULES);
957
+ addRules(builder, EXTRA_REACT_RULES);
1114
958
  }
1115
959
  function addStylisticReactRules(builder) {
1116
- builder.addRule("@stylistic/jsx-self-closing-comp", "error");
1117
- builder.addRule("@stylistic/jsx-curly-brace-presence", [
1118
- "error",
1119
- { props: "never", children: "never" }
1120
- ]);
960
+ builder.addRule("@stylistic/jsx-self-closing-comp", "error");
961
+ builder.addRule("@stylistic/jsx-curly-brace-presence", ["error", {
962
+ props: "never",
963
+ children: "never"
964
+ }]);
1121
965
  }
1122
966
  function addReactRefreshRules(builder) {
1123
- builder.addRule("react-refresh/only-export-components", [
1124
- "warn",
1125
- { allowConstantExport: true }
1126
- ]);
967
+ builder.addRule("react-refresh/only-export-components", ["warn", { allowConstantExport: true }]);
1127
968
  }
1128
969
  function addAccessibilityRules(builder) {
1129
- addRules2(builder, JSX_A11Y_ERROR_RULES);
1130
- builder.addRule("jsx-a11y/no-autofocus", ["error", { ignoreNonDOM: true }]);
970
+ addRules(builder, JSX_A11Y_ERROR_RULES);
971
+ builder.addRule("jsx-a11y/no-autofocus", ["error", { ignoreNonDOM: true }]);
1131
972
  }
1132
973
  function applyAiReactRules(builder) {
1133
- for (const ruleName of AI_PROMOTED_REACT_RULES) {
1134
- builder.overrideSeverity(ruleName, "error");
1135
- }
1136
- addRules2(builder, AI_ADDED_REACT_RULES);
1137
- addRules2(builder, AI_REACT_PERF_RULES);
1138
- for (const ruleName of AI_A11Y_RULES) {
1139
- builder.addRule(ruleName, "error");
1140
- }
974
+ for (const ruleName of AI_PROMOTED_REACT_RULES) builder.overrideSeverity(ruleName, "error");
975
+ addRules(builder, AI_ADDED_REACT_RULES);
976
+ addRules(builder, AI_REACT_PERF_RULES);
977
+ for (const ruleName of AI_A11Y_RULES) builder.addRule(ruleName, "error");
1141
978
  }
1142
979
  function addReactFileOverrides(builder) {
1143
- builder.addFileOverride(
1144
- "eslint-config-setup/react-typescript",
1145
- [...TYPESCRIPT_SOURCE_FILES],
1146
- {
1147
- // Allow async event handlers — onClick={async () => {...}} is idiomatic React
1148
- "@typescript-eslint/no-misused-promises": [
1149
- "error",
1150
- { checksVoidReturn: false }
1151
- ]
1152
- }
1153
- );
980
+ builder.addFileOverride("eslint-config-setup/react-typescript", [...TYPESCRIPT_SOURCE_FILES], { "@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }] });
1154
981
  }
982
+ /**
983
+ * React config — React 19+, Hooks, JSX accessibility, and Web API leak detection.
984
+ *
985
+ * Uses `@eslint-react` (eslint-plugin-react-x) for React, DOM, Web API,
986
+ * naming-convention, and RSC rules under the `react/` compat namespace.
987
+ * Rules of Hooks and React Compiler linting come from Meta's
988
+ * eslint-plugin-react-hooks under the native `react-hooks/` namespace.
989
+ *
990
+ * Base React severities come from the `@eslint-react` `strict-type-checked` preset.
991
+ * When `ai: true`, preset "warn" rules that AI should always get right are
992
+ * promoted to "error", and React performance rules are enabled as errors.
993
+ *
994
+ * @see https://eslint-react.xyz/docs/rules/overview
995
+ * @see https://www.npmjs.com/package/eslint-plugin-react-hooks
996
+ * @see https://github.com/cvazac/eslint-plugin-react-perf
997
+ * @see https://github.com/ArnaudBarre/eslint-plugin-react-refresh
998
+ * @see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#supported-rules
999
+ */
1155
1000
  function reactConfig(opts) {
1156
- const isAi = opts?.ai ?? false;
1157
- const builder = createConfig({
1158
- name: "eslint-config-setup/react",
1159
- presets: [{ rules: presetRules }, reactHooksRecommendedLatest],
1160
- plugins: {
1161
- react: reactCompatPlugin,
1162
- "@stylistic": stylisticPlugin,
1163
- "react-hooks": reactHooksPluginForEslint,
1164
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Plugin from untyped module
1165
- "react-perf": reactPerfPlugin,
1166
- "react-refresh": reactRefreshPlugin,
1167
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Plugin from untyped module
1168
- "jsx-a11y": jsxA11yPlugin
1169
- },
1170
- languageOptions: {
1171
- globals: {
1172
- ...globals2.browser
1173
- },
1174
- parserOptions: {
1175
- ecmaFeatures: {
1176
- jsx: true
1177
- }
1178
- }
1179
- }
1180
- });
1181
- addExtraReactRules(builder);
1182
- addStylisticReactRules(builder);
1183
- addReactRefreshRules(builder);
1184
- addAccessibilityRules(builder);
1185
- if (isAi) {
1186
- applyAiReactRules(builder);
1187
- }
1188
- addReactFileOverrides(builder);
1189
- return builder.build();
1001
+ const isAi = opts?.ai ?? false;
1002
+ const builder = createConfig({
1003
+ name: "eslint-config-setup/react",
1004
+ presets: [{ rules: presetRules }, reactHooksRecommendedLatest],
1005
+ plugins: {
1006
+ react: reactCompatPlugin,
1007
+ "@stylistic": stylisticPlugin,
1008
+ "react-hooks": reactHooksPluginForEslint,
1009
+ "react-perf": reactPerfPlugin,
1010
+ "react-refresh": reactRefreshPlugin,
1011
+ "jsx-a11y": jsxA11yPlugin
1012
+ },
1013
+ languageOptions: {
1014
+ globals: { ...globals.browser },
1015
+ parserOptions: { ecmaFeatures: { jsx: true } }
1016
+ }
1017
+ });
1018
+ addExtraReactRules(builder);
1019
+ addStylisticReactRules(builder);
1020
+ addReactRefreshRules(builder);
1021
+ addAccessibilityRules(builder);
1022
+ if (isAi) applyAiReactRules(builder);
1023
+ addReactFileOverrides(builder);
1024
+ return builder.build();
1190
1025
  }
1191
-
1192
- // src/configs/react-effect.ts
1193
- import reactEffectPlugin from "eslint-plugin-react-you-might-not-need-an-effect";
1026
+ //#endregion
1027
+ //#region src/configs/react-effect.ts
1028
+ /**
1029
+ * React effect config — catches unnecessary useEffect anti-patterns.
1030
+ * Codifies the patterns from the React docs article "You Might Not Need an Effect".
1031
+ *
1032
+ * Complements react-hooks (which checks dependency arrays are correct) by
1033
+ * detecting when the entire effect is unnecessary — derived state, chained
1034
+ * state updates, prop-triggered resets, and more.
1035
+ *
1036
+ * @see https://react.dev/learn/you-might-not-need-an-effect
1037
+ * @see https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1038
+ */
1194
1039
  function reactEffectConfig() {
1195
- return [
1196
- {
1197
- name: "eslint-config-setup/react-effect",
1198
- plugins: {
1199
- "react-you-might-not-need-an-effect": reactEffectPlugin
1200
- },
1201
- rules: {
1202
- // Disallow storing derived state in an effect — compute at render time or useMemo
1203
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1204
- "react-you-might-not-need-an-effect/no-derived-state": "error",
1205
- // Disallow chaining state updates in an effect — update together instead
1206
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1207
- "react-you-might-not-need-an-effect/no-chain-state-updates": "error",
1208
- // Disallow using state + effect as an event handler — call logic directly
1209
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1210
- "react-you-might-not-need-an-effect/no-event-handler": "error",
1211
- // Disallow adjusting state when a prop changes — compute inline during render
1212
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1213
- "react-you-might-not-need-an-effect/no-adjust-state-on-prop-change": "warn",
1214
- // Disallow resetting all state when a prop changes — use the key prop instead
1215
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1216
- "react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change": "error",
1217
- // Disallow passing live state to parent via effect — lift state up instead
1218
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1219
- "react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "error",
1220
- // Disallow passing fetched data to parent via effect — fetch in parent instead
1221
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1222
- "react-you-might-not-need-an-effect/no-pass-data-to-parent": "error",
1223
- // Disallow external-store subscriptions in an effect — use useSyncExternalStore
1224
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1225
- "react-you-might-not-need-an-effect/no-external-store-subscription": "error",
1226
- // Disallow initializing state in an effect — pass initial value to useState
1227
- // https://github.com/nickjvandyke/eslint-plugin-react-you-might-not-need-an-effect
1228
- "react-you-might-not-need-an-effect/no-initialize-state": "error"
1229
- }
1230
- }
1231
- ];
1040
+ return [{
1041
+ name: "eslint-config-setup/react-effect",
1042
+ plugins: { "react-you-might-not-need-an-effect": reactEffectPlugin },
1043
+ rules: {
1044
+ "react-you-might-not-need-an-effect/no-derived-state": "error",
1045
+ "react-you-might-not-need-an-effect/no-chain-state-updates": "error",
1046
+ "react-you-might-not-need-an-effect/no-event-handler": "error",
1047
+ "react-you-might-not-need-an-effect/no-adjust-state-on-prop-change": "warn",
1048
+ "react-you-might-not-need-an-effect/no-reset-all-state-on-prop-change": "error",
1049
+ "react-you-might-not-need-an-effect/no-pass-live-state-to-parent": "error",
1050
+ "react-you-might-not-need-an-effect/no-pass-data-to-parent": "error",
1051
+ "react-you-might-not-need-an-effect/no-external-store-subscription": "error",
1052
+ "react-you-might-not-need-an-effect/no-initialize-state": "error"
1053
+ }
1054
+ }];
1232
1055
  }
1233
-
1234
- // src/configs/regexp.ts
1235
- import { configs as regexpConfigs } from "eslint-plugin-regexp";
1056
+ //#endregion
1057
+ //#region src/configs/regexp.ts
1058
+ /**
1059
+ * RegExp config — uses the full `flat/recommended` preset from eslint-plugin-regexp.
1060
+ * Validates regular expression syntax, detects common mistakes, and suggests
1061
+ * simpler patterns.
1062
+ *
1063
+ * Preset: eslint-plugin-regexp flat/recommended (all rules at their default severity)
1064
+ * @see https://ota-meshi.github.io/eslint-plugin-regexp/
1065
+ * @see https://ota-meshi.github.io/eslint-plugin-regexp/rules/
1066
+ */
1236
1067
  function regexpConfig(opts) {
1237
- const isAi = opts?.ai ?? false;
1238
- const builder = createConfig({
1239
- name: "eslint-config-setup/regexp",
1240
- presets: [regexpConfigs["flat/recommended"]]
1241
- }).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");
1242
- if (isAi) {
1243
- builder.addRule("regexp/prefer-lookaround", "error");
1244
- builder.addRule("regexp/prefer-named-backreference", "error");
1245
- builder.addRule("regexp/prefer-named-replacement", "error");
1246
- builder.addRule("regexp/prefer-quantifier", "error");
1247
- builder.addRule("regexp/prefer-result-array-groups", "error");
1248
- builder.addRule("regexp/require-unicode-sets-regexp", "error");
1249
- }
1250
- return builder.build();
1068
+ const isAi = opts?.ai ?? false;
1069
+ const builder = createConfig({
1070
+ name: "eslint-config-setup/regexp",
1071
+ presets: [configs$1["flat/recommended"]]
1072
+ }).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");
1073
+ if (isAi) {
1074
+ builder.addRule("regexp/prefer-lookaround", "error");
1075
+ builder.addRule("regexp/prefer-named-backreference", "error");
1076
+ builder.addRule("regexp/prefer-named-replacement", "error");
1077
+ builder.addRule("regexp/prefer-quantifier", "error");
1078
+ builder.addRule("regexp/prefer-result-array-groups", "error");
1079
+ builder.addRule("regexp/require-unicode-sets-regexp", "error");
1080
+ }
1081
+ return builder.build();
1251
1082
  }
1252
-
1253
- // src/configs/security.ts
1254
- import securityPlugin from "eslint-plugin-security";
1255
- var plugin2 = securityPlugin;
1083
+ //#endregion
1084
+ //#region src/configs/security.ts
1085
+ const plugin = securityPlugin;
1086
+ /**
1087
+ * Security config — Node.js security patterns from eslint-plugin-security.
1088
+ * We manually select rules instead of using the preset because the preset
1089
+ * enables `detect-object-injection` which produces too many false positives.
1090
+ *
1091
+ * @see https://github.com/eslint-community/eslint-plugin-security#rules
1092
+ */
1256
1093
  function securityConfig() {
1257
- return [
1258
- {
1259
- name: "eslint-config-setup/security",
1260
- plugins: {
1261
- security: plugin2
1262
- },
1263
- rules: {
1264
- // ── Errors: dangerous patterns that should never appear ────────
1265
- // Detect Buffer read/write without noAssert — can read out of bounds
1266
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-buffer-noassert.md
1267
- "security/detect-buffer-noassert": "error",
1268
- // Detect child_process usage — potential command injection vector
1269
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-child-process.md
1270
- "security/detect-child-process": "error",
1271
- // Detect disabled mustache escaping — XSS risk in templates
1272
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-disable-mustache-escape.md
1273
- "security/detect-disable-mustache-escape": "error",
1274
- // Detect eval() with variable arguments — code injection risk
1275
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-eval-with-expression.md
1276
- "security/detect-eval-with-expression": "error",
1277
- // Detect new Buffer(n) — deprecated, use Buffer.alloc() instead
1278
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-new-buffer.md
1279
- "security/detect-new-buffer": "error",
1280
- // Detect CSRF middleware placed after method-override — CSRF bypass
1281
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-no-csrf-before-method-override.md
1282
- "security/detect-no-csrf-before-method-override": "error",
1283
- // Detect Math.random() / pseudoRandomBytes — not cryptographically secure
1284
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-pseudoRandomBytes.md
1285
- "security/detect-pseudoRandomBytes": "error",
1286
- // Detect regexes vulnerable to ReDoS (catastrophic backtracking)
1287
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-unsafe-regex.md
1288
- "security/detect-unsafe-regex": "error",
1289
- // ── Warnings: worth reviewing but may have legitimate uses ─────
1290
- // Detect dynamic fs paths — potential path traversal (many false positives)
1291
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-non-literal-fs-filename.md
1292
- "security/detect-non-literal-fs-filename": "warn",
1293
- // Detect dynamic regex construction — potential ReDoS
1294
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-non-literal-regexp.md
1295
- "security/detect-non-literal-regexp": "warn",
1296
- // Detect dynamic require() — potential code injection
1297
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-non-literal-require.md
1298
- "security/detect-non-literal-require": "warn",
1299
- // Detect string comparisons that may leak timing info — side-channel risk
1300
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-possible-timing-attacks.md
1301
- "security/detect-possible-timing-attacks": "warn",
1302
- // Detect Unicode bidirectional control characters — source code trojan attack
1303
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-bidi-characters.md
1304
- "security/detect-bidi-characters": "error",
1305
- // ── Disabled: too many false positives ────────────────────────
1306
- // Flags all bracket notation (obj[key]) — nearly every codebase triggers this
1307
- // https://github.com/eslint-community/eslint-plugin-security/blob/main/docs/rules/detect-object-injection.md
1308
- "security/detect-object-injection": "off"
1309
- }
1310
- }
1311
- ];
1094
+ return [{
1095
+ name: "eslint-config-setup/security",
1096
+ plugins: { security: plugin },
1097
+ rules: {
1098
+ "security/detect-buffer-noassert": "error",
1099
+ "security/detect-child-process": "error",
1100
+ "security/detect-disable-mustache-escape": "error",
1101
+ "security/detect-eval-with-expression": "error",
1102
+ "security/detect-new-buffer": "error",
1103
+ "security/detect-no-csrf-before-method-override": "error",
1104
+ "security/detect-pseudoRandomBytes": "error",
1105
+ "security/detect-unsafe-regex": "error",
1106
+ "security/detect-non-literal-fs-filename": "warn",
1107
+ "security/detect-non-literal-regexp": "warn",
1108
+ "security/detect-non-literal-require": "warn",
1109
+ "security/detect-possible-timing-attacks": "warn",
1110
+ "security/detect-bidi-characters": "error",
1111
+ "security/detect-object-injection": "off"
1112
+ }
1113
+ }];
1312
1114
  }
1313
-
1314
- // src/configs/sonarjs.ts
1315
- import sonarjsPlugin from "eslint-plugin-sonarjs";
1115
+ //#endregion
1116
+ //#region src/configs/sonarjs.ts
1117
+ /**
1118
+ * SonarJS config — code-quality rules from SonarSource.
1119
+ * Hand-picked subset focused on bug detection and code smells.
1120
+ * We don't use the full preset because many SonarJS rules overlap with
1121
+ * typescript-eslint and unicorn.
1122
+ *
1123
+ * @see https://github.com/SonarSource/SonarJS/tree/master/packages/jsts/src/rules#readme
1124
+ */
1316
1125
  function sonarjsConfig(opts) {
1317
- const isAi = opts?.ai === true;
1318
- const builder = createConfig({
1319
- name: "eslint-config-setup/sonarjs",
1320
- presets: [{
1321
- plugins: { sonarjs: sonarjsPlugin },
1322
- rules: {
1323
- // Detect copy-pasted functions — extract to shared helper instead
1324
- // https://sonarsource.github.io/rspec/#/rspec/S4144/javascript
1325
- "sonarjs/no-identical-functions": "error",
1326
- // Merge nested if-statements that can be combined — reduces nesting
1327
- // https://sonarsource.github.io/rspec/#/rspec/S1066/javascript
1328
- "sonarjs/no-collapsible-if": "error",
1329
- // Simplify `if (x) return true; else return false;` → `return x`
1330
- // https://sonarsource.github.io/rspec/#/rspec/S1125/javascript
1331
- "sonarjs/no-redundant-boolean": "error",
1332
- // Detect collections that are populated but never read — dead code
1333
- // https://sonarsource.github.io/rspec/#/rspec/S4030/javascript
1334
- "sonarjs/no-unused-collection": "error",
1335
- // Return value directly instead of storing in temp variable first
1336
- // https://sonarsource.github.io/rspec/#/rspec/S1488/javascript
1337
- "sonarjs/prefer-immediate-return": "error",
1338
- // Simplify boolean return patterns — `return cond` instead of `if/else`
1339
- // https://sonarsource.github.io/rspec/#/rspec/S1126/javascript
1340
- "sonarjs/prefer-single-boolean-return": "error",
1341
- // Detect identical sub-expressions on both sides of operator (x && x)
1342
- // https://sonarsource.github.io/rspec/#/rspec/S1764/javascript
1343
- "sonarjs/no-identical-expressions": "error",
1344
- // Simplify `!(a === b)` → `a !== b` — more readable
1345
- // https://sonarsource.github.io/rspec/#/rspec/S1940/javascript
1346
- "sonarjs/no-inverted-boolean-check": "error",
1347
- // Detect size/length comparisons that are always true/false
1348
- // https://sonarsource.github.io/rspec/#/rspec/S3981/javascript
1349
- "sonarjs/no-collection-size-mischeck": "error",
1350
- // Detect duplicate conditions in if/else-if chains — copy-paste bug
1351
- // https://sonarsource.github.io/rspec/#/rspec/S1862/javascript
1352
- "sonarjs/no-identical-conditions": "error",
1353
- // Detect identical code in if and else branches — refactoring leftover
1354
- // https://sonarsource.github.io/rspec/#/rspec/S1871/javascript
1355
- "sonarjs/no-duplicated-branches": "error",
1356
- // Detect .filter()/.map()/.slice() called without using the result
1357
- // https://sonarsource.github.io/rspec/#/rspec/S2201/javascript
1358
- "sonarjs/no-ignored-return": "error",
1359
- // Detect redundant return/break/continue at end of block
1360
- // https://sonarsource.github.io/rspec/#/rspec/S3626/javascript
1361
- "sonarjs/no-redundant-jump": "error",
1362
- // Prevent .only() from being committed — blocks CI for others
1363
- // https://sonarsource.github.io/rspec/#/rspec/S6426/javascript
1364
- "sonarjs/no-exclusive-tests": "error",
1365
- // Detect `const sorted = arr.sort()` — .sort() mutates the original
1366
- // https://sonarsource.github.io/rspec/#/rspec/S4043/javascript
1367
- "sonarjs/no-misleading-array-reverse": "error",
1368
- // Require initial value for .reduce() — crashes on empty arrays without it
1369
- // https://sonarsource.github.io/rspec/#/rspec/S6959/javascript
1370
- "sonarjs/reduce-initial-value": "error",
1371
- // Disallow async operations in constructors — use factory methods
1372
- // https://sonarsource.github.io/rspec/#/rspec/S7059/javascript
1373
- "sonarjs/no-async-constructor": "error",
1374
- // Detect `field?: string | undefined` — the `?` already implies undefined
1375
- // https://sonarsource.github.io/rspec/#/rspec/S4782/javascript
1376
- "sonarjs/no-redundant-optional": "error",
1377
- // Detect `string | number | string` — duplicate constituents in unions
1378
- // https://sonarsource.github.io/rspec/#/rspec/S4621/javascript
1379
- "sonarjs/no-duplicate-in-composite": "error",
1380
- // Detect hardcoded secrets (API keys, passwords, tokens) in source code
1381
- // https://sonarsource.github.io/rspec/#/rspec/S6418/javascript
1382
- "sonarjs/no-hardcoded-secrets": "warn"
1383
- }
1384
- }]
1385
- });
1386
- if (isAi) {
1387
- builder.addRule("sonarjs/no-nested-switch", "error");
1388
- builder.addRule("sonarjs/no-nested-template-literals", "error");
1389
- builder.addRule("sonarjs/max-union-size", ["error", { threshold: 5 }]);
1390
- builder.addRule("sonarjs/prefer-type-guard", "error");
1391
- builder.addRule("sonarjs/public-static-readonly", "error");
1392
- builder.addRule("sonarjs/no-duplicate-string", ["error", { threshold: 3 }]);
1393
- }
1394
- return builder.build();
1126
+ const isAi = opts?.ai === true;
1127
+ const builder = createConfig({
1128
+ name: "eslint-config-setup/sonarjs",
1129
+ presets: [{
1130
+ plugins: { sonarjs: sonarjsPlugin },
1131
+ rules: {
1132
+ "sonarjs/no-identical-functions": "error",
1133
+ "sonarjs/no-collapsible-if": "error",
1134
+ "sonarjs/no-redundant-boolean": "error",
1135
+ "sonarjs/no-unused-collection": "error",
1136
+ "sonarjs/prefer-immediate-return": "error",
1137
+ "sonarjs/prefer-single-boolean-return": "error",
1138
+ "sonarjs/no-identical-expressions": "error",
1139
+ "sonarjs/no-inverted-boolean-check": "error",
1140
+ "sonarjs/no-collection-size-mischeck": "error",
1141
+ "sonarjs/no-identical-conditions": "error",
1142
+ "sonarjs/no-duplicated-branches": "error",
1143
+ "sonarjs/no-ignored-return": "error",
1144
+ "sonarjs/no-redundant-jump": "error",
1145
+ "sonarjs/no-exclusive-tests": "error",
1146
+ "sonarjs/no-misleading-array-reverse": "error",
1147
+ "sonarjs/reduce-initial-value": "error",
1148
+ "sonarjs/no-async-constructor": "error",
1149
+ "sonarjs/no-redundant-optional": "error",
1150
+ "sonarjs/no-duplicate-in-composite": "error",
1151
+ "sonarjs/no-hardcoded-secrets": "warn"
1152
+ }
1153
+ }]
1154
+ });
1155
+ if (isAi) {
1156
+ builder.addRule("sonarjs/no-nested-switch", "error");
1157
+ builder.addRule("sonarjs/no-nested-template-literals", "error");
1158
+ builder.addRule("sonarjs/max-union-size", ["error", { threshold: 5 }]);
1159
+ builder.addRule("sonarjs/prefer-type-guard", "error");
1160
+ builder.addRule("sonarjs/public-static-readonly", "error");
1161
+ builder.addRule("sonarjs/no-duplicate-string", ["error", { threshold: 3 }]);
1162
+ }
1163
+ return builder.build();
1395
1164
  }
1396
-
1397
- // src/configs/typescript.ts
1398
- import tseslint from "typescript-eslint";
1165
+ //#endregion
1166
+ //#region src/configs/typescript.ts
1167
+ /**
1168
+ * TypeScript config — extends typescript-eslint strict presets with project-wide type checking.
1169
+ *
1170
+ * Presets used:
1171
+ * - `tseslint.configs.strictTypeChecked` — all recommended + strict rules with type info
1172
+ * - `tseslint.configs.stylisticTypeChecked` — consistent code style with type info
1173
+ *
1174
+ * The presets already handle many core ESLint rules by disabling them and enabling
1175
+ * TS-aware equivalents (no-implied-eval, dot-notation, no-throw-literal, etc.).
1176
+ *
1177
+ * @see https://typescript-eslint.io/getting-started/
1178
+ * @see https://typescript-eslint.io/rules/
1179
+ */
1399
1180
  function typescriptConfig(opts) {
1400
- const isAi = opts?.ai ?? false;
1401
- const isReact = opts?.react ?? false;
1402
- const typeChecked = tseslint.configs.strictTypeChecked;
1403
- const stylistic = tseslint.configs.stylisticTypeChecked;
1404
- const structuralBlocks = typeChecked.slice(0, 2);
1405
- const ruleBlocks = [typeChecked[2], stylistic[2]];
1406
- const builder = createConfig({
1407
- name: "eslint-config-setup/typescript",
1408
- passthrough: structuralBlocks,
1409
- presets: ruleBlocks,
1410
- files: [...TYPESCRIPT_SOURCE_FILES],
1411
- ignores: [...MARKDOWN_CODE_BLOCK_FILES],
1412
- languageOptions: {
1413
- parserOptions: {
1414
- // Use project service for automatic tsconfig resolution
1415
- // https://typescript-eslint.io/packages/parser#projectservice
1416
- projectService: true
1417
- }
1418
- }
1419
- });
1420
- builder.overrideRule("@typescript-eslint/no-unused-vars", [
1421
- "error",
1422
- {
1423
- args: "all",
1424
- argsIgnorePattern: "^_",
1425
- caughtErrors: "all",
1426
- caughtErrorsIgnorePattern: "^_",
1427
- destructuredArrayIgnorePattern: "^_",
1428
- varsIgnorePattern: "^_",
1429
- ignoreRestSiblings: true
1430
- }
1431
- ]);
1432
- builder.overrideRule("@typescript-eslint/array-type", [
1433
- "error",
1434
- { default: "array-simple" }
1435
- ]);
1436
- builder.overrideRule("@typescript-eslint/return-await", [
1437
- "error",
1438
- "in-try-catch"
1439
- ]);
1440
- builder.overrideSeverity("@typescript-eslint/no-deprecated", "warn");
1441
- builder.overrideRule("@typescript-eslint/no-explicit-any", [
1442
- "warn",
1443
- { fixToUnknown: true }
1444
- ]);
1445
- builder.overrideRule("@typescript-eslint/consistent-type-definitions", [
1446
- "error",
1447
- "type"
1448
- ]);
1449
- builder.overrideOptions("@typescript-eslint/ban-ts-comment", {
1450
- "ts-expect-error": "allow-with-description"
1451
- });
1452
- builder.overrideOptions("@typescript-eslint/no-floating-promises", {
1453
- checkThenables: true,
1454
- ignoreIIFE: true
1455
- });
1456
- builder.addRule("@typescript-eslint/consistent-type-imports", [
1457
- "error",
1458
- { fixStyle: isAi ? "inline-type-imports" : "separate-type-imports" }
1459
- ]);
1460
- builder.addRule("@typescript-eslint/consistent-type-exports", [
1461
- "error",
1462
- { fixMixedExportsWithInlineTypeSpecifier: true }
1463
- ]);
1464
- builder.addRule("@typescript-eslint/no-import-type-side-effects", "error");
1465
- builder.addRule("@typescript-eslint/no-unnecessary-qualifier", "error");
1466
- builder.addRule("@typescript-eslint/no-useless-empty-export", "error");
1467
- builder.addRule(
1468
- "@typescript-eslint/no-unnecessary-parameter-property-assignment",
1469
- "error"
1470
- );
1471
- builder.addRule("@typescript-eslint/strict-void-return", "error");
1472
- builder.addRule("@typescript-eslint/prefer-readonly", isAi ? "error" : "warn");
1473
- builder.addRule("@typescript-eslint/require-array-sort-compare", [
1474
- "error",
1475
- { ignoreStringArrays: true }
1476
- ]);
1477
- builder.addRule("@typescript-eslint/no-unsafe-type-assertion", "error");
1478
- builder.addRule("@typescript-eslint/switch-exhaustiveness-check", [
1479
- "error",
1480
- {
1481
- allowDefaultCaseForExhaustiveSwitch: false,
1482
- requireDefaultForNonUnion: true
1483
- }
1484
- ]);
1485
- builder.addRule("@typescript-eslint/promise-function-async", "error");
1486
- builder.addRule("@typescript-eslint/method-signature-style", [
1487
- "error",
1488
- "property"
1489
- ]);
1490
- builder.addRule("no-shadow", "off");
1491
- builder.addRule("@typescript-eslint/no-shadow", [
1492
- "error",
1493
- {
1494
- hoist: "all",
1495
- allow: ["resolve", "reject", "done", "next", "error"],
1496
- ignoreTypeValueShadow: true,
1497
- ignoreFunctionTypeParameterNameValueShadow: true
1498
- }
1499
- ]);
1500
- builder.overrideOptions("@typescript-eslint/restrict-template-expressions", { allowNumber: true });
1501
- builder.addRule("@typescript-eslint/strict-boolean-expressions", [
1502
- "error",
1503
- { allowNullableBoolean: true, allowNullableObject: true }
1504
- ]);
1505
- builder.addFileOverride(
1506
- "eslint-config-setup/typescript-js-compat",
1507
- ["**/*.{js,mjs,cjs}"],
1508
- tseslint.configs.disableTypeChecked.rules ?? {}
1509
- );
1510
- if (isAi) {
1511
- builder.overrideSeverity("@typescript-eslint/no-explicit-any", "error");
1512
- builder.overrideRule("@typescript-eslint/no-floating-promises", [
1513
- "error",
1514
- { checkThenables: true, ignoreVoid: true }
1515
- ]);
1516
- builder.addRule("@typescript-eslint/no-magic-numbers", [
1517
- "error",
1518
- {
1519
- ignore: [-1, 0, 1, 2],
1520
- ignoreArrayIndexes: true,
1521
- ignoreDefaultValues: true,
1522
- enforceConst: true,
1523
- ignoreClassFieldInitialValues: true,
1524
- ignoreEnums: true,
1525
- ignoreNumericLiteralTypes: true,
1526
- ignoreReadonlyClassProperties: true,
1527
- ignoreTypeIndexes: true
1528
- }
1529
- ]);
1530
- builder.addRule("@typescript-eslint/explicit-function-return-type", [
1531
- "error",
1532
- {
1533
- allowExpressions: true,
1534
- allowTypedFunctionExpressions: true,
1535
- allowHigherOrderFunctions: true,
1536
- allowIIFEs: true
1537
- }
1538
- ]);
1539
- builder.addRule(
1540
- "@typescript-eslint/explicit-member-accessibility",
1541
- "error"
1542
- );
1543
- builder.addRule(
1544
- "@typescript-eslint/prefer-enum-initializers",
1545
- "error"
1546
- );
1547
- builder.addRule("@typescript-eslint/naming-convention", [
1548
- "error",
1549
- {
1550
- selector: "variable",
1551
- format: isReact ? ["strictCamelCase", "UPPER_CASE", "StrictPascalCase"] : ["strictCamelCase", "UPPER_CASE"],
1552
- leadingUnderscore: "allowSingleOrDouble",
1553
- trailingUnderscore: "allow",
1554
- filter: { regex: "[- ]", match: false }
1555
- },
1556
- {
1557
- selector: "function",
1558
- format: isReact ? ["strictCamelCase", "StrictPascalCase"] : ["strictCamelCase"]
1559
- },
1560
- {
1561
- selector: "parameter",
1562
- format: ["strictCamelCase"],
1563
- leadingUnderscore: "allow"
1564
- },
1565
- {
1566
- selector: "import",
1567
- format: ["strictCamelCase", "StrictPascalCase", "UPPER_CASE"]
1568
- },
1569
- {
1570
- selector: [
1571
- "classProperty",
1572
- "parameterProperty",
1573
- "classMethod",
1574
- "objectLiteralMethod",
1575
- "typeMethod",
1576
- "accessor"
1577
- ],
1578
- format: ["strictCamelCase"],
1579
- leadingUnderscore: "allowSingleOrDouble",
1580
- trailingUnderscore: "allow",
1581
- filter: { regex: "[- ]", match: false }
1582
- },
1583
- {
1584
- selector: ["objectLiteralProperty", "typeProperty"],
1585
- format: null
1586
- },
1587
- { selector: "typeLike", format: ["StrictPascalCase"] },
1588
- {
1589
- selector: "interface",
1590
- format: ["StrictPascalCase"],
1591
- custom: { regex: "^I[A-Z]", match: false }
1592
- },
1593
- {
1594
- selector: "typeParameter",
1595
- format: ["PascalCase"],
1596
- custom: {
1597
- regex: "^(T([A-Z][a-zA-Z]*)?|[A-Z])$",
1598
- match: true
1599
- }
1600
- },
1601
- {
1602
- selector: "variable",
1603
- types: ["boolean"],
1604
- format: ["StrictPascalCase"],
1605
- prefix: ["is", "has", "can", "should", "will", "did"]
1606
- },
1607
- {
1608
- selector: ["classProperty", "objectLiteralProperty"],
1609
- format: null,
1610
- modifiers: ["requiresQuotes"]
1611
- }
1612
- ]);
1613
- builder.addRule("@typescript-eslint/member-ordering", [
1614
- "warn",
1615
- {
1616
- default: [
1617
- "signature",
1618
- "call-signature",
1619
- "public-static-field",
1620
- "protected-static-field",
1621
- "private-static-field",
1622
- "#private-static-field",
1623
- "static-field",
1624
- "public-static-method",
1625
- "protected-static-method",
1626
- "private-static-method",
1627
- "#private-static-method",
1628
- "static-method",
1629
- "public-decorated-field",
1630
- "protected-decorated-field",
1631
- "private-decorated-field",
1632
- "public-instance-field",
1633
- "protected-instance-field",
1634
- "private-instance-field",
1635
- "#private-instance-field",
1636
- "public-abstract-field",
1637
- "protected-abstract-field",
1638
- "field",
1639
- "public-constructor",
1640
- "protected-constructor",
1641
- "private-constructor",
1642
- "constructor",
1643
- ["public-get", "public-set"],
1644
- ["protected-get", "protected-set"],
1645
- ["private-get", "private-set"],
1646
- ["#private-get", "#private-set"],
1647
- "public-decorated-method",
1648
- "protected-decorated-method",
1649
- "private-decorated-method",
1650
- "public-instance-method",
1651
- "protected-instance-method",
1652
- "private-instance-method",
1653
- "#private-instance-method",
1654
- "public-abstract-method",
1655
- "protected-abstract-method",
1656
- "method"
1657
- ]
1658
- }
1659
- ]);
1660
- }
1661
- return builder.build();
1181
+ const isAi = opts?.ai ?? false;
1182
+ const isReact = opts?.react ?? false;
1183
+ const typeChecked = tseslint.configs.strictTypeChecked;
1184
+ const stylistic = tseslint.configs.stylisticTypeChecked;
1185
+ const builder = createConfig({
1186
+ name: "eslint-config-setup/typescript",
1187
+ passthrough: typeChecked.slice(0, 2),
1188
+ presets: [typeChecked[2], stylistic[2]],
1189
+ files: [...TYPESCRIPT_SOURCE_FILES],
1190
+ ignores: [...MARKDOWN_CODE_BLOCK_FILES],
1191
+ languageOptions: { parserOptions: { projectService: true } }
1192
+ });
1193
+ builder.overrideRule("@typescript-eslint/no-unused-vars", ["error", {
1194
+ args: "all",
1195
+ argsIgnorePattern: "^_",
1196
+ caughtErrors: "all",
1197
+ caughtErrorsIgnorePattern: "^_",
1198
+ destructuredArrayIgnorePattern: "^_",
1199
+ varsIgnorePattern: "^_",
1200
+ ignoreRestSiblings: true
1201
+ }]);
1202
+ builder.overrideRule("@typescript-eslint/array-type", ["error", { default: "array-simple" }]);
1203
+ builder.overrideRule("@typescript-eslint/return-await", ["error", "in-try-catch"]);
1204
+ builder.overrideSeverity("@typescript-eslint/no-deprecated", "warn");
1205
+ builder.overrideRule("@typescript-eslint/no-explicit-any", ["warn", { fixToUnknown: true }]);
1206
+ builder.overrideRule("@typescript-eslint/consistent-type-definitions", ["error", "type"]);
1207
+ builder.overrideOptions("@typescript-eslint/ban-ts-comment", { "ts-expect-error": "allow-with-description" });
1208
+ builder.overrideOptions("@typescript-eslint/no-floating-promises", {
1209
+ checkThenables: true,
1210
+ ignoreIIFE: true
1211
+ });
1212
+ builder.addRule("@typescript-eslint/consistent-type-imports", ["error", { fixStyle: isAi ? "inline-type-imports" : "separate-type-imports" }]);
1213
+ builder.addRule("@typescript-eslint/consistent-type-exports", ["error", { fixMixedExportsWithInlineTypeSpecifier: true }]);
1214
+ builder.addRule("@typescript-eslint/no-import-type-side-effects", "error");
1215
+ builder.addRule("@typescript-eslint/no-unnecessary-qualifier", "error");
1216
+ builder.addRule("@typescript-eslint/no-useless-empty-export", "error");
1217
+ builder.addRule("@typescript-eslint/no-unnecessary-parameter-property-assignment", "error");
1218
+ builder.addRule("@typescript-eslint/strict-void-return", "error");
1219
+ builder.addRule("@typescript-eslint/prefer-readonly", isAi ? "error" : "warn");
1220
+ builder.addRule("@typescript-eslint/require-array-sort-compare", ["error", { ignoreStringArrays: true }]);
1221
+ builder.addRule("@typescript-eslint/no-unsafe-type-assertion", "error");
1222
+ builder.addRule("@typescript-eslint/switch-exhaustiveness-check", ["error", {
1223
+ allowDefaultCaseForExhaustiveSwitch: false,
1224
+ requireDefaultForNonUnion: true
1225
+ }]);
1226
+ builder.addRule("@typescript-eslint/promise-function-async", "error");
1227
+ builder.addRule("@typescript-eslint/method-signature-style", ["error", "property"]);
1228
+ builder.addRule("no-shadow", "off");
1229
+ builder.addRule("@typescript-eslint/no-shadow", ["error", {
1230
+ hoist: "all",
1231
+ allow: [
1232
+ "resolve",
1233
+ "reject",
1234
+ "done",
1235
+ "next",
1236
+ "error"
1237
+ ],
1238
+ ignoreTypeValueShadow: true,
1239
+ ignoreFunctionTypeParameterNameValueShadow: true
1240
+ }]);
1241
+ builder.overrideOptions("@typescript-eslint/restrict-template-expressions", { allowNumber: true });
1242
+ builder.addRule("@typescript-eslint/strict-boolean-expressions", ["error", {
1243
+ allowNullableBoolean: true,
1244
+ allowNullableObject: true
1245
+ }]);
1246
+ builder.addFileOverride("eslint-config-setup/typescript-js-compat", ["**/*.{js,mjs,cjs}"], tseslint.configs.disableTypeChecked.rules ?? {});
1247
+ if (isAi) {
1248
+ builder.overrideSeverity("@typescript-eslint/no-explicit-any", "error");
1249
+ builder.overrideRule("@typescript-eslint/no-floating-promises", ["error", {
1250
+ checkThenables: true,
1251
+ ignoreVoid: true
1252
+ }]);
1253
+ builder.addRule("@typescript-eslint/no-magic-numbers", ["error", {
1254
+ ignore: [
1255
+ -1,
1256
+ 0,
1257
+ 1,
1258
+ 2
1259
+ ],
1260
+ ignoreArrayIndexes: true,
1261
+ ignoreDefaultValues: true,
1262
+ enforceConst: true,
1263
+ ignoreClassFieldInitialValues: true,
1264
+ ignoreEnums: true,
1265
+ ignoreNumericLiteralTypes: true,
1266
+ ignoreReadonlyClassProperties: true,
1267
+ ignoreTypeIndexes: true
1268
+ }]);
1269
+ builder.addRule("@typescript-eslint/explicit-function-return-type", ["error", {
1270
+ allowExpressions: true,
1271
+ allowTypedFunctionExpressions: true,
1272
+ allowHigherOrderFunctions: true,
1273
+ allowIIFEs: true
1274
+ }]);
1275
+ builder.addRule("@typescript-eslint/explicit-member-accessibility", "error");
1276
+ builder.addRule("@typescript-eslint/prefer-enum-initializers", "error");
1277
+ builder.addRule("@typescript-eslint/naming-convention", [
1278
+ "error",
1279
+ {
1280
+ selector: "variable",
1281
+ format: isReact ? [
1282
+ "strictCamelCase",
1283
+ "UPPER_CASE",
1284
+ "StrictPascalCase"
1285
+ ] : ["strictCamelCase", "UPPER_CASE"],
1286
+ leadingUnderscore: "allowSingleOrDouble",
1287
+ trailingUnderscore: "allow",
1288
+ filter: {
1289
+ regex: "[- ]",
1290
+ match: false
1291
+ }
1292
+ },
1293
+ {
1294
+ selector: "function",
1295
+ format: isReact ? ["strictCamelCase", "StrictPascalCase"] : ["strictCamelCase"]
1296
+ },
1297
+ {
1298
+ selector: "parameter",
1299
+ format: ["strictCamelCase"],
1300
+ leadingUnderscore: "allow"
1301
+ },
1302
+ {
1303
+ selector: "import",
1304
+ format: [
1305
+ "strictCamelCase",
1306
+ "StrictPascalCase",
1307
+ "UPPER_CASE"
1308
+ ]
1309
+ },
1310
+ {
1311
+ selector: [
1312
+ "classProperty",
1313
+ "parameterProperty",
1314
+ "classMethod",
1315
+ "objectLiteralMethod",
1316
+ "typeMethod",
1317
+ "accessor"
1318
+ ],
1319
+ format: ["strictCamelCase"],
1320
+ leadingUnderscore: "allowSingleOrDouble",
1321
+ trailingUnderscore: "allow",
1322
+ filter: {
1323
+ regex: "[- ]",
1324
+ match: false
1325
+ }
1326
+ },
1327
+ {
1328
+ selector: ["objectLiteralProperty", "typeProperty"],
1329
+ format: null
1330
+ },
1331
+ {
1332
+ selector: "typeLike",
1333
+ format: ["StrictPascalCase"]
1334
+ },
1335
+ {
1336
+ selector: "interface",
1337
+ format: ["StrictPascalCase"],
1338
+ custom: {
1339
+ regex: "^I[A-Z]",
1340
+ match: false
1341
+ }
1342
+ },
1343
+ {
1344
+ selector: "typeParameter",
1345
+ format: ["PascalCase"],
1346
+ custom: {
1347
+ regex: "^(T([A-Z][a-zA-Z]*)?|[A-Z])$",
1348
+ match: true
1349
+ }
1350
+ },
1351
+ {
1352
+ selector: "variable",
1353
+ types: ["boolean"],
1354
+ format: ["StrictPascalCase"],
1355
+ prefix: [
1356
+ "is",
1357
+ "has",
1358
+ "can",
1359
+ "should",
1360
+ "will",
1361
+ "did"
1362
+ ]
1363
+ },
1364
+ {
1365
+ selector: ["classProperty", "objectLiteralProperty"],
1366
+ format: null,
1367
+ modifiers: ["requiresQuotes"]
1368
+ }
1369
+ ]);
1370
+ builder.addRule("@typescript-eslint/member-ordering", ["warn", { default: [
1371
+ "signature",
1372
+ "call-signature",
1373
+ "public-static-field",
1374
+ "protected-static-field",
1375
+ "private-static-field",
1376
+ "#private-static-field",
1377
+ "static-field",
1378
+ "public-static-method",
1379
+ "protected-static-method",
1380
+ "private-static-method",
1381
+ "#private-static-method",
1382
+ "static-method",
1383
+ "public-decorated-field",
1384
+ "protected-decorated-field",
1385
+ "private-decorated-field",
1386
+ "public-instance-field",
1387
+ "protected-instance-field",
1388
+ "private-instance-field",
1389
+ "#private-instance-field",
1390
+ "public-abstract-field",
1391
+ "protected-abstract-field",
1392
+ "field",
1393
+ "public-constructor",
1394
+ "protected-constructor",
1395
+ "private-constructor",
1396
+ "constructor",
1397
+ ["public-get", "public-set"],
1398
+ ["protected-get", "protected-set"],
1399
+ ["private-get", "private-set"],
1400
+ ["#private-get", "#private-set"],
1401
+ "public-decorated-method",
1402
+ "protected-decorated-method",
1403
+ "private-decorated-method",
1404
+ "public-instance-method",
1405
+ "protected-instance-method",
1406
+ "private-instance-method",
1407
+ "#private-instance-method",
1408
+ "public-abstract-method",
1409
+ "protected-abstract-method",
1410
+ "method"
1411
+ ] }]);
1412
+ }
1413
+ return builder.build();
1662
1414
  }
1663
-
1664
- // src/configs/unicorn.ts
1665
- import unicornPlugin from "eslint-plugin-unicorn";
1415
+ //#endregion
1416
+ //#region src/configs/unicorn.ts
1417
+ /**
1418
+ * Unicorn config — modern JavaScript idioms and best practices.
1419
+ * We hand-pick rules instead of using `flat/recommended` because the
1420
+ * recommended preset includes opinionated rules we disagree with
1421
+ * (e.g., `no-null`, `no-nested-ternary`, `filename-case`).
1422
+ *
1423
+ * @see https://github.com/sindresorhus/eslint-plugin-unicorn#rules
1424
+ */
1666
1425
  function unicornConfig(opts) {
1667
- const isAi = opts?.ai === true;
1668
- const builder = createConfig({
1669
- name: "eslint-config-setup/unicorn",
1670
- presets: [{
1671
- plugins: { unicorn: unicornPlugin },
1672
- rules: {
1673
- // ── Error prevention ──────────────────────────────────────────
1674
- // Forbid `/* eslint-disable */` without specific rule — too broad
1675
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-abusive-eslint-disable.md
1676
- "unicorn/no-abusive-eslint-disable": "error",
1677
- // Disallow `instanceof` with built-in objects — use proper checks instead
1678
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-instanceof-builtins.md
1679
- "unicorn/no-instanceof-builtins": "error",
1680
- // Prevent passing non-function to removeEventListener — common mistake
1681
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-invalid-remove-event-listener.md
1682
- "unicorn/no-invalid-remove-event-listener": "error",
1683
- // Disallow invalid fetch() options — catches typos at lint time
1684
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-invalid-fetch-options.md
1685
- "unicorn/no-invalid-fetch-options": "error",
1686
- // Prefer direct undefined checks over typeof — cleaner with strict mode
1687
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-typeof-undefined.md
1688
- "unicorn/no-typeof-undefined": "error",
1689
- // Remove useless fallback in object spread ({ ...a, x: a.x ?? y })
1690
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-fallback-in-spread.md
1691
- "unicorn/no-useless-fallback-in-spread": "error",
1692
- // Remove unnecessary .length checks before array operations
1693
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-length-check.md
1694
- "unicorn/no-useless-length-check": "error",
1695
- // Remove unnecessary spread operators ([...array] when array already exists)
1696
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-spread.md
1697
- "unicorn/no-useless-spread": "error",
1698
- // Remove useless Promise.resolve/reject wrappers — simplify async code
1699
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-promise-resolve-reject.md
1700
- "unicorn/no-useless-promise-resolve-reject": "error",
1701
- // Disallow redundant undefined where omission preserves semantics.
1702
- // Keep call arguments unchecked: TypeScript often needs explicit
1703
- // `undefined` for required parameters typed as `T | undefined`.
1704
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-useless-undefined.md
1705
- "unicorn/no-useless-undefined": ["warn", { checkArguments: false }],
1706
- // Disallow `await` in Promise.all/race arguments — already a promise
1707
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-await-in-promise-methods.md
1708
- "unicorn/no-await-in-promise-methods": "error",
1709
- // Catch `!a === b` bugs — use `a !== b` instead
1710
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-negation-in-equality-check.md
1711
- "unicorn/no-negation-in-equality-check": "error",
1712
- // Disallow objects with `.then` property — prevents accidental thenables
1713
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-thenable.md
1714
- "unicorn/no-thenable": "error",
1715
- // Disallow `(await foo).bar` — assign to variable first for clarity
1716
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-await-expression-member.md
1717
- "unicorn/no-await-expression-member": "error",
1718
- // Disallow `const self = this` — use arrow functions instead
1719
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-this-assignment.md
1720
- "unicorn/no-this-assignment": "error",
1721
- // Disallow `await` on non-promise values — unnecessary overhead
1722
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unnecessary-await.md
1723
- "unicorn/no-unnecessary-await": "error",
1724
- // Disallow unnecessary slice end argument — `.length` is the default
1725
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unnecessary-slice-end.md
1726
- "unicorn/no-unnecessary-slice-end": "error",
1727
- // OFF: Stylistic — developers should decide their own array initialization pattern
1728
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-immediate-mutation.md
1729
- "unicorn/no-immediate-mutation": "off",
1730
- // Disallow recursive access in getters/setters — infinite loop risk
1731
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-accessor-recursion.md
1732
- "unicorn/no-accessor-recursion": "error",
1733
- // Disallow anonymous default exports — hard to find and refactor
1734
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-anonymous-default-export.md
1735
- "unicorn/no-anonymous-default-export": "error",
1736
- // Disallow `this` argument in array methods — use arrow functions
1737
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-method-this-argument.md
1738
- "unicorn/no-array-method-this-argument": "error",
1739
- // Disallow passing function references directly to iterator methods
1740
- // Prevents bugs like `['1','2'].map(parseInt)` → [1, NaN]
1741
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-callback-reference.md
1742
- "unicorn/no-array-callback-reference": "error",
1743
- // Disallow unreadable IIFEs — extract to named function
1744
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unreadable-iife.md
1745
- "unicorn/no-unreadable-iife": "error",
1746
- // Require Error messages — aids debugging
1747
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/error-message.md
1748
- "unicorn/error-message": "error",
1749
- // Require `new` keyword when throwing errors — consistent pattern
1750
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/throw-new-error.md
1751
- "unicorn/throw-new-error": "error",
1752
- // Prefer consistent types when spreading a ternary in an array literal
1753
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-empty-array-spread.md
1754
- "unicorn/consistent-empty-array-spread": "error",
1755
- // Prefer passing Date directly to constructor when cloning
1756
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-date-clone.md
1757
- "unicorn/consistent-date-clone": "error",
1758
- // Enforce consistent style for indexOf/findIndex existence checks
1759
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-existence-index-check.md
1760
- "unicorn/consistent-existence-index-check": "error",
1761
- // Move functions to the smallest possible scope — avoids re-creation on every call,
1762
- // improves testability, and makes dependencies explicit
1763
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/consistent-function-scoping.md
1764
- "unicorn/consistent-function-scoping": "warn",
1765
- // Enforce consistent filename casing — camelCase, PascalCase, or kebab-case (no snake_case)
1766
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
1767
- "unicorn/filename-case": [
1768
- "error",
1769
- {
1770
- cases: {
1771
- camelCase: true,
1772
- pascalCase: true,
1773
- kebabCase: true
1774
- },
1775
- ignore: ["__tests__"]
1776
- }
1777
- ],
1778
- // Discourage Array.reduce() — hard to read for many developers, prefer for...of or other methods
1779
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-array-reduce.md
1780
- "unicorn/no-array-reduce": "warn",
1781
- // Prefer for...of over C-style for loops — more readable when index isn't needed
1782
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-for-loop.md
1783
- "unicorn/no-for-loop": "warn",
1784
- // ── Modern API preferences ────────────────────────────────────
1785
- // Prefer .flatMap() over .map().flat() — single pass, more readable
1786
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-flat-map.md
1787
- "unicorn/prefer-array-flat-map": "error",
1788
- // Prefer .find() over .filter()[0] — stops at first match
1789
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-find.md
1790
- "unicorn/prefer-array-find": "error",
1791
- // Prefer .flat() over manual recursive flattening
1792
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-flat.md
1793
- "unicorn/prefer-array-flat": "error",
1794
- // Prefer .indexOf() over manual search loops
1795
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-index-of.md
1796
- "unicorn/prefer-array-index-of": "error",
1797
- // Prefer .some() over .find() !== undefined — semantic intent
1798
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-some.md
1799
- "unicorn/prefer-array-some": "error",
1800
- // Prefer .at(-1) over arr[arr.length - 1] — cleaner negative indexing
1801
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-at.md
1802
- "unicorn/prefer-at": "error",
1803
- // Prefer .includes() over .indexOf() !== -1 — boolean intent
1804
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-includes.md
1805
- "unicorn/prefer-includes": "error",
1806
- // Prefer .before()/.after()/.replaceWith() over parent.insertBefore()
1807
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-modern-dom-apis.md
1808
- "unicorn/prefer-modern-dom-apis": "error",
1809
- // Prefer element.dataset.foo over setAttribute('data-foo') — modern, readable
1810
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-dom-node-dataset.md
1811
- "unicorn/prefer-dom-node-dataset": "error",
1812
- // Prefer Math.log10/Math.hypot over manual math — accurate and readable
1813
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-modern-math-apis.md
1814
- "unicorn/prefer-modern-math-apis": "error",
1815
- // Prefer negative index over length-based — cleaner with .slice(-n)
1816
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-negative-index.md
1817
- "unicorn/prefer-negative-index": "error",
1818
- // Prefer Number.isFinite/Number.isNaN over global — no coercion
1819
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-number-properties.md
1820
- "unicorn/prefer-number-properties": "error",
1821
- // Prefer Object.fromEntries() over manual reduce for key-value mapping
1822
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-object-from-entries.md
1823
- "unicorn/prefer-object-from-entries": "error",
1824
- // Prefer Set.has() over Array.includes() for repeated lookups — O(1)
1825
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-set-has.md
1826
- "unicorn/prefer-set-has": "error",
1827
- // Prefer .replaceAll() over regex with global flag — clearer intent
1828
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-string-replace-all.md
1829
- "unicorn/prefer-string-replace-all": "error",
1830
- // Prefer .slice() over .substr()/.substring() — consistent, no gotchas
1831
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-string-slice.md
1832
- "unicorn/prefer-string-slice": "error",
1833
- // Prefer .startsWith()/.endsWith() over regex — simpler, faster
1834
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-string-starts-ends-with.md
1835
- "unicorn/prefer-string-starts-ends-with": "error",
1836
- // Prefer .trimStart()/.trimEnd() over .trimLeft()/.trimRight()
1837
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-string-trim-start-end.md
1838
- "unicorn/prefer-string-trim-start-end": "error",
1839
- // Prefer structuredClone() over JSON.parse(JSON.stringify()) — handles more types
1840
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-structured-clone.md
1841
- "unicorn/prefer-structured-clone": "error",
1842
- // Prefer top-level await over async IIFE — cleaner module pattern
1843
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-top-level-await.md
1844
- "unicorn/prefer-top-level-await": "error",
1845
- // Throw TypeError for type checks — correct error type
1846
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-type-error.md
1847
- "unicorn/prefer-type-error": "error",
1848
- // ── Regex ─────────────────────────────────────────────────────
1849
- // Disabled — conflicts with regexp/strict from eslint-plugin-regexp.
1850
- // Both rules auto-fix regex escaping but disagree on whether characters
1851
- // like { } ] should be escaped, causing circular fixes.
1852
- // regexp/strict is the more thorough rule, so we defer to it.
1853
- "unicorn/better-regex": "off",
1854
- // ── Misc ──────────────────────────────────────────────────────
1855
- // Enforce `error` name in catch blocks — consistent naming
1856
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/catch-error-name.md
1857
- "unicorn/catch-error-name": ["error", { name: "error" }],
1858
- // Enforce `new` for builtins that require it (Map, Set, WeakMap, etc.)
1859
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/new-for-builtins.md
1860
- "unicorn/new-for-builtins": "error",
1861
- // Prefer Array.from({length}) or fill() over new Array(n) — explicit intent
1862
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-new-array.md
1863
- "unicorn/no-new-array": "error",
1864
- // Prefer Buffer.from()/Buffer.alloc() over new Buffer() — deprecated
1865
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-new-buffer.md
1866
- "unicorn/no-new-buffer": "error",
1867
- // Forbid unreadable destructuring like `const [,,, d] = arr`
1868
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-unreadable-array-destructuring.md
1869
- "unicorn/no-unreadable-array-destructuring": "error",
1870
- // Remove unnecessary `.0` in numbers (1.0 → 1) — cleaner
1871
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-zero-fractions.md
1872
- "unicorn/no-zero-fractions": "error",
1873
- // Enforce lowercase hex (0xff not 0xFF) — consistent
1874
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/number-literal-case.md
1875
- "unicorn/number-literal-case": "error",
1876
- // Enforce numeric separators (1_000_000 not 1000000) — readable large numbers
1877
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/numeric-separators-style.md
1878
- "unicorn/numeric-separators-style": "error",
1879
- // Prefer `export { x } from 'y'` over import then re-export
1880
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-export-from.md
1881
- "unicorn/prefer-export-from": ["error", { checkUsedVariables: false }],
1882
- // Prefer built-in coercion (String, Number, Boolean) over wrapper functions
1883
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-native-coercion-functions.md
1884
- "unicorn/prefer-native-coercion-functions": "error",
1885
- // Prefer .test() over .match() for boolean regex checks
1886
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-regexp-test.md
1887
- "unicorn/prefer-regexp-test": "error",
1888
- // Prefer [...iterable] spread over Array.from(iterable) — concise
1889
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-spread.md
1890
- "unicorn/prefer-spread": "error",
1891
- // Prefer relative URLs over absolute same-origin URLs
1892
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/relative-url-style.md
1893
- "unicorn/relative-url-style": "error",
1894
- // Prefer `node:fs` over `fs` — explicit built-in module protocol
1895
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-node-protocol.md
1896
- "unicorn/prefer-node-protocol": "error",
1897
- // Prefer `globalThis` over `window`/`self`/`global` — universal
1898
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-global-this.md
1899
- "unicorn/prefer-global-this": "error",
1900
- // Prefer omitting unused catch binding — cleaner syntax
1901
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-optional-catch-binding.md
1902
- "unicorn/prefer-optional-catch-binding": "error",
1903
- // Prefer `Date.now()` over `new Date().getTime()` — direct and readable
1904
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-date-now.md
1905
- "unicorn/prefer-date-now": "error",
1906
- // Enforce consistent `utf-8` casing for text encoding identifiers
1907
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/text-encoding-identifier-case.md
1908
- "unicorn/text-encoding-identifier-case": "error",
1909
- // Prefer `import.meta.url`/`import.meta.dirname` over `__filename`/`__dirname`
1910
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-import-meta-properties.md
1911
- "unicorn/prefer-import-meta-properties": "error",
1912
- // OFF: Sequential .push() is natural in codegen and conditional builders.
1913
- // No real performance benefit in modern V8 — purely stylistic.
1914
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-single-call.md
1915
- "unicorn/prefer-single-call": "off"
1916
- }
1917
- }]
1918
- });
1919
- if (isAi) {
1920
- builder.overrideSeverity("unicorn/consistent-function-scoping", "error");
1921
- builder.overrideSeverity("unicorn/no-array-reduce", "error");
1922
- builder.overrideSeverity("unicorn/no-for-loop", "error");
1923
- builder.addRule("unicorn/no-for-each", "error");
1924
- builder.addRule("unicorn/prefer-ternary", ["error", "only-single-line"]);
1925
- builder.addRule("unicorn/prefer-switch", ["error", { minimumCases: 3 }]);
1926
- builder.addRule("unicorn/no-useless-switch-case", "error");
1927
- builder.addRule("unicorn/custom-error-definition", "error");
1928
- builder.addRule("unicorn/prefer-default-parameters", "error");
1929
- builder.addRule("unicorn/prefer-logical-operator-over-ternary", "error");
1930
- builder.addRule("unicorn/prefer-math-min-max", "error");
1931
- builder.addRule("unicorn/prefer-set-size", "error");
1932
- builder.addRule("unicorn/explicit-length-check", "error");
1933
- builder.addRule("unicorn/switch-case-braces", "error");
1934
- builder.addRule("unicorn/no-array-push-push", "error");
1935
- }
1936
- return builder.build();
1426
+ const isAi = opts?.ai === true;
1427
+ const builder = createConfig({
1428
+ name: "eslint-config-setup/unicorn",
1429
+ presets: [{
1430
+ plugins: { unicorn: unicornPlugin },
1431
+ rules: {
1432
+ "unicorn/no-abusive-eslint-disable": "error",
1433
+ "unicorn/no-instanceof-builtins": "error",
1434
+ "unicorn/no-invalid-remove-event-listener": "error",
1435
+ "unicorn/no-invalid-fetch-options": "error",
1436
+ "unicorn/no-typeof-undefined": "error",
1437
+ "unicorn/no-useless-fallback-in-spread": "error",
1438
+ "unicorn/no-useless-length-check": "error",
1439
+ "unicorn/no-useless-spread": "error",
1440
+ "unicorn/no-useless-promise-resolve-reject": "error",
1441
+ "unicorn/no-useless-undefined": ["warn", { checkArguments: false }],
1442
+ "unicorn/no-await-in-promise-methods": "error",
1443
+ "unicorn/no-negation-in-equality-check": "error",
1444
+ "unicorn/no-thenable": "error",
1445
+ "unicorn/no-await-expression-member": "error",
1446
+ "unicorn/no-this-assignment": "error",
1447
+ "unicorn/no-unnecessary-await": "error",
1448
+ "unicorn/no-unnecessary-slice-end": "error",
1449
+ "unicorn/no-immediate-mutation": "off",
1450
+ "unicorn/no-accessor-recursion": "error",
1451
+ "unicorn/no-anonymous-default-export": "error",
1452
+ "unicorn/no-array-method-this-argument": "error",
1453
+ "unicorn/no-array-callback-reference": "error",
1454
+ "unicorn/no-unreadable-iife": "error",
1455
+ "unicorn/error-message": "error",
1456
+ "unicorn/throw-new-error": "error",
1457
+ "unicorn/consistent-empty-array-spread": "error",
1458
+ "unicorn/consistent-date-clone": "error",
1459
+ "unicorn/consistent-existence-index-check": "error",
1460
+ "unicorn/consistent-function-scoping": "warn",
1461
+ "unicorn/filename-case": ["error", {
1462
+ cases: {
1463
+ camelCase: true,
1464
+ pascalCase: true,
1465
+ kebabCase: true
1466
+ },
1467
+ ignore: ["__tests__"]
1468
+ }],
1469
+ "unicorn/no-array-reduce": "warn",
1470
+ "unicorn/no-for-loop": "warn",
1471
+ "unicorn/prefer-array-flat-map": "error",
1472
+ "unicorn/prefer-array-find": "error",
1473
+ "unicorn/prefer-array-flat": "error",
1474
+ "unicorn/prefer-array-index-of": "error",
1475
+ "unicorn/prefer-array-some": "error",
1476
+ "unicorn/prefer-at": "error",
1477
+ "unicorn/prefer-includes": "error",
1478
+ "unicorn/prefer-modern-dom-apis": "error",
1479
+ "unicorn/prefer-dom-node-dataset": "error",
1480
+ "unicorn/prefer-modern-math-apis": "error",
1481
+ "unicorn/prefer-negative-index": "error",
1482
+ "unicorn/prefer-number-properties": "error",
1483
+ "unicorn/prefer-object-from-entries": "error",
1484
+ "unicorn/prefer-set-has": "error",
1485
+ "unicorn/prefer-string-replace-all": "error",
1486
+ "unicorn/prefer-string-slice": "error",
1487
+ "unicorn/prefer-string-starts-ends-with": "error",
1488
+ "unicorn/prefer-string-trim-start-end": "error",
1489
+ "unicorn/prefer-structured-clone": "error",
1490
+ "unicorn/prefer-top-level-await": "error",
1491
+ "unicorn/prefer-type-error": "error",
1492
+ "unicorn/better-regex": "off",
1493
+ "unicorn/catch-error-name": ["error", { name: "error" }],
1494
+ "unicorn/new-for-builtins": "error",
1495
+ "unicorn/no-new-array": "error",
1496
+ "unicorn/no-new-buffer": "error",
1497
+ "unicorn/no-unreadable-array-destructuring": "error",
1498
+ "unicorn/no-zero-fractions": "error",
1499
+ "unicorn/number-literal-case": "error",
1500
+ "unicorn/numeric-separators-style": "error",
1501
+ "unicorn/prefer-export-from": ["error", { checkUsedVariables: false }],
1502
+ "unicorn/prefer-native-coercion-functions": "error",
1503
+ "unicorn/prefer-regexp-test": "error",
1504
+ "unicorn/prefer-spread": "error",
1505
+ "unicorn/relative-url-style": "error",
1506
+ "unicorn/prefer-node-protocol": "error",
1507
+ "unicorn/prefer-global-this": "error",
1508
+ "unicorn/prefer-optional-catch-binding": "error",
1509
+ "unicorn/prefer-date-now": "error",
1510
+ "unicorn/text-encoding-identifier-case": "error",
1511
+ "unicorn/prefer-import-meta-properties": "error",
1512
+ "unicorn/prefer-single-call": "off"
1513
+ }
1514
+ }]
1515
+ });
1516
+ if (isAi) {
1517
+ builder.overrideSeverity("unicorn/consistent-function-scoping", "error");
1518
+ builder.overrideSeverity("unicorn/no-array-reduce", "error");
1519
+ builder.overrideSeverity("unicorn/no-for-loop", "error");
1520
+ builder.addRule("unicorn/no-for-each", "error");
1521
+ builder.addRule("unicorn/prefer-ternary", ["error", "only-single-line"]);
1522
+ builder.addRule("unicorn/prefer-switch", ["error", { minimumCases: 3 }]);
1523
+ builder.addRule("unicorn/no-useless-switch-case", "error");
1524
+ builder.addRule("unicorn/custom-error-definition", "error");
1525
+ builder.addRule("unicorn/prefer-default-parameters", "error");
1526
+ builder.addRule("unicorn/prefer-logical-operator-over-ternary", "error");
1527
+ builder.addRule("unicorn/prefer-math-min-max", "error");
1528
+ builder.addRule("unicorn/prefer-set-size", "error");
1529
+ builder.addRule("unicorn/explicit-length-check", "error");
1530
+ builder.addRule("unicorn/switch-case-braces", "error");
1531
+ builder.addRule("unicorn/no-array-push-push", "error");
1532
+ }
1533
+ return builder.build();
1937
1534
  }
1938
-
1939
- // src/overrides/config-files.ts
1535
+ //#endregion
1536
+ //#region src/overrides/config-files.ts
1537
+ /**
1538
+ * Config file overrides — relaxed rules for tool configuration files.
1539
+ * Config files (vite, vitest, next, tailwind, postcss) have their own
1540
+ * patterns that conflict with strict app-code rules.
1541
+ *
1542
+ * File patterns: *.config.*, vite.config.*, vitest.config.*, etc.
1543
+ */
1940
1544
  function configFilesOverride() {
1941
- return [
1942
- {
1943
- name: "eslint-config-setup/config-files",
1944
- files: [
1945
- "**/*.config.{ts,mts,cts,js,mjs,cjs}",
1946
- "**/vite.config.*",
1947
- "**/vitest.config.*",
1948
- "**/next.config.*",
1949
- "**/tailwind.config.*",
1950
- "**/postcss.config.*"
1951
- ],
1952
- rules: {
1953
- // Config files often require default exports (Vite, Next.js, Tailwind)
1954
- "import/no-default-export": "off",
1955
- // Config files can have complex configuration objects
1956
- complexity: "off",
1957
- "max-lines": "off",
1958
- "max-lines-per-function": "off",
1959
- "max-statements": "off",
1960
- // Config files may use require() for dynamic plugin loading
1961
- "@typescript-eslint/no-require-imports": "off",
1962
- // Config files may log build info to console
1963
- "no-console": "off",
1964
- // Config files often have magic numbers (ports, sizes, timeouts)
1965
- "no-magic-numbers": "off",
1966
- "@typescript-eslint/no-magic-numbers": "off"
1967
- }
1968
- }
1969
- ];
1545
+ return [{
1546
+ name: "eslint-config-setup/config-files",
1547
+ files: [
1548
+ "**/*.config.{ts,mts,cts,js,mjs,cjs}",
1549
+ "**/vite.config.*",
1550
+ "**/vitest.config.*",
1551
+ "**/next.config.*",
1552
+ "**/tailwind.config.*",
1553
+ "**/postcss.config.*"
1554
+ ],
1555
+ rules: {
1556
+ "import/no-default-export": "off",
1557
+ complexity: "off",
1558
+ "max-lines": "off",
1559
+ "max-lines-per-function": "off",
1560
+ "max-statements": "off",
1561
+ "@typescript-eslint/no-require-imports": "off",
1562
+ "no-console": "off",
1563
+ "no-magic-numbers": "off",
1564
+ "@typescript-eslint/no-magic-numbers": "off"
1565
+ }
1566
+ }];
1970
1567
  }
1971
-
1972
- // src/overrides/declarations.ts
1568
+ //#endregion
1569
+ //#region src/overrides/declarations.ts
1570
+ /**
1571
+ * Declaration file overrides — minimal rules for `.d.ts` files.
1572
+ * Declaration files follow their own patterns (ambient declarations,
1573
+ * interface merging, namespaces) that conflict with app-code rules.
1574
+ *
1575
+ * File pattern: *.d.ts files
1576
+ */
1973
1577
  function declarationsOverride() {
1974
- return [
1975
- {
1976
- name: "eslint-config-setup/declarations",
1977
- files: ["**/*.d.ts"],
1978
- rules: {
1979
- // Declaration files often have unused type parameters/variables
1980
- "@typescript-eslint/no-unused-vars": "off",
1981
- // Empty interfaces are valid for declaration merging
1982
- "@typescript-eslint/no-empty-interface": "off",
1983
- // Empty object types are used for extensible interfaces
1984
- "@typescript-eslint/no-empty-object-type": "off",
1985
- // `any` is sometimes necessary in third-party type declarations
1986
- "@typescript-eslint/no-explicit-any": "off",
1987
- // Both `type` and `interface` are valid in declarations
1988
- "@typescript-eslint/consistent-type-definitions": "off",
1989
- // Namespaces are standard in ambient declarations
1990
- "@typescript-eslint/no-namespace": "off",
1991
- // Duplicate imports happen with declaration merging
1992
- "import/no-duplicates": "off",
1993
- // Unused imports are common in re-export declaration files
1994
- "unused-imports/no-unused-imports": "off",
1995
- // ── AI mode rules that don't apply to declaration files ────────
1996
- // Declarations inherit return types from the implementation
1997
- "@typescript-eslint/explicit-function-return-type": "off",
1998
- // Third-party naming conventions must be followed as-is
1999
- "@typescript-eslint/naming-convention": "off",
2000
- // Abbreviations in third-party types are unavoidable
2001
- "unicorn/prevent-abbreviations": "off",
2002
- // .d.ts filename is dictated by the module it declares
2003
- "unicorn/filename-case": "off"
2004
- }
2005
- }
2006
- ];
1578
+ return [{
1579
+ name: "eslint-config-setup/declarations",
1580
+ files: ["**/*.d.ts"],
1581
+ rules: {
1582
+ "@typescript-eslint/no-unused-vars": "off",
1583
+ "@typescript-eslint/no-empty-interface": "off",
1584
+ "@typescript-eslint/no-empty-object-type": "off",
1585
+ "@typescript-eslint/no-explicit-any": "off",
1586
+ "@typescript-eslint/consistent-type-definitions": "off",
1587
+ "@typescript-eslint/no-namespace": "off",
1588
+ "import/no-duplicates": "off",
1589
+ "unused-imports/no-unused-imports": "off",
1590
+ "@typescript-eslint/explicit-function-return-type": "off",
1591
+ "@typescript-eslint/naming-convention": "off",
1592
+ "unicorn/prevent-abbreviations": "off",
1593
+ "unicorn/filename-case": "off"
1594
+ }
1595
+ }];
2007
1596
  }
2008
-
2009
- // src/overrides/e2e.ts
2010
- import playwrightPlugin from "eslint-plugin-playwright";
1597
+ //#endregion
1598
+ //#region src/overrides/e2e.ts
1599
+ /**
1600
+ * E2E test overrides — Playwright rules + relaxed strictness for E2E tests.
1601
+ *
1602
+ * File pattern: *.spec.ts (Playwright convention)
1603
+ *
1604
+ * @see https://github.com/playwright-community/eslint-plugin-playwright#rules
1605
+ */
2011
1606
  function e2eOverride() {
2012
- return [
2013
- {
2014
- name: "eslint-config-setup/e2e",
2015
- files: ["**/*.spec.ts"],
2016
- plugins: {
2017
- playwright: playwrightPlugin
2018
- },
2019
- rules: {
2020
- // ── Playwright rules ──────────────────────────────────────────
2021
- // Every test must contain at least one expect()
2022
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/expect-expect.md
2023
- "playwright/expect-expect": "error",
2024
- // Limit describe nesting to 3 levels — keeps tests readable
2025
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/max-nested-describe.md
2026
- "playwright/max-nested-describe": ["error", { max: 3 }],
2027
- // Detect missing await on Playwright async methods — common mistake
2028
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/missing-playwright-await.md
2029
- "playwright/missing-playwright-await": "error",
2030
- // No expect() inside conditionals — tests should be deterministic
2031
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-conditional-expect.md
2032
- "playwright/no-conditional-expect": "error",
2033
- // Warn on conditional logic in tests — tests should be linear
2034
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-conditional-in-test.md
2035
- "playwright/no-conditional-in-test": "warn",
2036
- // Prefer locators over element handles — auto-retry, less flaky
2037
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-element-handle.md
2038
- "playwright/no-element-handle": "error",
2039
- // No page.evaluate with arbitrary code — brittle, hard to debug
2040
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-eval.md
2041
- "playwright/no-eval": "error",
2042
- // No test.only — prevents accidentally skipping tests in CI
2043
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-focused-test.md
2044
- "playwright/no-focused-test": "error",
2045
- // Warn on { force: true } — bypasses visibility/actionability checks
2046
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-force-option.md
2047
- "playwright/no-force-option": "warn",
2048
- // No waitUntil: "networkidle" — unreliable, use specific waits
2049
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-networkidle.md
2050
- "playwright/no-networkidle": "error",
2051
- // No page.pause() — debugging artifact, breaks CI
2052
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-page-pause.md
2053
- "playwright/no-page-pause": "error",
2054
- // Warn on test.skip — track disabled tests
2055
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-skipped-test.md
2056
- "playwright/no-skipped-test": "warn",
2057
- // No unnecessary await on non-promise values
2058
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-useless-await.md
2059
- "playwright/no-useless-await": "error",
2060
- // No double-negative assertions: expect(x).not.not...
2061
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-useless-not.md
2062
- "playwright/no-useless-not": "error",
2063
- // Warn on waitForSelector — prefer locators with auto-wait
2064
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-wait-for-selector.md
2065
- "playwright/no-wait-for-selector": "warn",
2066
- // No hardcoded timeouts — use Playwright's built-in waiting
2067
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/no-wait-for-timeout.md
2068
- "playwright/no-wait-for-timeout": "error",
2069
- // Prefer web-first assertions (toBeVisible, toHaveText) — auto-retry
2070
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/prefer-web-first-assertions.md
2071
- "playwright/prefer-web-first-assertions": "error",
2072
- // Validate expect() usage — proper matcher and argument count
2073
- // https://github.com/playwright-community/eslint-plugin-playwright/blob/main/docs/rules/valid-expect.md
2074
- "playwright/valid-expect": "error",
2075
- // ── Relaxed rules for E2E tests ───────────────────────────────
2076
- // E2E tests are long procedural scripts with many page interactions
2077
- "max-lines": "off",
2078
- "max-lines-per-function": "off",
2079
- "max-statements": "off"
2080
- }
2081
- }
2082
- ];
1607
+ return [{
1608
+ name: "eslint-config-setup/e2e",
1609
+ files: ["**/*.spec.ts"],
1610
+ plugins: { playwright: playwrightPlugin },
1611
+ rules: {
1612
+ "playwright/expect-expect": "error",
1613
+ "playwright/max-nested-describe": ["error", { max: 3 }],
1614
+ "playwright/missing-playwright-await": "error",
1615
+ "playwright/no-conditional-expect": "error",
1616
+ "playwright/no-conditional-in-test": "warn",
1617
+ "playwright/no-element-handle": "error",
1618
+ "playwright/no-eval": "error",
1619
+ "playwright/no-focused-test": "error",
1620
+ "playwright/no-force-option": "warn",
1621
+ "playwright/no-networkidle": "error",
1622
+ "playwright/no-page-pause": "error",
1623
+ "playwright/no-skipped-test": "warn",
1624
+ "playwright/no-useless-await": "error",
1625
+ "playwright/no-useless-not": "error",
1626
+ "playwright/no-wait-for-selector": "warn",
1627
+ "playwright/no-wait-for-timeout": "error",
1628
+ "playwright/prefer-web-first-assertions": "error",
1629
+ "playwright/valid-expect": "error",
1630
+ "max-lines": "off",
1631
+ "max-lines-per-function": "off",
1632
+ "max-statements": "off"
1633
+ }
1634
+ }];
2083
1635
  }
2084
-
2085
- // src/overrides/scripts.ts
1636
+ //#endregion
1637
+ //#region src/overrides/scripts.ts
1638
+ /**
1639
+ * Script file overrides — relaxed rules for build/dev scripts.
1640
+ * Scripts are CLI tools that legitimately use console.log and process.exit.
1641
+ *
1642
+ * File pattern: scripts dir (*.ts, *.mts, *.js, *.mjs)
1643
+ */
2086
1644
  function scriptsOverride() {
2087
- return [
2088
- {
2089
- name: "eslint-config-setup/scripts",
2090
- files: ["**/scripts/**/*.{ts,mts,js,mjs}"],
2091
- rules: {
2092
- // Scripts use console for user-facing output
2093
- "no-console": "off",
2094
- // Scripts use process.exit for clean termination
2095
- // https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-process-exit.md
2096
- "node/no-process-exit": "off",
2097
- // Scripts can be long (build pipelines, code generators)
2098
- "max-lines": "off",
2099
- "max-lines-per-function": "off",
2100
- // Scripts are often procedural without explicit return types
2101
- "@typescript-eslint/explicit-function-return-type": "off",
2102
- // Scripts legitimately call process.exit()
2103
- // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-process-exit.md
2104
- "unicorn/no-process-exit": "off"
2105
- }
2106
- }
2107
- ];
1645
+ return [{
1646
+ name: "eslint-config-setup/scripts",
1647
+ files: ["**/scripts/**/*.{ts,mts,js,mjs}"],
1648
+ rules: {
1649
+ "no-console": "off",
1650
+ "node/no-process-exit": "off",
1651
+ "max-lines": "off",
1652
+ "max-lines-per-function": "off",
1653
+ "@typescript-eslint/explicit-function-return-type": "off",
1654
+ "unicorn/no-process-exit": "off"
1655
+ }
1656
+ }];
2108
1657
  }
2109
-
2110
- // src/overrides/stories.ts
2111
- import storybookPlugin from "eslint-plugin-storybook";
1658
+ //#endregion
1659
+ //#region src/overrides/stories.ts
1660
+ /**
1661
+ * Storybook overrides — Storybook best-practice rules for story files.
1662
+ *
1663
+ * File pattern: *.stories.{ts,tsx}
1664
+ *
1665
+ * @see https://github.com/storybookjs/eslint-plugin-storybook#supported-rules
1666
+ */
2112
1667
  function storiesOverride() {
2113
- return [
2114
- {
2115
- name: "eslint-config-setup/stories",
2116
- files: ["**/*.stories.{ts,tsx}"],
2117
- plugins: {
2118
- storybook: storybookPlugin
2119
- },
2120
- rules: {
2121
- // ── Storybook rules ───────────────────────────────────────────
2122
- // Await play function interactions — prevents race conditions
2123
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/await-interactions.md
2124
- "storybook/await-interactions": "error",
2125
- // Stories must have a default export (meta) — Storybook requirement
2126
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/default-exports.md
2127
- "storybook/default-exports": "error",
2128
- // Use / for hierarchy separators, not | or . — Storybook 7+ convention
2129
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/hierarchy-separator.md
2130
- "storybook/hierarchy-separator": "error",
2131
- // No redundant story names that match export name — DRY
2132
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/no-redundant-story-name.md
2133
- "storybook/no-redundant-story-name": "error",
2134
- // No deprecated storiesOf() API — use CSF (Component Story Format)
2135
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/no-stories-of.md
2136
- "storybook/no-stories-of": "error",
2137
- // Warn if title is in meta — auto-title is preferred in CSF 3
2138
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/no-title-property-in-meta.md
2139
- "storybook/no-title-property-in-meta": "warn",
2140
- // Story exports should be PascalCase — component naming convention
2141
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/prefer-pascal-case.md
2142
- "storybook/prefer-pascal-case": "error",
2143
- // File must export at least one story — prevents empty story files
2144
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/story-exports.md
2145
- "storybook/story-exports": "error",
2146
- // Use Storybook's expect() instead of Jest/Vitest in play functions
2147
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/use-storybook-expect.md
2148
- "storybook/use-storybook-expect": "error",
2149
- // Use Storybook's Testing Library, not direct import
2150
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/use-storybook-testing-library.md
2151
- "storybook/use-storybook-testing-library": "error",
2152
- // Enforce `satisfies Meta<typeof Component>` on default export — type-safe meta
2153
- // https://github.com/storybookjs/eslint-plugin-storybook/blob/main/docs/rules/meta-satisfies-type.md
2154
- "storybook/meta-satisfies-type": "error",
2155
- // ── Relaxed rules for stories ─────────────────────────────────
2156
- // Stories require default exports (meta object)
2157
- "import/no-default-export": "off",
2158
- // Stories can be long (many variants of a component)
2159
- "max-lines": "off",
2160
- "max-lines-per-function": "off"
2161
- }
2162
- }
2163
- ];
1668
+ return [{
1669
+ name: "eslint-config-setup/stories",
1670
+ files: ["**/*.stories.{ts,tsx}"],
1671
+ plugins: { storybook: storybookPlugin },
1672
+ rules: {
1673
+ "storybook/await-interactions": "error",
1674
+ "storybook/default-exports": "error",
1675
+ "storybook/hierarchy-separator": "error",
1676
+ "storybook/no-redundant-story-name": "error",
1677
+ "storybook/no-stories-of": "error",
1678
+ "storybook/no-title-property-in-meta": "warn",
1679
+ "storybook/prefer-pascal-case": "error",
1680
+ "storybook/story-exports": "error",
1681
+ "storybook/use-storybook-expect": "error",
1682
+ "storybook/use-storybook-testing-library": "error",
1683
+ "storybook/meta-satisfies-type": "error",
1684
+ "import/no-default-export": "off",
1685
+ "max-lines": "off",
1686
+ "max-lines-per-function": "off"
1687
+ }
1688
+ }];
2164
1689
  }
2165
-
2166
- // src/overrides/tests.ts
2167
- import vitestPlugin from "@vitest/eslint-plugin";
2168
- import testingLibraryPlugin from "eslint-plugin-testing-library";
2169
- var TEST_FILES2 = ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"];
2170
- var VITEST_RULES = {
2171
- // ── Vitest rules ──────────────────────────────────────────────
2172
- // Every test must contain at least one assertion
2173
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/expect-expect.md
2174
- "vitest/expect-expect": "error",
2175
- // Prevent duplicate test titles within a describe block
2176
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-identical-title.md
2177
- "vitest/no-identical-title": "error",
2178
- // No it.only / describe.only — prevents accidentally skipping tests in CI
2179
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-focused-tests.md
2180
- "vitest/no-focused-tests": "error",
2181
- // Warn on it.skip / describe.skip — track disabled tests
2182
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-disabled-tests.md
2183
- "vitest/no-disabled-tests": "warn",
2184
- // No duplicate beforeEach/afterEach hooks — merge them
2185
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-duplicate-hooks.md
2186
- "vitest/no-duplicate-hooks": "error",
2187
- // Prefer .toBe() over .toEqual() for primitives — clearer intent
2188
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-be.md
2189
- "vitest/prefer-to-be": "error",
2190
- // Prefer .toHaveLength() over .toBe(arr.length) — better errors
2191
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-to-have-length.md
2192
- "vitest/prefer-to-have-length": "error",
2193
- // Validate expect() usage — no dangling expect without assertion
2194
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-expect.md
2195
- "vitest/valid-expect": "error",
2196
- // Validate test/describe title format — no empty or invalid titles
2197
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/valid-title.md
2198
- "vitest/valid-title": "error",
2199
- // No conditional logic (if/else) inside tests — split into separate tests
2200
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-in-test.md
2201
- "vitest/no-conditional-in-test": "error",
2202
- // No expect() inside conditional blocks — always assert unconditionally
2203
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-conditional-expect.md
2204
- "vitest/no-conditional-expect": "error",
2205
- // No standalone expect() outside test blocks — always wrap in it/test
2206
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-standalone-expect.md
2207
- "vitest/no-standalone-expect": "error",
2208
- // Prefer .toStrictEqual() over .toEqual() — catches undefined vs missing
2209
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-strict-equal.md
2210
- "vitest/prefer-strict-equal": "error",
2211
- // Prefer vi.spyOn() over vi.fn() for method mocks — preserves original
2212
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/prefer-spy-on.md
2213
- "vitest/prefer-spy-on": "error",
2214
- // Require message argument in toThrow/toThrowError — verify correct error
2215
- // https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/require-to-throw-message.md
2216
- "vitest/require-to-throw-message": "error",
2217
- // ── Relaxed rules for tests ───────────────────────────────────
2218
- // Tests are naturally verbose and use patterns banned in prod code
2219
- // Tests can be long (setup + many assertions)
2220
- "max-lines": "off",
2221
- "max-lines-per-function": "off",
2222
- "max-statements": "off",
2223
- // Tests often need `any` for mocking and type assertions
2224
- "@typescript-eslint/no-explicit-any": "off",
2225
- "@typescript-eslint/no-non-null-assertion": "off",
2226
- "@typescript-eslint/no-unsafe-assignment": "off",
2227
- "@typescript-eslint/no-unsafe-member-access": "off"
1690
+ //#endregion
1691
+ //#region src/overrides/tests.ts
1692
+ const TEST_FILES = ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}"];
1693
+ const VITEST_RULES = {
1694
+ "vitest/expect-expect": "error",
1695
+ "vitest/no-identical-title": "error",
1696
+ "vitest/no-focused-tests": "error",
1697
+ "vitest/no-disabled-tests": "warn",
1698
+ "vitest/no-duplicate-hooks": "error",
1699
+ "vitest/prefer-to-be": "error",
1700
+ "vitest/prefer-to-have-length": "error",
1701
+ "vitest/valid-expect": "error",
1702
+ "vitest/valid-title": "error",
1703
+ "vitest/no-conditional-in-test": "error",
1704
+ "vitest/no-conditional-expect": "error",
1705
+ "vitest/no-standalone-expect": "error",
1706
+ "vitest/prefer-strict-equal": "error",
1707
+ "vitest/prefer-spy-on": "error",
1708
+ "vitest/require-to-throw-message": "error",
1709
+ "max-lines": "off",
1710
+ "max-lines-per-function": "off",
1711
+ "max-statements": "off",
1712
+ "@typescript-eslint/no-explicit-any": "off",
1713
+ "@typescript-eslint/no-non-null-assertion": "off",
1714
+ "@typescript-eslint/no-unsafe-assignment": "off",
1715
+ "@typescript-eslint/no-unsafe-member-access": "off"
2228
1716
  };
2229
- var TESTING_LIBRARY_RULES = {
2230
- // ── Testing Library rules ───────────────────────────────────
2231
- // Await async events (userEvent.click, etc.) — prevents race conditions
2232
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-events.md
2233
- "testing-library/await-async-events": "error",
2234
- // Await async queries (findBy*) — they return promises
2235
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-queries.md
2236
- "testing-library/await-async-queries": "error",
2237
- // Await async utilities (waitFor, waitForElementToBeRemoved)
2238
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/await-async-utils.md
2239
- "testing-library/await-async-utils": "error",
2240
- // Don't await synchronous events — misleading
2241
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-events.md
2242
- "testing-library/no-await-sync-events": "error",
2243
- // Don't await synchronous queries (getBy*, queryBy*) — not promises
2244
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-await-sync-queries.md
2245
- "testing-library/no-await-sync-queries": "error",
2246
- // Don't use container.querySelector — use queries instead
2247
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-container.md
2248
- "testing-library/no-container": "error",
2249
- // Warn on debug()/prettyDOM() left in tests — debugging artifacts
2250
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-debugging-utils.md
2251
- "testing-library/no-debugging-utils": "warn",
2252
- // Don't access DOM nodes directly — use queries
2253
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-node-access.md
2254
- "testing-library/no-node-access": "error",
2255
- // Don't call render in beforeEach — call in each test
2256
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-render-in-lifecycle.md
2257
- "testing-library/no-render-in-lifecycle": "error",
2258
- // No unnecessary act() wrappers — TL handles this internally
2259
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-unnecessary-act.md
2260
- "testing-library/no-unnecessary-act": "error",
2261
- // One assertion per waitFor — multiple can mask failures
2262
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-multiple-assertions.md
2263
- "testing-library/no-wait-for-multiple-assertions": "error",
2264
- // No side effects in waitFor — only assertions
2265
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-wait-for-side-effects.md
2266
- "testing-library/no-wait-for-side-effects": "error",
2267
- // Prefer findBy* over waitFor + getBy* — built-in combination
2268
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-find-by.md
2269
- "testing-library/prefer-find-by": "error",
2270
- // Use getBy* (throws) for present elements, queryBy* for absent
2271
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-presence-queries.md
2272
- "testing-library/prefer-presence-queries": "error",
2273
- // Use queryBy* for disappearance checks — returns null when gone
2274
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-query-by-disappearance.md
2275
- "testing-library/prefer-query-by-disappearance": "error",
2276
- // Use screen.getBy* over destructured render result — consistent
2277
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/prefer-screen-queries.md
2278
- "testing-library/prefer-screen-queries": "error",
2279
- // Name render result consistently (e.g., `const { getByText } = render(...)`)
2280
- // https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/render-result-naming-convention.md
2281
- "testing-library/render-result-naming-convention": "error"
1717
+ const TESTING_LIBRARY_RULES = {
1718
+ "testing-library/await-async-events": "error",
1719
+ "testing-library/await-async-queries": "error",
1720
+ "testing-library/await-async-utils": "error",
1721
+ "testing-library/no-await-sync-events": "error",
1722
+ "testing-library/no-await-sync-queries": "error",
1723
+ "testing-library/no-container": "error",
1724
+ "testing-library/no-debugging-utils": "warn",
1725
+ "testing-library/no-node-access": "error",
1726
+ "testing-library/no-render-in-lifecycle": "error",
1727
+ "testing-library/no-unnecessary-act": "error",
1728
+ "testing-library/no-wait-for-multiple-assertions": "error",
1729
+ "testing-library/no-wait-for-side-effects": "error",
1730
+ "testing-library/prefer-find-by": "error",
1731
+ "testing-library/prefer-presence-queries": "error",
1732
+ "testing-library/prefer-query-by-disappearance": "error",
1733
+ "testing-library/prefer-screen-queries": "error",
1734
+ "testing-library/render-result-naming-convention": "error"
2282
1735
  };
1736
+ /**
1737
+ * Test file overrides — Vitest rules + Testing Library + relaxed strictness.
1738
+ *
1739
+ * File patterns: *.test.{ts,tsx}, __tests__/*.{ts,tsx}
1740
+ *
1741
+ * @see https://github.com/vitest-dev/eslint-plugin-vitest#rules
1742
+ * @see https://github.com/testing-library/eslint-plugin-testing-library#supported-rules
1743
+ */
2283
1744
  function testsOverride() {
2284
- return [
2285
- {
2286
- name: "eslint-config-setup/tests",
2287
- files: TEST_FILES2,
2288
- plugins: {
2289
- vitest: vitestPlugin
2290
- },
2291
- rules: VITEST_RULES
2292
- },
2293
- {
2294
- name: "eslint-config-setup/tests-testing-library",
2295
- files: TEST_FILES2,
2296
- plugins: {
2297
- "testing-library": testingLibraryPlugin
2298
- },
2299
- rules: TESTING_LIBRARY_RULES
2300
- }
2301
- ];
1745
+ return [{
1746
+ name: "eslint-config-setup/tests",
1747
+ files: TEST_FILES,
1748
+ plugins: { vitest: vitestPlugin },
1749
+ rules: VITEST_RULES
1750
+ }, {
1751
+ name: "eslint-config-setup/tests-testing-library",
1752
+ files: TEST_FILES,
1753
+ plugins: { "testing-library": testingLibraryPlugin },
1754
+ rules: TESTING_LIBRARY_RULES
1755
+ }];
2302
1756
  }
2303
-
2304
- // src/oxlint/integration.ts
2305
- import oxlintPlugin from "eslint-plugin-oxlint";
2306
- var typedPlugin = oxlintPlugin;
1757
+ //#endregion
1758
+ //#region src/oxlint/integration.ts
1759
+ const typedPlugin = oxlintPlugin;
2307
1760
  function addOxlintConfig(configs, configName, blockName) {
2308
- const raw = typedPlugin.configs[configName];
2309
- const items = Array.isArray(raw) ? raw : [raw];
2310
- for (const [index, item] of items.entries()) {
2311
- const suffix = items.length > 1 ? `-${index + 1}` : "";
2312
- configs.push({ ...item, name: `eslint-config-setup/${blockName}${suffix}` });
2313
- }
1761
+ const raw = typedPlugin.configs[configName];
1762
+ const items = Array.isArray(raw) ? raw : [raw];
1763
+ for (const [index, item] of items.entries()) {
1764
+ const suffix = items.length > 1 ? `-${index + 1}` : "";
1765
+ configs.push({
1766
+ ...item,
1767
+ name: `eslint-config-setup/${blockName}${suffix}`
1768
+ });
1769
+ }
2314
1770
  }
2315
1771
  function addReactOxlintConfigs(configs, opts) {
2316
- if (!opts.react) {
2317
- return;
2318
- }
2319
- addOxlintConfig(configs, "flat/react", "oxlint-react");
2320
- addOxlintConfig(configs, "flat/jsx-a11y", "oxlint-jsx-a11y");
2321
- addOxlintConfig(configs, "flat/react-hooks", "oxlint-react-hooks");
2322
- if (opts.ai) {
2323
- addOxlintConfig(configs, "flat/react-perf", "oxlint-react-perf");
2324
- }
1772
+ if (!opts.react) return;
1773
+ addOxlintConfig(configs, "flat/react", "oxlint-react");
1774
+ addOxlintConfig(configs, "flat/jsx-a11y", "oxlint-jsx-a11y");
1775
+ addOxlintConfig(configs, "flat/react-hooks", "oxlint-react-hooks");
1776
+ if (opts.ai) addOxlintConfig(configs, "flat/react-perf", "oxlint-react-perf");
2325
1777
  }
1778
+ /**
1779
+ * Appends eslint-plugin-oxlint configs that disable all ESLint rules
1780
+ * already covered by OxLint. Must be the LAST config in the array.
1781
+ *
1782
+ * This allows running `oxlint && eslint` where OxLint handles the fast
1783
+ * checks and ESLint only runs type-aware and specialty rules.
1784
+ */
2326
1785
  function oxlintIntegration(opts) {
2327
- const configs = [];
2328
- addOxlintConfig(configs, "flat/recommended", "oxlint");
2329
- addReactOxlintConfigs(configs, opts);
2330
- if (opts.node) {
2331
- addOxlintConfig(configs, "flat/node", "oxlint-node");
2332
- }
2333
- addOxlintConfig(configs, "flat/typescript", "oxlint-typescript");
2334
- addOxlintConfig(configs, "flat/unicorn", "oxlint-unicorn");
2335
- addOxlintConfig(configs, "flat/import", "oxlint-import");
2336
- addOxlintConfig(configs, "flat/jsdoc", "oxlint-jsdoc");
2337
- return configs;
1786
+ const configs = [];
1787
+ addOxlintConfig(configs, "flat/recommended", "oxlint");
1788
+ addReactOxlintConfigs(configs, opts);
1789
+ if (opts.node) addOxlintConfig(configs, "flat/node", "oxlint-node");
1790
+ addOxlintConfig(configs, "flat/typescript", "oxlint-typescript");
1791
+ addOxlintConfig(configs, "flat/unicorn", "oxlint-unicorn");
1792
+ addOxlintConfig(configs, "flat/import", "oxlint-import");
1793
+ addOxlintConfig(configs, "flat/jsdoc", "oxlint-jsdoc");
1794
+ return configs;
2338
1795
  }
2339
-
2340
- // src/presets/standard.ts
1796
+ //#endregion
1797
+ //#region src/presets/standard.ts
1798
+ /**
1799
+ * Complexity preset — limits for production code that encourage small,
1800
+ * focused functions and aggressive extraction of helper functions.
1801
+ * @see https://eslint.org/docs/latest/rules/#suggestions (complexity rules)
1802
+ */
2341
1803
  function standardComplexity() {
2342
- return [
2343
- {
2344
- name: "eslint-config-setup/complexity",
2345
- rules: {
2346
- // Max cyclomatic complexity per function — 10 branches
2347
- // https://eslint.org/docs/latest/rules/complexity
2348
- complexity: ["error", 10],
2349
- // Max nesting depth — 3 levels
2350
- // https://eslint.org/docs/latest/rules/max-depth
2351
- "max-depth": ["error", 3],
2352
- // Max nested callbacks — 2 levels
2353
- // https://eslint.org/docs/latest/rules/max-nested-callbacks
2354
- "max-nested-callbacks": ["error", 2],
2355
- // Max function parameters — 3 before using options object
2356
- // https://eslint.org/docs/latest/rules/max-params
2357
- "max-params": ["error", 3],
2358
- // Max statements per function — 15
2359
- // https://eslint.org/docs/latest/rules/max-statements
2360
- "max-statements": ["error", 15],
2361
- // Max lines per function — 50 (excluding blanks and comments)
2362
- // https://eslint.org/docs/latest/rules/max-lines-per-function
2363
- "max-lines-per-function": [
2364
- "error",
2365
- { max: 50, skipBlankLines: true, skipComments: true }
2366
- ],
2367
- // Max lines per file — 300 (excluding blanks and comments)
2368
- // https://eslint.org/docs/latest/rules/max-lines
2369
- "max-lines": [
2370
- "error",
2371
- { max: 300, skipBlankLines: true, skipComments: true }
2372
- ],
2373
- // Cognitive complexity — measures how hard code is to understand
2374
- // https://sonarsource.github.io/rspec/#/rspec/S3776/javascript
2375
- "sonarjs/cognitive-complexity": ["error", 10]
2376
- }
2377
- },
2378
- {
2379
- name: "eslint-config-setup/complexity-tests-relaxed",
2380
- files: ["**/*.test.{ts,tsx}", "**/__tests__/**/*.{ts,tsx}", "**/*.spec.ts"],
2381
- rules: {
2382
- "max-nested-callbacks": "off",
2383
- "max-lines": "off",
2384
- "max-lines-per-function": "off",
2385
- "max-statements": "off"
2386
- }
2387
- }
2388
- ];
1804
+ return [{
1805
+ name: "eslint-config-setup/complexity",
1806
+ rules: {
1807
+ complexity: ["error", 10],
1808
+ "max-depth": ["error", 3],
1809
+ "max-nested-callbacks": ["error", 2],
1810
+ "max-params": ["error", 3],
1811
+ "max-statements": ["error", 15],
1812
+ "max-lines-per-function": ["error", {
1813
+ max: 50,
1814
+ skipBlankLines: true,
1815
+ skipComments: true
1816
+ }],
1817
+ "max-lines": ["error", {
1818
+ max: 300,
1819
+ skipBlankLines: true,
1820
+ skipComments: true
1821
+ }],
1822
+ "sonarjs/cognitive-complexity": ["error", 10]
1823
+ }
1824
+ }, {
1825
+ name: "eslint-config-setup/complexity-tests-relaxed",
1826
+ files: [
1827
+ "**/*.test.{ts,tsx}",
1828
+ "**/__tests__/**/*.{ts,tsx}",
1829
+ "**/*.spec.ts"
1830
+ ],
1831
+ rules: {
1832
+ "max-nested-callbacks": "off",
1833
+ "max-lines": "off",
1834
+ "max-lines-per-function": "off",
1835
+ "max-statements": "off"
1836
+ }
1837
+ }];
2389
1838
  }
2390
-
2391
- // src/build/compose.ts
1839
+ //#endregion
1840
+ //#region src/build/compose.ts
1841
+ /**
1842
+ * Composes a full flat config array from the given options.
1843
+ * This is the core logic used by the build system to generate configs.
1844
+ */
2392
1845
  function addCoreConfigs(config, opts) {
2393
- const ai = opts.ai;
2394
- config.push(
2395
- ...baseConfig({ ai }),
2396
- ...typescriptConfig({ ai, react: opts.react }),
2397
- ...importsConfig(),
2398
- ...perfectionistConfig(),
2399
- ...unicornConfig({ ai }),
2400
- ...regexpConfig({ ai }),
2401
- ...jsdocConfig({ ai }),
2402
- ...cspellConfig(),
2403
- ...sonarjsConfig({ ai }),
2404
- ...securityConfig(),
2405
- ...deMorganConfig()
2406
- );
1846
+ const ai = opts.ai;
1847
+ config.push(...baseConfig({ ai }), ...typescriptConfig({
1848
+ ai,
1849
+ react: opts.react
1850
+ }), ...importsConfig(), ...perfectionistConfig(), ...unicornConfig({ ai }), ...regexpConfig({ ai }), ...jsdocConfig({ ai }), ...cspellConfig(), ...sonarjsConfig({ ai }), ...securityConfig(), ...deMorganConfig());
2407
1851
  }
2408
1852
  function addConditionalConfigs(config, opts) {
2409
- if (!opts.node) {
2410
- config.push(...compatConfig());
2411
- }
2412
- if (!opts.ai) {
2413
- config.push(...standardComplexity());
2414
- }
2415
- if (opts.node) {
2416
- config.push(...nodeConfig({ ai: opts.ai }));
2417
- }
2418
- if (opts.react) {
2419
- config.push(...reactConfig({ ai: opts.ai }));
2420
- config.push(...reactEffectConfig());
2421
- }
2422
- if (opts.ai) {
2423
- config.push(...aiConfig());
2424
- config.push(...perfectionistAiConfig());
2425
- config.push(...packageJsonAiConfig());
2426
- }
1853
+ if (!opts.node) config.push(...compatConfig());
1854
+ if (!opts.ai) config.push(...standardComplexity());
1855
+ if (opts.node) config.push(...nodeConfig({ ai: opts.ai }));
1856
+ if (opts.react) {
1857
+ config.push(...reactConfig({ ai: opts.ai }));
1858
+ config.push(...reactEffectConfig());
1859
+ }
1860
+ if (opts.ai) {
1861
+ config.push(...aiConfig());
1862
+ config.push(...perfectionistAiConfig());
1863
+ config.push(...packageJsonAiConfig());
1864
+ }
2427
1865
  }
2428
1866
  function addFileTypeOverrides(config) {
2429
- config.push(
2430
- ...testsOverride(),
2431
- ...e2eOverride(),
2432
- ...storiesOverride(),
2433
- ...configFilesOverride(),
2434
- ...declarationsOverride(),
2435
- ...scriptsOverride()
2436
- );
1867
+ config.push(...testsOverride(), ...e2eOverride(), ...storiesOverride(), ...configFilesOverride(), ...declarationsOverride(), ...scriptsOverride());
2437
1868
  }
2438
1869
  function addFinalConfigs(config, opts) {
2439
- config.push(...jsonConfig(), ...packageJsonConfig(), ...markdownConfig(), ...prettierCompatConfig());
2440
- if (opts.oxlint) {
2441
- config.push(...oxlintIntegration(opts));
2442
- }
1870
+ config.push(...jsonConfig(), ...packageJsonConfig(), ...markdownConfig(), ...prettierCompatConfig());
1871
+ if (opts.oxlint) config.push(...oxlintIntegration(opts));
2443
1872
  }
2444
1873
  function composeConfig(opts) {
2445
- const config = [];
2446
- addCoreConfigs(config, opts);
2447
- addConditionalConfigs(config, opts);
2448
- addFileTypeOverrides(config);
2449
- addFinalConfigs(config, opts);
2450
- return config;
1874
+ const config = [];
1875
+ addCoreConfigs(config, opts);
1876
+ addConditionalConfigs(config, opts);
1877
+ addFileTypeOverrides(config);
1878
+ addFinalConfigs(config, opts);
1879
+ return config;
2451
1880
  }
2452
- export {
2453
- aiConfig as ai,
2454
- baseConfig as base,
2455
- composeConfig,
2456
- configFilesOverride as configFiles,
2457
- cspellConfig as cspell,
2458
- deMorganConfig as deMorgan,
2459
- declarationsOverride as declarations,
2460
- e2eOverride as e2e,
2461
- importsConfig as imports,
2462
- jsdocConfig as jsdoc,
2463
- jsonConfig as json,
2464
- markdownConfig as markdown,
2465
- nodeConfig as node,
2466
- oxlintIntegration as oxlint,
2467
- packageJsonConfig as packageJson,
2468
- packageJsonAiConfig as packageJsonAi,
2469
- perfectionistConfig as perfectionist,
2470
- perfectionistAiConfig as perfectionistAi,
2471
- prettierCompatConfig as prettier,
2472
- reactConfig as react,
2473
- reactEffectConfig as reactEffect,
2474
- regexpConfig as regexp,
2475
- scriptsOverride as scripts,
2476
- securityConfig as security,
2477
- sonarjsConfig as sonarjs,
2478
- standardComplexity,
2479
- storiesOverride as stories,
2480
- testsOverride as tests,
2481
- typescriptConfig as typescript,
2482
- unicornConfig as unicorn
2483
- };
1881
+ //#endregion
1882
+ export { aiConfig as ai, baseConfig as base, composeConfig, configFilesOverride as configFiles, cspellConfig as cspell, deMorganConfig as deMorgan, declarationsOverride as declarations, e2eOverride as e2e, importsConfig as imports, jsdocConfig as jsdoc, jsonConfig as json, markdownConfig as markdown, nodeConfig as node, oxlintIntegration as oxlint, packageJsonConfig as packageJson, packageJsonAiConfig as packageJsonAi, perfectionistConfig as perfectionist, perfectionistAiConfig as perfectionistAi, prettierCompatConfig as prettier, reactConfig as react, reactEffectConfig as reactEffect, regexpConfig as regexp, scriptsOverride as scripts, securityConfig as security, sonarjsConfig as sonarjs, standardComplexity, storiesOverride as stories, testsOverride as tests, typescriptConfig as typescript, unicornConfig as unicorn };
1883
+
2484
1884
  //# sourceMappingURL=modules.js.map