eslint-config-decent 1.1.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,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: {
@@ -533,7 +645,8 @@ const configs = {
533
645
  base
534
646
  };
535
647
 
536
- function defaultConfig(parserOptions) {
648
+ function defaultConfig(options) {
649
+ const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true;
537
650
  const languageOptions = {
538
651
  globals: {
539
652
  ...globals__default.node
@@ -545,7 +658,7 @@ function defaultConfig(parserOptions) {
545
658
  defaultProject: "tsconfig.json"
546
659
  },
547
660
  tsconfigRootDir: undefined,
548
- ...parserOptions
661
+ ...options?.parserOptions
549
662
  }
550
663
  };
551
664
  return [
@@ -564,13 +677,15 @@ function defaultConfig(parserOptions) {
564
677
  {
565
678
  files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"],
566
679
  plugins: {
680
+ ...configs$7.base.plugins,
567
681
  ...configs$6.base.plugins,
568
682
  ...configs$4.base.plugins,
569
683
  ...configs$2.base.plugins,
570
684
  ...configs.base.plugins
571
685
  },
572
686
  rules: {
573
- ...configs$7.base.rules,
687
+ ...configs$8.base.rules,
688
+ ...enableRequireExtensionRule ? configs$7.base.rules : {},
574
689
  ...configs$6.base.rules,
575
690
  ...configs$4.base.rules,
576
691
  ...configs$2.base.rules,
@@ -582,14 +697,14 @@ function defaultConfig(parserOptions) {
582
697
  languageOptions: {
583
698
  sourceType: "script"
584
699
  },
585
- ...configs$7.cjsAndEsm
700
+ ...configs$8.cjsAndEsm
586
701
  },
587
702
  {
588
703
  files: ["**/*.js", "**/*.cjs"],
589
704
  languageOptions: {
590
705
  sourceType: "script"
591
706
  },
592
- ...configs$7.cjs
707
+ ...configs$8.cjs
593
708
  },
594
709
  {
595
710
  files: ["**/*.ts", "**/*.tsx"],
@@ -608,7 +723,7 @@ function defaultConfig(parserOptions) {
608
723
  }
609
724
 
610
725
  exports.defaultConfig = defaultConfig;
611
- exports.eslintConfigs = configs$7;
726
+ exports.eslintConfigs = configs$8;
612
727
  exports.jsdocConfigs = configs$6;
613
728
  exports.promiseConfigs = configs$4;
614
729
  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: {
@@ -515,7 +627,8 @@ const configs = {
515
627
  base
516
628
  };
517
629
 
518
- function defaultConfig(parserOptions) {
630
+ function defaultConfig(options) {
631
+ const enableRequireExtensionRule = options?.enableRequireExtensionRule ?? true;
519
632
  const languageOptions = {
520
633
  globals: {
521
634
  ...globals.node
@@ -527,7 +640,7 @@ function defaultConfig(parserOptions) {
527
640
  defaultProject: "tsconfig.json"
528
641
  },
529
642
  tsconfigRootDir: import.meta.dirname,
530
- ...parserOptions
643
+ ...options?.parserOptions
531
644
  }
532
645
  };
533
646
  return [
@@ -546,13 +659,15 @@ function defaultConfig(parserOptions) {
546
659
  {
547
660
  files: ["**/*.ts", "**/*.js", "**/*.cjs", "**/*.mjs", "**/*.tsx"],
548
661
  plugins: {
662
+ ...configs$7.base.plugins,
549
663
  ...configs$6.base.plugins,
550
664
  ...configs$4.base.plugins,
551
665
  ...configs$2.base.plugins,
552
666
  ...configs.base.plugins
553
667
  },
554
668
  rules: {
555
- ...configs$7.base.rules,
669
+ ...configs$8.base.rules,
670
+ ...enableRequireExtensionRule ? configs$7.base.rules : {},
556
671
  ...configs$6.base.rules,
557
672
  ...configs$4.base.rules,
558
673
  ...configs$2.base.rules,
@@ -564,14 +679,14 @@ function defaultConfig(parserOptions) {
564
679
  languageOptions: {
565
680
  sourceType: "script"
566
681
  },
567
- ...configs$7.cjsAndEsm
682
+ ...configs$8.cjsAndEsm
568
683
  },
569
684
  {
570
685
  files: ["**/*.js", "**/*.cjs"],
571
686
  languageOptions: {
572
687
  sourceType: "script"
573
688
  },
574
- ...configs$7.cjs
689
+ ...configs$8.cjs
575
690
  },
576
691
  {
577
692
  files: ["**/*.ts", "**/*.tsx"],
@@ -589,4 +704,4 @@ function defaultConfig(parserOptions) {
589
704
  ];
590
705
  }
591
706
 
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 };
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.1.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",
@@ -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,
@@ -33,7 +40,7 @@ export function defaultConfig(parserOptions?: NonNullable<ConfigWithExtends['lan
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
+ };