afterbefore 0.1.12 → 0.1.13
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 +221 -71
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +220 -70
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -240,7 +240,7 @@ function getCurrentBranch(cwd) {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
// src/pipeline.ts
|
|
243
|
-
import { resolve as
|
|
243
|
+
import { resolve as resolve4 } from "path";
|
|
244
244
|
import { unlinkSync as unlinkSync2 } from "fs";
|
|
245
245
|
|
|
246
246
|
// src/config.ts
|
|
@@ -276,7 +276,7 @@ async function ensureDir(dir) {
|
|
|
276
276
|
// src/utils/port.ts
|
|
277
277
|
import { createServer } from "net";
|
|
278
278
|
function findPort() {
|
|
279
|
-
return new Promise((
|
|
279
|
+
return new Promise((resolve5, reject) => {
|
|
280
280
|
const server = createServer();
|
|
281
281
|
server.listen(0, () => {
|
|
282
282
|
const addr = server.address();
|
|
@@ -286,7 +286,7 @@ function findPort() {
|
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
const port = addr.port;
|
|
289
|
-
server.close(() =>
|
|
289
|
+
server.close(() => resolve5(port));
|
|
290
290
|
});
|
|
291
291
|
server.on("error", reject);
|
|
292
292
|
});
|
|
@@ -299,6 +299,105 @@ async function findAvailablePort(exclude) {
|
|
|
299
299
|
throw new Error("Failed to find available port after 5 attempts");
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// src/utils/bgcolor.ts
|
|
303
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
304
|
+
import { resolve as resolve2 } from "path";
|
|
305
|
+
var DEFAULT_BG = "#0a0a0a";
|
|
306
|
+
var GLOBAL_CSS_PATHS = [
|
|
307
|
+
"app/globals.css",
|
|
308
|
+
"src/app/globals.css",
|
|
309
|
+
"styles/globals.css",
|
|
310
|
+
"src/styles/globals.css",
|
|
311
|
+
"app/global.css",
|
|
312
|
+
"src/app/global.css"
|
|
313
|
+
];
|
|
314
|
+
function hslToHex(h, s, l) {
|
|
315
|
+
s /= 100;
|
|
316
|
+
l /= 100;
|
|
317
|
+
const a = s * Math.min(l, 1 - l);
|
|
318
|
+
const f = (n) => {
|
|
319
|
+
const k = (n + h / 30) % 12;
|
|
320
|
+
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
|
321
|
+
return Math.round(255 * color).toString(16).padStart(2, "0");
|
|
322
|
+
};
|
|
323
|
+
return `#${f(0)}${f(8)}${f(4)}`;
|
|
324
|
+
}
|
|
325
|
+
function parseColorValue(raw) {
|
|
326
|
+
const v = raw.trim();
|
|
327
|
+
if (/^#[0-9a-fA-F]{3,8}$/.test(v)) {
|
|
328
|
+
if (v.length === 4) {
|
|
329
|
+
return `#${v[1]}${v[1]}${v[2]}${v[2]}${v[3]}${v[3]}`;
|
|
330
|
+
}
|
|
331
|
+
return v.slice(0, 7);
|
|
332
|
+
}
|
|
333
|
+
const hslMatch = v.match(
|
|
334
|
+
/^hsl\(\s*([\d.]+)[,\s]+\s*([\d.]+)%[,\s]+\s*([\d.]+)%/
|
|
335
|
+
);
|
|
336
|
+
if (hslMatch) {
|
|
337
|
+
return hslToHex(
|
|
338
|
+
parseFloat(hslMatch[1]),
|
|
339
|
+
parseFloat(hslMatch[2]),
|
|
340
|
+
parseFloat(hslMatch[3])
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const bareHsl = v.match(/^([\d.]+)\s+([\d.]+)%\s+([\d.]+)%$/);
|
|
344
|
+
if (bareHsl) {
|
|
345
|
+
return hslToHex(
|
|
346
|
+
parseFloat(bareHsl[1]),
|
|
347
|
+
parseFloat(bareHsl[2]),
|
|
348
|
+
parseFloat(bareHsl[3])
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
const rgbMatch = v.match(
|
|
352
|
+
/^rgb\(\s*([\d.]+)[,\s]+\s*([\d.]+)[,\s]+\s*([\d.]+)/
|
|
353
|
+
);
|
|
354
|
+
if (rgbMatch) {
|
|
355
|
+
const toHex = (n) => Math.round(parseFloat(n)).toString(16).padStart(2, "0");
|
|
356
|
+
return `#${toHex(rgbMatch[1])}${toHex(rgbMatch[2])}${toHex(rgbMatch[3])}`;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
function detectBgColor(cwd) {
|
|
361
|
+
for (const relPath of GLOBAL_CSS_PATHS) {
|
|
362
|
+
const absPath = resolve2(cwd, relPath);
|
|
363
|
+
if (!existsSync2(absPath)) continue;
|
|
364
|
+
const css = readFileSync2(absPath, "utf-8");
|
|
365
|
+
const darkBlock = css.match(/\.dark\s*\{([^}]+)\}/);
|
|
366
|
+
if (darkBlock) {
|
|
367
|
+
const bgVar = darkBlock[1].match(/--background\s*:\s*([^;]+)/);
|
|
368
|
+
if (bgVar) {
|
|
369
|
+
const color = parseColorValue(bgVar[1]);
|
|
370
|
+
if (color) return color;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const rootBlock = css.match(/:root\s*\{([^}]+)\}/);
|
|
374
|
+
if (rootBlock) {
|
|
375
|
+
const bgVar = rootBlock[1].match(/--background\s*:\s*([^;]+)/);
|
|
376
|
+
if (bgVar) {
|
|
377
|
+
const color = parseColorValue(bgVar[1]);
|
|
378
|
+
if (color) return color;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const anyBgVar = css.match(/--background\s*:\s*([^;]+)/);
|
|
382
|
+
if (anyBgVar) {
|
|
383
|
+
const color = parseColorValue(anyBgVar[1]);
|
|
384
|
+
if (color) return color;
|
|
385
|
+
}
|
|
386
|
+
const bodyBg = css.match(
|
|
387
|
+
/body\s*\{[^}]*?background(?:-color)?\s*:\s*([^;]+)/
|
|
388
|
+
);
|
|
389
|
+
if (bodyBg) {
|
|
390
|
+
const val = bodyBg[1].trim();
|
|
391
|
+
if (!val.startsWith("var(")) {
|
|
392
|
+
const color = parseColorValue(val);
|
|
393
|
+
if (color) return color;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
return DEFAULT_BG;
|
|
399
|
+
}
|
|
400
|
+
|
|
302
401
|
// src/stages/diff.ts
|
|
303
402
|
var VALID_STATUSES = /* @__PURE__ */ new Set(["A", "M", "D", "R", "C"]);
|
|
304
403
|
function parseDiffOutput(raw) {
|
|
@@ -372,13 +471,13 @@ function classifyFiles(files) {
|
|
|
372
471
|
}
|
|
373
472
|
|
|
374
473
|
// src/stages/graph.ts
|
|
375
|
-
import { readdirSync, readFileSync as
|
|
474
|
+
import { readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
376
475
|
import { join as join2, relative } from "path";
|
|
377
476
|
import { init, parse } from "es-module-lexer";
|
|
378
477
|
|
|
379
478
|
// src/stages/resolve.ts
|
|
380
|
-
import { existsSync as
|
|
381
|
-
import { resolve as
|
|
479
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync } from "fs";
|
|
480
|
+
import { resolve as resolve3, dirname, join } from "path";
|
|
382
481
|
var EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
383
482
|
function createResolver(projectRoot) {
|
|
384
483
|
const mappings = loadPathMappings(projectRoot);
|
|
@@ -386,7 +485,7 @@ function createResolver(projectRoot) {
|
|
|
386
485
|
function cachedExists(p) {
|
|
387
486
|
const cached = existsCache.get(p);
|
|
388
487
|
if (cached !== void 0) return cached;
|
|
389
|
-
const result =
|
|
488
|
+
const result = existsSync3(p);
|
|
390
489
|
existsCache.set(p, result);
|
|
391
490
|
return result;
|
|
392
491
|
}
|
|
@@ -412,14 +511,14 @@ function createResolver(projectRoot) {
|
|
|
412
511
|
return (specifier, fromFile) => {
|
|
413
512
|
if (specifier.startsWith(".")) {
|
|
414
513
|
const dir = dirname(fromFile);
|
|
415
|
-
const candidate =
|
|
514
|
+
const candidate = resolve3(dir, specifier);
|
|
416
515
|
return tryResolve(candidate);
|
|
417
516
|
}
|
|
418
517
|
for (const mapping of mappings) {
|
|
419
518
|
if (!specifier.startsWith(mapping.prefix)) continue;
|
|
420
519
|
const rest = specifier.slice(mapping.prefix.length);
|
|
421
520
|
for (const target of mapping.targets) {
|
|
422
|
-
const candidate =
|
|
521
|
+
const candidate = resolve3(projectRoot, target + rest);
|
|
423
522
|
const result = tryResolve(candidate);
|
|
424
523
|
if (result) return result;
|
|
425
524
|
}
|
|
@@ -429,12 +528,12 @@ function createResolver(projectRoot) {
|
|
|
429
528
|
}
|
|
430
529
|
function loadPathMappings(projectRoot) {
|
|
431
530
|
const tsconfigPath = join(projectRoot, "tsconfig.json");
|
|
432
|
-
if (!
|
|
531
|
+
if (!existsSync3(tsconfigPath)) {
|
|
433
532
|
logger.dim("No tsconfig.json found, skipping path alias resolution");
|
|
434
533
|
return [];
|
|
435
534
|
}
|
|
436
535
|
try {
|
|
437
|
-
const raw =
|
|
536
|
+
const raw = readFileSync3(tsconfigPath, "utf-8");
|
|
438
537
|
const cleaned = stripJsonComments(raw);
|
|
439
538
|
const config = JSON.parse(cleaned);
|
|
440
539
|
const paths = config?.compilerOptions?.paths;
|
|
@@ -534,7 +633,7 @@ function collectFiles(dir) {
|
|
|
534
633
|
function parseImports(filePath) {
|
|
535
634
|
let source;
|
|
536
635
|
try {
|
|
537
|
-
source =
|
|
636
|
+
source = readFileSync4(filePath, "utf-8");
|
|
538
637
|
} catch {
|
|
539
638
|
return [];
|
|
540
639
|
}
|
|
@@ -554,7 +653,7 @@ function parseImports(filePath) {
|
|
|
554
653
|
}
|
|
555
654
|
async function buildImportGraph(projectRoot) {
|
|
556
655
|
await init;
|
|
557
|
-
const
|
|
656
|
+
const resolve5 = createResolver(projectRoot);
|
|
558
657
|
const allFiles = [];
|
|
559
658
|
for (const dir of SOURCE_DIRS) {
|
|
560
659
|
const fullDir = join2(projectRoot, dir);
|
|
@@ -568,7 +667,7 @@ async function buildImportGraph(projectRoot) {
|
|
|
568
667
|
const specifiers = parseImports(filePath);
|
|
569
668
|
const deps = /* @__PURE__ */ new Set();
|
|
570
669
|
for (const spec of specifiers) {
|
|
571
|
-
const resolved =
|
|
670
|
+
const resolved = resolve5(spec, filePath);
|
|
572
671
|
if (!resolved) continue;
|
|
573
672
|
const relResolved = relative(projectRoot, resolved);
|
|
574
673
|
deps.add(relResolved);
|
|
@@ -727,13 +826,13 @@ import { join as join4 } from "path";
|
|
|
727
826
|
import { tmpdir } from "os";
|
|
728
827
|
|
|
729
828
|
// src/utils/pm.ts
|
|
730
|
-
import { existsSync as
|
|
829
|
+
import { existsSync as existsSync4 } from "fs";
|
|
731
830
|
import { join as join3 } from "path";
|
|
732
831
|
function detectPackageManager(dir) {
|
|
733
|
-
if (
|
|
832
|
+
if (existsSync4(join3(dir, "bun.lockb")) || existsSync4(join3(dir, "bun.lock")))
|
|
734
833
|
return "bun";
|
|
735
|
-
if (
|
|
736
|
-
if (
|
|
834
|
+
if (existsSync4(join3(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
835
|
+
if (existsSync4(join3(dir, "yarn.lock"))) return "yarn";
|
|
737
836
|
return "npm";
|
|
738
837
|
}
|
|
739
838
|
function pmExec(pm) {
|
|
@@ -790,11 +889,11 @@ async function createWorktree(base, cwd) {
|
|
|
790
889
|
|
|
791
890
|
// src/stages/server.ts
|
|
792
891
|
import { spawn } from "child_process";
|
|
793
|
-
import { existsSync as
|
|
892
|
+
import { existsSync as existsSync5 } from "fs";
|
|
794
893
|
import { join as join5 } from "path";
|
|
795
894
|
function waitForServer(url, timeoutMs) {
|
|
796
895
|
const start = Date.now();
|
|
797
|
-
return new Promise((
|
|
896
|
+
return new Promise((resolve5, reject) => {
|
|
798
897
|
const poll = async () => {
|
|
799
898
|
if (Date.now() - start > timeoutMs) {
|
|
800
899
|
reject(
|
|
@@ -807,7 +906,7 @@ function waitForServer(url, timeoutMs) {
|
|
|
807
906
|
}
|
|
808
907
|
try {
|
|
809
908
|
await fetch(url);
|
|
810
|
-
|
|
909
|
+
resolve5();
|
|
811
910
|
} catch {
|
|
812
911
|
setTimeout(poll, 150);
|
|
813
912
|
}
|
|
@@ -821,7 +920,7 @@ async function startServer(projectDir, port) {
|
|
|
821
920
|
const exec2 = pmExec(pm);
|
|
822
921
|
const [cmd, ...baseArgs] = exec2.split(" ");
|
|
823
922
|
const lockFile = join5(projectDir, ".next", "dev", "lock");
|
|
824
|
-
if (
|
|
923
|
+
if (existsSync5(lockFile)) {
|
|
825
924
|
throw new AfterbeforeError(
|
|
826
925
|
`Another Next.js dev server is running in ${projectDir} (.next/dev/lock exists).`,
|
|
827
926
|
`Stop the other dev server first, or delete .next/dev/lock if it's stale.`
|
|
@@ -848,17 +947,17 @@ async function stopServer(server) {
|
|
|
848
947
|
process.kill(-pid, "SIGTERM");
|
|
849
948
|
} catch {
|
|
850
949
|
}
|
|
851
|
-
await new Promise((
|
|
950
|
+
await new Promise((resolve5) => {
|
|
852
951
|
const timeout = setTimeout(() => {
|
|
853
952
|
try {
|
|
854
953
|
process.kill(-pid, "SIGKILL");
|
|
855
954
|
} catch {
|
|
856
955
|
}
|
|
857
|
-
|
|
956
|
+
resolve5();
|
|
858
957
|
}, 5e3);
|
|
859
958
|
server.process.on("exit", () => {
|
|
860
959
|
clearTimeout(timeout);
|
|
861
|
-
|
|
960
|
+
resolve5();
|
|
862
961
|
});
|
|
863
962
|
});
|
|
864
963
|
}
|
|
@@ -1557,65 +1656,111 @@ async function captureRoutes(tasks, beforeUrl, afterUrl, outputDir, options) {
|
|
|
1557
1656
|
}
|
|
1558
1657
|
|
|
1559
1658
|
// src/stages/compare.ts
|
|
1560
|
-
import { join as join7 } from "path";
|
|
1561
|
-
import { unlinkSync } from "fs";
|
|
1659
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
1660
|
+
import { readFileSync as readFileSync5, unlinkSync } from "fs";
|
|
1562
1661
|
import { ODiffServer } from "odiff-bin";
|
|
1563
|
-
|
|
1662
|
+
import sharp from "sharp";
|
|
1663
|
+
function readPngSize(path) {
|
|
1664
|
+
const buf = readFileSync5(path);
|
|
1665
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
1666
|
+
}
|
|
1667
|
+
async function generateComposite(beforePath, afterPath, outputPath, bgColor) {
|
|
1668
|
+
const beforeSize = readPngSize(beforePath);
|
|
1669
|
+
const afterSize = readPngSize(afterPath);
|
|
1670
|
+
const imgW = Math.max(beforeSize.width, afterSize.width);
|
|
1671
|
+
const imgH = Math.max(beforeSize.height, afterSize.height);
|
|
1672
|
+
const PADDING = 120;
|
|
1673
|
+
const GAP = 80;
|
|
1674
|
+
const LABEL_H = 70;
|
|
1675
|
+
const canvasW = Math.max(600, Math.min(2400, imgW * 2 + GAP + PADDING * 2));
|
|
1676
|
+
const canvasH = Math.max(300, Math.min(2400, imgH + LABEL_H + PADDING * 2));
|
|
1677
|
+
const maxImgH = canvasH - PADDING * 2 - LABEL_H;
|
|
1678
|
+
const colW = Math.floor((canvasW - PADDING * 2 - GAP) / 2);
|
|
1679
|
+
const [beforeBuf, afterBuf] = await Promise.all([
|
|
1680
|
+
sharp(beforePath).resize(colW, maxImgH, { fit: "inside" }).toBuffer({ resolveWithObject: true }),
|
|
1681
|
+
sharp(afterPath).resize(colW, maxImgH, { fit: "inside" }).toBuffer({ resolveWithObject: true })
|
|
1682
|
+
]);
|
|
1683
|
+
const beforeLeft = PADDING + Math.floor((colW - beforeBuf.info.width) / 2);
|
|
1684
|
+
const beforeTop = PADDING + Math.floor((maxImgH - beforeBuf.info.height) / 2);
|
|
1685
|
+
const afterLeft = PADDING + colW + GAP + Math.floor((colW - afterBuf.info.width) / 2);
|
|
1686
|
+
const afterTop = PADDING + Math.floor((maxImgH - afterBuf.info.height) / 2);
|
|
1687
|
+
const labelY = PADDING + maxImgH + 50;
|
|
1688
|
+
const beforeLabelX = PADDING + Math.floor(colW / 2);
|
|
1689
|
+
const afterLabelX = PADDING + colW + GAP + Math.floor(colW / 2);
|
|
1690
|
+
const labelSvg = Buffer.from(
|
|
1691
|
+
`<svg width="${canvasW}" height="${canvasH}">
|
|
1692
|
+
<text x="${beforeLabelX}" y="${labelY}" text-anchor="middle"
|
|
1693
|
+
font-family="system-ui, sans-serif" font-size="30" font-weight="500"
|
|
1694
|
+
fill="#888">Before</text>
|
|
1695
|
+
<text x="${afterLabelX}" y="${labelY}" text-anchor="middle"
|
|
1696
|
+
font-family="system-ui, sans-serif" font-size="30" font-weight="500"
|
|
1697
|
+
fill="#22c55e">After</text>
|
|
1698
|
+
</svg>`
|
|
1699
|
+
);
|
|
1700
|
+
await sharp({
|
|
1701
|
+
create: {
|
|
1702
|
+
width: canvasW,
|
|
1703
|
+
height: canvasH,
|
|
1704
|
+
channels: 4,
|
|
1705
|
+
background: bgColor
|
|
1706
|
+
}
|
|
1707
|
+
}).composite([
|
|
1708
|
+
{ input: beforeBuf.data, left: beforeLeft, top: beforeTop },
|
|
1709
|
+
{ input: afterBuf.data, left: afterLeft, top: afterTop },
|
|
1710
|
+
{ input: labelSvg, left: 0, top: 0 }
|
|
1711
|
+
]).png().toFile(outputPath);
|
|
1712
|
+
}
|
|
1713
|
+
async function compareOne(capture, outputDir, threshold, server, options) {
|
|
1564
1714
|
const diffPath = join7(outputDir, `${capture.prefix}-diff.png`);
|
|
1715
|
+
const dir = dirname2(capture.beforePath);
|
|
1716
|
+
const comparePath = join7(dir, `${capture.prefix}-compare.png`);
|
|
1565
1717
|
const result = await server.compare(
|
|
1566
1718
|
capture.beforePath,
|
|
1567
1719
|
capture.afterPath,
|
|
1568
1720
|
diffPath,
|
|
1569
1721
|
{ threshold: 0.1, antialiasing: true }
|
|
1570
1722
|
);
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
prefix: capture.prefix,
|
|
1575
|
-
beforePath: capture.beforePath,
|
|
1576
|
-
afterPath: capture.afterPath,
|
|
1577
|
-
diffPixels: 0,
|
|
1578
|
-
totalPixels: 0,
|
|
1579
|
-
diffPercentage: 0,
|
|
1580
|
-
changed: false
|
|
1581
|
-
};
|
|
1723
|
+
try {
|
|
1724
|
+
unlinkSync(diffPath);
|
|
1725
|
+
} catch {
|
|
1582
1726
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
afterPath: capture.afterPath,
|
|
1596
|
-
diffPixels: result.diffCount,
|
|
1597
|
-
totalPixels: 0,
|
|
1598
|
-
diffPercentage: result.diffPercentage,
|
|
1599
|
-
changed
|
|
1600
|
-
};
|
|
1727
|
+
let diffPixels = 0;
|
|
1728
|
+
let totalPixels = 0;
|
|
1729
|
+
let diffPercentage = 0;
|
|
1730
|
+
let changed = false;
|
|
1731
|
+
if (result.match) {
|
|
1732
|
+
} else if (result.reason === "pixel-diff") {
|
|
1733
|
+
diffPixels = result.diffCount;
|
|
1734
|
+
diffPercentage = result.diffPercentage;
|
|
1735
|
+
changed = diffPercentage > threshold;
|
|
1736
|
+
} else {
|
|
1737
|
+
diffPercentage = 100;
|
|
1738
|
+
changed = true;
|
|
1601
1739
|
}
|
|
1740
|
+
await generateComposite(
|
|
1741
|
+
capture.beforePath,
|
|
1742
|
+
capture.afterPath,
|
|
1743
|
+
comparePath,
|
|
1744
|
+
options.bgColor
|
|
1745
|
+
);
|
|
1602
1746
|
return {
|
|
1603
1747
|
route: capture.route,
|
|
1604
1748
|
prefix: capture.prefix,
|
|
1605
1749
|
beforePath: capture.beforePath,
|
|
1606
1750
|
afterPath: capture.afterPath,
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1751
|
+
comparePath,
|
|
1752
|
+
diffPixels,
|
|
1753
|
+
totalPixels,
|
|
1754
|
+
diffPercentage,
|
|
1755
|
+
changed
|
|
1611
1756
|
};
|
|
1612
1757
|
}
|
|
1613
|
-
async function compareScreenshots(captures, outputDir, threshold = 0.1) {
|
|
1758
|
+
async function compareScreenshots(captures, outputDir, threshold = 0.1, options) {
|
|
1614
1759
|
const server = new ODiffServer();
|
|
1615
1760
|
try {
|
|
1616
1761
|
const results = [];
|
|
1617
1762
|
for (const capture of captures) {
|
|
1618
|
-
results.push(await compareOne(capture, outputDir, threshold, server));
|
|
1763
|
+
results.push(await compareOne(capture, outputDir, threshold, server, options));
|
|
1619
1764
|
}
|
|
1620
1765
|
return results;
|
|
1621
1766
|
} finally {
|
|
@@ -1662,10 +1807,10 @@ function generateSummaryMd(results, gitDiff, options) {
|
|
|
1662
1807
|
if (includeFilePaths) {
|
|
1663
1808
|
lines.push("");
|
|
1664
1809
|
lines.push("### Screenshots");
|
|
1665
|
-
lines.push("| Route | Before | After |");
|
|
1666
|
-
lines.push("
|
|
1810
|
+
lines.push("| Route | Before | After | Compare |");
|
|
1811
|
+
lines.push("|-------|--------|-------|---------|");
|
|
1667
1812
|
for (const r of changed) {
|
|
1668
|
-
lines.push(`| \`${r.route}\` | \`${r.beforePath}\` | \`${r.afterPath}\` |`);
|
|
1813
|
+
lines.push(`| \`${r.route}\` | \`${r.beforePath}\` | \`${r.afterPath}\` | \`${r.comparePath}\` |`);
|
|
1669
1814
|
}
|
|
1670
1815
|
lines.push("");
|
|
1671
1816
|
lines.push("Review the before/after screenshots above to verify the visual changes match the code diff.");
|
|
@@ -1791,10 +1936,10 @@ function expandRoutes(routes, config, routeComponentMap) {
|
|
|
1791
1936
|
async function runPipeline(options) {
|
|
1792
1937
|
const { base, output, post, cwd } = options;
|
|
1793
1938
|
const sessionName = generateSessionName(cwd);
|
|
1794
|
-
const outputDir =
|
|
1939
|
+
const outputDir = resolve4(cwd, output, sessionName);
|
|
1795
1940
|
const startTime = Date.now();
|
|
1796
1941
|
try {
|
|
1797
|
-
const version = true ? "0.1.
|
|
1942
|
+
const version = true ? "0.1.13" : "dev";
|
|
1798
1943
|
console.log(`
|
|
1799
1944
|
afterbefore v${version} \xB7 Comparing against ${base}
|
|
1800
1945
|
`);
|
|
@@ -1895,7 +2040,8 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
1895
2040
|
}
|
|
1896
2041
|
);
|
|
1897
2042
|
logger.pipeline(7, "Comparing screenshots...");
|
|
1898
|
-
const
|
|
2043
|
+
const bgColor = detectBgColor(cwd);
|
|
2044
|
+
const allResults = await compareScreenshots(captures, outputDir, options.threshold, { bgColor });
|
|
1899
2045
|
const results = allResults.filter((r) => {
|
|
1900
2046
|
const isSubCapture = r.prefix.includes("~");
|
|
1901
2047
|
if (isSubCapture && !r.changed) {
|
|
@@ -1907,6 +2053,10 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
1907
2053
|
unlinkSync2(r.afterPath);
|
|
1908
2054
|
} catch {
|
|
1909
2055
|
}
|
|
2056
|
+
try {
|
|
2057
|
+
unlinkSync2(r.comparePath);
|
|
2058
|
+
} catch {
|
|
2059
|
+
}
|
|
1910
2060
|
return false;
|
|
1911
2061
|
}
|
|
1912
2062
|
return true;
|
|
@@ -1923,7 +2073,7 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
1923
2073
|
);
|
|
1924
2074
|
} finally {
|
|
1925
2075
|
try {
|
|
1926
|
-
logger.writeLogFile(
|
|
2076
|
+
logger.writeLogFile(resolve4(outputDir, "debug.log"));
|
|
1927
2077
|
} catch {
|
|
1928
2078
|
}
|
|
1929
2079
|
await cleanupRegistry.runAll();
|
|
@@ -1934,7 +2084,7 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
1934
2084
|
var program = new Command();
|
|
1935
2085
|
program.name("afterbefore").description(
|
|
1936
2086
|
"Automatic before/after screenshot capture for PRs. Git diff is the config."
|
|
1937
|
-
).version("0.1.
|
|
2087
|
+
).version("0.1.13").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(
|
|
1938
2088
|
"--threshold <percent>",
|
|
1939
2089
|
"Diff threshold percentage (changes below this are ignored)",
|
|
1940
2090
|
"0.1"
|