@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Demon - Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.0-alpha] - 2025-02-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Initial release
|
|
7
|
+
- Auto-detection for frameworks (Next.js, Remix, SvelteKit, Vite, etc.)
|
|
8
|
+
- Database detection (Prisma, Drizzle, Neon, Supabase, local Postgres)
|
|
9
|
+
- Unit test generation (components, hooks, utils)
|
|
10
|
+
- Integration test templates (API routes, DB with transaction rollback)
|
|
11
|
+
- E2E test templates (Playwright)
|
|
12
|
+
- Performance test templates (k6)
|
|
13
|
+
- Dependency efficiency analysis (TanStack Router, React Query, Prisma, Zustand, React Compiler)
|
|
14
|
+
- Docker container with all testing tools
|
|
15
|
+
- Cross-platform support (Linux, macOS, Windows)
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
- **Unit Tests**: Component, hook, and utility testing with Vitest
|
|
19
|
+
- **Integration Tests**: API route and database testing with Prisma
|
|
20
|
+
- **E2E Tests**: User flow testing with Playwright
|
|
21
|
+
- **Performance Tests**: API load testing with k6
|
|
22
|
+
- **Analysis**: Dependency pattern analysis and recommendations
|
|
23
|
+
- **Remediation**: Automatic test failure categorization and fix suggestions
|
|
24
|
+
|
|
25
|
+
### Tools Included
|
|
26
|
+
- Vitest
|
|
27
|
+
- @testing-library/react
|
|
28
|
+
- Playwright
|
|
29
|
+
- k6
|
|
30
|
+
- supertest
|
|
31
|
+
- MSW
|
|
32
|
+
- @prisma/cli
|
|
33
|
+
|
|
34
|
+
### Documentation
|
|
35
|
+
- Comprehensive prompt system for AI agent
|
|
36
|
+
- Test templates for all test types
|
|
37
|
+
- Fix engine for common test failures
|
|
38
|
+
- Performance analysis and reporting
|
package/LICENSE
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Demon - License
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2025 Yanis
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Demon
|
|
2
|
+
|
|
3
|
+
AI-powered automated test generation and remediation for web applications.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# From your project directory
|
|
9
|
+
npx @oalacea/demon
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
First run installs the testing toolkit (~500 MB Docker image, takes 2-3 minutes).
|
|
13
|
+
|
|
14
|
+
## What You Need
|
|
15
|
+
|
|
16
|
+
- **Docker** - [Install](https://docs.docker.com/get-docker/)
|
|
17
|
+
- **AI coding agent** - Claude Code, Cursor, Windsurf, Aider, Codex...
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
| Category | Features |
|
|
22
|
+
|----------|----------|
|
|
23
|
+
| **Unit Tests** | Components, hooks, utils, validators, stores |
|
|
24
|
+
| **Integration Tests** | API routes, database operations (with transaction rollback) |
|
|
25
|
+
| **E2E Tests** | User flows, form interactions, navigation (Playwright) |
|
|
26
|
+
| **Performance Tests** | API load testing (k6), DB query benchmarks |
|
|
27
|
+
| **Dependency Analysis** | TanStack Router, React Query, Prisma, Zustand, React Compiler |
|
|
28
|
+
|
|
29
|
+
## How It Works
|
|
30
|
+
|
|
31
|
+
1. **Analyze** - Auto-detects your framework, database, and existing tests
|
|
32
|
+
2. **Generate** - Creates tests based on your code patterns
|
|
33
|
+
3. **Execute** - Runs tests inside Docker container
|
|
34
|
+
4. **Fix** - Analyzes failures and applies corrections
|
|
35
|
+
5. **Report** - Provides comprehensive coverage and performance report
|
|
36
|
+
|
|
37
|
+
## Included Tools
|
|
38
|
+
|
|
39
|
+
The Docker toolkit includes:
|
|
40
|
+
|
|
41
|
+
| Category | Tools |
|
|
42
|
+
|----------|-------|
|
|
43
|
+
| Testing | Vitest, @testing-library/react, happy-dom |
|
|
44
|
+
| E2E | Playwright (Chromium) |
|
|
45
|
+
| Performance | k6 |
|
|
46
|
+
| Utilities | supertest, MSW, @prisma/cli |
|
|
47
|
+
|
|
48
|
+
## Output Example
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
✓ Unit Tests: 45 created, 42 passing, 3 fixed
|
|
52
|
+
✓ Integration: 12 created, 12 passing
|
|
53
|
+
✓ E2E: 8 created, 7 passing, 1 requires manual review
|
|
54
|
+
✓ Performance: API p95 = 145ms (PASS)
|
|
55
|
+
✓ Dependencies: 3 improvements suggested
|
|
56
|
+
|
|
57
|
+
## Summary
|
|
58
|
+
Total Tests: 245
|
|
59
|
+
Passing: 238
|
|
60
|
+
Failing: 2 (requires manual review)
|
|
61
|
+
Coverage: 84%
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Safety
|
|
65
|
+
|
|
66
|
+
- Always use transaction rollback for database tests
|
|
67
|
+
- Never modify production data
|
|
68
|
+
- Git integration for safe rollbacks
|
|
69
|
+
- Non-destructive testing modes available
|
|
70
|
+
|
|
71
|
+
## Troubleshooting
|
|
72
|
+
|
|
73
|
+
### Rebuild toolkit image
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
docker rm -f demon-tools
|
|
77
|
+
docker rmi demon-tools
|
|
78
|
+
npx @oalacea/demon
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Run specific test
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
docker exec demon-tools npm test -- Button.test.ts
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Debug test
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
docker exec demon-tools npm test -- Button.test.ts --reporter=verbose
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Related
|
|
94
|
+
|
|
95
|
+
- **Guardian** - Security testing package: `npx @oalacea/guardian`
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT - Use at your own risk.
|
|
100
|
+
|
|
101
|
+
## Credits
|
|
102
|
+
|
|
103
|
+
Inspired by the need for comprehensive automated testing in modern web development.
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demon - Dependency Efficiency Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes codebase for dependency usage patterns and inefficiencies:
|
|
5
|
+
* - TanStack Router patterns
|
|
6
|
+
* - React Query usage
|
|
7
|
+
* - Prisma query patterns
|
|
8
|
+
* - Zustand store patterns
|
|
9
|
+
* - React Compiler readiness
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Find files matching a pattern
|
|
17
|
+
*/
|
|
18
|
+
function findFiles(dir, pattern, excludeDirs = ['node_modules', '.next', 'dist', 'build']) {
|
|
19
|
+
const files = [];
|
|
20
|
+
|
|
21
|
+
function traverse(currentDir) {
|
|
22
|
+
if (!fs.existsSync(currentDir)) return;
|
|
23
|
+
|
|
24
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
25
|
+
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (entry.isDirectory()) {
|
|
28
|
+
if (excludeDirs.includes(entry.name)) continue;
|
|
29
|
+
traverse(path.join(currentDir, entry.name));
|
|
30
|
+
} else if (entry.isFile() && entry.name.match(pattern)) {
|
|
31
|
+
files.push(path.join(currentDir, entry.name));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
traverse(dir);
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read file content
|
|
42
|
+
*/
|
|
43
|
+
function readFile(filePath) {
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
46
|
+
} catch {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Analyze TanStack Router usage
|
|
53
|
+
*/
|
|
54
|
+
function analyzeTanStackRouter(projectDir) {
|
|
55
|
+
const findings = {
|
|
56
|
+
good: [],
|
|
57
|
+
issues: [],
|
|
58
|
+
recommendations: [],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const routeFiles = findFiles(path.join(projectDir, 'src'), /routes/);
|
|
62
|
+
|
|
63
|
+
for (const file of routeFiles) {
|
|
64
|
+
const content = readFile(file);
|
|
65
|
+
|
|
66
|
+
// Check for typed routes
|
|
67
|
+
if (content.includes('useParams') || content.includes('$')) {
|
|
68
|
+
findings.good.push(`Typed params in ${path.relative(projectDir, file)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for loaders
|
|
72
|
+
if (content.includes('loader:') || content.includes('loaderBefore')) {
|
|
73
|
+
findings.good.push(`Data loader in ${path.relative(projectDir, file)}`);
|
|
74
|
+
} else if (content.includes('useQuery') || content.includes('useFetch')) {
|
|
75
|
+
findings.issues.push(`Missing loader in ${path.relative(projectDir, file)} - data fetching without loader`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check for error boundaries
|
|
79
|
+
if (content.includes('loader:') && !content.includes('errorComponent') && !content.includes('ErrorBoundary')) {
|
|
80
|
+
findings.issues.push(`Missing error boundary in ${path.relative(projectDir, file)}`);
|
|
81
|
+
findings.recommendations.push(`Add errorComponent to route in ${path.relative(projectDir, file)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check navigation for prefetching
|
|
86
|
+
const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
|
|
87
|
+
for (const file of componentFiles) {
|
|
88
|
+
const content = readFile(file);
|
|
89
|
+
if (content.includes('<Link') && !content.includes('prefetch=') && !content.includes('prefetchIntent')) {
|
|
90
|
+
findings.issues.push(`Link prefetching not enabled in ${path.relative(projectDir, file)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return findings;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Analyze React Query usage
|
|
99
|
+
*/
|
|
100
|
+
function analyzeReactQuery(projectDir) {
|
|
101
|
+
const findings = {
|
|
102
|
+
good: [],
|
|
103
|
+
issues: [],
|
|
104
|
+
recommendations: [],
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const hookFiles = findFiles(path.join(projectDir, 'src'), /hooks/);
|
|
108
|
+
const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
|
|
109
|
+
const allFiles = [...hookFiles, ...componentFiles];
|
|
110
|
+
|
|
111
|
+
for (const file of allFiles) {
|
|
112
|
+
const content = readFile(file);
|
|
113
|
+
|
|
114
|
+
if (!content.includes('useQuery') && !content.includes('useMutation')) continue;
|
|
115
|
+
|
|
116
|
+
// Check for array cache keys
|
|
117
|
+
if (content.includes('useQuery(') || content.includes('useInfiniteQuery(')) {
|
|
118
|
+
const hasArrayKey = /\['/.test(content) || /queryKey:\s*\[/.test(content);
|
|
119
|
+
if (hasArrayKey) {
|
|
120
|
+
findings.good.push(`Array-based cache keys in ${path.relative(projectDir, file)}`);
|
|
121
|
+
} else {
|
|
122
|
+
findings.issues.push(`Non-array cache keys in ${path.relative(projectDir, file)}`);
|
|
123
|
+
findings.recommendations.push(`Use array-based cache keys in ${path.relative(projectDir, file)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check for staleTime
|
|
128
|
+
if (content.includes('useQuery(') && !content.includes('staleTime')) {
|
|
129
|
+
findings.issues.push(`Missing staleTime in ${path.relative(projectDir, file)}`);
|
|
130
|
+
findings.recommendations.push(`Add staleTime to queries in ${path.relative(projectDir, file)}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check for mutation invalidation
|
|
134
|
+
if (content.includes('useMutation(')) {
|
|
135
|
+
if (content.includes('invalidateQueries')) {
|
|
136
|
+
findings.good.push(`Proper invalidation in ${path.relative(projectDir, file)}`);
|
|
137
|
+
} else {
|
|
138
|
+
findings.issues.push(`Missing invalidation in mutation at ${path.relative(projectDir, file)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return findings;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Analyze Prisma usage
|
|
148
|
+
*/
|
|
149
|
+
function analyzePrisma(projectDir) {
|
|
150
|
+
const findings = {
|
|
151
|
+
good: [],
|
|
152
|
+
issues: [],
|
|
153
|
+
recommendations: [],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const libFiles = findFiles(path.join(projectDir, 'src'), /.*\.(ts|js)$/);
|
|
157
|
+
|
|
158
|
+
for (const file of libFiles) {
|
|
159
|
+
const content = readFile(file);
|
|
160
|
+
|
|
161
|
+
if (!content.includes('prisma.')) continue;
|
|
162
|
+
|
|
163
|
+
// Check for select usage
|
|
164
|
+
if (content.includes('prisma.') && content.includes('findMany')) {
|
|
165
|
+
if (content.includes('select:')) {
|
|
166
|
+
findings.good.push(`Using select in ${path.relative(projectDir, file)}`);
|
|
167
|
+
} else if (content.includes('.findMany().then')) {
|
|
168
|
+
findings.issues.push(`Not using select in ${path.relative(projectDir, file)} - returning full objects`);
|
|
169
|
+
findings.recommendations.push(`Add select to prisma queries in ${path.relative(projectDir, file)}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check for potential N+1
|
|
174
|
+
const lines = content.split('\n');
|
|
175
|
+
for (let i = 0; i < lines.length; i++) {
|
|
176
|
+
if (lines[i].includes('findMany') || lines[i].includes('findFirst')) {
|
|
177
|
+
// Check next 10 lines for forEach with prisma query
|
|
178
|
+
for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
|
|
179
|
+
if (lines[j].includes('forEach') && lines[j].includes('prisma.')) {
|
|
180
|
+
findings.issues.push(`Potential N+1 query in ${path.relative(projectDir, file)}:${i + 1}`);
|
|
181
|
+
findings.recommendations.push(`Use include or separate query with WHERE in ${path.relative(projectDir, file)}`);
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check schema for indexes
|
|
190
|
+
const schemaPath = path.join(projectDir, 'prisma', 'schema.prisma');
|
|
191
|
+
if (fs.existsSync(schemaPath)) {
|
|
192
|
+
const schema = readFile(schemaPath);
|
|
193
|
+
const models = schema.matchAll(/model\s+(\w+)\s*{([^}]+)}/g);
|
|
194
|
+
|
|
195
|
+
for (const modelMatch of models) {
|
|
196
|
+
const modelName = modelMatch[1];
|
|
197
|
+
const body = modelMatch[2];
|
|
198
|
+
|
|
199
|
+
if (body.includes('email') && !body.includes('@@index') && !body.includes('@@unique')) {
|
|
200
|
+
findings.recommendations.push(`Add index on ${modelName}.email for faster lookups`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return findings;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Analyze Zustand usage
|
|
210
|
+
*/
|
|
211
|
+
function analyzeZustand(projectDir) {
|
|
212
|
+
const findings = {
|
|
213
|
+
good: [],
|
|
214
|
+
issues: [],
|
|
215
|
+
recommendations: [],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const storeFiles = findFiles(path.join(projectDir, 'src'), /store|stores/);
|
|
219
|
+
const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
|
|
220
|
+
|
|
221
|
+
for (const file of storeFiles) {
|
|
222
|
+
const content = readFile(file);
|
|
223
|
+
|
|
224
|
+
// Check store size
|
|
225
|
+
const lines = content.split('\n').length;
|
|
226
|
+
if (lines > 500) {
|
|
227
|
+
findings.issues.push(`Large store file ${path.relative(projectDir, file)} (${lines} lines)`);
|
|
228
|
+
findings.recommendations.push(`Consider splitting ${path.basename(file)} into multiple stores`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const file of componentFiles) {
|
|
233
|
+
const content = readFile(file);
|
|
234
|
+
|
|
235
|
+
if (!content.includes('useStore')) continue;
|
|
236
|
+
|
|
237
|
+
// Check for full-store subscriptions
|
|
238
|
+
if (content.includes('useStore()') || content.match(/useStore\(\s*state\s*=>\s*state/)) {
|
|
239
|
+
findings.issues.push(`Full-store subscription in ${path.relative(projectDir, file)}`);
|
|
240
|
+
findings.recommendations.push(`Use selectors for specific fields in ${path.relative(projectDir, file)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return findings;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Analyze React Compiler readiness
|
|
249
|
+
*/
|
|
250
|
+
function analyzeReactCompilerReadiness(projectDir) {
|
|
251
|
+
const findings = {
|
|
252
|
+
good: [],
|
|
253
|
+
issues: [],
|
|
254
|
+
recommendations: [],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const componentFiles = findFiles(path.join(projectDir, 'src'), /.*\.(tsx|jsx)$/);
|
|
258
|
+
|
|
259
|
+
for (const file of componentFiles) {
|
|
260
|
+
const content = readFile(file);
|
|
261
|
+
|
|
262
|
+
// Check for simple useMemo that can be removed
|
|
263
|
+
const simpleMemo = content.match(/useMemo\(\(\)\s*=>\s*([^,]+),\s*\[[^\]]*\]\)/g);
|
|
264
|
+
if (simpleMemo) {
|
|
265
|
+
for (const memo of simpleMemo) {
|
|
266
|
+
const value = memo.match(/=>\s*(.+),/)?.[1];
|
|
267
|
+
if (value && !value.includes('()') && !value.includes('function')) {
|
|
268
|
+
findings.recommendations.push(`Remove simple useMemo in ${path.basename(file)} - React Compiler will handle this`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check for useCallback dependencies
|
|
274
|
+
const useCallbacks = content.matchAll(/useCallback\([^)]+\)/g);
|
|
275
|
+
for (const callback of useCallbacks) {
|
|
276
|
+
const deps = callback[0].match(/\[([^\]]*)\]/)?.[1];
|
|
277
|
+
if (deps && deps.trim() === '') {
|
|
278
|
+
findings.issues.push(`useCallback with empty deps in ${path.basename(file)}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check for large inline objects
|
|
283
|
+
const largeObjects = content.match(/{{[\s\S]{200,}}}/g);
|
|
284
|
+
if (largeObjects) {
|
|
285
|
+
findings.issues.push(`Large inline object in ${path.basename(file)} - move outside component`);
|
|
286
|
+
findings.recommendations.push(`Extract large objects to constants in ${path.basename(file)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return findings;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Analyze bundle optimization
|
|
295
|
+
*/
|
|
296
|
+
function analyzeBundleOptimization(projectDir) {
|
|
297
|
+
const findings = {
|
|
298
|
+
good: [],
|
|
299
|
+
issues: [],
|
|
300
|
+
recommendations: [],
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const files = findFiles(path.join(projectDir, 'src'), /.*\.(ts|tsx|js|jsx)$/);
|
|
304
|
+
|
|
305
|
+
for (const file of files) {
|
|
306
|
+
const content = readFile(file);
|
|
307
|
+
|
|
308
|
+
// Check for namespace imports
|
|
309
|
+
if (content.includes('* as ')) {
|
|
310
|
+
findings.issues.push(`Namespace import in ${path.basename(file)}`);
|
|
311
|
+
findings.recommendations.push(`Use named imports for better tree-shaking in ${path.basename(file)}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check for large library imports
|
|
315
|
+
const largeLibs = ['monaco-editor', 'codemirror', 'pdfjs-dist', 'fabric'];
|
|
316
|
+
for (const lib of largeLibs) {
|
|
317
|
+
if (content.includes(`from '${lib}'`) || content.includes(`from "${lib}"`)) {
|
|
318
|
+
if (!content.includes('dynamic(') && !content.includes('React.lazy')) {
|
|
319
|
+
findings.recommendations.push(`Use dynamic import for ${lib} in ${path.basename(file)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check package.json for duplicate dependencies
|
|
326
|
+
const pkgPath = path.join(projectDir, 'package.json');
|
|
327
|
+
if (fs.existsSync(pkgPath)) {
|
|
328
|
+
const pkg = JSON.parse(readFile(pkgPath));
|
|
329
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
330
|
+
|
|
331
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
332
|
+
if (name.startsWith('@types/')) {
|
|
333
|
+
const mainPkg = name.substring(6);
|
|
334
|
+
if (mainPkg in deps) {
|
|
335
|
+
findings.good.push(`Types package for ${mainPkg} found`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return findings;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Run full analysis
|
|
346
|
+
*/
|
|
347
|
+
function analyze(projectDir) {
|
|
348
|
+
return {
|
|
349
|
+
tanStackRouter: analyzeTanStackRouter(projectDir),
|
|
350
|
+
reactQuery: analyzeReactQuery(projectDir),
|
|
351
|
+
prisma: analyzePrisma(projectDir),
|
|
352
|
+
zustand: analyzeZustand(projectDir),
|
|
353
|
+
reactCompiler: analyzeReactCompilerReadiness(projectDir),
|
|
354
|
+
bundleOptimization: analyzeBundleOptimization(projectDir),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = {
|
|
359
|
+
analyze,
|
|
360
|
+
analyzeTanStackRouter,
|
|
361
|
+
analyzeReactQuery,
|
|
362
|
+
analyzePrisma,
|
|
363
|
+
analyzeZustand,
|
|
364
|
+
analyzeReactCompilerReadiness,
|
|
365
|
+
analyzeBundleOptimization,
|
|
366
|
+
};
|