@detiq/crawler 1.1.9 → 1.1.11
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/dist/accessibility-auditor.js +54 -74
- package/dist/ai-login.js +227 -249
- package/dist/auth-token-rotator.js +57 -67
- package/dist/auth.js +299 -336
- package/dist/browser-extension.js +104 -142
- package/dist/captcha-solver.js +98 -125
- package/dist/color-contrast.js +77 -89
- package/dist/content-chunker.js +126 -126
- package/dist/content-dedup.js +54 -71
- package/dist/cookie-consent.js +68 -83
- package/dist/cookie-jar.js +53 -60
- package/dist/crawler.js +4494 -5353
- package/dist/custom-js-injector.js +45 -61
- package/dist/dark-mode-capture.js +67 -81
- package/dist/dead-link-detector.js +47 -55
- package/dist/element-hash.js +8 -12
- package/dist/email-otp.js +154 -195
- package/dist/engine-router.js +87 -98
- package/dist/export/bulk-export.js +70 -55
- package/dist/export/vector-export.js +77 -105
- package/dist/extractors/advanced.js +249 -266
- package/dist/extractors/artifact-envelope.js +59 -58
- package/dist/failure-analysis-agent.js +37 -33
- package/dist/feature-flag-detector.js +51 -66
- package/dist/form-detector.js +55 -75
- package/dist/frontier.js +76 -101
- package/dist/gap-pass.js +68 -89
- package/dist/graphql-detector.js +62 -72
- package/dist/http-engine.js +282 -330
- package/dist/incremental-diff.js +54 -87
- package/dist/index.js +67 -28
- package/dist/input-fuzzer.js +93 -97
- package/dist/interactive-probe.js +715 -827
- package/dist/job-queue.js +2151 -2169
- package/dist/js-coverage.js +52 -56
- package/dist/js-router-detector.js +86 -117
- package/dist/language-detector.js +51 -63
- package/dist/link-classifier.js +32 -50
- package/dist/live-view.js +91 -108
- package/dist/locator-extractor.js +151 -187
- package/dist/logger.js +9 -7
- package/dist/markdown-extract.js +48 -61
- package/dist/native-crawler.js +780 -946
- package/dist/network-observer.js +135 -156
- package/dist/network-throttle.js +29 -40
- package/dist/openapi-generator.js +77 -92
- package/dist/pdf-extractor.js +72 -90
- package/dist/performance-metrics.js +80 -102
- package/dist/phash.js +125 -144
- package/dist/pii-scrubber.js +65 -68
- package/dist/playwright-recorder.js +70 -58
- package/dist/policies/crawl-safety.js +31 -34
- package/dist/profiles/domain-profile.js +130 -125
- package/dist/proxy.js +146 -172
- package/dist/rate-limiter.js +58 -65
- package/dist/redirect-tracker.js +60 -64
- package/dist/redis-frontier.js +116 -136
- package/dist/redis-queue.js +157 -144
- package/dist/response-cache.js +127 -140
- package/dist/responsive-detector.js +42 -46
- package/dist/retry-policy.js +46 -50
- package/dist/robots-txt.js +69 -83
- package/dist/screen-reader.js +45 -70
- package/dist/screenshot-masker.js +59 -77
- package/dist/seo-checker.js +88 -107
- package/dist/session-capture.js +192 -212
- package/dist/session-export.js +107 -133
- package/dist/sitemap-writer.js +38 -41
- package/dist/sitemap.js +141 -202
- package/dist/skyvern.js +123 -148
- package/dist/sqs-queue.js +42 -42
- package/dist/sso-token.js +74 -90
- package/dist/stealth/human-behavior.js +156 -173
- package/dist/telemetry.js +68 -82
- package/dist/test-executor.js +309 -335
- package/dist/tls-fingerprint.js +77 -88
- package/dist/url-pattern-completer.js +71 -90
- package/dist/visual-diff-verdict.js +23 -38
- package/dist/webhook-notifier.js +71 -69
- package/dist/websocket-detector.js +56 -68
- package/dist/worker.js +29 -38
- package/package.json +1 -1
- package/src/job-queue.ts +136 -0
package/dist/js-coverage.js
CHANGED
|
@@ -1,60 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export async function startCoverage(page) {
|
|
10
|
-
try {
|
|
11
|
-
await Promise.all([
|
|
12
|
-
page.coverage.startJSCoverage({ resetOnNavigation: false }),
|
|
13
|
-
page.coverage.startCSSCoverage({ resetOnNavigation: false }),
|
|
14
|
-
]);
|
|
15
|
-
}
|
|
16
|
-
catch { /* coverage not available in all environments */ }
|
|
1
|
+
async function startCoverage(page) {
|
|
2
|
+
try {
|
|
3
|
+
await Promise.all([
|
|
4
|
+
page.coverage.startJSCoverage({ resetOnNavigation: false }),
|
|
5
|
+
page.coverage.startCSSCoverage({ resetOnNavigation: false })
|
|
6
|
+
]);
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
17
9
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
for (const entry of cssCoverage) {
|
|
33
|
-
const totalBytes = entry.text?.length ?? 0;
|
|
34
|
-
const usedBytes = (entry.ranges ?? []).reduce((sum, r) => sum + (r.end - r.start), 0);
|
|
35
|
-
if (totalBytes > 0) {
|
|
36
|
-
entries.push({ url: entry.url, type: 'css', totalBytes, usedBytes, unusedPercent: Math.round((1 - usedBytes / totalBytes) * 100) });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const jsEntries = entries.filter(e => e.type === 'js');
|
|
40
|
-
const cssEntries = entries.filter(e => e.type === 'css');
|
|
41
|
-
const totalJsBytes = jsEntries.reduce((s, e) => s + e.totalBytes, 0);
|
|
42
|
-
const usedJsBytes = jsEntries.reduce((s, e) => s + e.usedBytes, 0);
|
|
43
|
-
const totalCssBytes = cssEntries.reduce((s, e) => s + e.totalBytes, 0);
|
|
44
|
-
const usedCssBytes = cssEntries.reduce((s, e) => s + e.usedBytes, 0);
|
|
45
|
-
return {
|
|
46
|
-
pageUrl,
|
|
47
|
-
entries,
|
|
48
|
-
totalJsBytes,
|
|
49
|
-
usedJsBytes,
|
|
50
|
-
totalCssBytes,
|
|
51
|
-
usedCssBytes,
|
|
52
|
-
jsUnusedPercent: totalJsBytes > 0 ? Math.round((1 - usedJsBytes / totalJsBytes) * 100) : 0,
|
|
53
|
-
cssUnusedPercent: totalCssBytes > 0 ? Math.round((1 - usedCssBytes / totalCssBytes) * 100) : 0,
|
|
54
|
-
collectedAt: new Date().toISOString(),
|
|
55
|
-
};
|
|
10
|
+
async function stopAndCollectCoverage(page, pageUrl) {
|
|
11
|
+
try {
|
|
12
|
+
const [jsCoverage, cssCoverage] = await Promise.all([
|
|
13
|
+
page.coverage.stopJSCoverage().catch(() => []),
|
|
14
|
+
page.coverage.stopCSSCoverage().catch(() => [])
|
|
15
|
+
]);
|
|
16
|
+
const entries = [];
|
|
17
|
+
for (const entry of jsCoverage) {
|
|
18
|
+
const totalBytes = entry.text?.length ?? 0;
|
|
19
|
+
const usedBytes = (entry.ranges ?? []).reduce((sum, r) => sum + (r.end - r.start), 0);
|
|
20
|
+
if (totalBytes > 0) {
|
|
21
|
+
entries.push({ url: entry.url, type: "js", totalBytes, usedBytes, unusedPercent: Math.round((1 - usedBytes / totalBytes) * 100) });
|
|
22
|
+
}
|
|
56
23
|
}
|
|
57
|
-
|
|
58
|
-
|
|
24
|
+
for (const entry of cssCoverage) {
|
|
25
|
+
const totalBytes = entry.text?.length ?? 0;
|
|
26
|
+
const usedBytes = (entry.ranges ?? []).reduce((sum, r) => sum + (r.end - r.start), 0);
|
|
27
|
+
if (totalBytes > 0) {
|
|
28
|
+
entries.push({ url: entry.url, type: "css", totalBytes, usedBytes, unusedPercent: Math.round((1 - usedBytes / totalBytes) * 100) });
|
|
29
|
+
}
|
|
59
30
|
}
|
|
31
|
+
const jsEntries = entries.filter((e) => e.type === "js");
|
|
32
|
+
const cssEntries = entries.filter((e) => e.type === "css");
|
|
33
|
+
const totalJsBytes = jsEntries.reduce((s, e) => s + e.totalBytes, 0);
|
|
34
|
+
const usedJsBytes = jsEntries.reduce((s, e) => s + e.usedBytes, 0);
|
|
35
|
+
const totalCssBytes = cssEntries.reduce((s, e) => s + e.totalBytes, 0);
|
|
36
|
+
const usedCssBytes = cssEntries.reduce((s, e) => s + e.usedBytes, 0);
|
|
37
|
+
return {
|
|
38
|
+
pageUrl,
|
|
39
|
+
entries,
|
|
40
|
+
totalJsBytes,
|
|
41
|
+
usedJsBytes,
|
|
42
|
+
totalCssBytes,
|
|
43
|
+
usedCssBytes,
|
|
44
|
+
jsUnusedPercent: totalJsBytes > 0 ? Math.round((1 - usedJsBytes / totalJsBytes) * 100) : 0,
|
|
45
|
+
cssUnusedPercent: totalCssBytes > 0 ? Math.round((1 - usedCssBytes / totalCssBytes) * 100) : 0,
|
|
46
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
60
51
|
}
|
|
52
|
+
export {
|
|
53
|
+
startCoverage,
|
|
54
|
+
stopAndCollectCoverage
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=js-coverage.js.map
|
|
@@ -1,121 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
for (const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if (g === '__NEXT_DATA__' && Array.isArray(val.buildManifest?.pages)) {
|
|
35
|
-
for (const p of val.buildManifest.pages)
|
|
36
|
-
if (typeof p === 'string')
|
|
37
|
-
found.push(p);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
// Router objects: options.routes, routes, getRoutes()
|
|
41
|
-
const routeArr = (Array.isArray(val.options?.routes) ? val.options.routes : null) ??
|
|
42
|
-
(Array.isArray(val.routes) ? val.routes : null) ??
|
|
43
|
-
(typeof val.getRoutes === 'function' ? val.getRoutes() : null);
|
|
44
|
-
if (!Array.isArray(routeArr))
|
|
45
|
-
continue;
|
|
46
|
-
const flatRoutes = (arr, prefix = '') => {
|
|
47
|
-
for (const r of arr) {
|
|
48
|
-
if (!r || typeof r !== 'object')
|
|
49
|
-
continue;
|
|
50
|
-
const path = r.path ?? r.href ?? r.url ?? '';
|
|
51
|
-
const full = path.startsWith('/') ? path : `${prefix}/${path}`.replace(/\/\//g, '/');
|
|
52
|
-
if (full && full !== '/')
|
|
53
|
-
found.push(full);
|
|
54
|
-
if (Array.isArray(r.children))
|
|
55
|
-
flatRoutes(r.children, full);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
flatRoutes(routeArr);
|
|
59
|
-
}
|
|
60
|
-
catch { /* skip corrupt globals */ }
|
|
61
|
-
}
|
|
62
|
-
// 2. Scan inline <script> content for route patterns ───────────────────
|
|
63
|
-
// Two-pass: regex first, if >3 found trust them. Below 3 → skip (too noisy).
|
|
64
|
-
const routePatterns = [
|
|
65
|
-
/\bpath\s*:\s*['"](\/?[\w\-\/\.~@]+)['"]/g,
|
|
66
|
-
/\broute\s*\(\s*['"](\/?[\w\-\/\.~@]+)['"]/g,
|
|
67
|
-
/<Route[^>]+path\s*=\s*['"](\/?[\w\-\/\.~@]+)['"]/g,
|
|
68
|
-
/to\s*:\s*['"](\/?[\w\-\/\.~@]+)['"]/g,
|
|
69
|
-
];
|
|
70
|
-
const scripts = Array.from(document.querySelectorAll('script:not([src])'));
|
|
71
|
-
for (const el of scripts.slice(0, 15)) {
|
|
72
|
-
const src = el.textContent ?? '';
|
|
73
|
-
if (src.length < 100 || src.length > 800_000)
|
|
74
|
-
continue;
|
|
75
|
-
for (const pat of routePatterns) {
|
|
76
|
-
const hits = [];
|
|
77
|
-
let m;
|
|
78
|
-
pat.lastIndex = 0;
|
|
79
|
-
while ((m = pat.exec(src)) !== null && hits.length < 100) {
|
|
80
|
-
if (m[1] && m[1].startsWith('/') && m[1].length > 1)
|
|
81
|
-
hits.push(m[1]);
|
|
82
|
-
}
|
|
83
|
-
// Only trust if we found >2 hits (reduces false positives)
|
|
84
|
-
if (hits.length > 2)
|
|
85
|
-
for (const h of hits)
|
|
86
|
-
found.push(h);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return found;
|
|
90
|
-
});
|
|
91
|
-
// Post-process: resolve to absolute, dedup, drop dynamic segments
|
|
92
|
-
const DYNAMIC = /[*:?]/;
|
|
93
|
-
const ASSET = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|map|json|xml)$/i;
|
|
94
|
-
const seen = new Set();
|
|
95
|
-
const result = [];
|
|
96
|
-
for (const r of raw) {
|
|
97
|
-
if (!r || typeof r !== 'string' || DYNAMIC.test(r) || ASSET.test(r))
|
|
98
|
-
continue;
|
|
99
|
-
const abs = r.startsWith('http') ? r : `${origin}${r.startsWith('/') ? '' : '/'}${r}`;
|
|
100
|
-
try {
|
|
101
|
-
const parsed = new URL(abs);
|
|
102
|
-
if (parsed.origin !== origin)
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
catch {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (!seen.has(abs)) {
|
|
109
|
-
seen.add(abs);
|
|
110
|
-
result.push(abs);
|
|
1
|
+
import { logger as rootLogger } from "./logger.js";
|
|
2
|
+
const logger = rootLogger.child({ module: "js-router-detector" });
|
|
3
|
+
async function detectJsRoutes(page, origin) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await page.evaluate(() => {
|
|
6
|
+
const found = [];
|
|
7
|
+
const windowGlobals = [
|
|
8
|
+
"__routes__",
|
|
9
|
+
"__ROUTER__",
|
|
10
|
+
"__vue_router__",
|
|
11
|
+
"__router__",
|
|
12
|
+
"router",
|
|
13
|
+
"_router",
|
|
14
|
+
"__reactRouter",
|
|
15
|
+
"__NEXT_DATA__"
|
|
16
|
+
];
|
|
17
|
+
for (const g of windowGlobals) {
|
|
18
|
+
try {
|
|
19
|
+
const val = window[g];
|
|
20
|
+
if (!val || typeof val !== "object") continue;
|
|
21
|
+
if (g === "__NEXT_DATA__" && Array.isArray(val.buildManifest?.pages)) {
|
|
22
|
+
for (const p of val.buildManifest.pages) if (typeof p === "string") found.push(p);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const routeArr = (Array.isArray(val.options?.routes) ? val.options.routes : null) ?? (Array.isArray(val.routes) ? val.routes : null) ?? (typeof val.getRoutes === "function" ? val.getRoutes() : null);
|
|
26
|
+
if (!Array.isArray(routeArr)) continue;
|
|
27
|
+
const flatRoutes = (arr, prefix = "") => {
|
|
28
|
+
for (const r of arr) {
|
|
29
|
+
if (!r || typeof r !== "object") continue;
|
|
30
|
+
const path = r.path ?? r.href ?? r.url ?? "";
|
|
31
|
+
const full = path.startsWith("/") ? path : `${prefix}/${path}`.replace(/\/\//g, "/");
|
|
32
|
+
if (full && full !== "/") found.push(full);
|
|
33
|
+
if (Array.isArray(r.children)) flatRoutes(r.children, full);
|
|
111
34
|
}
|
|
35
|
+
};
|
|
36
|
+
flatRoutes(routeArr);
|
|
37
|
+
} catch {
|
|
112
38
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
39
|
+
}
|
|
40
|
+
const routePatterns = [
|
|
41
|
+
/\bpath\s*:\s*['"](\/?[\w\-\/\.~@]+)['"]/g,
|
|
42
|
+
/\broute\s*\(\s*['"](\/?[\w\-\/\.~@]+)['"]/g,
|
|
43
|
+
/<Route[^>]+path\s*=\s*['"](\/?[\w\-\/\.~@]+)['"]/g,
|
|
44
|
+
/to\s*:\s*['"](\/?[\w\-\/\.~@]+)['"]/g
|
|
45
|
+
];
|
|
46
|
+
const scripts = Array.from(document.querySelectorAll("script:not([src])"));
|
|
47
|
+
for (const el of scripts.slice(0, 15)) {
|
|
48
|
+
const src = el.textContent ?? "";
|
|
49
|
+
if (src.length < 100 || src.length > 8e5) continue;
|
|
50
|
+
for (const pat of routePatterns) {
|
|
51
|
+
const hits = [];
|
|
52
|
+
let m;
|
|
53
|
+
pat.lastIndex = 0;
|
|
54
|
+
while ((m = pat.exec(src)) !== null && hits.length < 100) {
|
|
55
|
+
if (m[1] && m[1].startsWith("/") && m[1].length > 1) hits.push(m[1]);
|
|
56
|
+
}
|
|
57
|
+
if (hits.length > 2) for (const h of hits) found.push(h);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
});
|
|
62
|
+
const DYNAMIC = /[*:?]/;
|
|
63
|
+
const ASSET = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|map|json|xml)$/i;
|
|
64
|
+
const seen = /* @__PURE__ */ new Set();
|
|
65
|
+
const result = [];
|
|
66
|
+
for (const r of raw) {
|
|
67
|
+
if (!r || typeof r !== "string" || DYNAMIC.test(r) || ASSET.test(r)) continue;
|
|
68
|
+
const abs = r.startsWith("http") ? r : `${origin}${r.startsWith("/") ? "" : "/"}${r}`;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = new URL(abs);
|
|
71
|
+
if (parsed.origin !== origin) continue;
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!seen.has(abs)) {
|
|
76
|
+
seen.add(abs);
|
|
77
|
+
result.push(abs);
|
|
78
|
+
}
|
|
120
79
|
}
|
|
80
|
+
if (result.length > 0) logger.info({ count: result.length }, "[P2] JS router: routes detected");
|
|
81
|
+
return result;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger.warn({ err: String(err) }, "[P2] JS router detection failed \u2014 non-fatal");
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
121
86
|
}
|
|
87
|
+
export {
|
|
88
|
+
detectJsRoutes
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=js-router-detector.js.map
|
|
@@ -1,69 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Language detector — identify page language from HTML attributes and content analysis.
|
|
3
|
-
*
|
|
4
|
-
* Sources (in priority order):
|
|
5
|
-
* 1. <html lang="..."> attribute
|
|
6
|
-
* 2. <meta http-equiv="Content-Language"> header
|
|
7
|
-
* 3. hreflang alternate links
|
|
8
|
-
* 4. Content-based n-gram fingerprint (detects top 20 languages)
|
|
9
|
-
*
|
|
10
|
-
* Used to: route pages to the correct markdown processor, group screens by locale,
|
|
11
|
-
* and detect missing hreflang implementations.
|
|
12
|
-
*/
|
|
13
|
-
// Frequency signatures for n-gram based detection
|
|
14
|
-
// Common trigrams by language (simplified — not production-grade)
|
|
15
1
|
const LANG_SIGNATURES = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
en: /\b(the|and|for|are|but|not|you|all|can|had|her|was|one|our|out|day|get|has|him|his|how)\b/gi,
|
|
3
|
+
es: /\b(que|con|una|por|del|las|los|para|como|pero|más|este|esto|sus|ser|hay|sobre|entre|también)\b/gi,
|
|
4
|
+
fr: /\b(les|des|une|est|dans|pour|qui|par|sur|avec|que|pas|son|tout|mais|bien|comme|vous)\b/gi,
|
|
5
|
+
de: /\b(die|der|und|den|das|ein|ist|von|mit|dem|auf|sich|des|aber|auch|bei|aus|nach)\b/gi,
|
|
6
|
+
pt: /\b(que|não|uma|com|por|dos|para|como|mais|mas|seu|sua|nas|nos|ele|ela|são)\b/gi,
|
|
7
|
+
it: /\b(che|con|del|per|una|nel|non|sono|gli|dei|alla|delle|questa|questo|come|più)\b/gi,
|
|
8
|
+
nl: /\b(van|het|een|zijn|maar|ook|dan|door|op|uit|aan|bij|om|al|veel|haar|hun)\b/gi,
|
|
9
|
+
ja: /[-ゟ゠-ヿ]/g,
|
|
10
|
+
zh: /[一-鿿]/g,
|
|
11
|
+
ar: /[-ۿ]/g,
|
|
12
|
+
hi: /[ऀ-ॿ]/g,
|
|
13
|
+
ko: /[가-ᄀ-ᇿ]/g
|
|
28
14
|
};
|
|
29
15
|
function detectFromContent(text) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
best = lang;
|
|
40
|
-
}
|
|
16
|
+
if (!text || text.length < 50) return "en";
|
|
17
|
+
let best = "en";
|
|
18
|
+
let bestScore = 0;
|
|
19
|
+
for (const [lang, re] of Object.entries(LANG_SIGNATURES)) {
|
|
20
|
+
const matches = (text.match(re) ?? []).length;
|
|
21
|
+
const score = matches / Math.max(text.length / 100, 1);
|
|
22
|
+
if (score > bestScore) {
|
|
23
|
+
bestScore = score;
|
|
24
|
+
best = lang;
|
|
41
25
|
}
|
|
42
|
-
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
43
28
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
29
|
+
async function detectLanguage(page, url) {
|
|
30
|
+
const data = await page.evaluate(() => {
|
|
31
|
+
const htmlLang = document.documentElement.lang?.toLowerCase().split("-")[0] ?? "";
|
|
32
|
+
const metaLang = document.querySelector('meta[http-equiv="Content-Language"]')?.content?.toLowerCase().split("-")[0] ?? "";
|
|
33
|
+
const alternates = {};
|
|
34
|
+
document.querySelectorAll('link[rel="alternate"][hreflang]').forEach((el) => {
|
|
35
|
+
const lang = el.getAttribute("hreflang")?.toLowerCase();
|
|
36
|
+
const href = el.getAttribute("href") ?? "";
|
|
37
|
+
if (lang && lang !== "x-default") alternates[lang] = href;
|
|
38
|
+
});
|
|
39
|
+
const bodyText = (document.body?.innerText ?? "").slice(0, 2e3);
|
|
40
|
+
return { htmlLang, metaLang, alternates, bodyText };
|
|
41
|
+
}).catch(() => ({ htmlLang: "", metaLang: "", alternates: {}, bodyText: "" }));
|
|
42
|
+
const declaredLang = data.htmlLang || data.metaLang || "";
|
|
43
|
+
const detectedLang = detectFromContent(data.bodyText);
|
|
44
|
+
const confidence = declaredLang ? "high" : data.bodyText.length > 200 ? "medium" : "low";
|
|
45
|
+
return {
|
|
46
|
+
url,
|
|
47
|
+
declaredLang,
|
|
48
|
+
detectedLang: declaredLang || detectedLang,
|
|
49
|
+
confidence,
|
|
50
|
+
alternates: data.alternates,
|
|
51
|
+
isMultilingual: Object.keys(data.alternates).length > 1
|
|
52
|
+
};
|
|
69
53
|
}
|
|
54
|
+
export {
|
|
55
|
+
detectLanguage
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=language-detector.js.map
|
package/dist/link-classifier.js
CHANGED
|
@@ -1,58 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Link classifier — assigns priority and category to discovered URLs.
|
|
3
|
-
*
|
|
4
|
-
* Categories:
|
|
5
|
-
* - 'nav' : site navigation (homepage, top-level pages) — highest priority
|
|
6
|
-
* - 'auth' : login/register/password flows — high priority
|
|
7
|
-
* - 'app' : authenticated app screens — high priority
|
|
8
|
-
* - 'content' : blog/docs/articles — medium priority
|
|
9
|
-
* - 'api' : REST/GraphQL API endpoints — detected, not crawled as screens
|
|
10
|
-
* - 'resource' : static assets (JS/CSS/img/fonts) — skip
|
|
11
|
-
* - 'external' : different domain — skip unless cross-domain crawl enabled
|
|
12
|
-
* - 'low-value' : pagination, sort, filter query strings — deprioritize
|
|
13
|
-
*/
|
|
14
1
|
const RESOURCE_EXT = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|mp4|mp3|pdf|zip|gz|tar|exe|dmg|apk)(\?|$)/i;
|
|
15
2
|
const API_PATTERN = /\/(api|graphql|gql|rest|v\d+)\//i;
|
|
16
3
|
const AUTH_PATTERN = /\/(login|signin|sign-in|register|signup|sign-up|auth|oauth|sso|password|forgot|reset|logout|signout)\b/i;
|
|
17
4
|
const LOW_VALUE_PATTERN = /[?&](page|p|offset|sort|order|filter|q|search|tag|category|color|size|brand)=/i;
|
|
18
5
|
const PAGINATION_PATTERN = /\/page\/\d+|\/p\/\d+|\bpage=\d+/i;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (LOW_VALUE_PATTERN.test(path))
|
|
42
|
-
return { url, category: 'low-value', priority: 20, reason: 'filter/sort query param' };
|
|
43
|
-
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
44
|
-
if (segments.length === 0)
|
|
45
|
-
return { url, category: 'nav', priority: 100, reason: 'homepage' };
|
|
46
|
-
if (segments.length === 1)
|
|
47
|
-
return { url, category: 'nav', priority: 80, reason: 'top-level nav' };
|
|
48
|
-
if (segments.length <= 3)
|
|
49
|
-
return { url, category: 'app', priority: 60, reason: 'app screen' };
|
|
50
|
-
return { url, category: 'content', priority: 40, reason: 'deep content page' };
|
|
6
|
+
function classifyLink(url, baseUrl) {
|
|
7
|
+
let parsed;
|
|
8
|
+
let baseHost;
|
|
9
|
+
try {
|
|
10
|
+
parsed = new URL(url);
|
|
11
|
+
baseHost = new URL(baseUrl).hostname;
|
|
12
|
+
} catch {
|
|
13
|
+
return { url, category: "low-value", priority: 0, reason: "invalid URL" };
|
|
14
|
+
}
|
|
15
|
+
const path = parsed.pathname + parsed.search;
|
|
16
|
+
const isExternal = parsed.hostname !== baseHost;
|
|
17
|
+
if (isExternal) return { url, category: "external", priority: 0, reason: "different domain" };
|
|
18
|
+
if (RESOURCE_EXT.test(path)) return { url, category: "resource", priority: 0, reason: "static asset extension" };
|
|
19
|
+
if (API_PATTERN.test(path)) return { url, category: "api", priority: 10, reason: "API path pattern" };
|
|
20
|
+
if (AUTH_PATTERN.test(path)) return { url, category: "auth", priority: 90, reason: "auth flow pattern" };
|
|
21
|
+
if (PAGINATION_PATTERN.test(path)) return { url, category: "low-value", priority: 15, reason: "pagination" };
|
|
22
|
+
if (LOW_VALUE_PATTERN.test(path)) return { url, category: "low-value", priority: 20, reason: "filter/sort query param" };
|
|
23
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
24
|
+
if (segments.length === 0) return { url, category: "nav", priority: 100, reason: "homepage" };
|
|
25
|
+
if (segments.length === 1) return { url, category: "nav", priority: 80, reason: "top-level nav" };
|
|
26
|
+
if (segments.length <= 3) return { url, category: "app", priority: 60, reason: "app screen" };
|
|
27
|
+
return { url, category: "content", priority: 40, reason: "deep content page" };
|
|
51
28
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.sort((a, b) => b.priority - a.priority);
|
|
29
|
+
function classifyLinks(urls, baseUrl) {
|
|
30
|
+
return urls.map((u) => classifyLink(u, baseUrl)).sort((a, b) => b.priority - a.priority);
|
|
55
31
|
}
|
|
56
|
-
|
|
57
|
-
|
|
32
|
+
function filterCrawlable(classified) {
|
|
33
|
+
return classified.filter((l) => l.category !== "resource" && l.category !== "external" && l.category !== "api");
|
|
58
34
|
}
|
|
35
|
+
export {
|
|
36
|
+
classifyLink,
|
|
37
|
+
classifyLinks,
|
|
38
|
+
filterCrawlable
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=link-classifier.js.map
|