@dmitryrechkin/eslint-standard 1.5.8 → 1.5.9

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
@@ -97,6 +97,7 @@ export default config({
97
97
  | **Repository CQRS** | `CommandRepository` cannot have query methods (`get`, `find`, `list`), `QueryRepository` cannot have command methods (`create`, `update`, `delete`) | Command Query Responsibility Segregation for scalable data access |
98
98
  | **Folder CamelCase** | All folder names must be camelCase | Consistent naming across the codebase |
99
99
  | **Function Name Match Filename** | Top-level function name must match filename | Predictable imports and file discovery |
100
+ | **No Utils Folder** | `utils/` folder is forbidden | Use `helpers/` for stateless logic or domain objects for stateful logic |
100
101
 
101
102
  #### Example Project Structure
102
103
 
package/eslint.config.mjs CHANGED
@@ -882,6 +882,17 @@ export default function ({
882
882
  // Repository CQRS enforcement
883
883
  'standard-conventions/repository-cqrs': 'error',
884
884
 
885
+ // Repository 'ById' naming enforcement
886
+ 'standard-conventions/repository-by-id': 'error',
887
+
888
+ // No 'utils' folder enforcement
889
+ 'standard-conventions/no-utils-folder': 'error',
890
+
891
+ // Enforce separation of schemas, types, and constants from class files
892
+ 'standard-conventions/no-schemas-in-class-files': 'error',
893
+ 'standard-conventions/no-types-in-class-files': 'error',
894
+ 'standard-conventions/no-constants-in-class-files': 'error',
895
+
885
896
  'unicorn/filename-case': ['error', {
886
897
  cases: {
887
898
  camelCase: true,
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@dmitryrechkin/eslint-standard",
3
3
  "description": "This package provides a shared ESLint configuration which includes TypeScript support and a set of specific linting rules designed to ensure high-quality and consistent code style across projects.",
4
- "version": "1.5.8",
4
+ "version": "1.5.9",
5
+ "type": "module",
5
6
  "main": "eslint.config.mjs",
6
7
  "exports": {
7
8
  ".": "./eslint.config.mjs"
@@ -32,7 +33,7 @@
32
33
  "test:switch-case": "node tests/test-switch-case-simple.mjs",
33
34
  "test:cli": "node tests/test-cli.mjs",
34
35
  "test:install": "node tests/test-install-simulation.mjs",
35
- "test:strict": "node tests/test-strict-conventions.mjs"
36
+ "test:strict": "node tests/test-strict-rules.mjs"
36
37
  },
37
38
  "keywords": [],
38
39
  "author": "",
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync } from 'fs';
4
- import { fileURLToPath } from 'url';
5
- import { dirname, join, resolve } from 'path';
6
- import { spawn } from 'child_process';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
7
6
 
8
7
  const __filename = fileURLToPath(import.meta.url);
9
8
  const __dirname = dirname(__filename);
@@ -17,18 +16,8 @@ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
17
16
  const peerDeps = packageJson.peerDependencies || {};
18
17
 
19
18
  // Check if running from node_modules and find project root
20
- const isInNodeModules = __dirname.includes('node_modules');
21
- let projectRoot;
22
-
23
- if (isInNodeModules) {
24
- // Handle different package manager structures
25
- // pnpm: .pnpm/@org+pkg@version_deps/node_modules/@org/pkg
26
- // npm/yarn: node_modules/@org/pkg
27
- const parts = __dirname.split('node_modules');
28
- projectRoot = parts[0].replace(/[\\/]$/, ''); // Remove trailing slash
29
- } else {
30
- projectRoot = process.cwd();
31
- }
19
+ // We primarily use process.cwd() as this is a CLI tool meant to run in the target project
20
+ const projectRoot = process.cwd();
32
21
 
33
22
  // Read project's package.json
34
23
  let projectPackageJson;
@@ -36,6 +25,7 @@ try {
36
25
  projectPackageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
37
26
  } catch (error) {
38
27
  console.error('❌ Could not read project package.json');
28
+ console.error(`Checked in: ${projectRoot}`);
39
29
  process.exit(1);
40
30
  }
41
31
 
@@ -69,13 +59,13 @@ if (missingDeps.length === 0 && outdatedDeps.length === 0) {
69
59
  if (missingDeps.length > 0) {
70
60
  console.log(`\n❌ Missing ${missingDeps.length} dependencies:`);
71
61
  missingDeps.forEach(dep => console.log(` - ${dep}`));
72
-
62
+
73
63
  if (shouldInstall) {
74
64
  console.log('\n🔧 Auto-installing missing dependencies...\n');
75
-
65
+
76
66
  // Import and run the install-deps script
77
67
  try {
78
- const installDepsModule = await import('./install-deps.mjs');
68
+ await import('./install-deps.mjs');
79
69
  // The install-deps script will handle the installation
80
70
  } catch (error) {
81
71
  console.error('❌ Failed to auto-install dependencies:', error.message);
@@ -91,7 +81,7 @@ if (missingDeps.length === 0 && outdatedDeps.length === 0) {
91
81
  console.log(`\n⚠️ ${outdatedDeps.length} dependencies may be outdated:`);
92
82
  outdatedDeps.forEach(dep => console.log(` - ${dep}`));
93
83
  }
94
-
84
+
95
85
  if (!shouldInstall && missingDeps.length > 0) {
96
86
  process.exit(1);
97
87
  }
@@ -99,4 +89,4 @@ if (missingDeps.length === 0 && outdatedDeps.length === 0) {
99
89
 
100
90
  export default function checkDeps() {
101
91
  // Export for programmatic use
102
- }
92
+ }
@@ -0,0 +1,22 @@
1
+
2
+ import { spawn } from 'node:child_process';
3
+
4
+ const args = process.argv.slice(3); // First 3 are node, script, command
5
+
6
+ console.log('Running formatting...');
7
+
8
+ // Determine eslint command (npx eslint or just eslint if in path)
9
+ // We inject --fix to enable auto-fixing/formatting
10
+ const eslint = spawn('npx', ['eslint', '--fix', ...args], {
11
+ stdio: 'inherit',
12
+ shell: true
13
+ });
14
+
15
+ eslint.on('close', (code) => {
16
+ if (code !== 0) {
17
+ console.log('\n\x1b[33m%s\x1b[0m', 'formatting completed with issues. please address remaining errors and warnings manually, make sure everything builds and all tests pass after modifying the code');
18
+ process.exit(code);
19
+ } else {
20
+ console.log('\n✅ Formatting passed');
21
+ }
22
+ });
package/src/cli/index.mjs CHANGED
@@ -9,6 +9,12 @@ switch (command) {
9
9
  case 'check-deps':
10
10
  await import('./check-deps.mjs');
11
11
  break;
12
+ case 'lint':
13
+ await import('./lint.mjs');
14
+ break;
15
+ case 'format':
16
+ await import('./format.mjs');
17
+ break;
12
18
  case 'help':
13
19
  case '--help':
14
20
  case '-h':
@@ -23,12 +29,16 @@ Commands:
23
29
  install-deps Install all peer dependencies
24
30
  check-deps Check if all peer dependencies are installed
25
31
  check-deps --install Auto-install missing dependencies if any
32
+ lint Run eslint with custom error message
33
+ format Run eslint --fix with custom error message
26
34
  help Show this help message
27
35
 
28
36
  Examples:
29
37
  npx @dmitryrechkin/eslint-standard install-deps
30
38
  npx @dmitryrechkin/eslint-standard check-deps
31
39
  npx @dmitryrechkin/eslint-standard check-deps --install
40
+ npx @dmitryrechkin/eslint-standard lint .
41
+ npx @dmitryrechkin/eslint-standard format .
32
42
  `);
33
43
  break;
34
44
  default:
@@ -1,49 +1,67 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { execSync } from 'child_process';
4
- import { readFileSync, existsSync } from 'fs';
5
- import { fileURLToPath } from 'url';
6
- import { dirname, join } from 'path';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = dirname(__filename);
10
10
 
11
- // Detect package manager
12
- function detectPackageManager() {
13
- if (existsSync('pnpm-lock.yaml')) return 'pnpm';
14
- if (existsSync('yarn.lock')) return 'yarn';
15
- if (existsSync('package-lock.json')) return 'npm';
16
- if (existsSync('bun.lockb')) return 'bun';
17
- return 'npm'; // default
11
+ // Get peer dependencies from our package.json
12
+ const packageJsonPath = join(__dirname, '../../package.json');
13
+ let peerDeps = {};
14
+
15
+ try {
16
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
17
+ peerDeps = packageJson.peerDependencies || {};
18
+ } catch (error) {
19
+ console.error('❌ Could not read package.json:', error.message);
20
+ process.exit(1);
18
21
  }
19
22
 
20
- // Get peer dependencies from package.json
21
- const packageJsonPath = join(__dirname, '../../package.json');
22
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
23
- const peerDeps = packageJson.peerDependencies || {};
23
+ const depsToInstall = Object.entries(peerDeps).map(([dep, version]) => `${dep}@${version}`);
24
+
25
+ if (depsToInstall.length === 0) {
26
+ console.log('✅ No peer dependencies to install.');
27
+ process.exit(0);
28
+ }
24
29
 
25
- // Build install command
26
- const packageManager = detectPackageManager();
27
- const deps = Object.entries(peerDeps).map(([name, version]) => `${name}@${version}`).join(' ');
30
+ // Check for lockfiles to determine package manager
31
+ const projectRoot = process.cwd();
32
+ let packageManager = 'npm';
33
+ let installArgs = ['install', '--save-dev'];
28
34
 
29
- const installCommands = {
30
- npm: `npm install --save-dev ${deps}`,
31
- pnpm: `pnpm add -D ${deps}`,
32
- yarn: `yarn add -D ${deps}`,
33
- bun: `bun add -d ${deps}`
34
- };
35
+ if (existsSync(join(projectRoot, 'pnpm-lock.yaml'))) {
36
+ packageManager = 'pnpm';
37
+ installArgs = ['add', '-D'];
38
+ } else if (existsSync(join(projectRoot, 'yarn.lock'))) {
39
+ packageManager = 'yarn';
40
+ installArgs = ['add', '-D'];
41
+ } else if (existsSync(join(projectRoot, 'bun.lockb'))) {
42
+ packageManager = 'bun';
43
+ installArgs = ['add', '-D'];
44
+ }
35
45
 
36
- const command = installCommands[packageManager];
46
+ console.log(`📦 Installing peer dependencies using ${packageManager}...`);
47
+ console.log(` ${depsToInstall.join(' ')}\n`);
37
48
 
38
- console.log(`🔧 Installing ESLint Standard peer dependencies...`);
39
- console.log(`📦 Detected package manager: ${packageManager}`);
40
- console.log(`📋 Running: ${command}\n`);
49
+ // Use shell: true on Windows compatibility or if packageManager is a .cmd/.bat file,
50
+ // but generally spawnSync handles this better than execSync.
51
+ // However, 'npm' often needs a shell on Windows or when run via npx.
52
+ // Using shell: true is standard for cross-platform npm execution unless using .cmd explicitly on Windows.
53
+ const result = spawnSync(packageManager, [...installArgs, ...depsToInstall], {
54
+ stdio: 'inherit',
55
+ shell: true
56
+ });
41
57
 
42
- try {
43
- execSync(command, { stdio: 'inherit' });
44
- console.log('\n✅ All peer dependencies installed successfully!');
45
- } catch (error) {
46
- console.error('\n❌ Failed to install dependencies. Please run manually:');
47
- console.error(command);
48
- process.exit(1);
49
- }
58
+ if (result.status !== 0) {
59
+ console.error('❌ Installation failed');
60
+ process.exit(result.status || 1);
61
+ }
62
+
63
+ console.log('\n✅ Peer dependencies installed successfully!');
64
+
65
+ export default function installDeps() {
66
+ // Export for programmatic use
67
+ }
@@ -0,0 +1,21 @@
1
+
2
+ import { spawn } from 'node:child_process';
3
+
4
+ const args = process.argv.slice(3); // First 3 are node, script, command
5
+
6
+ console.log('Running linting...');
7
+
8
+ // Determine eslint command (npx eslint or just eslint if in path)
9
+ const eslint = spawn('npx', ['eslint', ...args], {
10
+ stdio: 'inherit',
11
+ shell: true
12
+ });
13
+
14
+ eslint.on('close', (code) => {
15
+ if (code !== 0) {
16
+ console.log('\n\x1b[33m%s\x1b[0m', 'please address all the errors and warnings and re-run linting after, make sure everything builds and all tests pass after modifying the code');
17
+ process.exit(code);
18
+ } else {
19
+ console.log('\n✅ Linting passed');
20
+ }
21
+ });
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, existsSync } from 'fs';
4
- import { fileURLToPath } from 'url';
5
- import { dirname, join } from 'path';
6
- import { execSync } from 'child_process';
3
+ import { readFileSync, existsSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+ import { execSync } from 'node:child_process';
7
7
 
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = dirname(__filename);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { fileURLToPath } from 'url';
4
- import { dirname } from 'path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { dirname, join } from 'node:path';
5
5
 
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = dirname(__filename);
@@ -52,7 +52,7 @@ if (autoInstall) {
52
52
 
53
53
  try {
54
54
  // Run check-deps with --install flag
55
- const { execSync } = await import('child_process');
55
+ const { execSync } = await import('node:child_process');
56
56
  execSync('node ' + join(__dirname, 'index.mjs') + ' check-deps --install', {
57
57
  stdio: 'inherit',
58
58
  cwd: process.cwd()
@@ -45,6 +45,53 @@ const serviceSinglePublicMethodRule = {
45
45
  }
46
46
  };
47
47
 
48
+ /**
49
+ /**
50
+ * Rule: Factories must have only one public method
51
+ * @type {import('eslint').Rule.RuleModule}
52
+ */
53
+ const factorySinglePublicMethodRule = {
54
+ meta: {
55
+ type: 'suggestion',
56
+ docs: {
57
+ description: 'Enforce that factories have only one public method',
58
+ category: 'Best Practices',
59
+ recommended: false
60
+ },
61
+ schema: []
62
+ },
63
+ create(context) {
64
+ return {
65
+ ClassDeclaration(node) {
66
+ if (!node.id || !node.id.name.endsWith('Factory')) {
67
+ return;
68
+ }
69
+
70
+ const publicMethods = node.body.body.filter(member => {
71
+ return (
72
+ member.type === 'MethodDefinition' &&
73
+ member.kind === 'method' &&
74
+ (member.accessibility === 'public' || !member.accessibility) &&
75
+ !member.static
76
+ );
77
+ });
78
+
79
+ if (publicMethods.length > 1) {
80
+ context.report({
81
+ node: node.id,
82
+ message: 'Factory {{ name }} has {{ count }} public methods. Factories should have only one public method.',
83
+ data: {
84
+ name: node.id.name,
85
+ count: publicMethods.length
86
+ }
87
+ });
88
+ }
89
+ }
90
+ };
91
+ }
92
+ };
93
+
94
+
48
95
  /**
49
96
  * Rule: Top-level function name must match filename
50
97
  * @type {import('eslint').Rule.RuleModule}
@@ -226,8 +273,8 @@ const noStaticInNonHelpersRule = {
226
273
 
227
274
  const className = node.id.name;
228
275
 
229
- // Allow static methods in Helpers and Factories
230
- if (className.endsWith('Helper') || className.endsWith('Factory')) {
276
+ // Allow static methods in Helpers, Factories, and Registries
277
+ if (className.endsWith('Helper') || className.endsWith('Factory') || className.endsWith('Registry')) {
231
278
  return;
232
279
  }
233
280
 
@@ -589,9 +636,503 @@ const repositoryCqrsRule = {
589
636
  }
590
637
  };
591
638
 
639
+ /**
640
+ * Rule: No Zod schemas in files with classes
641
+ * @type {import('eslint').Rule.RuleModule}
642
+ */
643
+ const noSchemasInClassFilesRule = {
644
+ meta: {
645
+ type: 'suggestion',
646
+ docs: {
647
+ description: 'Enforce separation of schemas from class files',
648
+ category: 'Best Practices',
649
+ recommended: false
650
+ },
651
+ schema: []
652
+ },
653
+ create(context) {
654
+ let hasClass = false;
655
+ const schemas = [];
656
+
657
+ return {
658
+ ClassDeclaration() {
659
+ hasClass = true;
660
+ },
661
+ VariableDeclarator(node) {
662
+ if (node.id.type === 'Identifier') {
663
+ const name = node.id.name;
664
+ // Check for Schema suffix or z.object/z.string initialization
665
+ const isSchemaName = name.endsWith('Schema');
666
+ let isZodInit = false;
667
+
668
+ if (node.init && node.init.type === 'CallExpression' &&
669
+ node.init.callee.type === 'MemberExpression' &&
670
+ node.init.callee.object.type === 'Identifier' &&
671
+ node.init.callee.object.name === 'z') {
672
+ isZodInit = true;
673
+ }
674
+
675
+ if (isSchemaName || isZodInit) {
676
+ // Check if top-level
677
+ if (node.parent.parent.type === 'Program' ||
678
+ (node.parent.parent.type === 'ExportNamedDeclaration' && node.parent.parent.parent.type === 'Program')) {
679
+ schemas.push(node);
680
+ }
681
+ }
682
+ }
683
+ },
684
+ 'Program:exit'() {
685
+ if (hasClass && schemas.length > 0) {
686
+ schemas.forEach(node => {
687
+ context.report({
688
+ node: node.id,
689
+ message: 'Schema definition "{{ name }}" should not be in a class file. Move it to a separate schemas file or folder.',
690
+ data: {
691
+ name: node.id.name
692
+ }
693
+ });
694
+ });
695
+ }
696
+ }
697
+ };
698
+ }
699
+ };
700
+
701
+ /**
702
+ * Rule: No types/interfaces in files with classes
703
+ * @type {import('eslint').Rule.RuleModule}
704
+ */
705
+ const noTypesInClassFilesRule = {
706
+ meta: {
707
+ type: 'suggestion',
708
+ docs: {
709
+ description: 'Enforce separation of types and interfaces from class files',
710
+ category: 'Best Practices',
711
+ recommended: false
712
+ },
713
+ schema: []
714
+ },
715
+ create(context) {
716
+ let hasClass = false;
717
+ const types = [];
718
+
719
+ return {
720
+ ClassDeclaration() {
721
+ hasClass = true;
722
+ },
723
+ TSTypeAliasDeclaration(node) {
724
+ if (node.parent.type === 'Program' ||
725
+ (node.parent.type === 'ExportNamedDeclaration' && node.parent.parent.type === 'Program')) {
726
+ types.push(node);
727
+ }
728
+ },
729
+ TSInterfaceDeclaration(node) {
730
+ if (node.parent.type === 'Program' ||
731
+ (node.parent.type === 'ExportNamedDeclaration' && node.parent.parent.type === 'Program')) {
732
+ types.push(node);
733
+ }
734
+ },
735
+ 'Program:exit'() {
736
+ if (hasClass && types.length > 0) {
737
+ types.forEach(node => {
738
+ context.report({
739
+ node: node.id,
740
+ message: 'Type/Interface definition "{{ name }}" should not be in a class file. Move it to a separate types file or folder.',
741
+ data: {
742
+ name: node.id.name
743
+ }
744
+ });
745
+ });
746
+ }
747
+ }
748
+ };
749
+ }
750
+ };
751
+
752
+ /**
753
+ * Rule: No top-level constants in files with classes
754
+ * @type {import('eslint').Rule.RuleModule}
755
+ */
756
+ const noConstantsInClassFilesRule = {
757
+ meta: {
758
+ type: 'suggestion',
759
+ docs: {
760
+ description: 'Enforce separation of constants from class files',
761
+ category: 'Best Practices',
762
+ recommended: false
763
+ },
764
+ schema: []
765
+ },
766
+ create(context) {
767
+ let hasClass = false;
768
+ const constants = [];
769
+
770
+ return {
771
+ ClassDeclaration() {
772
+ hasClass = true;
773
+ },
774
+ VariableDeclaration(node) {
775
+ // Check if top-level const
776
+ if (node.kind === 'const' &&
777
+ (node.parent.type === 'Program' ||
778
+ (node.parent.type === 'ExportNamedDeclaration' && node.parent.parent.type === 'Program'))) {
779
+
780
+ node.declarations.forEach(decl => {
781
+ // Skip requires
782
+ if (decl.init && decl.init.type === 'CallExpression' && decl.init.callee.name === 'require') {
783
+ return;
784
+ }
785
+ // Skip Zod schemas (covered by other rule)
786
+ if (decl.id.name && decl.id.name.endsWith('Schema')) {
787
+ return;
788
+ }
789
+ if (decl.init && decl.init.type === 'CallExpression' &&
790
+ decl.init.callee.type === 'MemberExpression' &&
791
+ decl.init.callee.object.name === 'z') {
792
+ return;
793
+ }
794
+
795
+ constants.push(decl);
796
+ });
797
+ }
798
+ },
799
+ 'Program:exit'() {
800
+ if (hasClass && constants.length > 0) {
801
+ constants.forEach(node => {
802
+ context.report({
803
+ node: node.id,
804
+ message: 'Constant "{{ name }}" should not be in a class file. Move it to a separate constants file or use a static readonly class property.',
805
+ data: {
806
+ name: node.id.name
807
+ }
808
+ });
809
+ });
810
+ }
811
+ }
812
+ };
813
+ }
814
+ };
815
+
816
+ /**
817
+ * Rule: Interfaces must start with Type or end with Interface
818
+ * @type {import('eslint').Rule.RuleModule}
819
+ */
820
+ const interfaceNamingRule = {
821
+ meta: {
822
+ type: 'suggestion',
823
+ docs: {
824
+ description: 'Enforce that interface names start with Type or end with Interface',
825
+ category: 'Naming Conventions',
826
+ recommended: false
827
+ },
828
+ schema: []
829
+ },
830
+ create(context) {
831
+ return {
832
+ TSInterfaceDeclaration(node) {
833
+ if (!node.id) return;
834
+ const name = node.id.name;
835
+ if (!name.startsWith('Type') && !name.endsWith('Interface')) {
836
+ context.report({
837
+ node: node.id,
838
+ message: 'Interface "{{ name }}" should start with "Type" (for data) or end with "Interface" (for contracts).',
839
+ data: { name }
840
+ });
841
+ }
842
+ }
843
+ };
844
+ }
845
+ };
846
+
847
+ /**
848
+ * Rule: Functions must have explicit return types
849
+ * @type {import('eslint').Rule.RuleModule}
850
+ */
851
+ const explicitReturnTypeRule = {
852
+ meta: {
853
+ type: 'suggestion',
854
+ docs: {
855
+ description: 'Enforce explicit return types for functions',
856
+ category: 'Type Safety',
857
+ recommended: false
858
+ },
859
+ schema: []
860
+ },
861
+ create(context) {
862
+ function checkFunction(node) {
863
+ if (!node.returnType) {
864
+ const name = node.id ? node.id.name : (node.key ? node.key.name : 'anonymous');
865
+ context.report({
866
+ node: node.id || node.key || node,
867
+ message: 'Function/Method "{{ name }}" is missing an explicit return type.',
868
+ data: { name }
869
+ });
870
+ }
871
+ }
872
+
873
+ return {
874
+ FunctionDeclaration: checkFunction,
875
+ MethodDefinition: (node) => {
876
+ if (node.kind === 'constructor' || node.kind === 'set') return;
877
+ checkFunction(node.value);
878
+ },
879
+ ArrowFunctionExpression: checkFunction,
880
+ FunctionExpression: (node) => {
881
+ if (node.parent.type === 'MethodDefinition') return;
882
+ checkFunction(node);
883
+ }
884
+ };
885
+ }
886
+ };
887
+
888
+ /**
889
+ * Rule: No direct instantiation of classes inside other classes (dependency injection)
890
+ * @type {import('eslint').Rule.RuleModule}
891
+ */
892
+ const noDirectInstantiationRule = {
893
+ meta: {
894
+ type: 'suggestion',
895
+ docs: {
896
+ description: 'Enforce dependency injection by banning direct instantiation',
897
+ category: 'Best Practices',
898
+ recommended: false
899
+ },
900
+ schema: []
901
+ },
902
+ create(context) {
903
+ const allowedClasses = new Set([
904
+ 'Date', 'Error', 'Promise', 'Map', 'Set', 'WeakMap', 'WeakSet',
905
+ 'RegExp', 'URL', 'URLSearchParams', 'Buffer', 'Array', 'Object', 'String', 'Number', 'Boolean'
906
+ ]);
907
+
908
+ let inClass = false;
909
+ let currentClassName = '';
910
+
911
+ return {
912
+ ClassDeclaration(node) {
913
+ inClass = true;
914
+ if (node.id) {
915
+ currentClassName = node.id.name;
916
+ }
917
+ },
918
+ 'ClassDeclaration:exit'() {
919
+ inClass = false;
920
+ currentClassName = '';
921
+ },
922
+ NewExpression(node) {
923
+ if (!inClass) return;
924
+
925
+ // Allow factories to instantiate objects
926
+ if (currentClassName.endsWith('Factory')) {
927
+ return;
928
+ }
929
+
930
+ if (node.callee.type === 'Identifier') {
931
+ const className = node.callee.name;
932
+ if (!allowedClasses.has(className)) {
933
+ context.report({
934
+ node: node,
935
+ message: 'Avoid direct instantiation of "{{ name }}". Use dependency injection instead.',
936
+ data: { name: className }
937
+ });
938
+ }
939
+ }
940
+ }
941
+ };
942
+ }
943
+ };
944
+
945
+ /**
946
+ * Rule: Prefer Enums over union types of string literals
947
+ * @type {import('eslint').Rule.RuleModule}
948
+ */
949
+ const preferEnumsRule = {
950
+ meta: {
951
+ type: 'suggestion',
952
+ docs: {
953
+ description: 'Prefer Enums over union types of string literals',
954
+ category: 'TypeScript',
955
+ recommended: false
956
+ },
957
+ schema: []
958
+ },
959
+ create(context) {
960
+ return {
961
+ TSTypeAliasDeclaration(node) {
962
+ if (node.typeAnnotation.type === 'TSUnionType') {
963
+ const isAllLiterals = node.typeAnnotation.types.every(
964
+ t => t.type === 'TSLiteralType' && (typeof t.literal.value === 'string' || typeof t.literal.value === 'number')
965
+ );
966
+
967
+ if (isAllLiterals && node.typeAnnotation.types.length > 1) {
968
+ context.report({
969
+ node: node.id,
970
+ message: 'Avoid union types for "{{ name }}". Use an Enum instead.',
971
+ data: { name: node.id.name }
972
+ });
973
+ }
974
+ }
975
+ }
976
+ };
977
+ }
978
+ };
979
+
980
+ /**
981
+ * Rule: Schemas must end with Schema or Table
982
+ * @type {import('eslint').Rule.RuleModule}
983
+ */
984
+ const schemaNamingRule = {
985
+ meta: {
986
+ type: 'suggestion',
987
+ docs: {
988
+ description: 'Enforce that Zod schemas and Drizzle tables end with Schema or Table',
989
+ category: 'Naming Conventions',
990
+ recommended: false
991
+ },
992
+ schema: []
993
+ },
994
+ create(context) {
995
+ return {
996
+ VariableDeclarator(node) {
997
+ if (node.init && node.init.type === 'CallExpression') {
998
+ let isZodOrDrizzle = false;
999
+
1000
+ // Check for z.something()
1001
+ if (node.init.callee.type === 'MemberExpression' &&
1002
+ node.init.callee.object.type === 'Identifier' &&
1003
+ node.init.callee.object.name === 'z') {
1004
+ isZodOrDrizzle = true;
1005
+ }
1006
+
1007
+ // Check for pgTable()
1008
+ if (node.init.callee.type === 'Identifier' && node.init.callee.name === 'pgTable') {
1009
+ isZodOrDrizzle = true;
1010
+ }
1011
+
1012
+ if (isZodOrDrizzle && node.id.type === 'Identifier') {
1013
+ const name = node.id.name;
1014
+ if (!name.endsWith('Schema') && !name.endsWith('Table')) {
1015
+ context.report({
1016
+ node: node.id,
1017
+ message: 'Schema/Table definition "{{ name }}" should end with "Schema" or "Table".',
1018
+ data: { name }
1019
+ });
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ };
1025
+ }
1026
+ };
1027
+
1028
+ /**
1029
+ * Rule: Repository methods accepting 'id' must end with 'ById'
1030
+ * @type {import('eslint').Rule.RuleModule}
1031
+ */
1032
+ const repositoryByIdRule = {
1033
+ meta: {
1034
+ type: 'suggestion',
1035
+ docs: {
1036
+ description: 'Enforce "ById" suffix for repository methods that take an "id" parameter',
1037
+ category: 'Naming Conventions',
1038
+ recommended: false
1039
+ },
1040
+ schema: []
1041
+ },
1042
+ create(context) {
1043
+ return {
1044
+ MethodDefinition(node) {
1045
+ // Only check methods in Repository classes
1046
+ const classNode = node.parent.parent;
1047
+ if (!classNode || !classNode.id || !classNode.id.name.endsWith('Repository')) {
1048
+ return;
1049
+ }
1050
+
1051
+ // Skip constructors and static methods
1052
+ if (node.kind === 'constructor' || node.static) {
1053
+ return;
1054
+ }
1055
+
1056
+ const methodParam = node.value.params[0];
1057
+ if (!methodParam) {
1058
+ return;
1059
+ }
1060
+
1061
+ // Check if first parameter is named 'id'
1062
+ let paramName = '';
1063
+ if (methodParam.type === 'Identifier') {
1064
+ paramName = methodParam.name;
1065
+ }
1066
+
1067
+ if (paramName === 'id') {
1068
+ const methodName = node.key.name;
1069
+ if (!methodName.endsWith('ById')) {
1070
+ context.report({
1071
+ node: node.key,
1072
+ message: 'Repository method "{{ methodName }}" accepts "id" parameter but does not end with "ById". Rename to "{{ methodName }}ById".',
1073
+ data: { methodName }
1074
+ });
1075
+ }
1076
+ }
1077
+ }
1078
+ };
1079
+ }
1080
+ };
1081
+
1082
+ /**
1083
+ * Rule: Avoid using 'utils' folder, prefer 'helpers'
1084
+ * @type {import('eslint').Rule.RuleModule}
1085
+ */
1086
+ const noUtilsFolderRule = {
1087
+ meta: {
1088
+ type: 'suggestion',
1089
+ docs: {
1090
+ description: 'Enforce that "utils" folders are not used, preferring "helpers" instead',
1091
+ category: 'Best Practices',
1092
+ recommended: false
1093
+ },
1094
+ schema: []
1095
+ },
1096
+ create(context) {
1097
+ return {
1098
+ Program(node) {
1099
+ const fullPath = context.getFilename();
1100
+
1101
+ if (fullPath === '<input>' || fullPath === '<text>') {
1102
+ return;
1103
+ }
1104
+
1105
+ const dirPath = path.dirname(fullPath);
1106
+ const relativePath = path.relative(process.cwd(), dirPath);
1107
+
1108
+ if (!relativePath || relativePath === '.') {
1109
+ return;
1110
+ }
1111
+
1112
+ const folders = relativePath.split(path.sep);
1113
+
1114
+ // Check if any folder is named 'utils' or 'util'
1115
+ for (const folder of folders) {
1116
+ if (folder.toLowerCase() === 'utils' || folder.toLowerCase() === 'util') {
1117
+ context.report({
1118
+ node: node,
1119
+ loc: { line: 1, column: 0 },
1120
+ message: 'Avoid using "{{ folder }}" folder. Use "helpers" for shared logic, or specific domain names (e.g. "services", "factories").',
1121
+ data: { folder }
1122
+ });
1123
+ // Report only once per file
1124
+ break;
1125
+ }
1126
+ }
1127
+ }
1128
+ };
1129
+ }
1130
+ };
1131
+
592
1132
  export default {
593
1133
  rules: {
594
1134
  'service-single-public-method': serviceSinglePublicMethodRule,
1135
+ 'factory-single-public-method': factorySinglePublicMethodRule,
595
1136
  'function-name-match-filename': functionNameMatchFilenameRule,
596
1137
  'folder-camel-case': folderCamelCaseRule,
597
1138
  'helper-static-only': helperStaticOnlyRule,
@@ -600,6 +1141,16 @@ export default {
600
1141
  'type-location': typeLocationRule,
601
1142
  'transformer-single-public-method': transformerSinglePublicMethodRule,
602
1143
  'one-class-per-file': oneClassPerFileRule,
603
- 'repository-cqrs': repositoryCqrsRule
1144
+ 'repository-cqrs': repositoryCqrsRule,
1145
+ 'no-schemas-in-class-files': noSchemasInClassFilesRule,
1146
+ 'no-types-in-class-files': noTypesInClassFilesRule,
1147
+ 'no-constants-in-class-files': noConstantsInClassFilesRule,
1148
+ 'interface-naming': interfaceNamingRule,
1149
+ 'explicit-return-type': explicitReturnTypeRule,
1150
+ 'no-direct-instantiation': noDirectInstantiationRule,
1151
+ 'prefer-enums': preferEnumsRule,
1152
+ 'schema-naming': schemaNamingRule,
1153
+ 'repository-by-id': repositoryByIdRule,
1154
+ 'no-utils-folder': noUtilsFolderRule
604
1155
  }
605
1156
  };
@@ -1,97 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * @file CLI Setup for Aggressive Unused Code Detection
5
- *
6
- * This CLI tool helps set up aggressive unused code detection and cleanup
7
- * in your project with external tools and optimal configurations.
8
- *
9
- * @author PageFast Team
10
- * @version 1.0.0
11
- */
12
-
13
- import { setupAggressiveCleanup } from '../configs/external-tools-setup.mjs';
14
-
15
- // Simple command line argument parsing (avoiding external dependencies)
16
- const args = process.argv.slice(2);
17
- const options = {
18
- global: args.includes('--global') || args.includes('-g'),
19
- packageManager: args.find((arg, i) => (args[i-1] === '--package-manager' || args[i-1] === '-p')) || 'npm',
20
- dryRun: args.includes('--dry-run'),
21
- help: args.includes('--help') || args.includes('-h')
22
- };
23
-
24
- /**
25
- * Main CLI program for setting up aggressive cleanup
26
- */
27
- async function main()
28
- {
29
- try
30
- {
31
- if (options.help)
32
- {
33
- console.log(`
34
- @dmitryrechkin/eslint-standard - Aggressive Cleanup Setup
35
-
36
- Usage:
37
- npx @dmitryrechkin/eslint-standard setup-aggressive-cleanup [options]
38
-
39
- Options:
40
- -g, --global Install cleanup tools globally
41
- -p, --package-manager <type> Package manager to use (npm, pnpm, yarn)
42
- --dry-run Show what would be done without making changes
43
- -h, --help Show this help message
44
-
45
- Examples:
46
- npx @dmitryrechkin/eslint-standard setup-aggressive-cleanup
47
- npx @dmitryrechkin/eslint-standard setup-aggressive-cleanup --global
48
- npx @dmitryrechkin/eslint-standard setup-aggressive-cleanup -p pnpm
49
- npx @dmitryrechkin/eslint-standard setup-aggressive-cleanup --dry-run
50
- `);
51
- return;
52
- }
53
-
54
- if (options.dryRun)
55
- {
56
- console.log('🔍 DRY RUN - Would perform the following actions:\n');
57
- console.log('1. Install external cleanup tools:');
58
- console.log(' - ts-prune (find unused exports)');
59
- console.log(' - unimported (find unused files)');
60
- console.log(' - knip (advanced dead code elimination)');
61
- console.log(' - depcheck (find unused dependencies)');
62
- console.log(' - ts-remove-unused (remove unused imports)');
63
- console.log('\n2. Add cleanup scripts to package.json');
64
- console.log('\n3. Create optimal tsconfig.json for unused code detection');
65
- console.log('\n4. Create knip.json configuration');
66
- console.log('\nRun without --dry-run to actually perform setup.');
67
- return;
68
- }
69
-
70
- await setupAggressiveCleanup({
71
- packageManager: options.packageManager,
72
- globalTools: options.global
73
- });
74
-
75
- console.log('\n🎉 Setup complete! Next steps:');
76
- console.log('\n1. Enable aggressive cleanup in your ESLint config:');
77
- console.log(' ```javascript');
78
- console.log(' import baseConfig from "@dmitryrechkin/eslint-standard";');
79
- console.log(' export default baseConfig({');
80
- console.log(' aggressiveCleanup: true // 🔥 Enable aggressive mode');
81
- console.log(' });');
82
- console.log(' ```');
83
- console.log('\n2. Run cleanup check:');
84
- console.log(' npm run cleanup:check');
85
- console.log('\n3. Review and fix issues:');
86
- console.log(' npm run cleanup:fix');
87
-
88
- }
89
- catch (error)
90
- {
91
- console.error('\n❌ Setup failed:', error.message);
92
- process.exit(1);
93
- }
94
- }
95
-
96
- // Run the main function
97
- main();