afterbefore 0.1.3 → 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/dist/cli.js CHANGED
@@ -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
- this.spinner.stopAndPersist({
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
  }
@@ -223,8 +221,8 @@ function getCurrentBranch(cwd) {
223
221
 
224
222
  // src/pipeline.ts
225
223
  import { resolve as resolve4 } from "path";
224
+ import { unlinkSync } from "fs";
226
225
  import { exec } from "child_process";
227
- import chalk2 from "chalk";
228
226
 
229
227
  // src/config.ts
230
228
  import { resolve } from "path";
@@ -1480,7 +1478,6 @@ async function captureRoutes(tasks, beforeUrl, afterUrl, outputDir, options) {
1480
1478
 
1481
1479
  // src/stages/compare.ts
1482
1480
  import { readFileSync as readFileSync5 } from "fs";
1483
- import { writeFileSync } from "fs";
1484
1481
  import { join as join7, dirname as dirname2 } from "path";
1485
1482
  import { PNG } from "pngjs";
1486
1483
  import pixelmatch from "pixelmatch";
@@ -1528,9 +1525,7 @@ async function generateComposite(beforePath, afterPath, outputPath, browser, bgC
1528
1525
  }
1529
1526
  async function compareOne(capture, outputDir, threshold, options) {
1530
1527
  const dir = dirname2(capture.beforePath);
1531
- const diffPath = join7(dir, `${capture.prefix}-diff.png`);
1532
1528
  const comparePath = join7(dir, `${capture.prefix}-compare.png`);
1533
- const sliderPath = join7(dir, `${capture.prefix}-slider.html`);
1534
1529
  const beforeBuffer = readFileSync5(capture.beforePath);
1535
1530
  const afterBuffer = readFileSync5(capture.afterPath);
1536
1531
  const beforeImg = PNG.sync.read(beforeBuffer);
@@ -1538,35 +1533,29 @@ async function compareOne(capture, outputDir, threshold, options) {
1538
1533
  const [normBefore, normAfter] = normalizeDimensions(beforeImg, afterImg);
1539
1534
  const { width, height } = normBefore;
1540
1535
  const totalPixels = width * height;
1541
- const diffImg = new PNG({ width, height });
1542
1536
  const diffPixels = pixelmatch(
1543
1537
  normBefore.data,
1544
1538
  normAfter.data,
1545
- diffImg.data,
1539
+ new Uint8Array(width * height * 4),
1546
1540
  width,
1547
1541
  height,
1548
1542
  { threshold: 0.1 }
1549
1543
  );
1550
1544
  const diffPercentage = diffPixels / totalPixels * 100;
1551
1545
  const changed = diffPercentage > threshold;
1552
- writeFileSync(diffPath, PNG.sync.write(diffImg));
1553
- if (changed) {
1554
- await generateComposite(
1555
- capture.beforePath,
1556
- capture.afterPath,
1557
- comparePath,
1558
- options.browser,
1559
- options.bgColor
1560
- );
1561
- }
1546
+ await generateComposite(
1547
+ capture.beforePath,
1548
+ capture.afterPath,
1549
+ comparePath,
1550
+ options.browser,
1551
+ options.bgColor
1552
+ );
1562
1553
  return {
1563
1554
  route: capture.route,
1564
1555
  prefix: capture.prefix,
1565
1556
  beforePath: capture.beforePath,
1566
1557
  afterPath: capture.afterPath,
1567
- diffPath,
1568
1558
  comparePath,
1569
- sliderPath,
1570
1559
  diffPixels,
1571
1560
  totalPixels,
1572
1561
  diffPercentage,
@@ -1582,13 +1571,12 @@ async function compareScreenshots(captures, outputDir, threshold = 0.1, options)
1582
1571
  }
1583
1572
 
1584
1573
  // src/stages/report.ts
1585
- import { writeFileSync as writeFileSync2 } from "fs";
1574
+ import { writeFileSync } from "fs";
1586
1575
  import { join as join8 } from "path";
1587
1576
  import { execSync as execSync4 } from "child_process";
1588
1577
 
1589
1578
  // src/templates/report.html.ts
1590
1579
  import { readFileSync as readFileSync6 } from "fs";
1591
- import { relative as relative2 } from "path";
1592
1580
  function toBase64(filePath) {
1593
1581
  return readFileSync6(filePath).toString("base64");
1594
1582
  }
@@ -1599,7 +1587,6 @@ function generateReportHtml(results, outputDir) {
1599
1587
  const changed = results.filter((r) => r.changed);
1600
1588
  const unchanged = results.filter((r) => !r.changed);
1601
1589
  const card = (r) => {
1602
- const sliderHref = outputDir ? relative2(outputDir, r.sliderPath) : r.sliderPath;
1603
1590
  return `
1604
1591
  <div class="card ${r.changed ? "changed" : "unchanged"}">
1605
1592
  <div class="card-header">
@@ -1617,12 +1604,7 @@ function generateReportHtml(results, outputDir) {
1617
1604
  <div class="label">After</div>
1618
1605
  <img src="${imgSrc(r.afterPath)}" alt="After" />
1619
1606
  </div>
1620
- <div class="img-col">
1621
- <div class="label">Diff</div>
1622
- <img src="${imgSrc(r.diffPath)}" alt="Diff" />
1623
- </div>
1624
1607
  </div>
1625
- ${r.changed ? `<a class="slider-link" href="${sliderHref}">Open slider view</a>` : ""}
1626
1608
  </div>`;
1627
1609
  };
1628
1610
  return `<!DOCTYPE html>
@@ -1644,12 +1626,10 @@ function generateReportHtml(results, outputDir) {
1644
1626
  .badge { font-size: 12px; padding: 2px 8px; border-radius: 9999px; font-weight: 500; }
1645
1627
  .badge-changed { background: #fef3c7; color: #92400e; }
1646
1628
  .badge-unchanged { background: #d1fae5; color: #065f46; }
1647
- .images { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1px; background: #e5e7eb; }
1629
+ .images { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: #e5e7eb; }
1648
1630
  .img-col { background: #fff; padding: 8px; }
1649
1631
  .img-col img { width: 100%; height: auto; display: block; border-radius: 4px; }
1650
1632
  .label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280; margin-bottom: 4px; font-weight: 600; }
1651
- .slider-link { display: block; padding: 8px 16px; text-align: center; font-size: 13px; color: #2563eb; text-decoration: none; border-top: 1px solid #e5e7eb; }
1652
- .slider-link:hover { background: #eff6ff; }
1653
1633
  .section-title { font-size: 18px; font-weight: 600; margin: 24px 0 12px; }
1654
1634
  </style>
1655
1635
  </head>
@@ -1664,83 +1644,6 @@ ${unchanged.length > 0 ? `<h2 class="section-title">Unchanged (${unchanged.lengt
1664
1644
  </html>`;
1665
1645
  }
1666
1646
 
1667
- // src/templates/slider.html.ts
1668
- import { readFileSync as readFileSync7 } from "fs";
1669
- function toBase642(filePath) {
1670
- return readFileSync7(filePath).toString("base64");
1671
- }
1672
- function imgSrc2(filePath) {
1673
- return `data:image/png;base64,${toBase642(filePath)}`;
1674
- }
1675
- function generateSliderHtml(result) {
1676
- return `<!DOCTYPE html>
1677
- <html lang="en">
1678
- <head>
1679
- <meta charset="utf-8" />
1680
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1681
- <title>afterbefore Slider - ${result.route}</title>
1682
- <style>
1683
- * { box-sizing: border-box; margin: 0; padding: 0; }
1684
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f9fafb; color: #111827; padding: 24px; }
1685
- h1 { font-size: 20px; margin-bottom: 4px; }
1686
- .meta { color: #6b7280; font-size: 13px; margin-bottom: 16px; }
1687
- .container { position: relative; overflow: hidden; border-radius: 8px; border: 1px solid #e5e7eb; user-select: none; cursor: ew-resize; }
1688
- .container img { display: block; width: 100%; height: auto; }
1689
- .before-wrap { position: absolute; top: 0; left: 0; height: 100%; overflow: hidden; }
1690
- .before-wrap img { display: block; height: 100%; width: auto; min-width: 100%; object-fit: cover; }
1691
- .slider-line { position: absolute; top: 0; width: 3px; height: 100%; background: #2563eb; cursor: ew-resize; z-index: 10; }
1692
- .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; }
1693
- .slider-handle::before { content: '\\2194'; color: #fff; font-size: 18px; }
1694
- .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; }
1695
- </style>
1696
- </head>
1697
- <body>
1698
- <h1>${result.route}</h1>
1699
- <p class="meta">${result.diffPercentage.toFixed(2)}% changed (${result.diffPixels.toLocaleString()} pixels)</p>
1700
-
1701
- <div class="container" id="slider">
1702
- <img src="${imgSrc2(result.afterPath)}" alt="After" draggable="false" />
1703
- <div class="before-wrap" id="beforeWrap">
1704
- <img src="${imgSrc2(result.beforePath)}" alt="Before" draggable="false" />
1705
- </div>
1706
- <div class="slider-line" id="sliderLine">
1707
- <div class="slider-handle"></div>
1708
- </div>
1709
- </div>
1710
- <div class="labels"><span>Before</span><span>After</span></div>
1711
-
1712
- <script>
1713
- (function() {
1714
- const container = document.getElementById('slider');
1715
- const beforeWrap = document.getElementById('beforeWrap');
1716
- const sliderLine = document.getElementById('sliderLine');
1717
- let dragging = false;
1718
-
1719
- function setPosition(x) {
1720
- const rect = container.getBoundingClientRect();
1721
- let pct = ((x - rect.left) / rect.width) * 100;
1722
- pct = Math.max(0, Math.min(100, pct));
1723
- beforeWrap.style.width = pct + '%';
1724
- sliderLine.style.left = pct + '%';
1725
- }
1726
-
1727
- // Start at 50%
1728
- beforeWrap.style.width = '50%';
1729
- sliderLine.style.left = '50%';
1730
-
1731
- container.addEventListener('mousedown', function(e) { dragging = true; setPosition(e.clientX); });
1732
- window.addEventListener('mousemove', function(e) { if (dragging) setPosition(e.clientX); });
1733
- window.addEventListener('mouseup', function() { dragging = false; });
1734
-
1735
- container.addEventListener('touchstart', function(e) { dragging = true; setPosition(e.touches[0].clientX); }, { passive: true });
1736
- window.addEventListener('touchmove', function(e) { if (dragging) setPosition(e.touches[0].clientX); }, { passive: true });
1737
- window.addEventListener('touchend', function() { dragging = false; });
1738
- })();
1739
- </script>
1740
- </body>
1741
- </html>`;
1742
- }
1743
-
1744
1647
  // src/templates/summary.md.ts
1745
1648
  function generateSummaryMd(results, gitDiff, options) {
1746
1649
  const includeFilePaths = options?.includeFilePaths ?? true;
@@ -1841,18 +1744,12 @@ async function generateReport(results, outputDir, options) {
1841
1744
  await ensureDir(outputDir);
1842
1745
  const summaryMd = generateSummaryMd(results);
1843
1746
  const summaryPath = join8(outputDir, "summary.md");
1844
- writeFileSync2(summaryPath, summaryMd, "utf-8");
1747
+ writeFileSync(summaryPath, summaryMd, "utf-8");
1845
1748
  logger.success(`Written summary to ${summaryPath}`);
1846
1749
  const reportHtml = generateReportHtml(results, outputDir);
1847
1750
  const indexPath = join8(outputDir, "index.html");
1848
- writeFileSync2(indexPath, reportHtml, "utf-8");
1751
+ writeFileSync(indexPath, reportHtml, "utf-8");
1849
1752
  logger.success(`Written report to ${indexPath}`);
1850
- for (const result of results) {
1851
- if (!result.changed) continue;
1852
- const sliderHtml = generateSliderHtml(result);
1853
- writeFileSync2(result.sliderPath, sliderHtml, "utf-8");
1854
- logger.dim(`Written slider for ${result.route}`);
1855
- }
1856
1753
  if (options.post) {
1857
1754
  const prNumber = findPrNumber();
1858
1755
  if (!prNumber) {
@@ -1868,10 +1765,6 @@ async function generateReport(results, outputDir, options) {
1868
1765
  }
1869
1766
 
1870
1767
  // src/pipeline.ts
1871
- function formatTriggerChain(chain) {
1872
- if (chain.length <= 1) return chain[0] ?? "";
1873
- return chain.join(" \u2192 ");
1874
- }
1875
1768
  function generateSessionName(cwd) {
1876
1769
  const branch = getCurrentBranch(cwd);
1877
1770
  const name = branch.replace(/^(feat|fix|perf|chore|refactor|docs|style|test|ci|build)\//, "");
@@ -1958,7 +1851,7 @@ async function runPipeline(options) {
1958
1851
  const outputDir = resolve4(cwd, output, sessionName);
1959
1852
  const startTime = Date.now();
1960
1853
  try {
1961
- const version = true ? "0.1.3" : "dev";
1854
+ const version = true ? "0.1.4" : "dev";
1962
1855
  console.log(`
1963
1856
  afterbefore v${version} \xB7 Comparing against ${base}
1964
1857
  `);
@@ -2025,13 +1918,6 @@ afterbefore v${version} \xB7 Comparing against ${base}
2025
1918
  );
2026
1919
  return;
2027
1920
  }
2028
- console.log();
2029
- for (const r of affectedRoutes) {
2030
- const chain = formatTriggerChain(r.triggerChain);
2031
- const label = r.reason === "direct" ? "(direct)" : `\u2190 ${chain}`;
2032
- console.log(chalk2.dim(` ${r.route} ${label}`));
2033
- }
2034
- console.log();
2035
1921
  logger.pipeline(4, "Setting up worktree...");
2036
1922
  const worktree = await worktreePromise;
2037
1923
  logger.pipeline(5, "Starting servers...");
@@ -2068,7 +1954,26 @@ afterbefore v${version} \xB7 Comparing against ${base}
2068
1954
  );
2069
1955
  logger.pipeline(7, "Comparing screenshots...");
2070
1956
  const bgColor = detectBgColor(cwd);
2071
- const results = await compareScreenshots(captures, outputDir, options.threshold, { browser, bgColor });
1957
+ const allResults = await compareScreenshots(captures, outputDir, options.threshold, { browser, bgColor });
1958
+ const results = allResults.filter((r) => {
1959
+ const isSubCapture = r.prefix.includes("~");
1960
+ if (isSubCapture && !r.changed) {
1961
+ try {
1962
+ unlinkSync(r.beforePath);
1963
+ } catch {
1964
+ }
1965
+ try {
1966
+ unlinkSync(r.afterPath);
1967
+ } catch {
1968
+ }
1969
+ try {
1970
+ unlinkSync(r.comparePath);
1971
+ } catch {
1972
+ }
1973
+ return false;
1974
+ }
1975
+ return true;
1976
+ });
2072
1977
  logger.pipeline(8, "Generating report...");
2073
1978
  await generateReport(results, outputDir, { post });
2074
1979
  const summary = generateSummaryMd(results, gitDiff);
@@ -2092,7 +1997,7 @@ afterbefore v${version} \xB7 Comparing against ${base}
2092
1997
  var program = new Command();
2093
1998
  program.name("afterbefore").description(
2094
1999
  "Automatic before/after screenshot capture for PRs. Git diff is the config."
2095
- ).version("0.1.3").option("--base <ref>", "Base branch or ref to compare against", "main").option("--output <dir>", "Output directory for screenshots", ".afterbefore").option("--post", "Post results as a PR comment via gh CLI", false).option(
2000
+ ).version("0.1.4").option("--base <ref>", "Base branch or ref to compare against", "main").option("--output <dir>", "Output directory for screenshots", ".afterbefore").option("--post", "Post results as a PR comment via gh CLI", false).option(
2096
2001
  "--threshold <percent>",
2097
2002
  "Diff threshold percentage (changes below this are ignored)",
2098
2003
  "0.1"