ginskill-init 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +226 -0
- package/package.json +20 -0
- package/skills/ai-asset-generator/SKILL.md +255 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/generate-credit-assets.mjs +180 -0
- package/skills/ai-asset-generator/generate-ginbrowser-assets.mjs +242 -0
- package/skills/ai-asset-generator/generate-sty-icon.mjs +149 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +38 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +88 -0
- package/skills/ai-asset-generator/scripts/scaffold-generator.mjs +203 -0
- package/skills/ai-build-ai/SKILL.md +124 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/scripts/load-tutorial.sh +54 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +321 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/review-code/scripts/check-module.sh +201 -0
- package/skills/review-code/scripts/deep-scan.sh +604 -0
- package/skills/review-code/scripts/dep-check.sh +522 -0
- package/skills/review-code/scripts/detect-duplicates.sh +466 -0
- package/skills/review-code/scripts/format-check.sh +577 -0
- package/skills/review-code/scripts/run-review.sh +167 -0
- package/skills/review-code/scripts/scan-codebase.sh +152 -0
- package/skills/security-scanner/SKILL.md +327 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/security-scanner/scripts/security-scan.sh +478 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
# FSD Advanced Patterns
|
|
2
|
+
|
|
3
|
+
Detailed reference for advanced Feature-Sliced Design patterns. The main SKILL.md covers the core methodology — this file provides composition patterns, state management integration, testing strategy, and real-world edge cases.
|
|
4
|
+
|
|
5
|
+
## Composition Patterns
|
|
6
|
+
|
|
7
|
+
### How layers compose together
|
|
8
|
+
|
|
9
|
+
The key to FSD is **composition at higher layers**. Lower layers are independent; higher layers wire them together.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Page composes → Widgets + Features + Entities
|
|
13
|
+
Widget composes → Features + Entities
|
|
14
|
+
Feature uses → Entities + Shared
|
|
15
|
+
Entity uses → Shared only
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Example: Product page composition
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
// pages/product-detail/ui/ProductDetailPage.tsx
|
|
22
|
+
import { ProductInfo } from '@/widgets/product-info'
|
|
23
|
+
import { AddToCart } from '@/features/add-to-cart'
|
|
24
|
+
import { ProductReviews } from '@/widgets/product-reviews'
|
|
25
|
+
import { RecommendedProducts } from '@/widgets/recommended-products'
|
|
26
|
+
|
|
27
|
+
export const ProductDetailPage = ({ id }: { id: string }) => (
|
|
28
|
+
<div>
|
|
29
|
+
<ProductInfo productId={id} />
|
|
30
|
+
<AddToCart productId={id} />
|
|
31
|
+
<ProductReviews productId={id} />
|
|
32
|
+
<RecommendedProducts productId={id} />
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
// widgets/product-info/ui/ProductInfo.tsx
|
|
39
|
+
import { ProductCard } from '@/entities/product'
|
|
40
|
+
import { FavoriteButton } from '@/features/toggle-favorite'
|
|
41
|
+
import { ShareButton } from '@/features/share-product'
|
|
42
|
+
|
|
43
|
+
export const ProductInfo = ({ productId }: { productId: string }) => {
|
|
44
|
+
// Widget composes entity UI with feature actions
|
|
45
|
+
return (
|
|
46
|
+
<ProductCard id={productId}>
|
|
47
|
+
<FavoriteButton productId={productId} />
|
|
48
|
+
<ShareButton productId={productId} />
|
|
49
|
+
</ProductCard>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Render props / slots pattern
|
|
55
|
+
|
|
56
|
+
When an entity needs feature-level actions but can't import features (wrong direction), use composition via props:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// entities/todo/ui/TodoCard.tsx
|
|
60
|
+
interface TodoCardProps {
|
|
61
|
+
todo: Todo
|
|
62
|
+
actions?: React.ReactNode // slot for feature-level actions
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const TodoCard = ({ todo, actions }: TodoCardProps) => (
|
|
66
|
+
<div>
|
|
67
|
+
<h3>{todo.title}</h3>
|
|
68
|
+
<p>{todo.description}</p>
|
|
69
|
+
{actions && <div className="actions">{actions}</div>}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// widgets/todo-list/ui/TodoList.tsx — composes entity + features
|
|
76
|
+
import { TodoCard } from '@/entities/todo'
|
|
77
|
+
import { ToggleButton } from '@/features/toggle-todo'
|
|
78
|
+
import { DeleteButton } from '@/features/delete-todo'
|
|
79
|
+
|
|
80
|
+
export const TodoList = ({ todos }: { todos: Todo[] }) => (
|
|
81
|
+
<ul>
|
|
82
|
+
{todos.map(todo => (
|
|
83
|
+
<TodoCard
|
|
84
|
+
key={todo.id}
|
|
85
|
+
todo={todo}
|
|
86
|
+
actions={
|
|
87
|
+
<>
|
|
88
|
+
<ToggleButton todoId={todo.id} />
|
|
89
|
+
<DeleteButton todoId={todo.id} />
|
|
90
|
+
</>
|
|
91
|
+
}
|
|
92
|
+
/>
|
|
93
|
+
))}
|
|
94
|
+
</ul>
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## State Management Integration
|
|
99
|
+
|
|
100
|
+
### Zustand with FSD
|
|
101
|
+
|
|
102
|
+
Each entity/feature owns its own store:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// entities/user/model/user.store.ts
|
|
106
|
+
import { create } from 'zustand'
|
|
107
|
+
import type { User } from './user.types'
|
|
108
|
+
|
|
109
|
+
interface UserStore {
|
|
110
|
+
user: User | null
|
|
111
|
+
setUser: (user: User) => void
|
|
112
|
+
clear: () => void
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const useUserStore = create<UserStore>((set) => ({
|
|
116
|
+
user: null,
|
|
117
|
+
setUser: (user) => set({ user }),
|
|
118
|
+
clear: () => set({ user: null }),
|
|
119
|
+
}))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// entities/user/index.ts
|
|
124
|
+
export { useUserStore } from './model/user.store'
|
|
125
|
+
export type { User } from './model/user.types'
|
|
126
|
+
export { UserAvatar } from './ui/UserAvatar'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### React Query with FSD
|
|
130
|
+
|
|
131
|
+
Query hooks live in the entity/feature that owns the data:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// entities/product/api/product.api.ts
|
|
135
|
+
import { apiClient } from '@/shared/api'
|
|
136
|
+
import type { Product } from '../model/product.types'
|
|
137
|
+
|
|
138
|
+
export const productApi = {
|
|
139
|
+
getAll: (filters?: ProductFilters) =>
|
|
140
|
+
apiClient.get<Product[]>('/products', { params: filters }),
|
|
141
|
+
getById: (id: string) =>
|
|
142
|
+
apiClient.get<Product>(`/products/${id}`),
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// entities/product/model/product.queries.ts
|
|
148
|
+
import { useQuery } from '@tanstack/react-query'
|
|
149
|
+
import { productApi } from '../api/product.api'
|
|
150
|
+
|
|
151
|
+
const productKeys = {
|
|
152
|
+
all: ['products'] as const,
|
|
153
|
+
lists: () => [...productKeys.all, 'list'] as const,
|
|
154
|
+
list: (filters: ProductFilters) => [...productKeys.lists(), filters] as const,
|
|
155
|
+
details: () => [...productKeys.all, 'detail'] as const,
|
|
156
|
+
detail: (id: string) => [...productKeys.details(), id] as const,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const useProducts = (filters?: ProductFilters) =>
|
|
160
|
+
useQuery({
|
|
161
|
+
queryKey: productKeys.list(filters ?? {}),
|
|
162
|
+
queryFn: () => productApi.getAll(filters),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
export const useProduct = (id: string) =>
|
|
166
|
+
useQuery({
|
|
167
|
+
queryKey: productKeys.detail(id),
|
|
168
|
+
queryFn: () => productApi.getById(id),
|
|
169
|
+
enabled: !!id,
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// features/add-to-cart/model/useAddToCart.ts
|
|
175
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
176
|
+
import { cartApi } from '@/entities/cart'
|
|
177
|
+
|
|
178
|
+
export const useAddToCart = () => {
|
|
179
|
+
const queryClient = useQueryClient()
|
|
180
|
+
return useMutation({
|
|
181
|
+
mutationFn: cartApi.addItem,
|
|
182
|
+
onSuccess: () => {
|
|
183
|
+
queryClient.invalidateQueries({ queryKey: ['cart'] })
|
|
184
|
+
},
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Redux Toolkit with FSD
|
|
190
|
+
|
|
191
|
+
Each slice in Redux maps to an FSD entity or feature:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// entities/todo/model/todo.slice.ts
|
|
195
|
+
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
|
|
196
|
+
import { todoApi } from '../api/todo.api'
|
|
197
|
+
|
|
198
|
+
export const fetchTodos = createAsyncThunk('todos/fetch', todoApi.getAll)
|
|
199
|
+
|
|
200
|
+
const todoSlice = createSlice({
|
|
201
|
+
name: 'todos',
|
|
202
|
+
initialState: { items: [], status: 'idle' },
|
|
203
|
+
reducers: {},
|
|
204
|
+
extraReducers: (builder) => {
|
|
205
|
+
builder
|
|
206
|
+
.addCase(fetchTodos.pending, (state) => { state.status = 'loading' })
|
|
207
|
+
.addCase(fetchTodos.fulfilled, (state, action) => {
|
|
208
|
+
state.items = action.payload
|
|
209
|
+
state.status = 'idle'
|
|
210
|
+
})
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
export const todoReducer = todoSlice.reducer
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Root store lives in `app/`:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// app/store/index.ts
|
|
221
|
+
import { configureStore } from '@reduxjs/toolkit'
|
|
222
|
+
import { todoReducer } from '@/entities/todo'
|
|
223
|
+
import { userReducer } from '@/entities/user'
|
|
224
|
+
|
|
225
|
+
export const store = configureStore({
|
|
226
|
+
reducer: {
|
|
227
|
+
todos: todoReducer,
|
|
228
|
+
user: userReducer,
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Testing Strategy
|
|
234
|
+
|
|
235
|
+
### Where tests live
|
|
236
|
+
|
|
237
|
+
Tests live **inside their slice**, next to the code they test:
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
features/
|
|
241
|
+
├── authentication/
|
|
242
|
+
│ ├── ui/
|
|
243
|
+
│ │ ├── LoginForm.tsx
|
|
244
|
+
│ │ └── LoginForm.test.tsx ← component test
|
|
245
|
+
│ ├── model/
|
|
246
|
+
│ │ ├── auth.store.ts
|
|
247
|
+
│ │ └── auth.store.test.ts ← unit test
|
|
248
|
+
│ ├── api/
|
|
249
|
+
│ │ └── auth.api.test.ts ← API mock test
|
|
250
|
+
│ └── index.ts
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Or use a `__tests__` segment:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
features/
|
|
257
|
+
├── authentication/
|
|
258
|
+
│ ├── ui/
|
|
259
|
+
│ ├── model/
|
|
260
|
+
│ ├── __tests__/
|
|
261
|
+
│ │ ├── LoginForm.test.tsx
|
|
262
|
+
│ │ └── auth.store.test.ts
|
|
263
|
+
│ └── index.ts
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Test isolation
|
|
267
|
+
|
|
268
|
+
Tests should import from the **public API** just like any other consumer:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// features/authentication/__tests__/LoginForm.test.tsx
|
|
272
|
+
import { LoginForm } from '../index' // or '@/features/authentication'
|
|
273
|
+
|
|
274
|
+
// NOT from internal paths
|
|
275
|
+
// import { LoginForm } from '../ui/LoginForm' ← avoid
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Integration tests
|
|
279
|
+
|
|
280
|
+
Integration tests that span multiple slices live at the page or widget level:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
pages/
|
|
284
|
+
├── home/
|
|
285
|
+
│ ├── ui/
|
|
286
|
+
│ │ └── HomePage.tsx
|
|
287
|
+
│ ├── __tests__/
|
|
288
|
+
│ │ └── HomePage.integration.test.tsx
|
|
289
|
+
│ └── index.ts
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### E2E tests
|
|
293
|
+
|
|
294
|
+
E2E tests live outside the `src/` tree:
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
e2e/
|
|
298
|
+
├── authentication.spec.ts
|
|
299
|
+
├── checkout.spec.ts
|
|
300
|
+
└── search.spec.ts
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Handling Shared Types
|
|
304
|
+
|
|
305
|
+
### Types that belong in entities
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// entities/user/model/user.types.ts
|
|
309
|
+
export interface User {
|
|
310
|
+
id: string
|
|
311
|
+
name: string
|
|
312
|
+
email: string
|
|
313
|
+
avatar?: string
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Types that belong in shared
|
|
318
|
+
|
|
319
|
+
Only generic, non-domain types:
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
// shared/types/index.ts
|
|
323
|
+
export interface PaginatedResponse<T> {
|
|
324
|
+
items: T[]
|
|
325
|
+
total: number
|
|
326
|
+
page: number
|
|
327
|
+
pageSize: number
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface ApiError {
|
|
331
|
+
message: string
|
|
332
|
+
code: string
|
|
333
|
+
statusCode: number
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### DTO types
|
|
338
|
+
|
|
339
|
+
DTOs (Data Transfer Objects) live in the `api` segment of the entity that owns them:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// entities/product/api/product.dto.ts
|
|
343
|
+
export interface CreateProductDto {
|
|
344
|
+
name: string
|
|
345
|
+
price: number
|
|
346
|
+
categoryId: string
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export interface ProductResponseDto {
|
|
350
|
+
id: string
|
|
351
|
+
name: string
|
|
352
|
+
price: number
|
|
353
|
+
category: { id: string; name: string }
|
|
354
|
+
createdAt: string
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Real-World API Integration
|
|
359
|
+
|
|
360
|
+
Concrete examples using actual Styai/EasyCloset API endpoints for a log management admin dashboard.
|
|
361
|
+
|
|
362
|
+
### Entity: `system-log`
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
// entities/system-log/model/system-log.types.ts
|
|
366
|
+
export type LogKind = 'APPLICATION' | 'ACCESS' | 'ERROR' | 'AUDIT' | 'SECURITY'
|
|
367
|
+
export type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL'
|
|
368
|
+
|
|
369
|
+
export interface SystemLog {
|
|
370
|
+
id: string
|
|
371
|
+
kind: LogKind
|
|
372
|
+
level: LogLevel
|
|
373
|
+
message: string
|
|
374
|
+
source: string
|
|
375
|
+
timestamp: string
|
|
376
|
+
metadata?: Record<string, unknown>
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface LogFile {
|
|
380
|
+
filename: string
|
|
381
|
+
size: number
|
|
382
|
+
lastModified: string
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export interface LogSearchParams {
|
|
386
|
+
type?: LogKind
|
|
387
|
+
level?: LogLevel
|
|
388
|
+
startDate?: string
|
|
389
|
+
endDate?: string
|
|
390
|
+
keyword?: string
|
|
391
|
+
page?: number
|
|
392
|
+
pageSize?: number
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
// entities/system-log/api/system-log.api.ts
|
|
398
|
+
import { apiClient } from '@/shared/api'
|
|
399
|
+
import type { SystemLog, LogFile, LogSearchParams } from '../model/system-log.types'
|
|
400
|
+
import type { PaginatedResponse } from '@/shared/types'
|
|
401
|
+
|
|
402
|
+
export const systemLogApi = {
|
|
403
|
+
getTypes: () =>
|
|
404
|
+
apiClient.get<string[]>('/api/v1/system/logs/types'),
|
|
405
|
+
|
|
406
|
+
getFiles: (type: string) =>
|
|
407
|
+
apiClient.get<LogFile[]>(`/api/v1/system/logs/${type}/files`),
|
|
408
|
+
|
|
409
|
+
getContent: (type: string, filename: string, params?: { lines?: number }) =>
|
|
410
|
+
apiClient.get<string[]>(`/api/v1/system/logs/${type}/${filename}`, { params }),
|
|
411
|
+
|
|
412
|
+
search: (params: LogSearchParams) =>
|
|
413
|
+
apiClient.get<PaginatedResponse<SystemLog>>('/api/v1/system/logs/search', { params }),
|
|
414
|
+
|
|
415
|
+
download: (type: string, filename: string) =>
|
|
416
|
+
apiClient.get<Blob>(`/api/v1/system/logs/${type}/${filename}/download`, {
|
|
417
|
+
responseType: 'blob',
|
|
418
|
+
}),
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// entities/system-log/model/system-log.queries.ts
|
|
424
|
+
import { useQuery } from '@tanstack/react-query'
|
|
425
|
+
import { systemLogApi } from '../api/system-log.api'
|
|
426
|
+
import type { LogSearchParams } from './system-log.types'
|
|
427
|
+
|
|
428
|
+
export const systemLogKeys = {
|
|
429
|
+
all: ['system-logs'] as const,
|
|
430
|
+
types: () => [...systemLogKeys.all, 'types'] as const,
|
|
431
|
+
files: (type: string) => [...systemLogKeys.all, 'files', type] as const,
|
|
432
|
+
content: (type: string, filename: string) =>
|
|
433
|
+
[...systemLogKeys.all, 'content', type, filename] as const,
|
|
434
|
+
search: (params: LogSearchParams) =>
|
|
435
|
+
[...systemLogKeys.all, 'search', params] as const,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export const useLogTypes = () =>
|
|
439
|
+
useQuery({
|
|
440
|
+
queryKey: systemLogKeys.types(),
|
|
441
|
+
queryFn: () => systemLogApi.getTypes(),
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
export const useLogFiles = (type: string) =>
|
|
445
|
+
useQuery({
|
|
446
|
+
queryKey: systemLogKeys.files(type),
|
|
447
|
+
queryFn: () => systemLogApi.getFiles(type),
|
|
448
|
+
enabled: !!type,
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
export const useLogContent = (type: string, filename: string) =>
|
|
452
|
+
useQuery({
|
|
453
|
+
queryKey: systemLogKeys.content(type, filename),
|
|
454
|
+
queryFn: () => systemLogApi.getContent(type, filename),
|
|
455
|
+
enabled: !!type && !!filename,
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
export const useLogSearch = (params: LogSearchParams) =>
|
|
459
|
+
useQuery({
|
|
460
|
+
queryKey: systemLogKeys.search(params),
|
|
461
|
+
queryFn: () => systemLogApi.search(params),
|
|
462
|
+
})
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// entities/system-log/index.ts
|
|
467
|
+
export { LogTable } from './ui/LogTable'
|
|
468
|
+
export { LogLevelBadge } from './ui/LogLevelBadge'
|
|
469
|
+
export type { SystemLog, LogFile, LogKind, LogLevel, LogSearchParams } from './model/system-log.types'
|
|
470
|
+
export { useLogTypes, useLogFiles, useLogContent, useLogSearch, systemLogKeys } from './model/system-log.queries'
|
|
471
|
+
export { systemLogApi } from './api/system-log.api'
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Feature: `search-logs`
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// features/search-logs/model/useLogSearchForm.ts
|
|
478
|
+
import { useState, useCallback } from 'react'
|
|
479
|
+
import type { LogKind, LogLevel, LogSearchParams } from '@/entities/system-log'
|
|
480
|
+
|
|
481
|
+
export const useLogSearchForm = () => {
|
|
482
|
+
const [filters, setFilters] = useState<LogSearchParams>({})
|
|
483
|
+
|
|
484
|
+
const updateFilter = useCallback(<K extends keyof LogSearchParams>(
|
|
485
|
+
key: K,
|
|
486
|
+
value: LogSearchParams[K],
|
|
487
|
+
) => {
|
|
488
|
+
setFilters(prev => ({ ...prev, [key]: value }))
|
|
489
|
+
}, [])
|
|
490
|
+
|
|
491
|
+
const resetFilters = useCallback(() => setFilters({}), [])
|
|
492
|
+
|
|
493
|
+
return { filters, updateFilter, resetFilters }
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
// features/search-logs/ui/LogSearchForm.tsx
|
|
499
|
+
import { useLogTypes } from '@/entities/system-log'
|
|
500
|
+
import type { LogKind, LogLevel, LogSearchParams } from '@/entities/system-log'
|
|
501
|
+
import { Input, Select, Button } from '@/shared/ui'
|
|
502
|
+
|
|
503
|
+
interface LogSearchFormProps {
|
|
504
|
+
filters: LogSearchParams
|
|
505
|
+
onFilterChange: <K extends keyof LogSearchParams>(key: K, value: LogSearchParams[K]) => void
|
|
506
|
+
onReset: () => void
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export const LogSearchForm = ({ filters, onFilterChange, onReset }: LogSearchFormProps) => {
|
|
510
|
+
const { data: logTypes } = useLogTypes()
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<form className="flex gap-3 items-end flex-wrap">
|
|
514
|
+
<Select
|
|
515
|
+
label="Type"
|
|
516
|
+
value={filters.type ?? ''}
|
|
517
|
+
onChange={(v) => onFilterChange('type', v as LogKind)}
|
|
518
|
+
options={logTypes?.map(t => ({ label: t, value: t })) ?? []}
|
|
519
|
+
/>
|
|
520
|
+
<Select
|
|
521
|
+
label="Level"
|
|
522
|
+
value={filters.level ?? ''}
|
|
523
|
+
onChange={(v) => onFilterChange('level', v as LogLevel)}
|
|
524
|
+
options={['DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'].map(l => ({ label: l, value: l }))}
|
|
525
|
+
/>
|
|
526
|
+
<Input
|
|
527
|
+
label="From"
|
|
528
|
+
type="date"
|
|
529
|
+
value={filters.startDate ?? ''}
|
|
530
|
+
onChange={(v) => onFilterChange('startDate', v)}
|
|
531
|
+
/>
|
|
532
|
+
<Input
|
|
533
|
+
label="To"
|
|
534
|
+
type="date"
|
|
535
|
+
value={filters.endDate ?? ''}
|
|
536
|
+
onChange={(v) => onFilterChange('endDate', v)}
|
|
537
|
+
/>
|
|
538
|
+
<Input
|
|
539
|
+
label="Keyword"
|
|
540
|
+
value={filters.keyword ?? ''}
|
|
541
|
+
onChange={(v) => onFilterChange('keyword', v)}
|
|
542
|
+
placeholder="Search log messages..."
|
|
543
|
+
/>
|
|
544
|
+
<Button variant="ghost" onClick={onReset}>Reset</Button>
|
|
545
|
+
</form>
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
// features/search-logs/index.ts
|
|
552
|
+
export { LogSearchForm } from './ui/LogSearchForm'
|
|
553
|
+
export { useLogSearchForm } from './model/useLogSearchForm'
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Feature: `download-log-file`
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
// features/download-log-file/model/useDownloadLog.ts
|
|
560
|
+
import { useMutation } from '@tanstack/react-query'
|
|
561
|
+
import { systemLogApi } from '@/entities/system-log'
|
|
562
|
+
|
|
563
|
+
export const useDownloadLog = () =>
|
|
564
|
+
useMutation({
|
|
565
|
+
mutationFn: ({ type, filename }: { type: string; filename: string }) =>
|
|
566
|
+
systemLogApi.download(type, filename),
|
|
567
|
+
onSuccess: (blob, { filename }) => {
|
|
568
|
+
const url = URL.createObjectURL(blob)
|
|
569
|
+
const a = document.createElement('a')
|
|
570
|
+
a.href = url
|
|
571
|
+
a.download = filename
|
|
572
|
+
a.click()
|
|
573
|
+
URL.revokeObjectURL(url)
|
|
574
|
+
},
|
|
575
|
+
})
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
// features/download-log-file/ui/DownloadLogButton.tsx
|
|
580
|
+
import { Button } from '@/shared/ui'
|
|
581
|
+
import { useDownloadLog } from '../model/useDownloadLog'
|
|
582
|
+
|
|
583
|
+
interface DownloadLogButtonProps {
|
|
584
|
+
type: string
|
|
585
|
+
filename: string
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export const DownloadLogButton = ({ type, filename }: DownloadLogButtonProps) => {
|
|
589
|
+
const { mutate, isPending } = useDownloadLog()
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<Button
|
|
593
|
+
variant="ghost"
|
|
594
|
+
size="sm"
|
|
595
|
+
disabled={isPending}
|
|
596
|
+
onClick={() => mutate({ type, filename })}
|
|
597
|
+
>
|
|
598
|
+
{isPending ? 'Downloading...' : 'Download'}
|
|
599
|
+
</Button>
|
|
600
|
+
)
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// features/download-log-file/index.ts
|
|
606
|
+
export { DownloadLogButton } from './ui/DownloadLogButton'
|
|
607
|
+
export { useDownloadLog } from './model/useDownloadLog'
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Widget: `log-viewer`
|
|
611
|
+
|
|
612
|
+
```typescript
|
|
613
|
+
// widgets/log-viewer/ui/LogViewer.tsx
|
|
614
|
+
import { LogTable, useLogSearch } from '@/entities/system-log'
|
|
615
|
+
import { LogSearchForm, useLogSearchForm } from '@/features/search-logs'
|
|
616
|
+
import { DownloadLogButton } from '@/features/download-log-file'
|
|
617
|
+
|
|
618
|
+
export const LogViewer = () => {
|
|
619
|
+
const { filters, updateFilter, resetFilters } = useLogSearchForm()
|
|
620
|
+
const { data, isLoading } = useLogSearch(filters)
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<div className="space-y-4">
|
|
624
|
+
<LogSearchForm
|
|
625
|
+
filters={filters}
|
|
626
|
+
onFilterChange={updateFilter}
|
|
627
|
+
onReset={resetFilters}
|
|
628
|
+
/>
|
|
629
|
+
<LogTable
|
|
630
|
+
logs={data?.items ?? []}
|
|
631
|
+
isLoading={isLoading}
|
|
632
|
+
actions={(log) => (
|
|
633
|
+
<DownloadLogButton type={log.kind} filename={`${log.id}.log`} />
|
|
634
|
+
)}
|
|
635
|
+
/>
|
|
636
|
+
</div>
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
// widgets/log-viewer/index.ts
|
|
643
|
+
export { LogViewer } from './ui/LogViewer'
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Page: `log-management`
|
|
647
|
+
|
|
648
|
+
```typescript
|
|
649
|
+
// pages/@admin/log-management/ui/LogManagementPage.tsx
|
|
650
|
+
import { LogViewer } from '@/widgets/log-viewer'
|
|
651
|
+
|
|
652
|
+
export const LogManagementPage = () => (
|
|
653
|
+
<div className="container mx-auto py-6">
|
|
654
|
+
<h1 className="text-2xl font-bold mb-6">Log Management</h1>
|
|
655
|
+
<LogViewer />
|
|
656
|
+
</div>
|
|
657
|
+
)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
```typescript
|
|
661
|
+
// pages/@admin/log-management/index.ts
|
|
662
|
+
export { LogManagementPage } from './ui/LogManagementPage'
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Shared: `client-logger`
|
|
666
|
+
|
|
667
|
+
A fire-and-forget service for sending client-side logs to `POST /api/v1/logs/ingest`:
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
// shared/lib/client-logger.ts
|
|
671
|
+
import { apiClient } from '@/shared/api'
|
|
672
|
+
|
|
673
|
+
type ClientLogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'
|
|
674
|
+
|
|
675
|
+
interface ClientLogEntry {
|
|
676
|
+
level: ClientLogLevel
|
|
677
|
+
message: string
|
|
678
|
+
context?: Record<string, unknown>
|
|
679
|
+
timestamp?: string
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const sendLog = (entry: ClientLogEntry) => {
|
|
683
|
+
const payload = {
|
|
684
|
+
...entry,
|
|
685
|
+
timestamp: entry.timestamp ?? new Date().toISOString(),
|
|
686
|
+
userAgent: navigator.userAgent,
|
|
687
|
+
url: window.location.href,
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Fire-and-forget — don't block UI on logging
|
|
691
|
+
apiClient.post('/api/v1/logs/ingest', payload).catch(() => {
|
|
692
|
+
// Silently fail — logging should never break the app
|
|
693
|
+
})
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export const clientLogger = {
|
|
697
|
+
debug: (message: string, context?: Record<string, unknown>) =>
|
|
698
|
+
sendLog({ level: 'debug', message, context }),
|
|
699
|
+
info: (message: string, context?: Record<string, unknown>) =>
|
|
700
|
+
sendLog({ level: 'info', message, context }),
|
|
701
|
+
warn: (message: string, context?: Record<string, unknown>) =>
|
|
702
|
+
sendLog({ level: 'warn', message, context }),
|
|
703
|
+
error: (message: string, context?: Record<string, unknown>) =>
|
|
704
|
+
sendLog({ level: 'error', message, context }),
|
|
705
|
+
fatal: (message: string, context?: Record<string, unknown>) =>
|
|
706
|
+
sendLog({ level: 'fatal', message, context }),
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
// Usage from any layer:
|
|
712
|
+
import { clientLogger } from '@/shared/lib'
|
|
713
|
+
|
|
714
|
+
// In an error boundary
|
|
715
|
+
clientLogger.error('Unhandled render error', { component: 'App', error: err.message })
|
|
716
|
+
|
|
717
|
+
// In a feature
|
|
718
|
+
clientLogger.info('User completed checkout', { orderId: '123' })
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
## Monorepo / Multi-Package Patterns
|
|
722
|
+
|
|
723
|
+
For large projects, FSD layers can map to packages:
|
|
724
|
+
|
|
725
|
+
```
|
|
726
|
+
packages/
|
|
727
|
+
├── shared/ ← @myapp/shared
|
|
728
|
+
├── entities/ ← @myapp/entities
|
|
729
|
+
├── features/ ← @myapp/features
|
|
730
|
+
└── apps/
|
|
731
|
+
├── web/ ← uses @myapp/* packages
|
|
732
|
+
└── mobile/ ← uses @myapp/* packages
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
Each package maintains the same internal FSD structure.
|
|
736
|
+
|
|
737
|
+
## When NOT to Use FSD
|
|
738
|
+
|
|
739
|
+
- **Very small projects** (< 10 components) — overhead isn't worth it
|
|
740
|
+
- **Prototypes / hackathons** — speed matters more than structure
|
|
741
|
+
- **Solo projects with no growth plan** — simpler flat structure is fine
|
|
742
|
+
|
|
743
|
+
FSD shines when:
|
|
744
|
+
- Multiple developers work on the same codebase
|
|
745
|
+
- The project will grow over months/years
|
|
746
|
+
- Features are added, removed, or modified frequently
|
|
747
|
+
- You want to prevent "spaghetti" imports as the project scales
|