@oalacea/daemon 0.5.0 → 0.5.1
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 +46 -38
- package/LICENSE +23 -23
- package/README.md +147 -141
- package/agents/deps-analyzer.js +366 -366
- package/agents/detector.js +570 -570
- package/agents/fix-engine.js +305 -305
- package/agents/lighthouse-scanner.js +405 -405
- package/agents/perf-analyzer.js +294 -294
- package/agents/perf-front-analyzer.js +229 -229
- package/agents/test-generator.js +387 -387
- package/agents/test-runner.js +318 -318
- package/bin/Dockerfile +75 -74
- package/bin/cli.js +449 -449
- package/lib/config.js +250 -250
- package/lib/docker.js +207 -207
- package/lib/reporter.js +297 -297
- package/package.json +34 -34
- package/prompts/DEPS_EFFICIENCY.md +558 -558
- package/prompts/E2E.md +491 -491
- package/prompts/EXECUTE.md +1060 -1060
- package/prompts/INTEGRATION_API.md +484 -484
- package/prompts/INTEGRATION_DB.md +425 -425
- package/prompts/PERF_API.md +433 -433
- package/prompts/PERF_DB.md +430 -430
- package/prompts/PERF_FRONT.md +357 -357
- package/prompts/REMEDIATION.md +482 -482
- package/prompts/UNIT.md +260 -260
- package/scripts/dev.js +106 -106
- package/templates/README.md +38 -38
- package/templates/k6/load-test.js +54 -54
- package/templates/playwright/e2e.spec.ts +61 -61
- package/templates/vitest/angular-component.test.ts +38 -38
- package/templates/vitest/api.test.ts +51 -51
- package/templates/vitest/component.test.ts +27 -27
- package/templates/vitest/hook.test.ts +36 -36
- package/templates/vitest/solid-component.test.ts +34 -34
- package/templates/vitest/svelte-component.test.ts +33 -33
- package/templates/vitest/vue-component.test.ts +39 -39
package/agents/perf-analyzer.js
CHANGED
|
@@ -1,294 +1,294 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Daemon - 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: 'daemon-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
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Daemon - 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: 'daemon-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
|
+
};
|