design-clone 1.2.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +26 -12
  2. package/bin/commands/clone-site.js +75 -10
  3. package/bin/commands/init.js +33 -1
  4. package/bin/commands/verify.js +5 -3
  5. package/bin/utils/validate.js +24 -8
  6. package/docs/cli-reference.md +200 -2
  7. package/docs/codebase-summary.md +309 -0
  8. package/docs/design-clone-architecture.md +259 -42
  9. package/docs/pixel-perfect.md +35 -4
  10. package/docs/project-roadmap.md +382 -0
  11. package/docs/troubleshooting.md +5 -4
  12. package/package.json +10 -8
  13. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  14. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  15. package/src/ai/analyze-structure.py +73 -3
  16. package/src/ai/extract-design-tokens.py +356 -13
  17. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  18. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  19. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  20. package/src/ai/prompts/design_tokens.py +133 -0
  21. package/src/ai/prompts/structure_analysis.py +329 -10
  22. package/src/ai/prompts/ux_audit.py +198 -0
  23. package/src/ai/ux-audit.js +596 -0
  24. package/src/core/app-state-snapshot.js +511 -0
  25. package/src/core/content-counter.js +342 -0
  26. package/src/core/cookie-handler.js +1 -1
  27. package/src/core/css-extractor.js +4 -4
  28. package/src/core/dimension-extractor.js +93 -21
  29. package/src/core/dimension-output.js +103 -6
  30. package/src/core/discover-pages.js +242 -14
  31. package/src/core/dom-tree-analyzer.js +298 -0
  32. package/src/core/extract-assets.js +1 -1
  33. package/src/core/framework-detector.js +538 -0
  34. package/src/core/html-extractor.js +45 -4
  35. package/src/core/lazy-loader.js +7 -7
  36. package/src/core/multi-page-screenshot.js +9 -6
  37. package/src/core/page-readiness.js +8 -8
  38. package/src/core/screenshot.js +138 -9
  39. package/src/core/section-cropper.js +209 -0
  40. package/src/core/section-detector.js +386 -0
  41. package/src/core/semantic-enhancer.js +492 -0
  42. package/src/core/state-capture.js +18 -22
  43. package/src/core/tests/test-section-cropper.js +177 -0
  44. package/src/core/tests/test-section-detector.js +55 -0
  45. package/src/core/video-capture.js +152 -146
  46. package/src/route-discoverers/angular-discoverer.js +157 -0
  47. package/src/route-discoverers/astro-discoverer.js +123 -0
  48. package/src/route-discoverers/base-discoverer.js +242 -0
  49. package/src/route-discoverers/index.js +106 -0
  50. package/src/route-discoverers/next-discoverer.js +130 -0
  51. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  52. package/src/route-discoverers/react-discoverer.js +139 -0
  53. package/src/route-discoverers/svelte-discoverer.js +109 -0
  54. package/src/route-discoverers/universal-discoverer.js +227 -0
  55. package/src/route-discoverers/vue-discoverer.js +118 -0
  56. package/src/utils/__init__.py +1 -1
  57. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  58. package/src/utils/browser.js +11 -37
  59. package/src/utils/playwright.js +213 -0
  60. package/src/verification/generate-audit-report.js +398 -0
  61. package/src/verification/verify-footer.js +493 -0
  62. package/src/verification/verify-header.js +486 -0
  63. package/src/verification/verify-layout.js +2 -2
  64. package/src/verification/verify-menu.js +4 -20
  65. package/src/verification/verify-slider.js +533 -0
  66. package/src/utils/puppeteer.js +0 -281
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Angular Route Discoverer
3
+ *
4
+ * Extracts routes from Angular applications using:
5
+ * - ng.probe() for component inspection
6
+ * - routerLink attributes in DOM
7
+ * - app-root element analysis
8
+ */
9
+
10
+ import { BaseDiscoverer } from './base-discoverer.js';
11
+
12
+ export class AngularDiscoverer extends BaseDiscoverer {
13
+ /**
14
+ * Discover routes from an Angular application
15
+ * @returns {Promise<import('./base-discoverer.js').DiscoveredRoute[]>}
16
+ */
17
+ async discover() {
18
+ const rawRoutes = await this.page.evaluate(() => {
19
+ const routes = [];
20
+
21
+ // Method 1: ng.probe() to access Router
22
+ const appRoot = document.querySelector('app-root');
23
+ if (appRoot && window.ng?.probe) {
24
+ try {
25
+ const debugElement = window.ng.probe(appRoot);
26
+ if (debugElement?.injector) {
27
+ // Try to get Router from injector
28
+ // Note: This may not work in production builds
29
+ const injector = debugElement.injector;
30
+
31
+ // Look for router in provider tree
32
+ const getAllProviders = (inj) => {
33
+ const providers = [];
34
+ if (inj._records) {
35
+ inj._records.forEach((v, k) => {
36
+ if (k.toString().includes('Router')) {
37
+ providers.push(v);
38
+ }
39
+ });
40
+ }
41
+ return providers;
42
+ };
43
+
44
+ const routerProviders = getAllProviders(injector);
45
+ routerProviders.forEach(provider => {
46
+ if (provider?.config) {
47
+ extractAngularRoutes(provider.config, routes);
48
+ }
49
+ });
50
+ }
51
+ } catch (e) {
52
+ // ng.probe may not be available in production
53
+ }
54
+ }
55
+
56
+ // Method 2: routerLink attributes (most reliable)
57
+ document.querySelectorAll('[routerLink], [routerlink]').forEach(el => {
58
+ const path = el.getAttribute('routerLink') || el.getAttribute('routerlink');
59
+ if (path) {
60
+ const text = el.textContent?.trim();
61
+ routes.push({
62
+ path: path.startsWith('/') ? path : '/' + path,
63
+ name: text || '',
64
+ source: 'framework'
65
+ });
66
+ }
67
+ });
68
+
69
+ // Method 3: [routerLink] with binding syntax
70
+ document.querySelectorAll('a[href]').forEach(link => {
71
+ const href = link.getAttribute('href');
72
+ if (href && href.startsWith('/')) {
73
+ // Check if it's inside Angular app
74
+ const isAngularLink = link.closest('app-root') ||
75
+ link.hasAttribute('routerLinkActive') ||
76
+ link.classList.contains('active');
77
+
78
+ if (isAngularLink || link.closest('nav, header, [role="navigation"]')) {
79
+ const text = link.textContent?.trim();
80
+ if (!routes.some(r => r.path === href)) {
81
+ routes.push({
82
+ path: href,
83
+ name: text || '',
84
+ source: isAngularLink ? 'framework' : 'link-scrape'
85
+ });
86
+ }
87
+ }
88
+ }
89
+ });
90
+
91
+ // Method 4: routerLinkActive elements
92
+ document.querySelectorAll('[routerLinkActive], [routerlinkactive]').forEach(el => {
93
+ const link = el.tagName === 'A' ? el : el.querySelector('a');
94
+ if (link) {
95
+ const href = link.getAttribute('href') ||
96
+ link.getAttribute('routerLink') ||
97
+ link.getAttribute('routerlink');
98
+ if (href && !routes.some(r => r.path === href)) {
99
+ routes.push({
100
+ path: href.startsWith('/') ? href : '/' + href,
101
+ name: link.textContent?.trim() || '',
102
+ source: 'framework'
103
+ });
104
+ }
105
+ }
106
+ });
107
+
108
+ /**
109
+ * Extract routes from Angular Router config
110
+ */
111
+ function extractAngularRoutes(config, output, prefix = '') {
112
+ if (!Array.isArray(config)) return;
113
+
114
+ config.forEach(route => {
115
+ if (!route.path && route.path !== '') return;
116
+
117
+ let path = route.path;
118
+ if (prefix && !path.startsWith('/')) {
119
+ path = prefix + '/' + path;
120
+ }
121
+ if (!path.startsWith('/')) {
122
+ path = '/' + path;
123
+ }
124
+
125
+ // Skip wildcard and redirect-only routes
126
+ if (path === '/**' || path === '**' || (!route.component && route.redirectTo)) {
127
+ return;
128
+ }
129
+
130
+ output.push({
131
+ path,
132
+ name: route.data?.title || route.title || '',
133
+ component: route.component?.name || '',
134
+ source: 'framework'
135
+ });
136
+
137
+ // Process child routes
138
+ if (route.children) {
139
+ extractAngularRoutes(route.children, output, path);
140
+ }
141
+ });
142
+ }
143
+
144
+ return routes;
145
+ });
146
+
147
+ const processedRoutes = rawRoutes.map(route => ({
148
+ ...route,
149
+ name: route.name || this.extractPageName(route.path, route.component),
150
+ path: this.normalizeRoute(route.path)
151
+ }));
152
+
153
+ return this.deduplicateRoutes(processedRoutes);
154
+ }
155
+ }
156
+
157
+ export default AngularDiscoverer;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Astro Route Discoverer
3
+ *
4
+ * Astro is primarily a static site generator with islands architecture.
5
+ * Routes are extracted from:
6
+ * - astro-island components
7
+ * - data-astro-* prefetch attributes
8
+ * - Standard navigation links (Astro generates static HTML)
9
+ */
10
+
11
+ import { BaseDiscoverer } from './base-discoverer.js';
12
+
13
+ export class AstroDiscoverer extends BaseDiscoverer {
14
+ /**
15
+ * Discover routes from an Astro application
16
+ * @returns {Promise<import('./base-discoverer.js').DiscoveredRoute[]>}
17
+ */
18
+ async discover() {
19
+ const rawRoutes = await this.page.evaluate(() => {
20
+ const routes = [];
21
+
22
+ // Method 1: astro-island components (indicate interactive pages)
23
+ document.querySelectorAll('astro-island').forEach(island => {
24
+ // Islands don't directly indicate routes, but their presence
25
+ // confirms we're on an Astro site. Look for nearby links.
26
+ const nearbyLinks = island.querySelectorAll('a[href]');
27
+ nearbyLinks.forEach(link => {
28
+ const href = link.getAttribute('href');
29
+ if (href && href.startsWith('/')) {
30
+ routes.push({
31
+ path: href,
32
+ name: link.textContent?.trim() || '',
33
+ source: 'framework'
34
+ });
35
+ }
36
+ });
37
+ });
38
+
39
+ // Method 2: data-astro-prefetch links (Astro's View Transitions)
40
+ document.querySelectorAll('a[data-astro-prefetch]').forEach(link => {
41
+ const href = link.getAttribute('href');
42
+ if (href && href.startsWith('/')) {
43
+ if (!routes.some(r => r.path === href)) {
44
+ routes.push({
45
+ path: href,
46
+ name: link.textContent?.trim() || '',
47
+ source: 'framework'
48
+ });
49
+ }
50
+ }
51
+ });
52
+
53
+ // Method 3: Links with data-astro-cid-* (component IDs)
54
+ document.querySelectorAll('a[href]').forEach(link => {
55
+ // Check if link has Astro component ID attribute
56
+ const hasAstroAttr = Array.from(link.attributes).some(
57
+ attr => attr.name.startsWith('data-astro-cid-')
58
+ );
59
+
60
+ if (hasAstroAttr) {
61
+ const href = link.getAttribute('href');
62
+ if (href && href.startsWith('/')) {
63
+ if (!routes.some(r => r.path === href)) {
64
+ routes.push({
65
+ path: href,
66
+ name: link.textContent?.trim() || '',
67
+ source: 'framework'
68
+ });
69
+ }
70
+ }
71
+ }
72
+ });
73
+
74
+ // Method 4: Standard navigation (Astro generates static HTML)
75
+ document.querySelectorAll('nav a, header a, [role="navigation"] a, footer a').forEach(link => {
76
+ const href = link.getAttribute('href');
77
+ if (href && href.startsWith('/')) {
78
+ // Check if inside Astro component
79
+ const isAstroLink = link.closest('astro-island') ||
80
+ Array.from(link.attributes).some(
81
+ attr => attr.name.startsWith('data-astro-')
82
+ );
83
+
84
+ if (!routes.some(r => r.path === href)) {
85
+ routes.push({
86
+ path: href,
87
+ name: link.textContent?.trim() || '',
88
+ source: isAstroLink ? 'framework' : 'link-scrape'
89
+ });
90
+ }
91
+ }
92
+ });
93
+
94
+ // Method 5: Look for Astro's client:* directives in islands
95
+ document.querySelectorAll('[client\\:load], [client\\:idle], [client\\:visible]').forEach(el => {
96
+ // These indicate interactive components
97
+ const links = el.querySelectorAll('a[href^="/"]');
98
+ links.forEach(link => {
99
+ const href = link.getAttribute('href');
100
+ if (href && !routes.some(r => r.path === href)) {
101
+ routes.push({
102
+ path: href,
103
+ name: link.textContent?.trim() || '',
104
+ source: 'framework'
105
+ });
106
+ }
107
+ });
108
+ });
109
+
110
+ return routes;
111
+ });
112
+
113
+ const processedRoutes = rawRoutes.map(route => ({
114
+ ...route,
115
+ name: route.name || this.extractPageName(route.path),
116
+ path: this.normalizeRoute(route.path)
117
+ }));
118
+
119
+ return this.deduplicateRoutes(processedRoutes);
120
+ }
121
+ }
122
+
123
+ export default AstroDiscoverer;
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Base Discoverer - Abstract base class for route discoverers
3
+ *
4
+ * Provides common utilities for route normalization, deduplication,
5
+ * and page name extraction. Framework-specific discoverers extend this.
6
+ *
7
+ * Usage:
8
+ * class NextDiscoverer extends BaseDiscoverer {
9
+ * async discover() { ... }
10
+ * }
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} DiscoveredRoute
15
+ * @property {string} path - Route path (e.g., '/about', '/blog/[slug]')
16
+ * @property {string} name - Human-readable name
17
+ * @property {string} url - Full URL
18
+ * @property {boolean} dynamic - True if contains dynamic segments
19
+ * @property {string} [component] - Component name if available
20
+ * @property {string} source - Discovery source ('framework'|'link-scrape'|'interception')
21
+ */
22
+
23
+ // Dynamic segment patterns
24
+ const DYNAMIC_PATTERNS = [
25
+ /\[[\w-]+\]/, // Next.js [slug]
26
+ /\[\.\.\.([\w-]+)\]/, // Next.js catch-all [...slug]
27
+ /:[\w-]+/, // Vue/React :id
28
+ /\{[\w-]+\}/, // Angular {id}
29
+ /\*[\w-]*/ // Wildcard
30
+ ];
31
+
32
+ /**
33
+ * Abstract base class for route discoverers
34
+ */
35
+ export class BaseDiscoverer {
36
+ /**
37
+ * @param {import('playwright').Page} page - Playwright page object
38
+ * @param {string} baseUrl - Base URL of the site
39
+ */
40
+ constructor(page, baseUrl) {
41
+ this.page = page;
42
+ this.baseUrl = baseUrl;
43
+ this.baseOrigin = new URL(baseUrl).origin;
44
+ }
45
+
46
+ /**
47
+ * Discover routes - must be implemented by subclasses
48
+ * @returns {Promise<DiscoveredRoute[]>}
49
+ */
50
+ async discover() {
51
+ throw new Error('discover() must be implemented by subclass');
52
+ }
53
+
54
+ /**
55
+ * Normalize a route path
56
+ * - Removes trailing slashes (except for root)
57
+ * - Removes query params and hash
58
+ * - Ensures leading slash
59
+ * @param {string} path - Route path to normalize
60
+ * @returns {string} Normalized path
61
+ */
62
+ normalizeRoute(path) {
63
+ if (!path || typeof path !== 'string') return '/';
64
+
65
+ // Handle full URLs
66
+ if (path.startsWith('http')) {
67
+ try {
68
+ path = new URL(path).pathname;
69
+ } catch {
70
+ return '/';
71
+ }
72
+ }
73
+
74
+ // Ensure leading slash
75
+ if (!path.startsWith('/')) {
76
+ path = '/' + path;
77
+ }
78
+
79
+ // Remove query params and hash
80
+ path = path.split('?')[0].split('#')[0];
81
+
82
+ // Remove trailing slash (except for root)
83
+ if (path.length > 1 && path.endsWith('/')) {
84
+ path = path.slice(0, -1);
85
+ }
86
+
87
+ return path;
88
+ }
89
+
90
+ /**
91
+ * Check if a path contains dynamic segments
92
+ * @param {string} path - Route path
93
+ * @returns {boolean}
94
+ */
95
+ isDynamicRoute(path) {
96
+ return DYNAMIC_PATTERNS.some(pattern => pattern.test(path));
97
+ }
98
+
99
+ /**
100
+ * Extract a human-readable page name from a path
101
+ * @param {string} path - Route path
102
+ * @param {string} [componentName] - Optional component name
103
+ * @returns {string}
104
+ */
105
+ extractPageName(path, componentName) {
106
+ // Use component name if available
107
+ if (componentName && componentName !== 'default' && componentName !== 'index') {
108
+ // Convert camelCase/PascalCase to Title Case
109
+ return componentName
110
+ .replace(/([A-Z])/g, ' $1')
111
+ .replace(/^./, s => s.toUpperCase())
112
+ .trim();
113
+ }
114
+
115
+ // Extract from path
116
+ const normalized = this.normalizeRoute(path);
117
+
118
+ if (normalized === '/') return 'Home';
119
+
120
+ // Get last segment
121
+ const segments = normalized.split('/').filter(Boolean);
122
+ if (segments.length === 0) return 'Home';
123
+
124
+ let lastSegment = segments[segments.length - 1];
125
+
126
+ // Handle dynamic segments
127
+ if (this.isDynamicRoute(lastSegment)) {
128
+ lastSegment = lastSegment.replace(/[\[\]:{}*\.]/g, '');
129
+ return `${this.titleCase(lastSegment)} (Dynamic)`;
130
+ }
131
+
132
+ // Convert kebab-case/snake_case to Title Case
133
+ return this.titleCase(lastSegment);
134
+ }
135
+
136
+ /**
137
+ * Convert string to Title Case
138
+ * @param {string} str - Input string
139
+ * @returns {string}
140
+ */
141
+ titleCase(str) {
142
+ return str
143
+ .replace(/[-_]/g, ' ')
144
+ .replace(/\b\w/g, c => c.toUpperCase());
145
+ }
146
+
147
+ /**
148
+ * Deduplicate routes by path, preferring 'framework' source over others
149
+ * @param {DiscoveredRoute[]} routes - Array of routes
150
+ * @returns {DiscoveredRoute[]} Deduplicated routes
151
+ */
152
+ deduplicateRoutes(routes) {
153
+ const seen = new Map();
154
+
155
+ // Source priority: framework > interception > sitemap > link-scrape
156
+ const sourcePriority = {
157
+ 'framework': 4,
158
+ 'interception': 3,
159
+ 'sitemap': 2,
160
+ 'link-scrape': 1
161
+ };
162
+
163
+ for (const route of routes) {
164
+ const normalized = this.normalizeRoute(route.path);
165
+ const existing = seen.get(normalized);
166
+
167
+ const currentPriority = sourcePriority[route.source] || 0;
168
+ const existingPriority = existing ? (sourcePriority[existing.source] || 0) : -1;
169
+
170
+ // Replace if higher priority or if same priority but has a name while existing doesn't
171
+ if (!existing || currentPriority > existingPriority ||
172
+ (currentPriority === existingPriority && route.name && !existing.name)) {
173
+ seen.set(normalized, {
174
+ ...route,
175
+ path: normalized,
176
+ url: this.buildFullUrl(normalized),
177
+ dynamic: this.isDynamicRoute(normalized)
178
+ });
179
+ }
180
+ }
181
+
182
+ return Array.from(seen.values());
183
+ }
184
+
185
+ /**
186
+ * Build full URL from path
187
+ * @param {string} path - Route path
188
+ * @returns {string}
189
+ */
190
+ buildFullUrl(path) {
191
+ const normalized = this.normalizeRoute(path);
192
+ return `${this.baseOrigin}${normalized}`;
193
+ }
194
+
195
+ /**
196
+ * Scrape link elements from navigation areas
197
+ * Common utility for all discoverers as fallback
198
+ * @param {string[]} [selectors] - CSS selectors to search
199
+ * @returns {Promise<DiscoveredRoute[]>}
200
+ */
201
+ async scrapeLinkElements(selectors = ['nav a', 'header a', '[role="navigation"] a']) {
202
+ const selectorString = selectors.join(', ');
203
+ const baseOrigin = this.baseOrigin;
204
+
205
+ const routes = await this.page.evaluate(({ sel, origin }) => {
206
+ const links = [];
207
+ const elements = document.querySelectorAll(sel);
208
+
209
+ elements.forEach(el => {
210
+ const href = el.getAttribute('href');
211
+ if (!href) return;
212
+
213
+ // Skip non-http links
214
+ if (href.startsWith('mailto:') || href.startsWith('tel:') ||
215
+ href.startsWith('javascript:') || href === '#') return;
216
+
217
+ // Skip external links
218
+ try {
219
+ const url = new URL(href, origin);
220
+ if (url.origin !== origin) return;
221
+
222
+ links.push({
223
+ path: url.pathname,
224
+ name: el.textContent?.trim() || '',
225
+ source: 'link-scrape'
226
+ });
227
+ } catch {
228
+ // Invalid URL, skip
229
+ }
230
+ });
231
+
232
+ return links;
233
+ }, { sel: selectorString, origin: baseOrigin });
234
+
235
+ return routes.map(r => ({
236
+ ...r,
237
+ name: r.name || this.extractPageName(r.path)
238
+ }));
239
+ }
240
+ }
241
+
242
+ export default BaseDiscoverer;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Route Discoverers Registry
3
+ *
4
+ * Central registry for framework-specific route discoverers.
5
+ * Uses the framework detector to select the appropriate discoverer.
6
+ */
7
+
8
+ import { NextDiscoverer } from './next-discoverer.js';
9
+ import { NuxtDiscoverer } from './nuxt-discoverer.js';
10
+ import { VueDiscoverer } from './vue-discoverer.js';
11
+ import { ReactDiscoverer } from './react-discoverer.js';
12
+ import { AngularDiscoverer } from './angular-discoverer.js';
13
+ import { SvelteDiscoverer } from './svelte-discoverer.js';
14
+ import { AstroDiscoverer } from './astro-discoverer.js';
15
+ import { UniversalDiscoverer } from './universal-discoverer.js';
16
+
17
+ /**
18
+ * Registry mapping framework names to discoverer classes
19
+ */
20
+ export const DISCOVERER_REGISTRY = {
21
+ 'next': NextDiscoverer,
22
+ 'nuxt': NuxtDiscoverer,
23
+ 'vue': VueDiscoverer,
24
+ 'react': ReactDiscoverer,
25
+ 'angular': AngularDiscoverer,
26
+ 'svelte': SvelteDiscoverer,
27
+ 'astro': AstroDiscoverer,
28
+ 'unknown': UniversalDiscoverer
29
+ };
30
+
31
+ /**
32
+ * Get the appropriate discoverer class for a framework
33
+ * @param {string} framework - Framework name from detector
34
+ * @returns {typeof import('./base-discoverer.js').BaseDiscoverer}
35
+ */
36
+ export function getDiscovererClass(framework) {
37
+ const normalizedFramework = framework?.toLowerCase() || 'unknown';
38
+ return DISCOVERER_REGISTRY[normalizedFramework] || UniversalDiscoverer;
39
+ }
40
+
41
+ /**
42
+ * Create a discoverer instance for a framework
43
+ * @param {string} framework - Framework name
44
+ * @param {import('playwright').Page} page - Playwright page instance
45
+ * @param {string} baseUrl - Base URL of the site
46
+ * @returns {import('./base-discoverer.js').BaseDiscoverer}
47
+ */
48
+ export function createDiscoverer(framework, page, baseUrl) {
49
+ const DiscovererClass = getDiscovererClass(framework);
50
+ return new DiscovererClass(page, baseUrl);
51
+ }
52
+
53
+ /**
54
+ * Discover routes for a given page using framework detection
55
+ * @param {import('playwright').Page} page - Playwright page instance
56
+ * @param {string} baseUrl - Base URL of the site
57
+ * @param {object} [frameworkInfo] - Optional pre-detected framework info
58
+ * @returns {Promise<{routes: import('./base-discoverer.js').DiscoveredRoute[], framework: string, discoverer: string}>}
59
+ */
60
+ export async function discoverRoutes(page, baseUrl, frameworkInfo = null) {
61
+ // Import framework detector if we need to detect
62
+ let detectedFramework = frameworkInfo?.framework || 'unknown';
63
+
64
+ if (!frameworkInfo) {
65
+ try {
66
+ const { detectFramework } = await import('../core/framework-detector.js');
67
+ const info = await detectFramework(page);
68
+ detectedFramework = info.framework;
69
+ } catch {
70
+ // Framework detector not available, use universal
71
+ detectedFramework = 'unknown';
72
+ }
73
+ }
74
+
75
+ const discoverer = createDiscoverer(detectedFramework, page, baseUrl);
76
+ const routes = await discoverer.discover();
77
+
78
+ return {
79
+ routes,
80
+ framework: detectedFramework,
81
+ discoverer: discoverer.constructor.name
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Get list of supported frameworks
87
+ * @returns {string[]}
88
+ */
89
+ export function getSupportedFrameworks() {
90
+ return Object.keys(DISCOVERER_REGISTRY).filter(k => k !== 'unknown');
91
+ }
92
+
93
+ // Export all discoverer classes for direct use
94
+ export {
95
+ NextDiscoverer,
96
+ NuxtDiscoverer,
97
+ VueDiscoverer,
98
+ ReactDiscoverer,
99
+ AngularDiscoverer,
100
+ SvelteDiscoverer,
101
+ AstroDiscoverer,
102
+ UniversalDiscoverer
103
+ };
104
+
105
+ // Re-export base class
106
+ export { BaseDiscoverer } from './base-discoverer.js';