@hatem427/code-guard-ci 3.3.0 → 3.4.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/config/angular.config.ts +29 -708
- package/config/guidelines.config.ts +5 -130
- package/config/nextjs.config.ts +27 -511
- package/config/react.config.ts +19 -614
- package/dist/config/angular.config.d.ts +5 -8
- package/dist/config/angular.config.d.ts.map +1 -1
- package/dist/config/angular.config.js +28 -666
- package/dist/config/angular.config.js.map +1 -1
- package/dist/config/guidelines.config.d.ts.map +1 -1
- package/dist/config/guidelines.config.js +5 -127
- package/dist/config/guidelines.config.js.map +1 -1
- package/dist/config/nextjs.config.d.ts +7 -9
- package/dist/config/nextjs.config.d.ts.map +1 -1
- package/dist/config/nextjs.config.js +26 -472
- package/dist/config/nextjs.config.js.map +1 -1
- package/dist/config/react.config.d.ts +4 -5
- package/dist/config/react.config.d.ts.map +1 -1
- package/dist/config/react.config.js +19 -586
- package/dist/config/react.config.js.map +1 -1
- package/dist/scripts/auto-fix.d.ts +0 -5
- package/dist/scripts/auto-fix.d.ts.map +1 -1
- package/dist/scripts/auto-fix.js +0 -5
- package/dist/scripts/auto-fix.js.map +1 -1
- package/dist/scripts/cli.js +211 -415
- package/dist/scripts/cli.js.map +1 -1
- package/dist/scripts/config-generators/ai-config-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/ai-config-generator.js +71 -15
- package/dist/scripts/config-generators/ai-config-generator.js.map +1 -1
- package/dist/scripts/config-generators/eslint-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/eslint-generator.js +13 -625
- package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
- package/dist/scripts/config-generators/index.d.ts +0 -1
- package/dist/scripts/config-generators/index.d.ts.map +1 -1
- package/dist/scripts/config-generators/index.js +1 -5
- package/dist/scripts/config-generators/index.js.map +1 -1
- package/dist/scripts/config-generators/typescript-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/typescript-generator.js +0 -33
- package/dist/scripts/config-generators/typescript-generator.js.map +1 -1
- package/dist/scripts/config-generators/vscode-generator.d.ts.map +1 -1
- package/dist/scripts/config-generators/vscode-generator.js +28 -171
- package/dist/scripts/config-generators/vscode-generator.js.map +1 -1
- package/dist/scripts/generate-pr-checklist.d.ts +0 -5
- package/dist/scripts/generate-pr-checklist.d.ts.map +1 -1
- package/dist/scripts/generate-pr-checklist.js +1 -6
- package/dist/scripts/generate-pr-checklist.js.map +1 -1
- package/dist/scripts/postinstall.js +0 -38
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/precommit-check.d.ts +0 -5
- package/dist/scripts/precommit-check.d.ts.map +1 -1
- package/dist/scripts/precommit-check.js +92 -149
- package/dist/scripts/precommit-check.js.map +1 -1
- package/dist/scripts/utils/naming-validator.d.ts.map +1 -1
- package/dist/scripts/utils/naming-validator.js +2 -96
- package/dist/scripts/utils/naming-validator.js.map +1 -1
- package/dist/scripts/utils/project-detector.d.ts +9 -12
- package/dist/scripts/utils/project-detector.d.ts.map +1 -1
- package/dist/scripts/utils/project-detector.js +11 -63
- package/dist/scripts/utils/project-detector.js.map +1 -1
- package/dist/scripts/utils/report-generator.js +5 -17
- package/dist/scripts/utils/report-generator.js.map +1 -1
- package/dist/scripts/utils/structure-validator.d.ts.map +1 -1
- package/dist/scripts/utils/structure-validator.js +0 -50
- package/dist/scripts/utils/structure-validator.js.map +1 -1
- package/package.json +1 -12
- package/scripts/auto-fix.ts +0 -5
- package/scripts/cli.ts +226 -451
- package/scripts/config-generators/ai-config-generator.ts +78 -28
- package/scripts/config-generators/eslint-generator.ts +7 -621
- package/scripts/config-generators/index.ts +0 -1
- package/scripts/config-generators/typescript-generator.ts +0 -36
- package/scripts/config-generators/vscode-generator.ts +40 -178
- package/scripts/generate-pr-checklist.ts +1 -6
- package/scripts/postinstall.ts +0 -38
- package/scripts/precommit-check.ts +113 -278
- package/scripts/utils/naming-validator.ts +2 -104
- package/scripts/utils/project-detector.ts +11 -78
- package/scripts/utils/report-generator.ts +5 -19
- package/scripts/utils/structure-validator.ts +0 -54
- package/config/fastify.config.ts +0 -326
- package/config/hono.config.ts +0 -331
- package/config/nestjs.config.ts +0 -500
- package/config/python.config.ts +0 -512
- package/templates/feature-doc-api.md +0 -101
- package/templates/feature-doc-backend.md +0 -114
- package/templates/feature-doc-service.md +0 -113
- package/templates/feature-doc-ui.md +0 -91
package/config/angular.config.ts
CHANGED
|
@@ -1,133 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================================
|
|
3
|
-
* angular.config.ts — Angular-specific coding rules
|
|
3
|
+
* angular.config.ts — Angular-specific coding rules
|
|
4
4
|
* ============================================================================
|
|
5
5
|
*
|
|
6
6
|
* Registers rules that only apply to Angular projects:
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* - Zoneless change detection
|
|
12
|
-
* - Server-side rendering (hydration)
|
|
13
|
-
* - RxJS best practices
|
|
7
|
+
* - ngOnInit misuse
|
|
8
|
+
* - Deprecated APIs (::ng-deep, CommonModule, ngClass, ngStyle)
|
|
9
|
+
* - Function calls in templates
|
|
10
|
+
* - RxJS best practices (async pipe, takeUntilDestroyed, signals)
|
|
14
11
|
*/
|
|
15
12
|
|
|
16
13
|
import { registerRules, Rule } from './guidelines.config';
|
|
17
14
|
|
|
18
15
|
const angularRules: Rule[] = [
|
|
19
|
-
// ── NEW: Control Flow Syntax (Angular 17+) ────────────────────────────
|
|
20
|
-
|
|
21
|
-
// ─────────────────────────────────────────
|
|
22
|
-
// RULE: angular-use-new-control-flow
|
|
23
|
-
// ROLE: Enforce Angular 17+ @if/@for/@switch syntax
|
|
24
|
-
// PURPOSE: Prevents legacy *ngIf, *ngFor, *ngSwitch usage — the new
|
|
25
|
-
// control flow is built-in, tree-shakeable, and has better
|
|
26
|
-
// type narrowing than structural directives.
|
|
27
|
-
// EXAMPLE:
|
|
28
|
-
// WRONG:
|
|
29
|
-
// <div *ngIf="isVisible">
|
|
30
|
-
// <li *ngFor="let item of items">{{ item }}</li>
|
|
31
|
-
// </div>
|
|
32
|
-
// RIGHT:
|
|
33
|
-
// @if (isVisible) {
|
|
34
|
-
// @for (item of items; track item.id) {
|
|
35
|
-
// <li>{{ item }}</li>
|
|
36
|
-
// }
|
|
37
|
-
// }
|
|
38
|
-
// ─────────────────────────────────────────
|
|
39
|
-
{
|
|
40
|
-
id: 'angular-use-new-control-flow',
|
|
41
|
-
label: 'Use @if, @for, @switch instead of *ngIf, *ngFor',
|
|
42
|
-
description:
|
|
43
|
-
'Angular 17+ introduces new control flow syntax: @if, @for, @switch, @let. They are more performant and syntactically cleaner than structural directives.',
|
|
44
|
-
severity: 'warning',
|
|
45
|
-
fileExtensions: ['html'],
|
|
46
|
-
pattern: null,
|
|
47
|
-
customCheck: (file) => {
|
|
48
|
-
const violations: Array<{ line: number | null; message: string }> = [];
|
|
49
|
-
const lines = file.lines;
|
|
50
|
-
|
|
51
|
-
for (let i = 0; i < lines.length; i++) {
|
|
52
|
-
if (/\*ngIf|\*ngFor|\*ngSwitch/.test(lines[i])) {
|
|
53
|
-
violations.push({
|
|
54
|
-
line: i + 1,
|
|
55
|
-
message:
|
|
56
|
-
'Found structural directive. Use new Angular control flow syntax for better performance and readability.',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return violations;
|
|
61
|
-
},
|
|
62
|
-
applicableTo: ['angular'],
|
|
63
|
-
category: 'Template Syntax',
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
// ─────────────────────────────────────────
|
|
67
|
-
// RULE: angular-use-let-syntax
|
|
68
|
-
// ROLE: Recommend Angular 17+ @let template variables
|
|
69
|
-
// PURPOSE: Simplifies complex template expressions by extracting them
|
|
70
|
-
// into named variables, improving readability and avoiding
|
|
71
|
-
// repeated evaluation.
|
|
72
|
-
// EXAMPLE:
|
|
73
|
-
// WRONG:
|
|
74
|
-
// {{ user?.profile?.name ? user.profile.name : 'Guest' }}
|
|
75
|
-
// {{ user?.profile?.name ? user.profile.name : 'Guest' }}
|
|
76
|
-
// RIGHT:
|
|
77
|
-
// @let displayName = user?.profile?.name ?? 'Guest';
|
|
78
|
-
// {{ displayName }}
|
|
79
|
-
// {{ displayName }}
|
|
80
|
-
// ─────────────────────────────────────────
|
|
81
|
-
{
|
|
82
|
-
id: 'angular-use-let-syntax',
|
|
83
|
-
label: 'Use @let for template variables',
|
|
84
|
-
description:
|
|
85
|
-
'Angular 17+ @let syntax enables simple variable binding in templates without getters. Example: @let myVar = expression;',
|
|
86
|
-
severity: 'info',
|
|
87
|
-
fileExtensions: ['html'],
|
|
88
|
-
pattern: null,
|
|
89
|
-
customCheck: (file) => {
|
|
90
|
-
// This is informational - flag templates with complex expressions that could use @let
|
|
91
|
-
const violations: Array<{ line: number | null; message: string }> = [];
|
|
92
|
-
const hasComplexExpr = /\{\{\s*[\w\.]+\s*\?\s*[\w\.]+\s*:\s*[\w\.]+\s*\}\}/g.test(file.content);
|
|
93
|
-
if (hasComplexExpr && !file.content.includes('@let')) {
|
|
94
|
-
violations.push({
|
|
95
|
-
line: null,
|
|
96
|
-
message: 'Consider using @let syntax to simplify complex template expressions.',
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return violations;
|
|
100
|
-
},
|
|
101
|
-
applicableTo: ['angular'],
|
|
102
|
-
category: 'Template Syntax',
|
|
103
|
-
},
|
|
104
|
-
|
|
105
16
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
106
17
|
|
|
107
|
-
// ─────────────────────────────────────────
|
|
108
|
-
// RULE: angular-no-ngoninit
|
|
109
|
-
// ROLE: Enforce modern Angular initialization patterns
|
|
110
|
-
// PURPOSE: ngOnInit was needed when constructor injection was the norm.
|
|
111
|
-
// With inject() + signals, field initializers and effect()
|
|
112
|
-
// cover most use cases without lifecycle hooks.
|
|
113
|
-
// EXAMPLE:
|
|
114
|
-
// WRONG:
|
|
115
|
-
// export class UserComponent implements OnInit {
|
|
116
|
-
// private userService = inject(UserService);
|
|
117
|
-
// user: User | null = null;
|
|
118
|
-
// ngOnInit() {
|
|
119
|
-
// this.user = this.userService.getUser();
|
|
120
|
-
// }
|
|
121
|
-
// }
|
|
122
|
-
// RIGHT:
|
|
123
|
-
// export class UserComponent {
|
|
124
|
-
// private userService = inject(UserService);
|
|
125
|
-
// user = signal<User | null>(null);
|
|
126
|
-
// constructor() {
|
|
127
|
-
// effect(() => this.user.set(this.userService.getUser()));
|
|
128
|
-
// }
|
|
129
|
-
// }
|
|
130
|
-
// ─────────────────────────────────────────
|
|
131
18
|
{
|
|
132
19
|
id: 'angular-no-ngoninit',
|
|
133
20
|
label: 'Avoid ngOnInit for simple initialization',
|
|
@@ -142,20 +29,6 @@ const angularRules: Rule[] = [
|
|
|
142
29
|
|
|
143
30
|
// ── Deprecated APIs ────────────────────────────────────────────────────
|
|
144
31
|
|
|
145
|
-
// ─────────────────────────────────────────
|
|
146
|
-
// RULE: angular-no-ng-deep
|
|
147
|
-
// ROLE: Block deprecated view encapsulation piercing
|
|
148
|
-
// PURPOSE: ::ng-deep breaks component encapsulation and is officially
|
|
149
|
-
// deprecated. It leads to unpredictable style leakage and
|
|
150
|
-
// makes refactoring dangerous.
|
|
151
|
-
// EXAMPLE:
|
|
152
|
-
// WRONG:
|
|
153
|
-
// :host ::ng-deep .mat-card { background: red; }
|
|
154
|
-
// RIGHT:
|
|
155
|
-
// :host { --card-bg: red; }
|
|
156
|
-
// /* In child component CSS: */
|
|
157
|
-
// .mat-card { background: var(--card-bg); }
|
|
158
|
-
// ─────────────────────────────────────────
|
|
159
32
|
{
|
|
160
33
|
id: 'angular-no-ng-deep',
|
|
161
34
|
label: 'No ::ng-deep',
|
|
@@ -168,25 +41,6 @@ const angularRules: Rule[] = [
|
|
|
168
41
|
category: 'Angular Deprecated APIs',
|
|
169
42
|
},
|
|
170
43
|
|
|
171
|
-
// ─────────────────────────────────────────
|
|
172
|
-
// RULE: angular-no-common-module
|
|
173
|
-
// ROLE: Enforce tree-shakeable standalone imports
|
|
174
|
-
// PURPOSE: CommonModule imports NgIf, NgFor, NgClass, AsyncPipe, and
|
|
175
|
-
// 30+ other directives/pipes. Standalone components should
|
|
176
|
-
// import ONLY what they use — or better, use @if/@for.
|
|
177
|
-
// EXAMPLE:
|
|
178
|
-
// WRONG:
|
|
179
|
-
// @Component({
|
|
180
|
-
// standalone: true,
|
|
181
|
-
// imports: [CommonModule],
|
|
182
|
-
// })
|
|
183
|
-
// RIGHT:
|
|
184
|
-
// @Component({
|
|
185
|
-
// standalone: true,
|
|
186
|
-
// imports: [NgClass, NgTemplateOutlet],
|
|
187
|
-
// })
|
|
188
|
-
// // Or even better — use @if/@for and import nothing
|
|
189
|
-
// ─────────────────────────────────────────
|
|
190
44
|
{
|
|
191
45
|
id: 'angular-no-common-module',
|
|
192
46
|
label: 'No CommonModule import in standalone components',
|
|
@@ -199,20 +53,6 @@ const angularRules: Rule[] = [
|
|
|
199
53
|
category: 'Angular Deprecated APIs',
|
|
200
54
|
},
|
|
201
55
|
|
|
202
|
-
// ─────────────────────────────────────────
|
|
203
|
-
// RULE: angular-no-ngclass
|
|
204
|
-
// ROLE: Enforce modern class binding patterns
|
|
205
|
-
// PURPOSE: [ngClass] requires importing NgClass and has complex object
|
|
206
|
-
// syntax. [class.name] binding is simpler, and Tailwind
|
|
207
|
-
// pseudo-classes (disabled:, hover:) are even cleaner.
|
|
208
|
-
// EXAMPLE:
|
|
209
|
-
// WRONG:
|
|
210
|
-
// <button [ngClass]="{ 'btn-active': isActive, 'btn-disabled': isDisabled }">
|
|
211
|
-
// RIGHT:
|
|
212
|
-
// <button [class.btn-active]="isActive()" [class.btn-disabled]="isDisabled()">
|
|
213
|
-
// <!-- Or with Tailwind: -->
|
|
214
|
-
// <button class="not-disabled:bg-blue-500 disabled:bg-gray-300">
|
|
215
|
-
// ─────────────────────────────────────────
|
|
216
56
|
{
|
|
217
57
|
id: 'angular-no-ngclass',
|
|
218
58
|
label: 'No [ngClass] — use [class] binding or Tailwind',
|
|
@@ -225,20 +65,6 @@ const angularRules: Rule[] = [
|
|
|
225
65
|
category: 'Angular Deprecated APIs',
|
|
226
66
|
},
|
|
227
67
|
|
|
228
|
-
// ─────────────────────────────────────────
|
|
229
|
-
// RULE: angular-no-ngstyle
|
|
230
|
-
// ROLE: Enforce modern style binding patterns
|
|
231
|
-
// PURPOSE: [ngStyle] is verbose and requires importing NgStyle.
|
|
232
|
-
// Direct [style.prop] binding or Tailwind utilities are
|
|
233
|
-
// simpler and more performant.
|
|
234
|
-
// EXAMPLE:
|
|
235
|
-
// WRONG:
|
|
236
|
-
// <div [ngStyle]="{ 'background-color': bgColor, 'font-size': fontSize + 'px' }">
|
|
237
|
-
// RIGHT:
|
|
238
|
-
// <div [style.background-color]="bgColor" [style.font-size.rem]="fontSize">
|
|
239
|
-
// <!-- Or with Tailwind: -->
|
|
240
|
-
// <div class="bg-primary text-base">
|
|
241
|
-
// ─────────────────────────────────────────
|
|
242
68
|
{
|
|
243
69
|
id: 'angular-no-ngstyle',
|
|
244
70
|
label: 'No [ngStyle] — use [style] binding or Tailwind',
|
|
@@ -253,22 +79,6 @@ const angularRules: Rule[] = [
|
|
|
253
79
|
|
|
254
80
|
// ── Template best practices ────────────────────────────────────────────
|
|
255
81
|
|
|
256
|
-
// ─────────────────────────────────────────
|
|
257
|
-
// RULE: angular-no-function-in-template
|
|
258
|
-
// ROLE: Block function calls in template bindings
|
|
259
|
-
// PURPOSE: Functions in templates execute on EVERY change detection
|
|
260
|
-
// cycle, causing severe performance degradation. Signals and
|
|
261
|
-
// computed() are memoized and only re-evaluate when deps change.
|
|
262
|
-
// EXAMPLE:
|
|
263
|
-
// WRONG:
|
|
264
|
-
// <span>{{ getFullName() }}</span>
|
|
265
|
-
// <div [class.active]="isItemActive(item)">
|
|
266
|
-
// RIGHT:
|
|
267
|
-
// fullName = computed(() => this.firstName() + ' ' + this.lastName());
|
|
268
|
-
// <span>{{ fullName() }}</span>
|
|
269
|
-
// <!-- Or use a pipe: -->
|
|
270
|
-
// <span>{{ user | fullName }}</span>
|
|
271
|
-
// ─────────────────────────────────────────
|
|
272
82
|
{
|
|
273
83
|
id: 'angular-no-function-in-template',
|
|
274
84
|
label: 'No function calls in templates',
|
|
@@ -280,6 +90,16 @@ const angularRules: Rule[] = [
|
|
|
280
90
|
customCheck: (file) => {
|
|
281
91
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
282
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Regex explanation:
|
|
95
|
+
* Match patterns like {{ someFunc() }} or (click)="handler()" but EXCLUDE:
|
|
96
|
+
* - Event handlers: (click)="...", (submit)="...", on*="..."
|
|
97
|
+
* - Known safe pipes: | async, | date, | json, etc.
|
|
98
|
+
* - Structural directives: *ngIf, *ngFor, @if, @for
|
|
99
|
+
*
|
|
100
|
+
* We look for interpolation bindings {{ expr() }} and property bindings [prop]="expr()"
|
|
101
|
+
* that contain function calls which are NOT event bindings.
|
|
102
|
+
*/
|
|
283
103
|
const interpolationRegex = /\{\{[^}]*\w+\s*\([^)]*\)[^}]*\}\}/g;
|
|
284
104
|
const propertyBindingRegex = /\[(?!click|submit|keyup|keydown|change|input|focus|blur)\w+\]\s*=\s*"[^"]*\w+\s*\([^)]*\)[^"]*"/g;
|
|
285
105
|
|
|
@@ -289,6 +109,7 @@ const angularRules: Rule[] = [
|
|
|
289
109
|
// Check interpolation bindings {{ func() }}
|
|
290
110
|
interpolationRegex.lastIndex = 0;
|
|
291
111
|
if (interpolationRegex.test(line)) {
|
|
112
|
+
// Exclude pipe usages like {{ value | pipeName }}
|
|
292
113
|
const trimmed = line.trim();
|
|
293
114
|
if (!trimmed.includes(' | ')) {
|
|
294
115
|
violations.push({
|
|
@@ -316,24 +137,6 @@ const angularRules: Rule[] = [
|
|
|
316
137
|
|
|
317
138
|
// ── RxJS best practices ────────────────────────────────────────────────
|
|
318
139
|
|
|
319
|
-
// ─────────────────────────────────────────
|
|
320
|
-
// RULE: angular-use-async-pipe
|
|
321
|
-
// ROLE: Prevent manual subscription memory leaks
|
|
322
|
-
// PURPOSE: Manual .subscribe() in components is the #1 cause of memory
|
|
323
|
-
// leaks in Angular apps. The async pipe or toSignal()
|
|
324
|
-
// auto-unsubscribes when the component is destroyed.
|
|
325
|
-
// EXAMPLE:
|
|
326
|
-
// WRONG:
|
|
327
|
-
// ngOnInit() {
|
|
328
|
-
// this.userService.getUser().subscribe(user => this.user = user);
|
|
329
|
-
// }
|
|
330
|
-
// RIGHT:
|
|
331
|
-
// user$ = this.userService.getUser();
|
|
332
|
-
// <!-- In template: -->
|
|
333
|
-
// {{ user$ | async }}
|
|
334
|
-
// <!-- Or convert to signal: -->
|
|
335
|
-
// user = toSignal(this.userService.getUser());
|
|
336
|
-
// ─────────────────────────────────────────
|
|
337
140
|
{
|
|
338
141
|
id: 'angular-use-async-pipe',
|
|
339
142
|
label: 'Use async pipe or toSignal() instead of manual subscribe',
|
|
@@ -346,26 +149,6 @@ const angularRules: Rule[] = [
|
|
|
346
149
|
category: 'RxJS',
|
|
347
150
|
},
|
|
348
151
|
|
|
349
|
-
// ─────────────────────────────────────────
|
|
350
|
-
// RULE: angular-use-takeuntildestroyed
|
|
351
|
-
// ROLE: Enforce subscription cleanup in Angular classes
|
|
352
|
-
// PURPOSE: If subscribe() is unavoidable, takeUntilDestroyed() from
|
|
353
|
-
// @angular/core/rxjs-interop handles cleanup automatically
|
|
354
|
-
// tied to the component's DestroyRef lifecycle.
|
|
355
|
-
// EXAMPLE:
|
|
356
|
-
// WRONG:
|
|
357
|
-
// ngOnInit() {
|
|
358
|
-
// this.data$.subscribe(d => this.process(d));
|
|
359
|
-
// }
|
|
360
|
-
// // Memory leak — never unsubscribes!
|
|
361
|
-
// RIGHT:
|
|
362
|
-
// private destroyRef = inject(DestroyRef);
|
|
363
|
-
// ngOnInit() {
|
|
364
|
-
// this.data$.pipe(
|
|
365
|
-
// takeUntilDestroyed(this.destroyRef)
|
|
366
|
-
// ).subscribe(d => this.process(d));
|
|
367
|
-
// }
|
|
368
|
-
// ─────────────────────────────────────────
|
|
369
152
|
{
|
|
370
153
|
id: 'angular-use-takeuntildestroyed',
|
|
371
154
|
label: 'Use takeUntilDestroyed() for subscriptions',
|
|
@@ -377,6 +160,7 @@ const angularRules: Rule[] = [
|
|
|
377
160
|
customCheck: (file) => {
|
|
378
161
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
379
162
|
|
|
163
|
+
// Only check Angular component/directive/service files
|
|
380
164
|
if (
|
|
381
165
|
!file.content.includes('@Component') &&
|
|
382
166
|
!file.content.includes('@Directive') &&
|
|
@@ -403,20 +187,6 @@ const angularRules: Rule[] = [
|
|
|
403
187
|
category: 'RxJS',
|
|
404
188
|
},
|
|
405
189
|
|
|
406
|
-
// ─────────────────────────────────────────
|
|
407
|
-
// RULE: angular-prefer-signals
|
|
408
|
-
// ROLE: Recommend migration from BehaviorSubject to Signals
|
|
409
|
-
// PURPOSE: Angular Signals (signal(), computed(), effect()) are simpler,
|
|
410
|
-
// synchronous, and deeply integrated with change detection.
|
|
411
|
-
// BehaviorSubject is async and requires subscription management.
|
|
412
|
-
// EXAMPLE:
|
|
413
|
-
// WRONG:
|
|
414
|
-
// count$ = new BehaviorSubject<number>(0);
|
|
415
|
-
// increment() { this.count$.next(this.count$.value + 1); }
|
|
416
|
-
// RIGHT:
|
|
417
|
-
// count = signal(0);
|
|
418
|
-
// increment() { this.count.update(v => v + 1); }
|
|
419
|
-
// ─────────────────────────────────────────
|
|
420
190
|
{
|
|
421
191
|
id: 'angular-prefer-signals',
|
|
422
192
|
label: 'Consider using Angular Signals',
|
|
@@ -426,6 +196,7 @@ const angularRules: Rule[] = [
|
|
|
426
196
|
fileExtensions: ['ts'],
|
|
427
197
|
pattern: null,
|
|
428
198
|
customCheck: (file) => {
|
|
199
|
+
// Only flag if the component uses BehaviorSubject but no signals
|
|
429
200
|
const hasBehaviorSubject = file.content.includes('BehaviorSubject');
|
|
430
201
|
const hasSignal = file.content.includes('signal(') || file.content.includes('computed(');
|
|
431
202
|
|
|
@@ -445,28 +216,8 @@ const angularRules: Rule[] = [
|
|
|
445
216
|
category: 'Angular Signals',
|
|
446
217
|
},
|
|
447
218
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
// ROLE: Enforce inject() function over constructor DI
|
|
451
|
-
// PURPOSE: inject() enables dependency injection at field declaration
|
|
452
|
-
// level, works with standalone components, and doesn't require
|
|
453
|
-
// constructor parameter boilerplate.
|
|
454
|
-
// EXAMPLE:
|
|
455
|
-
// WRONG:
|
|
456
|
-
// export class UserComponent {
|
|
457
|
-
// constructor(
|
|
458
|
-
// private userService: UserService,
|
|
459
|
-
// private router: Router,
|
|
460
|
-
// private http: HttpClient
|
|
461
|
-
// ) {}
|
|
462
|
-
// }
|
|
463
|
-
// RIGHT:
|
|
464
|
-
// export class UserComponent {
|
|
465
|
-
// private userService = inject(UserService);
|
|
466
|
-
// private router = inject(Router);
|
|
467
|
-
// private http = inject(HttpClient);
|
|
468
|
-
// }
|
|
469
|
-
// ─────────────────────────────────────────
|
|
219
|
+
// ── Dependency Injection ────────────────────────────────────────────────
|
|
220
|
+
|
|
470
221
|
{
|
|
471
222
|
id: 'angular-prefer-inject',
|
|
472
223
|
label: 'Use inject() instead of constructor injection',
|
|
@@ -478,6 +229,7 @@ const angularRules: Rule[] = [
|
|
|
478
229
|
customCheck: (file) => {
|
|
479
230
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
480
231
|
|
|
232
|
+
// Only check Angular component/directive/service files
|
|
481
233
|
if (
|
|
482
234
|
!file.content.includes('@Component') &&
|
|
483
235
|
!file.content.includes('@Directive') &&
|
|
@@ -486,9 +238,12 @@ const angularRules: Rule[] = [
|
|
|
486
238
|
return [];
|
|
487
239
|
}
|
|
488
240
|
|
|
241
|
+
// Check for constructor with dependency injection parameters
|
|
242
|
+
// Matches: constructor(private service: Type) or constructor(public readonly http: HttpClient)
|
|
489
243
|
const constructorPattern = /constructor\s*\(\s*(?:private|public|protected|readonly|\s)+\w+\s*:\s*\w+/;
|
|
490
244
|
|
|
491
245
|
if (constructorPattern.test(file.content)) {
|
|
246
|
+
// Find the line number
|
|
492
247
|
for (let i = 0; i < file.lines.length; i++) {
|
|
493
248
|
if (constructorPattern.test(file.lines[i])) {
|
|
494
249
|
violations.push({
|
|
@@ -507,248 +262,8 @@ const angularRules: Rule[] = [
|
|
|
507
262
|
category: 'Angular Dependency Injection',
|
|
508
263
|
},
|
|
509
264
|
|
|
510
|
-
// ── NEW: Signals & Signal-based Features (Angular 16+, essential in v21) ────
|
|
511
|
-
|
|
512
|
-
// ─────────────────────────────────────────
|
|
513
|
-
// RULE: angular-use-signals
|
|
514
|
-
// ROLE: Enforce Signal-based reactive state
|
|
515
|
-
// PURPOSE: Signals are the future of Angular reactivity. They are
|
|
516
|
-
// synchronous, automatically tracked by change detection,
|
|
517
|
-
// and eliminate the need for most RxJS subscriptions.
|
|
518
|
-
// EXAMPLE:
|
|
519
|
-
// WRONG:
|
|
520
|
-
// import { BehaviorSubject } from 'rxjs';
|
|
521
|
-
// counter = new BehaviorSubject(0);
|
|
522
|
-
// counter.next(counter.value + 1);
|
|
523
|
-
// RIGHT:
|
|
524
|
-
// import { signal } from '@angular/core';
|
|
525
|
-
// counter = signal(0);
|
|
526
|
-
// counter.update(val => val + 1);
|
|
527
|
-
// ─────────────────────────────────────────
|
|
528
|
-
{
|
|
529
|
-
id: 'angular-use-signals',
|
|
530
|
-
label: 'Use Signals for reactive state',
|
|
531
|
-
description:
|
|
532
|
-
'Angular Signals replace RxJS in many cases. Use signal(), computed(), and effect() for simpler, more performant reactivity. Consider migrating from BehaviorSubject.',
|
|
533
|
-
severity: 'warning',
|
|
534
|
-
fileExtensions: ['ts'],
|
|
535
|
-
pattern: null,
|
|
536
|
-
customCheck: (file) => {
|
|
537
|
-
const violations: Array<{ line: number | null; message: string }> = [];
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
!file.content.includes('@Component') &&
|
|
541
|
-
!file.content.includes('@Injectable') &&
|
|
542
|
-
!file.content.includes('export')
|
|
543
|
-
) {
|
|
544
|
-
return [];
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const hasBehaviorSubject = /BehaviorSubject|Subject/.test(file.content);
|
|
548
|
-
const hasSignal = /signal\(|computed\(|effect\(/.test(file.content);
|
|
549
|
-
const hasRxJS = /rxjs|Observable<|\.pipe\(/.test(file.content);
|
|
550
|
-
|
|
551
|
-
if (hasBehaviorSubject && !hasSignal && !hasRxJS) {
|
|
552
|
-
violations.push({
|
|
553
|
-
line: null,
|
|
554
|
-
message:
|
|
555
|
-
'BehaviorSubject detected. Migrate to Angular Signals for cleaner reactive state.\n\nWhy: Signals are simpler, more performant, and better integrated with Angular\'s change detection.\n\n- import { BehaviorSubject } from "rxjs";\n- counter = new BehaviorSubject(0);\n- counter.next(counter.value + 1);\n\n+ import { signal } from "@angular/core";\n+ counter = signal(0);\n+ counter.update(val => val + 1);',
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return violations;
|
|
560
|
-
},
|
|
561
|
-
applicableTo: ['angular'],
|
|
562
|
-
category: 'Angular Signals',
|
|
563
|
-
},
|
|
564
|
-
|
|
565
|
-
// ─────────────────────────────────────────
|
|
566
|
-
// RULE: angular-use-computed
|
|
567
|
-
// ROLE: Enforce computed() for derived state
|
|
568
|
-
// PURPOSE: computed() is memoized and only recalculates when its
|
|
569
|
-
// signal dependencies change. Getters re-evaluate on every
|
|
570
|
-
// change detection cycle.
|
|
571
|
-
// EXAMPLE:
|
|
572
|
-
// WRONG:
|
|
573
|
-
// get totalPrice(): number {
|
|
574
|
-
// return this.items.reduce((sum, item) => sum + item.price, 0);
|
|
575
|
-
// }
|
|
576
|
-
// RIGHT:
|
|
577
|
-
// totalPrice = computed(() =>
|
|
578
|
-
// this.items().reduce((sum, item) => sum + item.price, 0)
|
|
579
|
-
// );
|
|
580
|
-
// ─────────────────────────────────────────
|
|
581
|
-
{
|
|
582
|
-
id: 'angular-use-computed',
|
|
583
|
-
label: 'Use computed() for derived state',
|
|
584
|
-
description:
|
|
585
|
-
'Instead of getters or pipes, use computed() from @angular/core to create derived signal state. Better type safety and automatic memoization.',
|
|
586
|
-
severity: 'info',
|
|
587
|
-
fileExtensions: ['ts'],
|
|
588
|
-
pattern: null,
|
|
589
|
-
customCheck: (file) => {
|
|
590
|
-
const violations: Array<{ line: number | null; message: string }> = [];
|
|
591
|
-
|
|
592
|
-
if (/get\s+\w+\s*\(\s*\).*\{[\s\S]*?return.*signal|this\.\w+\(\)/.test(file.content)) {
|
|
593
|
-
violations.push({
|
|
594
|
-
line: null,
|
|
595
|
-
message:
|
|
596
|
-
'Found getter function. Use computed() from signals for automatic memoization and better performance.',
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
return violations;
|
|
600
|
-
},
|
|
601
|
-
applicableTo: ['angular'],
|
|
602
|
-
category: 'Angular Signals',
|
|
603
|
-
},
|
|
604
|
-
|
|
605
|
-
// ─────────────────────────────────────────
|
|
606
|
-
// RULE: angular-use-effect
|
|
607
|
-
// ROLE: Enforce effect() for signal-driven side effects
|
|
608
|
-
// PURPOSE: effect() automatically tracks signal dependencies and
|
|
609
|
-
// re-runs when they change, replacing ngOnInit-based
|
|
610
|
-
// subscription patterns with declarative reactivity.
|
|
611
|
-
// EXAMPLE:
|
|
612
|
-
// WRONG:
|
|
613
|
-
// ngOnInit() {
|
|
614
|
-
// this.data$.subscribe(data => this.processData(data));
|
|
615
|
-
// }
|
|
616
|
-
// RIGHT:
|
|
617
|
-
// constructor() {
|
|
618
|
-
// effect(() => this.processData(this.data()));
|
|
619
|
-
// }
|
|
620
|
-
// ─────────────────────────────────────────
|
|
621
|
-
{
|
|
622
|
-
id: 'angular-use-effect',
|
|
623
|
-
label: 'Use effect() for side effects instead of ngOnInit',
|
|
624
|
-
description:
|
|
625
|
-
'Use effect() from @angular/core for side effects that depend on signals. It automatically tracks dependencies and runs when signals change.',
|
|
626
|
-
severity: 'info',
|
|
627
|
-
fileExtensions: ['ts'],
|
|
628
|
-
pattern: null,
|
|
629
|
-
customCheck: (file) => {
|
|
630
|
-
const violations: Array<{ line: number | null; message: string }> = [];
|
|
631
|
-
|
|
632
|
-
if (/ngOnInit\s*\(\)[\s\S]*?\{[\s\S]*?(?:subscribe|addEventListener|setTimeout)/.test(file.content)) {
|
|
633
|
-
violations.push({
|
|
634
|
-
line: null,
|
|
635
|
-
message:
|
|
636
|
-
'Found ngOnInit with side effects. Use effect() from signals for cleaner dependency tracking.',
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
return violations;
|
|
640
|
-
},
|
|
641
|
-
applicableTo: ['angular'],
|
|
642
|
-
category: 'Angular Signals',
|
|
643
|
-
},
|
|
644
|
-
|
|
645
|
-
// ─────────────────────────────────────────
|
|
646
|
-
// RULE: angular-use-input-output-functions
|
|
647
|
-
// ROLE: Enforce signal-based input/output API
|
|
648
|
-
// PURPOSE: input(), output(), and model() functions provide better
|
|
649
|
-
// null-safety, type inference, and signal integration than
|
|
650
|
-
// @Input/@Output decorators.
|
|
651
|
-
// EXAMPLE:
|
|
652
|
-
// WRONG:
|
|
653
|
-
// @Input() title: string = '';
|
|
654
|
-
// @Output() clicked = new EventEmitter<void>();
|
|
655
|
-
// RIGHT:
|
|
656
|
-
// title = input<string>('');
|
|
657
|
-
// clicked = output<void>();
|
|
658
|
-
// // Two-way binding:
|
|
659
|
-
// name = model<string>('');
|
|
660
|
-
// ─────────────────────────────────────────
|
|
661
|
-
{
|
|
662
|
-
id: 'angular-use-input-output-functions',
|
|
663
|
-
label: 'Use input(), output(), model() functions',
|
|
664
|
-
description:
|
|
665
|
-
'Angular 17+ supports input(), output(), and model() functions instead of @Input/@Output decorators. They provide better null-safety and type inference.',
|
|
666
|
-
severity: 'warning',
|
|
667
|
-
fileExtensions: ['ts'],
|
|
668
|
-
pattern: /@Input|@Output/g,
|
|
669
|
-
applicableTo: ['angular'],
|
|
670
|
-
category: 'Angular Components',
|
|
671
|
-
},
|
|
672
|
-
|
|
673
|
-
// ─────────────────────────────────────────
|
|
674
|
-
// RULE: angular-signal-based-viewchild
|
|
675
|
-
// ROLE: Recommend signal-based queries
|
|
676
|
-
// PURPOSE: viewChild() and viewChildren() return signals, eliminating
|
|
677
|
-
// the need for AfterViewInit lifecycle hooks and providing
|
|
678
|
-
// automatic change detection integration.
|
|
679
|
-
// EXAMPLE:
|
|
680
|
-
// WRONG:
|
|
681
|
-
// @ViewChild('myInput') myInput!: ElementRef;
|
|
682
|
-
// ngAfterViewInit() { this.myInput.nativeElement.focus(); }
|
|
683
|
-
// RIGHT:
|
|
684
|
-
// myInput = viewChild<ElementRef>('myInput');
|
|
685
|
-
// constructor() {
|
|
686
|
-
// afterNextRender(() => this.myInput()?.nativeElement.focus());
|
|
687
|
-
// }
|
|
688
|
-
// ─────────────────────────────────────────
|
|
689
|
-
{
|
|
690
|
-
id: 'angular-signal-based-viewchild',
|
|
691
|
-
label: 'Use signal-based @ViewChild where applicable',
|
|
692
|
-
description:
|
|
693
|
-
'For @ViewChild queries, consider using signal queries with viewChild() function for better type safety and automatic change detection.',
|
|
694
|
-
severity: 'info',
|
|
695
|
-
fileExtensions: ['ts'],
|
|
696
|
-
pattern: /@ViewChild\s*\(|@ViewChildren\s*\(/g,
|
|
697
|
-
applicableTo: ['angular'],
|
|
698
|
-
category: 'Angular Templates',
|
|
699
|
-
},
|
|
700
|
-
|
|
701
|
-
// ── NEW: Zoneless Change Detection (Angular 18+, recommended in v21) ───
|
|
702
|
-
|
|
703
|
-
// ─────────────────────────────────────────
|
|
704
|
-
// RULE: angular-consider-zoneless
|
|
705
|
-
// ROLE: Recommend zoneless change detection
|
|
706
|
-
// PURPOSE: Zone.js patches every async API (setTimeout, Promise, XHR)
|
|
707
|
-
// adding ~100KB to the bundle. Signals make Zone.js optional
|
|
708
|
-
// since they notify Angular directly of changes.
|
|
709
|
-
// EXAMPLE:
|
|
710
|
-
// WRONG:
|
|
711
|
-
// // app.config.ts (with Zone.js)
|
|
712
|
-
// provideZoneChangeDetection({ eventCoalescing: true })
|
|
713
|
-
// RIGHT:
|
|
714
|
-
// // app.config.ts (zoneless)
|
|
715
|
-
// provideExperimentalZonelessChangeDetection()
|
|
716
|
-
// // angular.json
|
|
717
|
-
// "polyfills": [] // remove zone.js
|
|
718
|
-
// ─────────────────────────────────────────
|
|
719
|
-
{
|
|
720
|
-
id: 'angular-consider-zoneless',
|
|
721
|
-
label: 'Consider zoneless change detection',
|
|
722
|
-
description:
|
|
723
|
-
'Angular 18+ supports zoneless applications, which improves performance by removing Zone.js. Set "zone": "none" in component config when possible.',
|
|
724
|
-
severity: 'info',
|
|
725
|
-
fileExtensions: ['ts'],
|
|
726
|
-
pattern: null,
|
|
727
|
-
applicableTo: ['angular'],
|
|
728
|
-
category: 'Performance',
|
|
729
|
-
},
|
|
730
|
-
|
|
731
265
|
// ── Performance Rules ──────────────────────────────────────────────────
|
|
732
266
|
|
|
733
|
-
// ─────────────────────────────────────────
|
|
734
|
-
// RULE: angular-onpush-change-detection
|
|
735
|
-
// ROLE: Recommend OnPush change detection strategy
|
|
736
|
-
// PURPOSE: Default change detection runs on EVERY browser event.
|
|
737
|
-
// OnPush only checks when @Input references change or signals
|
|
738
|
-
// update, dramatically reducing unnecessary checks.
|
|
739
|
-
// EXAMPLE:
|
|
740
|
-
// WRONG:
|
|
741
|
-
// @Component({
|
|
742
|
-
// selector: 'app-user-card',
|
|
743
|
-
// templateUrl: './user-card.component.html',
|
|
744
|
-
// })
|
|
745
|
-
// RIGHT:
|
|
746
|
-
// @Component({
|
|
747
|
-
// selector: 'app-user-card',
|
|
748
|
-
// templateUrl: './user-card.component.html',
|
|
749
|
-
// changeDetection: ChangeDetectionStrategy.OnPush,
|
|
750
|
-
// })
|
|
751
|
-
// ─────────────────────────────────────────
|
|
752
267
|
{
|
|
753
268
|
id: 'angular-onpush-change-detection',
|
|
754
269
|
label: 'Consider OnPush change detection',
|
|
@@ -761,156 +276,18 @@ const angularRules: Rule[] = [
|
|
|
761
276
|
category: 'Performance',
|
|
762
277
|
},
|
|
763
278
|
|
|
764
|
-
// ─────────────────────────────────────────
|
|
765
|
-
// RULE: angular-trackby-for-loops
|
|
766
|
-
// ROLE: Enforce proper DOM identity tracking in loops
|
|
767
|
-
// PURPOSE: Without track, Angular destroys and recreates ALL DOM nodes
|
|
768
|
-
// on every list change. track item.id lets Angular reuse
|
|
769
|
-
// existing DOM nodes, improving performance 10-100x for lists.
|
|
770
|
-
// EXAMPLE:
|
|
771
|
-
// WRONG:
|
|
772
|
-
// <li *ngFor="let item of items">{{ item.name }}</li>
|
|
773
|
-
// @for (item of items) { <li>{{ item.name }}</li> }
|
|
774
|
-
// RIGHT:
|
|
775
|
-
// @for (item of items; track item.id) {
|
|
776
|
-
// <li>{{ item.name }}</li>
|
|
777
|
-
// }
|
|
778
|
-
// ─────────────────────────────────────────
|
|
779
279
|
{
|
|
780
|
-
id: 'angular-trackby-
|
|
781
|
-
label: 'Use trackBy with *ngFor
|
|
280
|
+
id: 'angular-trackby-ngfor',
|
|
281
|
+
label: 'Use trackBy with *ngFor',
|
|
782
282
|
description:
|
|
783
|
-
'
|
|
283
|
+
'Always provide trackBy function for *ngFor to prevent unnecessary DOM re-renders when data changes.',
|
|
784
284
|
severity: 'warning',
|
|
785
285
|
fileExtensions: ['html'],
|
|
786
|
-
pattern: /\*ngFor\s*=\s*"[^"]*"(?![^<]*trackBy)
|
|
286
|
+
pattern: /\*ngFor\s*=\s*"[^"]*"(?![^<]*trackBy)/g,
|
|
787
287
|
applicableTo: ['angular'],
|
|
788
288
|
category: 'Performance',
|
|
789
289
|
},
|
|
790
290
|
|
|
791
|
-
// ─────────────────────────────────────────
|
|
792
|
-
// RULE: angular-no-unused-imports
|
|
793
|
-
// ROLE: Flag unnecessary RxJS imports when Signals are available
|
|
794
|
-
// PURPOSE: Importing Observable/Subject when Signals could be used
|
|
795
|
-
// adds unnecessary bundle weight and cognitive overhead.
|
|
796
|
-
// Prefer signals for simple reactive state.
|
|
797
|
-
// EXAMPLE:
|
|
798
|
-
// WRONG:
|
|
799
|
-
// import { Observable, BehaviorSubject } from 'rxjs';
|
|
800
|
-
// data$ = new BehaviorSubject<string[]>([]);
|
|
801
|
-
// RIGHT:
|
|
802
|
-
// import { signal } from '@angular/core';
|
|
803
|
-
// data = signal<string[]>([]);
|
|
804
|
-
// ─────────────────────────────────────────
|
|
805
|
-
{
|
|
806
|
-
id: 'angular-no-unused-imports',
|
|
807
|
-
label: 'Avoid importing RxJS operators not used',
|
|
808
|
-
description:
|
|
809
|
-
'Remove unused RxJS imports (Observable, Subject). Prefer Signals if you are not heavily using RxJS pipelines.',
|
|
810
|
-
severity: 'info',
|
|
811
|
-
fileExtensions: ['ts'],
|
|
812
|
-
pattern: /import\s*\{[^}]*Observable[^}]*\}\s*from\s+['"]rxjs['"]/g,
|
|
813
|
-
applicableTo: ['angular'],
|
|
814
|
-
category: 'Best Practices',
|
|
815
|
-
},
|
|
816
|
-
|
|
817
|
-
// ── NEW: Server-Side Rendering Hydration (Angular 17+) ────────────────
|
|
818
|
-
|
|
819
|
-
// ─────────────────────────────────────────
|
|
820
|
-
// RULE: angular-ssr-hydration
|
|
821
|
-
// ROLE: Block direct DOM access in SSR-compatible components
|
|
822
|
-
// PURPOSE: document.*, window.*, localStorage do not exist on the
|
|
823
|
-
// server. Direct usage crashes SSR pre-rendering. Always
|
|
824
|
-
// guard with isPlatformBrowser() or use afterNextRender().
|
|
825
|
-
// EXAMPLE:
|
|
826
|
-
// WRONG:
|
|
827
|
-
// ngOnInit() {
|
|
828
|
-
// const token = localStorage.getItem('token');
|
|
829
|
-
// document.title = 'My App';
|
|
830
|
-
// }
|
|
831
|
-
// RIGHT:
|
|
832
|
-
// constructor() {
|
|
833
|
-
// afterNextRender(() => {
|
|
834
|
-
// const token = localStorage.getItem('token');
|
|
835
|
-
// document.title = 'My App';
|
|
836
|
-
// });
|
|
837
|
-
// }
|
|
838
|
-
// ─────────────────────────────────────────
|
|
839
|
-
{
|
|
840
|
-
id: 'angular-ssr-hydration',
|
|
841
|
-
label: 'Ensure SSR hydration compatibility',
|
|
842
|
-
description:
|
|
843
|
-
'When using SSR, ensure components are hydration-compatible. Avoid direct DOM manipulation (document.*, window.*) in server contexts.',
|
|
844
|
-
severity: 'warning',
|
|
845
|
-
fileExtensions: ['ts'],
|
|
846
|
-
pattern: /(?:document\.|window\.|localStorage|sessionStorage)/g,
|
|
847
|
-
applicableTo: ['angular'],
|
|
848
|
-
category: 'Server-Side Rendering',
|
|
849
|
-
},
|
|
850
|
-
|
|
851
|
-
// ─────────────────────────────────────────
|
|
852
|
-
// RULE: angular-ssr-safe-initialization
|
|
853
|
-
// ROLE: Enforce browser API guards for SSR safety
|
|
854
|
-
// PURPOSE: Browser-only code (navigator, localStorage, window) must
|
|
855
|
-
// be wrapped in platform guards to prevent server crashes
|
|
856
|
-
// during pre-rendering.
|
|
857
|
-
// EXAMPLE:
|
|
858
|
-
// WRONG:
|
|
859
|
-
// const lang = navigator.language;
|
|
860
|
-
// window.scrollTo(0, 0);
|
|
861
|
-
// RIGHT:
|
|
862
|
-
// private platformId = inject(PLATFORM_ID);
|
|
863
|
-
// constructor() {
|
|
864
|
-
// if (isPlatformBrowser(this.platformId)) {
|
|
865
|
-
// const lang = navigator.language;
|
|
866
|
-
// window.scrollTo(0, 0);
|
|
867
|
-
// }
|
|
868
|
-
// }
|
|
869
|
-
// ─────────────────────────────────────────
|
|
870
|
-
{
|
|
871
|
-
id: 'angular-ssr-safe-initialization',
|
|
872
|
-
label: 'Guard browser-only APIs in SSR',
|
|
873
|
-
description:
|
|
874
|
-
'Wrap browser-only code in isPlatformBrowser() or effect() guards to prevent errors during SSR pre-rendering.',
|
|
875
|
-
severity: 'warning',
|
|
876
|
-
fileExtensions: ['ts'],
|
|
877
|
-
pattern: null,
|
|
878
|
-
customCheck: (file) => {
|
|
879
|
-
const violations: Array<{ line: number | null; message: string }> = [];
|
|
880
|
-
|
|
881
|
-
const hasBrowserAPI = /(?:document\.|window\.|localStorage|navigator|sessionStorage)/.test(file.content);
|
|
882
|
-
const hasGuard = /isPlatformBrowser|platformBrowser|isBrowser/.test(file.content);
|
|
883
|
-
|
|
884
|
-
if (hasBrowserAPI && !hasGuard && !file.relativePath.includes('client')) {
|
|
885
|
-
violations.push({
|
|
886
|
-
line: null,
|
|
887
|
-
message:
|
|
888
|
-
'Browser-only API detected without SSR guard. Wrap in isPlatformBrowser() or use effect() for safe execution.',
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
return violations;
|
|
892
|
-
},
|
|
893
|
-
applicableTo: ['angular'],
|
|
894
|
-
category: 'Server-Side Rendering',
|
|
895
|
-
},
|
|
896
|
-
|
|
897
|
-
// ─────────────────────────────────────────
|
|
898
|
-
// RULE: angular-lazy-load-modules
|
|
899
|
-
// ROLE: Enforce lazy loading for route modules
|
|
900
|
-
// PURPOSE: Eagerly loading all routes increases the initial bundle
|
|
901
|
-
// size and slows Time-to-Interactive. Lazy loading defers
|
|
902
|
-
// feature code until the user navigates to it.
|
|
903
|
-
// EXAMPLE:
|
|
904
|
-
// WRONG:
|
|
905
|
-
// import { UsersComponent } from './users/users.component';
|
|
906
|
-
// { path: 'users', component: UsersComponent }
|
|
907
|
-
// RIGHT:
|
|
908
|
-
// {
|
|
909
|
-
// path: 'users',
|
|
910
|
-
// loadComponent: () => import('./users/users.component')
|
|
911
|
-
// .then(m => m.UsersComponent)
|
|
912
|
-
// }
|
|
913
|
-
// ─────────────────────────────────────────
|
|
914
291
|
{
|
|
915
292
|
id: 'angular-lazy-load-modules',
|
|
916
293
|
label: 'Use lazy loading for routes',
|
|
@@ -918,34 +295,13 @@ const angularRules: Rule[] = [
|
|
|
918
295
|
'Lazy load feature modules in routing to reduce initial bundle size and improve load time.',
|
|
919
296
|
severity: 'info',
|
|
920
297
|
fileExtensions: ['ts'],
|
|
921
|
-
pattern: /loadChildren:\s*\(\)\s*=>\s*import\(/g,
|
|
298
|
+
pattern: /loadChildren:\s*\(\)\s*=>\s*import\(['"]\.['"]\)/g,
|
|
922
299
|
applicableTo: ['angular'],
|
|
923
300
|
category: 'Performance',
|
|
924
301
|
},
|
|
925
302
|
|
|
926
303
|
// ── Best Practices ─────────────────────────────────────────────────────
|
|
927
304
|
|
|
928
|
-
// ─────────────────────────────────────────
|
|
929
|
-
// RULE: angular-standalone-components
|
|
930
|
-
// ROLE: Enforce standalone component architecture
|
|
931
|
-
// PURPOSE: NgModules add indirection and make dependency tracking hard.
|
|
932
|
-
// Standalone components declare their own imports, are
|
|
933
|
-
// tree-shakeable, and are the recommended Angular architecture.
|
|
934
|
-
// EXAMPLE:
|
|
935
|
-
// WRONG:
|
|
936
|
-
// @NgModule({
|
|
937
|
-
// declarations: [UserComponent],
|
|
938
|
-
// imports: [CommonModule],
|
|
939
|
-
// })
|
|
940
|
-
// export class UserModule {}
|
|
941
|
-
// RIGHT:
|
|
942
|
-
// @Component({
|
|
943
|
-
// standalone: true,
|
|
944
|
-
// imports: [NgClass],
|
|
945
|
-
// selector: 'app-user',
|
|
946
|
-
// })
|
|
947
|
-
// export class UserComponent {}
|
|
948
|
-
// ─────────────────────────────────────────
|
|
949
305
|
{
|
|
950
306
|
id: 'angular-standalone-components',
|
|
951
307
|
label: 'Prefer standalone components',
|
|
@@ -958,24 +314,6 @@ const angularRules: Rule[] = [
|
|
|
958
314
|
category: 'Best Practices',
|
|
959
315
|
},
|
|
960
316
|
|
|
961
|
-
// ─────────────────────────────────────────
|
|
962
|
-
// RULE: angular-strict-templates
|
|
963
|
-
// ROLE: Enforce strict template type checking
|
|
964
|
-
// PURPOSE: Without strictTemplates, Angular templates are essentially
|
|
965
|
-
// untyped — typos in property names, wrong types, and missing
|
|
966
|
-
// fields are silently ignored at compile time.
|
|
967
|
-
// EXAMPLE:
|
|
968
|
-
// WRONG (tsconfig.json):
|
|
969
|
-
// "angularCompilerOptions": {
|
|
970
|
-
// "strictTemplates": false
|
|
971
|
-
// }
|
|
972
|
-
// RIGHT (tsconfig.json):
|
|
973
|
-
// "angularCompilerOptions": {
|
|
974
|
-
// "strictTemplates": true,
|
|
975
|
-
// "strictInjectionParameters": true,
|
|
976
|
-
// "strictInputAccessModifiers": true
|
|
977
|
-
// }
|
|
978
|
-
// ─────────────────────────────────────────
|
|
979
317
|
{
|
|
980
318
|
id: 'angular-strict-templates',
|
|
981
319
|
label: 'Enable strict template checking',
|
|
@@ -988,23 +326,6 @@ const angularRules: Rule[] = [
|
|
|
988
326
|
category: 'TypeScript',
|
|
989
327
|
},
|
|
990
328
|
|
|
991
|
-
// ─────────────────────────────────────────
|
|
992
|
-
// RULE: angular-defer-for-lazy
|
|
993
|
-
// ROLE: Recommend @defer for template-level lazy loading
|
|
994
|
-
// PURPOSE: @defer blocks let you lazy-load sections of a template
|
|
995
|
-
// (heavy charts, tables, modals) without component-level
|
|
996
|
-
// code splitting. Triggers include viewport, interaction, etc.
|
|
997
|
-
// EXAMPLE:
|
|
998
|
-
// WRONG:
|
|
999
|
-
// <app-heavy-chart [data]="chartData()"></app-heavy-chart>
|
|
1000
|
-
// <!-- Always loaded, even if below the fold -->
|
|
1001
|
-
// RIGHT:
|
|
1002
|
-
// @defer (on viewport) {
|
|
1003
|
-
// <app-heavy-chart [data]="chartData()"></app-heavy-chart>
|
|
1004
|
-
// } @placeholder {
|
|
1005
|
-
// <div class="h-64 animate-pulse bg-gray-200"></div>
|
|
1006
|
-
// }
|
|
1007
|
-
// ─────────────────────────────────────────
|
|
1008
329
|
{
|
|
1009
330
|
id: 'angular-defer-for-lazy',
|
|
1010
331
|
label: 'Use @defer for lazy views',
|