afterbefore 0.1.2 → 0.1.4
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 +114 -1
- package/dist/cli.js +37 -132
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +36 -131
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/pipeline.ts
|
|
2
2
|
import { resolve as resolve4 } from "path";
|
|
3
|
+
import { unlinkSync } from "fs";
|
|
3
4
|
import { exec } from "child_process";
|
|
4
|
-
import chalk2 from "chalk";
|
|
5
5
|
|
|
6
6
|
// src/logger.ts
|
|
7
7
|
import chalk from "chalk";
|
|
@@ -100,11 +100,9 @@ var Logger = class {
|
|
|
100
100
|
this.pipelineActive = false;
|
|
101
101
|
if (this.spinner) {
|
|
102
102
|
if (finished) {
|
|
103
|
+
this.spinner.stop();
|
|
103
104
|
const bar = "\u2588".repeat(BAR_WIDTH);
|
|
104
|
-
|
|
105
|
-
symbol: chalk.green("\u2714"),
|
|
106
|
-
text: bar
|
|
107
|
-
});
|
|
105
|
+
console.log(`${chalk.green("\u2714")} ${bar}`);
|
|
108
106
|
} else {
|
|
109
107
|
this.spinner.stop();
|
|
110
108
|
}
|
|
@@ -1467,7 +1465,6 @@ async function captureRoutes(tasks, beforeUrl, afterUrl, outputDir, options) {
|
|
|
1467
1465
|
|
|
1468
1466
|
// src/stages/compare.ts
|
|
1469
1467
|
import { readFileSync as readFileSync5 } from "fs";
|
|
1470
|
-
import { writeFileSync } from "fs";
|
|
1471
1468
|
import { join as join7, dirname as dirname2 } from "path";
|
|
1472
1469
|
import { PNG } from "pngjs";
|
|
1473
1470
|
import pixelmatch from "pixelmatch";
|
|
@@ -1515,9 +1512,7 @@ async function generateComposite(beforePath, afterPath, outputPath, browser, bgC
|
|
|
1515
1512
|
}
|
|
1516
1513
|
async function compareOne(capture, outputDir, threshold, options) {
|
|
1517
1514
|
const dir = dirname2(capture.beforePath);
|
|
1518
|
-
const diffPath = join7(dir, `${capture.prefix}-diff.png`);
|
|
1519
1515
|
const comparePath = join7(dir, `${capture.prefix}-compare.png`);
|
|
1520
|
-
const sliderPath = join7(dir, `${capture.prefix}-slider.html`);
|
|
1521
1516
|
const beforeBuffer = readFileSync5(capture.beforePath);
|
|
1522
1517
|
const afterBuffer = readFileSync5(capture.afterPath);
|
|
1523
1518
|
const beforeImg = PNG.sync.read(beforeBuffer);
|
|
@@ -1525,35 +1520,29 @@ async function compareOne(capture, outputDir, threshold, options) {
|
|
|
1525
1520
|
const [normBefore, normAfter] = normalizeDimensions(beforeImg, afterImg);
|
|
1526
1521
|
const { width, height } = normBefore;
|
|
1527
1522
|
const totalPixels = width * height;
|
|
1528
|
-
const diffImg = new PNG({ width, height });
|
|
1529
1523
|
const diffPixels = pixelmatch(
|
|
1530
1524
|
normBefore.data,
|
|
1531
1525
|
normAfter.data,
|
|
1532
|
-
|
|
1526
|
+
new Uint8Array(width * height * 4),
|
|
1533
1527
|
width,
|
|
1534
1528
|
height,
|
|
1535
1529
|
{ threshold: 0.1 }
|
|
1536
1530
|
);
|
|
1537
1531
|
const diffPercentage = diffPixels / totalPixels * 100;
|
|
1538
1532
|
const changed = diffPercentage > threshold;
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
options.bgColor
|
|
1547
|
-
);
|
|
1548
|
-
}
|
|
1533
|
+
await generateComposite(
|
|
1534
|
+
capture.beforePath,
|
|
1535
|
+
capture.afterPath,
|
|
1536
|
+
comparePath,
|
|
1537
|
+
options.browser,
|
|
1538
|
+
options.bgColor
|
|
1539
|
+
);
|
|
1549
1540
|
return {
|
|
1550
1541
|
route: capture.route,
|
|
1551
1542
|
prefix: capture.prefix,
|
|
1552
1543
|
beforePath: capture.beforePath,
|
|
1553
1544
|
afterPath: capture.afterPath,
|
|
1554
|
-
diffPath,
|
|
1555
1545
|
comparePath,
|
|
1556
|
-
sliderPath,
|
|
1557
1546
|
diffPixels,
|
|
1558
1547
|
totalPixels,
|
|
1559
1548
|
diffPercentage,
|
|
@@ -1569,13 +1558,12 @@ async function compareScreenshots(captures, outputDir, threshold = 0.1, options)
|
|
|
1569
1558
|
}
|
|
1570
1559
|
|
|
1571
1560
|
// src/stages/report.ts
|
|
1572
|
-
import { writeFileSync
|
|
1561
|
+
import { writeFileSync } from "fs";
|
|
1573
1562
|
import { join as join8 } from "path";
|
|
1574
1563
|
import { execSync as execSync4 } from "child_process";
|
|
1575
1564
|
|
|
1576
1565
|
// src/templates/report.html.ts
|
|
1577
1566
|
import { readFileSync as readFileSync6 } from "fs";
|
|
1578
|
-
import { relative as relative2 } from "path";
|
|
1579
1567
|
function toBase64(filePath) {
|
|
1580
1568
|
return readFileSync6(filePath).toString("base64");
|
|
1581
1569
|
}
|
|
@@ -1586,7 +1574,6 @@ function generateReportHtml(results, outputDir) {
|
|
|
1586
1574
|
const changed = results.filter((r) => r.changed);
|
|
1587
1575
|
const unchanged = results.filter((r) => !r.changed);
|
|
1588
1576
|
const card = (r) => {
|
|
1589
|
-
const sliderHref = outputDir ? relative2(outputDir, r.sliderPath) : r.sliderPath;
|
|
1590
1577
|
return `
|
|
1591
1578
|
<div class="card ${r.changed ? "changed" : "unchanged"}">
|
|
1592
1579
|
<div class="card-header">
|
|
@@ -1604,12 +1591,7 @@ function generateReportHtml(results, outputDir) {
|
|
|
1604
1591
|
<div class="label">After</div>
|
|
1605
1592
|
<img src="${imgSrc(r.afterPath)}" alt="After" />
|
|
1606
1593
|
</div>
|
|
1607
|
-
<div class="img-col">
|
|
1608
|
-
<div class="label">Diff</div>
|
|
1609
|
-
<img src="${imgSrc(r.diffPath)}" alt="Diff" />
|
|
1610
|
-
</div>
|
|
1611
1594
|
</div>
|
|
1612
|
-
${r.changed ? `<a class="slider-link" href="${sliderHref}">Open slider view</a>` : ""}
|
|
1613
1595
|
</div>`;
|
|
1614
1596
|
};
|
|
1615
1597
|
return `<!DOCTYPE html>
|
|
@@ -1631,12 +1613,10 @@ function generateReportHtml(results, outputDir) {
|
|
|
1631
1613
|
.badge { font-size: 12px; padding: 2px 8px; border-radius: 9999px; font-weight: 500; }
|
|
1632
1614
|
.badge-changed { background: #fef3c7; color: #92400e; }
|
|
1633
1615
|
.badge-unchanged { background: #d1fae5; color: #065f46; }
|
|
1634
|
-
.images { display: grid; grid-template-columns: 1fr 1fr
|
|
1616
|
+
.images { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: #e5e7eb; }
|
|
1635
1617
|
.img-col { background: #fff; padding: 8px; }
|
|
1636
1618
|
.img-col img { width: 100%; height: auto; display: block; border-radius: 4px; }
|
|
1637
1619
|
.label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; margin-bottom: 4px; font-weight: 600; }
|
|
1638
|
-
.slider-link { display: block; padding: 8px 16px; text-align: center; font-size: 13px; color: #2563eb; text-decoration: none; border-top: 1px solid #e5e7eb; }
|
|
1639
|
-
.slider-link:hover { background: #eff6ff; }
|
|
1640
1620
|
.section-title { font-size: 18px; font-weight: 600; margin: 24px 0 12px; }
|
|
1641
1621
|
</style>
|
|
1642
1622
|
</head>
|
|
@@ -1651,83 +1631,6 @@ ${unchanged.length > 0 ? `<h2 class="section-title">Unchanged (${unchanged.lengt
|
|
|
1651
1631
|
</html>`;
|
|
1652
1632
|
}
|
|
1653
1633
|
|
|
1654
|
-
// src/templates/slider.html.ts
|
|
1655
|
-
import { readFileSync as readFileSync7 } from "fs";
|
|
1656
|
-
function toBase642(filePath) {
|
|
1657
|
-
return readFileSync7(filePath).toString("base64");
|
|
1658
|
-
}
|
|
1659
|
-
function imgSrc2(filePath) {
|
|
1660
|
-
return `data:image/png;base64,${toBase642(filePath)}`;
|
|
1661
|
-
}
|
|
1662
|
-
function generateSliderHtml(result) {
|
|
1663
|
-
return `<!DOCTYPE html>
|
|
1664
|
-
<html lang="en">
|
|
1665
|
-
<head>
|
|
1666
|
-
<meta charset="utf-8" />
|
|
1667
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1668
|
-
<title>afterbefore Slider - ${result.route}</title>
|
|
1669
|
-
<style>
|
|
1670
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1671
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f9fafb; color: #111827; padding: 24px; }
|
|
1672
|
-
h1 { font-size: 20px; margin-bottom: 4px; }
|
|
1673
|
-
.meta { color: #6b7280; font-size: 13px; margin-bottom: 16px; }
|
|
1674
|
-
.container { position: relative; overflow: hidden; border-radius: 8px; border: 1px solid #e5e7eb; user-select: none; cursor: ew-resize; }
|
|
1675
|
-
.container img { display: block; width: 100%; height: auto; }
|
|
1676
|
-
.before-wrap { position: absolute; top: 0; left: 0; height: 100%; overflow: hidden; }
|
|
1677
|
-
.before-wrap img { display: block; height: 100%; width: auto; min-width: 100%; object-fit: cover; }
|
|
1678
|
-
.slider-line { position: absolute; top: 0; width: 3px; height: 100%; background: #2563eb; cursor: ew-resize; z-index: 10; }
|
|
1679
|
-
.slider-handle { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 36px; height: 36px; background: #2563eb; border-radius: 50%; border: 3px solid #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; }
|
|
1680
|
-
.slider-handle::before { content: '\\2194'; color: #fff; font-size: 18px; }
|
|
1681
|
-
.labels { display: flex; justify-content: space-between; margin-top: 8px; font-size: 12px; color: #6b7280; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
1682
|
-
</style>
|
|
1683
|
-
</head>
|
|
1684
|
-
<body>
|
|
1685
|
-
<h1>${result.route}</h1>
|
|
1686
|
-
<p class="meta">${result.diffPercentage.toFixed(2)}% changed (${result.diffPixels.toLocaleString()} pixels)</p>
|
|
1687
|
-
|
|
1688
|
-
<div class="container" id="slider">
|
|
1689
|
-
<img src="${imgSrc2(result.afterPath)}" alt="After" draggable="false" />
|
|
1690
|
-
<div class="before-wrap" id="beforeWrap">
|
|
1691
|
-
<img src="${imgSrc2(result.beforePath)}" alt="Before" draggable="false" />
|
|
1692
|
-
</div>
|
|
1693
|
-
<div class="slider-line" id="sliderLine">
|
|
1694
|
-
<div class="slider-handle"></div>
|
|
1695
|
-
</div>
|
|
1696
|
-
</div>
|
|
1697
|
-
<div class="labels"><span>Before</span><span>After</span></div>
|
|
1698
|
-
|
|
1699
|
-
<script>
|
|
1700
|
-
(function() {
|
|
1701
|
-
const container = document.getElementById('slider');
|
|
1702
|
-
const beforeWrap = document.getElementById('beforeWrap');
|
|
1703
|
-
const sliderLine = document.getElementById('sliderLine');
|
|
1704
|
-
let dragging = false;
|
|
1705
|
-
|
|
1706
|
-
function setPosition(x) {
|
|
1707
|
-
const rect = container.getBoundingClientRect();
|
|
1708
|
-
let pct = ((x - rect.left) / rect.width) * 100;
|
|
1709
|
-
pct = Math.max(0, Math.min(100, pct));
|
|
1710
|
-
beforeWrap.style.width = pct + '%';
|
|
1711
|
-
sliderLine.style.left = pct + '%';
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
// Start at 50%
|
|
1715
|
-
beforeWrap.style.width = '50%';
|
|
1716
|
-
sliderLine.style.left = '50%';
|
|
1717
|
-
|
|
1718
|
-
container.addEventListener('mousedown', function(e) { dragging = true; setPosition(e.clientX); });
|
|
1719
|
-
window.addEventListener('mousemove', function(e) { if (dragging) setPosition(e.clientX); });
|
|
1720
|
-
window.addEventListener('mouseup', function() { dragging = false; });
|
|
1721
|
-
|
|
1722
|
-
container.addEventListener('touchstart', function(e) { dragging = true; setPosition(e.touches[0].clientX); }, { passive: true });
|
|
1723
|
-
window.addEventListener('touchmove', function(e) { if (dragging) setPosition(e.touches[0].clientX); }, { passive: true });
|
|
1724
|
-
window.addEventListener('touchend', function() { dragging = false; });
|
|
1725
|
-
})();
|
|
1726
|
-
</script>
|
|
1727
|
-
</body>
|
|
1728
|
-
</html>`;
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
1634
|
// src/templates/summary.md.ts
|
|
1732
1635
|
function generateSummaryMd(results, gitDiff, options) {
|
|
1733
1636
|
const includeFilePaths = options?.includeFilePaths ?? true;
|
|
@@ -1828,18 +1731,12 @@ async function generateReport(results, outputDir, options) {
|
|
|
1828
1731
|
await ensureDir(outputDir);
|
|
1829
1732
|
const summaryMd = generateSummaryMd(results);
|
|
1830
1733
|
const summaryPath = join8(outputDir, "summary.md");
|
|
1831
|
-
|
|
1734
|
+
writeFileSync(summaryPath, summaryMd, "utf-8");
|
|
1832
1735
|
logger.success(`Written summary to ${summaryPath}`);
|
|
1833
1736
|
const reportHtml = generateReportHtml(results, outputDir);
|
|
1834
1737
|
const indexPath = join8(outputDir, "index.html");
|
|
1835
|
-
|
|
1738
|
+
writeFileSync(indexPath, reportHtml, "utf-8");
|
|
1836
1739
|
logger.success(`Written report to ${indexPath}`);
|
|
1837
|
-
for (const result of results) {
|
|
1838
|
-
if (!result.changed) continue;
|
|
1839
|
-
const sliderHtml = generateSliderHtml(result);
|
|
1840
|
-
writeFileSync2(result.sliderPath, sliderHtml, "utf-8");
|
|
1841
|
-
logger.dim(`Written slider for ${result.route}`);
|
|
1842
|
-
}
|
|
1843
1740
|
if (options.post) {
|
|
1844
1741
|
const prNumber = findPrNumber();
|
|
1845
1742
|
if (!prNumber) {
|
|
@@ -1855,10 +1752,6 @@ async function generateReport(results, outputDir, options) {
|
|
|
1855
1752
|
}
|
|
1856
1753
|
|
|
1857
1754
|
// src/pipeline.ts
|
|
1858
|
-
function formatTriggerChain(chain) {
|
|
1859
|
-
if (chain.length <= 1) return chain[0] ?? "";
|
|
1860
|
-
return chain.join(" \u2192 ");
|
|
1861
|
-
}
|
|
1862
1755
|
function generateSessionName(cwd) {
|
|
1863
1756
|
const branch = getCurrentBranch(cwd);
|
|
1864
1757
|
const name = branch.replace(/^(feat|fix|perf|chore|refactor|docs|style|test|ci|build)\//, "");
|
|
@@ -1945,7 +1838,7 @@ async function runPipeline(options) {
|
|
|
1945
1838
|
const outputDir = resolve4(cwd, output, sessionName);
|
|
1946
1839
|
const startTime = Date.now();
|
|
1947
1840
|
try {
|
|
1948
|
-
const version = true ? "0.1.
|
|
1841
|
+
const version = true ? "0.1.4" : "dev";
|
|
1949
1842
|
console.log(`
|
|
1950
1843
|
afterbefore v${version} \xB7 Comparing against ${base}
|
|
1951
1844
|
`);
|
|
@@ -2012,13 +1905,6 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
2012
1905
|
);
|
|
2013
1906
|
return;
|
|
2014
1907
|
}
|
|
2015
|
-
console.log();
|
|
2016
|
-
for (const r of affectedRoutes) {
|
|
2017
|
-
const chain = formatTriggerChain(r.triggerChain);
|
|
2018
|
-
const label = r.reason === "direct" ? "(direct)" : `\u2190 ${chain}`;
|
|
2019
|
-
console.log(chalk2.dim(` ${r.route} ${label}`));
|
|
2020
|
-
}
|
|
2021
|
-
console.log();
|
|
2022
1908
|
logger.pipeline(4, "Setting up worktree...");
|
|
2023
1909
|
const worktree = await worktreePromise;
|
|
2024
1910
|
logger.pipeline(5, "Starting servers...");
|
|
@@ -2055,7 +1941,26 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
2055
1941
|
);
|
|
2056
1942
|
logger.pipeline(7, "Comparing screenshots...");
|
|
2057
1943
|
const bgColor = detectBgColor(cwd);
|
|
2058
|
-
const
|
|
1944
|
+
const allResults = await compareScreenshots(captures, outputDir, options.threshold, { browser, bgColor });
|
|
1945
|
+
const results = allResults.filter((r) => {
|
|
1946
|
+
const isSubCapture = r.prefix.includes("~");
|
|
1947
|
+
if (isSubCapture && !r.changed) {
|
|
1948
|
+
try {
|
|
1949
|
+
unlinkSync(r.beforePath);
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1952
|
+
try {
|
|
1953
|
+
unlinkSync(r.afterPath);
|
|
1954
|
+
} catch {
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
unlinkSync(r.comparePath);
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
return true;
|
|
1963
|
+
});
|
|
2059
1964
|
logger.pipeline(8, "Generating report...");
|
|
2060
1965
|
await generateReport(results, outputDir, { post });
|
|
2061
1966
|
const summary = generateSummaryMd(results, gitDiff);
|