design-clone 1.2.0 → 2.3.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 +32 -39
- package/SKILL.md +69 -45
- package/bin/cli.js +22 -4
- package/bin/commands/clone-site.js +31 -106
- package/bin/commands/help.js +19 -6
- package/bin/commands/init.js +11 -56
- package/bin/commands/uninstall.js +105 -0
- package/bin/commands/update.js +70 -0
- package/bin/commands/verify.js +11 -16
- package/bin/utils/paths.js +28 -0
- package/bin/utils/validate.js +24 -28
- package/bin/utils/version.js +23 -0
- package/docs/code-standards.md +789 -0
- package/docs/codebase-summary.md +556 -0
- package/docs/index.md +74 -0
- package/docs/project-overview-pdr.md +797 -0
- package/docs/system-architecture.md +718 -0
- package/package.json +20 -21
- 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-extractor.js → css/css-extractor.js} +4 -4
- 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/{cookie-handler.js → page-prep/cookie-handler.js} +1 -1
- package/src/core/{lazy-loader.js → page-prep/lazy-loader.js} +44 -46
- package/src/core/{page-readiness.js → page-prep/page-readiness.js} +8 -8
- package/src/core/section/section-cropper-helpers.js +43 -0
- package/src/core/section/section-cropper.js +132 -0
- 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 +177 -0
- package/src/core/tests/test-section-detector.js +55 -0
- 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/angular-discoverer.js +157 -0
- package/src/route-discoverers/astro-discoverer.js +123 -0
- package/src/route-discoverers/base-discoverer-utils.js +137 -0
- package/src/route-discoverers/base-discoverer.js +153 -0
- package/src/route-discoverers/index.js +106 -0
- package/src/route-discoverers/next-discoverer.js +130 -0
- package/src/route-discoverers/nuxt-discoverer.js +138 -0
- package/src/route-discoverers/react-discoverer.js +139 -0
- package/src/route-discoverers/svelte-discoverer.js +109 -0
- package/src/route-discoverers/universal-discoverer.js +227 -0
- package/src/route-discoverers/vue-discoverer.js +118 -0
- 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 +11 -44
- 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 +147 -0
- 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 +122 -0
- 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 +135 -0
- package/src/verification/verify-header-checks.js +104 -0
- package/src/verification/verify-header-helpers.js +156 -0
- package/src/verification/verify-header.js +144 -0
- package/src/verification/verify-layout-report.js +101 -0
- package/src/verification/verify-layout.js +14 -260
- package/src/verification/verify-menu-checks.js +104 -0
- package/src/verification/verify-menu-helpers.js +112 -0
- package/src/verification/verify-menu.js +18 -302
- 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 +142 -0
- package/.env.example +0 -14
- package/docs/basic-clone.md +0 -63
- package/docs/cli-reference.md +0 -118
- package/docs/design-clone-architecture.md +0 -275
- package/docs/pixel-perfect.md +0 -86
- package/docs/troubleshooting.md +0 -169
- package/requirements.txt +0 -5
- package/src/ai/analyze-structure.py +0 -305
- package/src/ai/extract-design-tokens.py +0 -439
- 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/design_tokens.py +0 -183
- package/src/ai/prompts/structure_analysis.py +0 -273
- package/src/core/animation-extractor.js +0 -526
- package/src/core/design-tokens.js +0 -103
- package/src/core/dimension-extractor.js +0 -366
- package/src/core/dimension-output.js +0 -208
- package/src/core/discover-pages.js +0 -314
- package/src/core/extract-assets.js +0 -468
- package/src/core/filter-css.js +0 -499
- package/src/core/html-extractor.js +0 -171
- package/src/core/merge-css.js +0 -407
- package/src/core/multi-page-screenshot.js +0 -377
- package/src/core/rewrite-links.js +0 -226
- package/src/core/screenshot.js +0 -572
- package/src/core/state-capture.js +0 -602
- package/src/core/video-capture.js +0 -540
- 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/utils/puppeteer.js +0 -281
|
@@ -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
|
@@ -1,32 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser abstraction facade for design-clone scripts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* 1. chrome-devtools skill (if installed) - Preferred
|
|
6
|
-
* 2. Standalone puppeteer wrapper - Fallback
|
|
4
|
+
* Uses Playwright wrapper for browser automation.
|
|
7
5
|
*
|
|
8
|
-
* Exports same API
|
|
6
|
+
* Exports same API:
|
|
9
7
|
* - getBrowser(options)
|
|
10
8
|
* - getPage(browser)
|
|
11
9
|
* - closeBrowser()
|
|
12
10
|
* - disconnectBrowser()
|
|
13
|
-
* - parseArgs(argv)
|
|
14
|
-
* - outputJSON(data)
|
|
15
|
-
* - outputError(error)
|
|
16
11
|
*/
|
|
17
12
|
|
|
18
|
-
import fs from 'fs';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
import { fileURLToPath } from 'url';
|
|
21
|
-
|
|
22
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
-
|
|
24
|
-
// Chrome DevTools skill path
|
|
25
|
-
const CHROME_DEVTOOLS_PATH = path.join(
|
|
26
|
-
process.env.HOME,
|
|
27
|
-
'.claude/skills/chrome-devtools/scripts/lib/browser.js'
|
|
28
|
-
);
|
|
29
|
-
|
|
30
13
|
let browserModule = null;
|
|
31
14
|
let providerName = 'unknown';
|
|
32
15
|
|
|
@@ -36,31 +19,14 @@ let providerName = 'unknown';
|
|
|
36
19
|
async function initProvider() {
|
|
37
20
|
if (browserModule) return;
|
|
38
21
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
browserModule = await import(CHROME_DEVTOOLS_PATH);
|
|
43
|
-
providerName = 'chrome-devtools';
|
|
44
|
-
console.error('[browser] Using chrome-devtools skill');
|
|
45
|
-
return;
|
|
46
|
-
} catch (e) {
|
|
47
|
-
console.error('[browser] chrome-devtools found but failed to load:', e.message);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Fall back to standalone puppeteer wrapper
|
|
52
|
-
browserModule = await import('./puppeteer.js');
|
|
53
|
-
providerName = 'standalone';
|
|
54
|
-
console.error('[browser] Using standalone puppeteer wrapper');
|
|
22
|
+
browserModule = await import('./playwright.js');
|
|
23
|
+
providerName = 'playwright';
|
|
24
|
+
console.error('[browser] Using Playwright wrapper');
|
|
55
25
|
}
|
|
56
26
|
|
|
57
|
-
// Import utilities (always use local helpers)
|
|
58
|
-
import { parseArgs, outputJSON, outputError } from './helpers.js';
|
|
59
|
-
export { parseArgs, outputJSON, outputError };
|
|
60
|
-
|
|
61
27
|
/**
|
|
62
28
|
* Get current browser provider name
|
|
63
|
-
* @returns {string} '
|
|
29
|
+
* @returns {string} 'playwright'
|
|
64
30
|
*/
|
|
65
31
|
export function getProviderName() {
|
|
66
32
|
return providerName;
|
|
@@ -79,15 +45,16 @@ export async function getBrowser(options = {}) {
|
|
|
79
45
|
/**
|
|
80
46
|
* Get page from browser
|
|
81
47
|
* @param {Browser} browser - Browser instance
|
|
48
|
+
* @param {Object} [options] - Page options
|
|
82
49
|
* @returns {Promise<Page>} Page instance
|
|
83
50
|
*/
|
|
84
|
-
export async function getPage(browser) {
|
|
51
|
+
export async function getPage(browser, options = {}) {
|
|
85
52
|
await initProvider();
|
|
86
|
-
return browserModule.getPage(browser);
|
|
53
|
+
return browserModule.getPage(browser, options);
|
|
87
54
|
}
|
|
88
55
|
|
|
89
56
|
/**
|
|
90
|
-
* Close browser
|
|
57
|
+
* Close browser
|
|
91
58
|
*/
|
|
92
59
|
export async function closeBrowser() {
|
|
93
60
|
await initProvider();
|
|
@@ -95,7 +62,7 @@ export async function closeBrowser() {
|
|
|
95
62
|
}
|
|
96
63
|
|
|
97
64
|
/**
|
|
98
|
-
* Disconnect from browser
|
|
65
|
+
* Disconnect from browser (alias for close in Playwright)
|
|
99
66
|
*/
|
|
100
67
|
export async function disconnectBrowser() {
|
|
101
68
|
await initProvider();
|
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
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone Playwright browser wrapper for design-clone scripts
|
|
3
|
+
* Provides browser automation with Playwright
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Auto-detects Chrome installation path (macOS, Linux, Windows)
|
|
7
|
+
* - Fast browser launch (no session persistence needed)
|
|
8
|
+
* - Compatible API with previous Puppeteer wrapper
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { VIEWPORTS_HD } from '../shared/viewports.js';
|
|
12
|
+
import { detectChromePath, loadPlaywright } from './playwright-loader.js';
|
|
13
|
+
|
|
14
|
+
/** @type {import('playwright').Browser|null} */
|
|
15
|
+
let browserInstance = null;
|
|
16
|
+
/** @type {import('playwright').Page|null} */
|
|
17
|
+
let pageInstance = null;
|
|
18
|
+
|
|
19
|
+
/** Default viewport dimensions */
|
|
20
|
+
const DEFAULT_VIEWPORT = { width: VIEWPORTS_HD.desktop.width, height: VIEWPORTS_HD.desktop.height };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Launch browser instance
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} options - Browser options
|
|
26
|
+
* @param {boolean} [options.headless=true] - Run in headless mode
|
|
27
|
+
* @param {Object} [options.viewport] - Default viewport dimensions (applied per context)
|
|
28
|
+
* @param {string} [options.executablePath] - Chrome executable path override
|
|
29
|
+
* @param {string[]} [options.args] - Additional Chrome arguments
|
|
30
|
+
* @returns {Promise<Browser>} Playwright browser instance
|
|
31
|
+
* @throws {Error} If Chrome not found and no executablePath provided (playwright-core)
|
|
32
|
+
*/
|
|
33
|
+
export async function getBrowser(options = {}) {
|
|
34
|
+
const pw = await loadPlaywright();
|
|
35
|
+
|
|
36
|
+
// Reuse existing browser if connected
|
|
37
|
+
if (browserInstance && browserInstance.isConnected()) {
|
|
38
|
+
return browserInstance;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Determine executable path for playwright-core
|
|
42
|
+
let executablePath = options.executablePath;
|
|
43
|
+
if (!executablePath) {
|
|
44
|
+
// Check if we're using playwright-core (no bundled browser)
|
|
45
|
+
const isCore = !pw.chromium?.executablePath;
|
|
46
|
+
if (isCore) {
|
|
47
|
+
executablePath = detectChromePath();
|
|
48
|
+
if (!executablePath) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'Chrome not found. Either:\n' +
|
|
51
|
+
'1. Install Google Chrome\n' +
|
|
52
|
+
'2. Use full playwright (npm install playwright)\n' +
|
|
53
|
+
'3. Set executablePath option'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build launch options
|
|
60
|
+
const launchOptions = {
|
|
61
|
+
headless: options.headless !== false,
|
|
62
|
+
args: [
|
|
63
|
+
'--no-sandbox',
|
|
64
|
+
'--disable-setuid-sandbox',
|
|
65
|
+
'--disable-dev-shm-usage',
|
|
66
|
+
...(options.args || [])
|
|
67
|
+
]
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Only set executablePath if needed (playwright-core or override)
|
|
71
|
+
if (executablePath) {
|
|
72
|
+
launchOptions.executablePath = executablePath;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Launch browser
|
|
76
|
+
browserInstance = await pw.chromium.launch(launchOptions);
|
|
77
|
+
console.error('[browser] Launched Playwright browser');
|
|
78
|
+
|
|
79
|
+
return browserInstance;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get current page or create new one
|
|
84
|
+
* Reuses existing page if available
|
|
85
|
+
*
|
|
86
|
+
* @param {import('playwright').Browser} browser - Playwright browser instance
|
|
87
|
+
* @param {Object} [options] - Page options
|
|
88
|
+
* @param {{width: number, height: number}} [options.viewport] - Viewport dimensions
|
|
89
|
+
* @returns {Promise<import('playwright').Page>} Playwright page instance
|
|
90
|
+
* @throws {Error} If browser is null or disconnected
|
|
91
|
+
*/
|
|
92
|
+
export async function getPage(browser, options = {}) {
|
|
93
|
+
if (!browser || !browser.isConnected()) {
|
|
94
|
+
throw new Error('Browser not connected. Call getBrowser() first.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pageInstance && !pageInstance.isClosed()) {
|
|
98
|
+
return pageInstance;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get existing pages or create new context + page
|
|
102
|
+
const contexts = browser.contexts();
|
|
103
|
+
if (contexts.length > 0) {
|
|
104
|
+
const pages = contexts[0].pages();
|
|
105
|
+
if (pages.length > 0) {
|
|
106
|
+
pageInstance = pages[0];
|
|
107
|
+
return pageInstance;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Create new context with default viewport
|
|
112
|
+
const contextOptions = {
|
|
113
|
+
viewport: options.viewport || DEFAULT_VIEWPORT
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const context = await browser.newContext(contextOptions);
|
|
117
|
+
pageInstance = await context.newPage();
|
|
118
|
+
|
|
119
|
+
return pageInstance;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Close browser
|
|
124
|
+
* Use when completely done with browser
|
|
125
|
+
*/
|
|
126
|
+
export async function closeBrowser() {
|
|
127
|
+
if (browserInstance) {
|
|
128
|
+
try {
|
|
129
|
+
await browserInstance.close();
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`[browser] Error closing browser: ${err.message}`);
|
|
132
|
+
}
|
|
133
|
+
browserInstance = null;
|
|
134
|
+
pageInstance = null;
|
|
135
|
+
console.error('[browser] Closed browser');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Disconnect from browser (alias for closeBrowser in Playwright)
|
|
141
|
+
* Playwright doesn't support disconnect without close, so this is an alias
|
|
142
|
+
*/
|
|
143
|
+
export async function disconnectBrowser() {
|
|
144
|
+
// Playwright doesn't have disconnect concept like Puppeteer
|
|
145
|
+
// Just close the browser for API compatibility
|
|
146
|
+
return closeBrowser();
|
|
147
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Report Section Generators
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for generating markdown table and section content
|
|
5
|
+
* extracted from generate-audit-report.js to keep each file under 200 lines.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { generateCSSFixes } from './generate-audit-report-css-fixes.js';
|
|
10
|
+
|
|
11
|
+
// Re-export so callers only need this one module
|
|
12
|
+
export { generateCSSFixes };
|
|
13
|
+
|
|
14
|
+
// Status icons
|
|
15
|
+
export const STATUS_ICONS = {
|
|
16
|
+
pass: '✅',
|
|
17
|
+
warn: '⚠️',
|
|
18
|
+
fail: '❌',
|
|
19
|
+
info: 'ℹ️'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Calculate component status from verification result
|
|
24
|
+
* @param {Object|null} result
|
|
25
|
+
* @returns {{status: string, icon: string, label: string}}
|
|
26
|
+
*/
|
|
27
|
+
export function getComponentStatus(result) {
|
|
28
|
+
if (!result) return { status: 'skip', icon: STATUS_ICONS.info, label: 'Not tested' };
|
|
29
|
+
const { summary } = result;
|
|
30
|
+
if (!summary) return { status: 'skip', icon: STATUS_ICONS.info, label: 'No data' };
|
|
31
|
+
if (summary.failed > 0) return { status: 'fail', icon: STATUS_ICONS.fail, label: `${summary.failed} failed` };
|
|
32
|
+
if (summary.warnings?.length > 0) return { status: 'warn', icon: STATUS_ICONS.warn, label: `${summary.warnings.length} warnings` };
|
|
33
|
+
return { status: 'pass', icon: STATUS_ICONS.pass, label: 'Passed' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generate summary table markdown
|
|
38
|
+
* @param {Object} results - Map of component name to result
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function generateSummaryTable(results) {
|
|
42
|
+
let table = `| Component | Status | Tests | Details |\n|-----------|--------|-------|----------|\n`;
|
|
43
|
+
for (const [component, result] of Object.entries(results)) {
|
|
44
|
+
const status = getComponentStatus(result);
|
|
45
|
+
const tests = result?.summary ? `${result.summary.passed}/${result.summary.totalTests}` : '-';
|
|
46
|
+
const label = component.charAt(0).toUpperCase() + component.slice(1);
|
|
47
|
+
table += `| ${label} | ${status.icon} ${status.label} | ${tests} | ${result?.url || '-'} |\n`;
|
|
48
|
+
}
|
|
49
|
+
return table;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Generate responsive viewport breakdown table
|
|
54
|
+
* @param {Object} results
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function generateViewportTable(results) {
|
|
58
|
+
const viewports = ['mobile', 'tablet', 'desktop'];
|
|
59
|
+
const components = Object.keys(results).filter(c => results[c]?.viewports);
|
|
60
|
+
if (components.length === 0) return '';
|
|
61
|
+
|
|
62
|
+
let table = `| Component | Mobile | Tablet | Desktop |\n|-----------|--------|--------|----------|\n`;
|
|
63
|
+
for (const component of components) {
|
|
64
|
+
const result = results[component];
|
|
65
|
+
const row = [component.charAt(0).toUpperCase() + component.slice(1)];
|
|
66
|
+
for (const vp of viewports) {
|
|
67
|
+
const vpResult = result.viewports?.[vp];
|
|
68
|
+
if (vpResult) {
|
|
69
|
+
const icon = vpResult.failed > 0 ? STATUS_ICONS.fail
|
|
70
|
+
: vpResult.warnings?.length > 0 ? STATUS_ICONS.warn
|
|
71
|
+
: STATUS_ICONS.pass;
|
|
72
|
+
row.push(`${icon} ${vpResult.passed}/${vpResult.tests?.length || 0}`);
|
|
73
|
+
} else {
|
|
74
|
+
row.push('-');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
table += `| ${row.join(' | ')} |\n`;
|
|
78
|
+
}
|
|
79
|
+
return table;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate a markdown section for a single component
|
|
84
|
+
* @param {string} component
|
|
85
|
+
* @param {Object|null} result
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
export function generateComponentSection(component, result) {
|
|
89
|
+
const label = component.charAt(0).toUpperCase() + component.slice(1);
|
|
90
|
+
if (!result) return `### ${label}\n\n${STATUS_ICONS.info} Not tested\n\n---\n\n`;
|
|
91
|
+
|
|
92
|
+
const status = getComponentStatus(result);
|
|
93
|
+
let section = `### ${label} ${status.icon}\n\n`;
|
|
94
|
+
|
|
95
|
+
if (component === 'slider' && result.sliderLibrary) {
|
|
96
|
+
section += `**Library:** ${result.sliderLibrary}\n\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.viewports) {
|
|
100
|
+
for (const [viewport, vpResult] of Object.entries(result.viewports)) {
|
|
101
|
+
const vpLabel = viewport.charAt(0).toUpperCase() + viewport.slice(1);
|
|
102
|
+
section += `#### ${vpLabel} (${vpResult.dimensions?.width}x${vpResult.dimensions?.height})\n\n`;
|
|
103
|
+
|
|
104
|
+
if (vpResult.tests?.length > 0) {
|
|
105
|
+
for (const test of vpResult.tests) {
|
|
106
|
+
const icon = test.passed ? STATUS_ICONS.pass : STATUS_ICONS.fail;
|
|
107
|
+
section += `- ${icon} **${test.name}**`;
|
|
108
|
+
if (test.selector) section += ` - \`${test.selector}\``;
|
|
109
|
+
if (test.count !== undefined) section += ` (${test.count} found)`;
|
|
110
|
+
if (test.note) section += ` - ${test.note}`;
|
|
111
|
+
if (test.error) section += ` - ⚠️ ${test.error}`;
|
|
112
|
+
section += '\n';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (vpResult.warnings?.length > 0) {
|
|
117
|
+
section += '\n**Warnings:**\n';
|
|
118
|
+
for (const warning of vpResult.warnings) {
|
|
119
|
+
section += `- ${STATUS_ICONS.warn} ${warning}\n`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
section += '\n';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (result.screenshots?.length > 0) {
|
|
127
|
+
section += `#### Screenshots\n\n| Viewport | Screenshot |\n|----------|------------|\n`;
|
|
128
|
+
for (const screenshot of result.screenshots) {
|
|
129
|
+
const name = path.basename(screenshot);
|
|
130
|
+
const viewport = name.replace(/^[a-z]+-test-/, '').replace('.png', '');
|
|
131
|
+
section += `| ${viewport} | [${name}](${screenshot}) |\n`;
|
|
132
|
+
}
|
|
133
|
+
section += '\n';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
section += '---\n\n';
|
|
137
|
+
return section;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Generate the full markdown audit report string
|
|
142
|
+
* @param {Object} results
|
|
143
|
+
* @param {string|undefined} url
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
export function generateMarkdownReport(results, url) {
|
|
147
|
+
const timestamp = new Date().toISOString();
|
|
148
|
+
let report = `# Component Audit Report\n\n**Generated:** ${timestamp}\n**URL:** ${url || 'N/A'}\n\n`;
|
|
149
|
+
report += `## Summary\n\n${generateSummaryTable(results)}\n`;
|
|
150
|
+
report += `## Responsive Breakdown\n\n${generateViewportTable(results)}\n`;
|
|
151
|
+
report += `## Component Details\n\n`;
|
|
152
|
+
for (const [component, result] of Object.entries(results)) {
|
|
153
|
+
report += generateComponentSection(component, result);
|
|
154
|
+
}
|
|
155
|
+
report += generateCSSFixes(results);
|
|
156
|
+
report += `---\n\n*Report generated by design-clone verification suite*\n`;
|
|
157
|
+
return report;
|
|
158
|
+
}
|