@hatem427/code-guard-ci 2.2.9 → 3.0.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 +468 -27
- package/config/guidelines.config.ts +130 -5
- package/config/nextjs.config.ts +284 -11
- package/config/react.config.ts +440 -16
- package/dist/config/angular.config.d.ts.map +1 -1
- package/dist/config/angular.config.js +468 -26
- 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 +127 -5
- package/dist/config/guidelines.config.js.map +1 -1
- package/dist/config/nextjs.config.d.ts.map +1 -1
- package/dist/config/nextjs.config.js +284 -11
- package/dist/config/nextjs.config.js.map +1 -1
- package/dist/config/react.config.d.ts.map +1 -1
- package/dist/config/react.config.js +440 -16
- package/dist/config/react.config.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 +9 -71
- 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 +517 -13
- package/dist/scripts/config-generators/eslint-generator.js.map +1 -1
- package/dist/scripts/config-generators/frameworks/angular.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/angular.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/angular.js +81 -0
- package/dist/scripts/config-generators/frameworks/angular.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/general.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/general.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/general.js +15 -0
- package/dist/scripts/config-generators/frameworks/general.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/index.d.ts +17 -0
- package/dist/scripts/config-generators/frameworks/index.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/index.js +28 -0
- package/dist/scripts/config-generators/frameworks/index.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/nextjs.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/nextjs.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/nextjs.js +115 -0
- package/dist/scripts/config-generators/frameworks/nextjs.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/node.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/node.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/node.js +19 -0
- package/dist/scripts/config-generators/frameworks/node.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/nuxt.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/nuxt.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/nuxt.js +18 -0
- package/dist/scripts/config-generators/frameworks/nuxt.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/react.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/react.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/react.js +117 -0
- package/dist/scripts/config-generators/frameworks/react.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/svelte.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/svelte.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/svelte.js +17 -0
- package/dist/scripts/config-generators/frameworks/svelte.js.map +1 -0
- package/dist/scripts/config-generators/frameworks/vue.d.ts +6 -0
- package/dist/scripts/config-generators/frameworks/vue.d.ts.map +1 -0
- package/dist/scripts/config-generators/frameworks/vue.js +19 -0
- package/dist/scripts/config-generators/frameworks/vue.js.map +1 -0
- package/package.json +1 -1
- package/scripts/config-generators/ai-config-generator.ts +19 -78
- package/scripts/config-generators/eslint-generator.ts +511 -7
- package/scripts/config-generators/frameworks/angular.ts +78 -0
- package/scripts/config-generators/frameworks/general.ts +12 -0
- package/scripts/config-generators/frameworks/index.ts +17 -0
- package/scripts/config-generators/frameworks/nextjs.ts +112 -0
- package/scripts/config-generators/frameworks/node.ts +16 -0
- package/scripts/config-generators/frameworks/nuxt.ts +15 -0
- package/scripts/config-generators/frameworks/react.ts +114 -0
- package/scripts/config-generators/frameworks/svelte.ts +14 -0
- package/scripts/config-generators/frameworks/vue.ts +16 -0
package/config/angular.config.ts
CHANGED
|
@@ -18,6 +18,24 @@ import { registerRules, Rule } from './guidelines.config';
|
|
|
18
18
|
const angularRules: Rule[] = [
|
|
19
19
|
// ── NEW: Control Flow Syntax (Angular 17+) ────────────────────────────
|
|
20
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
|
+
// ─────────────────────────────────────────
|
|
21
39
|
{
|
|
22
40
|
id: 'angular-use-new-control-flow',
|
|
23
41
|
label: 'Use @if, @for, @switch instead of *ngIf, *ngFor',
|
|
@@ -35,7 +53,7 @@ const angularRules: Rule[] = [
|
|
|
35
53
|
violations.push({
|
|
36
54
|
line: i + 1,
|
|
37
55
|
message:
|
|
38
|
-
'Found structural directive. Use new Angular control flow syntax for better performance and readability
|
|
56
|
+
'Found structural directive. Use new Angular control flow syntax for better performance and readability.',
|
|
39
57
|
});
|
|
40
58
|
}
|
|
41
59
|
}
|
|
@@ -45,6 +63,21 @@ const angularRules: Rule[] = [
|
|
|
45
63
|
category: 'Template Syntax',
|
|
46
64
|
},
|
|
47
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
|
+
// ─────────────────────────────────────────
|
|
48
81
|
{
|
|
49
82
|
id: 'angular-use-let-syntax',
|
|
50
83
|
label: 'Use @let for template variables',
|
|
@@ -71,6 +104,30 @@ const angularRules: Rule[] = [
|
|
|
71
104
|
|
|
72
105
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
73
106
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
74
131
|
{
|
|
75
132
|
id: 'angular-no-ngoninit',
|
|
76
133
|
label: 'Avoid ngOnInit for simple initialization',
|
|
@@ -85,6 +142,20 @@ const angularRules: Rule[] = [
|
|
|
85
142
|
|
|
86
143
|
// ── Deprecated APIs ────────────────────────────────────────────────────
|
|
87
144
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
88
159
|
{
|
|
89
160
|
id: 'angular-no-ng-deep',
|
|
90
161
|
label: 'No ::ng-deep',
|
|
@@ -97,6 +168,25 @@ const angularRules: Rule[] = [
|
|
|
97
168
|
category: 'Angular Deprecated APIs',
|
|
98
169
|
},
|
|
99
170
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
100
190
|
{
|
|
101
191
|
id: 'angular-no-common-module',
|
|
102
192
|
label: 'No CommonModule import in standalone components',
|
|
@@ -109,6 +199,20 @@ const angularRules: Rule[] = [
|
|
|
109
199
|
category: 'Angular Deprecated APIs',
|
|
110
200
|
},
|
|
111
201
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
112
216
|
{
|
|
113
217
|
id: 'angular-no-ngclass',
|
|
114
218
|
label: 'No [ngClass] — use [class] binding or Tailwind',
|
|
@@ -121,6 +225,20 @@ const angularRules: Rule[] = [
|
|
|
121
225
|
category: 'Angular Deprecated APIs',
|
|
122
226
|
},
|
|
123
227
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
124
242
|
{
|
|
125
243
|
id: 'angular-no-ngstyle',
|
|
126
244
|
label: 'No [ngStyle] — use [style] binding or Tailwind',
|
|
@@ -135,6 +253,22 @@ const angularRules: Rule[] = [
|
|
|
135
253
|
|
|
136
254
|
// ── Template best practices ────────────────────────────────────────────
|
|
137
255
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
138
272
|
{
|
|
139
273
|
id: 'angular-no-function-in-template',
|
|
140
274
|
label: 'No function calls in templates',
|
|
@@ -146,16 +280,6 @@ const angularRules: Rule[] = [
|
|
|
146
280
|
customCheck: (file) => {
|
|
147
281
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
148
282
|
|
|
149
|
-
/**
|
|
150
|
-
* Regex explanation:
|
|
151
|
-
* Match patterns like {{ someFunc() }} or (click)="handler()" but EXCLUDE:
|
|
152
|
-
* - Event handlers: (click)="...", (submit)="...", on*="..."
|
|
153
|
-
* - Known safe pipes: | async, | date, | json, etc.
|
|
154
|
-
* - Structural directives: *ngIf, *ngFor, @if, @for
|
|
155
|
-
*
|
|
156
|
-
* We look for interpolation bindings {{ expr() }} and property bindings [prop]="expr()"
|
|
157
|
-
* that contain function calls which are NOT event bindings.
|
|
158
|
-
*/
|
|
159
283
|
const interpolationRegex = /\{\{[^}]*\w+\s*\([^)]*\)[^}]*\}\}/g;
|
|
160
284
|
const propertyBindingRegex = /\[(?!click|submit|keyup|keydown|change|input|focus|blur)\w+\]\s*=\s*"[^"]*\w+\s*\([^)]*\)[^"]*"/g;
|
|
161
285
|
|
|
@@ -165,7 +289,6 @@ const angularRules: Rule[] = [
|
|
|
165
289
|
// Check interpolation bindings {{ func() }}
|
|
166
290
|
interpolationRegex.lastIndex = 0;
|
|
167
291
|
if (interpolationRegex.test(line)) {
|
|
168
|
-
// Exclude pipe usages like {{ value | pipeName }}
|
|
169
292
|
const trimmed = line.trim();
|
|
170
293
|
if (!trimmed.includes(' | ')) {
|
|
171
294
|
violations.push({
|
|
@@ -193,6 +316,24 @@ const angularRules: Rule[] = [
|
|
|
193
316
|
|
|
194
317
|
// ── RxJS best practices ────────────────────────────────────────────────
|
|
195
318
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
196
337
|
{
|
|
197
338
|
id: 'angular-use-async-pipe',
|
|
198
339
|
label: 'Use async pipe or toSignal() instead of manual subscribe',
|
|
@@ -205,6 +346,26 @@ const angularRules: Rule[] = [
|
|
|
205
346
|
category: 'RxJS',
|
|
206
347
|
},
|
|
207
348
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
208
369
|
{
|
|
209
370
|
id: 'angular-use-takeuntildestroyed',
|
|
210
371
|
label: 'Use takeUntilDestroyed() for subscriptions',
|
|
@@ -216,7 +377,6 @@ const angularRules: Rule[] = [
|
|
|
216
377
|
customCheck: (file) => {
|
|
217
378
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
218
379
|
|
|
219
|
-
// Only check Angular component/directive/service files
|
|
220
380
|
if (
|
|
221
381
|
!file.content.includes('@Component') &&
|
|
222
382
|
!file.content.includes('@Directive') &&
|
|
@@ -243,6 +403,20 @@ const angularRules: Rule[] = [
|
|
|
243
403
|
category: 'RxJS',
|
|
244
404
|
},
|
|
245
405
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
246
420
|
{
|
|
247
421
|
id: 'angular-prefer-signals',
|
|
248
422
|
label: 'Consider using Angular Signals',
|
|
@@ -252,7 +426,6 @@ const angularRules: Rule[] = [
|
|
|
252
426
|
fileExtensions: ['ts'],
|
|
253
427
|
pattern: null,
|
|
254
428
|
customCheck: (file) => {
|
|
255
|
-
// Only flag if the component uses BehaviorSubject but no signals
|
|
256
429
|
const hasBehaviorSubject = file.content.includes('BehaviorSubject');
|
|
257
430
|
const hasSignal = file.content.includes('signal(') || file.content.includes('computed(');
|
|
258
431
|
|
|
@@ -272,6 +445,28 @@ const angularRules: Rule[] = [
|
|
|
272
445
|
category: 'Angular Signals',
|
|
273
446
|
},
|
|
274
447
|
|
|
448
|
+
// ─────────────────────────────────────────
|
|
449
|
+
// RULE: angular-prefer-inject
|
|
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
|
+
// ─────────────────────────────────────────
|
|
275
470
|
{
|
|
276
471
|
id: 'angular-prefer-inject',
|
|
277
472
|
label: 'Use inject() instead of constructor injection',
|
|
@@ -283,7 +478,6 @@ const angularRules: Rule[] = [
|
|
|
283
478
|
customCheck: (file) => {
|
|
284
479
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
285
480
|
|
|
286
|
-
// Only check Angular component/directive/service files
|
|
287
481
|
if (
|
|
288
482
|
!file.content.includes('@Component') &&
|
|
289
483
|
!file.content.includes('@Directive') &&
|
|
@@ -292,12 +486,9 @@ const angularRules: Rule[] = [
|
|
|
292
486
|
return [];
|
|
293
487
|
}
|
|
294
488
|
|
|
295
|
-
// Check for constructor with dependency injection parameters
|
|
296
|
-
// Matches: constructor(private service: Type) or constructor(public readonly http: HttpClient)
|
|
297
489
|
const constructorPattern = /constructor\s*\(\s*(?:private|public|protected|readonly|\s)+\w+\s*:\s*\w+/;
|
|
298
490
|
|
|
299
491
|
if (constructorPattern.test(file.content)) {
|
|
300
|
-
// Find the line number
|
|
301
492
|
for (let i = 0; i < file.lines.length; i++) {
|
|
302
493
|
if (constructorPattern.test(file.lines[i])) {
|
|
303
494
|
violations.push({
|
|
@@ -318,6 +509,22 @@ const angularRules: Rule[] = [
|
|
|
318
509
|
|
|
319
510
|
// ── NEW: Signals & Signal-based Features (Angular 16+, essential in v21) ────
|
|
320
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
|
+
// ─────────────────────────────────────────
|
|
321
528
|
{
|
|
322
529
|
id: 'angular-use-signals',
|
|
323
530
|
label: 'Use Signals for reactive state',
|
|
@@ -329,7 +536,6 @@ const angularRules: Rule[] = [
|
|
|
329
536
|
customCheck: (file) => {
|
|
330
537
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
331
538
|
|
|
332
|
-
// Only check component/service files
|
|
333
539
|
if (
|
|
334
540
|
!file.content.includes('@Component') &&
|
|
335
541
|
!file.content.includes('@Injectable') &&
|
|
@@ -338,7 +544,6 @@ const angularRules: Rule[] = [
|
|
|
338
544
|
return [];
|
|
339
545
|
}
|
|
340
546
|
|
|
341
|
-
// Flag BehaviorSubject usage without signal migration
|
|
342
547
|
const hasBehaviorSubject = /BehaviorSubject|Subject/.test(file.content);
|
|
343
548
|
const hasSignal = /signal\(|computed\(|effect\(/.test(file.content);
|
|
344
549
|
const hasRxJS = /rxjs|Observable<|\.pipe\(/.test(file.content);
|
|
@@ -357,6 +562,22 @@ const angularRules: Rule[] = [
|
|
|
357
562
|
category: 'Angular Signals',
|
|
358
563
|
},
|
|
359
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
|
+
// ─────────────────────────────────────────
|
|
360
581
|
{
|
|
361
582
|
id: 'angular-use-computed',
|
|
362
583
|
label: 'Use computed() for derived state',
|
|
@@ -368,12 +589,11 @@ const angularRules: Rule[] = [
|
|
|
368
589
|
customCheck: (file) => {
|
|
369
590
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
370
591
|
|
|
371
|
-
// Flag getters in component classes that could be computed signals
|
|
372
592
|
if (/get\s+\w+\s*\(\s*\).*\{[\s\S]*?return.*signal|this\.\w+\(\)/.test(file.content)) {
|
|
373
593
|
violations.push({
|
|
374
594
|
line: null,
|
|
375
595
|
message:
|
|
376
|
-
'Found getter function. Use computed() from signals for automatic memoization and better performance
|
|
596
|
+
'Found getter function. Use computed() from signals for automatic memoization and better performance.',
|
|
377
597
|
});
|
|
378
598
|
}
|
|
379
599
|
return violations;
|
|
@@ -382,6 +602,22 @@ const angularRules: Rule[] = [
|
|
|
382
602
|
category: 'Angular Signals',
|
|
383
603
|
},
|
|
384
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
|
+
// ─────────────────────────────────────────
|
|
385
621
|
{
|
|
386
622
|
id: 'angular-use-effect',
|
|
387
623
|
label: 'Use effect() for side effects instead of ngOnInit',
|
|
@@ -393,12 +629,11 @@ const angularRules: Rule[] = [
|
|
|
393
629
|
customCheck: (file) => {
|
|
394
630
|
const violations: Array<{ line: number | null; message: string }> = [];
|
|
395
631
|
|
|
396
|
-
// Flag ngOnInit with subscriptions or side effects
|
|
397
632
|
if (/ngOnInit\s*\(\)[\s\S]*?\{[\s\S]*?(?:subscribe|addEventListener|setTimeout)/.test(file.content)) {
|
|
398
633
|
violations.push({
|
|
399
634
|
line: null,
|
|
400
635
|
message:
|
|
401
|
-
'Found ngOnInit with side effects. Use effect() from signals for cleaner dependency tracking
|
|
636
|
+
'Found ngOnInit with side effects. Use effect() from signals for cleaner dependency tracking.',
|
|
402
637
|
});
|
|
403
638
|
}
|
|
404
639
|
return violations;
|
|
@@ -407,6 +642,22 @@ const angularRules: Rule[] = [
|
|
|
407
642
|
category: 'Angular Signals',
|
|
408
643
|
},
|
|
409
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
|
+
// ─────────────────────────────────────────
|
|
410
661
|
{
|
|
411
662
|
id: 'angular-use-input-output-functions',
|
|
412
663
|
label: 'Use input(), output(), model() functions',
|
|
@@ -419,6 +670,22 @@ const angularRules: Rule[] = [
|
|
|
419
670
|
category: 'Angular Components',
|
|
420
671
|
},
|
|
421
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
|
+
// ─────────────────────────────────────────
|
|
422
689
|
{
|
|
423
690
|
id: 'angular-signal-based-viewchild',
|
|
424
691
|
label: 'Use signal-based @ViewChild where applicable',
|
|
@@ -433,6 +700,22 @@ const angularRules: Rule[] = [
|
|
|
433
700
|
|
|
434
701
|
// ── NEW: Zoneless Change Detection (Angular 18+, recommended in v21) ───
|
|
435
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
|
+
// ─────────────────────────────────────────
|
|
436
719
|
{
|
|
437
720
|
id: 'angular-consider-zoneless',
|
|
438
721
|
label: 'Consider zoneless change detection',
|
|
@@ -445,10 +728,27 @@ const angularRules: Rule[] = [
|
|
|
445
728
|
category: 'Performance',
|
|
446
729
|
},
|
|
447
730
|
|
|
448
|
-
// ── NEW: Server-Side Rendering (Angular 17+) ──────────────────────────
|
|
449
|
-
|
|
450
731
|
// ── Performance Rules ──────────────────────────────────────────────────
|
|
451
732
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
452
752
|
{
|
|
453
753
|
id: 'angular-onpush-change-detection',
|
|
454
754
|
label: 'Consider OnPush change detection',
|
|
@@ -461,6 +761,21 @@ const angularRules: Rule[] = [
|
|
|
461
761
|
category: 'Performance',
|
|
462
762
|
},
|
|
463
763
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
464
779
|
{
|
|
465
780
|
id: 'angular-trackby-for-loops',
|
|
466
781
|
label: 'Use trackBy with *ngFor or proper @for iteration',
|
|
@@ -473,6 +788,20 @@ const angularRules: Rule[] = [
|
|
|
473
788
|
category: 'Performance',
|
|
474
789
|
},
|
|
475
790
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
476
805
|
{
|
|
477
806
|
id: 'angular-no-unused-imports',
|
|
478
807
|
label: 'Avoid importing RxJS operators not used',
|
|
@@ -487,6 +816,26 @@ const angularRules: Rule[] = [
|
|
|
487
816
|
|
|
488
817
|
// ── NEW: Server-Side Rendering Hydration (Angular 17+) ────────────────
|
|
489
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
|
+
// ─────────────────────────────────────────
|
|
490
839
|
{
|
|
491
840
|
id: 'angular-ssr-hydration',
|
|
492
841
|
label: 'Ensure SSR hydration compatibility',
|
|
@@ -499,6 +848,25 @@ const angularRules: Rule[] = [
|
|
|
499
848
|
category: 'Server-Side Rendering',
|
|
500
849
|
},
|
|
501
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
|
+
// ─────────────────────────────────────────
|
|
502
870
|
{
|
|
503
871
|
id: 'angular-ssr-safe-initialization',
|
|
504
872
|
label: 'Guard browser-only APIs in SSR',
|
|
@@ -526,6 +894,23 @@ const angularRules: Rule[] = [
|
|
|
526
894
|
category: 'Server-Side Rendering',
|
|
527
895
|
},
|
|
528
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
|
+
// ─────────────────────────────────────────
|
|
529
914
|
{
|
|
530
915
|
id: 'angular-lazy-load-modules',
|
|
531
916
|
label: 'Use lazy loading for routes',
|
|
@@ -533,13 +918,34 @@ const angularRules: Rule[] = [
|
|
|
533
918
|
'Lazy load feature modules in routing to reduce initial bundle size and improve load time.',
|
|
534
919
|
severity: 'info',
|
|
535
920
|
fileExtensions: ['ts'],
|
|
536
|
-
pattern: /loadChildren:\s*\(\)\s*=>\s*import\(
|
|
921
|
+
pattern: /loadChildren:\s*\(\)\s*=>\s*import\(/g,
|
|
537
922
|
applicableTo: ['angular'],
|
|
538
923
|
category: 'Performance',
|
|
539
924
|
},
|
|
540
925
|
|
|
541
926
|
// ── Best Practices ─────────────────────────────────────────────────────
|
|
542
927
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
543
949
|
{
|
|
544
950
|
id: 'angular-standalone-components',
|
|
545
951
|
label: 'Prefer standalone components',
|
|
@@ -552,6 +958,24 @@ const angularRules: Rule[] = [
|
|
|
552
958
|
category: 'Best Practices',
|
|
553
959
|
},
|
|
554
960
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
555
979
|
{
|
|
556
980
|
id: 'angular-strict-templates',
|
|
557
981
|
label: 'Enable strict template checking',
|
|
@@ -564,6 +988,23 @@ const angularRules: Rule[] = [
|
|
|
564
988
|
category: 'TypeScript',
|
|
565
989
|
},
|
|
566
990
|
|
|
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
|
+
// ─────────────────────────────────────────
|
|
567
1008
|
{
|
|
568
1009
|
id: 'angular-defer-for-lazy',
|
|
569
1010
|
label: 'Use @defer for lazy views',
|