@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,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demon - Fix Engine
|
|
3
|
+
*
|
|
4
|
+
* Analyzes test failures and applies fixes.
|
|
5
|
+
* Handles common test issues automatically.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Analyze test failure and categorize
|
|
13
|
+
*/
|
|
14
|
+
function categorizeFailure(errorOutput, testPath) {
|
|
15
|
+
if (!errorOutput) return { category: 'unknown' };
|
|
16
|
+
|
|
17
|
+
if (errorOutput.includes('Cannot find module')) {
|
|
18
|
+
return { category: 'import', message: 'Module import issue' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (errorOutput.includes('is not defined') || errorOutput.includes('is not a function')) {
|
|
22
|
+
return { category: 'mock', message: 'Missing mock' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (errorOutput.includes('timeout')) {
|
|
26
|
+
return { category: 'timeout', message: 'Test timeout' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (errorOutput.includes('Expected') && errorOutput.includes('Received')) {
|
|
30
|
+
return { category: 'assertion', message: 'Assertion failed' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (errorOutput.includes('Cannot read property')) {
|
|
34
|
+
return { category: 'null', message: 'Null/undefined access' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (errorOutput.includes('ECONNREFUSED') || errorOutput.includes('connect')) {
|
|
38
|
+
return { category: 'connection', message: 'Connection refused' };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { category: 'unknown', message: 'Unknown error' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fix import issues
|
|
46
|
+
*/
|
|
47
|
+
function fixImportIssue(testPath, error) {
|
|
48
|
+
const content = fs.readFileSync(testPath, 'utf-8');
|
|
49
|
+
const missingModule = error.match(/Cannot find module ['"](.+)['"]/)?.[1];
|
|
50
|
+
|
|
51
|
+
if (!missingModule) return false;
|
|
52
|
+
|
|
53
|
+
// Check if it's a relative import issue
|
|
54
|
+
if (missingModule.startsWith('@/')) {
|
|
55
|
+
// Check if vitest.config.ts has the alias
|
|
56
|
+
const vitestConfig = path.join(process.cwd(), 'vitest.config.ts');
|
|
57
|
+
if (fs.existsSync(vitestConfig)) {
|
|
58
|
+
const config = fs.readFileSync(vitestConfig, 'utf-8');
|
|
59
|
+
if (!config.includes('resolve:') || !config.includes('alias:')) {
|
|
60
|
+
return {
|
|
61
|
+
fixType: 'config',
|
|
62
|
+
message: 'Add path alias to vitest.config.ts',
|
|
63
|
+
configFix: `
|
|
64
|
+
export default defineConfig({
|
|
65
|
+
resolve: {
|
|
66
|
+
alias: {
|
|
67
|
+
'@': path.resolve(__dirname, './src'),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
fixType: 'import',
|
|
78
|
+
message: `Create or update import for ${missingModule}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fix mock issues
|
|
84
|
+
*/
|
|
85
|
+
function fixMockIssue(testPath, error) {
|
|
86
|
+
const content = fs.readFileSync(testPath, 'utf-8');
|
|
87
|
+
const missingVar = error.match(/(.+) is not defined/)?.[1];
|
|
88
|
+
|
|
89
|
+
if (!missingVar) return false;
|
|
90
|
+
|
|
91
|
+
// Check if it's a hook/function that needs mocking
|
|
92
|
+
if (missingVar.startsWith('use')) {
|
|
93
|
+
return {
|
|
94
|
+
fixType: 'mock',
|
|
95
|
+
message: `Add mock for ${missingVar}`,
|
|
96
|
+
mock: `vi.mock('@/hooks/${missingVar}', () => ({
|
|
97
|
+
${missingVar}: vi.fn(() => ({/* default return */})),
|
|
98
|
+
}));`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
fixType: 'mock',
|
|
104
|
+
message: `Add mock for ${missingVar}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Fix assertion issues
|
|
110
|
+
*/
|
|
111
|
+
function fixAssertionIssue(testPath, error) {
|
|
112
|
+
const content = fs.readFileSync(testPath, 'utf-8');
|
|
113
|
+
|
|
114
|
+
// Check for common issues
|
|
115
|
+
if (error.includes('querySelector returned null')) {
|
|
116
|
+
return {
|
|
117
|
+
fixType: 'selector',
|
|
118
|
+
message: 'Element not found - use queryBy or add waitFor',
|
|
119
|
+
suggestion: 'Replace getBy with queryBy for optional elements, or add waitFor',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (error.includes('received null') || error.includes('received undefined')) {
|
|
124
|
+
return {
|
|
125
|
+
fixType: 'assertion',
|
|
126
|
+
message: 'Element/property is null/undefined',
|
|
127
|
+
suggestion: 'Check if component renders with required props',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
fixType: 'manual',
|
|
133
|
+
message: 'Manual fix required',
|
|
134
|
+
error,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fix timeout issues
|
|
140
|
+
*/
|
|
141
|
+
function fixTimeoutIssue(testPath, error) {
|
|
142
|
+
return {
|
|
143
|
+
fixType: 'timeout',
|
|
144
|
+
message: 'Increase test timeout or fix async operation',
|
|
145
|
+
suggestion: `Add timeout to test: it('test name', async () => { ... }, { timeout: 10000 })`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Fix connection issues
|
|
151
|
+
*/
|
|
152
|
+
function fixConnectionIssue(testPath, error) {
|
|
153
|
+
return {
|
|
154
|
+
fixType: 'mock',
|
|
155
|
+
message: 'Mock external API calls',
|
|
156
|
+
suggestion: 'Use vi.mock or MSW to mock fetch/axios calls',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Generate fix for a failing test
|
|
162
|
+
*/
|
|
163
|
+
function generateFix(testPath, error) {
|
|
164
|
+
const failure = categorizeFailure(error, testPath);
|
|
165
|
+
|
|
166
|
+
switch (failure.category) {
|
|
167
|
+
case 'import':
|
|
168
|
+
return fixImportIssue(testPath, error);
|
|
169
|
+
case 'mock':
|
|
170
|
+
return fixMockIssue(testPath, error);
|
|
171
|
+
case 'timeout':
|
|
172
|
+
return fixTimeoutIssue(testPath, error);
|
|
173
|
+
case 'assertion':
|
|
174
|
+
return fixAssertionIssue(testPath, error);
|
|
175
|
+
case 'connection':
|
|
176
|
+
return fixConnectionIssue(testPath, error);
|
|
177
|
+
default:
|
|
178
|
+
return {
|
|
179
|
+
fixType: 'manual',
|
|
180
|
+
message: 'Manual analysis required',
|
|
181
|
+
error,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Apply fix to file
|
|
188
|
+
*/
|
|
189
|
+
function applyFix(testPath, fix) {
|
|
190
|
+
if (!fix) return false;
|
|
191
|
+
|
|
192
|
+
const content = fs.readFileSync(testPath, 'utf-8');
|
|
193
|
+
let newContent = content;
|
|
194
|
+
|
|
195
|
+
switch (fix.fixType) {
|
|
196
|
+
case 'mock':
|
|
197
|
+
// Add mock at top of file after imports
|
|
198
|
+
const importsEnd = content.indexOf(');', content.indexOf('import '));
|
|
199
|
+
if (importsEnd > -1) {
|
|
200
|
+
newContent = content.slice(0, importsEnd + 2) +
|
|
201
|
+
'\n\n' + fix.mock +
|
|
202
|
+
content.slice(importsEnd + 2);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case 'timeout':
|
|
207
|
+
// Add timeout option to it() call
|
|
208
|
+
newContent = content.replace(
|
|
209
|
+
/it\('([^']+)',\s*(async\s*)?\(([^)]*)\)\s*=>/g,
|
|
210
|
+
"it('$1', async ($2) => { ... }, { timeout: 10000 })"
|
|
211
|
+
);
|
|
212
|
+
break;
|
|
213
|
+
|
|
214
|
+
case 'selector':
|
|
215
|
+
// Suggest using queryBy instead of getBy
|
|
216
|
+
newContent = content.replace(
|
|
217
|
+
/getBy\(([^)]+)\)/g,
|
|
218
|
+
'queryBy($1) || getBy($1)'
|
|
219
|
+
);
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
default:
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (newContent !== content) {
|
|
227
|
+
fs.writeFileSync(testPath, newContent);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Fix all failing tests
|
|
236
|
+
*/
|
|
237
|
+
function fixFailures(testResults) {
|
|
238
|
+
const fixes = [];
|
|
239
|
+
|
|
240
|
+
for (const result of testResults) {
|
|
241
|
+
if (result.status === 'failed') {
|
|
242
|
+
const fix = generateFix(result.file, result.error);
|
|
243
|
+
fixes.push({
|
|
244
|
+
file: result.file,
|
|
245
|
+
error: result.error,
|
|
246
|
+
fix,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return fixes;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Create fix summary
|
|
256
|
+
*/
|
|
257
|
+
function createFixSummary(fixes) {
|
|
258
|
+
const summary = {
|
|
259
|
+
total: fixes.length,
|
|
260
|
+
applied: 0,
|
|
261
|
+
manual: 0,
|
|
262
|
+
failed: 0,
|
|
263
|
+
details: [],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
for (const fix of fixes) {
|
|
267
|
+
const detail = {
|
|
268
|
+
file: fix.file,
|
|
269
|
+
type: fix.fix?.fixType || 'unknown',
|
|
270
|
+
message: fix.fix?.message || 'No fix available',
|
|
271
|
+
applied: false,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
if (fix.fix?.fixType === 'manual') {
|
|
275
|
+
summary.manual++;
|
|
276
|
+
detail.applied = false;
|
|
277
|
+
} else if (fix.fix) {
|
|
278
|
+
// Try to apply fix
|
|
279
|
+
// const applied = applyFix(fix.file, fix.fix);
|
|
280
|
+
// if (applied) summary.applied++;
|
|
281
|
+
// else summary.failed++;
|
|
282
|
+
summary.manual++; // For safety, mark as manual
|
|
283
|
+
detail.applied = false;
|
|
284
|
+
} else {
|
|
285
|
+
summary.failed++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
summary.details.push(detail);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return summary;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
categorizeFailure,
|
|
296
|
+
fixImportIssue,
|
|
297
|
+
fixMockIssue,
|
|
298
|
+
fixAssertionIssue,
|
|
299
|
+
fixTimeoutIssue,
|
|
300
|
+
fixConnectionIssue,
|
|
301
|
+
generateFix,
|
|
302
|
+
applyFix,
|
|
303
|
+
fixFailures,
|
|
304
|
+
createFixSummary,
|
|
305
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demon - Performance Analyzer Agent
|
|
3
|
+
*
|
|
4
|
+
* Analyzes application performance including:
|
|
5
|
+
* - API response times
|
|
6
|
+
* - Database query efficiency
|
|
7
|
+
* - Bundle size
|
|
8
|
+
* - Web Vitals
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const CONFIG = {
|
|
16
|
+
container: 'demon-tools',
|
|
17
|
+
docker: 'docker exec',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run k6 performance test
|
|
22
|
+
*/
|
|
23
|
+
function runK6Test(testFile, options = {}) {
|
|
24
|
+
const stages = options.stages || [
|
|
25
|
+
{ duration: '30s', target: 20 },
|
|
26
|
+
{ duration: '1m', target: 20 },
|
|
27
|
+
{ duration: '30s', target: 0 },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const thresholds = options.thresholds || [
|
|
31
|
+
'http_req_duration[p(95)]<200',
|
|
32
|
+
'http_req_failed<0.01',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Create temp k6 script
|
|
36
|
+
const script = generateK6Script(options.target || 'http://host.docker.internal:3000', stages, thresholds);
|
|
37
|
+
|
|
38
|
+
return runDockerCommand(`k6 run -`, { input: script });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate k6 test script
|
|
43
|
+
*/
|
|
44
|
+
function generateK6Script(target, stages, thresholds) {
|
|
45
|
+
return `
|
|
46
|
+
import http from 'k6/http';
|
|
47
|
+
import { check, sleep } from 'k6';
|
|
48
|
+
|
|
49
|
+
export const options = {
|
|
50
|
+
stages: ${JSON.stringify(stages)},
|
|
51
|
+
thresholds: {
|
|
52
|
+
${thresholds.map(t => `'${t}': true`).join(',\n ')}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const BASE_URL = '${target}';
|
|
57
|
+
|
|
58
|
+
export default function () {
|
|
59
|
+
let res = http.get(\`\${BASE_URL}/\`);
|
|
60
|
+
check(res, {
|
|
61
|
+
'homepage OK': r => r.status === 200,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
sleep(1);
|
|
65
|
+
|
|
66
|
+
res = http.get(\`\${BASE_URL}/api/users\`);
|
|
67
|
+
check(res, {
|
|
68
|
+
'users API OK': r => r.status === 200,
|
|
69
|
+
'response time < 200ms': r => r.timings.duration < 200,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
sleep(1);
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Analyze bundle size
|
|
79
|
+
*/
|
|
80
|
+
function analyzeBundleSize(projectDir) {
|
|
81
|
+
const buildDir = path.join(projectDir, '.next', 'static', 'chunks');
|
|
82
|
+
const distDir = path.join(projectDir, 'dist', 'assets');
|
|
83
|
+
|
|
84
|
+
let chunksDir;
|
|
85
|
+
if (fs.existsSync(buildDir)) chunksDir = buildDir;
|
|
86
|
+
else if (fs.existsSync(distDir)) chunksDir = distDir;
|
|
87
|
+
else return null;
|
|
88
|
+
|
|
89
|
+
const chunks = [];
|
|
90
|
+
let totalSize = 0;
|
|
91
|
+
|
|
92
|
+
function analyzeDir(dir) {
|
|
93
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const fullPath = path.join(dir, entry.name);
|
|
97
|
+
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
analyzeDir(fullPath);
|
|
100
|
+
} else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.css'))) {
|
|
101
|
+
const stats = fs.statSync(fullPath);
|
|
102
|
+
const size = stats.size;
|
|
103
|
+
totalSize += size;
|
|
104
|
+
|
|
105
|
+
chunks.push({
|
|
106
|
+
name: entry.name,
|
|
107
|
+
size: size,
|
|
108
|
+
sizeKB: (size / 1024).toFixed(2),
|
|
109
|
+
path: path.relative(projectDir, fullPath),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
analyzeDir(chunksDir);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
totalSize,
|
|
119
|
+
totalSizeKB: (totalSize / 1024).toFixed(2),
|
|
120
|
+
totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
|
|
121
|
+
chunks: chunks.sort((a, b) => b.size - a.size).slice(0, 20), // Top 20
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Analyze database queries (Prisma)
|
|
127
|
+
*/
|
|
128
|
+
function analyzeDbQueries(projectDir) {
|
|
129
|
+
const prismaDir = path.join(projectDir, 'prisma');
|
|
130
|
+
if (!fs.existsSync(prismaDir)) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const schemaPath = path.join(prismaDir, 'schema.prisma');
|
|
135
|
+
if (!fs.existsSync(schemaPath)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
|
140
|
+
|
|
141
|
+
const models = [];
|
|
142
|
+
const modelMatches = schema.matchAll(/model\s+(\w+)\s*{([^}]+)}/g);
|
|
143
|
+
|
|
144
|
+
for (const match of modelMatches) {
|
|
145
|
+
const modelName = match[1];
|
|
146
|
+
const body = match[2];
|
|
147
|
+
|
|
148
|
+
const fields = [];
|
|
149
|
+
const fieldMatches = body.matchAll(/(\w+)\s+(\w+)(?:\s+@([^{\s]+[^}]*))?/g);
|
|
150
|
+
|
|
151
|
+
for (const fieldMatch of fieldMatches) {
|
|
152
|
+
const fieldName = fieldMatch[1];
|
|
153
|
+
const fieldType = fieldMatch[2];
|
|
154
|
+
const attributes = fieldMatch[3] || '';
|
|
155
|
+
|
|
156
|
+
fields.push({
|
|
157
|
+
name: fieldName,
|
|
158
|
+
type: fieldType,
|
|
159
|
+
attributes: attributes,
|
|
160
|
+
isIndexed: attributes.includes('index') || attributes.includes('unique') || attributes.includes('id'),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
models.push({
|
|
165
|
+
name: modelName,
|
|
166
|
+
fields: fields,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
models,
|
|
172
|
+
recommendations: generateDbRecommendations(models),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generate database recommendations
|
|
178
|
+
*/
|
|
179
|
+
function generateDbRecommendations(models) {
|
|
180
|
+
const recommendations = [];
|
|
181
|
+
|
|
182
|
+
for (const model of models) {
|
|
183
|
+
// Check for common filter fields without indexes
|
|
184
|
+
const filterFields = ['email', 'username', 'slug', 'status', 'published', 'createdAt'];
|
|
185
|
+
|
|
186
|
+
for (const field of model.fields) {
|
|
187
|
+
if (filterFields.includes(field.name) && !field.isIndexed) {
|
|
188
|
+
recommendations.push({
|
|
189
|
+
type: 'index',
|
|
190
|
+
model: model.name,
|
|
191
|
+
field: field.name,
|
|
192
|
+
message: `Consider adding index on ${model.name}.${field.name}`,
|
|
193
|
+
priority: field.name === 'email' ? 'high' : 'medium',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return recommendations;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Analyze Web Vitals (Lighthouse)
|
|
204
|
+
*/
|
|
205
|
+
function analyzeWebVitals(target) {
|
|
206
|
+
// This would run Playwright with metrics
|
|
207
|
+
return runDockerCommand(`npx playwright test --reporter=json`, {
|
|
208
|
+
cwd: path.join(process.cwd(), 'tests', 'e2e'),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Run command in Docker container
|
|
214
|
+
*/
|
|
215
|
+
function runDockerCommand(command, options = {}) {
|
|
216
|
+
const dockerCmd = `${CONFIG.docker} ${CONFIG.container} ${command}`;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
return {
|
|
220
|
+
success: true,
|
|
221
|
+
output: execSync(dockerCmd, {
|
|
222
|
+
encoding: 'utf-8',
|
|
223
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
224
|
+
input: options.input,
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
error: error.message,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Generate performance report
|
|
237
|
+
*/
|
|
238
|
+
function generateReport(analysis) {
|
|
239
|
+
const report = {
|
|
240
|
+
summary: {},
|
|
241
|
+
recommendations: [],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Bundle analysis
|
|
245
|
+
if (analysis.bundle) {
|
|
246
|
+
report.summary.bundle = {
|
|
247
|
+
totalSize: analysis.bundle.totalSizeMB + ' MB',
|
|
248
|
+
largestChunks: analysis.bundle.chunks.slice(0, 5),
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Check for large bundles
|
|
252
|
+
analysis.bundle.chunks.forEach((chunk) => {
|
|
253
|
+
if (chunk.size > 200 * 1024) { // > 200KB
|
|
254
|
+
report.recommendations.push({
|
|
255
|
+
type: 'bundle',
|
|
256
|
+
message: `Large chunk: ${chunk.name} (${chunk.sizeKB} KB)`,
|
|
257
|
+
priority: 'medium',
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// DB analysis
|
|
264
|
+
if (analysis.db) {
|
|
265
|
+
report.summary.database = {
|
|
266
|
+
models: analysis.db.models.length,
|
|
267
|
+
recommendations: analysis.db.recommendations,
|
|
268
|
+
};
|
|
269
|
+
report.recommendations.push(...analysis.db.recommendations);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return report;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Full performance analysis
|
|
277
|
+
*/
|
|
278
|
+
function analyze(projectDir) {
|
|
279
|
+
const analysis = {
|
|
280
|
+
bundle: analyzeBundleSize(projectDir),
|
|
281
|
+
db: analyzeDbQueries(projectDir),
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return generateReport(analysis);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
runK6Test,
|
|
289
|
+
analyzeBundleSize,
|
|
290
|
+
analyzeDbQueries,
|
|
291
|
+
analyzeWebVitals,
|
|
292
|
+
generateReport,
|
|
293
|
+
analyze,
|
|
294
|
+
};
|