@nitpicker/crawler 0.18.1 → 0.20.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/lib/append-retry-failed-common-setup-phases.d.ts +15 -0
- package/lib/append-retry-failed-common-setup-phases.js +21 -0
- package/lib/append-setup-phases.d.ts +17 -0
- package/lib/append-setup-phases.js +22 -0
- package/lib/archive/archive-accessor.d.ts +31 -5
- package/lib/archive/archive-accessor.js +46 -24
- package/lib/archive/archive.d.ts +115 -37
- package/lib/archive/archive.js +118 -52
- package/lib/archive/cache/extract-archive-to-cache.d.ts +10 -1
- package/lib/archive/cache/extract-archive-to-cache.js +14 -4
- package/lib/archive/create-adjunct-tables.d.ts +12 -2
- package/lib/archive/create-adjunct-tables.js +91 -29
- package/lib/archive/create-entity-tables.d.ts +5 -1
- package/lib/archive/create-entity-tables.js +6 -1
- package/lib/archive/database.d.ts +39 -9
- package/lib/archive/database.js +63 -18
- package/lib/archive/db-ops/lifecycle/init.d.ts +14 -2
- package/lib/archive/db-ops/lifecycle/init.js +29 -11
- package/lib/archive/db-ops/meta/get-custom-elements-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-custom-elements-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-page-technologies-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-page-technologies-of-page.js +15 -0
- package/lib/archive/db-ops/meta/get-technology-signals-of-page.d.ts +11 -0
- package/lib/archive/db-ops/meta/get-technology-signals-of-page.js +15 -0
- package/lib/archive/db-ops/pages/order/set-url-order.d.ts +6 -1
- package/lib/archive/db-ops/pages/order/set-url-order.js +7 -1
- package/lib/archive/db-ops/pages/read/build-page-query.js +1 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.d.ts +6 -1
- package/lib/archive/db-ops/pages/reset/repromote-external-pages.js +12 -3
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.d.ts +6 -1
- package/lib/archive/db-ops/pages/reset/reset-failed-pages.js +10 -2
- package/lib/archive/db-ops/pages/write/insert-custom-elements.d.ts +27 -0
- package/lib/archive/db-ops/pages/write/insert-custom-elements.js +40 -0
- package/lib/archive/db-ops/pages/write/insert-page.d.ts +4 -4
- package/lib/archive/db-ops/pages/write/insert-page.js +3 -2
- package/lib/archive/db-ops/pages/write/insert-technologies.d.ts +25 -0
- package/lib/archive/db-ops/pages/write/insert-technologies.js +45 -0
- package/lib/archive/db-ops/pages/write/update-page.d.ts +4 -1
- package/lib/archive/db-ops/pages/write/update-page.js +38 -16
- package/lib/archive/db-ops/resources/get-resource-url-list.d.ts +12 -1
- package/lib/archive/db-ops/resources/get-resource-url-list.js +43 -5
- package/lib/archive/filesystem/copy-file-with-progress.d.ts +20 -0
- package/lib/archive/filesystem/copy-file-with-progress.js +34 -0
- package/lib/archive/filesystem/parse-pax-path.d.ts +18 -0
- package/lib/archive/filesystem/parse-pax-path.js +47 -0
- package/lib/archive/filesystem/parse-tar-size-field.d.ts +13 -0
- package/lib/archive/filesystem/parse-tar-size-field.js +40 -0
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +10 -3
- package/lib/archive/filesystem/peek-tar-top-dir.js +166 -16
- package/lib/archive/filesystem/tar.d.ts +13 -1
- package/lib/archive/filesystem/tar.js +63 -7
- package/lib/archive/filesystem/untar.d.ts +13 -0
- package/lib/archive/filesystem/untar.js +68 -8
- package/lib/archive/init-schema.d.ts +1 -1
- package/lib/archive/init-schema.js +6 -4
- package/lib/archive/meta/compute-main-contents-denormalized.d.ts +13 -2
- package/lib/archive/meta/compute-main-contents-denormalized.js +15 -2
- package/lib/archive/meta/technologies/combine-technology-confidence.d.ts +46 -0
- package/lib/archive/meta/technologies/combine-technology-confidence.js +93 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.d.ts +45 -0
- package/lib/archive/meta/technologies/convert-legacy-page-tags-to-inserts.js +53 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.d.ts +33 -0
- package/lib/archive/meta/technologies/convert-tag-row-to-wappalyzer-signal.js +46 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.d.ts +30 -0
- package/lib/archive/meta/technologies/extract-technologies-for-archive.js +27 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.d.ts +13 -0
- package/lib/archive/meta/technologies/match-structural-technology-signals.js +44 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.d.ts +25 -0
- package/lib/archive/meta/technologies/normalize-wappalyzer-entries.js +35 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.d.ts +43 -0
- package/lib/archive/meta/technologies/technology-signal-definitions.js +193 -0
- package/lib/archive/meta/technologies/types.d.ts +68 -0
- package/lib/archive/meta/technologies/types.js +13 -0
- package/lib/archive/meta/types.d.ts +97 -30
- package/lib/archive/migrate-content-items-alias-of-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-alias-of-id.js +14 -3
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.d.ts +6 -1
- package/lib/archive/migrate-content-items-dedupe-cap-event-id.js +14 -3
- package/lib/archive/migrate-info-main-content-selector.d.ts +6 -1
- package/lib/archive/migrate-info-main-content-selector.js +14 -3
- package/lib/archive/migrate-info-roots.d.ts +6 -1
- package/lib/archive/migrate-info-roots.js +14 -3
- package/lib/archive/migrate-inventory-runs-exclude-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-exclude-skipped.js +14 -3
- package/lib/archive/migrate-inventory-runs-invalid-skipped.d.ts +6 -1
- package/lib/archive/migrate-inventory-runs-invalid-skipped.js +14 -3
- package/lib/archive/migrate-main-contents-columns.d.ts +6 -1
- package/lib/archive/migrate-main-contents-columns.js +14 -3
- package/lib/archive/migrate-page-meta-body-hash.d.ts +6 -1
- package/lib/archive/migrate-page-meta-body-hash.js +14 -3
- package/lib/archive/migrate-page-meta-console-error-count.d.ts +6 -1
- package/lib/archive/migrate-page-meta-console-error-count.js +14 -3
- package/lib/archive/migrate-page-meta-custom-element-count.d.ts +33 -0
- package/lib/archive/migrate-page-meta-custom-element-count.js +51 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.d.ts +39 -0
- package/lib/archive/migrate-page-tags-to-page-technologies.js +79 -0
- package/lib/archive/page.d.ts +37 -13
- package/lib/archive/page.js +45 -17
- package/lib/archive/retarget-legacy-fk-tables.d.ts +27 -14
- package/lib/archive/retarget-legacy-fk-tables.js +61 -15
- package/lib/archive/types.d.ts +18 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.d.ts +24 -0
- package/lib/crawler/assert-puppeteer-shared-with-beholder.js +36 -0
- package/lib/crawler/capture-custom-elements.d.ts +33 -0
- package/lib/crawler/capture-custom-elements.js +39 -0
- package/lib/crawler/close-browser-safely.d.ts +3 -3
- package/lib/crawler/close-browser-safely.js +8 -17
- package/lib/crawler/collect-custom-elements.d.ts +33 -0
- package/lib/crawler/collect-custom-elements.js +110 -0
- package/lib/crawler/crawler.js +140 -56
- package/lib/crawler/fetch-destination.js +6 -13
- package/lib/crawler/find-package-dir.d.ts +24 -0
- package/lib/crawler/find-package-dir.js +39 -0
- package/lib/crawler/handle-scrape-end.js +19 -10
- package/lib/crawler/resolve-result-went-off-host.d.ts +34 -0
- package/lib/crawler/resolve-result-went-off-host.js +39 -0
- package/lib/crawler/scan-js-resource-for-license-comment.d.ts +32 -0
- package/lib/crawler/scan-js-resource-for-license-comment.js +140 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.d.ts +58 -0
- package/lib/crawler/scan-js-resources-for-technology-signals.js +196 -0
- package/lib/crawler/types.d.ts +34 -1
- package/lib/crawler-orchestrator.d.ts +53 -9
- package/lib/crawler-orchestrator.js +223 -46
- package/lib/crawler.d.ts +11 -1
- package/lib/crawler.js +9 -0
- package/lib/inventory-setup-phases.d.ts +32 -0
- package/lib/inventory-setup-phases.js +45 -0
- package/lib/resume-setup-phases.d.ts +16 -0
- package/lib/resume-setup-phases.js +24 -0
- package/lib/retry-failed-setup-phases.d.ts +19 -0
- package/lib/retry-failed-setup-phases.js +24 -0
- package/lib/setup-recovery-phase-labels.d.ts +21 -0
- package/lib/setup-recovery-phase-labels.js +24 -0
- package/lib/types.d.ts +145 -0
- package/package.json +11 -8
- package/lib/archive/db-ops/meta/get-tags-of-page.d.ts +0 -12
- package/lib/archive/db-ops/meta/get-tags-of-page.js +0 -28
- package/lib/archive/db-ops/pages/write/insert-tags.d.ts +0 -16
- package/lib/archive/db-ops/pages/write/insert-tags.js +0 -34
- package/lib/archive/meta/summarize-tags.d.ts +0 -16
- package/lib/archive/meta/summarize-tags.js +0 -33
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import type { Knex } from 'knex';
|
|
2
2
|
/**
|
|
3
3
|
* Rewrites the FK declarations of the adjunct tables from the legacy
|
|
4
|
-
* `pages(id)` to `content_items(id)` by rebuilding each table
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `pages(id)` to `content_items(id)` by rebuilding each table, AND converts
|
|
5
|
+
* `page_tags` (if present) into `technology_signals` / `page_technologies`.
|
|
6
|
+
* SQLite has no `ALTER TABLE … DROP CONSTRAINT`, so the only way to change
|
|
7
|
+
* an FK target is to recreate the table:
|
|
7
8
|
*
|
|
8
|
-
* 1. Stage each table's rows into a constraint-free
|
|
9
|
-
* copy (`CREATE TABLE … AS SELECT *`) and drop the
|
|
10
|
-
* its index names.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
9
|
+
* 1. Stage each {@link RETARGET_TABLES} table's rows into a constraint-free
|
|
10
|
+
* `<table>__retarget` copy (`CREATE TABLE … AS SELECT *`) and drop the
|
|
11
|
+
* original, freeing its index names. `page_tags`, if present, is read
|
|
12
|
+
* into memory here too (converted via `convertLegacyPageTagsToInserts` —
|
|
13
|
+
* the same helper `migrate-page-tags-to-page-technologies.ts` uses for
|
|
14
|
+
* already-0.13+ archives) but NOT staged the same way: it has no
|
|
15
|
+
* current-schema table to retarget onto, so there is nothing to copy
|
|
16
|
+
* back — only something to drop, once its data lives in the tables
|
|
17
|
+
* from step 2.
|
|
18
|
+
* 2. Recreate every {@link RETARGET_TABLES} table through
|
|
19
|
+
* {@link createAdjunctTables} — the same DDL fresh archives get, so
|
|
20
|
+
* migrated archives end up identical in shape and index naming.
|
|
21
|
+
* Rebuilding the DDL inline here instead would recreate the exact
|
|
22
|
+
* drift this function repairs. This same call also creates
|
|
23
|
+
* `technology_signals` / `page_technologies` (idempotent
|
|
24
|
+
* `hasTable`-guarded DDL — a no-op if a prior partial run already
|
|
25
|
+
* created them).
|
|
26
|
+
* 3. Copy the staged {@link RETARGET_TABLES} rows back using the recreated
|
|
27
|
+
* table's own column list (read via `pragma_table_info`). A column the
|
|
28
|
+
* canonical DDL has but the input archive lacks aborts the INSERT with
|
|
29
|
+
* a clear error — never a silent data drop. The converted `page_tags`
|
|
30
|
+
* signals/technologies are inserted here too, then `page_tags` itself
|
|
31
|
+
* is dropped (not recreated).
|
|
19
32
|
*
|
|
20
33
|
* A rename-based recipe (`ALTER TABLE x RENAME TO x__retarget`, skip
|
|
21
34
|
* copy #1) is deliberately NOT used: SQLite keeps the renamed table's
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { eachSplitted } from '../utils/array/each-splitted.js';
|
|
1
2
|
import { createAdjunctTables } from './create-adjunct-tables.js';
|
|
3
|
+
import { convertLegacyPageTagsToInserts } from './meta/technologies/convert-legacy-page-tags-to-inserts.js';
|
|
2
4
|
/**
|
|
3
5
|
* The adjunct tables whose FK declarations may still point at the legacy
|
|
4
6
|
* `pages(id)` on migrated archives. Only names are listed — the column
|
|
@@ -6,10 +8,15 @@ import { createAdjunctTables } from './create-adjunct-tables.js';
|
|
|
6
8
|
* {@link createAdjunctTables}, so a future column addition there cannot
|
|
7
9
|
* silently drift from a hardcoded list (a missing column in the staged
|
|
8
10
|
* copy fails the copy-back INSERT loudly instead of dropping data).
|
|
11
|
+
*
|
|
12
|
+
* `page_tags` is deliberately NOT here — it has no current-schema
|
|
13
|
+
* equivalent to retarget onto (removed in favor of `technology_signals` /
|
|
14
|
+
* `page_technologies`). {@link retargetLegacyFkTables} handles it as a
|
|
15
|
+
* special case: convert its rows, then drop it outright rather than
|
|
16
|
+
* recreating it. See that function's JSDoc.
|
|
9
17
|
*/
|
|
10
18
|
const RETARGET_TABLES = [
|
|
11
19
|
'page_html_ref',
|
|
12
|
-
'page_tags',
|
|
13
20
|
'page_jsonld',
|
|
14
21
|
'page_errors',
|
|
15
22
|
'analysis_violations',
|
|
@@ -32,21 +39,34 @@ const NULLABLE_ON_RETARGET = {
|
|
|
32
39
|
};
|
|
33
40
|
/**
|
|
34
41
|
* Rewrites the FK declarations of the adjunct tables from the legacy
|
|
35
|
-
* `pages(id)` to `content_items(id)` by rebuilding each table
|
|
36
|
-
*
|
|
37
|
-
*
|
|
42
|
+
* `pages(id)` to `content_items(id)` by rebuilding each table, AND converts
|
|
43
|
+
* `page_tags` (if present) into `technology_signals` / `page_technologies`.
|
|
44
|
+
* SQLite has no `ALTER TABLE … DROP CONSTRAINT`, so the only way to change
|
|
45
|
+
* an FK target is to recreate the table:
|
|
38
46
|
*
|
|
39
|
-
* 1. Stage each table's rows into a constraint-free
|
|
40
|
-
* copy (`CREATE TABLE … AS SELECT *`) and drop the
|
|
41
|
-
* its index names.
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* 1. Stage each {@link RETARGET_TABLES} table's rows into a constraint-free
|
|
48
|
+
* `<table>__retarget` copy (`CREATE TABLE … AS SELECT *`) and drop the
|
|
49
|
+
* original, freeing its index names. `page_tags`, if present, is read
|
|
50
|
+
* into memory here too (converted via `convertLegacyPageTagsToInserts` —
|
|
51
|
+
* the same helper `migrate-page-tags-to-page-technologies.ts` uses for
|
|
52
|
+
* already-0.13+ archives) but NOT staged the same way: it has no
|
|
53
|
+
* current-schema table to retarget onto, so there is nothing to copy
|
|
54
|
+
* back — only something to drop, once its data lives in the tables
|
|
55
|
+
* from step 2.
|
|
56
|
+
* 2. Recreate every {@link RETARGET_TABLES} table through
|
|
57
|
+
* {@link createAdjunctTables} — the same DDL fresh archives get, so
|
|
58
|
+
* migrated archives end up identical in shape and index naming.
|
|
59
|
+
* Rebuilding the DDL inline here instead would recreate the exact
|
|
60
|
+
* drift this function repairs. This same call also creates
|
|
61
|
+
* `technology_signals` / `page_technologies` (idempotent
|
|
62
|
+
* `hasTable`-guarded DDL — a no-op if a prior partial run already
|
|
63
|
+
* created them).
|
|
64
|
+
* 3. Copy the staged {@link RETARGET_TABLES} rows back using the recreated
|
|
65
|
+
* table's own column list (read via `pragma_table_info`). A column the
|
|
66
|
+
* canonical DDL has but the input archive lacks aborts the INSERT with
|
|
67
|
+
* a clear error — never a silent data drop. The converted `page_tags`
|
|
68
|
+
* signals/technologies are inserted here too, then `page_tags` itself
|
|
69
|
+
* is dropped (not recreated).
|
|
50
70
|
*
|
|
51
71
|
* A rename-based recipe (`ALTER TABLE x RENAME TO x__retarget`, skip
|
|
52
72
|
* copy #1) is deliberately NOT used: SQLite keeps the renamed table's
|
|
@@ -85,6 +105,18 @@ export async function retargetLegacyFkTables(trx) {
|
|
|
85
105
|
await trx.raw(`DROP TABLE "${table}"`);
|
|
86
106
|
staged.push(table);
|
|
87
107
|
}
|
|
108
|
+
// `page_tags` has no current-schema table to retarget onto (removed —
|
|
109
|
+
// see the module JSDoc), so its rows are converted to
|
|
110
|
+
// `technology_signals` / `page_technologies` shape HERE, while
|
|
111
|
+
// `page_tags` still exists, then only dropped (not staged/recreated)
|
|
112
|
+
// after `createAdjunctTables` below provisions the tables that will
|
|
113
|
+
// receive them.
|
|
114
|
+
const hasLegacyPageTags = await trx.schema.hasTable('page_tags');
|
|
115
|
+
const legacyTagRows = hasLegacyPageTags
|
|
116
|
+
? await trx
|
|
117
|
+
.select('pageId', 'provider', 'version', 'confidence', 'categories')
|
|
118
|
+
.from('page_tags')
|
|
119
|
+
: [];
|
|
88
120
|
await createAdjunctTables(trx);
|
|
89
121
|
for (const table of staged) {
|
|
90
122
|
const columns = await trx
|
|
@@ -104,4 +136,18 @@ export async function retargetLegacyFkTables(trx) {
|
|
|
104
136
|
await trx.raw(`INSERT INTO "${table}" (${insertColumnList}) SELECT ${selectColumnList} FROM "${table}__retarget"`);
|
|
105
137
|
await trx.raw(`DROP TABLE "${table}__retarget"`);
|
|
106
138
|
}
|
|
139
|
+
if (hasLegacyPageTags) {
|
|
140
|
+
const { signalInserts, technologyInserts } = convertLegacyPageTagsToInserts(legacyTagRows);
|
|
141
|
+
if (signalInserts.length > 0) {
|
|
142
|
+
await eachSplitted(signalInserts, 100, async (chunk) => {
|
|
143
|
+
await trx('technology_signals').insert(chunk);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (technologyInserts.length > 0) {
|
|
147
|
+
await eachSplitted(technologyInserts, 100, async (chunk) => {
|
|
148
|
+
await trx('page_technologies').insert(chunk);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
await trx.raw('DROP TABLE "page_tags"');
|
|
152
|
+
}
|
|
107
153
|
}
|
package/lib/archive/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomPathCandidate } from './populate-entity-tables/types.js';
|
|
2
|
+
import type { MainContentCustomElementCandidate } from '../crawler/types.js';
|
|
2
3
|
import type { PageData } from '../utils/types/types.js';
|
|
3
4
|
import type { ParseURLOptions } from '@d-zero/shared/parse-url';
|
|
4
5
|
/**
|
|
@@ -13,6 +14,13 @@ import type { ParseURLOptions } from '@d-zero/shared/parse-url';
|
|
|
13
14
|
export type PageDataWithDomPaths = PageData & {
|
|
14
15
|
/** In-browser dom-path capture for the rendered document's images. */
|
|
15
16
|
imageDomPaths?: readonly DomPathCandidate[];
|
|
17
|
+
/**
|
|
18
|
+
* In-browser Web Component (custom element) capture for the rendered
|
|
19
|
+
* document's main-content region (`crawler/capture-custom-elements.ts`).
|
|
20
|
+
* `undefined` means capture was not attempted or failed — a distinct
|
|
21
|
+
* state from "capture succeeded, zero elements found" (empty array).
|
|
22
|
+
*/
|
|
23
|
+
mainContentCustomElements?: readonly MainContentCustomElementCandidate[];
|
|
16
24
|
};
|
|
17
25
|
/**
|
|
18
26
|
* Event map for database-related events emitted by the Database and ArchiveAccessor classes.
|
|
@@ -388,6 +396,8 @@ export interface DB_Page {
|
|
|
388
396
|
main_content_audio_count: number | null;
|
|
389
397
|
/** Number of canvases within the main region, or null. */
|
|
390
398
|
main_content_canvas_count: number | null;
|
|
399
|
+
/** Number of Web Components (custom elements) within the main region, or null. */
|
|
400
|
+
main_content_custom_element_count: number | null;
|
|
391
401
|
/** `document.body.scrollHeight` at the desktop-compact preset, or null. */
|
|
392
402
|
scroll_height_desktop: number | null;
|
|
393
403
|
/** `document.body.scrollHeight` at the mobile-small preset, or null. */
|
|
@@ -637,4 +647,12 @@ export interface DatabaseOption {
|
|
|
637
647
|
* tmpDir, where any write would race the live crawler.
|
|
638
648
|
*/
|
|
639
649
|
readOnly?: boolean;
|
|
650
|
+
/**
|
|
651
|
+
* Called instead of `console.error` for self-healing schema migration
|
|
652
|
+
* notices (issue #294) — see
|
|
653
|
+
* {@link import('./db-ops/lifecycle/init.js').init}'s `onLog` param for
|
|
654
|
+
* why a bare `console.error` at this point is unsafe. Ignored when
|
|
655
|
+
* `readOnly` is `true` (migrations never run there).
|
|
656
|
+
*/
|
|
657
|
+
onLog?: (message: string) => void;
|
|
640
658
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies that crawler's own `puppeteer` install and the `puppeteer` used
|
|
3
|
+
* internally by `@d-zero/beholder` resolve to the same directory on disk.
|
|
4
|
+
*
|
|
5
|
+
* `Crawler#scrapePage` launches the browser through crawler's own
|
|
6
|
+
* `puppeteer` import and hands the resulting `Page` across the module
|
|
7
|
+
* boundary into `Scraper.scrapeStart()` (`@d-zero/beholder`). If crawler's
|
|
8
|
+
* pinned `puppeteer` version drifts from whatever version `@d-zero/beholder`
|
|
9
|
+
* depends on internally, yarn installs two separate `puppeteer` copies and
|
|
10
|
+
* the `Page` instances are structurally similar but not the same class —
|
|
11
|
+
* this surfaces as a `TS2345` deep inside application code with no
|
|
12
|
+
* indication that a version mismatch is the actual cause. Comparing the
|
|
13
|
+
* resolved directories catches the drift directly, at the boundary where it
|
|
14
|
+
* actually matters.
|
|
15
|
+
* @throws {Error} When crawler's and beholder's `puppeteer` resolve to
|
|
16
|
+
* different directories, naming both.
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { assertPuppeteerSharedWithBeholder } from '@nitpicker/crawler';
|
|
20
|
+
*
|
|
21
|
+
* assertPuppeteerSharedWithBeholder();
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function assertPuppeteerSharedWithBeholder(): void;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { findPackageDir } from './find-package-dir.js';
|
|
4
|
+
/**
|
|
5
|
+
* Verifies that crawler's own `puppeteer` install and the `puppeteer` used
|
|
6
|
+
* internally by `@d-zero/beholder` resolve to the same directory on disk.
|
|
7
|
+
*
|
|
8
|
+
* `Crawler#scrapePage` launches the browser through crawler's own
|
|
9
|
+
* `puppeteer` import and hands the resulting `Page` across the module
|
|
10
|
+
* boundary into `Scraper.scrapeStart()` (`@d-zero/beholder`). If crawler's
|
|
11
|
+
* pinned `puppeteer` version drifts from whatever version `@d-zero/beholder`
|
|
12
|
+
* depends on internally, yarn installs two separate `puppeteer` copies and
|
|
13
|
+
* the `Page` instances are structurally similar but not the same class —
|
|
14
|
+
* this surfaces as a `TS2345` deep inside application code with no
|
|
15
|
+
* indication that a version mismatch is the actual cause. Comparing the
|
|
16
|
+
* resolved directories catches the drift directly, at the boundary where it
|
|
17
|
+
* actually matters.
|
|
18
|
+
* @throws {Error} When crawler's and beholder's `puppeteer` resolve to
|
|
19
|
+
* different directories, naming both.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { assertPuppeteerSharedWithBeholder } from '@nitpicker/crawler';
|
|
23
|
+
*
|
|
24
|
+
* assertPuppeteerSharedWithBeholder();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function assertPuppeteerSharedWithBeholder() {
|
|
28
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const crawlerPuppeteerDir = findPackageDir(here, 'puppeteer');
|
|
30
|
+
const beholderDir = findPackageDir(here, '@d-zero/beholder');
|
|
31
|
+
const beholderPuppeteerDir = findPackageDir(beholderDir, 'puppeteer');
|
|
32
|
+
if (crawlerPuppeteerDir !== beholderPuppeteerDir) {
|
|
33
|
+
throw new Error(`crawler's puppeteer (${crawlerPuppeteerDir}) and @d-zero/beholder's puppeteer (${beholderPuppeteerDir}) resolve to different installs. ` +
|
|
34
|
+
`Pin crawler's own "puppeteer" version in package.json to match the version @d-zero/beholder depends on internally.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MainContentCustomElementCandidate } from './types.js';
|
|
2
|
+
import type { Page as PuppeteerPage } from 'puppeteer';
|
|
3
|
+
/**
|
|
4
|
+
* Captures every Web Component (custom element) inside a page's
|
|
5
|
+
* main-content region, in document order.
|
|
6
|
+
*
|
|
7
|
+
* `@d-zero/beholder`'s `MainContentsData` has no `customElements` category
|
|
8
|
+
* (confirmed by reading its `types.ts` / `get-main-contents.ts`), so the
|
|
9
|
+
* crawler runs this one extra `page.evaluate` after `scrapeStart` returns,
|
|
10
|
+
* while the page is still alive — the exact same pattern as
|
|
11
|
+
* {@link ./capture-image-dom-paths.ts}, which fills a different gap in
|
|
12
|
+
* beholder's image metadata for the same reason (beholder does not own
|
|
13
|
+
* this detection and does not need to; nitpicker already owns the live
|
|
14
|
+
* `page` object at this point in the scrape).
|
|
15
|
+
*
|
|
16
|
+
* Returns `undefined` on any evaluation failure (page context died,
|
|
17
|
+
* navigation raced the call) — custom-element capture is best-effort
|
|
18
|
+
* enrichment and a capture failure must not fail the scrape. Callers
|
|
19
|
+
* treat `undefined` as "unknown" (a distinct state from "zero elements
|
|
20
|
+
* found"), matching
|
|
21
|
+
* {@link ../archive/meta/compute-main-contents-denormalized.ts}'s
|
|
22
|
+
* three-value handling of `main_content_custom_element_count`.
|
|
23
|
+
* @param page - The live puppeteer page, after the scrape completed and
|
|
24
|
+
* before the browser closes.
|
|
25
|
+
* @param mainContentSelector - Optional selector override, forwarded
|
|
26
|
+
* verbatim to {@link ./collect-custom-elements.ts}.
|
|
27
|
+
* @returns Candidates in document order, or `undefined` when the page
|
|
28
|
+
* could not be evaluated.
|
|
29
|
+
* @example
|
|
30
|
+
* const result = await scraper.scrapeStart(page, url, options);
|
|
31
|
+
* const customElements = await captureCustomElements(page, options.mainContentSelector);
|
|
32
|
+
*/
|
|
33
|
+
export declare function captureCustomElements(page: PuppeteerPage, mainContentSelector?: string | null): Promise<MainContentCustomElementCandidate[] | undefined>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { collectCustomElements } from './collect-custom-elements.js';
|
|
2
|
+
/**
|
|
3
|
+
* Captures every Web Component (custom element) inside a page's
|
|
4
|
+
* main-content region, in document order.
|
|
5
|
+
*
|
|
6
|
+
* `@d-zero/beholder`'s `MainContentsData` has no `customElements` category
|
|
7
|
+
* (confirmed by reading its `types.ts` / `get-main-contents.ts`), so the
|
|
8
|
+
* crawler runs this one extra `page.evaluate` after `scrapeStart` returns,
|
|
9
|
+
* while the page is still alive — the exact same pattern as
|
|
10
|
+
* {@link ./capture-image-dom-paths.ts}, which fills a different gap in
|
|
11
|
+
* beholder's image metadata for the same reason (beholder does not own
|
|
12
|
+
* this detection and does not need to; nitpicker already owns the live
|
|
13
|
+
* `page` object at this point in the scrape).
|
|
14
|
+
*
|
|
15
|
+
* Returns `undefined` on any evaluation failure (page context died,
|
|
16
|
+
* navigation raced the call) — custom-element capture is best-effort
|
|
17
|
+
* enrichment and a capture failure must not fail the scrape. Callers
|
|
18
|
+
* treat `undefined` as "unknown" (a distinct state from "zero elements
|
|
19
|
+
* found"), matching
|
|
20
|
+
* {@link ../archive/meta/compute-main-contents-denormalized.ts}'s
|
|
21
|
+
* three-value handling of `main_content_custom_element_count`.
|
|
22
|
+
* @param page - The live puppeteer page, after the scrape completed and
|
|
23
|
+
* before the browser closes.
|
|
24
|
+
* @param mainContentSelector - Optional selector override, forwarded
|
|
25
|
+
* verbatim to {@link ./collect-custom-elements.ts}.
|
|
26
|
+
* @returns Candidates in document order, or `undefined` when the page
|
|
27
|
+
* could not be evaluated.
|
|
28
|
+
* @example
|
|
29
|
+
* const result = await scraper.scrapeStart(page, url, options);
|
|
30
|
+
* const customElements = await captureCustomElements(page, options.mainContentSelector);
|
|
31
|
+
*/
|
|
32
|
+
export async function captureCustomElements(page, mainContentSelector) {
|
|
33
|
+
try {
|
|
34
|
+
return await page.evaluate(collectCustomElements, mainContentSelector ?? null);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -44,9 +44,9 @@ export interface CloseBrowserSafelyDeps {
|
|
|
44
44
|
* expiry guarantees the worker always completes and no orphan subprocesses are
|
|
45
45
|
* left behind.
|
|
46
46
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
47
|
+
* Timer cleanup is delegated to `raceWithTimeout` (`@d-zero/shared`), which
|
|
48
|
+
* clears the losing timer internally so it never keeps the event loop alive
|
|
49
|
+
* after the race settles.
|
|
50
50
|
*
|
|
51
51
|
* The tree-kill happens via {@link killProcessTree}, which enumerates
|
|
52
52
|
* descendants through `ps` (POSIX) or delegates to `taskkill /T /F` (Windows).
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { raceWithTimeout } from '@d-zero/shared/race-with-timeout';
|
|
1
2
|
import { crawlerLog } from '../debug.js';
|
|
2
3
|
import { killProcessTree } from './kill-process-tree.js';
|
|
3
4
|
/**
|
|
@@ -18,9 +19,9 @@ const DEFAULT_CLOSE_TIMEOUT_MS = 30 * 1000;
|
|
|
18
19
|
* expiry guarantees the worker always completes and no orphan subprocesses are
|
|
19
20
|
* left behind.
|
|
20
21
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Timer cleanup is delegated to `raceWithTimeout` (`@d-zero/shared`), which
|
|
23
|
+
* clears the losing timer internally so it never keeps the event loop alive
|
|
24
|
+
* after the race settles.
|
|
24
25
|
*
|
|
25
26
|
* The tree-kill happens via {@link killProcessTree}, which enumerates
|
|
26
27
|
* descendants through `ps` (POSIX) or delegates to `taskkill /T /F` (Windows).
|
|
@@ -40,20 +41,10 @@ export async function closeBrowserSafely(browser, timeoutMs = DEFAULT_CLOSE_TIME
|
|
|
40
41
|
// releases its internal reference and process() returns null, so we would
|
|
41
42
|
// have no handle to tree-kill on timeout.
|
|
42
43
|
const childProcess = browser.process();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.then(() => false)
|
|
48
|
-
.catch(() => false),
|
|
49
|
-
new Promise((resolve) => {
|
|
50
|
-
timeoutHandle = setTimeout(() => resolve(true), timeoutMs);
|
|
51
|
-
}),
|
|
52
|
-
]).finally(() => {
|
|
53
|
-
if (timeoutHandle) {
|
|
54
|
-
clearTimeout(timeoutHandle);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
44
|
+
const { timeout: timedOut } = await raceWithTimeout(() => browser
|
|
45
|
+
.close()
|
|
46
|
+
.then(() => { })
|
|
47
|
+
.catch(() => { }), timeoutMs);
|
|
57
48
|
if (timedOut && childProcess && !childProcess.killed) {
|
|
58
49
|
// Mark the Node ChildProcess as killed so Node's reaping logic treats
|
|
59
50
|
// it correctly; then walk the OS process tree.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MainContentCustomElementCandidate } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Collects every Web Component (custom element) inside a page's main-content
|
|
4
|
+
* region — its `nodeName`, `id`, and `classList` — in document order.
|
|
5
|
+
*
|
|
6
|
+
* **Self-contained by contract.** Passed verbatim to Puppeteer's
|
|
7
|
+
* `page.evaluate`, which serialises the function source and executes it
|
|
8
|
+
* inside the browser: it must not reference imports, module-scope bindings,
|
|
9
|
+
* or any closure state. This is why the main-content selector priority
|
|
10
|
+
* lists below are inlined rather than imported from
|
|
11
|
+
* `@d-zero/beholder`'s `main-content-selectors.ts` — the same
|
|
12
|
+
* closure-free constraint that forces beholder's own
|
|
13
|
+
* `extractMainContentsFromDocument` to inline them. The two copies (this
|
|
14
|
+
* file's and beholder's) are kept in sync by
|
|
15
|
+
* {@link ./collect-custom-elements.spec.ts}, which asserts the resolved
|
|
16
|
+
* main element matches beholder's own resolution for the same fixture.
|
|
17
|
+
*
|
|
18
|
+
* A "custom element" here means: the tag name (case-insensitively)
|
|
19
|
+
* contains a hyphen, and is not one of the eight hyphenated SVG/MathML
|
|
20
|
+
* element names the Custom Elements spec reserves as non-registrable
|
|
21
|
+
* (`annotation-xml`, `color-profile`, `font-face`, `font-face-src`,
|
|
22
|
+
* `font-face-uri`, `font-face-format`, `font-face-name`, `missing-glyph`).
|
|
23
|
+
* @param mainContentSelector - Optional selector prepended to the default
|
|
24
|
+
* list, mirroring beholder's `getMainContents` option of the same name.
|
|
25
|
+
* @param doc - The document to walk. Defaults to the global `document`,
|
|
26
|
+
* which is how the in-browser `page.evaluate(collectCustomElements, sel)`
|
|
27
|
+
* call resolves it; Node callers (specs) pass a jsdom document.
|
|
28
|
+
* @returns Candidates in document order, or an empty array when no
|
|
29
|
+
* main-content region can be resolved.
|
|
30
|
+
* @example
|
|
31
|
+
* const candidates = await page.evaluate(collectCustomElements, mainContentSelector);
|
|
32
|
+
*/
|
|
33
|
+
export declare function collectCustomElements(mainContentSelector?: string | null, doc?: Document): MainContentCustomElementCandidate[];
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects every Web Component (custom element) inside a page's main-content
|
|
3
|
+
* region — its `nodeName`, `id`, and `classList` — in document order.
|
|
4
|
+
*
|
|
5
|
+
* **Self-contained by contract.** Passed verbatim to Puppeteer's
|
|
6
|
+
* `page.evaluate`, which serialises the function source and executes it
|
|
7
|
+
* inside the browser: it must not reference imports, module-scope bindings,
|
|
8
|
+
* or any closure state. This is why the main-content selector priority
|
|
9
|
+
* lists below are inlined rather than imported from
|
|
10
|
+
* `@d-zero/beholder`'s `main-content-selectors.ts` — the same
|
|
11
|
+
* closure-free constraint that forces beholder's own
|
|
12
|
+
* `extractMainContentsFromDocument` to inline them. The two copies (this
|
|
13
|
+
* file's and beholder's) are kept in sync by
|
|
14
|
+
* {@link ./collect-custom-elements.spec.ts}, which asserts the resolved
|
|
15
|
+
* main element matches beholder's own resolution for the same fixture.
|
|
16
|
+
*
|
|
17
|
+
* A "custom element" here means: the tag name (case-insensitively)
|
|
18
|
+
* contains a hyphen, and is not one of the eight hyphenated SVG/MathML
|
|
19
|
+
* element names the Custom Elements spec reserves as non-registrable
|
|
20
|
+
* (`annotation-xml`, `color-profile`, `font-face`, `font-face-src`,
|
|
21
|
+
* `font-face-uri`, `font-face-format`, `font-face-name`, `missing-glyph`).
|
|
22
|
+
* @param mainContentSelector - Optional selector prepended to the default
|
|
23
|
+
* list, mirroring beholder's `getMainContents` option of the same name.
|
|
24
|
+
* @param doc - The document to walk. Defaults to the global `document`,
|
|
25
|
+
* which is how the in-browser `page.evaluate(collectCustomElements, sel)`
|
|
26
|
+
* call resolves it; Node callers (specs) pass a jsdom document.
|
|
27
|
+
* @returns Candidates in document order, or an empty array when no
|
|
28
|
+
* main-content region can be resolved.
|
|
29
|
+
* @example
|
|
30
|
+
* const candidates = await page.evaluate(collectCustomElements, mainContentSelector);
|
|
31
|
+
*/
|
|
32
|
+
export function collectCustomElements(mainContentSelector = null, doc = document) {
|
|
33
|
+
// Kept in sync with beholder's MAIN_CONTENT_SELECTORS
|
|
34
|
+
// (main-content-selectors.ts) — see the module JSDoc. Inlined rather
|
|
35
|
+
// than imported because this function must stay closure-free.
|
|
36
|
+
const selectors = [
|
|
37
|
+
'main',
|
|
38
|
+
'[role="main"]',
|
|
39
|
+
'#main',
|
|
40
|
+
'.main',
|
|
41
|
+
'#content',
|
|
42
|
+
'.content',
|
|
43
|
+
'#contents',
|
|
44
|
+
'.contents',
|
|
45
|
+
'#main-content',
|
|
46
|
+
'.main-content',
|
|
47
|
+
'#main_content',
|
|
48
|
+
'.main_content',
|
|
49
|
+
'#mainContent',
|
|
50
|
+
'.mainContent',
|
|
51
|
+
];
|
|
52
|
+
if (mainContentSelector) {
|
|
53
|
+
selectors.unshift(mainContentSelector);
|
|
54
|
+
}
|
|
55
|
+
let $main = null;
|
|
56
|
+
for (const sel of selectors) {
|
|
57
|
+
try {
|
|
58
|
+
$main = doc.querySelector(sel);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if ($main) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!$main) {
|
|
68
|
+
// Kept in sync with beholder's MAIN_CONTENT_FALLBACK_SELECTORS.
|
|
69
|
+
const fallbackSelectors = [
|
|
70
|
+
'[id*="main" i]',
|
|
71
|
+
'[class*="main" i]',
|
|
72
|
+
'[id*="content" i]',
|
|
73
|
+
'[class*="content" i]',
|
|
74
|
+
];
|
|
75
|
+
for (const sel of fallbackSelectors) {
|
|
76
|
+
const candidate = doc.querySelector(sel);
|
|
77
|
+
if (candidate && candidate !== doc.body && candidate !== doc.documentElement) {
|
|
78
|
+
$main = candidate;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!$main)
|
|
84
|
+
return [];
|
|
85
|
+
// The eight hyphenated element names the Custom Elements spec reserves
|
|
86
|
+
// as non-registrable (SVG/MathML legacy names) — the sole exception to
|
|
87
|
+
// "a hyphenated tag name is a custom element".
|
|
88
|
+
const reservedHyphenatedNames = new Set([
|
|
89
|
+
'annotation-xml',
|
|
90
|
+
'color-profile',
|
|
91
|
+
'font-face',
|
|
92
|
+
'font-face-src',
|
|
93
|
+
'font-face-uri',
|
|
94
|
+
'font-face-format',
|
|
95
|
+
'font-face-name',
|
|
96
|
+
'missing-glyph',
|
|
97
|
+
]);
|
|
98
|
+
const candidates = [];
|
|
99
|
+
for (const el of $main.querySelectorAll('*')) {
|
|
100
|
+
const tagName = el.nodeName.toLowerCase();
|
|
101
|
+
if (!tagName.includes('-') || reservedHyphenatedNames.has(tagName))
|
|
102
|
+
continue;
|
|
103
|
+
candidates.push({
|
|
104
|
+
nodeName: el.nodeName,
|
|
105
|
+
elementId: el.id || null,
|
|
106
|
+
classList: [...el.classList],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return candidates;
|
|
110
|
+
}
|