design-clone 1.2.0 ā 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -12
- package/bin/commands/clone-site.js +75 -10
- package/bin/commands/init.js +33 -1
- package/bin/commands/verify.js +5 -3
- package/bin/utils/validate.js +24 -8
- package/docs/cli-reference.md +200 -2
- package/docs/codebase-summary.md +309 -0
- package/docs/design-clone-architecture.md +259 -42
- package/docs/pixel-perfect.md +35 -4
- package/docs/project-roadmap.md +382 -0
- package/docs/troubleshooting.md +5 -4
- package/package.json +10 -8
- 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 +73 -3
- package/src/ai/extract-design-tokens.py +356 -13
- 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 +133 -0
- package/src/ai/prompts/structure_analysis.py +329 -10
- package/src/ai/prompts/ux_audit.py +198 -0
- package/src/ai/ux-audit.js +596 -0
- package/src/core/app-state-snapshot.js +511 -0
- package/src/core/content-counter.js +342 -0
- package/src/core/cookie-handler.js +1 -1
- package/src/core/css-extractor.js +4 -4
- package/src/core/dimension-extractor.js +93 -21
- package/src/core/dimension-output.js +103 -6
- package/src/core/discover-pages.js +242 -14
- package/src/core/dom-tree-analyzer.js +298 -0
- package/src/core/extract-assets.js +1 -1
- package/src/core/framework-detector.js +538 -0
- package/src/core/html-extractor.js +45 -4
- package/src/core/lazy-loader.js +7 -7
- package/src/core/multi-page-screenshot.js +9 -6
- package/src/core/page-readiness.js +8 -8
- package/src/core/screenshot.js +138 -9
- package/src/core/section-cropper.js +209 -0
- package/src/core/section-detector.js +386 -0
- package/src/core/semantic-enhancer.js +492 -0
- package/src/core/state-capture.js +18 -22
- package/src/core/tests/test-section-cropper.js +177 -0
- package/src/core/tests/test-section-detector.js +55 -0
- package/src/core/video-capture.js +152 -146
- package/src/route-discoverers/angular-discoverer.js +157 -0
- package/src/route-discoverers/astro-discoverer.js +123 -0
- package/src/route-discoverers/base-discoverer.js +242 -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/utils/__init__.py +1 -1
- package/src/utils/__pycache__/__init__.cpython-313.pyc +0 -0
- package/src/utils/browser.js +11 -37
- package/src/utils/playwright.js +213 -0
- package/src/verification/generate-audit-report.js +398 -0
- package/src/verification/verify-footer.js +493 -0
- package/src/verification/verify-header.js +486 -0
- package/src/verification/verify-layout.js +2 -2
- package/src/verification/verify-menu.js +4 -20
- package/src/verification/verify-slider.js +533 -0
- package/src/utils/puppeteer.js +0 -281
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Audit Report Generator
|
|
4
|
+
*
|
|
5
|
+
* Aggregates verification results from header, footer, slider, menu, and layout
|
|
6
|
+
* verifiers into a consolidated markdown report with:
|
|
7
|
+
* - Summary table with pass/warn/fail status
|
|
8
|
+
* - Component sections with side-by-side screenshots
|
|
9
|
+
* - Responsive breakpoint analysis
|
|
10
|
+
* - CSS fix suggestions
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node generate-audit-report.js --dir <results-dir> [--output <report-path>]
|
|
14
|
+
*
|
|
15
|
+
* Options:
|
|
16
|
+
* --dir Directory containing verification JSON results
|
|
17
|
+
* --output Output path for markdown report (default: component-audit.md)
|
|
18
|
+
* --verbose Show detailed progress
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs/promises';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
import { parseArgs, outputJSON, outputError } from '../utils/browser.js';
|
|
24
|
+
|
|
25
|
+
// Component types and their result files
|
|
26
|
+
const COMPONENT_FILES = {
|
|
27
|
+
header: 'header-results.json',
|
|
28
|
+
footer: 'footer-results.json',
|
|
29
|
+
slider: 'slider-results.json',
|
|
30
|
+
menu: 'menu-results.json',
|
|
31
|
+
layout: 'layout-results.json'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Status icons
|
|
35
|
+
const STATUS_ICONS = {
|
|
36
|
+
pass: 'ā
',
|
|
37
|
+
warn: 'ā ļø',
|
|
38
|
+
fail: 'ā',
|
|
39
|
+
info: 'ā¹ļø'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load verification results from directory
|
|
44
|
+
*/
|
|
45
|
+
async function loadVerificationResults(dir) {
|
|
46
|
+
const results = {};
|
|
47
|
+
|
|
48
|
+
for (const [component, filename] of Object.entries(COMPONENT_FILES)) {
|
|
49
|
+
const filePath = path.join(dir, filename);
|
|
50
|
+
try {
|
|
51
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
52
|
+
results[component] = JSON.parse(content);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err.code !== 'ENOENT') {
|
|
55
|
+
console.error(`Warning: Failed to load ${filename}: ${err.message}`);
|
|
56
|
+
}
|
|
57
|
+
results[component] = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Calculate component status
|
|
66
|
+
*/
|
|
67
|
+
function getComponentStatus(result) {
|
|
68
|
+
if (!result) return { status: 'skip', icon: STATUS_ICONS.info, label: 'Not tested' };
|
|
69
|
+
|
|
70
|
+
const { summary } = result;
|
|
71
|
+
if (!summary) return { status: 'skip', icon: STATUS_ICONS.info, label: 'No data' };
|
|
72
|
+
|
|
73
|
+
if (summary.failed > 0) {
|
|
74
|
+
return { status: 'fail', icon: STATUS_ICONS.fail, label: `${summary.failed} failed` };
|
|
75
|
+
}
|
|
76
|
+
if (summary.warnings?.length > 0) {
|
|
77
|
+
return { status: 'warn', icon: STATUS_ICONS.warn, label: `${summary.warnings.length} warnings` };
|
|
78
|
+
}
|
|
79
|
+
return { status: 'pass', icon: STATUS_ICONS.pass, label: 'Passed' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generate summary table
|
|
84
|
+
*/
|
|
85
|
+
function generateSummaryTable(results) {
|
|
86
|
+
let table = `| Component | Status | Tests | Details |
|
|
87
|
+
|-----------|--------|-------|---------|
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
for (const [component, result] of Object.entries(results)) {
|
|
91
|
+
const status = getComponentStatus(result);
|
|
92
|
+
const tests = result?.summary
|
|
93
|
+
? `${result.summary.passed}/${result.summary.totalTests}`
|
|
94
|
+
: '-';
|
|
95
|
+
table += `| ${component.charAt(0).toUpperCase() + component.slice(1)} | ${status.icon} ${status.label} | ${tests} | ${result?.url || '-'} |\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return table;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate viewport breakdown table
|
|
103
|
+
*/
|
|
104
|
+
function generateViewportTable(results) {
|
|
105
|
+
const viewports = ['mobile', 'tablet', 'desktop'];
|
|
106
|
+
const components = Object.keys(results).filter(c => results[c]?.viewports);
|
|
107
|
+
|
|
108
|
+
if (components.length === 0) return '';
|
|
109
|
+
|
|
110
|
+
let table = `| Component | Mobile | Tablet | Desktop |
|
|
111
|
+
|-----------|--------|--------|---------|
|
|
112
|
+
`;
|
|
113
|
+
|
|
114
|
+
for (const component of components) {
|
|
115
|
+
const result = results[component];
|
|
116
|
+
const row = [component.charAt(0).toUpperCase() + component.slice(1)];
|
|
117
|
+
|
|
118
|
+
for (const vp of viewports) {
|
|
119
|
+
const vpResult = result.viewports?.[vp];
|
|
120
|
+
if (vpResult) {
|
|
121
|
+
const icon = vpResult.failed > 0 ? STATUS_ICONS.fail
|
|
122
|
+
: vpResult.warnings?.length > 0 ? STATUS_ICONS.warn
|
|
123
|
+
: STATUS_ICONS.pass;
|
|
124
|
+
row.push(`${icon} ${vpResult.passed}/${vpResult.tests?.length || 0}`);
|
|
125
|
+
} else {
|
|
126
|
+
row.push('-');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
table += `| ${row.join(' | ')} |\n`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return table;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Generate component section
|
|
138
|
+
*/
|
|
139
|
+
function generateComponentSection(component, result) {
|
|
140
|
+
if (!result) {
|
|
141
|
+
return `### ${component.charAt(0).toUpperCase() + component.slice(1)}
|
|
142
|
+
|
|
143
|
+
${STATUS_ICONS.info} Not tested
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const status = getComponentStatus(result);
|
|
151
|
+
let section = `### ${component.charAt(0).toUpperCase() + component.slice(1)} ${status.icon}
|
|
152
|
+
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
// Add component-specific info
|
|
156
|
+
if (component === 'slider' && result.sliderLibrary) {
|
|
157
|
+
section += `**Library:** ${result.sliderLibrary}\n\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Test results by viewport
|
|
161
|
+
if (result.viewports) {
|
|
162
|
+
for (const [viewport, vpResult] of Object.entries(result.viewports)) {
|
|
163
|
+
section += `#### ${viewport.charAt(0).toUpperCase() + viewport.slice(1)} (${vpResult.dimensions?.width}x${vpResult.dimensions?.height})\n\n`;
|
|
164
|
+
|
|
165
|
+
// List tests
|
|
166
|
+
if (vpResult.tests?.length > 0) {
|
|
167
|
+
for (const test of vpResult.tests) {
|
|
168
|
+
const icon = test.passed ? STATUS_ICONS.pass : STATUS_ICONS.fail;
|
|
169
|
+
section += `- ${icon} **${test.name}**`;
|
|
170
|
+
if (test.selector) section += ` - \`${test.selector}\``;
|
|
171
|
+
if (test.count !== undefined) section += ` (${test.count} found)`;
|
|
172
|
+
if (test.note) section += ` - ${test.note}`;
|
|
173
|
+
if (test.error) section += ` - ā ļø ${test.error}`;
|
|
174
|
+
section += '\n';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Warnings
|
|
179
|
+
if (vpResult.warnings?.length > 0) {
|
|
180
|
+
section += '\n**Warnings:**\n';
|
|
181
|
+
for (const warning of vpResult.warnings) {
|
|
182
|
+
section += `- ${STATUS_ICONS.warn} ${warning}\n`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
section += '\n';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Screenshots
|
|
191
|
+
if (result.screenshots?.length > 0) {
|
|
192
|
+
section += `#### Screenshots\n\n`;
|
|
193
|
+
section += `| Viewport | Screenshot |\n|----------|------------|\n`;
|
|
194
|
+
for (const screenshot of result.screenshots) {
|
|
195
|
+
const name = path.basename(screenshot);
|
|
196
|
+
const viewport = name.replace(/^[a-z]+-test-/, '').replace('.png', '');
|
|
197
|
+
section += `| ${viewport} | [${name}](${screenshot}) |\n`;
|
|
198
|
+
}
|
|
199
|
+
section += '\n';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
section += '---\n\n';
|
|
203
|
+
return section;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate CSS fixes section
|
|
208
|
+
*/
|
|
209
|
+
function generateCSSFixes(results) {
|
|
210
|
+
const fixes = [];
|
|
211
|
+
|
|
212
|
+
// Collect issues that might need CSS fixes
|
|
213
|
+
for (const [component, result] of Object.entries(results)) {
|
|
214
|
+
if (!result?.viewports) continue;
|
|
215
|
+
|
|
216
|
+
for (const [viewport, vpResult] of Object.entries(result.viewports)) {
|
|
217
|
+
// Check for layout issues
|
|
218
|
+
if (component === 'header' && vpResult.headerHeight) {
|
|
219
|
+
// Height consistency check already done in verify-header
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (component === 'footer') {
|
|
223
|
+
const positionTest = vpResult.tests?.find(t => t.name === 'Footer at page bottom');
|
|
224
|
+
if (positionTest && !positionTest.passed) {
|
|
225
|
+
fixes.push({
|
|
226
|
+
component,
|
|
227
|
+
viewport,
|
|
228
|
+
issue: 'Footer not at page bottom',
|
|
229
|
+
suggestion: `/* Ensure footer sticks to bottom */
|
|
230
|
+
footer {
|
|
231
|
+
margin-top: auto;
|
|
232
|
+
}
|
|
233
|
+
body {
|
|
234
|
+
min-height: 100vh;
|
|
235
|
+
display: flex;
|
|
236
|
+
flex-direction: column;
|
|
237
|
+
}`
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check warnings
|
|
243
|
+
if (vpResult.warnings) {
|
|
244
|
+
for (const warning of vpResult.warnings) {
|
|
245
|
+
if (warning.includes('z-index')) {
|
|
246
|
+
fixes.push({
|
|
247
|
+
component,
|
|
248
|
+
viewport,
|
|
249
|
+
issue: warning,
|
|
250
|
+
suggestion: `/* Increase header z-index */
|
|
251
|
+
header, .header, [role="banner"] {
|
|
252
|
+
z-index: 1000;
|
|
253
|
+
}`
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (fixes.length === 0) return '';
|
|
262
|
+
|
|
263
|
+
let section = `## Suggested CSS Fixes
|
|
264
|
+
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
for (const fix of fixes) {
|
|
268
|
+
section += `### ${fix.component} (${fix.viewport})
|
|
269
|
+
|
|
270
|
+
**Issue:** ${fix.issue}
|
|
271
|
+
|
|
272
|
+
\`\`\`css
|
|
273
|
+
${fix.suggestion}
|
|
274
|
+
\`\`\`
|
|
275
|
+
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return section;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Generate full markdown report
|
|
284
|
+
*/
|
|
285
|
+
function generateMarkdownReport(results, url) {
|
|
286
|
+
const timestamp = new Date().toISOString();
|
|
287
|
+
|
|
288
|
+
let report = `# Component Audit Report
|
|
289
|
+
|
|
290
|
+
**Generated:** ${timestamp}
|
|
291
|
+
**URL:** ${url || 'N/A'}
|
|
292
|
+
|
|
293
|
+
## Summary
|
|
294
|
+
|
|
295
|
+
${generateSummaryTable(results)}
|
|
296
|
+
|
|
297
|
+
## Responsive Breakdown
|
|
298
|
+
|
|
299
|
+
${generateViewportTable(results)}
|
|
300
|
+
|
|
301
|
+
## Component Details
|
|
302
|
+
|
|
303
|
+
`;
|
|
304
|
+
|
|
305
|
+
// Add each component section
|
|
306
|
+
for (const [component, result] of Object.entries(results)) {
|
|
307
|
+
report += generateComponentSection(component, result);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Add CSS fixes if any
|
|
311
|
+
report += generateCSSFixes(results);
|
|
312
|
+
|
|
313
|
+
// Footer
|
|
314
|
+
report += `---
|
|
315
|
+
|
|
316
|
+
*Report generated by design-clone verification suite*
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
return report;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Main function
|
|
324
|
+
*/
|
|
325
|
+
async function generateAuditReport() {
|
|
326
|
+
const args = parseArgs(process.argv.slice(2));
|
|
327
|
+
|
|
328
|
+
if (!args.dir) {
|
|
329
|
+
outputError(new Error('--dir is required'));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const verbose = args.verbose === 'true';
|
|
334
|
+
const outputPath = args.output || path.join(args.dir, 'component-audit.md');
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
if (verbose) console.error(`\nš Generating audit report from ${args.dir}\n`);
|
|
338
|
+
|
|
339
|
+
// Load all verification results
|
|
340
|
+
const results = await loadVerificationResults(args.dir);
|
|
341
|
+
|
|
342
|
+
// Get URL from first available result
|
|
343
|
+
const url = Object.values(results).find(r => r?.url)?.url;
|
|
344
|
+
|
|
345
|
+
// Count loaded components
|
|
346
|
+
const loadedCount = Object.values(results).filter(r => r !== null).length;
|
|
347
|
+
if (verbose) console.error(` Loaded ${loadedCount}/${Object.keys(COMPONENT_FILES).length} component results`);
|
|
348
|
+
|
|
349
|
+
// Generate report
|
|
350
|
+
const report = generateMarkdownReport(results, url);
|
|
351
|
+
|
|
352
|
+
// Write report
|
|
353
|
+
await fs.writeFile(outputPath, report, 'utf-8');
|
|
354
|
+
if (verbose) console.error(` ā Report written to ${outputPath}`);
|
|
355
|
+
|
|
356
|
+
// Calculate overall stats
|
|
357
|
+
let totalTests = 0, totalPassed = 0, totalFailed = 0, totalWarnings = 0;
|
|
358
|
+
for (const result of Object.values(results)) {
|
|
359
|
+
if (result?.summary) {
|
|
360
|
+
totalTests += result.summary.totalTests || 0;
|
|
361
|
+
totalPassed += result.summary.passed || 0;
|
|
362
|
+
totalFailed += result.summary.failed || 0;
|
|
363
|
+
totalWarnings += result.summary.warnings?.length || 0;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const output = {
|
|
368
|
+
success: totalFailed === 0,
|
|
369
|
+
reportPath: outputPath,
|
|
370
|
+
url,
|
|
371
|
+
summary: {
|
|
372
|
+
components: loadedCount,
|
|
373
|
+
totalTests,
|
|
374
|
+
passed: totalPassed,
|
|
375
|
+
failed: totalFailed,
|
|
376
|
+
warnings: totalWarnings
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (verbose) {
|
|
381
|
+
console.error('\nš Report Summary:');
|
|
382
|
+
console.error(` Components: ${loadedCount}`);
|
|
383
|
+
console.error(` Tests: ${totalPassed}/${totalTests} passed`);
|
|
384
|
+
console.error(` Failures: ${totalFailed}`);
|
|
385
|
+
console.error(` Warnings: ${totalWarnings}`);
|
|
386
|
+
console.error(` Status: ${output.success ? 'ā PASS' : 'ā ISSUES FOUND'}\n`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
outputJSON(output);
|
|
390
|
+
process.exit(output.success ? 0 : 1);
|
|
391
|
+
|
|
392
|
+
} catch (error) {
|
|
393
|
+
outputError(error);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
generateAuditReport();
|