@d-zero/archaeologist 3.5.10 → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 D-ZERO Co., Ltd.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
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 dataPair = [];
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
- const outputUrl = 'vs ' + c.gray(urlPair[1]);
38
- for (const [name, screenshotA] of Object.entries(a.screenshots)) {
39
- const screenshotB = b.screenshots[name];
40
- const sizeName = c.bgMagenta(` ${name} `);
41
- const id = `${index}_${name}`;
42
- if (!screenshotB) {
43
- throw new Error(`Failed to get screenshotB: ${id}`);
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
- let image = null;
46
- if (types.includes('image')) {
47
- const imageDiff = await diffImages(screenshotA, screenshotB, (phase, data) => {
48
- switch (phase) {
49
- case 'create': {
50
- logger(`${sizeName} ${outputUrl} 🖼️ Create images`);
51
- break;
52
- }
53
- case 'resize': {
54
- const { width, height } = data;
55
- logger(`${sizeName} ${outputUrl} ↔️ Resize images to ${width}x${height}`);
56
- break;
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
- case 'diff': {
59
- logger(`${sizeName} ${outputUrl} 📊 Compare images`);
60
- break;
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
- if (imageDiff) {
65
- logger(`${sizeName} ${outputUrl} 🧩 Matches ${score(imageDiff.matches, 0.9)}`);
66
- await delay(1500);
67
- await writeFile(path.resolve(dir, `${id}_a.png`), imageDiff.images.a);
68
- await writeFile(path.resolve(dir, `${id}_b.png`), imageDiff.images.b);
69
- const outFilePath = path.resolve(dir, `${id}_diff.png`);
70
- logger(`${sizeName} ${outputUrl} 📊 Save diff image to ${path.relative(dir, outFilePath)}`);
71
- await writeFile(outFilePath, imageDiff.images.diff);
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
- let dom = null;
86
- if (types.includes('dom')) {
87
- const htmlDiff = diffTree(a.url, b.url, screenshotA.domTree, screenshotB.domTree);
88
- const outFilePath = path.resolve(dir, `${id}_html.diff`);
89
- await writeFile(outFilePath, htmlDiff.result, { encoding: 'utf8' });
90
- dom = {
91
- matches: htmlDiff.matches,
92
- diff: htmlDiff.changed ? htmlDiff.result : null,
93
- file: outFilePath,
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
- let text = null;
97
- if (types.includes('text')) {
98
- const contentA = normalizeTextDocument(screenshotA.text.textContent);
99
- const contentB = normalizeTextDocument(screenshotB.text.textContent);
100
- const altTextListA = screenshotA.text.altTextList.join('\n');
101
- const altTextListB = screenshotB.text.altTextList.join('\n');
102
- const textA = `${contentA}\n\n${altTextListA}`;
103
- const textB = `${contentB}\n\n${altTextListB}`;
104
- const textDiff = diffText(a.url, b.url, textA, textB);
105
- const outFilePath = path.resolve(dir, `${id}_text.diff`);
106
- await writeFile(outFilePath, `${textDiff.phrases.result}\n\n${textDiff.tokens.result}`, { encoding: 'utf8' });
107
- text = {
108
- matches: textDiff.tokens.matches,
109
- diff: textDiff.tokens.changed ? textDiff.tokens.result : null,
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: [a.url, b.url],
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 {};
@@ -1,6 +1,7 @@
1
1
  /**
2
- *
3
- * @param list
2
+ * URLリストを解析し、Basic認証やHTTP(非SSL)の使用有無を判定する
3
+ * @param list - 解析対象のURLペアまたはURL文字列のリスト
4
+ * @returns 認証情報の有無とSSL未使用の有無
4
5
  */
5
6
  export function analyzeUrlList(list) {
6
7
  const result = {
@@ -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>;
@@ -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;
@@ -1,5 +1,6 @@
1
1
  /**
2
- *
3
- * @param text
2
+ * テキストを正規化する(連続空白を改行に変換、句点で改行、連続改行を単一改行に統合)
3
+ * @param text - 正規化対象のテキスト
4
+ * @returns 正規化されたテキスト
4
5
  */
5
6
  export declare function normalizeTextDocument(text: string): string;
@@ -1,6 +1,7 @@
1
1
  /**
2
- *
3
- * @param text
2
+ * テキストを正規化する(連続空白を改行に変換、句点で改行、連続改行を単一改行に統合)
3
+ * @param text - 正規化対象のテキスト
4
+ * @returns 正規化されたテキスト
4
5
  */
5
6
  export function normalizeTextDocument(text) {
6
7
  return (text
@@ -1,6 +1,7 @@
1
1
  /**
2
- *
3
- * @param filePath
2
+ * Frontmatter形式の設定ファイルを読み込み、URLペアリストとページフックを返す
3
+ * @param filePath - 設定ファイルのパス
4
+ * @returns URLペアのリストとページフック関数の配列
4
5
  */
5
6
  export declare function readConfig(filePath: string): Promise<{
6
7
  pairList: [string, string][];
@@ -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.5.10",
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.5",
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.7",
31
- "@d-zero/puppeteer-page-scan": "4.4.6",
32
- "@d-zero/puppeteer-screenshot": "3.3.6",
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.21.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",
@@ -48,5 +48,6 @@
48
48
  "@types/diff": "8.0.0",
49
49
  "@types/pixelmatch": "5.2.6",
50
50
  "@types/pngjs": "6.0.5"
51
- }
51
+ },
52
+ "gitHead": "6f13c72151f3d6fe5ddf72d0cde61b31a7fc96ac"
52
53
  }