snapdiff-cli 0.1.4 → 0.1.6

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  <p align="center">一行命令的视觉回归测试工具</p>
4
4
  <p align="center">
5
5
  <a href="https://www.npmjs.com/package/snapdiff-cli"><img src="https://img.shields.io/npm/v/snapdiff-cli" alt="npm"></a>
6
- <a href="https://github.com/zixuanlongxi/snapdiff"><img src="https://img.shields.io/badge/license-MIT-blue" alt="license"></a>
6
+ <a href="https://github.com/zixuan57/snapdiff"><img src="https://img.shields.io/badge/license-MIT-blue" alt="license"></a>
7
7
  </p>
8
8
  </p>
9
9
 
@@ -19,6 +19,8 @@
19
19
  - PR 提交前自动对比视觉差异
20
20
  - CI 流水线中集成视觉回归检查
21
21
 
22
+ <img src="https://raw.githubusercontent.com/zixuan57/snapdiff/main/packages/cli/demo.svg" alt="snapdiff demo" width="800">
23
+
22
24
  ---
23
25
 
24
26
  ## 快速开始
@@ -1,10 +1,9 @@
1
1
  import pc from "picocolors";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { baselineImagePath, ensureDirs, captureSnapshot, saveBaselineMeta, loadConfig } from "../core/index.js";
4
+ import { baselineImagePath, ensureDirs, captureSnapshot, saveBaselineMeta, loadConfig, baselineExists } from "@snapdiff/core";
5
5
  export async function approveCommand(name) {
6
6
  const cwd = process.cwd();
7
- const { baselineExists } = await import("@snapdiff/core");
8
7
  if (!(await baselineExists(cwd, name))) {
9
8
  console.log(pc.yellow(`⚠ "${name}" 没有基线截图,无法接受。`));
10
9
  console.log(` 请先运行: ${pc.bold(`npx snapdiff capture <url> --name ${name}`)}`);
@@ -1,4 +1,4 @@
1
- import { ensureDirs, captureSnapshotsParallel, baselineImagePath, saveBaselineMeta, loadConfig, } from "../core/index.js";
1
+ import { ensureDirs, captureSnapshotsParallel, captureSnapshot, baselineImagePath, saveBaselineMeta, loadConfig } from "@snapdiff/core";
2
2
  import pc from "picocolors";
3
3
  export async function captureCommand(url, options) {
4
4
  const cwd = process.cwd();
@@ -34,7 +34,6 @@ export async function captureCommand(url, options) {
34
34
  return;
35
35
  }
36
36
  await ensureDirs(cwd);
37
- const { captureSnapshot } = await import("@snapdiff/core");
38
37
  const snap = {
39
38
  name: options.name,
40
39
  url,
@@ -1,15 +1,44 @@
1
- import { ensureDirs, captureSnapshot, captureSnapshotsParallel, baselineImagePath, diffImagePath, compareSnapshots, loadConfig, generateReportSummary, generateHtmlReport, baselineExists, } from "../core/index.js";
1
+ import { join } from "node:path";
2
+ import { unlink, readdir } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
2
4
  import pc from "picocolors";
3
- import { unlink } from "node:fs/promises";
5
+ import { ensureDirs, captureSnapshot, captureSnapshotsParallel, baselineImagePath, diffImagePath, compareSnapshots, loadConfig, generateReportSummary, generateHtmlReport, baselineExists, makePercentBar, } from "@snapdiff/core";
6
+ const TEMP_DIR = ".snapdiff/tmp";
7
+ function tempImagePath(cwd, name) {
8
+ return join(cwd, TEMP_DIR, `current-${name}.png`);
9
+ }
10
+ async function cleanupOldFiles(cwd) {
11
+ const maxAge = 7 * 24 * 60 * 60 * 1000;
12
+ const now = Date.now();
13
+ for (const subDir of ["diffs", "reports"]) {
14
+ const dir = join(cwd, ".snapdiff", subDir);
15
+ if (!existsSync(dir))
16
+ continue;
17
+ try {
18
+ const files = await readdir(dir);
19
+ for (const file of files) {
20
+ const filePath = join(dir, file);
21
+ try {
22
+ const stat = await import("node:fs/promises").then(m => m.stat(filePath));
23
+ if (now - stat.mtimeMs > maxAge) {
24
+ await unlink(filePath);
25
+ }
26
+ }
27
+ catch { }
28
+ }
29
+ }
30
+ catch { }
31
+ }
32
+ }
4
33
  export async function diffCommand(url, options) {
5
34
  const cwd = process.cwd();
6
35
  await ensureDirs(cwd);
7
36
  const threshold = parseFloat(options.threshold || "0.1");
8
- // No args run all snaps from config (parallel capture)
37
+ // No args -> run all snaps from config (parallel capture)
9
38
  if (!url && !options.name) {
10
39
  const config = await loadConfig(cwd);
11
40
  if (!config || config.snaps.length === 0) {
12
- console.log(pc.yellow(" 未找到配置,请先运行 snapdiff init 或提供参数"));
41
+ console.log(pc.yellow("\u26a0 \u672a\u627e\u5230\u914d\u7f6e\uff0c\u8bf7\u5148\u8fd0\u884c snapdiff init \u6216\u63d0\u4f9b\u53c2\u6570"));
13
42
  console.log(` ${pc.bold("npx snapdiff diff <url> --name <name>")}`);
14
43
  return;
15
44
  }
@@ -20,79 +49,102 @@ export async function diffCommand(url, options) {
20
49
  validSnaps.push(snap);
21
50
  }
22
51
  else {
23
- console.log(pc.yellow(`\n "${snap.name}" 还没有基线截图。`));
52
+ console.log(pc.yellow(`\n \u26a0 "${snap.name}" \u8fd8\u6ca1\u6709\u57fa\u7ebf\u622a\u56fe\u3002`));
24
53
  console.log(` ${pc.bold("npx snapdiff capture " + snap.url + " --name " + snap.name)}`);
25
54
  }
26
55
  }
27
56
  if (validSnaps.length === 0) {
28
- console.log(pc.dim("\n 没有需要对比的页面。"));
57
+ console.log(pc.dim("\n \u6ca1\u6709\u9700\u8981\u5bf9\u6bd4\u7684\u9875\u9762\u3002"));
29
58
  return;
30
59
  }
31
- console.log(pc.cyan(`\n 正在并行截取 ${validSnaps.length} 个页面...`));
60
+ await cleanupOldFiles(cwd);
61
+ console.log(pc.cyan(`\n \u6b63\u5728\u5e76\u884c\u622a\u53d6 ${validSnaps.length} \u4e2a\u9875\u9762...`));
32
62
  const timestamp = String(Math.floor(Date.now() / 1000));
33
- // Phase 1: capture all current states in parallel
63
+ // Phase 1: capture all current states in parallel (to tmp dir)
34
64
  const captureTasks = validSnaps.map((snap) => ({
35
65
  config: {
36
66
  ...snap,
37
67
  viewport: snap.viewport ?? { width: 1440, height: 900 },
38
68
  threshold: snap.threshold ?? 0.1,
39
69
  },
40
- outputPath: baselineImagePath(cwd, "current-" + snap.name),
70
+ outputPath: tempImagePath(cwd, snap.name),
41
71
  }));
42
72
  const captureResults = await captureSnapshotsParallel(captureTasks, 3);
43
- // Phase 2: compare all diffs (sequential, fast operations)
73
+ // Phase 2: compare all diffs
44
74
  const results = [];
75
+ const currentPaths = {};
45
76
  for (let i = 0; i < validSnaps.length; i++) {
46
77
  const snap = validSnaps[i];
47
- const currentPath = captureResults[i].imagePath;
78
+ const captureResult = captureResults[i];
79
+ if (captureResult.error) {
80
+ results.push({
81
+ name: snap.name,
82
+ url: snap.url,
83
+ diffPixels: 0,
84
+ totalPixels: 0,
85
+ diffPercent: 0,
86
+ error: '\u622a\u56fe\u5931\u8d25: ' + captureResult.error,
87
+ passed: false,
88
+ });
89
+ console.log(' \u26a0 ' + snap.name + ': \u622a\u56fe\u5931\u8d25 - ' + captureResult.error);
90
+ continue;
91
+ }
92
+ const currentPath = captureResult.imagePath;
93
+ currentPaths[snap.name] = currentPath;
48
94
  const diffOut = diffImagePath(cwd, snap.name, timestamp);
49
95
  const result = await compareSnapshots({
50
96
  baselinePath: baselineImagePath(cwd, snap.name),
51
97
  currentPath,
52
98
  diffOutputPath: diffOut,
53
99
  threshold: threshold / 100,
100
+ maskRegions: snap.maskRegions,
54
101
  });
55
102
  result.name = snap.name;
56
103
  result.url = snap.url;
57
104
  results.push(result);
58
- await unlink(currentPath).catch(() => { });
59
105
  if (result.error) {
60
- console.log(" " + snap.name + ": " + result.error);
106
+ console.log(" \u26a0 " + snap.name + ": " + result.error);
61
107
  }
62
108
  else if (result.passed) {
63
- console.log(pc.green(" " + snap.name + " ── 无变化 (差异 " + result.diffPercent + "%)"));
109
+ console.log(pc.green(" \u2705 " + snap.name + " \u2014\u2014 \u65e0\u53d8\u5316 (\u5dee\u5f02 " + result.diffPercent + "%)"));
64
110
  }
65
111
  else {
66
- const bar = makeBar(result.diffPercent, 20);
67
- console.log(" " + snap.name);
68
- console.log(" " + bar + " " + result.diffPercent + "% (" + result.diffPixels + " 像素)");
69
- console.log(pc.dim(" 📄 diff 图: " + result.diffImagePath));
70
- console.log(pc.cyan(" 如果这是预期的变更: " + pc.bold("npx snapdiff approve " + snap.name)));
112
+ const bar = makePercentBar(result.diffPercent, 20);
113
+ console.log(" \u274c " + snap.name);
114
+ console.log(" " + bar + " " + result.diffPercent + "% (" + result.diffPixels + " \u50cf\u7d20)");
115
+ console.log(pc.dim(" \ud83d\udcc4 diff \u56fe: " + result.diffImagePath));
116
+ console.log(pc.cyan(" \u5982\u679c\u8fd9\u662f\u9884\u671f\u7684\u53d8\u66f4: " + pc.bold("npx snapdiff approve " + snap.name)));
71
117
  }
72
118
  }
119
+ // Generate report BEFORE cleanup (so current images are available)
73
120
  if (results.length > 0) {
74
121
  console.log(generateReportSummary(results));
75
- const htmlPath = await generateHtmlReport(results, cwd);
76
- console.log(pc.cyan("\n 📊 HTML 报告: " + htmlPath));
122
+ const htmlPath = await generateHtmlReport(results, cwd, { currentPaths });
123
+ console.log(pc.cyan("\n \ud83d\udcca HTML \u62a5\u544a: " + htmlPath));
124
+ }
125
+ // Cleanup temp current images
126
+ for (const path of Object.values(currentPaths)) {
127
+ await unlink(path).catch(() => { });
77
128
  }
78
129
  return;
79
130
  }
80
131
  // Single URL mode
81
132
  if (!url || !options.name) {
82
- console.log(pc.yellow("请提供 URL 和名称:"));
133
+ console.log(pc.yellow("\u8bf7\u63d0\u4f9b URL \u548c\u540d\u79f0\uff1a"));
83
134
  console.log(" " + pc.bold("npx snapdiff diff <url> --name <name>"));
84
135
  return;
85
136
  }
86
137
  const name = options.name;
87
138
  if (!(await baselineExists(cwd, name))) {
88
- console.log(pc.yellow('\n "' + name + '" 还没有基线截图。'));
89
- console.log(" 请先运行: " + pc.bold("npx snapdiff capture " + url + " --name " + name));
139
+ console.log(pc.yellow('\n \u26a0 "' + name + '" \u8fd8\u6ca1\u6709\u57fa\u7ebf\u622a\u56fe\u3002'));
140
+ console.log(" \u8bf7\u5148\u8fd0\u884c: " + pc.bold("npx snapdiff capture " + url + " --name " + name));
90
141
  return;
91
142
  }
92
- console.log(pc.cyan("正在对比 " + name + "..."));
143
+ await cleanupOldFiles(cwd);
144
+ console.log(pc.cyan("\u6b63\u5728\u5bf9\u6bd4 " + name + "..."));
93
145
  const viewport = { width: 1440, height: 900 };
94
146
  const t = String(Math.floor(Date.now() / 1000));
95
- const currentPath = baselineImagePath(cwd, "current-" + name);
147
+ const currentPath = tempImagePath(cwd, name);
96
148
  const { imagePath: curPath } = await captureSnapshot({
97
149
  config: { name, url, viewport, threshold: threshold / 100 },
98
150
  outputPath: currentPath,
@@ -103,28 +155,26 @@ export async function diffCommand(url, options) {
103
155
  currentPath: curPath,
104
156
  diffOutputPath: diffOut,
105
157
  threshold: threshold / 100,
158
+ maskRegions: undefined,
106
159
  });
107
160
  result.name = name;
108
161
  result.url = url;
162
+ // Generate report before cleanup
163
+ const htmlPath = await generateHtmlReport([result], cwd, { currentPaths: { [name]: curPath } });
164
+ // Cleanup temp
109
165
  await unlink(curPath).catch(() => { });
110
166
  if (result.error) {
111
- console.log(" " + result.error);
167
+ console.log(" \u26a0 " + result.error);
112
168
  }
113
169
  else if (result.passed) {
114
- console.log(pc.green(" 无变化 (差异 " + result.diffPercent + "%)"));
170
+ console.log(pc.green(" \u2705 \u65e0\u53d8\u5316 (\u5dee\u5f02 " + result.diffPercent + "%)"));
115
171
  }
116
172
  else {
117
- const bar = makeBar(result.diffPercent, 20);
118
- console.log(" " + name);
119
- console.log(" " + bar + " " + result.diffPercent + "% (" + result.diffPixels + " 像素)");
120
- console.log(pc.dim(" 📄 diff 图: " + result.diffImagePath));
121
- console.log(pc.cyan(" 如果这是预期的变更: " + pc.bold("npx snapdiff approve " + name)));
173
+ const bar = makePercentBar(result.diffPercent, 20);
174
+ console.log(" \u274c " + name);
175
+ console.log(" " + bar + " " + result.diffPercent + "% (" + result.diffPixels + " \u50cf\u7d20)");
176
+ console.log(pc.dim(" \ud83d\udcc4 diff \u56fe: " + result.diffImagePath));
177
+ console.log(pc.cyan(" \u5982\u679c\u8fd9\u662f\u9884\u671f\u7684\u53d8\u66f4: " + pc.bold("npx snapdiff approve " + name)));
122
178
  }
123
- const htmlPath = await generateHtmlReport([result], cwd);
124
- console.log(pc.cyan("\n 📊 HTML 报告: " + htmlPath));
125
- }
126
- function makeBar(percent, width) {
127
- const filled = Math.min(Math.round((percent / 100) * width), width);
128
- const empty = width - filled;
129
- return pc.red("█".repeat(filled)) + pc.dim("░".repeat(empty));
179
+ console.log(pc.cyan("\n \ud83d\udcca HTML \u62a5\u544a: " + htmlPath));
130
180
  }
@@ -1,3 +1,4 @@
1
+ import { ensureDirs, captureSnapshot, baselineImagePath, saveBaselineMeta } from "@snapdiff/core";
1
2
  import path from "node:path";
2
3
  import { writeFile, mkdir, readFile, appendFile } from "node:fs/promises";
3
4
  import { existsSync } from "node:fs";
@@ -86,7 +87,6 @@ async function setupGitignore(cwd) {
86
87
  }
87
88
  }
88
89
  async function takeFirstBaseline(cwd, configPath) {
89
- const { ensureDirs, captureSnapshot, baselineImagePath, saveBaselineMeta } = await import("@snapdiff/core");
90
90
  const config = JSON.parse(await readFile(configPath, "utf-8"));
91
91
  await ensureDirs(cwd);
92
92
  console.log(pc.cyan("\n 正在截取基线..."));
@@ -111,17 +111,17 @@ async function maybeSetupCi(cwd, ci) {
111
111
  if (!existsSync(ciDir)) {
112
112
  await mkdir(ciDir, { recursive: true });
113
113
  }
114
- const ciYaml = `name: Visual Regression Test
115
- on: [pull_request]
116
- jobs:
117
- snapdiff:
118
- runs-on: ubuntu-latest
119
- steps:
120
- - uses: actions/checkout@v4
121
- - uses: actions/setup-node@v4
122
- with:
123
- node-version: 20
124
- - run: npx snapdiff diff
114
+ const ciYaml = `name: Visual Regression Test
115
+ on: [pull_request]
116
+ jobs:
117
+ snapdiff:
118
+ runs-on: ubuntu-latest
119
+ steps:
120
+ - uses: actions/checkout@v4
121
+ - uses: actions/setup-node@v4
122
+ with:
123
+ node-version: 20
124
+ - run: npx snapdiff diff
125
125
  `;
126
126
  await writeFile(path.join(ciDir, "snapdiff.yml"), ciYaml, "utf-8");
127
127
  console.log(pc.green(" ✔ 已创建 .github/workflows/snapdiff.yml"));
@@ -1,5 +1,5 @@
1
1
  import pc from "picocolors";
2
- import { listBaselines, loadConfig } from "../core/index.js";
2
+ import { listBaselines, loadConfig } from "@snapdiff/core";
3
3
  export async function statusCommand() {
4
4
  const cwd = process.cwd();
5
5
  const baselines = await listBaselines(cwd);
package/dist/index.js CHANGED
@@ -67,5 +67,5 @@ program
67
67
  .description("\u67e5\u770b\u6240\u6709\u57fa\u7ebf\u72b6\u6001\uff08\u8868\u683c\u5c55\u793a\uff09")
68
68
  .addHelpText("after", pc.dim("\n\u793a\u4f8b:\n $ snapdiff status \u67e5\u770b\u54ea\u4e9b\u9875\u9762\u6709\u57fa\u7ebf\uff0c\u54ea\u4e9b\u8fd8\u672a\u622a\u53d6\n"))
69
69
  .action(statusCommand);
70
- program.addHelpText("afterAll", "\n" + pc.bold("\u5feb\u901f\u5f00\u59cb") + ":\n $ snapdiff init \u9996\u6b21\u8fd0\u884c\uff0c\u8d70\u4e00\u904d\u5411\u5bfc\n $ snapdiff diff \u6539\u5b8c\u4ee3\u7801\u540e\u5bf9\u6bd4\u53d8\u5316\n\n" + pc.bold("\u5178\u578b\u5de5\u4f5c\u6d41") + ":\n 1. " + pc.dim("snapdiff init") + " \u521d\u59cb\u5316\u9879\u76ee\uff0c\u81ea\u52a8\u622a\u53d6\u9996\u5f20\u57fa\u7ebf\n 2. " + pc.dim("\u4fee\u6539\u4ee3\u7801") + " \u6539\u4f60\u7684 CSS/\u7ec4\u4ef6/\u9875\u9762\n 3. " + pc.dim("snapdiff diff") + " \u5bf9\u6bd4\u53d8\u5316\uff0c\u67e5\u770b\u5dee\u5f02\n 4. " + pc.dim("snapdiff approve <name>") + " \u786e\u8ba4\u53d8\u66f4\uff0c\u66f4\u65b0\u57fa\u7ebf\n\n" + pc.dim("\u5b8c\u6574\u6587\u6863: https://github.com/your-org/snapdiff") + "\n");
70
+ program.addHelpText("afterAll", "\n" + pc.bold("\u5feb\u901f\u5f00\u59cb") + ":\n $ snapdiff init \u9996\u6b21\u8fd0\u884c\uff0c\u8d70\u4e00\u904d\u5411\u5bfc\n $ snapdiff diff \u6539\u5b8c\u4ee3\u7801\u540e\u5bf9\u6bd4\u53d8\u5316\n\n" + pc.bold("\u5178\u578b\u5de5\u4f5c\u6d41") + ":\n 1. " + pc.dim("snapdiff init") + " \u521d\u59cb\u5316\u9879\u76ee\uff0c\u81ea\u52a8\u622a\u53d6\u9996\u5f20\u57fa\u7ebf\n 2. " + pc.dim("\u4fee\u6539\u4ee3\u7801") + " \u6539\u4f60\u7684 CSS/\u7ec4\u4ef6/\u9875\u9762\n 3. " + pc.dim("snapdiff diff") + " \u5bf9\u6bd4\u53d8\u5316\uff0c\u67e5\u770b\u5dee\u5f02\n 4. " + pc.dim("snapdiff approve <name>") + " \u786e\u8ba4\u53d8\u66f4\uff0c\u66f4\u65b0\u57fa\u7ebf\n\n" + pc.dim("\u5b8c\u6574\u6587\u6863: https://github.com/zixuan57/snapdiff") + "\n");
71
71
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapdiff-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "????????????????????????? Playwright + pixelmatch",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  "prepublishOnly": "npm run build"
17
17
  },
18
18
  "dependencies": {
19
+ "@snapdiff/core": "*",
19
20
  "playwright": "^1.52.0",
20
21
  "pixelmatch": "^6.0.0",
21
22
  "pngjs": "^7.0.0",
@@ -32,11 +33,11 @@
32
33
  },
33
34
  "repository": {
34
35
  "type": "git",
35
- "url": "git+https://github.com/zixuanlongxi/snapdiff.git"
36
+ "url": "git+https://github.com/zixuan57/snapdiff.git"
36
37
  },
37
- "homepage": "https://github.com/zixuanlongxi/snapdiff",
38
+ "homepage": "https://github.com/zixuan57/snapdiff",
38
39
  "bugs": {
39
- "url": "https://github.com/zixuanlongxi/snapdiff/issues"
40
+ "url": "https://github.com/zixuan57/snapdiff/issues"
40
41
  },
41
42
  "license": "MIT",
42
43
  "keywords": [
@@ -47,4 +48,4 @@
47
48
  "diff",
48
49
  "cli"
49
50
  ]
50
- }
51
+ }