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