@el-j/magic-helix-core 4.0.0-beta.2 → 4.0.0-beta.3
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/dist/index-B88j4AyE.js +13 -0
- package/dist/index-B88j4AyE.js.map +1 -0
- package/dist/index-CY-pQbuu.cjs +2 -0
- package/dist/index-CY-pQbuu.cjs.map +1 -0
- package/dist/index.cjs +75 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +2234 -51
- package/dist/index.mjs.map +1 -1
- package/dist/pattern-combiner.d.ts +1 -1
- package/dist/plugin-loader.d.ts +2 -1
- package/package.json +4 -4
- package/dist/BasePlugin-6wv0hYJ9.js +0 -98
- package/dist/BasePlugin-6wv0hYJ9.js.map +0 -1
- package/dist/BasePlugin-odQJAKA-.cjs +0 -2
- package/dist/BasePlugin-odQJAKA-.cjs.map +0 -1
- package/dist/builtin-plugins/base/BasePlugin.d.ts +0 -69
- package/dist/builtin-plugins/cpp/index.d.ts +0 -46
- package/dist/builtin-plugins/csharp/index.d.ts +0 -20
- package/dist/builtin-plugins/go/index.d.ts +0 -23
- package/dist/builtin-plugins/index.d.ts +0 -16
- package/dist/builtin-plugins/java/index.d.ts +0 -22
- package/dist/builtin-plugins/nodejs/index.d.ts +0 -44
- package/dist/builtin-plugins/php/index.d.ts +0 -20
- package/dist/builtin-plugins/python/index.d.ts +0 -27
- package/dist/builtin-plugins/ruby/index.d.ts +0 -20
- package/dist/builtin-plugins/rust/index.d.ts +0 -53
- package/dist/builtin-plugins/swift/index.d.ts +0 -22
- package/dist/default_templates/angular/angular-core.md +0 -19
- package/dist/default_templates/architecture/codeowners.md +0 -123
- package/dist/default_templates/architecture/monorepo.md +0 -146
- package/dist/default_templates/architecture/nx.md +0 -122
- package/dist/default_templates/architecture/turborepo.md +0 -114
- package/dist/default_templates/ci/github-actions.md +0 -268
- package/dist/default_templates/ci/gitlab-ci.md +0 -330
- package/dist/default_templates/containers/docker-multistage.md +0 -120
- package/dist/default_templates/containers/kubernetes-deploy.md +0 -210
- package/dist/default_templates/devops/docker-compose.md +0 -111
- package/dist/default_templates/devops/docker-dockerfile.md +0 -94
- package/dist/default_templates/devops/github-actions.md +0 -160
- package/dist/default_templates/devops/gitlab-ci.md +0 -210
- package/dist/default_templates/dotnet/framework-aspnetcore.md +0 -205
- package/dist/default_templates/dotnet/framework-blazor.md +0 -271
- package/dist/default_templates/dotnet/lang-csharp.md +0 -162
- package/dist/default_templates/generic/lang-typescript.md +0 -57
- package/dist/default_templates/generic/state-redux.md +0 -21
- package/dist/default_templates/generic/state-rxjs.md +0 -6
- package/dist/default_templates/generic/style-mui.md +0 -23
- package/dist/default_templates/generic/style-tailwind.md +0 -76
- package/dist/default_templates/generic/test-cypress.md +0 -21
- package/dist/default_templates/generic/test-jest.md +0 -20
- package/dist/default_templates/generic/test-playwright.md +0 -21
- package/dist/default_templates/generic/test-vitest.md +0 -131
- package/dist/default_templates/go/lang-go.md +0 -571
- package/dist/default_templates/java/build-gradle.md +0 -102
- package/dist/default_templates/java/build-maven.md +0 -86
- package/dist/default_templates/java/framework-spring-boot.md +0 -179
- package/dist/default_templates/java/lang-java.md +0 -78
- package/dist/default_templates/java/lang-kotlin.md +0 -88
- package/dist/default_templates/meta/magic-helix-meta.md +0 -213
- package/dist/default_templates/meta/meta-debug.md +0 -459
- package/dist/default_templates/meta/meta-implement.md +0 -450
- package/dist/default_templates/meta/meta-roadmap.md +0 -265
- package/dist/default_templates/nestjs/nestjs-core.md +0 -7
- package/dist/default_templates/patterns/architecture/clean-architecture.md +0 -469
- package/dist/default_templates/patterns/architecture/dependency-injection.md +0 -517
- package/dist/default_templates/patterns/architecture/domain-driven-design.md +0 -621
- package/dist/default_templates/patterns/architecture/layered-architecture.md +0 -382
- package/dist/default_templates/patterns/architecture/repository-pattern.md +0 -408
- package/dist/default_templates/patterns/domain-expertise/nextjs-rules.md +0 -115
- package/dist/default_templates/patterns/domain-expertise/react-patterns.md +0 -181
- package/dist/default_templates/patterns/domain-expertise/server-components.md +0 -212
- package/dist/default_templates/patterns/domain-expertise/shadcn-ui.md +0 -52
- package/dist/default_templates/patterns/domain-expertise/tailwind-patterns.md +0 -52
- package/dist/default_templates/patterns/environment/container-awareness.md +0 -17
- package/dist/default_templates/patterns/environment/ide-features.md +0 -17
- package/dist/default_templates/patterns/environment/os-commands.md +0 -17
- package/dist/default_templates/patterns/organization/heading-hierarchy.md +0 -103
- package/dist/default_templates/patterns/organization/sequential-workflows.md +0 -102
- package/dist/default_templates/patterns/organization/xml-rule-groups.md +0 -64
- package/dist/default_templates/patterns/reasoning/agent-loop.md +0 -151
- package/dist/default_templates/patterns/reasoning/confirmation-gates.md +0 -141
- package/dist/default_templates/patterns/reasoning/dependency-analysis.md +0 -132
- package/dist/default_templates/patterns/reasoning/one-tool-per-iteration.md +0 -152
- package/dist/default_templates/patterns/reasoning/preview-before-action.md +0 -194
- package/dist/default_templates/patterns/reasoning/reflection-checkpoints.md +0 -166
- package/dist/default_templates/patterns/reasoning/result-verification.md +0 -157
- package/dist/default_templates/patterns/reasoning/subtask-breakdown.md +0 -131
- package/dist/default_templates/patterns/reasoning/thinking-tags.md +0 -100
- package/dist/default_templates/patterns/role-definition/capability-declarations.md +0 -72
- package/dist/default_templates/patterns/role-definition/expert-identity.md +0 -45
- package/dist/default_templates/patterns/role-definition/scope-boundaries.md +0 -61
- package/dist/default_templates/patterns/safety/code-safety-rules.md +0 -17
- package/dist/default_templates/patterns/safety/credential-handling.md +0 -17
- package/dist/default_templates/patterns/safety/destructive-warnings.md +0 -17
- package/dist/default_templates/patterns/safety/refusal-messages.md +0 -17
- package/dist/default_templates/patterns/tone/adaptive-tone.md +0 -17
- package/dist/default_templates/patterns/tone/concise-communication.md +0 -17
- package/dist/default_templates/patterns/tone/forbidden-phrases.md +0 -17
- package/dist/default_templates/patterns/tool-guidelines/function-schemas.md +0 -143
- package/dist/default_templates/patterns/tool-guidelines/parameter-examples.md +0 -137
- package/dist/default_templates/patterns/tool-guidelines/usage-policies.md +0 -105
- package/dist/default_templates/php/framework-laravel.md +0 -112
- package/dist/default_templates/php/lang-php.md +0 -94
- package/dist/default_templates/python/lang-python.md +0 -508
- package/dist/default_templates/react/react-core.md +0 -677
- package/dist/default_templates/react/react-zustand.md +0 -7
- package/dist/default_templates/ruby/framework-rails.md +0 -309
- package/dist/default_templates/ruby/framework-sinatra.md +0 -227
- package/dist/default_templates/ruby/lang-ruby.md +0 -216
- package/dist/default_templates/rust/lang-rust.md +0 -89
- package/dist/default_templates/swift/framework-vapor.md +0 -352
- package/dist/default_templates/swift/lang-swift.md +0 -291
- package/dist/default_templates/vue/style-primevue.md +0 -6
- package/dist/default_templates/vue/style-quasar.md +0 -22
- package/dist/default_templates/vue/vue-core.md +0 -108
- package/dist/default_templates/vue/vue-pinia.md +0 -5
- package/dist/index-0GK4RlUx.js +0 -1748
- package/dist/index-0GK4RlUx.js.map +0 -1
- package/dist/index-AkVwRl-r.js +0 -92
- package/dist/index-AkVwRl-r.js.map +0 -1
- package/dist/index-B6BeG1yT.cjs +0 -68
- package/dist/index-B6BeG1yT.cjs.map +0 -1
- package/dist/index-B8pyjKdF.js +0 -94
- package/dist/index-B8pyjKdF.js.map +0 -1
- package/dist/index-BQ6v041y.js +0 -13
- package/dist/index-BQ6v041y.js.map +0 -1
- package/dist/index-Baxb1vI_.js +0 -210
- package/dist/index-Baxb1vI_.js.map +0 -1
- package/dist/index-Bg8DD8ku.js +0 -216
- package/dist/index-Bg8DD8ku.js.map +0 -1
- package/dist/index-BqTqxCpG.cjs +0 -89
- package/dist/index-BqTqxCpG.cjs.map +0 -1
- package/dist/index-Bv4Q1Pr7.cjs +0 -33
- package/dist/index-Bv4Q1Pr7.cjs.map +0 -1
- package/dist/index-CN8J45Nc.cjs +0 -24
- package/dist/index-CN8J45Nc.cjs.map +0 -1
- package/dist/index-CPbv2Od1.js +0 -62
- package/dist/index-CPbv2Od1.js.map +0 -1
- package/dist/index-Cf-MC6Al.js +0 -63
- package/dist/index-Cf-MC6Al.js.map +0 -1
- package/dist/index-DDPXXXDy.cjs +0 -19
- package/dist/index-DDPXXXDy.cjs.map +0 -1
- package/dist/index-DO30AzDe.cjs +0 -19
- package/dist/index-DO30AzDe.cjs.map +0 -1
- package/dist/index-DkvW5yBY.js +0 -2249
- package/dist/index-DkvW5yBY.js.map +0 -1
- package/dist/index-Dn1ehjIj.cjs +0 -80
- package/dist/index-Dn1ehjIj.cjs.map +0 -1
- package/dist/index-DqHvgoXJ.cjs +0 -19
- package/dist/index-DqHvgoXJ.cjs.map +0 -1
- package/dist/index-K39pdw94.cjs +0 -31
- package/dist/index-K39pdw94.cjs.map +0 -1
- package/dist/index-OT2XAJkc.js +0 -117
- package/dist/index-OT2XAJkc.js.map +0 -1
- package/dist/index-TPAX4XKg.cjs +0 -30
- package/dist/index-TPAX4XKg.cjs.map +0 -1
- package/dist/index-WmVSB57y.js +0 -107
- package/dist/index-WmVSB57y.js.map +0 -1
- package/dist/index-mYXvc3Fs.js +0 -68
- package/dist/index-mYXvc3Fs.js.map +0 -1
- package/dist/index-nioXOg4m.cjs +0 -76
- package/dist/index-nioXOg4m.cjs.map +0 -1
- package/dist/index-okhY3fWD.cjs +0 -2
- package/dist/index-okhY3fWD.cjs.map +0 -1
|
@@ -1,677 +0,0 @@
|
|
|
1
|
-
# Framework: React
|
|
2
|
-
|
|
3
|
-
## Modern React: Functional, Composable, Framework-Agnostic
|
|
4
|
-
|
|
5
|
-
**PREFER** functional programming over classes. Use plain objects, pure functions, and React hooks.
|
|
6
|
-
|
|
7
|
-
## Architecture: Separation of Concerns
|
|
8
|
-
|
|
9
|
-
**Components should be logic-free presentation layers. Business logic lives in framework-free services.**
|
|
10
|
-
|
|
11
|
-
### Layer Structure
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
src/
|
|
15
|
-
├── components/ # Presentation layer (React-specific, logic-free)
|
|
16
|
-
│ ├── UserProfile.tsx # UI only: rendering, event handlers → call hooks
|
|
17
|
-
│ └── OrderList.tsx
|
|
18
|
-
│
|
|
19
|
-
├── hooks/ # React bridge layer (connects React to business logic)
|
|
20
|
-
│ ├── useUser.ts # Wraps services in React state/effects
|
|
21
|
-
│ └── useOrders.ts
|
|
22
|
-
│
|
|
23
|
-
├── services/ # Business logic layer (framework-free, pure functions)
|
|
24
|
-
│ ├── user.ts # Pure functions, no React dependencies
|
|
25
|
-
│ └── order.ts
|
|
26
|
-
│
|
|
27
|
-
└── models/ # Domain models (types + pure functions)
|
|
28
|
-
├── user.ts # Plain types + validators
|
|
29
|
-
└── order.ts
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Modern Patterns
|
|
33
|
-
|
|
34
|
-
### ✅ DO: Use Functional Components + Hooks
|
|
35
|
-
|
|
36
|
-
```tsx
|
|
37
|
-
// ✅ Modern: Functional component
|
|
38
|
-
export function UserProfile({ userId }: { userId: string }) {
|
|
39
|
-
const { user, loading } = useUser(userId);
|
|
40
|
-
|
|
41
|
-
if (loading) return <Spinner />;
|
|
42
|
-
return <div>{user.name}</div>;
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
### ❌ DON'T: Use Class Components
|
|
47
|
-
|
|
48
|
-
```tsx
|
|
49
|
-
// ❌ Outdated: Class component
|
|
50
|
-
class UserProfile extends React.Component {
|
|
51
|
-
// Don't use classes in modern React
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### ✅ DO: Use Plain Objects and Pure Functions
|
|
56
|
-
|
|
57
|
-
```tsx
|
|
58
|
-
// ✅ Modern: Plain type + pure functions
|
|
59
|
-
export type User = {
|
|
60
|
-
id: string;
|
|
61
|
-
name: string;
|
|
62
|
-
email: string;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export function validateUser(user: User): boolean {
|
|
66
|
-
return user.email.includes('@');
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### ❌ DON'T: Use Classes for Models
|
|
71
|
-
|
|
72
|
-
```tsx
|
|
73
|
-
// ❌ Outdated: Class-based model
|
|
74
|
-
class User {
|
|
75
|
-
constructor(public id: string, public name: string) {}
|
|
76
|
-
validate() { /* ... */ }
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Rule: Components are Logic-Free
|
|
81
|
-
|
|
82
|
-
**ALWAYS** keep components focused on rendering and user interactions. **NEVER** put business logic in components.
|
|
83
|
-
|
|
84
|
-
```tsx
|
|
85
|
-
// ✅ Good: Logic-free component
|
|
86
|
-
export function UserProfile({ userId }: { userId: string }) {
|
|
87
|
-
const { user, loading, updateEmail } = useUser(userId);
|
|
88
|
-
|
|
89
|
-
if (loading) return <Spinner />;
|
|
90
|
-
if (!user) return <NotFound />;
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<div>
|
|
94
|
-
<h1>{user.name}</h1>
|
|
95
|
-
<EmailForm
|
|
96
|
-
currentEmail={user.email}
|
|
97
|
-
onSubmit={updateEmail} // Hook handles the logic
|
|
98
|
-
/>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ❌ Bad: Business logic in component
|
|
104
|
-
export function UserProfile({ userId }: { userId: string }) {
|
|
105
|
-
const [user, setUser] = useState<User | null>(null);
|
|
106
|
-
|
|
107
|
-
async function updateEmail(newEmail: string) {
|
|
108
|
-
// ❌ Validation logic in component
|
|
109
|
-
if (!newEmail.includes('@')) {
|
|
110
|
-
alert('Invalid email');
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ❌ API call in component
|
|
115
|
-
const response = await fetch(`/api/users/${userId}`, {
|
|
116
|
-
method: 'PATCH',
|
|
117
|
-
body: JSON.stringify({ email: newEmail })
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const updated = await response.json();
|
|
121
|
-
setUser(updated);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return (/* ... */);
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Rule: Hooks Wire React to Services
|
|
129
|
-
|
|
130
|
-
**ALWAYS** use custom hooks to bridge React state/effects with framework-free services.
|
|
131
|
-
|
|
132
|
-
```tsx
|
|
133
|
-
// ✅ hooks/useUser.ts - React bridge
|
|
134
|
-
import { useState, useEffect } from 'react';
|
|
135
|
-
import * as userService from '@/services/userService';
|
|
136
|
-
import type { User } from '@/models/User';
|
|
137
|
-
|
|
138
|
-
export function useUser(userId: string) {
|
|
139
|
-
const [user, setUser] = useState<User | null>(null);
|
|
140
|
-
const [loading, setLoading] = useState(true);
|
|
141
|
-
const [error, setError] = useState<Error | null>(null);
|
|
142
|
-
|
|
143
|
-
useEffect(() => {
|
|
144
|
-
let cancelled = false;
|
|
145
|
-
|
|
146
|
-
async function loadUser() {
|
|
147
|
-
try {
|
|
148
|
-
setLoading(true);
|
|
149
|
-
const data = await userService.getUser(userId); // Pure function call
|
|
150
|
-
if (!cancelled) {
|
|
151
|
-
setUser(data);
|
|
152
|
-
}
|
|
153
|
-
} catch (err) {
|
|
154
|
-
if (!cancelled) {
|
|
155
|
-
setError(err as Error);
|
|
156
|
-
}
|
|
157
|
-
} finally {
|
|
158
|
-
if (!cancelled) {
|
|
159
|
-
setLoading(false);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
loadUser();
|
|
165
|
-
|
|
166
|
-
return () => {
|
|
167
|
-
cancelled = true; // Cleanup
|
|
168
|
-
};
|
|
169
|
-
}, [userId]);
|
|
170
|
-
|
|
171
|
-
const updateEmail = async (newEmail: string) => {
|
|
172
|
-
const updated = await userService.updateUserEmail(userId, newEmail);
|
|
173
|
-
setUser(updated);
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
return { user, loading, error, updateEmail };
|
|
177
|
-
}
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
### Rule: Services are Framework-Free
|
|
181
|
-
|
|
182
|
-
**ALWAYS** write services as pure functions or plain objects. **NEVER** import React or use hooks in services.
|
|
183
|
-
|
|
184
|
-
```typescript
|
|
185
|
-
// ✅ Modern: services/userService.ts - Pure functions (no class)
|
|
186
|
-
import type { User } from '@/models/User';
|
|
187
|
-
import { validateEmail } from '@/models/User';
|
|
188
|
-
import { apiClient } from '@/lib/apiClient';
|
|
189
|
-
|
|
190
|
-
export async function getUser(userId: string): Promise<User> {
|
|
191
|
-
const response = await apiClient.get(`/users/${userId}`);
|
|
192
|
-
return {
|
|
193
|
-
id: response.data.id,
|
|
194
|
-
name: response.data.name,
|
|
195
|
-
email: response.data.email,
|
|
196
|
-
createdAt: new Date(response.data.created_at),
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export async function updateUserEmail(userId: string, newEmail: string): Promise<User> {
|
|
201
|
-
// Business logic: validation
|
|
202
|
-
if (!validateEmail(newEmail)) {
|
|
203
|
-
throw new Error('Invalid email format');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// API call
|
|
207
|
-
const response = await apiClient.patch(`/users/${userId}`, {
|
|
208
|
-
email: newEmail
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
id: response.data.id,
|
|
213
|
-
name: response.data.name,
|
|
214
|
-
email: response.data.email,
|
|
215
|
-
createdAt: new Date(response.data.created_at),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export async function deleteUser(userId: string): Promise<void> {
|
|
220
|
-
await apiClient.delete(`/users/${userId}`);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Alternative: Object grouping for organization
|
|
224
|
-
export const userService = {
|
|
225
|
-
getUser,
|
|
226
|
-
updateUserEmail,
|
|
227
|
-
deleteUser,
|
|
228
|
-
} as const;
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
**Benefits:**
|
|
232
|
-
- Easy to test (no React mocking needed)
|
|
233
|
-
- Reusable in Node.js scripts, CLI tools, React Native
|
|
234
|
-
- Can swap React for Vue/Svelte/Angular without rewriting logic
|
|
235
|
-
- Tree-shakeable (only import functions you use)
|
|
236
|
-
|
|
237
|
-
### Rule: Domain Models are Plain Objects + Pure Functions
|
|
238
|
-
|
|
239
|
-
**PREFER** plain TypeScript types with pure utility functions over classes.
|
|
240
|
-
|
|
241
|
-
```typescript
|
|
242
|
-
// ✅ Modern: models/User.ts - Type + pure functions
|
|
243
|
-
export type User = {
|
|
244
|
-
readonly id: string;
|
|
245
|
-
name: string;
|
|
246
|
-
email: string;
|
|
247
|
-
createdAt: Date;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
// Pure utility functions
|
|
251
|
-
export function createUser(data: { name: string; email: string }): User {
|
|
252
|
-
if (!validateEmail(data.email)) {
|
|
253
|
-
throw new Error('Invalid email format');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
id: crypto.randomUUID(),
|
|
258
|
-
name: data.name,
|
|
259
|
-
email: data.email,
|
|
260
|
-
createdAt: new Date(),
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export function isAdmin(user: User): boolean {
|
|
265
|
-
return user.email.endsWith('@company.com');
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function validateEmail(email: string): boolean {
|
|
269
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// For complex transformations
|
|
273
|
-
export function userFromAPI(data: unknown): User {
|
|
274
|
-
// Add runtime validation with zod or similar
|
|
275
|
-
return {
|
|
276
|
-
id: data.id,
|
|
277
|
-
name: data.name,
|
|
278
|
-
email: data.email,
|
|
279
|
-
createdAt: new Date(data.created_at),
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
```
|
|
283
|
-
|
|
284
|
-
### Complete Example: Order Management
|
|
285
|
-
|
|
286
|
-
```
|
|
287
|
-
src/
|
|
288
|
-
├── components/
|
|
289
|
-
│ └── OrderList.tsx # UI only
|
|
290
|
-
├── hooks/
|
|
291
|
-
│ └── useOrders.ts # React state bridge
|
|
292
|
-
├── services/
|
|
293
|
-
│ └── orderService.ts # Business logic
|
|
294
|
-
└── models/
|
|
295
|
-
└── Order.ts # Domain model
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
```tsx
|
|
299
|
-
// components/OrderList.tsx - Presentation
|
|
300
|
-
export function OrderList() {
|
|
301
|
-
const { orders, loading, createOrder, cancelOrder } = useOrders();
|
|
302
|
-
|
|
303
|
-
if (loading) return <Spinner />;
|
|
304
|
-
|
|
305
|
-
return (
|
|
306
|
-
<div>
|
|
307
|
-
<button onClick={() => createOrder({ items: [...] })}>
|
|
308
|
-
New Order
|
|
309
|
-
</button>
|
|
310
|
-
{orders.map(order => (
|
|
311
|
-
<OrderCard
|
|
312
|
-
key={order.id}
|
|
313
|
-
order={order}
|
|
314
|
-
onCancel={() => cancelOrder(order.id)}
|
|
315
|
-
/>
|
|
316
|
-
))}
|
|
317
|
-
</div>
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// hooks/useOrders.ts - React bridge
|
|
322
|
-
export function useOrders() {
|
|
323
|
-
const [orders, setOrders] = useState<Order[]>([]);
|
|
324
|
-
const [loading, setLoading] = useState(true);
|
|
325
|
-
|
|
326
|
-
useEffect(() => {
|
|
327
|
-
orderService.getOrders().then(setOrders).finally(() => setLoading(false));
|
|
328
|
-
}, []);
|
|
329
|
-
|
|
330
|
-
const createOrder = async (data: CreateOrderData) => {
|
|
331
|
-
const newOrder = await orderService.createOrder(data);
|
|
332
|
-
setOrders(prev => [...prev, newOrder]);
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
const cancelOrder = async (orderId: string) => {
|
|
336
|
-
await orderService.cancelOrder(orderId);
|
|
337
|
-
setOrders(prev => prev.filter(o => o.id !== orderId));
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
return { orders, loading, createOrder, cancelOrder };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// services/orderService.ts - Framework-free logic
|
|
344
|
-
class OrderService {
|
|
345
|
-
async getOrders(): Promise<Order[]> {
|
|
346
|
-
const response = await apiClient.get('/orders');
|
|
347
|
-
return response.data.map(Order.fromAPI);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async createOrder(data: CreateOrderData): Promise<Order> {
|
|
351
|
-
// Validation
|
|
352
|
-
if (data.items.length === 0) {
|
|
353
|
-
throw new Error('Order must have at least one item');
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const response = await apiClient.post('/orders', data);
|
|
357
|
-
return Order.fromAPI(response.data);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async cancelOrder(orderId: string): Promise<void> {
|
|
361
|
-
await apiClient.post(`/orders/${orderId}/cancel`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export const orderService = new OrderService();
|
|
366
|
-
|
|
367
|
-
// models/Order.ts - Domain model
|
|
368
|
-
export class Order {
|
|
369
|
-
constructor(
|
|
370
|
-
public readonly id: string,
|
|
371
|
-
public status: OrderStatus,
|
|
372
|
-
public items: OrderItem[],
|
|
373
|
-
public total: number
|
|
374
|
-
) {}
|
|
375
|
-
|
|
376
|
-
static fromAPI(data: any): Order {
|
|
377
|
-
return new Order(
|
|
378
|
-
data.id,
|
|
379
|
-
data.status,
|
|
380
|
-
data.items.map(OrderItem.fromAPI),
|
|
381
|
-
data.total
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
canBeCancelled(): boolean {
|
|
386
|
-
return this.status === 'pending' || this.status === 'processing';
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
## React Best Practices
|
|
392
|
-
|
|
393
|
-
### Modern React Patterns
|
|
394
|
-
|
|
395
|
-
- **ALWAYS** use Functional Components with Hooks (never classes)
|
|
396
|
-
- **ALWAYS** use plain objects and pure functions over classes for models/services
|
|
397
|
-
- **PREFER** function exports over default exports for better tree-shaking
|
|
398
|
-
- **PREFER** named exports: `export function Component()` over `export default`
|
|
399
|
-
- **HOOKS**: Use `useState` for simple state, `useReducer` for complex state logic
|
|
400
|
-
- **EFFECTS**: `useEffect` dependencies must be complete. Use `eslint-plugin-react-hooks`
|
|
401
|
-
- **MEMOIZATION**: Use `useCallback` for functions passed as props, `useMemo` for expensive calculations
|
|
402
|
-
- **NAMING**: Components are `PascalCase.tsx`, utilities are `camelCase.ts`
|
|
403
|
-
|
|
404
|
-
### Recommended Modern Libraries
|
|
405
|
-
|
|
406
|
-
**State Management:**
|
|
407
|
-
- ✅ Zustand (simple, functional, no boilerplate)
|
|
408
|
-
- ✅ Jotai (atomic state, React-like)
|
|
409
|
-
- ✅ TanStack Query (server state, caching)
|
|
410
|
-
- ⚠️ Redux Toolkit (if you need Redux, use RTK not classic Redux)
|
|
411
|
-
- ❌ Classic Redux (too much boilerplate)
|
|
412
|
-
|
|
413
|
-
**Forms:**
|
|
414
|
-
- ✅ React Hook Form (performant, minimal re-renders)
|
|
415
|
-
- ✅ Zod (schema validation, type-safe)
|
|
416
|
-
- ❌ Formik (legacy, prefer React Hook Form)
|
|
417
|
-
|
|
418
|
-
**Data Fetching:**
|
|
419
|
-
- ✅ TanStack Query (React Query) - best for server state
|
|
420
|
-
- ✅ SWR (simple, from Vercel)
|
|
421
|
-
- ✅ Native `fetch` + custom hooks for simple cases
|
|
422
|
-
|
|
423
|
-
**Styling:**
|
|
424
|
-
- ✅ Tailwind CSS (utility-first, fast)
|
|
425
|
-
- ✅ CSS Modules (scoped, simple)
|
|
426
|
-
- ✅ Vanilla Extract (type-safe CSS-in-TS)
|
|
427
|
-
- ⚠️ Styled Components (runtime cost, slower)
|
|
428
|
-
- ⚠️ Emotion (runtime cost, slower)
|
|
429
|
-
|
|
430
|
-
**Type Safety:**
|
|
431
|
-
- ✅ TypeScript (essential)
|
|
432
|
-
- ✅ Zod (runtime validation + type inference)
|
|
433
|
-
- ✅ ts-pattern (pattern matching)
|
|
434
|
-
|
|
435
|
-
**Testing:**
|
|
436
|
-
- ✅ Vitest (modern, fast, Vite-compatible)
|
|
437
|
-
- ✅ Testing Library (user-centric testing)
|
|
438
|
-
- ⚠️ Jest (slower, but widely used)
|
|
439
|
-
|
|
440
|
-
### Example: Modern Stack
|
|
441
|
-
|
|
442
|
-
```tsx
|
|
443
|
-
// ✅ Modern React with TanStack Query + Zod
|
|
444
|
-
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
445
|
-
import { z } from 'zod';
|
|
446
|
-
|
|
447
|
-
// Schema validation with Zod
|
|
448
|
-
const UserSchema = z.object({
|
|
449
|
-
id: z.string(),
|
|
450
|
-
name: z.string(),
|
|
451
|
-
email: z.string().email(),
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
type User = z.infer<typeof UserSchema>;
|
|
455
|
-
|
|
456
|
-
// Pure function for API call
|
|
457
|
-
async function fetchUser(userId: string): Promise<User> {
|
|
458
|
-
const res = await fetch(`/api/users/${userId}`);
|
|
459
|
-
const data = await res.json();
|
|
460
|
-
return UserSchema.parse(data); // Runtime validation
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Component using TanStack Query (no custom hook needed!)
|
|
464
|
-
export function UserProfile({ userId }: { userId: string }) {
|
|
465
|
-
const { data: user, isLoading } = useQuery({
|
|
466
|
-
queryKey: ['user', userId],
|
|
467
|
-
queryFn: () => fetchUser(userId),
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
if (isLoading) return <Spinner />;
|
|
471
|
-
if (!user) return <NotFound />;
|
|
472
|
-
|
|
473
|
-
return <div>{user.name}</div>;
|
|
474
|
-
}
|
|
475
|
-
```
|
|
476
|
-
|
|
477
|
-
### Example: Zustand for Client State
|
|
478
|
-
|
|
479
|
-
```typescript
|
|
480
|
-
// stores/userStore.ts - Simple, functional state
|
|
481
|
-
import { create } from 'zustand';
|
|
482
|
-
|
|
483
|
-
type UserStore = {
|
|
484
|
-
currentUser: User | null;
|
|
485
|
-
setUser: (user: User) => void;
|
|
486
|
-
logout: () => void;
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
export const useUserStore = create<UserStore>((set) => ({
|
|
490
|
-
currentUser: null,
|
|
491
|
-
setUser: (user) => set({ currentUser: user }),
|
|
492
|
-
logout: () => set({ currentUser: null }),
|
|
493
|
-
}));
|
|
494
|
-
|
|
495
|
-
// Usage in component
|
|
496
|
-
export function Header() {
|
|
497
|
-
const currentUser = useUserStore((state) => state.currentUser);
|
|
498
|
-
const logout = useUserStore((state) => state.logout);
|
|
499
|
-
|
|
500
|
-
return <button onClick={logout}>{currentUser?.name}</button>;
|
|
501
|
-
}
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
## File Organization
|
|
505
|
-
|
|
506
|
-
```
|
|
507
|
-
src/
|
|
508
|
-
├── components/ # React components (presentation only)
|
|
509
|
-
│ ├── UserProfile/
|
|
510
|
-
│ │ ├── UserProfile.tsx
|
|
511
|
-
│ │ ├── UserProfile.test.tsx
|
|
512
|
-
│ │ └── index.ts
|
|
513
|
-
│ └── shared/ # Reusable UI components
|
|
514
|
-
│ ├── Button.tsx
|
|
515
|
-
│ └── Card.tsx
|
|
516
|
-
│
|
|
517
|
-
├── hooks/ # Custom hooks (React bridge to services)
|
|
518
|
-
│ ├── useUser.ts
|
|
519
|
-
│ ├── useOrders.ts
|
|
520
|
-
│ └── useAuth.ts
|
|
521
|
-
│
|
|
522
|
-
├── services/ # Business logic (framework-free)
|
|
523
|
-
│ ├── userService.ts
|
|
524
|
-
│ ├── orderService.ts
|
|
525
|
-
│ └── authService.ts
|
|
526
|
-
│
|
|
527
|
-
├── models/ # Domain models and types
|
|
528
|
-
│ ├── User.ts
|
|
529
|
-
│ ├── Order.ts
|
|
530
|
-
│ └── types.ts
|
|
531
|
-
│
|
|
532
|
-
├── lib/ # Utilities (API client, formatters, etc.)
|
|
533
|
-
│ ├── apiClient.ts
|
|
534
|
-
│ └── validators.ts
|
|
535
|
-
│
|
|
536
|
-
└── App.tsx
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
## Testing Strategy
|
|
540
|
-
|
|
541
|
-
**ALWAYS write tests BEFORE implementation (Test-Driven Development).**
|
|
542
|
-
|
|
543
|
-
### TDD Workflow
|
|
544
|
-
|
|
545
|
-
```typescript
|
|
546
|
-
// 1. RED: Write failing test first
|
|
547
|
-
describe('UserService', () => {
|
|
548
|
-
it('should validate email format before API call', async () => {
|
|
549
|
-
await expect(
|
|
550
|
-
userService.updateUserEmail('1', 'invalid-email')
|
|
551
|
-
).rejects.toThrow('Invalid email format');
|
|
552
|
-
|
|
553
|
-
expect(apiClient.patch).not.toHaveBeenCalled();
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
// 2. GREEN: Write minimal code to pass
|
|
558
|
-
export async function updateUserEmail(userId: string, email: string): Promise<User> {
|
|
559
|
-
if (!isValidEmail(email)) {
|
|
560
|
-
throw new Error('Invalid email format');
|
|
561
|
-
}
|
|
562
|
-
const response = await apiClient.patch(`/users/${userId}`, { email });
|
|
563
|
-
return response.data;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// 3. REFACTOR: Clean up while keeping tests green
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
### Test Layer Strategy
|
|
570
|
-
|
|
571
|
-
**Services (Framework-Free):** Test first, 90%+ coverage
|
|
572
|
-
```typescript
|
|
573
|
-
// userService.test.ts - Write BEFORE userService.ts
|
|
574
|
-
import { userService } from './userService';
|
|
575
|
-
import { apiClient } from '@/lib/apiClient';
|
|
576
|
-
|
|
577
|
-
jest.mock('@/lib/apiClient');
|
|
578
|
-
|
|
579
|
-
describe('UserService - TDD', () => {
|
|
580
|
-
// Test validates business rules
|
|
581
|
-
test('validates email before updating', async () => {
|
|
582
|
-
await expect(
|
|
583
|
-
userService.updateUserEmail('1', 'invalid-email')
|
|
584
|
-
).rejects.toThrow('Invalid email format');
|
|
585
|
-
|
|
586
|
-
expect(apiClient.patch).not.toHaveBeenCalled();
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
test('updates email when valid', async () => {
|
|
590
|
-
const mockUser = { id: '1', email: 'new@example.com' };
|
|
591
|
-
(apiClient.patch as jest.Mock).mockResolvedValue({ data: mockUser });
|
|
592
|
-
|
|
593
|
-
const result = await userService.updateUserEmail('1', 'new@example.com');
|
|
594
|
-
|
|
595
|
-
expect(apiClient.patch).toHaveBeenCalledWith('/users/1', {
|
|
596
|
-
email: 'new@example.com'
|
|
597
|
-
});
|
|
598
|
-
expect(result.email).toBe('new@example.com');
|
|
599
|
-
});
|
|
600
|
-
});
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
**Hooks (React Integration):** Test React behavior
|
|
604
|
-
```tsx
|
|
605
|
-
// useUser.test.ts
|
|
606
|
-
import { renderHook, waitFor } from '@testing-library/react';
|
|
607
|
-
import { useUser } from './useUser';
|
|
608
|
-
import { userService } from '@/services/userService';
|
|
609
|
-
|
|
610
|
-
jest.mock('@/services/userService');
|
|
611
|
-
|
|
612
|
-
test('loads user on mount', async () => {
|
|
613
|
-
const mockUser = { id: '1', name: 'John', email: 'john@example.com' };
|
|
614
|
-
(userService.getUser as jest.Mock).mockResolvedValue(mockUser);
|
|
615
|
-
|
|
616
|
-
const { result } = renderHook(() => useUser('1'));
|
|
617
|
-
|
|
618
|
-
expect(result.current.loading).toBe(true);
|
|
619
|
-
|
|
620
|
-
await waitFor(() => {
|
|
621
|
-
expect(result.current.loading).toBe(false);
|
|
622
|
-
expect(result.current.user).toEqual(mockUser);
|
|
623
|
-
});
|
|
624
|
-
});
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
**Components (UI):** Test rendering and user interactions
|
|
628
|
-
```tsx
|
|
629
|
-
// UserProfile.test.tsx
|
|
630
|
-
import { render, screen } from '@testing-library/react';
|
|
631
|
-
import { UserProfile } from './UserProfile';
|
|
632
|
-
|
|
633
|
-
// Mock the hook
|
|
634
|
-
jest.mock('@/hooks/useUser', () => ({
|
|
635
|
-
useUser: () => ({
|
|
636
|
-
user: { id: '1', name: 'John', email: 'john@example.com' },
|
|
637
|
-
loading: false,
|
|
638
|
-
updateEmail: jest.fn()
|
|
639
|
-
})
|
|
640
|
-
}));
|
|
641
|
-
|
|
642
|
-
test('renders user name', () => {
|
|
643
|
-
render(<UserProfile userId="1" />);
|
|
644
|
-
expect(screen.getByText('John')).toBeInTheDocument();
|
|
645
|
-
});
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
### Test Coverage Goals
|
|
649
|
-
|
|
650
|
-
- **Services/Business Logic**: 90%+ coverage (test-first, critical paths)
|
|
651
|
-
- **Hooks**: 80%+ coverage (state/effect integration)
|
|
652
|
-
- **Components**: 70%+ coverage (user interactions, conditional rendering)
|
|
653
|
-
- **Models/Utilities**: 100% coverage (pure functions, edge cases)
|
|
654
|
-
|
|
655
|
-
## Architecture Summary
|
|
656
|
-
|
|
657
|
-
1. **Components** = Presentation (UI rendering, event handlers)
|
|
658
|
-
- Import: hooks, UI libraries, CSS
|
|
659
|
-
- Export: JSX components
|
|
660
|
-
- No business logic, no API calls
|
|
661
|
-
|
|
662
|
-
2. **Hooks** = React Bridge (state management, effects)
|
|
663
|
-
- Import: React hooks, services
|
|
664
|
-
- Export: State + actions
|
|
665
|
-
- Minimal logic (mostly wiring)
|
|
666
|
-
|
|
667
|
-
3. **Services** = Business Logic (framework-free)
|
|
668
|
-
- Import: models, utilities, API client
|
|
669
|
-
- Export: Business operations
|
|
670
|
-
- No React dependencies
|
|
671
|
-
|
|
672
|
-
4. **Models** = Domain Rules (types, validation, transformations)
|
|
673
|
-
- Import: nothing or minimal utilities
|
|
674
|
-
- Export: Classes, types, validators
|
|
675
|
-
- Pure TypeScript
|
|
676
|
-
|
|
677
|
-
**Reference:** See `patterns/architecture/clean-architecture.md` and `patterns/architecture/layered-architecture.md` for detailed architectural guidance.
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# State: Zustand
|
|
2
|
-
- **ALWAYS** use Zustand for global state management.
|
|
3
|
-
- Define stores in `src/stores/` or `src/hooks/`.
|
|
4
|
-
- **PREFER** the `create(set => ({ ... }))` syntax.
|
|
5
|
-
- **ACTIONS** should be defined as methods inside the created store object.
|
|
6
|
-
- **NEVER** mutate state directly. Always use the `set` function.
|
|
7
|
-
- `set({ count: state.count + 1 })`
|