@d-zero/beholder 2.1.5 → 2.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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [2.1.6](https://github.com/d-zero-dev/tools/compare/@d-zero/beholder@2.1.5...@d-zero/beholder@2.1.6) (2026-06-15)
7
+
8
+ ### Bug Fixes
9
+
10
+ - **beholder:** prevent getMeta from hanging on unresponsive pages ([f55bb26](https://github.com/d-zero-dev/tools/commit/f55bb261c1868b40709cbae6aa17d273c5516e74)), closes [#874](https://github.com/d-zero-dev/tools/issues/874)
11
+
6
12
  ## [2.1.5](https://github.com/d-zero-dev/tools/compare/@d-zero/beholder@2.1.4...@d-zero/beholder@2.1.5) (2026-05-27)
7
13
 
8
14
  **Note:** Version bump only for package @d-zero/beholder
package/README.md CHANGED
@@ -1,58 +1,16 @@
1
1
  # `@d-zero/beholder`
2
2
 
3
- Puppeteer を使用してWebページをスクレイピングし、メタデータ・リンク・画像・ネットワークリソースを収集するライブラリです。
3
+ Puppeteer の `Page` を受け取り、単一ページのメタデータ・リンク・画像・ネットワークリソースを収集するインプロセス型スクレイパー。結果は `ScrapeResult` として戻り値で返却される(イベント経由ではない)。ブラウザ管理は呼び出し側の責任。
4
4
 
5
- **名前の由来**: **[Beholder](https://forgottenrealms.fandom.com/wiki/Beholder)** という名前は、多くの目を持つ神話上の生物に由来しています。この生物のように、このツールはWebページを正確かつ徹底的にスクレイピングして記録し、詳細を見逃しません。
5
+ ## Installation
6
6
 
7
- ## インストール
8
-
9
- ```bash
7
+ ```sh
10
8
  yarn add @d-zero/beholder
11
9
  ```
12
10
 
13
- ## 概要
14
-
15
- `@d-zero/beholder` は、Puppeteer の `Page` オブジェクトを受け取り、単一ページのスクレイピングを行うインプロセス型のスクレイパーです。
16
-
17
- **主な特徴:**
18
-
19
- - 結果は `ScrapeResult` として戻り値で返却(イベント経由ではない)
20
- - ストリーミングイベント(`changePhase`, `resourceResponse`)で進捗を監視可能
21
- - キーワード・パス除外によるページスキップ
22
- - 複数デバイスプリセット対応のレスポンシブ画像キャプチャ
23
- - ブラウザ管理は呼び出し側の責任(Scraperはページ操作のみ)
24
-
25
- ## エクスポートされるAPI
11
+ ## Usage
26
12
 
27
- ### `Scraper`(デフォルトエクスポート)
28
-
29
- ページレベルのスクレイパークラスです。`TypedAwaitEventEmitter` を継承しています。
30
-
31
- ```typescript
32
- import Scraper from '@d-zero/beholder';
33
- ```
34
-
35
- #### `scrapeStart(page, url, options?, isSkip?)`
36
-
37
- Puppeteer ページ上でスクレイピングを実行します。
38
-
39
- **パラメータ:**
40
-
41
- - `page` (`Page`) — Puppeteer ページインスタンス
42
- - `url` (`ExURL`) — スクレイピング対象のURL
43
- - `options` (`Partial<ScraperOptions>`, 省略可) — スクレイピングオプション
44
- - `isSkip` (`boolean`, 省略可) — `true` でネットワークリクエストなしにスキップ
45
-
46
- **戻り値:** `Promise<ScrapeResult>`
47
-
48
- - `type: "success"` — `pageData` にスクレイピング結果を格納
49
- - `type: "skipped"` — `ignored` にスキップ理由を格納
50
- - `type: "error"` — `error` にエラー詳細を格納
51
- - `failedRequests` — ネットワーク切断等で失敗したサブリソースリクエストの一覧(存在する場合のみ)
52
-
53
- **使用例:**
54
-
55
- ```typescript
13
+ ```ts
56
14
  import Scraper from '@d-zero/beholder';
57
15
  import { parseUrl } from '@d-zero/shared/parse-url';
58
16
  import { launch } from 'puppeteer';
@@ -61,241 +19,16 @@ const browser = await launch();
61
19
  const page = await browser.newPage();
62
20
 
63
21
  const scraper = new Scraper();
22
+ scraper.on('changePhase', (event) => console.log(event.message));
64
23
 
65
- // 進捗イベントを監視
66
- scraper.on('changePhase', async (event) => {
67
- console.log(`[${event.pid}] ${event.name}: ${event.message}`);
68
- });
69
-
70
- // スクレイピングを実行
71
- const url = parseUrl('https://example.com');
72
- const result = await scraper.scrapeStart(page, url, {
24
+ const result = await scraper.scrapeStart(page, parseUrl('https://example.com'), {
73
25
  captureImages: true,
74
- excludeKeywords: ['広告'],
75
26
  isExternal: false,
76
27
  });
77
28
 
78
29
  if (result.type === 'success') {
79
- console.log('タイトル:', result.pageData?.meta.title);
80
- console.log('リンク数:', result.pageData?.anchorList.length);
81
- console.log('画像数:', result.pageData?.imageList.length);
82
- console.log('サブリソース数:', result.resources.length);
30
+ console.log(result.pageData?.meta.title);
83
31
  }
84
-
85
- // クリーンアップはブラウザレベルで行う
86
- await page.close();
87
- await browser.close();
88
- ```
89
-
90
- #### イベント
91
-
92
- | イベント名 | 説明 |
93
- | ------------------ | ---------------------------------------------- |
94
- | `changePhase` | スクレイピングフェーズが遷移した場合 |
95
- | `resourceResponse` | サブリソースのレスポンスがキャプチャされた場合 |
96
-
97
- ### `ScraperOptions`
98
-
99
- | プロパティ | 型 | デフォルト | 説明 |
100
- | ------------------- | ---------- | ---------- | ---------------------------------------------------------------- |
101
- | `isExternal` | `boolean` | `false` | 外部URLかどうか |
102
- | `captureImages` | `boolean` | `true` | 画像データを取得するかどうか |
103
- | `excludeKeywords` | `string[]` | `[]` | HTML内にマッチしたらスキップするキーワード |
104
- | `metadataOnly` | `boolean` | `false` | メタデータのみ取得(ブラウザスクレイピングなし) |
105
- | `imageLoadTimeout` | `number` | `5000` | 画像読み込み待機のタイムアウト(ms) |
106
- | `disableQueries` | `boolean` | - | URLパース時にクエリパラメータを除去するかどうか |
107
- | `retries` | `number` | - | ネットワーク操作のリトライ回数 |
108
- | `headCheckResult` | `PageData` | - | 事前取得したHEADチェック結果(省略時はHEADリクエストをスキップ) |
109
- | `navigationTimeout` | `number` | `60000` | `page.goto()` のタイムアウト(ms) |
110
-
111
- ### ユーティリティ関数
112
-
113
- #### `isError(status)`
114
-
115
- HTTPステータスコードがエラーかどうかを判定します。200-399 は成功、それ以外はエラーです。
116
-
117
- ```typescript
118
- import { isError } from '@d-zero/beholder';
119
-
120
- isError(200); // false
121
- isError(404); // true
122
- ```
123
-
124
- #### `detectCompress(headers)` / `detectCDN(headers)`
125
-
126
- レスポンスヘッダーから圧縮方式・CDNプロバイダを検出します(`@d-zero/shared` からの再エクスポート)。
127
-
128
- ### 型定義
129
-
130
- #### `ScrapeResult`
131
-
132
- スクレイピング操作の結果を表します。
133
-
134
- ```typescript
135
- type ScrapeResult = {
136
- type: 'success' | 'skipped' | 'error';
137
- pageData?: PageData;
138
- resources: ResourceEntry[];
139
- ignored?: { url: ExURL; matchedText: string; excludeKeywords: string[] };
140
- error?: { name: string; message: string; stack?: string; shutdown: boolean };
141
- failedRequests?: ReadonlyArray<{ url: string; errorText: string }>;
142
- };
143
32
  ```
144
33
 
145
- #### `PageData`
146
-
147
- スクレイピング成功時のページデータです。
148
-
149
- ```typescript
150
- type PageData = {
151
- url: ExURL;
152
- redirectPaths: string[];
153
- isTarget: boolean;
154
- isExternal: boolean;
155
- status: number;
156
- statusText: string;
157
- contentType: string | null;
158
- contentLength: number | null;
159
- responseHeaders: Record<string, string | string[] | undefined> | null;
160
- meta: Meta;
161
- anchorList: AnchorData[];
162
- imageList: ImageElement[];
163
- html: string;
164
- isSkipped: false;
165
- };
166
- ```
167
-
168
- #### `Meta`
169
-
170
- ページの `<head>` から抽出されたメタデータです。
171
-
172
- ```typescript
173
- type Meta = {
174
- lang?: string;
175
- title: string;
176
- description?: string;
177
- keywords?: string;
178
- noindex?: boolean;
179
- nofollow?: boolean;
180
- noarchive?: boolean;
181
- canonical?: string;
182
- alternate?: string;
183
- 'og:type'?: string;
184
- 'og:title'?: string;
185
- 'og:site_name'?: string;
186
- 'og:description'?: string;
187
- 'og:url'?: string;
188
- 'og:image'?: string;
189
- 'twitter:card'?: string;
190
- };
191
- ```
192
-
193
- #### `AnchorData`
194
-
195
- アンカー要素(`<a>` / `<area>`)のデータです。
196
-
197
- ```typescript
198
- type AnchorData = {
199
- href: ExURL;
200
- textContent: string;
201
- isExternal?: boolean;
202
- };
203
- ```
204
-
205
- #### `ImageElement`
206
-
207
- 画像要素のデータです。デバイスプリセットごとにキャプチャされます。
208
-
209
- ```typescript
210
- type ImageElement = {
211
- src: string;
212
- currentSrc: string;
213
- alt: string;
214
- width: number;
215
- height: number;
216
- naturalWidth: number;
217
- naturalHeight: number;
218
- isLazy: boolean;
219
- viewportWidth: number;
220
- sourceCode: string;
221
- };
222
- ```
223
-
224
- #### `ResourceEntry`
225
-
226
- ページ読み込み中にキャプチャされたサブリソースです。
227
-
228
- ```typescript
229
- type ResourceEntry = {
230
- log: NetworkLog;
231
- resource: Omit<Resource, 'uid'>;
232
- pageUrl: string;
233
- };
234
- ```
235
-
236
- #### `NetworkLog`
237
-
238
- ネットワークリクエスト/レスポンスのログエントリです。
239
-
240
- ```typescript
241
- type NetworkLog = {
242
- url: ExURL;
243
- status: number | null;
244
- contentLength: number;
245
- contentType: string;
246
- isError: boolean;
247
- request: { ts: number; headers: Record<string, string>; method: string };
248
- response?: {
249
- ts: number;
250
- status: number;
251
- statusText: string;
252
- fromCache: boolean;
253
- headers: Record<string, string>;
254
- };
255
- };
256
- ```
257
-
258
- #### `Resource`
259
-
260
- ネットワークリソースのメタデータです。
261
-
262
- ```typescript
263
- type Resource = {
264
- url: ExURL;
265
- isExternal: boolean;
266
- isError: boolean;
267
- status: number | null;
268
- statusText: string | null;
269
- contentType: string | null;
270
- contentLength: number | null;
271
- compress: false | CompressType;
272
- cdn: false | CDNType;
273
- headers: Record<string, string | string[] | undefined> | null;
274
- };
275
- ```
276
-
277
- #### `ChangePhaseEvent`
278
-
279
- スクレイピングライフサイクルのフェーズ遷移イベントです。
280
-
281
- 主なフェーズ: `scrapeStart` → `openPage` → `loadDOMContent` → `waitNetworkIdle` → `getHTML` → `getAnchors` → `getMeta` → `extractImages` → `getImages` → `scrapeEnd`
282
-
283
- その他のフェーズ: `launchBrowser`, `headRequest`, `headRequestTimeout`, `newPage`, `setViewport`, `scrollToBottom`, `waitImageLoad`, `pageSkipped`, `retryWait`, `retryExhausted`, `beforeCleanup`, `cleanedUp`
284
-
285
- #### `SkippedPageData`
286
-
287
- キーワードまたはパス除外によりスキップされたページのデータです。
288
-
289
- ```typescript
290
- type SkippedPageData = {
291
- isSkipped: true;
292
- url: ExURL;
293
- matched:
294
- | { type: 'keyword'; text: string; excludeKeywords: string[] }
295
- | { type: 'path'; excludes: string[] };
296
- };
297
- ```
298
-
299
- ## ライセンス
300
-
301
- MIT
34
+ 設計判断(イベントではなく戻り値で返す理由、`page` のライフサイクル責務、リトライ機構など)は `src/scraper.ts` の JSDoc を参照。
@@ -3,10 +3,24 @@
3
3
  *
4
4
  * These functions are called by {@link ./scraper.ts | Scraper.#fetchData} to extract
5
5
  * anchors, images, and meta information after page navigation completes.
6
+ *
7
+ * WHY timeouts everywhere: A page whose main thread is blocked (heavy JS, autoplay
8
+ * video players, infinite loops) makes every CDP round-trip hang. `getMeta` and
9
+ * `getImageList` therefore collect all data in a single `page.evaluate` and wrap it
10
+ * in {@link raceWithTimeout} so a blocked thread is abandoned after a bounded budget
11
+ * instead of accumulating per-property timeouts up to the caller's global timeout.
12
+ * Note that `page.evaluate` itself runs on the page's main thread and has no built-in
13
+ * timeout, so the surrounding race is what actually bounds the hang.
6
14
  * @see {@link ./types.ts} for the data types returned by these functions
7
15
  */
8
- import type { AnchorData, ImageElement, ParseURLOptions } from './types.js';
16
+ import type { AnchorData, ImageElement, Meta, ParseURLOptions } from './types.js';
9
17
  import type { ElementHandle, Page } from 'puppeteer';
18
+ /**
19
+ * Default timeout (ms) applied to DOM evaluation operations when the caller does not
20
+ * specify one. Bounds how long a single `page.evaluate` / property read may hang on a
21
+ * page whose main thread is unresponsive.
22
+ */
23
+ export declare const DEFAULT_DOM_EVALUATION_TIMEOUT = 30000;
10
24
  /**
11
25
  * Parameters for {@link getProp}.
12
26
  * @template T - The expected type of the property value.
@@ -22,61 +36,53 @@ export interface GetPropParams<T> {
22
36
  /**
23
37
  * Retrieves a DOM property value from a Puppeteer element handle with a timeout.
24
38
  *
25
- * Races the actual property retrieval against a 10-second timeout.
39
+ * Races the actual property retrieval against a timeout via {@link raceWithTimeout},
40
+ * which clears the loser-side timer so it cannot keep the event loop alive.
26
41
  * If the property cannot be read or the timeout expires, the fallback value is returned.
27
42
  * @template T - The expected type of the property value.
28
43
  * @param params - Parameters containing the element, property name, and fallback.
29
- * @returns The property value, or the fallback if retrieval fails.
44
+ * @param timeout - Timeout in ms before falling back. Defaults to {@link DEFAULT_DOM_EVALUATION_TIMEOUT}.
45
+ * @returns The property value, or the fallback if retrieval fails or times out.
30
46
  */
31
- export declare function getProp<T>(params: GetPropParams<T>): Promise<T>;
32
- /**
33
- * Parameters for {@link getPropBySelector}.
34
- * @template T - The expected type of the property value.
35
- */
36
- export interface GetPropBySelectorParams<T> {
37
- /** The Puppeteer page to query. */
38
- readonly page: Page;
39
- /** A CSS selector to find the target element. */
40
- readonly selector: string;
41
- /** The DOM property name to read from the matched element. */
42
- readonly propName: string;
43
- /** The default value if no element matches or the property cannot be read. */
44
- readonly fallback: T;
45
- }
46
- /**
47
- * Retrieves a DOM property value from the first element matching a CSS selector.
48
- *
49
- * Combines `page.$()` with {@link getProp} for convenient single-element lookups.
50
- * @template T - The expected type of the property value.
51
- * @param params - Parameters containing the page, selector, property name, and fallback.
52
- * @returns The property value, or the fallback if the element is not found or retrieval fails.
53
- */
54
- export declare function getPropBySelector<T>(params: GetPropBySelectorParams<T>): Promise<T>;
47
+ export declare function getProp<T>(params: GetPropParams<T>, timeout?: number): Promise<T>;
55
48
  /**
56
49
  * Extracts all `<img>` elements from the page and returns their properties.
57
50
  *
58
- * For each image, collects the `src`, `currentSrc`, `alt`, bounding box dimensions,
59
- * natural dimensions, lazy-loading status, and the outer HTML source code.
51
+ * Collects every image's `src`, `currentSrc`, `alt`, layout dimensions,
52
+ * natural dimensions, lazy-loading status, and outer HTML in a single
53
+ * `page.evaluate` call, wrapped in {@link raceWithTimeout}. On timeout (an
54
+ * unresponsive page) an empty array is returned rather than hanging.
60
55
  * @param page - The Puppeteer page to extract images from.
61
56
  * @param viewportWidth - The current viewport width in pixels, recorded alongside each image entry.
57
+ * @param timeout - Timeout in ms for the evaluation. Defaults to {@link DEFAULT_DOM_EVALUATION_TIMEOUT}.
62
58
  * @returns An array of {@link ImageElement} objects describing each image on the page.
63
59
  */
64
- export declare function getImageList(page: Page, viewportWidth: number): Promise<ImageElement[]>;
60
+ export declare function getImageList(page: Page, viewportWidth: number, timeout?: number): Promise<ImageElement[]>;
65
61
  /**
66
62
  * Extracts all anchor (`<a>` and `<area>`) elements with `href` attributes from the page.
67
63
  *
68
64
  * For each anchor, resolves the `href` to an `ExURL` via `parseUrl`, retrieves
69
65
  * the accessible name (from the accessibility tree, falling back to `textContent`),
70
66
  * and filters out non-HTTP links.
67
+ *
68
+ * WHY this keeps per-element CDP calls (unlike {@link getMeta} / {@link getImageList}):
69
+ * the accessible name comes from Chrome's computed accessibility tree
70
+ * (`page.accessibility.snapshot`), which is a CDP-only feature unavailable to in-page
71
+ * DOM APIs. Each {@link getProp} read is still bounded by `timeout`.
71
72
  * @param page - The Puppeteer page to extract anchors from.
72
73
  * @param options - Optional URL parsing options (e.g., `disableQueries`).
74
+ * @param timeout - Timeout in ms per property read. Defaults to {@link DEFAULT_DOM_EVALUATION_TIMEOUT}.
73
75
  * @returns An array of {@link AnchorData} objects for all HTTP(S) links found on the page.
74
76
  */
75
- export declare function getAnchorList(page: Page, options?: ParseURLOptions): Promise<AnchorData[]>;
77
+ export declare function getAnchorList(page: Page, options?: ParseURLOptions, timeout?: number): Promise<AnchorData[]>;
76
78
  /**
77
79
  * Extracts comprehensive meta information from the page's `<head>`.
78
80
  *
79
- * Collects the following metadata:
81
+ * Collects all metadata in a single `page.evaluate` call (14 CDP round-trips
82
+ * collapsed into 1) wrapped in {@link raceWithTimeout}. On timeout (an unresponsive
83
+ * page) a minimal `{ title: '' }` is returned rather than hanging.
84
+ *
85
+ * Collected metadata:
80
86
  * - `title` - The document title.
81
87
  * - `lang` - The `lang` attribute of the `<html>` element.
82
88
  * - `description` - The `<meta name="description">` content.
@@ -87,23 +93,7 @@ export declare function getAnchorList(page: Page, options?: ParseURLOptions): Pr
87
93
  * - Open Graph tags: `og:type`, `og:title`, `og:site_name`, `og:description`, `og:url`, `og:image`.
88
94
  * - `twitter:card` - The Twitter Card type.
89
95
  * @param page - The Puppeteer page to extract meta information from.
96
+ * @param timeout - Timeout in ms for the evaluation. Defaults to {@link DEFAULT_DOM_EVALUATION_TIMEOUT}.
90
97
  * @returns An object containing all extracted meta properties.
91
98
  */
92
- export declare function getMeta(page: Page): Promise<{
93
- title: string;
94
- lang: string;
95
- description: string;
96
- keywords: string;
97
- noindex: boolean;
98
- nofollow: boolean;
99
- noarchive: boolean;
100
- canonical: string;
101
- alternate: string;
102
- 'og:type': string;
103
- 'og:title': string;
104
- 'og:site_name': string;
105
- 'og:description': string;
106
- 'og:url': string;
107
- 'og:image': string;
108
- 'twitter:card': string;
109
- }>;
99
+ export declare function getMeta(page: Page, timeout?: number): Promise<Meta>;