eslint-config-decent 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,35 @@ import tsEslint from 'typescript-eslint';
15
15
  export default tsEslint.config(...defaultConfig());
16
16
  ```
17
17
 
18
+ ## Override parserOptions
19
+
20
+ ````mjs
21
+ // eslint.config.mjs
22
+
23
+ import { defaultConfig } from 'eslint-config-decent';
24
+ import tsEslint from 'typescript-eslint';
25
+
26
+ export default tsEslint.config(...defaultConfig({
27
+ projectService: {
28
+ allowedDefaultProject: ['./*.js', './*.cjs', './*.mjs', './tests/**/*.ts', './tests/**/*.js', './tests/**/*.cjs', './tests/**/*.mjs'],
29
+ defaultProject: 'tsconfig.json',
30
+ },
31
+ tsconfigRootDir: import.meta.dirname,
32
+ }));
33
+
34
+ ## Disable require-extensions rules
35
+
36
+ ```mjs
37
+ // eslint.config.mjs
38
+
39
+ import { defaultConfig } from 'eslint-config-decent';
40
+ import tsEslint from 'typescript-eslint';
41
+
42
+ export default tsEslint.config(...defaultConfig({
43
+ enableRequireExtensions: false,
44
+ }));
45
+ ````
46
+
18
47
  ## License
19
48
 
20
49
  MIT
package/dist/index.cjs CHANGED
@@ -4,9 +4,15 @@ const eslint = require('@eslint/js');
4
4
  const globals = require('globals');
5
5
  const tsEslint = require('typescript-eslint');
6
6
  const prettier = require('eslint-plugin-prettier/recommended');
7
+ const fs = require('fs');
8
+ const path = require('path');
7
9
  const jsdoc = require('eslint-plugin-jsdoc');
8
10
  const mocha = require('eslint-plugin-mocha');
9
11
  const promise = require('eslint-plugin-promise');
12
+ const a11y = require('eslint-plugin-jsx-a11y');
13
+ const react = require('eslint-plugin-react');
14
+ const reactHooks = require('eslint-plugin-react-hooks');
15
+ const testingLibrary = require('eslint-plugin-testing-library');
10
16
  const security = require('eslint-plugin-security');
11
17
  const unicorn = require('eslint-plugin-unicorn');
12
18
 
@@ -19,10 +25,14 @@ const prettier__default = /*#__PURE__*/_interopDefaultCompat(prettier);
19
25
  const jsdoc__default = /*#__PURE__*/_interopDefaultCompat(jsdoc);
20
26
  const mocha__default = /*#__PURE__*/_interopDefaultCompat(mocha);
21
27
  const promise__default = /*#__PURE__*/_interopDefaultCompat(promise);
28
+ const a11y__default = /*#__PURE__*/_interopDefaultCompat(a11y);
29
+ const react__default = /*#__PURE__*/_interopDefaultCompat(react);
30
+ const reactHooks__default = /*#__PURE__*/_interopDefaultCompat(reactHooks);
31
+ const testingLibrary__default = /*#__PURE__*/_interopDefaultCompat(testingLibrary);
22
32
  const security__default = /*#__PURE__*/_interopDefaultCompat(security);
23
33
  const unicorn__default = /*#__PURE__*/_interopDefaultCompat(unicorn);
24
34
 
25
- const base$6 = {
35
+ const base$8 = {
26
36
  rules: {
27
37
  "array-callback-return": ["error", { allowImplicit: true }],
28
38
  "block-scoped-var": "error",
@@ -269,13 +279,123 @@ const cjs = {
269
279
  strict: ["error", "global"]
270
280
  }
271
281
  };
272
- const configs$6 = {
273
- base: base$6,
282
+ const configs$8 = {
283
+ base: base$8,
274
284
  cjsAndEsm,
275
285
  cjs
276
286
  };
277
287
 
278
- const base$5 = {
288
+ const requireExtensionRule = {
289
+ meta: {
290
+ type: "suggestion",
291
+ docs: {
292
+ description: "Ensure import and export statements include a file extension",
293
+ category: "Best Practices",
294
+ recommended: true
295
+ },
296
+ fixable: "code",
297
+ schema: []
298
+ },
299
+ create(context) {
300
+ function checkSource(source) {
301
+ const importPath = source.value;
302
+ if (!importPath || !importPath.startsWith(".") || importPath.endsWith(".js")) {
303
+ return;
304
+ }
305
+ const resolvedPath = path.resolve(path.dirname(context.filename), importPath);
306
+ if (!fs.existsSync(resolvedPath)) {
307
+ context.report({
308
+ node: source,
309
+ message: "Relative imports and exports must include a file extension.",
310
+ fix(fixer) {
311
+ const fixedPath = `${importPath}.js`;
312
+ return fixer.replaceText(source, `'${fixedPath}'`);
313
+ }
314
+ });
315
+ }
316
+ }
317
+ return {
318
+ ImportDeclaration(node) {
319
+ checkSource(node.source);
320
+ },
321
+ ExportNamedDeclaration(node) {
322
+ if (node.source) {
323
+ checkSource(node.source);
324
+ }
325
+ },
326
+ ExportAllDeclaration(node) {
327
+ checkSource(node.source);
328
+ }
329
+ };
330
+ }
331
+ };
332
+
333
+ const requireIndexRule = {
334
+ meta: {
335
+ type: "suggestion",
336
+ docs: {
337
+ description: "Ensure directory import and export statements use index.js",
338
+ category: "Best Practices",
339
+ recommended: true
340
+ },
341
+ fixable: "code",
342
+ schema: []
343
+ },
344
+ create(context) {
345
+ function checkSource(source) {
346
+ const importPath = source.value;
347
+ const resolvedPath = path.resolve(path.dirname(context.filename), importPath);
348
+ const isDirectory = fs.existsSync(resolvedPath) && fs.lstatSync(resolvedPath).isDirectory();
349
+ if (isDirectory) {
350
+ context.report({
351
+ node: source,
352
+ message: "Directory imports and exports must use index.js.",
353
+ fix(fixer) {
354
+ const fixedPath = importPath.replace(/\/?$/, "/index.js");
355
+ return fixer.replaceText(source, `'${fixedPath}'`);
356
+ }
357
+ });
358
+ }
359
+ }
360
+ return {
361
+ ImportDeclaration(node) {
362
+ checkSource(node.source);
363
+ },
364
+ ExportNamedDeclaration(node) {
365
+ if (node.source) {
366
+ checkSource(node.source);
367
+ }
368
+ },
369
+ ExportAllDeclaration(node) {
370
+ checkSource(node.source);
371
+ }
372
+ };
373
+ }
374
+ };
375
+
376
+ const base$7 = {
377
+ plugins: {
378
+ "decent-extension": {
379
+ meta: {
380
+ name: "decent-extension",
381
+ version: "1.0.0"
382
+ },
383
+ rules: {
384
+ "require-extension": requireExtensionRule,
385
+ "require-index": requireIndexRule
386
+ }
387
+ }
388
+ },
389
+ rules: {
390
+ "decent-extension/require-extension": "error",
391
+ "decent-extension/require-index": "error"
392
+ }
393
+ };
394
+ const configs$7 = {
395
+ base: base$7
396
+ };
397
+
398
+ const base$6 = {
279
399
  settings: {
280
400
  jsdoc: {
281
401
  preferredTypes: {
@@ -300,11 +420,11 @@ const base$5 = {
300
420
  "unicorn/prefer-set-has": "error"
301
421
  }
302
422
  };
303
- const configs$5 = {
304
- base: base$5
423
+ const configs$6 = {
424
+ base: base$6
305
425
  };
306
426
 
307
- const base$4 = {
427
+ const base$5 = {
308
428
  plugins: {
309
429
  mocha: mocha__default
310
430
  },
@@ -322,11 +442,11 @@ const base$4 = {
322
442
  "mocha/no-mocha-arrows": "off"
323
443
  }
324
444
  };
325
- const configs$4 = {
326
- base: base$4
445
+ const configs$5 = {
446
+ base: base$5
327
447
  };
328
448
 
329
- const base$3 = {
449
+ const base$4 = {
330
450
  plugins: {
331
451
  promise: promise__default
332
452
  },
@@ -343,6 +463,70 @@ const base$3 = {
343
463
  "promise/param-names": "error"
344
464
  }
345
465
  };
466
+ const configs$4 = {
467
+ base: base$4
468
+ };
469
+
470
+ const base$3 = {
471
+ plugins: {
472
+ "jsx-a11y": a11y__default,
473
+ react: react__default,
474
+ "react-hooks": reactHooks__default,
475
+ "testing-library": testingLibrary__default
476
+ },
477
+ rules: {
478
+ ...a11y__default.configs.recommended.rules,
479
+ "jsx-a11y/aria-proptypes": "error",
480
+ "jsx-a11y/aria-role": ["error", { ignoreNonDOM: true }],
481
+ "jsx-a11y/aria-unsupported-elements": "error",
482
+ "jsx-a11y/role-has-required-aria-props": "error",
483
+ ...react__default.configs.recommended.rules,
484
+ "react/default-props-match-prop-types": "error",
485
+ "react/display-name": ["error", { ignoreTranspilerName: false }],
486
+ "react/forbid-foreign-prop-types": ["error", { allowInPropTypes: true }],
487
+ "react/iframe-missing-sandbox": "warn",
488
+ "react/jsx-closing-bracket-location": "error",
489
+ "react/jsx-fragments": "error",
490
+ "react/jsx-no-leaked-render": ["error", { validStrategies: ["ternary"] }],
491
+ "react/jsx-no-script-url": "error",
492
+ "react/jsx-no-target-blank": "error",
493
+ "react/jsx-no-undef": "error",
494
+ "react/jsx-no-useless-fragment": "error",
495
+ "react/jsx-pascal-case": [
496
+ "error",
497
+ {
498
+ allowAllCaps: true,
499
+ ignore: []
500
+ }
501
+ ],
502
+ "react/jsx-props-no-multi-spaces": "error",
503
+ "react/no-access-state-in-setstate": "error",
504
+ "react/no-arrow-function-lifecycle": "error",
505
+ "react/no-danger-with-children": "error",
506
+ "react/no-deprecated": "error",
507
+ "react/no-did-mount-set-state": "error",
508
+ "react/no-did-update-set-state": "error",
509
+ "react/no-direct-mutation-state": "error",
510
+ "react/no-invalid-html-attribute": "error",
511
+ "react/no-namespace": "error",
512
+ "react/no-redundant-should-component-update": "error",
513
+ "react/no-this-in-sfc": "error",
514
+ "react/no-typos": "error",
515
+ "react/no-unsafe": "error",
516
+ "react/no-unstable-nested-components": "error",
517
+ "react/no-unused-class-component-methods": "error",
518
+ "react/no-unused-prop-types": "error",
519
+ "react/no-unused-state": "error",
520
+ "react/no-will-update-set-state": "error",
521
+ "react/prefer-stateless-function": "error",
522
+ "react/require-render-return": "error",
523
+ "react/self-closing-comp": "error",
524
+ "react/style-prop-object": "error",
525
+ "react-hooks/rules-of-hooks": "error",
526
+ "react-hooks/exhaustive-deps": "error",
527
+ ...testingLibrary__default.configs.react.rules
528
+ }
529
+ };
346
530
  const configs$3 = {
347
531
  base: base$3
348
532
  };
@@ -461,7 +645,8 @@ const configs = {
461
645
  base
462
646
  };
463
647
 
464
- function defaultConfig(parserOptions) {
648
+ function defaultConfig(options) {
649
+ const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true;
465
650
  const languageOptions = {
466
651
  globals: {
467
652
  ...globals__default.node
@@ -473,7 +658,7 @@ function defaultConfig(parserOptions) {
473
658
  defaultProject: "tsconfig.json"
474
659
  },
475
660
  tsconfigRootDir: undefined,
476
- ...parserOptions
661
+ ...options?.parserOptions
477
662
  }
478
663
  };
479
664
  return [
@@ -486,21 +671,23 @@ function defaultConfig(parserOptions) {
486
671
  {
487
672
  languageOptions,
488
673
  settings: {
489
- ...configs$5.base.settings
674
+ ...configs$6.base.settings
490
675
  }
491
676
  },
492
677
  {
493
- files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs"],
678
+ files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"],
494
679
  plugins: {
495
- ...configs$5.base.plugins,
496
- ...configs$3.base.plugins,
680
+ ...configs$7.base.plugins,
681
+ ...configs$6.base.plugins,
682
+ ...configs$4.base.plugins,
497
683
  ...configs$2.base.plugins,
498
684
  ...configs.base.plugins
499
685
  },
500
686
  rules: {
687
+ ...configs$8.base.rules,
688
+ ...enableRequireExtensionRule ? configs$7.base.rules : {},
501
689
  ...configs$6.base.rules,
502
- ...configs$5.base.rules,
503
- ...configs$3.base.rules,
690
+ ...configs$4.base.rules,
504
691
  ...configs$2.base.rules,
505
692
  ...configs.base.rules
506
693
  }
@@ -510,31 +697,36 @@ function defaultConfig(parserOptions) {
510
697
  languageOptions: {
511
698
  sourceType: "script"
512
699
  },
513
- ...configs$6.cjsAndEsm
700
+ ...configs$8.cjsAndEsm
514
701
  },
515
702
  {
516
703
  files: ["**/*.js", "**/*.cjs"],
517
704
  languageOptions: {
518
705
  sourceType: "script"
519
706
  },
520
- ...configs$6.cjs
707
+ ...configs$8.cjs
521
708
  },
522
709
  {
523
- files: ["**/*.ts"],
710
+ files: ["**/*.ts", "**/*.tsx"],
524
711
  ...configs$1.base
525
712
  },
713
+ {
714
+ files: ["**/*.tsx"],
715
+ ...configs$3.base
716
+ },
526
717
  {
527
718
  files: ["**/*.tests.ts", "tests/tests.ts"],
528
- ...configs$4.base
719
+ ...configs$5.base
529
720
  },
530
721
  prettier__default
531
722
  ];
532
723
  }
533
724
 
534
725
  exports.defaultConfig = defaultConfig;
535
- exports.eslintConfigs = configs$6;
536
- exports.jsdocConfigs = configs$5;
537
- exports.promiseConfigs = configs$3;
726
+ exports.eslintConfigs = configs$8;
727
+ exports.jsdocConfigs = configs$6;
728
+ exports.promiseConfigs = configs$4;
729
+ exports.reactConfigs = configs$3;
538
730
  exports.securityConfigs = configs$2;
539
731
  exports.typescriptEslintConfigs = configs$1;
540
732
  exports.unicornConfigs = configs;
package/dist/index.d.cts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { ConfigWithExtends } from 'typescript-eslint';
2
2
 
3
- declare const configs$5: {
3
+ declare const configs$6: {
4
4
  base: ConfigWithExtends;
5
5
  cjsAndEsm: ConfigWithExtends;
6
6
  cjs: ConfigWithExtends;
7
7
  };
8
8
 
9
+ declare const configs$5: {
10
+ base: ConfigWithExtends;
11
+ };
12
+
9
13
  declare const configs$4: {
10
14
  base: ConfigWithExtends;
11
15
  };
@@ -26,6 +30,10 @@ declare const configs: {
26
30
  base: ConfigWithExtends;
27
31
  };
28
32
 
29
- declare function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions']): ConfigWithExtends[];
33
+ interface DefaultConfigOptions {
34
+ parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions'];
35
+ enableRequireExtensionRule?: boolean;
36
+ }
37
+ declare function defaultConfig(options?: DefaultConfigOptions): ConfigWithExtends[];
30
38
 
31
- export { defaultConfig, configs$5 as eslintConfigs, configs$4 as jsdocConfigs, configs$3 as promiseConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
39
+ export { type DefaultConfigOptions, defaultConfig, configs$6 as eslintConfigs, configs$5 as jsdocConfigs, configs$4 as promiseConfigs, configs$3 as reactConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
package/dist/index.d.mts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { ConfigWithExtends } from 'typescript-eslint';
2
2
 
3
- declare const configs$5: {
3
+ declare const configs$6: {
4
4
  base: ConfigWithExtends;
5
5
  cjsAndEsm: ConfigWithExtends;
6
6
  cjs: ConfigWithExtends;
7
7
  };
8
8
 
9
+ declare const configs$5: {
10
+ base: ConfigWithExtends;
11
+ };
12
+
9
13
  declare const configs$4: {
10
14
  base: ConfigWithExtends;
11
15
  };
@@ -26,6 +30,10 @@ declare const configs: {
26
30
  base: ConfigWithExtends;
27
31
  };
28
32
 
29
- declare function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions']): ConfigWithExtends[];
33
+ interface DefaultConfigOptions {
34
+ parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions'];
35
+ enableRequireExtensionRule?: boolean;
36
+ }
37
+ declare function defaultConfig(options?: DefaultConfigOptions): ConfigWithExtends[];
30
38
 
31
- export { defaultConfig, configs$5 as eslintConfigs, configs$4 as jsdocConfigs, configs$3 as promiseConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
39
+ export { type DefaultConfigOptions, defaultConfig, configs$6 as eslintConfigs, configs$5 as jsdocConfigs, configs$4 as promiseConfigs, configs$3 as reactConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
package/dist/index.d.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import { ConfigWithExtends } from 'typescript-eslint';
2
2
 
3
- declare const configs$5: {
3
+ declare const configs$6: {
4
4
  base: ConfigWithExtends;
5
5
  cjsAndEsm: ConfigWithExtends;
6
6
  cjs: ConfigWithExtends;
7
7
  };
8
8
 
9
+ declare const configs$5: {
10
+ base: ConfigWithExtends;
11
+ };
12
+
9
13
  declare const configs$4: {
10
14
  base: ConfigWithExtends;
11
15
  };
@@ -26,6 +30,10 @@ declare const configs: {
26
30
  base: ConfigWithExtends;
27
31
  };
28
32
 
29
- declare function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions']): ConfigWithExtends[];
33
+ interface DefaultConfigOptions {
34
+ parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions'];
35
+ enableRequireExtensionRule?: boolean;
36
+ }
37
+ declare function defaultConfig(options?: DefaultConfigOptions): ConfigWithExtends[];
30
38
 
31
- export { defaultConfig, configs$5 as eslintConfigs, configs$4 as jsdocConfigs, configs$3 as promiseConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
39
+ export { type DefaultConfigOptions, defaultConfig, configs$6 as eslintConfigs, configs$5 as jsdocConfigs, configs$4 as promiseConfigs, configs$3 as reactConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
package/dist/index.mjs CHANGED
@@ -2,13 +2,19 @@ import eslint from '@eslint/js';
2
2
  import globals from 'globals';
3
3
  import tsEslint from 'typescript-eslint';
4
4
  import prettier from 'eslint-plugin-prettier/recommended';
5
+ import { existsSync, lstatSync } from 'fs';
6
+ import { resolve, dirname } from 'path';
5
7
  import jsdoc from 'eslint-plugin-jsdoc';
6
8
  import mocha from 'eslint-plugin-mocha';
7
9
  import promise from 'eslint-plugin-promise';
10
+ import a11y from 'eslint-plugin-jsx-a11y';
11
+ import react from 'eslint-plugin-react';
12
+ import reactHooks from 'eslint-plugin-react-hooks';
13
+ import testingLibrary from 'eslint-plugin-testing-library';
8
14
  import security from 'eslint-plugin-security';
9
15
  import unicorn from 'eslint-plugin-unicorn';
10
16
 
11
- const base$6 = {
17
+ const base$8 = {
12
18
  rules: {
13
19
  "array-callback-return": ["error", { allowImplicit: true }],
14
20
  "block-scoped-var": "error",
@@ -255,13 +261,123 @@ const cjs = {
255
261
  strict: ["error", "global"]
256
262
  }
257
263
  };
258
- const configs$6 = {
259
- base: base$6,
264
+ const configs$8 = {
265
+ base: base$8,
260
266
  cjsAndEsm,
261
267
  cjs
262
268
  };
263
269
 
264
- const base$5 = {
270
+ const requireExtensionRule = {
271
+ meta: {
272
+ type: "suggestion",
273
+ docs: {
274
+ description: "Ensure import and export statements include a file extension",
275
+ category: "Best Practices",
276
+ recommended: true
277
+ },
278
+ fixable: "code",
279
+ schema: []
280
+ },
281
+ create(context) {
282
+ function checkSource(source) {
283
+ const importPath = source.value;
284
+ if (!importPath || !importPath.startsWith(".") || importPath.endsWith(".js")) {
285
+ return;
286
+ }
287
+ const resolvedPath = resolve(dirname(context.filename), importPath);
288
+ if (!existsSync(resolvedPath)) {
289
+ context.report({
290
+ node: source,
291
+ message: "Relative imports and exports must include a file extension.",
292
+ fix(fixer) {
293
+ const fixedPath = `${importPath}.js`;
294
+ return fixer.replaceText(source, `'${fixedPath}'`);
295
+ }
296
+ });
297
+ }
298
+ }
299
+ return {
300
+ ImportDeclaration(node) {
301
+ checkSource(node.source);
302
+ },
303
+ ExportNamedDeclaration(node) {
304
+ if (node.source) {
305
+ checkSource(node.source);
306
+ }
307
+ },
308
+ ExportAllDeclaration(node) {
309
+ checkSource(node.source);
310
+ }
311
+ };
312
+ }
313
+ };
314
+
315
+ const requireIndexRule = {
316
+ meta: {
317
+ type: "suggestion",
318
+ docs: {
319
+ description: "Ensure directory import and export statements use index.js",
320
+ category: "Best Practices",
321
+ recommended: true
322
+ },
323
+ fixable: "code",
324
+ schema: []
325
+ },
326
+ create(context) {
327
+ function checkSource(source) {
328
+ const importPath = source.value;
329
+ const resolvedPath = resolve(dirname(context.filename), importPath);
330
+ const isDirectory = existsSync(resolvedPath) && lstatSync(resolvedPath).isDirectory();
331
+ if (isDirectory) {
332
+ context.report({
333
+ node: source,
334
+ message: "Directory imports and exports must use index.js.",
335
+ fix(fixer) {
336
+ const fixedPath = importPath.replace(/\/?$/, "/index.js");
337
+ return fixer.replaceText(source, `'${fixedPath}'`);
338
+ }
339
+ });
340
+ }
341
+ }
342
+ return {
343
+ ImportDeclaration(node) {
344
+ checkSource(node.source);
345
+ },
346
+ ExportNamedDeclaration(node) {
347
+ if (node.source) {
348
+ checkSource(node.source);
349
+ }
350
+ },
351
+ ExportAllDeclaration(node) {
352
+ checkSource(node.source);
353
+ }
354
+ };
355
+ }
356
+ };
357
+
358
+ const base$7 = {
359
+ plugins: {
360
+ "decent-extension": {
361
+ meta: {
362
+ name: "decent-extension",
363
+ version: "1.0.0"
364
+ },
365
+ rules: {
366
+ "require-extension": requireExtensionRule,
367
+ "require-index": requireIndexRule
368
+ }
369
+ }
370
+ },
371
+ rules: {
372
+ "decent-extension/require-extension": "error",
373
+ "decent-extension/require-index": "error"
374
+ }
375
+ };
376
+ const configs$7 = {
377
+ base: base$7
378
+ };
379
+
380
+ const base$6 = {
265
381
  settings: {
266
382
  jsdoc: {
267
383
  preferredTypes: {
@@ -286,11 +402,11 @@ const base$5 = {
286
402
  "unicorn/prefer-set-has": "error"
287
403
  }
288
404
  };
289
- const configs$5 = {
290
- base: base$5
405
+ const configs$6 = {
406
+ base: base$6
291
407
  };
292
408
 
293
- const base$4 = {
409
+ const base$5 = {
294
410
  plugins: {
295
411
  mocha
296
412
  },
@@ -308,11 +424,11 @@ const base$4 = {
308
424
  "mocha/no-mocha-arrows": "off"
309
425
  }
310
426
  };
311
- const configs$4 = {
312
- base: base$4
427
+ const configs$5 = {
428
+ base: base$5
313
429
  };
314
430
 
315
- const base$3 = {
431
+ const base$4 = {
316
432
  plugins: {
317
433
  promise
318
434
  },
@@ -329,6 +445,70 @@ const base$3 = {
329
445
  "promise/param-names": "error"
330
446
  }
331
447
  };
448
+ const configs$4 = {
449
+ base: base$4
450
+ };
451
+
452
+ const base$3 = {
453
+ plugins: {
454
+ "jsx-a11y": a11y,
455
+ react,
456
+ "react-hooks": reactHooks,
457
+ "testing-library": testingLibrary
458
+ },
459
+ rules: {
460
+ ...a11y.configs.recommended.rules,
461
+ "jsx-a11y/aria-proptypes": "error",
462
+ "jsx-a11y/aria-role": ["error", { ignoreNonDOM: true }],
463
+ "jsx-a11y/aria-unsupported-elements": "error",
464
+ "jsx-a11y/role-has-required-aria-props": "error",
465
+ ...react.configs.recommended.rules,
466
+ "react/default-props-match-prop-types": "error",
467
+ "react/display-name": ["error", { ignoreTranspilerName: false }],
468
+ "react/forbid-foreign-prop-types": ["error", { allowInPropTypes: true }],
469
+ "react/iframe-missing-sandbox": "warn",
470
+ "react/jsx-closing-bracket-location": "error",
471
+ "react/jsx-fragments": "error",
472
+ "react/jsx-no-leaked-render": ["error", { validStrategies: ["ternary"] }],
473
+ "react/jsx-no-script-url": "error",
474
+ "react/jsx-no-target-blank": "error",
475
+ "react/jsx-no-undef": "error",
476
+ "react/jsx-no-useless-fragment": "error",
477
+ "react/jsx-pascal-case": [
478
+ "error",
479
+ {
480
+ allowAllCaps: true,
481
+ ignore: []
482
+ }
483
+ ],
484
+ "react/jsx-props-no-multi-spaces": "error",
485
+ "react/no-access-state-in-setstate": "error",
486
+ "react/no-arrow-function-lifecycle": "error",
487
+ "react/no-danger-with-children": "error",
488
+ "react/no-deprecated": "error",
489
+ "react/no-did-mount-set-state": "error",
490
+ "react/no-did-update-set-state": "error",
491
+ "react/no-direct-mutation-state": "error",
492
+ "react/no-invalid-html-attribute": "error",
493
+ "react/no-namespace": "error",
494
+ "react/no-redundant-should-component-update": "error",
495
+ "react/no-this-in-sfc": "error",
496
+ "react/no-typos": "error",
497
+ "react/no-unsafe": "error",
498
+ "react/no-unstable-nested-components": "error",
499
+ "react/no-unused-class-component-methods": "error",
500
+ "react/no-unused-prop-types": "error",
501
+ "react/no-unused-state": "error",
502
+ "react/no-will-update-set-state": "error",
503
+ "react/prefer-stateless-function": "error",
504
+ "react/require-render-return": "error",
505
+ "react/self-closing-comp": "error",
506
+ "react/style-prop-object": "error",
507
+ "react-hooks/rules-of-hooks": "error",
508
+ "react-hooks/exhaustive-deps": "error",
509
+ ...testingLibrary.configs.react.rules
510
+ }
511
+ };
332
512
  const configs$3 = {
333
513
  base: base$3
334
514
  };
@@ -447,7 +627,8 @@ const configs = {
447
627
  base
448
628
  };
449
629
 
450
- function defaultConfig(parserOptions) {
630
+ function defaultConfig(options) {
631
+ const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true;
451
632
  const languageOptions = {
452
633
  globals: {
453
634
  ...globals.node
@@ -459,7 +640,7 @@ function defaultConfig(parserOptions) {
459
640
  defaultProject: "tsconfig.json"
460
641
  },
461
642
  tsconfigRootDir: import.meta.dirname,
462
- ...parserOptions
643
+ ...options?.parserOptions
463
644
  }
464
645
  };
465
646
  return [
@@ -472,21 +653,23 @@ function defaultConfig(parserOptions) {
472
653
  {
473
654
  languageOptions,
474
655
  settings: {
475
- ...configs$5.base.settings
656
+ ...configs$6.base.settings
476
657
  }
477
658
  },
478
659
  {
479
- files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs"],
660
+ files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"],
480
661
  plugins: {
481
- ...configs$5.base.plugins,
482
- ...configs$3.base.plugins,
662
+ ...configs$7.base.plugins,
663
+ ...configs$6.base.plugins,
664
+ ...configs$4.base.plugins,
483
665
  ...configs$2.base.plugins,
484
666
  ...configs.base.plugins
485
667
  },
486
668
  rules: {
669
+ ...configs$8.base.rules,
670
+ ...enableRequireExtensionRule ? configs$7.base.rules : {},
487
671
  ...configs$6.base.rules,
488
- ...configs$5.base.rules,
489
- ...configs$3.base.rules,
672
+ ...configs$4.base.rules,
490
673
  ...configs$2.base.rules,
491
674
  ...configs.base.rules
492
675
  }
@@ -496,25 +679,29 @@ function defaultConfig(parserOptions) {
496
679
  languageOptions: {
497
680
  sourceType: "script"
498
681
  },
499
- ...configs$6.cjsAndEsm
682
+ ...configs$8.cjsAndEsm
500
683
  },
501
684
  {
502
685
  files: ["**/*.js", "**/*.cjs"],
503
686
  languageOptions: {
504
687
  sourceType: "script"
505
688
  },
506
- ...configs$6.cjs
689
+ ...configs$8.cjs
507
690
  },
508
691
  {
509
- files: ["**/*.ts"],
692
+ files: ["**/*.ts", "**/*.tsx"],
510
693
  ...configs$1.base
511
694
  },
695
+ {
696
+ files: ["**/*.tsx"],
697
+ ...configs$3.base
698
+ },
512
699
  {
513
700
  files: ["**/*.tests.ts", "tests/tests.ts"],
514
- ...configs$4.base
701
+ ...configs$5.base
515
702
  },
516
703
  prettier
517
704
  ];
518
705
  }
519
706
 
520
- export { defaultConfig, configs$6 as eslintConfigs, configs$5 as jsdocConfigs, configs$3 as promiseConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
707
+ export { defaultConfig, configs$8 as eslintConfigs, configs$6 as jsdocConfigs, configs$4 as promiseConfigs, configs$3 as reactConfigs, configs$2 as securityConfigs, configs$1 as typescriptEslintConfigs, configs as unicornConfigs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-config-decent",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "A decent ESLint configuration",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -78,7 +78,11 @@
78
78
  "eslint-plugin-mocha": "^10.4.3",
79
79
  "eslint-plugin-prettier": "^5.1.3",
80
80
  "eslint-plugin-promise": "^6.2.0",
81
+ "eslint-plugin-jsx-a11y": "^6.8.0",
82
+ "eslint-plugin-react": "^7.34.2",
83
+ "eslint-plugin-react-hooks": "^4.6.2",
81
84
  "eslint-plugin-security": "^3.0.0",
85
+ "eslint-plugin-testing-library": "^6.2.2",
82
86
  "eslint-plugin-unicorn": "^53.0.0",
83
87
  "globals": "^15.3.0",
84
88
  "typescript-eslint": "^8.0.0-alpha.25"
@@ -96,5 +100,19 @@
96
100
  "rimraf": "^5.0.7",
97
101
  "typescript": "^5.4.5",
98
102
  "unbuild": "2.0.0"
103
+ },
104
+ "overrides": {
105
+ "eslint-plugin-jsx-a11y": {
106
+ "eslint": "$eslint"
107
+ },
108
+ "eslint-plugin-react": {
109
+ "eslint": "$eslint"
110
+ },
111
+ "eslint-plugin-react-hooks": {
112
+ "eslint": "$eslint"
113
+ },
114
+ "eslint-plugin-testing-library": {
115
+ "eslint": "$eslint"
116
+ }
99
117
  }
100
118
  }
@@ -0,0 +1,30 @@
1
+ import type { ConfigWithExtends } from 'typescript-eslint';
2
+ import { requireExtensionRule } from './rules/requireExtensionRule.js';
3
+ import { requireIndexRule } from './rules/requireIndexRule.js';
4
+
5
+ const base: ConfigWithExtends = {
6
+ plugins: {
7
+ 'decent-extension': {
8
+ meta: {
9
+ name: 'decent-extension',
10
+ version: '1.0.0',
11
+ },
12
+ rules: {
13
+ 'require-extension': requireExtensionRule,
14
+ 'require-index': requireIndexRule,
15
+ },
16
+ },
17
+ },
18
+ rules: {
19
+ 'decent-extension/require-extension': 'error',
20
+ 'decent-extension/require-index': 'error',
21
+ },
22
+ };
23
+
24
+ export const configs = {
25
+ base,
26
+ };
27
+
28
+ export default {
29
+ configs,
30
+ };
package/src/index.ts CHANGED
@@ -3,9 +3,11 @@ import globals from 'globals';
3
3
  import tsEslint, { type ConfigWithExtends } from 'typescript-eslint';
4
4
  import prettier from 'eslint-plugin-prettier/recommended';
5
5
  import { configs as eslintConfigs } from './eslint.js';
6
+ import { configs as extensionConfigs } from './extension.js';
6
7
  import { configs as jsdocConfigs } from './jsdoc.js';
7
8
  import { configs as mochaConfigs } from './mocha.js';
8
9
  import { configs as promiseConfigs } from './promise.js';
10
+ import { configs as reactConfigs } from './react.js';
9
11
  import { configs as securityConfigs } from './security.js';
10
12
  import { configs as typescriptEslintConfigs } from './typescriptEslint.js';
11
13
  import { configs as unicornConfigs } from './unicorn.js';
@@ -14,12 +16,19 @@ export {
14
16
  eslintConfigs, //
15
17
  jsdocConfigs,
16
18
  promiseConfigs,
19
+ reactConfigs,
17
20
  securityConfigs,
18
21
  typescriptEslintConfigs,
19
22
  unicornConfigs,
20
23
  };
21
24
 
22
- export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions']): ConfigWithExtends[] {
25
+ export interface DefaultConfigOptions {
26
+ parserOptions?: NonNullable<ConfigWithExtends['languageOptions']>['parserOptions'];
27
+ enableRequireExtensionRule?: boolean;
28
+ }
29
+
30
+ export function defaultConfig(options?: DefaultConfigOptions): ConfigWithExtends[] {
31
+ const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true;
23
32
  const languageOptions: ConfigWithExtends['languageOptions'] = {
24
33
  globals: {
25
34
  ...globals.node,
@@ -31,7 +40,7 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
31
40
  defaultProject: 'tsconfig.json',
32
41
  },
33
42
  tsconfigRootDir: import.meta.dirname,
34
- ...parserOptions,
43
+ ...options?.parserOptions,
35
44
  },
36
45
  };
37
46
 
@@ -49,8 +58,9 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
49
58
  },
50
59
  },
51
60
  {
52
- files: ['**/*.ts', '**/*.js', '**/*.cjs', '**/*.mjs'],
61
+ files: ['**/*.ts', '**/*.js', '**/*.cjs', '**/*.mjs', '**/*.tsx'],
53
62
  plugins: {
63
+ ...extensionConfigs.base.plugins,
54
64
  ...jsdocConfigs.base.plugins,
55
65
  ...promiseConfigs.base.plugins,
56
66
  ...securityConfigs.base.plugins,
@@ -58,6 +68,7 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
58
68
  },
59
69
  rules: {
60
70
  ...eslintConfigs.base.rules,
71
+ ...(enableRequireExtensionRule ? extensionConfigs.base.rules : {}),
61
72
  ...jsdocConfigs.base.rules,
62
73
  ...promiseConfigs.base.rules,
63
74
  ...securityConfigs.base.rules,
@@ -79,9 +90,13 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
79
90
  ...eslintConfigs.cjs,
80
91
  },
81
92
  {
82
- files: ['**/*.ts'],
93
+ files: ['**/*.ts', '**/*.tsx'],
83
94
  ...typescriptEslintConfigs.base,
84
95
  },
96
+ {
97
+ files: ['**/*.tsx'],
98
+ ...reactConfigs.base,
99
+ },
85
100
  {
86
101
  files: ['**/*.tests.ts', 'tests/tests.ts'],
87
102
 
package/src/react.ts ADDED
@@ -0,0 +1,77 @@
1
+ import a11y from 'eslint-plugin-jsx-a11y';
2
+ import react from 'eslint-plugin-react';
3
+ import reactHooks from 'eslint-plugin-react-hooks';
4
+ import testingLibrary from 'eslint-plugin-testing-library';
5
+ import type { ConfigWithExtends } from 'typescript-eslint';
6
+
7
+ const base: ConfigWithExtends = {
8
+ plugins: {
9
+ 'jsx-a11y': a11y,
10
+ react,
11
+ 'react-hooks': reactHooks,
12
+ 'testing-library': testingLibrary,
13
+ },
14
+ rules: {
15
+ ...a11y.configs.recommended.rules,
16
+ 'jsx-a11y/aria-proptypes': 'error',
17
+ 'jsx-a11y/aria-role': ['error', { ignoreNonDOM: true }],
18
+ 'jsx-a11y/aria-unsupported-elements': 'error',
19
+ 'jsx-a11y/role-has-required-aria-props': 'error',
20
+
21
+ ...react.configs.recommended.rules,
22
+ 'react/default-props-match-prop-types': 'error',
23
+ 'react/display-name': ['error', { ignoreTranspilerName: false }],
24
+ 'react/forbid-foreign-prop-types': ['error', { allowInPropTypes: true }],
25
+ 'react/iframe-missing-sandbox': 'warn',
26
+ 'react/jsx-closing-bracket-location': 'error',
27
+ 'react/jsx-fragments': 'error',
28
+ 'react/jsx-no-leaked-render': ['error', { validStrategies: ['ternary'] }],
29
+ 'react/jsx-no-script-url': 'error',
30
+ 'react/jsx-no-target-blank': 'error',
31
+ 'react/jsx-no-undef': 'error',
32
+ 'react/jsx-no-useless-fragment': 'error',
33
+ 'react/jsx-pascal-case': [
34
+ 'error',
35
+ {
36
+ allowAllCaps: true,
37
+ ignore: [],
38
+ },
39
+ ],
40
+ 'react/jsx-props-no-multi-spaces': 'error',
41
+ 'react/no-access-state-in-setstate': 'error',
42
+ 'react/no-arrow-function-lifecycle': 'error',
43
+ 'react/no-danger-with-children': 'error',
44
+ 'react/no-deprecated': 'error',
45
+ 'react/no-did-mount-set-state': 'error',
46
+ 'react/no-did-update-set-state': 'error',
47
+ 'react/no-direct-mutation-state': 'error',
48
+ 'react/no-invalid-html-attribute': 'error',
49
+ 'react/no-namespace': 'error',
50
+ 'react/no-redundant-should-component-update': 'error',
51
+ 'react/no-this-in-sfc': 'error',
52
+ 'react/no-typos': 'error',
53
+ 'react/no-unsafe': 'error',
54
+ 'react/no-unstable-nested-components': 'error',
55
+ 'react/no-unused-class-component-methods': 'error',
56
+ 'react/no-unused-prop-types': 'error',
57
+ 'react/no-unused-state': 'error',
58
+ 'react/no-will-update-set-state': 'error',
59
+ 'react/prefer-stateless-function': 'error',
60
+ 'react/require-render-return': 'error',
61
+ 'react/self-closing-comp': 'error',
62
+ 'react/style-prop-object': 'error',
63
+
64
+ 'react-hooks/rules-of-hooks': 'error',
65
+ 'react-hooks/exhaustive-deps': 'error',
66
+
67
+ ...testingLibrary.configs.react.rules,
68
+ },
69
+ };
70
+
71
+ export const configs = {
72
+ base,
73
+ };
74
+
75
+ export default {
76
+ configs,
77
+ };
@@ -0,0 +1,54 @@
1
+ import { existsSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import type { Rule } from 'eslint';
4
+
5
+ export const requireExtensionRule: Rule.RuleModule = {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: {
9
+ description: 'Ensure import and export statements include a file extension',
10
+ category: 'Best Practices',
11
+ recommended: true,
12
+ },
13
+ fixable: 'code',
14
+ schema: [],
15
+ },
16
+ create(context: Rule.RuleContext) {
17
+ function checkSource(source: Parameters<NonNullable<Rule.NodeListener['ImportDeclaration']>>[0]['source']): void {
18
+ const importPath = source.value as string;
19
+
20
+ if (!importPath || !importPath.startsWith('.') || importPath.endsWith('.js')) {
21
+ return;
22
+ }
23
+
24
+ const resolvedPath = resolve(dirname(context.filename), importPath);
25
+
26
+ // If the import/export path doesn't end with a file extension, report an error
27
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
28
+ if (!existsSync(resolvedPath)) {
29
+ context.report({
30
+ node: source,
31
+ message: 'Relative imports and exports must include a file extension.',
32
+ fix(fixer) {
33
+ const fixedPath = `${importPath}.js`;
34
+ return fixer.replaceText(source, `'${fixedPath}'`);
35
+ },
36
+ });
37
+ }
38
+ }
39
+
40
+ return {
41
+ ImportDeclaration(node: Parameters<NonNullable<Rule.NodeListener['ImportDeclaration']>>[0]): void {
42
+ checkSource(node.source);
43
+ },
44
+ ExportNamedDeclaration(node: Parameters<NonNullable<Rule.NodeListener['ExportNamedDeclaration']>>[0]): void {
45
+ if (node.source) {
46
+ checkSource(node.source);
47
+ }
48
+ },
49
+ ExportAllDeclaration(node: Parameters<NonNullable<Rule.NodeListener['ExportAllDeclaration']>>[0]): void {
50
+ checkSource(node.source);
51
+ },
52
+ };
53
+ },
54
+ };
@@ -0,0 +1,51 @@
1
+ import { existsSync, lstatSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+ import type { Rule } from 'eslint';
4
+
5
+ export const requireIndexRule: Rule.RuleModule = {
6
+ meta: {
7
+ type: 'suggestion',
8
+ docs: {
9
+ description: 'Ensure directory import and export statements use index.js',
10
+ category: 'Best Practices',
11
+ recommended: true,
12
+ },
13
+ fixable: 'code',
14
+ schema: [],
15
+ },
16
+ create(context: Rule.RuleContext) {
17
+ function checkSource(source: Parameters<NonNullable<Rule.NodeListener['ImportDeclaration']>>[0]['source']): void {
18
+ const importPath = source.value as string;
19
+
20
+ // Resolve the path relative to the file being linted
21
+ const resolvedPath = resolve(dirname(context.filename), importPath);
22
+
23
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
24
+ const isDirectory = existsSync(resolvedPath) && lstatSync(resolvedPath).isDirectory();
25
+ if (isDirectory) {
26
+ context.report({
27
+ node: source,
28
+ message: 'Directory imports and exports must use index.js.',
29
+ fix(fixer) {
30
+ const fixedPath = importPath.replace(/\/?$/, '/index.js');
31
+ return fixer.replaceText(source, `'${fixedPath}'`);
32
+ },
33
+ });
34
+ }
35
+ }
36
+
37
+ return {
38
+ ImportDeclaration(node: Parameters<NonNullable<Rule.NodeListener['ImportDeclaration']>>[0]): void {
39
+ checkSource(node.source);
40
+ },
41
+ ExportNamedDeclaration(node: Parameters<NonNullable<Rule.NodeListener['ExportNamedDeclaration']>>[0]): void {
42
+ if (node.source) {
43
+ checkSource(node.source);
44
+ }
45
+ },
46
+ ExportAllDeclaration(node: Parameters<NonNullable<Rule.NodeListener['ExportAllDeclaration']>>[0]): void {
47
+ checkSource(node.source);
48
+ },
49
+ };
50
+ },
51
+ };
@@ -0,0 +1,9 @@
1
+ declare module 'eslint-plugin-jsx-a11y' {
2
+ import type { ESLint, Linter } from 'eslint';
3
+ const value: ESLint.Plugin & {
4
+ configs: {
5
+ recommended: Linter.FlatConfig;
6
+ };
7
+ };
8
+ export default value;
9
+ }
@@ -0,0 +1,5 @@
1
+ declare module 'eslint-plugin-react-hooks' {
2
+ import type { ESLint } from 'eslint';
3
+ const value: ESLint.Plugin;
4
+ export default value;
5
+ }
@@ -0,0 +1,9 @@
1
+ declare module 'eslint-plugin-react' {
2
+ import type { ESLint, Linter } from 'eslint';
3
+ const value: ESLint.Plugin & {
4
+ configs: {
5
+ recommended: Linter.FlatConfig;
6
+ };
7
+ };
8
+ export default value;
9
+ }
@@ -0,0 +1,9 @@
1
+ declare module 'eslint-plugin-testing-library' {
2
+ import type { ESLint, Linter } from 'eslint';
3
+ const value: ESLint.Plugin & {
4
+ configs: {
5
+ react: Linter.FlatConfig;
6
+ };
7
+ };
8
+ export default value;
9
+ }