@malamute/ai-rules 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.
Files changed (46) hide show
  1. package/README.md +174 -0
  2. package/bin/cli.js +5 -0
  3. package/configs/_shared/.claude/commands/fix-issue.md +38 -0
  4. package/configs/_shared/.claude/commands/generate-tests.md +49 -0
  5. package/configs/_shared/.claude/commands/review-pr.md +77 -0
  6. package/configs/_shared/.claude/rules/accessibility.md +270 -0
  7. package/configs/_shared/.claude/rules/performance.md +226 -0
  8. package/configs/_shared/.claude/rules/security.md +188 -0
  9. package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
  10. package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
  11. package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
  12. package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
  13. package/configs/_shared/CLAUDE.md +174 -0
  14. package/configs/angular/.claude/rules/components.md +257 -0
  15. package/configs/angular/.claude/rules/state.md +250 -0
  16. package/configs/angular/.claude/rules/testing.md +422 -0
  17. package/configs/angular/.claude/settings.json +31 -0
  18. package/configs/angular/CLAUDE.md +251 -0
  19. package/configs/dotnet/.claude/rules/api.md +370 -0
  20. package/configs/dotnet/.claude/rules/architecture.md +199 -0
  21. package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
  22. package/configs/dotnet/.claude/rules/testing.md +389 -0
  23. package/configs/dotnet/.claude/settings.json +9 -0
  24. package/configs/dotnet/CLAUDE.md +319 -0
  25. package/configs/nestjs/.claude/rules/auth.md +321 -0
  26. package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
  27. package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
  28. package/configs/nestjs/.claude/rules/modules.md +215 -0
  29. package/configs/nestjs/.claude/rules/testing.md +315 -0
  30. package/configs/nestjs/.claude/rules/validation.md +279 -0
  31. package/configs/nestjs/.claude/settings.json +15 -0
  32. package/configs/nestjs/CLAUDE.md +263 -0
  33. package/configs/nextjs/.claude/rules/components.md +211 -0
  34. package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
  35. package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
  36. package/configs/nextjs/.claude/rules/testing.md +315 -0
  37. package/configs/nextjs/.claude/settings.json +29 -0
  38. package/configs/nextjs/CLAUDE.md +376 -0
  39. package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
  40. package/configs/python/.claude/rules/fastapi.md +272 -0
  41. package/configs/python/.claude/rules/flask.md +332 -0
  42. package/configs/python/.claude/rules/testing.md +374 -0
  43. package/configs/python/.claude/settings.json +18 -0
  44. package/configs/python/CLAUDE.md +273 -0
  45. package/package.json +41 -0
  46. package/src/install.js +315 -0
@@ -0,0 +1,422 @@
1
+ ---
2
+ paths:
3
+ - "**/*.spec.ts"
4
+ - "**/*.test.ts"
5
+ - "**/*.e2e.ts"
6
+ ---
7
+
8
+ # Testing Guidelines
9
+
10
+ ## Framework
11
+
12
+ - **Vitest** (preferred for Angular 21+)
13
+ - **Playwright** for E2E testing
14
+ - Jest as fallback when required
15
+
16
+ ## Critical Rules
17
+
18
+ ### No `.subscribe()` for RxJS Testing
19
+
20
+ Never use `.subscribe()` for testing RxJS streams (services, effects, observables). Always use `TestScheduler` with marble testing.
21
+
22
+ ```typescript
23
+ // FORBIDDEN - async callback hell
24
+ it('should load users', (done) => {
25
+ service.getUsers().subscribe(users => {
26
+ expect(users.length).toBe(2);
27
+ done();
28
+ });
29
+ });
30
+
31
+ // FORBIDDEN - fakeAsync with subscribe
32
+ it('should load users', fakeAsync(() => {
33
+ let result: User[];
34
+ service.getUsers().subscribe(users => result = users);
35
+ tick();
36
+ expect(result.length).toBe(2);
37
+ }));
38
+
39
+ // CORRECT - marble testing with TestScheduler
40
+ it('should load users', () => {
41
+ testScheduler.run(({ cold, expectObservable }) => {
42
+ const users = [{ id: '1' }, { id: '2' }];
43
+ jest.spyOn(service, 'getUsers').mockReturnValue(cold('--a|', { a: users }));
44
+
45
+ expectObservable(service.getUsers()).toBe('--a|', { a: users });
46
+ });
47
+ });
48
+ ```
49
+
50
+ **Exception**: Component `output()` testing uses `.subscribe()` because OutputEmitterRef is event-based, not RxJS stream-based.
51
+
52
+ ## Test File Structure
53
+
54
+ ```
55
+ component.ts
56
+ component.spec.ts # Unit tests
57
+
58
+ # or in separate folder
59
+ __tests__/
60
+ component.spec.ts
61
+ ```
62
+
63
+ ## Component Testing (Zoneless)
64
+
65
+ Angular 21 is zoneless by default - tests must not rely on zone.js:
66
+
67
+ ```typescript
68
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
69
+
70
+ describe('UserListComponent', () => {
71
+ let component: UserListComponent;
72
+ let fixture: ComponentFixture<UserListComponent>;
73
+
74
+ beforeEach(async () => {
75
+ await TestBed.configureTestingModule({
76
+ imports: [UserListComponent],
77
+ // No need for provideZonelessChangeDetection() - it's the default in Angular 21
78
+ }).compileComponents();
79
+
80
+ fixture = TestBed.createComponent(UserListComponent);
81
+ component = fixture.componentInstance;
82
+ });
83
+
84
+ it('should display users', async () => {
85
+ // Set input signal
86
+ fixture.componentRef.setInput('users', [
87
+ { id: '1', name: 'John' },
88
+ { id: '2', name: 'Jane' },
89
+ ]);
90
+
91
+ // Use whenStable() instead of detectChanges() for zoneless
92
+ await fixture.whenStable();
93
+
94
+ const items = fixture.nativeElement.querySelectorAll('.user-item');
95
+ expect(items.length).toBe(2);
96
+ });
97
+
98
+ it('should emit on user click', async () => {
99
+ const spy = vi.fn();
100
+
101
+ // Subscribe to output signal
102
+ const subscription = component.userSelected.subscribe(spy);
103
+
104
+ fixture.componentRef.setInput('users', [{ id: '1', name: 'John' }]);
105
+ await fixture.whenStable();
106
+
107
+ fixture.nativeElement.querySelector('.user-item').click();
108
+ await fixture.whenStable();
109
+
110
+ expect(spy).toHaveBeenCalledWith({ id: '1', name: 'John' });
111
+ subscription.unsubscribe();
112
+ });
113
+ });
114
+ ```
115
+
116
+ ### Key Testing Patterns for Zoneless
117
+
118
+ ```typescript
119
+ // Use whenStable() instead of detectChanges()
120
+ await fixture.whenStable();
121
+
122
+ // For signal inputs
123
+ fixture.componentRef.setInput('inputName', value);
124
+
125
+ // For checking signal values directly
126
+ expect(component.mySignal()).toBe(expectedValue);
127
+
128
+ // For outputs (OutputEmitterRef)
129
+ const spy = vi.fn();
130
+ component.myOutput.subscribe(spy);
131
+ ```
132
+
133
+ ## Service Mocking with createSpyFromClass
134
+
135
+ Use `jasmine-auto-spies` or equivalent:
136
+
137
+ ```typescript
138
+ import { createSpyFromClass, Spy } from 'jasmine-auto-spies';
139
+
140
+ describe('UserEffects', () => {
141
+ let userService: Spy<UserService>;
142
+
143
+ beforeEach(() => {
144
+ userService = createSpyFromClass(UserService);
145
+
146
+ TestBed.configureTestingModule({
147
+ providers: [
148
+ { provide: UserService, useValue: userService },
149
+ ],
150
+ });
151
+ });
152
+
153
+ it('should load users', () => {
154
+ userService.getAll.and.returnValue(of([{ id: '1', name: 'John' }]));
155
+ // ... test
156
+ });
157
+
158
+ it('should handle error', () => {
159
+ userService.getAll.and.returnValue(throwError(() => new Error('API Error')));
160
+ // ... test
161
+ });
162
+ });
163
+ ```
164
+
165
+ ## NgRx Effects Testing with Marble
166
+
167
+ Use `TestScheduler` for RxJS marble testing:
168
+
169
+ ```typescript
170
+ import { TestScheduler } from 'rxjs/testing';
171
+
172
+ describe('User Effects', () => {
173
+ let testScheduler: TestScheduler;
174
+ let actions$: Observable<Action>;
175
+ let userService: Spy<UserService>;
176
+
177
+ beforeEach(() => {
178
+ testScheduler = new TestScheduler((actual, expected) => {
179
+ expect(actual).toEqual(expected);
180
+ });
181
+
182
+ userService = createSpyFromClass(UserService);
183
+ });
184
+
185
+ it('should load users successfully', () => {
186
+ testScheduler.run(({ hot, cold, expectObservable }) => {
187
+ // Setup
188
+ const users = [{ id: '1', name: 'John' }];
189
+ actions$ = hot('-a', { a: UserActions.loadUsers() });
190
+ userService.getAll.and.returnValue(cold('--b|', { b: users }));
191
+
192
+ // Execute
193
+ const effect = loadUsers$(actions$, userService);
194
+
195
+ // Assert
196
+ expectObservable(effect).toBe('---c', {
197
+ c: UserActions.loadUsersSuccess({ users }),
198
+ });
199
+ });
200
+ });
201
+
202
+ it('should handle load error', () => {
203
+ testScheduler.run(({ hot, cold, expectObservable }) => {
204
+ const error = new Error('API Error');
205
+ actions$ = hot('-a', { a: UserActions.loadUsers() });
206
+ userService.getAll.and.returnValue(cold('--#', {}, error));
207
+
208
+ const effect = loadUsers$(actions$, userService);
209
+
210
+ expectObservable(effect).toBe('---c', {
211
+ c: UserActions.loadUsersFailure({ error: 'API Error' }),
212
+ });
213
+ });
214
+ });
215
+ });
216
+ ```
217
+
218
+ ## Marble Syntax Reference
219
+
220
+ | Symbol | Meaning |
221
+ |--------|---------|
222
+ | `-` | 10ms of time passing |
223
+ | `a-z` | Emission with value from values object |
224
+ | `\|` | Complete |
225
+ | `#` | Error |
226
+ | `^` | Subscription point |
227
+ | `!` | Unsubscription point |
228
+ | `()` | Sync grouping |
229
+
230
+ ## Selector Testing
231
+
232
+ Test selectors in isolation:
233
+
234
+ ```typescript
235
+ describe('User Selectors', () => {
236
+ const initialState: UserState = {
237
+ ids: ['1', '2'],
238
+ entities: {
239
+ '1': { id: '1', name: 'John', active: true },
240
+ '2': { id: '2', name: 'Jane', active: false },
241
+ },
242
+ selectedId: '1',
243
+ loading: false,
244
+ error: null,
245
+ };
246
+
247
+ it('should select all users', () => {
248
+ const result = selectAllUsers.projector(initialState);
249
+ expect(result.length).toBe(2);
250
+ });
251
+
252
+ it('should select active users only', () => {
253
+ const allUsers = selectAllUsers.projector(initialState);
254
+ const result = selectActiveUsers.projector(allUsers);
255
+ expect(result.length).toBe(1);
256
+ expect(result[0].name).toBe('John');
257
+ });
258
+
259
+ it('should select current user', () => {
260
+ const entities = selectUserEntities.projector(initialState);
261
+ const result = selectSelectedUser.projector(entities, '1');
262
+ expect(result?.name).toBe('John');
263
+ });
264
+ });
265
+ ```
266
+
267
+ ## Reducer Testing
268
+
269
+ ```typescript
270
+ describe('User Reducer', () => {
271
+ it('should set loading on loadUsers', () => {
272
+ const state = userReducer(initialUserState, UserActions.loadUsers());
273
+ expect(state.loading).toBe(true);
274
+ expect(state.error).toBeNull();
275
+ });
276
+
277
+ it('should add users on loadUsersSuccess', () => {
278
+ const users = [{ id: '1', name: 'John' }];
279
+ const state = userReducer(
280
+ { ...initialUserState, loading: true },
281
+ UserActions.loadUsersSuccess({ users })
282
+ );
283
+
284
+ expect(state.loading).toBe(false);
285
+ expect(state.ids).toEqual(['1']);
286
+ expect(state.entities['1'].name).toBe('John');
287
+ });
288
+ });
289
+ ```
290
+
291
+ ## Test Organization
292
+
293
+ ```typescript
294
+ describe('FeatureName', () => {
295
+ // Setup
296
+ beforeEach(() => { /* ... */ });
297
+ afterEach(() => { /* ... */ });
298
+
299
+ // Group by behavior
300
+ describe('when initialized', () => {
301
+ it('should have default state', () => {});
302
+ });
303
+
304
+ describe('when loading data', () => {
305
+ it('should show loading indicator', () => {});
306
+ it('should handle success', () => {});
307
+ it('should handle error', () => {});
308
+ });
309
+
310
+ describe('when user interacts', () => {
311
+ it('should emit selection event', () => {});
312
+ });
313
+ });
314
+ ```
315
+
316
+ ## E2E Testing with Playwright
317
+
318
+ Prefer Playwright for E2E tests when possible.
319
+
320
+ ### Setup
321
+
322
+ ```typescript
323
+ // playwright.config.ts
324
+ import { defineConfig } from '@playwright/test';
325
+
326
+ export default defineConfig({
327
+ testDir: './e2e',
328
+ use: {
329
+ baseURL: 'http://localhost:4200',
330
+ trace: 'on-first-retry',
331
+ },
332
+ webServer: {
333
+ command: 'nx serve app-name',
334
+ url: 'http://localhost:4200',
335
+ reuseExistingServer: !process.env.CI,
336
+ },
337
+ });
338
+ ```
339
+
340
+ ### E2E Test Example
341
+
342
+ ```typescript
343
+ // e2e/user-flow.e2e.ts
344
+ import { test, expect } from '@playwright/test';
345
+
346
+ test.describe('User Management', () => {
347
+ test.beforeEach(async ({ page }) => {
348
+ await page.goto('/users');
349
+ });
350
+
351
+ test('should display user list', async ({ page }) => {
352
+ await expect(page.getByTestId('user-list')).toBeVisible();
353
+ await expect(page.getByTestId('user-item')).toHaveCount(3);
354
+ });
355
+
356
+ test('should filter users by name', async ({ page }) => {
357
+ await page.getByPlaceholder('Search users').fill('John');
358
+ await expect(page.getByTestId('user-item')).toHaveCount(1);
359
+ await expect(page.getByText('John Doe')).toBeVisible();
360
+ });
361
+
362
+ test('should navigate to user details', async ({ page }) => {
363
+ await page.getByTestId('user-item').first().click();
364
+ await expect(page).toHaveURL(/\/users\/\d+/);
365
+ await expect(page.getByTestId('user-details')).toBeVisible();
366
+ });
367
+ });
368
+ ```
369
+
370
+ ### Page Object Pattern
371
+
372
+ ```typescript
373
+ // e2e/pages/user-list.page.ts
374
+ import { Page, Locator } from '@playwright/test';
375
+
376
+ export class UserListPage {
377
+ readonly page: Page;
378
+ readonly searchInput: Locator;
379
+ readonly userItems: Locator;
380
+ readonly addUserButton: Locator;
381
+
382
+ constructor(page: Page) {
383
+ this.page = page;
384
+ this.searchInput = page.getByPlaceholder('Search users');
385
+ this.userItems = page.getByTestId('user-item');
386
+ this.addUserButton = page.getByRole('button', { name: 'Add User' });
387
+ }
388
+
389
+ async goto(): Promise<void> {
390
+ await this.page.goto('/users');
391
+ }
392
+
393
+ async searchUser(name: string): Promise<void> {
394
+ await this.searchInput.fill(name);
395
+ }
396
+
397
+ async selectUser(index: number): Promise<void> {
398
+ await this.userItems.nth(index).click();
399
+ }
400
+ }
401
+ ```
402
+
403
+ ### Commands
404
+
405
+ ```bash
406
+ # Run E2E tests
407
+ nx e2e app-name-e2e
408
+
409
+ # Run with UI mode
410
+ nx e2e app-name-e2e --ui
411
+
412
+ # Run specific test file
413
+ nx e2e app-name-e2e --grep "User Management"
414
+ ```
415
+
416
+ ## Coverage Expectations
417
+
418
+ - Aim for >80% coverage on business logic
419
+ - 100% coverage on reducers and selectors
420
+ - Effects: test success and error paths (with marble)
421
+ - UI components: test inputs, outputs, rendering
422
+ - E2E: critical user flows
@@ -0,0 +1,31 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(nx serve *)",
5
+ "Bash(nx build *)",
6
+ "Bash(nx test *)",
7
+ "Bash(nx lint *)",
8
+ "Bash(nx run-many *)",
9
+ "Bash(nx affected *)",
10
+ "Bash(nx g *)",
11
+ "Bash(nx generate *)",
12
+ "Bash(npm run *)",
13
+ "Bash(npm install *)",
14
+ "Bash(npm ci)",
15
+ "Bash(npx nx *)",
16
+ "Read",
17
+ "Edit",
18
+ "Write"
19
+ ],
20
+ "deny": [
21
+ "Bash(rm -rf *)",
22
+ "Bash(nx reset)",
23
+ "Read(.env)",
24
+ "Read(.env.*)",
25
+ "Read(**/secrets/**)"
26
+ ]
27
+ },
28
+ "env": {
29
+ "NX_DAEMON": "true"
30
+ }
31
+ }
@@ -0,0 +1,251 @@
1
+ # Angular Project Guidelines
2
+
3
+ @../_shared/CLAUDE.md
4
+
5
+ ## Stack
6
+
7
+ - Angular 21+ (latest)
8
+ - Nx monorepo
9
+ - NgRx (store, effects, entity)
10
+ - Vitest
11
+ - TypeScript strict mode
12
+
13
+ ## Architecture - Nx Structure
14
+
15
+ ```
16
+ apps/
17
+ [app-name]/
18
+
19
+ libs/
20
+ [domain]/ # Ex: users, products, checkout
21
+ feature/ # Smart components, pages, routing (lazy-loaded)
22
+ data-access/ # Services API + NgRx state
23
+ src/lib/+state/ # Actions, reducers, effects, selectors
24
+ ui/ # Dumb/presentational components
25
+ util/ # Domain-specific helpers
26
+
27
+ shared/
28
+ ui/ # Reusable UI components
29
+ data-access/ # Shared services (auth, http interceptors)
30
+ util/ # Pure functions, helpers
31
+ ```
32
+
33
+ ### Dependency Rules (enforce via Nx tags)
34
+
35
+ | Type | Can import |
36
+ |------|------------|
37
+ | `feature` | Everything |
38
+ | `ui` | `ui`, `util` only |
39
+ | `data-access` | `data-access`, `util` only |
40
+ | `util` | `util` only |
41
+
42
+ ## Angular 21 - Core Principles
43
+
44
+ ### Zoneless by Default
45
+
46
+ - No zone.js - use signals for reactivity
47
+ - Use `ChangeDetectionStrategy.OnPush` on all components
48
+ - Never rely on zone.js for change detection
49
+
50
+ ### Signals Everywhere
51
+
52
+ ```typescript
53
+ // State
54
+ count = signal(0);
55
+ items = signal<Item[]>([]);
56
+
57
+ // Derived state
58
+ doubleCount = computed(() => this.count() * 2);
59
+ isEmpty = computed(() => this.items().length === 0);
60
+
61
+ // Effects for side effects
62
+ effect(() => {
63
+ console.log('Count changed:', this.count());
64
+ });
65
+ ```
66
+
67
+ ### Signal Forms (experimental but preferred)
68
+
69
+ ```typescript
70
+ // Use signal forms, NOT reactive forms
71
+ import { SignalForm } from '@angular/forms';
72
+
73
+ form = signalForm({
74
+ name: '',
75
+ email: '',
76
+ });
77
+
78
+ // Access values
79
+ form.value(); // { name: '', email: '' }
80
+ form.controls.name(); // ''
81
+ form.valid(); // boolean signal
82
+ ```
83
+
84
+ ### Standalone Components (Default)
85
+
86
+ - No NgModules for components
87
+ - `standalone: true` is the default - don't add it
88
+ - Import dependencies directly in component
89
+ - Always use separate template files (`.html`)
90
+
91
+ ```typescript
92
+ @Component({
93
+ selector: 'app-example',
94
+ imports: [RouterModule],
95
+ changeDetection: ChangeDetectionStrategy.OnPush,
96
+ templateUrl: './example.component.html',
97
+ styleUrl: './example.component.scss',
98
+ })
99
+ export class ExampleComponent {}
100
+ ```
101
+
102
+ ### Inject Function
103
+
104
+ ```typescript
105
+ // Preferred
106
+ export class MyComponent {
107
+ private readonly store = inject(Store);
108
+ private readonly http = inject(HttpClient);
109
+ }
110
+
111
+ // Avoid constructor injection
112
+ ```
113
+
114
+ ### Signal Inputs/Outputs (not decorators)
115
+
116
+ ```typescript
117
+ // Inputs - use input() function, NOT @Input() decorator
118
+ name = input<string>(); // Optional
119
+ name = input('default'); // With default
120
+ name = input.required<string>(); // Required
121
+
122
+ // Outputs - use output() function, NOT @Output() decorator
123
+ clicked = output<void>();
124
+ selected = output<Item>();
125
+
126
+ // Two-way binding - use model() function
127
+ value = model<string>(''); // Creates input + output pair
128
+ value = model.required<string>(); // Required two-way binding
129
+ ```
130
+
131
+ ## Component Architecture
132
+
133
+ ### Smart Components (feature/)
134
+
135
+ - Located in `feature/` libs
136
+ - Inject store, dispatch actions
137
+ - Handle routing logic
138
+ - Pass data to UI components via inputs
139
+
140
+ ```typescript
141
+ // user-list-page.component.ts
142
+ @Component({
143
+ selector: 'app-user-list-page',
144
+ imports: [UserListComponent],
145
+ templateUrl: './user-list-page.component.html',
146
+ changeDetection: ChangeDetectionStrategy.OnPush,
147
+ })
148
+ export class UserListPageComponent {
149
+ private readonly store = inject(Store);
150
+
151
+ users = this.store.selectSignal(selectAllUsers);
152
+ loading = this.store.selectSignal(selectUsersLoading);
153
+
154
+ onUserSelect(user: User): void {
155
+ this.store.dispatch(UserActions.selectUser({ user }));
156
+ }
157
+ }
158
+ ```
159
+
160
+ ```html
161
+ <!-- user-list-page.component.html -->
162
+ <app-user-list
163
+ [users]="users()"
164
+ [loading]="loading()"
165
+ (userSelected)="onUserSelect($event)"
166
+ />
167
+ ```
168
+
169
+ ### UI Components (ui/)
170
+
171
+ - Located in `ui/` libs
172
+ - NO store injection - never!
173
+ - Pure inputs/outputs only
174
+ - Fully presentational
175
+
176
+ ```typescript
177
+ // user-list.component.ts
178
+ @Component({
179
+ selector: 'app-user-list',
180
+ templateUrl: './user-list.component.html',
181
+ styleUrl: './user-list.component.scss',
182
+ changeDetection: ChangeDetectionStrategy.OnPush,
183
+ })
184
+ export class UserListComponent {
185
+ users = input.required<User[]>();
186
+ loading = input(false);
187
+ userSelected = output<User>();
188
+ }
189
+ ```
190
+
191
+ ```html
192
+ <!-- user-list.component.html -->
193
+ @for (user of users(); track user.id) {
194
+ <div class="user-item" (click)="userSelected.emit(user)">
195
+ {{ user.name }}
196
+ </div>
197
+ } @empty {
198
+ <p>No users found</p>
199
+ }
200
+ ```
201
+
202
+ ## Build & Commands
203
+
204
+ ```bash
205
+ # Development
206
+ nx serve [app-name]
207
+
208
+ # Build
209
+ nx build [app-name]
210
+ nx build [app-name] --configuration=production
211
+
212
+ # Test
213
+ nx test [lib-name] # Single lib
214
+ nx run-many -t test # All tests
215
+ nx affected -t test # Only affected
216
+
217
+ # Lint
218
+ nx lint [project-name]
219
+ nx run-many -t lint
220
+
221
+ # Generate
222
+ nx g @nx/angular:component [name] --project=[lib]
223
+ nx g @nx/angular:library [name] --directory=libs/[domain]
224
+ ```
225
+
226
+ ## Code Style
227
+
228
+ - Prefix: configurable per project (default: `app`)
229
+ - File structure: folder-based (`user-list/user-list.component.ts`)
230
+ - Always explicit return types on public methods
231
+ - Use `readonly` for injected services
232
+ - Use `track` in `@for` loops
233
+
234
+ ## RxJS Guidelines
235
+
236
+ - Prefer signals over observables when possible
237
+ - Use `toSignal()` to convert observables
238
+ - Clean subscriptions with `takeUntilDestroyed()`
239
+ - Avoid nested subscribes - use operators
240
+
241
+ ```typescript
242
+ // Convert observable to signal
243
+ data = toSignal(this.http.get<Data[]>('/api/data'), { initialValue: [] });
244
+
245
+ // If you must use observables
246
+ private readonly destroyRef = inject(DestroyRef);
247
+
248
+ this.source$.pipe(
249
+ takeUntilDestroyed(this.destroyRef)
250
+ ).subscribe();
251
+ ```