@detiq/crawler 0.1.3
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/Dockerfile +49 -0
- package/README.md +11 -0
- package/dist/accessibility-auditor.d.ts +29 -0
- package/dist/accessibility-auditor.js +57 -0
- package/dist/accessibility-auditor.js.map +7 -0
- package/dist/ai-login.d.ts +32 -0
- package/dist/ai-login.js +177 -0
- package/dist/ai-login.js.map +7 -0
- package/dist/auth-token-rotator.d.ts +47 -0
- package/dist/auth-token-rotator.js +58 -0
- package/dist/auth-token-rotator.js.map +7 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +329 -0
- package/dist/auth.js.map +7 -0
- package/dist/browser-extension.d.ts +37 -0
- package/dist/browser-extension.js +113 -0
- package/dist/browser-extension.js.map +7 -0
- package/dist/captcha-solver.d.ts +16 -0
- package/dist/captcha-solver.js +105 -0
- package/dist/captcha-solver.js.map +7 -0
- package/dist/color-contrast.d.ts +30 -0
- package/dist/color-contrast.js +78 -0
- package/dist/color-contrast.js.map +7 -0
- package/dist/content-chunker.d.ts +18 -0
- package/dist/content-chunker.js +152 -0
- package/dist/content-chunker.js.map +7 -0
- package/dist/content-dedup.d.ts +23 -0
- package/dist/content-dedup.js +60 -0
- package/dist/content-dedup.js.map +7 -0
- package/dist/cookie-consent.d.ts +25 -0
- package/dist/cookie-consent.js +78 -0
- package/dist/cookie-consent.js.map +7 -0
- package/dist/cookie-jar.d.ts +20 -0
- package/dist/cookie-jar.js +61 -0
- package/dist/cookie-jar.js.map +7 -0
- package/dist/crawler.d.ts +179 -0
- package/dist/crawler.js +3672 -0
- package/dist/crawler.js.map +7 -0
- package/dist/custom-js-injector.d.ts +42 -0
- package/dist/custom-js-injector.js +50 -0
- package/dist/custom-js-injector.js.map +7 -0
- package/dist/dark-mode-capture.d.ts +23 -0
- package/dist/dark-mode-capture.js +75 -0
- package/dist/dark-mode-capture.js.map +7 -0
- package/dist/dead-link-detector.d.ts +37 -0
- package/dist/dead-link-detector.js +50 -0
- package/dist/dead-link-detector.js.map +7 -0
- package/dist/element-hash.d.ts +9 -0
- package/dist/element-hash.js +9 -0
- package/dist/element-hash.js.map +7 -0
- package/dist/email-otp.d.ts +42 -0
- package/dist/email-otp.js +177 -0
- package/dist/email-otp.js.map +7 -0
- package/dist/engine-router.d.ts +47 -0
- package/dist/engine-router.js +94 -0
- package/dist/engine-router.js.map +7 -0
- package/dist/export/bulk-export.d.ts +26 -0
- package/dist/export/bulk-export.js +76 -0
- package/dist/export/bulk-export.js.map +7 -0
- package/dist/export/vector-export.d.ts +76 -0
- package/dist/export/vector-export.js +85 -0
- package/dist/export/vector-export.js.map +7 -0
- package/dist/extractors/advanced.d.ts +54 -0
- package/dist/extractors/advanced.js +245 -0
- package/dist/extractors/advanced.js.map +7 -0
- package/dist/extractors/artifact-envelope.d.ts +63 -0
- package/dist/extractors/artifact-envelope.js +63 -0
- package/dist/extractors/artifact-envelope.js.map +7 -0
- package/dist/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +40 -0
- package/dist/failure-analysis-agent.js.map +7 -0
- package/dist/feature-flag-detector.d.ts +30 -0
- package/dist/feature-flag-detector.js +55 -0
- package/dist/feature-flag-detector.js.map +7 -0
- package/dist/form-detector.d.ts +34 -0
- package/dist/form-detector.js +58 -0
- package/dist/form-detector.js.map +7 -0
- package/dist/frontier.d.ts +32 -0
- package/dist/frontier.js +83 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +35 -0
- package/dist/gap-pass.js +81 -0
- package/dist/gap-pass.js.map +7 -0
- package/dist/graphql-detector.d.ts +39 -0
- package/dist/graphql-detector.js +86 -0
- package/dist/graphql-detector.js.map +7 -0
- package/dist/http-engine.d.ts +80 -0
- package/dist/http-engine.js +304 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +59 -0
- package/dist/incremental-diff.js +61 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +7 -0
- package/dist/input-fuzzer.d.ts +30 -0
- package/dist/input-fuzzer.js +105 -0
- package/dist/input-fuzzer.js.map +7 -0
- package/dist/interactive-probe.d.ts +104 -0
- package/dist/interactive-probe.js +442 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +171 -0
- package/dist/job-queue.js +2256 -0
- package/dist/job-queue.js.map +7 -0
- package/dist/js-coverage.d.ts +29 -0
- package/dist/js-coverage.js +56 -0
- package/dist/js-coverage.js.map +7 -0
- package/dist/js-router-detector.d.ts +17 -0
- package/dist/js-router-detector.js +90 -0
- package/dist/js-router-detector.js.map +7 -0
- package/dist/language-detector.d.ts +22 -0
- package/dist/language-detector.js +57 -0
- package/dist/language-detector.js.map +7 -0
- package/dist/link-classifier.d.ts +23 -0
- package/dist/link-classifier.js +40 -0
- package/dist/link-classifier.js.map +7 -0
- package/dist/live-view.d.ts +36 -0
- package/dist/live-view.js +100 -0
- package/dist/live-view.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +10 -0
- package/dist/logger.js.map +7 -0
- package/dist/markdown-extract.d.ts +8 -0
- package/dist/markdown-extract.js +54 -0
- package/dist/markdown-extract.js.map +7 -0
- package/dist/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.js.map +7 -0
- package/dist/network-observer.d.ts +49 -0
- package/dist/network-observer.js +149 -0
- package/dist/network-observer.js.map +7 -0
- package/dist/network-throttle.d.ts +25 -0
- package/dist/network-throttle.js +34 -0
- package/dist/network-throttle.js.map +7 -0
- package/dist/openapi-generator.d.ts +49 -0
- package/dist/openapi-generator.js +89 -0
- package/dist/openapi-generator.js.map +7 -0
- package/dist/pdf-extractor.d.ts +28 -0
- package/dist/pdf-extractor.js +81 -0
- package/dist/pdf-extractor.js.map +7 -0
- package/dist/performance-metrics.d.ts +53 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +38 -0
- package/dist/phash.js +115 -0
- package/dist/phash.js.map +7 -0
- package/dist/pii-scrubber.d.ts +21 -0
- package/dist/pii-scrubber.js +75 -0
- package/dist/pii-scrubber.js.map +7 -0
- package/dist/playwright-recorder.d.ts +39 -0
- package/dist/playwright-recorder.js +123 -0
- package/dist/playwright-recorder.js.map +7 -0
- package/dist/policies/crawl-safety.d.ts +9 -0
- package/dist/policies/crawl-safety.js +37 -0
- package/dist/policies/crawl-safety.js.map +7 -0
- package/dist/profiles/domain-profile.d.ts +19 -0
- package/dist/profiles/domain-profile.js +140 -0
- package/dist/profiles/domain-profile.js.map +7 -0
- package/dist/proxy.d.ts +50 -0
- package/dist/proxy.js +165 -0
- package/dist/proxy.js.map +7 -0
- package/dist/rate-limiter.d.ts +31 -0
- package/dist/rate-limiter.js +64 -0
- package/dist/rate-limiter.js.map +7 -0
- package/dist/redirect-tracker.d.ts +37 -0
- package/dist/redirect-tracker.js +65 -0
- package/dist/redirect-tracker.js.map +7 -0
- package/dist/redis-frontier.d.ts +48 -0
- package/dist/redis-frontier.js +126 -0
- package/dist/redis-frontier.js.map +7 -0
- package/dist/redis-queue.d.ts +48 -0
- package/dist/redis-queue.js +165 -0
- package/dist/redis-queue.js.map +7 -0
- package/dist/response-cache.d.ts +38 -0
- package/dist/response-cache.js +140 -0
- package/dist/response-cache.js.map +7 -0
- package/dist/responsive-detector.d.ts +29 -0
- package/dist/responsive-detector.js +46 -0
- package/dist/responsive-detector.js.map +7 -0
- package/dist/retry-policy.d.ts +20 -0
- package/dist/retry-policy.js +54 -0
- package/dist/retry-policy.js.map +7 -0
- package/dist/robots-txt.d.ts +9 -0
- package/dist/robots-txt.js +77 -0
- package/dist/robots-txt.js.map +7 -0
- package/dist/screen-reader.d.ts +16 -0
- package/dist/screen-reader.js +46 -0
- package/dist/screen-reader.js.map +7 -0
- package/dist/screenshot-masker.d.ts +28 -0
- package/dist/screenshot-masker.js +62 -0
- package/dist/screenshot-masker.js.map +7 -0
- package/dist/seo-checker.d.ts +33 -0
- package/dist/seo-checker.js +91 -0
- package/dist/seo-checker.js.map +7 -0
- package/dist/session-capture.d.ts +35 -0
- package/dist/session-capture.js +267 -0
- package/dist/session-capture.js.map +7 -0
- package/dist/session-export.d.ts +49 -0
- package/dist/session-export.js +117 -0
- package/dist/session-export.js.map +7 -0
- package/dist/sitemap-writer.d.ts +21 -0
- package/dist/sitemap-writer.js +47 -0
- package/dist/sitemap-writer.js.map +7 -0
- package/dist/sitemap.d.ts +51 -0
- package/dist/sitemap.js +152 -0
- package/dist/sitemap.js.map +7 -0
- package/dist/skyvern.d.ts +56 -0
- package/dist/skyvern.js +129 -0
- package/dist/skyvern.js.map +7 -0
- package/dist/sqs-queue.d.ts +2 -0
- package/dist/sqs-queue.js +47 -0
- package/dist/sqs-queue.js.map +7 -0
- package/dist/sso-token.d.ts +29 -0
- package/dist/sso-token.js +76 -0
- package/dist/sso-token.js.map +7 -0
- package/dist/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +168 -0
- package/dist/stealth/human-behavior.js.map +7 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +77 -0
- package/dist/telemetry.js.map +7 -0
- package/dist/test-executor.d.ts +67 -0
- package/dist/test-executor.js +311 -0
- package/dist/test-executor.js.map +7 -0
- package/dist/tls-fingerprint.d.ts +50 -0
- package/dist/tls-fingerprint.js +81 -0
- package/dist/tls-fingerprint.js.map +7 -0
- package/dist/url-pattern-completer.d.ts +28 -0
- package/dist/url-pattern-completer.js +87 -0
- package/dist/url-pattern-completer.js.map +7 -0
- package/dist/visual-diff-verdict.d.ts +40 -0
- package/dist/visual-diff-verdict.js +25 -0
- package/dist/visual-diff-verdict.js.map +7 -0
- package/dist/webhook-notifier.d.ts +30 -0
- package/dist/webhook-notifier.js +76 -0
- package/dist/webhook-notifier.js.map +7 -0
- package/dist/websocket-detector.d.ts +24 -0
- package/dist/websocket-detector.js +60 -0
- package/dist/websocket-detector.js.map +7 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +33 -0
- package/dist/worker.js.map +7 -0
- package/package.json +45 -0
- package/src/accessibility-auditor.ts +102 -0
- package/src/ai-login.ts +218 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +396 -0
- package/src/browser-extension.ts +186 -0
- package/src/captcha-solver.ts +143 -0
- package/src/color-contrast.ts +116 -0
- package/src/content-chunker.ts +170 -0
- package/src/content-dedup.ts +99 -0
- package/src/cookie-consent.ts +106 -0
- package/src/cookie-jar.ts +91 -0
- package/src/crawler.ts +4305 -0
- package/src/custom-js-injector.ts +93 -0
- package/src/dark-mode-capture.ts +110 -0
- package/src/dead-link-detector.ts +76 -0
- package/src/element-hash.ts +16 -0
- package/src/email-otp.ts +252 -0
- package/src/engine-router.ts +141 -0
- package/src/export/bulk-export.ts +85 -0
- package/src/export/vector-export.ts +160 -0
- package/src/extractors/advanced.ts +283 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/failure-analysis-agent.ts +49 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +112 -0
- package/src/gap-pass.ts +122 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +433 -0
- package/src/incremental-diff.ts +121 -0
- package/src/index.ts +35 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +623 -0
- package/src/job-queue.test.ts +120 -0
- package/src/job-queue.ts +2701 -0
- package/src/js-coverage.ts +87 -0
- package/src/js-router-detector.ts +112 -0
- package/src/language-detector.ts +82 -0
- package/src/link-classifier.ts +65 -0
- package/src/live-view.ts +141 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/native-crawler.ts +1106 -0
- package/src/network-observer.ts +204 -0
- package/src/network-throttle.ts +69 -0
- package/src/openapi-generator.ts +135 -0
- package/src/pdf-extractor.ts +123 -0
- package/src/performance-metrics.ts +134 -0
- package/src/phash.ts +151 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +46 -0
- package/src/profiles/domain-profile.ts +167 -0
- package/src/proxy.ts +238 -0
- package/src/rate-limiter.ts +87 -0
- package/src/redirect-tracker.ts +97 -0
- package/src/redis-frontier.ts +143 -0
- package/src/redis-queue.ts +208 -0
- package/src/response-cache.ts +150 -0
- package/src/responsive-detector.ts +92 -0
- package/src/retry-policy.ts +87 -0
- package/src/robots-txt.ts +96 -0
- package/src/screen-reader.ts +89 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/session-capture.ts +325 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +131 -0
- package/src/sitemap.ts +224 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +49 -0
- package/src/sso-token.ts +120 -0
- package/src/stealth/human-behavior.ts +191 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +424 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +114 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +57 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +48 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -0
- package/types/ai-login.d.ts +32 -0
- package/types/auth-token-rotator.d.ts +47 -0
- package/types/auth.d.ts +20 -0
- package/types/browser-extension.d.ts +37 -0
- package/types/bull-worker.d.ts +6 -0
- package/types/captcha-solver.d.ts +16 -0
- package/types/color-contrast.d.ts +30 -0
- package/types/content-chunker.d.ts +18 -0
- package/types/content-dedup.d.ts +23 -0
- package/types/cookie-consent.d.ts +25 -0
- package/types/cookie-jar.d.ts +20 -0
- package/types/crawler.d.ts +179 -0
- package/types/custom-js-injector.d.ts +42 -0
- package/types/dark-mode-capture.d.ts +23 -0
- package/types/dead-link-detector.d.ts +37 -0
- package/types/element-hash.d.ts +9 -0
- package/types/email-otp.d.ts +42 -0
- package/types/engine-router.d.ts +47 -0
- package/types/export/bulk-export.d.ts +26 -0
- package/types/export/vector-export.d.ts +76 -0
- package/types/extractors/advanced.d.ts +54 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +32 -0
- package/types/gap-pass.d.ts +35 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +59 -0
- package/types/index.d.ts +35 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +104 -0
- package/types/job-queue.d.ts +171 -0
- package/types/js-coverage.d.ts +29 -0
- package/types/js-router-detector.d.ts +17 -0
- package/types/language-detector.d.ts +22 -0
- package/types/link-classifier.d.ts +23 -0
- package/types/live-view.d.ts +36 -0
- package/types/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/native-crawler.d.ts +69 -0
- package/types/network-observer.d.ts +49 -0
- package/types/network-throttle.d.ts +25 -0
- package/types/openapi-generator.d.ts +49 -0
- package/types/pdf-extractor.d.ts +28 -0
- package/types/performance-metrics.d.ts +53 -0
- package/types/phash.d.ts +38 -0
- package/types/pii-scrubber.d.ts +21 -0
- package/types/playwright-recorder.d.ts +39 -0
- package/types/policies/crawl-safety.d.ts +9 -0
- package/types/profiles/domain-profile.d.ts +19 -0
- package/types/proxy.d.ts +50 -0
- package/types/rate-limiter.d.ts +31 -0
- package/types/redirect-tracker.d.ts +37 -0
- package/types/redis-frontier.d.ts +48 -0
- package/types/redis-queue.d.ts +48 -0
- package/types/response-cache.d.ts +38 -0
- package/types/responsive-detector.d.ts +29 -0
- package/types/retry-policy.d.ts +20 -0
- package/types/robots-txt.d.ts +9 -0
- package/types/screen-reader.d.ts +16 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/session-capture.d.ts +38 -0
- package/types/session-export.d.ts +49 -0
- package/types/sitemap-writer.d.ts +21 -0
- package/types/sitemap.d.ts +51 -0
- package/types/skyvern.d.ts +56 -0
- package/types/sqs-queue.d.ts +2 -0
- package/types/sso-token.d.ts +29 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +67 -0
- package/types/tls-fingerprint.d.ts +50 -0
- package/types/url-pattern-completer.d.ts +28 -0
- package/types/visual-diff-verdict.d.ts +40 -0
- package/types/webhook-notifier.d.ts +30 -0
- package/types/websocket-detector.d.ts +24 -0
- package/types/worker.d.ts +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
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);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
flatRoutes(routeArr);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
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
|
+
}
|
|
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
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
detectJsRoutes
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=js-router-detector.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/js-router-detector.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * P2: JS Router Detection\n *\n * Scans the loaded page for client-side route tables declared by React Router v6,\n * Vue Router, Angular Router, or custom window globals. Supplements the existing\n * history.pushState interception (which only captures runtime navigation) with a\n * static analysis of routes declared in the JS bundle.\n *\n * Called once per page after goto(). Runs entirely in-browser via page.evaluate().\n * Adds discovered routes to the BFS frontier so the crawler visits them.\n */\n\nimport { logger as rootLogger } from './logger.js';\n\nconst logger = rootLogger.child({ module: 'js-router-detector' });\n\n/**\n * Extract absolute page URLs from the current page's JS router config.\n * Returns deduplicated absolute URLs (same origin). Dynamic segments (:id, *)\n * are skipped \u2014 the crawler can't resolve them without knowing valid IDs.\n */\nexport async function detectJsRoutes(page: any, origin: string): Promise<string[]> {\n try {\n const raw: string[] = await page.evaluate(() => {\n const found: string[] = [];\n\n // 1. Check common framework globals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const windowGlobals = [\n '__routes__', '__ROUTER__', '__vue_router__', '__router__',\n 'router', '_router', '__reactRouter', '__NEXT_DATA__',\n ];\n for (const g of windowGlobals) {\n try {\n const val = (window as any)[g];\n if (!val || typeof val !== 'object') continue;\n\n // Next.js page manifest\n if (g === '__NEXT_DATA__' && Array.isArray(val.buildManifest?.pages)) {\n for (const p of val.buildManifest.pages) if (typeof p === 'string') found.push(p);\n continue;\n }\n\n // Router objects: options.routes, routes, getRoutes()\n const routeArr =\n (Array.isArray(val.options?.routes) ? val.options.routes : null) ??\n (Array.isArray(val.routes) ? val.routes : null) ??\n (typeof val.getRoutes === 'function' ? val.getRoutes() : null);\n\n if (!Array.isArray(routeArr)) continue;\n\n const flatRoutes = (arr: any[], prefix = ''): void => {\n for (const r of arr) {\n if (!r || typeof r !== 'object') continue;\n const path: string = r.path ?? r.href ?? r.url ?? '';\n const full = path.startsWith('/') ? path : `${prefix}/${path}`.replace(/\\/\\//g, '/');\n if (full && full !== '/') found.push(full);\n if (Array.isArray(r.children)) flatRoutes(r.children, full);\n }\n };\n flatRoutes(routeArr);\n } catch { /* skip corrupt globals */ }\n }\n\n // 2. Scan inline <script> content for route patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Two-pass: regex first, if >3 found trust them. Below 3 \u2192 skip (too noisy).\n const routePatterns = [\n /\\bpath\\s*:\\s*['\"](\\/?[\\w\\-\\/\\.~@]+)['\"]/g,\n /\\broute\\s*\\(\\s*['\"](\\/?[\\w\\-\\/\\.~@]+)['\"]/g,\n /<Route[^>]+path\\s*=\\s*['\"](\\/?[\\w\\-\\/\\.~@]+)['\"]/g,\n /to\\s*:\\s*['\"](\\/?[\\w\\-\\/\\.~@]+)['\"]/g,\n ];\n const scripts = Array.from(document.querySelectorAll('script:not([src])'));\n for (const el of scripts.slice(0, 15)) {\n const src = el.textContent ?? '';\n if (src.length < 100 || src.length > 800_000) continue;\n for (const pat of routePatterns) {\n const hits: string[] = [];\n let m: RegExpExecArray | null;\n pat.lastIndex = 0;\n while ((m = pat.exec(src)) !== null && hits.length < 100) {\n if (m[1] && m[1].startsWith('/') && m[1].length > 1) hits.push(m[1]);\n }\n // Only trust if we found >2 hits (reduces false positives)\n if (hits.length > 2) for (const h of hits) found.push(h);\n }\n }\n\n return found;\n });\n\n // Post-process: resolve to absolute, dedup, drop dynamic segments\n const DYNAMIC = /[*:?]/;\n const ASSET = /\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|map|json|xml)$/i;\n const seen = new Set<string>();\n const result: string[] = [];\n for (const r of raw) {\n if (!r || typeof r !== 'string' || DYNAMIC.test(r) || ASSET.test(r)) continue;\n const abs = r.startsWith('http') ? r : `${origin}${r.startsWith('/') ? '' : '/'}${r}`;\n try {\n const parsed = new URL(abs);\n if (parsed.origin !== origin) continue;\n } catch { continue; }\n if (!seen.has(abs)) { seen.add(abs); result.push(abs); }\n }\n\n if (result.length > 0) logger.info({ count: result.length }, '[P2] JS router: routes detected');\n return result;\n } catch (err) {\n logger.warn({ err: String(err) }, '[P2] JS router detection failed \u2014 non-fatal');\n return [];\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
import type { Page } from 'playwright';
|
|
14
|
+
export interface LanguageDetectionResult {
|
|
15
|
+
url: string;
|
|
16
|
+
declaredLang: string;
|
|
17
|
+
detectedLang: string;
|
|
18
|
+
confidence: 'high' | 'medium' | 'low';
|
|
19
|
+
alternates: Record<string, string>;
|
|
20
|
+
isMultilingual: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function detectLanguage(page: Page, url: string): Promise<LanguageDetectionResult>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const LANG_SIGNATURES = {
|
|
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
|
|
14
|
+
};
|
|
15
|
+
function detectFromContent(text) {
|
|
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;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return best;
|
|
28
|
+
}
|
|
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
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
detectLanguage
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=language-detector.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/language-detector.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Language detector \u2014 identify page language from HTML attributes and content analysis.\n *\n * Sources (in priority order):\n * 1. <html lang=\"...\"> attribute\n * 2. <meta http-equiv=\"Content-Language\"> header\n * 3. hreflang alternate links\n * 4. Content-based n-gram fingerprint (detects top 20 languages)\n *\n * Used to: route pages to the correct markdown processor, group screens by locale,\n * and detect missing hreflang implementations.\n */\n\nimport type { Page } from 'playwright';\n\n// Frequency signatures for n-gram based detection\n// Common trigrams by language (simplified \u2014 not production-grade)\nconst LANG_SIGNATURES: Record<string, RegExp> = {\n 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,\n es: /\\b(que|con|una|por|del|las|los|para|como|pero|m\u00E1s|este|esto|sus|ser|hay|sobre|entre|tambi\u00E9n)\\b/gi,\n fr: /\\b(les|des|une|est|dans|pour|qui|par|sur|avec|que|pas|son|tout|mais|bien|comme|vous)\\b/gi,\n de: /\\b(die|der|und|den|das|ein|ist|von|mit|dem|auf|sich|des|aber|auch|bei|aus|nach)\\b/gi,\n pt: /\\b(que|n\u00E3o|uma|com|por|dos|para|como|mais|mas|seu|sua|nas|nos|ele|ela|s\u00E3o)\\b/gi,\n it: /\\b(che|con|del|per|una|nel|non|sono|gli|dei|alla|delle|questa|questo|come|pi\u00F9)\\b/gi,\n nl: /\\b(van|het|een|zijn|maar|ook|dan|door|op|uit|aan|bij|om|al|veel|haar|hun)\\b/gi,\n ja: /[\u3040-\u309F\u30A0-\u30FF]/g,\n zh: /[\u4E00-\u9FFF]/g,\n ar: /[\u0600-\u06FF]/g,\n hi: /[\u0900-\u097F]/g,\n ko: /[\uAC00-\uD7AF\u1100-\u11FF]/g,\n};\n\nfunction detectFromContent(text: string): string {\n if (!text || text.length < 50) return 'en';\n let best = 'en';\n let bestScore = 0;\n for (const [lang, re] of Object.entries(LANG_SIGNATURES)) {\n const matches = (text.match(re) ?? []).length;\n const score = matches / Math.max(text.length / 100, 1);\n if (score > bestScore) { bestScore = score; best = lang; }\n }\n return best;\n}\n\nexport interface LanguageDetectionResult {\n url: string;\n declaredLang: string; // from <html lang> or meta\n detectedLang: string; // from content analysis\n confidence: 'high' | 'medium' | 'low';\n alternates: Record<string, string>; // lang \u2192 url from hreflang\n isMultilingual: boolean;\n}\n\nexport async function detectLanguage(page: Page, url: string): Promise<LanguageDetectionResult> {\n const data = await page.evaluate(() => {\n const htmlLang = document.documentElement.lang?.toLowerCase().split('-')[0] ?? '';\n const metaLang = (document.querySelector('meta[http-equiv=\"Content-Language\"]') as any)?.content?.toLowerCase().split('-')[0] ?? '';\n const alternates: Record<string, string> = {};\n document.querySelectorAll('link[rel=\"alternate\"][hreflang]').forEach((el: any) => {\n const lang = el.getAttribute('hreflang')?.toLowerCase();\n const href = el.getAttribute('href') ?? '';\n if (lang && lang !== 'x-default') alternates[lang] = href;\n });\n const bodyText = (document.body?.innerText ?? '').slice(0, 2000);\n return { htmlLang, metaLang, alternates, bodyText };\n }).catch(() => ({ htmlLang: '', metaLang: '', alternates: {}, bodyText: '' }));\n\n const declaredLang = data.htmlLang || data.metaLang || '';\n const detectedLang = detectFromContent(data.bodyText);\n\n const confidence: LanguageDetectionResult['confidence'] =\n declaredLang ? 'high' : data.bodyText.length > 200 ? 'medium' : 'low';\n\n return {\n url,\n declaredLang,\n detectedLang: declaredLang || detectedLang,\n confidence,\n alternates: data.alternates,\n isMultilingual: Object.keys(data.alternates).length > 1,\n };\n}\n"],
|
|
5
|
+
"mappings": "AAiBA,MAAM,kBAA0C;AAAA,EAC9C,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,SAAS,kBAAkB,MAAsB;AAC/C,MAAI,CAAC,QAAQ,KAAK,SAAS,GAAI,QAAO;AACtC,MAAI,OAAO;AACX,MAAI,YAAY;AAChB,aAAW,CAAC,MAAM,EAAE,KAAK,OAAO,QAAQ,eAAe,GAAG;AACxD,UAAM,WAAW,KAAK,MAAM,EAAE,KAAK,CAAC,GAAG;AACvC,UAAM,QAAQ,UAAU,KAAK,IAAI,KAAK,SAAS,KAAK,CAAC;AACrD,QAAI,QAAQ,WAAW;AAAE,kBAAY;AAAO,aAAO;AAAA,IAAM;AAAA,EAC3D;AACA,SAAO;AACT;AAWA,eAAsB,eAAe,MAAY,KAA+C;AAC9F,QAAM,OAAO,MAAM,KAAK,SAAS,MAAM;AACrC,UAAM,WAAW,SAAS,gBAAgB,MAAM,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK;AAC/E,UAAM,WAAY,SAAS,cAAc,qCAAqC,GAAW,SAAS,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK;AACjI,UAAM,aAAqC,CAAC;AAC5C,aAAS,iBAAiB,iCAAiC,EAAE,QAAQ,CAAC,OAAY;AAChF,YAAM,OAAO,GAAG,aAAa,UAAU,GAAG,YAAY;AACtD,YAAM,OAAO,GAAG,aAAa,MAAM,KAAK;AACxC,UAAI,QAAQ,SAAS,YAAa,YAAW,IAAI,IAAI;AAAA,IACvD,CAAC;AACD,UAAM,YAAY,SAAS,MAAM,aAAa,IAAI,MAAM,GAAG,GAAI;AAC/D,WAAO,EAAE,UAAU,UAAU,YAAY,SAAS;AAAA,EACpD,CAAC,EAAE,MAAM,OAAO,EAAE,UAAU,IAAI,UAAU,IAAI,YAAY,CAAC,GAAG,UAAU,GAAG,EAAE;AAE7E,QAAM,eAAe,KAAK,YAAY,KAAK,YAAY;AACvD,QAAM,eAAe,kBAAkB,KAAK,QAAQ;AAEpD,QAAM,aACJ,eAAe,SAAS,KAAK,SAAS,SAAS,MAAM,WAAW;AAElE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,cAAc,gBAAgB;AAAA,IAC9B;AAAA,IACA,YAAY,KAAK;AAAA,IACjB,gBAAgB,OAAO,KAAK,KAAK,UAAU,EAAE,SAAS;AAAA,EACxD;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
export type LinkCategory = 'nav' | 'auth' | 'app' | 'content' | 'api' | 'resource' | 'external' | 'low-value';
|
|
15
|
+
export interface ClassifiedLink {
|
|
16
|
+
url: string;
|
|
17
|
+
category: LinkCategory;
|
|
18
|
+
priority: number;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function classifyLink(url: string, baseUrl: string): ClassifiedLink;
|
|
22
|
+
export declare function classifyLinks(urls: string[], baseUrl: string): ClassifiedLink[];
|
|
23
|
+
export declare function filterCrawlable(classified: ClassifiedLink[]): ClassifiedLink[];
|
|
@@ -0,0 +1,40 @@
|
|
|
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;
|
|
2
|
+
const API_PATTERN = /\/(api|graphql|gql|rest|v\d+)\//i;
|
|
3
|
+
const AUTH_PATTERN = /\/(login|signin|sign-in|register|signup|sign-up|auth|oauth|sso|password|forgot|reset|logout|signout)\b/i;
|
|
4
|
+
const LOW_VALUE_PATTERN = /[?&](page|p|offset|sort|order|filter|q|search|tag|category|color|size|brand)=/i;
|
|
5
|
+
const PAGINATION_PATTERN = /\/page\/\d+|\/p\/\d+|\bpage=\d+/i;
|
|
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" };
|
|
28
|
+
}
|
|
29
|
+
function classifyLinks(urls, baseUrl) {
|
|
30
|
+
return urls.map((u) => classifyLink(u, baseUrl)).sort((a, b) => b.priority - a.priority);
|
|
31
|
+
}
|
|
32
|
+
function filterCrawlable(classified) {
|
|
33
|
+
return classified.filter((l) => l.category !== "resource" && l.category !== "external" && l.category !== "api");
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
classifyLink,
|
|
37
|
+
classifyLinks,
|
|
38
|
+
filterCrawlable
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=link-classifier.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/link-classifier.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Link classifier \u2014 assigns priority and category to discovered URLs.\n *\n * Categories:\n * - 'nav' : site navigation (homepage, top-level pages) \u2014 highest priority\n * - 'auth' : login/register/password flows \u2014 high priority\n * - 'app' : authenticated app screens \u2014 high priority\n * - 'content' : blog/docs/articles \u2014 medium priority\n * - 'api' : REST/GraphQL API endpoints \u2014 detected, not crawled as screens\n * - 'resource' : static assets (JS/CSS/img/fonts) \u2014 skip\n * - 'external' : different domain \u2014 skip unless cross-domain crawl enabled\n * - 'low-value' : pagination, sort, filter query strings \u2014 deprioritize\n */\n\nexport type LinkCategory = 'nav' | 'auth' | 'app' | 'content' | 'api' | 'resource' | 'external' | 'low-value';\n\nexport interface ClassifiedLink {\n url: string;\n category: LinkCategory;\n priority: number; // 0-100, higher = crawl sooner\n reason: string;\n}\n\nconst RESOURCE_EXT = /\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|mp4|mp3|pdf|zip|gz|tar|exe|dmg|apk)(\\?|$)/i;\nconst API_PATTERN = /\\/(api|graphql|gql|rest|v\\d+)\\//i;\nconst AUTH_PATTERN = /\\/(login|signin|sign-in|register|signup|sign-up|auth|oauth|sso|password|forgot|reset|logout|signout)\\b/i;\nconst LOW_VALUE_PATTERN = /[?&](page|p|offset|sort|order|filter|q|search|tag|category|color|size|brand)=/i;\nconst PAGINATION_PATTERN = /\\/page\\/\\d+|\\/p\\/\\d+|\\bpage=\\d+/i;\n\nexport function classifyLink(url: string, baseUrl: string): ClassifiedLink {\n let parsed: URL;\n let baseHost: string;\n try {\n parsed = new URL(url);\n baseHost = new URL(baseUrl).hostname;\n } catch {\n return { url, category: 'low-value', priority: 0, reason: 'invalid URL' };\n }\n\n const path = parsed.pathname + parsed.search;\n const isExternal = parsed.hostname !== baseHost;\n\n if (isExternal) return { url, category: 'external', priority: 0, reason: 'different domain' };\n if (RESOURCE_EXT.test(path)) return { url, category: 'resource', priority: 0, reason: 'static asset extension' };\n if (API_PATTERN.test(path)) return { url, category: 'api', priority: 10, reason: 'API path pattern' };\n if (AUTH_PATTERN.test(path)) return { url, category: 'auth', priority: 90, reason: 'auth flow pattern' };\n if (PAGINATION_PATTERN.test(path)) return { url, category: 'low-value', priority: 15, reason: 'pagination' };\n if (LOW_VALUE_PATTERN.test(path)) return { url, category: 'low-value', priority: 20, reason: 'filter/sort query param' };\n\n const segments = parsed.pathname.split('/').filter(Boolean);\n if (segments.length === 0) return { url, category: 'nav', priority: 100, reason: 'homepage' };\n if (segments.length === 1) return { url, category: 'nav', priority: 80, reason: 'top-level nav' };\n if (segments.length <= 3) return { url, category: 'app', priority: 60, reason: 'app screen' };\n\n return { url, category: 'content', priority: 40, reason: 'deep content page' };\n}\n\nexport function classifyLinks(urls: string[], baseUrl: string): ClassifiedLink[] {\n return urls.map(u => classifyLink(u, baseUrl))\n .sort((a, b) => b.priority - a.priority);\n}\n\nexport function filterCrawlable(classified: ClassifiedLink[]): ClassifiedLink[] {\n return classified.filter(l => l.category !== 'resource' && l.category !== 'external' && l.category !== 'api');\n}\n"],
|
|
5
|
+
"mappings": "AAuBA,MAAM,eAAe;AACrB,MAAM,cAAc;AACpB,MAAM,eAAe;AACrB,MAAM,oBAAoB;AAC1B,MAAM,qBAAqB;AAEpB,SAAS,aAAa,KAAa,SAAiC;AACzE,MAAI;AACJ,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,GAAG;AACpB,eAAW,IAAI,IAAI,OAAO,EAAE;AAAA,EAC9B,QAAQ;AACN,WAAO,EAAE,KAAK,UAAU,aAAa,UAAU,GAAG,QAAQ,cAAc;AAAA,EAC1E;AAEA,QAAM,OAAO,OAAO,WAAW,OAAO;AACtC,QAAM,aAAa,OAAO,aAAa;AAEvC,MAAI,WAAY,QAAO,EAAE,KAAK,UAAU,YAAY,UAAU,GAAG,QAAQ,mBAAmB;AAC5F,MAAI,aAAa,KAAK,IAAI,EAAG,QAAO,EAAE,KAAK,UAAU,YAAY,UAAU,GAAG,QAAQ,yBAAyB;AAC/G,MAAI,YAAY,KAAK,IAAI,EAAG,QAAO,EAAE,KAAK,UAAU,OAAO,UAAU,IAAI,QAAQ,mBAAmB;AACpG,MAAI,aAAa,KAAK,IAAI,EAAG,QAAO,EAAE,KAAK,UAAU,QAAQ,UAAU,IAAI,QAAQ,oBAAoB;AACvG,MAAI,mBAAmB,KAAK,IAAI,EAAG,QAAO,EAAE,KAAK,UAAU,aAAa,UAAU,IAAI,QAAQ,aAAa;AAC3G,MAAI,kBAAkB,KAAK,IAAI,EAAG,QAAO,EAAE,KAAK,UAAU,aAAa,UAAU,IAAI,QAAQ,0BAA0B;AAEvH,QAAM,WAAW,OAAO,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO;AAC1D,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE,KAAK,UAAU,OAAO,UAAU,KAAK,QAAQ,WAAW;AAC5F,MAAI,SAAS,WAAW,EAAG,QAAO,EAAE,KAAK,UAAU,OAAO,UAAU,IAAI,QAAQ,gBAAgB;AAChG,MAAI,SAAS,UAAU,EAAG,QAAO,EAAE,KAAK,UAAU,OAAO,UAAU,IAAI,QAAQ,aAAa;AAE5F,SAAO,EAAE,KAAK,UAAU,WAAW,UAAU,IAAI,QAAQ,oBAAoB;AAC/E;AAEO,SAAS,cAAc,MAAgB,SAAmC;AAC/E,SAAO,KAAK,IAAI,OAAK,aAAa,GAAG,OAAO,CAAC,EAC1C,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,QAAQ;AAC3C;AAEO,SAAS,gBAAgB,YAAgD;AAC9E,SAAO,WAAW,OAAO,OAAK,EAAE,aAAa,cAAc,EAAE,aAAa,cAAc,EAAE,aAAa,KAAK;AAC9G;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start a live view session: launches a headless browser, navigates to appUrl,
|
|
3
|
+
* begins capturing frames. Returns the session ID.
|
|
4
|
+
*/
|
|
5
|
+
export declare function startLiveView(appUrl: string, storageState?: string): Promise<string>;
|
|
6
|
+
/**
|
|
7
|
+
* Get the latest frame for a session (base64 JPEG).
|
|
8
|
+
*/
|
|
9
|
+
export declare function getFrame(id: string): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Navigate the live view browser to a URL.
|
|
12
|
+
*/
|
|
13
|
+
export declare function liveViewNavigate(id: string, url: string): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Capture storageState from the live view session, close the browser, return JSON string.
|
|
16
|
+
*/
|
|
17
|
+
export declare function completeLiveView(id: string): Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Send a mouse or keyboard input event to the live view browser.
|
|
20
|
+
*/
|
|
21
|
+
export declare function sendInput(id: string, event: {
|
|
22
|
+
type: 'click' | 'type' | 'key';
|
|
23
|
+
x?: number;
|
|
24
|
+
y?: number;
|
|
25
|
+
button?: 'left' | 'right' | 'middle';
|
|
26
|
+
text?: string;
|
|
27
|
+
key?: string;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Close a live view session without capturing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function closeLiveView(id: string): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* List all active session IDs (for health checks / cleanup).
|
|
35
|
+
*/
|
|
36
|
+
export declare function listLiveViews(): string[];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
4
|
+
const SESSION_TTL_MS = 15 * 60 * 1e3;
|
|
5
|
+
function generateId() {
|
|
6
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
7
|
+
}
|
|
8
|
+
async function startLiveView(appUrl, storageState) {
|
|
9
|
+
const id = generateId();
|
|
10
|
+
const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.platform === "linux";
|
|
11
|
+
const browser = await chromium.launch({
|
|
12
|
+
headless: true,
|
|
13
|
+
args: isDocker ? ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", "--disable-gpu"] : []
|
|
14
|
+
});
|
|
15
|
+
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
16
|
+
if (storageState) {
|
|
17
|
+
try {
|
|
18
|
+
const state = JSON.parse(storageState);
|
|
19
|
+
if (state.cookies?.length) await context.addCookies(state.cookies);
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const page = await context.newPage();
|
|
24
|
+
await page.goto(appUrl, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
25
|
+
const session = {
|
|
26
|
+
id,
|
|
27
|
+
browser,
|
|
28
|
+
context,
|
|
29
|
+
page,
|
|
30
|
+
appUrl,
|
|
31
|
+
lastFrame: null,
|
|
32
|
+
streaming: true,
|
|
33
|
+
frameInterval: null,
|
|
34
|
+
createdAt: Date.now()
|
|
35
|
+
};
|
|
36
|
+
session.frameInterval = setInterval(async () => {
|
|
37
|
+
if (!session.streaming) return;
|
|
38
|
+
try {
|
|
39
|
+
const buf = await page.screenshot({ type: "jpeg", quality: 60, fullPage: false });
|
|
40
|
+
session.lastFrame = buf.toString("base64");
|
|
41
|
+
} catch {
|
|
42
|
+
session.streaming = false;
|
|
43
|
+
}
|
|
44
|
+
}, 500);
|
|
45
|
+
sessions.set(id, session);
|
|
46
|
+
setTimeout(() => closeLiveView(id).catch(() => {
|
|
47
|
+
}), SESSION_TTL_MS);
|
|
48
|
+
logger.info({ id, appUrl }, "[live-view] Session started");
|
|
49
|
+
return id;
|
|
50
|
+
}
|
|
51
|
+
function getFrame(id) {
|
|
52
|
+
return sessions.get(id)?.lastFrame ?? null;
|
|
53
|
+
}
|
|
54
|
+
async function liveViewNavigate(id, url) {
|
|
55
|
+
const session = sessions.get(id);
|
|
56
|
+
if (!session) throw new Error(`Live view session ${id} not found`);
|
|
57
|
+
await session.page.goto(url, { waitUntil: "domcontentloaded" });
|
|
58
|
+
}
|
|
59
|
+
async function completeLiveView(id) {
|
|
60
|
+
const session = sessions.get(id);
|
|
61
|
+
if (!session) throw new Error(`Live view session ${id} not found or expired`);
|
|
62
|
+
const state = await session.context.storageState();
|
|
63
|
+
const json = JSON.stringify(state);
|
|
64
|
+
await closeLiveView(id);
|
|
65
|
+
return json;
|
|
66
|
+
}
|
|
67
|
+
async function sendInput(id, event) {
|
|
68
|
+
const session = sessions.get(id);
|
|
69
|
+
if (!session) throw new Error(`Live view session ${id} not found`);
|
|
70
|
+
if (event.type === "click" && event.x !== void 0 && event.y !== void 0) {
|
|
71
|
+
await session.page.mouse.click(event.x, event.y, { button: event.button ?? "left" });
|
|
72
|
+
} else if (event.type === "type" && event.text) {
|
|
73
|
+
await session.page.keyboard.type(event.text);
|
|
74
|
+
} else if (event.type === "key" && event.key) {
|
|
75
|
+
await session.page.keyboard.press(event.key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function closeLiveView(id) {
|
|
79
|
+
const session = sessions.get(id);
|
|
80
|
+
if (!session) return;
|
|
81
|
+
session.streaming = false;
|
|
82
|
+
if (session.frameInterval) clearInterval(session.frameInterval);
|
|
83
|
+
sessions.delete(id);
|
|
84
|
+
await session.browser.close().catch(() => {
|
|
85
|
+
});
|
|
86
|
+
logger.info({ id }, "[live-view] Session closed");
|
|
87
|
+
}
|
|
88
|
+
function listLiveViews() {
|
|
89
|
+
return [...sessions.keys()];
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
closeLiveView,
|
|
93
|
+
completeLiveView,
|
|
94
|
+
getFrame,
|
|
95
|
+
listLiveViews,
|
|
96
|
+
liveViewNavigate,
|
|
97
|
+
sendInput,
|
|
98
|
+
startLiveView
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=live-view.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/live-view.ts"],
|
|
4
|
+
"sourcesContent": ["import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';\nimport { logger } from './logger.js';\n\ninterface LiveViewSession {\n id: string;\n browser: Browser;\n context: BrowserContext;\n page: Page;\n appUrl: string;\n lastFrame: string | null; // base64 JPEG\n streaming: boolean;\n frameInterval: ReturnType<typeof setInterval> | null;\n createdAt: number;\n}\n\nconst sessions = new Map<string, LiveViewSession>();\nconst SESSION_TTL_MS = 15 * 60 * 1_000; // 15 min\n\nfunction generateId(): string {\n return Math.random().toString(36).slice(2) + Date.now().toString(36);\n}\n\n/**\n * Start a live view session: launches a headless browser, navigates to appUrl,\n * begins capturing frames. Returns the session ID.\n */\nexport async function startLiveView(appUrl: string, storageState?: string): Promise<string> {\n const id = generateId();\n const isDocker = !!process.env.PLAYWRIGHT_IN_DOCKER || process.platform === 'linux';\n\n const browser = await chromium.launch({\n headless: true,\n args: isDocker ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'] : [],\n });\n\n const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });\n\n // Restore session if provided\n if (storageState) {\n try {\n const state = JSON.parse(storageState);\n if (state.cookies?.length) await context.addCookies(state.cookies);\n } catch { /* ignore */ }\n }\n\n const page = await context.newPage();\n await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 30_000 });\n\n const session: LiveViewSession = {\n id, browser, context, page, appUrl,\n lastFrame: null, streaming: true, frameInterval: null,\n createdAt: Date.now(),\n };\n\n // Start capturing frames (every 500ms)\n session.frameInterval = setInterval(async () => {\n if (!session.streaming) return;\n try {\n const buf = await page.screenshot({ type: 'jpeg', quality: 60, fullPage: false });\n session.lastFrame = buf.toString('base64');\n } catch { session.streaming = false; }\n }, 500);\n\n sessions.set(id, session);\n\n // Auto-cleanup after TTL\n setTimeout(() => closeLiveView(id).catch(() => {}), SESSION_TTL_MS);\n\n logger.info({ id, appUrl }, '[live-view] Session started');\n return id;\n}\n\n/**\n * Get the latest frame for a session (base64 JPEG).\n */\nexport function getFrame(id: string): string | null {\n return sessions.get(id)?.lastFrame ?? null;\n}\n\n/**\n * Navigate the live view browser to a URL.\n */\nexport async function liveViewNavigate(id: string, url: string): Promise<void> {\n const session = sessions.get(id);\n if (!session) throw new Error(`Live view session ${id} not found`);\n await session.page.goto(url, { waitUntil: 'domcontentloaded' });\n}\n\n/**\n * Capture storageState from the live view session, close the browser, return JSON string.\n */\nexport async function completeLiveView(id: string): Promise<string> {\n const session = sessions.get(id);\n if (!session) throw new Error(`Live view session ${id} not found or expired`);\n\n const state = await session.context.storageState();\n const json = JSON.stringify(state);\n await closeLiveView(id);\n return json;\n}\n\n/**\n * Send a mouse or keyboard input event to the live view browser.\n */\nexport async function sendInput(id: string, event: {\n type: 'click' | 'type' | 'key';\n x?: number; y?: number;\n button?: 'left' | 'right' | 'middle';\n text?: string;\n key?: string;\n}): Promise<void> {\n const session = sessions.get(id);\n if (!session) throw new Error(`Live view session ${id} not found`);\n if (event.type === 'click' && event.x !== undefined && event.y !== undefined) {\n await session.page.mouse.click(event.x, event.y, { button: event.button ?? 'left' });\n } else if (event.type === 'type' && event.text) {\n await session.page.keyboard.type(event.text);\n } else if (event.type === 'key' && event.key) {\n await session.page.keyboard.press(event.key);\n }\n}\n\n/**\n * Close a live view session without capturing.\n */\nexport async function closeLiveView(id: string): Promise<void> {\n const session = sessions.get(id);\n if (!session) return;\n session.streaming = false;\n if (session.frameInterval) clearInterval(session.frameInterval);\n sessions.delete(id);\n await session.browser.close().catch(() => {});\n logger.info({ id }, '[live-view] Session closed');\n}\n\n/**\n * List all active session IDs (for health checks / cleanup).\n */\nexport function listLiveViews(): string[] {\n return [...sessions.keys()];\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
const logger = pino({
|
|
3
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
4
|
+
...process.env.NODE_ENV === "development" ? { transport: { target: "pino-pretty", options: { colorize: true } } } : {},
|
|
5
|
+
base: { service: "crawler" }
|
|
6
|
+
});
|
|
7
|
+
export {
|
|
8
|
+
logger
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/logger.ts"],
|
|
4
|
+
"sourcesContent": ["import pino from 'pino';\n\nexport const logger = pino({\n level: process.env.LOG_LEVEL ?? 'info',\n ...(process.env.NODE_ENV === 'development'\n ? { transport: { target: 'pino-pretty', options: { colorize: true } } }\n : {}),\n base: { service: 'crawler' },\n});\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,UAAU;AAEV,MAAM,SAAS,KAAK;AAAA,EACzB,OAAO,QAAQ,IAAI,aAAa;AAAA,EAChC,GAAI,QAAQ,IAAI,aAAa,gBACzB,EAAE,WAAW,EAAE,QAAQ,eAAe,SAAS,EAAE,UAAU,KAAK,EAAE,EAAE,IACpE,CAAC;AAAA,EACL,MAAM,EAAE,SAAS,UAAU;AAC7B,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ExtractedMarkdown {
|
|
2
|
+
markdown: string;
|
|
3
|
+
method: 'readability' | 'full-page';
|
|
4
|
+
title: string | null;
|
|
5
|
+
wordCount: number;
|
|
6
|
+
}
|
|
7
|
+
/** Best-effort HTML→Markdown for one crawled page. Never throws. */
|
|
8
|
+
export declare function extractMarkdown(html: string, url: string): ExtractedMarkdown | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseHTML } from "linkedom";
|
|
2
|
+
import { Readability } from "@mozilla/readability";
|
|
3
|
+
import TurndownService from "turndown";
|
|
4
|
+
const turndown = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
|
|
5
|
+
turndown.remove(["script", "style", "noscript", "svg", "iframe"]);
|
|
6
|
+
function stripNoiseElements(document) {
|
|
7
|
+
const noisy = document.querySelectorAll('script, style, noscript, svg, iframe, [aria-hidden="true"]');
|
|
8
|
+
noisy.forEach((el) => el.remove());
|
|
9
|
+
}
|
|
10
|
+
function extractMarkdown(html, url) {
|
|
11
|
+
try {
|
|
12
|
+
const { document } = parseHTML(html);
|
|
13
|
+
stripNoiseElements(document);
|
|
14
|
+
let readabilityResult = null;
|
|
15
|
+
try {
|
|
16
|
+
const { document: readerDoc } = parseHTML(html);
|
|
17
|
+
stripNoiseElements(readerDoc);
|
|
18
|
+
readabilityResult = new Readability(readerDoc, { charThreshold: 50 }).parse();
|
|
19
|
+
} catch {
|
|
20
|
+
readabilityResult = null;
|
|
21
|
+
}
|
|
22
|
+
if (readabilityResult?.content && readabilityResult.textContent.trim().length >= 50) {
|
|
23
|
+
const markdown2 = turndown.turndown(readabilityResult.content);
|
|
24
|
+
return {
|
|
25
|
+
markdown: `# ${readabilityResult.title || document.title || url}
|
|
26
|
+
|
|
27
|
+
${markdown2}`,
|
|
28
|
+
method: "readability",
|
|
29
|
+
title: readabilityResult.title || document.title || null,
|
|
30
|
+
wordCount: readabilityResult.textContent.trim().split(/\s+/).length
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const body = document.body;
|
|
34
|
+
if (!body) return null;
|
|
35
|
+
const markdown = turndown.turndown(body.innerHTML || "");
|
|
36
|
+
const text = markdown.trim();
|
|
37
|
+
if (!text) return null;
|
|
38
|
+
const title = document.title || null;
|
|
39
|
+
return {
|
|
40
|
+
markdown: title ? `# ${title}
|
|
41
|
+
|
|
42
|
+
${markdown}` : markdown,
|
|
43
|
+
method: "full-page",
|
|
44
|
+
title,
|
|
45
|
+
wordCount: text.split(/\s+/).length
|
|
46
|
+
};
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export {
|
|
52
|
+
extractMarkdown
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=markdown-extract.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/markdown-extract.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * HTML \u2192 Markdown extraction for crawled pages \u2014 matches crawl4AI's markdown\n * output capability. Two-tier, mirroring crawl4AI's \"markdown\" vs \"fit markdown\":\n *\n * - \"fit\" markdown: Readability's best guess at the primary content (title +\n * article body), stripped of nav/ads/chrome. Ideal for content/marketing\n * pages and RAG chunking. null when Readability finds no article (most\n * app UI screens \u2014 dashboards, forms \u2014 legitimately have none).\n * - \"full\" markdown: the whole visible page converted straight to Markdown,\n * always present. The fallback for app screens where there's no \"article\".\n *\n * Runs entirely on an already-captured `page.content()` string \u2014 no extra\n * network fetch, no extra page navigation.\n */\nimport { parseHTML } from 'linkedom';\nimport { Readability } from '@mozilla/readability';\nimport TurndownService from 'turndown';\n\nconst turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });\nturndown.remove(['script', 'style', 'noscript', 'svg', 'iframe'] as unknown as (keyof HTMLElementTagNameMap)[]);\n\nexport interface ExtractedMarkdown {\n markdown: string;\n method: 'readability' | 'full-page';\n title: string | null;\n wordCount: number;\n}\n\nfunction stripNoiseElements(document: Document): void {\n const noisy = document.querySelectorAll('script, style, noscript, svg, iframe, [aria-hidden=\"true\"]');\n noisy.forEach((el: any) => el.remove());\n}\n\n/** Best-effort HTML\u2192Markdown for one crawled page. Never throws. */\nexport function extractMarkdown(html: string, url: string): ExtractedMarkdown | null {\n try {\n const { document } = parseHTML(html);\n stripNoiseElements(document);\n\n let readabilityResult: { title: string; content: string; textContent: string } | null = null;\n try {\n // Readability mutates the DOM it's given \u2014 clone via re-parsing so the\n // full-page fallback below still has an intact document.\n const { document: readerDoc } = parseHTML(html);\n stripNoiseElements(readerDoc);\n readabilityResult = new Readability(readerDoc as any, { charThreshold: 50 }).parse() as any;\n } catch {\n readabilityResult = null;\n }\n\n if (readabilityResult?.content && readabilityResult.textContent.trim().length >= 50) {\n const markdown = turndown.turndown(readabilityResult.content);\n return {\n markdown: `# ${readabilityResult.title || document.title || url}\\n\\n${markdown}`,\n method: 'readability',\n title: readabilityResult.title || document.title || null,\n wordCount: readabilityResult.textContent.trim().split(/\\s+/).length,\n };\n }\n\n const body = document.body;\n if (!body) return null;\n const markdown = turndown.turndown(body.innerHTML || '');\n const text = markdown.trim();\n if (!text) return null;\n const title = document.title || null;\n return {\n markdown: title ? `# ${title}\\n\\n${markdown}` : markdown,\n method: 'full-page',\n title,\n wordCount: text.split(/\\s+/).length,\n };\n } catch {\n return null;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAcA,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAC5B,OAAO,qBAAqB;AAE5B,MAAM,WAAW,IAAI,gBAAgB,EAAE,cAAc,OAAO,gBAAgB,SAAS,CAAC;AACtF,SAAS,OAAO,CAAC,UAAU,SAAS,YAAY,OAAO,QAAQ,CAA+C;AAS9G,SAAS,mBAAmB,UAA0B;AACpD,QAAM,QAAQ,SAAS,iBAAiB,4DAA4D;AACpG,QAAM,QAAQ,CAAC,OAAY,GAAG,OAAO,CAAC;AACxC;AAGO,SAAS,gBAAgB,MAAc,KAAuC;AACnF,MAAI;AACF,UAAM,EAAE,SAAS,IAAI,UAAU,IAAI;AACnC,uBAAmB,QAAQ;AAE3B,QAAI,oBAAoF;AACxF,QAAI;AAGF,YAAM,EAAE,UAAU,UAAU,IAAI,UAAU,IAAI;AAC9C,yBAAmB,SAAS;AAC5B,0BAAoB,IAAI,YAAY,WAAkB,EAAE,eAAe,GAAG,CAAC,EAAE,MAAM;AAAA,IACrF,QAAQ;AACN,0BAAoB;AAAA,IACtB;AAEA,QAAI,mBAAmB,WAAW,kBAAkB,YAAY,KAAK,EAAE,UAAU,IAAI;AACnF,YAAMA,YAAW,SAAS,SAAS,kBAAkB,OAAO;AAC5D,aAAO;AAAA,QACL,UAAU,KAAK,kBAAkB,SAAS,SAAS,SAAS,GAAG;AAAA;AAAA,EAAOA,SAAQ;AAAA,QAC9E,QAAQ;AAAA,QACR,OAAO,kBAAkB,SAAS,SAAS,SAAS;AAAA,QACpD,WAAW,kBAAkB,YAAY,KAAK,EAAE,MAAM,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAEA,UAAM,OAAO,SAAS;AACtB,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,WAAW,SAAS,SAAS,KAAK,aAAa,EAAE;AACvD,UAAM,OAAO,SAAS,KAAK;AAC3B,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,QAAQ,SAAS,SAAS;AAChC,WAAO;AAAA,MACL,UAAU,QAAQ,KAAK,KAAK;AAAA;AAAA,EAAO,QAAQ,KAAK;AAAA,MAChD,QAAQ;AAAA,MACR;AAAA,MACA,WAAW,KAAK,MAAM,KAAK,EAAE;AAAA,IAC/B;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
|
|
6
|
+
"names": ["markdown"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZeTa Native App Crawler — Appium-based crawler for Android APK and iOS IPA.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* Appium Server (running at APPIUM_SERVER_URL) → WebDriverIO session
|
|
6
|
+
* → UiAutomator2 (Android) / XCUITest (iOS)
|
|
7
|
+
* → accessibility tree XML → ZeTa Element records
|
|
8
|
+
*
|
|
9
|
+
* TODO: requires Appium server running at APPIUM_SERVER_URL env var
|
|
10
|
+
* TODO: requires `webdriverio` package — add "webdriverio": "^9.x" to package.json
|
|
11
|
+
*
|
|
12
|
+
* Device farm support:
|
|
13
|
+
* - local_emulator: Appium server + local emulator/simulator
|
|
14
|
+
* - aws_device_farm: presigned S3 URL uploaded to Device Farm, session via WDIO
|
|
15
|
+
* - browserstack: BrowserStack Automate Appium endpoint
|
|
16
|
+
*/
|
|
17
|
+
export interface NativeCrawlerOptions {
|
|
18
|
+
tenantId: string;
|
|
19
|
+
projectId: string;
|
|
20
|
+
jobId: string;
|
|
21
|
+
platform: 'android' | 'ios' | 'windows' | 'macos';
|
|
22
|
+
appS3Path: string;
|
|
23
|
+
packageName?: string;
|
|
24
|
+
deviceModel?: string;
|
|
25
|
+
osVersion?: string;
|
|
26
|
+
appActivity?: string;
|
|
27
|
+
appWaitActivity?: string;
|
|
28
|
+
deviceFarm: 'local_emulator' | 'aws_device_farm' | 'browserstack';
|
|
29
|
+
appiumServerUrl?: string;
|
|
30
|
+
browserstackUsername?: string;
|
|
31
|
+
browserstackAccessKey?: string;
|
|
32
|
+
awsAccessKeyId?: string;
|
|
33
|
+
awsSecretAccessKey?: string;
|
|
34
|
+
awsRegion?: string;
|
|
35
|
+
awsDeviceFarmProjectArn?: string;
|
|
36
|
+
credentials?: {
|
|
37
|
+
username?: string;
|
|
38
|
+
password?: string;
|
|
39
|
+
usernameFieldLabel?: string;
|
|
40
|
+
passwordFieldLabel?: string;
|
|
41
|
+
loginButtonLabel?: string;
|
|
42
|
+
};
|
|
43
|
+
visionLlmConfig?: {
|
|
44
|
+
provider: string;
|
|
45
|
+
model: string;
|
|
46
|
+
apiKey?: string;
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
} | null;
|
|
49
|
+
gestureConfig?: {
|
|
50
|
+
enableSwipe?: boolean;
|
|
51
|
+
enableLongPress?: boolean;
|
|
52
|
+
};
|
|
53
|
+
mitmProxyPort?: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run a native app crawl session via Appium.
|
|
57
|
+
*
|
|
58
|
+
* Flow:
|
|
59
|
+
* 1. Download APK/IPA from S3 to local temp dir
|
|
60
|
+
* 2. Build Appium WebDriverIO capabilities for the target platform
|
|
61
|
+
* 3. Create WebDriverIO remote session (Appium server at APPIUM_SERVER_URL)
|
|
62
|
+
* 4. For each discovered screen:
|
|
63
|
+
* a. Get page source XML → parse elements
|
|
64
|
+
* b. Take screenshot → save to screenshotDir → persist via AppBrain
|
|
65
|
+
* c. Save screen + elements to DB
|
|
66
|
+
* d. Tap nav elements to discover next screens
|
|
67
|
+
* 5. Update PlatformJob as DONE (or FAILED on error)
|
|
68
|
+
*/
|
|
69
|
+
export declare function runNativeCrawl(opts: NativeCrawlerOptions): Promise<void>;
|