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.
Files changed (177) hide show
  1. package/README.md +13 -34
  2. package/SKILL.md +69 -45
  3. package/bin/cli.js +22 -4
  4. package/bin/commands/clone-site.js +31 -171
  5. package/bin/commands/help.js +19 -6
  6. package/bin/commands/init.js +9 -86
  7. package/bin/commands/uninstall.js +105 -0
  8. package/bin/commands/update.js +70 -0
  9. package/bin/commands/verify.js +7 -14
  10. package/bin/utils/paths.js +28 -0
  11. package/bin/utils/validate.js +2 -22
  12. package/bin/utils/version.js +23 -0
  13. package/docs/code-standards.md +789 -0
  14. package/docs/codebase-summary.md +533 -286
  15. package/docs/index.md +74 -0
  16. package/docs/project-overview-pdr.md +797 -0
  17. package/docs/system-architecture.md +718 -0
  18. package/package.json +14 -17
  19. package/src/ai/prompts/design-tokens/basic.md +80 -0
  20. package/src/ai/prompts/design-tokens/section-with-css.md +41 -0
  21. package/src/ai/prompts/design-tokens/section.md +48 -0
  22. package/src/ai/prompts/design-tokens/with-css.md +87 -0
  23. package/src/ai/prompts/structure-analysis/basic.md +55 -0
  24. package/src/ai/prompts/structure-analysis/with-context.md +59 -0
  25. package/src/ai/prompts/structure-analysis/with-dimensions.md +63 -0
  26. package/src/ai/prompts/structure-analysis/with-hierarchy.md +73 -0
  27. package/src/ai/prompts/ux-audit/aggregation.md +42 -0
  28. package/src/ai/prompts/ux-audit/desktop.md +92 -0
  29. package/src/ai/prompts/ux-audit/mobile.md +93 -0
  30. package/src/ai/prompts/ux-audit/tablet.md +92 -0
  31. package/src/core/animation/animation-extractor-ast.js +183 -0
  32. package/src/core/animation/animation-extractor-output.js +152 -0
  33. package/src/core/animation/animation-extractor.js +178 -0
  34. package/src/core/animation/state-capture-detection.js +200 -0
  35. package/src/core/animation/state-capture.js +193 -0
  36. package/src/core/capture/browser-context-pool.js +96 -0
  37. package/src/core/capture/multi-page-screenshot-page.js +110 -0
  38. package/src/core/capture/multi-page-screenshot.js +208 -0
  39. package/src/core/capture/screenshot-extraction.js +186 -0
  40. package/src/core/capture/screenshot-helpers.js +175 -0
  41. package/src/core/capture/screenshot-orchestrator.js +174 -0
  42. package/src/core/capture/screenshot-viewport.js +93 -0
  43. package/src/core/capture/screenshot.js +192 -0
  44. package/src/core/content/content-counter-dom.js +191 -0
  45. package/src/core/content/content-counter.js +76 -0
  46. package/src/core/css/breakpoint-detector.js +66 -0
  47. package/src/core/css/chromium-defaults.json +23 -0
  48. package/src/core/css/computed-style-extractor.js +102 -0
  49. package/src/core/css/css-chunker.js +103 -0
  50. package/src/core/css/filter-css-dead-code.js +120 -0
  51. package/src/core/css/filter-css-html-analyzer.js +110 -0
  52. package/src/core/css/filter-css-selector-matcher.js +172 -0
  53. package/src/core/css/filter-css.js +206 -0
  54. package/src/core/css/merge-css-atrule-processor.js +158 -0
  55. package/src/core/css/merge-css-file-io.js +68 -0
  56. package/src/core/css/merge-css.js +148 -0
  57. package/src/core/detection/framework-detector-routing.js +68 -0
  58. package/src/core/detection/framework-detector-signals.js +65 -0
  59. package/src/core/detection/framework-detector.js +198 -0
  60. package/src/core/dimension/dimension-extractor-card-detector.js +82 -0
  61. package/src/core/dimension/dimension-extractor.js +317 -0
  62. package/src/core/dimension/dimension-output-ai-summary.js +111 -0
  63. package/src/core/dimension/dimension-output.js +173 -0
  64. package/src/core/dimension/dom-tree-analyzer-tree-builders.js +95 -0
  65. package/src/core/dimension/dom-tree-analyzer.js +191 -0
  66. package/src/core/discovery/app-state-snapshot-capture.js +195 -0
  67. package/src/core/discovery/app-state-snapshot-utils.js +178 -0
  68. package/src/core/discovery/app-state-snapshot.js +131 -0
  69. package/src/core/discovery/discover-pages-routes.js +84 -0
  70. package/src/core/discovery/discover-pages-utils.js +177 -0
  71. package/src/core/discovery/discover-pages.js +191 -0
  72. package/src/core/html/html-extractor-inline-styler.js +70 -0
  73. package/src/core/html/html-extractor.js +147 -0
  74. package/src/core/html/semantic-enhancer-mappings.js +200 -0
  75. package/src/core/html/semantic-enhancer-page.js +148 -0
  76. package/src/core/html/semantic-enhancer.js +135 -0
  77. package/src/core/links/rewrite-links-css-rewriter.js +53 -0
  78. package/src/core/links/rewrite-links.js +173 -0
  79. package/src/core/media/asset-validator.js +118 -0
  80. package/src/core/media/extract-assets-downloader.js +187 -0
  81. package/src/core/media/extract-assets-page-scraper.js +115 -0
  82. package/src/core/media/extract-assets.js +159 -0
  83. package/src/core/media/video-capture-convert.js +200 -0
  84. package/src/core/media/video-capture.js +201 -0
  85. package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +37 -39
  86. package/src/core/section/section-cropper-helpers.js +43 -0
  87. package/src/core/{section-cropper.js → section/section-cropper.js} +11 -88
  88. package/src/core/section/section-detector-strategies.js +139 -0
  89. package/src/core/section/section-detector-utils.js +100 -0
  90. package/src/core/section/section-detector.js +88 -0
  91. package/src/core/tests/test-section-cropper.js +2 -2
  92. package/src/core/tests/test-section-detector.js +2 -2
  93. package/src/post-process/enhance-assets.js +29 -4
  94. package/src/post-process/fetch-images-unsplash-client.js +123 -0
  95. package/src/post-process/fetch-images.js +60 -263
  96. package/src/post-process/inject-gosnap.js +88 -0
  97. package/src/post-process/inject-icons-svg-replacer.js +76 -0
  98. package/src/post-process/inject-icons.js +47 -200
  99. package/src/route-discoverers/base-discoverer-utils.js +137 -0
  100. package/src/route-discoverers/base-discoverer.js +29 -118
  101. package/src/route-discoverers/index.js +1 -1
  102. package/src/shared/config.js +38 -0
  103. package/src/shared/error-codes.js +31 -0
  104. package/src/shared/viewports.js +46 -0
  105. package/src/utils/browser.js +0 -7
  106. package/src/utils/helpers.js +4 -0
  107. package/src/utils/log.js +12 -0
  108. package/src/utils/playwright-loader.js +76 -0
  109. package/src/utils/playwright.js +3 -69
  110. package/src/utils/progress.js +32 -0
  111. package/src/verification/generate-audit-report-css-fixes.js +52 -0
  112. package/src/verification/generate-audit-report-sections.js +158 -0
  113. package/src/verification/generate-audit-report.js +5 -281
  114. package/src/verification/quality-scorer.js +92 -0
  115. package/src/verification/verify-footer-checks.js +103 -0
  116. package/src/verification/verify-footer-helpers.js +178 -0
  117. package/src/verification/verify-footer.js +23 -381
  118. package/src/verification/verify-header-checks.js +104 -0
  119. package/src/verification/verify-header-helpers.js +156 -0
  120. package/src/verification/verify-header.js +23 -365
  121. package/src/verification/verify-layout-report.js +101 -0
  122. package/src/verification/verify-layout.js +13 -259
  123. package/src/verification/verify-menu-checks.js +104 -0
  124. package/src/verification/verify-menu-helpers.js +112 -0
  125. package/src/verification/verify-menu.js +17 -285
  126. package/src/verification/verify-slider-checks.js +115 -0
  127. package/src/verification/verify-slider-constants.js +65 -0
  128. package/src/verification/verify-slider-helpers.js +164 -0
  129. package/src/verification/verify-slider.js +23 -414
  130. package/.env.example +0 -14
  131. package/docs/basic-clone.md +0 -63
  132. package/docs/cli-reference.md +0 -316
  133. package/docs/design-clone-architecture.md +0 -492
  134. package/docs/pixel-perfect.md +0 -117
  135. package/docs/project-roadmap.md +0 -382
  136. package/docs/troubleshooting.md +0 -170
  137. package/requirements.txt +0 -5
  138. package/src/ai/__pycache__/analyze-structure.cpython-313.pyc +0 -0
  139. package/src/ai/__pycache__/extract-design-tokens.cpython-313.pyc +0 -0
  140. package/src/ai/analyze-structure.py +0 -375
  141. package/src/ai/extract-design-tokens.py +0 -782
  142. package/src/ai/prompts/__init__.py +0 -2
  143. package/src/ai/prompts/__pycache__/__init__.cpython-313.pyc +0 -0
  144. package/src/ai/prompts/__pycache__/design_tokens.cpython-313.pyc +0 -0
  145. package/src/ai/prompts/__pycache__/structure_analysis.cpython-313.pyc +0 -0
  146. package/src/ai/prompts/__pycache__/ux_audit.cpython-313.pyc +0 -0
  147. package/src/ai/prompts/design_tokens.py +0 -316
  148. package/src/ai/prompts/structure_analysis.py +0 -592
  149. package/src/ai/prompts/ux_audit.py +0 -198
  150. package/src/ai/ux-audit.js +0 -596
  151. package/src/core/animation-extractor.js +0 -526
  152. package/src/core/app-state-snapshot.js +0 -511
  153. package/src/core/content-counter.js +0 -342
  154. package/src/core/design-tokens.js +0 -103
  155. package/src/core/dimension-extractor.js +0 -438
  156. package/src/core/dimension-output.js +0 -305
  157. package/src/core/discover-pages.js +0 -542
  158. package/src/core/dom-tree-analyzer.js +0 -298
  159. package/src/core/extract-assets.js +0 -468
  160. package/src/core/filter-css.js +0 -499
  161. package/src/core/framework-detector.js +0 -538
  162. package/src/core/html-extractor.js +0 -212
  163. package/src/core/merge-css.js +0 -407
  164. package/src/core/multi-page-screenshot.js +0 -380
  165. package/src/core/rewrite-links.js +0 -226
  166. package/src/core/screenshot.js +0 -701
  167. package/src/core/section-detector.js +0 -386
  168. package/src/core/semantic-enhancer.js +0 -492
  169. package/src/core/state-capture.js +0 -598
  170. package/src/core/video-capture.js +0 -546
  171. package/src/utils/__init__.py +0 -16
  172. package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  173. package/src/utils/__pycache__/env.cpython-313.pyc +0 -0
  174. package/src/utils/env.py +0 -134
  175. /package/src/core/{css-extractor.js → css/css-extractor.js} +0 -0
  176. /package/src/core/{cookie-handler.js → page-prep/cookie-handler.js} +0 -0
  177. /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
- * - 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
54
+ * Normalize a route path (delegates to utility)
55
+ * @param {string} path
56
+ * @returns {string}
61
57
  */
62
58
  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;
59
+ return normalizeRoute(path);
88
60
  }
89
61
 
90
62
  /**
91
- * Check if a path contains dynamic segments
92
- * @param {string} path - Route 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 DYNAMIC_PATTERNS.some(pattern => pattern.test(path));
68
+ return isDynamicRoute(path);
97
69
  }
98
70
 
99
71
  /**
100
- * Extract a human-readable page name from a path
101
- * @param {string} path - Route path
102
- * @param {string} [componentName] - Optional component name
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
- // 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);
78
+ return extractPageName(path, componentName);
134
79
  }
135
80
 
136
81
  /**
137
- * Convert string to Title Case
138
- * @param {string} str - Input string
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 'framework' source over others
149
- * @param {DiscoveredRoute[]} routes - Array of routes
150
- * @returns {DiscoveredRoute[]} Deduplicated routes
91
+ * Deduplicate routes by path, preferring higher-priority sources
92
+ * @param {DiscoveredRoute[]} routes
93
+ * @returns {DiscoveredRoute[]}
151
94
  */
152
95
  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());
96
+ return deduplicateRoutes(routes, this.baseOrigin);
183
97
  }
184
98
 
185
99
  /**
186
100
  * Build full URL from path
187
- * @param {string} path - Route path
101
+ * @param {string} path
188
102
  * @returns {string}
189
103
  */
190
104
  buildFullUrl(path) {
191
- const normalized = this.normalizeRoute(path);
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] - CSS selectors to search
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'];
@@ -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'
@@ -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);
@@ -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
+ }
@@ -8,82 +8,16 @@
8
8
  * - Compatible API with previous Puppeteer wrapper
9
9
  */
10
10
 
11
- import fs from 'fs';
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: 1920, height: 1080 };
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
+ }