@d-zero/archaeologist 1.0.2 → 1.1.0

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
@@ -12,11 +12,19 @@
12
12
  ## CLI
13
13
 
14
14
  ```sh
15
- npx @d-zero/archaeologist -f <filepath>
15
+ npx @d-zero/archaeologist -f <filepath> [--limit <number>] [--debug]
16
16
  ```
17
17
 
18
18
  URLリストを持つファイルを指定して実行します。
19
19
 
20
+ ### オプション
21
+
22
+ - `-f, --file <filepath>`: URLリストを持つファイルのパス(必須)
23
+ - `--limit <number>`: 並列実行数の上限(デフォルト: 10)
24
+ - `--debug`: デバッグモード(デフォルト: false)
25
+
26
+ ### ファイルフォーマット
27
+
20
28
  ファイルの先頭には比較対象のホストを指定します。[Frontmatter](https://jekyllrb.com/docs/front-matter/)形式で`comparisonHost`に指定します。
21
29
 
22
30
  ```txt
package/dist/analyze.d.ts CHANGED
@@ -1,3 +1,9 @@
1
1
  import type { Result, URLPair } from './types.js';
2
2
  import type { PageHook } from '@d-zero/puppeteer-screenshot';
3
- export declare function analyze(list: readonly URLPair[], hooks: readonly PageHook[]): Promise<Result[]>;
3
+ export interface AnalyzeOptions {
4
+ readonly hooks: readonly PageHook[];
5
+ readonly htmlDiffOnly?: boolean;
6
+ readonly limit?: number;
7
+ readonly debug?: boolean;
8
+ }
9
+ export declare function analyze(list: readonly URLPair[], options?: AnalyzeOptions): Promise<Result[]>;
package/dist/analyze.js CHANGED
@@ -8,7 +8,7 @@ import { diffImages } from './diff-images.js';
8
8
  import { diffTree } from './diff-tree.js';
9
9
  import { getData } from './get-data.js';
10
10
  import { label, score } from './output-utils.js';
11
- export async function analyze(list, hooks) {
11
+ export async function analyze(list, options) {
12
12
  const urlInfo = analyzeUrlList(list);
13
13
  const useOldMode = urlInfo.hasAuth && urlInfo.hasNoSSL;
14
14
  const browser = await puppeteer.launch({
@@ -35,7 +35,9 @@ export async function analyze(list, hooks) {
35
35
  return async () => {
36
36
  const dataPair = [];
37
37
  for (const url of urlPair) {
38
- const data = await getData(page, url, hooks, (phase, data) => {
38
+ const data = await getData(page, url, {
39
+ ...options,
40
+ }, (phase, data) => {
39
41
  const outputUrl = c.gray(url);
40
42
  const sizeName = label(data.name);
41
43
  switch (phase) {
@@ -102,33 +104,41 @@ export async function analyze(list, hooks) {
102
104
  }
103
105
  }
104
106
  });
105
- update(`%braille% ${outputUrl} ${sizeName}: 🧩 Matches ${score(imageDiff.matches, 0.9)}`);
106
- await delay(1500);
107
- await writeFile(path.resolve(dir, `${id}_a.png`), imageDiff.images.a);
108
- await writeFile(path.resolve(dir, `${id}_b.png`), imageDiff.images.b);
109
- const outFilePath = path.resolve(dir, `${id}_diff.png`);
110
- update(`%braille% ${outputUrl} ${sizeName}: 📊 Save diff image to ${path.relative(dir, outFilePath)}`);
111
- await writeFile(outFilePath, imageDiff.images.diff);
107
+ let image = null;
108
+ if (imageDiff) {
109
+ update(`%braille% ${outputUrl} ${sizeName}: 🧩 Matches ${score(imageDiff.matches, 0.9)}`);
110
+ await delay(1500);
111
+ await writeFile(path.resolve(dir, `${id}_a.png`), imageDiff.images.a);
112
+ await writeFile(path.resolve(dir, `${id}_b.png`), imageDiff.images.b);
113
+ const outFilePath = path.resolve(dir, `${id}_diff.png`);
114
+ update(`%braille% ${outputUrl} ${sizeName}: 📊 Save diff image to ${path.relative(dir, outFilePath)}`);
115
+ await writeFile(outFilePath, imageDiff.images.diff);
116
+ image = {
117
+ matches: imageDiff.matches,
118
+ file: outFilePath,
119
+ };
120
+ }
121
+ const htmlDiff = diffTree(a.url, b.url, screenshotA.domTree, screenshotB.domTree);
122
+ const outFilePath = path.resolve(dir, `${id}_html.diff`);
123
+ await writeFile(outFilePath, htmlDiff.result, { encoding: 'utf8' });
112
124
  screenshotResult[name] = {
113
- matches: imageDiff.matches,
114
- file: outFilePath,
125
+ image,
126
+ dom: {
127
+ matches: htmlDiff.matches,
128
+ diff: htmlDiff.changed ? htmlDiff.result : null,
129
+ file: outFilePath,
130
+ },
115
131
  };
116
132
  }
117
- const htmlDiff = diffTree(a.serializedHtml, b.serializedHtml);
118
- const outFilePath = path.resolve(dir, `${index}_html.diff`);
119
- await writeFile(outFilePath, htmlDiff.result, { encoding: 'utf8' });
120
133
  const result = {
121
134
  target: [a.url, b.url],
122
135
  screenshots: screenshotResult,
123
- html: {
124
- matches: htmlDiff.matches,
125
- diff: htmlDiff.changed ? htmlDiff.result : null,
126
- file: outFilePath,
127
- },
128
136
  };
129
137
  results.push(result);
130
138
  };
131
139
  }, {
140
+ limit: options?.limit,
141
+ debug: options?.debug,
132
142
  header(_, done, total) {
133
143
  return `${c.bold.magenta('🕵️ Archaeologist')} ${done}/${total}`;
134
144
  },
@@ -1,3 +1,5 @@
1
+ import type { AnalyzeOptions } from './analyze.js';
1
2
  import type { URLPair } from './types.js';
2
- import type { PageHook } from '@d-zero/puppeteer-screenshot';
3
- export declare function archaeologist(list: readonly URLPair[], pageHooks?: readonly PageHook[]): Promise<void>;
3
+ export interface ArchaeologistOptions extends AnalyzeOptions {
4
+ }
5
+ export declare function archaeologist(list: readonly URLPair[], options?: ArchaeologistOptions): Promise<void>;
@@ -1,15 +1,18 @@
1
1
  import c from 'ansi-colors';
2
2
  import { analyze } from './analyze.js';
3
3
  import { label, score } from './output-utils.js';
4
- export async function archaeologist(list, pageHooks) {
5
- const results = await analyze(list, pageHooks ?? []);
4
+ export async function archaeologist(list, options) {
5
+ const results = await analyze(list, options);
6
6
  const output = [];
7
7
  for (const result of results) {
8
8
  output.push(c.gray(`${result.target.join(' vs ')}`));
9
- for (const [sizeName, { matches, file }] of Object.entries(result.screenshots)) {
10
- output.push(` ${label(sizeName)} ${score(matches, 0.9)} ${file}`);
9
+ for (const [sizeName, { image, dom }] of Object.entries(result.screenshots)) {
10
+ if (image) {
11
+ const { matches, file } = image;
12
+ output.push(` ${label(sizeName)} ${score(matches, 0.9)} ${file}`);
13
+ }
14
+ output.push(` ${label('HTML', c.bgBlueBright)}: ${score(dom.matches, 0.995)} ${dom.file}`);
11
15
  }
12
- output.push(` ${label('HTML', c.bgBlueBright)}: ${score(result.html.matches, 0.995)} ${result.html.file}`);
13
16
  }
14
17
  process.stdout.write(output.join('\n') + '\n');
15
18
  }
package/dist/cli.js CHANGED
@@ -8,9 +8,14 @@ const cli = minimist(process.argv.slice(2), {
8
8
  },
9
9
  });
10
10
  if (cli.listfile) {
11
- const { pairList, pageHooks } = await readConfig(cli.listfile);
12
- await archaeologist(pairList, pageHooks);
11
+ const { pairList, hooks } = await readConfig(cli.listfile);
12
+ await archaeologist(pairList, {
13
+ hooks,
14
+ limit: cli.limit ? Number.parseInt(cli.limit) : undefined,
15
+ debug: !!cli.debug,
16
+ htmlDiffOnly: !!cli.htmlDiffOnly,
17
+ });
13
18
  process.exit(0);
14
19
  }
15
- process.stdout.write('Usage: archaeologist -f <listfile>\n');
20
+ process.stdout.write('Usage: archaeologist -f <listfile> [--limit <number>]');
16
21
  process.exit(1);
@@ -1,4 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import type { Screenshot } from '@d-zero/puppeteer-screenshot';
3
2
  export type DiffImagesPhase = {
4
3
  create: {
@@ -24,5 +23,5 @@ export declare function diffImages(dataA: Screenshot, dataB: Screenshot, listene
24
23
  b: Buffer;
25
24
  diff: Buffer;
26
25
  };
27
- }>;
26
+ } | null>;
28
27
  export {};
@@ -2,6 +2,9 @@ import Jimp from 'jimp';
2
2
  import pixelmatch from 'pixelmatch';
3
3
  import { PNG } from 'pngjs';
4
4
  export async function diffImages(dataA, dataB, listener) {
5
+ if (!dataA.binary || !dataB.binary) {
6
+ return null;
7
+ }
5
8
  listener('create', { a: dataA.binary, b: dataB.binary });
6
9
  const imgA = PNG.sync.read(dataA.binary);
7
10
  const imgB = PNG.sync.read(dataB.binary);
@@ -1,7 +1,6 @@
1
- export declare function diffTree(dataA: string, dataB: string): {
1
+ export declare function diffTree(urlA: string, urlB: string, dataA: string, dataB: string): {
2
2
  changed: boolean;
3
3
  maxLine: number;
4
- changedLines: number;
5
4
  matches: number;
6
5
  result: string;
7
6
  };
package/dist/diff-tree.js CHANGED
@@ -1,32 +1,18 @@
1
- import { diffLines } from 'diff';
2
- export function diffTree(dataA, dataB) {
3
- const changes = diffLines(dataA, dataB);
1
+ import { createTwoFilesPatch } from 'diff';
2
+ import parse from 'parse-diff';
3
+ export function diffTree(urlA, urlB, dataA, dataB) {
4
+ const result = createTwoFilesPatch(urlA, urlB, dataA, dataB);
5
+ const info = parse(result)[0];
6
+ if (!info) {
7
+ throw new Error('Failed to parse diff');
8
+ }
4
9
  const lineA = dataA.split('\n').length;
5
10
  const lineB = dataB.split('\n').length;
6
11
  const maxLine = Math.max(lineA, lineB);
7
- let changedLines = 0;
8
- const result = changes
9
- .map((change) => {
10
- if (change.added) {
11
- changedLines++;
12
- return `+${change.value}`;
13
- }
14
- if (change.removed) {
15
- changedLines++;
16
- return `-${change.value}`;
17
- }
18
- return change.value
19
- .split('\n')
20
- .map((line) => (line.trim() ? ` ${line}` : line))
21
- .join('\n');
22
- })
23
- .filter(Boolean)
24
- .join('');
25
12
  return {
26
- changed: changedLines > 0,
13
+ changed: dataA !== dataB,
27
14
  maxLine,
28
- changedLines,
29
- matches: 1 - changedLines / maxLine,
15
+ matches: 1 - Math.abs((info.additions - info.deletions) / maxLine),
30
16
  result,
31
17
  };
32
18
  }
@@ -1,4 +1,8 @@
1
1
  import type { PageData } from './types.js';
2
2
  import type { Listener, PageHook } from '@d-zero/puppeteer-screenshot';
3
3
  import type { Page } from 'puppeteer';
4
- export declare function getData(page: Page, url: string, hooks: readonly PageHook[], listener: Listener): Promise<PageData>;
4
+ export interface GetDataOptions {
5
+ readonly hooks?: readonly PageHook[];
6
+ readonly htmlDiffOnly?: boolean;
7
+ }
8
+ export declare function getData(page: Page, url: string, options: GetDataOptions, listener: Listener): Promise<PageData>;
package/dist/get-data.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { distill } from '@d-zero/html-distiller';
2
2
  import { screenshot } from '@d-zero/puppeteer-screenshot';
3
- export async function getData(page, url, hooks, listener) {
3
+ export async function getData(page, url, options, listener) {
4
+ const htmlDiffOnly = options.htmlDiffOnly ?? false;
4
5
  const screenshots = await screenshot(page, url, {
5
6
  sizes: {
6
7
  desktop: {
@@ -11,15 +12,16 @@ export async function getData(page, url, hooks, listener) {
11
12
  resolution: 2,
12
13
  },
13
14
  },
14
- hooks,
15
+ hooks: options?.hooks ?? [],
15
16
  listener,
17
+ domOnly: htmlDiffOnly,
16
18
  });
17
- const html = await page.content();
18
- const serializedHtmlTree = distill(html).tree;
19
- const serializedHtml = JSON.stringify(serializedHtmlTree, null, 2);
20
- return {
21
- url,
22
- serializedHtml,
23
- screenshots,
24
- };
19
+ const data = { url, screenshots: {} };
20
+ for (const [sizeName, screenshot] of Object.entries(screenshots)) {
21
+ data.screenshots[sizeName] = {
22
+ ...screenshot,
23
+ domTree: JSON.stringify(distill(screenshot.dom).tree, null, 2),
24
+ };
25
+ }
26
+ return data;
25
27
  }
@@ -1,4 +1,4 @@
1
1
  export declare function readConfig(filePath: string): Promise<{
2
2
  pairList: [string, string][];
3
- pageHooks: import("@d-zero/puppeteer-screenshot").PageHook[];
3
+ hooks: import("@d-zero/puppeteer-screenshot").PageHook[];
4
4
  }>;
@@ -15,9 +15,9 @@ export async function readConfig(filePath) {
15
15
  `${content.attributes.comparisonHost}${url.pathname}${url.search}`,
16
16
  ];
17
17
  });
18
- const pageHooks = await readHooks(content.attributes?.hooks ?? [], filePath);
18
+ const hooks = await readHooks(content.attributes?.hooks ?? [], filePath);
19
19
  return {
20
20
  pairList,
21
- pageHooks,
21
+ hooks,
22
22
  };
23
23
  }
package/dist/types.d.ts CHANGED
@@ -2,19 +2,25 @@ export type { PageHook } from '@d-zero/puppeteer-screenshot';
2
2
  import type { Screenshot } from '@d-zero/puppeteer-screenshot';
3
3
  export type PageData = {
4
4
  url: string;
5
- serializedHtml: string;
6
- screenshots: Record<string, Screenshot>;
5
+ screenshots: Record<string, Screenshot & {
6
+ domTree: string;
7
+ }>;
7
8
  };
8
9
  export type URLPair = readonly [urlA: string, urlB: string];
9
10
  export type Result = {
10
11
  target: [urlA: string, urlB: string];
11
- screenshots: Record<string, {
12
- matches: number;
13
- file: string;
14
- }>;
15
- html: {
16
- matches: number;
17
- diff: string | null;
18
- file: string;
19
- };
12
+ screenshots: Record<string, MediaResult>;
13
+ };
14
+ export type MediaResult = {
15
+ image: ImageResult | null;
16
+ dom: DOMResult;
17
+ };
18
+ export type ImageResult = {
19
+ matches: number;
20
+ file: string;
21
+ };
22
+ export type DOMResult = {
23
+ matches: number;
24
+ diff: string | null;
25
+ file: string;
20
26
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d-zero/archaeologist",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Uncover visual and HTML differences in web pages with precision",
5
5
  "author": "D-ZERO",
6
6
  "license": "MIT",
@@ -26,24 +26,25 @@
26
26
  "clean": "tsc --build --clean"
27
27
  },
28
28
  "dependencies": {
29
- "@d-zero/dealer": "1.0.1",
29
+ "@d-zero/dealer": "1.0.2",
30
30
  "@d-zero/html-distiller": "1.0.0",
31
- "@d-zero/puppeteer-screenshot": "1.0.2",
32
- "@d-zero/readtext": "1.0.1",
31
+ "@d-zero/puppeteer-screenshot": "1.1.0",
32
+ "@d-zero/readtext": "1.0.3",
33
33
  "ansi-colors": "4.1.3",
34
34
  "diff": "5.2.0",
35
35
  "front-matter": "4.0.2",
36
36
  "jimp": "0.22.12",
37
37
  "minimist": "1.2.8",
38
+ "parse-diff": "0.11.1",
38
39
  "pixelmatch": "5.3.0",
39
40
  "pngjs": "7.0.0",
40
- "puppeteer": "22.9.0"
41
+ "puppeteer": "22.12.0"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/diff": "5.2.1",
44
45
  "@types/pixelmatch": "5.2.6",
45
46
  "@types/pngjs": "6.0.5",
46
- "puppeteer": "22.9.0"
47
+ "puppeteer": "22.12.0"
47
48
  },
48
- "gitHead": "acfefd8b20364c2aec1998517fc0b190fdc3b404"
49
+ "gitHead": "9b4c167933f33237bb688e4835f17b499dc4c48d"
49
50
  }