@malamute/ai-rules 1.0.0 → 1.2.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 (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,445 @@
1
+ ---
2
+ name: signal-store
3
+ description: Generate an @ngrx/signals SignalStore with state, computed, methods, and optional entities
4
+ argument-hint: <name> [--entity <EntityName>] [--root]
5
+ ---
6
+
7
+ # Generate NgRx SignalStore
8
+
9
+ Generate a modern SignalStore using `@ngrx/signals` package.
10
+
11
+ ## Syntax
12
+
13
+ ```
14
+ /signal-store <name> [--entity <EntityName>] [--root]
15
+ ```
16
+
17
+ ## Options
18
+
19
+ | Option | Description |
20
+ |--------|-------------|
21
+ | `--entity <Name>` | Add entity management with `withEntities()` |
22
+ | `--root` | Make store a singleton (`providedIn: 'root'`) |
23
+
24
+ ## Examples
25
+
26
+ ```bash
27
+ /signal-store cart
28
+ /signal-store users --entity User --root
29
+ /signal-store todo --entity TodoItem
30
+ /signal-store auth --root
31
+ ```
32
+
33
+ ## Generated Structure
34
+
35
+ ```
36
+ libs/<domain>/data-access/src/lib/stores/
37
+ ├── <name>.store.ts # SignalStore definition
38
+ ├── <name>.store.spec.ts # Store tests
39
+ └── index.ts # Public API
40
+ ```
41
+
42
+ ## File Templates
43
+
44
+ ### Basic Store (`<name>.store.ts`)
45
+
46
+ ```typescript
47
+ import { computed, inject } from '@angular/core';
48
+ import {
49
+ signalStore,
50
+ withState,
51
+ withComputed,
52
+ withMethods,
53
+ withHooks,
54
+ patchState,
55
+ } from '@ngrx/signals';
56
+ import { <Name>Service } from '../services/<name>.service';
57
+ import { firstValueFrom } from 'rxjs';
58
+
59
+ // State interface
60
+ interface <Name>State {
61
+ items: <Item>[];
62
+ selectedId: string | null;
63
+ loading: boolean;
64
+ error: string | null;
65
+ }
66
+
67
+ // Initial state
68
+ const initialState: <Name>State = {
69
+ items: [],
70
+ selectedId: null,
71
+ loading: false,
72
+ error: null,
73
+ };
74
+
75
+ export const <Name>Store = signalStore(
76
+ // { providedIn: 'root' }, // Uncomment for singleton store
77
+ withState(initialState),
78
+
79
+ withComputed(({ items, selectedId }) => ({
80
+ selectedItem: computed(() => {
81
+ const id = selectedId();
82
+ return id ? items().find(item => item.id === id) ?? null : null;
83
+ }),
84
+ itemCount: computed(() => items().length),
85
+ isEmpty: computed(() => items().length === 0),
86
+ })),
87
+
88
+ withMethods((store, <name>Service = inject(<Name>Service)) => ({
89
+ async load(): Promise<void> {
90
+ patchState(store, { loading: true, error: null });
91
+
92
+ try {
93
+ const items = await firstValueFrom(<name>Service.getAll());
94
+ patchState(store, { items, loading: false });
95
+ } catch (error) {
96
+ patchState(store, {
97
+ loading: false,
98
+ error: error instanceof Error ? error.message : 'Failed to load',
99
+ });
100
+ }
101
+ },
102
+
103
+ async add(item: Omit<<Item>, 'id'>): Promise<void> {
104
+ const created = await firstValueFrom(<name>Service.create(item));
105
+ patchState(store, { items: [...store.items(), created] });
106
+ },
107
+
108
+ async update(id: string, changes: Partial<<Item>>): Promise<void> {
109
+ const updated = await firstValueFrom(<name>Service.update(id, changes));
110
+ patchState(store, {
111
+ items: store.items().map(item =>
112
+ item.id === id ? updated : item
113
+ ),
114
+ });
115
+ },
116
+
117
+ async remove(id: string): Promise<void> {
118
+ await firstValueFrom(<name>Service.delete(id));
119
+ patchState(store, {
120
+ items: store.items().filter(item => item.id !== id),
121
+ selectedId: store.selectedId() === id ? null : store.selectedId(),
122
+ });
123
+ },
124
+
125
+ select(id: string | null): void {
126
+ patchState(store, { selectedId: id });
127
+ },
128
+
129
+ clearError(): void {
130
+ patchState(store, { error: null });
131
+ },
132
+ })),
133
+
134
+ withHooks({
135
+ onInit(store) {
136
+ // Optionally load data on init
137
+ // store.load();
138
+ },
139
+ }),
140
+ );
141
+ ```
142
+
143
+ ### Entity Store (`<name>.store.ts` with `--entity`)
144
+
145
+ ```typescript
146
+ import { computed, inject } from '@angular/core';
147
+ import {
148
+ signalStore,
149
+ withComputed,
150
+ withMethods,
151
+ withHooks,
152
+ patchState,
153
+ } from '@ngrx/signals';
154
+ import {
155
+ withEntities,
156
+ setAllEntities,
157
+ addEntity,
158
+ updateEntity,
159
+ removeEntity,
160
+ } from '@ngrx/signals/entities';
161
+ import { <Entity>Service } from '../services/<entity>.service';
162
+ import { firstValueFrom } from 'rxjs';
163
+
164
+ export interface <Entity> {
165
+ id: string;
166
+ name: string;
167
+ // Add more properties
168
+ }
169
+
170
+ interface <Entity>StoreState {
171
+ loading: boolean;
172
+ error: string | null;
173
+ filter: string;
174
+ }
175
+
176
+ export const <Entity>Store = signalStore(
177
+ { providedIn: 'root' },
178
+
179
+ // Entity adapter - provides: entities(), ids(), entityMap()
180
+ withEntities<<Entity>>(),
181
+
182
+ // Additional state
183
+ withState<<Entity>StoreState>({
184
+ loading: false,
185
+ error: null,
186
+ filter: '',
187
+ }),
188
+
189
+ withComputed(({ entities, filter }) => ({
190
+ filtered<Entities>: computed(() => {
191
+ const filterValue = filter().toLowerCase();
192
+ if (!filterValue) return entities();
193
+ return entities().filter(entity =>
194
+ entity.name.toLowerCase().includes(filterValue)
195
+ );
196
+ }),
197
+ total: computed(() => entities().length),
198
+ })),
199
+
200
+ withMethods((store, <entity>Service = inject(<Entity>Service)) => ({
201
+ async load(): Promise<void> {
202
+ patchState(store, { loading: true, error: null });
203
+
204
+ try {
205
+ const <entities> = await firstValueFrom(<entity>Service.getAll());
206
+ patchState(store, setAllEntities(<entities>), { loading: false });
207
+ } catch (error) {
208
+ patchState(store, {
209
+ loading: false,
210
+ error: error instanceof Error ? error.message : 'Failed to load',
211
+ });
212
+ }
213
+ },
214
+
215
+ async add(data: Omit<<Entity>, 'id'>): Promise<void> {
216
+ const <entity> = await firstValueFrom(<entity>Service.create(data));
217
+ patchState(store, addEntity(<entity>));
218
+ },
219
+
220
+ async update(id: string, changes: Partial<<Entity>>): Promise<void> {
221
+ const <entity> = await firstValueFrom(<entity>Service.update(id, changes));
222
+ patchState(store, updateEntity({ id, changes: <entity> }));
223
+ },
224
+
225
+ async remove(id: string): Promise<void> {
226
+ await firstValueFrom(<entity>Service.delete(id));
227
+ patchState(store, removeEntity(id));
228
+ },
229
+
230
+ setFilter(filter: string): void {
231
+ patchState(store, { filter });
232
+ },
233
+ })),
234
+
235
+ withHooks({
236
+ onInit(store) {
237
+ store.load();
238
+ },
239
+ }),
240
+ );
241
+ ```
242
+
243
+ ### Store Tests (`<name>.store.spec.ts`)
244
+
245
+ ```typescript
246
+ import { TestBed } from '@angular/core/testing';
247
+ import { <Name>Store } from './<name>.store';
248
+ import { <Name>Service } from '../services/<name>.service';
249
+ import { of, throwError } from 'rxjs';
250
+
251
+ describe('<Name>Store', () => {
252
+ let store: InstanceType<typeof <Name>Store>;
253
+ let <name>ServiceSpy: jasmine.SpyObj<<Name>Service>;
254
+
255
+ beforeEach(() => {
256
+ <name>ServiceSpy = jasmine.createSpyObj<<Name>Service>('<Name>Service', [
257
+ 'getAll',
258
+ 'create',
259
+ 'update',
260
+ 'delete',
261
+ ]);
262
+
263
+ TestBed.configureTestingModule({
264
+ providers: [
265
+ <Name>Store,
266
+ { provide: <Name>Service, useValue: <name>ServiceSpy },
267
+ ],
268
+ });
269
+
270
+ store = TestBed.inject(<Name>Store);
271
+ });
272
+
273
+ describe('initial state', () => {
274
+ it('should have empty items', () => {
275
+ expect(store.items()).toEqual([]);
276
+ });
277
+
278
+ it('should not be loading', () => {
279
+ expect(store.loading()).toBe(false);
280
+ });
281
+
282
+ it('should have no error', () => {
283
+ expect(store.error()).toBeNull();
284
+ });
285
+ });
286
+
287
+ describe('load', () => {
288
+ it('should set loading to true while loading', async () => {
289
+ <name>ServiceSpy.getAll.and.returnValue(
290
+ of([{ id: '1', name: 'Item 1' }])
291
+ );
292
+
293
+ const loadPromise = store.load();
294
+ // Note: In real tests, you'd need to check intermediate state
295
+ await loadPromise;
296
+
297
+ expect(store.loading()).toBe(false);
298
+ });
299
+
300
+ it('should populate items on success', async () => {
301
+ const items = [{ id: '1', name: 'Item 1' }];
302
+ <name>ServiceSpy.getAll.and.returnValue(of(items));
303
+
304
+ await store.load();
305
+
306
+ expect(store.items()).toEqual(items);
307
+ });
308
+
309
+ it('should set error on failure', async () => {
310
+ <name>ServiceSpy.getAll.and.returnValue(
311
+ throwError(() => new Error('Network error'))
312
+ );
313
+
314
+ await store.load();
315
+
316
+ expect(store.error()).toBe('Network error');
317
+ });
318
+ });
319
+
320
+ describe('add', () => {
321
+ it('should add item to store', async () => {
322
+ const newItem = { id: '1', name: 'New Item' };
323
+ <name>ServiceSpy.create.and.returnValue(of(newItem));
324
+
325
+ await store.add({ name: 'New Item' });
326
+
327
+ expect(store.items()).toContain(newItem);
328
+ });
329
+ });
330
+
331
+ describe('computed', () => {
332
+ it('should compute itemCount', async () => {
333
+ const items = [
334
+ { id: '1', name: 'Item 1' },
335
+ { id: '2', name: 'Item 2' },
336
+ ];
337
+ <name>ServiceSpy.getAll.and.returnValue(of(items));
338
+
339
+ await store.load();
340
+
341
+ expect(store.itemCount()).toBe(2);
342
+ });
343
+ });
344
+ });
345
+ ```
346
+
347
+ ## Component Usage
348
+
349
+ ```typescript
350
+ @Component({
351
+ selector: 'app-<name>-list',
352
+ providers: [<Name>Store], // Component-level store (or omit for root)
353
+ template: `
354
+ @if (store.loading()) {
355
+ <app-spinner />
356
+ }
357
+
358
+ @if (store.error()) {
359
+ <app-error
360
+ [message]="store.error()!"
361
+ (retry)="store.load()"
362
+ />
363
+ }
364
+
365
+ @for (item of store.items(); track item.id) {
366
+ <app-<name>-card
367
+ [item]="item"
368
+ [selected]="store.selectedItem()?.id === item.id"
369
+ (select)="store.select(item.id)"
370
+ (delete)="store.remove(item.id)"
371
+ />
372
+ } @empty {
373
+ <p>No items found</p>
374
+ }
375
+ `,
376
+ })
377
+ export class <Name>ListComponent {
378
+ protected readonly store = inject(<Name>Store);
379
+
380
+ constructor() {
381
+ // Load data on component init if store is component-level
382
+ this.store.load();
383
+ }
384
+ }
385
+ ```
386
+
387
+ ## Execution Steps
388
+
389
+ 1. **Parse Arguments**
390
+ - Extract store name
391
+ - Check for `--entity` flag
392
+ - Check for `--root` flag
393
+
394
+ 2. **Generate Files**
395
+ - Create store file with appropriate template
396
+ - Create test file
397
+ - Update index.ts
398
+
399
+ 3. **Show Usage**
400
+ - Display component usage example
401
+ - Show how to inject the store
402
+
403
+ ## Output Summary
404
+
405
+ ```
406
+ ✓ Created SignalStore: libs/<domain>/data-access/src/lib/stores/<name>.store.ts
407
+
408
+ Store name: <Name>Store
409
+ Type: [Basic | Entity]
410
+ Scope: [Component | Root]
411
+
412
+ Features:
413
+ - withState (loading, error, custom state)
414
+ - withComputed (derived values)
415
+ - withMethods (CRUD operations)
416
+ - withHooks (onInit)
417
+ [- withEntities (entity adapter)]
418
+
419
+ Usage:
420
+ // In component
421
+ protected readonly store = inject(<Name>Store);
422
+
423
+ // Access state
424
+ store.items()
425
+ store.loading()
426
+ store.selectedItem()
427
+
428
+ // Call methods
429
+ store.load()
430
+ store.add(item)
431
+ store.update(id, changes)
432
+ store.remove(id)
433
+ ```
434
+
435
+ ## Placeholders
436
+
437
+ | Placeholder | Example (product) |
438
+ |-------------|-------------------|
439
+ | `<name>` | product |
440
+ | `<Name>` | Product |
441
+ | `<entity>` | product |
442
+ | `<Entity>` | Product |
443
+ | `<entities>` | products |
444
+ | `<Entities>` | Products |
445
+ | `<Item>` | Product (or custom type) |
@@ -4,33 +4,32 @@
4
4
 
5
5
  ## Stack
6
6
 
7
- - Angular 21+ (latest)
7
+ - Angular 21+ (Zoneless, Signals)
8
8
  - Nx monorepo
9
- - NgRx (store, effects, entity)
10
- - Vitest
9
+ - NgRx (Entity Adapter, Functional Effects)
10
+ - Vitest, Playwright
11
11
  - TypeScript strict mode
12
12
 
13
- ## Architecture - Nx Structure
13
+ ## Architecture - Nx
14
14
 
15
15
  ```
16
- apps/
17
- [app-name]/
16
+ apps/[app-name]/
18
17
 
19
18
  libs/
20
- [domain]/ # Ex: users, products, checkout
21
- feature/ # Smart components, pages, routing (lazy-loaded)
22
- data-access/ # Services API + NgRx state
19
+ [domain]/ # users, products, checkout
20
+ feature/ # Smart components, pages (lazy-loaded)
21
+ data-access/ # NgRx store, API services
23
22
  src/lib/+state/ # Actions, reducers, effects, selectors
24
23
  ui/ # Dumb/presentational components
25
- util/ # Domain-specific helpers
24
+ util/ # Domain helpers
26
25
 
27
26
  shared/
28
- ui/ # Reusable UI components
29
- data-access/ # Shared services (auth, http interceptors)
30
- util/ # Pure functions, helpers
27
+ ui/ # Design system components
28
+ data-access/ # Auth, interceptors
29
+ util/ # Pure functions
31
30
  ```
32
31
 
33
- ### Dependency Rules (enforce via Nx tags)
32
+ ### Dependency Rules (Nx tags)
34
33
 
35
34
  | Type | Can import |
36
35
  |------|------------|
@@ -39,213 +38,22 @@ libs/
39
38
  | `data-access` | `data-access`, `util` only |
40
39
  | `util` | `util` only |
41
40
 
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
41
+ ## Commands
203
42
 
204
43
  ```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
44
+ nx serve [app] # Dev server
45
+ nx build [app] --configuration=production
46
+ nx test [lib] # Unit tests
47
+ nx affected -t test # Test affected
48
+ nx e2e [app]-e2e # E2E tests
222
49
  nx g @nx/angular:component [name] --project=[lib]
223
50
  nx g @nx/angular:library [name] --directory=libs/[domain]
224
51
  ```
225
52
 
226
53
  ## Code Style
227
54
 
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
- ```
55
+ - Folder-based structure: `user-list/user-list.component.ts`
56
+ - Explicit return types on public methods
57
+ - `readonly` for injected services
58
+ - `track` required in `@for` loops
59
+ - Prefix configurable per project (default: `app`)