angular-doctor 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,199 +1,219 @@
1
- # Angular Doctor
2
-
3
- Diagnose and improve Angular codebases with a single command.
4
-
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
-
7
- ---
8
-
9
- ## ✨ Features
10
-
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
16
-
17
- ---
18
-
19
- ## ✅ Quick start
20
-
21
- Run at your Angular project root (or workspace root):
22
-
23
- ```bash
24
- npx -y angular-doctor@latest .
25
- ```
26
-
27
- ![CLI output](docs/assets/cli-output.png)
28
-
29
- Generate a Markdown report in the current directory:
30
-
31
- ```bash
32
- npx -y angular-doctor@latest . --report .
33
- ```
34
-
35
- Show affected files and line numbers:
36
-
37
- ```bash
38
- npx -y angular-doctor@latest . --verbose
39
- ```
40
-
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):
56
-
57
- ```bash
58
- npx -y angular-doctor@latest . --project my-app,my-lib
59
- ```
60
-
61
- ---
62
-
63
- ## ⚙️ CLI Options
64
-
65
- ```
66
- Usage: angular-doctor [directory] [options]
67
-
68
- Options:
69
- -v, --version display the version number
70
- --no-lint skip linting
71
- --no-dead-code skip dead code detection
72
- --verbose show file details per rule
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
76
- -y, --yes skip prompts, scan all workspace projects
77
- --project <name> select workspace project (comma-separated for multiple)
78
- --diff [base] scan only files changed vs base branch
79
- -h, --help display help for command
80
- ```
81
-
82
- ---
83
-
84
- ## 📝 Reports
85
-
86
- Use `--report` to write a Markdown report:
87
-
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:
98
-
99
- ```json
100
- {
101
- "ignore": {
102
- "rules": ["@angular-eslint/prefer-standalone"],
103
- "files": ["src/generated/**"]
104
- }
105
- }
106
- ```
107
-
108
- Or use the `angularDoctor` key in `package.json`:
109
-
110
- ```json
111
- {
112
- "angularDoctor": {
113
- "ignore": {
114
- "rules": ["@angular-eslint/prefer-standalone"]
115
- }
116
- }
117
- }
118
- ```
119
-
120
- ### Config options
121
-
122
- | Key | Type | Default | Description |
123
- |-----|------|---------|-------------|
124
- | `ignore.rules` | `string[]` | `[]` | Rules to suppress using the `plugin/rule` format |
125
- | `ignore.files` | `string[]` | `[]` | File paths to exclude, supports glob patterns |
126
- | `lint` | `boolean` | `true` | Enable/disable lint checks |
127
- | `deadCode` | `boolean` | `true` | Enable/disable dead code detection |
128
- | `verbose` | `boolean` | `false` | Show file details per rule |
129
- | `diff` | `boolean | string` | — | Scan only changed files |
130
-
131
- ---
132
-
133
- ## 📦 Node.js API
134
-
135
- ```typescript
136
- import { diagnose } from "angular-doctor/api";
137
-
138
- const result = await diagnose("./path/to/your/angular-project");
139
-
140
- console.log(result.score); // { score: 82, label: "Great" }
141
- console.log(result.diagnostics); // Array of Diagnostic objects
142
- console.log(result.project); // Detected framework, Angular version, etc.
143
- ```
144
-
145
- Each diagnostic has the following shape:
146
-
147
- ```typescript
148
- interface Diagnostic {
149
- filePath: string;
150
- plugin: string;
151
- rule: string;
152
- severity: "error" | "warning";
153
- message: string;
154
- help: string;
155
- line: number;
156
- column: number;
157
- category: string;
158
- }
159
- ```
160
-
161
- ---
162
-
163
- ## 🧪 What it checks
164
-
165
- ### Components
166
- - Missing `Component` / `Directive` class suffixes
167
- - Empty lifecycle methods
168
- - Missing lifecycle interfaces
169
- - Pipe not implementing `PipeTransform`
170
-
171
- ### Performance
172
- - Missing `OnPush` change detection strategy
173
- - Outputs shadowing native DOM events
174
-
175
- ### Architecture
176
- - Conflicting lifecycle hooks (`DoCheck` + `OnChanges`)
177
- - Use of `forwardRef`
178
- - Renamed inputs/outputs
179
- - Inline `inputs`/`outputs` metadata properties
180
- - Non-standalone components (Angular 17+)
181
-
182
- ### TypeScript
183
- - Explicit `any` usage
184
-
185
- ### Dead Code
186
- - Unused files
187
- - Unused exports and types
188
-
189
- ---
190
-
191
- ## 💡 Inspiration
192
-
193
- Inspired by [react-doctor](https://github.com/millionco/react-doctor).
194
-
195
- ---
196
-
197
- ## 📄 License
198
-
199
- MIT
1
+ # Angular Doctor
2
+
3
+ Diagnose and improve Angular codebases with a single command.
4
+
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
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
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
16
+
17
+ ---
18
+
19
+ ## ✅ Quick start
20
+
21
+ Run at your Angular project root (or workspace root):
22
+
23
+ ```bash
24
+ npx -y angular-doctor@latest .
25
+ ```
26
+
27
+ ![CLI output](docs/assets/cli-output.png)
28
+
29
+ Generate a Markdown report in the current directory:
30
+
31
+ ```bash
32
+ npx -y angular-doctor@latest . --report .
33
+ ```
34
+
35
+ Show affected files and line numbers:
36
+
37
+ ```bash
38
+ npx -y angular-doctor@latest . --verbose
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 🤖 Install for your coding agent
44
+
45
+ Teach your coding agent to run Angular Doctor automatically after every Angular change:
46
+
47
+ ```bash
48
+ curl -fsSL https://raw.githubusercontent.com/antonygiomarxdev/angular-doctor/main/install-skill.sh | bash
49
+ ```
50
+
51
+ Supports **Cursor**, **Claude Code**, **Windsurf**, **Amp Code**, **Codex**, **Gemini CLI**, and **OpenCode**.
52
+
53
+ Once installed, your agent will automatically run:
54
+
55
+ ```bash
56
+ npx -y angular-doctor@latest . --verbose --diff
57
+ ```
58
+
59
+ …after making Angular changes, catching issues before they reach review.
60
+
61
+ ---
62
+
63
+ ## 🧭 Workspace support
64
+
65
+ Angular Doctor automatically detects multiple projects:
66
+
67
+ - **Angular CLI workspaces** — reads `angular.json` and scans each project inside `projects/`
68
+ - **npm / pnpm workspaces** — detects packages with `@angular/core` from `workspaces` or `pnpm-workspace.yaml`
69
+
70
+ When multiple projects are found:
71
+
72
+ - **Interactive mode**: prompts for which projects to scan
73
+ - **Non-interactive mode** (`-y`, CI): scans all detected projects
74
+
75
+ Target a specific project (comma-separated for multiple):
76
+
77
+ ```bash
78
+ npx -y angular-doctor@latest . --project my-app,my-lib
79
+ ```
80
+
81
+ ---
82
+
83
+ ## ⚙️ CLI Options
84
+
85
+ ```
86
+ Usage: angular-doctor [directory] [options]
87
+
88
+ Options:
89
+ -v, --version display the version number
90
+ --no-lint skip linting
91
+ --no-dead-code skip dead code detection
92
+ --verbose show file details per rule
93
+ --score output only the score
94
+ --report [path] write a markdown report (optional output path)
95
+ --fast speed up by skipping dead code and type-aware lint
96
+ -y, --yes skip prompts, scan all workspace projects
97
+ --project <name> select workspace project (comma-separated for multiple)
98
+ --diff [base] scan only files changed vs base branch
99
+ -h, --help display help for command
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 📝 Reports
105
+
106
+ Use `--report` to write a Markdown report:
107
+
108
+ - `--report` writes to the diagnostics temp folder
109
+ - `--report .` writes to the current project directory
110
+ - `--report ./reports` writes to a custom folder
111
+ - `--report ./reports/scan.md` writes to a specific file
112
+
113
+ ---
114
+
115
+ ## 🔧 Configuration
116
+
117
+ Create an `angular-doctor.config.json` in your project root:
118
+
119
+ ```json
120
+ {
121
+ "ignore": {
122
+ "rules": ["@angular-eslint/prefer-standalone"],
123
+ "files": ["src/generated/**"]
124
+ }
125
+ }
126
+ ```
127
+
128
+ Or use the `angularDoctor` key in `package.json`:
129
+
130
+ ```json
131
+ {
132
+ "angularDoctor": {
133
+ "ignore": {
134
+ "rules": ["@angular-eslint/prefer-standalone"]
135
+ }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### Config options
141
+
142
+ | Key | Type | Default | Description |
143
+ |-----|------|---------|-------------|
144
+ | `ignore.rules` | `string[]` | `[]` | Rules to suppress using the `plugin/rule` format |
145
+ | `ignore.files` | `string[]` | `[]` | File paths to exclude, supports glob patterns |
146
+ | `lint` | `boolean` | `true` | Enable/disable lint checks |
147
+ | `deadCode` | `boolean` | `true` | Enable/disable dead code detection |
148
+ | `verbose` | `boolean` | `false` | Show file details per rule |
149
+ | `diff` | `boolean | string` | — | Scan only changed files |
150
+
151
+ ---
152
+
153
+ ## 📦 Node.js API
154
+
155
+ ```typescript
156
+ import { diagnose } from "angular-doctor/api";
157
+
158
+ const result = await diagnose("./path/to/your/angular-project");
159
+
160
+ console.log(result.score); // { score: 82, label: "Great" }
161
+ console.log(result.diagnostics); // Array of Diagnostic objects
162
+ console.log(result.project); // Detected framework, Angular version, etc.
163
+ ```
164
+
165
+ Each diagnostic has the following shape:
166
+
167
+ ```typescript
168
+ interface Diagnostic {
169
+ filePath: string;
170
+ plugin: string;
171
+ rule: string;
172
+ severity: "error" | "warning";
173
+ message: string;
174
+ help: string;
175
+ line: number;
176
+ column: number;
177
+ category: string;
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## 🧪 What it checks
184
+
185
+ ### Components
186
+ - Missing `Component` / `Directive` class suffixes
187
+ - Empty lifecycle methods
188
+ - Missing lifecycle interfaces
189
+ - Pipe not implementing `PipeTransform`
190
+
191
+ ### Performance
192
+ - Missing `OnPush` change detection strategy
193
+ - Outputs shadowing native DOM events
194
+
195
+ ### Architecture
196
+ - Conflicting lifecycle hooks (`DoCheck` + `OnChanges`)
197
+ - Use of `forwardRef`
198
+ - Renamed inputs/outputs
199
+ - Inline `inputs`/`outputs` metadata properties
200
+ - Non-standalone components (Angular 17+)
201
+
202
+ ### TypeScript
203
+ - Explicit `any` usage
204
+
205
+ ### Dead Code
206
+ - Unused files
207
+ - Unused exports and types
208
+
209
+ ---
210
+
211
+ ## 💡 Inspiration
212
+
213
+ Inspired by [react-doctor](https://github.com/millionco/react-doctor).
214
+
215
+ ---
216
+
217
+ ## 📄 License
218
+
219
+ MIT
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
2
- import "../dist/cli.mjs";
1
+ #!/usr/bin/env node
2
+ import "../dist/cli.mjs";
package/dist/cli.mjs CHANGED
@@ -566,18 +566,22 @@ const KNIP_SEVERITY_MAP = {
566
566
  };
567
567
  const collectIssueRecords = (records, issueType, rootDirectory) => {
568
568
  const diagnostics = [];
569
- for (const issues of Object.values(records)) for (const issue of Object.values(issues)) diagnostics.push({
570
- filePath: path.relative(rootDirectory, issue.filePath),
571
- plugin: "knip",
572
- rule: issueType,
573
- severity: KNIP_SEVERITY_MAP[issueType] ?? "warning",
574
- message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,
575
- help: "",
576
- line: 0,
577
- column: 0,
578
- category: KNIP_CATEGORY_MAP[issueType] ?? "Dead Code",
579
- weight: 1
580
- });
569
+ for (const issues of Object.values(records)) for (const issue of Object.values(issues)) {
570
+ const filePath = path.relative(rootDirectory, issue.filePath);
571
+ if (filePath.startsWith("..")) continue;
572
+ diagnostics.push({
573
+ filePath,
574
+ plugin: "knip",
575
+ rule: issueType,
576
+ severity: KNIP_SEVERITY_MAP[issueType] ?? "warning",
577
+ message: `${KNIP_MESSAGE_MAP[issueType]}: ${issue.symbol}`,
578
+ help: "",
579
+ line: 0,
580
+ column: 0,
581
+ category: KNIP_CATEGORY_MAP[issueType] ?? "Dead Code",
582
+ weight: 1
583
+ });
584
+ }
581
585
  return diagnostics;
582
586
  };
583
587
  const silenced = async (fn) => {
@@ -602,6 +606,7 @@ const CONFIG_LOADING_ERROR_PATTERN = /Error loading .*\/([a-z-]+)\.config\./;
602
606
  const extractFailedPluginName = (error) => {
603
607
  return String(error).match(CONFIG_LOADING_ERROR_PATTERN)?.[1] ?? null;
604
608
  };
609
+ const ANGULAR_ENTRY_PATTERNS = ["**/*.module.ts", "**/*.routes.ts"];
605
610
  const runKnipWithOptions = async (knipCwd, workspaceName) => {
606
611
  const options = await silenced(() => createOptions({
607
612
  cwd: knipCwd,
@@ -609,6 +614,7 @@ const runKnipWithOptions = async (knipCwd, workspaceName) => {
609
614
  ...workspaceName ? { workspace: workspaceName } : {}
610
615
  }));
611
616
  const parsedConfig = options.parsedConfig;
617
+ if (fs.existsSync(path.join(knipCwd, "angular.json")) && parsedConfig["entry"] === void 0) parsedConfig["entry"] = ANGULAR_ENTRY_PATTERNS;
612
618
  for (let attempt = 0; attempt <= MAX_KNIP_RETRIES; attempt++) try {
613
619
  return await silenced(() => main(options));
614
620
  } catch (error) {
@@ -622,22 +628,39 @@ const hasNodeModules = (directory) => {
622
628
  const nodeModulesPath = path.join(directory, "node_modules");
623
629
  return fs.existsSync(nodeModulesPath) && fs.statSync(nodeModulesPath).isDirectory();
624
630
  };
631
+ /**
632
+ * Searches upward from `directory` for an `angular.json` file and returns
633
+ * the directory that contains it, or `null` if none is found.
634
+ */
635
+ const findAngularWorkspaceRoot = (directory) => {
636
+ let current = directory;
637
+ while (true) {
638
+ if (fs.existsSync(path.join(current, "angular.json"))) return current;
639
+ const parent = path.dirname(current);
640
+ if (parent === current) return null;
641
+ current = parent;
642
+ }
643
+ };
625
644
  const runKnip = async (rootDirectory) => {
626
645
  if (!hasNodeModules(rootDirectory)) return [];
627
- const { issues } = await runKnipWithOptions(rootDirectory);
646
+ const { issues } = await runKnipWithOptions(findAngularWorkspaceRoot(rootDirectory) ?? rootDirectory);
628
647
  const diagnostics = [];
629
- for (const unusedFile of issues.files) diagnostics.push({
630
- filePath: path.relative(rootDirectory, unusedFile),
631
- plugin: "knip",
632
- rule: "files",
633
- severity: KNIP_SEVERITY_MAP["files"],
634
- message: KNIP_MESSAGE_MAP["files"],
635
- help: "This file is not imported by any other file in the project.",
636
- line: 0,
637
- column: 0,
638
- category: KNIP_CATEGORY_MAP["files"],
639
- weight: 1
640
- });
648
+ for (const unusedFile of issues.files) {
649
+ const filePath = path.relative(rootDirectory, unusedFile);
650
+ if (filePath.startsWith("..")) continue;
651
+ diagnostics.push({
652
+ filePath,
653
+ plugin: "knip",
654
+ rule: "files",
655
+ severity: KNIP_SEVERITY_MAP["files"],
656
+ message: KNIP_MESSAGE_MAP["files"],
657
+ help: "This file is not imported by any other file in the project.",
658
+ line: 0,
659
+ column: 0,
660
+ category: KNIP_CATEGORY_MAP["files"],
661
+ weight: 1
662
+ });
663
+ }
641
664
  for (const issueType of [
642
665
  "exports",
643
666
  "types",
@@ -1137,7 +1160,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
1137
1160
 
1138
1161
  //#endregion
1139
1162
  //#region src/cli.ts
1140
- const VERSION = "1.0.2";
1163
+ const VERSION = "1.2.0";
1141
1164
  const exitWithHint = () => {
1142
1165
  logger.break();
1143
1166
  logger.log("Cancelled.");