@d-zero/archaeologist 1.0.3 → 1.1.1

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,40 +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) => {
39
- const outputUrl = c.gray(url);
40
- const sizeName = label(data.name);
41
- switch (phase) {
42
- case 'setViewport': {
43
- const { width } = data;
44
- update(`%braille% ${outputUrl} ${sizeName}: ↔️ Change viewport size to ${width}px`);
45
- break;
46
- }
47
- case 'load': {
48
- const { type } = data;
49
- update(`%braille% ${outputUrl} ${sizeName}: %earth% ${type === 'open' ? 'Open' : 'Reload'} page`);
50
- break;
51
- }
52
- case 'hook': {
53
- const { message } = data;
54
- update(`%braille% ${outputUrl} ${sizeName}: ${message}`);
55
- break;
56
- }
57
- case 'scroll': {
58
- update(`%braille% ${outputUrl} ${sizeName}: %propeller% Scroll the page`);
59
- break;
60
- }
61
- case 'screenshotStart': {
62
- update(`%braille% ${outputUrl} ${sizeName}: 📸 Take a screenshot`);
63
- break;
64
- }
65
- case 'screenshotEnd': {
66
- const { binary } = data;
67
- update(`%braille% ${outputUrl} ${sizeName}: 📸 Screenshot taken (${binary.length} bytes)`);
68
- break;
69
- }
70
- }
71
- });
38
+ const data = await getData(page, url, {
39
+ ...options,
40
+ }, update);
72
41
  dataPair.push(data);
73
42
  await delay(600);
74
43
  }
@@ -102,33 +71,41 @@ export async function analyze(list, hooks) {
102
71
  }
103
72
  }
104
73
  });
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);
74
+ let image = null;
75
+ if (imageDiff) {
76
+ update(`%braille% ${outputUrl} ${sizeName}: 🧩 Matches ${score(imageDiff.matches, 0.9)}`);
77
+ await delay(1500);
78
+ await writeFile(path.resolve(dir, `${id}_a.png`), imageDiff.images.a);
79
+ await writeFile(path.resolve(dir, `${id}_b.png`), imageDiff.images.b);
80
+ const outFilePath = path.resolve(dir, `${id}_diff.png`);
81
+ update(`%braille% ${outputUrl} ${sizeName}: 📊 Save diff image to ${path.relative(dir, outFilePath)}`);
82
+ await writeFile(outFilePath, imageDiff.images.diff);
83
+ image = {
84
+ matches: imageDiff.matches,
85
+ file: outFilePath,
86
+ };
87
+ }
88
+ const htmlDiff = diffTree(a.url, b.url, screenshotA.domTree, screenshotB.domTree);
89
+ const outFilePath = path.resolve(dir, `${id}_html.diff`);
90
+ await writeFile(outFilePath, htmlDiff.result, { encoding: 'utf8' });
112
91
  screenshotResult[name] = {
113
- matches: imageDiff.matches,
114
- file: outFilePath,
92
+ image,
93
+ dom: {
94
+ matches: htmlDiff.matches,
95
+ diff: htmlDiff.changed ? htmlDiff.result : null,
96
+ file: outFilePath,
97
+ },
115
98
  };
116
99
  }
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
100
  const result = {
121
101
  target: [a.url, b.url],
122
102
  screenshots: screenshotResult,
123
- html: {
124
- matches: htmlDiff.matches,
125
- diff: htmlDiff.changed ? htmlDiff.result : null,
126
- file: outFilePath,
127
- },
128
103
  };
129
104
  results.push(result);
130
105
  };
131
106
  }, {
107
+ limit: options?.limit,
108
+ debug: options?.debug,
132
109
  header(_, done, total) {
133
110
  return `${c.bold.magenta('🕵️ Archaeologist')} ${done}/${total}`;
134
111
  },
@@ -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
@@ -7,10 +7,15 @@ const cli = minimist(process.argv.slice(2), {
7
7
  f: 'listfile',
8
8
  },
9
9
  });
10
- if (cli.listfile) {
11
- const { pairList, pageHooks } = await readConfig(cli.listfile);
12
- await archaeologist(pairList, pageHooks);
10
+ if (cli.listfile?.length) {
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.stderr.write('Usage: archaeologist -f <listfile> [--limit <number>]\n');
16
21
  process.exit(1);
@@ -1,19 +1,18 @@
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: {
5
- a: Buffer;
6
- b: Buffer;
4
+ a: Uint8Array;
5
+ b: Uint8Array;
7
6
  };
8
7
  resize: {
9
- a: Buffer;
10
- b: Buffer;
8
+ a: Uint8Array;
9
+ b: Uint8Array;
11
10
  width: number;
12
11
  height: number;
13
12
  };
14
13
  diff: {
15
- a: Buffer;
16
- b: Buffer;
14
+ a: Uint8Array;
15
+ b: Uint8Array;
17
16
  };
18
17
  };
19
18
  type DiffImagesListener = (phase: keyof DiffImagesPhase, data: DiffImagesPhase[keyof DiffImagesPhase]) => void;
@@ -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 {};
@@ -1,15 +1,18 @@
1
- import Jimp from 'jimp';
1
+ import { Jimp, HorizontalAlign, VerticalAlign, JimpMime } 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
- const imgA = PNG.sync.read(dataA.binary);
7
- const imgB = PNG.sync.read(dataB.binary);
9
+ const imgA = PNG.sync.read(Buffer.from(dataA.binary));
10
+ const imgB = PNG.sync.read(Buffer.from(dataB.binary));
8
11
  const width = Math.max(imgA.width, imgB.width);
9
12
  const height = Math.max(imgA.height, imgB.height);
10
13
  listener('resize', { a: dataA.binary, b: dataB.binary, width, height });
11
- const resizedA = await resizeImg(dataA.binary, width, height);
12
- const resizedB = await resizeImg(dataB.binary, width, height);
14
+ const resizedA = await resizeImg(Buffer.from(dataA.binary), width, height);
15
+ const resizedB = await resizeImg(Buffer.from(dataB.binary), width, height);
13
16
  listener('diff', { a: resizedA, b: resizedB });
14
17
  const imgA_ = PNG.sync.read(resizedA);
15
18
  const imgB_ = PNG.sync.read(resizedB);
@@ -30,12 +33,10 @@ export async function diffImages(dataA, dataB, listener) {
30
33
  }
31
34
  async function resizeImg(bin, width, height) {
32
35
  const img = await Jimp.read(bin);
33
- img.contain(width, height, Jimp.HORIZONTAL_ALIGN_LEFT | Jimp.VERTICAL_ALIGN_TOP);
34
- return new Promise((resolve, reject) => {
35
- img.getBuffer(Jimp.MIME_PNG, (err, buffer) => {
36
- if (err)
37
- reject(err);
38
- resolve(buffer);
39
- });
36
+ img.contain({
37
+ w: width,
38
+ h: height,
39
+ align: HorizontalAlign.LEFT | VerticalAlign.TOP,
40
40
  });
41
+ return img.getBuffer(JimpMime.png);
41
42
  }
@@ -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
- import type { Listener, PageHook } from '@d-zero/puppeteer-screenshot';
2
+ import type { 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, update: (log: string) => void): Promise<PageData>;
package/dist/get-data.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { distill } from '@d-zero/html-distiller';
2
- import { screenshot } from '@d-zero/puppeteer-screenshot';
3
- export async function getData(page, url, hooks, listener) {
2
+ import { screenshotListener, screenshot } from '@d-zero/puppeteer-screenshot';
3
+ export async function getData(page, url, options, update) {
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
- listener,
15
+ hooks: options?.hooks ?? [],
16
+ listener: screenshotListener(update),
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-page-scan").PageHook[];
4
4
  }>;
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readPageHooks } from '@d-zero/puppeteer-page-scan';
2
4
  import { toList } from '@d-zero/readtext/list';
3
5
  import fm from 'front-matter';
4
- import { readHooks } from './read-hooks.js';
5
6
  export async function readConfig(filePath) {
6
7
  const fileContent = await fs.readFile(filePath, 'utf8');
7
8
  const content =
@@ -15,9 +16,10 @@ export async function readConfig(filePath) {
15
16
  `${content.attributes.comparisonHost}${url.pathname}${url.search}`,
16
17
  ];
17
18
  });
18
- const pageHooks = await readHooks(content.attributes?.hooks ?? [], filePath);
19
+ const baseDir = path.dirname(filePath);
20
+ const hooks = await readPageHooks(content.attributes?.hooks ?? [], baseDir);
19
21
  return {
20
22
  pairList,
21
- pageHooks,
23
+ hooks,
22
24
  };
23
25
  }
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.3",
3
+ "version": "1.1.1",
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.1.0",
30
30
  "@d-zero/html-distiller": "1.0.0",
31
- "@d-zero/puppeteer-screenshot": "1.0.3",
32
- "@d-zero/readtext": "1.0.2",
31
+ "@d-zero/puppeteer-page-scan": "1.0.0",
32
+ "@d-zero/puppeteer-screenshot": "1.2.0",
33
+ "@d-zero/readtext": "1.1.0",
33
34
  "ansi-colors": "4.1.3",
34
- "diff": "5.2.0",
35
+ "diff": "7.0.0",
35
36
  "front-matter": "4.0.2",
36
- "jimp": "0.22.12",
37
+ "jimp": "1.6.0",
37
38
  "minimist": "1.2.8",
38
- "pixelmatch": "5.3.0",
39
+ "parse-diff": "0.11.1",
40
+ "pixelmatch": "6.0.0",
39
41
  "pngjs": "7.0.0",
40
- "puppeteer": "22.11.0"
42
+ "puppeteer": "23.5.0"
41
43
  },
42
44
  "devDependencies": {
43
- "@types/diff": "5.2.1",
45
+ "@types/diff": "5.2.2",
44
46
  "@types/pixelmatch": "5.2.6",
45
- "@types/pngjs": "6.0.5",
46
- "puppeteer": "22.11.0"
47
+ "@types/pngjs": "6.0.5"
47
48
  },
48
- "gitHead": "8181fac55ec45eb13c5a25e4187b7b3621f2e34c"
49
+ "gitHead": "e8f65086bf7c316dda6667f1173da8585a5ef19c"
49
50
  }