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/index.d.ts CHANGED
@@ -1,22 +1,3 @@
1
- import * as child_process from 'child_process';
2
- import * as playwright from 'playwright';
3
-
4
- type DiffStatus = "A" | "M" | "D" | "R" | "C";
5
- interface DiffFile {
6
- status: DiffStatus;
7
- path: string;
8
- oldPath?: string;
9
- }
10
- type FileCategory = "page" | "component" | "style" | "layout" | "utility" | "config" | "test" | "other";
11
- interface ClassifiedFile extends DiffFile {
12
- category: FileCategory;
13
- }
14
- interface ImportGraph {
15
- /** file → set of files it imports */
16
- forward: Map<string, Set<string>>;
17
- /** file → set of files that import it */
18
- reverse: Map<string, Set<string>>;
19
- }
20
1
  interface AffectedRoute {
21
2
  /** File system path to the page file (e.g. app/about/page.tsx) */
22
3
  pagePath: string;
@@ -48,16 +29,6 @@ interface ScenarioConfig {
48
29
  interface AfterbeforeConfig {
49
30
  scenarios?: Record<string, ScenarioConfig[]>;
50
31
  }
51
- interface CaptureTask {
52
- route: string;
53
- label: string;
54
- prefix: string;
55
- actions?: ScenarioAction[];
56
- selector?: string;
57
- skipAutoTabs?: boolean;
58
- skipAutoSections?: boolean;
59
- changedComponents?: string[];
60
- }
61
32
  interface CompareResult {
62
33
  route: string;
63
34
  prefix: string;
@@ -69,28 +40,6 @@ interface CompareResult {
69
40
  diffPercentage: number;
70
41
  changed: boolean;
71
42
  }
72
- interface CaptureOptions {
73
- /** Browser instance to reuse (avoids double launch) */
74
- browser?: playwright.Browser;
75
- /** Viewport width (default: 1280) */
76
- width: number;
77
- /** Viewport height (default: 720) */
78
- height: number;
79
- /** Playwright device descriptor name (e.g. "iPhone 14") */
80
- device?: string;
81
- /** Extra delay in ms after page load (default: 0) */
82
- delay: number;
83
- /** Auto-detect and capture ARIA tab states (default: true) */
84
- autoTabs: boolean;
85
- /** Maximum auto-detected tabs per route (default: 5) */
86
- maxTabsPerRoute: number;
87
- /** Auto-detect and capture heading-labeled sections (default: false) */
88
- autoSections: boolean;
89
- /** Maximum auto-detected sections per page state (default: 10) */
90
- maxSectionsPerRoute: number;
91
- /** Progress callback: called with (1-based index, route) for each route */
92
- onProgress?: (index: number, route: string) => void;
93
- }
94
43
  interface PipelineOptions {
95
44
  /** Base branch/ref to compare against (default: "main") */
96
45
  base: string;
@@ -116,24 +65,14 @@ interface PipelineOptions {
116
65
  autoTabs: boolean;
117
66
  /** Maximum auto-detected tabs per route (default: 5) */
118
67
  maxTabsPerRoute: number;
119
- /** Auto-detect and capture heading-labeled sections (default: false) */
68
+ /** Auto-detect and capture heading-labeled sections (default: true) */
120
69
  autoSections: boolean;
121
70
  /** Maximum auto-detected sections per page state (default: 10) */
122
71
  maxSectionsPerRoute: number;
123
72
  }
124
- interface ServerInfo {
125
- port: number;
126
- process: child_process.ChildProcess;
127
- url: string;
128
- }
129
- interface WorktreeInfo {
130
- path: string;
131
- ref: string;
132
- cleanup: () => Promise<void>;
133
- }
134
73
 
135
74
  declare function runPipeline(options: PipelineOptions): Promise<void>;
136
75
 
137
76
  declare function loadConfig(cwd: string): Promise<AfterbeforeConfig | null>;
138
77
 
139
- export { type AffectedRoute, type AfterbeforeConfig, type CaptureOptions, type CaptureResult, type CaptureTask, type ClassifiedFile, type CompareResult, type DiffFile, type DiffStatus, type FileCategory, type ImportGraph, type PipelineOptions, type ScenarioConfig, type ServerInfo, type WorktreeInfo, loadConfig, runPipeline };
78
+ export { type AffectedRoute, type AfterbeforeConfig, type CaptureResult, type CompareResult, type PipelineOptions, loadConfig, runPipeline };
package/dist/index.js CHANGED
@@ -108,6 +108,7 @@ var Logger = class {
108
108
  return;
109
109
  }
110
110
  this.spinner.text = text;
111
+ this.spinner.render();
111
112
  }
112
113
  completePipeline(finished = false) {
113
114
  this.pipelineActive = false;
@@ -410,12 +411,6 @@ function isGlobalVisualFile(filePath) {
410
411
  const p = filePath.replace(/^src\//, "");
411
412
  return GLOBAL_FILES.has(p) || /globals?\.(css|scss)$/.test(p);
412
413
  }
413
- var VISUAL_CATEGORIES = /* @__PURE__ */ new Set([
414
- "page",
415
- "component",
416
- "style",
417
- "layout"
418
- ]);
419
414
  function classifyFile(filePath) {
420
415
  const p = filePath.replace(/^src\//, "");
421
416
  if (/\.(test|spec)\.[tj]sx?$/.test(p) || /^tests?\//.test(p) || /\/__tests__\//.test(p) || p.includes(".test.") || p.includes(".spec.")) {
@@ -452,9 +447,6 @@ function classifyFiles(files) {
452
447
  category: classifyFile(f.path)
453
448
  }));
454
449
  }
455
- function filterVisuallyRelevant(files) {
456
- return files.filter((f) => VISUAL_CATEGORIES.has(f.category));
457
- }
458
450
 
459
451
  // src/stages/graph.ts
460
452
  import { readdirSync, readFileSync as readFileSync4 } from "fs";
@@ -706,6 +698,14 @@ function getLayoutDir(filePath) {
706
698
  return match[1];
707
699
  }
708
700
 
701
+ // src/utils/path.ts
702
+ function normalizePath(filePath) {
703
+ return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
704
+ }
705
+ function sanitizeLabel(label, maxLength = 40) {
706
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, maxLength);
707
+ }
708
+
709
709
  // src/stages/impact.ts
710
710
  var MAX_DEPTH = 3;
711
711
  function findAffectedRoutes(changedFiles, graph, projectRoot, maxRoutes = 0) {
@@ -773,6 +773,28 @@ function findAffectedRoutes(changedFiles, graph, projectRoot, maxRoutes = 0) {
773
773
  logger.dim(`Found ${routes.length} affected route(s)`);
774
774
  return routes;
775
775
  }
776
+ function findRoutesForFile(file, graph) {
777
+ const routes = /* @__PURE__ */ new Set();
778
+ const start = normalizePath(file);
779
+ const queue = [{ path: start, depth: 0 }];
780
+ const visited = /* @__PURE__ */ new Set([start]);
781
+ while (queue.length > 0) {
782
+ const { path, depth } = queue.shift();
783
+ if (isPageFile(path)) {
784
+ const route = pagePathToRoute(path);
785
+ if (route) routes.add(route);
786
+ }
787
+ if (depth >= MAX_DEPTH) continue;
788
+ const importers = graph.reverse.get(path);
789
+ if (!importers) continue;
790
+ for (const importer of importers) {
791
+ if (visited.has(importer)) continue;
792
+ visited.add(importer);
793
+ queue.push({ path: importer, depth: depth + 1 });
794
+ }
795
+ }
796
+ return Array.from(routes);
797
+ }
776
798
 
777
799
  // src/stages/worktree.ts
778
800
  import { execSync as execSync2 } from "child_process";
@@ -956,7 +978,7 @@ async function detectTabs(page, maxTabs) {
956
978
  return allTabs.filter((t) => !t.selected).slice(0, maxTabs);
957
979
  }
958
980
  function sanitizeTabLabel(label) {
959
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
981
+ return sanitizeLabel(label);
960
982
  }
961
983
 
962
984
  // src/utils/sections.ts
@@ -1048,7 +1070,7 @@ async function cleanupSectionTags(page) {
1048
1070
  });
1049
1071
  }
1050
1072
  function sanitizeSectionLabel(label) {
1051
- return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
1073
+ return sanitizeLabel(label);
1052
1074
  }
1053
1075
 
1054
1076
  // src/stages/capture.ts
@@ -1064,11 +1086,9 @@ async function launchBrowser() {
1064
1086
  }
1065
1087
  }
1066
1088
  var MAX_COMPONENT_INSTANCES_PER_SOURCE = 20;
1067
- function normalizePath(filePath) {
1068
- return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
1069
- }
1070
1089
  function sanitizeComponentLabel(label) {
1071
- return label.toLowerCase().replace(/\.[a-z0-9]+$/i, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
1090
+ const noExt = label.replace(/\.[a-z0-9]+$/i, "");
1091
+ return sanitizeLabel(noExt, 60);
1072
1092
  }
1073
1093
  function groupBySource(instances) {
1074
1094
  const map = /* @__PURE__ */ new Map();
@@ -1383,6 +1403,88 @@ async function captureAutoSections(afterPage, beforePage, parentPrefix, parentLa
1383
1403
  await cleanupSectionTags(afterPage);
1384
1404
  await cleanupSectionTags(beforePage);
1385
1405
  }
1406
+ async function captureAutoTabs(afterPage, beforePage, task, beforeUrl, afterUrl, outputDir, options, settle, results) {
1407
+ const tabs = await detectTabs(afterPage, options.maxTabsPerRoute);
1408
+ const usedPrefixes = /* @__PURE__ */ new Set();
1409
+ for (const tab of tabs) {
1410
+ let slug = sanitizeTabLabel(tab.label);
1411
+ if (!slug) continue;
1412
+ if (usedPrefixes.has(slug)) {
1413
+ let suffix = 2;
1414
+ while (usedPrefixes.has(`${slug}-${suffix}`)) suffix++;
1415
+ slug = `${slug}-${suffix}`;
1416
+ }
1417
+ usedPrefixes.add(slug);
1418
+ const tabPrefix = `${task.prefix}~${slug}`;
1419
+ const tabLabel = `${task.label} [${tab.label}]`;
1420
+ const tabBeforePath = join6(outputDir, `${tabPrefix}-before.png`);
1421
+ const tabAfterPath = join6(outputDir, `${tabPrefix}-after.png`);
1422
+ try {
1423
+ const afterUrlBefore = afterPage.url();
1424
+ await afterPage.getByRole("tab", { name: tab.label }).first().click();
1425
+ await settle(afterPage);
1426
+ if (afterPage.url() !== afterUrlBefore) {
1427
+ await afterPage.goBack({ waitUntil: "networkidle" });
1428
+ await settle(afterPage);
1429
+ continue;
1430
+ }
1431
+ await afterPage.screenshot({ path: tabAfterPath, fullPage: true });
1432
+ try {
1433
+ const beforeUrlBefore = beforePage.url();
1434
+ await beforePage.getByRole("tab", { name: tab.label }).first().click({ timeout: 2e3 });
1435
+ await settle(beforePage);
1436
+ if (beforePage.url() !== beforeUrlBefore) {
1437
+ await beforePage.goBack({ waitUntil: "networkidle" });
1438
+ await settle(beforePage);
1439
+ await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
1440
+ } else {
1441
+ await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
1442
+ }
1443
+ } catch {
1444
+ await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
1445
+ }
1446
+ results.push({
1447
+ route: tabLabel,
1448
+ prefix: tabPrefix,
1449
+ beforePath: tabBeforePath,
1450
+ afterPath: tabAfterPath
1451
+ });
1452
+ if ((task.changedComponents?.length ?? 0) > 0) {
1453
+ await captureComponentInstances(
1454
+ afterPage,
1455
+ beforePage,
1456
+ task.changedComponents,
1457
+ tabPrefix,
1458
+ tabLabel,
1459
+ outputDir,
1460
+ tabBeforePath,
1461
+ tabAfterPath,
1462
+ results
1463
+ );
1464
+ }
1465
+ if (options.autoSections && !task.skipAutoSections) {
1466
+ await captureAutoSections(
1467
+ afterPage,
1468
+ beforePage,
1469
+ tabPrefix,
1470
+ tabLabel,
1471
+ outputDir,
1472
+ options,
1473
+ settle,
1474
+ results
1475
+ );
1476
+ }
1477
+ } catch {
1478
+ logger.dim(` Skipped tab "${tab.label}" on ${task.route}`);
1479
+ }
1480
+ }
1481
+ if (tabs.length > 0) {
1482
+ await Promise.all([
1483
+ beforePage.goto(`${beforeUrl}${task.route}`, { waitUntil: "networkidle" }),
1484
+ afterPage.goto(`${afterUrl}${task.route}`, { waitUntil: "networkidle" })
1485
+ ]);
1486
+ }
1487
+ }
1386
1488
  async function captureRoutes(tasks, beforeUrl, afterUrl, outputDir, options) {
1387
1489
  const browser = options.browser ?? await launchBrowser();
1388
1490
  const ownsBrowser = !options.browser;
@@ -1465,86 +1567,17 @@ async function captureRoutes(tasks, beforeUrl, afterUrl, outputDir, options) {
1465
1567
  );
1466
1568
  }
1467
1569
  if (options.autoTabs && !task.actions && !task.selector && !task.skipAutoTabs) {
1468
- const tabs = await detectTabs(afterPage, options.maxTabsPerRoute);
1469
- const usedPrefixes = /* @__PURE__ */ new Set();
1470
- for (const tab of tabs) {
1471
- let slug = sanitizeTabLabel(tab.label);
1472
- if (!slug) continue;
1473
- if (usedPrefixes.has(slug)) {
1474
- let suffix = 2;
1475
- while (usedPrefixes.has(`${slug}-${suffix}`)) suffix++;
1476
- slug = `${slug}-${suffix}`;
1477
- }
1478
- usedPrefixes.add(slug);
1479
- const tabPrefix = `${task.prefix}~${slug}`;
1480
- const tabLabel = `${task.label} [${tab.label}]`;
1481
- const tabBeforePath = join6(outputDir, `${tabPrefix}-before.png`);
1482
- const tabAfterPath = join6(outputDir, `${tabPrefix}-after.png`);
1483
- try {
1484
- const afterUrlBefore = afterPage.url();
1485
- await afterPage.getByRole("tab", { name: tab.label }).first().click();
1486
- await settle(afterPage);
1487
- if (afterPage.url() !== afterUrlBefore) {
1488
- await afterPage.goBack({ waitUntil: "networkidle" });
1489
- await settle(afterPage);
1490
- continue;
1491
- }
1492
- await afterPage.screenshot({ path: tabAfterPath, fullPage: true });
1493
- try {
1494
- const beforeUrlBefore = beforePage.url();
1495
- await beforePage.getByRole("tab", { name: tab.label }).first().click({ timeout: 2e3 });
1496
- await settle(beforePage);
1497
- if (beforePage.url() !== beforeUrlBefore) {
1498
- await beforePage.goBack({ waitUntil: "networkidle" });
1499
- await settle(beforePage);
1500
- await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
1501
- } else {
1502
- await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
1503
- }
1504
- } catch {
1505
- await beforePage.screenshot({ path: tabBeforePath, fullPage: true });
1506
- }
1507
- results.push({
1508
- route: tabLabel,
1509
- prefix: tabPrefix,
1510
- beforePath: tabBeforePath,
1511
- afterPath: tabAfterPath
1512
- });
1513
- if ((task.changedComponents?.length ?? 0) > 0) {
1514
- await captureComponentInstances(
1515
- afterPage,
1516
- beforePage,
1517
- task.changedComponents,
1518
- tabPrefix,
1519
- tabLabel,
1520
- outputDir,
1521
- tabBeforePath,
1522
- tabAfterPath,
1523
- results
1524
- );
1525
- }
1526
- if (options.autoSections && !task.skipAutoSections) {
1527
- await captureAutoSections(
1528
- afterPage,
1529
- beforePage,
1530
- tabPrefix,
1531
- tabLabel,
1532
- outputDir,
1533
- options,
1534
- settle,
1535
- results
1536
- );
1537
- }
1538
- } catch {
1539
- logger.dim(` Skipped tab "${tab.label}" on ${task.route}`);
1540
- }
1541
- }
1542
- if (tabs.length > 0) {
1543
- await Promise.all([
1544
- beforePage.goto(`${beforeUrl}${task.route}`, { waitUntil: "networkidle" }),
1545
- afterPage.goto(`${afterUrl}${task.route}`, { waitUntil: "networkidle" })
1546
- ]);
1547
- }
1570
+ await captureAutoTabs(
1571
+ afterPage,
1572
+ beforePage,
1573
+ task,
1574
+ beforeUrl,
1575
+ afterUrl,
1576
+ outputDir,
1577
+ options,
1578
+ settle,
1579
+ results
1580
+ );
1548
1581
  }
1549
1582
  }
1550
1583
  await Promise.all([beforeCtx.close(), afterCtx.close()]);
@@ -1775,36 +1808,10 @@ function routeToPrefix(route) {
1775
1808
  if (route === "/") return "_root";
1776
1809
  return route.replace(/^\//, "").replace(/\//g, "-");
1777
1810
  }
1778
- var ROUTE_IMPACT_MAX_DEPTH = 3;
1779
- function normalizePath2(filePath) {
1780
- return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
1781
- }
1782
- function findRoutesForChangedFile(changedFile, graph) {
1783
- const routes = /* @__PURE__ */ new Set();
1784
- const start = normalizePath2(changedFile);
1785
- const queue = [{ file: start, depth: 0 }];
1786
- const visited = /* @__PURE__ */ new Set([start]);
1787
- while (queue.length > 0) {
1788
- const { file, depth } = queue.shift();
1789
- if (isPageFile(file)) {
1790
- const route = pagePathToRoute(file);
1791
- if (route) routes.add(route);
1792
- }
1793
- if (depth >= ROUTE_IMPACT_MAX_DEPTH) continue;
1794
- const importers = graph.reverse.get(file);
1795
- if (!importers) continue;
1796
- for (const importer of importers) {
1797
- if (visited.has(importer)) continue;
1798
- visited.add(importer);
1799
- queue.push({ file: importer, depth: depth + 1 });
1800
- }
1801
- }
1802
- return Array.from(routes);
1803
- }
1804
1811
  function mapRouteToChangedComponents(changedComponentFiles, graph) {
1805
1812
  const routeMap = /* @__PURE__ */ new Map();
1806
1813
  for (const componentPath of changedComponentFiles) {
1807
- const routes = findRoutesForChangedFile(componentPath, graph);
1814
+ const routes = findRoutesForFile(componentPath, graph);
1808
1815
  for (const route of routes) {
1809
1816
  const next = routeMap.get(route) ?? [];
1810
1817
  next.push(componentPath);
@@ -1847,7 +1854,7 @@ async function runPipeline(options) {
1847
1854
  const outputDir = resolve4(cwd, output, sessionName);
1848
1855
  const startTime = Date.now();
1849
1856
  try {
1850
- const version = true ? "0.1.7" : "dev";
1857
+ const version = true ? "0.1.8" : "dev";
1851
1858
  console.log(`
1852
1859
  afterbefore v${version} \xB7 Comparing against ${base}
1853
1860
  `);
@@ -1862,7 +1869,6 @@ afterbefore v${version} \xB7 Comparing against ${base}
1862
1869
  return;
1863
1870
  }
1864
1871
  const classified = classifyFiles(diffFiles);
1865
- const visualFiles = filterVisuallyRelevant(classified);
1866
1872
  const impactfulFiles = classified.filter(
1867
1873
  (f) => f.category !== "test" && f.category !== "other"
1868
1874
  );