@d-zero/archaeologist 3.5.9 → 3.6.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 +23 -1
- package/dist/analyze-child-process.js +112 -86
- package/dist/analyze-main-process.d.ts +3 -3
- package/dist/analyze-main-process.js +6 -3
- package/dist/cli.js +3 -1
- package/dist/freeze-main-process.d.ts +4 -3
- package/dist/freeze-main-process.js +4 -3
- package/dist/modules/analize-url.d.ts +3 -2
- package/dist/modules/analize-url.js +3 -2
- package/dist/modules/diff-images.d.ts +5 -4
- package/dist/modules/diff-images.js +5 -4
- package/dist/modules/diff-text.d.ts +6 -5
- package/dist/modules/diff-text.js +6 -5
- package/dist/modules/diff-tree.d.ts +6 -5
- package/dist/modules/diff-tree.js +6 -5
- package/dist/modules/fetch-html.d.ts +10 -0
- package/dist/modules/fetch-html.js +37 -0
- package/dist/modules/get-data.d.ts +6 -5
- package/dist/modules/get-data.js +6 -5
- package/dist/modules/normalize-text-document.d.ts +3 -2
- package/dist/modules/normalize-text-document.js +3 -2
- package/dist/read-config.d.ts +3 -2
- package/dist/read-config.js +3 -2
- package/dist/types.d.ts +6 -0
- package/dist/utils.d.ts +4 -3
- package/dist/utils.js +4 -3
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
- 複数のデバイスサイズでスクリーンショットを撮影可能(7種類のプリセット + カスタム設定)
|
|
9
9
|
- スクリーンショットは画像差分(ビジュアルリグレッション)を検出・出力します
|
|
10
10
|
- HTMLの差分も検出します
|
|
11
|
+
- 生HTMLソースの差分も検出します(ブラウザ不要)
|
|
11
12
|
- レスポンシブデザインの差分検証に最適
|
|
12
13
|
|
|
13
14
|
## CLI
|
|
@@ -22,7 +23,7 @@ URLリストを持つファイルを指定して実行します。
|
|
|
22
23
|
|
|
23
24
|
- `-v, --version`: バージョンを表示
|
|
24
25
|
- `-f, --listfile <filepath>`: URLリストを持つファイルのパス(必須)
|
|
25
|
-
- `-t, --type <types>`: 比較タイプの指定(`image,dom,text`、カンマ区切り)
|
|
26
|
+
- `-t, --type <types>`: 比較タイプの指定(`image,dom,text,code`、カンマ区切り)
|
|
26
27
|
- `-s, --selector <selector>`: 比較対象を限定するCSSセレクター
|
|
27
28
|
- `-i, --ignore <selector>`: 無視するCSSセレクター
|
|
28
29
|
- `-d, --devices <devices>`: デバイスプリセット(カンマ区切り、デフォルト: desktop-compact,mobile)
|
|
@@ -44,6 +45,19 @@ URLリストを持つファイルを指定して実行します。
|
|
|
44
45
|
- `mobile-large`: 414px幅(3倍解像度)
|
|
45
46
|
- `mobile-small`: 320px幅(2倍解像度)
|
|
46
47
|
|
|
48
|
+
### 比較タイプ
|
|
49
|
+
|
|
50
|
+
`-t` オプションで指定する比較タイプの詳細:
|
|
51
|
+
|
|
52
|
+
| タイプ | 説明 | ブラウザ |
|
|
53
|
+
| ------- | -------------------------------------------------------------------------------------- | -------- |
|
|
54
|
+
| `image` | スクリーンショットのピクセル単位のビジュアル差分 | 必要 |
|
|
55
|
+
| `dom` | ブラウザでレンダリングされた後のDOMツリーの差分(JavaScript実行後の状態) | 必要 |
|
|
56
|
+
| `text` | ページのテキストコンテンツの差分(形態素解析による比較) | 必要 |
|
|
57
|
+
| `code` | HTTPで取得した生HTMLソースの差分(JavaScript実行前の状態、デバイスサイズに依存しない) | 不要 |
|
|
58
|
+
|
|
59
|
+
デフォルトではすべてのタイプが有効です。`code` のみを指定した場合はブラウザを起動せずに実行されます。
|
|
60
|
+
|
|
47
61
|
### 使用例
|
|
48
62
|
|
|
49
63
|
```sh
|
|
@@ -56,6 +70,12 @@ npx @d-zero/archaeologist -f urls.txt --devices desktop,tablet,mobile
|
|
|
56
70
|
# 合成画像を出力(2つの環境のスクリーンショットを左右に並べて表示)
|
|
57
71
|
npx @d-zero/archaeologist -f urls.txt --combined
|
|
58
72
|
|
|
73
|
+
# 生HTMLソースの差分のみ比較(ブラウザ不要)
|
|
74
|
+
npx @d-zero/archaeologist -f urls.txt -t code
|
|
75
|
+
|
|
76
|
+
# 画像差分と生HTMLソース差分を併用
|
|
77
|
+
npx @d-zero/archaeologist -f urls.txt -t image,code
|
|
78
|
+
|
|
59
79
|
# フリーズモード(参照用スクリーンショット作成)
|
|
60
80
|
npx @d-zero/archaeologist --freeze urls.txt
|
|
61
81
|
```
|
|
@@ -138,3 +158,5 @@ export default async function (page, { name, width, resolution, log }) {
|
|
|
138
158
|
Basic認証が必要なページの場合はURLにユーザー名とパスワードを含めます。
|
|
139
159
|
|
|
140
160
|
例: `https://user:pass@example.com`
|
|
161
|
+
|
|
162
|
+
`image`/`dom`/`text` タイプではPuppeteerが認証を処理します。`code` タイプではHTTPリクエストに`Authorization`ヘッダーとして認証情報を付与します。サーバーがリダイレクトを返す場合でも認証ヘッダーは維持されます。
|
|
@@ -7,118 +7,144 @@ import { combineImages } from './modules/combine-images.js';
|
|
|
7
7
|
import { diffImages } from './modules/diff-images.js';
|
|
8
8
|
import { diffText } from './modules/diff-text.js';
|
|
9
9
|
import { diffTree } from './modules/diff-tree.js';
|
|
10
|
+
import { fetchHtml } from './modules/fetch-html.js';
|
|
10
11
|
import { getData } from './modules/get-data.js';
|
|
11
12
|
import { normalizeTextDocument } from './modules/normalize-text-document.js';
|
|
12
13
|
import { score } from './utils.js';
|
|
13
14
|
createChildProcess((param) => {
|
|
14
|
-
const { list, dir, types = ['image', 'dom', 'text'], selector, ignore, devices, combined = false, } = param;
|
|
15
|
+
const { list, dir, types = ['image', 'dom', 'text', 'code'], selector, ignore, devices, combined = false, } = param;
|
|
15
16
|
return {
|
|
16
17
|
async eachPage({ page, url: urlA, index }, logger) {
|
|
17
18
|
const urlPair = list.find(([url]) => url === urlA);
|
|
18
19
|
if (!urlPair) {
|
|
19
20
|
throw new Error(`Failed to find urlPair: ${urlA}`);
|
|
20
21
|
}
|
|
21
|
-
const
|
|
22
|
-
for (const url of urlPair) {
|
|
23
|
-
const data = await getData(page, url, {
|
|
24
|
-
htmlDiffOnly: !types.includes('image'),
|
|
25
|
-
selector,
|
|
26
|
-
ignore,
|
|
27
|
-
devices,
|
|
28
|
-
}, logger);
|
|
29
|
-
dataPair.push(data);
|
|
30
|
-
await delay(600);
|
|
31
|
-
}
|
|
32
|
-
const [a, b] = dataPair;
|
|
33
|
-
if (!a || !b) {
|
|
34
|
-
throw new Error('Failed to get screenshots');
|
|
35
|
-
}
|
|
22
|
+
const needsBrowser = types.some((t) => ['image', 'dom', 'text'].includes(t));
|
|
36
23
|
const screenshotResult = {};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
24
|
+
if (needsBrowser) {
|
|
25
|
+
const dataPair = [];
|
|
26
|
+
for (const url of urlPair) {
|
|
27
|
+
const data = await getData(page, url, {
|
|
28
|
+
htmlDiffOnly: !types.includes('image'),
|
|
29
|
+
selector,
|
|
30
|
+
ignore,
|
|
31
|
+
devices,
|
|
32
|
+
}, logger);
|
|
33
|
+
dataPair.push(data);
|
|
34
|
+
await delay(600);
|
|
44
35
|
}
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
36
|
+
const [a, b] = dataPair;
|
|
37
|
+
if (!a || !b) {
|
|
38
|
+
throw new Error('Failed to get screenshots');
|
|
39
|
+
}
|
|
40
|
+
const outputUrl = 'vs ' + c.gray(urlPair[1]);
|
|
41
|
+
for (const [name, screenshotA] of Object.entries(a.screenshots)) {
|
|
42
|
+
const screenshotB = b.screenshots[name];
|
|
43
|
+
const sizeName = c.bgMagenta(` ${name} `);
|
|
44
|
+
const id = `${index}_${name}`;
|
|
45
|
+
if (!screenshotB) {
|
|
46
|
+
throw new Error(`Failed to get screenshotB: ${id}`);
|
|
47
|
+
}
|
|
48
|
+
let image = null;
|
|
49
|
+
if (types.includes('image')) {
|
|
50
|
+
const imageDiff = await diffImages(screenshotA, screenshotB, (phase, data) => {
|
|
51
|
+
switch (phase) {
|
|
52
|
+
case 'create': {
|
|
53
|
+
logger(`${sizeName} ${outputUrl} 🖼️ Create images`);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'resize': {
|
|
57
|
+
const { width, height } = data;
|
|
58
|
+
logger(`${sizeName} ${outputUrl} ↔️ Resize images to ${width}x${height}`);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case 'diff': {
|
|
62
|
+
logger(`${sizeName} ${outputUrl} 📊 Compare images`);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
57
65
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
});
|
|
67
|
+
if (imageDiff) {
|
|
68
|
+
logger(`${sizeName} ${outputUrl} 🧩 Matches ${score(imageDiff.matches, 0.9)}`);
|
|
69
|
+
await delay(1500);
|
|
70
|
+
await writeFile(path.resolve(dir, `${id}_a.png`), imageDiff.images.a);
|
|
71
|
+
await writeFile(path.resolve(dir, `${id}_b.png`), imageDiff.images.b);
|
|
72
|
+
const outFilePath = path.resolve(dir, `${id}_diff.png`);
|
|
73
|
+
logger(`${sizeName} ${outputUrl} 📊 Save diff image to ${path.relative(dir, outFilePath)}`);
|
|
74
|
+
await writeFile(outFilePath, imageDiff.images.diff);
|
|
75
|
+
// 合成画像を出力
|
|
76
|
+
if (combined) {
|
|
77
|
+
const combinedImage = await combineImages(imageDiff.images.a, imageDiff.images.b);
|
|
78
|
+
const combinedFilePath = path.resolve(dir, `${id}_combined.png`);
|
|
79
|
+
logger(`${sizeName} ${outputUrl} 🖼️ Save combined image to ${path.relative(dir, combinedFilePath)}`);
|
|
80
|
+
await writeFile(combinedFilePath, combinedImage);
|
|
61
81
|
}
|
|
82
|
+
image = {
|
|
83
|
+
matches: imageDiff.matches,
|
|
84
|
+
file: outFilePath,
|
|
85
|
+
};
|
|
62
86
|
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await writeFile(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// 合成画像を出力
|
|
73
|
-
if (combined) {
|
|
74
|
-
const combinedImage = await combineImages(imageDiff.images.a, imageDiff.images.b);
|
|
75
|
-
const combinedFilePath = path.resolve(dir, `${id}_combined.png`);
|
|
76
|
-
logger(`${sizeName} ${outputUrl} 🖼️ Save combined image to ${path.relative(dir, combinedFilePath)}`);
|
|
77
|
-
await writeFile(combinedFilePath, combinedImage);
|
|
78
|
-
}
|
|
79
|
-
image = {
|
|
80
|
-
matches: imageDiff.matches,
|
|
87
|
+
}
|
|
88
|
+
let dom = null;
|
|
89
|
+
if (types.includes('dom')) {
|
|
90
|
+
const htmlDiff = diffTree(a.url, b.url, screenshotA.domTree, screenshotB.domTree);
|
|
91
|
+
const outFilePath = path.resolve(dir, `${id}_html.diff`);
|
|
92
|
+
await writeFile(outFilePath, htmlDiff.result, { encoding: 'utf8' });
|
|
93
|
+
dom = {
|
|
94
|
+
matches: htmlDiff.matches,
|
|
95
|
+
diff: htmlDiff.changed ? htmlDiff.result : null,
|
|
81
96
|
file: outFilePath,
|
|
82
97
|
};
|
|
83
98
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
let text = null;
|
|
100
|
+
if (types.includes('text')) {
|
|
101
|
+
const contentA = normalizeTextDocument(screenshotA.text.textContent);
|
|
102
|
+
const contentB = normalizeTextDocument(screenshotB.text.textContent);
|
|
103
|
+
const altTextListA = screenshotA.text.altTextList.join('\n');
|
|
104
|
+
const altTextListB = screenshotB.text.altTextList.join('\n');
|
|
105
|
+
const textA = `${contentA}\n\n${altTextListA}`;
|
|
106
|
+
const textB = `${contentB}\n\n${altTextListB}`;
|
|
107
|
+
const textDiff = diffText(a.url, b.url, textA, textB);
|
|
108
|
+
const outFilePath = path.resolve(dir, `${id}_text.diff`);
|
|
109
|
+
await writeFile(outFilePath, `${textDiff.phrases.result}\n\n${textDiff.tokens.result}`, { encoding: 'utf8' });
|
|
110
|
+
text = {
|
|
111
|
+
matches: textDiff.tokens.matches,
|
|
112
|
+
diff: textDiff.tokens.changed ? textDiff.tokens.result : null,
|
|
113
|
+
file: outFilePath,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
screenshotResult[name] = {
|
|
117
|
+
image,
|
|
118
|
+
dom,
|
|
119
|
+
text,
|
|
94
120
|
};
|
|
95
121
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
}
|
|
123
|
+
let code = null;
|
|
124
|
+
if (types.includes('code')) {
|
|
125
|
+
const fetched = await Promise.all([
|
|
126
|
+
fetchHtml(urlPair[0]),
|
|
127
|
+
fetchHtml(urlPair[1]),
|
|
128
|
+
]).catch((error) => {
|
|
129
|
+
logger(`⚠️ CODE fetch failed: ${error instanceof Error ? error.message : error}`);
|
|
130
|
+
return null;
|
|
131
|
+
});
|
|
132
|
+
if (fetched) {
|
|
133
|
+
const [htmlA, htmlB] = fetched;
|
|
134
|
+
const codeDiff = diffTree(urlPair[0], urlPair[1], htmlA, htmlB);
|
|
135
|
+
const outFilePath = path.resolve(dir, `${index}_code.diff`);
|
|
136
|
+
await writeFile(outFilePath, codeDiff.result, { encoding: 'utf8' });
|
|
137
|
+
code = {
|
|
138
|
+
matches: codeDiff.matches,
|
|
139
|
+
diff: codeDiff.changed ? codeDiff.result : null,
|
|
110
140
|
file: outFilePath,
|
|
111
141
|
};
|
|
112
142
|
}
|
|
113
|
-
screenshotResult[name] = {
|
|
114
|
-
image,
|
|
115
|
-
dom,
|
|
116
|
-
text,
|
|
117
|
-
};
|
|
118
143
|
}
|
|
119
144
|
const result = {
|
|
120
|
-
target: [
|
|
145
|
+
target: [...urlPair],
|
|
121
146
|
screenshots: screenshotResult,
|
|
147
|
+
code,
|
|
122
148
|
};
|
|
123
149
|
return result;
|
|
124
150
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AnalyzeOptions, URLPair } from './types.js';
|
|
2
2
|
import type { DealOptions } from '@d-zero/dealer';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param list
|
|
6
|
-
* @param options
|
|
4
|
+
* URLペアのリストを比較分析し、結果を`.archaeologist`ディレクトリに出力する
|
|
5
|
+
* @param list - 比較対象のURLペアのリスト
|
|
6
|
+
* @param options - 分析オプション(比較タイプ、デバイス、並列実行数など)
|
|
7
7
|
*/
|
|
8
8
|
export declare function analyze(list: readonly URLPair[], options?: AnalyzeOptions & DealOptions): Promise<void>;
|
|
@@ -6,9 +6,9 @@ import stripAnsi from 'strip-ansi';
|
|
|
6
6
|
import { analyzeUrlList } from './modules/analize-url.js';
|
|
7
7
|
import { score } from './utils.js';
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @param list
|
|
11
|
-
* @param options
|
|
9
|
+
* URLペアのリストを比較分析し、結果を`.archaeologist`ディレクトリに出力する
|
|
10
|
+
* @param list - 比較対象のURLペアのリスト
|
|
11
|
+
* @param options - 分析オプション(比較タイプ、デバイス、並列実行数など)
|
|
12
12
|
*/
|
|
13
13
|
export async function analyze(list, options) {
|
|
14
14
|
const results = [];
|
|
@@ -54,6 +54,9 @@ export async function analyze(list, options) {
|
|
|
54
54
|
output.push(` ${c.bgGreenBright(' TEXT ')}: ${score(text.matches, 0.995)} ${text.file}`);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
if (result.code) {
|
|
58
|
+
output.push(` ${c.bgCyan(' CODE ')}: ${score(result.code.matches, 0.995)} ${result.code.file}`);
|
|
59
|
+
}
|
|
57
60
|
}
|
|
58
61
|
await writeFile(path.resolve(dir, 'RESULT.txt'), stripAnsi(output.join('\n').replaceAll(dir, '.')), 'utf8');
|
|
59
62
|
process.stdout.write(output.join('\n') + '\n');
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,9 @@ const { options, hasConfigFile } = createCLI({
|
|
|
21
21
|
'',
|
|
22
22
|
'Options:',
|
|
23
23
|
'\t-f, --listfile <file> File containing URL pairs to analyze',
|
|
24
|
-
'\t-t, --type <types> Analysis types (comma-separated): image,dom,text',
|
|
24
|
+
'\t-t, --type <types> Analysis types (comma-separated): image,dom,text,code',
|
|
25
|
+
'\t image: visual diff, dom: rendered DOM diff,',
|
|
26
|
+
'\t text: text content diff, code: raw HTML source diff',
|
|
25
27
|
'\t-d, --devices <devices> Device presets (comma-separated, default: desktop-compact,mobile)',
|
|
26
28
|
'\t-s, --selector <selector> CSS selector for specific elements',
|
|
27
29
|
'\t-i, --ignore <ignore> CSS selector for elements to ignore',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { FreezeOptions } from './types.js';
|
|
2
2
|
import type { DealOptions } from '@d-zero/dealer';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param list
|
|
6
|
-
* @param options
|
|
4
|
+
* URLリストのスクリーンショットとDOMを参照用として保存し、ZIPアーカイブを生成する
|
|
5
|
+
* @param list - フリーズ対象のURLリスト
|
|
6
|
+
* @param options - フリーズオプション(並列実行数など)
|
|
7
|
+
* @returns 生成されたZIPファイルの絶対パス
|
|
7
8
|
*/
|
|
8
9
|
export declare function freeze(list: readonly string[], options?: FreezeOptions & DealOptions): Promise<string>;
|
|
@@ -6,9 +6,10 @@ import { timestamp } from '@d-zero/shared/timestamp';
|
|
|
6
6
|
import c from 'ansi-colors';
|
|
7
7
|
import { analyzeUrlList } from './modules/analize-url.js';
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @param list
|
|
11
|
-
* @param options
|
|
9
|
+
* URLリストのスクリーンショットとDOMを参照用として保存し、ZIPアーカイブを生成する
|
|
10
|
+
* @param list - フリーズ対象のURLリスト
|
|
11
|
+
* @param options - フリーズオプション(並列実行数など)
|
|
12
|
+
* @returns 生成されたZIPファイルの絶対パス
|
|
12
13
|
*/
|
|
13
14
|
export async function freeze(list, options) {
|
|
14
15
|
const name = `${timestamp('YYYYMMDD')}.archae`;
|
|
@@ -4,8 +4,9 @@ type AnalyzedUrlList = {
|
|
|
4
4
|
hasNoSSL: boolean;
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @param list
|
|
7
|
+
* URLリストを解析し、Basic認証やHTTP(非SSL)の使用有無を判定する
|
|
8
|
+
* @param list - 解析対象のURLペアまたはURL文字列のリスト
|
|
9
|
+
* @returns 認証情報の有無とSSL未使用の有無
|
|
9
10
|
*/
|
|
10
11
|
export declare function analyzeUrlList(list: readonly (URLPair | string)[]): AnalyzedUrlList;
|
|
11
12
|
export {};
|
|
@@ -17,10 +17,11 @@ export type DiffImagesPhase = {
|
|
|
17
17
|
};
|
|
18
18
|
type DiffImagesListener = (phase: keyof DiffImagesPhase, data: DiffImagesPhase[keyof DiffImagesPhase]) => void;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
* @param dataA
|
|
22
|
-
* @param dataB
|
|
23
|
-
* @param listener
|
|
20
|
+
* 2つのスクリーンショットのピクセル単位の差分を生成する
|
|
21
|
+
* @param dataA - 比較元のスクリーンショットデータ
|
|
22
|
+
* @param dataB - 比較先のスクリーンショットデータ
|
|
23
|
+
* @param listener - 処理フェーズの進捗を受け取るリスナー
|
|
24
|
+
* @returns 一致率と差分画像バッファ。バイナリが存在しない場合はnull
|
|
24
25
|
*/
|
|
25
26
|
export declare function diffImages(dataA: Screenshot, dataB: Screenshot, listener: DiffImagesListener): Promise<{
|
|
26
27
|
matches: number;
|
|
@@ -2,10 +2,11 @@ import { Jimp, HorizontalAlign, VerticalAlign, JimpMime } from 'jimp';
|
|
|
2
2
|
import pixelmatch from 'pixelmatch';
|
|
3
3
|
import { PNG } from 'pngjs';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param dataA
|
|
7
|
-
* @param dataB
|
|
8
|
-
* @param listener
|
|
5
|
+
* 2つのスクリーンショットのピクセル単位の差分を生成する
|
|
6
|
+
* @param dataA - 比較元のスクリーンショットデータ
|
|
7
|
+
* @param dataB - 比較先のスクリーンショットデータ
|
|
8
|
+
* @param listener - 処理フェーズの進捗を受け取るリスナー
|
|
9
|
+
* @returns 一致率と差分画像バッファ。バイナリが存在しない場合はnull
|
|
9
10
|
*/
|
|
10
11
|
export async function diffImages(dataA, dataB, listener) {
|
|
11
12
|
if (!dataA.binary || !dataB.binary) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @param urlA
|
|
4
|
-
* @param urlB
|
|
5
|
-
* @param phraseA
|
|
6
|
-
* @param phraseB
|
|
2
|
+
* 2つのテキストを形態素解析し、フレーズ単位とトークン頻度の両方で差分を比較する
|
|
3
|
+
* @param urlA - 比較元のURL(diffヘッダーに使用)
|
|
4
|
+
* @param urlB - 比較先のURL(diffヘッダーに使用)
|
|
5
|
+
* @param phraseA - 比較元のテキスト
|
|
6
|
+
* @param phraseB - 比較先のテキスト
|
|
7
|
+
* @returns フレーズ差分とトークン頻度差分の両方を含むオブジェクト
|
|
7
8
|
*/
|
|
8
9
|
export declare function diffText(urlA: string, urlB: string, phraseA: string, phraseB: string): {
|
|
9
10
|
phrases: {
|
|
@@ -27,11 +27,12 @@ function frequencyMap(tokens) {
|
|
|
27
27
|
.toSorted((a, b) => a.localeCompare(b));
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param urlA
|
|
32
|
-
* @param urlB
|
|
33
|
-
* @param phraseA
|
|
34
|
-
* @param phraseB
|
|
30
|
+
* 2つのテキストを形態素解析し、フレーズ単位とトークン頻度の両方で差分を比較する
|
|
31
|
+
* @param urlA - 比較元のURL(diffヘッダーに使用)
|
|
32
|
+
* @param urlB - 比較先のURL(diffヘッダーに使用)
|
|
33
|
+
* @param phraseA - 比較元のテキスト
|
|
34
|
+
* @param phraseB - 比較先のテキスト
|
|
35
|
+
* @returns フレーズ差分とトークン頻度差分の両方を含むオブジェクト
|
|
35
36
|
*/
|
|
36
37
|
export function diffText(urlA, urlB, phraseA, phraseB) {
|
|
37
38
|
const tokensA = tokenList(phraseA);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @param urlA
|
|
4
|
-
* @param urlB
|
|
5
|
-
* @param dataA
|
|
6
|
-
* @param dataB
|
|
2
|
+
* 2つのテキストデータの行差分を生成し、一致率を算出する
|
|
3
|
+
* @param urlA - 比較元のURL(diffヘッダーに使用)
|
|
4
|
+
* @param urlB - 比較先のURL(diffヘッダーに使用)
|
|
5
|
+
* @param dataA - 比較元のテキストデータ
|
|
6
|
+
* @param dataB - 比較先のテキストデータ
|
|
7
|
+
* @returns 差分結果(変更有無、最大行数、一致率、unified diff文字列)
|
|
7
8
|
*/
|
|
8
9
|
export declare function diffTree(urlA: string, urlB: string, dataA: string, dataB: string): {
|
|
9
10
|
changed: boolean;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createTwoFilesPatch } from 'diff';
|
|
2
2
|
import parse from 'parse-diff';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @param urlA
|
|
6
|
-
* @param urlB
|
|
7
|
-
* @param dataA
|
|
8
|
-
* @param dataB
|
|
4
|
+
* 2つのテキストデータの行差分を生成し、一致率を算出する
|
|
5
|
+
* @param urlA - 比較元のURL(diffヘッダーに使用)
|
|
6
|
+
* @param urlB - 比較先のURL(diffヘッダーに使用)
|
|
7
|
+
* @param dataA - 比較元のテキストデータ
|
|
8
|
+
* @param dataB - 比較先のテキストデータ
|
|
9
|
+
* @returns 差分結果(変更有無、最大行数、一致率、unified diff文字列)
|
|
9
10
|
*/
|
|
10
11
|
export function diffTree(urlA, urlB, dataA, dataB) {
|
|
11
12
|
const result = createTwoFilesPatch(urlA, urlB, dataA, dataB);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTPで生のHTMLソースを取得する
|
|
3
|
+
*
|
|
4
|
+
* URLにBasic認証情報(user:pass)が含まれている場合、
|
|
5
|
+
* Authorizationヘッダーに変換して送信する。
|
|
6
|
+
* リダイレクト先でも認証ヘッダーを維持するため、リダイレクトを手動で追跡する。
|
|
7
|
+
* @param url - 取得対象のURL
|
|
8
|
+
* @returns HTMLソース文字列
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchHtml(url: string): Promise<string>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTPで生のHTMLソースを取得する
|
|
3
|
+
*
|
|
4
|
+
* URLにBasic認証情報(user:pass)が含まれている場合、
|
|
5
|
+
* Authorizationヘッダーに変換して送信する。
|
|
6
|
+
* リダイレクト先でも認証ヘッダーを維持するため、リダイレクトを手動で追跡する。
|
|
7
|
+
* @param url - 取得対象のURL
|
|
8
|
+
* @returns HTMLソース文字列
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchHtml(url) {
|
|
11
|
+
const urlObj = new URL(url);
|
|
12
|
+
const headers = {};
|
|
13
|
+
if (urlObj.username || urlObj.password) {
|
|
14
|
+
const credentials = btoa(`${urlObj.username}:${urlObj.password}`);
|
|
15
|
+
headers['Authorization'] = `Basic ${credentials}`;
|
|
16
|
+
urlObj.username = '';
|
|
17
|
+
urlObj.password = '';
|
|
18
|
+
}
|
|
19
|
+
const maxRedirects = 10;
|
|
20
|
+
let target = urlObj.toString();
|
|
21
|
+
for (let i = 0; i <= maxRedirects; i++) {
|
|
22
|
+
const res = await fetch(target, { headers, redirect: 'manual' });
|
|
23
|
+
if (res.status >= 300 && res.status < 400) {
|
|
24
|
+
const location = res.headers.get('location');
|
|
25
|
+
if (!location) {
|
|
26
|
+
throw new Error(`Redirect without Location header from ${target}`);
|
|
27
|
+
}
|
|
28
|
+
target = new URL(location, target).toString();
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
33
|
+
}
|
|
34
|
+
return res.text();
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Too many redirects fetching ${url}`);
|
|
37
|
+
}
|
|
@@ -9,10 +9,11 @@ export interface GetDataOptions {
|
|
|
9
9
|
readonly devices?: readonly string[];
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @param page
|
|
14
|
-
* @param url
|
|
15
|
-
* @param options
|
|
16
|
-
* @param update
|
|
12
|
+
* Puppeteerでページを開き、各デバイスサイズのスクリーンショットとDOMツリーを取得する
|
|
13
|
+
* @param page - PuppeteerのPageインスタンス
|
|
14
|
+
* @param url - 取得対象のURL
|
|
15
|
+
* @param options - スクリーンショット取得のオプション
|
|
16
|
+
* @param update - 進捗ログを受け取るコールバック
|
|
17
|
+
* @returns 各デバイスサイズのスクリーンショットとDOMツリーを含むページデータ
|
|
17
18
|
*/
|
|
18
19
|
export declare function getData(page: Page, url: string, options: GetDataOptions, update: (log: string) => void): Promise<PageData>;
|
package/dist/modules/get-data.js
CHANGED
|
@@ -2,11 +2,12 @@ import { distill } from '@d-zero/html-distiller';
|
|
|
2
2
|
import { createSizesFromDevices } from '@d-zero/puppeteer-page-scan';
|
|
3
3
|
import { screenshotListener, screenshot } from '@d-zero/puppeteer-screenshot';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param page
|
|
7
|
-
* @param url
|
|
8
|
-
* @param options
|
|
9
|
-
* @param update
|
|
5
|
+
* Puppeteerでページを開き、各デバイスサイズのスクリーンショットとDOMツリーを取得する
|
|
6
|
+
* @param page - PuppeteerのPageインスタンス
|
|
7
|
+
* @param url - 取得対象のURL
|
|
8
|
+
* @param options - スクリーンショット取得のオプション
|
|
9
|
+
* @param update - 進捗ログを受け取るコールバック
|
|
10
|
+
* @returns 各デバイスサイズのスクリーンショットとDOMツリーを含むページデータ
|
|
10
11
|
*/
|
|
11
12
|
export async function getData(page, url, options, update) {
|
|
12
13
|
const htmlDiffOnly = options.htmlDiffOnly ?? false;
|
package/dist/read-config.d.ts
CHANGED
package/dist/read-config.js
CHANGED
|
@@ -2,8 +2,9 @@ import { readPageHooks } from '@d-zero/puppeteer-page-scan';
|
|
|
2
2
|
import { toList } from '@d-zero/readtext/list';
|
|
3
3
|
import { readConfigFile } from '@d-zero/shared/config-reader';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param filePath
|
|
5
|
+
* Frontmatter形式の設定ファイルを読み込み、URLペアリストとページフックを返す
|
|
6
|
+
* @param filePath - 設定ファイルのパス
|
|
7
|
+
* @returns URLペアのリストとページフック関数の配列
|
|
7
8
|
*/
|
|
8
9
|
export async function readConfig(filePath) {
|
|
9
10
|
const { content, baseDir } = await readConfigFile(filePath);
|
package/dist/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export type URLPair = readonly [urlA: string, urlB: string];
|
|
|
10
10
|
export type Result = {
|
|
11
11
|
target: [urlA: string, urlB: string];
|
|
12
12
|
screenshots: Record<string, MediaResult>;
|
|
13
|
+
code: CodeResult | null;
|
|
13
14
|
};
|
|
14
15
|
export type MediaResult = {
|
|
15
16
|
image: ImageResult | null;
|
|
@@ -30,6 +31,11 @@ export type TextResult = {
|
|
|
30
31
|
diff: string | null;
|
|
31
32
|
file: string;
|
|
32
33
|
};
|
|
34
|
+
export type CodeResult = {
|
|
35
|
+
matches: number;
|
|
36
|
+
diff: string | null;
|
|
37
|
+
file: string;
|
|
38
|
+
};
|
|
33
39
|
export interface ArchaeologistOptions extends AnalyzeOptions {
|
|
34
40
|
}
|
|
35
41
|
export interface AnalyzeOptions extends GeneralOptions {
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @param matches
|
|
4
|
-
* @param threshold
|
|
2
|
+
* 一致率をパーセント表示し、閾値に基づいて色付けする
|
|
3
|
+
* @param matches - 一致率(0〜1の範囲)
|
|
4
|
+
* @param threshold - 合格とみなす閾値(超えると緑、以下は赤)
|
|
5
|
+
* @returns ANSI色付きのパーセント文字列
|
|
5
6
|
*/
|
|
6
7
|
export declare function score(matches: number, threshold: number): string;
|
package/dist/utils.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import c from 'ansi-colors';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* @param matches
|
|
5
|
-
* @param threshold
|
|
3
|
+
* 一致率をパーセント表示し、閾値に基づいて色付けする
|
|
4
|
+
* @param matches - 一致率(0〜1の範囲)
|
|
5
|
+
* @param threshold - 合格とみなす閾値(超えると緑、以下は赤)
|
|
6
|
+
* @returns ANSI色付きのパーセント文字列
|
|
6
7
|
*/
|
|
7
8
|
export function score(matches, threshold) {
|
|
8
9
|
const color = matches > threshold ? c.green : c.red;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d-zero/archaeologist",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Uncover visual and HTML differences in web pages with precision",
|
|
5
5
|
"author": "D-ZERO",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"clean": "tsc --build --clean"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@d-zero/cli-core": "1.3.
|
|
27
|
+
"@d-zero/cli-core": "1.3.6",
|
|
28
28
|
"@d-zero/fs": "0.2.2",
|
|
29
29
|
"@d-zero/html-distiller": "2.0.4",
|
|
30
|
-
"@d-zero/puppeteer-dealer": "0.7.
|
|
31
|
-
"@d-zero/puppeteer-page-scan": "4.4.
|
|
32
|
-
"@d-zero/puppeteer-screenshot": "3.3.
|
|
30
|
+
"@d-zero/puppeteer-dealer": "0.7.8",
|
|
31
|
+
"@d-zero/puppeteer-page-scan": "4.4.7",
|
|
32
|
+
"@d-zero/puppeteer-screenshot": "3.3.7",
|
|
33
33
|
"@d-zero/readtext": "1.1.20",
|
|
34
|
-
"@d-zero/shared": "0.
|
|
34
|
+
"@d-zero/shared": "0.21.1",
|
|
35
35
|
"ansi-colors": "4.1.3",
|
|
36
36
|
"diff": "8.0.3",
|
|
37
37
|
"front-matter": "4.0.2",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"@types/pixelmatch": "5.2.6",
|
|
50
50
|
"@types/pngjs": "6.0.5"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "6f13c72151f3d6fe5ddf72d0cde61b31a7fc96ac"
|
|
53
53
|
}
|