@oalacea/demon 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/CHANGELOG.md +38 -0
- package/LICENSE +23 -0
- package/README.md +103 -0
- package/agents/deps-analyzer.js +366 -0
- package/agents/detector.js +570 -0
- package/agents/fix-engine.js +305 -0
- package/agents/perf-analyzer.js +294 -0
- package/agents/test-generator.js +387 -0
- package/agents/test-runner.js +318 -0
- package/bin/Dockerfile +65 -0
- package/bin/cli.js +455 -0
- package/lib/config.js +237 -0
- package/lib/docker.js +207 -0
- package/lib/reporter.js +297 -0
- package/package.json +34 -0
- package/prompts/DEPS_EFFICIENCY.md +558 -0
- package/prompts/E2E.md +491 -0
- package/prompts/EXECUTE.md +782 -0
- package/prompts/INTEGRATION_API.md +484 -0
- package/prompts/INTEGRATION_DB.md +425 -0
- package/prompts/PERF_API.md +433 -0
- package/prompts/PERF_DB.md +430 -0
- package/prompts/REMEDIATION.md +482 -0
- package/prompts/UNIT.md +260 -0
- package/scripts/dev.js +106 -0
- package/templates/README.md +22 -0
- package/templates/k6/load-test.js +54 -0
- package/templates/playwright/e2e.spec.ts +61 -0
- package/templates/vitest/api.test.ts +51 -0
- package/templates/vitest/component.test.ts +27 -0
- package/templates/vitest/hook.test.ts +36 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
# Dependency Efficiency Analysis Guide
|
|
2
|
+
|
|
3
|
+
This prompt is included by EXECUTE.md. It provides detailed guidance for analyzing dependency usage patterns.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## TanStack Router Analysis
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Check for TanStack Router patterns
|
|
11
|
+
describe('TanStack Router Efficiency', () => {
|
|
12
|
+
it('should have type-safe routes', () => {
|
|
13
|
+
// Check if routes are using typed params
|
|
14
|
+
const routes = findFiles('routes', '.tsx');
|
|
15
|
+
|
|
16
|
+
routes.forEach((route) => {
|
|
17
|
+
const content = readFile(route);
|
|
18
|
+
|
|
19
|
+
// Should use $params or useParams with types
|
|
20
|
+
expect(content).toMatch(/useParams.*</);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should use loaders for data fetching', () => {
|
|
25
|
+
const routes = findFiles('routes', '.tsx');
|
|
26
|
+
|
|
27
|
+
routes.forEach((route) => {
|
|
28
|
+
const content = readFile(route);
|
|
29
|
+
|
|
30
|
+
// Should have loader if route needs data
|
|
31
|
+
if (content.includes('useQuery') || content.includes('useFetch')) {
|
|
32
|
+
expect(content).toMatch(/loader.*=/);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should have error boundaries', () => {
|
|
38
|
+
const routes = findFiles('routes', '.tsx');
|
|
39
|
+
|
|
40
|
+
routes.forEach((route) => {
|
|
41
|
+
const content = readFile(route);
|
|
42
|
+
|
|
43
|
+
// Should have errorBoundary component
|
|
44
|
+
if (content.includes('loader')) {
|
|
45
|
+
expect(content).toMatch(/errorComponent|ErrorBoundary/);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should enable link prefetching', () => {
|
|
51
|
+
const components = findFiles('components', '.tsx');
|
|
52
|
+
|
|
53
|
+
components.forEach((comp) => {
|
|
54
|
+
const content = readFile(comp);
|
|
55
|
+
|
|
56
|
+
// Navigation links should have prefetch
|
|
57
|
+
if (content.includes('<Link')) {
|
|
58
|
+
expect(content).toMatch(/prefetch=|prefetchIntent=/);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Findings Template
|
|
66
|
+
|
|
67
|
+
```markdown
|
|
68
|
+
### TanStack Router Analysis
|
|
69
|
+
|
|
70
|
+
**Good:**
|
|
71
|
+
- ✓ All routes use typed params
|
|
72
|
+
- ✓ Loaders properly implemented for data fetching
|
|
73
|
+
- ✓ Search parameters are typed
|
|
74
|
+
|
|
75
|
+
**Issues Found:**
|
|
76
|
+
- ✗ 3 routes missing error boundaries
|
|
77
|
+
- routes/dashboard.tsx
|
|
78
|
+
- routes/posts.tsx
|
|
79
|
+
- routes/users.tsx
|
|
80
|
+
|
|
81
|
+
- ✗ Navigation links not using prefetch
|
|
82
|
+
- components/Nav.tsx - main menu links
|
|
83
|
+
- components/Footer.tsx - footer links
|
|
84
|
+
|
|
85
|
+
**Recommendations:**
|
|
86
|
+
1. Add errorComponent to routes with loaders
|
|
87
|
+
2. Enable prefetch for frequently accessed navigation links
|
|
88
|
+
3. Consider using preload for critical routes
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## React Query Analysis
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Check for React Query patterns
|
|
97
|
+
describe('React Query Efficiency', () => {
|
|
98
|
+
it('should have proper cache keys', () => {
|
|
99
|
+
const hooks = findFiles('hooks', '.ts');
|
|
100
|
+
const components = findFiles('components', '.tsx');
|
|
101
|
+
|
|
102
|
+
const allFiles = [...hooks, ...components];
|
|
103
|
+
|
|
104
|
+
allFiles.forEach((file) => {
|
|
105
|
+
const content = readFile(file);
|
|
106
|
+
|
|
107
|
+
if (content.includes('useQuery') || content.includes('useInfiniteQuery')) {
|
|
108
|
+
// Should use array-based keys for proper serialization
|
|
109
|
+
expect(content).toMatch(/\[['"`]\w+['"`],/);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should configure staleTime appropriately', () => {
|
|
115
|
+
const hooks = findFiles('hooks', '.ts');
|
|
116
|
+
|
|
117
|
+
hooks.forEach((hook) => {
|
|
118
|
+
const content = readFile(hook);
|
|
119
|
+
|
|
120
|
+
if (content.includes('useQuery')) {
|
|
121
|
+
// Should have staleTime configured
|
|
122
|
+
expect(content).toMatch(/staleTime/);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should have proper invalidation', () => {
|
|
128
|
+
const mutations = findFiles('mutations', '.ts');
|
|
129
|
+
|
|
130
|
+
mutations.forEach((mutation) => {
|
|
131
|
+
const content = readFile(mutation);
|
|
132
|
+
|
|
133
|
+
if (content.includes('useMutation')) {
|
|
134
|
+
// Should invalidate related queries on success
|
|
135
|
+
expect(content).toMatch(/invalidateQueries/);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should use suspense when beneficial', () => {
|
|
141
|
+
const components = findFiles('components', '.tsx');
|
|
142
|
+
|
|
143
|
+
components.forEach((comp) => {
|
|
144
|
+
const content = readFile(comp);
|
|
145
|
+
|
|
146
|
+
// Server components or suspense boundary
|
|
147
|
+
if (content.includes('useSuspenseQuery') || content.includes('Suspense')) {
|
|
148
|
+
expect(content).toBeTruthy();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Findings Template
|
|
156
|
+
|
|
157
|
+
```markdown
|
|
158
|
+
### React Query Analysis
|
|
159
|
+
|
|
160
|
+
**Good:**
|
|
161
|
+
- ✓ All queries use array-based cache keys
|
|
162
|
+
- ✓ Mutations properly invalidate related queries
|
|
163
|
+
- ✓ QueryClient configured with reasonable defaults
|
|
164
|
+
|
|
165
|
+
**Issues Found:**
|
|
166
|
+
- ✗ 5 queries missing staleTime configuration
|
|
167
|
+
- hooks/useUsers.ts - will refetch on focus
|
|
168
|
+
- hooks/usePosts.ts - no cache duration
|
|
169
|
+
- hooks/useComments.ts - no cache duration
|
|
170
|
+
- hooks/useAuth.ts - no cache duration
|
|
171
|
+
- hooks/useSettings.ts - no cache duration
|
|
172
|
+
|
|
173
|
+
- ✗ No retry configuration for failed queries
|
|
174
|
+
- ✗ Missing optimistic updates for like/comment mutations
|
|
175
|
+
|
|
176
|
+
**Recommendations:**
|
|
177
|
+
1. Add staleTime: 30000 for semi-static data
|
|
178
|
+
2. Configure retry: 1 for user-facing queries
|
|
179
|
+
3. Implement optimistic updates for social interactions
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Prisma Analysis
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
// Check for Prisma patterns
|
|
188
|
+
describe('Prisma Efficiency', () => {
|
|
189
|
+
it('should use select for partial data', () => {
|
|
190
|
+
const files = findFiles(['src', 'lib'], '.ts');
|
|
191
|
+
|
|
192
|
+
files.forEach((file) => {
|
|
193
|
+
const content = readFile(file);
|
|
194
|
+
|
|
195
|
+
// If only using some fields, should use select
|
|
196
|
+
if (content.includes('prisma.') && content.includes('findMany')) {
|
|
197
|
+
// Check if result is destructured
|
|
198
|
+
if (content.match(/findMany.*\{[^}]*\}/)) {
|
|
199
|
+
expect(content).toMatch(/select:/);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should not have N+1 queries', () => {
|
|
206
|
+
const files = findFiles(['src', 'lib'], '.ts');
|
|
207
|
+
|
|
208
|
+
files.forEach((file) => {
|
|
209
|
+
const content = readFile(file);
|
|
210
|
+
|
|
211
|
+
// Look for potential N+1 pattern
|
|
212
|
+
if (content.includes('findMany') && content.includes('forEach')) {
|
|
213
|
+
// If iterating over results and querying inside loop
|
|
214
|
+
const lines = content.split('\n');
|
|
215
|
+
let inFindMany = false;
|
|
216
|
+
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
if (line.includes('findMany')) inFindMany = true;
|
|
219
|
+
if (inFindMany && line.includes('forEach')) {
|
|
220
|
+
// Check if prisma call inside forEach
|
|
221
|
+
expect(line).not.toMatch(/prisma\.\w+\.find/);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should use indexes for filtered fields', () => {
|
|
229
|
+
const schema = readFile('prisma/schema.prisma');
|
|
230
|
+
|
|
231
|
+
// Check for common filter fields without indexes
|
|
232
|
+
const models = schema.match(/model \w+ {([^}]+)}/g);
|
|
233
|
+
|
|
234
|
+
models?.forEach((model) => {
|
|
235
|
+
const fields = model.match(/(\w+)\s+\w+/g);
|
|
236
|
+
|
|
237
|
+
// Common filter fields that should be indexed
|
|
238
|
+
const filterFields = ['email', 'username', 'slug', 'status', 'published'];
|
|
239
|
+
|
|
240
|
+
fields?.forEach((field) => {
|
|
241
|
+
const [name] = field.split(' ');
|
|
242
|
+
if (filterFields.includes(name)) {
|
|
243
|
+
expect(model).toMatch(/@@index/);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Findings Template
|
|
252
|
+
|
|
253
|
+
```markdown
|
|
254
|
+
### Prisma Analysis
|
|
255
|
+
|
|
256
|
+
**Good:**
|
|
257
|
+
- ✓ Using select for API responses
|
|
258
|
+
- ✓ Transactions for multi-step operations
|
|
259
|
+
- ✓ Proper connection pooling configured
|
|
260
|
+
|
|
261
|
+
**Issues Found:**
|
|
262
|
+
- ✗ N+1 query in dashboard data loader
|
|
263
|
+
- lib/dashboard.ts - Loading user posts in loop
|
|
264
|
+
- Fix: Use include or separate query with where
|
|
265
|
+
|
|
266
|
+
- ✗ Missing index on User.email
|
|
267
|
+
- Frequently filtered but not indexed
|
|
268
|
+
- Fix: Add @@index([email]) to User model
|
|
269
|
+
|
|
270
|
+
- ✗ Missing index on Post.slug
|
|
271
|
+
- Used for routing but not indexed
|
|
272
|
+
- Fix: Add @@unique([slug]) or @@index([slug])
|
|
273
|
+
|
|
274
|
+
- ✗ Not using select for list views
|
|
275
|
+
- API returns full objects when only partial needed
|
|
276
|
+
- Fix: Add select to prisma.user.findMany()
|
|
277
|
+
|
|
278
|
+
**Recommendations:**
|
|
279
|
+
1. Add indexes to frequently queried fields
|
|
280
|
+
2. Use include instead of separate queries for relations
|
|
281
|
+
3. Implement select for all public API endpoints
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Zustand Store Analysis
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Check for Zustand patterns
|
|
290
|
+
describe('Zustand Efficiency', () => {
|
|
291
|
+
it('should use selectors for component subscriptions', () => {
|
|
292
|
+
const components = findFiles('components', '.tsx');
|
|
293
|
+
|
|
294
|
+
components.forEach((comp) => {
|
|
295
|
+
const content = readFile(comp);
|
|
296
|
+
|
|
297
|
+
// Should not subscribe to entire store
|
|
298
|
+
if (content.includes('useStore')) {
|
|
299
|
+
// Check if destructuring specific fields
|
|
300
|
+
expect(content).toMatch(/useStore\(state\s*=>\s*state\.\w+/);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should avoid unnecessary re-renders', () => {
|
|
306
|
+
const stores = findFiles('stores', '.ts');
|
|
307
|
+
|
|
308
|
+
stores.forEach((store) => {
|
|
309
|
+
const content = readFile(store);
|
|
310
|
+
|
|
311
|
+
// Should have shallow comparison for objects
|
|
312
|
+
if (content.includes('subscribeWithSelector')) {
|
|
313
|
+
expect(content).toMatch(/shallow/);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should split stores by domain', () => {
|
|
319
|
+
const stores = findFiles('stores', '.ts');
|
|
320
|
+
|
|
321
|
+
// Check if store file is too large (>500 lines)
|
|
322
|
+
stores.forEach((store) => {
|
|
323
|
+
const lines = readFile(store).split('\n');
|
|
324
|
+
expect(lines.length).toBeLessThan(500);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## React Compiler Analysis
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// Check for React Compiler readiness
|
|
336
|
+
describe('React Compiler Readiness', () => {
|
|
337
|
+
it('should remove unnecessary useMemo', () => {
|
|
338
|
+
const components = findFiles('components', '.tsx');
|
|
339
|
+
|
|
340
|
+
components.forEach((comp) => {
|
|
341
|
+
const content = readFile(comp);
|
|
342
|
+
|
|
343
|
+
// Simple useMemo can be removed by compiler
|
|
344
|
+
const simpleMemo = content.match(
|
|
345
|
+
/useMemo\(\(\)\s*=>\s*([^,]+),\s*\[[^\]]*\]\)/g
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
simpleMemo?.forEach((memo) => {
|
|
349
|
+
const value = memo.match(/=>\s*(.+),/)?.[1];
|
|
350
|
+
// If value is a simple primitive or object literal
|
|
351
|
+
if (value && !value.includes('()') && !value.includes('function')) {
|
|
352
|
+
// Mark for potential removal
|
|
353
|
+
expect(comp).toBeDefined();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should use valid dependency arrays', () => {
|
|
360
|
+
const components = findFiles('components', '.tsx');
|
|
361
|
+
|
|
362
|
+
components.forEach((comp) => {
|
|
363
|
+
const content = readFile(comp);
|
|
364
|
+
|
|
365
|
+
// Check useEffect and useMemo
|
|
366
|
+
const hooks = content.match(/use(?:Effect|Memo|Callback)\([^)]+\)/g);
|
|
367
|
+
|
|
368
|
+
hooks?.forEach((hook) => {
|
|
369
|
+
const deps = hook.match(/\[([^\]]*)\]/)?.[1];
|
|
370
|
+
|
|
371
|
+
if (deps) {
|
|
372
|
+
// Should not have empty deps when using values
|
|
373
|
+
const usedValues = hook.match(/[\w.]+/g);
|
|
374
|
+
const depArray = deps.split(',').map((d) => d.trim());
|
|
375
|
+
|
|
376
|
+
// Basic check - if values used, deps shouldn't be empty
|
|
377
|
+
if (usedValues && usedValues.length > 1 && depArray.length === 0) {
|
|
378
|
+
expect(deps).toBeTruthy();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should avoid expensive renders', () => {
|
|
386
|
+
const components = findFiles('components', '.tsx');
|
|
387
|
+
|
|
388
|
+
components.forEach((comp) => {
|
|
389
|
+
const content = readFile(comp);
|
|
390
|
+
|
|
391
|
+
// Large inline objects should be memoized or moved outside
|
|
392
|
+
const largeObjects = content.match(
|
|
393
|
+
/{{[\s\S]{500,}}}/g
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(largeObjects).toBeNull();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Bundle Size Analysis
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// Check for bundle optimization
|
|
408
|
+
describe('Bundle Size', () => {
|
|
409
|
+
it('should use tree-shakeable imports', () => {
|
|
410
|
+
const files = findFiles(['src', 'lib'], '.ts');
|
|
411
|
+
|
|
412
|
+
files.forEach((file) => {
|
|
413
|
+
const content = readFile(file);
|
|
414
|
+
|
|
415
|
+
// Should use named imports instead of namespace imports
|
|
416
|
+
expect(content).not.toMatch(/\* as \w+ from/);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('should avoid duplicate dependencies', () => {
|
|
421
|
+
const pkg = readFile('package.json');
|
|
422
|
+
const { dependencies, devDependencies } = JSON.parse(pkg);
|
|
423
|
+
|
|
424
|
+
const allDeps = { ...dependencies, ...devDependencies };
|
|
425
|
+
|
|
426
|
+
// Check for duplicate packages with different versions
|
|
427
|
+
const dupes = Object.entries(allDeps).filter(([name]) => {
|
|
428
|
+
return name.startsWith('@types/') && name.substring(6) in allDeps;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(dupes).toHaveLength(0);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should use dynamic imports for large libraries', () => {
|
|
435
|
+
const components = findFiles('components', '.tsx');
|
|
436
|
+
|
|
437
|
+
components.forEach((comp) => {
|
|
438
|
+
const content = readFile(comp);
|
|
439
|
+
|
|
440
|
+
// Large libraries should use dynamic import
|
|
441
|
+
const largeLibs = ['monaco-editor', 'codemirror', 'pdfjs-dist'];
|
|
442
|
+
|
|
443
|
+
largeLibs.forEach((lib) => {
|
|
444
|
+
if (content.includes(lib)) {
|
|
445
|
+
expect(content).toMatch(/dynamic\(|React\.lazy\(/);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Complete Analysis Template
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// tests/deps/efficiency-analysis.test.ts
|
|
459
|
+
import { describe, it, expect } from 'vitest';
|
|
460
|
+
import fs from 'fs';
|
|
461
|
+
import path from 'path';
|
|
462
|
+
|
|
463
|
+
function findFiles(dir: string, ext: string): string[] {
|
|
464
|
+
const files: string[] = [];
|
|
465
|
+
|
|
466
|
+
function traverse(currentDir: string) {
|
|
467
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
468
|
+
|
|
469
|
+
for (const entry of entries) {
|
|
470
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
471
|
+
|
|
472
|
+
if (entry.isDirectory()) {
|
|
473
|
+
if (['node_modules', '.next', 'dist'].includes(entry.name)) continue;
|
|
474
|
+
traverse(fullPath);
|
|
475
|
+
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
476
|
+
files.push(fullPath);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
traverse(dir);
|
|
482
|
+
return files;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function readFile(filePath: string): string {
|
|
486
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
describe('Dependency Efficiency Analysis', () => {
|
|
490
|
+
describe('TanStack Router', () => {
|
|
491
|
+
it('should have typed routes', () => {
|
|
492
|
+
const routes = findFiles('src/routes', '.tsx');
|
|
493
|
+
|
|
494
|
+
routes.forEach((route) => {
|
|
495
|
+
const content = readFile(route);
|
|
496
|
+
// Checks for proper typing patterns
|
|
497
|
+
if (content.includes('$')) {
|
|
498
|
+
expect(content).toMatch(/UseParams|useParams/);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe('React Query', () => {
|
|
505
|
+
it('should use array cache keys', () => {
|
|
506
|
+
const hooks = findFiles('src/hooks', '.ts');
|
|
507
|
+
|
|
508
|
+
hooks.forEach((hook) => {
|
|
509
|
+
const content = readFile(hook);
|
|
510
|
+
|
|
511
|
+
if (content.includes('useQuery')) {
|
|
512
|
+
expect(content).toMatch(/\['/, 'Should use array-based cache keys');
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe('Prisma', () => {
|
|
519
|
+
it('should avoid N+1 queries', () => {
|
|
520
|
+
const files = findFiles('src', '.ts');
|
|
521
|
+
|
|
522
|
+
let hasN1Warning = false;
|
|
523
|
+
|
|
524
|
+
files.forEach((file) => {
|
|
525
|
+
const content = readFile(file);
|
|
526
|
+
|
|
527
|
+
// Pattern: findMany followed by forEach with query inside
|
|
528
|
+
if (content.includes('findMany') && content.includes('forEach')) {
|
|
529
|
+
const lines = content.split('\n');
|
|
530
|
+
for (let i = 0; i < lines.length; i++) {
|
|
531
|
+
if (lines[i].includes('findMany')) {
|
|
532
|
+
// Check next 10 lines for forEach with prisma query
|
|
533
|
+
for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
|
|
534
|
+
if (lines[j].includes('forEach') && lines[j].includes('prisma.')) {
|
|
535
|
+
hasN1Warning = true;
|
|
536
|
+
console.warn(`Potential N+1 in ${file}:${i + 1}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## Running Analysis
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
# Run dependency analysis
|
|
554
|
+
docker exec demon-tools npm test -- tests/deps/efficiency-analysis.test.ts
|
|
555
|
+
|
|
556
|
+
# Generate bundle analysis
|
|
557
|
+
docker exec demon-tools npm run build -- --analyze
|
|
558
|
+
```
|