@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +46 -38
  2. package/LICENSE +23 -23
  3. package/README.md +147 -141
  4. package/agents/deps-analyzer.js +366 -366
  5. package/agents/detector.js +570 -570
  6. package/agents/fix-engine.js +305 -305
  7. package/agents/lighthouse-scanner.js +405 -405
  8. package/agents/perf-analyzer.js +294 -294
  9. package/agents/perf-front-analyzer.js +229 -229
  10. package/agents/test-generator.js +387 -387
  11. package/agents/test-runner.js +318 -318
  12. package/bin/Dockerfile +75 -74
  13. package/bin/cli.js +449 -449
  14. package/lib/config.js +250 -250
  15. package/lib/docker.js +207 -207
  16. package/lib/reporter.js +297 -297
  17. package/package.json +34 -34
  18. package/prompts/DEPS_EFFICIENCY.md +558 -558
  19. package/prompts/E2E.md +491 -491
  20. package/prompts/EXECUTE.md +1060 -1060
  21. package/prompts/INTEGRATION_API.md +484 -484
  22. package/prompts/INTEGRATION_DB.md +425 -425
  23. package/prompts/PERF_API.md +433 -433
  24. package/prompts/PERF_DB.md +430 -430
  25. package/prompts/PERF_FRONT.md +357 -357
  26. package/prompts/REMEDIATION.md +482 -482
  27. package/prompts/UNIT.md +260 -260
  28. package/scripts/dev.js +106 -106
  29. package/templates/README.md +38 -38
  30. package/templates/k6/load-test.js +54 -54
  31. package/templates/playwright/e2e.spec.ts +61 -61
  32. package/templates/vitest/angular-component.test.ts +38 -38
  33. package/templates/vitest/api.test.ts +51 -51
  34. package/templates/vitest/component.test.ts +27 -27
  35. package/templates/vitest/hook.test.ts +36 -36
  36. package/templates/vitest/solid-component.test.ts +34 -34
  37. package/templates/vitest/svelte-component.test.ts +33 -33
  38. 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
+ };