design-clone 2.1.0 → 3.0.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.
- package/README.md +13 -34
- package/SKILL.md +69 -45
- package/bin/cli.js +22 -4
- package/bin/commands/clone-site.js +31 -171
- package/bin/commands/help.js +19 -6
- package/bin/commands/init.js +9 -86
- package/bin/commands/uninstall.js +105 -0
- package/bin/commands/update.js +70 -0
- package/bin/commands/verify.js +7 -14
- package/bin/utils/paths.js +28 -0
- package/bin/utils/validate.js +2 -22
- package/bin/utils/version.js +23 -0
- package/docs/code-standards.md +789 -0
- package/docs/codebase-summary.md +533 -286
- package/docs/index.md +74 -0
- package/docs/project-overview-pdr.md +797 -0
- package/docs/system-architecture.md +718 -0
- package/package.json +14 -17
- package/src/ai/prompts/design-tokens/basic.md +80 -0
- package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
- package/src/ai/prompts/design-tokens/section.md +48 -0
- package/src/ai/prompts/design-tokens/with-css.md +87 -0
- package/src/ai/prompts/structure-analysis/basic.md +55 -0
- package/src/ai/prompts/structure-analysis/with-context.md +59 -0
- package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
- package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
- package/src/ai/prompts/ux-audit/aggregation.md +42 -0
- package/src/ai/prompts/ux-audit/desktop.md +92 -0
- package/src/ai/prompts/ux-audit/mobile.md +93 -0
- package/src/ai/prompts/ux-audit/tablet.md +92 -0
- package/src/core/animation/animation-extractor-ast.js +183 -0
- package/src/core/animation/animation-extractor-output.js +152 -0
- package/src/core/animation/animation-extractor.js +178 -0
- package/src/core/animation/state-capture-detection.js +200 -0
- package/src/core/animation/state-capture.js +193 -0
- package/src/core/capture/browser-context-pool.js +96 -0
- package/src/core/capture/multi-page-screenshot-page.js +110 -0
- package/src/core/capture/multi-page-screenshot.js +208 -0
- package/src/core/capture/screenshot-extraction.js +186 -0
- package/src/core/capture/screenshot-helpers.js +175 -0
- package/src/core/capture/screenshot-orchestrator.js +174 -0
- package/src/core/capture/screenshot-viewport.js +93 -0
- package/src/core/capture/screenshot.js +192 -0
- package/src/core/content/content-counter-dom.js +191 -0
- package/src/core/content/content-counter.js +76 -0
- package/src/core/css/breakpoint-detector.js +66 -0
- package/src/core/css/chromium-defaults.json +23 -0
- package/src/core/css/computed-style-extractor.js +102 -0
- package/src/core/css/css-chunker.js +103 -0
- package/src/core/css/filter-css-dead-code.js +120 -0
- package/src/core/css/filter-css-html-analyzer.js +110 -0
- package/src/core/css/filter-css-selector-matcher.js +172 -0
- package/src/core/css/filter-css.js +206 -0
- package/src/core/css/merge-css-atrule-processor.js +158 -0
- package/src/core/css/merge-css-file-io.js +68 -0
- package/src/core/css/merge-css.js +148 -0
- package/src/core/detection/framework-detector-routing.js +68 -0
- package/src/core/detection/framework-detector-signals.js +65 -0
- package/src/core/detection/framework-detector.js +198 -0
- package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
- package/src/core/dimension/dimension-extractor.js +317 -0
- package/src/core/dimension/dimension-output-ai-summary.js +111 -0
- package/src/core/dimension/dimension-output.js +173 -0
- package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
- package/src/core/dimension/dom-tree-analyzer.js +191 -0
- package/src/core/discovery/app-state-snapshot-capture.js +195 -0
- package/src/core/discovery/app-state-snapshot-utils.js +178 -0
- package/src/core/discovery/app-state-snapshot.js +131 -0
- package/src/core/discovery/discover-pages-routes.js +84 -0
- package/src/core/discovery/discover-pages-utils.js +177 -0
- package/src/core/discovery/discover-pages.js +191 -0
- package/src/core/html/html-extractor-inline-styler.js +70 -0
- package/src/core/html/html-extractor.js +147 -0
- package/src/core/html/semantic-enhancer-mappings.js +200 -0
- package/src/core/html/semantic-enhancer-page.js +148 -0
- package/src/core/html/semantic-enhancer.js +135 -0
- package/src/core/links/rewrite-links-css-rewriter.js +53 -0
- package/src/core/links/rewrite-links.js +173 -0
- package/src/core/media/asset-validator.js +118 -0
- package/src/core/media/extract-assets-downloader.js +187 -0
- package/src/core/media/extract-assets-page-scraper.js +115 -0
- package/src/core/media/extract-assets.js +159 -0
- package/src/core/media/video-capture-convert.js +200 -0
- package/src/core/media/video-capture.js +201 -0
- package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
- package/src/core/section/section-cropper-helpers.js +43 -0
- package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
- package/src/core/section/section-detector-strategies.js +139 -0
- package/src/core/section/section-detector-utils.js +100 -0
- package/src/core/section/section-detector.js +88 -0
- package/src/core/tests/test-section-cropper.js +2 -2
- package/src/core/tests/test-section-detector.js +2 -2
- package/src/post-process/enhance-assets.js +29 -4
- package/src/post-process/fetch-images-unsplash-client.js +123 -0
- package/src/post-process/fetch-images.js +60 -263
- package/src/post-process/inject-gosnap.js +88 -0
- package/src/post-process/inject-icons-svg-replacer.js +76 -0
- package/src/post-process/inject-icons.js +47 -200
- package/src/route-discoverers/base-discoverer-utils.js +137 -0
- package/src/route-discoverers/base-discoverer.js +29 -118
- package/src/route-discoverers/index.js +1 -1
- package/src/shared/config.js +38 -0
- package/src/shared/error-codes.js +31 -0
- package/src/shared/viewports.js +46 -0
- package/src/utils/browser.js +0 -7
- package/src/utils/helpers.js +4 -0
- package/src/utils/log.js +12 -0
- package/src/utils/playwright-loader.js +76 -0
- package/src/utils/playwright.js +3 -69
- package/src/utils/progress.js +32 -0
- package/src/verification/generate-audit-report-css-fixes.js +52 -0
- package/src/verification/generate-audit-report-sections.js +158 -0
- package/src/verification/generate-audit-report.js +5 -281
- package/src/verification/quality-scorer.js +92 -0
- package/src/verification/verify-footer-checks.js +103 -0
- package/src/verification/verify-footer-helpers.js +178 -0
- package/src/verification/verify-footer.js +23 -381
- package/src/verification/verify-header-checks.js +104 -0
- package/src/verification/verify-header-helpers.js +156 -0
- package/src/verification/verify-header.js +23 -365
- package/src/verification/verify-layout-report.js +101 -0
- package/src/verification/verify-layout.js +13 -259
- package/src/verification/verify-menu-checks.js +104 -0
- package/src/verification/verify-menu-helpers.js +112 -0
- package/src/verification/verify-menu.js +17 -285
- package/src/verification/verify-slider-checks.js +115 -0
- package/src/verification/verify-slider-constants.js +65 -0
- package/src/verification/verify-slider-helpers.js +164 -0
- package/src/verification/verify-slider.js +23 -414
- package/.env.example +0 -14
- package/docs/basic-clone.md +0 -63
- package/docs/cli-reference.md +0 -316
- package/docs/design-clone-architecture.md +0 -492
- package/docs/pixel-perfect.md +0 -117
- package/docs/project-roadmap.md +0 -382
- package/docs/troubleshooting.md +0 -170
- package/requirements.txt +0 -5
- package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
- package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
- package/src/ai/analyze-structure.py +0 -375
- package/src/ai/extract-design-tokens.py +0 -782
- package/src/ai/prompts/__init__.py +0 -2
- package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
- package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
- package/src/ai/prompts/design_tokens.py +0 -316
- package/src/ai/prompts/structure_analysis.py +0 -592
- package/src/ai/prompts/ux_audit.py +0 -198
- package/src/ai/ux-audit.js +0 -596
- package/src/core/animation-extractor.js +0 -526
- package/src/core/app-state-snapshot.js +0 -511
- package/src/core/content-counter.js +0 -342
- package/src/core/design-tokens.js +0 -103
- package/src/core/dimension-extractor.js +0 -438
- package/src/core/dimension-output.js +0 -305
- package/src/core/discover-pages.js +0 -542
- package/src/core/dom-tree-analyzer.js +0 -298
- package/src/core/extract-assets.js +0 -468
- package/src/core/filter-css.js +0 -499
- package/src/core/framework-detector.js +0 -538
- package/src/core/html-extractor.js +0 -212
- package/src/core/merge-css.js +0 -407
- package/src/core/multi-page-screenshot.js +0 -380
- package/src/core/rewrite-links.js +0 -226
- package/src/core/screenshot.js +0 -701
- package/src/core/section-detector.js +0 -386
- package/src/core/semantic-enhancer.js +0 -492
- package/src/core/state-capture.js +0 -598
- package/src/core/video-capture.js +0 -546
- package/src/utils/__init__.py +0 -16
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
- package/src/utils/env.py +0 -134
- /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
- /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
- /package/src/core/{page-readiness.js → page-prep/page-readiness.js} +0 -0
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
* }
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import {
|
|
14
|
+
normalizeRoute,
|
|
15
|
+
isDynamicRoute,
|
|
16
|
+
titleCase,
|
|
17
|
+
extractPageName,
|
|
18
|
+
deduplicateRoutes
|
|
19
|
+
} from './base-discoverer-utils.js';
|
|
20
|
+
|
|
13
21
|
/**
|
|
14
22
|
* @typedef {Object} DiscoveredRoute
|
|
15
23
|
* @property {string} path - Route path (e.g., '/about', '/blog/[slug]')
|
|
@@ -20,15 +28,6 @@
|
|
|
20
28
|
* @property {string} source - Discovery source ('framework'|'link-scrape'|'interception')
|
|
21
29
|
*/
|
|
22
30
|
|
|
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
31
|
/**
|
|
33
32
|
* Abstract base class for route discoverers
|
|
34
33
|
*/
|
|
@@ -52,150 +51,64 @@ export class BaseDiscoverer {
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
/**
|
|
55
|
-
* Normalize a route path
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* - Ensures leading slash
|
|
59
|
-
* @param {string} path - Route path to normalize
|
|
60
|
-
* @returns {string} Normalized path
|
|
54
|
+
* Normalize a route path (delegates to utility)
|
|
55
|
+
* @param {string} path
|
|
56
|
+
* @returns {string}
|
|
61
57
|
*/
|
|
62
58
|
normalizeRoute(path) {
|
|
63
|
-
|
|
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;
|
|
59
|
+
return normalizeRoute(path);
|
|
88
60
|
}
|
|
89
61
|
|
|
90
62
|
/**
|
|
91
|
-
* Check if a path contains dynamic segments
|
|
92
|
-
* @param {string} path
|
|
63
|
+
* Check if a path contains dynamic segments (delegates to utility)
|
|
64
|
+
* @param {string} path
|
|
93
65
|
* @returns {boolean}
|
|
94
66
|
*/
|
|
95
67
|
isDynamicRoute(path) {
|
|
96
|
-
return
|
|
68
|
+
return isDynamicRoute(path);
|
|
97
69
|
}
|
|
98
70
|
|
|
99
71
|
/**
|
|
100
|
-
* Extract a human-readable page name from a path
|
|
101
|
-
* @param {string} path
|
|
102
|
-
* @param {string} [componentName]
|
|
72
|
+
* Extract a human-readable page name from a path (delegates to utility)
|
|
73
|
+
* @param {string} path
|
|
74
|
+
* @param {string} [componentName]
|
|
103
75
|
* @returns {string}
|
|
104
76
|
*/
|
|
105
77
|
extractPageName(path, componentName) {
|
|
106
|
-
|
|
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);
|
|
78
|
+
return extractPageName(path, componentName);
|
|
134
79
|
}
|
|
135
80
|
|
|
136
81
|
/**
|
|
137
|
-
* Convert string to Title Case
|
|
138
|
-
* @param {string} str
|
|
82
|
+
* Convert string to Title Case (delegates to utility)
|
|
83
|
+
* @param {string} str
|
|
139
84
|
* @returns {string}
|
|
140
85
|
*/
|
|
141
86
|
titleCase(str) {
|
|
142
|
-
return str
|
|
143
|
-
.replace(/[-_]/g, ' ')
|
|
144
|
-
.replace(/\b\w/g, c => c.toUpperCase());
|
|
87
|
+
return titleCase(str);
|
|
145
88
|
}
|
|
146
89
|
|
|
147
90
|
/**
|
|
148
|
-
* Deduplicate routes by path, preferring
|
|
149
|
-
* @param {DiscoveredRoute[]} routes
|
|
150
|
-
* @returns {DiscoveredRoute[]}
|
|
91
|
+
* Deduplicate routes by path, preferring higher-priority sources
|
|
92
|
+
* @param {DiscoveredRoute[]} routes
|
|
93
|
+
* @returns {DiscoveredRoute[]}
|
|
151
94
|
*/
|
|
152
95
|
deduplicateRoutes(routes) {
|
|
153
|
-
|
|
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());
|
|
96
|
+
return deduplicateRoutes(routes, this.baseOrigin);
|
|
183
97
|
}
|
|
184
98
|
|
|
185
99
|
/**
|
|
186
100
|
* Build full URL from path
|
|
187
|
-
* @param {string} path
|
|
101
|
+
* @param {string} path
|
|
188
102
|
* @returns {string}
|
|
189
103
|
*/
|
|
190
104
|
buildFullUrl(path) {
|
|
191
|
-
|
|
192
|
-
return `${this.baseOrigin}${normalized}`;
|
|
105
|
+
return `${this.baseOrigin}${normalizeRoute(path)}`;
|
|
193
106
|
}
|
|
194
107
|
|
|
195
108
|
/**
|
|
196
109
|
* Scrape link elements from navigation areas
|
|
197
110
|
* Common utility for all discoverers as fallback
|
|
198
|
-
* @param {string[]} [selectors]
|
|
111
|
+
* @param {string[]} [selectors]
|
|
199
112
|
* @returns {Promise<DiscoveredRoute[]>}
|
|
200
113
|
*/
|
|
201
114
|
async scrapeLinkElements(selectors = ['nav a', 'header a', '[role="navigation"] a']) {
|
|
@@ -210,11 +123,9 @@ export class BaseDiscoverer {
|
|
|
210
123
|
const href = el.getAttribute('href');
|
|
211
124
|
if (!href) return;
|
|
212
125
|
|
|
213
|
-
// Skip non-http links
|
|
214
126
|
if (href.startsWith('mailto:') || href.startsWith('tel:') ||
|
|
215
127
|
href.startsWith('javascript:') || href === '#') return;
|
|
216
128
|
|
|
217
|
-
// Skip external links
|
|
218
129
|
try {
|
|
219
130
|
const url = new URL(href, origin);
|
|
220
131
|
if (url.origin !== origin) return;
|
|
@@ -63,7 +63,7 @@ export async function discoverRoutes(page, baseUrl, frameworkInfo = null) {
|
|
|
63
63
|
|
|
64
64
|
if (!frameworkInfo) {
|
|
65
65
|
try {
|
|
66
|
-
const { detectFramework } = await import('../core/framework-detector.js');
|
|
66
|
+
const { detectFramework } = await import('../core/detection/framework-detector.js');
|
|
67
67
|
const info = await detectFramework(page);
|
|
68
68
|
detectedFramework = info.framework;
|
|
69
69
|
} catch {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized configuration constants for design-clone
|
|
3
|
+
* Single source of truth for timeouts, size limits, and CDN URLs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Browser timing constants (ms) */
|
|
7
|
+
export const TIMING = {
|
|
8
|
+
VIEWPORT_SETTLE_DELAY: 1500,
|
|
9
|
+
NETWORK_IDLE_TIMEOUT: 8000,
|
|
10
|
+
POST_NAVIGATION_DELAY: 3000,
|
|
11
|
+
POST_RESIZE_DELAY: 2000,
|
|
12
|
+
VERIFICATION_DELAY: 500,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Size limits (bytes) */
|
|
16
|
+
export const SIZE_LIMITS = {
|
|
17
|
+
MAX_CSS_INPUT: 50 * 1024 * 1024, // 50MB (supports large enterprise CSS)
|
|
18
|
+
MAX_STATE: 1024 * 1024, // 1MB
|
|
19
|
+
CSS_CHUNK_THRESHOLD: 2 * 1024 * 1024, // 2MB: use chunked processing above this
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Browser context pool defaults */
|
|
23
|
+
export const POOL = {
|
|
24
|
+
MAX_BROWSER_CONTEXTS: 3,
|
|
25
|
+
MIN_FREE_MEMORY_MB: 500,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** CDN URLs */
|
|
29
|
+
export const CDN = {
|
|
30
|
+
FONT_AWESOME_VERSION: '6.5.1',
|
|
31
|
+
FONT_AWESOME_CSS: 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css',
|
|
32
|
+
GOSNAP_WIDGET_VERSION: '1.0.1',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Layout constants */
|
|
36
|
+
export const LAYOUT = {
|
|
37
|
+
SIDEBAR_MAX_WIDTH: 400,
|
|
38
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured error catalog for design-clone.
|
|
3
|
+
* Descriptive codes for easy parsing by LLM agents.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const ERROR_CODES = {
|
|
7
|
+
CSS_SIZE_EXCEEDED: { message: 'CSS file exceeds size limit', suggestion: 'Split CSS or increase SIZE_LIMITS.MAX_CSS_INPUT in config.js' },
|
|
8
|
+
CSS_PARSE_FAILED: { message: 'CSS parse failed', suggestion: 'Check for syntax errors in source CSS. Try --verbose for details' },
|
|
9
|
+
CSS_CORS_BLOCKED: { message: 'Stylesheet blocked by CORS', suggestion: 'Site restricts cross-origin CSS access. Inline styles still captured' },
|
|
10
|
+
HTML_EXTRACTION_FAILED: { message: 'HTML extraction failed', suggestion: 'Page may use heavy JS rendering. Try increasing --scroll-delay' },
|
|
11
|
+
ASSET_DOWNLOAD_FAILED: { message: 'Asset download failed', suggestion: 'Check network connectivity. CORS or auth may block downloads' },
|
|
12
|
+
BROWSER_LAUNCH_FAILED: { message: 'Browser launch failed', suggestion: 'Run: npx playwright install chromium' },
|
|
13
|
+
NAV_TIMEOUT: { message: 'Page navigation timeout', suggestion: 'Site may be slow. Try increasing timeout or check URL' },
|
|
14
|
+
FILE_IO_FAILED: { message: 'File read/write failed', suggestion: 'Check file permissions and disk space' },
|
|
15
|
+
DISCOVERY_FAILED: { message: 'Page discovery failed', suggestion: 'Site may block bots. Try with --no-spa-detect' },
|
|
16
|
+
SCREENSHOT_FAILED: { message: 'Screenshot capture failed', suggestion: 'Page may have infinite scroll. Try --full-page false' },
|
|
17
|
+
INVALID_ARGS: { message: 'Invalid arguments', suggestion: 'Run: design-clone help' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class DesignCloneError extends Error {
|
|
21
|
+
constructor(code, context = {}) {
|
|
22
|
+
const def = ERROR_CODES[code] || { message: 'Unknown error', suggestion: '' };
|
|
23
|
+
super(def.message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.suggestion = def.suggestion;
|
|
26
|
+
this.context = context;
|
|
27
|
+
this.name = 'DesignCloneError';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createError(code, context) { return new DesignCloneError(code, context); }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared viewport configurations for Design Clone
|
|
3
|
+
*
|
|
4
|
+
* Two viewport sets are provided:
|
|
5
|
+
* - VIEWPORTS: Standard capture viewports (1440px desktop)
|
|
6
|
+
* - VIEWPORTS_HD: High-resolution verification viewports (1920px desktop)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Standard viewport configurations for multi-device capture
|
|
11
|
+
* Used by: screenshot.js, dimension-output.js
|
|
12
|
+
* @type {Object.<string, {width: number, height: number, deviceScaleFactor: number}>}
|
|
13
|
+
*/
|
|
14
|
+
export const VIEWPORTS = {
|
|
15
|
+
desktop: { width: 1440, height: 900, deviceScaleFactor: 1 },
|
|
16
|
+
tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
|
|
17
|
+
mobile: { width: 375, height: 812, deviceScaleFactor: 2 }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* High-resolution viewport configurations for verification
|
|
22
|
+
* Used by: verify-menu.js, verify-layout.js, verify-header.js, verify-footer.js, verify-slider.js
|
|
23
|
+
* @type {Object.<string, {width: number, height: number, deviceScaleFactor: number}>}
|
|
24
|
+
*/
|
|
25
|
+
export const VIEWPORTS_HD = {
|
|
26
|
+
desktop: { width: 1920, height: 1080, deviceScaleFactor: 1 },
|
|
27
|
+
tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
|
|
28
|
+
mobile: { width: 375, height: 812, deviceScaleFactor: 2 }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* UX Audit viewport configurations (no deviceScaleFactor)
|
|
33
|
+
* Used by: UX audit prompt templates
|
|
34
|
+
* @type {Object.<string, {width: number, height: number}>}
|
|
35
|
+
*/
|
|
36
|
+
export const VIEWPORTS_UX = {
|
|
37
|
+
desktop: { width: 1920, height: 1080 },
|
|
38
|
+
tablet: { width: 768, height: 1024 },
|
|
39
|
+
mobile: { width: 375, height: 812 }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Viewport names array for iteration
|
|
44
|
+
* @type {string[]}
|
|
45
|
+
*/
|
|
46
|
+
export const VIEWPORT_NAMES = ['desktop', 'tablet', 'mobile'];
|
package/src/utils/browser.js
CHANGED
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
* - getPage(browser)
|
|
9
9
|
* - closeBrowser()
|
|
10
10
|
* - disconnectBrowser()
|
|
11
|
-
* - parseArgs(argv)
|
|
12
|
-
* - outputJSON(data)
|
|
13
|
-
* - outputError(error)
|
|
14
11
|
*/
|
|
15
12
|
|
|
16
13
|
let browserModule = null;
|
|
@@ -27,10 +24,6 @@ async function initProvider() {
|
|
|
27
24
|
console.error('[browser] Using Playwright wrapper');
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
// Import utilities (always use local helpers)
|
|
31
|
-
import { parseArgs, outputJSON, outputError } from './helpers.js';
|
|
32
|
-
export { parseArgs, outputJSON, outputError };
|
|
33
|
-
|
|
34
27
|
/**
|
|
35
28
|
* Get current browser provider name
|
|
36
29
|
* @returns {string} 'playwright'
|
package/src/utils/helpers.js
CHANGED
|
@@ -61,10 +61,14 @@ export function outputJSON(data) {
|
|
|
61
61
|
export function outputError(error) {
|
|
62
62
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
63
63
|
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
64
|
+
const isDesignCloneError = error?.name === 'DesignCloneError';
|
|
64
65
|
|
|
65
66
|
console.error(JSON.stringify({
|
|
66
67
|
success: false,
|
|
67
68
|
error: errorMessage,
|
|
69
|
+
code: isDesignCloneError ? error.code : undefined,
|
|
70
|
+
suggestion: isDesignCloneError ? error.suggestion : undefined,
|
|
71
|
+
context: isDesignCloneError ? error.context : undefined,
|
|
68
72
|
stack: process.env.DEBUG ? errorStack : undefined
|
|
69
73
|
}, null, 2));
|
|
70
74
|
process.exit(1);
|
package/src/utils/log.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized TTY-aware logging for CLI output.
|
|
3
|
+
* Logs to stderr only when attached to a terminal.
|
|
4
|
+
* Keeps stdout clean for JSON output.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const isTTY = process.stderr.isTTY;
|
|
8
|
+
|
|
9
|
+
export function logInfo(msg) { if (isTTY) console.error(`[INFO] ${msg}`); }
|
|
10
|
+
export function logWarn(msg) { if (isTTY) console.error(`[WARN] ${msg}`); }
|
|
11
|
+
export function logError(msg) { if (isTTY) console.error(`[ERROR] ${msg}`); }
|
|
12
|
+
export { isTTY };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright Loader Helpers
|
|
3
|
+
*
|
|
4
|
+
* Chrome path detection and playwright module loading utilities.
|
|
5
|
+
* Extracted from playwright.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect Chrome executable path by platform
|
|
12
|
+
* Used for playwright-core fallback when full playwright is not installed
|
|
13
|
+
* @returns {string|null} Chrome path or null if not found
|
|
14
|
+
*/
|
|
15
|
+
export function detectChromePath() {
|
|
16
|
+
const platform = process.platform;
|
|
17
|
+
|
|
18
|
+
const paths = {
|
|
19
|
+
darwin: [
|
|
20
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
21
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
22
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
|
|
23
|
+
],
|
|
24
|
+
linux: [
|
|
25
|
+
'/usr/bin/google-chrome',
|
|
26
|
+
'/usr/bin/google-chrome-stable',
|
|
27
|
+
'/usr/bin/chromium',
|
|
28
|
+
'/usr/bin/chromium-browser',
|
|
29
|
+
'/snap/bin/chromium'
|
|
30
|
+
],
|
|
31
|
+
win32: [
|
|
32
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
33
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
34
|
+
...(process.env.LOCALAPPDATA
|
|
35
|
+
? [`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`]
|
|
36
|
+
: [])
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const candidates = paths[platform] || [];
|
|
41
|
+
for (const chromePath of candidates) {
|
|
42
|
+
if (fs.existsSync(chromePath)) {
|
|
43
|
+
return chromePath;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @type {typeof import('playwright')|null} */
|
|
51
|
+
let playwright = null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load playwright module (try playwright first, then playwright-core)
|
|
55
|
+
* @returns {Promise<Object>} Playwright module with chromium browser type
|
|
56
|
+
* @throws {Error} If neither playwright nor playwright-core is installed
|
|
57
|
+
*/
|
|
58
|
+
export async function loadPlaywright() {
|
|
59
|
+
if (playwright) return playwright;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
playwright = await import('playwright');
|
|
63
|
+
return playwright;
|
|
64
|
+
} catch (e1) {
|
|
65
|
+
try {
|
|
66
|
+
playwright = await import('playwright-core');
|
|
67
|
+
return playwright;
|
|
68
|
+
} catch (e2) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'Playwright not found. Install with: npm install playwright\n' +
|
|
71
|
+
'Or for smaller install: npm install playwright-core\n' +
|
|
72
|
+
`Details: playwright: ${e1.message}, playwright-core: ${e2.message}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/utils/playwright.js
CHANGED
|
@@ -8,82 +8,16 @@
|
|
|
8
8
|
* - Compatible API with previous Puppeteer wrapper
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import
|
|
11
|
+
import { VIEWPORTS_HD } from '../shared/viewports.js';
|
|
12
|
+
import { detectChromePath, loadPlaywright } from './playwright-loader.js';
|
|
12
13
|
|
|
13
14
|
/** @type {import('playwright').Browser|null} */
|
|
14
15
|
let browserInstance = null;
|
|
15
16
|
/** @type {import('playwright').Page|null} */
|
|
16
17
|
let pageInstance = null;
|
|
17
|
-
/** @type {typeof import('playwright')|null} */
|
|
18
|
-
let playwright = null;
|
|
19
18
|
|
|
20
19
|
/** Default viewport dimensions */
|
|
21
|
-
const DEFAULT_VIEWPORT = { width:
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Detect Chrome executable path by platform
|
|
25
|
-
* Used for playwright-core fallback when full playwright is not installed
|
|
26
|
-
* @returns {string|null} Chrome path or null if not found
|
|
27
|
-
*/
|
|
28
|
-
function detectChromePath() {
|
|
29
|
-
const platform = process.platform;
|
|
30
|
-
|
|
31
|
-
const paths = {
|
|
32
|
-
darwin: [
|
|
33
|
-
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
34
|
-
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
35
|
-
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary'
|
|
36
|
-
],
|
|
37
|
-
linux: [
|
|
38
|
-
'/usr/bin/google-chrome',
|
|
39
|
-
'/usr/bin/google-chrome-stable',
|
|
40
|
-
'/usr/bin/chromium',
|
|
41
|
-
'/usr/bin/chromium-browser',
|
|
42
|
-
'/snap/bin/chromium'
|
|
43
|
-
],
|
|
44
|
-
win32: [
|
|
45
|
-
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
46
|
-
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
47
|
-
...(process.env.LOCALAPPDATA ? [`${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`] : [])
|
|
48
|
-
]
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const candidates = paths[platform] || [];
|
|
52
|
-
for (const chromePath of candidates) {
|
|
53
|
-
if (fs.existsSync(chromePath)) {
|
|
54
|
-
return chromePath;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Load playwright module (try playwright first, then playwright-core)
|
|
63
|
-
* @returns {Promise<Object>} Playwright module with chromium browser type
|
|
64
|
-
* @throws {Error} If neither playwright nor playwright-core is installed
|
|
65
|
-
*/
|
|
66
|
-
async function loadPlaywright() {
|
|
67
|
-
if (playwright) return playwright;
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
// Try full playwright first (includes bundled browsers)
|
|
71
|
-
playwright = await import('playwright');
|
|
72
|
-
return playwright;
|
|
73
|
-
} catch (e1) {
|
|
74
|
-
try {
|
|
75
|
-
// Fall back to playwright-core (requires Chrome)
|
|
76
|
-
playwright = await import('playwright-core');
|
|
77
|
-
return playwright;
|
|
78
|
-
} catch (e2) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
'Playwright not found. Install with: npm install playwright\n' +
|
|
81
|
-
'Or for smaller install: npm install playwright-core\n' +
|
|
82
|
-
`Details: playwright: ${e1.message}, playwright-core: ${e2.message}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
20
|
+
const DEFAULT_VIEWPORT = { width: VIEWPORTS_HD.desktop.width, height: VIEWPORTS_HD.desktop.height };
|
|
87
21
|
|
|
88
22
|
/**
|
|
89
23
|
* Launch browser instance
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTY-aware progress reporting for extraction/capture pipelines.
|
|
3
|
+
* Writes to stderr only when attached to a terminal.
|
|
4
|
+
* Keeps stdout clean for JSON output.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isTTY } from './log.js';
|
|
8
|
+
|
|
9
|
+
export class ProgressReporter {
|
|
10
|
+
#current = 0;
|
|
11
|
+
#total = 0;
|
|
12
|
+
#label = '';
|
|
13
|
+
|
|
14
|
+
start(totalSteps, label = '') {
|
|
15
|
+
this.#total = totalSteps;
|
|
16
|
+
this.#current = 0;
|
|
17
|
+
this.#label = label;
|
|
18
|
+
if (isTTY) process.stderr.write(`[0/${totalSteps}] ${label}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
step(label, details = '') {
|
|
22
|
+
this.#current++;
|
|
23
|
+
const detailStr = details ? ` (${details})` : '';
|
|
24
|
+
if (isTTY) process.stderr.write(`[${this.#current}/${this.#total}] ${label}${detailStr}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
complete(summary = '') {
|
|
28
|
+
if (isTTY) process.stderr.write(`[done] ${summary}\n`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createProgress() { return new ProgressReporter(); }
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Report CSS Fix Suggestions
|
|
3
|
+
*
|
|
4
|
+
* Analyzes verification results and generates CSS fix suggestions.
|
|
5
|
+
* Extracted from generate-audit-report-sections.js to keep files under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate CSS fixes section markdown from all component results
|
|
10
|
+
* @param {Object} results - Map of component name to verification result
|
|
11
|
+
* @returns {string} Markdown section or empty string if no fixes
|
|
12
|
+
*/
|
|
13
|
+
export function generateCSSFixes(results) {
|
|
14
|
+
const fixes = [];
|
|
15
|
+
|
|
16
|
+
for (const [component, result] of Object.entries(results)) {
|
|
17
|
+
if (!result?.viewports) continue;
|
|
18
|
+
|
|
19
|
+
for (const [viewport, vpResult] of Object.entries(result.viewports)) {
|
|
20
|
+
if (component === 'footer') {
|
|
21
|
+
const positionTest = vpResult.tests?.find(t => t.name === 'Footer at page bottom');
|
|
22
|
+
if (positionTest && !positionTest.passed) {
|
|
23
|
+
fixes.push({
|
|
24
|
+
component, viewport,
|
|
25
|
+
issue: 'Footer not at page bottom',
|
|
26
|
+
suggestion: `/* Ensure footer sticks to bottom */\nfooter {\n margin-top: auto;\n}\nbody {\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}`
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (vpResult.warnings) {
|
|
32
|
+
for (const warning of vpResult.warnings) {
|
|
33
|
+
if (warning.includes('z-index')) {
|
|
34
|
+
fixes.push({
|
|
35
|
+
component, viewport,
|
|
36
|
+
issue: warning,
|
|
37
|
+
suggestion: `/* Increase header z-index */\nheader, .header, [role="banner"] {\n z-index: 1000;\n}`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (fixes.length === 0) return '';
|
|
46
|
+
|
|
47
|
+
let section = `## Suggested CSS Fixes\n\n`;
|
|
48
|
+
for (const fix of fixes) {
|
|
49
|
+
section += `### ${fix.component} (${fix.viewport})\n\n**Issue:** ${fix.issue}\n\n\`\`\`css\n${fix.suggestion}\n\`\`\`\n\n`;
|
|
50
|
+
}
|
|
51
|
+
return section;
|
|
52
|
+
}
|