difflens-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -0
- package/dist/differ.d.ts +2 -0
- package/dist/differ.js +293 -0
- package/dist/differ.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +432 -0
- package/dist/index.js.map +1 -0
- package/dist/layout-analyzer.d.ts +4 -0
- package/dist/layout-analyzer.js +140 -0
- package/dist/layout-analyzer.js.map +1 -0
- package/dist/report-builder.d.ts +6 -0
- package/dist/report-builder.js +66 -0
- package/dist/report-builder.js.map +1 -0
- package/dist/screenshotter.d.ts +4 -0
- package/dist/screenshotter.js +142 -0
- package/dist/screenshotter.js.map +1 -0
- package/dist/storage.d.ts +11 -0
- package/dist/storage.js +142 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export function buildReport(diff) {
|
|
2
|
+
const { percentChanged, dimensions, regions, diffImagePath, overlayImagePath } = diff;
|
|
3
|
+
let status;
|
|
4
|
+
if (percentChanged === 0) {
|
|
5
|
+
status = 'no_changes';
|
|
6
|
+
}
|
|
7
|
+
else if (percentChanged < 1) {
|
|
8
|
+
status = 'minor';
|
|
9
|
+
}
|
|
10
|
+
else if (percentChanged < 10) {
|
|
11
|
+
status = 'significant';
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
status = 'major';
|
|
15
|
+
}
|
|
16
|
+
// Build human-readable summary
|
|
17
|
+
const parts = [];
|
|
18
|
+
parts.push(`${status.replace('_', ' ').toUpperCase()}: ${percentChanged.toFixed(2)}% pixels changed.`);
|
|
19
|
+
if (dimensions.resized) {
|
|
20
|
+
parts.push(`Page dimensions changed from ${dimensions.before.width}x${dimensions.before.height} ` +
|
|
21
|
+
`to ${dimensions.after.width}x${dimensions.after.height}.`);
|
|
22
|
+
}
|
|
23
|
+
if (regions.length > 0) {
|
|
24
|
+
parts.push(`${regions.length} changed region(s) detected:`);
|
|
25
|
+
const topRegions = regions.slice(0, 5);
|
|
26
|
+
for (let i = 0; i < topRegions.length; i++) {
|
|
27
|
+
const r = topRegions[i];
|
|
28
|
+
parts.push(` ${i + 1}. ${r.description} — ${r.changedPixels} pixels (${r.percentChanged.toFixed(1)}% of region)`);
|
|
29
|
+
}
|
|
30
|
+
if (regions.length > 5) {
|
|
31
|
+
parts.push(` ... and ${regions.length - 5} more region(s)`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
status,
|
|
36
|
+
summary: parts.join('\n'),
|
|
37
|
+
percentChanged,
|
|
38
|
+
dimensions,
|
|
39
|
+
regions,
|
|
40
|
+
diffImagePath,
|
|
41
|
+
overlayImagePath,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function buildResponsiveReport(url, viewportResults) {
|
|
45
|
+
const parts = [`Responsive check for ${url} across ${viewportResults.length} viewport(s):\n`];
|
|
46
|
+
for (const { viewport, report } of viewportResults) {
|
|
47
|
+
parts.push(`--- ${viewport.width}x${viewport.height} ---`);
|
|
48
|
+
parts.push(report.summary);
|
|
49
|
+
parts.push('');
|
|
50
|
+
}
|
|
51
|
+
// Highlight cross-viewport differences
|
|
52
|
+
const statuses = viewportResults.map(v => v.report.status);
|
|
53
|
+
const uniqueStatuses = new Set(statuses);
|
|
54
|
+
if (uniqueStatuses.size > 1) {
|
|
55
|
+
parts.push('NOTE: Different viewports show different levels of change:');
|
|
56
|
+
for (const { viewport, report } of viewportResults) {
|
|
57
|
+
parts.push(` ${viewport.width}px: ${report.status} (${report.percentChanged.toFixed(2)}%)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
url,
|
|
62
|
+
viewports: viewportResults,
|
|
63
|
+
summary: parts.join('\n'),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=report-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report-builder.js","sourceRoot":"","sources":["../src/report-builder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ScreenshotOptions, Viewport } from './types.js';
|
|
2
|
+
export declare function takeScreenshot(options: ScreenshotOptions): Promise<Buffer>;
|
|
3
|
+
export declare function takeElementScreenshot(url: string, selector: string, viewport?: Viewport, waitForSelector?: string, waitForTimeout?: number): Promise<Buffer>;
|
|
4
|
+
export declare function closeBrowser(): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { chromium } from 'playwright';
|
|
2
|
+
import { DiffLensError } from './types.js';
|
|
3
|
+
const log = (msg) => process.stderr.write(`[difflens] ${msg}\n`);
|
|
4
|
+
let browserInstance = null;
|
|
5
|
+
let launchPromise = null;
|
|
6
|
+
const ANTI_ANIMATION_CSS = `
|
|
7
|
+
*, *::before, *::after {
|
|
8
|
+
animation-duration: 0s !important;
|
|
9
|
+
animation-delay: 0s !important;
|
|
10
|
+
transition-duration: 0s !important;
|
|
11
|
+
transition-delay: 0s !important;
|
|
12
|
+
caret-color: transparent !important;
|
|
13
|
+
scroll-behavior: auto !important;
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
async function ensureBrowser() {
|
|
17
|
+
if (browserInstance?.isConnected())
|
|
18
|
+
return browserInstance;
|
|
19
|
+
// Guard against duplicate launches from concurrent calls
|
|
20
|
+
if (launchPromise)
|
|
21
|
+
return launchPromise;
|
|
22
|
+
launchPromise = chromium.launch({
|
|
23
|
+
headless: true,
|
|
24
|
+
args: ['--disable-gpu', '--hide-scrollbars', '--no-sandbox'],
|
|
25
|
+
});
|
|
26
|
+
try {
|
|
27
|
+
browserInstance = await launchPromise;
|
|
28
|
+
log('Browser launched');
|
|
29
|
+
browserInstance.on('disconnected', () => {
|
|
30
|
+
browserInstance = null;
|
|
31
|
+
launchPromise = null;
|
|
32
|
+
log('Browser disconnected');
|
|
33
|
+
});
|
|
34
|
+
return browserInstance;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
launchPromise = null;
|
|
38
|
+
throw new DiffLensError('BROWSER_ERROR', `Failed to launch browser: ${err}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function createContext(viewport) {
|
|
42
|
+
const browser = await ensureBrowser();
|
|
43
|
+
return browser.newContext({
|
|
44
|
+
viewport: { width: viewport.width, height: viewport.height },
|
|
45
|
+
locale: 'en-US',
|
|
46
|
+
timezoneId: 'UTC',
|
|
47
|
+
reducedMotion: 'reduce',
|
|
48
|
+
deviceScaleFactor: 1,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async function navigateAndPrepare(page, url, options) {
|
|
52
|
+
try {
|
|
53
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
57
|
+
if (message.includes('ECONNREFUSED') || message.includes('ERR_CONNECTION_REFUSED')) {
|
|
58
|
+
throw new DiffLensError('CONNECTION_REFUSED', `Cannot connect to ${url}. Is the dev server running?`);
|
|
59
|
+
}
|
|
60
|
+
if (message.includes('Timeout') || message.includes('timeout')) {
|
|
61
|
+
throw new DiffLensError('TIMEOUT', `Page load timed out after 30s for ${url}`);
|
|
62
|
+
}
|
|
63
|
+
throw new DiffLensError('BROWSER_ERROR', `Navigation failed: ${message}`);
|
|
64
|
+
}
|
|
65
|
+
if (options.waitForSelector) {
|
|
66
|
+
try {
|
|
67
|
+
await page.waitForSelector(options.waitForSelector, { timeout: 10_000 });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new DiffLensError('SELECTOR_NOT_FOUND', `Selector "${options.waitForSelector}" not found within 10s`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options.waitForTimeout) {
|
|
74
|
+
await page.waitForTimeout(options.waitForTimeout);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function takeScreenshot(options) {
|
|
78
|
+
const viewport = options.viewport ?? { width: 1280, height: 720 };
|
|
79
|
+
const fullPage = options.fullPage ?? true;
|
|
80
|
+
log(`Screenshot: ${options.url} @ ${viewport.width}x${viewport.height}`);
|
|
81
|
+
const context = await createContext(viewport);
|
|
82
|
+
try {
|
|
83
|
+
const page = await context.newPage();
|
|
84
|
+
// Inject anti-animation CSS before any page JS runs
|
|
85
|
+
await page.addInitScript(`{
|
|
86
|
+
const style = document.createElement('style');
|
|
87
|
+
style.textContent = ${JSON.stringify(ANTI_ANIMATION_CSS)};
|
|
88
|
+
if (document.head) {
|
|
89
|
+
document.head.appendChild(style);
|
|
90
|
+
} else {
|
|
91
|
+
document.addEventListener('DOMContentLoaded', () => document.head.appendChild(style));
|
|
92
|
+
}
|
|
93
|
+
}`);
|
|
94
|
+
await navigateAndPrepare(page, options.url, options);
|
|
95
|
+
const buffer = await page.screenshot({
|
|
96
|
+
fullPage,
|
|
97
|
+
type: 'png',
|
|
98
|
+
animations: 'disabled',
|
|
99
|
+
});
|
|
100
|
+
return Buffer.from(buffer);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await context.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export async function takeElementScreenshot(url, selector, viewport, waitForSelector, waitForTimeout) {
|
|
107
|
+
const vp = viewport ?? { width: 1280, height: 720 };
|
|
108
|
+
log(`Element screenshot: ${selector} @ ${url}`);
|
|
109
|
+
const context = await createContext(vp);
|
|
110
|
+
try {
|
|
111
|
+
const page = await context.newPage();
|
|
112
|
+
await page.addInitScript(`{
|
|
113
|
+
const style = document.createElement('style');
|
|
114
|
+
style.textContent = ${JSON.stringify(ANTI_ANIMATION_CSS)};
|
|
115
|
+
if (document.head) {
|
|
116
|
+
document.head.appendChild(style);
|
|
117
|
+
} else {
|
|
118
|
+
document.addEventListener('DOMContentLoaded', () => document.head.appendChild(style));
|
|
119
|
+
}
|
|
120
|
+
}`);
|
|
121
|
+
await navigateAndPrepare(page, url, { waitForSelector, waitForTimeout });
|
|
122
|
+
const locator = page.locator(selector);
|
|
123
|
+
const count = await locator.count();
|
|
124
|
+
if (count === 0) {
|
|
125
|
+
throw new DiffLensError('SELECTOR_NOT_FOUND', `Element "${selector}" not found on page`);
|
|
126
|
+
}
|
|
127
|
+
const buffer = await locator.first().screenshot({ type: 'png', animations: 'disabled' });
|
|
128
|
+
return Buffer.from(buffer);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await context.close();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function closeBrowser() {
|
|
135
|
+
if (browserInstance?.isConnected()) {
|
|
136
|
+
await browserInstance.close();
|
|
137
|
+
log('Browser closed');
|
|
138
|
+
}
|
|
139
|
+
browserInstance = null;
|
|
140
|
+
launchPromise = null;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=screenshotter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshotter.js","sourceRoot":"","sources":["../src/screenshotter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SnapshotMetadata, ListSnapshotsOptions, CleanupOptions, CleanupResult } from './types.js';
|
|
2
|
+
export declare function ensureStorageDirs(): Promise<void>;
|
|
3
|
+
export declare function getSnapshotsDir(): string;
|
|
4
|
+
export declare function getDiffsDir(): string;
|
|
5
|
+
export declare function generateSnapshotId(): string;
|
|
6
|
+
export declare function saveSnapshot(id: string, imageBuffer: Buffer, metadata: Omit<SnapshotMetadata, 'imagePath'>): Promise<SnapshotMetadata>;
|
|
7
|
+
export declare function loadSnapshotMetadata(id: string): Promise<SnapshotMetadata>;
|
|
8
|
+
export declare function loadSnapshotImage(id: string): Promise<Buffer>;
|
|
9
|
+
export declare function findLatestSnapshot(url: string): Promise<SnapshotMetadata | null>;
|
|
10
|
+
export declare function listSnapshots(options?: ListSnapshotsOptions): Promise<SnapshotMetadata[]>;
|
|
11
|
+
export declare function cleanupSnapshots(options?: CleanupOptions): Promise<CleanupResult>;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile, rename, unlink, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { DiffLensError } from './types.js';
|
|
5
|
+
const STORAGE_ROOT = '.difflens';
|
|
6
|
+
const SNAPSHOTS_DIR = join(STORAGE_ROOT, 'snapshots');
|
|
7
|
+
const DIFFS_DIR = join(STORAGE_ROOT, 'diffs');
|
|
8
|
+
// Sequential write queue for concurrent safety
|
|
9
|
+
let writeQueue = Promise.resolve();
|
|
10
|
+
function enqueueWrite(fn) {
|
|
11
|
+
const result = writeQueue.then(fn);
|
|
12
|
+
writeQueue = result.then(() => { }, () => { });
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
export async function ensureStorageDirs() {
|
|
16
|
+
await mkdir(SNAPSHOTS_DIR, { recursive: true });
|
|
17
|
+
await mkdir(DIFFS_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
export function getSnapshotsDir() {
|
|
20
|
+
return SNAPSHOTS_DIR;
|
|
21
|
+
}
|
|
22
|
+
export function getDiffsDir() {
|
|
23
|
+
return DIFFS_DIR;
|
|
24
|
+
}
|
|
25
|
+
export function generateSnapshotId() {
|
|
26
|
+
const timestamp = Date.now();
|
|
27
|
+
const hex = randomBytes(4).toString('hex');
|
|
28
|
+
return `${timestamp}-${hex}`;
|
|
29
|
+
}
|
|
30
|
+
export async function saveSnapshot(id, imageBuffer, metadata) {
|
|
31
|
+
return enqueueWrite(async () => {
|
|
32
|
+
const imagePath = join(SNAPSHOTS_DIR, `${id}.png`);
|
|
33
|
+
const metadataPath = join(SNAPSHOTS_DIR, `${id}.json`);
|
|
34
|
+
const tmpImagePath = `${imagePath}.tmp`;
|
|
35
|
+
const tmpMetadataPath = `${metadataPath}.tmp`;
|
|
36
|
+
const fullMetadata = { ...metadata, imagePath };
|
|
37
|
+
// Atomic writes: write to .tmp then rename
|
|
38
|
+
await writeFile(tmpImagePath, imageBuffer);
|
|
39
|
+
await rename(tmpImagePath, imagePath);
|
|
40
|
+
await writeFile(tmpMetadataPath, JSON.stringify(fullMetadata, null, 2));
|
|
41
|
+
await rename(tmpMetadataPath, metadataPath);
|
|
42
|
+
return fullMetadata;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function loadSnapshotMetadata(id) {
|
|
46
|
+
const metadataPath = join(SNAPSHOTS_DIR, `${id}.json`);
|
|
47
|
+
try {
|
|
48
|
+
const data = await readFile(metadataPath, 'utf-8');
|
|
49
|
+
return JSON.parse(data);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
throw new DiffLensError('SNAPSHOT_NOT_FOUND', `Snapshot "${id}" not found`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export async function loadSnapshotImage(id) {
|
|
56
|
+
const imagePath = join(SNAPSHOTS_DIR, `${id}.png`);
|
|
57
|
+
try {
|
|
58
|
+
return await readFile(imagePath);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
throw new DiffLensError('SNAPSHOT_NOT_FOUND', `Snapshot image "${id}" not found`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function findLatestSnapshot(url) {
|
|
65
|
+
const snapshots = await listSnapshots({ url });
|
|
66
|
+
if (snapshots.length === 0)
|
|
67
|
+
return null;
|
|
68
|
+
// Already sorted by timestamp descending
|
|
69
|
+
return snapshots[0];
|
|
70
|
+
}
|
|
71
|
+
export async function listSnapshots(options = {}) {
|
|
72
|
+
try {
|
|
73
|
+
const files = await readdir(SNAPSHOTS_DIR);
|
|
74
|
+
const metadataFiles = files.filter(f => f.endsWith('.json'));
|
|
75
|
+
const snapshots = [];
|
|
76
|
+
for (const file of metadataFiles) {
|
|
77
|
+
try {
|
|
78
|
+
const data = await readFile(join(SNAPSHOTS_DIR, file), 'utf-8');
|
|
79
|
+
const metadata = JSON.parse(data);
|
|
80
|
+
if (options.url && metadata.url !== options.url)
|
|
81
|
+
continue;
|
|
82
|
+
snapshots.push(metadata);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Skip corrupted metadata files
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Sort by timestamp descending (newest first)
|
|
89
|
+
snapshots.sort((a, b) => b.timestamp - a.timestamp);
|
|
90
|
+
if (options.limit && options.limit > 0) {
|
|
91
|
+
return snapshots.slice(0, options.limit);
|
|
92
|
+
}
|
|
93
|
+
return snapshots;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function cleanupSnapshots(options = {}) {
|
|
100
|
+
const { maxAge, maxCount, dryRun = false } = options;
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
const allSnapshots = await listSnapshots();
|
|
103
|
+
const toDelete = [];
|
|
104
|
+
const toKeep = [];
|
|
105
|
+
for (const snapshot of allSnapshots) {
|
|
106
|
+
let shouldDelete = false;
|
|
107
|
+
if (maxAge !== undefined && (now - snapshot.timestamp) > maxAge) {
|
|
108
|
+
shouldDelete = true;
|
|
109
|
+
}
|
|
110
|
+
if (shouldDelete) {
|
|
111
|
+
toDelete.push(snapshot.id);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
toKeep.push(snapshot);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// If maxCount is specified, mark excess snapshots for deletion (oldest first)
|
|
118
|
+
if (maxCount !== undefined && toKeep.length > maxCount) {
|
|
119
|
+
const excess = toKeep.splice(maxCount);
|
|
120
|
+
for (const snapshot of excess) {
|
|
121
|
+
toDelete.push(snapshot.id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!dryRun) {
|
|
125
|
+
for (const id of toDelete) {
|
|
126
|
+
try {
|
|
127
|
+
await unlink(join(SNAPSHOTS_DIR, `${id}.png`));
|
|
128
|
+
}
|
|
129
|
+
catch { /* ignore */ }
|
|
130
|
+
try {
|
|
131
|
+
await unlink(join(SNAPSHOTS_DIR, `${id}.json`));
|
|
132
|
+
}
|
|
133
|
+
catch { /* ignore */ }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
deleted: toDelete,
|
|
138
|
+
kept: toKeep.length,
|
|
139
|
+
dryRun,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface Viewport {
|
|
2
|
+
width: number;
|
|
3
|
+
height: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ScreenshotOptions {
|
|
6
|
+
url: string;
|
|
7
|
+
viewport?: Viewport;
|
|
8
|
+
fullPage?: boolean;
|
|
9
|
+
selector?: string;
|
|
10
|
+
waitForSelector?: string;
|
|
11
|
+
waitForTimeout?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface SnapshotMetadata {
|
|
14
|
+
id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
viewport: Viewport;
|
|
17
|
+
fullPage: boolean;
|
|
18
|
+
selector?: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
imagePath: string;
|
|
21
|
+
}
|
|
22
|
+
export interface DiffRegion {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
changedPixels: number;
|
|
28
|
+
percentChanged: number;
|
|
29
|
+
description: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DiffResult {
|
|
32
|
+
totalPixels: number;
|
|
33
|
+
changedPixels: number;
|
|
34
|
+
percentChanged: number;
|
|
35
|
+
dimensions: {
|
|
36
|
+
before: Viewport;
|
|
37
|
+
after: Viewport;
|
|
38
|
+
resized: boolean;
|
|
39
|
+
};
|
|
40
|
+
regions: DiffRegion[];
|
|
41
|
+
diffImagePath: string;
|
|
42
|
+
overlayImagePath: string;
|
|
43
|
+
}
|
|
44
|
+
export interface DiffReport {
|
|
45
|
+
status: 'no_changes' | 'minor' | 'significant' | 'major';
|
|
46
|
+
summary: string;
|
|
47
|
+
percentChanged: number;
|
|
48
|
+
dimensions: DiffResult['dimensions'];
|
|
49
|
+
regions: DiffRegion[];
|
|
50
|
+
diffImagePath: string;
|
|
51
|
+
overlayImagePath: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ResponsiveCheckResult {
|
|
54
|
+
url: string;
|
|
55
|
+
viewports: Array<{
|
|
56
|
+
viewport: Viewport;
|
|
57
|
+
report: DiffReport;
|
|
58
|
+
}>;
|
|
59
|
+
summary: string;
|
|
60
|
+
}
|
|
61
|
+
export interface ListSnapshotsOptions {
|
|
62
|
+
url?: string;
|
|
63
|
+
limit?: number;
|
|
64
|
+
}
|
|
65
|
+
export interface CleanupOptions {
|
|
66
|
+
maxAge?: number;
|
|
67
|
+
maxCount?: number;
|
|
68
|
+
dryRun?: boolean;
|
|
69
|
+
}
|
|
70
|
+
export interface CleanupResult {
|
|
71
|
+
deleted: string[];
|
|
72
|
+
kept: number;
|
|
73
|
+
dryRun: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface LayoutElement {
|
|
76
|
+
selector: string;
|
|
77
|
+
tagName: string;
|
|
78
|
+
id?: string;
|
|
79
|
+
boundingBox: {
|
|
80
|
+
x: number;
|
|
81
|
+
y: number;
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
};
|
|
85
|
+
styles: Record<string, string>;
|
|
86
|
+
}
|
|
87
|
+
export interface LayoutChange {
|
|
88
|
+
selector: string;
|
|
89
|
+
type: 'moved' | 'resized' | 'style_changed' | 'added' | 'removed';
|
|
90
|
+
description: string;
|
|
91
|
+
before?: Partial<LayoutElement>;
|
|
92
|
+
after?: Partial<LayoutElement>;
|
|
93
|
+
}
|
|
94
|
+
export type DiffLensErrorCode = 'CONNECTION_REFUSED' | 'TIMEOUT' | 'SELECTOR_NOT_FOUND' | 'SNAPSHOT_NOT_FOUND' | 'NO_BASELINE' | 'STORAGE_ERROR' | 'BROWSER_ERROR' | 'INVALID_INPUT';
|
|
95
|
+
export declare class DiffLensError extends Error {
|
|
96
|
+
code: DiffLensErrorCode;
|
|
97
|
+
constructor(code: DiffLensErrorCode, message: string);
|
|
98
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA8GA,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC,IAAI,CAAoB;IAExB,YAAY,IAAuB,EAAE,OAAe;QAClD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "difflens-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that gives AI coding agents visual diff capabilities for UI work",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"difflens": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"postinstall": "npx playwright install chromium",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"mcp-server",
|
|
22
|
+
"claude",
|
|
23
|
+
"visual-diff",
|
|
24
|
+
"screenshot",
|
|
25
|
+
"ui-testing",
|
|
26
|
+
"visual-regression",
|
|
27
|
+
"playwright"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/byzkhan/difflens.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/byzkhan/difflens",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/byzkhan/difflens/issues"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
43
|
+
"chokidar": "^5.0.0",
|
|
44
|
+
"pixelmatch": "^7.1.0",
|
|
45
|
+
"playwright": "^1.58.2",
|
|
46
|
+
"pngjs": "^7.0.0",
|
|
47
|
+
"sharp": "^0.34.5",
|
|
48
|
+
"zod": "^4.3.6"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^25.3.2",
|
|
52
|
+
"@types/pngjs": "^6.0.5",
|
|
53
|
+
"typescript": "^5.9.3"
|
|
54
|
+
}
|
|
55
|
+
}
|