angular-doctor 1.2.0 → 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 +110 -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/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -4,9 +4,13 @@ interface ProjectInfo {
|
|
|
4
4
|
rootDirectory: string;
|
|
5
5
|
projectName: string;
|
|
6
6
|
angularVersion: string | null;
|
|
7
|
+
angularMajorVersion: number | null;
|
|
7
8
|
framework: AngularFramework;
|
|
8
9
|
hasTypeScript: boolean;
|
|
9
10
|
hasStandaloneComponents: boolean;
|
|
11
|
+
hasNgRx: boolean;
|
|
12
|
+
hasAngularMaterial: boolean;
|
|
13
|
+
hasSignals: boolean;
|
|
10
14
|
sourceFileCount: number;
|
|
11
15
|
}
|
|
12
16
|
interface Diagnostic {
|
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,mBAAA;EACA,SAAA,EAAW,gBAAA;EACX,aAAA;EACA,uBAAA;EACA,OAAA;EACA,kBAAA;EACA,UAAA;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,UA+Ce,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;;;cCpEW,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
|
@@ -103,12 +103,26 @@ const detectFramework = (dependencies) => {
|
|
|
103
103
|
return "unknown";
|
|
104
104
|
};
|
|
105
105
|
const detectAngularVersion = (dependencies) => dependencies["@angular/core"] ?? null;
|
|
106
|
+
const detectAngularMajorVersion = (version) => {
|
|
107
|
+
if (!version) return null;
|
|
108
|
+
const major = parseInt(version.match(/\d+/)?.[0] ?? "", 10);
|
|
109
|
+
return isNaN(major) ? null : major;
|
|
110
|
+
};
|
|
106
111
|
const detectStandaloneComponents = (packageJson) => {
|
|
107
112
|
const angularVersion = collectAllDependencies(packageJson)["@angular/core"];
|
|
108
113
|
if (!angularVersion) return false;
|
|
109
114
|
const majorVersion = parseInt(angularVersion.match(/\d+/)?.[0] ?? "", 10);
|
|
110
115
|
return !isNaN(majorVersion) && majorVersion >= 14;
|
|
111
116
|
};
|
|
117
|
+
const detectNgRxPackages = (dependencies) => {
|
|
118
|
+
return Object.keys(dependencies).some((dep) => dep.startsWith("@ngrx/") || dep === "@ngrx/store");
|
|
119
|
+
};
|
|
120
|
+
const detectAngularMaterial = (dependencies) => {
|
|
121
|
+
return Object.keys(dependencies).includes("@angular/material");
|
|
122
|
+
};
|
|
123
|
+
const detectSignals = (angularMajorVersion) => {
|
|
124
|
+
return angularMajorVersion !== null && angularMajorVersion >= 17;
|
|
125
|
+
};
|
|
112
126
|
const countSourceFiles = (rootDirectory) => {
|
|
113
127
|
const result = spawnSync("git", [
|
|
114
128
|
"ls-files",
|
|
@@ -142,9 +156,13 @@ const discoverProject = (directory) => {
|
|
|
142
156
|
const packageJson = readPackageJson(path.join(packageJsonDir, "package.json"));
|
|
143
157
|
const allDeps = collectAllDependencies(packageJson);
|
|
144
158
|
const angularVersion = detectAngularVersion(allDeps);
|
|
159
|
+
const angularMajorVersion = detectAngularMajorVersion(angularVersion);
|
|
145
160
|
const framework = detectFramework(allDeps);
|
|
146
161
|
const hasTypeScript = fs.existsSync(path.join(directory, "tsconfig.json")) || fs.existsSync(path.join(packageJsonDir, "tsconfig.json"));
|
|
147
162
|
const hasStandaloneComponents = detectStandaloneComponents(packageJson);
|
|
163
|
+
const hasNgRx = detectNgRxPackages(allDeps);
|
|
164
|
+
const hasAngularMaterial = detectAngularMaterial(allDeps);
|
|
165
|
+
const hasSignals = detectSignals(angularMajorVersion);
|
|
148
166
|
const sourceFileCount = countSourceFiles(directory);
|
|
149
167
|
const angularJsonPath = path.join(packageJsonDir, "angular.json");
|
|
150
168
|
let projectName = packageJson.name ?? path.basename(directory);
|
|
@@ -153,9 +171,13 @@ const discoverProject = (directory) => {
|
|
|
153
171
|
rootDirectory: directory,
|
|
154
172
|
projectName,
|
|
155
173
|
angularVersion,
|
|
174
|
+
angularMajorVersion,
|
|
156
175
|
framework,
|
|
157
176
|
hasTypeScript,
|
|
158
177
|
hasStandaloneComponents,
|
|
178
|
+
hasNgRx,
|
|
179
|
+
hasAngularMaterial,
|
|
180
|
+
hasSignals,
|
|
159
181
|
sourceFileCount
|
|
160
182
|
};
|
|
161
183
|
};
|
|
@@ -194,30 +216,88 @@ const loadConfig = (rootDirectory) => {
|
|
|
194
216
|
//#region src/utils/run-eslint.ts
|
|
195
217
|
const RULE_CATEGORY_MAP = {
|
|
196
218
|
"@angular-eslint/component-class-suffix": "Components",
|
|
219
|
+
"@angular-eslint/component-max-inline-declarations": "Performance",
|
|
220
|
+
"@angular-eslint/component-selector": "Components",
|
|
197
221
|
"@angular-eslint/directive-class-suffix": "Components",
|
|
222
|
+
"@angular-eslint/directive-selector": "Components",
|
|
198
223
|
"@angular-eslint/pipe-prefix": "Components",
|
|
199
224
|
"@angular-eslint/use-pipe-transform-interface": "Components",
|
|
200
225
|
"@angular-eslint/no-empty-lifecycle-method": "Components",
|
|
201
226
|
"@angular-eslint/use-lifecycle-interface": "Components",
|
|
202
227
|
"@angular-eslint/consistent-component-styles": "Components",
|
|
228
|
+
"@angular-eslint/sort-lifecycle-methods": "Components",
|
|
229
|
+
"@angular-eslint/use-component-selector": "Components",
|
|
203
230
|
"@angular-eslint/prefer-on-push-component-change-detection": "Performance",
|
|
204
231
|
"@angular-eslint/no-output-native": "Performance",
|
|
232
|
+
"@angular-eslint/no-pipe-impure": "Performance",
|
|
205
233
|
"@angular-eslint/no-conflicting-lifecycle": "Correctness",
|
|
206
234
|
"@angular-eslint/contextual-lifecycle": "Correctness",
|
|
235
|
+
"@angular-eslint/contextual-decorator": "Correctness",
|
|
236
|
+
"@angular-eslint/no-async-lifecycle-method": "Correctness",
|
|
237
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "Correctness",
|
|
238
|
+
"@angular-eslint/no-lifecycle-call": "Correctness",
|
|
239
|
+
"@angular-eslint/require-lifecycle-on-prototype": "Correctness",
|
|
240
|
+
"@angular-eslint/no-attribute-decorator": "Architecture",
|
|
207
241
|
"@angular-eslint/no-forward-ref": "Architecture",
|
|
208
242
|
"@angular-eslint/no-input-rename": "Architecture",
|
|
209
243
|
"@angular-eslint/no-output-rename": "Architecture",
|
|
210
244
|
"@angular-eslint/no-inputs-metadata-property": "Architecture",
|
|
211
245
|
"@angular-eslint/no-outputs-metadata-property": "Architecture",
|
|
246
|
+
"@angular-eslint/no-queries-metadata-property": "Architecture",
|
|
212
247
|
"@angular-eslint/prefer-standalone": "Architecture",
|
|
248
|
+
"@angular-eslint/prefer-host-metadata-property": "Architecture",
|
|
249
|
+
"@angular-eslint/prefer-inject": "Architecture",
|
|
250
|
+
"@angular-eslint/prefer-output-emitter-ref": "Architecture",
|
|
251
|
+
"@angular-eslint/prefer-output-readonly": "Architecture",
|
|
252
|
+
"@angular-eslint/use-component-view-encapsulation": "Architecture",
|
|
253
|
+
"@angular-eslint/use-injectable-provided-in": "Architecture",
|
|
254
|
+
"@angular-eslint/no-input-prefix": "Architecture",
|
|
255
|
+
"@angular-eslint/no-output-on-prefix": "Architecture",
|
|
256
|
+
"@angular-eslint/relative-url-prefix": "Security",
|
|
257
|
+
"@angular-eslint/prefer-signals": "Signals",
|
|
258
|
+
"@angular-eslint/prefer-signal-model": "Signals",
|
|
259
|
+
"@angular-eslint/no-uncalled-signals": "Signals",
|
|
260
|
+
"@angular-eslint/template/accessibility": "Accessibility",
|
|
261
|
+
"@angular-eslint/template/alt-text": "Accessibility",
|
|
262
|
+
"@angular-eslint/template/click-events-have-key-events": "Accessibility",
|
|
263
|
+
"@angular-eslint/template/control-events-have-key-events": "Accessibility",
|
|
264
|
+
"@angular-eslint/template/elements-have-content": "Accessibility",
|
|
265
|
+
"@angular-eslint/template/interactive-supports-focus": "Accessibility",
|
|
266
|
+
"@angular-eslint/template/mouse-events-have-key-events": "Accessibility",
|
|
267
|
+
"@angular-eslint/template/no-any": "Accessibility",
|
|
268
|
+
"@angular-eslint/template/table-scope": "Accessibility",
|
|
269
|
+
"@angular-eslint/template/valid-aria": "Accessibility",
|
|
270
|
+
"@ngrx/contextual-action-creator": "NgRx",
|
|
271
|
+
"@ngrx/no-cyclic-action-creators": "NgRx",
|
|
272
|
+
"@ngrx/no-discrete-actions": "NgRx",
|
|
273
|
+
"@ngrx/no-effect-decorator": "NgRx",
|
|
274
|
+
"@ngrx/no-effect-decorator-and-creator": "NgRx",
|
|
275
|
+
"@ngrx/no-multiple-actions-in-effects": "NgRx",
|
|
276
|
+
"@ngrx/no-reordering-in-effect-reducers": "NgRx",
|
|
277
|
+
"@ngrx/no-typed-global-store": "NgRx",
|
|
278
|
+
"@ngrx/on-function-explicit-return-type": "NgRx",
|
|
279
|
+
"@ngrx/prefix-selectors-with-namespace": "NgRx",
|
|
280
|
+
"@ngrx/require-middleware-selector": "NgRx",
|
|
281
|
+
"@ngrx/select-style": "NgRx",
|
|
282
|
+
"@ngrx/use-consumer-selector": "NgRx",
|
|
283
|
+
"@angular/material/prefix-selector": "Material",
|
|
284
|
+
"@angular/material/no-conflicting-mixins": "Material",
|
|
213
285
|
"@typescript-eslint/no-explicit-any": "TypeScript",
|
|
214
|
-
"@typescript-eslint/no-unused-vars": "Dead Code"
|
|
286
|
+
"@typescript-eslint/no-unused-vars": "Dead Code",
|
|
287
|
+
"@typescript-eslint/sort-keys": "TypeScript"
|
|
215
288
|
};
|
|
216
289
|
const RULE_SEVERITY_MAP = {
|
|
217
290
|
"@angular-eslint/no-conflicting-lifecycle": "error",
|
|
218
291
|
"@angular-eslint/contextual-lifecycle": "error",
|
|
219
292
|
"@angular-eslint/use-pipe-transform-interface": "error",
|
|
220
293
|
"@angular-eslint/no-output-native": "error",
|
|
294
|
+
"@angular-eslint/no-async-lifecycle-method": "error",
|
|
295
|
+
"@angular-eslint/no-lifecycle-call": "error",
|
|
296
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "error",
|
|
297
|
+
"@angular-eslint/require-lifecycle-on-prototype": "error",
|
|
298
|
+
"@angular-eslint/relative-url-prefix": "error",
|
|
299
|
+
"@angular-eslint/contextual-decorator": "error",
|
|
300
|
+
"@angular-eslint/no-uncalled-signals": "error",
|
|
221
301
|
"@angular-eslint/component-class-suffix": "warning",
|
|
222
302
|
"@angular-eslint/directive-class-suffix": "warning",
|
|
223
303
|
"@angular-eslint/pipe-prefix": "warning",
|
|
@@ -231,8 +311,42 @@ const RULE_SEVERITY_MAP = {
|
|
|
231
311
|
"@angular-eslint/no-inputs-metadata-property": "warning",
|
|
232
312
|
"@angular-eslint/no-outputs-metadata-property": "warning",
|
|
233
313
|
"@angular-eslint/prefer-standalone": "warning",
|
|
314
|
+
"@angular-eslint/component-selector": "warning",
|
|
315
|
+
"@angular-eslint/directive-selector": "warning",
|
|
316
|
+
"@angular-eslint/no-pipe-impure": "warning",
|
|
317
|
+
"@angular-eslint/no-attribute-decorator": "warning",
|
|
318
|
+
"@angular-eslint/no-queries-metadata-property": "warning",
|
|
319
|
+
"@angular-eslint/prefer-host-metadata-property": "warning",
|
|
320
|
+
"@angular-eslint/prefer-inject": "warning",
|
|
321
|
+
"@angular-eslint/prefer-output-emitter-ref": "warning",
|
|
322
|
+
"@angular-eslint/prefer-output-readonly": "warning",
|
|
323
|
+
"@angular-eslint/use-component-selector": "warning",
|
|
324
|
+
"@angular-eslint/use-component-view-encapsulation": "warning",
|
|
325
|
+
"@angular-eslint/use-injectable-provided-in": "warning",
|
|
326
|
+
"@angular-eslint/component-max-inline-declarations": "warning",
|
|
327
|
+
"@angular-eslint/sort-lifecycle-methods": "warning",
|
|
328
|
+
"@angular-eslint/no-input-prefix": "warning",
|
|
329
|
+
"@angular-eslint/no-output-on-prefix": "warning",
|
|
330
|
+
"@angular-eslint/prefer-signals": "warning",
|
|
331
|
+
"@angular-eslint/prefer-signal-model": "warning",
|
|
234
332
|
"@typescript-eslint/no-explicit-any": "warning",
|
|
235
|
-
"@typescript-eslint/no-unused-vars": "warning"
|
|
333
|
+
"@typescript-eslint/no-unused-vars": "warning",
|
|
334
|
+
"@typescript-eslint/sort-keys": "warning",
|
|
335
|
+
"@ngrx/contextual-action-creator": "warning",
|
|
336
|
+
"@ngrx/no-cyclic-action-creators": "error",
|
|
337
|
+
"@ngrx/no-discrete-actions": "warning",
|
|
338
|
+
"@ngrx/no-effect-decorator": "warning",
|
|
339
|
+
"@ngrx/no-effect-decorator-and-creator": "error",
|
|
340
|
+
"@ngrx/no-multiple-actions-in-effects": "error",
|
|
341
|
+
"@ngrx/no-reordering-in-effect-reducers": "error",
|
|
342
|
+
"@ngrx/no-typed-global-store": "error",
|
|
343
|
+
"@ngrx/on-function-explicit-return-type": "warning",
|
|
344
|
+
"@ngrx/prefix-selectors-with-namespace": "warning",
|
|
345
|
+
"@ngrx/require-middleware-selector": "error",
|
|
346
|
+
"@ngrx/select-style": "warning",
|
|
347
|
+
"@ngrx/use-consumer-selector": "warning",
|
|
348
|
+
"@angular/material/prefix-selector": "warning",
|
|
349
|
+
"@angular/material/no-conflicting-mixins": "error"
|
|
236
350
|
};
|
|
237
351
|
const RULE_MESSAGE_MAP = {
|
|
238
352
|
"@angular-eslint/component-class-suffix": "Component class should end with 'Component'",
|
|
@@ -246,14 +360,55 @@ const RULE_MESSAGE_MAP = {
|
|
|
246
360
|
"@angular-eslint/no-output-native": "Avoid shadowing native DOM events in output names",
|
|
247
361
|
"@angular-eslint/no-conflicting-lifecycle": "Lifecycle hooks DoCheck and OnChanges cannot be used together",
|
|
248
362
|
"@angular-eslint/contextual-lifecycle": "Lifecycle hook is not available in this context",
|
|
363
|
+
"@angular-eslint/contextual-decorator": "Use contextual decorator to specify injection context",
|
|
249
364
|
"@angular-eslint/no-forward-ref": "Avoid using forwardRef — restructure to avoid circular dependency",
|
|
250
365
|
"@angular-eslint/no-input-rename": "Avoid renaming directive inputs — use the property name as the binding name",
|
|
251
366
|
"@angular-eslint/no-output-rename": "Avoid renaming directive outputs — use the property name as the binding name",
|
|
252
367
|
"@angular-eslint/no-inputs-metadata-property": "Use @Input() decorator instead of inputs metadata property",
|
|
253
368
|
"@angular-eslint/no-outputs-metadata-property": "Use @Output() decorator instead of outputs metadata property",
|
|
254
369
|
"@angular-eslint/prefer-standalone": "Prefer standalone components over NgModule-based components",
|
|
370
|
+
"@angular-eslint/component-selector": "Component selector should follow naming convention",
|
|
371
|
+
"@angular-eslint/directive-selector": "Directive selector should follow naming convention",
|
|
372
|
+
"@angular-eslint/no-pipe-impure": "Avoid impure pipes — they run on every change detection cycle",
|
|
373
|
+
"@angular-eslint/no-async-lifecycle-method": "Avoid async lifecycle methods — use signals instead",
|
|
374
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "Remove duplicate entries in decorator metadata arrays",
|
|
375
|
+
"@angular-eslint/no-lifecycle-call": "Don't call lifecycle method directly — let Angular call them",
|
|
376
|
+
"@angular-eslint/require-lifecycle-on-prototype": "Lifecycle methods should be declared on prototype",
|
|
377
|
+
"@angular-eslint/no-attribute-decorator": "Avoid @Attribute() — use @Input() for property bindings",
|
|
378
|
+
"@angular-eslint/no-queries-metadata-property": "Use decorator-based queries instead of metadata properties",
|
|
379
|
+
"@angular-eslint/prefer-host-metadata-property": "Prefer host metadata property over @Host decorator",
|
|
380
|
+
"@angular-eslint/prefer-inject": "Prefer inject() function over constructor dependency injection",
|
|
381
|
+
"@angular-eslint/prefer-output-emitter-ref": "Use EventEmitter with Output instead of Subject",
|
|
382
|
+
"@angular-eslint/prefer-output-readonly": "Mark outputs as readonly when possible",
|
|
383
|
+
"@angular-eslint/use-component-selector": "Components must have selector for proper encapsulation",
|
|
384
|
+
"@angular-eslint/use-component-view-encapsulation": "Specify view encapsulation strategy explicitly",
|
|
385
|
+
"@angular-eslint/use-injectable-provided-in": "Specify providedIn scope for @Injectable()",
|
|
386
|
+
"@angular-eslint/component-max-inline-declarations": "Too many inline declarations in component — extract to separate files",
|
|
387
|
+
"@angular-eslint/sort-lifecycle-methods": "Lifecycle methods should be declared in correct order",
|
|
388
|
+
"@angular-eslint/no-input-prefix": "Avoid prefix for input property names",
|
|
389
|
+
"@angular-eslint/no-output-on-prefix": "Avoid 'on' prefix for output event names",
|
|
390
|
+
"@angular-eslint/relative-url-prefix": "Use relative URL prefixes for better security",
|
|
391
|
+
"@angular-eslint/prefer-signals": "Prefer Angular signals over other reactive patterns",
|
|
392
|
+
"@angular-eslint/prefer-signal-model": "Prefer signal-based model for component state",
|
|
393
|
+
"@angular-eslint/no-uncalled-signals": "Signal getters must be called to access value",
|
|
255
394
|
"@typescript-eslint/no-explicit-any": "Avoid 'any' type — use specific types for better type safety",
|
|
256
|
-
"@typescript-eslint/no-unused-vars": "Remove unused variable declaration"
|
|
395
|
+
"@typescript-eslint/no-unused-vars": "Remove unused variable declaration",
|
|
396
|
+
"@typescript-eslint/sort-keys": "Sort object keys consistently",
|
|
397
|
+
"@ngrx/contextual-action-creator": "Use contextual action creators for typed actions",
|
|
398
|
+
"@ngrx/no-cyclic-action-creators": "Action creators should not reference each other cyclically",
|
|
399
|
+
"@ngrx/no-discrete-actions": "Use discrete actions instead of broad action types",
|
|
400
|
+
"@ngrx/no-effect-decorator": "Consider using functional effects instead of @Effect decorator",
|
|
401
|
+
"@ngrx/no-effect-decorator-and-creator": "Don't use both @Effect decorator and createEffect function",
|
|
402
|
+
"@ngrx/no-multiple-actions-in-effects": "Effects should dispatch a single action or none",
|
|
403
|
+
"@ngrx/no-reordering-in-effect-reducers": "Don't reorder actions in effect reducers",
|
|
404
|
+
"@ngrx/no-typed-global-store": "Use typed GlobalStore for better type safety",
|
|
405
|
+
"@ngrx/on-function-explicit-return-type": "Specify explicit return type for on() reducer functions",
|
|
406
|
+
"@ngrx/prefix-selectors-with-namespace": "Prefix selectors with feature namespace",
|
|
407
|
+
"@ngrx/require-middleware-selector": "Middleware must have selector for proper scoping",
|
|
408
|
+
"@ngrx/select-style": "Prefer selector functions over props in select",
|
|
409
|
+
"@ngrx/use-consumer-selector": "Use useSelector with selector function for proper memoization",
|
|
410
|
+
"@angular/material/prefix-selector": "Material components should use proper selector prefix",
|
|
411
|
+
"@angular/material/no-conflicting-mixins": "Avoid conflicting mixins in Material components"
|
|
257
412
|
};
|
|
258
413
|
const RULE_HELP_MAP = {
|
|
259
414
|
"@angular-eslint/component-class-suffix": "Add 'Component' suffix: `export class UserProfileComponent { }`",
|
|
@@ -270,9 +425,31 @@ const RULE_HELP_MAP = {
|
|
|
270
425
|
"@angular-eslint/no-outputs-metadata-property": "Use `@Output() myEvent = new EventEmitter()` instead of `outputs: ['myEvent']` in the decorator metadata",
|
|
271
426
|
"@angular-eslint/prefer-standalone": "Add `standalone: true` to component: `@Component({ standalone: true, ... })`",
|
|
272
427
|
"@typescript-eslint/no-explicit-any": "Replace `any` with a specific type or `unknown` if the type is truly unknown",
|
|
273
|
-
"@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused"
|
|
428
|
+
"@typescript-eslint/no-unused-vars": "Remove the unused variable or prefix with `_` to indicate it's intentionally unused",
|
|
429
|
+
"@typescript-eslint/sort-keys": "Sort object keys alphabetically or by a consistent pattern",
|
|
430
|
+
"@angular-eslint/prefer-inject": "Use `inject(MyService)` instead of constructor injection: `constructor(private myService: MyService) {}`",
|
|
431
|
+
"@angular-eslint/no-pipe-impure": "Remove `pure: false` from pipe decorator or refactor to use a service",
|
|
432
|
+
"@angular-eslint/prefer-signals": "Replace observables with signals for simpler reactive state: `count = signal(0)`",
|
|
433
|
+
"@angular-eslint/prefer-signal-model": "Use signal-based input/output model: `count = model(0)` instead of `@Input() count: number`",
|
|
434
|
+
"@angular-eslint/no-uncalled-signals": "Call the signal getter to access its value: `this.count()` not `this.count`",
|
|
435
|
+
"@angular-eslint/component-max-inline-declarations": "Move templates/styles to separate files or use inline with caution — consider extracting when > 3 inline declarations",
|
|
436
|
+
"@angular-eslint/relative-url-prefix": "Use relative URLs (no leading slash) or ensure absolute URLs are intentional for security",
|
|
437
|
+
"@ngrx/contextual-action-creator": "Use `createActionGroup` or `createAction` with props for type-safe actions",
|
|
438
|
+
"@ngrx/no-multiple-actions-in-effects": "Split effect into multiple effects or use `mergeMap` with individual actions"
|
|
439
|
+
};
|
|
440
|
+
const detectPackagePresence = (packageJson) => {
|
|
441
|
+
const deps = {
|
|
442
|
+
...packageJson.dependencies,
|
|
443
|
+
...packageJson.devDependencies,
|
|
444
|
+
...packageJson.peerDependencies
|
|
445
|
+
};
|
|
446
|
+
return {
|
|
447
|
+
hasNgRx: Object.keys(deps).some((dep) => dep.startsWith("@ngrx/") && !dep.includes("store")) || Object.keys(deps).includes("@ngrx/store"),
|
|
448
|
+
hasAngularMaterial: Object.keys(deps).includes("@angular/material"),
|
|
449
|
+
hasSignals: true
|
|
450
|
+
};
|
|
274
451
|
};
|
|
275
|
-
const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
452
|
+
const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware, packagePresence) => {
|
|
276
453
|
const languageOptions = {
|
|
277
454
|
parser: tsEslint.parser,
|
|
278
455
|
parserOptions: {
|
|
@@ -284,20 +461,73 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
|
284
461
|
const angularRules = {
|
|
285
462
|
"@angular-eslint/component-class-suffix": "warn",
|
|
286
463
|
"@angular-eslint/directive-class-suffix": "warn",
|
|
464
|
+
"@angular-eslint/pipe-prefix": "warn",
|
|
287
465
|
"@angular-eslint/no-empty-lifecycle-method": "warn",
|
|
288
466
|
"@angular-eslint/use-lifecycle-interface": "warn",
|
|
289
|
-
"@angular-eslint/
|
|
467
|
+
"@angular-eslint/consistent-component-styles": "warn",
|
|
468
|
+
"@angular-eslint/sort-lifecycle-methods": "warn",
|
|
469
|
+
"@angular-eslint/use-component-selector": "warn",
|
|
470
|
+
"@angular-eslint/component-selector": "warn",
|
|
471
|
+
"@angular-eslint/directive-selector": "warn",
|
|
290
472
|
"@angular-eslint/prefer-on-push-component-change-detection": "warn",
|
|
291
473
|
"@angular-eslint/no-output-native": "error",
|
|
474
|
+
"@angular-eslint/no-pipe-impure": "warn",
|
|
475
|
+
"@angular-eslint/component-max-inline-declarations": "warn",
|
|
476
|
+
"@angular-eslint/use-pipe-transform-interface": "error",
|
|
292
477
|
"@angular-eslint/no-conflicting-lifecycle": "error",
|
|
293
478
|
"@angular-eslint/contextual-lifecycle": "error",
|
|
479
|
+
"@angular-eslint/contextual-decorator": "error",
|
|
480
|
+
"@angular-eslint/no-async-lifecycle-method": "error",
|
|
481
|
+
"@angular-eslint/no-duplicates-in-metadata-arrays": "error",
|
|
482
|
+
"@angular-eslint/no-lifecycle-call": "error",
|
|
483
|
+
"@angular-eslint/require-lifecycle-on-prototype": "error",
|
|
294
484
|
"@angular-eslint/no-forward-ref": "warn",
|
|
295
485
|
"@angular-eslint/no-input-rename": "warn",
|
|
296
486
|
"@angular-eslint/no-output-rename": "warn",
|
|
297
487
|
"@angular-eslint/no-inputs-metadata-property": "warn",
|
|
298
|
-
"@angular-eslint/no-outputs-metadata-property": "warn"
|
|
488
|
+
"@angular-eslint/no-outputs-metadata-property": "warn",
|
|
489
|
+
"@angular-eslint/no-queries-metadata-property": "warn",
|
|
490
|
+
"@angular-eslint/prefer-standalone": "warn",
|
|
491
|
+
"@angular-eslint/prefer-host-metadata-property": "warn",
|
|
492
|
+
"@angular-eslint/prefer-inject": "warn",
|
|
493
|
+
"@angular-eslint/prefer-output-emitter-ref": "warn",
|
|
494
|
+
"@angular-eslint/prefer-output-readonly": "warn",
|
|
495
|
+
"@angular-eslint/use-component-view-encapsulation": "warn",
|
|
496
|
+
"@angular-eslint/use-injectable-provided-in": "warn",
|
|
497
|
+
"@angular-eslint/no-attribute-decorator": "warn",
|
|
498
|
+
"@angular-eslint/no-input-prefix": "warn",
|
|
499
|
+
"@angular-eslint/no-output-on-prefix": "warn",
|
|
500
|
+
"@angular-eslint/relative-url-prefix": "error"
|
|
299
501
|
};
|
|
300
|
-
const
|
|
502
|
+
const signalsRules = packagePresence.hasSignals ? {
|
|
503
|
+
"@angular-eslint/prefer-signals": "warn",
|
|
504
|
+
"@angular-eslint/prefer-signal-model": "warn",
|
|
505
|
+
"@angular-eslint/no-uncalled-signals": "error"
|
|
506
|
+
} : {};
|
|
507
|
+
const tsRules = {
|
|
508
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
509
|
+
"@typescript-eslint/no-unused-vars": "warn",
|
|
510
|
+
"@typescript-eslint/sort-keys": "warn"
|
|
511
|
+
};
|
|
512
|
+
const ngrxRules = packagePresence.hasNgRx ? {
|
|
513
|
+
"@ngrx/contextual-action-creator": "warn",
|
|
514
|
+
"@ngrx/no-cyclic-action-creators": "error",
|
|
515
|
+
"@ngrx/no-discrete-actions": "warn",
|
|
516
|
+
"@ngrx/no-effect-decorator": "warn",
|
|
517
|
+
"@ngrx/no-effect-decorator-and-creator": "error",
|
|
518
|
+
"@ngrx/no-multiple-actions-in-effects": "error",
|
|
519
|
+
"@ngrx/no-reordering-in-effect-reducers": "error",
|
|
520
|
+
"@ngrx/no-typed-global-store": "error",
|
|
521
|
+
"@ngrx/on-function-explicit-return-type": "warn",
|
|
522
|
+
"@ngrx/prefix-selectors-with-namespace": "warn",
|
|
523
|
+
"@ngrx/require-middleware-selector": "error",
|
|
524
|
+
"@ngrx/select-style": "warn",
|
|
525
|
+
"@ngrx/use-consumer-selector": "warn"
|
|
526
|
+
} : {};
|
|
527
|
+
const materialRules = packagePresence.hasAngularMaterial ? {
|
|
528
|
+
"@angular/material/prefix-selector": "warn",
|
|
529
|
+
"@angular/material/no-conflicting-mixins": "error"
|
|
530
|
+
} : {};
|
|
301
531
|
return [{
|
|
302
532
|
files: ["**/*.ts"],
|
|
303
533
|
plugins: {
|
|
@@ -307,7 +537,10 @@ const buildEslintConfig = (hasTypeScript, tsconfigPath, useTypeAware) => {
|
|
|
307
537
|
languageOptions,
|
|
308
538
|
rules: {
|
|
309
539
|
...angularRules,
|
|
310
|
-
...tsRules
|
|
540
|
+
...tsRules,
|
|
541
|
+
...signalsRules,
|
|
542
|
+
...ngrxRules,
|
|
543
|
+
...materialRules
|
|
311
544
|
}
|
|
312
545
|
}];
|
|
313
546
|
};
|
|
@@ -330,24 +563,70 @@ const parsePluginAndRule = (ruleId) => {
|
|
|
330
563
|
};
|
|
331
564
|
};
|
|
332
565
|
const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) => {
|
|
333
|
-
if (includePaths !== void 0 && includePaths.length === 0) return
|
|
566
|
+
if (includePaths !== void 0 && includePaths.length === 0) return {
|
|
567
|
+
diagnostics: [],
|
|
568
|
+
errors: []
|
|
569
|
+
};
|
|
334
570
|
const tsconfigPath = hasTypeScript ? path.join(rootDirectory, "tsconfig.json") : null;
|
|
571
|
+
let packagePresence;
|
|
572
|
+
if (options?.frameworkInfo) packagePresence = {
|
|
573
|
+
hasNgRx: options.frameworkInfo.hasNgRx,
|
|
574
|
+
hasAngularMaterial: options.frameworkInfo.hasAngularMaterial,
|
|
575
|
+
hasSignals: options.frameworkInfo.hasSignals
|
|
576
|
+
};
|
|
577
|
+
else {
|
|
578
|
+
packagePresence = {
|
|
579
|
+
hasNgRx: false,
|
|
580
|
+
hasAngularMaterial: false,
|
|
581
|
+
hasSignals: true
|
|
582
|
+
};
|
|
583
|
+
try {
|
|
584
|
+
const packageJsonPath = path.join(rootDirectory, "package.json");
|
|
585
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
586
|
+
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
587
|
+
packagePresence = detectPackagePresence(JSON.parse(packageJsonContent));
|
|
588
|
+
}
|
|
589
|
+
} catch {}
|
|
590
|
+
}
|
|
335
591
|
const cacheRoot = path.join(rootDirectory, "node_modules", ".cache", "angular-doctor");
|
|
336
592
|
fs.mkdirSync(cacheRoot, { recursive: true });
|
|
337
593
|
const eslint = new ESLint({
|
|
338
594
|
cwd: rootDirectory,
|
|
339
595
|
overrideConfigFile: null,
|
|
340
|
-
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true),
|
|
596
|
+
overrideConfig: buildEslintConfig(hasTypeScript, tsconfigPath && fs.existsSync(tsconfigPath) ? tsconfigPath : null, options?.useTypeAware ?? true, packagePresence),
|
|
341
597
|
ignore: true,
|
|
342
598
|
cache: true,
|
|
343
599
|
cacheLocation: path.join(cacheRoot, ".eslintcache")
|
|
344
600
|
});
|
|
345
601
|
const patterns = includePaths ?? ["**/*.ts"];
|
|
346
602
|
let results;
|
|
603
|
+
const errors = [];
|
|
347
604
|
try {
|
|
348
605
|
results = await eslint.lintFiles(patterns);
|
|
349
|
-
} catch {
|
|
350
|
-
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
608
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
609
|
+
const filePathMatch = errorMessage.match(/^(.+?):\s* /);
|
|
610
|
+
const errorFilePath = filePathMatch ? path.relative(rootDirectory, filePathMatch[1]) : void 0;
|
|
611
|
+
errors.push({
|
|
612
|
+
message: errorMessage,
|
|
613
|
+
stack: errorStack,
|
|
614
|
+
filePath: errorFilePath
|
|
615
|
+
});
|
|
616
|
+
return {
|
|
617
|
+
diagnostics: [{
|
|
618
|
+
filePath: errorFilePath ?? "unknown",
|
|
619
|
+
plugin: "eslint",
|
|
620
|
+
rule: "parse-error",
|
|
621
|
+
severity: "error",
|
|
622
|
+
message: errorMessage,
|
|
623
|
+
help: "Fix the syntax error in this file. ESLint could not parse the file.",
|
|
624
|
+
line: 0,
|
|
625
|
+
column: 0,
|
|
626
|
+
category: "Parse Error"
|
|
627
|
+
}],
|
|
628
|
+
errors
|
|
629
|
+
};
|
|
351
630
|
}
|
|
352
631
|
const diagnostics = [];
|
|
353
632
|
for (const result of results) for (const message of result.messages) {
|
|
@@ -366,7 +645,10 @@ const runEslint = async (rootDirectory, hasTypeScript, includePaths, options) =>
|
|
|
366
645
|
category: resolveDiagnosticCategory(ruleKey)
|
|
367
646
|
});
|
|
368
647
|
}
|
|
369
|
-
return
|
|
648
|
+
return {
|
|
649
|
+
diagnostics,
|
|
650
|
+
errors
|
|
651
|
+
};
|
|
370
652
|
};
|
|
371
653
|
|
|
372
654
|
//#endregion
|
|
@@ -583,13 +865,20 @@ const diagnose = async (directory, options = {}) => {
|
|
|
583
865
|
const emptyDiagnostics = [];
|
|
584
866
|
const lintPromise = effectiveLint ? runEslint(resolvedDirectory, projectInfo.hasTypeScript, computedIncludePaths).catch((error) => {
|
|
585
867
|
console.error("Lint failed:", error);
|
|
586
|
-
return
|
|
587
|
-
|
|
868
|
+
return {
|
|
869
|
+
diagnostics: emptyDiagnostics,
|
|
870
|
+
errors: []
|
|
871
|
+
};
|
|
872
|
+
}) : Promise.resolve({
|
|
873
|
+
diagnostics: emptyDiagnostics,
|
|
874
|
+
errors: []
|
|
875
|
+
});
|
|
588
876
|
const deadCodePromise = effectiveDeadCode && !isDiffMode ? runKnip(resolvedDirectory).catch((error) => {
|
|
589
877
|
console.error("Dead code analysis failed:", error);
|
|
590
878
|
return emptyDiagnostics;
|
|
591
879
|
}) : Promise.resolve(emptyDiagnostics);
|
|
592
|
-
const [
|
|
880
|
+
const [lintResult, deadCodeDiagnostics] = await Promise.all([lintPromise, deadCodePromise]);
|
|
881
|
+
const lintDiagnostics = lintResult.diagnostics;
|
|
593
882
|
const diagnostics = combineDiagnostics(lintDiagnostics, deadCodeDiagnostics, userConfig);
|
|
594
883
|
const elapsedMilliseconds = performance.now() - startTime;
|
|
595
884
|
return {
|