@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.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /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+ (
|
|
7
|
+
- Angular 21+ (Zoneless, Signals)
|
|
8
8
|
- Nx monorepo
|
|
9
|
-
- NgRx (
|
|
10
|
-
- Vitest
|
|
9
|
+
- NgRx (Entity Adapter, Functional Effects)
|
|
10
|
+
- Vitest, Playwright
|
|
11
11
|
- TypeScript strict mode
|
|
12
12
|
|
|
13
|
-
## Architecture - Nx
|
|
13
|
+
## Architecture - Nx
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
apps/
|
|
17
|
-
[app-name]/
|
|
16
|
+
apps/[app-name]/
|
|
18
17
|
|
|
19
18
|
libs/
|
|
20
|
-
[domain]/ #
|
|
21
|
-
feature/ # Smart components, pages
|
|
22
|
-
data-access/ #
|
|
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
|
|
24
|
+
util/ # Domain helpers
|
|
26
25
|
|
|
27
26
|
shared/
|
|
28
|
-
ui/ #
|
|
29
|
-
data-access/ #
|
|
30
|
-
util/ # Pure functions
|
|
27
|
+
ui/ # Design system components
|
|
28
|
+
data-access/ # Auth, interceptors
|
|
29
|
+
util/ # Pure functions
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
### Dependency Rules (
|
|
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
|
-
##
|
|
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
|
-
#
|
|
206
|
-
nx
|
|
207
|
-
|
|
208
|
-
#
|
|
209
|
-
nx
|
|
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
|
-
-
|
|
229
|
-
-
|
|
230
|
-
-
|
|
231
|
-
-
|
|
232
|
-
-
|
|
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`)
|