@mlaursen/eslint-config 12.0.6 → 12.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,645 @@
1
+ import eslint from '@eslint/js';
2
+ import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
3
+ import reactPlugin from 'eslint-plugin-react';
4
+ import reactHooksPlugin from 'eslint-plugin-react-hooks';
5
+ import reactRefreshPlugin from 'eslint-plugin-react-refresh';
6
+ import vitestPlugin from '@vitest/eslint-plugin';
7
+ import jestPlugin from 'eslint-plugin-jest';
8
+ import jestDomPlugin from 'eslint-plugin-jest-dom';
9
+ import tseslint from 'typescript-eslint';
10
+ import eslintPluiginUnicorn from 'eslint-plugin-unicorn';
11
+ import testingLibraryPlugin from 'eslint-plugin-testing-library';
12
+ import { includeIgnoreFile } from '@eslint/compat';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const DEV_WARNING_PROD_ERROR = process.env["NODE_ENV"] === "production" ? "error" : "warn";
16
+ /**
17
+ * This is a "temporary" workaround until autofixable rules can be disabled with
18
+ * a config option because of the "autofix + format(+ save)?" behavior. It
19
+ * should be something closer to `DEV_WARN_PROD_ERROR_AND_FIX`
20
+ */ const DEV_OFF_PROD_ERROR = process.env["NODE_ENV"] === "production" ? "error" : "off";
21
+ const BASE_NAME = "@mlaursen/eslint-config";
22
+ const TS_FILES = [
23
+ "**/*.{ts,tsx,mts,mtsx}"
24
+ ];
25
+ const TEST_FILES = [
26
+ "**/__tests__/**",
27
+ "**/*.{spec,test}.{ts,tsx,js,jsx}"
28
+ ];
29
+ const JSX_FILES = [
30
+ "**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}"
31
+ ];
32
+ const VITE_MAIN_FILES = [
33
+ "**/main.tsx"
34
+ ];
35
+
36
+ /**
37
+ * @example
38
+ * ```js
39
+ * import { configs } from "@mlaursen/eslint-config";
40
+ * import { defineConfig } from "eslint/config";
41
+ *
42
+ * export default defineConfig(configs.base);
43
+ * ```
44
+ */ const base = [
45
+ eslint.configs.recommended,
46
+ {
47
+ name: `${BASE_NAME}/base`,
48
+ rules: {
49
+ // I use typescript instead
50
+ "no-undef": "off",
51
+ // You normally do not want `console.{whatever}` in prod but is fine for
52
+ // development in debugging
53
+ "no-console": DEV_WARNING_PROD_ERROR,
54
+ "no-var": "error",
55
+ "no-use-before-define": "warn",
56
+ // I want to enforce all statements to require curly braces even if it
57
+ // could be omitted for consistency
58
+ curly: "error",
59
+ // Since this is auto-fixable, I like `{ someproperty }` instead of
60
+ // `{ someproperty: someproperty }`
61
+ "object-shorthand": [
62
+ "error",
63
+ "always"
64
+ ],
65
+ // This is about the same as `object-shorthand`. Only rename if it has to
66
+ // be renamed
67
+ "no-useless-rename": [
68
+ "error"
69
+ ],
70
+ "no-eval": "error",
71
+ "no-alert": "error",
72
+ "no-lonely-if": "error",
73
+ "no-else-return": "error",
74
+ eqeqeq: "error",
75
+ // 100% stylistic, but do not allow `a = b = c = "whatever"` / `let a = whatever, b = whatever, c = whatever;`
76
+ // these should be different statements
77
+ "no-multi-assign": "error",
78
+ "no-sequences": "error",
79
+ // use template strings instead
80
+ "no-multi-str": "error",
81
+ // better to use new variables most of the time
82
+ "no-param-reassign": "error",
83
+ // i'd never hit these, but who trusts other people and AI?
84
+ "no-return-assign": "error",
85
+ "no-script-url": "error"
86
+ }
87
+ }
88
+ ];
89
+
90
+ /**
91
+ * @example
92
+ * ```ts
93
+ * import { configs } from "@mlaursen/eslint-config";
94
+ * import { defineConfig } from "eslint/config";
95
+ *
96
+ * export default defineConfig(configs.jsxA11y);
97
+ * ```
98
+ */ const jsxA11y = [
99
+ {
100
+ name: `${BASE_NAME}/jsx-a11y`,
101
+ files: JSX_FILES,
102
+ ...jsxA11yPlugin.flatConfigs.recommended,
103
+ rules: {
104
+ ...jsxA11yPlugin.flatConfigs.recommended.rules,
105
+ // I **only** use autoFocus within dialogs which provide the correct
106
+ // context for screen readers.
107
+ "jsx-a11y/no-autofocus": "off"
108
+ }
109
+ },
110
+ {
111
+ name: `${BASE_NAME}/jsx-a11y/testing`,
112
+ files: TEST_FILES,
113
+ rules: {
114
+ "jsx-a11y/anchor-has-content": "off",
115
+ "jsx-a11y/click-events-have-key-events": "off",
116
+ "jsx-a11y/no-static-element-interactions": "off"
117
+ }
118
+ }
119
+ ];
120
+
121
+ /**
122
+ * @example
123
+ * ```ts
124
+ * import { configs } from "@mlaursen/eslint-config";
125
+ * import { defineConfig } from "eslint/config";
126
+ *
127
+ * export default defineConfig(configs.react());
128
+ *
129
+ * // or with react compiler rules enabled
130
+ * export default defineConfig(configs.react(true));
131
+ * ```
132
+ *
133
+ * Enables:
134
+ * - `eslint-plugin-react` with:
135
+ * - flat.recommended
136
+ * - flat['jsx-runtime']
137
+ * - `eslint-plugin-react-hooks` with:
138
+ * - recommended rules
139
+ * - compiler rules (if `true` is provided)
140
+ */ const react = (options = {})=>{
141
+ const { reactRefresh, reactCompiler } = options;
142
+ return [
143
+ {
144
+ ...reactPlugin.configs.flat["recommended"],
145
+ name: `${BASE_NAME}/react`,
146
+ files: JSX_FILES,
147
+ settings: {
148
+ react: {
149
+ version: "detect"
150
+ }
151
+ },
152
+ rules: {
153
+ ...reactPlugin.configs.flat["recommended"]?.rules,
154
+ ...reactPlugin.configs.flat["jsx-runtime"]?.rules
155
+ }
156
+ },
157
+ {
158
+ ...reactHooksPlugin.configs.flat.recommended,
159
+ name: `${BASE_NAME}/react-hooks`,
160
+ rules: {
161
+ ...reactCompiler && reactHooksPlugin.configs.flat.recommended.rules,
162
+ "react-hooks/rules-of-hooks": "error",
163
+ "react-hooks/exhaustive-deps": [
164
+ "error",
165
+ {
166
+ additionalHooks: "(useIsomorphicLayoutEffect)"
167
+ }
168
+ ]
169
+ }
170
+ },
171
+ ...reactRefresh ? [
172
+ {
173
+ ...reactRefreshPlugin.configs[reactRefresh],
174
+ name: `${BASE_NAME}/react-refresh`,
175
+ ignores: [
176
+ "**/test-utils*",
177
+ "**/test-utils/**"
178
+ ]
179
+ }
180
+ ] : []
181
+ ];
182
+ };
183
+
184
+ /**
185
+ * @example
186
+ * ```ts
187
+ * import { configs } from "@mlaursen/eslint-config";
188
+ * import { defineConfig } from "eslint/config";
189
+ *
190
+ * export default defineConfig(configs.scripts);
191
+ * ```
192
+ */ const scripts = [
193
+ {
194
+ name: `${BASE_NAME}/scripts`,
195
+ files: [
196
+ "scripts/**"
197
+ ],
198
+ rules: {
199
+ "no-console": "off"
200
+ }
201
+ }
202
+ ];
203
+
204
+ /**
205
+ * @example
206
+ * ```ts
207
+ * import { configs } from "@mlaursen/eslint-config";
208
+ * import { defineConfig } from "eslint/config";
209
+ *
210
+ * export default defineConfig(configs.vitest);
211
+ * ```
212
+ */ const vitest = [
213
+ {
214
+ name: `${BASE_NAME}/vitest`,
215
+ files: TEST_FILES,
216
+ plugins: {
217
+ vitest: vitestPlugin
218
+ },
219
+ rules: {
220
+ ...vitestPlugin.configs.recommended.rules,
221
+ "vitest/no-alias-methods": "error",
222
+ "vitest/no-focused-tests": DEV_OFF_PROD_ERROR,
223
+ "vitest/no-disabled-tests": DEV_OFF_PROD_ERROR,
224
+ "vitest/no-duplicate-hooks": "error",
225
+ "vitest/no-standalone-expect": "error",
226
+ "vitest/prefer-expect-resolves": "error",
227
+ "vitest/prefer-spy-on": "error",
228
+ "vitest/prefer-vi-mocked": "error"
229
+ }
230
+ }
231
+ ];
232
+ /**
233
+ * @example
234
+ * ```ts
235
+ * import { configs } from "@mlaursen/eslint-config";
236
+ * import { defineConfig } from "eslint/config";
237
+ *
238
+ * export default defineConfig(configs.jest);
239
+ * ```
240
+ */ const jest = [
241
+ {
242
+ name: `${BASE_NAME}/jest`,
243
+ files: TEST_FILES,
244
+ ...jestPlugin.configs["flat/recommended"]
245
+ }
246
+ ];
247
+ /**
248
+ * @example
249
+ * ```ts
250
+ * import { configs } from "@mlaursen/eslint-config";
251
+ * import { defineConfig } from "eslint/config";
252
+ *
253
+ * export default defineConfig([
254
+ * ...configs.jest,
255
+ * ...configs.jestDom,
256
+ * ]);
257
+ * ```
258
+ */ const jestDom = [
259
+ {
260
+ name: `${BASE_NAME}/jest-dom`,
261
+ files: TEST_FILES,
262
+ ...jestDomPlugin.configs["flat/recommended"]
263
+ }
264
+ ];
265
+ /**
266
+ * @example
267
+ * ```ts
268
+ * import { configs } from "@mlaursen/eslint-config";
269
+ * import { defineConfig } from "eslint/config";
270
+ *
271
+ * export default defineConfig(configs.testing({ testFramework: "jest" }));
272
+ *
273
+ * // or
274
+ * export default defineConfig(configs.testing({ testFramework: "vitest" }));
275
+ * ```
276
+ */ const testing = (options = {})=>{
277
+ const { testFramework = "vitest" } = options;
278
+ let config;
279
+ switch(testFramework){
280
+ case "jest":
281
+ config = jest;
282
+ break;
283
+ case "vitest":
284
+ config = vitest;
285
+ break;
286
+ }
287
+ return [
288
+ ...config,
289
+ ...jestDom
290
+ ];
291
+ };
292
+
293
+ /**
294
+ * @example
295
+ * ```ts
296
+ * import { configs, gitignore } from "@mlaursen/eslint-config";
297
+ * import { defineConfig } from "eslint/config";
298
+ *
299
+ * export default defineConfig([
300
+ * gitignore(import.meta.url),
301
+ * ...configs.typescript(),
302
+ * ]);
303
+ * ```
304
+ */ const typescript = (options = {})=>{
305
+ const { tsconfigRootDir, strictTypeChecked } = options;
306
+ const configs = [
307
+ ...base,
308
+ ...tseslint.configs.strict,
309
+ {
310
+ name: `${BASE_NAME}/typescript`,
311
+ files: TS_FILES,
312
+ rules: {
313
+ // I normally do not want unified signatures since it helps improve type
314
+ // inference with function overloading
315
+ "@typescript-eslint/unified-signatures": "off",
316
+ // I prefer specifying when something is used only as a type
317
+ "@typescript-eslint/consistent-type-imports": [
318
+ "error",
319
+ {
320
+ fixStyle: "inline-type-imports"
321
+ }
322
+ ],
323
+ // I prefer shorthand syntax
324
+ "@typescript-eslint/array-type": [
325
+ "error",
326
+ {
327
+ default: "array"
328
+ }
329
+ ],
330
+ // I prefer using `interface` over `type = {}`
331
+ "@typescript-eslint/consistent-type-definitions": [
332
+ "error",
333
+ "interface"
334
+ ],
335
+ // Allow expressions to work with react hooks. Annoying to have to
336
+ // typedef each arrow function in a `useEffect` or `useCallback` when
337
+ // it can be derived.
338
+ "@typescript-eslint/explicit-function-return-type": [
339
+ "error",
340
+ {
341
+ allowExpressions: true,
342
+ // allow FC definitions for React
343
+ allowTypedFunctionExpressions: true
344
+ }
345
+ ],
346
+ "@typescript-eslint/no-unused-vars": [
347
+ "error",
348
+ {
349
+ argsIgnorePattern: "^_",
350
+ varsIgnorePattern: "^_"
351
+ }
352
+ ],
353
+ "no-use-before-define": "off",
354
+ "@typescript-eslint/no-use-before-define": [
355
+ "warn",
356
+ {
357
+ ignoreTypeReferences: true
358
+ }
359
+ ]
360
+ }
361
+ },
362
+ {
363
+ name: `${BASE_NAME}/typescript/tests`,
364
+ files: TEST_FILES,
365
+ rules: {
366
+ // allow tests to be less strict
367
+ "@typescript-eslint/ban-ts-comment": "off",
368
+ "@typescript-eslint/explicit-function-return-type": "off",
369
+ "@typescript-eslint/no-empty-function": "off",
370
+ "@typescript-eslint/no-explicit-any": "off",
371
+ "@typescript-eslint/no-object-literal-type-assertion": "off",
372
+ "@typescript-eslint/no-var-requires": "off"
373
+ }
374
+ },
375
+ {
376
+ name: `${BASE_NAME}/typescript/vite`,
377
+ files: VITE_MAIN_FILES,
378
+ rules: {
379
+ // allow `createRoot(document.getElementById("root")).render(...)` for
380
+ // `vite` without disabling eslint
381
+ "@typescript-eslint/no-non-null-assertion": "off"
382
+ }
383
+ }
384
+ ];
385
+ if (tsconfigRootDir) {
386
+ configs.push({
387
+ name: `${BASE_NAME}/typescript-type-checking-language-options`,
388
+ languageOptions: {
389
+ parserOptions: {
390
+ projectService: strictTypeChecked,
391
+ tsconfigRootDir
392
+ }
393
+ }
394
+ });
395
+ }
396
+ if (strictTypeChecked) {
397
+ configs.push(...tseslint.configs.strictTypeCheckedOnly, {
398
+ name: `${BASE_NAME}/typescript-type-checking`,
399
+ files: TS_FILES,
400
+ rules: {
401
+ // I do not enable the `noUncheckedIndexedAccess` tsconfig option, so I
402
+ // still need to verify that stuff exists. There are other cases where I
403
+ // know it exists, so I can ignore those as well
404
+ "@typescript-eslint/no-unnecessary-condition": "off",
405
+ // I never use `this`
406
+ "@typescript-eslint/unbound-method": "off",
407
+ "@typescript-eslint/restrict-template-expressions": [
408
+ "error",
409
+ {
410
+ allowNumber: true
411
+ }
412
+ ],
413
+ // I want to allow `onClick={async (event) => { ... }}`
414
+ "@typescript-eslint/no-misused-promises": [
415
+ "error",
416
+ {
417
+ checksVoidReturn: false
418
+ }
419
+ ]
420
+ }
421
+ }, {
422
+ name: `${BASE_NAME}/typescript-type-checking/tests`,
423
+ files: TEST_FILES,
424
+ rules: {
425
+ // like base typescript, can be less strict in tests
426
+ "@typescript-eslint/no-unsafe-return": "off",
427
+ "@typescript-eslint/no-unsafe-assignment": "off",
428
+ "@typescript-eslint/restrict-template-expressions": "off"
429
+ }
430
+ });
431
+ }
432
+ return configs;
433
+ };
434
+
435
+ const unicorn = [
436
+ {
437
+ ...eslintPluiginUnicorn.configs.recommended,
438
+ name: `${BASE_NAME}/unicorn`,
439
+ rules: {
440
+ ...eslintPluiginUnicorn.configs.recommended.rules,
441
+ // this flags `dist` and `dest` which is annoying
442
+ "unicorn/prevent-abbreviations": "off",
443
+ // I do not like using the default exports from `node:path`, `node:fs`,
444
+ // etc
445
+ "unicorn/import-style": "off",
446
+ // I want PascalCase for React components/classes, camelCase for others.
447
+ // Don't care enough to enforce through a rule and don't think it's
448
+ // possible.
449
+ "unicorn/filename-case": "off",
450
+ // prettier instead
451
+ "unicorn/empty-brace-spaces": "off",
452
+ // I like `null`
453
+ "unicorn/no-null": "off",
454
+ // the description is incorrect since it attempts converting multi-line
455
+ // statements to ternary which is awful:
456
+ //
457
+ // > This rule enforces the use of ternary expressions over 'simple'
458
+ // > if-else statements, where 'simple' means the consequent and alternate
459
+ // > are each one line and have the same basic type and form.
460
+ "unicorn/prefer-ternary": "off",
461
+ // I don't care about adding braces to switch cases. I'd prefer
462
+ // `["error", "avoid"]` more though since it only adds them when needed.
463
+ "unicorn/switch-case-braces": "off"
464
+ }
465
+ },
466
+ {
467
+ name: `${BASE_NAME}/unicorn/vite`,
468
+ files: VITE_MAIN_FILES,
469
+ rules: {
470
+ // allow `createRoot(document.getElementById("root")).render(...)` for
471
+ // `vite` without disabling eslint
472
+ "unicorn/prefer-query-selector": "off"
473
+ }
474
+ },
475
+ {
476
+ name: `${BASE_NAME}/unicorn/testing`,
477
+ files: TEST_FILES,
478
+ rules: {
479
+ // allow functions to be scoped to tests
480
+ "unicorn/consistent-function-scoping": "off"
481
+ }
482
+ }
483
+ ];
484
+
485
+ /**
486
+ * @example
487
+ * ```ts
488
+ * import { configs, gitignore } from "@mlaursen/eslint-config";
489
+ * import { defineConfig } from "eslint/config";
490
+ *
491
+ * export default defineConfig([
492
+ * gitignore(import.meta.url),
493
+ * ...configs.recommended(),
494
+ * ]);
495
+ * ```
496
+ */ const recommended = (options = {})=>{
497
+ return [
498
+ ...typescript(options),
499
+ ...scripts,
500
+ ...testing(options),
501
+ ...unicorn
502
+ ];
503
+ };
504
+
505
+ const mui = [
506
+ {
507
+ name: `${BASE_NAME}/material-ui`,
508
+ files: JSX_FILES,
509
+ rules: {
510
+ "no-restricted-imports": [
511
+ "error",
512
+ {
513
+ // https://mui.com/material-ui/guides/minimizing-bundle-size/#enforce-best-practices-with-eslint
514
+ patterns: [
515
+ {
516
+ regex: "^@mui/(?!(x-|utils))[^/]+$"
517
+ }
518
+ ]
519
+ }
520
+ ]
521
+ }
522
+ }
523
+ ];
524
+
525
+ /**
526
+ * @example
527
+ * ```ts
528
+ * import { configs } from "@mlaursen/eslint-config";
529
+ * import { defineConfig } from "eslint/config";
530
+ *
531
+ * export default defineConfig([
532
+ * ...configs.react,
533
+ * ...configs.jest,
534
+ * ...configs.jestDom,
535
+ * ...configs.testingLibraryReact
536
+ * ]);
537
+ * ```
538
+ *
539
+ * NOTE: Only choose this or the {@link testingLibraryDom}. Do not use
540
+ * both.
541
+ */ const testingLibraryReact = [
542
+ {
543
+ name: `${BASE_NAME}/testing-library/react`,
544
+ files: TEST_FILES,
545
+ ...testingLibraryPlugin.configs["flat/react"],
546
+ rules: {
547
+ ...testingLibraryPlugin.configs["flat/react"].rules,
548
+ // it can be useful to reassign screen.* queries without reusing to
549
+ // verify it still exists
550
+ "no-useless-assignment": "off"
551
+ }
552
+ }
553
+ ];
554
+ /**
555
+ * @example
556
+ * ```ts
557
+ * import { configs, defineConfig } from "@mlaursen/eslint-config";
558
+ *
559
+ * export default defineConfig([
560
+ * ...configs.jest,
561
+ * ...configs.jestDom,
562
+ * ...configs.testingLibraryDom
563
+ * ]);
564
+ * ```
565
+ *
566
+ * NOTE: Only choose this or the {@link testingLibraryReact}. Do not use
567
+ * both.
568
+ */ const testingLibraryDom = [
569
+ {
570
+ name: `${BASE_NAME}/testing-library/dom`,
571
+ files: TEST_FILES,
572
+ ...testingLibraryPlugin.configs["flat/dom"],
573
+ rules: {
574
+ ...testingLibraryPlugin.configs["flat/dom"].rules,
575
+ // it can be useful to reassign screen.* queries without reusing to
576
+ // verify it still exists
577
+ "no-useless-assignment": "off"
578
+ }
579
+ }
580
+ ];
581
+
582
+ /**
583
+ * @example
584
+ * ```ts
585
+ * import { configs, gitignore } from "@mlaursen/eslint-config";
586
+ * import { defineConfig } from "eslint/config";
587
+ *
588
+ * export default defineConfig([
589
+ * gitignore(import.meta.url),
590
+ * ...configs.recommendedFrontend(),
591
+ * ]);
592
+ * ```
593
+ */ const recommendedFrontend = (options = {})=>{
594
+ return [
595
+ ...recommended(options),
596
+ ...mui,
597
+ ...react(options),
598
+ ...jsxA11y,
599
+ ...testingLibraryReact
600
+ ];
601
+ };
602
+
603
+ /**
604
+ * @example
605
+ * ```ts
606
+ * import { configs, gitignore } from "@mlaursen/eslint-config";
607
+ * import { defineConfig } from "eslint/config";
608
+ *
609
+ * export default defineConfig([gitignore(import.meta.url), ...configs.typescript]);
610
+ * ```
611
+ *
612
+ * @example .gitignore in a different folder
613
+ * ```ts
614
+ * import { configs, gitignore } from "@mlaursen/eslint-config";
615
+ * import { defineConfig } from "eslint/config";
616
+ * import { join } from "node:path";
617
+ *
618
+ * export default defineConfig([
619
+ * gitignore(join(import.meta.url, "..", "..")),
620
+ * ...configs.typescript,
621
+ * ]);
622
+ * ```
623
+ */ function gitignore(importMetaUrl) {
624
+ return includeIgnoreFile(fileURLToPath(new URL(".gitignore", importMetaUrl)));
625
+ }
626
+
627
+ const configs = {
628
+ recommended,
629
+ recommendedFrontend,
630
+ base,
631
+ jest,
632
+ jestDom,
633
+ react,
634
+ typescript,
635
+ testingLibraryDom,
636
+ testingLibraryReact,
637
+ scripts,
638
+ jsxA11y,
639
+ testing,
640
+ vitest,
641
+ unicorn
642
+ };
643
+
644
+ export { BASE_NAME, DEV_OFF_PROD_ERROR, DEV_WARNING_PROD_ERROR, JSX_FILES, TEST_FILES, TS_FILES, VITE_MAIN_FILES, configs, gitignore };
645
+ //# sourceMappingURL=index.mjs.map