angular-doctor 1.0.2 → 1.1.3

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,199 @@
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
+ ## 🧭 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,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.1.3";
1141
1164
  const exitWithHint = () => {
1142
1165
  logger.break();
1143
1166
  logger.log("Cancelled.");