@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
|
@@ -1,405 +1,405 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Daemon - Lighthouse Page Scanner
|
|
3
|
-
*
|
|
4
|
-
* Automatically discovers and tests all pages with Lighthouse:
|
|
5
|
-
* 1. Scans project for routes/pages
|
|
6
|
-
* 2. Generates page list
|
|
7
|
-
* 3. Runs Lighthouse on each page
|
|
8
|
-
* 4. Aggregates results
|
|
9
|
-
* 5. Generates recommendations
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Discover pages based on framework
|
|
17
|
-
*/
|
|
18
|
-
function discoverPages(projectDir, framework) {
|
|
19
|
-
const pages = [];
|
|
20
|
-
|
|
21
|
-
switch (framework) {
|
|
22
|
-
case 'Next.js':
|
|
23
|
-
// App Router
|
|
24
|
-
const appDir = path.join(projectDir, 'app');
|
|
25
|
-
if (fs.existsSync(appDir)) {
|
|
26
|
-
pages.push(...scanNextjsAppDir(appDir));
|
|
27
|
-
}
|
|
28
|
-
// Pages Router
|
|
29
|
-
const pagesDir = path.join(projectDir, 'pages');
|
|
30
|
-
if (fs.existsSync(pagesDir)) {
|
|
31
|
-
pages.push(...scanNextjsPagesDir(pagesDir));
|
|
32
|
-
}
|
|
33
|
-
break;
|
|
34
|
-
|
|
35
|
-
case 'Remix':
|
|
36
|
-
const remixRoutesDir = path.join(projectDir, 'app', 'routes');
|
|
37
|
-
if (fs.existsSync(remixRoutesDir)) {
|
|
38
|
-
pages.push(...scanRemixRoutes(remixRoutesDir));
|
|
39
|
-
}
|
|
40
|
-
break;
|
|
41
|
-
|
|
42
|
-
case 'SvelteKit':
|
|
43
|
-
const svelteRoutesDir = path.join(projectDir, 'src', 'routes');
|
|
44
|
-
if (fs.existsSync(svelteRoutesDir)) {
|
|
45
|
-
pages.push(...scanSvelteKitRoutes(svelteRoutesDir));
|
|
46
|
-
}
|
|
47
|
-
break;
|
|
48
|
-
|
|
49
|
-
case 'Nuxt':
|
|
50
|
-
const nuxtPagesDir = path.join(projectDir, 'pages');
|
|
51
|
-
if (fs.existsSync(nuxtPagesDir)) {
|
|
52
|
-
pages.push(...scanNuxtPages(nuxtPagesDir));
|
|
53
|
-
}
|
|
54
|
-
break;
|
|
55
|
-
|
|
56
|
-
case 'Vite':
|
|
57
|
-
case 'React':
|
|
58
|
-
case 'Vue':
|
|
59
|
-
case 'Solid':
|
|
60
|
-
case 'Svelte':
|
|
61
|
-
const srcIndex = path.join(projectDir, 'src', 'App.tsx');
|
|
62
|
-
const srcIndexVue = path.join(projectDir, 'src', 'App.vue');
|
|
63
|
-
const mainJsx = path.join(projectDir, 'src', 'main.jsx');
|
|
64
|
-
|
|
65
|
-
if (fs.existsSync(srcIndex) || fs.existsSync(srcIndexVue) || fs.existsSync(mainJsx)) {
|
|
66
|
-
pages.push({
|
|
67
|
-
path: '/',
|
|
68
|
-
name: 'Home',
|
|
69
|
-
priority: 'critical',
|
|
70
|
-
source: 'SPA entry point'
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
break;
|
|
74
|
-
|
|
75
|
-
case 'Angular':
|
|
76
|
-
const angularRoutes = path.join(projectDir, 'src', 'app', 'app-routing.module.ts');
|
|
77
|
-
if (fs.existsSync(angularRoutes)) {
|
|
78
|
-
pages.push(...scanAngularRoutes(angularRoutes));
|
|
79
|
-
}
|
|
80
|
-
break;
|
|
81
|
-
|
|
82
|
-
case 'Express':
|
|
83
|
-
case 'NestJS':
|
|
84
|
-
pages.push(
|
|
85
|
-
...scanExpressRoutes(projectDir)
|
|
86
|
-
);
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Always add common routes for web apps
|
|
91
|
-
const commonRoutes = [
|
|
92
|
-
{ path: '/', name: 'Home', priority: 'critical' },
|
|
93
|
-
{ path: '/login', name: 'Login', priority: 'high' },
|
|
94
|
-
{ path: '/register', name: 'Register', priority: 'medium' },
|
|
95
|
-
{ path: '/dashboard', name: 'Dashboard', priority: 'high' },
|
|
96
|
-
];
|
|
97
|
-
|
|
98
|
-
// Merge without duplicates
|
|
99
|
-
const existingPaths = new Set(pages.map(p => p.path));
|
|
100
|
-
for (const route of commonRoutes) {
|
|
101
|
-
if (!existingPaths.has(route.path)) {
|
|
102
|
-
pages.push({ ...route, source: 'common route' });
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return pages.sort((a, b) => {
|
|
107
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
108
|
-
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Scan Next.js App Router
|
|
114
|
-
*/
|
|
115
|
-
function scanNextjsAppDir(appDir, basePath = '') {
|
|
116
|
-
const pages = [];
|
|
117
|
-
const items = fs.readdirSync(appDir, { withFileTypes: true });
|
|
118
|
-
|
|
119
|
-
for (const item of items) {
|
|
120
|
-
if (item.name.startsWith('_')) continue; // Ignore _layout, _error, etc.
|
|
121
|
-
|
|
122
|
-
const fullPath = path.join(appDir, item.name);
|
|
123
|
-
const routePath = basePath + (item.name === 'page.tsx' || item.name === 'page.ts'
|
|
124
|
-
? '/'
|
|
125
|
-
: `/${item.name.replace(/\.tsx?$/, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
126
|
-
|
|
127
|
-
if (item.isDirectory()) {
|
|
128
|
-
pages.push(...scanNextjsAppDir(fullPath, routePath));
|
|
129
|
-
} else if (item.name.startsWith('page.')) {
|
|
130
|
-
pages.push({
|
|
131
|
-
path: routePath === '' ? '/' : routePath,
|
|
132
|
-
name: getPageName(routePath),
|
|
133
|
-
priority: routePath === '/' ? 'critical' : 'medium',
|
|
134
|
-
source: fullPath
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return pages;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Scan Next.js Pages Router
|
|
144
|
-
*/
|
|
145
|
-
function scanNextjsPagesDir(pagesDir, basePath = '') {
|
|
146
|
-
const pages = [];
|
|
147
|
-
const items = fs.readdirSync(pagesDir, { withFileTypes: true });
|
|
148
|
-
|
|
149
|
-
for (const item of items) {
|
|
150
|
-
if (item.name.startsWith('_')) continue;
|
|
151
|
-
|
|
152
|
-
const fullPath = path.join(pagesDir, item.name);
|
|
153
|
-
const routePath = basePath + (item.name === 'index.tsx' || item.name === 'index.ts'
|
|
154
|
-
? '/'
|
|
155
|
-
: `/${item.name.replace(/\.tsx?$/, '')}`);
|
|
156
|
-
|
|
157
|
-
if (item.isDirectory()) {
|
|
158
|
-
pages.push(...scanNextjsPagesDir(fullPath, routePath));
|
|
159
|
-
} else if (item.name.endsWith('.tsx') || item.name.endsWith('.ts')) {
|
|
160
|
-
pages.push({
|
|
161
|
-
path: routePath,
|
|
162
|
-
name: getPageName(routePath),
|
|
163
|
-
priority: routePath === '/' || routePath.includes('dashboard') ? 'high' : 'medium',
|
|
164
|
-
source: fullPath
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return pages;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Scan Remix routes
|
|
174
|
-
*/
|
|
175
|
-
function scanRemixRoutes(routesDir, basePath = '') {
|
|
176
|
-
const pages = [];
|
|
177
|
-
const items = fs.readdirSync(routesDir, { withFileTypes: true });
|
|
178
|
-
|
|
179
|
-
for (const item of items) {
|
|
180
|
-
if (item.name.startsWith('_')) continue;
|
|
181
|
-
|
|
182
|
-
const fullPath = path.join(routesDir, item.name);
|
|
183
|
-
const routePath = basePath + (item.name === 'root.tsx' || item.name === 'root.ts'
|
|
184
|
-
? '/'
|
|
185
|
-
: `/${item.name.replace(/\.tsx?$/, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
186
|
-
|
|
187
|
-
if (item.isDirectory()) {
|
|
188
|
-
pages.push(...scanRemixRoutes(fullPath, routePath));
|
|
189
|
-
} else if (!item.name.startsWith('_')) {
|
|
190
|
-
pages.push({
|
|
191
|
-
path: routePath === '' ? '/' : routePath,
|
|
192
|
-
name: getPageName(routePath),
|
|
193
|
-
priority: routePath === '/' ? 'critical' : 'medium',
|
|
194
|
-
source: fullPath
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return pages;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Scan SvelteKit routes
|
|
204
|
-
*/
|
|
205
|
-
function scanSvelteKitRoutes(routesDir, basePath = '') {
|
|
206
|
-
const pages = [];
|
|
207
|
-
const items = fs.readdirSync(routesDir, { withFileTypes: true });
|
|
208
|
-
|
|
209
|
-
for (const item of items) {
|
|
210
|
-
if (item.name.startsWith('_')) continue;
|
|
211
|
-
|
|
212
|
-
const fullPath = path.join(routesDir, item.name);
|
|
213
|
-
const routePath = basePath + (item.name === '+page.svelte'
|
|
214
|
-
? '/'
|
|
215
|
-
: `/${item.name.replace(/\+\w+\./, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
216
|
-
|
|
217
|
-
if (item.isDirectory()) {
|
|
218
|
-
pages.push(...scanSvelteKitRoutes(fullPath, routePath));
|
|
219
|
-
} else if (item.name.includes('+page')) {
|
|
220
|
-
pages.push({
|
|
221
|
-
path: routePath === '' ? '/' : routePath,
|
|
222
|
-
name: getPageName(routePath),
|
|
223
|
-
priority: routePath === '/' ? 'critical' : 'medium',
|
|
224
|
-
source: fullPath
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return pages;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Scan Nuxt pages
|
|
234
|
-
*/
|
|
235
|
-
function scanNuxtPages(pagesDir, basePath = '') {
|
|
236
|
-
const pages = [];
|
|
237
|
-
const items = fs.readdirSync(pagesDir, { withFileTypes: true });
|
|
238
|
-
|
|
239
|
-
for (const item of items) {
|
|
240
|
-
const fullPath = path.join(pagesDir, item.name);
|
|
241
|
-
const routePath = basePath + (item.name === 'index.vue'
|
|
242
|
-
? '/'
|
|
243
|
-
: `/${item.name.replace(/\.vue$/, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
244
|
-
|
|
245
|
-
if (item.isDirectory()) {
|
|
246
|
-
pages.push(...scanNuxtPages(fullPath, routePath));
|
|
247
|
-
} else if (item.name.endsWith('.vue')) {
|
|
248
|
-
pages.push({
|
|
249
|
-
path: routePath,
|
|
250
|
-
name: getPageName(routePath),
|
|
251
|
-
priority: routePath === '/' ? 'critical' : 'medium',
|
|
252
|
-
source: fullPath
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return pages;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Scan Angular routes (basic)
|
|
262
|
-
*/
|
|
263
|
-
function scanAngularRoutes(routingFile) {
|
|
264
|
-
const content = fs.readFileSync(routingFile, 'utf-8');
|
|
265
|
-
const pages = [
|
|
266
|
-
{ path: '/', name: 'Home', priority: 'critical', source: routingFile }
|
|
267
|
-
];
|
|
268
|
-
|
|
269
|
-
// Extract path definitions from RouterModule
|
|
270
|
-
const pathMatches = content.matchAll(/path:\s*['"`]([^'"`]+)['"`]/g);
|
|
271
|
-
for (const match of pathMatches) {
|
|
272
|
-
const routePath = match[1];
|
|
273
|
-
if (routePath !== '' && routePath !== '**') {
|
|
274
|
-
pages.push({
|
|
275
|
-
path: `/${routePath}`,
|
|
276
|
-
name: getPageName(`/${routePath}`),
|
|
277
|
-
priority: 'medium',
|
|
278
|
-
source: routingFile
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return pages;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Scan Express/NestJS routes (basic API routes)
|
|
288
|
-
*/
|
|
289
|
-
function scanExpressRoutes(projectDir) {
|
|
290
|
-
const pages = [];
|
|
291
|
-
|
|
292
|
-
// For API routes, we typically test the API documentation or main endpoints
|
|
293
|
-
const commonApiRoutes = [
|
|
294
|
-
{ path: '/api', name: 'API Root', priority: 'low', source: 'API routes' },
|
|
295
|
-
{ path: '/api/health', name: 'Health Check', priority: 'low', source: 'API routes' },
|
|
296
|
-
];
|
|
297
|
-
|
|
298
|
-
// Try to find route definitions
|
|
299
|
-
const srcFiles = findFiles(projectDir, ['.ts', '.js'], ['routes', 'controllers']);
|
|
300
|
-
|
|
301
|
-
for (const file of srcFiles) {
|
|
302
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
303
|
-
|
|
304
|
-
// Look for route definitions like app.get(), router.get(), @Get(), etc.
|
|
305
|
-
const getRoutes = content.matchAll(/(?:app|router)\.get\(['"`]([^'"`]+)['"`]/g);
|
|
306
|
-
for (const match of getRoutes) {
|
|
307
|
-
pages.push({
|
|
308
|
-
path: `/api${match[1]}`,
|
|
309
|
-
name: `API: ${match[1]}`,
|
|
310
|
-
priority: 'low',
|
|
311
|
-
source: file
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return pages.length > 0 ? pages : commonApiRoutes;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Helper: Find files recursively
|
|
321
|
-
*/
|
|
322
|
-
function findFiles(dir, extensions, keywords = []) {
|
|
323
|
-
const results = [];
|
|
324
|
-
|
|
325
|
-
if (!fs.existsSync(dir)) return results;
|
|
326
|
-
|
|
327
|
-
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
328
|
-
|
|
329
|
-
for (const item of items) {
|
|
330
|
-
const fullPath = path.join(dir, item.name);
|
|
331
|
-
|
|
332
|
-
if (item.isDirectory()) {
|
|
333
|
-
if (item.name !== 'node_modules' && !item.name.startsWith('.')) {
|
|
334
|
-
results.push(...findFiles(fullPath, extensions, keywords));
|
|
335
|
-
}
|
|
336
|
-
} else if (extensions.some(ext => item.name.endsWith(ext))) {
|
|
337
|
-
if (keywords.length === 0 || keywords.some(kw => fullPath.includes(kw))) {
|
|
338
|
-
results.push(fullPath);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return results;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Helper: Get readable page name
|
|
348
|
-
*/
|
|
349
|
-
function getPageName(routePath) {
|
|
350
|
-
if (routePath === '/') return 'Home';
|
|
351
|
-
|
|
352
|
-
const parts = routePath.split('/').filter(Boolean);
|
|
353
|
-
return parts
|
|
354
|
-
.map(p => p.replace(/[:-]/g, ' ').replace(/\*/g, 'wildcard'))
|
|
355
|
-
.map(p => p.charAt(0).toUpperCase() + p.slice(1))
|
|
356
|
-
.join(' / ');
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Generate Lighthouse commands for all pages
|
|
361
|
-
*/
|
|
362
|
-
function generateLighthouseCommands(pages, baseUrl) {
|
|
363
|
-
return pages.map((page, index) => ({
|
|
364
|
-
...page,
|
|
365
|
-
command: `npx lighthouse "${baseUrl}${page.path}" --output=json --output=html --chrome-flags="--headless --no-sandbox" --quiet`,
|
|
366
|
-
outputPath: path.join(process.cwd(), 'reports', `lighthouse-${index}-${page.path.replace(/\//g, '-')}.json`)
|
|
367
|
-
}));
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Create page scanner summary
|
|
372
|
-
*/
|
|
373
|
-
function createPageSummary(pages) {
|
|
374
|
-
const byPriority = {
|
|
375
|
-
critical: pages.filter(p => p.priority === 'critical'),
|
|
376
|
-
high: pages.filter(p => p.priority === 'high'),
|
|
377
|
-
medium: pages.filter(p => p.priority === 'medium'),
|
|
378
|
-
low: pages.filter(p => p.priority === 'low'),
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
total: pages.length,
|
|
383
|
-
byPriority,
|
|
384
|
-
pages: pages.map(p => ({
|
|
385
|
-
path: p.path,
|
|
386
|
-
name: p.name,
|
|
387
|
-
priority: p.priority,
|
|
388
|
-
source: p.source
|
|
389
|
-
}))
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
module.exports = {
|
|
394
|
-
discoverPages,
|
|
395
|
-
generateLighthouseCommands,
|
|
396
|
-
createPageSummary,
|
|
397
|
-
// Export scanner functions for testing
|
|
398
|
-
scanNextjsAppDir,
|
|
399
|
-
scanNextjsPagesDir,
|
|
400
|
-
scanRemixRoutes,
|
|
401
|
-
scanSvelteKitRoutes,
|
|
402
|
-
scanNuxtPages,
|
|
403
|
-
scanAngularRoutes,
|
|
404
|
-
scanExpressRoutes,
|
|
405
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* Daemon - Lighthouse Page Scanner
|
|
3
|
+
*
|
|
4
|
+
* Automatically discovers and tests all pages with Lighthouse:
|
|
5
|
+
* 1. Scans project for routes/pages
|
|
6
|
+
* 2. Generates page list
|
|
7
|
+
* 3. Runs Lighthouse on each page
|
|
8
|
+
* 4. Aggregates results
|
|
9
|
+
* 5. Generates recommendations
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Discover pages based on framework
|
|
17
|
+
*/
|
|
18
|
+
function discoverPages(projectDir, framework) {
|
|
19
|
+
const pages = [];
|
|
20
|
+
|
|
21
|
+
switch (framework) {
|
|
22
|
+
case 'Next.js':
|
|
23
|
+
// App Router
|
|
24
|
+
const appDir = path.join(projectDir, 'app');
|
|
25
|
+
if (fs.existsSync(appDir)) {
|
|
26
|
+
pages.push(...scanNextjsAppDir(appDir));
|
|
27
|
+
}
|
|
28
|
+
// Pages Router
|
|
29
|
+
const pagesDir = path.join(projectDir, 'pages');
|
|
30
|
+
if (fs.existsSync(pagesDir)) {
|
|
31
|
+
pages.push(...scanNextjsPagesDir(pagesDir));
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
|
|
35
|
+
case 'Remix':
|
|
36
|
+
const remixRoutesDir = path.join(projectDir, 'app', 'routes');
|
|
37
|
+
if (fs.existsSync(remixRoutesDir)) {
|
|
38
|
+
pages.push(...scanRemixRoutes(remixRoutesDir));
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
|
|
42
|
+
case 'SvelteKit':
|
|
43
|
+
const svelteRoutesDir = path.join(projectDir, 'src', 'routes');
|
|
44
|
+
if (fs.existsSync(svelteRoutesDir)) {
|
|
45
|
+
pages.push(...scanSvelteKitRoutes(svelteRoutesDir));
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case 'Nuxt':
|
|
50
|
+
const nuxtPagesDir = path.join(projectDir, 'pages');
|
|
51
|
+
if (fs.existsSync(nuxtPagesDir)) {
|
|
52
|
+
pages.push(...scanNuxtPages(nuxtPagesDir));
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
|
|
56
|
+
case 'Vite':
|
|
57
|
+
case 'React':
|
|
58
|
+
case 'Vue':
|
|
59
|
+
case 'Solid':
|
|
60
|
+
case 'Svelte':
|
|
61
|
+
const srcIndex = path.join(projectDir, 'src', 'App.tsx');
|
|
62
|
+
const srcIndexVue = path.join(projectDir, 'src', 'App.vue');
|
|
63
|
+
const mainJsx = path.join(projectDir, 'src', 'main.jsx');
|
|
64
|
+
|
|
65
|
+
if (fs.existsSync(srcIndex) || fs.existsSync(srcIndexVue) || fs.existsSync(mainJsx)) {
|
|
66
|
+
pages.push({
|
|
67
|
+
path: '/',
|
|
68
|
+
name: 'Home',
|
|
69
|
+
priority: 'critical',
|
|
70
|
+
source: 'SPA entry point'
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case 'Angular':
|
|
76
|
+
const angularRoutes = path.join(projectDir, 'src', 'app', 'app-routing.module.ts');
|
|
77
|
+
if (fs.existsSync(angularRoutes)) {
|
|
78
|
+
pages.push(...scanAngularRoutes(angularRoutes));
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case 'Express':
|
|
83
|
+
case 'NestJS':
|
|
84
|
+
pages.push(
|
|
85
|
+
...scanExpressRoutes(projectDir)
|
|
86
|
+
);
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Always add common routes for web apps
|
|
91
|
+
const commonRoutes = [
|
|
92
|
+
{ path: '/', name: 'Home', priority: 'critical' },
|
|
93
|
+
{ path: '/login', name: 'Login', priority: 'high' },
|
|
94
|
+
{ path: '/register', name: 'Register', priority: 'medium' },
|
|
95
|
+
{ path: '/dashboard', name: 'Dashboard', priority: 'high' },
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
// Merge without duplicates
|
|
99
|
+
const existingPaths = new Set(pages.map(p => p.path));
|
|
100
|
+
for (const route of commonRoutes) {
|
|
101
|
+
if (!existingPaths.has(route.path)) {
|
|
102
|
+
pages.push({ ...route, source: 'common route' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return pages.sort((a, b) => {
|
|
107
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
108
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Scan Next.js App Router
|
|
114
|
+
*/
|
|
115
|
+
function scanNextjsAppDir(appDir, basePath = '') {
|
|
116
|
+
const pages = [];
|
|
117
|
+
const items = fs.readdirSync(appDir, { withFileTypes: true });
|
|
118
|
+
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
if (item.name.startsWith('_')) continue; // Ignore _layout, _error, etc.
|
|
121
|
+
|
|
122
|
+
const fullPath = path.join(appDir, item.name);
|
|
123
|
+
const routePath = basePath + (item.name === 'page.tsx' || item.name === 'page.ts'
|
|
124
|
+
? '/'
|
|
125
|
+
: `/${item.name.replace(/\.tsx?$/, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
126
|
+
|
|
127
|
+
if (item.isDirectory()) {
|
|
128
|
+
pages.push(...scanNextjsAppDir(fullPath, routePath));
|
|
129
|
+
} else if (item.name.startsWith('page.')) {
|
|
130
|
+
pages.push({
|
|
131
|
+
path: routePath === '' ? '/' : routePath,
|
|
132
|
+
name: getPageName(routePath),
|
|
133
|
+
priority: routePath === '/' ? 'critical' : 'medium',
|
|
134
|
+
source: fullPath
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return pages;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Scan Next.js Pages Router
|
|
144
|
+
*/
|
|
145
|
+
function scanNextjsPagesDir(pagesDir, basePath = '') {
|
|
146
|
+
const pages = [];
|
|
147
|
+
const items = fs.readdirSync(pagesDir, { withFileTypes: true });
|
|
148
|
+
|
|
149
|
+
for (const item of items) {
|
|
150
|
+
if (item.name.startsWith('_')) continue;
|
|
151
|
+
|
|
152
|
+
const fullPath = path.join(pagesDir, item.name);
|
|
153
|
+
const routePath = basePath + (item.name === 'index.tsx' || item.name === 'index.ts'
|
|
154
|
+
? '/'
|
|
155
|
+
: `/${item.name.replace(/\.tsx?$/, '')}`);
|
|
156
|
+
|
|
157
|
+
if (item.isDirectory()) {
|
|
158
|
+
pages.push(...scanNextjsPagesDir(fullPath, routePath));
|
|
159
|
+
} else if (item.name.endsWith('.tsx') || item.name.endsWith('.ts')) {
|
|
160
|
+
pages.push({
|
|
161
|
+
path: routePath,
|
|
162
|
+
name: getPageName(routePath),
|
|
163
|
+
priority: routePath === '/' || routePath.includes('dashboard') ? 'high' : 'medium',
|
|
164
|
+
source: fullPath
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return pages;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Scan Remix routes
|
|
174
|
+
*/
|
|
175
|
+
function scanRemixRoutes(routesDir, basePath = '') {
|
|
176
|
+
const pages = [];
|
|
177
|
+
const items = fs.readdirSync(routesDir, { withFileTypes: true });
|
|
178
|
+
|
|
179
|
+
for (const item of items) {
|
|
180
|
+
if (item.name.startsWith('_')) continue;
|
|
181
|
+
|
|
182
|
+
const fullPath = path.join(routesDir, item.name);
|
|
183
|
+
const routePath = basePath + (item.name === 'root.tsx' || item.name === 'root.ts'
|
|
184
|
+
? '/'
|
|
185
|
+
: `/${item.name.replace(/\.tsx?$/, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
186
|
+
|
|
187
|
+
if (item.isDirectory()) {
|
|
188
|
+
pages.push(...scanRemixRoutes(fullPath, routePath));
|
|
189
|
+
} else if (!item.name.startsWith('_')) {
|
|
190
|
+
pages.push({
|
|
191
|
+
path: routePath === '' ? '/' : routePath,
|
|
192
|
+
name: getPageName(routePath),
|
|
193
|
+
priority: routePath === '/' ? 'critical' : 'medium',
|
|
194
|
+
source: fullPath
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return pages;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Scan SvelteKit routes
|
|
204
|
+
*/
|
|
205
|
+
function scanSvelteKitRoutes(routesDir, basePath = '') {
|
|
206
|
+
const pages = [];
|
|
207
|
+
const items = fs.readdirSync(routesDir, { withFileTypes: true });
|
|
208
|
+
|
|
209
|
+
for (const item of items) {
|
|
210
|
+
if (item.name.startsWith('_')) continue;
|
|
211
|
+
|
|
212
|
+
const fullPath = path.join(routesDir, item.name);
|
|
213
|
+
const routePath = basePath + (item.name === '+page.svelte'
|
|
214
|
+
? '/'
|
|
215
|
+
: `/${item.name.replace(/\+\w+\./, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
216
|
+
|
|
217
|
+
if (item.isDirectory()) {
|
|
218
|
+
pages.push(...scanSvelteKitRoutes(fullPath, routePath));
|
|
219
|
+
} else if (item.name.includes('+page')) {
|
|
220
|
+
pages.push({
|
|
221
|
+
path: routePath === '' ? '/' : routePath,
|
|
222
|
+
name: getPageName(routePath),
|
|
223
|
+
priority: routePath === '/' ? 'critical' : 'medium',
|
|
224
|
+
source: fullPath
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return pages;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Scan Nuxt pages
|
|
234
|
+
*/
|
|
235
|
+
function scanNuxtPages(pagesDir, basePath = '') {
|
|
236
|
+
const pages = [];
|
|
237
|
+
const items = fs.readdirSync(pagesDir, { withFileTypes: true });
|
|
238
|
+
|
|
239
|
+
for (const item of items) {
|
|
240
|
+
const fullPath = path.join(pagesDir, item.name);
|
|
241
|
+
const routePath = basePath + (item.name === 'index.vue'
|
|
242
|
+
? '/'
|
|
243
|
+
: `/${item.name.replace(/\.vue$/, '').replace(/\[\.{3}.+\]/, '*').replace(/\[(.+)\]/, ':$1')}`);
|
|
244
|
+
|
|
245
|
+
if (item.isDirectory()) {
|
|
246
|
+
pages.push(...scanNuxtPages(fullPath, routePath));
|
|
247
|
+
} else if (item.name.endsWith('.vue')) {
|
|
248
|
+
pages.push({
|
|
249
|
+
path: routePath,
|
|
250
|
+
name: getPageName(routePath),
|
|
251
|
+
priority: routePath === '/' ? 'critical' : 'medium',
|
|
252
|
+
source: fullPath
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return pages;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Scan Angular routes (basic)
|
|
262
|
+
*/
|
|
263
|
+
function scanAngularRoutes(routingFile) {
|
|
264
|
+
const content = fs.readFileSync(routingFile, 'utf-8');
|
|
265
|
+
const pages = [
|
|
266
|
+
{ path: '/', name: 'Home', priority: 'critical', source: routingFile }
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// Extract path definitions from RouterModule
|
|
270
|
+
const pathMatches = content.matchAll(/path:\s*['"`]([^'"`]+)['"`]/g);
|
|
271
|
+
for (const match of pathMatches) {
|
|
272
|
+
const routePath = match[1];
|
|
273
|
+
if (routePath !== '' && routePath !== '**') {
|
|
274
|
+
pages.push({
|
|
275
|
+
path: `/${routePath}`,
|
|
276
|
+
name: getPageName(`/${routePath}`),
|
|
277
|
+
priority: 'medium',
|
|
278
|
+
source: routingFile
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return pages;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Scan Express/NestJS routes (basic API routes)
|
|
288
|
+
*/
|
|
289
|
+
function scanExpressRoutes(projectDir) {
|
|
290
|
+
const pages = [];
|
|
291
|
+
|
|
292
|
+
// For API routes, we typically test the API documentation or main endpoints
|
|
293
|
+
const commonApiRoutes = [
|
|
294
|
+
{ path: '/api', name: 'API Root', priority: 'low', source: 'API routes' },
|
|
295
|
+
{ path: '/api/health', name: 'Health Check', priority: 'low', source: 'API routes' },
|
|
296
|
+
];
|
|
297
|
+
|
|
298
|
+
// Try to find route definitions
|
|
299
|
+
const srcFiles = findFiles(projectDir, ['.ts', '.js'], ['routes', 'controllers']);
|
|
300
|
+
|
|
301
|
+
for (const file of srcFiles) {
|
|
302
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
303
|
+
|
|
304
|
+
// Look for route definitions like app.get(), router.get(), @Get(), etc.
|
|
305
|
+
const getRoutes = content.matchAll(/(?:app|router)\.get\(['"`]([^'"`]+)['"`]/g);
|
|
306
|
+
for (const match of getRoutes) {
|
|
307
|
+
pages.push({
|
|
308
|
+
path: `/api${match[1]}`,
|
|
309
|
+
name: `API: ${match[1]}`,
|
|
310
|
+
priority: 'low',
|
|
311
|
+
source: file
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return pages.length > 0 ? pages : commonApiRoutes;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Helper: Find files recursively
|
|
321
|
+
*/
|
|
322
|
+
function findFiles(dir, extensions, keywords = []) {
|
|
323
|
+
const results = [];
|
|
324
|
+
|
|
325
|
+
if (!fs.existsSync(dir)) return results;
|
|
326
|
+
|
|
327
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
328
|
+
|
|
329
|
+
for (const item of items) {
|
|
330
|
+
const fullPath = path.join(dir, item.name);
|
|
331
|
+
|
|
332
|
+
if (item.isDirectory()) {
|
|
333
|
+
if (item.name !== 'node_modules' && !item.name.startsWith('.')) {
|
|
334
|
+
results.push(...findFiles(fullPath, extensions, keywords));
|
|
335
|
+
}
|
|
336
|
+
} else if (extensions.some(ext => item.name.endsWith(ext))) {
|
|
337
|
+
if (keywords.length === 0 || keywords.some(kw => fullPath.includes(kw))) {
|
|
338
|
+
results.push(fullPath);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return results;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Helper: Get readable page name
|
|
348
|
+
*/
|
|
349
|
+
function getPageName(routePath) {
|
|
350
|
+
if (routePath === '/') return 'Home';
|
|
351
|
+
|
|
352
|
+
const parts = routePath.split('/').filter(Boolean);
|
|
353
|
+
return parts
|
|
354
|
+
.map(p => p.replace(/[:-]/g, ' ').replace(/\*/g, 'wildcard'))
|
|
355
|
+
.map(p => p.charAt(0).toUpperCase() + p.slice(1))
|
|
356
|
+
.join(' / ');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Generate Lighthouse commands for all pages
|
|
361
|
+
*/
|
|
362
|
+
function generateLighthouseCommands(pages, baseUrl) {
|
|
363
|
+
return pages.map((page, index) => ({
|
|
364
|
+
...page,
|
|
365
|
+
command: `npx lighthouse "${baseUrl}${page.path}" --output=json --output=html --chrome-flags="--headless --no-sandbox" --quiet`,
|
|
366
|
+
outputPath: path.join(process.cwd(), 'reports', `lighthouse-${index}-${page.path.replace(/\//g, '-')}.json`)
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Create page scanner summary
|
|
372
|
+
*/
|
|
373
|
+
function createPageSummary(pages) {
|
|
374
|
+
const byPriority = {
|
|
375
|
+
critical: pages.filter(p => p.priority === 'critical'),
|
|
376
|
+
high: pages.filter(p => p.priority === 'high'),
|
|
377
|
+
medium: pages.filter(p => p.priority === 'medium'),
|
|
378
|
+
low: pages.filter(p => p.priority === 'low'),
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
total: pages.length,
|
|
383
|
+
byPriority,
|
|
384
|
+
pages: pages.map(p => ({
|
|
385
|
+
path: p.path,
|
|
386
|
+
name: p.name,
|
|
387
|
+
priority: p.priority,
|
|
388
|
+
source: p.source
|
|
389
|
+
}))
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
module.exports = {
|
|
394
|
+
discoverPages,
|
|
395
|
+
generateLighthouseCommands,
|
|
396
|
+
createPageSummary,
|
|
397
|
+
// Export scanner functions for testing
|
|
398
|
+
scanNextjsAppDir,
|
|
399
|
+
scanNextjsPagesDir,
|
|
400
|
+
scanRemixRoutes,
|
|
401
|
+
scanSvelteKitRoutes,
|
|
402
|
+
scanNuxtPages,
|
|
403
|
+
scanAngularRoutes,
|
|
404
|
+
scanExpressRoutes,
|
|
405
|
+
};
|