@nitpicker/crawler 0.8.0 → 0.11.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/archive/archive-accessor.d.ts +87 -14
- package/lib/archive/archive-accessor.js +162 -36
- package/lib/archive/archive.d.ts +147 -24
- package/lib/archive/archive.js +252 -86
- package/lib/archive/database.d.ts +209 -25
- package/lib/archive/database.js +928 -108
- package/lib/archive/filesystem/peek-tar-top-dir.d.ts +28 -0
- package/lib/archive/filesystem/peek-tar-top-dir.js +65 -0
- package/lib/archive/init-schema.d.ts +53 -2
- package/lib/archive/init-schema.js +247 -15
- package/lib/archive/meta/assert-compatible-version.d.ts +39 -0
- package/lib/archive/meta/assert-compatible-version.js +72 -0
- package/lib/archive/meta/classify-jsonld-type.d.ts +23 -0
- package/lib/archive/meta/classify-jsonld-type.js +43 -0
- package/lib/archive/meta/compare-semver.d.ts +23 -0
- package/lib/archive/meta/compare-semver.js +51 -0
- package/lib/archive/meta/compute-page-denormalized.d.ts +21 -0
- package/lib/archive/meta/compute-page-denormalized.js +35 -0
- package/lib/archive/meta/derive-flat-from-meta.d.ts +35 -0
- package/lib/archive/meta/derive-flat-from-meta.js +158 -0
- package/lib/archive/meta/derive-meta-extras.d.ts +20 -0
- package/lib/archive/meta/derive-meta-extras.js +23 -0
- package/lib/archive/meta/extract-tags-for-archive.d.ts +18 -0
- package/lib/archive/meta/extract-tags-for-archive.js +36 -0
- package/lib/archive/meta/summarize-jsonld.d.ts +17 -0
- package/lib/archive/meta/summarize-jsonld.js +29 -0
- package/lib/archive/meta/summarize-tags.d.ts +16 -0
- package/lib/archive/meta/summarize-tags.js +33 -0
- package/lib/archive/meta/types.d.ts +207 -0
- package/lib/archive/meta/types.js +33 -0
- package/lib/archive/migrate-crawl-errors.d.ts +20 -0
- package/lib/archive/migrate-crawl-errors.js +38 -0
- package/lib/archive/migrate-html-blob-tables.d.ts +24 -0
- package/lib/archive/migrate-html-blob-tables.js +53 -0
- package/lib/archive/migrate-page-errors.d.ts +16 -0
- package/lib/archive/migrate-page-errors.js +35 -0
- package/lib/archive/migrate-pages-resources-source.d.ts +16 -0
- package/lib/archive/migrate-pages-resources-source.js +46 -0
- package/lib/archive/page.d.ts +187 -49
- package/lib/archive/page.js +258 -63
- package/lib/archive/peek-archive-lock.d.ts +40 -0
- package/lib/archive/peek-archive-lock.js +62 -0
- package/lib/archive/resolve-redirect-chain.d.ts +33 -0
- package/lib/archive/resolve-redirect-chain.js +27 -0
- package/lib/archive/types.d.ts +135 -26
- package/lib/crawler/close-browser-safely.d.ts +64 -0
- package/lib/crawler/close-browser-safely.js +73 -0
- package/lib/crawler/crawler.d.ts +4 -1
- package/lib/crawler/crawler.js +290 -32
- package/lib/crawler/create-change-phase-handler.d.ts +54 -0
- package/lib/crawler/create-change-phase-handler.js +44 -0
- package/lib/crawler/derive-page-source.d.ts +23 -0
- package/lib/crawler/derive-page-source.js +28 -0
- package/lib/crawler/derive-resource-source.d.ts +23 -0
- package/lib/crawler/derive-resource-source.js +26 -0
- package/lib/crawler/drain-phase-errors.d.ts +48 -0
- package/lib/crawler/drain-phase-errors.js +35 -0
- package/lib/crawler/fetch-destination.js +38 -2
- package/lib/crawler/format-crawl-progress.d.ts +12 -3
- package/lib/crawler/format-crawl-progress.js +14 -6
- package/lib/crawler/handle-browser-close.d.ts +29 -0
- package/lib/crawler/handle-browser-close.js +28 -0
- package/lib/crawler/is-html-content-type.d.ts +17 -0
- package/lib/crawler/is-html-content-type.js +19 -0
- package/lib/crawler/is-likely-html-url.d.ts +22 -0
- package/lib/crawler/is-likely-html-url.js +65 -0
- package/lib/crawler/kill-process-tree.d.ts +94 -0
- package/lib/crawler/kill-process-tree.js +178 -0
- package/lib/crawler/link-list.js +2 -1
- package/lib/crawler/link-to-page-data.d.ts +13 -5
- package/lib/crawler/link-to-page-data.js +26 -5
- package/lib/crawler/log-undrained-phase-errors.d.ts +37 -0
- package/lib/crawler/log-undrained-phase-errors.js +34 -0
- package/lib/crawler/normalize-content-type.d.ts +14 -0
- package/lib/crawler/normalize-content-type.js +20 -0
- package/lib/crawler/partition-urls-by-html.d.ts +16 -0
- package/lib/crawler/partition-urls-by-html.js +23 -0
- package/lib/crawler/redirect-dest-key.d.ts +19 -0
- package/lib/crawler/redirect-dest-key.js +27 -0
- package/lib/crawler/resource-to-page-data.d.ts +28 -0
- package/lib/crawler/resource-to-page-data.js +59 -0
- package/lib/crawler/types.d.ts +122 -1
- package/lib/crawler-orchestrator.d.ts +93 -1
- package/lib/crawler-orchestrator.js +389 -12
- package/lib/crawler.d.ts +5 -0
- package/lib/crawler.js +3 -0
- package/lib/resource-row-to-lookup-result.d.ts +13 -0
- package/lib/resource-row-to-lookup-result.js +20 -0
- package/lib/types.d.ts +11 -1
- package/lib/utils/object/parse-response-headers.d.ts +12 -0
- package/lib/utils/object/parse-response-headers.js +26 -0
- package/package.json +4 -4
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two semver-like version strings.
|
|
3
|
+
*
|
|
4
|
+
* Only the major / minor / patch numeric components are considered;
|
|
5
|
+
* pre-release tags (`-alpha.1`) and build metadata (`+sha.abc`) are
|
|
6
|
+
* dropped before comparison so `"0.10.0-alpha.1"` compares equal to
|
|
7
|
+
* `"0.10.0"`. Missing components default to `0`, so `"0.10"` compares
|
|
8
|
+
* equal to `"0.10.0"`.
|
|
9
|
+
*
|
|
10
|
+
* Designed for `assertCompatibleVersion`'s
|
|
11
|
+
* "is the archive's `info.version` at least the required format
|
|
12
|
+
* version?" check — we do not need full semver semantics there, and
|
|
13
|
+
* pulling in the `semver` package would be overkill for one numeric
|
|
14
|
+
* comparison.
|
|
15
|
+
* @param a - The left-hand version string.
|
|
16
|
+
* @param b - The right-hand version string.
|
|
17
|
+
* @returns Negative when `a < b`, positive when `a > b`, zero when equal.
|
|
18
|
+
* @example
|
|
19
|
+
* compareSemver('0.9.0', '0.10.0') // → -1
|
|
20
|
+
* compareSemver('0.10.0', '0.10.0') // → 0
|
|
21
|
+
* compareSemver('0.10.5-alpha.1', '0.10.0') // → +5
|
|
22
|
+
*/
|
|
23
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two semver-like version strings.
|
|
3
|
+
*
|
|
4
|
+
* Only the major / minor / patch numeric components are considered;
|
|
5
|
+
* pre-release tags (`-alpha.1`) and build metadata (`+sha.abc`) are
|
|
6
|
+
* dropped before comparison so `"0.10.0-alpha.1"` compares equal to
|
|
7
|
+
* `"0.10.0"`. Missing components default to `0`, so `"0.10"` compares
|
|
8
|
+
* equal to `"0.10.0"`.
|
|
9
|
+
*
|
|
10
|
+
* Designed for `assertCompatibleVersion`'s
|
|
11
|
+
* "is the archive's `info.version` at least the required format
|
|
12
|
+
* version?" check — we do not need full semver semantics there, and
|
|
13
|
+
* pulling in the `semver` package would be overkill for one numeric
|
|
14
|
+
* comparison.
|
|
15
|
+
* @param a - The left-hand version string.
|
|
16
|
+
* @param b - The right-hand version string.
|
|
17
|
+
* @returns Negative when `a < b`, positive when `a > b`, zero when equal.
|
|
18
|
+
* @example
|
|
19
|
+
* compareSemver('0.9.0', '0.10.0') // → -1
|
|
20
|
+
* compareSemver('0.10.0', '0.10.0') // → 0
|
|
21
|
+
* compareSemver('0.10.5-alpha.1', '0.10.0') // → +5
|
|
22
|
+
*/
|
|
23
|
+
export function compareSemver(a, b) {
|
|
24
|
+
const pa = parseComponents(a);
|
|
25
|
+
const pb = parseComponents(b);
|
|
26
|
+
for (let i = 0; i < 3; i++) {
|
|
27
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
28
|
+
if (diff !== 0)
|
|
29
|
+
return diff;
|
|
30
|
+
}
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Strips pre-release / build metadata and parses the first three numeric
|
|
35
|
+
* components. Missing components default to `0`. Non-numeric components
|
|
36
|
+
* collapse to `0` rather than throwing — `assertCompatibleVersion` needs
|
|
37
|
+
* defensive behavior against hand-edited `info.version` strings.
|
|
38
|
+
* @param version
|
|
39
|
+
*/
|
|
40
|
+
function parseComponents(version) {
|
|
41
|
+
const core = version.split(/[-+]/)[0] ?? '';
|
|
42
|
+
const parts = core.split('.');
|
|
43
|
+
const major = Number.parseInt(parts[0] ?? '0', 10);
|
|
44
|
+
const minor = Number.parseInt(parts[1] ?? '0', 10);
|
|
45
|
+
const patch = Number.parseInt(parts[2] ?? '0', 10);
|
|
46
|
+
return [
|
|
47
|
+
Number.isFinite(major) ? major : 0,
|
|
48
|
+
Number.isFinite(minor) ? minor : 0,
|
|
49
|
+
Number.isFinite(patch) ? patch : 0,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PageDenormalizedColumns } from './types.js';
|
|
2
|
+
import type { Meta } from '@d-zero/beholder';
|
|
3
|
+
/**
|
|
4
|
+
* Computes the denormalised aggregate columns stored on the `pages` table at
|
|
5
|
+
* write time: `tag_count`, `jsonld_count`, `tags_providers_csv`.
|
|
6
|
+
*
|
|
7
|
+
* Why denormalise: the Sheets report and the `get-page-detail` summary need
|
|
8
|
+
* "how many JSON-LD entries?" and "which Wappalyzer providers?" for every
|
|
9
|
+
* page row. Computing those at read time forces either a per-page sub-query
|
|
10
|
+
* (N+1) or a site-wide `GROUP BY` (heavy on 1M-page archives). Writing them
|
|
11
|
+
* once at scrape time keeps the read path to a single `pages` projection.
|
|
12
|
+
*
|
|
13
|
+
* `tags_providers_csv` uses comma as separator (no escaping). Wappalyzer
|
|
14
|
+
* provider names do not contain commas in practice; if that ever changes,
|
|
15
|
+
* switch to a JSON array column or use `\x1f` as separator. The CSV form
|
|
16
|
+
* exists because Sheets / Google Spreadsheet renders comma-separated lists
|
|
17
|
+
* natively without unwrapping JSON.
|
|
18
|
+
* @param meta - Beholder-derived metadata for the page.
|
|
19
|
+
* @returns The three denormalised columns.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computePageDenormalized(meta: Meta): PageDenormalizedColumns;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes the denormalised aggregate columns stored on the `pages` table at
|
|
3
|
+
* write time: `tag_count`, `jsonld_count`, `tags_providers_csv`.
|
|
4
|
+
*
|
|
5
|
+
* Why denormalise: the Sheets report and the `get-page-detail` summary need
|
|
6
|
+
* "how many JSON-LD entries?" and "which Wappalyzer providers?" for every
|
|
7
|
+
* page row. Computing those at read time forces either a per-page sub-query
|
|
8
|
+
* (N+1) or a site-wide `GROUP BY` (heavy on 1M-page archives). Writing them
|
|
9
|
+
* once at scrape time keeps the read path to a single `pages` projection.
|
|
10
|
+
*
|
|
11
|
+
* `tags_providers_csv` uses comma as separator (no escaping). Wappalyzer
|
|
12
|
+
* provider names do not contain commas in practice; if that ever changes,
|
|
13
|
+
* switch to a JSON array column or use `\x1f` as separator. The CSV form
|
|
14
|
+
* exists because Sheets / Google Spreadsheet renders comma-separated lists
|
|
15
|
+
* natively without unwrapping JSON.
|
|
16
|
+
* @param meta - Beholder-derived metadata for the page.
|
|
17
|
+
* @returns The three denormalised columns.
|
|
18
|
+
*/
|
|
19
|
+
export function computePageDenormalized(meta) {
|
|
20
|
+
// `??` guards tolerate the legacy "minimal meta" shape (`{ title: 'X' }`)
|
|
21
|
+
// produced by older test fixtures. Real beholder 3.0.0 always populates
|
|
22
|
+
// these required fields; the guards only matter for test fixtures that
|
|
23
|
+
// pre-date the v2 schema.
|
|
24
|
+
const tagEntries = meta.tags?.entries ?? [];
|
|
25
|
+
const jsonLd = meta.jsonLd ?? [];
|
|
26
|
+
const speculationRules = meta.speculationRules ?? [];
|
|
27
|
+
const tag_count = tagEntries.length;
|
|
28
|
+
const jsonld_count = jsonLd.length + speculationRules.length;
|
|
29
|
+
const providers = new Set();
|
|
30
|
+
for (const entry of tagEntries) {
|
|
31
|
+
providers.add(entry.provider);
|
|
32
|
+
}
|
|
33
|
+
const tags_providers_csv = [...providers].toSorted().join(',');
|
|
34
|
+
return { tag_count, jsonld_count, tags_providers_csv };
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FlatPageMetaColumns } from './types.js';
|
|
2
|
+
import type { Meta } from '@d-zero/beholder';
|
|
3
|
+
/**
|
|
4
|
+
* Derives the {@link FlatPageMetaColumns} (`pages` table flat columns) from a
|
|
5
|
+
* beholder {@link Meta} object.
|
|
6
|
+
*
|
|
7
|
+
* Handles three concerns the database layer should not re-implement:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Null-safe traversal** of the new nested Meta shape
|
|
10
|
+
* (`meta.og?.title`, `meta.robots?.noindex`, `meta.link?.canonical`, etc.).
|
|
11
|
+
* All optional sub-objects are guarded so an absent meta tag becomes
|
|
12
|
+
* `null` rather than a runtime crash.
|
|
13
|
+
*
|
|
14
|
+
* 2. **URL absolutisation** for every URL-shaped column. beholder 3.0.0
|
|
15
|
+
* extracts link/og href values via `link.getAttribute('href')`, which
|
|
16
|
+
* preserves relative URLs as-written. `find-mismatches` compares
|
|
17
|
+
* `canonical != url` directly, so we must resolve everything against the
|
|
18
|
+
* page URL (or `<base href>` if present) before persistence. Malformed
|
|
19
|
+
* URLs become `null` rather than blowing up the entire insert.
|
|
20
|
+
*
|
|
21
|
+
* 3. **Boolean → integer coercion** for SQLite-style boolean columns
|
|
22
|
+
* (`robots_*`, `formatDetection_telephone`). `undefined` and unparseable
|
|
23
|
+
* values map to `null`.
|
|
24
|
+
*
|
|
25
|
+
* URL-shaped columns: `canonical`, `og_url`, `og_image`, `amphtml`,
|
|
26
|
+
* `manifest`, `icon_href`, `appleTouchIcon_href`, `twitter_image`.
|
|
27
|
+
* @param meta - Beholder-derived metadata for the page.
|
|
28
|
+
* @param pageUrl - Absolute URL of the page being persisted. Used as the
|
|
29
|
+
* fallback base when `meta.baseHref` is absent or relative.
|
|
30
|
+
* @returns A flat object whose keys map 1:1 to `pages` table columns.
|
|
31
|
+
* @example
|
|
32
|
+
* deriveFlatFromMeta(meta, 'https://example.com/about')
|
|
33
|
+
* // → { canonical: 'https://example.com/about', og_title: '...', ... }
|
|
34
|
+
*/
|
|
35
|
+
export declare function deriveFlatFromMeta(meta: Meta, pageUrl: string): FlatPageMetaColumns;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the {@link FlatPageMetaColumns} (`pages` table flat columns) from a
|
|
3
|
+
* beholder {@link Meta} object.
|
|
4
|
+
*
|
|
5
|
+
* Handles three concerns the database layer should not re-implement:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Null-safe traversal** of the new nested Meta shape
|
|
8
|
+
* (`meta.og?.title`, `meta.robots?.noindex`, `meta.link?.canonical`, etc.).
|
|
9
|
+
* All optional sub-objects are guarded so an absent meta tag becomes
|
|
10
|
+
* `null` rather than a runtime crash.
|
|
11
|
+
*
|
|
12
|
+
* 2. **URL absolutisation** for every URL-shaped column. beholder 3.0.0
|
|
13
|
+
* extracts link/og href values via `link.getAttribute('href')`, which
|
|
14
|
+
* preserves relative URLs as-written. `find-mismatches` compares
|
|
15
|
+
* `canonical != url` directly, so we must resolve everything against the
|
|
16
|
+
* page URL (or `<base href>` if present) before persistence. Malformed
|
|
17
|
+
* URLs become `null` rather than blowing up the entire insert.
|
|
18
|
+
*
|
|
19
|
+
* 3. **Boolean → integer coercion** for SQLite-style boolean columns
|
|
20
|
+
* (`robots_*`, `formatDetection_telephone`). `undefined` and unparseable
|
|
21
|
+
* values map to `null`.
|
|
22
|
+
*
|
|
23
|
+
* URL-shaped columns: `canonical`, `og_url`, `og_image`, `amphtml`,
|
|
24
|
+
* `manifest`, `icon_href`, `appleTouchIcon_href`, `twitter_image`.
|
|
25
|
+
* @param meta - Beholder-derived metadata for the page.
|
|
26
|
+
* @param pageUrl - Absolute URL of the page being persisted. Used as the
|
|
27
|
+
* fallback base when `meta.baseHref` is absent or relative.
|
|
28
|
+
* @returns A flat object whose keys map 1:1 to `pages` table columns.
|
|
29
|
+
* @example
|
|
30
|
+
* deriveFlatFromMeta(meta, 'https://example.com/about')
|
|
31
|
+
* // → { canonical: 'https://example.com/about', og_title: '...', ... }
|
|
32
|
+
*/
|
|
33
|
+
export function deriveFlatFromMeta(meta, pageUrl) {
|
|
34
|
+
const base = resolveBase(meta.baseHref, pageUrl);
|
|
35
|
+
const robots = meta.robots;
|
|
36
|
+
const og = meta.og;
|
|
37
|
+
const ogArticle = og?.article;
|
|
38
|
+
const twitter = meta.twitter;
|
|
39
|
+
const link = meta.link;
|
|
40
|
+
const formatDetection = meta.formatDetection;
|
|
41
|
+
return {
|
|
42
|
+
// Document basics
|
|
43
|
+
lang: nullableString(meta.lang),
|
|
44
|
+
dir: nullableString(meta.dir),
|
|
45
|
+
charset: nullableString(meta.charset),
|
|
46
|
+
baseHref: absolutizeUrl(meta.baseHref, pageUrl),
|
|
47
|
+
viewport_raw: nullableString(meta.viewport?.raw),
|
|
48
|
+
themeColor: nullableString(meta.themeColor),
|
|
49
|
+
applicationName: nullableString(meta.applicationName),
|
|
50
|
+
author: nullableString(meta.author),
|
|
51
|
+
generator: nullableString(meta.generator),
|
|
52
|
+
publisher: nullableString(meta.publisher),
|
|
53
|
+
// Robots
|
|
54
|
+
robots_raw: nullableString(robots?.raw),
|
|
55
|
+
robots_noindex: booleanToInt(robots?.noindex),
|
|
56
|
+
robots_nofollow: booleanToInt(robots?.nofollow),
|
|
57
|
+
robots_noarchive: booleanToInt(robots?.noarchive),
|
|
58
|
+
robots_noimageindex: booleanToInt(robots?.noimageindex),
|
|
59
|
+
googlebot: nullableString(meta.googlebot),
|
|
60
|
+
// Link (1:1 only)
|
|
61
|
+
canonical: absolutizeUrl(link?.canonical, base),
|
|
62
|
+
amphtml: absolutizeUrl(link?.amphtml, base),
|
|
63
|
+
manifest: absolutizeUrl(link?.manifest, base),
|
|
64
|
+
icon_href: absolutizeUrl(link?.icon?.href, base),
|
|
65
|
+
appleTouchIcon_href: absolutizeUrl(link?.appleTouchIcon?.href, base),
|
|
66
|
+
// Open Graph
|
|
67
|
+
og_type: nullableString(og?.type),
|
|
68
|
+
og_title: nullableString(og?.title),
|
|
69
|
+
og_url: absolutizeUrl(og?.url, base),
|
|
70
|
+
og_site_name: nullableString(og?.siteName),
|
|
71
|
+
og_description: nullableString(og?.description),
|
|
72
|
+
og_image: absolutizeUrl(og?.imageUrl ?? og?.image?.[0], base),
|
|
73
|
+
og_image_alt: nullableString(og?.imageAlt),
|
|
74
|
+
og_image_width: nullableString(og?.imageWidth),
|
|
75
|
+
og_image_height: nullableString(og?.imageHeight),
|
|
76
|
+
og_locale: nullableString(og?.locale),
|
|
77
|
+
og_article_published_time: nullableString(ogArticle?.publishedTime),
|
|
78
|
+
og_article_modified_time: nullableString(ogArticle?.modifiedTime),
|
|
79
|
+
// Twitter
|
|
80
|
+
twitter_card: nullableString(twitter?.card),
|
|
81
|
+
twitter_site: nullableString(twitter?.site),
|
|
82
|
+
twitter_creator: nullableString(twitter?.creator),
|
|
83
|
+
twitter_title: nullableString(twitter?.title),
|
|
84
|
+
twitter_description: nullableString(twitter?.description),
|
|
85
|
+
twitter_image: absolutizeUrl(twitter?.image ?? twitter?.imageSrc, base),
|
|
86
|
+
// One-offs
|
|
87
|
+
fb_app_id: nullableString(meta.fb?.appId),
|
|
88
|
+
verification_google: nullableString(meta.verification?.google),
|
|
89
|
+
formatDetection_telephone: booleanToInt(formatDetection?.telephone),
|
|
90
|
+
// Title / description / keywords (top-level Meta fields)
|
|
91
|
+
title: nullableString(meta.title),
|
|
92
|
+
description: nullableString(meta.description),
|
|
93
|
+
keywords: nullableString(meta.keywords),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolves the effective base URL for relative-URL absolutisation.
|
|
98
|
+
*
|
|
99
|
+
* Falls back to `pageUrl` when `baseHref` is absent. When `baseHref` itself
|
|
100
|
+
* is a relative URL (rare but spec-permitted), resolves it against
|
|
101
|
+
* `pageUrl` first so subsequent `new URL(value, base)` calls see an
|
|
102
|
+
* absolute base.
|
|
103
|
+
* @param baseHref
|
|
104
|
+
* @param pageUrl
|
|
105
|
+
*/
|
|
106
|
+
function resolveBase(baseHref, pageUrl) {
|
|
107
|
+
if (!baseHref)
|
|
108
|
+
return pageUrl;
|
|
109
|
+
try {
|
|
110
|
+
return new URL(baseHref, pageUrl).href;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return pageUrl;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolves a possibly-relative URL against a base.
|
|
118
|
+
*
|
|
119
|
+
* Returns `null` for `undefined` / empty string / unparseable input so a
|
|
120
|
+
* malformed page does not abort the entire `#insertPage` call.
|
|
121
|
+
* @param value
|
|
122
|
+
* @param base
|
|
123
|
+
*/
|
|
124
|
+
function absolutizeUrl(value, base) {
|
|
125
|
+
if (value === undefined || value === null || value === '')
|
|
126
|
+
return null;
|
|
127
|
+
try {
|
|
128
|
+
return new URL(value, base).href;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Normalises optional / empty strings to `null` so SQL `IS NULL` queries
|
|
136
|
+
* behave consistently. Trim is intentional — leading/trailing whitespace in
|
|
137
|
+
* `<meta content=>` is meaningless for downstream consumers.
|
|
138
|
+
* @param value
|
|
139
|
+
*/
|
|
140
|
+
function nullableString(value) {
|
|
141
|
+
if (value === undefined || value === null)
|
|
142
|
+
return null;
|
|
143
|
+
const trimmed = value.trim();
|
|
144
|
+
return trimmed === '' ? null : trimmed;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Converts an optional boolean to SQLite's 0/1 integer representation. Any
|
|
148
|
+
* non-boolean (`undefined`, non-boolean string from `boolean-true` keys)
|
|
149
|
+
* becomes `null`.
|
|
150
|
+
* @param value
|
|
151
|
+
*/
|
|
152
|
+
function booleanToInt(value) {
|
|
153
|
+
if (value === true)
|
|
154
|
+
return 1;
|
|
155
|
+
if (value === false)
|
|
156
|
+
return 0;
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Meta } from '@d-zero/beholder';
|
|
2
|
+
/**
|
|
3
|
+
* Derives the `meta_extras` JSON payload from a beholder {@link Meta} object.
|
|
4
|
+
*
|
|
5
|
+
* Strategy: pass-through everything except data that lives in dedicated
|
|
6
|
+
* tables (`page_jsonld`, `page_tags`) or that is debug-only
|
|
7
|
+
* (`_raw`). Flat columns are intentionally **not** stripped — the modest
|
|
8
|
+
* storage duplication is preferred over a fragile field-by-field denylist
|
|
9
|
+
* that breaks every time beholder grows new Meta fields.
|
|
10
|
+
*
|
|
11
|
+
* The output is JSON-stringified by the caller (`#insertPage`) and stored in
|
|
12
|
+
* `pages.meta_extras`. Consumers read it back via `get-page-detail` and the
|
|
13
|
+
* Page wrapper's `metaExtras` getter.
|
|
14
|
+
*
|
|
15
|
+
* Plan note: "ファイルサイズが多少増えてもいいから取り出しパフォーマンスを優先" — this is exactly that
|
|
16
|
+
* trade-off. Future Meta fields are auto-captured without code change.
|
|
17
|
+
* @param meta - Beholder-derived metadata for the page.
|
|
18
|
+
* @returns Plain object suitable for `JSON.stringify`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function deriveMetaExtras(meta: Meta): Record<string, unknown>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the `meta_extras` JSON payload from a beholder {@link Meta} object.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: pass-through everything except data that lives in dedicated
|
|
5
|
+
* tables (`page_jsonld`, `page_tags`) or that is debug-only
|
|
6
|
+
* (`_raw`). Flat columns are intentionally **not** stripped — the modest
|
|
7
|
+
* storage duplication is preferred over a fragile field-by-field denylist
|
|
8
|
+
* that breaks every time beholder grows new Meta fields.
|
|
9
|
+
*
|
|
10
|
+
* The output is JSON-stringified by the caller (`#insertPage`) and stored in
|
|
11
|
+
* `pages.meta_extras`. Consumers read it back via `get-page-detail` and the
|
|
12
|
+
* Page wrapper's `metaExtras` getter.
|
|
13
|
+
*
|
|
14
|
+
* Plan note: "ファイルサイズが多少増えてもいいから取り出しパフォーマンスを優先" — this is exactly that
|
|
15
|
+
* trade-off. Future Meta fields are auto-captured without code change.
|
|
16
|
+
* @param meta - Beholder-derived metadata for the page.
|
|
17
|
+
* @returns Plain object suitable for `JSON.stringify`.
|
|
18
|
+
*/
|
|
19
|
+
export function deriveMetaExtras(meta) {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we destructure to omit these from the spread
|
|
21
|
+
const { jsonLd, speculationRules, tags, _raw, ...rest } = meta;
|
|
22
|
+
return rest;
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TagRowForInsert } from './types.js';
|
|
2
|
+
import type { TagsMeta } from '@d-zero/beholder';
|
|
3
|
+
/**
|
|
4
|
+
* Flattens {@link TagsMeta.entries} (per provider × external-id tuple) into
|
|
5
|
+
* insert-ready rows for the `page_tags` table.
|
|
6
|
+
*
|
|
7
|
+
* One Wappalyzer entry → one row. `entry.id` becomes `externalId`. `category`
|
|
8
|
+
* is the first element of `categories`; the full list is preserved in the
|
|
9
|
+
* `categories` JSON column. `sources` is preserved verbatim as a JSON column.
|
|
10
|
+
*
|
|
11
|
+
* `pageId` is **not** filled here — the database layer injects it because the
|
|
12
|
+
* page row is `INSERT`-ed in the same transaction and its ID is known only at
|
|
13
|
+
* insert time.
|
|
14
|
+
* @param tags - The `meta.tags` object from beholder.
|
|
15
|
+
* @returns Rows ready for the `page_tags` insert; empty array when no tags
|
|
16
|
+
* were detected.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractTagsForArchive(tags: TagsMeta | undefined): ReadonlyArray<Omit<TagRowForInsert, 'pageId'>>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flattens {@link TagsMeta.entries} (per provider × external-id tuple) into
|
|
3
|
+
* insert-ready rows for the `page_tags` table.
|
|
4
|
+
*
|
|
5
|
+
* One Wappalyzer entry → one row. `entry.id` becomes `externalId`. `category`
|
|
6
|
+
* is the first element of `categories`; the full list is preserved in the
|
|
7
|
+
* `categories` JSON column. `sources` is preserved verbatim as a JSON column.
|
|
8
|
+
*
|
|
9
|
+
* `pageId` is **not** filled here — the database layer injects it because the
|
|
10
|
+
* page row is `INSERT`-ed in the same transaction and its ID is known only at
|
|
11
|
+
* insert time.
|
|
12
|
+
* @param tags - The `meta.tags` object from beholder.
|
|
13
|
+
* @returns Rows ready for the `page_tags` insert; empty array when no tags
|
|
14
|
+
* were detected.
|
|
15
|
+
*/
|
|
16
|
+
export function extractTagsForArchive(tags) {
|
|
17
|
+
// `tags` can be undefined when fed legacy minimal-Meta test fixtures.
|
|
18
|
+
// Real beholder 3.0.0 always populates this required field.
|
|
19
|
+
const entries = tags?.entries ?? [];
|
|
20
|
+
if (entries.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
return entries.map((entry) => ({
|
|
23
|
+
provider: entry.provider,
|
|
24
|
+
category: entry.categories[0] ?? null,
|
|
25
|
+
externalId: entry.id ?? null,
|
|
26
|
+
version: entry.version ?? null,
|
|
27
|
+
confidence: entry.confidence ?? null,
|
|
28
|
+
categories: [...entry.categories],
|
|
29
|
+
sources: entry.sources.map((s) => ({
|
|
30
|
+
type: s.type,
|
|
31
|
+
src: s.src,
|
|
32
|
+
location: s.location,
|
|
33
|
+
globalName: s.globalName,
|
|
34
|
+
})),
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { JsonLdRow, JsonLdSummary } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the {@link JsonLdSummary} object returned by `get-page-detail` from a
|
|
4
|
+
* page's JSON-LD rows.
|
|
5
|
+
*
|
|
6
|
+
* Why summary: the raw payload of a single page's JSON-LD can reach several
|
|
7
|
+
* MB on e-commerce sites (Amazon product page = 50 schemas × 50KB). Returning
|
|
8
|
+
* that inline blows up MCP / LLM token budgets. The summary preserves the
|
|
9
|
+
* shape of the data (counts + unique types + parse error count) so consumers
|
|
10
|
+
* can decide whether to drill in via `get-page-jsonld(url)`.
|
|
11
|
+
*
|
|
12
|
+
* Entries with `type === null` are surfaced as `'(unknown)'` so the type
|
|
13
|
+
* list is enumerable without sentinel handling on the consumer side.
|
|
14
|
+
* @param rows - All `page_jsonld` rows for one page.
|
|
15
|
+
* @returns Summary with `count`, sorted unique `types[]`, and `parseErrorCount`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function summarizeJsonLd(rows: readonly JsonLdRow[]): JsonLdSummary;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the {@link JsonLdSummary} object returned by `get-page-detail` from a
|
|
3
|
+
* page's JSON-LD rows.
|
|
4
|
+
*
|
|
5
|
+
* Why summary: the raw payload of a single page's JSON-LD can reach several
|
|
6
|
+
* MB on e-commerce sites (Amazon product page = 50 schemas × 50KB). Returning
|
|
7
|
+
* that inline blows up MCP / LLM token budgets. The summary preserves the
|
|
8
|
+
* shape of the data (counts + unique types + parse error count) so consumers
|
|
9
|
+
* can decide whether to drill in via `get-page-jsonld(url)`.
|
|
10
|
+
*
|
|
11
|
+
* Entries with `type === null` are surfaced as `'(unknown)'` so the type
|
|
12
|
+
* list is enumerable without sentinel handling on the consumer side.
|
|
13
|
+
* @param rows - All `page_jsonld` rows for one page.
|
|
14
|
+
* @returns Summary with `count`, sorted unique `types[]`, and `parseErrorCount`.
|
|
15
|
+
*/
|
|
16
|
+
export function summarizeJsonLd(rows) {
|
|
17
|
+
const types = new Set();
|
|
18
|
+
let parseErrorCount = 0;
|
|
19
|
+
for (const row of rows) {
|
|
20
|
+
types.add(row.type ?? '(unknown)');
|
|
21
|
+
if (row.parseError !== null)
|
|
22
|
+
parseErrorCount++;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
count: rows.length,
|
|
26
|
+
types: [...types].toSorted(),
|
|
27
|
+
parseErrorCount,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TagRow, TagsSummary } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the {@link TagsSummary} object returned by `get-page-detail` from a
|
|
4
|
+
* page's tag rows.
|
|
5
|
+
*
|
|
6
|
+
* Provides a `provider → unique IDs[]` map so consumers can answer questions
|
|
7
|
+
* like "what GTM containers are on this page?" without fetching the full
|
|
8
|
+
* `categories` / `sources` JSON columns.
|
|
9
|
+
*
|
|
10
|
+
* IDs within each provider are sorted and de-duplicated. Providers with no
|
|
11
|
+
* `externalId` rows still appear in the map with an empty `[]` so the
|
|
12
|
+
* caller knows the provider was detected.
|
|
13
|
+
* @param rows - All `page_tags` rows for one page.
|
|
14
|
+
* @returns Summary with `count` and `providerIds` map.
|
|
15
|
+
*/
|
|
16
|
+
export declare function summarizeTags(rows: readonly TagRow[]): TagsSummary;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the {@link TagsSummary} object returned by `get-page-detail` from a
|
|
3
|
+
* page's tag rows.
|
|
4
|
+
*
|
|
5
|
+
* Provides a `provider → unique IDs[]` map so consumers can answer questions
|
|
6
|
+
* like "what GTM containers are on this page?" without fetching the full
|
|
7
|
+
* `categories` / `sources` JSON columns.
|
|
8
|
+
*
|
|
9
|
+
* IDs within each provider are sorted and de-duplicated. Providers with no
|
|
10
|
+
* `externalId` rows still appear in the map with an empty `[]` so the
|
|
11
|
+
* caller knows the provider was detected.
|
|
12
|
+
* @param rows - All `page_tags` rows for one page.
|
|
13
|
+
* @returns Summary with `count` and `providerIds` map.
|
|
14
|
+
*/
|
|
15
|
+
export function summarizeTags(rows) {
|
|
16
|
+
const providerIds = {};
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
if (!(row.provider in providerIds)) {
|
|
19
|
+
providerIds[row.provider] = new Set();
|
|
20
|
+
}
|
|
21
|
+
if (row.externalId !== null) {
|
|
22
|
+
providerIds[row.provider].add(row.externalId);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const sorted = {};
|
|
26
|
+
for (const provider of Object.keys(providerIds).toSorted()) {
|
|
27
|
+
sorted[provider] = [...providerIds[provider]].toSorted();
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
count: rows.length,
|
|
31
|
+
providerIds: sorted,
|
|
32
|
+
};
|
|
33
|
+
}
|