@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 +1 -0
- package/eslint.config.mjs +11 -0
- package/package.json +3 -2
- package/src/cli/check-deps.mjs +11 -21
- package/src/cli/format.mjs +22 -0
- package/src/cli/index.mjs +10 -0
- package/src/cli/install-deps.mjs +54 -36
- package/src/cli/lint.mjs +21 -0
- package/src/cli/postinstall-check.mjs +4 -4
- package/src/cli/postinstall.mjs +3 -3
- package/src/plugins/standard-conventions.mjs +554 -3
- package/src/cli/setup-aggressive-cleanup.mjs +0 -97
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.
|
|
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-
|
|
36
|
+
"test:strict": "node tests/test-strict-rules.mjs"
|
|
36
37
|
},
|
|
37
38
|
"keywords": [],
|
|
38
39
|
"author": "",
|
package/src/cli/check-deps.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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:
|
package/src/cli/install-deps.mjs
CHANGED
|
@@ -1,49 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
46
|
+
console.log(`📦 Installing peer dependencies using ${packageManager}...`);
|
|
47
|
+
console.log(` ${depsToInstall.join(' ')}\n`);
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
package/src/cli/lint.mjs
ADDED
|
@@ -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);
|
package/src/cli/postinstall.mjs
CHANGED
|
@@ -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
|
|
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();
|