design-clone 1.1.1 → 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 (70) hide show
  1. package/README.md +42 -20
  2. package/SKILL.md +74 -0
  3. package/bin/commands/clone-site.js +75 -10
  4. package/bin/commands/init.js +33 -1
  5. package/bin/commands/verify.js +5 -3
  6. package/bin/utils/validate.js +24 -8
  7. package/docs/cli-reference.md +224 -2
  8. package/docs/codebase-summary.md +309 -0
  9. package/docs/design-clone-architecture.md +290 -45
  10. package/docs/pixel-perfect.md +35 -4
  11. package/docs/project-roadmap.md +382 -0
  12. package/docs/troubleshooting.md +5 -4
  13. package/package.json +12 -6
  14. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  15. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  16. package/src/ai/analyze-structure.py +73 -3
  17. package/src/ai/extract-design-tokens.py +356 -13
  18. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  19. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  20. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  21. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  22. package/src/ai/prompts/design_tokens.py +133 -0
  23. package/src/ai/prompts/structure_analysis.py +329 -10
  24. package/src/ai/prompts/ux_audit.py +198 -0
  25. package/src/ai/ux-audit.js +596 -0
  26. package/src/core/animation-extractor.js +526 -0
  27. package/src/core/app-state-snapshot.js +511 -0
  28. package/src/core/content-counter.js +342 -0
  29. package/src/core/cookie-handler.js +1 -1
  30. package/src/core/css-extractor.js +4 -4
  31. package/src/core/dimension-extractor.js +93 -21
  32. package/src/core/dimension-output.js +103 -6
  33. package/src/core/discover-pages.js +242 -14
  34. package/src/core/dom-tree-analyzer.js +298 -0
  35. package/src/core/extract-assets.js +1 -1
  36. package/src/core/framework-detector.js +538 -0
  37. package/src/core/html-extractor.js +45 -4
  38. package/src/core/lazy-loader.js +7 -7
  39. package/src/core/multi-page-screenshot.js +9 -6
  40. package/src/core/page-readiness.js +8 -8
  41. package/src/core/screenshot.js +311 -7
  42. package/src/core/section-cropper.js +209 -0
  43. package/src/core/section-detector.js +386 -0
  44. package/src/core/semantic-enhancer.js +492 -0
  45. package/src/core/state-capture.js +598 -0
  46. package/src/core/tests/test-section-cropper.js +177 -0
  47. package/src/core/tests/test-section-detector.js +55 -0
  48. package/src/core/video-capture.js +546 -0
  49. package/src/route-discoverers/angular-discoverer.js +157 -0
  50. package/src/route-discoverers/astro-discoverer.js +123 -0
  51. package/src/route-discoverers/base-discoverer.js +242 -0
  52. package/src/route-discoverers/index.js +106 -0
  53. package/src/route-discoverers/next-discoverer.js +130 -0
  54. package/src/route-discoverers/nuxt-discoverer.js +138 -0
  55. package/src/route-discoverers/react-discoverer.js +139 -0
  56. package/src/route-discoverers/svelte-discoverer.js +109 -0
  57. package/src/route-discoverers/universal-discoverer.js +227 -0
  58. package/src/route-discoverers/vue-discoverer.js +118 -0
  59. package/src/utils/__init__.py +1 -1
  60. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  61. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  62. package/src/utils/browser.js +11 -37
  63. package/src/utils/playwright.js +213 -0
  64. package/src/verification/generate-audit-report.js +398 -0
  65. package/src/verification/verify-footer.js +493 -0
  66. package/src/verification/verify-header.js +486 -0
  67. package/src/verification/verify-layout.js +2 -2
  68. package/src/verification/verify-menu.js +4 -20
  69. package/src/verification/verify-slider.js +533 -0
  70. package/src/utils/puppeteer.js +0 -281
@@ -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';
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Next.js Route Discoverer
3
+ *
4
+ * Extracts routes from Next.js applications using:
5
+ * - window.__NEXT_DATA__ (always present)
6
+ * - window.__BUILD_MANIFEST (pages router)
7
+ * - window.__NEXT_LOADED_PAGES__ (loaded pages)
8
+ *
9
+ * Supports both Pages Router and App Router.
10
+ */
11
+
12
+ import { BaseDiscoverer } from './base-discoverer.js';
13
+
14
+ export class NextDiscoverer extends BaseDiscoverer {
15
+ /**
16
+ * Discover routes from a Next.js application
17
+ * @returns {Promise<import('./base-discoverer.js').DiscoveredRoute[]>}
18
+ */
19
+ async discover() {
20
+ const rawRoutes = await this.page.evaluate(() => {
21
+ const routes = [];
22
+
23
+ // Method 1: __NEXT_DATA__ (always present in Next.js)
24
+ if (window.__NEXT_DATA__) {
25
+ const nextData = window.__NEXT_DATA__;
26
+
27
+ // Current page
28
+ if (nextData.page) {
29
+ routes.push({
30
+ path: nextData.page,
31
+ name: 'Current Page',
32
+ source: 'framework',
33
+ component: nextData.page
34
+ });
35
+ }
36
+
37
+ // Dynamic route info from query
38
+ if (nextData.query && Object.keys(nextData.query).length > 0) {
39
+ // The page path with dynamic segments
40
+ }
41
+ }
42
+
43
+ // Method 2: __BUILD_MANIFEST (Pages Router - contains all static routes)
44
+ if (window.__BUILD_MANIFEST && typeof window.__BUILD_MANIFEST === 'object') {
45
+ const manifest = window.__BUILD_MANIFEST;
46
+ const manifestKeys = Object.keys(manifest);
47
+ if (!Array.isArray(manifestKeys)) return routes;
48
+
49
+ const pages = manifestKeys.filter(p =>
50
+ !p.startsWith('/_') && // Skip internal pages
51
+ (!p.includes('[') || p === '/') // Include root and static pages (fixed precedence)
52
+ );
53
+
54
+ pages.forEach(page => {
55
+ if (!routes.some(r => r.path === page)) {
56
+ routes.push({
57
+ path: page,
58
+ source: 'framework',
59
+ component: page
60
+ });
61
+ }
62
+ });
63
+
64
+ // Also get dynamic routes
65
+ const dynamicPages = Object.keys(manifest).filter(p =>
66
+ p.includes('[') && !p.startsWith('/_')
67
+ );
68
+
69
+ dynamicPages.forEach(page => {
70
+ if (!routes.some(r => r.path === page)) {
71
+ routes.push({
72
+ path: page,
73
+ source: 'framework',
74
+ component: page,
75
+ dynamic: true
76
+ });
77
+ }
78
+ });
79
+ }
80
+
81
+ // Method 3: __NEXT_LOADED_PAGES__ (pages that have been loaded)
82
+ if (window.__NEXT_LOADED_PAGES__ && Array.isArray(window.__NEXT_LOADED_PAGES__)) {
83
+ window.__NEXT_LOADED_PAGES__.forEach(page => {
84
+ if (!routes.some(r => r.path === page) && !page.startsWith('/_')) {
85
+ routes.push({
86
+ path: page,
87
+ source: 'framework',
88
+ component: page
89
+ });
90
+ }
91
+ });
92
+ }
93
+
94
+ // Method 4: Next.js Link components in DOM
95
+ document.querySelectorAll('a[href]').forEach(link => {
96
+ const href = link.getAttribute('href');
97
+ if (href && href.startsWith('/') && !href.startsWith('/_')) {
98
+ // Check if it's a Next.js Link by looking for data attributes
99
+ const isNextLink = link.hasAttribute('data-next') ||
100
+ link.closest('[data-next]') ||
101
+ link.hasAttribute('data-nscript');
102
+
103
+ if (isNextLink || link.closest('nav, header, [role="navigation"]')) {
104
+ const text = link.textContent?.trim();
105
+ if (!routes.some(r => r.path === href)) {
106
+ routes.push({
107
+ path: href,
108
+ name: text || '',
109
+ source: isNextLink ? 'framework' : 'link-scrape'
110
+ });
111
+ }
112
+ }
113
+ }
114
+ });
115
+
116
+ return routes;
117
+ });
118
+
119
+ // Process and deduplicate
120
+ const processedRoutes = rawRoutes.map(route => ({
121
+ ...route,
122
+ name: route.name || this.extractPageName(route.path, route.component),
123
+ path: this.normalizeRoute(route.path)
124
+ }));
125
+
126
+ return this.deduplicateRoutes(processedRoutes);
127
+ }
128
+ }
129
+
130
+ export default NextDiscoverer;