@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/deps-analyzer.js
CHANGED
|
@@ -1,366 +1,366 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Daemon - 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
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Daemon - 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
|
+
};
|