eslint-config-decent 1.1.0 → 1.2.1

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,6 +4,8 @@ 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');
@@ -30,7 +32,7 @@ const testingLibrary__default = /*#__PURE__*/_interopDefaultCompat(testingLibrar
30
32
  const security__default = /*#__PURE__*/_interopDefaultCompat(security);
31
33
  const unicorn__default = /*#__PURE__*/_interopDefaultCompat(unicorn);
32
34
 
33
- const base$7 = {
35
+ const base$8 = {
34
36
  rules: {
35
37
  "array-callback-return": ["error", { allowImplicit: true }],
36
38
  "block-scoped-var": "error",
@@ -277,12 +279,122 @@ const cjs = {
277
279
  strict: ["error", "global"]
278
280
  }
279
281
  };
280
- const configs$7 = {
281
- base: base$7,
282
+ const configs$8 = {
283
+ base: base$8,
282
284
  cjsAndEsm,
283
285
  cjs
284
286
  };
285
287
 
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
+
286
398
  const base$6 = {
287
399
  settings: {
288
400
  jsdoc: {
@@ -492,14 +604,6 @@ const base$1 = {
492
604
  "@typescript-eslint/no-empty-interface": "error",
493
605
  "@typescript-eslint/no-extra-semi": "error",
494
606
  "@typescript-eslint/no-shadow": "error",
495
- "@typescript-eslint/no-use-before-define": [
496
- "error",
497
- {
498
- functions: true,
499
- classes: true,
500
- variables: true
501
- }
502
- ],
503
607
  "@typescript-eslint/parameter-properties": [
504
608
  "error",
505
609
  {
@@ -508,7 +612,8 @@ const base$1 = {
508
612
  ],
509
613
  "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }],
510
614
  "@typescript-eslint/return-await": "error",
511
- "@typescript-eslint/sort-type-constituents": "error"
615
+ "@typescript-eslint/sort-type-constituents": "error",
616
+ "@typescript-eslint/use-unknown-in-catch-callback-variable": "off"
512
617
  }
513
618
  };
514
619
  const configs$1 = {
@@ -533,7 +638,8 @@ const configs = {
533
638
  base
534
639
  };
535
640
 
536
- function defaultConfig(parserOptions) {
641
+ function defaultConfig(options) {
642
+ const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true;
537
643
  const languageOptions = {
538
644
  globals: {
539
645
  ...globals__default.node
@@ -541,11 +647,11 @@ function defaultConfig(parserOptions) {
541
647
  parserOptions: {
542
648
  // @ts-expect-error - This is a valid option
543
649
  projectService: {
544
- allowedDefaultProject: ["./*.js", "./*.cjs", "./*.mjs", "./tests/**/*.ts", "./tests/**/*.js", "./tests/**/*.cjs", "./tests/**/*.mjs"],
650
+ allowedDefaultProject: ["./*.{js,cjs,mjs}", "./tests/**/*.{ts,js,cjs,mjs}"],
545
651
  defaultProject: "tsconfig.json"
546
652
  },
547
653
  tsconfigRootDir: undefined,
548
- ...parserOptions
654
+ ...options?.parserOptions
549
655
  }
550
656
  };
551
657
  return [
@@ -564,13 +670,15 @@ function defaultConfig(parserOptions) {
564
670
  {
565
671
  files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"],
566
672
  plugins: {
673
+ ...configs$7.base.plugins,
567
674
  ...configs$6.base.plugins,
568
675
  ...configs$4.base.plugins,
569
676
  ...configs$2.base.plugins,
570
677
  ...configs.base.plugins
571
678
  },
572
679
  rules: {
573
- ...configs$7.base.rules,
680
+ ...configs$8.base.rules,
681
+ ...enableRequireExtensionRule ? configs$7.base.rules : {},
574
682
  ...configs$6.base.rules,
575
683
  ...configs$4.base.rules,
576
684
  ...configs$2.base.rules,
@@ -582,14 +690,14 @@ function defaultConfig(parserOptions) {
582
690
  languageOptions: {
583
691
  sourceType: "script"
584
692
  },
585
- ...configs$7.cjsAndEsm
693
+ ...configs$8.cjsAndEsm
586
694
  },
587
695
  {
588
696
  files: ["**/*.js", "**/*.cjs"],
589
697
  languageOptions: {
590
698
  sourceType: "script"
591
699
  },
592
- ...configs$7.cjs
700
+ ...configs$8.cjs
593
701
  },
594
702
  {
595
703
  files: ["**/*.ts", "**/*.tsx"],
@@ -608,7 +716,7 @@ function defaultConfig(parserOptions) {
608
716
  }
609
717
 
610
718
  exports.defaultConfig = defaultConfig;
611
- exports.eslintConfigs = configs$7;
719
+ exports.eslintConfigs = configs$8;
612
720
  exports.jsdocConfigs = configs$6;
613
721
  exports.promiseConfigs = configs$4;
614
722
  exports.reactConfigs = configs$3;
package/dist/index.d.cts CHANGED
@@ -30,6 +30,10 @@ declare const configs: {
30
30
  base: ConfigWithExtends;
31
31
  };
32
32
 
33
- 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[];
34
38
 
35
- export { 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 };
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
@@ -30,6 +30,10 @@ declare const configs: {
30
30
  base: ConfigWithExtends;
31
31
  };
32
32
 
33
- 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[];
34
38
 
35
- export { 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 };
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
@@ -30,6 +30,10 @@ declare const configs: {
30
30
  base: ConfigWithExtends;
31
31
  };
32
32
 
33
- 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[];
34
38
 
35
- export { 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 };
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,6 +2,8 @@ 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';
@@ -12,7 +14,7 @@ import testingLibrary from 'eslint-plugin-testing-library';
12
14
  import security from 'eslint-plugin-security';
13
15
  import unicorn from 'eslint-plugin-unicorn';
14
16
 
15
- const base$7 = {
17
+ const base$8 = {
16
18
  rules: {
17
19
  "array-callback-return": ["error", { allowImplicit: true }],
18
20
  "block-scoped-var": "error",
@@ -259,12 +261,122 @@ const cjs = {
259
261
  strict: ["error", "global"]
260
262
  }
261
263
  };
262
- const configs$7 = {
263
- base: base$7,
264
+ const configs$8 = {
265
+ base: base$8,
264
266
  cjsAndEsm,
265
267
  cjs
266
268
  };
267
269
 
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
+
268
380
  const base$6 = {
269
381
  settings: {
270
382
  jsdoc: {
@@ -474,14 +586,6 @@ const base$1 = {
474
586
  "@typescript-eslint/no-empty-interface": "error",
475
587
  "@typescript-eslint/no-extra-semi": "error",
476
588
  "@typescript-eslint/no-shadow": "error",
477
- "@typescript-eslint/no-use-before-define": [
478
- "error",
479
- {
480
- functions: true,
481
- classes: true,
482
- variables: true
483
- }
484
- ],
485
589
  "@typescript-eslint/parameter-properties": [
486
590
  "error",
487
591
  {
@@ -490,7 +594,8 @@ const base$1 = {
490
594
  ],
491
595
  "@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true }],
492
596
  "@typescript-eslint/return-await": "error",
493
- "@typescript-eslint/sort-type-constituents": "error"
597
+ "@typescript-eslint/sort-type-constituents": "error",
598
+ "@typescript-eslint/use-unknown-in-catch-callback-variable": "off"
494
599
  }
495
600
  };
496
601
  const configs$1 = {
@@ -515,7 +620,8 @@ const configs = {
515
620
  base
516
621
  };
517
622
 
518
- function defaultConfig(parserOptions) {
623
+ function defaultConfig(options) {
624
+ const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true;
519
625
  const languageOptions = {
520
626
  globals: {
521
627
  ...globals.node
@@ -523,11 +629,11 @@ function defaultConfig(parserOptions) {
523
629
  parserOptions: {
524
630
  // @ts-expect-error - This is a valid option
525
631
  projectService: {
526
- allowedDefaultProject: ["./*.js", "./*.cjs", "./*.mjs", "./tests/**/*.ts", "./tests/**/*.js", "./tests/**/*.cjs", "./tests/**/*.mjs"],
632
+ allowedDefaultProject: ["./*.{js,cjs,mjs}", "./tests/**/*.{ts,js,cjs,mjs}"],
527
633
  defaultProject: "tsconfig.json"
528
634
  },
529
635
  tsconfigRootDir: import.meta.dirname,
530
- ...parserOptions
636
+ ...options?.parserOptions
531
637
  }
532
638
  };
533
639
  return [
@@ -546,13 +652,15 @@ function defaultConfig(parserOptions) {
546
652
  {
547
653
  files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"],
548
654
  plugins: {
655
+ ...configs$7.base.plugins,
549
656
  ...configs$6.base.plugins,
550
657
  ...configs$4.base.plugins,
551
658
  ...configs$2.base.plugins,
552
659
  ...configs.base.plugins
553
660
  },
554
661
  rules: {
555
- ...configs$7.base.rules,
662
+ ...configs$8.base.rules,
663
+ ...enableRequireExtensionRule ? configs$7.base.rules : {},
556
664
  ...configs$6.base.rules,
557
665
  ...configs$4.base.rules,
558
666
  ...configs$2.base.rules,
@@ -564,14 +672,14 @@ function defaultConfig(parserOptions) {
564
672
  languageOptions: {
565
673
  sourceType: "script"
566
674
  },
567
- ...configs$7.cjsAndEsm
675
+ ...configs$8.cjsAndEsm
568
676
  },
569
677
  {
570
678
  files: ["**/*.js", "**/*.cjs"],
571
679
  languageOptions: {
572
680
  sourceType: "script"
573
681
  },
574
- ...configs$7.cjs
682
+ ...configs$8.cjs
575
683
  },
576
684
  {
577
685
  files: ["**/*.ts", "**/*.tsx"],
@@ -589,4 +697,4 @@ function defaultConfig(parserOptions) {
589
697
  ];
590
698
  }
591
699
 
592
- export { defaultConfig, configs$7 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 };
700
+ 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.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A decent ESLint configuration",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -72,33 +72,33 @@
72
72
  "node": ">=20.11.0"
73
73
  },
74
74
  "dependencies": {
75
- "@eslint/js": "^9.4.0",
75
+ "@eslint/js": "^9.6.0",
76
76
  "eslint-config-prettier": "^9.1.0",
77
- "eslint-plugin-jsdoc": "^48.2.7",
77
+ "eslint-plugin-jsdoc": "^48.5.0",
78
78
  "eslint-plugin-mocha": "^10.4.3",
79
79
  "eslint-plugin-prettier": "^5.1.3",
80
- "eslint-plugin-promise": "^6.2.0",
81
- "eslint-plugin-jsx-a11y": "^6.8.0",
82
- "eslint-plugin-react": "^7.34.2",
80
+ "eslint-plugin-promise": "^6.4.0",
81
+ "eslint-plugin-jsx-a11y": "^6.9.0",
82
+ "eslint-plugin-react": "^7.34.3",
83
83
  "eslint-plugin-react-hooks": "^4.6.2",
84
- "eslint-plugin-security": "^3.0.0",
84
+ "eslint-plugin-security": "^3.0.1",
85
85
  "eslint-plugin-testing-library": "^6.2.2",
86
- "eslint-plugin-unicorn": "^53.0.0",
87
- "globals": "^15.3.0",
88
- "typescript-eslint": "^8.0.0-alpha.25"
86
+ "eslint-plugin-unicorn": "^54.0.0",
87
+ "globals": "^15.7.0",
88
+ "typescript-eslint": "8.0.0-alpha.29"
89
89
  },
90
90
  "devDependencies": {
91
- "@swc/core": "1.5.24",
91
+ "@swc/core": "1.6.6",
92
92
  "@types/node": ">=20",
93
- "eslint": "^9.4.0",
93
+ "eslint": "^9.6.0",
94
94
  "husky": "^9.0.11",
95
- "lint-staged": "^15.2.5",
95
+ "lint-staged": "^15.2.7",
96
96
  "markdownlint-cli": "^0.41.0",
97
97
  "npm-run-all": "^4.1.5",
98
98
  "pinst": "^3.0.0",
99
- "prettier": "^3.3.0",
99
+ "prettier": "^3.3.2",
100
100
  "rimraf": "^5.0.7",
101
- "typescript": "^5.4.5",
101
+ "typescript": "^5.5.3",
102
102
  "unbuild": "2.0.0"
103
103
  },
104
104
  "overrides": {
@@ -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,6 +3,7 @@ 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';
@@ -21,7 +22,13 @@ export {
21
22
  unicornConfigs,
22
23
  };
23
24
 
24
- 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;
25
32
  const languageOptions: ConfigWithExtends['languageOptions'] = {
26
33
  globals: {
27
34
  ...globals.node,
@@ -29,11 +36,11 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
29
36
  parserOptions: {
30
37
  // @ts-expect-error - This is a valid option
31
38
  projectService: {
32
- allowedDefaultProject: ['./*.js', './*.cjs', './*.mjs', './tests/**/*.ts', './tests/**/*.js', './tests/**/*.cjs', './tests/**/*.mjs'],
39
+ allowedDefaultProject: ['./*.{js,cjs,mjs}', './tests/**/*.{ts,js,cjs,mjs}'],
33
40
  defaultProject: 'tsconfig.json',
34
41
  },
35
42
  tsconfigRootDir: import.meta.dirname,
36
- ...parserOptions,
43
+ ...options?.parserOptions,
37
44
  },
38
45
  };
39
46
 
@@ -53,6 +60,7 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
53
60
  {
54
61
  files: ['**/*.ts', '**/*.js', '**/*.cjs', '**/*.mjs', '**/*.tsx'],
55
62
  plugins: {
63
+ ...extensionConfigs.base.plugins,
56
64
  ...jsdocConfigs.base.plugins,
57
65
  ...promiseConfigs.base.plugins,
58
66
  ...securityConfigs.base.plugins,
@@ -60,6 +68,7 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
60
68
  },
61
69
  rules: {
62
70
  ...eslintConfigs.base.rules,
71
+ ...(enableRequireExtensionRule ? extensionConfigs.base.rules : {}),
63
72
  ...jsdocConfigs.base.rules,
64
73
  ...promiseConfigs.base.rules,
65
74
  ...securityConfigs.base.rules,
@@ -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
+ };
@@ -50,14 +50,6 @@ const base: ConfigWithExtends = {
50
50
  '@typescript-eslint/no-empty-interface': 'error',
51
51
  '@typescript-eslint/no-extra-semi': 'error',
52
52
  '@typescript-eslint/no-shadow': 'error',
53
- '@typescript-eslint/no-use-before-define': [
54
- 'error',
55
- {
56
- functions: true,
57
- classes: true,
58
- variables: true,
59
- },
60
- ],
61
53
  '@typescript-eslint/parameter-properties': [
62
54
  'error',
63
55
  {
@@ -67,6 +59,7 @@ const base: ConfigWithExtends = {
67
59
  '@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }],
68
60
  '@typescript-eslint/return-await': 'error',
69
61
  '@typescript-eslint/sort-type-constituents': 'error',
62
+ '@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
70
63
  },
71
64
  };
72
65