afterbefore 0.1.7 → 0.1.8
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 +133 -127
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -63
- package/dist/index.js +130 -124
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -109,6 +109,7 @@ var Logger = class {
|
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
this.spinner.text = text;
|
|
112
|
+
this.spinner.render();
|
|
112
113
|
}
|
|
113
114
|
completePipeline(finished = false) {
|
|
114
115
|
this.pipelineActive = false;
|
|
@@ -432,12 +433,6 @@ function isGlobalVisualFile(filePath) {
|
|
|
432
433
|
const p = filePath.replace(/^src\//, "");
|
|
433
434
|
return GLOBAL_FILES.has(p) || /globals?\.(css|scss)$/.test(p);
|
|
434
435
|
}
|
|
435
|
-
var VISUAL_CATEGORIES = /* @__PURE__ */ new Set([
|
|
436
|
-
"page",
|
|
437
|
-
"component",
|
|
438
|
-
"style",
|
|
439
|
-
"layout"
|
|
440
|
-
]);
|
|
441
436
|
function classifyFile(filePath) {
|
|
442
437
|
const p = filePath.replace(/^src\//, "");
|
|
443
438
|
if (/\.(test|spec)\.[tj]sx?$/.test(p) || /^tests?\//.test(p) || /\/__tests__\//.test(p) || p.includes(".test.") || p.includes(".spec.")) {
|
|
@@ -474,9 +469,6 @@ function classifyFiles(files) {
|
|
|
474
469
|
category: classifyFile(f.path)
|
|
475
470
|
}));
|
|
476
471
|
}
|
|
477
|
-
function filterVisuallyRelevant(files) {
|
|
478
|
-
return files.filter((f) => VISUAL_CATEGORIES.has(f.category));
|
|
479
|
-
}
|
|
480
472
|
|
|
481
473
|
// src/stages/graph.ts
|
|
482
474
|
import { readdirSync, readFileSync as readFileSync4 } from "fs";
|
|
@@ -728,6 +720,14 @@ function getLayoutDir(filePath) {
|
|
|
728
720
|
return match[1];
|
|
729
721
|
}
|
|
730
722
|
|
|
723
|
+
// src/utils/path.ts
|
|
724
|
+
function normalizePath(filePath) {
|
|
725
|
+
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
726
|
+
}
|
|
727
|
+
function sanitizeLabel(label, maxLength = 40) {
|
|
728
|
+
return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, maxLength);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
731
|
// src/stages/impact.ts
|
|
732
732
|
var MAX_DEPTH = 3;
|
|
733
733
|
function findAffectedRoutes(changedFiles, graph, projectRoot, maxRoutes = 0) {
|
|
@@ -795,6 +795,28 @@ function findAffectedRoutes(changedFiles, graph, projectRoot, maxRoutes = 0) {
|
|
|
795
795
|
logger.dim(`Found ${routes.length} affected route(s)`);
|
|
796
796
|
return routes;
|
|
797
797
|
}
|
|
798
|
+
function findRoutesForFile(file, graph) {
|
|
799
|
+
const routes = /* @__PURE__ */ new Set();
|
|
800
|
+
const start = normalizePath(file);
|
|
801
|
+
const queue = [{ path: start, depth: 0 }];
|
|
802
|
+
const visited = /* @__PURE__ */ new Set([start]);
|
|
803
|
+
while (queue.length > 0) {
|
|
804
|
+
const { path, depth } = queue.shift();
|
|
805
|
+
if (isPageFile(path)) {
|
|
806
|
+
const route = pagePathToRoute(path);
|
|
807
|
+
if (route) routes.add(route);
|
|
808
|
+
}
|
|
809
|
+
if (depth >= MAX_DEPTH) continue;
|
|
810
|
+
const importers = graph.reverse.get(path);
|
|
811
|
+
if (!importers) continue;
|
|
812
|
+
for (const importer of importers) {
|
|
813
|
+
if (visited.has(importer)) continue;
|
|
814
|
+
visited.add(importer);
|
|
815
|
+
queue.push({ path: importer, depth: depth + 1 });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return Array.from(routes);
|
|
819
|
+
}
|
|
798
820
|
|
|
799
821
|
// src/stages/worktree.ts
|
|
800
822
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -969,7 +991,7 @@ async function detectTabs(page, maxTabs) {
|
|
|
969
991
|
return allTabs.filter((t) => !t.selected).slice(0, maxTabs);
|
|
970
992
|
}
|
|
971
993
|
function sanitizeTabLabel(label) {
|
|
972
|
-
return label
|
|
994
|
+
return sanitizeLabel(label);
|
|
973
995
|
}
|
|
974
996
|
|
|
975
997
|
// src/utils/sections.ts
|
|
@@ -1061,7 +1083,7 @@ async function cleanupSectionTags(page) {
|
|
|
1061
1083
|
});
|
|
1062
1084
|
}
|
|
1063
1085
|
function sanitizeSectionLabel(label) {
|
|
1064
|
-
return label
|
|
1086
|
+
return sanitizeLabel(label);
|
|
1065
1087
|
}
|
|
1066
1088
|
|
|
1067
1089
|
// src/stages/capture.ts
|
|
@@ -1077,11 +1099,9 @@ async function launchBrowser() {
|
|
|
1077
1099
|
}
|
|
1078
1100
|
}
|
|
1079
1101
|
var MAX_COMPONENT_INSTANCES_PER_SOURCE = 20;
|
|
1080
|
-
function normalizePath(filePath) {
|
|
1081
|
-
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1082
|
-
}
|
|
1083
1102
|
function sanitizeComponentLabel(label) {
|
|
1084
|
-
|
|
1103
|
+
const noExt = label.replace(/\.[a-z0-9]+$/i, "");
|
|
1104
|
+
return sanitizeLabel(noExt, 60);
|
|
1085
1105
|
}
|
|
1086
1106
|
function groupBySource(instances) {
|
|
1087
1107
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -1396,6 +1416,88 @@ async function captureAutoSections(afterPage, beforePage, parentPrefix, parentLa
|
|
|
1396
1416
|
await cleanupSectionTags(afterPage);
|
|
1397
1417
|
await cleanupSectionTags(beforePage);
|
|
1398
1418
|
}
|
|
1419
|
+
async function captureAutoTabs(afterPage, beforePage, task, beforeUrl, afterUrl, outputDir, options, settle, results) {
|
|
1420
|
+
const tabs = await detectTabs(afterPage, options.maxTabsPerRoute);
|
|
1421
|
+
const usedPrefixes = /* @__PURE__ */ new Set();
|
|
1422
|
+
for (const tab of tabs) {
|
|
1423
|
+
let slug = sanitizeTabLabel(tab.label);
|
|
1424
|
+
if (!slug) continue;
|
|
1425
|
+
if (usedPrefixes.has(slug)) {
|
|
1426
|
+
let suffix = 2;
|
|
1427
|
+
while (usedPrefixes.has(`${slug}-${suffix}`)) suffix++;
|
|
1428
|
+
slug = `${slug}-${suffix}`;
|
|
1429
|
+
}
|
|
1430
|
+
usedPrefixes.add(slug);
|
|
1431
|
+
const tabPrefix = `${task.prefix}~${slug}`;
|
|
1432
|
+
const tabLabel = `${task.label} [${tab.label}]`;
|
|
1433
|
+
const tabBeforePath = join6(outputDir, `${tabPrefix}-before.png`);
|
|
1434
|
+
const tabAfterPath = join6(outputDir, `${tabPrefix}-after.png`);
|
|
1435
|
+
try {
|
|
1436
|
+
const afterUrlBefore = afterPage.url();
|
|
1437
|
+
await afterPage.getByRole("tab", { name: tab.label }).first().click();
|
|
1438
|
+
await settle(afterPage);
|
|
1439
|
+
if (afterPage.url() !== afterUrlBefore) {
|
|
1440
|
+
await afterPage.goBack({ waitUntil: "networkidle" });
|
|
1441
|
+
await settle(afterPage);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
await afterPage.screenshot({ path: tabAfterPath, fullPage: true });
|
|
1445
|
+
try {
|
|
1446
|
+
const beforeUrlBefore = beforePage.url();
|
|
1447
|
+
await beforePage.getByRole("tab", { name: tab.label }).first().click({ timeout: 2e3 });
|
|
1448
|
+
await settle(beforePage);
|
|
1449
|
+
if (beforePage.url() !== beforeUrlBefore) {
|
|
1450
|
+
await beforePage.goBack({ waitUntil: "networkidle" });
|
|
1451
|
+
await settle(beforePage);
|
|
1452
|
+
await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
|
|
1453
|
+
} else {
|
|
1454
|
+
await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
|
|
1455
|
+
}
|
|
1456
|
+
} catch {
|
|
1457
|
+
await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
|
|
1458
|
+
}
|
|
1459
|
+
results.push({
|
|
1460
|
+
route: tabLabel,
|
|
1461
|
+
prefix: tabPrefix,
|
|
1462
|
+
beforePath: tabBeforePath,
|
|
1463
|
+
afterPath: tabAfterPath
|
|
1464
|
+
});
|
|
1465
|
+
if ((task.changedComponents?.length ?? 0) > 0) {
|
|
1466
|
+
await captureComponentInstances(
|
|
1467
|
+
afterPage,
|
|
1468
|
+
beforePage,
|
|
1469
|
+
task.changedComponents,
|
|
1470
|
+
tabPrefix,
|
|
1471
|
+
tabLabel,
|
|
1472
|
+
outputDir,
|
|
1473
|
+
tabBeforePath,
|
|
1474
|
+
tabAfterPath,
|
|
1475
|
+
results
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
if (options.autoSections && !task.skipAutoSections) {
|
|
1479
|
+
await captureAutoSections(
|
|
1480
|
+
afterPage,
|
|
1481
|
+
beforePage,
|
|
1482
|
+
tabPrefix,
|
|
1483
|
+
tabLabel,
|
|
1484
|
+
outputDir,
|
|
1485
|
+
options,
|
|
1486
|
+
settle,
|
|
1487
|
+
results
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
} catch {
|
|
1491
|
+
logger.dim(` Skipped tab "${tab.label}" on ${task.route}`);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
if (tabs.length > 0) {
|
|
1495
|
+
await Promise.all([
|
|
1496
|
+
beforePage.goto(`${beforeUrl}${task.route}`, { waitUntil: "networkidle" }),
|
|
1497
|
+
afterPage.goto(`${afterUrl}${task.route}`, { waitUntil: "networkidle" })
|
|
1498
|
+
]);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1399
1501
|
async function captureRoutes(tasks, beforeUrl, afterUrl, outputDir, options) {
|
|
1400
1502
|
const browser = options.browser ?? await launchBrowser();
|
|
1401
1503
|
const ownsBrowser = !options.browser;
|
|
@@ -1478,86 +1580,17 @@ async function captureRoutes(tasks, beforeUrl, afterUrl, outputDir, options) {
|
|
|
1478
1580
|
);
|
|
1479
1581
|
}
|
|
1480
1582
|
if (options.autoTabs && !task.actions && !task.selector && !task.skipAutoTabs) {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const tabPrefix = `${task.prefix}~${slug}`;
|
|
1493
|
-
const tabLabel = `${task.label} [${tab.label}]`;
|
|
1494
|
-
const tabBeforePath = join6(outputDir, `${tabPrefix}-before.png`);
|
|
1495
|
-
const tabAfterPath = join6(outputDir, `${tabPrefix}-after.png`);
|
|
1496
|
-
try {
|
|
1497
|
-
const afterUrlBefore = afterPage.url();
|
|
1498
|
-
await afterPage.getByRole("tab", { name: tab.label }).first().click();
|
|
1499
|
-
await settle(afterPage);
|
|
1500
|
-
if (afterPage.url() !== afterUrlBefore) {
|
|
1501
|
-
await afterPage.goBack({ waitUntil: "networkidle" });
|
|
1502
|
-
await settle(afterPage);
|
|
1503
|
-
continue;
|
|
1504
|
-
}
|
|
1505
|
-
await afterPage.screenshot({ path: tabAfterPath, fullPage: true });
|
|
1506
|
-
try {
|
|
1507
|
-
const beforeUrlBefore = beforePage.url();
|
|
1508
|
-
await beforePage.getByRole("tab", { name: tab.label }).first().click({ timeout: 2e3 });
|
|
1509
|
-
await settle(beforePage);
|
|
1510
|
-
if (beforePage.url() !== beforeUrlBefore) {
|
|
1511
|
-
await beforePage.goBack({ waitUntil: "networkidle" });
|
|
1512
|
-
await settle(beforePage);
|
|
1513
|
-
await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
|
|
1514
|
-
} else {
|
|
1515
|
-
await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
|
|
1516
|
-
}
|
|
1517
|
-
} catch {
|
|
1518
|
-
await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
|
|
1519
|
-
}
|
|
1520
|
-
results.push({
|
|
1521
|
-
route: tabLabel,
|
|
1522
|
-
prefix: tabPrefix,
|
|
1523
|
-
beforePath: tabBeforePath,
|
|
1524
|
-
afterPath: tabAfterPath
|
|
1525
|
-
});
|
|
1526
|
-
if ((task.changedComponents?.length ?? 0) > 0) {
|
|
1527
|
-
await captureComponentInstances(
|
|
1528
|
-
afterPage,
|
|
1529
|
-
beforePage,
|
|
1530
|
-
task.changedComponents,
|
|
1531
|
-
tabPrefix,
|
|
1532
|
-
tabLabel,
|
|
1533
|
-
outputDir,
|
|
1534
|
-
tabBeforePath,
|
|
1535
|
-
tabAfterPath,
|
|
1536
|
-
results
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
if (options.autoSections && !task.skipAutoSections) {
|
|
1540
|
-
await captureAutoSections(
|
|
1541
|
-
afterPage,
|
|
1542
|
-
beforePage,
|
|
1543
|
-
tabPrefix,
|
|
1544
|
-
tabLabel,
|
|
1545
|
-
outputDir,
|
|
1546
|
-
options,
|
|
1547
|
-
settle,
|
|
1548
|
-
results
|
|
1549
|
-
);
|
|
1550
|
-
}
|
|
1551
|
-
} catch {
|
|
1552
|
-
logger.dim(` Skipped tab "${tab.label}" on ${task.route}`);
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
if (tabs.length > 0) {
|
|
1556
|
-
await Promise.all([
|
|
1557
|
-
beforePage.goto(`${beforeUrl}${task.route}`, { waitUntil: "networkidle" }),
|
|
1558
|
-
afterPage.goto(`${afterUrl}${task.route}`, { waitUntil: "networkidle" })
|
|
1559
|
-
]);
|
|
1560
|
-
}
|
|
1583
|
+
await captureAutoTabs(
|
|
1584
|
+
afterPage,
|
|
1585
|
+
beforePage,
|
|
1586
|
+
task,
|
|
1587
|
+
beforeUrl,
|
|
1588
|
+
afterUrl,
|
|
1589
|
+
outputDir,
|
|
1590
|
+
options,
|
|
1591
|
+
settle,
|
|
1592
|
+
results
|
|
1593
|
+
);
|
|
1561
1594
|
}
|
|
1562
1595
|
}
|
|
1563
1596
|
await Promise.all([beforeCtx.close(), afterCtx.close()]);
|
|
@@ -1788,36 +1821,10 @@ function routeToPrefix(route) {
|
|
|
1788
1821
|
if (route === "/") return "_root";
|
|
1789
1822
|
return route.replace(/^\//, "").replace(/\//g, "-");
|
|
1790
1823
|
}
|
|
1791
|
-
var ROUTE_IMPACT_MAX_DEPTH = 3;
|
|
1792
|
-
function normalizePath2(filePath) {
|
|
1793
|
-
return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1794
|
-
}
|
|
1795
|
-
function findRoutesForChangedFile(changedFile, graph) {
|
|
1796
|
-
const routes = /* @__PURE__ */ new Set();
|
|
1797
|
-
const start = normalizePath2(changedFile);
|
|
1798
|
-
const queue = [{ file: start, depth: 0 }];
|
|
1799
|
-
const visited = /* @__PURE__ */ new Set([start]);
|
|
1800
|
-
while (queue.length > 0) {
|
|
1801
|
-
const { file, depth } = queue.shift();
|
|
1802
|
-
if (isPageFile(file)) {
|
|
1803
|
-
const route = pagePathToRoute(file);
|
|
1804
|
-
if (route) routes.add(route);
|
|
1805
|
-
}
|
|
1806
|
-
if (depth >= ROUTE_IMPACT_MAX_DEPTH) continue;
|
|
1807
|
-
const importers = graph.reverse.get(file);
|
|
1808
|
-
if (!importers) continue;
|
|
1809
|
-
for (const importer of importers) {
|
|
1810
|
-
if (visited.has(importer)) continue;
|
|
1811
|
-
visited.add(importer);
|
|
1812
|
-
queue.push({ file: importer, depth: depth + 1 });
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
return Array.from(routes);
|
|
1816
|
-
}
|
|
1817
1824
|
function mapRouteToChangedComponents(changedComponentFiles, graph) {
|
|
1818
1825
|
const routeMap = /* @__PURE__ */ new Map();
|
|
1819
1826
|
for (const componentPath of changedComponentFiles) {
|
|
1820
|
-
const routes =
|
|
1827
|
+
const routes = findRoutesForFile(componentPath, graph);
|
|
1821
1828
|
for (const route of routes) {
|
|
1822
1829
|
const next = routeMap.get(route) ?? [];
|
|
1823
1830
|
next.push(componentPath);
|
|
@@ -1860,7 +1867,7 @@ async function runPipeline(options) {
|
|
|
1860
1867
|
const outputDir = resolve4(cwd, output, sessionName);
|
|
1861
1868
|
const startTime = Date.now();
|
|
1862
1869
|
try {
|
|
1863
|
-
const version = true ? "0.1.
|
|
1870
|
+
const version = true ? "0.1.8" : "dev";
|
|
1864
1871
|
console.log(`
|
|
1865
1872
|
afterbefore v${version} \xB7 Comparing against ${base}
|
|
1866
1873
|
`);
|
|
@@ -1875,7 +1882,6 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
1875
1882
|
return;
|
|
1876
1883
|
}
|
|
1877
1884
|
const classified = classifyFiles(diffFiles);
|
|
1878
|
-
const visualFiles = filterVisuallyRelevant(classified);
|
|
1879
1885
|
const impactfulFiles = classified.filter(
|
|
1880
1886
|
(f) => f.category !== "test" && f.category !== "other"
|
|
1881
1887
|
);
|
|
@@ -2006,7 +2012,7 @@ afterbefore v${version} \xB7 Comparing against ${base}
|
|
|
2006
2012
|
var program = new Command();
|
|
2007
2013
|
program.name("afterbefore").description(
|
|
2008
2014
|
"Automatic before/after screenshot capture for PRs. Git diff is the config."
|
|
2009
|
-
).version("0.1.
|
|
2015
|
+
).version("0.1.8").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(
|
|
2010
2016
|
"--threshold <percent>",
|
|
2011
2017
|
"Diff threshold percentage (changes below this are ignored)",
|
|
2012
2018
|
"0.1"
|
|
@@ -2014,7 +2020,7 @@ program.name("afterbefore").description(
|
|
|
2014
2020
|
"--max-routes <count>",
|
|
2015
2021
|
"Maximum routes to capture (0 = unlimited)",
|
|
2016
2022
|
"6"
|
|
2017
|
-
).option("--width <pixels>", "Viewport width", "1280").option("--height <pixels>", "Viewport height", "720").option("--device <name>", 'Playwright device descriptor (e.g. "iPhone 14")').option("--delay <ms>", "Extra wait time (ms) after page load", "0").option("--no-auto-tabs", "Disable auto-detection of ARIA tab states").option("--max-tabs <count>", "Max auto-detected tabs per route", "5").option("--auto-sections", "
|
|
2023
|
+
).option("--width <pixels>", "Viewport width", "1280").option("--height <pixels>", "Viewport height", "720").option("--device <name>", 'Playwright device descriptor (e.g. "iPhone 14")').option("--delay <ms>", "Extra wait time (ms) after page load", "0").option("--no-auto-tabs", "Disable auto-detection of ARIA tab states").option("--max-tabs <count>", "Max auto-detected tabs per route", "5").option("--no-auto-sections", "Disable auto-detection of heading-labeled sections").option("--max-sections <count>", "Max auto-detected sections per page state", "10").action(async (opts) => {
|
|
2018
2024
|
const cwd = process.cwd();
|
|
2019
2025
|
if (!isGitRepo(cwd)) {
|
|
2020
2026
|
logger.error("Not a git repository. Run this from inside a git repo.");
|
|
@@ -2032,7 +2038,7 @@ program.name("afterbefore").description(
|
|
|
2032
2038
|
delay: parseInt(opts.delay, 10),
|
|
2033
2039
|
autoTabs: opts.autoTabs,
|
|
2034
2040
|
maxTabsPerRoute: parseInt(opts.maxTabs, 10),
|
|
2035
|
-
autoSections: opts.autoSections
|
|
2041
|
+
autoSections: opts.autoSections,
|
|
2036
2042
|
maxSectionsPerRoute: parseInt(opts.maxSections, 10),
|
|
2037
2043
|
cwd
|
|
2038
2044
|
};
|