angular-doctor 1.1.3 → 1.3.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 +130 -0
- package/dist/cli.mjs +462 -40
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +4 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +306 -17
- package/dist/index.mjs.map +1 -1
- package/install-skill.sh +181 -0
- package/package.json +4 -2
- package/skills/angular-doctor/SKILL.md +19 -0
package/dist/cli.mjs
CHANGED
|
@@ -130,12 +130,26 @@ const detectFramework = (dependencies) => {
|
|
|
130
130
|
return "unknown";
|
|
131
131
|
};
|
|
132
132
|
const detectAngularVersion = (dependencies) => dependencies["@angular/core"] ?? null;
|
|
133
|
+
const detectAngularMajorVersion = (version) => {
|
|
134
|
+
if (!version) return null;
|
|
135
|
+
const major = parseInt(version.match(/\d+/)?.[0] ?? "", 10);
|
|
136
|
+
return isNaN(major) ? null : major;
|
|
137
|
+
};
|
|
133
138
|
const detectStandaloneComponents = (packageJson) => {
|
|
134
139
|
const angularVersion = collectAllDependencies(packageJson)["@angular/core"];
|
|
135
140
|
if (!angularVersion) return false;
|
|
136
141
|
const majorVersion = parseInt(angularVersion.match(/\d+/)?.[0] ?? "", 10);
|
|
137
142
|
return !isNaN(majorVersion) && majorVersion >= 14;
|
|
138
143
|
};
|
|
144
|
+
const detectNgRxPackages = (dependencies) => {
|
|
145
|
+
return Object.keys(dependencies).some((dep) => dep.startsWith("@ngrx/") || dep === "@ngrx/store");
|
|
146
|
+
};
|
|
147
|
+
const detectAngularMaterial = (dependencies) => {
|
|
148
|
+
return Object.keys(dependencies).includes("@angular/material");
|
|
149
|
+
};
|
|
150
|
+
const detectSignals = (angularMajorVersion) => {
|
|
151
|
+
return angularMajorVersion !== null && angularMajorVersion >= 17;
|
|
152
|
+
};
|
|
139
153
|
const countSourceFiles = (rootDirectory) => {
|
|
140
154
|
const result = spawnSync("git", [
|
|
141
155
|
"ls-files",
|
|
@@ -173,9 +187,13 @@ const discoverProject = (directory) => {
|
|
|
173
187
|
const packageJson = readPackageJson(path.join(packageJsonDir, "package.json"));
|
|
174
188
|
const allDeps = collectAllDependencies(packageJson);
|
|
175
189
|
const angularVersion = detectAngularVersion(allDeps);
|
|
190
|
+
const angularMajorVersion = detectAngularMajorVersion(angularVersion);
|
|
176
191
|
const framework = detectFramework(allDeps);
|
|
177
192
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json")) || fs.existsSync(path.join(packageJsonDir, "tsconfig.json"));
|
|
178
193
|
const hasStandaloneComponents = detectStandaloneComponents(packageJson);
|
|
194
|
+
const hasNgRx = detectNgRxPackages(allDeps);
|
|
195
|
+
const hasAngularMaterial = detectAngularMaterial(allDeps);
|
|
196
|
+
const hasSignals = detectSignals(angularMajorVersion);
|
|
179
197
|
const sourceFileCount = countSourceFiles(directory);
|
|
180
198
|
const angularJsonPath = path.join(packageJsonDir, "angular.json");
|
|
181
199
|
let projectName = packageJson.name ?? path.basename(directory);
|
|
@@ -184,9 +202,13 @@ const discoverProject = (directory) => {
|
|
|
184
202
|
rootDirectory: directory,
|
|
185
203
|
projectName,
|
|
186
204
|
angularVersion,
|
|
205
|
+
angularMajorVersion,
|
|
187
206
|
framework,
|
|
188
207
|
hasTypeScript,
|
|
189
208
|
hasStandaloneComponents,
|
|
209
|
+
hasNgRx,
|
|
210
|
+
hasAngularMaterial,
|
|
211
|
+
hasSignals,
|
|
190
212
|
sourceFileCount
|
|
191
213
|
};
|
|
192
214
|
};
|
|
@@ -369,30 +391,88 @@ const loadConfig = (rootDirectory) => {
|
|
|
369
391
|
//#region src/utils/run-eslint.ts
|
|
370
392
|
const RULE_CATEGORY_MAP = {
|
|
371
393
|
"@angular-eslint/component-class-suffix": "Components",
|
|
394
|
+
"@angular-eslint/component-max-inline-declarations": "Performance",
|
|
395
|
+
"@angular-eslint/component-selector": "Components",
|
|
372
396
|
"@angular-eslint/directive-class-suffix": "Components",
|
|
397
|
+
"@angular-eslint/directive-selector": "Components",
|
|
373
398
|
"@angular-eslint/pipe-prefix": "Components",
|
|
374
399
|
"@angular-eslint/use-pipe-transform-interface": "Components",
|
|
375
400
|
"@angular-eslint/no-empty-lifecycle-method": "Components",
|
|
376
401
|
"@angular-eslint/use-lifecycle-interface": "Components",
|
|
377
402
|
"@angular-eslint/consistent-component-styles": "Components",
|
|
403
|
+
"@angular-eslint/sort-lifecycle-methods": "Components",
|
|
404
|
+
"@angular-eslint/use-component-selector": "Components",
|
|
378
405
|
"@angular-eslint/prefer-on-push-component-change-detection": "Performance",
|
|
379
406
|
"@angular-eslint/no-output-native": "Performance",
|
|
407
|
+
"@angular-eslint/no-pipe-impure": "Performance",
|
|
380
408
|
"@angular-eslint/no-conflicting-lifecycle": "Correctness",
|
|
381
409
|
"@angular-eslint/contextual-lifecycle": "Correctness",
|
|
410
|
+
"@angular-eslint/contextual-decorator": "Correctness",
|
|
411
|
+
"@angular-eslint/no-async-lifecycle-method": "Correctness",
|
|
412
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "Correctness",
|
|
413
|
+
"@angular-eslint/no-lifecycle-call": "Correctness",
|
|
414
|
+
"@angular-eslint/require-lifecycle-on-prototype": "Correctness",
|
|
415
|
+
"@angular-eslint/no-attribute-decorator": "Architecture",
|
|
382
416
|
"@angular-eslint/no-forward-ref": "Architecture",
|
|
383
417
|
"@angular-eslint/no-input-rename": "Architecture",
|
|
384
418
|
"@angular-eslint/no-output-rename": "Architecture",
|
|
385
419
|
"@angular-eslint/no-inputs-metadata-property": "Architecture",
|
|
386
420
|
"@angular-eslint/no-outputs-metadata-property": "Architecture",
|
|
421
|
+
"@angular-eslint/no-queries-metadata-property": "Architecture",
|
|
387
422
|
"@angular-eslint/prefer-standalone": "Architecture",
|
|
423
|
+
"@angular-eslint/prefer-host-metadata-property": "Architecture",
|
|
424
|
+
"@angular-eslint/prefer-inject": "Architecture",
|
|
425
|
+
"@angular-eslint/prefer-output-emitter-ref": "Architecture",
|
|
426
|
+
"@angular-eslint/prefer-output-readonly": "Architecture",
|
|
427
|
+
"@angular-eslint/use-component-view-encapsulation": "Architecture",
|
|
428
|
+
"@angular-eslint/use-injectable-provided-in": "Architecture",
|
|
429
|
+
"@angular-eslint/no-input-prefix": "Architecture",
|
|
430
|
+
"@angular-eslint/no-output-on-prefix": "Architecture",
|
|
431
|
+
"@angular-eslint/relative-url-prefix": "Security",
|
|
432
|
+
"@angular-eslint/prefer-signals": "Signals",
|
|
433
|
+
"@angular-eslint/prefer-signal-model": "Signals",
|
|
434
|
+
"@angular-eslint/no-uncalled-signals": "Signals",
|
|
435
|
+
"@angular-eslint/template/accessibility": "Accessibility",
|
|
436
|
+
"@angular-eslint/template/alt-text": "Accessibility",
|
|
437
|
+
"@angular-eslint/template/click-events-have-key-events": "Accessibility",
|
|
438
|
+
"@angular-eslint/template/control-events-have-key-events": "Accessibility",
|
|
439
|
+
"@angular-eslint/template/elements-have-content": "Accessibility",
|
|
440
|
+
"@angular-eslint/template/interactive-supports-focus": "Accessibility",
|
|
441
|
+
"@angular-eslint/template/mouse-events-have-key-events": "Accessibility",
|
|
442
|
+
"@angular-eslint/template/no-any": "Accessibility",
|
|
443
|
+
"@angular-eslint/template/table-scope": "Accessibility",
|
|
444
|
+
"@angular-eslint/template/valid-aria": "Accessibility",
|
|
445
|
+
"@ngrx/contextual-action-creator": "NgRx",
|
|
446
|
+
"@ngrx/no-cyclic-action-creators": "NgRx",
|
|
447
|
+
"@ngrx/no-discrete-actions": "NgRx",
|
|
448
|
+
"@ngrx/no-effect-decorator": "NgRx",
|
|
449
|
+
"@ngrx/no-effect-decorator-and-creator": "NgRx",
|
|
450
|
+
"@ngrx/no-multiple-actions-in-effects": "NgRx",
|
|
451
|
+
"@ngrx/no-reordering-in-effect-reducers": "NgRx",
|
|
452
|
+
"@ngrx/no-typed-global-store": "NgRx",
|
|
453
|
+
"@ngrx/on-function-explicit-return-type": "NgRx",
|
|
454
|
+
"@ngrx/prefix-selectors-with-namespace": "NgRx",
|
|
455
|
+
"@ngrx/require-middleware-selector": "NgRx",
|
|
456
|
+
"@ngrx/select-style": "NgRx",
|
|
457
|
+
"@ngrx/use-consumer-selector": "NgRx",
|
|
458
|
+
"@angular/material/prefix-selector": "Material",
|
|
459
|
+
"@angular/material/no-conflicting-mixins": "Material",
|
|
388
460
|
"@typescript-eslint/no-explicit-any": "TypeScript",
|
|
389
|
-
"@typescript-eslint/no-unused-vars": "Dead Code"
|
|
461
|
+
"@typescript-eslint/no-unused-vars": "Dead Code",
|
|
462
|
+
"@typescript-eslint/sort-keys": "TypeScript"
|
|
390
463
|
};
|
|
391
464
|
const RULE_SEVERITY_MAP = {
|
|
392
465
|
"@angular-eslint/no-conflicting-lifecycle": "error",
|
|
393
466
|
"@angular-eslint/contextual-lifecycle": "error",
|
|
394
467
|
"@angular-eslint/use-pipe-transform-interface": "error",
|
|
395
468
|
"@angular-eslint/no-output-native": "error",
|
|
469
|
+
"@angular-eslint/no-async-lifecycle-method": "error",
|
|
470
|
+
"@angular-eslint/no-lifecycle-call": "error",
|
|
471
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "error",
|
|
472
|
+
"@angular-eslint/require-lifecycle-on-prototype": "error",
|
|
473
|
+
"@angular-eslint/relative-url-prefix": "error",
|
|
474
|
+
"@angular-eslint/contextual-decorator": "error",
|
|
475
|
+
"@angular-eslint/no-uncalled-signals": "error",
|
|
396
476
|
"@angular-eslint/component-class-suffix": "warning",
|
|
397
477
|
"@angular-eslint/directive-class-suffix": "warning",
|
|
398
478
|
"@angular-eslint/pipe-prefix": "warning",
|
|
@@ -406,8 +486,42 @@ const RULE_SEVERITY_MAP = {
|
|
|
406
486
|
"@angular-eslint/no-inputs-metadata-property": "warning",
|
|
407
487
|
"@angular-eslint/no-outputs-metadata-property": "warning",
|
|
408
488
|
"@angular-eslint/prefer-standalone": "warning",
|
|
489
|
+
"@angular-eslint/component-selector": "warning",
|
|
490
|
+
"@angular-eslint/directive-selector": "warning",
|
|
491
|
+
"@angular-eslint/no-pipe-impure": "warning",
|
|
492
|
+
"@angular-eslint/no-attribute-decorator": "warning",
|
|
493
|
+
"@angular-eslint/no-queries-metadata-property": "warning",
|
|
494
|
+
"@angular-eslint/prefer-host-metadata-property": "warning",
|
|
495
|
+
"@angular-eslint/prefer-inject": "warning",
|
|
496
|
+
"@angular-eslint/prefer-output-emitter-ref": "warning",
|
|
497
|
+
"@angular-eslint/prefer-output-readonly": "warning",
|
|
498
|
+
"@angular-eslint/use-component-selector": "warning",
|
|
499
|
+
"@angular-eslint/use-component-view-encapsulation": "warning",
|
|
500
|
+
"@angular-eslint/use-injectable-provided-in": "warning",
|
|
501
|
+
"@angular-eslint/component-max-inline-declarations": "warning",
|
|
502
|
+
"@angular-eslint/sort-lifecycle-methods": "warning",
|
|
503
|
+
"@angular-eslint/no-input-prefix": "warning",
|
|
504
|
+
"@angular-eslint/no-output-on-prefix": "warning",
|
|
505
|
+
"@angular-eslint/prefer-signals": "warning",
|
|
506
|
+
"@angular-eslint/prefer-signal-model": "warning",
|
|
409
507
|
"@typescript-eslint/no-explicit-any": "warning",
|
|
410
|
-
"@typescript-eslint/no-unused-vars": "warning"
|
|
508
|
+
"@typescript-eslint/no-unused-vars": "warning",
|
|
509
|
+
"@typescript-eslint/sort-keys": "warning",
|
|
510
|
+
"@ngrx/contextual-action-creator": "warning",
|
|
511
|
+
"@ngrx/no-cyclic-action-creators": "error",
|
|
512
|
+
"@ngrx/no-discrete-actions": "warning",
|
|
513
|
+
"@ngrx/no-effect-decorator": "warning",
|
|
514
|
+
"@ngrx/no-effect-decorator-and-creator": "error",
|
|
515
|
+
"@ngrx/no-multiple-actions-in-effects": "error",
|
|
516
|
+
"@ngrx/no-reordering-in-effect-reducers": "error",
|
|
517
|
+
"@ngrx/no-typed-global-store": "error",
|
|
518
|
+
"@ngrx/on-function-explicit-return-type": "warning",
|
|
519
|
+
"@ngrx/prefix-selectors-with-namespace": "warning",
|
|
520
|
+
"@ngrx/require-middleware-selector": "error",
|
|
521
|
+
"@ngrx/select-style": "warning",
|
|
522
|
+
"@ngrx/use-consumer-selector": "warning",
|
|
523
|
+
"@angular/material/prefix-selector": "warning",
|
|
524
|
+
"@angular/material/no-conflicting-mixins": "error"
|
|
411
525
|
};
|
|
412
526
|
const RULE_MESSAGE_MAP = {
|
|
413
527
|
"@angular-eslint/component-class-suffix": "Component class should end with 'Component'",
|
|
@@ -421,14 +535,55 @@ const RULE_MESSAGE_MAP = {
|
|
|
421
535
|
"@angular-eslint/no-output-native": "Avoid shadowing native DOM events in output names",
|
|
422
536
|
"@angular-eslint/no-conflicting-lifecycle": "Lifecycle hooks DoCheck and OnChanges cannot be used together",
|
|
423
537
|
"@angular-eslint/contextual-lifecycle": "Lifecycle hook is not available in this context",
|
|
538
|
+
"@angular-eslint/contextual-decorator": "Use contextual decorator to specify injection context",
|
|
424
539
|
"@angular-eslint/no-forward-ref": "Avoid using forwardRef — restructure to avoid circular dependency",
|
|
425
540
|
"@angular-eslint/no-input-rename": "Avoid renaming directive inputs — use the property name as the binding name",
|
|
426
541
|
"@angular-eslint/no-output-rename": "Avoid renaming directive outputs — use the property name as the binding name",
|
|
427
542
|
"@angular-eslint/no-inputs-metadata-property": "Use @Input() decorator instead of inputs metadata property",
|
|
428
543
|
"@angular-eslint/no-outputs-metadata-property": "Use @Output() decorator instead of outputs metadata property",
|
|
429
544
|
"@angular-eslint/prefer-standalone": "Prefer standalone components over NgModule-based components",
|
|
545
|
+
"@angular-eslint/component-selector": "Component selector should follow naming convention",
|
|
546
|
+
"@angular-eslint/directive-selector": "Directive selector should follow naming convention",
|
|
547
|
+
"@angular-eslint/no-pipe-impure": "Avoid impure pipes — they run on every change detection cycle",
|
|
548
|
+
"@angular-eslint/no-async-lifecycle-method": "Avoid async lifecycle methods — use signals instead",
|
|
549
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "Remove duplicate entries in decorator metadata arrays",
|
|
550
|
+
"@angular-eslint/no-lifecycle-call": "Don't call lifecycle method directly — let Angular call them",
|
|
551
|
+
"@angular-eslint/require-lifecycle-on-prototype": "Lifecycle methods should be declared on prototype",
|
|
552
|
+
"@angular-eslint/no-attribute-decorator": "Avoid @Attribute() — use @Input() for property bindings",
|
|
553
|
+
"@angular-eslint/no-queries-metadata-property": "Use decorator-based queries instead of metadata properties",
|
|
554
|
+
"@angular-eslint/prefer-host-metadata-property": "Prefer host metadata property over @Host decorator",
|
|
555
|
+
"@angular-eslint/prefer-inject": "Prefer inject() function over constructor dependency injection",
|
|
556
|
+
"@angular-eslint/prefer-output-emitter-ref": "Use EventEmitter with Output instead of Subject",
|
|
557
|
+
"@angular-eslint/prefer-output-readonly": "Mark outputs as readonly when possible",
|
|
558
|
+
"@angular-eslint/use-component-selector": "Components must have selector for proper encapsulation",
|
|
559
|
+
"@angular-eslint/use-component-view-encapsulation": "Specify view encapsulation strategy explicitly",
|
|
560
|
+
"@angular-eslint/use-injectable-provided-in": "Specify providedIn scope for @Injectable()",
|
|
561
|
+
"@angular-eslint/component-max-inline-declarations": "Too many inline declarations in component — extract to separate files",
|
|
562
|
+
"@angular-eslint/sort-lifecycle-methods": "Lifecycle methods should be declared in correct order",
|
|
563
|
+
"@angular-eslint/no-input-prefix": "Avoid prefix for input property names",
|
|
564
|
+
"@angular-eslint/no-output-on-prefix": "Avoid 'on' prefix for output event names",
|
|
565
|
+
"@angular-eslint/relative-url-prefix": "Use relative URL prefixes for better security",
|
|
566
|
+
"@angular-eslint/prefer-signals": "Prefer Angular signals over other reactive patterns",
|
|
567
|
+
"@angular-eslint/prefer-signal-model": "Prefer signal-based model for component state",
|
|
568
|
+
"@angular-eslint/no-uncalled-signals": "Signal getters must be called to access value",
|
|
430
569
|
"@typescript-eslint/no-explicit-any": "Avoid 'any' type — use specific types for better type safety",
|
|
431
|
-
"@typescript-eslint/no-unused-vars": "Remove unused variable declaration"
|
|
570
|
+
"@typescript-eslint/no-unused-vars": "Remove unused variable declaration",
|
|
571
|
+
"@typescript-eslint/sort-keys": "Sort object keys consistently",
|
|
572
|
+
"@ngrx/contextual-action-creator": "Use contextual action creators for typed actions",
|
|
573
|
+
"@ngrx/no-cyclic-action-creators": "Action creators should not reference each other cyclically",
|
|
574
|
+
"@ngrx/no-discrete-actions": "Use discrete actions instead of broad action types",
|
|
575
|
+
"@ngrx/no-effect-decorator": "Consider using functional effects instead of @Effect decorator",
|
|
576
|
+
"@ngrx/no-effect-decorator-and-creator": "Don't use both @Effect decorator and createEffect function",
|
|
577
|
+
"@ngrx/no-multiple-actions-in-effects": "Effects should dispatch a single action or none",
|
|
578
|
+
"@ngrx/no-reordering-in-effect-reducers": "Don't reorder actions in effect reducers",
|
|
579
|
+
"@ngrx/no-typed-global-store": "Use typed GlobalStore for better type safety",
|
|
580
|
+
"@ngrx/on-function-explicit-return-type": "Specify explicit return type for on() reducer functions",
|
|
581
|
+
"@ngrx/prefix-selectors-with-namespace": "Prefix selectors with feature namespace",
|
|
582
|
+
"@ngrx/require-middleware-selector": "Middleware must have selector for proper scoping",
|
|
583
|
+
"@ngrx/select-style": "Prefer selector functions over props in select",
|
|
584
|
+
"@ngrx/use-consumer-selector": "Use useSelector with selector function for proper memoization",
|
|
585
|
+
"@angular/material/prefix-selector": "Material components should use proper selector prefix",
|
|
586
|
+
"@angular/material/no-conflicting-mixins": "Avoid conflicting mixins in Material components"
|
|
432
587
|
};
|
|
433
588
|
const RULE_HELP_MAP = {
|
|
434
589
|
"@angular-eslint/component-class-suffix": "Add 'Component' suffix: `export class UserProfileComponent { }`",
|
|
@@ -445,9 +600,31 @@ const RULE_HELP_MAP = {
|
|
|
445
600
|
"@angular-eslint/no-outputs-metadata-property": "Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata",
|
|
446
601
|
"@angular-eslint/prefer-standalone": "Add `standalone: true` to component: `@Component({ standalone: true, ... })`",
|
|
447
602
|
"@typescript-eslint/no-explicit-any": "Replace `any` with a specific type or `unknown` if the type is truly unknown",
|
|
448
|
-
"@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused"
|
|
603
|
+
"@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused",
|
|
604
|
+
"@typescript-eslint/sort-keys": "Sort object keys alphabetically or by a consistent pattern",
|
|
605
|
+
"@angular-eslint/prefer-inject": "Use `inject(MyService)` instead of constructor injection: `constructor(private myService: MyService) {}`",
|
|
606
|
+
"@angular-eslint/no-pipe-impure": "Remove `pure: false` from pipe decorator or refactor to use a service",
|
|
607
|
+
"@angular-eslint/prefer-signals": "Replace observables with signals for simpler reactive state: `count = signal(0)`",
|
|
608
|
+
"@angular-eslint/prefer-signal-model": "Use signal-based input/output model: `count = model(0)` instead of `@Input() count: number`",
|
|
609
|
+
"@angular-eslint/no-uncalled-signals": "Call the signal getter to access its value: `this.count()` not `this.count`",
|
|
610
|
+
"@angular-eslint/component-max-inline-declarations": "Move templates/styles to separate files or use inline with caution — consider extracting when > 3 inline declarations",
|
|
611
|
+
"@angular-eslint/relative-url-prefix": "Use relative URLs (no leading slash) or ensure absolute URLs are intentional for security",
|
|
612
|
+
"@ngrx/contextual-action-creator": "Use `createActionGroup` or `createAction` with props for type-safe actions",
|
|
613
|
+
"@ngrx/no-multiple-actions-in-effects": "Split effect into multiple effects or use `mergeMap` with individual actions"
|
|
614
|
+
};
|
|
615
|
+
const detectPackagePresence = (packageJson) => {
|
|
616
|
+
const deps = {
|
|
617
|
+
...packageJson.dependencies,
|
|
618
|
+
...packageJson.devDependencies,
|
|
619
|
+
...packageJson.peerDependencies
|
|
620
|
+
};
|
|
621
|
+
return {
|
|
622
|
+
hasNgRx: Object.keys(deps).some((dep) => dep.startsWith("@ngrx/") && !dep.includes("store")) || Object.keys(deps).includes("@ngrx/store"),
|
|
623
|
+
hasAngularMaterial: Object.keys(deps).includes("@angular/material"),
|
|
624
|
+
hasSignals: true
|
|
625
|
+
};
|
|
449
626
|
};
|
|
450
|
-
const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
627
|
+
const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware, packagePresence) => {
|
|
451
628
|
const languageOptions = {
|
|
452
629
|
parser: tsEslint.parser,
|
|
453
630
|
parserOptions: {
|
|
@@ -459,20 +636,73 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
|
459
636
|
const angularRules = {
|
|
460
637
|
"@angular-eslint/component-class-suffix": "warn",
|
|
461
638
|
"@angular-eslint/directive-class-suffix": "warn",
|
|
639
|
+
"@angular-eslint/pipe-prefix": "warn",
|
|
462
640
|
"@angular-eslint/no-empty-lifecycle-method": "warn",
|
|
463
641
|
"@angular-eslint/use-lifecycle-interface": "warn",
|
|
464
|
-
"@angular-eslint/
|
|
642
|
+
"@angular-eslint/consistent-component-styles": "warn",
|
|
643
|
+
"@angular-eslint/sort-lifecycle-methods": "warn",
|
|
644
|
+
"@angular-eslint/use-component-selector": "warn",
|
|
645
|
+
"@angular-eslint/component-selector": "warn",
|
|
646
|
+
"@angular-eslint/directive-selector": "warn",
|
|
465
647
|
"@angular-eslint/prefer-on-push-component-change-detection": "warn",
|
|
466
648
|
"@angular-eslint/no-output-native": "error",
|
|
649
|
+
"@angular-eslint/no-pipe-impure": "warn",
|
|
650
|
+
"@angular-eslint/component-max-inline-declarations": "warn",
|
|
651
|
+
"@angular-eslint/use-pipe-transform-interface": "error",
|
|
467
652
|
"@angular-eslint/no-conflicting-lifecycle": "error",
|
|
468
653
|
"@angular-eslint/contextual-lifecycle": "error",
|
|
654
|
+
"@angular-eslint/contextual-decorator": "error",
|
|
655
|
+
"@angular-eslint/no-async-lifecycle-method": "error",
|
|
656
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "error",
|
|
657
|
+
"@angular-eslint/no-lifecycle-call": "error",
|
|
658
|
+
"@angular-eslint/require-lifecycle-on-prototype": "error",
|
|
469
659
|
"@angular-eslint/no-forward-ref": "warn",
|
|
470
660
|
"@angular-eslint/no-input-rename": "warn",
|
|
471
661
|
"@angular-eslint/no-output-rename": "warn",
|
|
472
662
|
"@angular-eslint/no-inputs-metadata-property": "warn",
|
|
473
|
-
"@angular-eslint/no-outputs-metadata-property": "warn"
|
|
663
|
+
"@angular-eslint/no-outputs-metadata-property": "warn",
|
|
664
|
+
"@angular-eslint/no-queries-metadata-property": "warn",
|
|
665
|
+
"@angular-eslint/prefer-standalone": "warn",
|
|
666
|
+
"@angular-eslint/prefer-host-metadata-property": "warn",
|
|
667
|
+
"@angular-eslint/prefer-inject": "warn",
|
|
668
|
+
"@angular-eslint/prefer-output-emitter-ref": "warn",
|
|
669
|
+
"@angular-eslint/prefer-output-readonly": "warn",
|
|
670
|
+
"@angular-eslint/use-component-view-encapsulation": "warn",
|
|
671
|
+
"@angular-eslint/use-injectable-provided-in": "warn",
|
|
672
|
+
"@angular-eslint/no-attribute-decorator": "warn",
|
|
673
|
+
"@angular-eslint/no-input-prefix": "warn",
|
|
674
|
+
"@angular-eslint/no-output-on-prefix": "warn",
|
|
675
|
+
"@angular-eslint/relative-url-prefix": "error"
|
|
676
|
+
};
|
|
677
|
+
const signalsRules = packagePresence.hasSignals ? {
|
|
678
|
+
"@angular-eslint/prefer-signals": "warn",
|
|
679
|
+
"@angular-eslint/prefer-signal-model": "warn",
|
|
680
|
+
"@angular-eslint/no-uncalled-signals": "error"
|
|
681
|
+
} : {};
|
|
682
|
+
const tsRules = {
|
|
683
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
684
|
+
"@typescript-eslint/no-unused-vars": "warn",
|
|
685
|
+
"@typescript-eslint/sort-keys": "warn"
|
|
474
686
|
};
|
|
475
|
-
const
|
|
687
|
+
const ngrxRules = packagePresence.hasNgRx ? {
|
|
688
|
+
"@ngrx/contextual-action-creator": "warn",
|
|
689
|
+
"@ngrx/no-cyclic-action-creators": "error",
|
|
690
|
+
"@ngrx/no-discrete-actions": "warn",
|
|
691
|
+
"@ngrx/no-effect-decorator": "warn",
|
|
692
|
+
"@ngrx/no-effect-decorator-and-creator": "error",
|
|
693
|
+
"@ngrx/no-multiple-actions-in-effects": "error",
|
|
694
|
+
"@ngrx/no-reordering-in-effect-reducers": "error",
|
|
695
|
+
"@ngrx/no-typed-global-store": "error",
|
|
696
|
+
"@ngrx/on-function-explicit-return-type": "warn",
|
|
697
|
+
"@ngrx/prefix-selectors-with-namespace": "warn",
|
|
698
|
+
"@ngrx/require-middleware-selector": "error",
|
|
699
|
+
"@ngrx/select-style": "warn",
|
|
700
|
+
"@ngrx/use-consumer-selector": "warn"
|
|
701
|
+
} : {};
|
|
702
|
+
const materialRules = packagePresence.hasAngularMaterial ? {
|
|
703
|
+
"@angular/material/prefix-selector": "warn",
|
|
704
|
+
"@angular/material/no-conflicting-mixins": "error"
|
|
705
|
+
} : {};
|
|
476
706
|
return [{
|
|
477
707
|
files: ["**/*.ts"],
|
|
478
708
|
plugins: {
|
|
@@ -482,7 +712,10 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
|
482
712
|
languageOptions,
|
|
483
713
|
rules: {
|
|
484
714
|
...angularRules,
|
|
485
|
-
...tsRules
|
|
715
|
+
...tsRules,
|
|
716
|
+
...signalsRules,
|
|
717
|
+
...ngrxRules,
|
|
718
|
+
...materialRules
|
|
486
719
|
}
|
|
487
720
|
}];
|
|
488
721
|
};
|
|
@@ -505,24 +738,70 @@ const parsePluginAndRule = (ruleId) => {
|
|
|
505
738
|
};
|
|
506
739
|
};
|
|
507
740
|
const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) => {
|
|
508
|
-
if (includePaths !== void 0 && includePaths.length === 0) return
|
|
741
|
+
if (includePaths !== void 0 && includePaths.length === 0) return {
|
|
742
|
+
diagnostics: [],
|
|
743
|
+
errors: []
|
|
744
|
+
};
|
|
509
745
|
const tsconfigPath = hasTypeScript ? path.join(rootDirectory, "tsconfig.json") : null;
|
|
746
|
+
let packagePresence;
|
|
747
|
+
if (options?.frameworkInfo) packagePresence = {
|
|
748
|
+
hasNgRx: options.frameworkInfo.hasNgRx,
|
|
749
|
+
hasAngularMaterial: options.frameworkInfo.hasAngularMaterial,
|
|
750
|
+
hasSignals: options.frameworkInfo.hasSignals
|
|
751
|
+
};
|
|
752
|
+
else {
|
|
753
|
+
packagePresence = {
|
|
754
|
+
hasNgRx: false,
|
|
755
|
+
hasAngularMaterial: false,
|
|
756
|
+
hasSignals: true
|
|
757
|
+
};
|
|
758
|
+
try {
|
|
759
|
+
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
760
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
761
|
+
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
762
|
+
packagePresence = detectPackagePresence(JSON.parse(packageJsonContent));
|
|
763
|
+
}
|
|
764
|
+
} catch {}
|
|
765
|
+
}
|
|
510
766
|
const cacheRoot = path.join(rootDirectory, "node_modules", ".cache", "angular-doctor");
|
|
511
767
|
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
512
768
|
const eslint = new ESLint({
|
|
513
769
|
cwd: rootDirectory,
|
|
514
770
|
overrideConfigFile: null,
|
|
515
|
-
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true),
|
|
771
|
+
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true, packagePresence),
|
|
516
772
|
ignore: true,
|
|
517
773
|
cache: true,
|
|
518
774
|
cacheLocation: path.join(cacheRoot, ".eslintcache")
|
|
519
775
|
});
|
|
520
776
|
const patterns = includePaths ?? ["**/*.ts"];
|
|
521
777
|
let results;
|
|
778
|
+
const errors = [];
|
|
522
779
|
try {
|
|
523
780
|
results = await eslint.lintFiles(patterns);
|
|
524
|
-
} catch {
|
|
525
|
-
|
|
781
|
+
} catch (error) {
|
|
782
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
783
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
784
|
+
const filePathMatch = errorMessage.match(/^(.+?):\s* /);
|
|
785
|
+
const errorFilePath = filePathMatch ? path.relative(rootDirectory, filePathMatch[1]) : void 0;
|
|
786
|
+
errors.push({
|
|
787
|
+
message: errorMessage,
|
|
788
|
+
stack: errorStack,
|
|
789
|
+
filePath: errorFilePath
|
|
790
|
+
});
|
|
791
|
+
return {
|
|
792
|
+
diagnostics: [{
|
|
793
|
+
filePath: errorFilePath ?? "unknown",
|
|
794
|
+
plugin: "eslint",
|
|
795
|
+
rule: "parse-error",
|
|
796
|
+
severity: "error",
|
|
797
|
+
message: errorMessage,
|
|
798
|
+
help: "Fix the syntax error in this file. ESLint could not parse the file.",
|
|
799
|
+
line: 0,
|
|
800
|
+
column: 0,
|
|
801
|
+
category: "Parse Error"
|
|
802
|
+
}],
|
|
803
|
+
errors
|
|
804
|
+
};
|
|
526
805
|
}
|
|
527
806
|
const diagnostics = [];
|
|
528
807
|
for (const result of results) for (const message of result.messages) {
|
|
@@ -541,7 +820,10 @@ const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) =>
|
|
|
541
820
|
category: resolveDiagnosticCategory(ruleKey)
|
|
542
821
|
});
|
|
543
822
|
}
|
|
544
|
-
return
|
|
823
|
+
return {
|
|
824
|
+
diagnostics,
|
|
825
|
+
errors
|
|
826
|
+
};
|
|
545
827
|
};
|
|
546
828
|
|
|
547
829
|
//#endregion
|
|
@@ -696,24 +978,106 @@ const buildFileLineMap = (diagnostics) => {
|
|
|
696
978
|
}
|
|
697
979
|
return fileLines;
|
|
698
980
|
};
|
|
981
|
+
/**
|
|
982
|
+
* Format file:line:column location string
|
|
983
|
+
*/
|
|
984
|
+
const formatLocation = (diagnostic) => {
|
|
985
|
+
const { filePath, line, column } = diagnostic;
|
|
986
|
+
return `${filePath}:${line}${column > 0 ? `:${column}` : ""}`;
|
|
987
|
+
};
|
|
988
|
+
/**
|
|
989
|
+
* Print summary breakdown showing:
|
|
990
|
+
* - Count by severity
|
|
991
|
+
* - Count by category
|
|
992
|
+
* - Top rules by frequency
|
|
993
|
+
*/
|
|
994
|
+
const printSummaryBreakdown = (diagnostics) => {
|
|
995
|
+
const errorCount = diagnostics.filter((d) => d.severity === "error").length;
|
|
996
|
+
const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
|
|
997
|
+
const sortedCategories = [...groupBy(diagnostics, (d) => d.category).entries()].sort(([, a], [, b]) => b.length - a.length);
|
|
998
|
+
const sortedRules = [...groupBy(diagnostics, (d) => `${d.plugin}/${d.rule}`).entries()].sort(([, a], [, b]) => b.length - a.length).slice(0, 5);
|
|
999
|
+
logger.break();
|
|
1000
|
+
logger.log(" Summary Breakdown");
|
|
1001
|
+
logger.log(" ─────────────────");
|
|
1002
|
+
const severityParts = [];
|
|
1003
|
+
if (errorCount > 0) severityParts.push(highlighter.error(`✗ ${errorCount} error${errorCount === 1 ? "" : "s"}`));
|
|
1004
|
+
if (warningCount > 0) severityParts.push(highlighter.warn(`⚠ ${warningCount} warning${warningCount === 1 ? "" : "s"}`));
|
|
1005
|
+
if (severityParts.length > 0) {
|
|
1006
|
+
logger.log(` ${severityParts.join(", ")}`);
|
|
1007
|
+
logger.break();
|
|
1008
|
+
}
|
|
1009
|
+
if (sortedCategories.length > 0) {
|
|
1010
|
+
logger.dim(" Categories:");
|
|
1011
|
+
for (const [category, categoryDiags] of sortedCategories) {
|
|
1012
|
+
const count = categoryDiags.length;
|
|
1013
|
+
logger.dim(` ${category}: ${count}`);
|
|
1014
|
+
}
|
|
1015
|
+
logger.break();
|
|
1016
|
+
}
|
|
1017
|
+
if (sortedRules.length > 0) {
|
|
1018
|
+
logger.dim(" Top rules:");
|
|
1019
|
+
for (const [rule, ruleDiags] of sortedRules) {
|
|
1020
|
+
const count = ruleDiags.length;
|
|
1021
|
+
const severity = ruleDiags[0].severity === "error" ? highlighter.error("✗") : highlighter.warn("⚠");
|
|
1022
|
+
logger.dim(` ${severity} ${rule}: ${count}`);
|
|
1023
|
+
}
|
|
1024
|
+
logger.break();
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
699
1027
|
const printDiagnostics = (diagnostics, isVerbose) => {
|
|
700
1028
|
const sortedRuleGroups = sortBySeverity([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
|
|
701
|
-
|
|
1029
|
+
const errorGroups = sortedRuleGroups.filter(([, diags]) => diags[0]?.severity === "error");
|
|
1030
|
+
const warningGroups = sortedRuleGroups.filter(([, diags]) => diags[0]?.severity === "warning");
|
|
1031
|
+
if (errorGroups.length > 0) {
|
|
1032
|
+
logger.error(` ${errorGroups.length} error${errorGroups.length === 1 ? "" : "s"}:`);
|
|
1033
|
+
logger.log(highlighter.error(" ─────────────────────────────────────────"));
|
|
1034
|
+
}
|
|
1035
|
+
for (const [ruleKey, ruleDiagnostics] of errorGroups) {
|
|
702
1036
|
const firstDiagnostic = ruleDiagnostics[0];
|
|
703
|
-
const icon = colorizeBySeverity(
|
|
1037
|
+
const icon = colorizeBySeverity("✗", firstDiagnostic.severity);
|
|
704
1038
|
const count = ruleDiagnostics.length;
|
|
705
1039
|
const countLabel = count > 1 ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity) : "";
|
|
706
|
-
logger.log(
|
|
1040
|
+
logger.log(" ┌────────────────────────────────────────────────");
|
|
1041
|
+
logger.log(` │ ${icon} ${firstDiagnostic.message}${countLabel}`);
|
|
707
1042
|
if (firstDiagnostic.help) logger.dim(indentMultilineText(firstDiagnostic.help, " "));
|
|
708
1043
|
if (isVerbose) {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1044
|
+
logger.dim(` ${highlighter.info("category:")} ${firstDiagnostic.category}`);
|
|
1045
|
+
logger.dim(` ${highlighter.info("rule:")} ${ruleKey}`);
|
|
1046
|
+
for (const diagnostic of ruleDiagnostics) {
|
|
1047
|
+
const location = formatLocation(diagnostic);
|
|
1048
|
+
logger.dim(` at ${location}`);
|
|
713
1049
|
}
|
|
1050
|
+
if (ruleDiagnostics.length > 5) logger.dim(` ... and ${ruleDiagnostics.length - 5} more occurrences`);
|
|
714
1051
|
}
|
|
1052
|
+
logger.log(" └────────────────────────────────────────────────");
|
|
715
1053
|
logger.break();
|
|
716
1054
|
}
|
|
1055
|
+
if (warningGroups.length > 0) {
|
|
1056
|
+
if (errorGroups.length > 0) logger.break();
|
|
1057
|
+
logger.warn(` ${warningGroups.length} warning${warningGroups.length === 1 ? "" : "s"}:`);
|
|
1058
|
+
logger.log(highlighter.warn(" ─────────────────────────────────────────"));
|
|
1059
|
+
}
|
|
1060
|
+
for (const [ruleKey, ruleDiagnostics] of warningGroups) {
|
|
1061
|
+
const firstDiagnostic = ruleDiagnostics[0];
|
|
1062
|
+
const icon = colorizeBySeverity("⚠", firstDiagnostic.severity);
|
|
1063
|
+
const count = ruleDiagnostics.length;
|
|
1064
|
+
const countLabel = count > 1 ? colorizeBySeverity(` (${count})`, firstDiagnostic.severity) : "";
|
|
1065
|
+
logger.log(" ┌────────────────────────────────────────────────");
|
|
1066
|
+
logger.log(` │ ${icon} ${firstDiagnostic.message}${countLabel}`);
|
|
1067
|
+
if (firstDiagnostic.help) logger.dim(indentMultilineText(firstDiagnostic.help, " "));
|
|
1068
|
+
if (isVerbose) {
|
|
1069
|
+
logger.dim(` ${highlighter.info("category:")} ${firstDiagnostic.category}`);
|
|
1070
|
+
logger.dim(` ${highlighter.info("rule:")} ${ruleKey}`);
|
|
1071
|
+
for (const diagnostic of ruleDiagnostics) {
|
|
1072
|
+
const location = formatLocation(diagnostic);
|
|
1073
|
+
logger.dim(` at ${location}`);
|
|
1074
|
+
}
|
|
1075
|
+
if (ruleDiagnostics.length > 5) logger.dim(` ... and ${ruleDiagnostics.length - 5} more occurrences`);
|
|
1076
|
+
}
|
|
1077
|
+
logger.log(" └────────────────────────────────────────────────");
|
|
1078
|
+
logger.break();
|
|
1079
|
+
}
|
|
1080
|
+
if (isVerbose && diagnostics.length > 0) printSummaryBreakdown(diagnostics);
|
|
717
1081
|
};
|
|
718
1082
|
const formatElapsedTime = (elapsedMilliseconds) => {
|
|
719
1083
|
if (elapsedMilliseconds < MILLISECONDS_PER_SECOND) return `${Math.round(elapsedMilliseconds)}ms`;
|
|
@@ -738,7 +1102,7 @@ const formatRuleSummary = (ruleKey, ruleDiagnostics) => {
|
|
|
738
1102
|
}
|
|
739
1103
|
return sections.join("\n") + "\n";
|
|
740
1104
|
};
|
|
741
|
-
const buildMarkdownReport = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount) => {
|
|
1105
|
+
const buildMarkdownReport = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, isDiffMode = false) => {
|
|
742
1106
|
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
743
1107
|
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
744
1108
|
const affectedFileCount = collectAffectedFiles(diagnostics).size;
|
|
@@ -749,8 +1113,11 @@ const buildMarkdownReport = (diagnostics, elapsedMilliseconds, scoreResult, tota
|
|
|
749
1113
|
`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
750
1114
|
""
|
|
751
1115
|
];
|
|
752
|
-
if (scoreResult)
|
|
753
|
-
|
|
1116
|
+
if (scoreResult) {
|
|
1117
|
+
const labelSuffix = isDiffMode ? " (partial)" : "";
|
|
1118
|
+
lines.push("## Score", "", `**${scoreResult.score} / ${PERFECT_SCORE}** — ${scoreResult.label}${labelSuffix}`, "");
|
|
1119
|
+
}
|
|
1120
|
+
lines.push("## Summary", "", `- Errors: **${errorCount}**`, `- Warnings: **${warningCount}**`, totalSourceFileCount > 0 ? `- Affected files: **${affectedFileCount}/${totalSourceFileCount}**` : `- Affected files: **${affectedFileCount}**`, `- Elapsed: **${elapsed}**`, isDiffMode ? `- Mode: **diff (dead code detection skipped)**` : "", "");
|
|
754
1121
|
if (diagnostics.length === 0) {
|
|
755
1122
|
lines.push("## Diagnostics", "", "No issues found.", "");
|
|
756
1123
|
return lines.join("\n");
|
|
@@ -781,7 +1148,7 @@ const resolveReportPath = (report, outputDirectory, baseDirectory) => {
|
|
|
781
1148
|
}
|
|
782
1149
|
return join(outputDirectory, "report.md");
|
|
783
1150
|
};
|
|
784
|
-
const writeDiagnosticsDirectory = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory) => {
|
|
1151
|
+
const writeDiagnosticsDirectory = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory, isDiffMode = false) => {
|
|
785
1152
|
const outputDirectory = join(tmpdir(), `angular-doctor-${randomUUID()}`);
|
|
786
1153
|
mkdirSync(outputDirectory);
|
|
787
1154
|
const sortedRuleGroups = sortBySeverity([...groupBy(diagnostics, (diagnostic) => `${diagnostic.plugin}/${diagnostic.rule}`).entries()]);
|
|
@@ -790,7 +1157,7 @@ const writeDiagnosticsDirectory = (diagnostics, elapsedMilliseconds, scoreResult
|
|
|
790
1157
|
const markdownPath = resolveReportPath(report, outputDirectory, baseDirectory);
|
|
791
1158
|
if (markdownPath) {
|
|
792
1159
|
mkdirSync(dirname(markdownPath), { recursive: true });
|
|
793
|
-
writeFileSync(markdownPath, buildMarkdownReport(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount));
|
|
1160
|
+
writeFileSync(markdownPath, buildMarkdownReport(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, isDiffMode));
|
|
794
1161
|
}
|
|
795
1162
|
return {
|
|
796
1163
|
outputDirectory,
|
|
@@ -886,10 +1253,10 @@ const buildCountsSummaryLine = (diagnostics, totalSourceFileCount, elapsedMillis
|
|
|
886
1253
|
renderedParts.push(highlighter.dim(fileCountText), highlighter.dim(elapsedTimeText));
|
|
887
1254
|
return createFramedLine(plainParts.join(" "), renderedParts.join(" "));
|
|
888
1255
|
};
|
|
889
|
-
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory) => {
|
|
1256
|
+
const printSummary = (diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory, isDiffMode = false) => {
|
|
890
1257
|
printFramedBox([...buildBrandingLines(scoreResult), buildCountsSummaryLine(diagnostics, totalSourceFileCount, elapsedMilliseconds)]);
|
|
891
1258
|
try {
|
|
892
|
-
const { outputDirectory, markdownPath } = writeDiagnosticsDirectory(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory);
|
|
1259
|
+
const { outputDirectory, markdownPath } = writeDiagnosticsDirectory(diagnostics, elapsedMilliseconds, scoreResult, totalSourceFileCount, report, baseDirectory, isDiffMode);
|
|
893
1260
|
logger.break();
|
|
894
1261
|
logger.dim(` Full diagnostics written to ${outputDirectory}`);
|
|
895
1262
|
if (markdownPath) logger.dim(` Markdown report written to ${markdownPath}`);
|
|
@@ -906,9 +1273,24 @@ const mergeScanOptions = (inputOptions, userConfig) => {
|
|
|
906
1273
|
scoreOnly: inputOptions.scoreOnly ?? false,
|
|
907
1274
|
report: inputOptions.report ?? false,
|
|
908
1275
|
useTypeAwareLint: !fastMode,
|
|
909
|
-
includePaths: inputOptions.includePaths ?? []
|
|
1276
|
+
includePaths: inputOptions.includePaths ?? [],
|
|
1277
|
+
rules: inputOptions.rules
|
|
910
1278
|
};
|
|
911
1279
|
};
|
|
1280
|
+
/**
|
|
1281
|
+
* Parses the --rules CLI flag and returns a FrameworkInfo override.
|
|
1282
|
+
* Categories: signals, ngrx, material
|
|
1283
|
+
* Example: "signals,ngrx" force-enables both signals and ngrx rules.
|
|
1284
|
+
*/
|
|
1285
|
+
const parseRulesOverride = (rules) => {
|
|
1286
|
+
if (!rules || rules === "all") return void 0;
|
|
1287
|
+
const override = {};
|
|
1288
|
+
const categories = rules.split(",").map((c) => c.trim().toLowerCase());
|
|
1289
|
+
for (const category of categories) if (category === "signals") override.hasSignals = true;
|
|
1290
|
+
else if (category === "ngrx") override.hasNgRx = true;
|
|
1291
|
+
else if (category === "material") override.hasAngularMaterial = true;
|
|
1292
|
+
return Object.keys(override).length > 0 ? override : void 0;
|
|
1293
|
+
};
|
|
912
1294
|
const printProjectDetection = (projectInfo, userConfig, isDiffMode, includePaths) => {
|
|
913
1295
|
const frameworkLabel = formatFrameworkName(projectInfo.framework);
|
|
914
1296
|
const languageLabel = "TypeScript";
|
|
@@ -940,9 +1322,34 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
940
1322
|
if (!options.lint) return [];
|
|
941
1323
|
const lintSpinner = options.scoreOnly ? null : spinner("Running lint checks...").start();
|
|
942
1324
|
try {
|
|
943
|
-
const
|
|
1325
|
+
const detectedFrameworkInfo = {
|
|
1326
|
+
hasNgRx: projectInfo.hasNgRx,
|
|
1327
|
+
hasAngularMaterial: projectInfo.hasAngularMaterial,
|
|
1328
|
+
hasSignals: projectInfo.hasSignals
|
|
1329
|
+
};
|
|
1330
|
+
const rulesOverride = parseRulesOverride(options.rules);
|
|
1331
|
+
const frameworkInfo = rulesOverride ? {
|
|
1332
|
+
...detectedFrameworkInfo,
|
|
1333
|
+
...rulesOverride
|
|
1334
|
+
} : detectedFrameworkInfo;
|
|
1335
|
+
const result = await runEslint(directory, projectInfo.hasTypeScript, computedIncludePaths, {
|
|
1336
|
+
useTypeAware: options.useTypeAwareLint,
|
|
1337
|
+
frameworkInfo
|
|
1338
|
+
});
|
|
944
1339
|
lintSpinner?.succeed("Running lint checks.");
|
|
945
|
-
|
|
1340
|
+
if (result.errors.length > 0) {
|
|
1341
|
+
result.errors;
|
|
1342
|
+
if (options.verbose) {
|
|
1343
|
+
logger.break();
|
|
1344
|
+
logger.error("ESLint encountered the following errors:");
|
|
1345
|
+
for (const error of result.errors) {
|
|
1346
|
+
logger.error(` ${error.message}`);
|
|
1347
|
+
if (error.stack) logger.dim(error.stack);
|
|
1348
|
+
}
|
|
1349
|
+
logger.break();
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return result.diagnostics;
|
|
946
1353
|
} catch (error) {
|
|
947
1354
|
didLintFail = true;
|
|
948
1355
|
lintSpinner?.fail("Lint checks failed (non-fatal, skipping).");
|
|
@@ -964,7 +1371,11 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
964
1371
|
return [];
|
|
965
1372
|
}
|
|
966
1373
|
};
|
|
967
|
-
const
|
|
1374
|
+
const parallelStartTime = performance.now();
|
|
1375
|
+
const [lintDiagnostics, deadCodeDiagnostics] = await Promise.all([runLint(), runDeadCode()]);
|
|
1376
|
+
const parallelElapsed = performance.now() - parallelStartTime;
|
|
1377
|
+
const sequentialTime = (options.lint ? parallelElapsed * .6 : 0) + (options.deadCode && !isDiffMode ? parallelElapsed * .4 : 0);
|
|
1378
|
+
if (parallelElapsed > 1e3) logger.dim(` Parallel scan: ${formatElapsedTime(parallelElapsed)} (sequential would be ~${formatElapsedTime(sequentialTime)})`);
|
|
968
1379
|
const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);
|
|
969
1380
|
const elapsedMilliseconds = performance.now() - startTime;
|
|
970
1381
|
const skippedChecks = [];
|
|
@@ -972,12 +1383,16 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
972
1383
|
if (didDeadCodeFail) skippedChecks.push("dead code");
|
|
973
1384
|
const hasSkippedChecks = skippedChecks.length > 0;
|
|
974
1385
|
const scoreResult = calculateScore(diagnostics);
|
|
1386
|
+
const errorCount = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
|
|
1387
|
+
const warningCount = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
|
|
975
1388
|
if (options.scoreOnly) {
|
|
976
1389
|
logger.log(`${scoreResult.score}`);
|
|
977
1390
|
return {
|
|
978
1391
|
diagnostics,
|
|
979
1392
|
scoreResult,
|
|
980
|
-
skippedChecks
|
|
1393
|
+
skippedChecks,
|
|
1394
|
+
errorCount,
|
|
1395
|
+
warningCount
|
|
981
1396
|
};
|
|
982
1397
|
}
|
|
983
1398
|
if (diagnostics.length === 0) {
|
|
@@ -996,11 +1411,13 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
996
1411
|
return {
|
|
997
1412
|
diagnostics,
|
|
998
1413
|
scoreResult,
|
|
999
|
-
skippedChecks
|
|
1414
|
+
skippedChecks,
|
|
1415
|
+
errorCount,
|
|
1416
|
+
warningCount
|
|
1000
1417
|
};
|
|
1001
1418
|
}
|
|
1002
1419
|
printDiagnostics(diagnostics, options.verbose);
|
|
1003
|
-
printSummary(diagnostics, elapsedMilliseconds, scoreResult, isDiffMode ? includePaths.length : projectInfo.sourceFileCount, options.report, directory);
|
|
1420
|
+
printSummary(diagnostics, elapsedMilliseconds, scoreResult, isDiffMode ? includePaths.length : projectInfo.sourceFileCount, options.report, directory, isDiffMode);
|
|
1004
1421
|
if (hasSkippedChecks) {
|
|
1005
1422
|
const skippedLabel = skippedChecks.join(" and ");
|
|
1006
1423
|
logger.break();
|
|
@@ -1009,7 +1426,9 @@ const scan = async (directory, inputOptions = {}) => {
|
|
|
1009
1426
|
return {
|
|
1010
1427
|
diagnostics,
|
|
1011
1428
|
scoreResult,
|
|
1012
|
-
skippedChecks
|
|
1429
|
+
skippedChecks,
|
|
1430
|
+
errorCount,
|
|
1431
|
+
warningCount
|
|
1013
1432
|
};
|
|
1014
1433
|
};
|
|
1015
1434
|
|
|
@@ -1160,7 +1579,7 @@ const selectProjects = async (rootDirectory, projectFlag, skipPrompts) => {
|
|
|
1160
1579
|
|
|
1161
1580
|
//#endregion
|
|
1162
1581
|
//#region src/cli.ts
|
|
1163
|
-
const VERSION = "1.
|
|
1582
|
+
const VERSION = "1.3.0";
|
|
1164
1583
|
const exitWithHint = () => {
|
|
1165
1584
|
logger.break();
|
|
1166
1585
|
logger.log("Cancelled.");
|
|
@@ -1186,7 +1605,8 @@ const resolveCliScanOptions = (flags, userConfig, programInstance) => {
|
|
|
1186
1605
|
verbose: isCliOverride("verbose") ? Boolean(flags.verbose) : userConfig?.verbose ?? false,
|
|
1187
1606
|
scoreOnly: flags.score,
|
|
1188
1607
|
report: flags.report,
|
|
1189
|
-
fast: isCliOverride("fast") ? Boolean(flags.fast) : userConfig?.fast ?? false
|
|
1608
|
+
fast: isCliOverride("fast") ? Boolean(flags.fast) : userConfig?.fast ?? false,
|
|
1609
|
+
rules: flags.rules
|
|
1190
1610
|
};
|
|
1191
1611
|
};
|
|
1192
1612
|
const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isScoreOnly) => {
|
|
@@ -1204,7 +1624,7 @@ const resolveDiffMode = async (diffInfo, effectiveDiff, shouldSkipPrompts, isSco
|
|
|
1204
1624
|
if (isScoreOnly) return false;
|
|
1205
1625
|
return false;
|
|
1206
1626
|
};
|
|
1207
|
-
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) => {
|
|
1627
|
+
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 (Note: dead code detection is skipped in diff mode)").option("--rules <categories>", "force-enable specific rule categories (signals,ngrx,material) or 'all'").option("--exit-code", "exit with non-zero code when ESLint errors are found (for CI integration)").action(async (directory, flags) => {
|
|
1208
1628
|
const isScoreOnly = flags.score;
|
|
1209
1629
|
try {
|
|
1210
1630
|
const resolvedDirectory = path.resolve(directory);
|
|
@@ -1223,6 +1643,7 @@ const program = new Command().name("angular-doctor").description("Diagnose Angul
|
|
|
1223
1643
|
if (isDiffMode && diffInfo && !isScoreOnly) {
|
|
1224
1644
|
if (diffInfo.isCurrentChanges) logger.log("Scanning uncommitted changes");
|
|
1225
1645
|
else logger.log(`Scanning changes: ${highlighter.info(diffInfo.currentBranch)} → ${highlighter.info(diffInfo.baseBranch)}`);
|
|
1646
|
+
logger.warn("Note: Dead code detection skipped in diff mode. Run without --diff for full scan.");
|
|
1226
1647
|
logger.break();
|
|
1227
1648
|
}
|
|
1228
1649
|
for (const projectDirectory of projectDirectories) {
|
|
@@ -1245,10 +1666,11 @@ const program = new Command().name("angular-doctor").description("Diagnose Angul
|
|
|
1245
1666
|
logger.dim(`Scanning ${projectDirectory}...`);
|
|
1246
1667
|
logger.break();
|
|
1247
1668
|
}
|
|
1248
|
-
await scan(projectDirectory, {
|
|
1669
|
+
const scanResult = await scan(projectDirectory, {
|
|
1249
1670
|
...scanOptions,
|
|
1250
1671
|
includePaths
|
|
1251
1672
|
});
|
|
1673
|
+
if ((flags.exitCode || isAutomatedEnvironment()) && scanResult.errorCount > 0) process.exitCode = 1;
|
|
1252
1674
|
if (!isScoreOnly) logger.break();
|
|
1253
1675
|
}
|
|
1254
1676
|
} catch (error) {
|