@corbat-tech/coding-standards-mcp 1.0.3 → 2.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/README.md +233 -337
- package/dist/agent.d.ts +5 -6
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +95 -217
- package/dist/agent.js.map +1 -1
- package/dist/analysis/code-analyzer.d.ts +44 -0
- package/dist/analysis/code-analyzer.d.ts.map +1 -0
- package/dist/analysis/code-analyzer.js +528 -0
- package/dist/analysis/code-analyzer.js.map +1 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +112 -0
- package/dist/errors.js.map +1 -0
- package/dist/guardrails.d.ts +35 -0
- package/dist/guardrails.d.ts.map +1 -0
- package/dist/guardrails.js +303 -0
- package/dist/guardrails.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +36 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +63 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +40 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +97 -0
- package/dist/metrics.js.map +1 -0
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +239 -108
- package/dist/profiles.js.map +1 -1
- package/dist/prompts.js +1 -1
- package/dist/prompts.js.map +1 -1
- package/dist/tools/definitions.d.ts +143 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +229 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/handlers/get-context.d.ts +12 -0
- package/dist/tools/handlers/get-context.d.ts.map +1 -0
- package/dist/tools/handlers/get-context.js +233 -0
- package/dist/tools/handlers/get-context.js.map +1 -0
- package/dist/tools/handlers/health.d.ts +11 -0
- package/dist/tools/handlers/health.d.ts.map +1 -0
- package/dist/tools/handlers/health.js +57 -0
- package/dist/tools/handlers/health.js.map +1 -0
- package/dist/tools/handlers/index.d.ts +12 -0
- package/dist/tools/handlers/index.d.ts.map +1 -0
- package/dist/tools/handlers/index.js +12 -0
- package/dist/tools/handlers/index.js.map +1 -0
- package/dist/tools/handlers/init.d.ts +12 -0
- package/dist/tools/handlers/init.d.ts.map +1 -0
- package/dist/tools/handlers/init.js +102 -0
- package/dist/tools/handlers/init.js.map +1 -0
- package/dist/tools/handlers/profiles.d.ts +11 -0
- package/dist/tools/handlers/profiles.d.ts.map +1 -0
- package/dist/tools/handlers/profiles.js +25 -0
- package/dist/tools/handlers/profiles.js.map +1 -0
- package/dist/tools/handlers/search.d.ts +12 -0
- package/dist/tools/handlers/search.d.ts.map +1 -0
- package/dist/tools/handlers/search.js +58 -0
- package/dist/tools/handlers/search.js.map +1 -0
- package/dist/tools/handlers/validate.d.ts +15 -0
- package/dist/tools/handlers/validate.d.ts.map +1 -0
- package/dist/tools/handlers/validate.js +71 -0
- package/dist/tools/handlers/validate.js.map +1 -0
- package/dist/tools/handlers/verify.d.ts +38 -0
- package/dist/tools/handlers/verify.d.ts.map +1 -0
- package/dist/tools/handlers/verify.js +172 -0
- package/dist/tools/handlers/verify.js.map +1 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +75 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/schemas.d.ts +29 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +20 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +141 -71
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +92 -40
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/profiles/examples/microservice-kafka.yaml +122 -0
- package/profiles/examples/startup-fast.yaml +67 -0
- package/profiles/examples/strict-enterprise.yaml +62 -0
- package/profiles/templates/angular.yaml +614 -0
- package/profiles/templates/csharp-dotnet.yaml +529 -0
- package/profiles/templates/flutter.yaml +547 -0
- package/profiles/templates/go.yaml +1276 -0
- package/profiles/templates/java-spring-backend.yaml +326 -0
- package/profiles/templates/kotlin-spring.yaml +417 -0
- package/profiles/templates/nextjs.yaml +536 -0
- package/profiles/templates/nodejs.yaml +594 -0
- package/profiles/templates/python.yaml +546 -0
- package/profiles/templates/react.yaml +456 -0
- package/profiles/templates/rust.yaml +508 -0
- package/profiles/templates/vue.yaml +483 -0
|
@@ -492,3 +492,617 @@ forms:
|
|
|
492
492
|
#
|
|
493
493
|
# e2e/ # End-to-end tests
|
|
494
494
|
# └── specs/
|
|
495
|
+
|
|
496
|
+
# ----------------------------------------------------------------------------
|
|
497
|
+
# CODE EXAMPLES
|
|
498
|
+
# ----------------------------------------------------------------------------
|
|
499
|
+
codeExamples:
|
|
500
|
+
standaloneComponent:
|
|
501
|
+
description: "Modern standalone component with signals"
|
|
502
|
+
code: |
|
|
503
|
+
// features/users/user-card.component.ts
|
|
504
|
+
import { Component, input, output, computed } from '@angular/core';
|
|
505
|
+
import { RouterLink } from '@angular/router';
|
|
506
|
+
import type { User } from '@/core/models/user.model';
|
|
507
|
+
|
|
508
|
+
@Component({
|
|
509
|
+
selector: 'app-user-card',
|
|
510
|
+
standalone: true,
|
|
511
|
+
imports: [RouterLink],
|
|
512
|
+
template: `
|
|
513
|
+
@if (user(); as u) {
|
|
514
|
+
<article class="user-card" [class.active]="isActive()">
|
|
515
|
+
<img [src]="u.avatar" [alt]="fullName() + '\\'s avatar'" />
|
|
516
|
+
<h3>{{ fullName() }}</h3>
|
|
517
|
+
<p>{{ u.email }}</p>
|
|
518
|
+
<button (click)="handleSelect()">Select</button>
|
|
519
|
+
<a [routerLink]="['/users', u.id]">View Profile</a>
|
|
520
|
+
</article>
|
|
521
|
+
}
|
|
522
|
+
`,
|
|
523
|
+
styles: [`
|
|
524
|
+
.user-card { @apply p-4 rounded-lg shadow; }
|
|
525
|
+
.user-card.active { @apply ring-2 ring-primary; }
|
|
526
|
+
`]
|
|
527
|
+
})
|
|
528
|
+
export class UserCardComponent {
|
|
529
|
+
// Signal-based inputs
|
|
530
|
+
user = input.required<User>();
|
|
531
|
+
isActive = input(false);
|
|
532
|
+
|
|
533
|
+
// Signal-based output
|
|
534
|
+
selected = output<string>();
|
|
535
|
+
|
|
536
|
+
// Computed signal
|
|
537
|
+
fullName = computed(() => {
|
|
538
|
+
const u = this.user();
|
|
539
|
+
return `${u.firstName} ${u.lastName}`;
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
handleSelect() {
|
|
543
|
+
this.selected.emit(this.user().id);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
signalState:
|
|
548
|
+
description: "Component state with signals"
|
|
549
|
+
code: |
|
|
550
|
+
// features/counter/counter.component.ts
|
|
551
|
+
import { Component, signal, computed, effect } from '@angular/core';
|
|
552
|
+
|
|
553
|
+
@Component({
|
|
554
|
+
selector: 'app-counter',
|
|
555
|
+
standalone: true,
|
|
556
|
+
template: `
|
|
557
|
+
<div class="counter">
|
|
558
|
+
<p>Count: {{ count() }}</p>
|
|
559
|
+
<p>Double: {{ doubleCount() }}</p>
|
|
560
|
+
<button (click)="increment()">+</button>
|
|
561
|
+
<button (click)="decrement()">-</button>
|
|
562
|
+
<button (click)="reset()">Reset</button>
|
|
563
|
+
</div>
|
|
564
|
+
`
|
|
565
|
+
})
|
|
566
|
+
export class CounterComponent {
|
|
567
|
+
// Writable signal
|
|
568
|
+
count = signal(0);
|
|
569
|
+
|
|
570
|
+
// Computed signal (derived state)
|
|
571
|
+
doubleCount = computed(() => this.count() * 2);
|
|
572
|
+
|
|
573
|
+
constructor() {
|
|
574
|
+
// Effect for side effects (logging, localStorage, etc.)
|
|
575
|
+
effect(() => {
|
|
576
|
+
console.log(`Count changed to: ${this.count()}`);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
increment() {
|
|
581
|
+
this.count.update(c => c + 1);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
decrement() {
|
|
585
|
+
this.count.update(c => c - 1);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
reset() {
|
|
589
|
+
this.count.set(0);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
service:
|
|
594
|
+
description: "Injectable service with signals and HttpClient"
|
|
595
|
+
code: |
|
|
596
|
+
// core/services/user.service.ts
|
|
597
|
+
import { Injectable, inject, signal, computed } from '@angular/core';
|
|
598
|
+
import { HttpClient } from '@angular/common/http';
|
|
599
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
600
|
+
import type { User } from '@/core/models/user.model';
|
|
601
|
+
|
|
602
|
+
@Injectable({ providedIn: 'root' })
|
|
603
|
+
export class UserService {
|
|
604
|
+
private http = inject(HttpClient);
|
|
605
|
+
|
|
606
|
+
private usersSignal = signal<User[]>([]);
|
|
607
|
+
private loadingSignal = signal(false);
|
|
608
|
+
private errorSignal = signal<Error | null>(null);
|
|
609
|
+
|
|
610
|
+
// Public readonly signals
|
|
611
|
+
readonly users = this.usersSignal.asReadonly();
|
|
612
|
+
readonly isLoading = this.loadingSignal.asReadonly();
|
|
613
|
+
readonly error = this.errorSignal.asReadonly();
|
|
614
|
+
|
|
615
|
+
// Computed
|
|
616
|
+
readonly userCount = computed(() => this.usersSignal().length);
|
|
617
|
+
|
|
618
|
+
async loadUsers(): Promise<void> {
|
|
619
|
+
this.loadingSignal.set(true);
|
|
620
|
+
this.errorSignal.set(null);
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const users = await firstValueFrom(
|
|
624
|
+
this.http.get<User[]>('/api/users')
|
|
625
|
+
);
|
|
626
|
+
this.usersSignal.set(users);
|
|
627
|
+
} catch (error) {
|
|
628
|
+
this.errorSignal.set(error as Error);
|
|
629
|
+
} finally {
|
|
630
|
+
this.loadingSignal.set(false);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
getUserById(id: string): User | undefined {
|
|
635
|
+
return this.usersSignal().find(u => u.id === id);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
functionalGuard:
|
|
640
|
+
description: "Functional route guard"
|
|
641
|
+
code: |
|
|
642
|
+
// core/guards/auth.guard.ts
|
|
643
|
+
import { inject } from '@angular/core';
|
|
644
|
+
import { Router, type CanActivateFn } from '@angular/router';
|
|
645
|
+
import { AuthService } from '@/core/services/auth.service';
|
|
646
|
+
|
|
647
|
+
export const authGuard: CanActivateFn = (route, state) => {
|
|
648
|
+
const authService = inject(AuthService);
|
|
649
|
+
const router = inject(Router);
|
|
650
|
+
|
|
651
|
+
if (authService.isAuthenticated()) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Redirect to login with return URL
|
|
656
|
+
return router.createUrlTree(['/login'], {
|
|
657
|
+
queryParams: { returnUrl: state.url }
|
|
658
|
+
});
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// Usage in routes
|
|
662
|
+
export const routes: Routes = [
|
|
663
|
+
{
|
|
664
|
+
path: 'dashboard',
|
|
665
|
+
loadComponent: () => import('./dashboard/dashboard.component'),
|
|
666
|
+
canActivate: [authGuard]
|
|
667
|
+
}
|
|
668
|
+
];
|
|
669
|
+
|
|
670
|
+
functionalInterceptor:
|
|
671
|
+
description: "Functional HTTP interceptor"
|
|
672
|
+
code: |
|
|
673
|
+
// core/interceptors/auth.interceptor.ts
|
|
674
|
+
import { inject } from '@angular/core';
|
|
675
|
+
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
|
|
676
|
+
import { catchError, throwError } from 'rxjs';
|
|
677
|
+
import { AuthService } from '@/core/services/auth.service';
|
|
678
|
+
import { Router } from '@angular/router';
|
|
679
|
+
|
|
680
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
681
|
+
const authService = inject(AuthService);
|
|
682
|
+
const router = inject(Router);
|
|
683
|
+
|
|
684
|
+
const token = authService.token();
|
|
685
|
+
|
|
686
|
+
// Add auth header if token exists
|
|
687
|
+
if (token) {
|
|
688
|
+
req = req.clone({
|
|
689
|
+
setHeaders: {
|
|
690
|
+
Authorization: `Bearer ${token}`
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return next(req).pipe(
|
|
696
|
+
catchError((error: HttpErrorResponse) => {
|
|
697
|
+
if (error.status === 401) {
|
|
698
|
+
authService.logout();
|
|
699
|
+
router.navigate(['/login']);
|
|
700
|
+
}
|
|
701
|
+
return throwError(() => error);
|
|
702
|
+
})
|
|
703
|
+
);
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Register in app.config.ts
|
|
707
|
+
export const appConfig: ApplicationConfig = {
|
|
708
|
+
providers: [
|
|
709
|
+
provideHttpClient(withInterceptors([authInterceptor]))
|
|
710
|
+
]
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
signalStore:
|
|
714
|
+
description: "NgRx SignalStore for feature state"
|
|
715
|
+
code: |
|
|
716
|
+
// features/products/products.store.ts
|
|
717
|
+
import { signalStore, withState, withComputed, withMethods, patchState } from '@ngrx/signals';
|
|
718
|
+
import { inject } from '@angular/core';
|
|
719
|
+
import { ProductService } from './products.service';
|
|
720
|
+
import type { Product } from '@/core/models/product.model';
|
|
721
|
+
|
|
722
|
+
interface ProductsState {
|
|
723
|
+
products: Product[];
|
|
724
|
+
selectedId: string | null;
|
|
725
|
+
isLoading: boolean;
|
|
726
|
+
error: string | null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const initialState: ProductsState = {
|
|
730
|
+
products: [],
|
|
731
|
+
selectedId: null,
|
|
732
|
+
isLoading: false,
|
|
733
|
+
error: null
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
export const ProductsStore = signalStore(
|
|
737
|
+
withState(initialState),
|
|
738
|
+
|
|
739
|
+
withComputed((store) => ({
|
|
740
|
+
selectedProduct: computed(() => {
|
|
741
|
+
const id = store.selectedId();
|
|
742
|
+
return store.products().find(p => p.id === id) ?? null;
|
|
743
|
+
}),
|
|
744
|
+
productCount: computed(() => store.products().length)
|
|
745
|
+
})),
|
|
746
|
+
|
|
747
|
+
withMethods((store, productService = inject(ProductService)) => ({
|
|
748
|
+
async loadProducts() {
|
|
749
|
+
patchState(store, { isLoading: true, error: null });
|
|
750
|
+
try {
|
|
751
|
+
const products = await productService.getAll();
|
|
752
|
+
patchState(store, { products, isLoading: false });
|
|
753
|
+
} catch (error) {
|
|
754
|
+
patchState(store, { error: (error as Error).message, isLoading: false });
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
selectProduct(id: string) {
|
|
759
|
+
patchState(store, { selectedId: id });
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
clearSelection() {
|
|
763
|
+
patchState(store, { selectedId: null });
|
|
764
|
+
}
|
|
765
|
+
}))
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
componentTest:
|
|
769
|
+
description: "Component test with Angular Testing Library"
|
|
770
|
+
code: |
|
|
771
|
+
// features/users/user-card.component.spec.ts
|
|
772
|
+
import { render, screen } from '@testing-library/angular';
|
|
773
|
+
import userEvent from '@testing-library/user-event';
|
|
774
|
+
import { UserCardComponent } from './user-card.component';
|
|
775
|
+
|
|
776
|
+
describe('UserCardComponent', () => {
|
|
777
|
+
const mockUser = {
|
|
778
|
+
id: '1',
|
|
779
|
+
firstName: 'John',
|
|
780
|
+
lastName: 'Doe',
|
|
781
|
+
email: 'john@example.com',
|
|
782
|
+
avatar: '/avatar.png'
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
it('displays user information', async () => {
|
|
786
|
+
await render(UserCardComponent, {
|
|
787
|
+
inputs: { user: mockUser }
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
791
|
+
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('emits selected event when button clicked', async () => {
|
|
795
|
+
const user = userEvent.setup();
|
|
796
|
+
const selectedSpy = jest.fn();
|
|
797
|
+
|
|
798
|
+
await render(UserCardComponent, {
|
|
799
|
+
inputs: { user: mockUser },
|
|
800
|
+
on: { selected: selectedSpy }
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
await user.click(screen.getByRole('button', { name: /select/i }));
|
|
804
|
+
|
|
805
|
+
expect(selectedSpy).toHaveBeenCalledWith('1');
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('applies active class when isActive is true', async () => {
|
|
809
|
+
await render(UserCardComponent, {
|
|
810
|
+
inputs: { user: mockUser, isActive: true }
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
expect(screen.getByRole('article')).toHaveClass('active');
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
deferBlock:
|
|
818
|
+
description: "@defer for lazy loading and performance"
|
|
819
|
+
code: |
|
|
820
|
+
// features/dashboard/dashboard.component.ts
|
|
821
|
+
@Component({
|
|
822
|
+
selector: 'app-dashboard',
|
|
823
|
+
standalone: true,
|
|
824
|
+
imports: [HeavyChartComponent, DataTableComponent],
|
|
825
|
+
template: `
|
|
826
|
+
<main>
|
|
827
|
+
<h1>Dashboard</h1>
|
|
828
|
+
|
|
829
|
+
<!-- Load chart when visible in viewport -->
|
|
830
|
+
@defer (on viewport) {
|
|
831
|
+
<app-heavy-chart [data]="chartData()" />
|
|
832
|
+
} @placeholder {
|
|
833
|
+
<div class="chart-placeholder">Loading chart...</div>
|
|
834
|
+
} @loading (minimum 300ms) {
|
|
835
|
+
<app-skeleton type="chart" />
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
<!-- Load table when user scrolls down -->
|
|
839
|
+
@defer (on viewport; prefetch on idle) {
|
|
840
|
+
<app-data-table [rows]="tableData()" />
|
|
841
|
+
} @placeholder {
|
|
842
|
+
<p>Scroll to see data table</p>
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
<!-- Load on interaction -->
|
|
846
|
+
@defer (on interaction) {
|
|
847
|
+
<app-comments [postId]="postId()" />
|
|
848
|
+
} @placeholder {
|
|
849
|
+
<button>Click to load comments</button>
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
<!-- Conditional defer -->
|
|
853
|
+
@defer (when showAdvanced()) {
|
|
854
|
+
<app-advanced-settings />
|
|
855
|
+
}
|
|
856
|
+
</main>
|
|
857
|
+
`
|
|
858
|
+
})
|
|
859
|
+
export class DashboardComponent {
|
|
860
|
+
chartData = signal<ChartData[]>([]);
|
|
861
|
+
tableData = signal<TableRow[]>([]);
|
|
862
|
+
postId = input.required<string>();
|
|
863
|
+
showAdvanced = signal(false);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
# ----------------------------------------------------------------------------
|
|
867
|
+
# ANTI-PATTERNS
|
|
868
|
+
# ----------------------------------------------------------------------------
|
|
869
|
+
antiPatterns:
|
|
870
|
+
ngModulesForNewCode:
|
|
871
|
+
name: "Using NgModules for New Components"
|
|
872
|
+
description: "NgModules are legacy; use standalone components"
|
|
873
|
+
bad: |
|
|
874
|
+
// ❌ NgModule-based component
|
|
875
|
+
@NgModule({
|
|
876
|
+
declarations: [UserCardComponent],
|
|
877
|
+
imports: [CommonModule],
|
|
878
|
+
exports: [UserCardComponent]
|
|
879
|
+
})
|
|
880
|
+
export class UserModule {}
|
|
881
|
+
|
|
882
|
+
@Component({
|
|
883
|
+
selector: 'app-user-card',
|
|
884
|
+
templateUrl: './user-card.component.html'
|
|
885
|
+
})
|
|
886
|
+
export class UserCardComponent {}
|
|
887
|
+
good: |
|
|
888
|
+
// ✅ Standalone component
|
|
889
|
+
@Component({
|
|
890
|
+
selector: 'app-user-card',
|
|
891
|
+
standalone: true,
|
|
892
|
+
imports: [RouterLink],
|
|
893
|
+
template: `...`
|
|
894
|
+
})
|
|
895
|
+
export class UserCardComponent {}
|
|
896
|
+
|
|
897
|
+
constructorInjection:
|
|
898
|
+
name: "Constructor Injection Instead of inject()"
|
|
899
|
+
description: "Use inject() function for cleaner dependency injection"
|
|
900
|
+
bad: |
|
|
901
|
+
// ❌ Constructor injection
|
|
902
|
+
@Component({ ... })
|
|
903
|
+
export class UserListComponent {
|
|
904
|
+
constructor(
|
|
905
|
+
private userService: UserService,
|
|
906
|
+
private router: Router,
|
|
907
|
+
private route: ActivatedRoute
|
|
908
|
+
) {}
|
|
909
|
+
}
|
|
910
|
+
good: |
|
|
911
|
+
// ✅ inject() function
|
|
912
|
+
@Component({ ... })
|
|
913
|
+
export class UserListComponent {
|
|
914
|
+
private userService = inject(UserService);
|
|
915
|
+
private router = inject(Router);
|
|
916
|
+
private route = inject(ActivatedRoute);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
oldControlFlow:
|
|
920
|
+
name: "Using *ngIf/*ngFor Instead of @if/@for"
|
|
921
|
+
description: "Use new built-in control flow (@if, @for, @switch)"
|
|
922
|
+
bad: |
|
|
923
|
+
// ❌ Old structural directives
|
|
924
|
+
<div *ngIf="user; else loading">{{ user.name }}</div>
|
|
925
|
+
<ng-template #loading>Loading...</ng-template>
|
|
926
|
+
|
|
927
|
+
<li *ngFor="let item of items; trackBy: trackById">
|
|
928
|
+
{{ item.name }}
|
|
929
|
+
</li>
|
|
930
|
+
good: |
|
|
931
|
+
// ✅ New control flow
|
|
932
|
+
@if (user(); as u) {
|
|
933
|
+
<div>{{ u.name }}</div>
|
|
934
|
+
} @else {
|
|
935
|
+
<span>Loading...</span>
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
@for (item of items(); track item.id) {
|
|
939
|
+
<li>{{ item.name }}</li>
|
|
940
|
+
} @empty {
|
|
941
|
+
<li>No items found</li>
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
behaviorSubjectOverSignals:
|
|
945
|
+
name: "Using BehaviorSubject When Signals Are Better"
|
|
946
|
+
description: "For synchronous state, prefer signals over RxJS"
|
|
947
|
+
bad: |
|
|
948
|
+
// ❌ BehaviorSubject for simple state
|
|
949
|
+
@Injectable({ providedIn: 'root' })
|
|
950
|
+
export class CounterService {
|
|
951
|
+
private count$ = new BehaviorSubject(0);
|
|
952
|
+
readonly count = this.count$.asObservable();
|
|
953
|
+
|
|
954
|
+
increment() {
|
|
955
|
+
this.count$.next(this.count$.value + 1);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
good: |
|
|
959
|
+
// ✅ Signals for synchronous state
|
|
960
|
+
@Injectable({ providedIn: 'root' })
|
|
961
|
+
export class CounterService {
|
|
962
|
+
private _count = signal(0);
|
|
963
|
+
readonly count = this._count.asReadonly();
|
|
964
|
+
|
|
965
|
+
increment() {
|
|
966
|
+
this._count.update(c => c + 1);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
subscribeWithoutCleanup:
|
|
971
|
+
name: "Manual Subscribe Without Cleanup"
|
|
972
|
+
description: "Use takeUntilDestroyed() or async pipe instead"
|
|
973
|
+
bad: |
|
|
974
|
+
// ❌ Manual subscribe leaks memory
|
|
975
|
+
@Component({ ... })
|
|
976
|
+
export class UserComponent implements OnInit {
|
|
977
|
+
user: User | null = null;
|
|
978
|
+
|
|
979
|
+
ngOnInit() {
|
|
980
|
+
this.userService.getUser().subscribe(user => {
|
|
981
|
+
this.user = user;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
good: |
|
|
986
|
+
// ✅ Use toSignal() or takeUntilDestroyed()
|
|
987
|
+
@Component({ ... })
|
|
988
|
+
export class UserComponent {
|
|
989
|
+
private userService = inject(UserService);
|
|
990
|
+
|
|
991
|
+
// Option 1: toSignal (preferred)
|
|
992
|
+
user = toSignal(this.userService.getUser());
|
|
993
|
+
|
|
994
|
+
// Option 2: takeUntilDestroyed
|
|
995
|
+
private destroyRef = inject(DestroyRef);
|
|
996
|
+
|
|
997
|
+
ngOnInit() {
|
|
998
|
+
this.userService.getUser().pipe(
|
|
999
|
+
takeUntilDestroyed(this.destroyRef)
|
|
1000
|
+
).subscribe(user => {
|
|
1001
|
+
this.userSignal.set(user);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
defaultChangeDetection:
|
|
1007
|
+
name: "Using Default Change Detection"
|
|
1008
|
+
description: "OnPush with signals is more performant"
|
|
1009
|
+
bad: |
|
|
1010
|
+
// ❌ Default change detection
|
|
1011
|
+
@Component({
|
|
1012
|
+
selector: 'app-user-list',
|
|
1013
|
+
template: `...`
|
|
1014
|
+
// No changeDetection specified = Default
|
|
1015
|
+
})
|
|
1016
|
+
export class UserListComponent {
|
|
1017
|
+
users: User[] = [];
|
|
1018
|
+
}
|
|
1019
|
+
good: |
|
|
1020
|
+
// ✅ OnPush with signals
|
|
1021
|
+
@Component({
|
|
1022
|
+
selector: 'app-user-list',
|
|
1023
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1024
|
+
template: `
|
|
1025
|
+
@for (user of users(); track user.id) {
|
|
1026
|
+
<app-user-card [user]="user" />
|
|
1027
|
+
}
|
|
1028
|
+
`
|
|
1029
|
+
})
|
|
1030
|
+
export class UserListComponent {
|
|
1031
|
+
users = signal<User[]>([]);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
classBasedGuards:
|
|
1035
|
+
name: "Class-Based Guards and Resolvers"
|
|
1036
|
+
description: "Use functional guards and resolvers instead"
|
|
1037
|
+
bad: |
|
|
1038
|
+
// ❌ Class-based guard
|
|
1039
|
+
@Injectable({ providedIn: 'root' })
|
|
1040
|
+
export class AuthGuard implements CanActivate {
|
|
1041
|
+
constructor(private auth: AuthService, private router: Router) {}
|
|
1042
|
+
|
|
1043
|
+
canActivate(): boolean {
|
|
1044
|
+
if (this.auth.isAuthenticated()) return true;
|
|
1045
|
+
this.router.navigate(['/login']);
|
|
1046
|
+
return false;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
good: |
|
|
1050
|
+
// ✅ Functional guard
|
|
1051
|
+
export const authGuard: CanActivateFn = () => {
|
|
1052
|
+
const auth = inject(AuthService);
|
|
1053
|
+
const router = inject(Router);
|
|
1054
|
+
|
|
1055
|
+
return auth.isAuthenticated()
|
|
1056
|
+
? true
|
|
1057
|
+
: router.createUrlTree(['/login']);
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
templateFunctionCalls:
|
|
1061
|
+
name: "Function Calls in Templates"
|
|
1062
|
+
description: "Use computed signals instead of functions in templates"
|
|
1063
|
+
bad: |
|
|
1064
|
+
// ❌ Function called on every change detection
|
|
1065
|
+
<p>{{ getFullName() }}</p>
|
|
1066
|
+
<p>{{ calculateTotal(items) }}</p>
|
|
1067
|
+
good: |
|
|
1068
|
+
// ✅ Use computed signals
|
|
1069
|
+
fullName = computed(() => `${this.user().firstName} ${this.user().lastName}`);
|
|
1070
|
+
total = computed(() => this.items().reduce((sum, i) => sum + i.price, 0));
|
|
1071
|
+
|
|
1072
|
+
// Template
|
|
1073
|
+
<p>{{ fullName() }}</p>
|
|
1074
|
+
<p>{{ total() }}</p>
|
|
1075
|
+
|
|
1076
|
+
nestedSubscribes:
|
|
1077
|
+
name: "Nested Subscribes"
|
|
1078
|
+
description: "Use higher-order mapping operators instead"
|
|
1079
|
+
bad: |
|
|
1080
|
+
// ❌ Nested subscribes
|
|
1081
|
+
this.route.params.subscribe(params => {
|
|
1082
|
+
this.userService.getUser(params['id']).subscribe(user => {
|
|
1083
|
+
this.user = user;
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
good: |
|
|
1087
|
+
// ✅ Use switchMap
|
|
1088
|
+
user = toSignal(
|
|
1089
|
+
this.route.params.pipe(
|
|
1090
|
+
switchMap(params => this.userService.getUser(params['id']))
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
hardcodedStringsInTemplates:
|
|
1095
|
+
name: "Hardcoded Strings in Templates"
|
|
1096
|
+
description: "Use i18n or constants for user-facing strings"
|
|
1097
|
+
bad: |
|
|
1098
|
+
// ❌ Hardcoded strings
|
|
1099
|
+
<button>Submit</button>
|
|
1100
|
+
<p>No users found</p>
|
|
1101
|
+
<h1>User Management</h1>
|
|
1102
|
+
good: |
|
|
1103
|
+
// ✅ Use i18n
|
|
1104
|
+
<button i18n>Submit</button>
|
|
1105
|
+
<p i18n>No users found</p>
|
|
1106
|
+
|
|
1107
|
+
// ✅ Or constants file
|
|
1108
|
+
<button>{{ LABELS.submit }}</button>
|