design-clone 2.1.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 +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
package/src/core/screenshot.js
DELETED
|
@@ -1,701 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Multi-viewport screenshot capture for design cloning
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node screenshot.js --url https://example.com --output ./analysis
|
|
7
|
-
*
|
|
8
|
-
* Options:
|
|
9
|
-
* --url Target website URL (required)
|
|
10
|
-
* --output Output directory for screenshots (required)
|
|
11
|
-
* --viewports Comma-separated viewport names: desktop,tablet,mobile (default: all)
|
|
12
|
-
* --full-page Capture full page height (default: true)
|
|
13
|
-
* --max-size Max file size in MB before compression (default: 5)
|
|
14
|
-
* --headless Run in headless mode (default: false)
|
|
15
|
-
* --scroll-delay Pause time in ms between scroll steps (default: 1500)
|
|
16
|
-
* --close Close browser after capture (default: false)
|
|
17
|
-
* --extract-html Extract cleaned HTML (default: false)
|
|
18
|
-
* --extract-css Extract all CSS from page (default: false)
|
|
19
|
-
* --filter-unused Filter CSS to remove unused selectors (default: true)
|
|
20
|
-
* --capture-hover Capture hover state screenshots and CSS (default: false)
|
|
21
|
-
* --video Record scroll preview video (default: false)
|
|
22
|
-
* --video-format Video format: webm, mp4, gif (default: webm)
|
|
23
|
-
* --video-duration Video duration in ms (default: 12000)
|
|
24
|
-
* --section-mode Enable section-based capture for AI analysis (default: false)
|
|
25
|
-
* --no-semantic Disable WordPress semantic HTML enhancement (default: false)
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import path from 'path';
|
|
29
|
-
import fs from 'fs/promises';
|
|
30
|
-
|
|
31
|
-
// Import modules
|
|
32
|
-
import { filterCssFile } from './filter-css.js';
|
|
33
|
-
import { getBrowser, getPage, closeBrowser, disconnectBrowser, parseArgs, outputJSON, outputError } from '../utils/browser.js';
|
|
34
|
-
|
|
35
|
-
// Import extracted modules
|
|
36
|
-
import { waitForDomStable, waitForFontsLoaded, waitForStylesStable, waitForPageReady } from './page-readiness.js';
|
|
37
|
-
import { dismissCookieBanner } from './cookie-handler.js';
|
|
38
|
-
import { forceLazyImages, forceAnimatedElementsVisible, triggerLazyLoad, waitForAllImages, LAZY_LOAD_MAX_ITERATIONS } from './lazy-loader.js';
|
|
39
|
-
import { extractCleanHtml, extractAndEnhanceHtml, JS_FRAMEWORK_PATTERNS, MAX_HTML_SIZE } from './html-extractor.js';
|
|
40
|
-
import { extractContentCounts, generateContentSummary } from './content-counter.js';
|
|
41
|
-
import { extractAllCss, MAX_CSS_SIZE } from './css-extractor.js';
|
|
42
|
-
import { extractComponentDimensions } from './dimension-extractor.js';
|
|
43
|
-
import { buildDimensionsOutput, generateAISummary } from './dimension-output.js';
|
|
44
|
-
import { extractDOMHierarchy } from './dom-tree-analyzer.js';
|
|
45
|
-
import { extractAnimations, generateAnimationsCss, generateAnimationTokens } from './animation-extractor.js';
|
|
46
|
-
import { captureAllHoverStates, generateHoverCss } from './state-capture.js';
|
|
47
|
-
import { captureVideo, hasFfmpeg, FFMPEG_REQUIRED_FORMATS } from './video-capture.js';
|
|
48
|
-
|
|
49
|
-
// Try to import Sharp for compression
|
|
50
|
-
let sharp = null;
|
|
51
|
-
try {
|
|
52
|
-
sharp = (await import('sharp')).default;
|
|
53
|
-
} catch {
|
|
54
|
-
// Sharp not available
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Constants
|
|
58
|
-
const VIEWPORTS = {
|
|
59
|
-
desktop: { width: 1440, height: 900, deviceScaleFactor: 1 },
|
|
60
|
-
tablet: { width: 768, height: 1024, deviceScaleFactor: 1 },
|
|
61
|
-
mobile: { width: 375, height: 812, deviceScaleFactor: 2 }
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const VIEWPORT_SETTLE_DELAY = 1500;
|
|
65
|
-
const NETWORK_IDLE_TIMEOUT = 8000;
|
|
66
|
-
const DEFAULT_SCROLL_DELAY = 1500;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Compress image if it exceeds max size
|
|
70
|
-
*/
|
|
71
|
-
async function compressIfNeeded(filePath, maxSizeMB = 5) {
|
|
72
|
-
const stats = await fs.stat(filePath);
|
|
73
|
-
const originalSize = stats.size;
|
|
74
|
-
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
75
|
-
|
|
76
|
-
if (originalSize <= maxBytes || !sharp) {
|
|
77
|
-
return { compressed: false, originalSize, finalSize: originalSize };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
const buffer = await fs.readFile(filePath);
|
|
82
|
-
const meta = await sharp(buffer).metadata();
|
|
83
|
-
|
|
84
|
-
const newWidth = Math.round(meta.width * 0.85);
|
|
85
|
-
let output = await sharp(buffer)
|
|
86
|
-
.resize(newWidth)
|
|
87
|
-
.png({ quality: 80, compressionLevel: 9 })
|
|
88
|
-
.toBuffer();
|
|
89
|
-
|
|
90
|
-
if (output.length > maxBytes) {
|
|
91
|
-
const smallerWidth = Math.round(meta.width * 0.7);
|
|
92
|
-
output = await sharp(buffer)
|
|
93
|
-
.resize(smallerWidth)
|
|
94
|
-
.png({ quality: 70, compressionLevel: 9 })
|
|
95
|
-
.toBuffer();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
await fs.writeFile(filePath, output);
|
|
99
|
-
return { compressed: true, originalSize, finalSize: output.length };
|
|
100
|
-
} catch (err) {
|
|
101
|
-
return { compressed: false, originalSize, finalSize: originalSize, error: err.message };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Capture screenshot for a single viewport
|
|
107
|
-
*/
|
|
108
|
-
async function captureViewport(page, viewport, outputPath, fullPage = true, maxSize = 5, scrollDelay = DEFAULT_SCROLL_DELAY) {
|
|
109
|
-
await page.setViewportSize(VIEWPORTS[viewport]);
|
|
110
|
-
await new Promise(r => setTimeout(r, VIEWPORT_SETTLE_DELAY));
|
|
111
|
-
await waitForDomStable(page, 300, 5000);
|
|
112
|
-
await waitForFontsLoaded(page, 3000);
|
|
113
|
-
await waitForStylesStable(page, 200, 2000);
|
|
114
|
-
|
|
115
|
-
const componentDimensions = await extractComponentDimensions(page, viewport);
|
|
116
|
-
|
|
117
|
-
// Extract DOM hierarchy (desktop only for efficiency)
|
|
118
|
-
let domHierarchy = null;
|
|
119
|
-
if (viewport === 'desktop') {
|
|
120
|
-
try {
|
|
121
|
-
domHierarchy = await extractDOMHierarchy(page, { maxDepth: 8 });
|
|
122
|
-
} catch (err) {
|
|
123
|
-
console.error(`[WARN] DOM hierarchy extraction failed: ${err.message}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const lazyStats = await forceLazyImages(page);
|
|
128
|
-
const scrollInfo = await triggerLazyLoad(page, LAZY_LOAD_MAX_ITERATIONS, scrollDelay);
|
|
129
|
-
await forceLazyImages(page);
|
|
130
|
-
const imageStats = await waitForAllImages(page, 15000);
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
await page.waitForLoadState('networkidle', { timeout: NETWORK_IDLE_TIMEOUT });
|
|
134
|
-
} catch {
|
|
135
|
-
// Timeout ok
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
139
|
-
await waitForDomStable(page, 300, 3000);
|
|
140
|
-
await waitForFontsLoaded(page, 2000);
|
|
141
|
-
const animStats = await forceAnimatedElementsVisible(page);
|
|
142
|
-
await new Promise(r => setTimeout(r, 300));
|
|
143
|
-
|
|
144
|
-
await page.evaluate(() => {
|
|
145
|
-
window.scrollTo(0, 0);
|
|
146
|
-
document.documentElement.scrollTop = 0;
|
|
147
|
-
document.body.scrollTop = 0;
|
|
148
|
-
});
|
|
149
|
-
await new Promise(r => setTimeout(r, 500));
|
|
150
|
-
|
|
151
|
-
await page.screenshot({ path: outputPath, type: 'png', fullPage: fullPage });
|
|
152
|
-
const compression = await compressIfNeeded(outputPath, maxSize);
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
viewport,
|
|
156
|
-
path: path.resolve(outputPath),
|
|
157
|
-
dimensions: VIEWPORTS[viewport],
|
|
158
|
-
componentDimensions,
|
|
159
|
-
domHierarchy, // DOM hierarchy (desktop only)
|
|
160
|
-
scrollInfo,
|
|
161
|
-
imageStats,
|
|
162
|
-
size: compression.finalSize,
|
|
163
|
-
compressed: compression.compressed
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Main capture function
|
|
169
|
-
*/
|
|
170
|
-
async function captureMultiViewport() {
|
|
171
|
-
const args = parseArgs(process.argv.slice(2));
|
|
172
|
-
|
|
173
|
-
if (!args.url) {
|
|
174
|
-
outputError(new Error('--url is required'));
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
if (!args.output) {
|
|
178
|
-
outputError(new Error('--output directory is required'));
|
|
179
|
-
process.exit(1);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const requestedViewports = args.viewports
|
|
183
|
-
? args.viewports.split(',').map(v => v.trim().toLowerCase())
|
|
184
|
-
: ['desktop', 'tablet', 'mobile'];
|
|
185
|
-
const fullPage = args['full-page'] !== 'false';
|
|
186
|
-
const maxSize = args['max-size'] ? parseFloat(args['max-size']) : 5;
|
|
187
|
-
const scrollDelay = args['scroll-delay'] ? parseInt(args['scroll-delay'], 10) : DEFAULT_SCROLL_DELAY;
|
|
188
|
-
const extractHtml = args['extract-html'] === 'true';
|
|
189
|
-
const extractCss = args['extract-css'] === 'true';
|
|
190
|
-
const filterUnused = args['filter-unused'] !== 'false';
|
|
191
|
-
const captureHover = args['capture-hover'] === 'true';
|
|
192
|
-
const captureVideoFlag = args['video'] === 'true';
|
|
193
|
-
const videoFormat = args['video-format'] || 'webm';
|
|
194
|
-
const videoDuration = args['video-duration']
|
|
195
|
-
? parseInt(args['video-duration'], 10)
|
|
196
|
-
: 12000;
|
|
197
|
-
const sectionMode = args['section-mode'] === 'true';
|
|
198
|
-
|
|
199
|
-
for (const vp of requestedViewports) {
|
|
200
|
-
if (!VIEWPORTS[vp]) {
|
|
201
|
-
outputError(new Error(`Invalid viewport: ${vp}. Valid: desktop, tablet, mobile`));
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
await fs.mkdir(args.output, { recursive: true });
|
|
208
|
-
|
|
209
|
-
const cliHeadless = args.headless === 'true';
|
|
210
|
-
const getHeadlessForViewport = (viewport) => viewport === 'desktop' ? true : cliHeadless;
|
|
211
|
-
|
|
212
|
-
let currentHeadless = null;
|
|
213
|
-
let browser = null;
|
|
214
|
-
let page = null;
|
|
215
|
-
let cookieResult = null;
|
|
216
|
-
|
|
217
|
-
const initBrowser = async (headless, navigateUrl = null) => {
|
|
218
|
-
if (browser && currentHeadless !== headless) {
|
|
219
|
-
await closeBrowser();
|
|
220
|
-
browser = null;
|
|
221
|
-
page = null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!browser) {
|
|
225
|
-
browser = await getBrowser({
|
|
226
|
-
headless,
|
|
227
|
-
args: headless ? [] : ['--start-maximized', '--window-position=0,0']
|
|
228
|
-
});
|
|
229
|
-
page = await getPage(browser);
|
|
230
|
-
currentHeadless = headless;
|
|
231
|
-
|
|
232
|
-
if (navigateUrl) {
|
|
233
|
-
await page.setViewportSize(VIEWPORTS.desktop);
|
|
234
|
-
await page.goto(navigateUrl, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
|
235
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
236
|
-
cookieResult = await dismissCookieBanner(page);
|
|
237
|
-
await waitForPageReady(page);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return { browser, page };
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const firstViewportHeadless = getHeadlessForViewport(requestedViewports[0]);
|
|
244
|
-
await initBrowser(firstViewportHeadless, args.url);
|
|
245
|
-
|
|
246
|
-
// Extract HTML/CSS
|
|
247
|
-
let extraction = null;
|
|
248
|
-
const extractionWarnings = [];
|
|
249
|
-
|
|
250
|
-
if (extractHtml || extractCss) {
|
|
251
|
-
extraction = { html: null, css: null, warnings: [] };
|
|
252
|
-
|
|
253
|
-
// Extract content counts BEFORE cleaning HTML (to count hidden items too)
|
|
254
|
-
if (extractHtml) {
|
|
255
|
-
try {
|
|
256
|
-
const contentCounts = await extractContentCounts(page);
|
|
257
|
-
const countsPath = path.join(args.output, 'content-counts.json');
|
|
258
|
-
await fs.writeFile(countsPath, JSON.stringify(contentCounts, null, 2), 'utf-8');
|
|
259
|
-
|
|
260
|
-
// Generate summary for structure analysis
|
|
261
|
-
const contentSummary = generateContentSummary(contentCounts);
|
|
262
|
-
const summaryPath = path.join(args.output, 'content-summary.md');
|
|
263
|
-
await fs.writeFile(summaryPath, contentSummary, 'utf-8');
|
|
264
|
-
|
|
265
|
-
extraction.contentCounts = {
|
|
266
|
-
path: path.resolve(countsPath),
|
|
267
|
-
summaryPath: path.resolve(summaryPath),
|
|
268
|
-
summary: contentCounts.summary
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
if (process.stderr.isTTY) {
|
|
272
|
-
console.error(`[INFO] Content counts: ${contentCounts.grids.total} grids, ${contentCounts.repeatedItems.total} items`);
|
|
273
|
-
}
|
|
274
|
-
} catch (error) {
|
|
275
|
-
extractionWarnings.push(`Content counting failed: ${error.message}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (extractHtml) {
|
|
280
|
-
try {
|
|
281
|
-
// Use semantic enhancement unless --no-semantic flag is set
|
|
282
|
-
const enhanceSemantic = args['no-semantic'] !== 'true';
|
|
283
|
-
const htmlResult = enhanceSemantic
|
|
284
|
-
? await extractAndEnhanceHtml(page, { enhanceSemantic: true })
|
|
285
|
-
: await extractCleanHtml(page, JS_FRAMEWORK_PATTERNS);
|
|
286
|
-
|
|
287
|
-
const html = htmlResult.html;
|
|
288
|
-
const htmlSize = Buffer.byteLength(html, 'utf-8');
|
|
289
|
-
|
|
290
|
-
if (htmlSize > MAX_HTML_SIZE) {
|
|
291
|
-
throw new Error(`HTML size exceeds ${MAX_HTML_SIZE / 1024 / 1024}MB limit`);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const htmlPath = path.join(args.output, 'source.html');
|
|
295
|
-
await fs.writeFile(htmlPath, html, 'utf-8');
|
|
296
|
-
extraction.html = {
|
|
297
|
-
path: path.resolve(htmlPath),
|
|
298
|
-
size: htmlSize,
|
|
299
|
-
elementCount: htmlResult.elementCount,
|
|
300
|
-
semanticEnhanced: enhanceSemantic,
|
|
301
|
-
semanticStats: htmlResult.semanticStats || null
|
|
302
|
-
};
|
|
303
|
-
if (htmlResult.warnings.length > 0) extractionWarnings.push(...htmlResult.warnings);
|
|
304
|
-
} catch (error) {
|
|
305
|
-
extraction.html = { error: error.message, failed: true };
|
|
306
|
-
extractionWarnings.push(`HTML extraction failed: ${error.message}`);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (extractCss) {
|
|
311
|
-
try {
|
|
312
|
-
const cssData = await extractAllCss(page, args.url);
|
|
313
|
-
const rawCss = cssData.cssBlocks.map(b => `/* Source: ${b.source} */\n${b.css}`).join('\n\n');
|
|
314
|
-
const cssSize = Buffer.byteLength(rawCss, 'utf-8');
|
|
315
|
-
|
|
316
|
-
if (cssSize > MAX_CSS_SIZE) {
|
|
317
|
-
throw new Error(`CSS size exceeds ${MAX_CSS_SIZE / 1024 / 1024}MB limit`);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const rawCssPath = path.join(args.output, 'source-raw.css');
|
|
321
|
-
await fs.writeFile(rawCssPath, rawCss, 'utf-8');
|
|
322
|
-
|
|
323
|
-
extraction.css = {
|
|
324
|
-
path: path.resolve(rawCssPath),
|
|
325
|
-
size: cssSize,
|
|
326
|
-
blocks: cssData.cssBlocks.length,
|
|
327
|
-
totalRules: cssData.totalRules,
|
|
328
|
-
corsBlocked: cssData.corsBlocked,
|
|
329
|
-
computedStyles: cssData.computedStyles
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
if (Object.keys(cssData.computedStyles).length > 0) {
|
|
333
|
-
const stylesPath = path.join(args.output, 'computed-styles.json');
|
|
334
|
-
await fs.writeFile(stylesPath, JSON.stringify(cssData.computedStyles, null, 2));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (cssData.warnings.length > 0) extractionWarnings.push(...cssData.warnings);
|
|
338
|
-
if (cssData.corsBlocked.length > 0) extractionWarnings.push(`${cssData.corsBlocked.length} CORS-blocked stylesheets`);
|
|
339
|
-
} catch (error) {
|
|
340
|
-
extraction.css = { error: error.message, failed: true };
|
|
341
|
-
extractionWarnings.push(`CSS extraction failed: ${error.message}`);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Filter CSS
|
|
346
|
-
if (filterUnused && extraction?.html?.path && extraction?.css?.path && !extraction.html.failed && !extraction.css.failed) {
|
|
347
|
-
try {
|
|
348
|
-
const filteredCssPath = path.join(args.output, 'source.css');
|
|
349
|
-
const filterResult = await filterCssFile(extraction.html.path, extraction.css.path, filteredCssPath, false, args.output);
|
|
350
|
-
extraction.filtered = {
|
|
351
|
-
path: filterResult.output.path,
|
|
352
|
-
size: filterResult.output.size,
|
|
353
|
-
reduction: filterResult.stats.reduction,
|
|
354
|
-
stats: { totalRules: filterResult.stats.totalRules, keptRules: filterResult.stats.keptRules, removedRules: filterResult.stats.removedRules }
|
|
355
|
-
};
|
|
356
|
-
if (process.stderr.isTTY) console.error(`[INFO] CSS filtered: ${filterResult.stats.reduction} reduction`);
|
|
357
|
-
} catch (error) {
|
|
358
|
-
extraction.filtered = { error: error.message, failed: true };
|
|
359
|
-
extractionWarnings.push(`CSS filtering failed: ${error.message}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Extract animations (enabled by default with CSS extraction)
|
|
364
|
-
const extractAnimationsFlag = args['extract-animations'] !== 'false';
|
|
365
|
-
if (extractCss && extractAnimationsFlag && extraction?.css?.path && !extraction.css.failed) {
|
|
366
|
-
try {
|
|
367
|
-
const rawCss = await fs.readFile(extraction.css.path, 'utf-8');
|
|
368
|
-
const animData = await extractAnimations(rawCss);
|
|
369
|
-
|
|
370
|
-
if (!animData.error) {
|
|
371
|
-
// Write animations.css
|
|
372
|
-
const animCss = generateAnimationsCss(animData);
|
|
373
|
-
const animPath = path.join(args.output, 'animations.css');
|
|
374
|
-
await fs.writeFile(animPath, animCss, 'utf-8');
|
|
375
|
-
|
|
376
|
-
// Generate animation tokens
|
|
377
|
-
const animTokens = generateAnimationTokens(animData);
|
|
378
|
-
|
|
379
|
-
// Write animation-tokens.json
|
|
380
|
-
const animTokensPath = path.join(args.output, 'animation-tokens.json');
|
|
381
|
-
await fs.writeFile(animTokensPath, JSON.stringify({
|
|
382
|
-
keyframes: animData.keyframes,
|
|
383
|
-
transitions: animData.transitions,
|
|
384
|
-
animatedElements: animData.animatedElements,
|
|
385
|
-
summary: animTokens
|
|
386
|
-
}, null, 2), 'utf-8');
|
|
387
|
-
|
|
388
|
-
extraction.animations = {
|
|
389
|
-
path: path.resolve(animPath),
|
|
390
|
-
tokensPath: path.resolve(animTokensPath),
|
|
391
|
-
keyframeCount: animTokens.keyframeCount,
|
|
392
|
-
transitionCount: animTokens.transitions,
|
|
393
|
-
animatedElementCount: animTokens.animatedElements,
|
|
394
|
-
tokens: animTokens
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
if (process.stderr.isTTY) {
|
|
398
|
-
console.error(`[INFO] Animations: ${animTokens.keyframeCount} keyframes, ${animTokens.transitions} transitions`);
|
|
399
|
-
}
|
|
400
|
-
} else {
|
|
401
|
-
extraction.animations = { error: animData.error, failed: true };
|
|
402
|
-
extractionWarnings.push(`Animation extraction failed: ${animData.error}`);
|
|
403
|
-
}
|
|
404
|
-
} catch (error) {
|
|
405
|
-
extraction.animations = { error: error.message, failed: true };
|
|
406
|
-
extractionWarnings.push(`Animation extraction failed: ${error.message}`);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
extraction.warnings = extractionWarnings;
|
|
411
|
-
if (extractionWarnings.length > 0 && process.stderr.isTTY) {
|
|
412
|
-
extractionWarnings.forEach(w => console.error(`[WARN] ${w}`));
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Capture hover states (requires headless mode per Playwright #5255)
|
|
417
|
-
let hoverResult = null;
|
|
418
|
-
if (captureHover) {
|
|
419
|
-
try {
|
|
420
|
-
// Try headed mode first, fallback to headless (per validation decision)
|
|
421
|
-
const wasHeadless = currentHeadless;
|
|
422
|
-
let hoverCaptureSuccess = false;
|
|
423
|
-
|
|
424
|
-
// Attempt headed mode first
|
|
425
|
-
if (!wasHeadless) {
|
|
426
|
-
try {
|
|
427
|
-
const cssContent = extraction?.css?.path
|
|
428
|
-
? await fs.readFile(extraction.css.path, 'utf-8')
|
|
429
|
-
: null;
|
|
430
|
-
hoverResult = await captureAllHoverStates(page, cssContent, args.output);
|
|
431
|
-
hoverCaptureSuccess = hoverResult.captured > 0;
|
|
432
|
-
} catch (headedError) {
|
|
433
|
-
if (process.stderr.isTTY) {
|
|
434
|
-
console.error(`[WARN] Headed hover capture failed, switching to headless: ${headedError.message}`);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Fallback to headless if headed failed or was already headless
|
|
440
|
-
if (!hoverCaptureSuccess) {
|
|
441
|
-
if (!currentHeadless) {
|
|
442
|
-
await initBrowser(true, args.url);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const cssContent = extraction?.css?.path
|
|
446
|
-
? await fs.readFile(extraction.css.path, 'utf-8')
|
|
447
|
-
: null;
|
|
448
|
-
hoverResult = await captureAllHoverStates(page, cssContent, args.output);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// Generate hover.css from captured diffs
|
|
452
|
-
if (hoverResult && hoverResult.elements && hoverResult.captured > 0) {
|
|
453
|
-
const hoverCss = generateHoverCss(hoverResult.elements);
|
|
454
|
-
const hoverCssPath = path.join(args.output, 'hover.css');
|
|
455
|
-
await fs.writeFile(hoverCssPath, hoverCss, 'utf-8');
|
|
456
|
-
hoverResult.generatedCss = path.resolve(hoverCssPath);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (process.stderr.isTTY && hoverResult) {
|
|
460
|
-
console.error(`[INFO] Hover states: ${hoverResult.captured}/${hoverResult.detected} captured`);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Restore browser mode for viewport captures if needed
|
|
464
|
-
if (!wasHeadless && currentHeadless && requestedViewports.some(v => !getHeadlessForViewport(v))) {
|
|
465
|
-
await initBrowser(false, args.url);
|
|
466
|
-
}
|
|
467
|
-
} catch (error) {
|
|
468
|
-
if (process.stderr.isTTY) {
|
|
469
|
-
console.error(`[WARN] Hover capture failed: ${error.message}`);
|
|
470
|
-
}
|
|
471
|
-
hoverResult = { error: error.message, failed: true };
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Capture viewports
|
|
476
|
-
const screenshots = [];
|
|
477
|
-
const browserRestarts = [];
|
|
478
|
-
for (const viewport of requestedViewports) {
|
|
479
|
-
const viewportHeadless = getHeadlessForViewport(viewport);
|
|
480
|
-
if (currentHeadless !== viewportHeadless) {
|
|
481
|
-
browserRestarts.push({ viewport, from: currentHeadless ? 'headless' : 'headed', to: viewportHeadless ? 'headless' : 'headed' });
|
|
482
|
-
if (process.stderr.isTTY) console.error(`[INFO] Switching to ${viewportHeadless ? 'headless' : 'headed'} for ${viewport}`);
|
|
483
|
-
await initBrowser(viewportHeadless, args.url);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const outputPath = path.join(args.output, `${viewport}.png`);
|
|
487
|
-
const result = await captureViewport(page, viewport, outputPath, fullPage, maxSize, scrollDelay);
|
|
488
|
-
screenshots.push(result);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Capture video (opt-in, after screenshots)
|
|
492
|
-
let videoResult = null;
|
|
493
|
-
if (captureVideoFlag) {
|
|
494
|
-
try {
|
|
495
|
-
// Check if ffmpeg is needed but not available
|
|
496
|
-
if (FFMPEG_REQUIRED_FORMATS.includes(videoFormat)) {
|
|
497
|
-
const hasFf = await hasFfmpeg();
|
|
498
|
-
if (!hasFf && process.stderr.isTTY) {
|
|
499
|
-
console.error(`[WARN] ffmpeg not found. Will output WebM instead of ${videoFormat}`);
|
|
500
|
-
console.error('[WARN] Install: npm install fluent-ffmpeg @ffmpeg-installer/ffmpeg');
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Use desktop viewport for video
|
|
505
|
-
await page.setViewportSize(VIEWPORTS.desktop);
|
|
506
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
507
|
-
|
|
508
|
-
if (process.stderr.isTTY) {
|
|
509
|
-
console.error(`[INFO] Recording video (${videoDuration / 1000}s)...`);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
videoResult = await captureVideo(page, args.output, {
|
|
513
|
-
format: videoFormat,
|
|
514
|
-
duration: videoDuration,
|
|
515
|
-
filename: 'preview'
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
if (process.stderr.isTTY) {
|
|
519
|
-
const outputFormat = videoResult.output.split('.').pop();
|
|
520
|
-
console.error(`[INFO] Video saved: ${outputFormat} (${(videoResult.duration / 1000).toFixed(1)}s)`);
|
|
521
|
-
}
|
|
522
|
-
} catch (error) {
|
|
523
|
-
if (process.stderr.isTTY) {
|
|
524
|
-
console.error(`[WARN] Video capture failed: ${error.message}`);
|
|
525
|
-
}
|
|
526
|
-
videoResult = { error: error.message, failed: true };
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Build dimension output
|
|
531
|
-
const allViewportDimensions = {};
|
|
532
|
-
for (const screenshot of screenshots) {
|
|
533
|
-
if (screenshot.componentDimensions) {
|
|
534
|
-
allViewportDimensions[screenshot.viewport] = screenshot.componentDimensions;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const dimensionsOutput = buildDimensionsOutput(allViewportDimensions, args.url);
|
|
539
|
-
const dimensionsPath = path.join(args.output, 'component-dimensions.json');
|
|
540
|
-
await fs.writeFile(dimensionsPath, JSON.stringify(dimensionsOutput, null, 2));
|
|
541
|
-
|
|
542
|
-
const aiSummary = generateAISummary(dimensionsOutput);
|
|
543
|
-
const summaryPath = path.join(args.output, 'dimensions-summary.json');
|
|
544
|
-
await fs.writeFile(summaryPath, JSON.stringify(aiSummary, null, 2));
|
|
545
|
-
|
|
546
|
-
// Write DOM hierarchy if available (from desktop capture)
|
|
547
|
-
const desktopScreenshot = screenshots.find(s => s.viewport === 'desktop');
|
|
548
|
-
let hierarchyPath = null;
|
|
549
|
-
if (desktopScreenshot?.domHierarchy) {
|
|
550
|
-
hierarchyPath = path.join(args.output, 'dom-hierarchy.json');
|
|
551
|
-
await fs.writeFile(hierarchyPath, JSON.stringify(desktopScreenshot.domHierarchy, null, 2));
|
|
552
|
-
|
|
553
|
-
if (process.stderr.isTTY) {
|
|
554
|
-
const stats = desktopScreenshot.domHierarchy.stats || {};
|
|
555
|
-
console.error(`[INFO] DOM hierarchy: ${stats.totalNodes || 0} nodes, ${stats.landmarkCount || 0} landmarks, ${stats.extractionTimeMs || 0}ms`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Section-based capture (for improved AI analysis)
|
|
560
|
-
let sectionResult = null;
|
|
561
|
-
if (sectionMode && desktopScreenshot) {
|
|
562
|
-
try {
|
|
563
|
-
// Lazy import section modules
|
|
564
|
-
const { detectSections } = await import('./section-detector.js');
|
|
565
|
-
const { cropSections } = await import('./section-cropper.js');
|
|
566
|
-
|
|
567
|
-
// Reset to desktop viewport for section detection
|
|
568
|
-
await page.setViewportSize(VIEWPORTS.desktop);
|
|
569
|
-
await new Promise(r => setTimeout(r, 500));
|
|
570
|
-
|
|
571
|
-
if (process.stderr.isTTY) {
|
|
572
|
-
console.error('[INFO] Detecting sections...');
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const sections = await detectSections(page, {
|
|
576
|
-
padding: 40,
|
|
577
|
-
minSections: 3,
|
|
578
|
-
fallbackToViewport: true
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
if (process.stderr.isTTY) {
|
|
582
|
-
console.error(`[INFO] Found ${sections.length} sections, cropping...`);
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
const croppedResult = await cropSections(
|
|
586
|
-
desktopScreenshot.path,
|
|
587
|
-
sections,
|
|
588
|
-
args.output
|
|
589
|
-
);
|
|
590
|
-
|
|
591
|
-
sectionResult = {
|
|
592
|
-
enabled: true,
|
|
593
|
-
count: croppedResult.sections.length,
|
|
594
|
-
skipped: croppedResult.skipped.length,
|
|
595
|
-
sections: croppedResult.sections.map(s => ({
|
|
596
|
-
index: s.index,
|
|
597
|
-
name: s.name,
|
|
598
|
-
path: s.relativePath,
|
|
599
|
-
bounds: s.bounds,
|
|
600
|
-
role: s.role
|
|
601
|
-
})),
|
|
602
|
-
directory: croppedResult.directory,
|
|
603
|
-
summary: croppedResult.summary
|
|
604
|
-
};
|
|
605
|
-
|
|
606
|
-
if (process.stderr.isTTY) {
|
|
607
|
-
console.error(`[INFO] Sections: ${croppedResult.sections.length} cropped, ${croppedResult.skipped.length} skipped`);
|
|
608
|
-
}
|
|
609
|
-
} catch (err) {
|
|
610
|
-
sectionResult = { enabled: true, error: err.message };
|
|
611
|
-
if (process.stderr.isTTY) {
|
|
612
|
-
console.error(`[WARN] Section processing failed: ${err.message}`);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const totalContainers = Object.values(dimensionsOutput.viewports).reduce((sum, vp) => sum + (vp.containers?.length || 0), 0);
|
|
618
|
-
const totalCards = Object.values(dimensionsOutput.viewports).reduce((sum, vp) => sum + (vp.cards?.length || 0), 0);
|
|
619
|
-
const totalGrids = Object.values(dimensionsOutput.viewports).reduce((sum, vp) => sum + (vp.gridLayouts?.length || 0), 0);
|
|
620
|
-
|
|
621
|
-
if (process.stderr.isTTY) {
|
|
622
|
-
console.error(`[INFO] Extracted: ${totalContainers} containers, ${totalCards} card groups, ${totalGrids} grid layouts`);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const result = {
|
|
626
|
-
success: true,
|
|
627
|
-
url: args.url,
|
|
628
|
-
outputDir: path.resolve(args.output),
|
|
629
|
-
cookieHandling: cookieResult,
|
|
630
|
-
extraction,
|
|
631
|
-
hoverStates: hoverResult && !hoverResult.failed ? {
|
|
632
|
-
directory: hoverResult.directory,
|
|
633
|
-
detected: hoverResult.detected,
|
|
634
|
-
captured: hoverResult.captured,
|
|
635
|
-
summaryPath: hoverResult.summaryPath,
|
|
636
|
-
generatedCss: hoverResult.generatedCss
|
|
637
|
-
} : (hoverResult?.error ? { error: hoverResult.error } : undefined),
|
|
638
|
-
video: videoResult && !videoResult.failed ? {
|
|
639
|
-
path: videoResult.output,
|
|
640
|
-
format: videoResult.output.split('.').pop(),
|
|
641
|
-
duration: videoResult.duration,
|
|
642
|
-
pageHeight: videoResult.pageHeight,
|
|
643
|
-
webm: videoResult.webm,
|
|
644
|
-
mp4: videoResult.mp4,
|
|
645
|
-
gif: videoResult.gif,
|
|
646
|
-
conversionError: videoResult.conversionError
|
|
647
|
-
} : (videoResult?.error ? { error: videoResult.error } : undefined),
|
|
648
|
-
componentDimensions: {
|
|
649
|
-
full: path.resolve(dimensionsPath),
|
|
650
|
-
summary: path.resolve(summaryPath),
|
|
651
|
-
viewports: Object.keys(dimensionsOutput.viewports),
|
|
652
|
-
stats: { containers: totalContainers, cards: totalCards, gridLayouts: totalGrids,
|
|
653
|
-
typography: Object.values(dimensionsOutput.viewports).reduce((sum, vp) => sum + (vp.typography?.length || 0), 0) }
|
|
654
|
-
},
|
|
655
|
-
domHierarchy: desktopScreenshot?.domHierarchy ? {
|
|
656
|
-
path: path.resolve(hierarchyPath),
|
|
657
|
-
stats: desktopScreenshot.domHierarchy.stats
|
|
658
|
-
} : undefined,
|
|
659
|
-
sections: sectionResult,
|
|
660
|
-
screenshots,
|
|
661
|
-
browserRestarts: browserRestarts.length > 0 ? browserRestarts : undefined,
|
|
662
|
-
scrollDelay,
|
|
663
|
-
totalSize: screenshots.reduce((sum, s) => sum + s.size, 0),
|
|
664
|
-
capturedAt: new Date().toISOString()
|
|
665
|
-
};
|
|
666
|
-
|
|
667
|
-
outputJSON(result);
|
|
668
|
-
|
|
669
|
-
if (args.close === 'true') {
|
|
670
|
-
await closeBrowser();
|
|
671
|
-
} else {
|
|
672
|
-
await disconnectBrowser();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
process.exit(0);
|
|
676
|
-
} catch (error) {
|
|
677
|
-
outputError(error);
|
|
678
|
-
process.exit(1);
|
|
679
|
-
} finally {
|
|
680
|
-
try { await closeBrowser(); } catch { /* ignore */ }
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Export for module use
|
|
685
|
-
export {
|
|
686
|
-
captureViewport,
|
|
687
|
-
VIEWPORTS,
|
|
688
|
-
VIEWPORT_SETTLE_DELAY,
|
|
689
|
-
DEFAULT_SCROLL_DELAY,
|
|
690
|
-
compressIfNeeded
|
|
691
|
-
};
|
|
692
|
-
|
|
693
|
-
// Run if called directly (not imported as module)
|
|
694
|
-
const isMainModule = process.argv[1] && (
|
|
695
|
-
process.argv[1].endsWith('screenshot.js') ||
|
|
696
|
-
process.argv[1].includes('screenshot')
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
if (isMainModule) {
|
|
700
|
-
captureMultiViewport();
|
|
701
|
-
}
|