@nitpicker/crawler 0.6.3 → 0.6.5-alpha.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.
Files changed (65) hide show
  1. package/package.json +2 -2
  2. package/lib/archive/common-queries.d.ts +0 -14
  3. package/lib/archive/common-queries.js +0 -40
  4. package/lib/archive/filesystem/index.d.ts +0 -17
  5. package/lib/archive/filesystem/index.js +0 -17
  6. package/lib/archive/filesystem/utils.d.ts +0 -109
  7. package/lib/archive/filesystem/utils.js +0 -185
  8. package/lib/archive/filesystem/zip.d.ts +0 -29
  9. package/lib/archive/filesystem/zip.js +0 -53
  10. package/lib/archive/index.d.ts +0 -6
  11. package/lib/archive/index.js +0 -11
  12. package/lib/crawler/index.d.ts +0 -2
  13. package/lib/crawler/index.js +0 -2
  14. package/lib/crawler/network.d.ts +0 -30
  15. package/lib/crawler/network.js +0 -226
  16. package/lib/crawler/result-handler.d.ts +0 -118
  17. package/lib/crawler/result-handler.js +0 -153
  18. package/lib/crawler/speculative-pagination.d.ts +0 -52
  19. package/lib/crawler/speculative-pagination.js +0 -215
  20. package/lib/crawler/url-filter.d.ts +0 -56
  21. package/lib/crawler/url-filter.js +0 -110
  22. package/lib/index.d.ts +0 -16
  23. package/lib/index.js +0 -18
  24. package/lib/qzilla.d.ts +0 -136
  25. package/lib/qzilla.js +0 -292
  26. package/lib/utils/array/index.d.ts +0 -1
  27. package/lib/utils/array/index.js +0 -1
  28. package/lib/utils/async/index.d.ts +0 -1
  29. package/lib/utils/async/index.js +0 -1
  30. package/lib/utils/error/index.d.ts +0 -3
  31. package/lib/utils/error/index.js +0 -2
  32. package/lib/utils/event-emitter/index.d.ts +0 -6
  33. package/lib/utils/event-emitter/index.js +0 -6
  34. package/lib/utils/index.d.ts +0 -5
  35. package/lib/utils/index.js +0 -5
  36. package/lib/utils/network/index.d.ts +0 -1
  37. package/lib/utils/network/index.js +0 -1
  38. package/lib/utils/object/index.d.ts +0 -1
  39. package/lib/utils/object/index.js +0 -1
  40. package/lib/utils/path/index.d.ts +0 -1
  41. package/lib/utils/path/index.js +0 -1
  42. package/lib/utils/path/safe-filepath.d.ts +0 -7
  43. package/lib/utils/path/safe-filepath.js +0 -12
  44. package/lib/utils/regexp/index.d.ts +0 -1
  45. package/lib/utils/regexp/index.js +0 -1
  46. package/lib/utils/retryable/index.d.ts +0 -2
  47. package/lib/utils/retryable/index.js +0 -1
  48. package/lib/utils/sort/index.d.ts +0 -14
  49. package/lib/utils/sort/index.js +0 -61
  50. package/lib/utils/sort/remove-matches.d.ts +0 -9
  51. package/lib/utils/sort/remove-matches.js +0 -23
  52. package/lib/utils/types/index.d.ts +0 -1
  53. package/lib/utils/types/index.js +0 -1
  54. package/lib/utils/url/index.d.ts +0 -5
  55. package/lib/utils/url/index.js +0 -5
  56. package/lib/utils/url/is-lower-layer.d.ts +0 -15
  57. package/lib/utils/url/is-lower-layer.js +0 -55
  58. package/lib/utils/url/parse-url.d.ts +0 -11
  59. package/lib/utils/url/parse-url.js +0 -20
  60. package/lib/utils/url/path-match.d.ts +0 -11
  61. package/lib/utils/url/path-match.js +0 -18
  62. package/lib/utils/url/sort-url.d.ts +0 -10
  63. package/lib/utils/url/sort-url.js +0 -24
  64. package/lib/utils/url/url-partial-match.d.ts +0 -11
  65. package/lib/utils/url/url-partial-match.js +0 -32
@@ -1,226 +0,0 @@
1
- import { delay } from '@d-zero/shared/delay';
2
- import redirects from 'follow-redirects';
3
- import NetTimeoutError from './net-timeout-error.js';
4
- /**
5
- * In-memory cache of HEAD request results keyed by URL (without hash).
6
- * Stores either the successful {@link PageData} or the {@link Error} to avoid
7
- * repeated requests to the same destination.
8
- */
9
- const cacheMap = new Map();
10
- /**
11
- * Clears the in-memory cache of HTTP request results.
12
- * Should be called between crawl sessions to prevent memory leaks.
13
- */
14
- export function clearDestinationCache() {
15
- cacheMap.clear();
16
- }
17
- /**
18
- * Fetches the destination metadata for a URL using an HTTP HEAD request (or GET as fallback).
19
- *
20
- * Results are cached in memory so that repeated calls for the same URL
21
- * (without hash) return immediately. The request races against a 10-second
22
- * timeout; if the server does not respond in time, a {@link NetTimeoutError} is thrown.
23
- *
24
- * If the server returns 405 (Method Not Allowed), 501 (Not Implemented), or 503
25
- * (Service Unavailable) for a HEAD request, the function automatically retries with GET.
26
- * @param url - The extended URL to fetch.
27
- * @param isExternal - Whether the URL is external to the crawl scope.
28
- * @param method - The HTTP method to use. Defaults to `"HEAD"`.
29
- * @param options - Additional options.
30
- * @param options.titleBytesLimit - When set, forces a GET request and reads up to this many
31
- * bytes from the response body to extract an HTML `<title>` tag. The connection is
32
- * destroyed as soon as the limit is reached or a title is found.
33
- * @returns The page metadata obtained from the HTTP response.
34
- * @throws {NetTimeoutError} If the request exceeds the 10-second timeout.
35
- * @throws {Error} If the HTTP request fails for any other reason.
36
- */
37
- export async function fetchDestination(url, isExternal, method = 'HEAD', options) {
38
- const titleBytesLimit = options?.titleBytesLimit;
39
- const cacheKey = titleBytesLimit == null ? url.withoutHash : `${url.withoutHash}:title`;
40
- if (cacheMap.has(cacheKey)) {
41
- const cache = cacheMap.get(cacheKey);
42
- if (cache instanceof Error) {
43
- throw cache;
44
- }
45
- return cache;
46
- }
47
- const effectiveMethod = titleBytesLimit == null ? method : 'GET';
48
- const result = await Promise.race([
49
- _fetchHead(url, isExternal, effectiveMethod, titleBytesLimit).catch((error) => (error instanceof Error ? error : new Error(String(error)))),
50
- (async () => {
51
- await delay(10 * 1000);
52
- return new NetTimeoutError(url.href);
53
- })(),
54
- ]);
55
- cacheMap.set(cacheKey, result);
56
- if (result instanceof Error) {
57
- throw result;
58
- }
59
- return result;
60
- }
61
- /**
62
- * Performs the actual HTTP request to retrieve page metadata.
63
- *
64
- * Handles both HTTP and HTTPS protocols via `follow-redirects`, tracks redirect chains,
65
- * and falls back to GET on certain status codes (405, 501, 503).
66
- * @param url - The extended URL to request.
67
- * @param isExternal - Whether the URL is external to the crawl scope.
68
- * @param method - The HTTP method (`"HEAD"` or `"GET"`).
69
- * @param titleBytesLimit - When set, reads up to this many bytes from the response body
70
- * to extract a `<title>` tag, then destroys the connection.
71
- * @returns A promise resolving to {@link PageData} with response metadata.
72
- */
73
- async function _fetchHead(url, isExternal, method, titleBytesLimit) {
74
- return new Promise((resolve, reject) => {
75
- const hostHeader = url.port ? `${url.hostname}:${url.port}` : url.hostname;
76
- const request = {
77
- protocol: url.protocol,
78
- hostname: url.hostname,
79
- port: url.port || undefined,
80
- path: url.pathname,
81
- method,
82
- headers: {
83
- host: hostHeader,
84
- Connection: 'keep-alive',
85
- Pragma: 'no-cache',
86
- 'Cache-Control': 'no-cache',
87
- 'Upgrade-Insecure-Requests': 1,
88
- Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
89
- 'Accept-Encoding': 'gzip, deflate',
90
- 'Accept-Language': 'ja,en;q=0.9,zh;q=0.8,en-US;q=0.7,pl;q=0.6,de;q=0.5,zh-CN;q=0.4,zh-TW;q=0.3,th;q=0.2,ko;q=0.1,fr;q=0.1',
91
- // Range: url.extname?.toLowerCase() === 'pdf' ? 'bytes=0-0' : undefined,
92
- },
93
- };
94
- if (url.username && url.password) {
95
- request.auth = `${url.username}:${url.password}`;
96
- }
97
- let req;
98
- let destroyed = false;
99
- const response = (res) => {
100
- const chunks = [];
101
- let totalBytes = 0;
102
- let settled = false;
103
- const buildPageData = (title) => {
104
- const redirectPaths = res.redirects.map((r) => r.url);
105
- const _contentLength = Number.parseInt(res.headers['content-length'] || '');
106
- const contentLength = Number.isFinite(_contentLength) ? _contentLength : null;
107
- return {
108
- url,
109
- isTarget: !isExternal,
110
- isExternal,
111
- redirectPaths,
112
- status: res.statusCode || 0,
113
- statusText: res.statusMessage || '',
114
- contentType: res.headers['content-type']?.split(';')[0] || null,
115
- contentLength,
116
- responseHeaders: res.headers,
117
- meta: { title },
118
- imageList: [],
119
- anchorList: [],
120
- html: '',
121
- isSkipped: false,
122
- };
123
- };
124
- if (titleBytesLimit == null) {
125
- res.on('data', () => { });
126
- res.on('end', async () => {
127
- let rep = buildPageData('');
128
- if (rep.status === 405) {
129
- if (method === 'GET') {
130
- reject(new Error(`Method Not Allowed: ${url.href} ${rep.statusText}`));
131
- return;
132
- }
133
- try {
134
- rep = await fetchDestination(url, isExternal, 'GET');
135
- }
136
- catch (error) {
137
- reject(error);
138
- return;
139
- }
140
- }
141
- if (rep.status === 501) {
142
- if (method === 'GET') {
143
- reject(new Error(`Method Not Implemented: ${url.href} ${rep.statusText}`));
144
- return;
145
- }
146
- await delay(5 * 1000);
147
- try {
148
- rep = await fetchDestination(url, isExternal, 'GET');
149
- }
150
- catch (error) {
151
- reject(error);
152
- return;
153
- }
154
- }
155
- if (rep.status === 503) {
156
- if (method === 'GET') {
157
- reject(new Error(`Retrying failed: ${url.href} ${rep.statusText}`));
158
- return;
159
- }
160
- await delay(5 * 1000);
161
- try {
162
- rep = await fetchDestination(url, isExternal, 'GET');
163
- }
164
- catch (error) {
165
- reject(error);
166
- return;
167
- }
168
- }
169
- resolve(rep);
170
- });
171
- }
172
- else {
173
- res.on('data', (chunk) => {
174
- if (settled)
175
- return;
176
- chunks.push(chunk);
177
- totalBytes += chunk.length;
178
- // Check for title in accumulated data so far
179
- const body = Buffer.concat(chunks).toString('utf8');
180
- const titleMatch = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(body);
181
- if (titleMatch) {
182
- settled = true;
183
- const title = titleMatch[1]?.trim() ?? '';
184
- resolve(buildPageData(title));
185
- destroyed = true;
186
- req.destroy();
187
- return;
188
- }
189
- // Reached byte limit without finding title
190
- if (totalBytes >= titleBytesLimit) {
191
- settled = true;
192
- resolve(buildPageData(''));
193
- destroyed = true;
194
- req.destroy();
195
- }
196
- });
197
- res.on('end', () => {
198
- if (settled)
199
- return;
200
- settled = true;
201
- // Stream ended before limit — try to extract title from what we have
202
- const body = Buffer.concat(chunks).toString('utf8');
203
- const titleMatch = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(body);
204
- const title = titleMatch?.[1]?.trim() ?? '';
205
- resolve(buildPageData(title));
206
- });
207
- }
208
- };
209
- if (url.protocol === 'https:') {
210
- req = redirects.https.request({
211
- ...request,
212
- rejectUnauthorized: false,
213
- }, response);
214
- }
215
- else {
216
- req = redirects.http.request(request, response);
217
- }
218
- req.on('error', (error) => {
219
- // Ignore errors caused by intentional req.destroy()
220
- if (destroyed)
221
- return;
222
- reject(error);
223
- });
224
- req.end();
225
- });
226
- }
@@ -1,118 +0,0 @@
1
- import type LinkList from './link-list.js';
2
- import type { Link, PageData, Resource } from '../utils/index.js';
3
- import type { ExURL, ParseURLOptions } from '@d-zero/shared/parse-url';
4
- /**
5
- * Configuration options that control crawler behavior.
6
- *
7
- * Used by the result handler functions to determine how to process
8
- * scrape results, which URLs to follow, and how to handle external links.
9
- * @see {@link ./crawler.ts | Crawler} for the main consumer of this type
10
- * @see {@link ../crawler-orchestrator.ts | CrawlerOrchestrator} for factory methods that build these options
11
- */
12
- export type CrawlerOptions = {
13
- /** Delay in milliseconds between page requests. */
14
- interval: number;
15
- /** Maximum number of concurrent scraping processes. 0 uses the default. */
16
- parallels: number;
17
- /** Whether to recursively follow discovered links within the scope. */
18
- recursive: boolean;
19
- /** Whether the crawl was started from a pre-defined URL list. */
20
- fromList: boolean;
21
- /** Whether to capture image resources during scraping. */
22
- isGettingImages: boolean;
23
- /** Path to the Chromium/Chrome executable, or `null` for the bundled version. */
24
- executablePath: string | null;
25
- /** Whether to fetch and scrape external (out-of-scope) pages. */
26
- fetchExternal: boolean;
27
- /** List of scope URL strings that define the crawl boundary. */
28
- scope: string[];
29
- /** Glob patterns for URLs to exclude from crawling. */
30
- excludes: string[];
31
- /** Keywords that trigger page exclusion when found in content. */
32
- excludeKeywords: string[];
33
- /** URL prefixes to exclude from crawling (merged defaults + user additions). */
34
- excludeUrls: readonly string[];
35
- /** Maximum directory depth for crawling avoidance heuristics. */
36
- depthOnAvoid: number;
37
- /** Maximum number of retry attempts per URL on scrape failure. */
38
- retry: number;
39
- /** Whether to enable verbose logging. */
40
- verbose: boolean;
41
- } & Required<Pick<ParseURLOptions, 'disableQueries'>>;
42
- /**
43
- * Process the result of a successful page scrape.
44
- *
45
- * Extracts anchors from the page (unless in title-only mode), enqueues
46
- * newly discovered URLs via the `addUrl` callback, and marks the URL
47
- * as done in the link list.
48
- * @param result - The scraped page data.
49
- * @param linkList - The link list managing the crawl queue.
50
- * @param scope - Map of hostnames to their scope URLs.
51
- * @param options - Crawler configuration options.
52
- * @param addUrl - Callback to enqueue a newly discovered URL. Accepts optional
53
- * `{ titleOnly: true }` to request metadata-only scraping.
54
- * @returns An object containing the constructed link and whether the page is external.
55
- */
56
- export declare function handleScrapeEnd(result: PageData, linkList: LinkList, scope: ReadonlyMap<string, readonly ExURL[]>, options: CrawlerOptions, addUrl: (url: ExURL, opts?: {
57
- titleOnly?: true;
58
- }) => void): {
59
- link: Link | null;
60
- isExternal: boolean;
61
- };
62
- /**
63
- * Handle a URL that was ignored or skipped during scraping.
64
- *
65
- * Marks the URL as done in the link list without any page data,
66
- * effectively recording that it was encountered but not scraped.
67
- * @param url - The URL that was skipped.
68
- * @param linkList - The link list managing the crawl queue.
69
- * @param scope - Map of hostnames to their scope URLs.
70
- * @param options - Crawler configuration options.
71
- * @returns The constructed {@link Link} object, or `null` if the URL was not in the queue.
72
- */
73
- export declare function handleIgnoreAndSkip(url: ExURL, linkList: LinkList, scope: ReadonlyMap<string, readonly ExURL[]>, options: CrawlerOptions): Link | null;
74
- /**
75
- * Track a network resource response and determine if it is newly discovered.
76
- *
77
- * Checks whether the resource URL has already been seen. If it is new,
78
- * adds it to the known resources set.
79
- * @param resource - The captured network resource data.
80
- * @param resources - The set of already-known resource URLs (without hash).
81
- * @returns An object with `isNew` indicating whether this resource was seen for the first time.
82
- */
83
- export declare function handleResourceResponse(resource: Resource, resources: Set<string>): {
84
- isNew: boolean;
85
- };
86
- /**
87
- * Handle an error that occurred during page scraping.
88
- *
89
- * Marks the URL as done and creates a fallback {@link PageData} from the
90
- * link, regardless of whether the error caused a shutdown. This ensures
91
- * that errored URLs are recorded in the DB (`status = -1, scraped = 1`)
92
- * and not re-queued on resume.
93
- * @param payload - The error payload from the scraper.
94
- * @param payload.url - The URL being scraped when the error occurred, or `null`.
95
- * @param payload.error - The error details including name, message, and optional stack.
96
- * @param payload.error.name
97
- * @param payload.error.message
98
- * @param payload.error.stack
99
- * @param payload.shutdown - Whether the error caused the scraper process to shut down.
100
- * @param payload.pid - The process ID of the scraper, or `undefined`.
101
- * @param linkList - The link list managing the crawl queue.
102
- * @param scope - Map of hostnames to their scope URLs.
103
- * @param options - Crawler configuration options.
104
- * @returns An object with the link and an optional fallback PageData result.
105
- */
106
- export declare function handleScrapeError(payload: {
107
- url: ExURL | null;
108
- error: {
109
- name: string;
110
- message: string;
111
- stack?: string;
112
- };
113
- shutdown: boolean;
114
- pid: number | undefined;
115
- }, linkList: LinkList, scope: ReadonlyMap<string, readonly ExURL[]>, options: CrawlerOptions): {
116
- link: Link | null;
117
- result?: PageData;
118
- };
@@ -1,153 +0,0 @@
1
- import { crawlerErrorLog, crawlerLog } from '../debug.js';
2
- import { linkToPageData } from './link-to-page-data.js';
3
- import { injectScopeAuth } from './inject-scope-auth.js';
4
- import { isExternalUrl } from './is-external-url.js';
5
- import { isInAnyLowerLayer } from './is-in-any-lower-layer.js';
6
- /**
7
- * Process the result of a successful page scrape.
8
- *
9
- * Extracts anchors from the page (unless in title-only mode), enqueues
10
- * newly discovered URLs via the `addUrl` callback, and marks the URL
11
- * as done in the link list.
12
- * @param result - The scraped page data.
13
- * @param linkList - The link list managing the crawl queue.
14
- * @param scope - Map of hostnames to their scope URLs.
15
- * @param options - Crawler configuration options.
16
- * @param addUrl - Callback to enqueue a newly discovered URL. Accepts optional
17
- * `{ titleOnly: true }` to request metadata-only scraping.
18
- * @returns An object containing the constructed link and whether the page is external.
19
- */
20
- export function handleScrapeEnd(result, linkList, scope, options, addUrl) {
21
- const isTitleOnly = linkList.isTitleOnly(result.url.withoutHash);
22
- if (!isTitleOnly) {
23
- processAnchors(result.anchorList, scope, options, addUrl);
24
- }
25
- const link = linkList.done(result.url, scope, {
26
- page: result,
27
- }, options);
28
- crawlerLog('Scrape end URL: %s', result.url.href);
29
- crawlerLog('Scrape end Status: %d', result.status);
30
- crawlerLog('Scrape end Type: %s', result.contentType);
31
- if (!result.isExternal) {
32
- crawlerLog('Scrape end Anchors: %d URLs', result.anchorList.length);
33
- }
34
- return { link, isExternal: result.isExternal };
35
- }
36
- /**
37
- * Handle a URL that was ignored or skipped during scraping.
38
- *
39
- * Marks the URL as done in the link list without any page data,
40
- * effectively recording that it was encountered but not scraped.
41
- * @param url - The URL that was skipped.
42
- * @param linkList - The link list managing the crawl queue.
43
- * @param scope - Map of hostnames to their scope URLs.
44
- * @param options - Crawler configuration options.
45
- * @returns The constructed {@link Link} object, or `null` if the URL was not in the queue.
46
- */
47
- export function handleIgnoreAndSkip(url, linkList, scope, options) {
48
- const updated = linkList.done(url, scope, {}, options);
49
- if (updated) {
50
- crawlerLog('Skipped URL: %s', url.href);
51
- }
52
- return updated;
53
- }
54
- /**
55
- * Track a network resource response and determine if it is newly discovered.
56
- *
57
- * Checks whether the resource URL has already been seen. If it is new,
58
- * adds it to the known resources set.
59
- * @param resource - The captured network resource data.
60
- * @param resources - The set of already-known resource URLs (without hash).
61
- * @returns An object with `isNew` indicating whether this resource was seen for the first time.
62
- */
63
- export function handleResourceResponse(resource, resources) {
64
- const isNew = !resources.has(resource.url.withoutHash);
65
- if (isNew) {
66
- resources.add(resource.url.withoutHash);
67
- }
68
- return { isNew };
69
- }
70
- /**
71
- * Handle an error that occurred during page scraping.
72
- *
73
- * Marks the URL as done and creates a fallback {@link PageData} from the
74
- * link, regardless of whether the error caused a shutdown. This ensures
75
- * that errored URLs are recorded in the DB (`status = -1, scraped = 1`)
76
- * and not re-queued on resume.
77
- * @param payload - The error payload from the scraper.
78
- * @param payload.url - The URL being scraped when the error occurred, or `null`.
79
- * @param payload.error - The error details including name, message, and optional stack.
80
- * @param payload.error.name
81
- * @param payload.error.message
82
- * @param payload.error.stack
83
- * @param payload.shutdown - Whether the error caused the scraper process to shut down.
84
- * @param payload.pid - The process ID of the scraper, or `undefined`.
85
- * @param linkList - The link list managing the crawl queue.
86
- * @param scope - Map of hostnames to their scope URLs.
87
- * @param options - Crawler configuration options.
88
- * @returns An object with the link and an optional fallback PageData result.
89
- */
90
- export function handleScrapeError(payload, linkList, scope, options) {
91
- const { url, error, shutdown, pid } = payload;
92
- let link = null;
93
- let result;
94
- if (url) {
95
- const updated = linkList.done(url, scope, { error }, options);
96
- if (updated) {
97
- link = updated;
98
- result = linkToPageData(updated);
99
- }
100
- }
101
- crawlerErrorLog('From %d(%s)', pid, url?.href ?? 'UNKNOWN_URL');
102
- crawlerErrorLog('Then shutdown?: %s', shutdown ? 'Yes' : 'No');
103
- crawlerErrorLog('%O', error);
104
- return { link, result };
105
- }
106
- /**
107
- * Process anchor elements extracted from a scraped page and enqueue new URLs.
108
- *
109
- * For each anchor:
110
- * 1. Determines if it is external (outside the crawl scope)
111
- * 2. Injects authentication credentials from matching scope URLs
112
- * 3. Reconstructs the `withoutHash` URL with injected auth
113
- * 4. In recursive mode: enqueues internal lower-layer URLs for full scraping,
114
- * and external URLs for title-only scraping (if `fetchExternal` is enabled)
115
- * 5. In non-recursive mode: enqueues all URLs for title-only scraping
116
- * @param anchors - The list of anchor data extracted from the page.
117
- * @param scope - Map of hostnames to their scope URLs.
118
- * @param options - Crawler configuration options.
119
- * @param addUrl - Callback to enqueue a newly discovered URL. Accepts optional
120
- * `{ titleOnly: true }` to request metadata-only scraping.
121
- */
122
- function processAnchors(anchors, scope, options, addUrl) {
123
- for (const anchor of anchors) {
124
- const isExternal = isExternalUrl(anchor.href, scope);
125
- anchor.isExternal = isExternal;
126
- if (!isExternal && (!anchor.href.username || !anchor.href.password)) {
127
- injectScopeAuth(anchor.href, scope);
128
- const auth = anchor.href.username && anchor.href.password
129
- ? `${anchor.href.username}:${anchor.href.password}@`
130
- : '';
131
- const host = anchor.href.hostname + (anchor.href.port ? `:${anchor.href.port}` : '');
132
- const newSearch = anchor.href.query ? `?${anchor.href.query}` : '';
133
- const body = anchor.href.dirname
134
- ? `${anchor.href.paths.join('/')}${newSearch}`
135
- : newSearch
136
- ? `${newSearch}`
137
- : '';
138
- const withoutHash = `${anchor.href.protocol}//${auth}${host}${body ? `/${body}` : ''}`;
139
- anchor.href.withoutHash = withoutHash;
140
- }
141
- if (options.recursive) {
142
- const scopes = scope.get(anchor.href.hostname);
143
- if (scopes && isInAnyLowerLayer(anchor.href, scopes, options)) {
144
- addUrl(anchor.href);
145
- }
146
- else if (isExternal && options.fetchExternal) {
147
- addUrl(anchor.href, { titleOnly: true });
148
- }
149
- continue;
150
- }
151
- addUrl(anchor.href, { titleOnly: true });
152
- }
153
- }
@@ -1,52 +0,0 @@
1
- import type { ScrapeResult } from '@nitpicker/beholder';
2
- /**
3
- * Describes a detected pagination pattern between two consecutive URLs.
4
- */
5
- export interface PaginationPattern {
6
- /** Index within the combined token array (path segments + query values) where the numeric difference was found. */
7
- tokenIndex: number;
8
- /** The numeric increment (always > 0). */
9
- step: number;
10
- /** The number found at `tokenIndex` in the "current" URL. */
11
- currentNumber: number;
12
- }
13
- /**
14
- * Compares two consecutive URL strings and detects a single-token numeric
15
- * pagination pattern (e.g. `/page/1` → `/page/2`, or `?p=1` → `?p=2`).
16
- *
17
- * The algorithm decomposes each URL into tokens (path segments + sorted query values),
18
- * then checks that exactly one token differs and both values are integers with a
19
- * positive step. Returns `null` when no pattern is detected.
20
- *
21
- * WHY single-token constraint: Multi-token differences (e.g. both path and query
22
- * changing) indicate different routes rather than pagination, so they are rejected.
23
- * @param prevUrl - The previously pushed URL (protocol-agnostic, without hash/auth)
24
- * @param currentUrl - The newly discovered URL
25
- * @returns The detected pattern, or `null` if no pagination pattern was found
26
- */
27
- export declare function detectPaginationPattern(prevUrl: string, currentUrl: string): PaginationPattern | null;
28
- /**
29
- * Generates speculative URLs by extrapolating the detected pagination pattern.
30
- *
31
- * Starting from `currentUrl`, applies the pattern's step `count` times to produce
32
- * future page URLs (e.g. if step=1 and currentNumber=2, generates page 3, 4, ...).
33
- * These URLs are pushed into the crawl queue and discarded later if they 404.
34
- * @param pattern - The detected pagination pattern from {@link detectPaginationPattern}
35
- * @param currentUrl - The URL to extrapolate from (protocol-agnostic, without hash/auth)
36
- * @param count - Number of speculative URLs to generate (typically equals concurrency)
37
- * @returns Array of speculative URL strings
38
- */
39
- export declare function generateSpeculativeUrls(pattern: PaginationPattern, currentUrl: string, count: number): string[];
40
- /**
41
- * Determines whether a speculative URL's scrape result should be discarded.
42
- *
43
- * Speculative URLs are pre-emptively pushed into the crawl queue before
44
- * knowing if they exist. This function filters out invalid results:
45
- * - `error` type → discard (server unreachable, timeout, etc.)
46
- * - `ignoreAndSkip` type → discard (matched exclusion rule)
47
- * - `scrapeEnd` with HTTP error status (4xx/5xx) → discard
48
- * - `scrapeEnd` with 2xx/3xx → keep
49
- * @param result - The scrape result for the speculative URL
50
- * @returns `true` if the result should be discarded (not saved to archive)
51
- */
52
- export declare function shouldDiscardSpeculative(result: ScrapeResult): boolean;