angular-doctor 1.0.0 → 1.0.2
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 +70 -33
- package/dist/cli.mjs +34 -23
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +10 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,50 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Angular Doctor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Diagnose and improve Angular codebases with a single command.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Angular Doctor scans your project for **Angular-specific lint issues** and **dead code**, then produces a **0–100 health score** plus actionable diagnostics.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
---
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## ✨ Features
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
- **Angular-aware linting** (components, directives, pipes, performance, architecture, TypeScript)
|
|
12
|
+
- **Dead code detection** (unused files, exports, types) via [knip](https://knip.dev)
|
|
13
|
+
- **Workspace support** (Angular CLI + npm/pnpm workspaces)
|
|
14
|
+
- **Diff mode** to scan only changed files
|
|
15
|
+
- **Markdown reports** for sharing results
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
---
|
|
15
18
|
|
|
16
|
-
##
|
|
19
|
+
## ✅ Quick start
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
Run at your Angular project root (or workspace root):
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
-
|
|
23
|
+
```bash
|
|
24
|
+
npx -y angular-doctor@latest .
|
|
25
|
+
```
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
- **Interactive mode**: shows a multi-select prompt to choose which projects to scan
|
|
25
|
-
- **Non-interactive mode** (`-y / --yes`, CI): scans all detected projects automatically
|
|
27
|
+

|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
Generate a Markdown report in the current directory:
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
|
-
npx -y angular-doctor@latest . --
|
|
32
|
+
npx -y angular-doctor@latest . --report .
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Run this at your Angular project root (or workspace root):
|
|
35
|
+
Show affected files and line numbers:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
npx -y angular-doctor@latest .
|
|
38
|
+
npx -y angular-doctor@latest . --verbose
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🧭 Workspace support
|
|
44
|
+
|
|
45
|
+
Angular Doctor automatically detects multiple projects:
|
|
46
|
+
|
|
47
|
+
- **Angular CLI workspaces** — reads `angular.json` and scans each project inside `projects/`
|
|
48
|
+
- **npm / pnpm workspaces** — detects packages with `@angular/core` from `workspaces` or `pnpm-workspace.yaml`
|
|
49
|
+
|
|
50
|
+
When multiple projects are found:
|
|
51
|
+
|
|
52
|
+
- **Interactive mode**: prompts for which projects to scan
|
|
53
|
+
- **Non-interactive mode** (`-y`, CI): scans all detected projects
|
|
54
|
+
|
|
55
|
+
Target a specific project (comma-separated for multiple):
|
|
42
56
|
|
|
43
57
|
```bash
|
|
44
|
-
npx -y angular-doctor@latest . --
|
|
58
|
+
npx -y angular-doctor@latest . --project my-app,my-lib
|
|
45
59
|
```
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## ⚙️ CLI Options
|
|
48
64
|
|
|
49
65
|
```
|
|
50
66
|
Usage: angular-doctor [directory] [options]
|
|
@@ -55,15 +71,30 @@ Options:
|
|
|
55
71
|
--no-dead-code skip dead code detection
|
|
56
72
|
--verbose show file details per rule
|
|
57
73
|
--score output only the score
|
|
74
|
+
--report [path] write a markdown report (optional output path)
|
|
75
|
+
--fast speed up by skipping dead code and type-aware lint
|
|
58
76
|
-y, --yes skip prompts, scan all workspace projects
|
|
59
77
|
--project <name> select workspace project (comma-separated for multiple)
|
|
60
78
|
--diff [base] scan only files changed vs base branch
|
|
61
79
|
-h, --help display help for command
|
|
62
80
|
```
|
|
63
81
|
|
|
64
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 📝 Reports
|
|
85
|
+
|
|
86
|
+
Use `--report` to write a Markdown report:
|
|
65
87
|
|
|
66
|
-
|
|
88
|
+
- `--report` writes to the diagnostics temp folder
|
|
89
|
+
- `--report .` writes to the current project directory
|
|
90
|
+
- `--report ./reports` writes to a custom folder
|
|
91
|
+
- `--report ./reports/scan.md` writes to a specific file
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 🔧 Configuration
|
|
96
|
+
|
|
97
|
+
Create an `angular-doctor.config.json` in your project root:
|
|
67
98
|
|
|
68
99
|
```json
|
|
69
100
|
{
|
|
@@ -74,7 +105,7 @@ Create an `angular-doctor.config.json` in your project root to customize behavio
|
|
|
74
105
|
}
|
|
75
106
|
```
|
|
76
107
|
|
|
77
|
-
|
|
108
|
+
Or use the `angularDoctor` key in `package.json`:
|
|
78
109
|
|
|
79
110
|
```json
|
|
80
111
|
{
|
|
@@ -97,7 +128,9 @@ You can also use the `"angularDoctor"` key in your `package.json`:
|
|
|
97
128
|
| `verbose` | `boolean` | `false` | Show file details per rule |
|
|
98
129
|
| `diff` | `boolean | string` | — | Scan only changed files |
|
|
99
130
|
|
|
100
|
-
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 📦 Node.js API
|
|
101
134
|
|
|
102
135
|
```typescript
|
|
103
136
|
import { diagnose } from "angular-doctor/api";
|
|
@@ -125,9 +158,9 @@ interface Diagnostic {
|
|
|
125
158
|
}
|
|
126
159
|
```
|
|
127
160
|
|
|
128
|
-
|
|
161
|
+
---
|
|
129
162
|
|
|
130
|
-
|
|
163
|
+
## 🧪 What it checks
|
|
131
164
|
|
|
132
165
|
### Components
|
|
133
166
|
- Missing `Component` / `Directive` class suffixes
|
|
@@ -153,10 +186,14 @@ Angular Doctor checks the following categories of issues:
|
|
|
153
186
|
- Unused files
|
|
154
187
|
- Unused exports and types
|
|
155
188
|
|
|
156
|
-
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 💡 Inspiration
|
|
192
|
+
|
|
193
|
+
Inspired by [react-doctor](https://github.com/millionco/react-doctor).
|
|
157
194
|
|
|
158
|
-
|
|
195
|
+
---
|
|
159
196
|
|
|
160
|
-
## License
|
|
197
|
+
## 📄 License
|
|
161
198
|
|
|
162
199
|
MIT
|
package/dist/cli.mjs
CHANGED
|
@@ -447,13 +447,13 @@ const RULE_HELP_MAP = {
|
|
|
447
447
|
"@typescript-eslint/no-explicit-any": "Replace `any` with a specific type or `unknown` if the type is truly unknown",
|
|
448
448
|
"@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused"
|
|
449
449
|
};
|
|
450
|
-
const buildEslintConfig = (hasTypeScript, tsconfigPath) => {
|
|
450
|
+
const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
451
451
|
const languageOptions = {
|
|
452
452
|
parser: tsEslint.parser,
|
|
453
453
|
parserOptions: {
|
|
454
454
|
ecmaVersion: "latest",
|
|
455
455
|
sourceType: "module",
|
|
456
|
-
...hasTypeScript && tsconfigPath ? { project: tsconfigPath } : {}
|
|
456
|
+
...hasTypeScript && tsconfigPath && useTypeAware ? { project: tsconfigPath } : {}
|
|
457
457
|
}
|
|
458
458
|
};
|
|
459
459
|
const angularRules = {
|
|
@@ -504,14 +504,18 @@ const parsePluginAndRule = (ruleId) => {
|
|
|
504
504
|
rule: ruleId
|
|
505
505
|
};
|
|
506
506
|
};
|
|
507
|
-
const runEslint = async (rootDirectory, hasTypeScript, includePaths) => {
|
|
507
|
+
const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) => {
|
|
508
508
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
509
509
|
const tsconfigPath = hasTypeScript ? path.join(rootDirectory, "tsconfig.json") : null;
|
|
510
|
+
const cacheRoot = path.join(rootDirectory, "node_modules", ".cache", "angular-doctor");
|
|
511
|
+
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
510
512
|
const eslint = new ESLint({
|
|
511
513
|
cwd: rootDirectory,
|
|
512
|
-
overrideConfigFile:
|
|
513
|
-
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null),
|
|
514
|
-
ignore: true
|
|
514
|
+
overrideConfigFile: null,
|
|
515
|
+
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true),
|
|
516
|
+
ignore: true,
|
|
517
|
+
cache: true,
|
|
518
|
+
cacheLocation: path.join(cacheRoot, ".eslintcache")
|
|
515
519
|
});
|
|
516
520
|
const patterns = includePaths ?? ["**/*.ts"];
|
|
517
521
|
let results;
|
|
@@ -870,14 +874,18 @@ const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, totalSource
|
|
|
870
874
|
logger.break();
|
|
871
875
|
}
|
|
872
876
|
};
|
|
873
|
-
const mergeScanOptions = (inputOptions, userConfig) =>
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
877
|
+
const mergeScanOptions = (inputOptions, userConfig) => {
|
|
878
|
+
const fastMode = inputOptions.fast ?? userConfig?.fast ?? false;
|
|
879
|
+
return {
|
|
880
|
+
lint: inputOptions.lint ?? userConfig?.lint ?? true,
|
|
881
|
+
deadCode: fastMode ? false : inputOptions.deadCode ?? userConfig?.deadCode ?? true,
|
|
882
|
+
verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,
|
|
883
|
+
scoreOnly: inputOptions.scoreOnly ?? false,
|
|
884
|
+
report: inputOptions.report ?? false,
|
|
885
|
+
useTypeAwareLint: !fastMode,
|
|
886
|
+
includePaths: inputOptions.includePaths ?? []
|
|
887
|
+
};
|
|
888
|
+
};
|
|
881
889
|
const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths) => {
|
|
882
890
|
const frameworkLabel = formatFrameworkName(projectInfo.framework);
|
|
883
891
|
const languageLabel = "TypeScript";
|
|
@@ -905,10 +913,11 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
905
913
|
const computedIncludePaths = computeIncludePaths(includePaths);
|
|
906
914
|
let didLintFail = false;
|
|
907
915
|
let didDeadCodeFail = false;
|
|
908
|
-
const
|
|
916
|
+
const runLint = async () => {
|
|
917
|
+
if (!options.lint) return [];
|
|
909
918
|
const lintSpinner = options.scoreOnly ? null : spinner("Running lint checks...").start();
|
|
910
919
|
try {
|
|
911
|
-
const lintDiagnostics = await runEslint(directory, projectInfo.hasTypeScript, computedIncludePaths);
|
|
920
|
+
const lintDiagnostics = await runEslint(directory, projectInfo.hasTypeScript, computedIncludePaths, { useTypeAware: options.useTypeAwareLint });
|
|
912
921
|
lintSpinner?.succeed("Running lint checks.");
|
|
913
922
|
return lintDiagnostics;
|
|
914
923
|
} catch (error) {
|
|
@@ -917,8 +926,9 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
917
926
|
logger.error(String(error));
|
|
918
927
|
return [];
|
|
919
928
|
}
|
|
920
|
-
}
|
|
921
|
-
const
|
|
929
|
+
};
|
|
930
|
+
const runDeadCode = async () => {
|
|
931
|
+
if (!options.deadCode || isDiffMode) return [];
|
|
922
932
|
const deadCodeSpinner = options.scoreOnly ? null : spinner("Detecting dead code...").start();
|
|
923
933
|
try {
|
|
924
934
|
const knipDiagnostics = await runKnip(directory);
|
|
@@ -930,8 +940,8 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
930
940
|
logger.error(String(error));
|
|
931
941
|
return [];
|
|
932
942
|
}
|
|
933
|
-
}
|
|
934
|
-
const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([
|
|
943
|
+
};
|
|
944
|
+
const [lintDiagnostics, deadCodeDiagnostics] = options.scoreOnly ? await Promise.all([runLint(), runDeadCode()]) : [await runLint(), await runDeadCode()];
|
|
935
945
|
const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);
|
|
936
946
|
const elapsedMilliseconds = performance.now() - startTime;
|
|
937
947
|
const skippedChecks = [];
|
|
@@ -1127,7 +1137,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
|
1127
1137
|
|
|
1128
1138
|
//#endregion
|
|
1129
1139
|
//#region src/cli.ts
|
|
1130
|
-
const VERSION = "1.0.
|
|
1140
|
+
const VERSION = "1.0.2";
|
|
1131
1141
|
const exitWithHint = () => {
|
|
1132
1142
|
logger.break();
|
|
1133
1143
|
logger.log("Cancelled.");
|
|
@@ -1152,7 +1162,8 @@ const resolveCliScanOptions = (flags, userConfig, programInstance) => {
|
|
|
1152
1162
|
deadCode: isCliOverride("deadCode") ? flags.deadCode : userConfig?.deadCode ?? flags.deadCode,
|
|
1153
1163
|
verbose: isCliOverride("verbose") ? Boolean(flags.verbose) : userConfig?.verbose ?? false,
|
|
1154
1164
|
scoreOnly: flags.score,
|
|
1155
|
-
report: flags.report
|
|
1165
|
+
report: flags.report,
|
|
1166
|
+
fast: isCliOverride("fast") ? Boolean(flags.fast) : userConfig?.fast ?? false
|
|
1156
1167
|
};
|
|
1157
1168
|
};
|
|
1158
1169
|
const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isScoreOnly) => {
|
|
@@ -1170,7 +1181,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
1170
1181
|
if (isScoreOnly) return false;
|
|
1171
1182
|
return false;
|
|
1172
1183
|
};
|
|
1173
|
-
const program = new Command().name("angular-doctor").description("Diagnose Angular codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("--report [path]", "write a markdown report (optional output path)").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").action(async (directory, flags) => {
|
|
1184
|
+
const program = new Command().name("angular-doctor").description("Diagnose Angular codebase health").version(VERSION, "-v, --version", "display the version number").argument("[directory]", "project directory to scan", ".").option("--no-lint", "skip linting").option("--no-dead-code", "skip dead code detection").option("--verbose", "show file details per rule").option("--score", "output only the score").option("--report [path]", "write a markdown report (optional output path)").option("--fast", "speed up by skipping dead code and type-aware lint").option("-y, --yes", "skip prompts, scan all workspace projects").option("--project <name>", "select workspace project (comma-separated for multiple)").option("--diff [base]", "scan only files changed vs base branch").action(async (directory, flags) => {
|
|
1174
1185
|
const isScoreOnly = flags.score;
|
|
1175
1186
|
try {
|
|
1176
1187
|
const resolvedDirectory = path.resolve(directory);
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["main"],"sources":["../src/constants.ts","../src/utils/calculate-score.ts","../src/utils/colorize-by-score.ts","../src/utils/filter-diagnostics.ts","../src/utils/combine-diagnostics.ts","../src/utils/discover-project.ts","../src/utils/highlighter.ts","../src/utils/logger.ts","../src/utils/framed-box.ts","../src/utils/group-by.ts","../src/utils/indent-multiline-text.ts","../src/utils/load-config.ts","../src/utils/run-eslint.ts","../src/utils/run-knip.ts","../src/utils/spinner.ts","../src/scan.ts","../src/utils/get-diff-files.ts","../src/utils/handle-error.ts","../src/utils/select-projects.ts","../src/cli.ts"],"sourcesContent":["export const SOURCE_FILE_PATTERN = /\\.ts$/;\r\n\r\nexport const MILLISECONDS_PER_SECOND = 1000;\r\n\r\nexport const PERFECT_SCORE = 100;\r\n\r\nexport const SCORE_GOOD_THRESHOLD = 75;\r\n\r\nexport const SCORE_OK_THRESHOLD = 50;\r\n\r\nexport const SCORE_BAR_WIDTH_CHARS = 50;\r\n\r\nexport const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;\r\n\r\nexport const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;\r\n\r\nexport const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\r\n\r\nexport const MAX_KNIP_RETRIES = 5;\r\n\r\nexport const ERROR_RULE_PENALTY = 1.5;\r\n\r\nexport const WARNING_RULE_PENALTY = 0.75;\r\n\r\nexport const DEFAULT_BRANCH_CANDIDATES = [\"main\", \"master\"];\r\n","import {\r\n ERROR_RULE_PENALTY,\r\n PERFECT_SCORE,\r\n SCORE_GOOD_THRESHOLD,\r\n SCORE_OK_THRESHOLD,\r\n WARNING_RULE_PENALTY,\r\n} from \"../constants.js\";\r\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\r\n\r\nexport const getScoreLabel = (score: number): string => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return \"Great\";\r\n if (score >= SCORE_OK_THRESHOLD) return \"Needs work\";\r\n return \"Critical\";\r\n};\r\n\r\nconst countUniqueRules = (\r\n diagnostics: Diagnostic[],\r\n): { errorRuleCount: number; warningRuleCount: number } => {\r\n const errorRules = new Set<string>();\r\n const warningRules = new Set<string>();\r\n\r\n for (const diagnostic of diagnostics) {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (diagnostic.severity === \"error\") {\r\n errorRules.add(ruleKey);\r\n } else {\r\n warningRules.add(ruleKey);\r\n }\r\n }\r\n\r\n return { errorRuleCount: errorRules.size, warningRuleCount: warningRules.size };\r\n};\r\n\r\nexport const calculateScore = (diagnostics: Diagnostic[]): ScoreResult => {\r\n const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);\r\n const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;\r\n const score = Math.max(0, Math.round(PERFECT_SCORE - penalty));\r\n return { score, label: getScoreLabel(score) };\r\n};\r\n","import pc from \"picocolors\";\r\nimport { SCORE_GOOD_THRESHOLD, SCORE_OK_THRESHOLD } from \"../constants.js\";\r\n\r\nexport const colorizeByScore = (text: string, score: number): string => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return pc.green(text);\r\n if (score >= SCORE_OK_THRESHOLD) return pc.yellow(text);\r\n return pc.red(text);\r\n};\r\n","import type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\n\r\nconst matchesGlob = (filePath: string, pattern: string): boolean => {\r\n const escapedPattern = pattern\r\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\r\n .replace(/\\*\\*/g, \".*\")\r\n .replace(/\\*/g, \"[^/]*\");\r\n return new RegExp(`^${escapedPattern}$`).test(filePath);\r\n};\r\n\r\nexport const filterIgnoredDiagnostics = (\r\n diagnostics: Diagnostic[],\r\n config: AngularDoctorConfig,\r\n): Diagnostic[] => {\r\n const ignoredRules = new Set(config.ignore?.rules ?? []);\r\n const ignoredFilePatterns = config.ignore?.files ?? [];\r\n\r\n return diagnostics.filter((diagnostic) => {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (ignoredRules.has(ruleKey)) return false;\r\n\r\n if (ignoredFilePatterns.some((pattern) => matchesGlob(diagnostic.filePath, pattern))) {\r\n return false;\r\n }\r\n\r\n return true;\r\n });\r\n};\r\n","import { SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\nimport { filterIgnoredDiagnostics } from \"./filter-diagnostics.js\";\r\n\r\nexport const computeIncludePaths = (includePaths: string[]): string[] | undefined =>\r\n includePaths.length > 0\r\n ? includePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath))\r\n : undefined;\r\n\r\nexport const combineDiagnostics = (\r\n lintDiagnostics: Diagnostic[],\r\n deadCodeDiagnostics: Diagnostic[],\r\n userConfig: AngularDoctorConfig | null,\r\n): Diagnostic[] => {\r\n const allDiagnostics = [...lintDiagnostics, ...deadCodeDiagnostics];\r\n return userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { spawnSync } from \"node:child_process\";\r\nimport { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularFramework, PackageJson, ProjectInfo, WorkspacePackage } from \"../types.js\";\r\n\r\nconst ANGULAR_FRAMEWORK_PACKAGES: Record<string, AngularFramework> = {\r\n \"@nrwl/angular\": \"nx\",\r\n \"@nx/angular\": \"nx\",\r\n \"@analogjs/platform\": \"analog\",\r\n \"@ionic/angular\": \"ionic\",\r\n \"@angular/ssr\": \"universal\",\r\n \"@nguniversal/express-engine\": \"universal\",\r\n};\r\n\r\nconst ANGULAR_FRAMEWORK_DISPLAY_NAMES: Record<AngularFramework, string> = {\r\n \"angular-cli\": \"Angular CLI\",\r\n nx: \"Nx\",\r\n analog: \"AnalogJS\",\r\n ionic: \"Ionic\",\r\n universal: \"Angular SSR\",\r\n unknown: \"Angular\",\r\n};\r\n\r\nexport const formatFrameworkName = (framework: AngularFramework): string =>\r\n ANGULAR_FRAMEWORK_DISPLAY_NAMES[framework];\r\n\r\nconst readPackageJson = (filePath: string): PackageJson => {\r\n try {\r\n const content = fs.readFileSync(filePath, \"utf-8\");\r\n return JSON.parse(content) as PackageJson;\r\n } catch {\r\n return {};\r\n }\r\n};\r\n\r\nconst collectAllDependencies = (packageJson: PackageJson): Record<string, string> => ({\r\n ...packageJson.peerDependencies,\r\n ...packageJson.dependencies,\r\n ...packageJson.devDependencies,\r\n});\r\n\r\nconst detectFramework = (dependencies: Record<string, string>): AngularFramework => {\r\n for (const [packageName, frameworkName] of Object.entries(ANGULAR_FRAMEWORK_PACKAGES)) {\r\n if (dependencies[packageName]) {\r\n return frameworkName;\r\n }\r\n }\r\n if (dependencies[\"@angular/cli\"] || dependencies[\"@angular-devkit/build-angular\"] || dependencies[\"@angular-devkit/core\"]) {\r\n return \"angular-cli\";\r\n }\r\n return \"unknown\";\r\n};\r\n\r\nconst detectAngularVersion = (dependencies: Record<string, string>): string | null =>\r\n dependencies[\"@angular/core\"] ?? null;\r\n\r\nconst detectStandaloneComponents = (packageJson: PackageJson): boolean => {\r\n const deps = collectAllDependencies(packageJson);\r\n const angularVersion = deps[\"@angular/core\"];\r\n if (!angularVersion) return false;\r\n // Angular 14+ supports standalone components (standalone: true flag)\r\n // Angular 17+ makes standalone the default\r\n const majorVersion = parseInt(angularVersion.match(/\\d+/)?.[0] ?? \"\", 10);\r\n return !isNaN(majorVersion) && majorVersion >= 14;\r\n};\r\n\r\nconst countSourceFiles = (rootDirectory: string): number => {\r\n const result = spawnSync(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\r\n cwd: rootDirectory,\r\n encoding: \"utf-8\",\r\n maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,\r\n });\r\n\r\n if (result.error || result.status !== 0) {\r\n return 0;\r\n }\r\n\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;\r\n};\r\n\r\nconst hasAngularDependency = (packageJson: PackageJson): boolean => {\r\n const allDependencies = collectAllDependencies(packageJson);\r\n return Boolean(allDependencies[\"@angular/core\"]);\r\n};\r\n\r\n/**\r\n * Walks up the directory tree from `directory` until it finds a `package.json`.\r\n * Returns the directory containing the package.json, or null if not found.\r\n */\r\nconst findNearestPackageJsonDir = (directory: string): string | null => {\r\n let current = directory;\r\n while (true) {\r\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\r\n const parent = path.dirname(current);\r\n if (parent === current) return null;\r\n current = parent;\r\n }\r\n};\r\n\r\nexport const discoverProject = (directory: string): ProjectInfo => {\r\n // First try the given directory, then walk up to find the nearest package.json\r\n const packageJsonDir = fs.existsSync(path.join(directory, \"package.json\"))\r\n ? directory\r\n : findNearestPackageJsonDir(directory);\r\n\r\n if (!packageJsonDir) {\r\n throw new Error(`No package.json found in ${directory} or any parent directory`);\r\n }\r\n\r\n const packageJson = readPackageJson(path.join(packageJsonDir, \"package.json\"));\r\n const allDeps = collectAllDependencies(packageJson);\r\n const angularVersion = detectAngularVersion(allDeps);\r\n const framework = detectFramework(allDeps);\r\n\r\n // tsconfig.json — check the project directory first, then the package.json directory\r\n const hasTypeScript =\r\n fs.existsSync(path.join(directory, \"tsconfig.json\")) ||\r\n fs.existsSync(path.join(packageJsonDir, \"tsconfig.json\"));\r\n\r\n const hasStandaloneComponents = detectStandaloneComponents(packageJson);\r\n const sourceFileCount = countSourceFiles(directory);\r\n\r\n // Use the Angular project name from angular.json if possible, otherwise from package.json\r\n const angularJsonPath = path.join(packageJsonDir, \"angular.json\");\r\n let projectName = packageJson.name ?? path.basename(directory);\r\n if (packageJsonDir !== directory && fs.existsSync(angularJsonPath)) {\r\n // Use the directory name as a more meaningful project name for workspace sub-projects\r\n projectName = path.basename(directory);\r\n }\r\n\r\n return {\r\n rootDirectory: directory,\r\n projectName,\r\n angularVersion,\r\n framework,\r\n hasTypeScript,\r\n hasStandaloneComponents,\r\n sourceFileCount,\r\n };\r\n};\r\n\r\nconst parsePnpmWorkspacePatterns = (rootDirectory: string): string[] => {\r\n const workspacePath = path.join(rootDirectory, \"pnpm-workspace.yaml\");\r\n if (!fs.existsSync(workspacePath)) return [];\r\n\r\n const content = fs.readFileSync(workspacePath, \"utf-8\");\r\n const patterns: string[] = [];\r\n let isInsidePackagesBlock = false;\r\n\r\n for (const line of content.split(\"\\n\")) {\r\n const trimmed = line.trim();\r\n if (trimmed === \"packages:\") {\r\n isInsidePackagesBlock = true;\r\n continue;\r\n }\r\n if (isInsidePackagesBlock && trimmed.startsWith(\"-\")) {\r\n patterns.push(trimmed.replace(/^-\\s*/, \"\").replace(/[\"']/g, \"\"));\r\n } else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith(\"#\")) {\r\n isInsidePackagesBlock = false;\r\n }\r\n }\r\n\r\n return patterns;\r\n};\r\n\r\nconst getWorkspacePatterns = (rootDirectory: string, packageJson: PackageJson): string[] => {\r\n const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);\r\n if (pnpmPatterns.length > 0) return pnpmPatterns;\r\n\r\n if (Array.isArray(packageJson.workspaces)) {\r\n return packageJson.workspaces;\r\n }\r\n\r\n if (packageJson.workspaces?.packages) {\r\n return packageJson.workspaces.packages;\r\n }\r\n\r\n return [];\r\n};\r\n\r\nconst resolveWorkspaceDirectories = (rootDirectory: string, pattern: string): string[] => {\r\n const cleanPattern = pattern.replace(/[\"']/g, \"\").replace(/\\/\\*\\*$/, \"/*\");\r\n\r\n if (!cleanPattern.includes(\"*\")) {\r\n const directoryPath = path.join(rootDirectory, cleanPattern);\r\n if (fs.existsSync(directoryPath) && fs.existsSync(path.join(directoryPath, \"package.json\"))) {\r\n return [directoryPath];\r\n }\r\n return [];\r\n }\r\n\r\n const wildcardIndex = cleanPattern.indexOf(\"*\");\r\n const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));\r\n const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);\r\n\r\n if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) {\r\n return [];\r\n }\r\n\r\n return fs\r\n .readdirSync(baseDirectory)\r\n .map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard))\r\n .filter(\r\n (entryPath) =>\r\n fs.existsSync(entryPath) &&\r\n fs.statSync(entryPath).isDirectory() &&\r\n fs.existsSync(path.join(entryPath, \"package.json\")),\r\n );\r\n};\r\n\r\nexport const listWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (!fs.existsSync(packageJsonPath)) return [];\r\n\r\n const packageJson = readPackageJson(packageJsonPath);\r\n const patterns = getWorkspacePatterns(rootDirectory, packageJson);\r\n if (patterns.length === 0) return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const pattern of patterns) {\r\n const directories = resolveWorkspaceDirectories(rootDirectory, pattern);\r\n for (const workspaceDirectory of directories) {\r\n const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, \"package.json\"));\r\n\r\n if (!hasAngularDependency(workspacePackageJson)) continue;\r\n\r\n const name = workspacePackageJson.name ?? path.basename(workspaceDirectory);\r\n packages.push({ name, directory: workspaceDirectory });\r\n }\r\n }\r\n\r\n return packages;\r\n};\r\n\r\ninterface AngularWorkspaceProject {\r\n projectType?: string;\r\n root?: string;\r\n}\r\n\r\ninterface AngularWorkspace {\r\n version?: number;\r\n projects?: Record<string, AngularWorkspaceProject | string>;\r\n}\r\n\r\n/**\r\n * Reads `angular.json` (Angular CLI workspace config) and returns one\r\n * WorkspacePackage per project whose root directory contains an Angular dependency.\r\n */\r\nexport const listAngularWorkspaceProjects = (rootDirectory: string): WorkspacePackage[] => {\r\n const angularJsonPath = path.join(rootDirectory, \"angular.json\");\r\n if (!fs.existsSync(angularJsonPath)) return [];\r\n\r\n let workspace: AngularWorkspace;\r\n try {\r\n workspace = JSON.parse(fs.readFileSync(angularJsonPath, \"utf-8\")) as AngularWorkspace;\r\n } catch {\r\n return [];\r\n }\r\n\r\n if (!workspace.projects || typeof workspace.projects !== \"object\") return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const [name, projectConfig] of Object.entries(workspace.projects)) {\r\n // Older angular.json formats store the root as a plain string\r\n const root =\r\n typeof projectConfig === \"string\"\r\n ? projectConfig\r\n : (projectConfig.root ?? \"\");\r\n\r\n const projectDirectory = root ? path.resolve(rootDirectory, root) : rootDirectory;\r\n\r\n // Only include directories that actually exist\r\n if (!fs.existsSync(projectDirectory) || !fs.statSync(projectDirectory).isDirectory()) {\r\n continue;\r\n }\r\n\r\n packages.push({ name, directory: projectDirectory });\r\n }\r\n\r\n return packages;\r\n};\r\n","import pc from \"picocolors\";\r\n\r\nexport const highlighter = {\r\n error: pc.red,\r\n warn: pc.yellow,\r\n info: pc.cyan,\r\n success: pc.green,\r\n dim: pc.dim,\r\n};\r\n","import { highlighter } from \"./highlighter.js\";\r\n\r\nexport const logger = {\r\n log: (message: string) => process.stdout.write(`${message}\\n`),\r\n break: () => process.stdout.write(\"\\n\"),\r\n dim: (message: string) => process.stdout.write(`${highlighter.dim(message)}\\n`),\r\n warn: (message: string) => process.stdout.write(`${highlighter.warn(message)}\\n`),\r\n error: (message: string) => process.stderr.write(`${highlighter.error(message)}\\n`),\r\n success: (message: string) => process.stdout.write(`${highlighter.success(message)}\\n`),\r\n info: (message: string) => process.stdout.write(`${highlighter.info(message)}\\n`),\r\n};\r\n","import { SUMMARY_BOX_HORIZONTAL_PADDING_CHARS, SUMMARY_BOX_OUTER_INDENT_CHARS } from \"../constants.js\";\r\nimport { highlighter } from \"./highlighter.js\";\r\nimport { logger } from \"./logger.js\";\r\n\r\nexport interface FramedLine {\r\n plainText: string;\r\n renderedText: string;\r\n}\r\n\r\nexport const createFramedLine = (plainText: string, renderedText?: string): FramedLine => ({\r\n plainText,\r\n renderedText: renderedText ?? plainText,\r\n});\r\n\r\nconst calculateMaxWidth = (lines: FramedLine[]): number =>\r\n Math.max(...lines.map((line) => line.plainText.length));\r\n\r\nconst buildBoxRow = (content: string, width: number): string => {\r\n const padding = \" \".repeat(SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n const rightPad = \" \".repeat(width - content.length + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n return `│${padding}${content}${rightPad}│`;\r\n};\r\n\r\nconst buildRenderedBoxRow = (plainContent: string, renderedContent: string, width: number): string => {\r\n const padding = \" \".repeat(SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n const rightPad = \" \".repeat(width - plainContent.length + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n return highlighter.dim(`│`) + padding + renderedContent + rightPad + highlighter.dim(`│`);\r\n};\r\n\r\nexport const renderFramedBoxString = (lines: FramedLine[]): string => {\r\n const width = calculateMaxWidth(lines);\r\n const indent = \" \".repeat(SUMMARY_BOX_OUTER_INDENT_CHARS);\r\n const topBorder = `┌${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┐`;\r\n const bottomBorder = `└${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┘`;\r\n\r\n const rows = lines.map((line) => buildBoxRow(line.plainText, width));\r\n return [topBorder, ...rows, bottomBorder]\r\n .map((row) => `${indent}${row}`)\r\n .join(\"\\n\");\r\n};\r\n\r\nexport const printFramedBox = (lines: FramedLine[]): void => {\r\n const width = calculateMaxWidth(lines);\r\n const indent = \" \".repeat(SUMMARY_BOX_OUTER_INDENT_CHARS);\r\n const topBorder = highlighter.dim(`┌${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┐`);\r\n const bottomBorder = highlighter.dim(`└${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┘`);\r\n\r\n logger.log(`${indent}${topBorder}`);\r\n for (const line of lines) {\r\n logger.log(`${indent}${buildRenderedBoxRow(line.plainText, line.renderedText, width)}`);\r\n }\r\n logger.log(`${indent}${bottomBorder}`);\r\n};\r\n","export const groupBy = <T>(items: T[], key: (item: T) => string): Map<string, T[]> => {\r\n const map = new Map<string, T[]>();\r\n for (const item of items) {\r\n const k = key(item);\r\n const group = map.get(k) ?? [];\r\n group.push(item);\r\n map.set(k, group);\r\n }\r\n return map;\r\n};\r\n","export const indentMultilineText = (text: string, indent: string): string =>\r\n text\r\n .split(\"\\n\")\r\n .map((line) => `${indent}${line}`)\r\n .join(\"\\n\");\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { AngularDoctorConfig } from \"../types.js\";\r\n\r\nconst CONFIG_FILENAME = \"angular-doctor.config.json\";\r\nconst PACKAGE_JSON_CONFIG_KEY = \"angularDoctor\";\r\n\r\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\r\n typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n\r\nexport const loadConfig = (rootDirectory: string): AngularDoctorConfig | null => {\r\n const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);\r\n\r\n if (fs.existsSync(configFilePath)) {\r\n try {\r\n const fileContent = fs.readFileSync(configFilePath, \"utf-8\");\r\n const parsed: unknown = JSON.parse(fileContent);\r\n if (!isPlainObject(parsed)) {\r\n console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);\r\n return null;\r\n }\r\n return parsed as AngularDoctorConfig;\r\n } catch (error) {\r\n console.warn(\r\n `Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n return null;\r\n }\r\n }\r\n\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (fs.existsSync(packageJsonPath)) {\r\n try {\r\n const fileContent = fs.readFileSync(packageJsonPath, \"utf-8\");\r\n const packageJson = JSON.parse(fileContent) as Record<string, unknown>;\r\n const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];\r\n if (isPlainObject(embeddedConfig)) {\r\n return embeddedConfig as AngularDoctorConfig;\r\n }\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { ESLint, type Linter } from \"eslint\";\r\nimport angularEslintPlugin from \"@angular-eslint/eslint-plugin\";\r\nimport tsEslint from \"typescript-eslint\";\r\nimport type { Diagnostic } from \"../types.js\";\r\n\r\n// Rule category mapping\r\nconst RULE_CATEGORY_MAP: Record<string, string> = {\r\n // Angular component best practices\r\n \"@angular-eslint/component-class-suffix\": \"Components\",\r\n \"@angular-eslint/directive-class-suffix\": \"Components\",\r\n \"@angular-eslint/pipe-prefix\": \"Components\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"Components\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Components\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"Components\",\r\n \"@angular-eslint/consistent-component-styles\": \"Components\",\r\n\r\n // Angular performance\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Performance\",\r\n \"@angular-eslint/no-output-native\": \"Performance\",\r\n\r\n // Angular architecture / correctness\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/contextual-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/no-forward-ref\": \"Architecture\",\r\n \"@angular-eslint/no-input-rename\": \"Architecture\",\r\n \"@angular-eslint/no-output-rename\": \"Architecture\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/prefer-standalone\": \"Architecture\",\r\n\r\n // TypeScript quality\r\n \"@typescript-eslint/no-explicit-any\": \"TypeScript\",\r\n \"@typescript-eslint/no-unused-vars\": \"Dead Code\",\r\n};\r\n\r\n// Rule severity mapping\r\nconst RULE_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/component-class-suffix\": \"warning\",\r\n \"@angular-eslint/directive-class-suffix\": \"warning\",\r\n \"@angular-eslint/pipe-prefix\": \"warning\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warning\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warning\",\r\n \"@angular-eslint/consistent-component-styles\": \"warning\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warning\",\r\n \"@angular-eslint/no-forward-ref\": \"warning\",\r\n \"@angular-eslint/no-input-rename\": \"warning\",\r\n \"@angular-eslint/no-output-rename\": \"warning\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/prefer-standalone\": \"warning\",\r\n \"@typescript-eslint/no-explicit-any\": \"warning\",\r\n \"@typescript-eslint/no-unused-vars\": \"warning\",\r\n};\r\n\r\n// Human-readable messages and help text\r\nconst RULE_MESSAGE_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\": \"Component class should end with 'Component'\",\r\n \"@angular-eslint/directive-class-suffix\": \"Directive class should end with 'Directive'\",\r\n \"@angular-eslint/pipe-prefix\": \"Pipe name should have a consistent prefix\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"Pipe class must implement PipeTransform interface\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Remove empty lifecycle methods\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"Implement the lifecycle interface for lifecycle hooks\",\r\n \"@angular-eslint/consistent-component-styles\": \"Use consistent styles type in component decorator\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Use OnPush change detection for better performance\",\r\n \"@angular-eslint/no-output-native\": \"Avoid shadowing native DOM events in output names\",\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"Lifecycle hooks DoCheck and OnChanges cannot be used together\",\r\n \"@angular-eslint/contextual-lifecycle\": \"Lifecycle hook is not available in this context\",\r\n \"@angular-eslint/no-forward-ref\": \"Avoid using forwardRef — restructure to avoid circular dependency\",\r\n \"@angular-eslint/no-input-rename\": \"Avoid renaming directive inputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-output-rename\": \"Avoid renaming directive outputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"Use @Input() decorator instead of inputs metadata property\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"Use @Output() decorator instead of outputs metadata property\",\r\n \"@angular-eslint/prefer-standalone\": \"Prefer standalone components over NgModule-based components\",\r\n \"@typescript-eslint/no-explicit-any\": \"Avoid 'any' type — use specific types for better type safety\",\r\n \"@typescript-eslint/no-unused-vars\": \"Remove unused variable declaration\",\r\n};\r\n\r\nconst RULE_HELP_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\":\r\n \"Add 'Component' suffix: `export class UserProfileComponent { }`\",\r\n \"@angular-eslint/directive-class-suffix\":\r\n \"Add 'Directive' suffix: `export class HighlightDirective { }`\",\r\n \"@angular-eslint/use-pipe-transform-interface\":\r\n \"Implement PipeTransform: `export class MyPipe implements PipeTransform { transform(value: unknown) { } }`\",\r\n \"@angular-eslint/no-empty-lifecycle-method\":\r\n \"Remove the empty lifecycle method or add logic to it\",\r\n \"@angular-eslint/use-lifecycle-interface\":\r\n \"Add the interface: `export class MyComponent implements OnInit, OnDestroy { }`\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\":\r\n \"Add to decorator: `@Component({ changeDetection: ChangeDetectionStrategy.OnPush })`\",\r\n \"@angular-eslint/no-output-native\":\r\n \"Rename the output: use a descriptive name like `(valueChange)` instead of `(click)` or `(change)`\",\r\n \"@angular-eslint/no-forward-ref\":\r\n \"Restructure your code to avoid circular dependencies, or use `inject()` with a lazy function\",\r\n \"@angular-eslint/no-input-rename\":\r\n \"Remove the alias: `@Input() myProp: string` instead of `@Input('myAlias') myProp: string`\",\r\n \"@angular-eslint/no-output-rename\":\r\n \"Remove the alias: `@Output() myEvent = new EventEmitter()` instead of aliased version\",\r\n \"@angular-eslint/no-inputs-metadata-property\":\r\n \"Use `@Input() myProp: string` decorator on the property instead of `inputs: ['myProp']` in the decorator metadata\",\r\n \"@angular-eslint/no-outputs-metadata-property\":\r\n \"Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata\",\r\n \"@angular-eslint/prefer-standalone\":\r\n \"Add `standalone: true` to component: `@Component({ standalone: true, ... })`\",\r\n \"@typescript-eslint/no-explicit-any\":\r\n \"Replace `any` with a specific type or `unknown` if the type is truly unknown\",\r\n \"@typescript-eslint/no-unused-vars\":\r\n \"Remove the unused variable or prefix with `_` to indicate it's intentionally unused\",\r\n};\r\n\r\nconst buildEslintConfig = (\r\n hasTypeScript: boolean,\r\n tsconfigPath: string | null,\r\n): Linter.Config[] => {\r\n const languageOptions: Linter.Config[\"languageOptions\"] = {\r\n parser: tsEslint.parser as Linter.Parser,\r\n parserOptions: {\r\n ecmaVersion: \"latest\",\r\n sourceType: \"module\",\r\n ...(hasTypeScript && tsconfigPath ? { project: tsconfigPath } : {}),\r\n },\r\n };\r\n\r\n const angularRules: Linter.RulesRecord = {\r\n \"@angular-eslint/component-class-suffix\": \"warn\",\r\n \"@angular-eslint/directive-class-suffix\": \"warn\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warn\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warn\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warn\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/no-forward-ref\": \"warn\",\r\n \"@angular-eslint/no-input-rename\": \"warn\",\r\n \"@angular-eslint/no-output-rename\": \"warn\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warn\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warn\",\r\n };\r\n\r\n const tsRules: Linter.RulesRecord = {\r\n \"@typescript-eslint/no-explicit-any\": \"warn\",\r\n };\r\n\r\n return [\r\n {\r\n files: [\"**/*.ts\"],\r\n plugins: {\r\n \"@angular-eslint\": angularEslintPlugin as unknown as ESLint.Plugin,\r\n \"@typescript-eslint\": tsEslint.plugin as unknown as ESLint.Plugin,\r\n },\r\n languageOptions,\r\n rules: {\r\n ...angularRules,\r\n ...tsRules,\r\n },\r\n },\r\n ];\r\n};\r\n\r\nconst mapEslintSeverity = (severity: number, ruleId: string | null): \"error\" | \"warning\" => {\r\n if (ruleId && RULE_SEVERITY_MAP[ruleId]) {\r\n return RULE_SEVERITY_MAP[ruleId];\r\n }\r\n return severity === 2 ? \"error\" : \"warning\";\r\n};\r\n\r\nconst resolveDiagnosticCategory = (ruleId: string): string =>\r\n RULE_CATEGORY_MAP[ruleId] ?? \"Other\";\r\n\r\nconst resolveMessage = (ruleId: string, defaultMessage: string): string =>\r\n RULE_MESSAGE_MAP[ruleId] ?? defaultMessage;\r\n\r\nconst resolveHelp = (ruleId: string): string => RULE_HELP_MAP[ruleId] ?? \"\";\r\n\r\nconst parsePluginAndRule = (ruleId: string): { plugin: string; rule: string } => {\r\n // e.g. \"@angular-eslint/component-class-suffix\" -> plugin: \"@angular-eslint\", rule: \"component-class-suffix\"\r\n // e.g. \"@typescript-eslint/no-explicit-any\" -> plugin: \"@typescript-eslint\", rule: \"no-explicit-any\"\r\n const match = ruleId.match(/^(@[^/]+\\/[^/]+|[^/]+)\\/(.+)$/);\r\n if (match) {\r\n return { plugin: match[1], rule: match[2] };\r\n }\r\n return { plugin: \"eslint\", rule: ruleId };\r\n};\r\n\r\nexport const runEslint = async (\r\n rootDirectory: string,\r\n hasTypeScript: boolean,\r\n includePaths?: string[],\r\n): Promise<Diagnostic[]> => {\r\n if (includePaths !== undefined && includePaths.length === 0) {\r\n return [];\r\n }\r\n\r\n const tsconfigPath = hasTypeScript\r\n ? path.join(rootDirectory, \"tsconfig.json\")\r\n : null;\r\n\r\n const eslint = new ESLint({\r\n cwd: rootDirectory,\r\n overrideConfigFile: true,\r\n overrideConfig: buildEslintConfig(\r\n hasTypeScript,\r\n tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null,\r\n ),\r\n ignore: true,\r\n });\r\n\r\n const patterns = includePaths ?? [\"**/*.ts\"];\r\n\r\n let results: ESLint.LintResult[];\r\n try {\r\n results = await eslint.lintFiles(patterns);\r\n } catch {\r\n return [];\r\n }\r\n\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const result of results) {\r\n for (const message of result.messages) {\r\n if (!message.ruleId) continue;\r\n const { plugin, rule } = parsePluginAndRule(message.ruleId);\r\n const ruleKey = message.ruleId;\r\n\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, result.filePath),\r\n plugin,\r\n rule,\r\n severity: mapEslintSeverity(message.severity, ruleKey),\r\n message: resolveMessage(ruleKey, message.message),\r\n help: resolveHelp(ruleKey),\r\n line: message.line ?? 0,\r\n column: message.column ?? 0,\r\n category: resolveDiagnosticCategory(ruleKey),\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { main } from \"knip\";\r\nimport { createOptions } from \"knip/session\";\r\nimport { MAX_KNIP_RETRIES } from \"../constants.js\";\r\nimport type { Diagnostic, KnipIssueRecords, KnipResults } from \"../types.js\";\r\n\r\nconst KNIP_CATEGORY_MAP: Record<string, string> = {\r\n files: \"Dead Code\",\r\n exports: \"Dead Code\",\r\n types: \"Dead Code\",\r\n duplicates: \"Dead Code\",\r\n};\r\n\r\nconst KNIP_MESSAGE_MAP: Record<string, string> = {\r\n files: \"Unused file\",\r\n exports: \"Unused export\",\r\n types: \"Unused type\",\r\n duplicates: \"Duplicate export\",\r\n};\r\n\r\nconst KNIP_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n files: \"warning\",\r\n exports: \"warning\",\r\n types: \"warning\",\r\n duplicates: \"warning\",\r\n};\r\n\r\nconst collectIssueRecords = (\r\n records: KnipIssueRecords,\r\n issueType: string,\r\n rootDirectory: string,\r\n): Diagnostic[] => {\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const issues of Object.values(records)) {\r\n for (const issue of Object.values(issues)) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, issue.filePath),\r\n plugin: \"knip\",\r\n rule: issueType,\r\n severity: KNIP_SEVERITY_MAP[issueType] ?? \"warning\",\r\n message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,\r\n help: \"\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[issueType] ?? \"Dead Code\",\r\n weight: 1,\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n\r\n// HACK: knip triggers dotenv which logs to stdout/stderr via console methods\r\nconst silenced = async <T>(fn: () => Promise<T>): Promise<T> => {\r\n const originalLog = console.log;\r\n const originalInfo = console.info;\r\n const originalWarn = console.warn;\r\n const originalError = console.error;\r\n console.log = () => {};\r\n console.info = () => {};\r\n console.warn = () => {};\r\n console.error = () => {};\r\n try {\r\n return await fn();\r\n } finally {\r\n console.log = originalLog;\r\n console.info = originalInfo;\r\n console.warn = originalWarn;\r\n console.error = originalError;\r\n }\r\n};\r\n\r\nconst CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\\/([a-z-]+)\\.config\\./;\r\n\r\nconst extractFailedPluginName = (error: unknown): string | null => {\r\n const match = String(error).match(CONFIG_LOADING_ERROR_PATTERN);\r\n return match?.[1] ?? null;\r\n};\r\n\r\nconst runKnipWithOptions = async (\r\n knipCwd: string,\r\n workspaceName?: string,\r\n): Promise<KnipResults> => {\r\n const options = await silenced(() =>\r\n createOptions({\r\n cwd: knipCwd,\r\n isShowProgress: false,\r\n ...(workspaceName ? { workspace: workspaceName } : {}),\r\n }),\r\n );\r\n\r\n const parsedConfig = options.parsedConfig as Record<string, unknown>;\r\n\r\n for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) {\r\n try {\r\n return (await silenced(() => main(options))) as KnipResults;\r\n } catch (error) {\r\n const failedPlugin = extractFailedPluginName(error);\r\n if (!failedPlugin || attempt === MAX_KNIP_RETRIES) {\r\n throw error;\r\n }\r\n parsedConfig[failedPlugin] = false;\r\n }\r\n }\r\n\r\n throw new Error(\"Unreachable\");\r\n};\r\n\r\nconst hasNodeModules = (directory: string): boolean => {\r\n const nodeModulesPath = path.join(directory, \"node_modules\");\r\n return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();\r\n};\r\n\r\nexport const runKnip = async (rootDirectory: string): Promise<Diagnostic[]> => {\r\n if (!hasNodeModules(rootDirectory)) {\r\n return [];\r\n }\r\n\r\n const knipResult = await runKnipWithOptions(rootDirectory);\r\n\r\n const { issues } = knipResult;\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const unusedFile of issues.files) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, unusedFile),\r\n plugin: \"knip\",\r\n rule: \"files\",\r\n severity: KNIP_SEVERITY_MAP[\"files\"],\r\n message: KNIP_MESSAGE_MAP[\"files\"],\r\n help: \"This file is not imported by any other file in the project.\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[\"files\"],\r\n weight: 1,\r\n });\r\n }\r\n\r\n const recordTypes = [\"exports\", \"types\", \"duplicates\"] as const;\r\n\r\n for (const issueType of recordTypes) {\r\n diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import ora from \"ora\";\r\n\r\nexport const spinner = (text: string) => ora({ text, isSilent: !process.stdout.isTTY });\r\n","import { randomUUID } from \"node:crypto\";\r\nimport { mkdirSync, writeFileSync } from \"node:fs\";\r\nimport { tmpdir } from \"node:os\";\r\nimport { dirname, extname, isAbsolute, join, resolve } from \"node:path\";\r\nimport { performance } from \"node:perf_hooks\";\r\nimport {\r\n MILLISECONDS_PER_SECOND,\r\n PERFECT_SCORE,\r\n SCORE_BAR_WIDTH_CHARS,\r\n SCORE_GOOD_THRESHOLD,\r\n SCORE_OK_THRESHOLD,\r\n} from \"./constants.js\";\r\nimport type {\r\n AngularDoctorConfig,\r\n Diagnostic,\r\n ProjectInfo,\r\n ScanOptions,\r\n ScanResult,\r\n ScoreResult,\r\n} from \"./types.js\";\r\nimport { calculateScore } from \"./utils/calculate-score.js\";\r\nimport { colorizeByScore } from \"./utils/colorize-by-score.js\";\r\nimport {\r\n combineDiagnostics,\r\n computeIncludePaths,\r\n} from \"./utils/combine-diagnostics.js\";\r\nimport {\r\n discoverProject,\r\n formatFrameworkName,\r\n} from \"./utils/discover-project.js\";\r\nimport {\r\n type FramedLine,\r\n createFramedLine,\r\n printFramedBox,\r\n} from \"./utils/framed-box.js\";\r\nimport { groupBy } from \"./utils/group-by.js\";\r\nimport { highlighter } from \"./utils/highlighter.js\";\r\nimport { indentMultilineText } from \"./utils/indent-multiline-text.js\";\r\nimport { loadConfig } from \"./utils/load-config.js\";\r\nimport { logger } from \"./utils/logger.js\";\r\nimport { runEslint } from \"./utils/run-eslint.js\";\r\nimport { runKnip } from \"./utils/run-knip.js\";\r\nimport { spinner } from \"./utils/spinner.js\";\r\n\r\ninterface ScoreBarSegments {\r\n filledSegment: string;\r\n emptySegment: string;\r\n}\r\n\r\nconst SEVERITY_ORDER: Record<Diagnostic[\"severity\"], number> = {\r\n error: 0,\r\n warning: 1,\r\n};\r\n\r\nconst colorizeBySeverity = (\r\n text: string,\r\n severity: Diagnostic[\"severity\"],\r\n): string =>\r\n severity === \"error\" ? highlighter.error(text) : highlighter.warn(text);\r\n\r\nconst sortBySeverity = (\r\n diagnosticGroups: [string, Diagnostic[]][],\r\n): [string, Diagnostic[]][] =>\r\n diagnosticGroups.toSorted(([, diagnosticsA], [, diagnosticsB]) => {\r\n const severityA = SEVERITY_ORDER[diagnosticsA[0].severity];\r\n const severityB = SEVERITY_ORDER[diagnosticsB[0].severity];\r\n return severityA - severityB;\r\n });\r\n\r\nconst collectAffectedFiles = (diagnostics: Diagnostic[]): Set<string> =>\r\n new Set(diagnostics.map((diagnostic) => diagnostic.filePath));\r\n\r\nconst buildFileLineMap = (diagnostics: Diagnostic[]): Map<string, number[]> => {\r\n const fileLines = new Map<string, number[]>();\r\n for (const diagnostic of diagnostics) {\r\n const lines = fileLines.get(diagnostic.filePath) ?? [];\r\n if (diagnostic.line > 0) {\r\n lines.push(diagnostic.line);\r\n }\r\n fileLines.set(diagnostic.filePath, lines);\r\n }\r\n return fileLines;\r\n};\r\n\r\nconst printDiagnostics = (\r\n diagnostics: Diagnostic[],\r\n isVerbose: boolean,\r\n): void => {\r\n const ruleGroups = groupBy(\r\n diagnostics,\r\n (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`,\r\n );\r\n\r\n const sortedRuleGroups = sortBySeverity([...ruleGroups.entries()]);\r\n\r\n for (const [, ruleDiagnostics] of sortedRuleGroups) {\r\n const firstDiagnostic = ruleDiagnostics[0];\r\n const severitySymbol = firstDiagnostic.severity === \"error\" ? \"✗\" : \"⚠\";\r\n const icon = colorizeBySeverity(severitySymbol, firstDiagnostic.severity);\r\n const count = ruleDiagnostics.length;\r\n const countLabel =\r\n count > 1\r\n ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity)\r\n : \"\";\r\n\r\n logger.log(` ${icon} ${firstDiagnostic.message}${countLabel}`);\r\n if (firstDiagnostic.help) {\r\n logger.dim(indentMultilineText(firstDiagnostic.help, \" \"));\r\n }\r\n\r\n if (isVerbose) {\r\n const fileLines = buildFileLineMap(ruleDiagnostics);\r\n\r\n for (const [filePath, lines] of fileLines) {\r\n const lineLabel = lines.length > 0 ? `: ${lines.join(\", \")}` : \"\";\r\n logger.dim(` ${filePath}${lineLabel}`);\r\n }\r\n }\r\n\r\n logger.break();\r\n }\r\n};\r\n\r\nconst formatElapsedTime = (elapsedMilliseconds: number): string => {\r\n if (elapsedMilliseconds < MILLISECONDS_PER_SECOND) {\r\n return `${Math.round(elapsedMilliseconds)}ms`;\r\n }\r\n return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;\r\n};\r\n\r\nconst formatRuleSummary = (\r\n ruleKey: string,\r\n ruleDiagnostics: Diagnostic[],\r\n): string => {\r\n const firstDiagnostic = ruleDiagnostics[0];\r\n const fileLines = buildFileLineMap(ruleDiagnostics);\r\n\r\n const sections = [\r\n `Rule: ${ruleKey}`,\r\n `Severity: ${firstDiagnostic.severity}`,\r\n `Category: ${firstDiagnostic.category}`,\r\n `Count: ${ruleDiagnostics.length}`,\r\n \"\",\r\n firstDiagnostic.message,\r\n ];\r\n\r\n if (firstDiagnostic.help) {\r\n sections.push(\"\", `Suggestion: ${firstDiagnostic.help}`);\r\n }\r\n\r\n sections.push(\"\", \"Files:\");\r\n for (const [filePath, lines] of fileLines) {\r\n const lineLabel = lines.length > 0 ? `: ${lines.join(\", \")}` : \"\";\r\n sections.push(` ${filePath}${lineLabel}`);\r\n }\r\n\r\n return sections.join(\"\\n\") + \"\\n\";\r\n};\r\n\r\nconst buildMarkdownReport = (\r\n diagnostics: Diagnostic[],\r\n elapsedMilliseconds: number,\r\n scoreResult: ScoreResult | null,\r\n totalSourceFileCount: number,\r\n): string => {\r\n const errorCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"error\",\r\n ).length;\r\n const warningCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"warning\",\r\n ).length;\r\n const affectedFileCount = collectAffectedFiles(diagnostics).size;\r\n const elapsed = formatElapsedTime(elapsedMilliseconds);\r\n\r\n const lines: string[] = [\r\n \"# Angular Doctor Report\",\r\n \"\",\r\n `Generated: ${new Date().toISOString()}`,\r\n \"\",\r\n ];\r\n\r\n if (scoreResult) {\r\n lines.push(\r\n \"## Score\",\r\n \"\",\r\n `**${scoreResult.score} / ${PERFECT_SCORE}** — ${scoreResult.label}`,\r\n \"\",\r\n );\r\n }\r\n\r\n lines.push(\r\n \"## Summary\",\r\n \"\",\r\n `- Errors: **${errorCount}**`,\r\n `- Warnings: **${warningCount}**`,\r\n totalSourceFileCount > 0\r\n ? `- Affected files: **${affectedFileCount}/${totalSourceFileCount}**`\r\n : `- Affected files: **${affectedFileCount}**`,\r\n `- Elapsed: **${elapsed}**`,\r\n \"\",\r\n );\r\n\r\n if (diagnostics.length === 0) {\r\n lines.push(\"## Diagnostics\", \"\", \"No issues found.\", \"\");\r\n return lines.join(\"\\n\");\r\n }\r\n\r\n const ruleGroups = groupBy(\r\n diagnostics,\r\n (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`,\r\n );\r\n const sortedRuleGroups = sortBySeverity([...ruleGroups.entries()]);\r\n\r\n lines.push(\"## Diagnostics\", \"\");\r\n\r\n for (const [ruleKey, ruleDiagnostics] of sortedRuleGroups) {\r\n const firstDiagnostic = ruleDiagnostics[0];\r\n const fileLines = buildFileLineMap(ruleDiagnostics);\r\n\r\n lines.push(`### ${ruleKey}`, \"\");\r\n lines.push(\r\n `- Severity: **${firstDiagnostic.severity}**`,\r\n `- Category: **${firstDiagnostic.category}**`,\r\n `- Count: **${ruleDiagnostics.length}**`,\r\n \"\",\r\n firstDiagnostic.message,\r\n \"\",\r\n );\r\n\r\n if (firstDiagnostic.help) {\r\n lines.push(`**Suggestion:** ${firstDiagnostic.help}`, \"\");\r\n }\r\n\r\n lines.push(\"**Files:**\");\r\n for (const [filePath, linesList] of fileLines) {\r\n const lineLabel = linesList.length > 0 ? `: ${linesList.join(\", \")}` : \"\";\r\n lines.push(`- ${filePath}${lineLabel}`);\r\n }\r\n lines.push(\"\");\r\n }\r\n\r\n return lines.join(\"\\n\");\r\n};\r\n\r\nconst resolveReportPath = (\r\n report: boolean | string | undefined,\r\n outputDirectory: string,\r\n baseDirectory: string,\r\n): string | null => {\r\n if (!report) return null;\r\n\r\n if (typeof report === \"string\") {\r\n const absolutePath = isAbsolute(report)\r\n ? report\r\n : resolve(baseDirectory, report);\r\n if (extname(absolutePath)) return absolutePath;\r\n return join(absolutePath, \"report.md\");\r\n }\r\n\r\n return join(outputDirectory, \"report.md\");\r\n};\r\n\r\nconst writeDiagnosticsDirectory = (\r\n diagnostics: Diagnostic[],\r\n elapsedMilliseconds: number,\r\n scoreResult: ScoreResult | null,\r\n totalSourceFileCount: number,\r\n report: boolean | string | undefined,\r\n baseDirectory: string,\r\n): { outputDirectory: string; markdownPath: string | null } => {\r\n const outputDirectory = join(tmpdir(), `angular-doctor-${randomUUID()}`);\r\n mkdirSync(outputDirectory);\r\n\r\n const ruleGroups = groupBy(\r\n diagnostics,\r\n (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`,\r\n );\r\n const sortedRuleGroups = sortBySeverity([...ruleGroups.entries()]);\r\n\r\n for (const [ruleKey, ruleDiagnostics] of sortedRuleGroups) {\r\n const fileName = ruleKey.replace(/\\//g, \"--\") + \".txt\";\r\n writeFileSync(\r\n join(outputDirectory, fileName),\r\n formatRuleSummary(ruleKey, ruleDiagnostics),\r\n );\r\n }\r\n\r\n writeFileSync(\r\n join(outputDirectory, \"diagnostics.json\"),\r\n JSON.stringify(diagnostics, null, 2),\r\n );\r\n\r\n const markdownPath = resolveReportPath(\r\n report,\r\n outputDirectory,\r\n baseDirectory,\r\n );\r\n if (markdownPath) {\r\n mkdirSync(dirname(markdownPath), { recursive: true });\r\n writeFileSync(\r\n markdownPath,\r\n buildMarkdownReport(\r\n diagnostics,\r\n elapsedMilliseconds,\r\n scoreResult,\r\n totalSourceFileCount,\r\n ),\r\n );\r\n }\r\n\r\n return { outputDirectory, markdownPath };\r\n};\r\n\r\nconst buildScoreBarSegments = (score: number): ScoreBarSegments => {\r\n const filledCount = Math.round(\r\n (score / PERFECT_SCORE) * SCORE_BAR_WIDTH_CHARS,\r\n );\r\n const emptyCount = SCORE_BAR_WIDTH_CHARS - filledCount;\r\n\r\n return {\r\n filledSegment: \"█\".repeat(filledCount),\r\n emptySegment: \"░\".repeat(emptyCount),\r\n };\r\n};\r\n\r\nconst buildPlainScoreBar = (score: number): string => {\r\n const { filledSegment, emptySegment } = buildScoreBarSegments(score);\r\n return `${filledSegment}${emptySegment}`;\r\n};\r\n\r\nconst buildScoreBar = (score: number): string => {\r\n const { filledSegment, emptySegment } = buildScoreBarSegments(score);\r\n return colorizeByScore(filledSegment, score) + highlighter.dim(emptySegment);\r\n};\r\n\r\nconst printScoreGauge = (score: number, label: string): void => {\r\n const scoreDisplay = colorizeByScore(`${score}`, score);\r\n const labelDisplay = colorizeByScore(label, score);\r\n logger.log(` ${scoreDisplay} / ${PERFECT_SCORE} ${labelDisplay}`);\r\n logger.break();\r\n logger.log(` ${buildScoreBar(score)}`);\r\n logger.break();\r\n};\r\n\r\nconst getDoctorFace = (score: number): string[] => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return [\"◠ ◠\", \" ▽ \"];\r\n if (score >= SCORE_OK_THRESHOLD) return [\"• •\", \" ─ \"];\r\n return [\"x x\", \" ▽ \"];\r\n};\r\n\r\nconst printBranding = (score?: number): void => {\r\n if (score !== undefined) {\r\n const [eyes, mouth] = getDoctorFace(score);\r\n const colorize = (text: string) => colorizeByScore(text, score);\r\n logger.log(colorize(\" ┌─────┐\"));\r\n logger.log(colorize(` │ ${eyes} │`));\r\n logger.log(colorize(` │ ${mouth} │`));\r\n logger.log(colorize(\" └─────┘\"));\r\n }\r\n logger.log(` Angular Doctor`);\r\n logger.break();\r\n};\r\n\r\nconst buildBrandingLines = (scoreResult: ScoreResult | null): FramedLine[] => {\r\n const lines: FramedLine[] = [];\r\n\r\n if (scoreResult) {\r\n const [eyes, mouth] = getDoctorFace(scoreResult.score);\r\n const scoreColorizer = (text: string): string =>\r\n colorizeByScore(text, scoreResult.score);\r\n\r\n lines.push(createFramedLine(\"┌─────┐\", scoreColorizer(\"┌─────┐\")));\r\n lines.push(createFramedLine(`│ ${eyes} │`, scoreColorizer(`│ ${eyes} │`)));\r\n lines.push(\r\n createFramedLine(`│ ${mouth} │`, scoreColorizer(`│ ${mouth} │`)),\r\n );\r\n lines.push(createFramedLine(\"└─────┘\", scoreColorizer(\"└─────┘\")));\r\n lines.push(createFramedLine(\"Angular Doctor\"));\r\n lines.push(createFramedLine(\"\"));\r\n\r\n const scoreLinePlainText = `${scoreResult.score} / ${PERFECT_SCORE} ${scoreResult.label}`;\r\n const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / ${PERFECT_SCORE} ${colorizeByScore(scoreResult.label, scoreResult.score)}`;\r\n lines.push(createFramedLine(scoreLinePlainText, scoreLineRenderedText));\r\n lines.push(createFramedLine(\"\"));\r\n lines.push(\r\n createFramedLine(\r\n buildPlainScoreBar(scoreResult.score),\r\n buildScoreBar(scoreResult.score),\r\n ),\r\n );\r\n lines.push(createFramedLine(\"\"));\r\n } else {\r\n lines.push(createFramedLine(\"Angular Doctor\"));\r\n lines.push(createFramedLine(\"\"));\r\n lines.push(\r\n createFramedLine(\r\n \"Score unavailable\",\r\n highlighter.dim(\"Score unavailable\"),\r\n ),\r\n );\r\n lines.push(createFramedLine(\"\"));\r\n }\r\n\r\n return lines;\r\n};\r\n\r\nconst buildCountsSummaryLine = (\r\n diagnostics: Diagnostic[],\r\n totalSourceFileCount: number,\r\n elapsedMilliseconds: number,\r\n): FramedLine => {\r\n const errorCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"error\",\r\n ).length;\r\n const warningCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"warning\",\r\n ).length;\r\n const affectedFileCount = collectAffectedFiles(diagnostics).size;\r\n const elapsed = formatElapsedTime(elapsedMilliseconds);\r\n\r\n const plainParts: string[] = [];\r\n const renderedParts: string[] = [];\r\n\r\n if (errorCount > 0) {\r\n const errorText = `✗ ${errorCount} error${errorCount === 1 ? \"\" : \"s\"}`;\r\n plainParts.push(errorText);\r\n renderedParts.push(highlighter.error(errorText));\r\n }\r\n if (warningCount > 0) {\r\n const warningText = `⚠ ${warningCount} warning${warningCount === 1 ? \"\" : \"s\"}`;\r\n plainParts.push(warningText);\r\n renderedParts.push(highlighter.warn(warningText));\r\n }\r\n\r\n const fileCountText =\r\n totalSourceFileCount > 0\r\n ? `across ${affectedFileCount}/${totalSourceFileCount} files`\r\n : `across ${affectedFileCount} file${affectedFileCount === 1 ? \"\" : \"s\"}`;\r\n const elapsedTimeText = `in ${elapsed}`;\r\n\r\n plainParts.push(fileCountText, elapsedTimeText);\r\n renderedParts.push(\r\n highlighter.dim(fileCountText),\r\n highlighter.dim(elapsedTimeText),\r\n );\r\n\r\n return createFramedLine(plainParts.join(\" \"), renderedParts.join(\" \"));\r\n};\r\n\r\nconst printSummary = (\r\n diagnostics: Diagnostic[],\r\n elapsedMilliseconds: number,\r\n scoreResult: ScoreResult | null,\r\n totalSourceFileCount: number,\r\n report: boolean | string | undefined,\r\n baseDirectory: string,\r\n): void => {\r\n const summaryFramedLines = [\r\n ...buildBrandingLines(scoreResult),\r\n buildCountsSummaryLine(\r\n diagnostics,\r\n totalSourceFileCount,\r\n elapsedMilliseconds,\r\n ),\r\n ];\r\n printFramedBox(summaryFramedLines);\r\n\r\n try {\r\n const { outputDirectory, markdownPath } = writeDiagnosticsDirectory(\r\n diagnostics,\r\n elapsedMilliseconds,\r\n scoreResult,\r\n totalSourceFileCount,\r\n report,\r\n baseDirectory,\r\n );\r\n logger.break();\r\n logger.dim(` Full diagnostics written to ${outputDirectory}`);\r\n if (markdownPath) {\r\n logger.dim(` Markdown report written to ${markdownPath}`);\r\n }\r\n } catch {\r\n logger.break();\r\n }\r\n};\r\n\r\ninterface ResolvedScanOptions {\r\n lint: boolean;\r\n deadCode: boolean;\r\n verbose: boolean;\r\n scoreOnly: boolean;\r\n report: boolean | string | undefined;\r\n includePaths: string[];\r\n}\r\n\r\nconst mergeScanOptions = (\r\n inputOptions: ScanOptions,\r\n userConfig: AngularDoctorConfig | null,\r\n): ResolvedScanOptions => ({\r\n lint: inputOptions.lint ?? userConfig?.lint ?? true,\r\n deadCode: inputOptions.deadCode ?? userConfig?.deadCode ?? true,\r\n verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,\r\n scoreOnly: inputOptions.scoreOnly ?? false,\r\n report: inputOptions.report ?? false,\r\n includePaths: inputOptions.includePaths ?? [],\r\n});\r\n\r\nconst printProjectDetection = (\r\n projectInfo: ProjectInfo,\r\n userConfig: AngularDoctorConfig | null,\r\n isDiffMode: boolean,\r\n includePaths: string[],\r\n): void => {\r\n const frameworkLabel = formatFrameworkName(projectInfo.framework);\r\n const languageLabel = \"TypeScript\";\r\n\r\n const completeStep = (message: string) => {\r\n spinner(message).start().succeed(message);\r\n };\r\n\r\n completeStep(\r\n `Detecting framework. Found ${highlighter.info(frameworkLabel)}.`,\r\n );\r\n completeStep(\r\n `Detecting Angular version. Found ${highlighter.info(`Angular ${projectInfo.angularVersion}`)}.`,\r\n );\r\n completeStep(`Detecting language. Found ${highlighter.info(languageLabel)}.`);\r\n completeStep(\r\n `Detecting standalone components. ${projectInfo.hasStandaloneComponents ? highlighter.info(\"Supported.\") : \"Not available (Angular 14+ required).\"}`,\r\n );\r\n\r\n if (isDiffMode) {\r\n completeStep(\r\n `Scanning ${highlighter.info(`${includePaths.length}`)} changed source files.`,\r\n );\r\n } else {\r\n completeStep(\r\n `Found ${highlighter.info(`${projectInfo.sourceFileCount}`)} source files.`,\r\n );\r\n }\r\n\r\n if (userConfig) {\r\n completeStep(`Loaded ${highlighter.info(\"angular-doctor config\")}.`);\r\n }\r\n\r\n logger.break();\r\n};\r\n\r\nexport const scan = async (\r\n directory: string,\r\n inputOptions: ScanOptions = {},\r\n): Promise<ScanResult> => {\r\n const startTime = performance.now();\r\n const projectInfo = discoverProject(directory);\r\n const userConfig = loadConfig(directory);\r\n const options = mergeScanOptions(inputOptions, userConfig);\r\n const { includePaths } = options;\r\n const isDiffMode = includePaths.length > 0;\r\n\r\n if (!projectInfo.angularVersion) {\r\n throw new Error(\"No Angular dependency found in package.json\");\r\n }\r\n\r\n if (!options.scoreOnly) {\r\n printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths);\r\n }\r\n\r\n const computedIncludePaths = computeIncludePaths(includePaths);\r\n\r\n let didLintFail = false;\r\n let didDeadCodeFail = false;\r\n\r\n const lintPromise = options.lint\r\n ? (async () => {\r\n const lintSpinner = options.scoreOnly\r\n ? null\r\n : spinner(\"Running lint checks...\").start();\r\n try {\r\n const lintDiagnostics = await runEslint(\r\n directory,\r\n projectInfo.hasTypeScript,\r\n computedIncludePaths,\r\n );\r\n lintSpinner?.succeed(\"Running lint checks.\");\r\n return lintDiagnostics;\r\n } catch (error) {\r\n didLintFail = true;\r\n lintSpinner?.fail(\"Lint checks failed (non-fatal, skipping).\");\r\n logger.error(String(error));\r\n return [];\r\n }\r\n })()\r\n : Promise.resolve<Diagnostic[]>([]);\r\n\r\n const deadCodePromise =\r\n options.deadCode && !isDiffMode\r\n ? (async () => {\r\n const deadCodeSpinner = options.scoreOnly\r\n ? null\r\n : spinner(\"Detecting dead code...\").start();\r\n try {\r\n const knipDiagnostics = await runKnip(directory);\r\n deadCodeSpinner?.succeed(\"Detecting dead code.\");\r\n return knipDiagnostics;\r\n } catch (error) {\r\n didDeadCodeFail = true;\r\n deadCodeSpinner?.fail(\r\n \"Dead code detection failed (non-fatal, skipping).\",\r\n );\r\n logger.error(String(error));\r\n return [];\r\n }\r\n })()\r\n : Promise.resolve<Diagnostic[]>([]);\r\n\r\n const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([\r\n lintPromise,\r\n deadCodePromise,\r\n ]);\r\n const diagnostics = combineDiagnostics(\r\n lintDiagnostics,\r\n deadCodeDiagnostics,\r\n userConfig,\r\n );\r\n\r\n const elapsedMilliseconds = performance.now() - startTime;\r\n\r\n const skippedChecks: string[] = [];\r\n if (didLintFail) skippedChecks.push(\"lint\");\r\n if (didDeadCodeFail) skippedChecks.push(\"dead code\");\r\n const hasSkippedChecks = skippedChecks.length > 0;\r\n\r\n const scoreResult = calculateScore(diagnostics);\r\n\r\n if (options.scoreOnly) {\r\n logger.log(`${scoreResult.score}`);\r\n return { diagnostics, scoreResult, skippedChecks };\r\n }\r\n\r\n if (diagnostics.length === 0) {\r\n if (hasSkippedChecks) {\r\n const skippedLabel = skippedChecks.join(\" and \");\r\n logger.warn(\r\n `No issues detected, but ${skippedLabel} checks failed — results are incomplete.`,\r\n );\r\n } else {\r\n logger.success(\"No issues found!\");\r\n }\r\n logger.break();\r\n if (hasSkippedChecks) {\r\n printBranding();\r\n logger.dim(\" Score not shown — some checks could not complete.\");\r\n } else {\r\n printBranding(scoreResult.score);\r\n printScoreGauge(scoreResult.score, scoreResult.label);\r\n }\r\n return { diagnostics, scoreResult, skippedChecks };\r\n }\r\n\r\n printDiagnostics(diagnostics, options.verbose);\r\n\r\n const displayedSourceFileCount = isDiffMode\r\n ? includePaths.length\r\n : projectInfo.sourceFileCount;\r\n\r\n printSummary(\r\n diagnostics,\r\n elapsedMilliseconds,\r\n scoreResult,\r\n displayedSourceFileCount,\r\n options.report,\r\n directory,\r\n );\r\n\r\n if (hasSkippedChecks) {\r\n const skippedLabel = skippedChecks.join(\" and \");\r\n logger.break();\r\n logger.warn(\r\n ` Note: ${skippedLabel} checks failed — score may be incomplete.`,\r\n );\r\n }\r\n\r\n return { diagnostics, scoreResult, skippedChecks };\r\n};\r\n","import { spawnSync } from \"node:child_process\";\r\nimport { SOURCE_FILE_PATTERN, DEFAULT_BRANCH_CANDIDATES } from \"../constants.js\";\r\nimport type { DiffInfo } from \"../types.js\";\r\n\r\nconst getCurrentBranch = (directory: string): string | null => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0 ? result.stdout.trim() : null;\r\n};\r\n\r\nconst getBranchChangedFiles = (directory: string, baseBranch: string): string[] => {\r\n const result = spawnSync(\"git\", [\"diff\", \"--name-only\", baseBranch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout.split(\"\\n\").filter(Boolean);\r\n};\r\n\r\nconst getUncommittedChangedFiles = (directory: string): string[] => {\r\n const result = spawnSync(\"git\", [\"status\", \"--porcelain\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter(Boolean)\r\n .map((line) => line.slice(3).trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst branchExists = (directory: string, branch: string): boolean => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--verify\", branch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0;\r\n};\r\n\r\nexport const filterSourceFiles = (files: string[]): string[] =>\r\n files.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));\r\n\r\nexport const getDiffInfo = (directory: string, explicitBaseBranch?: string): DiffInfo | null => {\r\n const currentBranch = getCurrentBranch(directory);\r\n if (!currentBranch) return null;\r\n\r\n if (explicitBaseBranch) {\r\n if (!branchExists(directory, explicitBaseBranch)) return null;\r\n const changedFiles = getBranchChangedFiles(directory, explicitBaseBranch);\r\n return { currentBranch, baseBranch: explicitBaseBranch, changedFiles };\r\n }\r\n\r\n const uncommittedFiles = getUncommittedChangedFiles(directory);\r\n if (uncommittedFiles.length > 0) {\r\n return {\r\n currentBranch,\r\n baseBranch: currentBranch,\r\n changedFiles: uncommittedFiles,\r\n isCurrentChanges: true,\r\n };\r\n }\r\n\r\n for (const candidate of DEFAULT_BRANCH_CANDIDATES) {\r\n if (branchExists(directory, candidate) && currentBranch !== candidate) {\r\n const changedFiles = getBranchChangedFiles(directory, candidate);\r\n if (changedFiles.length > 0) {\r\n return { currentBranch, baseBranch: candidate, changedFiles };\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import { logger } from \"./logger.js\";\r\n\r\nexport interface HandleErrorOptions {\r\n shouldExit: boolean;\r\n}\r\n\r\nexport const handleError = (\r\n error: unknown,\r\n options: HandleErrorOptions = { shouldExit: true },\r\n): void => {\r\n const message = error instanceof Error ? error.message : String(error);\r\n logger.error(message);\r\n if (options.shouldExit) {\r\n process.exit(1);\r\n }\r\n};\r\n","import path from \"node:path\";\r\nimport basePrompts from \"prompts\";\r\nimport type { WorkspacePackage } from \"../types.js\";\r\nimport { listAngularWorkspaceProjects, listWorkspacePackages } from \"./discover-project.js\";\r\nimport { highlighter } from \"./highlighter.js\";\r\nimport { logger } from \"./logger.js\";\r\n\r\nconst onCancel = () => {\r\n logger.break();\r\n logger.log(\"Cancelled.\");\r\n logger.break();\r\n process.exit(0);\r\n};\r\n\r\nconst promptProjectSelection = async (\r\n workspacePackages: WorkspacePackage[],\r\n rootDirectory: string,\r\n): Promise<string[]> => {\r\n const response = await basePrompts(\r\n {\r\n type: \"multiselect\",\r\n name: \"selectedDirectories\",\r\n message: \"Select projects to scan\",\r\n choices: workspacePackages.map((workspacePackage) => ({\r\n title: workspacePackage.name,\r\n description: path.relative(rootDirectory, workspacePackage.directory) || \".\",\r\n value: workspacePackage.directory,\r\n selected: true,\r\n })),\r\n min: 1,\r\n },\r\n { onCancel },\r\n );\r\n return (response.selectedDirectories as string[]) ?? [];\r\n};\r\n\r\nconst resolveProjectFlag = (\r\n projectFlag: string,\r\n workspacePackages: WorkspacePackage[],\r\n): string[] => {\r\n const requestedNames = projectFlag.split(\",\").map((name) => name.trim());\r\n const resolvedDirectories: string[] = [];\r\n\r\n for (const requestedName of requestedNames) {\r\n const matched = workspacePackages.find(\r\n (workspacePackage) =>\r\n workspacePackage.name === requestedName ||\r\n path.basename(workspacePackage.directory) === requestedName,\r\n );\r\n\r\n if (!matched) {\r\n const availableNames = workspacePackages\r\n .map((workspacePackage) => workspacePackage.name)\r\n .join(\", \");\r\n throw new Error(`Project \"${requestedName}\" not found. Available: ${availableNames}`);\r\n }\r\n\r\n resolvedDirectories.push(matched.directory);\r\n }\r\n\r\n return resolvedDirectories;\r\n};\r\n\r\nconst printDiscoveredProjects = (packages: WorkspacePackage[]): void => {\r\n logger.log(\r\n `${highlighter.success(\"✔\")} Select projects to scan ${highlighter.dim(\"›\")} ${packages.map((p) => p.name).join(\", \")}`,\r\n );\r\n};\r\n\r\n/**\r\n * Resolves the list of project directories to scan.\r\n *\r\n * Priority order:\r\n * 1. Angular CLI workspace (`angular.json`) — covers single-repo multi-project Angular workspaces\r\n * 2. npm/pnpm workspaces (package.json `workspaces` field or pnpm-workspace.yaml)\r\n * 3. Fall back to `rootDirectory` itself\r\n */\r\nexport const selectProjects = async (\r\n rootDirectory: string,\r\n projectFlag: string | undefined,\r\n skipPrompts: boolean,\r\n): Promise<string[]> => {\r\n // Prefer angular.json workspace projects (Angular CLI / Nx workspaces)\r\n let packages = listAngularWorkspaceProjects(rootDirectory);\r\n\r\n // Fall back to npm/pnpm workspace packages\r\n if (packages.length === 0) {\r\n packages = listWorkspacePackages(rootDirectory);\r\n }\r\n\r\n // No workspace found — scan the directory itself\r\n if (packages.length === 0) return [rootDirectory];\r\n\r\n // Single project — no need to prompt\r\n if (packages.length === 1) {\r\n logger.log(\r\n `${highlighter.success(\"✔\")} Select projects to scan ${highlighter.dim(\"›\")} ${packages[0].name}`,\r\n );\r\n logger.break();\r\n return [packages[0].directory];\r\n }\r\n\r\n // Explicit --project flag\r\n if (projectFlag) return resolveProjectFlag(projectFlag, packages);\r\n\r\n // Non-interactive mode: scan all\r\n if (skipPrompts) {\r\n printDiscoveredProjects(packages);\r\n logger.break();\r\n return packages.map((workspacePackage) => workspacePackage.directory);\r\n }\r\n\r\n // Interactive multi-select prompt\r\n return promptProjectSelection(packages, rootDirectory);\r\n};\r\n","import path from \"node:path\";\r\nimport { Command } from \"commander\";\r\nimport { scan } from \"./scan.js\";\r\nimport type { AngularDoctorConfig, DiffInfo, ScanOptions } from \"./types.js\";\r\nimport { loadConfig } from \"./utils/load-config.js\";\r\nimport { filterSourceFiles, getDiffInfo } from \"./utils/get-diff-files.js\";\r\nimport { handleError } from \"./utils/handle-error.js\";\r\nimport { highlighter } from \"./utils/highlighter.js\";\r\nimport { logger } from \"./utils/logger.js\";\r\nimport { selectProjects } from \"./utils/select-projects.js\";\r\n\r\nconst VERSION = process.env.VERSION ?? \"0.0.0\";\r\n\r\ninterface CliFlags {\r\n lint: boolean;\r\n deadCode: boolean;\r\n verbose: boolean;\r\n score: boolean;\r\n yes: boolean;\r\n report?: boolean | string;\r\n project?: string;\r\n diff?: boolean | string;\r\n}\r\n\r\nconst exitWithHint = () => {\r\n logger.break();\r\n logger.log(\"Cancelled.\");\r\n logger.break();\r\n process.exit(0);\r\n};\r\n\r\nprocess.on(\"SIGINT\", exitWithHint);\r\nprocess.on(\"SIGTERM\", exitWithHint);\r\n\r\nconst AUTOMATED_ENVIRONMENT_VARIABLES = [\r\n \"CI\",\r\n \"CLAUDECODE\",\r\n \"CURSOR_AGENT\",\r\n \"CODEX_CI\",\r\n \"OPENCODE\",\r\n \"AMP_HOME\",\r\n];\r\n\r\nconst isAutomatedEnvironment = (): boolean =>\r\n AUTOMATED_ENVIRONMENT_VARIABLES.some((envVariable) =>\r\n Boolean(process.env[envVariable]),\r\n );\r\n\r\nconst resolveCliScanOptions = (\r\n flags: CliFlags,\r\n userConfig: AngularDoctorConfig | null,\r\n programInstance: Command,\r\n): ScanOptions => {\r\n const isCliOverride = (optionName: string) =>\r\n programInstance.getOptionValueSource(optionName) === \"cli\";\r\n\r\n return {\r\n lint: isCliOverride(\"lint\") ? flags.lint : (userConfig?.lint ?? flags.lint),\r\n deadCode: isCliOverride(\"deadCode\")\r\n ? flags.deadCode\r\n : (userConfig?.deadCode ?? flags.deadCode),\r\n verbose: isCliOverride(\"verbose\")\r\n ? Boolean(flags.verbose)\r\n : (userConfig?.verbose ?? false),\r\n scoreOnly: flags.score,\r\n report: flags.report,\r\n };\r\n};\r\n\r\nconst resolveDiffMode = async (\r\n diffInfo: DiffInfo | null,\r\n effectiveDiff: boolean | string | undefined,\r\n shouldSkipPrompts: boolean,\r\n isScoreOnly: boolean,\r\n): Promise<boolean> => {\r\n if (effectiveDiff !== undefined && effectiveDiff !== false) {\r\n if (diffInfo) return true;\r\n if (!isScoreOnly) {\r\n logger.warn(\r\n \"No feature branch or uncommitted changes detected. Running full scan.\",\r\n );\r\n logger.break();\r\n }\r\n return false;\r\n }\r\n\r\n if (effectiveDiff === false || !diffInfo) return false;\r\n\r\n const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);\r\n if (changedSourceFiles.length === 0) return false;\r\n if (shouldSkipPrompts) return true;\r\n if (isScoreOnly) return false;\r\n\r\n // In non-interactive mode, skip diff prompts\r\n return false;\r\n};\r\n\r\nconst program = new Command()\r\n .name(\"angular-doctor\")\r\n .description(\"Diagnose Angular codebase health\")\r\n .version(VERSION, \"-v, --version\", \"display the version number\")\r\n .argument(\"[directory]\", \"project directory to scan\", \".\")\r\n .option(\"--no-lint\", \"skip linting\")\r\n .option(\"--no-dead-code\", \"skip dead code detection\")\r\n .option(\"--verbose\", \"show file details per rule\")\r\n .option(\"--score\", \"output only the score\")\r\n .option(\"--report [path]\", \"write a markdown report (optional output path)\")\r\n .option(\"-y, --yes\", \"skip prompts, scan all workspace projects\")\r\n .option(\r\n \"--project <name>\",\r\n \"select workspace project (comma-separated for multiple)\",\r\n )\r\n .option(\"--diff [base]\", \"scan only files changed vs base branch\")\r\n .action(async (directory: string, flags: CliFlags) => {\r\n const isScoreOnly = flags.score;\r\n\r\n try {\r\n const resolvedDirectory = path.resolve(directory);\r\n const userConfig = loadConfig(resolvedDirectory);\r\n\r\n if (!isScoreOnly) {\r\n logger.log(`angular-doctor v${VERSION}`);\r\n logger.break();\r\n }\r\n\r\n const scanOptions = resolveCliScanOptions(flags, userConfig, program);\r\n const shouldSkipPrompts =\r\n flags.yes || isAutomatedEnvironment() || !process.stdin.isTTY;\r\n\r\n // Discover and (optionally) prompt to select workspace projects\r\n const projectDirectories = await selectProjects(\r\n resolvedDirectory,\r\n flags.project,\r\n shouldSkipPrompts,\r\n );\r\n\r\n const isDiffCliOverride = program.getOptionValueSource(\"diff\") === \"cli\";\r\n const effectiveDiff = isDiffCliOverride ? flags.diff : userConfig?.diff;\r\n const explicitBaseBranch =\r\n typeof effectiveDiff === \"string\" ? effectiveDiff : undefined;\r\n const diffInfo = getDiffInfo(resolvedDirectory, explicitBaseBranch);\r\n const isDiffMode = await resolveDiffMode(\r\n diffInfo,\r\n effectiveDiff,\r\n shouldSkipPrompts,\r\n isScoreOnly,\r\n );\r\n\r\n if (isDiffMode && diffInfo && !isScoreOnly) {\r\n if (diffInfo.isCurrentChanges) {\r\n logger.log(\"Scanning uncommitted changes\");\r\n } else {\r\n logger.log(\r\n `Scanning changes: ${highlighter.info(diffInfo.currentBranch)} → ${highlighter.info(diffInfo.baseBranch)}`,\r\n );\r\n }\r\n logger.break();\r\n }\r\n\r\n for (const projectDirectory of projectDirectories) {\r\n let includePaths: string[] | undefined;\r\n\r\n if (isDiffMode) {\r\n const projectDiffInfo = getDiffInfo(\r\n projectDirectory,\r\n explicitBaseBranch,\r\n );\r\n if (projectDiffInfo) {\r\n const changedSourceFiles = filterSourceFiles(\r\n projectDiffInfo.changedFiles,\r\n );\r\n if (changedSourceFiles.length === 0) {\r\n if (!isScoreOnly) {\r\n logger.dim(\r\n `No changed source files in ${projectDirectory}, skipping.`,\r\n );\r\n logger.break();\r\n }\r\n continue;\r\n }\r\n includePaths = changedSourceFiles;\r\n }\r\n }\r\n\r\n if (!isScoreOnly) {\r\n logger.dim(`Scanning ${projectDirectory}...`);\r\n logger.break();\r\n }\r\n\r\n await scan(projectDirectory, { ...scanOptions, includePaths });\r\n\r\n if (!isScoreOnly) {\r\n logger.break();\r\n }\r\n }\r\n } catch (error) {\r\n handleError(error);\r\n }\r\n })\r\n .addHelpText(\r\n \"after\",\r\n `\r\n${highlighter.dim(\"Learn more:\")}\r\n ${highlighter.info(\"https://github.com/antonygiomarxdev/angular-doctor\")}\r\n`,\r\n );\r\n\r\nconst main = async () => {\r\n await program.parseAsync();\r\n};\r\n\r\nmain();\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["main"],"sources":["../src/constants.ts","../src/utils/calculate-score.ts","../src/utils/colorize-by-score.ts","../src/utils/filter-diagnostics.ts","../src/utils/combine-diagnostics.ts","../src/utils/discover-project.ts","../src/utils/highlighter.ts","../src/utils/logger.ts","../src/utils/framed-box.ts","../src/utils/group-by.ts","../src/utils/indent-multiline-text.ts","../src/utils/load-config.ts","../src/utils/run-eslint.ts","../src/utils/run-knip.ts","../src/utils/spinner.ts","../src/scan.ts","../src/utils/get-diff-files.ts","../src/utils/handle-error.ts","../src/utils/select-projects.ts","../src/cli.ts"],"sourcesContent":["export const SOURCE_FILE_PATTERN = /\\.ts$/;\r\n\r\nexport const MILLISECONDS_PER_SECOND = 1000;\r\n\r\nexport const PERFECT_SCORE = 100;\r\n\r\nexport const SCORE_GOOD_THRESHOLD = 75;\r\n\r\nexport const SCORE_OK_THRESHOLD = 50;\r\n\r\nexport const SCORE_BAR_WIDTH_CHARS = 50;\r\n\r\nexport const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;\r\n\r\nexport const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;\r\n\r\nexport const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\r\n\r\nexport const MAX_KNIP_RETRIES = 5;\r\n\r\nexport const ERROR_RULE_PENALTY = 1.5;\r\n\r\nexport const WARNING_RULE_PENALTY = 0.75;\r\n\r\nexport const DEFAULT_BRANCH_CANDIDATES = [\"main\", \"master\"];\r\n","import {\r\n ERROR_RULE_PENALTY,\r\n PERFECT_SCORE,\r\n SCORE_GOOD_THRESHOLD,\r\n SCORE_OK_THRESHOLD,\r\n WARNING_RULE_PENALTY,\r\n} from \"../constants.js\";\r\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\r\n\r\nexport const getScoreLabel = (score: number): string => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return \"Great\";\r\n if (score >= SCORE_OK_THRESHOLD) return \"Needs work\";\r\n return \"Critical\";\r\n};\r\n\r\nconst countUniqueRules = (\r\n diagnostics: Diagnostic[],\r\n): { errorRuleCount: number; warningRuleCount: number } => {\r\n const errorRules = new Set<string>();\r\n const warningRules = new Set<string>();\r\n\r\n for (const diagnostic of diagnostics) {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (diagnostic.severity === \"error\") {\r\n errorRules.add(ruleKey);\r\n } else {\r\n warningRules.add(ruleKey);\r\n }\r\n }\r\n\r\n return { errorRuleCount: errorRules.size, warningRuleCount: warningRules.size };\r\n};\r\n\r\nexport const calculateScore = (diagnostics: Diagnostic[]): ScoreResult => {\r\n const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);\r\n const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;\r\n const score = Math.max(0, Math.round(PERFECT_SCORE - penalty));\r\n return { score, label: getScoreLabel(score) };\r\n};\r\n","import pc from \"picocolors\";\r\nimport { SCORE_GOOD_THRESHOLD, SCORE_OK_THRESHOLD } from \"../constants.js\";\r\n\r\nexport const colorizeByScore = (text: string, score: number): string => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return pc.green(text);\r\n if (score >= SCORE_OK_THRESHOLD) return pc.yellow(text);\r\n return pc.red(text);\r\n};\r\n","import type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\n\r\nconst matchesGlob = (filePath: string, pattern: string): boolean => {\r\n const escapedPattern = pattern\r\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\r\n .replace(/\\*\\*/g, \".*\")\r\n .replace(/\\*/g, \"[^/]*\");\r\n return new RegExp(`^${escapedPattern}$`).test(filePath);\r\n};\r\n\r\nexport const filterIgnoredDiagnostics = (\r\n diagnostics: Diagnostic[],\r\n config: AngularDoctorConfig,\r\n): Diagnostic[] => {\r\n const ignoredRules = new Set(config.ignore?.rules ?? []);\r\n const ignoredFilePatterns = config.ignore?.files ?? [];\r\n\r\n return diagnostics.filter((diagnostic) => {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (ignoredRules.has(ruleKey)) return false;\r\n\r\n if (ignoredFilePatterns.some((pattern) => matchesGlob(diagnostic.filePath, pattern))) {\r\n return false;\r\n }\r\n\r\n return true;\r\n });\r\n};\r\n","import { SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\nimport { filterIgnoredDiagnostics } from \"./filter-diagnostics.js\";\r\n\r\nexport const computeIncludePaths = (includePaths: string[]): string[] | undefined =>\r\n includePaths.length > 0\r\n ? includePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath))\r\n : undefined;\r\n\r\nexport const combineDiagnostics = (\r\n lintDiagnostics: Diagnostic[],\r\n deadCodeDiagnostics: Diagnostic[],\r\n userConfig: AngularDoctorConfig | null,\r\n): Diagnostic[] => {\r\n const allDiagnostics = [...lintDiagnostics, ...deadCodeDiagnostics];\r\n return userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { spawnSync } from \"node:child_process\";\r\nimport { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularFramework, PackageJson, ProjectInfo, WorkspacePackage } from \"../types.js\";\r\n\r\nconst ANGULAR_FRAMEWORK_PACKAGES: Record<string, AngularFramework> = {\r\n \"@nrwl/angular\": \"nx\",\r\n \"@nx/angular\": \"nx\",\r\n \"@analogjs/platform\": \"analog\",\r\n \"@ionic/angular\": \"ionic\",\r\n \"@angular/ssr\": \"universal\",\r\n \"@nguniversal/express-engine\": \"universal\",\r\n};\r\n\r\nconst ANGULAR_FRAMEWORK_DISPLAY_NAMES: Record<AngularFramework, string> = {\r\n \"angular-cli\": \"Angular CLI\",\r\n nx: \"Nx\",\r\n analog: \"AnalogJS\",\r\n ionic: \"Ionic\",\r\n universal: \"Angular SSR\",\r\n unknown: \"Angular\",\r\n};\r\n\r\nexport const formatFrameworkName = (framework: AngularFramework): string =>\r\n ANGULAR_FRAMEWORK_DISPLAY_NAMES[framework];\r\n\r\nconst readPackageJson = (filePath: string): PackageJson => {\r\n try {\r\n const content = fs.readFileSync(filePath, \"utf-8\");\r\n return JSON.parse(content) as PackageJson;\r\n } catch {\r\n return {};\r\n }\r\n};\r\n\r\nconst collectAllDependencies = (packageJson: PackageJson): Record<string, string> => ({\r\n ...packageJson.peerDependencies,\r\n ...packageJson.dependencies,\r\n ...packageJson.devDependencies,\r\n});\r\n\r\nconst detectFramework = (dependencies: Record<string, string>): AngularFramework => {\r\n for (const [packageName, frameworkName] of Object.entries(ANGULAR_FRAMEWORK_PACKAGES)) {\r\n if (dependencies[packageName]) {\r\n return frameworkName;\r\n }\r\n }\r\n if (dependencies[\"@angular/cli\"] || dependencies[\"@angular-devkit/build-angular\"] || dependencies[\"@angular-devkit/core\"]) {\r\n return \"angular-cli\";\r\n }\r\n return \"unknown\";\r\n};\r\n\r\nconst detectAngularVersion = (dependencies: Record<string, string>): string | null =>\r\n dependencies[\"@angular/core\"] ?? null;\r\n\r\nconst detectStandaloneComponents = (packageJson: PackageJson): boolean => {\r\n const deps = collectAllDependencies(packageJson);\r\n const angularVersion = deps[\"@angular/core\"];\r\n if (!angularVersion) return false;\r\n // Angular 14+ supports standalone components (standalone: true flag)\r\n // Angular 17+ makes standalone the default\r\n const majorVersion = parseInt(angularVersion.match(/\\d+/)?.[0] ?? \"\", 10);\r\n return !isNaN(majorVersion) && majorVersion >= 14;\r\n};\r\n\r\nconst countSourceFiles = (rootDirectory: string): number => {\r\n const result = spawnSync(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\r\n cwd: rootDirectory,\r\n encoding: \"utf-8\",\r\n maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,\r\n });\r\n\r\n if (result.error || result.status !== 0) {\r\n return 0;\r\n }\r\n\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;\r\n};\r\n\r\nconst hasAngularDependency = (packageJson: PackageJson): boolean => {\r\n const allDependencies = collectAllDependencies(packageJson);\r\n return Boolean(allDependencies[\"@angular/core\"]);\r\n};\r\n\r\n/**\r\n * Walks up the directory tree from `directory` until it finds a `package.json`.\r\n * Returns the directory containing the package.json, or null if not found.\r\n */\r\nconst findNearestPackageJsonDir = (directory: string): string | null => {\r\n let current = directory;\r\n while (true) {\r\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\r\n const parent = path.dirname(current);\r\n if (parent === current) return null;\r\n current = parent;\r\n }\r\n};\r\n\r\nexport const discoverProject = (directory: string): ProjectInfo => {\r\n // First try the given directory, then walk up to find the nearest package.json\r\n const packageJsonDir = fs.existsSync(path.join(directory, \"package.json\"))\r\n ? directory\r\n : findNearestPackageJsonDir(directory);\r\n\r\n if (!packageJsonDir) {\r\n throw new Error(`No package.json found in ${directory} or any parent directory`);\r\n }\r\n\r\n const packageJson = readPackageJson(path.join(packageJsonDir, \"package.json\"));\r\n const allDeps = collectAllDependencies(packageJson);\r\n const angularVersion = detectAngularVersion(allDeps);\r\n const framework = detectFramework(allDeps);\r\n\r\n // tsconfig.json — check the project directory first, then the package.json directory\r\n const hasTypeScript =\r\n fs.existsSync(path.join(directory, \"tsconfig.json\")) ||\r\n fs.existsSync(path.join(packageJsonDir, \"tsconfig.json\"));\r\n\r\n const hasStandaloneComponents = detectStandaloneComponents(packageJson);\r\n const sourceFileCount = countSourceFiles(directory);\r\n\r\n // Use the Angular project name from angular.json if possible, otherwise from package.json\r\n const angularJsonPath = path.join(packageJsonDir, \"angular.json\");\r\n let projectName = packageJson.name ?? path.basename(directory);\r\n if (packageJsonDir !== directory && fs.existsSync(angularJsonPath)) {\r\n // Use the directory name as a more meaningful project name for workspace sub-projects\r\n projectName = path.basename(directory);\r\n }\r\n\r\n return {\r\n rootDirectory: directory,\r\n projectName,\r\n angularVersion,\r\n framework,\r\n hasTypeScript,\r\n hasStandaloneComponents,\r\n sourceFileCount,\r\n };\r\n};\r\n\r\nconst parsePnpmWorkspacePatterns = (rootDirectory: string): string[] => {\r\n const workspacePath = path.join(rootDirectory, \"pnpm-workspace.yaml\");\r\n if (!fs.existsSync(workspacePath)) return [];\r\n\r\n const content = fs.readFileSync(workspacePath, \"utf-8\");\r\n const patterns: string[] = [];\r\n let isInsidePackagesBlock = false;\r\n\r\n for (const line of content.split(\"\\n\")) {\r\n const trimmed = line.trim();\r\n if (trimmed === \"packages:\") {\r\n isInsidePackagesBlock = true;\r\n continue;\r\n }\r\n if (isInsidePackagesBlock && trimmed.startsWith(\"-\")) {\r\n patterns.push(trimmed.replace(/^-\\s*/, \"\").replace(/[\"']/g, \"\"));\r\n } else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith(\"#\")) {\r\n isInsidePackagesBlock = false;\r\n }\r\n }\r\n\r\n return patterns;\r\n};\r\n\r\nconst getWorkspacePatterns = (rootDirectory: string, packageJson: PackageJson): string[] => {\r\n const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);\r\n if (pnpmPatterns.length > 0) return pnpmPatterns;\r\n\r\n if (Array.isArray(packageJson.workspaces)) {\r\n return packageJson.workspaces;\r\n }\r\n\r\n if (packageJson.workspaces?.packages) {\r\n return packageJson.workspaces.packages;\r\n }\r\n\r\n return [];\r\n};\r\n\r\nconst resolveWorkspaceDirectories = (rootDirectory: string, pattern: string): string[] => {\r\n const cleanPattern = pattern.replace(/[\"']/g, \"\").replace(/\\/\\*\\*$/, \"/*\");\r\n\r\n if (!cleanPattern.includes(\"*\")) {\r\n const directoryPath = path.join(rootDirectory, cleanPattern);\r\n if (fs.existsSync(directoryPath) && fs.existsSync(path.join(directoryPath, \"package.json\"))) {\r\n return [directoryPath];\r\n }\r\n return [];\r\n }\r\n\r\n const wildcardIndex = cleanPattern.indexOf(\"*\");\r\n const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));\r\n const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);\r\n\r\n if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) {\r\n return [];\r\n }\r\n\r\n return fs\r\n .readdirSync(baseDirectory)\r\n .map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard))\r\n .filter(\r\n (entryPath) =>\r\n fs.existsSync(entryPath) &&\r\n fs.statSync(entryPath).isDirectory() &&\r\n fs.existsSync(path.join(entryPath, \"package.json\")),\r\n );\r\n};\r\n\r\nexport const listWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (!fs.existsSync(packageJsonPath)) return [];\r\n\r\n const packageJson = readPackageJson(packageJsonPath);\r\n const patterns = getWorkspacePatterns(rootDirectory, packageJson);\r\n if (patterns.length === 0) return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const pattern of patterns) {\r\n const directories = resolveWorkspaceDirectories(rootDirectory, pattern);\r\n for (const workspaceDirectory of directories) {\r\n const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, \"package.json\"));\r\n\r\n if (!hasAngularDependency(workspacePackageJson)) continue;\r\n\r\n const name = workspacePackageJson.name ?? path.basename(workspaceDirectory);\r\n packages.push({ name, directory: workspaceDirectory });\r\n }\r\n }\r\n\r\n return packages;\r\n};\r\n\r\ninterface AngularWorkspaceProject {\r\n projectType?: string;\r\n root?: string;\r\n}\r\n\r\ninterface AngularWorkspace {\r\n version?: number;\r\n projects?: Record<string, AngularWorkspaceProject | string>;\r\n}\r\n\r\n/**\r\n * Reads `angular.json` (Angular CLI workspace config) and returns one\r\n * WorkspacePackage per project whose root directory contains an Angular dependency.\r\n */\r\nexport const listAngularWorkspaceProjects = (rootDirectory: string): WorkspacePackage[] => {\r\n const angularJsonPath = path.join(rootDirectory, \"angular.json\");\r\n if (!fs.existsSync(angularJsonPath)) return [];\r\n\r\n let workspace: AngularWorkspace;\r\n try {\r\n workspace = JSON.parse(fs.readFileSync(angularJsonPath, \"utf-8\")) as AngularWorkspace;\r\n } catch {\r\n return [];\r\n }\r\n\r\n if (!workspace.projects || typeof workspace.projects !== \"object\") return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const [name, projectConfig] of Object.entries(workspace.projects)) {\r\n // Older angular.json formats store the root as a plain string\r\n const root =\r\n typeof projectConfig === \"string\"\r\n ? projectConfig\r\n : (projectConfig.root ?? \"\");\r\n\r\n const projectDirectory = root ? path.resolve(rootDirectory, root) : rootDirectory;\r\n\r\n // Only include directories that actually exist\r\n if (!fs.existsSync(projectDirectory) || !fs.statSync(projectDirectory).isDirectory()) {\r\n continue;\r\n }\r\n\r\n packages.push({ name, directory: projectDirectory });\r\n }\r\n\r\n return packages;\r\n};\r\n","import pc from \"picocolors\";\r\n\r\nexport const highlighter = {\r\n error: pc.red,\r\n warn: pc.yellow,\r\n info: pc.cyan,\r\n success: pc.green,\r\n dim: pc.dim,\r\n};\r\n","import { highlighter } from \"./highlighter.js\";\r\n\r\nexport const logger = {\r\n log: (message: string) => process.stdout.write(`${message}\\n`),\r\n break: () => process.stdout.write(\"\\n\"),\r\n dim: (message: string) => process.stdout.write(`${highlighter.dim(message)}\\n`),\r\n warn: (message: string) => process.stdout.write(`${highlighter.warn(message)}\\n`),\r\n error: (message: string) => process.stderr.write(`${highlighter.error(message)}\\n`),\r\n success: (message: string) => process.stdout.write(`${highlighter.success(message)}\\n`),\r\n info: (message: string) => process.stdout.write(`${highlighter.info(message)}\\n`),\r\n};\r\n","import { SUMMARY_BOX_HORIZONTAL_PADDING_CHARS, SUMMARY_BOX_OUTER_INDENT_CHARS } from \"../constants.js\";\r\nimport { highlighter } from \"./highlighter.js\";\r\nimport { logger } from \"./logger.js\";\r\n\r\nexport interface FramedLine {\r\n plainText: string;\r\n renderedText: string;\r\n}\r\n\r\nexport const createFramedLine = (plainText: string, renderedText?: string): FramedLine => ({\r\n plainText,\r\n renderedText: renderedText ?? plainText,\r\n});\r\n\r\nconst calculateMaxWidth = (lines: FramedLine[]): number =>\r\n Math.max(...lines.map((line) => line.plainText.length));\r\n\r\nconst buildBoxRow = (content: string, width: number): string => {\r\n const padding = \" \".repeat(SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n const rightPad = \" \".repeat(width - content.length + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n return `│${padding}${content}${rightPad}│`;\r\n};\r\n\r\nconst buildRenderedBoxRow = (plainContent: string, renderedContent: string, width: number): string => {\r\n const padding = \" \".repeat(SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n const rightPad = \" \".repeat(width - plainContent.length + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS);\r\n return highlighter.dim(`│`) + padding + renderedContent + rightPad + highlighter.dim(`│`);\r\n};\r\n\r\nexport const renderFramedBoxString = (lines: FramedLine[]): string => {\r\n const width = calculateMaxWidth(lines);\r\n const indent = \" \".repeat(SUMMARY_BOX_OUTER_INDENT_CHARS);\r\n const topBorder = `┌${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┐`;\r\n const bottomBorder = `└${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┘`;\r\n\r\n const rows = lines.map((line) => buildBoxRow(line.plainText, width));\r\n return [topBorder, ...rows, bottomBorder]\r\n .map((row) => `${indent}${row}`)\r\n .join(\"\\n\");\r\n};\r\n\r\nexport const printFramedBox = (lines: FramedLine[]): void => {\r\n const width = calculateMaxWidth(lines);\r\n const indent = \" \".repeat(SUMMARY_BOX_OUTER_INDENT_CHARS);\r\n const topBorder = highlighter.dim(`┌${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┐`);\r\n const bottomBorder = highlighter.dim(`└${\"─\".repeat(width + SUMMARY_BOX_HORIZONTAL_PADDING_CHARS * 2)}┘`);\r\n\r\n logger.log(`${indent}${topBorder}`);\r\n for (const line of lines) {\r\n logger.log(`${indent}${buildRenderedBoxRow(line.plainText, line.renderedText, width)}`);\r\n }\r\n logger.log(`${indent}${bottomBorder}`);\r\n};\r\n","export const groupBy = <T>(items: T[], key: (item: T) => string): Map<string, T[]> => {\r\n const map = new Map<string, T[]>();\r\n for (const item of items) {\r\n const k = key(item);\r\n const group = map.get(k) ?? [];\r\n group.push(item);\r\n map.set(k, group);\r\n }\r\n return map;\r\n};\r\n","export const indentMultilineText = (text: string, indent: string): string =>\r\n text\r\n .split(\"\\n\")\r\n .map((line) => `${indent}${line}`)\r\n .join(\"\\n\");\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { AngularDoctorConfig } from \"../types.js\";\r\n\r\nconst CONFIG_FILENAME = \"angular-doctor.config.json\";\r\nconst PACKAGE_JSON_CONFIG_KEY = \"angularDoctor\";\r\n\r\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\r\n typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n\r\nexport const loadConfig = (rootDirectory: string): AngularDoctorConfig | null => {\r\n const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);\r\n\r\n if (fs.existsSync(configFilePath)) {\r\n try {\r\n const fileContent = fs.readFileSync(configFilePath, \"utf-8\");\r\n const parsed: unknown = JSON.parse(fileContent);\r\n if (!isPlainObject(parsed)) {\r\n console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);\r\n return null;\r\n }\r\n return parsed as AngularDoctorConfig;\r\n } catch (error) {\r\n console.warn(\r\n `Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n return null;\r\n }\r\n }\r\n\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (fs.existsSync(packageJsonPath)) {\r\n try {\r\n const fileContent = fs.readFileSync(packageJsonPath, \"utf-8\");\r\n const packageJson = JSON.parse(fileContent) as Record<string, unknown>;\r\n const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];\r\n if (isPlainObject(embeddedConfig)) {\r\n return embeddedConfig as AngularDoctorConfig;\r\n }\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { ESLint, type Linter } from \"eslint\";\r\nimport angularEslintPlugin from \"@angular-eslint/eslint-plugin\";\r\nimport tsEslint from \"typescript-eslint\";\r\nimport type { Diagnostic } from \"../types.js\";\r\n\r\n// Rule category mapping\r\nconst RULE_CATEGORY_MAP: Record<string, string> = {\r\n // Angular component best practices\r\n \"@angular-eslint/component-class-suffix\": \"Components\",\r\n \"@angular-eslint/directive-class-suffix\": \"Components\",\r\n \"@angular-eslint/pipe-prefix\": \"Components\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"Components\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Components\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"Components\",\r\n \"@angular-eslint/consistent-component-styles\": \"Components\",\r\n\r\n // Angular performance\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Performance\",\r\n \"@angular-eslint/no-output-native\": \"Performance\",\r\n\r\n // Angular architecture / correctness\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/contextual-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/no-forward-ref\": \"Architecture\",\r\n \"@angular-eslint/no-input-rename\": \"Architecture\",\r\n \"@angular-eslint/no-output-rename\": \"Architecture\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/prefer-standalone\": \"Architecture\",\r\n\r\n // TypeScript quality\r\n \"@typescript-eslint/no-explicit-any\": \"TypeScript\",\r\n \"@typescript-eslint/no-unused-vars\": \"Dead Code\",\r\n};\r\n\r\n// Rule severity mapping\r\nconst RULE_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/component-class-suffix\": \"warning\",\r\n \"@angular-eslint/directive-class-suffix\": \"warning\",\r\n \"@angular-eslint/pipe-prefix\": \"warning\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warning\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warning\",\r\n \"@angular-eslint/consistent-component-styles\": \"warning\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warning\",\r\n \"@angular-eslint/no-forward-ref\": \"warning\",\r\n \"@angular-eslint/no-input-rename\": \"warning\",\r\n \"@angular-eslint/no-output-rename\": \"warning\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/prefer-standalone\": \"warning\",\r\n \"@typescript-eslint/no-explicit-any\": \"warning\",\r\n \"@typescript-eslint/no-unused-vars\": \"warning\",\r\n};\r\n\r\n// Human-readable messages and help text\r\nconst RULE_MESSAGE_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\":\r\n \"Component class should end with 'Component'\",\r\n \"@angular-eslint/directive-class-suffix\":\r\n \"Directive class should end with 'Directive'\",\r\n \"@angular-eslint/pipe-prefix\": \"Pipe name should have a consistent prefix\",\r\n \"@angular-eslint/use-pipe-transform-interface\":\r\n \"Pipe class must implement PipeTransform interface\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Remove empty lifecycle methods\",\r\n \"@angular-eslint/use-lifecycle-interface\":\r\n \"Implement the lifecycle interface for lifecycle hooks\",\r\n \"@angular-eslint/consistent-component-styles\":\r\n \"Use consistent styles type in component decorator\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\":\r\n \"Use OnPush change detection for better performance\",\r\n \"@angular-eslint/no-output-native\":\r\n \"Avoid shadowing native DOM events in output names\",\r\n \"@angular-eslint/no-conflicting-lifecycle\":\r\n \"Lifecycle hooks DoCheck and OnChanges cannot be used together\",\r\n \"@angular-eslint/contextual-lifecycle\":\r\n \"Lifecycle hook is not available in this context\",\r\n \"@angular-eslint/no-forward-ref\":\r\n \"Avoid using forwardRef — restructure to avoid circular dependency\",\r\n \"@angular-eslint/no-input-rename\":\r\n \"Avoid renaming directive inputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-output-rename\":\r\n \"Avoid renaming directive outputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-inputs-metadata-property\":\r\n \"Use @Input() decorator instead of inputs metadata property\",\r\n \"@angular-eslint/no-outputs-metadata-property\":\r\n \"Use @Output() decorator instead of outputs metadata property\",\r\n \"@angular-eslint/prefer-standalone\":\r\n \"Prefer standalone components over NgModule-based components\",\r\n \"@typescript-eslint/no-explicit-any\":\r\n \"Avoid 'any' type — use specific types for better type safety\",\r\n \"@typescript-eslint/no-unused-vars\": \"Remove unused variable declaration\",\r\n};\r\n\r\nconst RULE_HELP_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\":\r\n \"Add 'Component' suffix: `export class UserProfileComponent { }`\",\r\n \"@angular-eslint/directive-class-suffix\":\r\n \"Add 'Directive' suffix: `export class HighlightDirective { }`\",\r\n \"@angular-eslint/use-pipe-transform-interface\":\r\n \"Implement PipeTransform: `export class MyPipe implements PipeTransform { transform(value: unknown) { } }`\",\r\n \"@angular-eslint/no-empty-lifecycle-method\":\r\n \"Remove the empty lifecycle method or add logic to it\",\r\n \"@angular-eslint/use-lifecycle-interface\":\r\n \"Add the interface: `export class MyComponent implements OnInit, OnDestroy { }`\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\":\r\n \"Add to decorator: `@Component({ changeDetection: ChangeDetectionStrategy.OnPush })`\",\r\n \"@angular-eslint/no-output-native\":\r\n \"Rename the output: use a descriptive name like `(valueChange)` instead of `(click)` or `(change)`\",\r\n \"@angular-eslint/no-forward-ref\":\r\n \"Restructure your code to avoid circular dependencies, or use `inject()` with a lazy function\",\r\n \"@angular-eslint/no-input-rename\":\r\n \"Remove the alias: `@Input() myProp: string` instead of `@Input('myAlias') myProp: string`\",\r\n \"@angular-eslint/no-output-rename\":\r\n \"Remove the alias: `@Output() myEvent = new EventEmitter()` instead of aliased version\",\r\n \"@angular-eslint/no-inputs-metadata-property\":\r\n \"Use `@Input() myProp: string` decorator on the property instead of `inputs: ['myProp']` in the decorator metadata\",\r\n \"@angular-eslint/no-outputs-metadata-property\":\r\n \"Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata\",\r\n \"@angular-eslint/prefer-standalone\":\r\n \"Add `standalone: true` to component: `@Component({ standalone: true, ... })`\",\r\n \"@typescript-eslint/no-explicit-any\":\r\n \"Replace `any` with a specific type or `unknown` if the type is truly unknown\",\r\n \"@typescript-eslint/no-unused-vars\":\r\n \"Remove the unused variable or prefix with `_` to indicate it's intentionally unused\",\r\n};\r\n\r\nconst buildEslintConfig = (\r\n hasTypeScript: boolean,\r\n tsconfigPath: string | null,\r\n useTypeAware: boolean,\r\n): Linter.Config[] => {\r\n const languageOptions: Linter.Config[\"languageOptions\"] = {\r\n parser: tsEslint.parser as Linter.Parser,\r\n parserOptions: {\r\n ecmaVersion: \"latest\",\r\n sourceType: \"module\",\r\n ...(hasTypeScript && tsconfigPath && useTypeAware\r\n ? { project: tsconfigPath }\r\n : {}),\r\n },\r\n };\r\n\r\n const angularRules: Linter.RulesRecord = {\r\n \"@angular-eslint/component-class-suffix\": \"warn\",\r\n \"@angular-eslint/directive-class-suffix\": \"warn\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warn\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warn\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warn\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/no-forward-ref\": \"warn\",\r\n \"@angular-eslint/no-input-rename\": \"warn\",\r\n \"@angular-eslint/no-output-rename\": \"warn\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warn\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warn\",\r\n };\r\n\r\n const tsRules: Linter.RulesRecord = {\r\n \"@typescript-eslint/no-explicit-any\": \"warn\",\r\n };\r\n\r\n return [\r\n {\r\n files: [\"**/*.ts\"],\r\n plugins: {\r\n \"@angular-eslint\": angularEslintPlugin as unknown as ESLint.Plugin,\r\n \"@typescript-eslint\": tsEslint.plugin as unknown as ESLint.Plugin,\r\n },\r\n languageOptions,\r\n rules: {\r\n ...angularRules,\r\n ...tsRules,\r\n },\r\n },\r\n ];\r\n};\r\n\r\nconst mapEslintSeverity = (\r\n severity: number,\r\n ruleId: string | null,\r\n): \"error\" | \"warning\" => {\r\n if (ruleId && RULE_SEVERITY_MAP[ruleId]) {\r\n return RULE_SEVERITY_MAP[ruleId];\r\n }\r\n return severity === 2 ? \"error\" : \"warning\";\r\n};\r\n\r\nconst resolveDiagnosticCategory = (ruleId: string): string =>\r\n RULE_CATEGORY_MAP[ruleId] ?? \"Other\";\r\n\r\nconst resolveMessage = (ruleId: string, defaultMessage: string): string =>\r\n RULE_MESSAGE_MAP[ruleId] ?? defaultMessage;\r\n\r\nconst resolveHelp = (ruleId: string): string => RULE_HELP_MAP[ruleId] ?? \"\";\r\n\r\nconst parsePluginAndRule = (\r\n ruleId: string,\r\n): { plugin: string; rule: string } => {\r\n // e.g. \"@angular-eslint/component-class-suffix\" -> plugin: \"@angular-eslint\", rule: \"component-class-suffix\"\r\n // e.g. \"@typescript-eslint/no-explicit-any\" -> plugin: \"@typescript-eslint\", rule: \"no-explicit-any\"\r\n const match = ruleId.match(/^(@[^/]+\\/[^/]+|[^/]+)\\/(.+)$/);\r\n if (match) {\r\n return { plugin: match[1], rule: match[2] };\r\n }\r\n return { plugin: \"eslint\", rule: ruleId };\r\n};\r\n\r\nexport const runEslint = async (\r\n rootDirectory: string,\r\n hasTypeScript: boolean,\r\n includePaths?: string[],\r\n options?: { useTypeAware?: boolean },\r\n): Promise<Diagnostic[]> => {\r\n if (includePaths !== undefined && includePaths.length === 0) {\r\n return [];\r\n }\r\n\r\n const tsconfigPath = hasTypeScript\r\n ? path.join(rootDirectory, \"tsconfig.json\")\r\n : null;\r\n\r\n const cacheRoot = path.join(\r\n rootDirectory,\r\n \"node_modules\",\r\n \".cache\",\r\n \"angular-doctor\",\r\n );\r\n fs.mkdirSync(cacheRoot, { recursive: true });\r\n const eslint = new ESLint({\r\n cwd: rootDirectory,\r\n overrideConfigFile: null,\r\n overrideConfig: buildEslintConfig(\r\n hasTypeScript,\r\n tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null,\r\n options?.useTypeAware ?? true,\r\n ),\r\n ignore: true,\r\n cache: true,\r\n cacheLocation: path.join(cacheRoot, \".eslintcache\"),\r\n });\r\n\r\n const patterns = includePaths ?? [\"**/*.ts\"];\r\n\r\n let results: ESLint.LintResult[];\r\n try {\r\n results = await eslint.lintFiles(patterns);\r\n } catch {\r\n return [];\r\n }\r\n\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const result of results) {\r\n for (const message of result.messages) {\r\n if (!message.ruleId) continue;\r\n const { plugin, rule } = parsePluginAndRule(message.ruleId);\r\n const ruleKey = message.ruleId;\r\n\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, result.filePath),\r\n plugin,\r\n rule,\r\n severity: mapEslintSeverity(message.severity, ruleKey),\r\n message: resolveMessage(ruleKey, message.message),\r\n help: resolveHelp(ruleKey),\r\n line: message.line ?? 0,\r\n column: message.column ?? 0,\r\n category: resolveDiagnosticCategory(ruleKey),\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { main } from \"knip\";\r\nimport { createOptions } from \"knip/session\";\r\nimport { MAX_KNIP_RETRIES } from \"../constants.js\";\r\nimport type { Diagnostic, KnipIssueRecords, KnipResults } from \"../types.js\";\r\n\r\nconst KNIP_CATEGORY_MAP: Record<string, string> = {\r\n files: \"Dead Code\",\r\n exports: \"Dead Code\",\r\n types: \"Dead Code\",\r\n duplicates: \"Dead Code\",\r\n};\r\n\r\nconst KNIP_MESSAGE_MAP: Record<string, string> = {\r\n files: \"Unused file\",\r\n exports: \"Unused export\",\r\n types: \"Unused type\",\r\n duplicates: \"Duplicate export\",\r\n};\r\n\r\nconst KNIP_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n files: \"warning\",\r\n exports: \"warning\",\r\n types: \"warning\",\r\n duplicates: \"warning\",\r\n};\r\n\r\nconst collectIssueRecords = (\r\n records: KnipIssueRecords,\r\n issueType: string,\r\n rootDirectory: string,\r\n): Diagnostic[] => {\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const issues of Object.values(records)) {\r\n for (const issue of Object.values(issues)) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, issue.filePath),\r\n plugin: \"knip\",\r\n rule: issueType,\r\n severity: KNIP_SEVERITY_MAP[issueType] ?? \"warning\",\r\n message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,\r\n help: \"\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[issueType] ?? \"Dead Code\",\r\n weight: 1,\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n\r\n// HACK: knip triggers dotenv which logs to stdout/stderr via console methods\r\nconst silenced = async <T>(fn: () => Promise<T>): Promise<T> => {\r\n const originalLog = console.log;\r\n const originalInfo = console.info;\r\n const originalWarn = console.warn;\r\n const originalError = console.error;\r\n console.log = () => {};\r\n console.info = () => {};\r\n console.warn = () => {};\r\n console.error = () => {};\r\n try {\r\n return await fn();\r\n } finally {\r\n console.log = originalLog;\r\n console.info = originalInfo;\r\n console.warn = originalWarn;\r\n console.error = originalError;\r\n }\r\n};\r\n\r\nconst CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\\/([a-z-]+)\\.config\\./;\r\n\r\nconst extractFailedPluginName = (error: unknown): string | null => {\r\n const match = String(error).match(CONFIG_LOADING_ERROR_PATTERN);\r\n return match?.[1] ?? null;\r\n};\r\n\r\nconst runKnipWithOptions = async (\r\n knipCwd: string,\r\n workspaceName?: string,\r\n): Promise<KnipResults> => {\r\n const options = await silenced(() =>\r\n createOptions({\r\n cwd: knipCwd,\r\n isShowProgress: false,\r\n ...(workspaceName ? { workspace: workspaceName } : {}),\r\n }),\r\n );\r\n\r\n const parsedConfig = options.parsedConfig as Record<string, unknown>;\r\n\r\n for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) {\r\n try {\r\n return (await silenced(() => main(options))) as KnipResults;\r\n } catch (error) {\r\n const failedPlugin = extractFailedPluginName(error);\r\n if (!failedPlugin || attempt === MAX_KNIP_RETRIES) {\r\n throw error;\r\n }\r\n parsedConfig[failedPlugin] = false;\r\n }\r\n }\r\n\r\n throw new Error(\"Unreachable\");\r\n};\r\n\r\nconst hasNodeModules = (directory: string): boolean => {\r\n const nodeModulesPath = path.join(directory, \"node_modules\");\r\n return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();\r\n};\r\n\r\nexport const runKnip = async (rootDirectory: string): Promise<Diagnostic[]> => {\r\n if (!hasNodeModules(rootDirectory)) {\r\n return [];\r\n }\r\n\r\n const knipResult = await runKnipWithOptions(rootDirectory);\r\n\r\n const { issues } = knipResult;\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const unusedFile of issues.files) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, unusedFile),\r\n plugin: \"knip\",\r\n rule: \"files\",\r\n severity: KNIP_SEVERITY_MAP[\"files\"],\r\n message: KNIP_MESSAGE_MAP[\"files\"],\r\n help: \"This file is not imported by any other file in the project.\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[\"files\"],\r\n weight: 1,\r\n });\r\n }\r\n\r\n const recordTypes = [\"exports\", \"types\", \"duplicates\"] as const;\r\n\r\n for (const issueType of recordTypes) {\r\n diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import ora from \"ora\";\r\n\r\nexport const spinner = (text: string) => ora({ text, isSilent: !process.stdout.isTTY });\r\n","import { randomUUID } from \"node:crypto\";\r\nimport { mkdirSync, writeFileSync } from \"node:fs\";\r\nimport { tmpdir } from \"node:os\";\r\nimport { dirname, extname, isAbsolute, join, resolve } from \"node:path\";\r\nimport { performance } from \"node:perf_hooks\";\r\nimport {\r\n MILLISECONDS_PER_SECOND,\r\n PERFECT_SCORE,\r\n SCORE_BAR_WIDTH_CHARS,\r\n SCORE_GOOD_THRESHOLD,\r\n SCORE_OK_THRESHOLD,\r\n} from \"./constants.js\";\r\nimport type {\r\n AngularDoctorConfig,\r\n Diagnostic,\r\n ProjectInfo,\r\n ScanOptions,\r\n ScanResult,\r\n ScoreResult,\r\n} from \"./types.js\";\r\nimport { calculateScore } from \"./utils/calculate-score.js\";\r\nimport { colorizeByScore } from \"./utils/colorize-by-score.js\";\r\nimport {\r\n combineDiagnostics,\r\n computeIncludePaths,\r\n} from \"./utils/combine-diagnostics.js\";\r\nimport {\r\n discoverProject,\r\n formatFrameworkName,\r\n} from \"./utils/discover-project.js\";\r\nimport {\r\n type FramedLine,\r\n createFramedLine,\r\n printFramedBox,\r\n} from \"./utils/framed-box.js\";\r\nimport { groupBy } from \"./utils/group-by.js\";\r\nimport { highlighter } from \"./utils/highlighter.js\";\r\nimport { indentMultilineText } from \"./utils/indent-multiline-text.js\";\r\nimport { loadConfig } from \"./utils/load-config.js\";\r\nimport { logger } from \"./utils/logger.js\";\r\nimport { runEslint } from \"./utils/run-eslint.js\";\r\nimport { runKnip } from \"./utils/run-knip.js\";\r\nimport { spinner } from \"./utils/spinner.js\";\r\n\r\ninterface ScoreBarSegments {\r\n filledSegment: string;\r\n emptySegment: string;\r\n}\r\n\r\nconst SEVERITY_ORDER: Record<Diagnostic[\"severity\"], number> = {\r\n error: 0,\r\n warning: 1,\r\n};\r\n\r\nconst colorizeBySeverity = (\r\n text: string,\r\n severity: Diagnostic[\"severity\"],\r\n): string =>\r\n severity === \"error\" ? highlighter.error(text) : highlighter.warn(text);\r\n\r\nconst sortBySeverity = (\r\n diagnosticGroups: [string, Diagnostic[]][],\r\n): [string, Diagnostic[]][] =>\r\n diagnosticGroups.toSorted(([, diagnosticsA], [, diagnosticsB]) => {\r\n const severityA = SEVERITY_ORDER[diagnosticsA[0].severity];\r\n const severityB = SEVERITY_ORDER[diagnosticsB[0].severity];\r\n return severityA - severityB;\r\n });\r\n\r\nconst collectAffectedFiles = (diagnostics: Diagnostic[]): Set<string> =>\r\n new Set(diagnostics.map((diagnostic) => diagnostic.filePath));\r\n\r\nconst buildFileLineMap = (diagnostics: Diagnostic[]): Map<string, number[]> => {\r\n const fileLines = new Map<string, number[]>();\r\n for (const diagnostic of diagnostics) {\r\n const lines = fileLines.get(diagnostic.filePath) ?? [];\r\n if (diagnostic.line > 0) {\r\n lines.push(diagnostic.line);\r\n }\r\n fileLines.set(diagnostic.filePath, lines);\r\n }\r\n return fileLines;\r\n};\r\n\r\nconst printDiagnostics = (\r\n diagnostics: Diagnostic[],\r\n isVerbose: boolean,\r\n): void => {\r\n const ruleGroups = groupBy(\r\n diagnostics,\r\n (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`,\r\n );\r\n\r\n const sortedRuleGroups = sortBySeverity([...ruleGroups.entries()]);\r\n\r\n for (const [, ruleDiagnostics] of sortedRuleGroups) {\r\n const firstDiagnostic = ruleDiagnostics[0];\r\n const severitySymbol = firstDiagnostic.severity === \"error\" ? \"✗\" : \"⚠\";\r\n const icon = colorizeBySeverity(severitySymbol, firstDiagnostic.severity);\r\n const count = ruleDiagnostics.length;\r\n const countLabel =\r\n count > 1\r\n ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity)\r\n : \"\";\r\n\r\n logger.log(` ${icon} ${firstDiagnostic.message}${countLabel}`);\r\n if (firstDiagnostic.help) {\r\n logger.dim(indentMultilineText(firstDiagnostic.help, \" \"));\r\n }\r\n\r\n if (isVerbose) {\r\n const fileLines = buildFileLineMap(ruleDiagnostics);\r\n\r\n for (const [filePath, lines] of fileLines) {\r\n const lineLabel = lines.length > 0 ? `: ${lines.join(\", \")}` : \"\";\r\n logger.dim(` ${filePath}${lineLabel}`);\r\n }\r\n }\r\n\r\n logger.break();\r\n }\r\n};\r\n\r\nconst formatElapsedTime = (elapsedMilliseconds: number): string => {\r\n if (elapsedMilliseconds < MILLISECONDS_PER_SECOND) {\r\n return `${Math.round(elapsedMilliseconds)}ms`;\r\n }\r\n return `${(elapsedMilliseconds / MILLISECONDS_PER_SECOND).toFixed(1)}s`;\r\n};\r\n\r\nconst formatRuleSummary = (\r\n ruleKey: string,\r\n ruleDiagnostics: Diagnostic[],\r\n): string => {\r\n const firstDiagnostic = ruleDiagnostics[0];\r\n const fileLines = buildFileLineMap(ruleDiagnostics);\r\n\r\n const sections = [\r\n `Rule: ${ruleKey}`,\r\n `Severity: ${firstDiagnostic.severity}`,\r\n `Category: ${firstDiagnostic.category}`,\r\n `Count: ${ruleDiagnostics.length}`,\r\n \"\",\r\n firstDiagnostic.message,\r\n ];\r\n\r\n if (firstDiagnostic.help) {\r\n sections.push(\"\", `Suggestion: ${firstDiagnostic.help}`);\r\n }\r\n\r\n sections.push(\"\", \"Files:\");\r\n for (const [filePath, lines] of fileLines) {\r\n const lineLabel = lines.length > 0 ? `: ${lines.join(\", \")}` : \"\";\r\n sections.push(` ${filePath}${lineLabel}`);\r\n }\r\n\r\n return sections.join(\"\\n\") + \"\\n\";\r\n};\r\n\r\nconst buildMarkdownReport = (\r\n diagnostics: Diagnostic[],\r\n elapsedMilliseconds: number,\r\n scoreResult: ScoreResult | null,\r\n totalSourceFileCount: number,\r\n): string => {\r\n const errorCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"error\",\r\n ).length;\r\n const warningCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"warning\",\r\n ).length;\r\n const affectedFileCount = collectAffectedFiles(diagnostics).size;\r\n const elapsed = formatElapsedTime(elapsedMilliseconds);\r\n\r\n const lines: string[] = [\r\n \"# Angular Doctor Report\",\r\n \"\",\r\n `Generated: ${new Date().toISOString()}`,\r\n \"\",\r\n ];\r\n\r\n if (scoreResult) {\r\n lines.push(\r\n \"## Score\",\r\n \"\",\r\n `**${scoreResult.score} / ${PERFECT_SCORE}** — ${scoreResult.label}`,\r\n \"\",\r\n );\r\n }\r\n\r\n lines.push(\r\n \"## Summary\",\r\n \"\",\r\n `- Errors: **${errorCount}**`,\r\n `- Warnings: **${warningCount}**`,\r\n totalSourceFileCount > 0\r\n ? `- Affected files: **${affectedFileCount}/${totalSourceFileCount}**`\r\n : `- Affected files: **${affectedFileCount}**`,\r\n `- Elapsed: **${elapsed}**`,\r\n \"\",\r\n );\r\n\r\n if (diagnostics.length === 0) {\r\n lines.push(\"## Diagnostics\", \"\", \"No issues found.\", \"\");\r\n return lines.join(\"\\n\");\r\n }\r\n\r\n const ruleGroups = groupBy(\r\n diagnostics,\r\n (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`,\r\n );\r\n const sortedRuleGroups = sortBySeverity([...ruleGroups.entries()]);\r\n\r\n lines.push(\"## Diagnostics\", \"\");\r\n\r\n for (const [ruleKey, ruleDiagnostics] of sortedRuleGroups) {\r\n const firstDiagnostic = ruleDiagnostics[0];\r\n const fileLines = buildFileLineMap(ruleDiagnostics);\r\n\r\n lines.push(`### ${ruleKey}`, \"\");\r\n lines.push(\r\n `- Severity: **${firstDiagnostic.severity}**`,\r\n `- Category: **${firstDiagnostic.category}**`,\r\n `- Count: **${ruleDiagnostics.length}**`,\r\n \"\",\r\n firstDiagnostic.message,\r\n \"\",\r\n );\r\n\r\n if (firstDiagnostic.help) {\r\n lines.push(`**Suggestion:** ${firstDiagnostic.help}`, \"\");\r\n }\r\n\r\n lines.push(\"**Files:**\");\r\n for (const [filePath, linesList] of fileLines) {\r\n const lineLabel = linesList.length > 0 ? `: ${linesList.join(\", \")}` : \"\";\r\n lines.push(`- ${filePath}${lineLabel}`);\r\n }\r\n lines.push(\"\");\r\n }\r\n\r\n return lines.join(\"\\n\");\r\n};\r\n\r\nconst resolveReportPath = (\r\n report: boolean | string | undefined,\r\n outputDirectory: string,\r\n baseDirectory: string,\r\n): string | null => {\r\n if (!report) return null;\r\n\r\n if (typeof report === \"string\") {\r\n const absolutePath = isAbsolute(report)\r\n ? report\r\n : resolve(baseDirectory, report);\r\n if (extname(absolutePath)) return absolutePath;\r\n return join(absolutePath, \"report.md\");\r\n }\r\n\r\n return join(outputDirectory, \"report.md\");\r\n};\r\n\r\nconst writeDiagnosticsDirectory = (\r\n diagnostics: Diagnostic[],\r\n elapsedMilliseconds: number,\r\n scoreResult: ScoreResult | null,\r\n totalSourceFileCount: number,\r\n report: boolean | string | undefined,\r\n baseDirectory: string,\r\n): { outputDirectory: string; markdownPath: string | null } => {\r\n const outputDirectory = join(tmpdir(), `angular-doctor-${randomUUID()}`);\r\n mkdirSync(outputDirectory);\r\n\r\n const ruleGroups = groupBy(\r\n diagnostics,\r\n (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`,\r\n );\r\n const sortedRuleGroups = sortBySeverity([...ruleGroups.entries()]);\r\n\r\n for (const [ruleKey, ruleDiagnostics] of sortedRuleGroups) {\r\n const fileName = ruleKey.replace(/\\//g, \"--\") + \".txt\";\r\n writeFileSync(\r\n join(outputDirectory, fileName),\r\n formatRuleSummary(ruleKey, ruleDiagnostics),\r\n );\r\n }\r\n\r\n writeFileSync(\r\n join(outputDirectory, \"diagnostics.json\"),\r\n JSON.stringify(diagnostics, null, 2),\r\n );\r\n\r\n const markdownPath = resolveReportPath(\r\n report,\r\n outputDirectory,\r\n baseDirectory,\r\n );\r\n if (markdownPath) {\r\n mkdirSync(dirname(markdownPath), { recursive: true });\r\n writeFileSync(\r\n markdownPath,\r\n buildMarkdownReport(\r\n diagnostics,\r\n elapsedMilliseconds,\r\n scoreResult,\r\n totalSourceFileCount,\r\n ),\r\n );\r\n }\r\n\r\n return { outputDirectory, markdownPath };\r\n};\r\n\r\nconst buildScoreBarSegments = (score: number): ScoreBarSegments => {\r\n const filledCount = Math.round(\r\n (score / PERFECT_SCORE) * SCORE_BAR_WIDTH_CHARS,\r\n );\r\n const emptyCount = SCORE_BAR_WIDTH_CHARS - filledCount;\r\n\r\n return {\r\n filledSegment: \"█\".repeat(filledCount),\r\n emptySegment: \"░\".repeat(emptyCount),\r\n };\r\n};\r\n\r\nconst buildPlainScoreBar = (score: number): string => {\r\n const { filledSegment, emptySegment } = buildScoreBarSegments(score);\r\n return `${filledSegment}${emptySegment}`;\r\n};\r\n\r\nconst buildScoreBar = (score: number): string => {\r\n const { filledSegment, emptySegment } = buildScoreBarSegments(score);\r\n return colorizeByScore(filledSegment, score) + highlighter.dim(emptySegment);\r\n};\r\n\r\nconst printScoreGauge = (score: number, label: string): void => {\r\n const scoreDisplay = colorizeByScore(`${score}`, score);\r\n const labelDisplay = colorizeByScore(label, score);\r\n logger.log(` ${scoreDisplay} / ${PERFECT_SCORE} ${labelDisplay}`);\r\n logger.break();\r\n logger.log(` ${buildScoreBar(score)}`);\r\n logger.break();\r\n};\r\n\r\nconst getDoctorFace = (score: number): string[] => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return [\"◠ ◠\", \" ▽ \"];\r\n if (score >= SCORE_OK_THRESHOLD) return [\"• •\", \" ─ \"];\r\n return [\"x x\", \" ▽ \"];\r\n};\r\n\r\nconst printBranding = (score?: number): void => {\r\n if (score !== undefined) {\r\n const [eyes, mouth] = getDoctorFace(score);\r\n const colorize = (text: string) => colorizeByScore(text, score);\r\n logger.log(colorize(\" ┌─────┐\"));\r\n logger.log(colorize(` │ ${eyes} │`));\r\n logger.log(colorize(` │ ${mouth} │`));\r\n logger.log(colorize(\" └─────┘\"));\r\n }\r\n logger.log(` Angular Doctor`);\r\n logger.break();\r\n};\r\n\r\nconst buildBrandingLines = (scoreResult: ScoreResult | null): FramedLine[] => {\r\n const lines: FramedLine[] = [];\r\n\r\n if (scoreResult) {\r\n const [eyes, mouth] = getDoctorFace(scoreResult.score);\r\n const scoreColorizer = (text: string): string =>\r\n colorizeByScore(text, scoreResult.score);\r\n\r\n lines.push(createFramedLine(\"┌─────┐\", scoreColorizer(\"┌─────┐\")));\r\n lines.push(createFramedLine(`│ ${eyes} │`, scoreColorizer(`│ ${eyes} │`)));\r\n lines.push(\r\n createFramedLine(`│ ${mouth} │`, scoreColorizer(`│ ${mouth} │`)),\r\n );\r\n lines.push(createFramedLine(\"└─────┘\", scoreColorizer(\"└─────┘\")));\r\n lines.push(createFramedLine(\"Angular Doctor\"));\r\n lines.push(createFramedLine(\"\"));\r\n\r\n const scoreLinePlainText = `${scoreResult.score} / ${PERFECT_SCORE} ${scoreResult.label}`;\r\n const scoreLineRenderedText = `${colorizeByScore(String(scoreResult.score), scoreResult.score)} / ${PERFECT_SCORE} ${colorizeByScore(scoreResult.label, scoreResult.score)}`;\r\n lines.push(createFramedLine(scoreLinePlainText, scoreLineRenderedText));\r\n lines.push(createFramedLine(\"\"));\r\n lines.push(\r\n createFramedLine(\r\n buildPlainScoreBar(scoreResult.score),\r\n buildScoreBar(scoreResult.score),\r\n ),\r\n );\r\n lines.push(createFramedLine(\"\"));\r\n } else {\r\n lines.push(createFramedLine(\"Angular Doctor\"));\r\n lines.push(createFramedLine(\"\"));\r\n lines.push(\r\n createFramedLine(\r\n \"Score unavailable\",\r\n highlighter.dim(\"Score unavailable\"),\r\n ),\r\n );\r\n lines.push(createFramedLine(\"\"));\r\n }\r\n\r\n return lines;\r\n};\r\n\r\nconst buildCountsSummaryLine = (\r\n diagnostics: Diagnostic[],\r\n totalSourceFileCount: number,\r\n elapsedMilliseconds: number,\r\n): FramedLine => {\r\n const errorCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"error\",\r\n ).length;\r\n const warningCount = diagnostics.filter(\r\n (diagnostic) => diagnostic.severity === \"warning\",\r\n ).length;\r\n const affectedFileCount = collectAffectedFiles(diagnostics).size;\r\n const elapsed = formatElapsedTime(elapsedMilliseconds);\r\n\r\n const plainParts: string[] = [];\r\n const renderedParts: string[] = [];\r\n\r\n if (errorCount > 0) {\r\n const errorText = `✗ ${errorCount} error${errorCount === 1 ? \"\" : \"s\"}`;\r\n plainParts.push(errorText);\r\n renderedParts.push(highlighter.error(errorText));\r\n }\r\n if (warningCount > 0) {\r\n const warningText = `⚠ ${warningCount} warning${warningCount === 1 ? \"\" : \"s\"}`;\r\n plainParts.push(warningText);\r\n renderedParts.push(highlighter.warn(warningText));\r\n }\r\n\r\n const fileCountText =\r\n totalSourceFileCount > 0\r\n ? `across ${affectedFileCount}/${totalSourceFileCount} files`\r\n : `across ${affectedFileCount} file${affectedFileCount === 1 ? \"\" : \"s\"}`;\r\n const elapsedTimeText = `in ${elapsed}`;\r\n\r\n plainParts.push(fileCountText, elapsedTimeText);\r\n renderedParts.push(\r\n highlighter.dim(fileCountText),\r\n highlighter.dim(elapsedTimeText),\r\n );\r\n\r\n return createFramedLine(plainParts.join(\" \"), renderedParts.join(\" \"));\r\n};\r\n\r\nconst printSummary = (\r\n diagnostics: Diagnostic[],\r\n elapsedMilliseconds: number,\r\n scoreResult: ScoreResult | null,\r\n totalSourceFileCount: number,\r\n report: boolean | string | undefined,\r\n baseDirectory: string,\r\n): void => {\r\n const summaryFramedLines = [\r\n ...buildBrandingLines(scoreResult),\r\n buildCountsSummaryLine(\r\n diagnostics,\r\n totalSourceFileCount,\r\n elapsedMilliseconds,\r\n ),\r\n ];\r\n printFramedBox(summaryFramedLines);\r\n\r\n try {\r\n const { outputDirectory, markdownPath } = writeDiagnosticsDirectory(\r\n diagnostics,\r\n elapsedMilliseconds,\r\n scoreResult,\r\n totalSourceFileCount,\r\n report,\r\n baseDirectory,\r\n );\r\n logger.break();\r\n logger.dim(` Full diagnostics written to ${outputDirectory}`);\r\n if (markdownPath) {\r\n logger.dim(` Markdown report written to ${markdownPath}`);\r\n }\r\n } catch {\r\n logger.break();\r\n }\r\n};\r\n\r\ninterface ResolvedScanOptions {\r\n lint: boolean;\r\n deadCode: boolean;\r\n verbose: boolean;\r\n scoreOnly: boolean;\r\n report: boolean | string | undefined;\r\n useTypeAwareLint: boolean;\r\n includePaths: string[];\r\n}\r\n\r\nconst mergeScanOptions = (\r\n inputOptions: ScanOptions,\r\n userConfig: AngularDoctorConfig | null,\r\n): ResolvedScanOptions => {\r\n const fastMode = inputOptions.fast ?? userConfig?.fast ?? false;\r\n\r\n return {\r\n lint: inputOptions.lint ?? userConfig?.lint ?? true,\r\n deadCode: fastMode\r\n ? false\r\n : (inputOptions.deadCode ?? userConfig?.deadCode ?? true),\r\n verbose: inputOptions.verbose ?? userConfig?.verbose ?? false,\r\n scoreOnly: inputOptions.scoreOnly ?? false,\r\n report: inputOptions.report ?? false,\r\n useTypeAwareLint: !fastMode,\r\n includePaths: inputOptions.includePaths ?? [],\r\n };\r\n};\r\n\r\nconst printProjectDetection = (\r\n projectInfo: ProjectInfo,\r\n userConfig: AngularDoctorConfig | null,\r\n isDiffMode: boolean,\r\n includePaths: string[],\r\n): void => {\r\n const frameworkLabel = formatFrameworkName(projectInfo.framework);\r\n const languageLabel = \"TypeScript\";\r\n\r\n const completeStep = (message: string) => {\r\n spinner(message).start().succeed(message);\r\n };\r\n\r\n completeStep(\r\n `Detecting framework. Found ${highlighter.info(frameworkLabel)}.`,\r\n );\r\n completeStep(\r\n `Detecting Angular version. Found ${highlighter.info(`Angular ${projectInfo.angularVersion}`)}.`,\r\n );\r\n completeStep(`Detecting language. Found ${highlighter.info(languageLabel)}.`);\r\n completeStep(\r\n `Detecting standalone components. ${projectInfo.hasStandaloneComponents ? highlighter.info(\"Supported.\") : \"Not available (Angular 14+ required).\"}`,\r\n );\r\n\r\n if (isDiffMode) {\r\n completeStep(\r\n `Scanning ${highlighter.info(`${includePaths.length}`)} changed source files.`,\r\n );\r\n } else {\r\n completeStep(\r\n `Found ${highlighter.info(`${projectInfo.sourceFileCount}`)} source files.`,\r\n );\r\n }\r\n\r\n if (userConfig) {\r\n completeStep(`Loaded ${highlighter.info(\"angular-doctor config\")}.`);\r\n }\r\n\r\n logger.break();\r\n};\r\n\r\nexport const scan = async (\r\n directory: string,\r\n inputOptions: ScanOptions = {},\r\n): Promise<ScanResult> => {\r\n const startTime = performance.now();\r\n const projectInfo = discoverProject(directory);\r\n const userConfig = loadConfig(directory);\r\n const options = mergeScanOptions(inputOptions, userConfig);\r\n const { includePaths } = options;\r\n const isDiffMode = includePaths.length > 0;\r\n\r\n if (!projectInfo.angularVersion) {\r\n throw new Error(\"No Angular dependency found in package.json\");\r\n }\r\n\r\n if (!options.scoreOnly) {\r\n printProjectDetection(projectInfo, userConfig, isDiffMode, includePaths);\r\n }\r\n\r\n const computedIncludePaths = computeIncludePaths(includePaths);\r\n\r\n let didLintFail = false;\r\n let didDeadCodeFail = false;\r\n\r\n const runLint = async (): Promise<Diagnostic[]> => {\r\n if (!options.lint) return [];\r\n const lintSpinner = options.scoreOnly\r\n ? null\r\n : spinner(\"Running lint checks...\").start();\r\n try {\r\n const lintDiagnostics = await runEslint(\r\n directory,\r\n projectInfo.hasTypeScript,\r\n computedIncludePaths,\r\n { useTypeAware: options.useTypeAwareLint },\r\n );\r\n lintSpinner?.succeed(\"Running lint checks.\");\r\n return lintDiagnostics;\r\n } catch (error) {\r\n didLintFail = true;\r\n lintSpinner?.fail(\"Lint checks failed (non-fatal, skipping).\");\r\n logger.error(String(error));\r\n return [];\r\n }\r\n };\r\n\r\n const runDeadCode = async (): Promise<Diagnostic[]> => {\r\n if (!options.deadCode || isDiffMode) return [];\r\n const deadCodeSpinner = options.scoreOnly\r\n ? null\r\n : spinner(\"Detecting dead code...\").start();\r\n try {\r\n const knipDiagnostics = await runKnip(directory);\r\n deadCodeSpinner?.succeed(\"Detecting dead code.\");\r\n return knipDiagnostics;\r\n } catch (error) {\r\n didDeadCodeFail = true;\r\n deadCodeSpinner?.fail(\r\n \"Dead code detection failed (non-fatal, skipping).\",\r\n );\r\n logger.error(String(error));\r\n return [];\r\n }\r\n };\r\n\r\n const [lintDiagnostics, deadCodeDiagnostics] = options.scoreOnly\r\n ? await Promise.all([runLint(), runDeadCode()])\r\n : [await runLint(), await runDeadCode()];\r\n const diagnostics = combineDiagnostics(\r\n lintDiagnostics,\r\n deadCodeDiagnostics,\r\n userConfig,\r\n );\r\n\r\n const elapsedMilliseconds = performance.now() - startTime;\r\n\r\n const skippedChecks: string[] = [];\r\n if (didLintFail) skippedChecks.push(\"lint\");\r\n if (didDeadCodeFail) skippedChecks.push(\"dead code\");\r\n const hasSkippedChecks = skippedChecks.length > 0;\r\n\r\n const scoreResult = calculateScore(diagnostics);\r\n\r\n if (options.scoreOnly) {\r\n logger.log(`${scoreResult.score}`);\r\n return { diagnostics, scoreResult, skippedChecks };\r\n }\r\n\r\n if (diagnostics.length === 0) {\r\n if (hasSkippedChecks) {\r\n const skippedLabel = skippedChecks.join(\" and \");\r\n logger.warn(\r\n `No issues detected, but ${skippedLabel} checks failed — results are incomplete.`,\r\n );\r\n } else {\r\n logger.success(\"No issues found!\");\r\n }\r\n logger.break();\r\n if (hasSkippedChecks) {\r\n printBranding();\r\n logger.dim(\" Score not shown — some checks could not complete.\");\r\n } else {\r\n printBranding(scoreResult.score);\r\n printScoreGauge(scoreResult.score, scoreResult.label);\r\n }\r\n return { diagnostics, scoreResult, skippedChecks };\r\n }\r\n\r\n printDiagnostics(diagnostics, options.verbose);\r\n\r\n const displayedSourceFileCount = isDiffMode\r\n ? includePaths.length\r\n : projectInfo.sourceFileCount;\r\n\r\n printSummary(\r\n diagnostics,\r\n elapsedMilliseconds,\r\n scoreResult,\r\n displayedSourceFileCount,\r\n options.report,\r\n directory,\r\n );\r\n\r\n if (hasSkippedChecks) {\r\n const skippedLabel = skippedChecks.join(\" and \");\r\n logger.break();\r\n logger.warn(\r\n ` Note: ${skippedLabel} checks failed — score may be incomplete.`,\r\n );\r\n }\r\n\r\n return { diagnostics, scoreResult, skippedChecks };\r\n};\r\n","import { spawnSync } from \"node:child_process\";\r\nimport { SOURCE_FILE_PATTERN, DEFAULT_BRANCH_CANDIDATES } from \"../constants.js\";\r\nimport type { DiffInfo } from \"../types.js\";\r\n\r\nconst getCurrentBranch = (directory: string): string | null => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0 ? result.stdout.trim() : null;\r\n};\r\n\r\nconst getBranchChangedFiles = (directory: string, baseBranch: string): string[] => {\r\n const result = spawnSync(\"git\", [\"diff\", \"--name-only\", baseBranch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout.split(\"\\n\").filter(Boolean);\r\n};\r\n\r\nconst getUncommittedChangedFiles = (directory: string): string[] => {\r\n const result = spawnSync(\"git\", [\"status\", \"--porcelain\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter(Boolean)\r\n .map((line) => line.slice(3).trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst branchExists = (directory: string, branch: string): boolean => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--verify\", branch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0;\r\n};\r\n\r\nexport const filterSourceFiles = (files: string[]): string[] =>\r\n files.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));\r\n\r\nexport const getDiffInfo = (directory: string, explicitBaseBranch?: string): DiffInfo | null => {\r\n const currentBranch = getCurrentBranch(directory);\r\n if (!currentBranch) return null;\r\n\r\n if (explicitBaseBranch) {\r\n if (!branchExists(directory, explicitBaseBranch)) return null;\r\n const changedFiles = getBranchChangedFiles(directory, explicitBaseBranch);\r\n return { currentBranch, baseBranch: explicitBaseBranch, changedFiles };\r\n }\r\n\r\n const uncommittedFiles = getUncommittedChangedFiles(directory);\r\n if (uncommittedFiles.length > 0) {\r\n return {\r\n currentBranch,\r\n baseBranch: currentBranch,\r\n changedFiles: uncommittedFiles,\r\n isCurrentChanges: true,\r\n };\r\n }\r\n\r\n for (const candidate of DEFAULT_BRANCH_CANDIDATES) {\r\n if (branchExists(directory, candidate) && currentBranch !== candidate) {\r\n const changedFiles = getBranchChangedFiles(directory, candidate);\r\n if (changedFiles.length > 0) {\r\n return { currentBranch, baseBranch: candidate, changedFiles };\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import { logger } from \"./logger.js\";\r\n\r\nexport interface HandleErrorOptions {\r\n shouldExit: boolean;\r\n}\r\n\r\nexport const handleError = (\r\n error: unknown,\r\n options: HandleErrorOptions = { shouldExit: true },\r\n): void => {\r\n const message = error instanceof Error ? error.message : String(error);\r\n logger.error(message);\r\n if (options.shouldExit) {\r\n process.exit(1);\r\n }\r\n};\r\n","import path from \"node:path\";\r\nimport basePrompts from \"prompts\";\r\nimport type { WorkspacePackage } from \"../types.js\";\r\nimport { listAngularWorkspaceProjects, listWorkspacePackages } from \"./discover-project.js\";\r\nimport { highlighter } from \"./highlighter.js\";\r\nimport { logger } from \"./logger.js\";\r\n\r\nconst onCancel = () => {\r\n logger.break();\r\n logger.log(\"Cancelled.\");\r\n logger.break();\r\n process.exit(0);\r\n};\r\n\r\nconst promptProjectSelection = async (\r\n workspacePackages: WorkspacePackage[],\r\n rootDirectory: string,\r\n): Promise<string[]> => {\r\n const response = await basePrompts(\r\n {\r\n type: \"multiselect\",\r\n name: \"selectedDirectories\",\r\n message: \"Select projects to scan\",\r\n choices: workspacePackages.map((workspacePackage) => ({\r\n title: workspacePackage.name,\r\n description: path.relative(rootDirectory, workspacePackage.directory) || \".\",\r\n value: workspacePackage.directory,\r\n selected: true,\r\n })),\r\n min: 1,\r\n },\r\n { onCancel },\r\n );\r\n return (response.selectedDirectories as string[]) ?? [];\r\n};\r\n\r\nconst resolveProjectFlag = (\r\n projectFlag: string,\r\n workspacePackages: WorkspacePackage[],\r\n): string[] => {\r\n const requestedNames = projectFlag.split(\",\").map((name) => name.trim());\r\n const resolvedDirectories: string[] = [];\r\n\r\n for (const requestedName of requestedNames) {\r\n const matched = workspacePackages.find(\r\n (workspacePackage) =>\r\n workspacePackage.name === requestedName ||\r\n path.basename(workspacePackage.directory) === requestedName,\r\n );\r\n\r\n if (!matched) {\r\n const availableNames = workspacePackages\r\n .map((workspacePackage) => workspacePackage.name)\r\n .join(\", \");\r\n throw new Error(`Project \"${requestedName}\" not found. Available: ${availableNames}`);\r\n }\r\n\r\n resolvedDirectories.push(matched.directory);\r\n }\r\n\r\n return resolvedDirectories;\r\n};\r\n\r\nconst printDiscoveredProjects = (packages: WorkspacePackage[]): void => {\r\n logger.log(\r\n `${highlighter.success(\"✔\")} Select projects to scan ${highlighter.dim(\"›\")} ${packages.map((p) => p.name).join(\", \")}`,\r\n );\r\n};\r\n\r\n/**\r\n * Resolves the list of project directories to scan.\r\n *\r\n * Priority order:\r\n * 1. Angular CLI workspace (`angular.json`) — covers single-repo multi-project Angular workspaces\r\n * 2. npm/pnpm workspaces (package.json `workspaces` field or pnpm-workspace.yaml)\r\n * 3. Fall back to `rootDirectory` itself\r\n */\r\nexport const selectProjects = async (\r\n rootDirectory: string,\r\n projectFlag: string | undefined,\r\n skipPrompts: boolean,\r\n): Promise<string[]> => {\r\n // Prefer angular.json workspace projects (Angular CLI / Nx workspaces)\r\n let packages = listAngularWorkspaceProjects(rootDirectory);\r\n\r\n // Fall back to npm/pnpm workspace packages\r\n if (packages.length === 0) {\r\n packages = listWorkspacePackages(rootDirectory);\r\n }\r\n\r\n // No workspace found — scan the directory itself\r\n if (packages.length === 0) return [rootDirectory];\r\n\r\n // Single project — no need to prompt\r\n if (packages.length === 1) {\r\n logger.log(\r\n `${highlighter.success(\"✔\")} Select projects to scan ${highlighter.dim(\"›\")} ${packages[0].name}`,\r\n );\r\n logger.break();\r\n return [packages[0].directory];\r\n }\r\n\r\n // Explicit --project flag\r\n if (projectFlag) return resolveProjectFlag(projectFlag, packages);\r\n\r\n // Non-interactive mode: scan all\r\n if (skipPrompts) {\r\n printDiscoveredProjects(packages);\r\n logger.break();\r\n return packages.map((workspacePackage) => workspacePackage.directory);\r\n }\r\n\r\n // Interactive multi-select prompt\r\n return promptProjectSelection(packages, rootDirectory);\r\n};\r\n","import path from \"node:path\";\r\nimport { Command } from \"commander\";\r\nimport { scan } from \"./scan.js\";\r\nimport type { AngularDoctorConfig, DiffInfo, ScanOptions } from \"./types.js\";\r\nimport { loadConfig } from \"./utils/load-config.js\";\r\nimport { filterSourceFiles, getDiffInfo } from \"./utils/get-diff-files.js\";\r\nimport { handleError } from \"./utils/handle-error.js\";\r\nimport { highlighter } from \"./utils/highlighter.js\";\r\nimport { logger } from \"./utils/logger.js\";\r\nimport { selectProjects } from \"./utils/select-projects.js\";\r\n\r\nconst VERSION = process.env.VERSION ?? \"0.0.0\";\r\n\r\ninterface CliFlags {\r\n lint: boolean;\r\n deadCode: boolean;\r\n verbose: boolean;\r\n score: boolean;\r\n yes: boolean;\r\n report?: boolean | string;\r\n fast?: boolean;\r\n project?: string;\r\n diff?: boolean | string;\r\n}\r\n\r\nconst exitWithHint = () => {\r\n logger.break();\r\n logger.log(\"Cancelled.\");\r\n logger.break();\r\n process.exit(0);\r\n};\r\n\r\nprocess.on(\"SIGINT\", exitWithHint);\r\nprocess.on(\"SIGTERM\", exitWithHint);\r\n\r\nconst AUTOMATED_ENVIRONMENT_VARIABLES = [\r\n \"CI\",\r\n \"CLAUDECODE\",\r\n \"CURSOR_AGENT\",\r\n \"CODEX_CI\",\r\n \"OPENCODE\",\r\n \"AMP_HOME\",\r\n];\r\n\r\nconst isAutomatedEnvironment = (): boolean =>\r\n AUTOMATED_ENVIRONMENT_VARIABLES.some((envVariable) =>\r\n Boolean(process.env[envVariable]),\r\n );\r\n\r\nconst resolveCliScanOptions = (\r\n flags: CliFlags,\r\n userConfig: AngularDoctorConfig | null,\r\n programInstance: Command,\r\n): ScanOptions => {\r\n const isCliOverride = (optionName: string) =>\r\n programInstance.getOptionValueSource(optionName) === \"cli\";\r\n\r\n return {\r\n lint: isCliOverride(\"lint\") ? flags.lint : (userConfig?.lint ?? flags.lint),\r\n deadCode: isCliOverride(\"deadCode\")\r\n ? flags.deadCode\r\n : (userConfig?.deadCode ?? flags.deadCode),\r\n verbose: isCliOverride(\"verbose\")\r\n ? Boolean(flags.verbose)\r\n : (userConfig?.verbose ?? false),\r\n scoreOnly: flags.score,\r\n report: flags.report,\r\n fast: isCliOverride(\"fast\")\r\n ? Boolean(flags.fast)\r\n : (userConfig?.fast ?? false),\r\n };\r\n};\r\n\r\nconst resolveDiffMode = async (\r\n diffInfo: DiffInfo | null,\r\n effectiveDiff: boolean | string | undefined,\r\n shouldSkipPrompts: boolean,\r\n isScoreOnly: boolean,\r\n): Promise<boolean> => {\r\n if (effectiveDiff !== undefined && effectiveDiff !== false) {\r\n if (diffInfo) return true;\r\n if (!isScoreOnly) {\r\n logger.warn(\r\n \"No feature branch or uncommitted changes detected. Running full scan.\",\r\n );\r\n logger.break();\r\n }\r\n return false;\r\n }\r\n\r\n if (effectiveDiff === false || !diffInfo) return false;\r\n\r\n const changedSourceFiles = filterSourceFiles(diffInfo.changedFiles);\r\n if (changedSourceFiles.length === 0) return false;\r\n if (shouldSkipPrompts) return true;\r\n if (isScoreOnly) return false;\r\n\r\n // In non-interactive mode, skip diff prompts\r\n return false;\r\n};\r\n\r\nconst program = new Command()\r\n .name(\"angular-doctor\")\r\n .description(\"Diagnose Angular codebase health\")\r\n .version(VERSION, \"-v, --version\", \"display the version number\")\r\n .argument(\"[directory]\", \"project directory to scan\", \".\")\r\n .option(\"--no-lint\", \"skip linting\")\r\n .option(\"--no-dead-code\", \"skip dead code detection\")\r\n .option(\"--verbose\", \"show file details per rule\")\r\n .option(\"--score\", \"output only the score\")\r\n .option(\"--report [path]\", \"write a markdown report (optional output path)\")\r\n .option(\"--fast\", \"speed up by skipping dead code and type-aware lint\")\r\n .option(\"-y, --yes\", \"skip prompts, scan all workspace projects\")\r\n .option(\r\n \"--project <name>\",\r\n \"select workspace project (comma-separated for multiple)\",\r\n )\r\n .option(\"--diff [base]\", \"scan only files changed vs base branch\")\r\n .action(async (directory: string, flags: CliFlags) => {\r\n const isScoreOnly = flags.score;\r\n\r\n try {\r\n const resolvedDirectory = path.resolve(directory);\r\n const userConfig = loadConfig(resolvedDirectory);\r\n\r\n if (!isScoreOnly) {\r\n logger.log(`angular-doctor v${VERSION}`);\r\n logger.break();\r\n }\r\n\r\n const scanOptions = resolveCliScanOptions(flags, userConfig, program);\r\n const shouldSkipPrompts =\r\n flags.yes || isAutomatedEnvironment() || !process.stdin.isTTY;\r\n\r\n // Discover and (optionally) prompt to select workspace projects\r\n const projectDirectories = await selectProjects(\r\n resolvedDirectory,\r\n flags.project,\r\n shouldSkipPrompts,\r\n );\r\n\r\n const isDiffCliOverride = program.getOptionValueSource(\"diff\") === \"cli\";\r\n const effectiveDiff = isDiffCliOverride ? flags.diff : userConfig?.diff;\r\n const explicitBaseBranch =\r\n typeof effectiveDiff === \"string\" ? effectiveDiff : undefined;\r\n const diffInfo = getDiffInfo(resolvedDirectory, explicitBaseBranch);\r\n const isDiffMode = await resolveDiffMode(\r\n diffInfo,\r\n effectiveDiff,\r\n shouldSkipPrompts,\r\n isScoreOnly,\r\n );\r\n\r\n if (isDiffMode && diffInfo && !isScoreOnly) {\r\n if (diffInfo.isCurrentChanges) {\r\n logger.log(\"Scanning uncommitted changes\");\r\n } else {\r\n logger.log(\r\n `Scanning changes: ${highlighter.info(diffInfo.currentBranch)} → ${highlighter.info(diffInfo.baseBranch)}`,\r\n );\r\n }\r\n logger.break();\r\n }\r\n\r\n for (const projectDirectory of projectDirectories) {\r\n let includePaths: string[] | undefined;\r\n\r\n if (isDiffMode) {\r\n const projectDiffInfo = getDiffInfo(\r\n projectDirectory,\r\n explicitBaseBranch,\r\n );\r\n if (projectDiffInfo) {\r\n const changedSourceFiles = filterSourceFiles(\r\n projectDiffInfo.changedFiles,\r\n );\r\n if (changedSourceFiles.length === 0) {\r\n if (!isScoreOnly) {\r\n logger.dim(\r\n `No changed source files in ${projectDirectory}, skipping.`,\r\n );\r\n logger.break();\r\n }\r\n continue;\r\n }\r\n includePaths = changedSourceFiles;\r\n }\r\n }\r\n\r\n if (!isScoreOnly) {\r\n logger.dim(`Scanning ${projectDirectory}...`);\r\n logger.break();\r\n }\r\n\r\n await scan(projectDirectory, { ...scanOptions, includePaths });\r\n\r\n if (!isScoreOnly) {\r\n logger.break();\r\n }\r\n }\r\n } catch (error) {\r\n handleError(error);\r\n }\r\n })\r\n .addHelpText(\r\n \"after\",\r\n `\r\n${highlighter.dim(\"Learn more:\")}\r\n ${highlighter.info(\"https://github.com/antonygiomarxdev/angular-doctor\")}\r\n`,\r\n );\r\n\r\nconst main = async () => {\r\n await program.parseAsync();\r\n};\r\n\r\nmain();\r\n"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,gBAAA;AAAA,UAQK,WAAA;EACf,aAAA;EACA,WAAA;EACA,cAAA;EACA,SAAA,EAAW,gBAAA;EACX,aAAA;EACA,uBAAA;EACA,eAAA;AAAA;AAAA,UAGe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UAWe,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"mappings":";KAAY,gBAAA;AAAA,UAQK,WAAA;EACf,aAAA;EACA,WAAA;EACA,cAAA;EACA,SAAA,EAAW,gBAAA;EACX,aAAA;EACA,uBAAA;EACA,eAAA;AAAA;AAAA,UAGe,UAAA;EACf,QAAA;EACA,MAAA;EACA,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;AAAA,UAWe,WAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UA4Ce,QAAA;EACf,aAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;AAAA;AAAA,UAGe,yBAAA;EACf,KAAA;EACA,KAAA;AAAA;AAAA,UAGe,mBAAA;EACf,MAAA,GAAS,yBAAA;EACT,IAAA;EACA,QAAA;EACA,OAAA;EACA,IAAA;EACA,IAAA;AAAA;;;cC7DW,iBAAA,GAAqB,KAAA;AAAA,cAGrB,WAAA,GAAe,SAAA,UAAmB,kBAAA,cAA8B,QAAA;;;UChC5D,eAAA;EACf,IAAA;EACA,QAAA;EACA,YAAA;AAAA;AAAA,UAGe,cAAA;EACf,WAAA,EAAa,UAAA;EACb,KAAA,EAAO,WAAA;EACP,OAAA,EAAS,WAAA;EACT,mBAAA;AAAA;AAAA,cAGW,QAAA,GACX,SAAA,UACA,OAAA,GAAS,eAAA,KACR,OAAA,CAAQ,cAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -272,13 +272,13 @@ const RULE_HELP_MAP = {
|
|
|
272
272
|
"@typescript-eslint/no-explicit-any": "Replace `any` with a specific type or `unknown` if the type is truly unknown",
|
|
273
273
|
"@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused"
|
|
274
274
|
};
|
|
275
|
-
const buildEslintConfig = (hasTypeScript, tsconfigPath) => {
|
|
275
|
+
const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
276
276
|
const languageOptions = {
|
|
277
277
|
parser: tsEslint.parser,
|
|
278
278
|
parserOptions: {
|
|
279
279
|
ecmaVersion: "latest",
|
|
280
280
|
sourceType: "module",
|
|
281
|
-
...hasTypeScript && tsconfigPath ? { project: tsconfigPath } : {}
|
|
281
|
+
...hasTypeScript && tsconfigPath && useTypeAware ? { project: tsconfigPath } : {}
|
|
282
282
|
}
|
|
283
283
|
};
|
|
284
284
|
const angularRules = {
|
|
@@ -329,14 +329,18 @@ const parsePluginAndRule = (ruleId) => {
|
|
|
329
329
|
rule: ruleId
|
|
330
330
|
};
|
|
331
331
|
};
|
|
332
|
-
const runEslint = async (rootDirectory, hasTypeScript, includePaths) => {
|
|
332
|
+
const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) => {
|
|
333
333
|
if (includePaths !== void 0 && includePaths.length === 0) return [];
|
|
334
334
|
const tsconfigPath = hasTypeScript ? path.join(rootDirectory, "tsconfig.json") : null;
|
|
335
|
+
const cacheRoot = path.join(rootDirectory, "node_modules", ".cache", "angular-doctor");
|
|
336
|
+
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
335
337
|
const eslint = new ESLint({
|
|
336
338
|
cwd: rootDirectory,
|
|
337
|
-
overrideConfigFile:
|
|
338
|
-
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null),
|
|
339
|
-
ignore: true
|
|
339
|
+
overrideConfigFile: null,
|
|
340
|
+
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true),
|
|
341
|
+
ignore: true,
|
|
342
|
+
cache: true,
|
|
343
|
+
cacheLocation: path.join(cacheRoot, ".eslintcache")
|
|
340
344
|
});
|
|
341
345
|
const patterns = includePaths ?? ["**/*.ts"];
|
|
342
346
|
let results;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/utils/calculate-score.ts","../src/utils/filter-diagnostics.ts","../src/utils/combine-diagnostics.ts","../src/utils/discover-project.ts","../src/utils/load-config.ts","../src/utils/run-eslint.ts","../src/utils/run-knip.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"sourcesContent":["export const SOURCE_FILE_PATTERN = /\\.ts$/;\r\n\r\nexport const MILLISECONDS_PER_SECOND = 1000;\r\n\r\nexport const PERFECT_SCORE = 100;\r\n\r\nexport const SCORE_GOOD_THRESHOLD = 75;\r\n\r\nexport const SCORE_OK_THRESHOLD = 50;\r\n\r\nexport const SCORE_BAR_WIDTH_CHARS = 50;\r\n\r\nexport const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;\r\n\r\nexport const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;\r\n\r\nexport const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\r\n\r\nexport const MAX_KNIP_RETRIES = 5;\r\n\r\nexport const ERROR_RULE_PENALTY = 1.5;\r\n\r\nexport const WARNING_RULE_PENALTY = 0.75;\r\n\r\nexport const DEFAULT_BRANCH_CANDIDATES = [\"main\", \"master\"];\r\n","import {\r\n ERROR_RULE_PENALTY,\r\n PERFECT_SCORE,\r\n SCORE_GOOD_THRESHOLD,\r\n SCORE_OK_THRESHOLD,\r\n WARNING_RULE_PENALTY,\r\n} from \"../constants.js\";\r\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\r\n\r\nexport const getScoreLabel = (score: number): string => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return \"Great\";\r\n if (score >= SCORE_OK_THRESHOLD) return \"Needs work\";\r\n return \"Critical\";\r\n};\r\n\r\nconst countUniqueRules = (\r\n diagnostics: Diagnostic[],\r\n): { errorRuleCount: number; warningRuleCount: number } => {\r\n const errorRules = new Set<string>();\r\n const warningRules = new Set<string>();\r\n\r\n for (const diagnostic of diagnostics) {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (diagnostic.severity === \"error\") {\r\n errorRules.add(ruleKey);\r\n } else {\r\n warningRules.add(ruleKey);\r\n }\r\n }\r\n\r\n return { errorRuleCount: errorRules.size, warningRuleCount: warningRules.size };\r\n};\r\n\r\nexport const calculateScore = (diagnostics: Diagnostic[]): ScoreResult => {\r\n const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);\r\n const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;\r\n const score = Math.max(0, Math.round(PERFECT_SCORE - penalty));\r\n return { score, label: getScoreLabel(score) };\r\n};\r\n","import type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\n\r\nconst matchesGlob = (filePath: string, pattern: string): boolean => {\r\n const escapedPattern = pattern\r\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\r\n .replace(/\\*\\*/g, \".*\")\r\n .replace(/\\*/g, \"[^/]*\");\r\n return new RegExp(`^${escapedPattern}$`).test(filePath);\r\n};\r\n\r\nexport const filterIgnoredDiagnostics = (\r\n diagnostics: Diagnostic[],\r\n config: AngularDoctorConfig,\r\n): Diagnostic[] => {\r\n const ignoredRules = new Set(config.ignore?.rules ?? []);\r\n const ignoredFilePatterns = config.ignore?.files ?? [];\r\n\r\n return diagnostics.filter((diagnostic) => {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (ignoredRules.has(ruleKey)) return false;\r\n\r\n if (ignoredFilePatterns.some((pattern) => matchesGlob(diagnostic.filePath, pattern))) {\r\n return false;\r\n }\r\n\r\n return true;\r\n });\r\n};\r\n","import { SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\nimport { filterIgnoredDiagnostics } from \"./filter-diagnostics.js\";\r\n\r\nexport const computeIncludePaths = (includePaths: string[]): string[] | undefined =>\r\n includePaths.length > 0\r\n ? includePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath))\r\n : undefined;\r\n\r\nexport const combineDiagnostics = (\r\n lintDiagnostics: Diagnostic[],\r\n deadCodeDiagnostics: Diagnostic[],\r\n userConfig: AngularDoctorConfig | null,\r\n): Diagnostic[] => {\r\n const allDiagnostics = [...lintDiagnostics, ...deadCodeDiagnostics];\r\n return userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { spawnSync } from \"node:child_process\";\r\nimport { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularFramework, PackageJson, ProjectInfo, WorkspacePackage } from \"../types.js\";\r\n\r\nconst ANGULAR_FRAMEWORK_PACKAGES: Record<string, AngularFramework> = {\r\n \"@nrwl/angular\": \"nx\",\r\n \"@nx/angular\": \"nx\",\r\n \"@analogjs/platform\": \"analog\",\r\n \"@ionic/angular\": \"ionic\",\r\n \"@angular/ssr\": \"universal\",\r\n \"@nguniversal/express-engine\": \"universal\",\r\n};\r\n\r\nconst ANGULAR_FRAMEWORK_DISPLAY_NAMES: Record<AngularFramework, string> = {\r\n \"angular-cli\": \"Angular CLI\",\r\n nx: \"Nx\",\r\n analog: \"AnalogJS\",\r\n ionic: \"Ionic\",\r\n universal: \"Angular SSR\",\r\n unknown: \"Angular\",\r\n};\r\n\r\nexport const formatFrameworkName = (framework: AngularFramework): string =>\r\n ANGULAR_FRAMEWORK_DISPLAY_NAMES[framework];\r\n\r\nconst readPackageJson = (filePath: string): PackageJson => {\r\n try {\r\n const content = fs.readFileSync(filePath, \"utf-8\");\r\n return JSON.parse(content) as PackageJson;\r\n } catch {\r\n return {};\r\n }\r\n};\r\n\r\nconst collectAllDependencies = (packageJson: PackageJson): Record<string, string> => ({\r\n ...packageJson.peerDependencies,\r\n ...packageJson.dependencies,\r\n ...packageJson.devDependencies,\r\n});\r\n\r\nconst detectFramework = (dependencies: Record<string, string>): AngularFramework => {\r\n for (const [packageName, frameworkName] of Object.entries(ANGULAR_FRAMEWORK_PACKAGES)) {\r\n if (dependencies[packageName]) {\r\n return frameworkName;\r\n }\r\n }\r\n if (dependencies[\"@angular/cli\"] || dependencies[\"@angular-devkit/build-angular\"] || dependencies[\"@angular-devkit/core\"]) {\r\n return \"angular-cli\";\r\n }\r\n return \"unknown\";\r\n};\r\n\r\nconst detectAngularVersion = (dependencies: Record<string, string>): string | null =>\r\n dependencies[\"@angular/core\"] ?? null;\r\n\r\nconst detectStandaloneComponents = (packageJson: PackageJson): boolean => {\r\n const deps = collectAllDependencies(packageJson);\r\n const angularVersion = deps[\"@angular/core\"];\r\n if (!angularVersion) return false;\r\n // Angular 14+ supports standalone components (standalone: true flag)\r\n // Angular 17+ makes standalone the default\r\n const majorVersion = parseInt(angularVersion.match(/\\d+/)?.[0] ?? \"\", 10);\r\n return !isNaN(majorVersion) && majorVersion >= 14;\r\n};\r\n\r\nconst countSourceFiles = (rootDirectory: string): number => {\r\n const result = spawnSync(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\r\n cwd: rootDirectory,\r\n encoding: \"utf-8\",\r\n maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,\r\n });\r\n\r\n if (result.error || result.status !== 0) {\r\n return 0;\r\n }\r\n\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;\r\n};\r\n\r\nconst hasAngularDependency = (packageJson: PackageJson): boolean => {\r\n const allDependencies = collectAllDependencies(packageJson);\r\n return Boolean(allDependencies[\"@angular/core\"]);\r\n};\r\n\r\n/**\r\n * Walks up the directory tree from `directory` until it finds a `package.json`.\r\n * Returns the directory containing the package.json, or null if not found.\r\n */\r\nconst findNearestPackageJsonDir = (directory: string): string | null => {\r\n let current = directory;\r\n while (true) {\r\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\r\n const parent = path.dirname(current);\r\n if (parent === current) return null;\r\n current = parent;\r\n }\r\n};\r\n\r\nexport const discoverProject = (directory: string): ProjectInfo => {\r\n // First try the given directory, then walk up to find the nearest package.json\r\n const packageJsonDir = fs.existsSync(path.join(directory, \"package.json\"))\r\n ? directory\r\n : findNearestPackageJsonDir(directory);\r\n\r\n if (!packageJsonDir) {\r\n throw new Error(`No package.json found in ${directory} or any parent directory`);\r\n }\r\n\r\n const packageJson = readPackageJson(path.join(packageJsonDir, \"package.json\"));\r\n const allDeps = collectAllDependencies(packageJson);\r\n const angularVersion = detectAngularVersion(allDeps);\r\n const framework = detectFramework(allDeps);\r\n\r\n // tsconfig.json — check the project directory first, then the package.json directory\r\n const hasTypeScript =\r\n fs.existsSync(path.join(directory, \"tsconfig.json\")) ||\r\n fs.existsSync(path.join(packageJsonDir, \"tsconfig.json\"));\r\n\r\n const hasStandaloneComponents = detectStandaloneComponents(packageJson);\r\n const sourceFileCount = countSourceFiles(directory);\r\n\r\n // Use the Angular project name from angular.json if possible, otherwise from package.json\r\n const angularJsonPath = path.join(packageJsonDir, \"angular.json\");\r\n let projectName = packageJson.name ?? path.basename(directory);\r\n if (packageJsonDir !== directory && fs.existsSync(angularJsonPath)) {\r\n // Use the directory name as a more meaningful project name for workspace sub-projects\r\n projectName = path.basename(directory);\r\n }\r\n\r\n return {\r\n rootDirectory: directory,\r\n projectName,\r\n angularVersion,\r\n framework,\r\n hasTypeScript,\r\n hasStandaloneComponents,\r\n sourceFileCount,\r\n };\r\n};\r\n\r\nconst parsePnpmWorkspacePatterns = (rootDirectory: string): string[] => {\r\n const workspacePath = path.join(rootDirectory, \"pnpm-workspace.yaml\");\r\n if (!fs.existsSync(workspacePath)) return [];\r\n\r\n const content = fs.readFileSync(workspacePath, \"utf-8\");\r\n const patterns: string[] = [];\r\n let isInsidePackagesBlock = false;\r\n\r\n for (const line of content.split(\"\\n\")) {\r\n const trimmed = line.trim();\r\n if (trimmed === \"packages:\") {\r\n isInsidePackagesBlock = true;\r\n continue;\r\n }\r\n if (isInsidePackagesBlock && trimmed.startsWith(\"-\")) {\r\n patterns.push(trimmed.replace(/^-\\s*/, \"\").replace(/[\"']/g, \"\"));\r\n } else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith(\"#\")) {\r\n isInsidePackagesBlock = false;\r\n }\r\n }\r\n\r\n return patterns;\r\n};\r\n\r\nconst getWorkspacePatterns = (rootDirectory: string, packageJson: PackageJson): string[] => {\r\n const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);\r\n if (pnpmPatterns.length > 0) return pnpmPatterns;\r\n\r\n if (Array.isArray(packageJson.workspaces)) {\r\n return packageJson.workspaces;\r\n }\r\n\r\n if (packageJson.workspaces?.packages) {\r\n return packageJson.workspaces.packages;\r\n }\r\n\r\n return [];\r\n};\r\n\r\nconst resolveWorkspaceDirectories = (rootDirectory: string, pattern: string): string[] => {\r\n const cleanPattern = pattern.replace(/[\"']/g, \"\").replace(/\\/\\*\\*$/, \"/*\");\r\n\r\n if (!cleanPattern.includes(\"*\")) {\r\n const directoryPath = path.join(rootDirectory, cleanPattern);\r\n if (fs.existsSync(directoryPath) && fs.existsSync(path.join(directoryPath, \"package.json\"))) {\r\n return [directoryPath];\r\n }\r\n return [];\r\n }\r\n\r\n const wildcardIndex = cleanPattern.indexOf(\"*\");\r\n const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));\r\n const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);\r\n\r\n if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) {\r\n return [];\r\n }\r\n\r\n return fs\r\n .readdirSync(baseDirectory)\r\n .map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard))\r\n .filter(\r\n (entryPath) =>\r\n fs.existsSync(entryPath) &&\r\n fs.statSync(entryPath).isDirectory() &&\r\n fs.existsSync(path.join(entryPath, \"package.json\")),\r\n );\r\n};\r\n\r\nexport const listWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (!fs.existsSync(packageJsonPath)) return [];\r\n\r\n const packageJson = readPackageJson(packageJsonPath);\r\n const patterns = getWorkspacePatterns(rootDirectory, packageJson);\r\n if (patterns.length === 0) return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const pattern of patterns) {\r\n const directories = resolveWorkspaceDirectories(rootDirectory, pattern);\r\n for (const workspaceDirectory of directories) {\r\n const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, \"package.json\"));\r\n\r\n if (!hasAngularDependency(workspacePackageJson)) continue;\r\n\r\n const name = workspacePackageJson.name ?? path.basename(workspaceDirectory);\r\n packages.push({ name, directory: workspaceDirectory });\r\n }\r\n }\r\n\r\n return packages;\r\n};\r\n\r\ninterface AngularWorkspaceProject {\r\n projectType?: string;\r\n root?: string;\r\n}\r\n\r\ninterface AngularWorkspace {\r\n version?: number;\r\n projects?: Record<string, AngularWorkspaceProject | string>;\r\n}\r\n\r\n/**\r\n * Reads `angular.json` (Angular CLI workspace config) and returns one\r\n * WorkspacePackage per project whose root directory contains an Angular dependency.\r\n */\r\nexport const listAngularWorkspaceProjects = (rootDirectory: string): WorkspacePackage[] => {\r\n const angularJsonPath = path.join(rootDirectory, \"angular.json\");\r\n if (!fs.existsSync(angularJsonPath)) return [];\r\n\r\n let workspace: AngularWorkspace;\r\n try {\r\n workspace = JSON.parse(fs.readFileSync(angularJsonPath, \"utf-8\")) as AngularWorkspace;\r\n } catch {\r\n return [];\r\n }\r\n\r\n if (!workspace.projects || typeof workspace.projects !== \"object\") return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const [name, projectConfig] of Object.entries(workspace.projects)) {\r\n // Older angular.json formats store the root as a plain string\r\n const root =\r\n typeof projectConfig === \"string\"\r\n ? projectConfig\r\n : (projectConfig.root ?? \"\");\r\n\r\n const projectDirectory = root ? path.resolve(rootDirectory, root) : rootDirectory;\r\n\r\n // Only include directories that actually exist\r\n if (!fs.existsSync(projectDirectory) || !fs.statSync(projectDirectory).isDirectory()) {\r\n continue;\r\n }\r\n\r\n packages.push({ name, directory: projectDirectory });\r\n }\r\n\r\n return packages;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { AngularDoctorConfig } from \"../types.js\";\r\n\r\nconst CONFIG_FILENAME = \"angular-doctor.config.json\";\r\nconst PACKAGE_JSON_CONFIG_KEY = \"angularDoctor\";\r\n\r\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\r\n typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n\r\nexport const loadConfig = (rootDirectory: string): AngularDoctorConfig | null => {\r\n const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);\r\n\r\n if (fs.existsSync(configFilePath)) {\r\n try {\r\n const fileContent = fs.readFileSync(configFilePath, \"utf-8\");\r\n const parsed: unknown = JSON.parse(fileContent);\r\n if (!isPlainObject(parsed)) {\r\n console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);\r\n return null;\r\n }\r\n return parsed as AngularDoctorConfig;\r\n } catch (error) {\r\n console.warn(\r\n `Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n return null;\r\n }\r\n }\r\n\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (fs.existsSync(packageJsonPath)) {\r\n try {\r\n const fileContent = fs.readFileSync(packageJsonPath, \"utf-8\");\r\n const packageJson = JSON.parse(fileContent) as Record<string, unknown>;\r\n const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];\r\n if (isPlainObject(embeddedConfig)) {\r\n return embeddedConfig as AngularDoctorConfig;\r\n }\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { ESLint, type Linter } from \"eslint\";\r\nimport angularEslintPlugin from \"@angular-eslint/eslint-plugin\";\r\nimport tsEslint from \"typescript-eslint\";\r\nimport type { Diagnostic } from \"../types.js\";\r\n\r\n// Rule category mapping\r\nconst RULE_CATEGORY_MAP: Record<string, string> = {\r\n // Angular component best practices\r\n \"@angular-eslint/component-class-suffix\": \"Components\",\r\n \"@angular-eslint/directive-class-suffix\": \"Components\",\r\n \"@angular-eslint/pipe-prefix\": \"Components\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"Components\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Components\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"Components\",\r\n \"@angular-eslint/consistent-component-styles\": \"Components\",\r\n\r\n // Angular performance\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Performance\",\r\n \"@angular-eslint/no-output-native\": \"Performance\",\r\n\r\n // Angular architecture / correctness\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/contextual-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/no-forward-ref\": \"Architecture\",\r\n \"@angular-eslint/no-input-rename\": \"Architecture\",\r\n \"@angular-eslint/no-output-rename\": \"Architecture\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/prefer-standalone\": \"Architecture\",\r\n\r\n // TypeScript quality\r\n \"@typescript-eslint/no-explicit-any\": \"TypeScript\",\r\n \"@typescript-eslint/no-unused-vars\": \"Dead Code\",\r\n};\r\n\r\n// Rule severity mapping\r\nconst RULE_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/component-class-suffix\": \"warning\",\r\n \"@angular-eslint/directive-class-suffix\": \"warning\",\r\n \"@angular-eslint/pipe-prefix\": \"warning\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warning\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warning\",\r\n \"@angular-eslint/consistent-component-styles\": \"warning\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warning\",\r\n \"@angular-eslint/no-forward-ref\": \"warning\",\r\n \"@angular-eslint/no-input-rename\": \"warning\",\r\n \"@angular-eslint/no-output-rename\": \"warning\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/prefer-standalone\": \"warning\",\r\n \"@typescript-eslint/no-explicit-any\": \"warning\",\r\n \"@typescript-eslint/no-unused-vars\": \"warning\",\r\n};\r\n\r\n// Human-readable messages and help text\r\nconst RULE_MESSAGE_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\": \"Component class should end with 'Component'\",\r\n \"@angular-eslint/directive-class-suffix\": \"Directive class should end with 'Directive'\",\r\n \"@angular-eslint/pipe-prefix\": \"Pipe name should have a consistent prefix\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"Pipe class must implement PipeTransform interface\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Remove empty lifecycle methods\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"Implement the lifecycle interface for lifecycle hooks\",\r\n \"@angular-eslint/consistent-component-styles\": \"Use consistent styles type in component decorator\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Use OnPush change detection for better performance\",\r\n \"@angular-eslint/no-output-native\": \"Avoid shadowing native DOM events in output names\",\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"Lifecycle hooks DoCheck and OnChanges cannot be used together\",\r\n \"@angular-eslint/contextual-lifecycle\": \"Lifecycle hook is not available in this context\",\r\n \"@angular-eslint/no-forward-ref\": \"Avoid using forwardRef — restructure to avoid circular dependency\",\r\n \"@angular-eslint/no-input-rename\": \"Avoid renaming directive inputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-output-rename\": \"Avoid renaming directive outputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"Use @Input() decorator instead of inputs metadata property\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"Use @Output() decorator instead of outputs metadata property\",\r\n \"@angular-eslint/prefer-standalone\": \"Prefer standalone components over NgModule-based components\",\r\n \"@typescript-eslint/no-explicit-any\": \"Avoid 'any' type — use specific types for better type safety\",\r\n \"@typescript-eslint/no-unused-vars\": \"Remove unused variable declaration\",\r\n};\r\n\r\nconst RULE_HELP_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\":\r\n \"Add 'Component' suffix: `export class UserProfileComponent { }`\",\r\n \"@angular-eslint/directive-class-suffix\":\r\n \"Add 'Directive' suffix: `export class HighlightDirective { }`\",\r\n \"@angular-eslint/use-pipe-transform-interface\":\r\n \"Implement PipeTransform: `export class MyPipe implements PipeTransform { transform(value: unknown) { } }`\",\r\n \"@angular-eslint/no-empty-lifecycle-method\":\r\n \"Remove the empty lifecycle method or add logic to it\",\r\n \"@angular-eslint/use-lifecycle-interface\":\r\n \"Add the interface: `export class MyComponent implements OnInit, OnDestroy { }`\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\":\r\n \"Add to decorator: `@Component({ changeDetection: ChangeDetectionStrategy.OnPush })`\",\r\n \"@angular-eslint/no-output-native\":\r\n \"Rename the output: use a descriptive name like `(valueChange)` instead of `(click)` or `(change)`\",\r\n \"@angular-eslint/no-forward-ref\":\r\n \"Restructure your code to avoid circular dependencies, or use `inject()` with a lazy function\",\r\n \"@angular-eslint/no-input-rename\":\r\n \"Remove the alias: `@Input() myProp: string` instead of `@Input('myAlias') myProp: string`\",\r\n \"@angular-eslint/no-output-rename\":\r\n \"Remove the alias: `@Output() myEvent = new EventEmitter()` instead of aliased version\",\r\n \"@angular-eslint/no-inputs-metadata-property\":\r\n \"Use `@Input() myProp: string` decorator on the property instead of `inputs: ['myProp']` in the decorator metadata\",\r\n \"@angular-eslint/no-outputs-metadata-property\":\r\n \"Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata\",\r\n \"@angular-eslint/prefer-standalone\":\r\n \"Add `standalone: true` to component: `@Component({ standalone: true, ... })`\",\r\n \"@typescript-eslint/no-explicit-any\":\r\n \"Replace `any` with a specific type or `unknown` if the type is truly unknown\",\r\n \"@typescript-eslint/no-unused-vars\":\r\n \"Remove the unused variable or prefix with `_` to indicate it's intentionally unused\",\r\n};\r\n\r\nconst buildEslintConfig = (\r\n hasTypeScript: boolean,\r\n tsconfigPath: string | null,\r\n): Linter.Config[] => {\r\n const languageOptions: Linter.Config[\"languageOptions\"] = {\r\n parser: tsEslint.parser as Linter.Parser,\r\n parserOptions: {\r\n ecmaVersion: \"latest\",\r\n sourceType: \"module\",\r\n ...(hasTypeScript && tsconfigPath ? { project: tsconfigPath } : {}),\r\n },\r\n };\r\n\r\n const angularRules: Linter.RulesRecord = {\r\n \"@angular-eslint/component-class-suffix\": \"warn\",\r\n \"@angular-eslint/directive-class-suffix\": \"warn\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warn\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warn\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warn\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/no-forward-ref\": \"warn\",\r\n \"@angular-eslint/no-input-rename\": \"warn\",\r\n \"@angular-eslint/no-output-rename\": \"warn\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warn\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warn\",\r\n };\r\n\r\n const tsRules: Linter.RulesRecord = {\r\n \"@typescript-eslint/no-explicit-any\": \"warn\",\r\n };\r\n\r\n return [\r\n {\r\n files: [\"**/*.ts\"],\r\n plugins: {\r\n \"@angular-eslint\": angularEslintPlugin as unknown as ESLint.Plugin,\r\n \"@typescript-eslint\": tsEslint.plugin as unknown as ESLint.Plugin,\r\n },\r\n languageOptions,\r\n rules: {\r\n ...angularRules,\r\n ...tsRules,\r\n },\r\n },\r\n ];\r\n};\r\n\r\nconst mapEslintSeverity = (severity: number, ruleId: string | null): \"error\" | \"warning\" => {\r\n if (ruleId && RULE_SEVERITY_MAP[ruleId]) {\r\n return RULE_SEVERITY_MAP[ruleId];\r\n }\r\n return severity === 2 ? \"error\" : \"warning\";\r\n};\r\n\r\nconst resolveDiagnosticCategory = (ruleId: string): string =>\r\n RULE_CATEGORY_MAP[ruleId] ?? \"Other\";\r\n\r\nconst resolveMessage = (ruleId: string, defaultMessage: string): string =>\r\n RULE_MESSAGE_MAP[ruleId] ?? defaultMessage;\r\n\r\nconst resolveHelp = (ruleId: string): string => RULE_HELP_MAP[ruleId] ?? \"\";\r\n\r\nconst parsePluginAndRule = (ruleId: string): { plugin: string; rule: string } => {\r\n // e.g. \"@angular-eslint/component-class-suffix\" -> plugin: \"@angular-eslint\", rule: \"component-class-suffix\"\r\n // e.g. \"@typescript-eslint/no-explicit-any\" -> plugin: \"@typescript-eslint\", rule: \"no-explicit-any\"\r\n const match = ruleId.match(/^(@[^/]+\\/[^/]+|[^/]+)\\/(.+)$/);\r\n if (match) {\r\n return { plugin: match[1], rule: match[2] };\r\n }\r\n return { plugin: \"eslint\", rule: ruleId };\r\n};\r\n\r\nexport const runEslint = async (\r\n rootDirectory: string,\r\n hasTypeScript: boolean,\r\n includePaths?: string[],\r\n): Promise<Diagnostic[]> => {\r\n if (includePaths !== undefined && includePaths.length === 0) {\r\n return [];\r\n }\r\n\r\n const tsconfigPath = hasTypeScript\r\n ? path.join(rootDirectory, \"tsconfig.json\")\r\n : null;\r\n\r\n const eslint = new ESLint({\r\n cwd: rootDirectory,\r\n overrideConfigFile: true,\r\n overrideConfig: buildEslintConfig(\r\n hasTypeScript,\r\n tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null,\r\n ),\r\n ignore: true,\r\n });\r\n\r\n const patterns = includePaths ?? [\"**/*.ts\"];\r\n\r\n let results: ESLint.LintResult[];\r\n try {\r\n results = await eslint.lintFiles(patterns);\r\n } catch {\r\n return [];\r\n }\r\n\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const result of results) {\r\n for (const message of result.messages) {\r\n if (!message.ruleId) continue;\r\n const { plugin, rule } = parsePluginAndRule(message.ruleId);\r\n const ruleKey = message.ruleId;\r\n\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, result.filePath),\r\n plugin,\r\n rule,\r\n severity: mapEslintSeverity(message.severity, ruleKey),\r\n message: resolveMessage(ruleKey, message.message),\r\n help: resolveHelp(ruleKey),\r\n line: message.line ?? 0,\r\n column: message.column ?? 0,\r\n category: resolveDiagnosticCategory(ruleKey),\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { main } from \"knip\";\r\nimport { createOptions } from \"knip/session\";\r\nimport { MAX_KNIP_RETRIES } from \"../constants.js\";\r\nimport type { Diagnostic, KnipIssueRecords, KnipResults } from \"../types.js\";\r\n\r\nconst KNIP_CATEGORY_MAP: Record<string, string> = {\r\n files: \"Dead Code\",\r\n exports: \"Dead Code\",\r\n types: \"Dead Code\",\r\n duplicates: \"Dead Code\",\r\n};\r\n\r\nconst KNIP_MESSAGE_MAP: Record<string, string> = {\r\n files: \"Unused file\",\r\n exports: \"Unused export\",\r\n types: \"Unused type\",\r\n duplicates: \"Duplicate export\",\r\n};\r\n\r\nconst KNIP_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n files: \"warning\",\r\n exports: \"warning\",\r\n types: \"warning\",\r\n duplicates: \"warning\",\r\n};\r\n\r\nconst collectIssueRecords = (\r\n records: KnipIssueRecords,\r\n issueType: string,\r\n rootDirectory: string,\r\n): Diagnostic[] => {\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const issues of Object.values(records)) {\r\n for (const issue of Object.values(issues)) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, issue.filePath),\r\n plugin: \"knip\",\r\n rule: issueType,\r\n severity: KNIP_SEVERITY_MAP[issueType] ?? \"warning\",\r\n message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,\r\n help: \"\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[issueType] ?? \"Dead Code\",\r\n weight: 1,\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n\r\n// HACK: knip triggers dotenv which logs to stdout/stderr via console methods\r\nconst silenced = async <T>(fn: () => Promise<T>): Promise<T> => {\r\n const originalLog = console.log;\r\n const originalInfo = console.info;\r\n const originalWarn = console.warn;\r\n const originalError = console.error;\r\n console.log = () => {};\r\n console.info = () => {};\r\n console.warn = () => {};\r\n console.error = () => {};\r\n try {\r\n return await fn();\r\n } finally {\r\n console.log = originalLog;\r\n console.info = originalInfo;\r\n console.warn = originalWarn;\r\n console.error = originalError;\r\n }\r\n};\r\n\r\nconst CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\\/([a-z-]+)\\.config\\./;\r\n\r\nconst extractFailedPluginName = (error: unknown): string | null => {\r\n const match = String(error).match(CONFIG_LOADING_ERROR_PATTERN);\r\n return match?.[1] ?? null;\r\n};\r\n\r\nconst runKnipWithOptions = async (\r\n knipCwd: string,\r\n workspaceName?: string,\r\n): Promise<KnipResults> => {\r\n const options = await silenced(() =>\r\n createOptions({\r\n cwd: knipCwd,\r\n isShowProgress: false,\r\n ...(workspaceName ? { workspace: workspaceName } : {}),\r\n }),\r\n );\r\n\r\n const parsedConfig = options.parsedConfig as Record<string, unknown>;\r\n\r\n for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) {\r\n try {\r\n return (await silenced(() => main(options))) as KnipResults;\r\n } catch (error) {\r\n const failedPlugin = extractFailedPluginName(error);\r\n if (!failedPlugin || attempt === MAX_KNIP_RETRIES) {\r\n throw error;\r\n }\r\n parsedConfig[failedPlugin] = false;\r\n }\r\n }\r\n\r\n throw new Error(\"Unreachable\");\r\n};\r\n\r\nconst hasNodeModules = (directory: string): boolean => {\r\n const nodeModulesPath = path.join(directory, \"node_modules\");\r\n return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();\r\n};\r\n\r\nexport const runKnip = async (rootDirectory: string): Promise<Diagnostic[]> => {\r\n if (!hasNodeModules(rootDirectory)) {\r\n return [];\r\n }\r\n\r\n const knipResult = await runKnipWithOptions(rootDirectory);\r\n\r\n const { issues } = knipResult;\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const unusedFile of issues.files) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, unusedFile),\r\n plugin: \"knip\",\r\n rule: \"files\",\r\n severity: KNIP_SEVERITY_MAP[\"files\"],\r\n message: KNIP_MESSAGE_MAP[\"files\"],\r\n help: \"This file is not imported by any other file in the project.\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[\"files\"],\r\n weight: 1,\r\n });\r\n }\r\n\r\n const recordTypes = [\"exports\", \"types\", \"duplicates\"] as const;\r\n\r\n for (const issueType of recordTypes) {\r\n diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import { spawnSync } from \"node:child_process\";\r\nimport { SOURCE_FILE_PATTERN, DEFAULT_BRANCH_CANDIDATES } from \"../constants.js\";\r\nimport type { DiffInfo } from \"../types.js\";\r\n\r\nconst getCurrentBranch = (directory: string): string | null => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0 ? result.stdout.trim() : null;\r\n};\r\n\r\nconst getBranchChangedFiles = (directory: string, baseBranch: string): string[] => {\r\n const result = spawnSync(\"git\", [\"diff\", \"--name-only\", baseBranch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout.split(\"\\n\").filter(Boolean);\r\n};\r\n\r\nconst getUncommittedChangedFiles = (directory: string): string[] => {\r\n const result = spawnSync(\"git\", [\"status\", \"--porcelain\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter(Boolean)\r\n .map((line) => line.slice(3).trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst branchExists = (directory: string, branch: string): boolean => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--verify\", branch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0;\r\n};\r\n\r\nexport const filterSourceFiles = (files: string[]): string[] =>\r\n files.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));\r\n\r\nexport const getDiffInfo = (directory: string, explicitBaseBranch?: string): DiffInfo | null => {\r\n const currentBranch = getCurrentBranch(directory);\r\n if (!currentBranch) return null;\r\n\r\n if (explicitBaseBranch) {\r\n if (!branchExists(directory, explicitBaseBranch)) return null;\r\n const changedFiles = getBranchChangedFiles(directory, explicitBaseBranch);\r\n return { currentBranch, baseBranch: explicitBaseBranch, changedFiles };\r\n }\r\n\r\n const uncommittedFiles = getUncommittedChangedFiles(directory);\r\n if (uncommittedFiles.length > 0) {\r\n return {\r\n currentBranch,\r\n baseBranch: currentBranch,\r\n changedFiles: uncommittedFiles,\r\n isCurrentChanges: true,\r\n };\r\n }\r\n\r\n for (const candidate of DEFAULT_BRANCH_CANDIDATES) {\r\n if (branchExists(directory, candidate) && currentBranch !== candidate) {\r\n const changedFiles = getBranchChangedFiles(directory, candidate);\r\n if (changedFiles.length > 0) {\r\n return { currentBranch, baseBranch: candidate, changedFiles };\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import path from \"node:path\";\r\nimport { performance } from \"node:perf_hooks\";\r\nimport type { AngularDoctorConfig, Diagnostic, DiffInfo, ProjectInfo, ScoreResult } from \"./types.js\";\r\nimport { calculateScore } from \"./utils/calculate-score.js\";\r\nimport { combineDiagnostics, computeIncludePaths } from \"./utils/combine-diagnostics.js\";\r\nimport { discoverProject } from \"./utils/discover-project.js\";\r\nimport { loadConfig } from \"./utils/load-config.js\";\r\nimport { runEslint } from \"./utils/run-eslint.js\";\r\nimport { runKnip } from \"./utils/run-knip.js\";\r\n\r\nexport type { Diagnostic, DiffInfo, ProjectInfo, AngularDoctorConfig, ScoreResult };\r\nexport { getDiffInfo, filterSourceFiles } from \"./utils/get-diff-files.js\";\r\n\r\nexport interface DiagnoseOptions {\r\n lint?: boolean;\r\n deadCode?: boolean;\r\n includePaths?: string[];\r\n}\r\n\r\nexport interface DiagnoseResult {\r\n diagnostics: Diagnostic[];\r\n score: ScoreResult;\r\n project: ProjectInfo;\r\n elapsedMilliseconds: number;\r\n}\r\n\r\nexport const diagnose = async (\r\n directory: string,\r\n options: DiagnoseOptions = {},\r\n): Promise<DiagnoseResult> => {\r\n const { includePaths = [] } = options;\r\n const isDiffMode = includePaths.length > 0;\r\n\r\n const startTime = performance.now();\r\n const resolvedDirectory = path.resolve(directory);\r\n const projectInfo = discoverProject(resolvedDirectory);\r\n const userConfig = loadConfig(resolvedDirectory);\r\n\r\n const effectiveLint = options.lint ?? userConfig?.lint ?? true;\r\n const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;\r\n\r\n if (!projectInfo.angularVersion) {\r\n throw new Error(\"No Angular dependency found in package.json\");\r\n }\r\n\r\n const computedIncludePaths = computeIncludePaths(includePaths);\r\n\r\n const emptyDiagnostics: Diagnostic[] = [];\r\n\r\n const lintPromise = effectiveLint\r\n ? runEslint(\r\n resolvedDirectory,\r\n projectInfo.hasTypeScript,\r\n computedIncludePaths,\r\n ).catch((error: unknown) => {\r\n console.error(\"Lint failed:\", error);\r\n return emptyDiagnostics;\r\n })\r\n : Promise.resolve(emptyDiagnostics);\r\n\r\n const deadCodePromise =\r\n effectiveDeadCode && !isDiffMode\r\n ? runKnip(resolvedDirectory).catch((error: unknown) => {\r\n console.error(\"Dead code analysis failed:\", error);\r\n return emptyDiagnostics;\r\n })\r\n : Promise.resolve(emptyDiagnostics);\r\n\r\n const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);\r\n const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);\r\n\r\n const elapsedMilliseconds = performance.now() - startTime;\r\n const score = calculateScore(diagnostics);\r\n\r\n return {\r\n diagnostics,\r\n score,\r\n project: projectInfo,\r\n elapsedMilliseconds,\r\n };\r\n};\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/utils/calculate-score.ts","../src/utils/filter-diagnostics.ts","../src/utils/combine-diagnostics.ts","../src/utils/discover-project.ts","../src/utils/load-config.ts","../src/utils/run-eslint.ts","../src/utils/run-knip.ts","../src/utils/get-diff-files.ts","../src/index.ts"],"sourcesContent":["export const SOURCE_FILE_PATTERN = /\\.ts$/;\r\n\r\nexport const MILLISECONDS_PER_SECOND = 1000;\r\n\r\nexport const PERFECT_SCORE = 100;\r\n\r\nexport const SCORE_GOOD_THRESHOLD = 75;\r\n\r\nexport const SCORE_OK_THRESHOLD = 50;\r\n\r\nexport const SCORE_BAR_WIDTH_CHARS = 50;\r\n\r\nexport const SUMMARY_BOX_HORIZONTAL_PADDING_CHARS = 1;\r\n\r\nexport const SUMMARY_BOX_OUTER_INDENT_CHARS = 2;\r\n\r\nexport const GIT_LS_FILES_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\r\n\r\nexport const MAX_KNIP_RETRIES = 5;\r\n\r\nexport const ERROR_RULE_PENALTY = 1.5;\r\n\r\nexport const WARNING_RULE_PENALTY = 0.75;\r\n\r\nexport const DEFAULT_BRANCH_CANDIDATES = [\"main\", \"master\"];\r\n","import {\r\n ERROR_RULE_PENALTY,\r\n PERFECT_SCORE,\r\n SCORE_GOOD_THRESHOLD,\r\n SCORE_OK_THRESHOLD,\r\n WARNING_RULE_PENALTY,\r\n} from \"../constants.js\";\r\nimport type { Diagnostic, ScoreResult } from \"../types.js\";\r\n\r\nexport const getScoreLabel = (score: number): string => {\r\n if (score >= SCORE_GOOD_THRESHOLD) return \"Great\";\r\n if (score >= SCORE_OK_THRESHOLD) return \"Needs work\";\r\n return \"Critical\";\r\n};\r\n\r\nconst countUniqueRules = (\r\n diagnostics: Diagnostic[],\r\n): { errorRuleCount: number; warningRuleCount: number } => {\r\n const errorRules = new Set<string>();\r\n const warningRules = new Set<string>();\r\n\r\n for (const diagnostic of diagnostics) {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (diagnostic.severity === \"error\") {\r\n errorRules.add(ruleKey);\r\n } else {\r\n warningRules.add(ruleKey);\r\n }\r\n }\r\n\r\n return { errorRuleCount: errorRules.size, warningRuleCount: warningRules.size };\r\n};\r\n\r\nexport const calculateScore = (diagnostics: Diagnostic[]): ScoreResult => {\r\n const { errorRuleCount, warningRuleCount } = countUniqueRules(diagnostics);\r\n const penalty = errorRuleCount * ERROR_RULE_PENALTY + warningRuleCount * WARNING_RULE_PENALTY;\r\n const score = Math.max(0, Math.round(PERFECT_SCORE - penalty));\r\n return { score, label: getScoreLabel(score) };\r\n};\r\n","import type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\n\r\nconst matchesGlob = (filePath: string, pattern: string): boolean => {\r\n const escapedPattern = pattern\r\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\r\n .replace(/\\*\\*/g, \".*\")\r\n .replace(/\\*/g, \"[^/]*\");\r\n return new RegExp(`^${escapedPattern}$`).test(filePath);\r\n};\r\n\r\nexport const filterIgnoredDiagnostics = (\r\n diagnostics: Diagnostic[],\r\n config: AngularDoctorConfig,\r\n): Diagnostic[] => {\r\n const ignoredRules = new Set(config.ignore?.rules ?? []);\r\n const ignoredFilePatterns = config.ignore?.files ?? [];\r\n\r\n return diagnostics.filter((diagnostic) => {\r\n const ruleKey = `${diagnostic.plugin}/${diagnostic.rule}`;\r\n if (ignoredRules.has(ruleKey)) return false;\r\n\r\n if (ignoredFilePatterns.some((pattern) => matchesGlob(diagnostic.filePath, pattern))) {\r\n return false;\r\n }\r\n\r\n return true;\r\n });\r\n};\r\n","import { SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularDoctorConfig, Diagnostic } from \"../types.js\";\r\nimport { filterIgnoredDiagnostics } from \"./filter-diagnostics.js\";\r\n\r\nexport const computeIncludePaths = (includePaths: string[]): string[] | undefined =>\r\n includePaths.length > 0\r\n ? includePaths.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath))\r\n : undefined;\r\n\r\nexport const combineDiagnostics = (\r\n lintDiagnostics: Diagnostic[],\r\n deadCodeDiagnostics: Diagnostic[],\r\n userConfig: AngularDoctorConfig | null,\r\n): Diagnostic[] => {\r\n const allDiagnostics = [...lintDiagnostics, ...deadCodeDiagnostics];\r\n return userConfig ? filterIgnoredDiagnostics(allDiagnostics, userConfig) : allDiagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { spawnSync } from \"node:child_process\";\r\nimport { GIT_LS_FILES_MAX_BUFFER_BYTES, SOURCE_FILE_PATTERN } from \"../constants.js\";\r\nimport type { AngularFramework, PackageJson, ProjectInfo, WorkspacePackage } from \"../types.js\";\r\n\r\nconst ANGULAR_FRAMEWORK_PACKAGES: Record<string, AngularFramework> = {\r\n \"@nrwl/angular\": \"nx\",\r\n \"@nx/angular\": \"nx\",\r\n \"@analogjs/platform\": \"analog\",\r\n \"@ionic/angular\": \"ionic\",\r\n \"@angular/ssr\": \"universal\",\r\n \"@nguniversal/express-engine\": \"universal\",\r\n};\r\n\r\nconst ANGULAR_FRAMEWORK_DISPLAY_NAMES: Record<AngularFramework, string> = {\r\n \"angular-cli\": \"Angular CLI\",\r\n nx: \"Nx\",\r\n analog: \"AnalogJS\",\r\n ionic: \"Ionic\",\r\n universal: \"Angular SSR\",\r\n unknown: \"Angular\",\r\n};\r\n\r\nexport const formatFrameworkName = (framework: AngularFramework): string =>\r\n ANGULAR_FRAMEWORK_DISPLAY_NAMES[framework];\r\n\r\nconst readPackageJson = (filePath: string): PackageJson => {\r\n try {\r\n const content = fs.readFileSync(filePath, \"utf-8\");\r\n return JSON.parse(content) as PackageJson;\r\n } catch {\r\n return {};\r\n }\r\n};\r\n\r\nconst collectAllDependencies = (packageJson: PackageJson): Record<string, string> => ({\r\n ...packageJson.peerDependencies,\r\n ...packageJson.dependencies,\r\n ...packageJson.devDependencies,\r\n});\r\n\r\nconst detectFramework = (dependencies: Record<string, string>): AngularFramework => {\r\n for (const [packageName, frameworkName] of Object.entries(ANGULAR_FRAMEWORK_PACKAGES)) {\r\n if (dependencies[packageName]) {\r\n return frameworkName;\r\n }\r\n }\r\n if (dependencies[\"@angular/cli\"] || dependencies[\"@angular-devkit/build-angular\"] || dependencies[\"@angular-devkit/core\"]) {\r\n return \"angular-cli\";\r\n }\r\n return \"unknown\";\r\n};\r\n\r\nconst detectAngularVersion = (dependencies: Record<string, string>): string | null =>\r\n dependencies[\"@angular/core\"] ?? null;\r\n\r\nconst detectStandaloneComponents = (packageJson: PackageJson): boolean => {\r\n const deps = collectAllDependencies(packageJson);\r\n const angularVersion = deps[\"@angular/core\"];\r\n if (!angularVersion) return false;\r\n // Angular 14+ supports standalone components (standalone: true flag)\r\n // Angular 17+ makes standalone the default\r\n const majorVersion = parseInt(angularVersion.match(/\\d+/)?.[0] ?? \"\", 10);\r\n return !isNaN(majorVersion) && majorVersion >= 14;\r\n};\r\n\r\nconst countSourceFiles = (rootDirectory: string): number => {\r\n const result = spawnSync(\"git\", [\"ls-files\", \"--cached\", \"--others\", \"--exclude-standard\"], {\r\n cwd: rootDirectory,\r\n encoding: \"utf-8\",\r\n maxBuffer: GIT_LS_FILES_MAX_BUFFER_BYTES,\r\n });\r\n\r\n if (result.error || result.status !== 0) {\r\n return 0;\r\n }\r\n\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter((filePath) => filePath.length > 0 && SOURCE_FILE_PATTERN.test(filePath)).length;\r\n};\r\n\r\nconst hasAngularDependency = (packageJson: PackageJson): boolean => {\r\n const allDependencies = collectAllDependencies(packageJson);\r\n return Boolean(allDependencies[\"@angular/core\"]);\r\n};\r\n\r\n/**\r\n * Walks up the directory tree from `directory` until it finds a `package.json`.\r\n * Returns the directory containing the package.json, or null if not found.\r\n */\r\nconst findNearestPackageJsonDir = (directory: string): string | null => {\r\n let current = directory;\r\n while (true) {\r\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\r\n const parent = path.dirname(current);\r\n if (parent === current) return null;\r\n current = parent;\r\n }\r\n};\r\n\r\nexport const discoverProject = (directory: string): ProjectInfo => {\r\n // First try the given directory, then walk up to find the nearest package.json\r\n const packageJsonDir = fs.existsSync(path.join(directory, \"package.json\"))\r\n ? directory\r\n : findNearestPackageJsonDir(directory);\r\n\r\n if (!packageJsonDir) {\r\n throw new Error(`No package.json found in ${directory} or any parent directory`);\r\n }\r\n\r\n const packageJson = readPackageJson(path.join(packageJsonDir, \"package.json\"));\r\n const allDeps = collectAllDependencies(packageJson);\r\n const angularVersion = detectAngularVersion(allDeps);\r\n const framework = detectFramework(allDeps);\r\n\r\n // tsconfig.json — check the project directory first, then the package.json directory\r\n const hasTypeScript =\r\n fs.existsSync(path.join(directory, \"tsconfig.json\")) ||\r\n fs.existsSync(path.join(packageJsonDir, \"tsconfig.json\"));\r\n\r\n const hasStandaloneComponents = detectStandaloneComponents(packageJson);\r\n const sourceFileCount = countSourceFiles(directory);\r\n\r\n // Use the Angular project name from angular.json if possible, otherwise from package.json\r\n const angularJsonPath = path.join(packageJsonDir, \"angular.json\");\r\n let projectName = packageJson.name ?? path.basename(directory);\r\n if (packageJsonDir !== directory && fs.existsSync(angularJsonPath)) {\r\n // Use the directory name as a more meaningful project name for workspace sub-projects\r\n projectName = path.basename(directory);\r\n }\r\n\r\n return {\r\n rootDirectory: directory,\r\n projectName,\r\n angularVersion,\r\n framework,\r\n hasTypeScript,\r\n hasStandaloneComponents,\r\n sourceFileCount,\r\n };\r\n};\r\n\r\nconst parsePnpmWorkspacePatterns = (rootDirectory: string): string[] => {\r\n const workspacePath = path.join(rootDirectory, \"pnpm-workspace.yaml\");\r\n if (!fs.existsSync(workspacePath)) return [];\r\n\r\n const content = fs.readFileSync(workspacePath, \"utf-8\");\r\n const patterns: string[] = [];\r\n let isInsidePackagesBlock = false;\r\n\r\n for (const line of content.split(\"\\n\")) {\r\n const trimmed = line.trim();\r\n if (trimmed === \"packages:\") {\r\n isInsidePackagesBlock = true;\r\n continue;\r\n }\r\n if (isInsidePackagesBlock && trimmed.startsWith(\"-\")) {\r\n patterns.push(trimmed.replace(/^-\\s*/, \"\").replace(/[\"']/g, \"\"));\r\n } else if (isInsidePackagesBlock && trimmed.length > 0 && !trimmed.startsWith(\"#\")) {\r\n isInsidePackagesBlock = false;\r\n }\r\n }\r\n\r\n return patterns;\r\n};\r\n\r\nconst getWorkspacePatterns = (rootDirectory: string, packageJson: PackageJson): string[] => {\r\n const pnpmPatterns = parsePnpmWorkspacePatterns(rootDirectory);\r\n if (pnpmPatterns.length > 0) return pnpmPatterns;\r\n\r\n if (Array.isArray(packageJson.workspaces)) {\r\n return packageJson.workspaces;\r\n }\r\n\r\n if (packageJson.workspaces?.packages) {\r\n return packageJson.workspaces.packages;\r\n }\r\n\r\n return [];\r\n};\r\n\r\nconst resolveWorkspaceDirectories = (rootDirectory: string, pattern: string): string[] => {\r\n const cleanPattern = pattern.replace(/[\"']/g, \"\").replace(/\\/\\*\\*$/, \"/*\");\r\n\r\n if (!cleanPattern.includes(\"*\")) {\r\n const directoryPath = path.join(rootDirectory, cleanPattern);\r\n if (fs.existsSync(directoryPath) && fs.existsSync(path.join(directoryPath, \"package.json\"))) {\r\n return [directoryPath];\r\n }\r\n return [];\r\n }\r\n\r\n const wildcardIndex = cleanPattern.indexOf(\"*\");\r\n const baseDirectory = path.join(rootDirectory, cleanPattern.slice(0, wildcardIndex));\r\n const suffixAfterWildcard = cleanPattern.slice(wildcardIndex + 1);\r\n\r\n if (!fs.existsSync(baseDirectory) || !fs.statSync(baseDirectory).isDirectory()) {\r\n return [];\r\n }\r\n\r\n return fs\r\n .readdirSync(baseDirectory)\r\n .map((entry) => path.join(baseDirectory, entry, suffixAfterWildcard))\r\n .filter(\r\n (entryPath) =>\r\n fs.existsSync(entryPath) &&\r\n fs.statSync(entryPath).isDirectory() &&\r\n fs.existsSync(path.join(entryPath, \"package.json\")),\r\n );\r\n};\r\n\r\nexport const listWorkspacePackages = (rootDirectory: string): WorkspacePackage[] => {\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (!fs.existsSync(packageJsonPath)) return [];\r\n\r\n const packageJson = readPackageJson(packageJsonPath);\r\n const patterns = getWorkspacePatterns(rootDirectory, packageJson);\r\n if (patterns.length === 0) return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const pattern of patterns) {\r\n const directories = resolveWorkspaceDirectories(rootDirectory, pattern);\r\n for (const workspaceDirectory of directories) {\r\n const workspacePackageJson = readPackageJson(path.join(workspaceDirectory, \"package.json\"));\r\n\r\n if (!hasAngularDependency(workspacePackageJson)) continue;\r\n\r\n const name = workspacePackageJson.name ?? path.basename(workspaceDirectory);\r\n packages.push({ name, directory: workspaceDirectory });\r\n }\r\n }\r\n\r\n return packages;\r\n};\r\n\r\ninterface AngularWorkspaceProject {\r\n projectType?: string;\r\n root?: string;\r\n}\r\n\r\ninterface AngularWorkspace {\r\n version?: number;\r\n projects?: Record<string, AngularWorkspaceProject | string>;\r\n}\r\n\r\n/**\r\n * Reads `angular.json` (Angular CLI workspace config) and returns one\r\n * WorkspacePackage per project whose root directory contains an Angular dependency.\r\n */\r\nexport const listAngularWorkspaceProjects = (rootDirectory: string): WorkspacePackage[] => {\r\n const angularJsonPath = path.join(rootDirectory, \"angular.json\");\r\n if (!fs.existsSync(angularJsonPath)) return [];\r\n\r\n let workspace: AngularWorkspace;\r\n try {\r\n workspace = JSON.parse(fs.readFileSync(angularJsonPath, \"utf-8\")) as AngularWorkspace;\r\n } catch {\r\n return [];\r\n }\r\n\r\n if (!workspace.projects || typeof workspace.projects !== \"object\") return [];\r\n\r\n const packages: WorkspacePackage[] = [];\r\n\r\n for (const [name, projectConfig] of Object.entries(workspace.projects)) {\r\n // Older angular.json formats store the root as a plain string\r\n const root =\r\n typeof projectConfig === \"string\"\r\n ? projectConfig\r\n : (projectConfig.root ?? \"\");\r\n\r\n const projectDirectory = root ? path.resolve(rootDirectory, root) : rootDirectory;\r\n\r\n // Only include directories that actually exist\r\n if (!fs.existsSync(projectDirectory) || !fs.statSync(projectDirectory).isDirectory()) {\r\n continue;\r\n }\r\n\r\n packages.push({ name, directory: projectDirectory });\r\n }\r\n\r\n return packages;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type { AngularDoctorConfig } from \"../types.js\";\r\n\r\nconst CONFIG_FILENAME = \"angular-doctor.config.json\";\r\nconst PACKAGE_JSON_CONFIG_KEY = \"angularDoctor\";\r\n\r\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\r\n typeof value === \"object\" && value !== null && !Array.isArray(value);\r\n\r\nexport const loadConfig = (rootDirectory: string): AngularDoctorConfig | null => {\r\n const configFilePath = path.join(rootDirectory, CONFIG_FILENAME);\r\n\r\n if (fs.existsSync(configFilePath)) {\r\n try {\r\n const fileContent = fs.readFileSync(configFilePath, \"utf-8\");\r\n const parsed: unknown = JSON.parse(fileContent);\r\n if (!isPlainObject(parsed)) {\r\n console.warn(`Warning: ${CONFIG_FILENAME} must be a JSON object, ignoring.`);\r\n return null;\r\n }\r\n return parsed as AngularDoctorConfig;\r\n } catch (error) {\r\n console.warn(\r\n `Warning: Failed to parse ${CONFIG_FILENAME}: ${error instanceof Error ? error.message : String(error)}`,\r\n );\r\n return null;\r\n }\r\n }\r\n\r\n const packageJsonPath = path.join(rootDirectory, \"package.json\");\r\n if (fs.existsSync(packageJsonPath)) {\r\n try {\r\n const fileContent = fs.readFileSync(packageJsonPath, \"utf-8\");\r\n const packageJson = JSON.parse(fileContent) as Record<string, unknown>;\r\n const embeddedConfig = packageJson[PACKAGE_JSON_CONFIG_KEY];\r\n if (isPlainObject(embeddedConfig)) {\r\n return embeddedConfig as AngularDoctorConfig;\r\n }\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { ESLint, type Linter } from \"eslint\";\r\nimport angularEslintPlugin from \"@angular-eslint/eslint-plugin\";\r\nimport tsEslint from \"typescript-eslint\";\r\nimport type { Diagnostic } from \"../types.js\";\r\n\r\n// Rule category mapping\r\nconst RULE_CATEGORY_MAP: Record<string, string> = {\r\n // Angular component best practices\r\n \"@angular-eslint/component-class-suffix\": \"Components\",\r\n \"@angular-eslint/directive-class-suffix\": \"Components\",\r\n \"@angular-eslint/pipe-prefix\": \"Components\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"Components\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Components\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"Components\",\r\n \"@angular-eslint/consistent-component-styles\": \"Components\",\r\n\r\n // Angular performance\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"Performance\",\r\n \"@angular-eslint/no-output-native\": \"Performance\",\r\n\r\n // Angular architecture / correctness\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/contextual-lifecycle\": \"Correctness\",\r\n \"@angular-eslint/no-forward-ref\": \"Architecture\",\r\n \"@angular-eslint/no-input-rename\": \"Architecture\",\r\n \"@angular-eslint/no-output-rename\": \"Architecture\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"Architecture\",\r\n \"@angular-eslint/prefer-standalone\": \"Architecture\",\r\n\r\n // TypeScript quality\r\n \"@typescript-eslint/no-explicit-any\": \"TypeScript\",\r\n \"@typescript-eslint/no-unused-vars\": \"Dead Code\",\r\n};\r\n\r\n// Rule severity mapping\r\nconst RULE_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/component-class-suffix\": \"warning\",\r\n \"@angular-eslint/directive-class-suffix\": \"warning\",\r\n \"@angular-eslint/pipe-prefix\": \"warning\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warning\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warning\",\r\n \"@angular-eslint/consistent-component-styles\": \"warning\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warning\",\r\n \"@angular-eslint/no-forward-ref\": \"warning\",\r\n \"@angular-eslint/no-input-rename\": \"warning\",\r\n \"@angular-eslint/no-output-rename\": \"warning\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warning\",\r\n \"@angular-eslint/prefer-standalone\": \"warning\",\r\n \"@typescript-eslint/no-explicit-any\": \"warning\",\r\n \"@typescript-eslint/no-unused-vars\": \"warning\",\r\n};\r\n\r\n// Human-readable messages and help text\r\nconst RULE_MESSAGE_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\":\r\n \"Component class should end with 'Component'\",\r\n \"@angular-eslint/directive-class-suffix\":\r\n \"Directive class should end with 'Directive'\",\r\n \"@angular-eslint/pipe-prefix\": \"Pipe name should have a consistent prefix\",\r\n \"@angular-eslint/use-pipe-transform-interface\":\r\n \"Pipe class must implement PipeTransform interface\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"Remove empty lifecycle methods\",\r\n \"@angular-eslint/use-lifecycle-interface\":\r\n \"Implement the lifecycle interface for lifecycle hooks\",\r\n \"@angular-eslint/consistent-component-styles\":\r\n \"Use consistent styles type in component decorator\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\":\r\n \"Use OnPush change detection for better performance\",\r\n \"@angular-eslint/no-output-native\":\r\n \"Avoid shadowing native DOM events in output names\",\r\n \"@angular-eslint/no-conflicting-lifecycle\":\r\n \"Lifecycle hooks DoCheck and OnChanges cannot be used together\",\r\n \"@angular-eslint/contextual-lifecycle\":\r\n \"Lifecycle hook is not available in this context\",\r\n \"@angular-eslint/no-forward-ref\":\r\n \"Avoid using forwardRef — restructure to avoid circular dependency\",\r\n \"@angular-eslint/no-input-rename\":\r\n \"Avoid renaming directive inputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-output-rename\":\r\n \"Avoid renaming directive outputs — use the property name as the binding name\",\r\n \"@angular-eslint/no-inputs-metadata-property\":\r\n \"Use @Input() decorator instead of inputs metadata property\",\r\n \"@angular-eslint/no-outputs-metadata-property\":\r\n \"Use @Output() decorator instead of outputs metadata property\",\r\n \"@angular-eslint/prefer-standalone\":\r\n \"Prefer standalone components over NgModule-based components\",\r\n \"@typescript-eslint/no-explicit-any\":\r\n \"Avoid 'any' type — use specific types for better type safety\",\r\n \"@typescript-eslint/no-unused-vars\": \"Remove unused variable declaration\",\r\n};\r\n\r\nconst RULE_HELP_MAP: Record<string, string> = {\r\n \"@angular-eslint/component-class-suffix\":\r\n \"Add 'Component' suffix: `export class UserProfileComponent { }`\",\r\n \"@angular-eslint/directive-class-suffix\":\r\n \"Add 'Directive' suffix: `export class HighlightDirective { }`\",\r\n \"@angular-eslint/use-pipe-transform-interface\":\r\n \"Implement PipeTransform: `export class MyPipe implements PipeTransform { transform(value: unknown) { } }`\",\r\n \"@angular-eslint/no-empty-lifecycle-method\":\r\n \"Remove the empty lifecycle method or add logic to it\",\r\n \"@angular-eslint/use-lifecycle-interface\":\r\n \"Add the interface: `export class MyComponent implements OnInit, OnDestroy { }`\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\":\r\n \"Add to decorator: `@Component({ changeDetection: ChangeDetectionStrategy.OnPush })`\",\r\n \"@angular-eslint/no-output-native\":\r\n \"Rename the output: use a descriptive name like `(valueChange)` instead of `(click)` or `(change)`\",\r\n \"@angular-eslint/no-forward-ref\":\r\n \"Restructure your code to avoid circular dependencies, or use `inject()` with a lazy function\",\r\n \"@angular-eslint/no-input-rename\":\r\n \"Remove the alias: `@Input() myProp: string` instead of `@Input('myAlias') myProp: string`\",\r\n \"@angular-eslint/no-output-rename\":\r\n \"Remove the alias: `@Output() myEvent = new EventEmitter()` instead of aliased version\",\r\n \"@angular-eslint/no-inputs-metadata-property\":\r\n \"Use `@Input() myProp: string` decorator on the property instead of `inputs: ['myProp']` in the decorator metadata\",\r\n \"@angular-eslint/no-outputs-metadata-property\":\r\n \"Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata\",\r\n \"@angular-eslint/prefer-standalone\":\r\n \"Add `standalone: true` to component: `@Component({ standalone: true, ... })`\",\r\n \"@typescript-eslint/no-explicit-any\":\r\n \"Replace `any` with a specific type or `unknown` if the type is truly unknown\",\r\n \"@typescript-eslint/no-unused-vars\":\r\n \"Remove the unused variable or prefix with `_` to indicate it's intentionally unused\",\r\n};\r\n\r\nconst buildEslintConfig = (\r\n hasTypeScript: boolean,\r\n tsconfigPath: string | null,\r\n useTypeAware: boolean,\r\n): Linter.Config[] => {\r\n const languageOptions: Linter.Config[\"languageOptions\"] = {\r\n parser: tsEslint.parser as Linter.Parser,\r\n parserOptions: {\r\n ecmaVersion: \"latest\",\r\n sourceType: \"module\",\r\n ...(hasTypeScript && tsconfigPath && useTypeAware\r\n ? { project: tsconfigPath }\r\n : {}),\r\n },\r\n };\r\n\r\n const angularRules: Linter.RulesRecord = {\r\n \"@angular-eslint/component-class-suffix\": \"warn\",\r\n \"@angular-eslint/directive-class-suffix\": \"warn\",\r\n \"@angular-eslint/no-empty-lifecycle-method\": \"warn\",\r\n \"@angular-eslint/use-lifecycle-interface\": \"warn\",\r\n \"@angular-eslint/use-pipe-transform-interface\": \"error\",\r\n \"@angular-eslint/prefer-on-push-component-change-detection\": \"warn\",\r\n \"@angular-eslint/no-output-native\": \"error\",\r\n \"@angular-eslint/no-conflicting-lifecycle\": \"error\",\r\n \"@angular-eslint/contextual-lifecycle\": \"error\",\r\n \"@angular-eslint/no-forward-ref\": \"warn\",\r\n \"@angular-eslint/no-input-rename\": \"warn\",\r\n \"@angular-eslint/no-output-rename\": \"warn\",\r\n \"@angular-eslint/no-inputs-metadata-property\": \"warn\",\r\n \"@angular-eslint/no-outputs-metadata-property\": \"warn\",\r\n };\r\n\r\n const tsRules: Linter.RulesRecord = {\r\n \"@typescript-eslint/no-explicit-any\": \"warn\",\r\n };\r\n\r\n return [\r\n {\r\n files: [\"**/*.ts\"],\r\n plugins: {\r\n \"@angular-eslint\": angularEslintPlugin as unknown as ESLint.Plugin,\r\n \"@typescript-eslint\": tsEslint.plugin as unknown as ESLint.Plugin,\r\n },\r\n languageOptions,\r\n rules: {\r\n ...angularRules,\r\n ...tsRules,\r\n },\r\n },\r\n ];\r\n};\r\n\r\nconst mapEslintSeverity = (\r\n severity: number,\r\n ruleId: string | null,\r\n): \"error\" | \"warning\" => {\r\n if (ruleId && RULE_SEVERITY_MAP[ruleId]) {\r\n return RULE_SEVERITY_MAP[ruleId];\r\n }\r\n return severity === 2 ? \"error\" : \"warning\";\r\n};\r\n\r\nconst resolveDiagnosticCategory = (ruleId: string): string =>\r\n RULE_CATEGORY_MAP[ruleId] ?? \"Other\";\r\n\r\nconst resolveMessage = (ruleId: string, defaultMessage: string): string =>\r\n RULE_MESSAGE_MAP[ruleId] ?? defaultMessage;\r\n\r\nconst resolveHelp = (ruleId: string): string => RULE_HELP_MAP[ruleId] ?? \"\";\r\n\r\nconst parsePluginAndRule = (\r\n ruleId: string,\r\n): { plugin: string; rule: string } => {\r\n // e.g. \"@angular-eslint/component-class-suffix\" -> plugin: \"@angular-eslint\", rule: \"component-class-suffix\"\r\n // e.g. \"@typescript-eslint/no-explicit-any\" -> plugin: \"@typescript-eslint\", rule: \"no-explicit-any\"\r\n const match = ruleId.match(/^(@[^/]+\\/[^/]+|[^/]+)\\/(.+)$/);\r\n if (match) {\r\n return { plugin: match[1], rule: match[2] };\r\n }\r\n return { plugin: \"eslint\", rule: ruleId };\r\n};\r\n\r\nexport const runEslint = async (\r\n rootDirectory: string,\r\n hasTypeScript: boolean,\r\n includePaths?: string[],\r\n options?: { useTypeAware?: boolean },\r\n): Promise<Diagnostic[]> => {\r\n if (includePaths !== undefined && includePaths.length === 0) {\r\n return [];\r\n }\r\n\r\n const tsconfigPath = hasTypeScript\r\n ? path.join(rootDirectory, \"tsconfig.json\")\r\n : null;\r\n\r\n const cacheRoot = path.join(\r\n rootDirectory,\r\n \"node_modules\",\r\n \".cache\",\r\n \"angular-doctor\",\r\n );\r\n fs.mkdirSync(cacheRoot, { recursive: true });\r\n const eslint = new ESLint({\r\n cwd: rootDirectory,\r\n overrideConfigFile: null,\r\n overrideConfig: buildEslintConfig(\r\n hasTypeScript,\r\n tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null,\r\n options?.useTypeAware ?? true,\r\n ),\r\n ignore: true,\r\n cache: true,\r\n cacheLocation: path.join(cacheRoot, \".eslintcache\"),\r\n });\r\n\r\n const patterns = includePaths ?? [\"**/*.ts\"];\r\n\r\n let results: ESLint.LintResult[];\r\n try {\r\n results = await eslint.lintFiles(patterns);\r\n } catch {\r\n return [];\r\n }\r\n\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const result of results) {\r\n for (const message of result.messages) {\r\n if (!message.ruleId) continue;\r\n const { plugin, rule } = parsePluginAndRule(message.ruleId);\r\n const ruleKey = message.ruleId;\r\n\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, result.filePath),\r\n plugin,\r\n rule,\r\n severity: mapEslintSeverity(message.severity, ruleKey),\r\n message: resolveMessage(ruleKey, message.message),\r\n help: resolveHelp(ruleKey),\r\n line: message.line ?? 0,\r\n column: message.column ?? 0,\r\n category: resolveDiagnosticCategory(ruleKey),\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { main } from \"knip\";\r\nimport { createOptions } from \"knip/session\";\r\nimport { MAX_KNIP_RETRIES } from \"../constants.js\";\r\nimport type { Diagnostic, KnipIssueRecords, KnipResults } from \"../types.js\";\r\n\r\nconst KNIP_CATEGORY_MAP: Record<string, string> = {\r\n files: \"Dead Code\",\r\n exports: \"Dead Code\",\r\n types: \"Dead Code\",\r\n duplicates: \"Dead Code\",\r\n};\r\n\r\nconst KNIP_MESSAGE_MAP: Record<string, string> = {\r\n files: \"Unused file\",\r\n exports: \"Unused export\",\r\n types: \"Unused type\",\r\n duplicates: \"Duplicate export\",\r\n};\r\n\r\nconst KNIP_SEVERITY_MAP: Record<string, \"error\" | \"warning\"> = {\r\n files: \"warning\",\r\n exports: \"warning\",\r\n types: \"warning\",\r\n duplicates: \"warning\",\r\n};\r\n\r\nconst collectIssueRecords = (\r\n records: KnipIssueRecords,\r\n issueType: string,\r\n rootDirectory: string,\r\n): Diagnostic[] => {\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const issues of Object.values(records)) {\r\n for (const issue of Object.values(issues)) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, issue.filePath),\r\n plugin: \"knip\",\r\n rule: issueType,\r\n severity: KNIP_SEVERITY_MAP[issueType] ?? \"warning\",\r\n message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,\r\n help: \"\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[issueType] ?? \"Dead Code\",\r\n weight: 1,\r\n });\r\n }\r\n }\r\n\r\n return diagnostics;\r\n};\r\n\r\n// HACK: knip triggers dotenv which logs to stdout/stderr via console methods\r\nconst silenced = async <T>(fn: () => Promise<T>): Promise<T> => {\r\n const originalLog = console.log;\r\n const originalInfo = console.info;\r\n const originalWarn = console.warn;\r\n const originalError = console.error;\r\n console.log = () => {};\r\n console.info = () => {};\r\n console.warn = () => {};\r\n console.error = () => {};\r\n try {\r\n return await fn();\r\n } finally {\r\n console.log = originalLog;\r\n console.info = originalInfo;\r\n console.warn = originalWarn;\r\n console.error = originalError;\r\n }\r\n};\r\n\r\nconst CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\\/([a-z-]+)\\.config\\./;\r\n\r\nconst extractFailedPluginName = (error: unknown): string | null => {\r\n const match = String(error).match(CONFIG_LOADING_ERROR_PATTERN);\r\n return match?.[1] ?? null;\r\n};\r\n\r\nconst runKnipWithOptions = async (\r\n knipCwd: string,\r\n workspaceName?: string,\r\n): Promise<KnipResults> => {\r\n const options = await silenced(() =>\r\n createOptions({\r\n cwd: knipCwd,\r\n isShowProgress: false,\r\n ...(workspaceName ? { workspace: workspaceName } : {}),\r\n }),\r\n );\r\n\r\n const parsedConfig = options.parsedConfig as Record<string, unknown>;\r\n\r\n for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) {\r\n try {\r\n return (await silenced(() => main(options))) as KnipResults;\r\n } catch (error) {\r\n const failedPlugin = extractFailedPluginName(error);\r\n if (!failedPlugin || attempt === MAX_KNIP_RETRIES) {\r\n throw error;\r\n }\r\n parsedConfig[failedPlugin] = false;\r\n }\r\n }\r\n\r\n throw new Error(\"Unreachable\");\r\n};\r\n\r\nconst hasNodeModules = (directory: string): boolean => {\r\n const nodeModulesPath = path.join(directory, \"node_modules\");\r\n return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();\r\n};\r\n\r\nexport const runKnip = async (rootDirectory: string): Promise<Diagnostic[]> => {\r\n if (!hasNodeModules(rootDirectory)) {\r\n return [];\r\n }\r\n\r\n const knipResult = await runKnipWithOptions(rootDirectory);\r\n\r\n const { issues } = knipResult;\r\n const diagnostics: Diagnostic[] = [];\r\n\r\n for (const unusedFile of issues.files) {\r\n diagnostics.push({\r\n filePath: path.relative(rootDirectory, unusedFile),\r\n plugin: \"knip\",\r\n rule: \"files\",\r\n severity: KNIP_SEVERITY_MAP[\"files\"],\r\n message: KNIP_MESSAGE_MAP[\"files\"],\r\n help: \"This file is not imported by any other file in the project.\",\r\n line: 0,\r\n column: 0,\r\n category: KNIP_CATEGORY_MAP[\"files\"],\r\n weight: 1,\r\n });\r\n }\r\n\r\n const recordTypes = [\"exports\", \"types\", \"duplicates\"] as const;\r\n\r\n for (const issueType of recordTypes) {\r\n diagnostics.push(...collectIssueRecords(issues[issueType], issueType, rootDirectory));\r\n }\r\n\r\n return diagnostics;\r\n};\r\n","import { spawnSync } from \"node:child_process\";\r\nimport { SOURCE_FILE_PATTERN, DEFAULT_BRANCH_CANDIDATES } from \"../constants.js\";\r\nimport type { DiffInfo } from \"../types.js\";\r\n\r\nconst getCurrentBranch = (directory: string): string | null => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0 ? result.stdout.trim() : null;\r\n};\r\n\r\nconst getBranchChangedFiles = (directory: string, baseBranch: string): string[] => {\r\n const result = spawnSync(\"git\", [\"diff\", \"--name-only\", baseBranch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout.split(\"\\n\").filter(Boolean);\r\n};\r\n\r\nconst getUncommittedChangedFiles = (directory: string): string[] => {\r\n const result = spawnSync(\"git\", [\"status\", \"--porcelain\"], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n if (result.status !== 0) return [];\r\n return result.stdout\r\n .split(\"\\n\")\r\n .filter(Boolean)\r\n .map((line) => line.slice(3).trim())\r\n .filter(Boolean);\r\n};\r\n\r\nconst branchExists = (directory: string, branch: string): boolean => {\r\n const result = spawnSync(\"git\", [\"rev-parse\", \"--verify\", branch], {\r\n cwd: directory,\r\n encoding: \"utf-8\",\r\n });\r\n return result.status === 0;\r\n};\r\n\r\nexport const filterSourceFiles = (files: string[]): string[] =>\r\n files.filter((filePath) => SOURCE_FILE_PATTERN.test(filePath));\r\n\r\nexport const getDiffInfo = (directory: string, explicitBaseBranch?: string): DiffInfo | null => {\r\n const currentBranch = getCurrentBranch(directory);\r\n if (!currentBranch) return null;\r\n\r\n if (explicitBaseBranch) {\r\n if (!branchExists(directory, explicitBaseBranch)) return null;\r\n const changedFiles = getBranchChangedFiles(directory, explicitBaseBranch);\r\n return { currentBranch, baseBranch: explicitBaseBranch, changedFiles };\r\n }\r\n\r\n const uncommittedFiles = getUncommittedChangedFiles(directory);\r\n if (uncommittedFiles.length > 0) {\r\n return {\r\n currentBranch,\r\n baseBranch: currentBranch,\r\n changedFiles: uncommittedFiles,\r\n isCurrentChanges: true,\r\n };\r\n }\r\n\r\n for (const candidate of DEFAULT_BRANCH_CANDIDATES) {\r\n if (branchExists(directory, candidate) && currentBranch !== candidate) {\r\n const changedFiles = getBranchChangedFiles(directory, candidate);\r\n if (changedFiles.length > 0) {\r\n return { currentBranch, baseBranch: candidate, changedFiles };\r\n }\r\n }\r\n }\r\n\r\n return null;\r\n};\r\n","import path from \"node:path\";\r\nimport { performance } from \"node:perf_hooks\";\r\nimport type { AngularDoctorConfig, Diagnostic, DiffInfo, ProjectInfo, ScoreResult } from \"./types.js\";\r\nimport { calculateScore } from \"./utils/calculate-score.js\";\r\nimport { combineDiagnostics, computeIncludePaths } from \"./utils/combine-diagnostics.js\";\r\nimport { discoverProject } from \"./utils/discover-project.js\";\r\nimport { loadConfig } from \"./utils/load-config.js\";\r\nimport { runEslint } from \"./utils/run-eslint.js\";\r\nimport { runKnip } from \"./utils/run-knip.js\";\r\n\r\nexport type { Diagnostic, DiffInfo, ProjectInfo, AngularDoctorConfig, ScoreResult };\r\nexport { getDiffInfo, filterSourceFiles } from \"./utils/get-diff-files.js\";\r\n\r\nexport interface DiagnoseOptions {\r\n lint?: boolean;\r\n deadCode?: boolean;\r\n includePaths?: string[];\r\n}\r\n\r\nexport interface DiagnoseResult {\r\n diagnostics: Diagnostic[];\r\n score: ScoreResult;\r\n project: ProjectInfo;\r\n elapsedMilliseconds: number;\r\n}\r\n\r\nexport const diagnose = async (\r\n directory: string,\r\n options: DiagnoseOptions = {},\r\n): Promise<DiagnoseResult> => {\r\n const { includePaths = [] } = options;\r\n const isDiffMode = includePaths.length > 0;\r\n\r\n const startTime = performance.now();\r\n const resolvedDirectory = path.resolve(directory);\r\n const projectInfo = discoverProject(resolvedDirectory);\r\n const userConfig = loadConfig(resolvedDirectory);\r\n\r\n const effectiveLint = options.lint ?? userConfig?.lint ?? true;\r\n const effectiveDeadCode = options.deadCode ?? userConfig?.deadCode ?? true;\r\n\r\n if (!projectInfo.angularVersion) {\r\n throw new Error(\"No Angular dependency found in package.json\");\r\n }\r\n\r\n const computedIncludePaths = computeIncludePaths(includePaths);\r\n\r\n const emptyDiagnostics: Diagnostic[] = [];\r\n\r\n const lintPromise = effectiveLint\r\n ? runEslint(\r\n resolvedDirectory,\r\n projectInfo.hasTypeScript,\r\n computedIncludePaths,\r\n ).catch((error: unknown) => {\r\n console.error(\"Lint failed:\", error);\r\n return emptyDiagnostics;\r\n })\r\n : Promise.resolve(emptyDiagnostics);\r\n\r\n const deadCodePromise =\r\n effectiveDeadCode && !isDiffMode\r\n ? runKnip(resolvedDirectory).catch((error: unknown) => {\r\n console.error(\"Dead code analysis failed:\", error);\r\n return emptyDiagnostics;\r\n })\r\n : Promise.resolve(emptyDiagnostics);\r\n\r\n const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);\r\n const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);\r\n\r\n const elapsedMilliseconds = performance.now() - startTime;\r\n const score = calculateScore(diagnostics);\r\n\r\n return {\r\n diagnostics,\r\n score,\r\n project: projectInfo,\r\n elapsedMilliseconds,\r\n };\r\n};\r\n"],"mappings":"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"}
|