@eltonssouza/development-utility-kit 1.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/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +44 -0
- package/.claude/agents/stack-resolver.md +84 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/settings.json +44 -0
- package/.claude/skills/README.md +332 -0
- package/.claude/skills/active-project/SKILL.md +129 -0
- package/.claude/skills/api-integration-test/SKILL.md +64 -0
- package/.claude/skills/auto-test-guard/SKILL.md +237 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +60 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +187 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +79 -0
- package/.claude/skills/honcho-memory/SKILL.md +207 -0
- package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
- package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
- package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
- package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
- package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
- package/.claude/skills/honcho-memory/package.json +32 -0
- package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
- package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
- package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
- package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
- package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
- package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
- package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
- package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
- package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
- package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
- package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
- package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
- package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
- package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
- package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +58 -0
- package/.claude/skills/project-manager/SKILL.md +167 -0
- package/.claude/skills/quality-standards/SKILL.md +201 -0
- package/.claude/skills/quick-feature/SKILL.md +264 -0
- package/.claude/skills/run-sprint/SKILL.md +342 -0
- package/.claude/skills/scaffold/SKILL.md +58 -0
- package/.claude/skills/stack-discovery/SKILL.md +159 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +254 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +88 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +453 -0
- package/README.md +391 -0
- package/bin/cli.js +773 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/help.js +233 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/local-dir.js +69 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +199 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +709 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
- package/dashboard/public/content/docs/pipeline.en.md +400 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +500 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
- package/dashboard/public/content/manifest.json +102 -0
- package/dashboard/public/content/manual/backend.en.md +1138 -0
- package/dashboard/public/content/manual/existing-project.en.md +831 -0
- package/dashboard/public/content/manual/frontend.en.md +1065 -0
- package/dashboard/public/content/manual/fullstack.en.md +1508 -0
- package/dashboard/public/content/manual/mobile.en.md +866 -0
- package/dashboard/public/index.html +108 -0
- package/dashboard/public/style.css +610 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +403 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
---
|
|
2
|
+
stack: typescript/angular-18
|
|
3
|
+
versions_covered: "18.0.x — 18.2.x"
|
|
4
|
+
last_validated: 2026-05-27
|
|
5
|
+
validated_against: "sandbox — Angular 18.2.5 (legacy maintenance)"
|
|
6
|
+
status: active
|
|
7
|
+
pack_owner: "@elton"
|
|
8
|
+
security_review: 2026-05-27
|
|
9
|
+
next_review_due: 2027-05-27
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Angular 18
|
|
13
|
+
|
|
14
|
+
Knowledge pack for projects on Angular 18.x (released May 2024). Covers projects in legacy maintenance that have not yet migrated to Angular 19 (`angular-19.md`) or the harness default Angular 21 (`angular-21.md`). Standalone-first, Signals stable, new control flow stable; NO Signal Forms (only in 19+), NO Resource API (only in 19+).
|
|
15
|
+
|
|
16
|
+
## 1. When to use this pack
|
|
17
|
+
|
|
18
|
+
- Project declares `Frontend stack: Angular 18.x` in CLAUDE.md `## Project Identity`.
|
|
19
|
+
- `frontend/package.json` declares `@angular/core: ^18.0.0` through `^18.2.x`.
|
|
20
|
+
- **Greenfield: prefer `angular-21.md`** (harness default). For Angular 19 use `angular-19.md`.
|
|
21
|
+
- This pack exists for **maintained legacy** — apps in production not yet migrated.
|
|
22
|
+
|
|
23
|
+
## 2. Stack baseline (what this pack assumes)
|
|
24
|
+
|
|
25
|
+
| Component | Version range | Notes |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Angular | 18.0.x — 18.2.x | Released May 2024; LTS window short — plan upgrade |
|
|
28
|
+
| Node | 20.11+ (LTS) | Node 22 also supported |
|
|
29
|
+
| TypeScript | 5.4.x | Strict mode mandatory |
|
|
30
|
+
| RxJS | 7.8.x | Still required for HTTP / streams |
|
|
31
|
+
| ng-bootstrap | 17.x | NOT all components yet Signal-native |
|
|
32
|
+
| Jest | 29.x via jest-preset-angular 14 | Karma deprecated |
|
|
33
|
+
| @testing-library/angular | 17.x | Render API + userEvent |
|
|
34
|
+
| Playwright | 1.45+ | E2E + Chrome DevTools Protocol |
|
|
35
|
+
| @axe-core/playwright | 4.10+ | A11y on E2E flows |
|
|
36
|
+
| jest-axe | 9.x | A11y per component |
|
|
37
|
+
| @lhci/cli | 0.14+ | Lighthouse CI thresholds per ADR-007 |
|
|
38
|
+
| Zone.js | 0.14.x (still required) | Zoneless is EXPERIMENTAL in 18 — do NOT enable in prod |
|
|
39
|
+
|
|
40
|
+
**Key differences vs Angular 21:**
|
|
41
|
+
- Standalone is default, BUT NgModules still work (legacy apps may mix).
|
|
42
|
+
- Signals stable, but `effect()` still in **developer preview**.
|
|
43
|
+
- New control flow (`@if`, `@for`, `@switch`) stable since 17, mature in 18.
|
|
44
|
+
- **NO Signal Forms** — must use Reactive Forms (typed FormGroup).
|
|
45
|
+
- **NO Resource API** — must use RxJS + signal bridge (`toSignal`).
|
|
46
|
+
- `output()` function still optional — `@Output()` decorator still common.
|
|
47
|
+
- `inject()` preferred over constructor injection but both valid.
|
|
48
|
+
|
|
49
|
+
## 3. Project structure
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
frontend/
|
|
53
|
+
├── src/app/
|
|
54
|
+
│ ├── core/ ← auth, http interceptors, guards, singletons
|
|
55
|
+
│ │ ├── auth/
|
|
56
|
+
│ │ ├── interceptors/
|
|
57
|
+
│ │ └── guards/
|
|
58
|
+
│ ├── shared/ ← reusable standalone components, pipes, directives
|
|
59
|
+
│ ├── features/ ← lazy-loaded feature folders
|
|
60
|
+
│ │ └── products/
|
|
61
|
+
│ │ ├── product-list.component.ts
|
|
62
|
+
│ │ ├── product-list.component.html
|
|
63
|
+
│ │ ├── product-list.component.scss
|
|
64
|
+
│ │ └── product.service.ts
|
|
65
|
+
│ ├── app.config.ts ← root ApplicationConfig (providers)
|
|
66
|
+
│ ├── app.routes.ts ← root routing with loadComponent
|
|
67
|
+
│ └── app.component.{ts,html,scss}
|
|
68
|
+
├── src/environments/
|
|
69
|
+
│ ├── environment.ts
|
|
70
|
+
│ └── environment.prod.ts
|
|
71
|
+
├── src/styles/
|
|
72
|
+
└── angular.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 4. Code patterns
|
|
76
|
+
|
|
77
|
+
### THREE SEPARATE FILES (HARD RULE — harness universal)
|
|
78
|
+
|
|
79
|
+
Every component WITH a template uses 3 physical files: `name.component.ts`, `name.component.html`, `name.component.scss`. **NEVER** inline `template:` or `styles:` / `styleUrls: ['...inline...']` in the `.ts`. No size threshold, no exception. Violation = hard block in `code-reviewer` / `tech-lead`.
|
|
80
|
+
|
|
81
|
+
### Standalone component with Signal state
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// product-list.component.ts
|
|
85
|
+
import { Component, signal, ChangeDetectionStrategy, inject, OnInit } from '@angular/core';
|
|
86
|
+
import { CommonModule } from '@angular/common';
|
|
87
|
+
import { ProductService, Product } from './product.service';
|
|
88
|
+
|
|
89
|
+
@Component({
|
|
90
|
+
selector: 'app-product-list',
|
|
91
|
+
standalone: true,
|
|
92
|
+
imports: [CommonModule],
|
|
93
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
94
|
+
templateUrl: './product-list.component.html',
|
|
95
|
+
styleUrl: './product-list.component.scss',
|
|
96
|
+
})
|
|
97
|
+
export class ProductListComponent implements OnInit {
|
|
98
|
+
private svc = inject(ProductService);
|
|
99
|
+
|
|
100
|
+
readonly products = signal<Product[]>([]);
|
|
101
|
+
readonly loading = signal(false);
|
|
102
|
+
readonly error = signal<string | null>(null);
|
|
103
|
+
|
|
104
|
+
ngOnInit(): void {
|
|
105
|
+
this.loading.set(true);
|
|
106
|
+
this.svc.list().subscribe({
|
|
107
|
+
next: (data) => { this.products.set(data); this.loading.set(false); },
|
|
108
|
+
error: (e) => { this.error.set(e.message); this.loading.set(false); },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Rule**: `inject()` over constructor injection; `OnPush` always; `signal()` for local state; never expose `WritableSignal` publicly — wrap or use `asReadonly()`.
|
|
115
|
+
|
|
116
|
+
### Template using new control flow
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<!-- product-list.component.html -->
|
|
120
|
+
@if (loading()) {
|
|
121
|
+
<p>Loading...</p>
|
|
122
|
+
} @else if (error()) {
|
|
123
|
+
<p class="error">{{ error() }}</p>
|
|
124
|
+
} @else {
|
|
125
|
+
<ul>
|
|
126
|
+
@for (p of products(); track p.id) {
|
|
127
|
+
<li>{{ p.name }} — {{ p.price | currency }}</li>
|
|
128
|
+
} @empty {
|
|
129
|
+
<li>No products.</li>
|
|
130
|
+
}
|
|
131
|
+
</ul>
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
**Rule**: prefer `@if` / `@for` / `@switch` over `*ngIf` / `*ngFor` (faster, type-narrowing, no extra imports). `track` is **mandatory** on `@for`.
|
|
136
|
+
|
|
137
|
+
### Reactive Forms (Signal Forms only in 19+)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// product-form.component.ts
|
|
141
|
+
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
142
|
+
|
|
143
|
+
@Component({
|
|
144
|
+
selector: 'app-product-form',
|
|
145
|
+
standalone: true,
|
|
146
|
+
imports: [ReactiveFormsModule],
|
|
147
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
148
|
+
templateUrl: './product-form.component.html',
|
|
149
|
+
styleUrl: './product-form.component.scss',
|
|
150
|
+
})
|
|
151
|
+
export class ProductFormComponent {
|
|
152
|
+
private fb = inject(FormBuilder);
|
|
153
|
+
|
|
154
|
+
readonly form = this.fb.nonNullable.group({
|
|
155
|
+
name: ['', [Validators.required, Validators.maxLength(120)]],
|
|
156
|
+
email: ['', [Validators.required, Validators.email]],
|
|
157
|
+
price: [0, [Validators.required, Validators.min(0)]],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
submit(): void {
|
|
161
|
+
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
|
|
162
|
+
const payload = this.form.getRawValue();
|
|
163
|
+
// ...dispatch
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Rule**: always `fb.nonNullable.group(...)` for typed forms; never `any`; mark all touched on invalid submit.
|
|
169
|
+
|
|
170
|
+
### Lazy routing
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// app.routes.ts
|
|
174
|
+
import { Routes } from '@angular/router';
|
|
175
|
+
|
|
176
|
+
export const routes: Routes = [
|
|
177
|
+
{ path: '', pathMatch: 'full', redirectTo: 'products' },
|
|
178
|
+
{
|
|
179
|
+
path: 'products',
|
|
180
|
+
loadComponent: () =>
|
|
181
|
+
import('./features/products/product-list.component').then(m => m.ProductListComponent),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
path: 'admin',
|
|
185
|
+
canMatch: [adminGuard],
|
|
186
|
+
loadChildren: () => import('./features/admin/admin.routes').then(m => m.routes),
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Rule**: every feature lazy-loaded via `loadComponent` or `loadChildren`. Never eager-import features into root.
|
|
192
|
+
|
|
193
|
+
### HttpClient + functional interceptor
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// core/interceptors/auth.interceptor.ts
|
|
197
|
+
import { HttpInterceptorFn } from '@angular/common/http';
|
|
198
|
+
import { inject } from '@angular/core';
|
|
199
|
+
import { AuthService } from '../auth/auth.service';
|
|
200
|
+
|
|
201
|
+
export const authInterceptor: HttpInterceptorFn = (req, next) => {
|
|
202
|
+
const token = inject(AuthService).token();
|
|
203
|
+
if (!token) return next(req);
|
|
204
|
+
return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// app.config.ts
|
|
208
|
+
import { ApplicationConfig } from '@angular/core';
|
|
209
|
+
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
|
210
|
+
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
|
211
|
+
|
|
212
|
+
export const appConfig: ApplicationConfig = {
|
|
213
|
+
providers: [
|
|
214
|
+
provideHttpClient(withInterceptors([authInterceptor])),
|
|
215
|
+
provideRouter(routes, withComponentInputBinding()),
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Rule**: functional interceptors (`HttpInterceptorFn`) over class-based; typed `HttpClient<T>`; never raw `fetch()` in Angular code.
|
|
221
|
+
|
|
222
|
+
### Bridging RxJS to Signals (`toSignal`)
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
import { toSignal } from '@angular/core/rxjs-interop';
|
|
226
|
+
|
|
227
|
+
readonly user = toSignal(this.authService.user$, { initialValue: null });
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Rule**: prefer `toSignal` to bridge existing `Observable` services into Signal-based components.
|
|
231
|
+
|
|
232
|
+
## 5. Testing
|
|
233
|
+
|
|
234
|
+
### Unit + component (Jest + Testing Library)
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { render, screen } from '@testing-library/angular';
|
|
238
|
+
import userEvent from '@testing-library/user-event';
|
|
239
|
+
import { ProductListComponent } from './product-list.component';
|
|
240
|
+
|
|
241
|
+
describe('ProductListComponent', () => {
|
|
242
|
+
test('renders product names', async () => {
|
|
243
|
+
await render(ProductListComponent, {
|
|
244
|
+
providers: [{ provide: ProductService, useValue: { list: () => of([{ id: '1', name: 'widget', price: 10 }]) } }],
|
|
245
|
+
});
|
|
246
|
+
expect(await screen.findByText('widget')).toBeInTheDocument();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('shows error on failure', async () => {
|
|
250
|
+
await render(ProductListComponent, {
|
|
251
|
+
providers: [{ provide: ProductService, useValue: { list: () => throwError(() => new Error('boom')) } }],
|
|
252
|
+
});
|
|
253
|
+
expect(await screen.findByText(/boom/)).toBeInTheDocument();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### A11y per component (jest-axe — ADR-007)
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
262
|
+
expect.extend(toHaveNoViolations);
|
|
263
|
+
|
|
264
|
+
test('no a11y violations', async () => {
|
|
265
|
+
const { container } = await render(ProductListComponent, { providers: [/* ... */] });
|
|
266
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### E2E (Playwright) + @axe-core/playwright
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { test, expect } from '@playwright/test';
|
|
274
|
+
import AxeBuilder from '@axe-core/playwright';
|
|
275
|
+
|
|
276
|
+
test('product list is accessible', async ({ page }) => {
|
|
277
|
+
await page.goto('/products');
|
|
278
|
+
await expect(page.getByRole('list')).toBeVisible();
|
|
279
|
+
const results = await new AxeBuilder({ page }).analyze();
|
|
280
|
+
const serious = results.violations.filter(v => v.impact === 'serious' || v.impact === 'critical');
|
|
281
|
+
expect(serious).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Lighthouse CI
|
|
286
|
+
|
|
287
|
+
`lighthouserc.json` thresholds per ADR-007: performance ≥ 0.80, LCP ≤ 2500ms, CLS ≤ 0.1, TBT ≤ 300ms.
|
|
288
|
+
|
|
289
|
+
## 6. Build & run commands
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
npm install
|
|
293
|
+
npm start # ng serve, port 4200
|
|
294
|
+
npm run build -- --configuration=production # prod build (AOT + budget)
|
|
295
|
+
npm test -- --watchAll=false --coverage # Jest with coverage
|
|
296
|
+
npx playwright test # E2E
|
|
297
|
+
npx playwright test --ui # E2E debug
|
|
298
|
+
npx lhci autorun # Lighthouse CI thresholds
|
|
299
|
+
npm run lint # ESLint --max-warnings 0
|
|
300
|
+
npm audit --audit-level=high # CVE scan
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## 7. Security (per ADR-007 + ADR-027 — MANDATORY)
|
|
304
|
+
|
|
305
|
+
### 7.1 Authentication & Authorization
|
|
306
|
+
|
|
307
|
+
- **JWT validation** via functional `HttpInterceptorFn` injecting Authorization header.
|
|
308
|
+
- **Route guards**: `CanActivate` / `CanMatch` functional guards with `inject()`.
|
|
309
|
+
- **Token storage**: prefer `HttpOnly Secure SameSite=Lax` cookies (set by backend). Fall back to `sessionStorage` for short-lived tokens; NEVER `localStorage` for tokens (XSS-readable).
|
|
310
|
+
- **Refresh token**: rotate on every use; revoke on logout.
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
export const authGuard: CanMatchFn = () => {
|
|
314
|
+
const auth = inject(AuthService);
|
|
315
|
+
const router = inject(Router);
|
|
316
|
+
return auth.isAuthenticated() ? true : router.parseUrl('/login');
|
|
317
|
+
};
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 7.2 CORS
|
|
321
|
+
|
|
322
|
+
CORS is backend-side. Frontend uses absolute URLs from `environment.apiBaseUrl`. Backend MUST explicit-list `https://app.example.com` — NEVER `*` in production. Reference companion backend pack.
|
|
323
|
+
|
|
324
|
+
### 7.3 Validation & XSS
|
|
325
|
+
|
|
326
|
+
- Angular **auto-escapes** by default in interpolation (`{{ }}`) and property binding.
|
|
327
|
+
- **NEVER use `[innerHTML]`** with user content unless sanitized via `DomSanitizer.sanitize(SecurityContext.HTML, value)`.
|
|
328
|
+
- **NEVER use `bypassSecurityTrustHtml`** unless absolutely necessary + reviewed by `security-engineer`.
|
|
329
|
+
- Reactive Forms validators on every input. Backend re-validates (defense in depth).
|
|
330
|
+
- File uploads: validate MIME type + size client-side AND server-side.
|
|
331
|
+
|
|
332
|
+
### 7.4 Secrets management
|
|
333
|
+
|
|
334
|
+
- **NEVER** commit secrets in `environment.ts` / `environment.prod.ts`.
|
|
335
|
+
- Use build-time substitution: `environment.prod.ts` reads from CI env vars (`API_BASE_URL`, etc.) injected during `ng build`.
|
|
336
|
+
- Public values only (API base URL, feature flags). API keys, tokens, secrets stay server-side.
|
|
337
|
+
- Per-developer config: `environment.local.ts` in `.gitignore`.
|
|
338
|
+
|
|
339
|
+
### 7.5 Rate limiting
|
|
340
|
+
|
|
341
|
+
Frontend cannot enforce rate limiting — it lives in backend / API gateway. Frontend MUST handle 429 responses gracefully:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
catchError((err: HttpErrorResponse) => {
|
|
345
|
+
if (err.status === 429) {
|
|
346
|
+
this.toast.warning(`Too many requests. Retry after ${err.headers.get('Retry-After')}s.`);
|
|
347
|
+
}
|
|
348
|
+
return throwError(() => err);
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### 7.6 OWASP Top 10 mapping
|
|
353
|
+
|
|
354
|
+
| OWASP | Mitigation in Angular 18 |
|
|
355
|
+
|---|---|
|
|
356
|
+
| A01 Broken Access Control | Route guards (`CanMatch`/`CanActivate`); UI gating + backend re-check; never trust client RBAC alone |
|
|
357
|
+
| A02 Cryptographic Failures | HTTPS only (HSTS via backend); JWT signed RS256; NEVER store tokens in `localStorage` |
|
|
358
|
+
| A03 Injection | Angular auto-escape; `DomSanitizer` for trusted HTML; never string-build URLs without `encodeURIComponent` |
|
|
359
|
+
| A04 Insecure Design | Threat model per feature; PII flows reviewed by `security-engineer` |
|
|
360
|
+
| A05 Security Misconfiguration | Strict CSP via meta tag or backend header; `Content-Security-Policy: default-src 'self'` |
|
|
361
|
+
| A06 Vulnerable Components | `npm audit --audit-level=high` in CI; Renovate/Dependabot; remove unused deps |
|
|
362
|
+
| A07 Auth Failures | Short-lived access tokens; refresh rotation; auto-logout on 401; MFA flow |
|
|
363
|
+
| A08 Data Integrity | Subresource Integrity (SRI) on external scripts; webpack output integrity hashes |
|
|
364
|
+
| A09 Logging Failures | Never `console.log` PII or tokens; production builds strip `console.*` via `terserOptions.compress.drop_console` |
|
|
365
|
+
| A10 SSRF | N/A frontend; backend enforces outbound allowlist |
|
|
366
|
+
|
|
367
|
+
### 7.7 LGPD / GDPR specifics
|
|
368
|
+
|
|
369
|
+
- **Cookie consent**: opt-in banner before any non-essential cookie (analytics, ads). Default reject.
|
|
370
|
+
- **PII in logs**: never `console.log` user objects; redact email/CPF/phone via a logging wrapper.
|
|
371
|
+
- **Data subject access**: page that requests user data export from backend; render machine-readable JSON download.
|
|
372
|
+
- **Right to deletion**: UI to request deletion; backend soft-deletes + audits.
|
|
373
|
+
- **Analytics**: anonymize IP; respect `Do Not Track` header.
|
|
374
|
+
|
|
375
|
+
## 8. Anti-patterns (block in code-review)
|
|
376
|
+
|
|
377
|
+
| Bad | Good | Why |
|
|
378
|
+
|---|---|---|
|
|
379
|
+
| `template:` inline | separate `.html` via `templateUrl` | HARD RULE — harness universal |
|
|
380
|
+
| `styles:` inline | separate `.scss` via `styleUrl` | HARD RULE — harness universal |
|
|
381
|
+
| NgModule for new feature | standalone component | Angular 18 is standalone-first |
|
|
382
|
+
| `any` in TS | explicit type or generic | TS strict mode mandatory |
|
|
383
|
+
| field injection (`@Inject()` on property) | `inject()` in field initializer OR constructor | testable + immutable |
|
|
384
|
+
| `ChangeDetectionStrategy.Default` | `OnPush` always | perf + predictability |
|
|
385
|
+
| `signal().push(item)` mutation | `signal.update(arr => [...arr, item])` | signals require new ref |
|
|
386
|
+
| `[innerHTML]="userContent"` | `DomSanitizer.sanitize(SecurityContext.HTML, value)` | XSS vector |
|
|
387
|
+
| `localStorage.setItem('token', ...)` | HttpOnly cookie OR `sessionStorage` short-lived | XSS-readable storage |
|
|
388
|
+
| `*ngIf` / `*ngFor` in new code | `@if` / `@for` | Faster + type-narrowing |
|
|
389
|
+
| `@for` without `track` | `@for (... ; track item.id)` | Compile error in 18, mandatory |
|
|
390
|
+
| Reactive Forms without `nonNullable` | `fb.nonNullable.group(...)` | typed forms, no `null \| undefined` |
|
|
391
|
+
| `Subject` without `takeUntilDestroyed()` | `takeUntilDestroyed()` (Angular 16+) | memory leaks |
|
|
392
|
+
| direct `fetch()` call | `HttpClient` typed | misses interceptors, retries, error handling |
|
|
393
|
+
| eager-loaded feature in root | `loadComponent` / `loadChildren` lazy | bundle size |
|
|
394
|
+
| Zoneless enabled in prod | keep Zone.js | zoneless is EXPERIMENTAL in 18 |
|
|
395
|
+
|
|
396
|
+
## 9. Migration hints — Angular 18 → 19
|
|
397
|
+
|
|
398
|
+
When migrating a project from this pack to `angular-19.md`:
|
|
399
|
+
|
|
400
|
+
- **Signal Forms** arrive — alternative to Reactive Forms (opt-in, not breaking).
|
|
401
|
+
- **Resource API** (`resource()`) — async data loading as a signal, replaces many `toSignal(httpService.get(...))` patterns.
|
|
402
|
+
- **`linkedSignal()`** — derived state that can be reset, replaces some `computed` + `effect` patterns.
|
|
403
|
+
- **`effect()` goes stable** — safe to use broadly.
|
|
404
|
+
- **`output()` function** preferred over `@Output()` decorator (cleaner API, no decorator metadata).
|
|
405
|
+
- **Incremental hydration** for SSR — opt-in via `withIncrementalHydration()`.
|
|
406
|
+
- Zoneless leaves dev-preview in 19 — still opt-in; evaluate carefully.
|
|
407
|
+
- **Hot Module Replacement** for templates becomes stable.
|
|
408
|
+
- Use `migrator` agent — it knows both packs and proposes ADR per major cross.
|
|
409
|
+
- Run `ng update @angular/core@19 @angular/cli@19` and review the generated migration log.
|
|
410
|
+
|
|
411
|
+
## 10. References
|
|
412
|
+
|
|
413
|
+
- ADR-007 (Senior+ gate thresholds — a11y, Lighthouse, pyramid)
|
|
414
|
+
- ADR-026 (Generic agents + packs architecture)
|
|
415
|
+
- ADR-027 (Pack governance — frontmatter, security review cadence)
|
|
416
|
+
- ADR-029 (Canonical pack format)
|
|
417
|
+
- Angular 18 release notes: https://blog.angular.dev/angular-v18-is-now-available-e79d5ac0affe
|
|
418
|
+
- Angular Update Guide: https://angular.dev/update-guide
|
|
419
|
+
- OWASP Top 10 (2021): https://owasp.org/Top10/
|
|
420
|
+
- ng-bootstrap 17 docs: https://ng-bootstrap.github.io/
|