@crawlee/utils 4.0.0-beta.11 → 4.0.0-beta.110

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 (69) hide show
  1. package/README.md +17 -13
  2. package/index.d.ts +1 -7
  3. package/index.js +1 -6
  4. package/internals/blocked.d.ts +0 -1
  5. package/internals/blocked.js +0 -1
  6. package/internals/cheerio.d.ts +3 -2
  7. package/internals/cheerio.js +4 -5
  8. package/internals/extract-urls.d.ts +5 -1
  9. package/internals/extract-urls.js +8 -5
  10. package/internals/general.d.ts +0 -25
  11. package/internals/general.js +2 -110
  12. package/internals/iterables.d.ts +47 -0
  13. package/internals/iterables.js +96 -0
  14. package/internals/open_graph_parser.d.ts +2 -3
  15. package/internals/open_graph_parser.js +8 -9
  16. package/internals/robots.d.ts +14 -7
  17. package/internals/robots.js +37 -41
  18. package/internals/sitemap.d.ts +65 -8
  19. package/internals/sitemap.js +213 -73
  20. package/internals/social.d.ts +1 -2
  21. package/internals/social.js +7 -5
  22. package/internals/url.d.ts +1 -2
  23. package/internals/url.js +1 -2
  24. package/package.json +6 -6
  25. package/index.d.ts.map +0 -1
  26. package/index.js.map +0 -1
  27. package/internals/blocked.d.ts.map +0 -1
  28. package/internals/blocked.js.map +0 -1
  29. package/internals/cheerio.d.ts.map +0 -1
  30. package/internals/cheerio.js.map +0 -1
  31. package/internals/chunk.d.ts +0 -2
  32. package/internals/chunk.d.ts.map +0 -1
  33. package/internals/chunk.js +0 -40
  34. package/internals/chunk.js.map +0 -1
  35. package/internals/debug.d.ts +0 -31
  36. package/internals/debug.d.ts.map +0 -1
  37. package/internals/debug.js +0 -29
  38. package/internals/debug.js.map +0 -1
  39. package/internals/extract-urls.d.ts.map +0 -1
  40. package/internals/extract-urls.js.map +0 -1
  41. package/internals/general.d.ts.map +0 -1
  42. package/internals/general.js.map +0 -1
  43. package/internals/open_graph_parser.d.ts.map +0 -1
  44. package/internals/open_graph_parser.js.map +0 -1
  45. package/internals/robots.d.ts.map +0 -1
  46. package/internals/robots.js.map +0 -1
  47. package/internals/sitemap.d.ts.map +0 -1
  48. package/internals/sitemap.js.map +0 -1
  49. package/internals/social.d.ts.map +0 -1
  50. package/internals/social.js.map +0 -1
  51. package/internals/system-info/cpu-info.d.ts +0 -64
  52. package/internals/system-info/cpu-info.d.ts.map +0 -1
  53. package/internals/system-info/cpu-info.js +0 -211
  54. package/internals/system-info/cpu-info.js.map +0 -1
  55. package/internals/system-info/memory-info.d.ts +0 -28
  56. package/internals/system-info/memory-info.d.ts.map +0 -1
  57. package/internals/system-info/memory-info.js +0 -118
  58. package/internals/system-info/memory-info.js.map +0 -1
  59. package/internals/system-info/ps-tree.d.ts +0 -18
  60. package/internals/system-info/ps-tree.d.ts.map +0 -1
  61. package/internals/system-info/ps-tree.js +0 -145
  62. package/internals/system-info/ps-tree.js.map +0 -1
  63. package/internals/typedefs.d.ts +0 -5
  64. package/internals/typedefs.d.ts.map +0 -1
  65. package/internals/typedefs.js +0 -9
  66. package/internals/typedefs.js.map +0 -1
  67. package/internals/url.d.ts.map +0 -1
  68. package/internals/url.js.map +0 -1
  69. package/tsconfig.build.tsbuildinfo +0 -1
@@ -1,7 +1,6 @@
1
- import { gotScraping } from 'got-scraping';
1
+ import { FetchHttpClient } from '@crawlee/http-client';
2
2
  import robotsParser from 'robots-parser';
3
3
  import { Sitemap } from './sitemap.js';
4
- let HTTPError;
5
4
  /**
6
5
  * Loads and queries information from a [robots.txt file](https://en.wikipedia.org/wiki/Robots.txt).
7
6
  *
@@ -21,22 +20,27 @@ let HTTPError;
21
20
  * ```
22
21
  */
23
22
  export class RobotsTxtFile {
24
- robots;
25
- proxyUrl;
26
- constructor(robots, proxyUrl) {
27
- this.robots = robots;
28
- this.proxyUrl = proxyUrl;
23
+ #robots;
24
+ #proxyUrl;
25
+ #logger;
26
+ constructor(robots, proxyUrl, logger) {
27
+ this.#robots = robots;
28
+ this.#proxyUrl = proxyUrl;
29
+ this.#logger = logger;
29
30
  }
30
31
  /**
31
32
  * Determine the location of a robots.txt file for a URL and fetch it.
32
33
  * @param url the URL to fetch robots.txt for
33
- * @param [proxyUrl] a proxy to be used for fetching the robots.txt file
34
+ * @param [options] additional options
35
+ * @param [options.signal] an AbortSignal to cancel the request
36
+ * @param [options.timeoutMillis] timeout in milliseconds for the request
37
+ * @param [options.proxyUrl] a proxy to be used for fetching the robots.txt file
34
38
  */
35
- static async find(url, proxyUrl) {
39
+ static async find(url, options) {
36
40
  const robotsTxtFileUrl = new URL(url);
37
41
  robotsTxtFileUrl.pathname = '/robots.txt';
38
42
  robotsTxtFileUrl.search = '';
39
- return RobotsTxtFile.load(robotsTxtFileUrl.toString(), proxyUrl);
43
+ return RobotsTxtFile.load(robotsTxtFileUrl.toString(), options);
40
44
  }
41
45
  /**
42
46
  * Allows providing the URL and robots.txt content explicitly instead of loading it from the target site.
@@ -48,33 +52,28 @@ export class RobotsTxtFile {
48
52
  // @ts-ignore
49
53
  return new RobotsTxtFile(robotsParser(url, content), proxyUrl);
50
54
  }
51
- static async load(url, proxyUrl) {
52
- if (!HTTPError) {
53
- HTTPError = (await import('got-scraping')).HTTPError;
54
- }
55
- try {
56
- const response = await gotScraping({
57
- url,
58
- proxyUrl,
59
- method: 'GET',
60
- responseType: 'text',
61
- });
62
- // @ts-ignore
63
- return new RobotsTxtFile(robotsParser(url.toString(), response.body), proxyUrl);
55
+ static async load(url, options) {
56
+ const { proxyUrl, logger, httpClient = new FetchHttpClient() } = options || {};
57
+ const response = await httpClient.sendRequest(new Request(url, { method: 'GET' }), {
58
+ proxyUrl,
59
+ timeoutMillis: options?.timeoutMillis,
60
+ signal: options?.signal,
61
+ });
62
+ if (response.status < 200 || response.status >= 300) {
63
+ throw new Error(`Failed to load robots.txt from ${url}: HTTP ${response.status}`);
64
64
  }
65
- catch (e) {
66
- if (e instanceof HTTPError && e.response.statusCode === 404) {
67
- return new RobotsTxtFile({
68
- isAllowed() {
69
- return true;
70
- },
71
- getSitemaps() {
72
- return [];
73
- },
74
- }, proxyUrl);
75
- }
76
- throw e;
65
+ if (response.status === 404) {
66
+ return new RobotsTxtFile({
67
+ isAllowed() {
68
+ return true;
69
+ },
70
+ getSitemaps() {
71
+ return [];
72
+ },
73
+ }, proxyUrl, logger);
77
74
  }
75
+ // @ts-ignore
76
+ return new RobotsTxtFile(robotsParser(url.toString(), await response.text()), proxyUrl, logger);
78
77
  }
79
78
  /**
80
79
  * Check if a URL should be crawled by robots.
@@ -82,19 +81,19 @@ export class RobotsTxtFile {
82
81
  * @param [userAgent] relevant user agent, default to `*`
83
82
  */
84
83
  isAllowed(url, userAgent = '*') {
85
- return this.robots.isAllowed(url, userAgent) ?? true; // `undefined` means that there is no explicit rule for the requested URL - assume it's allowed
84
+ return this.#robots.isAllowed(url, userAgent) ?? true; // `undefined` means that there is no explicit rule for the requested URL - assume it's allowed
86
85
  }
87
86
  /**
88
87
  * Get URLs of sitemaps referenced in the robots file.
89
88
  */
90
89
  getSitemaps() {
91
- return this.robots.getSitemaps();
90
+ return this.#robots.getSitemaps();
92
91
  }
93
92
  /**
94
93
  * Parse all the sitemaps referenced in the robots file.
95
94
  */
96
95
  async parseSitemaps() {
97
- return Sitemap.load(this.robots.getSitemaps(), this.proxyUrl);
96
+ return Sitemap.load(this.#robots.getSitemaps(), this.#proxyUrl, { logger: this.#logger });
98
97
  }
99
98
  /**
100
99
  * Get all URLs from all the sitemaps referenced in the robots file. A shorthand for `(await robots.parseSitemaps()).urls`.
@@ -103,6 +102,3 @@ export class RobotsTxtFile {
103
102
  return (await this.parseSitemaps()).urls;
104
103
  }
105
104
  }
106
- // to stay backwards compatible
107
- export { RobotsTxtFile as RobotsFile };
108
- //# sourceMappingURL=robots.js.map
@@ -1,5 +1,4 @@
1
- // @ts-ignore optional peer dependency or compatibility with es2022
2
- import type { Delays } from 'got-scraping';
1
+ import type { BaseHttpClient, CrawleeLogger } from '@crawlee/types';
3
2
  interface SitemapUrlData {
4
3
  loc: string;
5
4
  lastmod?: Date;
@@ -36,9 +35,29 @@ export interface ParseSitemapOptions {
36
35
  */
37
36
  sitemapRetries?: number;
38
37
  /**
39
- * Network timeouts for sitemap fetching. See [Got documentation](https://github.com/sindresorhus/got/blob/main/documentation/6-timeout.md) for more details.
38
+ * Timeout settings for network requests when fetching sitemaps. By default this is `30000` milliseconds (30 seconds).
40
39
  */
41
- networkTimeouts?: Delays;
40
+ timeoutMillis?: number;
41
+ /**
42
+ * If true, the parser will log a warning if it fails to fetch a sitemap due to a network error
43
+ * @default true
44
+ */
45
+ reportNetworkErrors?: boolean;
46
+ /**
47
+ * Custom HTTP client to be used for fetching sitemaps.
48
+ */
49
+ httpClient?: BaseHttpClient;
50
+ /**
51
+ * Optional filter for nested sitemap URLs discovered in sitemap index files.
52
+ * Called with the URL of each child sitemap before it is fetched.
53
+ * Return `true` to include the sitemap, `false` to skip it.
54
+ * If not provided, all nested sitemaps are followed.
55
+ */
56
+ nestedSitemapFilter?: (sitemapUrl: string) => boolean;
57
+ /**
58
+ * Optional logger for reporting warnings during sitemap parsing.
59
+ */
60
+ logger?: CrawleeLogger;
42
61
  }
43
62
  export declare function parseSitemap<T extends ParseSitemapOptions>(initialSources: SitemapSource[], proxyUrl?: string, options?: T): AsyncIterable<T['emitNestedSitemaps'] extends true ? SitemapUrl | NestedSitemap : SitemapUrl>;
44
63
  /**
@@ -62,7 +81,7 @@ export declare class Sitemap {
62
81
  * @param url The domain URL to fetch the sitemap for.
63
82
  * @param proxyUrl A proxy to be used for fetching the sitemap file.
64
83
  */
65
- static tryCommonNames(url: string, proxyUrl?: string): Promise<Sitemap>;
84
+ static tryCommonNames(url: string, proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
66
85
  /**
67
86
  * Fetch sitemap content from given URL or URLs and return URLs of referenced pages.
68
87
  * @param urls sitemap URL(s)
@@ -74,8 +93,46 @@ export declare class Sitemap {
74
93
  * @param content XML sitemap content
75
94
  * @param proxyUrl URL of a proxy to be used for fetching sitemap contents
76
95
  */
77
- static fromXmlString(content: string, proxyUrl?: string): Promise<Sitemap>;
78
- protected static parse(sources: SitemapSource[], proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
96
+ static fromXmlString(content: string, proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
97
+ private static parse;
79
98
  }
99
+ /**
100
+ * Given a list of URLs, discover related sitemap files for these domains by checking the `robots.txt` file,
101
+ * the default `sitemap.xml` & `sitemap.txt` files and the URLs themselves.
102
+ * @param `urls` The list of URLs to discover sitemaps for.
103
+ * @param `options` Options for sitemap discovery
104
+ * @returns An async iterable with the discovered sitemap URLs.
105
+ */
106
+ export declare function discoverValidSitemaps(urls: string[], options?: {
107
+ /**
108
+ * Proxy URL to be used for network requests.
109
+ */
110
+ proxyUrl?: string;
111
+ /**
112
+ * Timeout in milliseconds for the entire `discoverValidSitemaps` call.
113
+ * An `AbortController` is created internally and its signal is passed to every HTTP request,
114
+ * so the whole discovery operation is cancelled once the timeout elapses.
115
+ * Defaults to `60_000` ms (60 seconds) to prevent indefinite hangs.
116
+ */
117
+ timeoutMillis?: number;
118
+ /**
119
+ * An external `AbortSignal` to cancel the entire discovery operation.
120
+ * If both `signal` and `timeout` are provided, the operation is cancelled
121
+ * when either the signal is aborted or the timeout elapses (whichever comes first).
122
+ */
123
+ signal?: AbortSignal;
124
+ /**
125
+ * Timeout in milliseconds for each individual HTTP request during discovery.
126
+ * Defaults to `20000` ms (20 seconds).
127
+ */
128
+ requestTimeoutMillis?: number;
129
+ /**
130
+ * HTTP client to be used for network requests.
131
+ */
132
+ httpClient?: BaseHttpClient;
133
+ /**
134
+ * Optional logger for reporting warnings during sitemap discovery.
135
+ */
136
+ logger?: CrawleeLogger;
137
+ }): AsyncIterable<string>;
80
138
  export {};
81
- //# sourceMappingURL=sitemap.d.ts.map
@@ -2,29 +2,31 @@ import { createHash } from 'node:crypto';
2
2
  import { PassThrough, pipeline, Readable, Transform } from 'node:stream';
3
3
  import { StringDecoder } from 'node:string_decoder';
4
4
  import { createGunzip } from 'node:zlib';
5
+ import { FetchHttpClient } from '@crawlee/http-client';
5
6
  import sax from 'sax';
6
7
  import MIMEType from 'whatwg-mimetype';
7
- import log from '@apify/log';
8
+ import { mergeAsyncIterables } from './iterables.js';
9
+ import { RobotsTxtFile } from './robots.js';
8
10
  class SitemapTxtParser extends Transform {
9
- decoder = new StringDecoder('utf8');
10
- buffer = '';
11
+ #decoder = new StringDecoder('utf8');
12
+ #buffer = '';
11
13
  constructor() {
12
14
  super({
13
15
  readableObjectMode: true,
14
16
  transform: (chunk, _encoding, callback) => {
15
- this.processBuffer(this.decoder.write(chunk), false);
17
+ this.processBuffer(this.#decoder.write(chunk), false);
16
18
  callback();
17
19
  },
18
20
  flush: (callback) => {
19
- this.processBuffer(this.decoder.end(), true);
21
+ this.processBuffer(this.#decoder.end(), true);
20
22
  callback();
21
23
  },
22
24
  });
23
25
  }
24
26
  processBuffer(input, finalize) {
25
- this.buffer += input;
26
- if (finalize || this.buffer.includes('\n')) {
27
- const parts = this.buffer
27
+ this.#buffer += input;
28
+ if (finalize || this.#buffer.includes('\n')) {
29
+ const parts = this.#buffer
28
30
  .split('\n')
29
31
  .map((part) => part.trim())
30
32
  .filter((part) => part.length > 0);
@@ -32,98 +34,101 @@ class SitemapTxtParser extends Transform {
32
34
  for (const url of parts) {
33
35
  this.push({ type: 'url', loc: url });
34
36
  }
35
- this.buffer = '';
37
+ this.#buffer = '';
36
38
  }
37
39
  else if (parts.length > 0) {
38
40
  for (const url of parts.slice(0, -1)) {
39
41
  this.push({ type: 'url', loc: url });
40
42
  }
41
- this.buffer = parts.at(-1);
43
+ this.#buffer = parts.at(-1);
42
44
  }
43
45
  }
44
46
  }
45
47
  }
46
48
  class SitemapXmlParser extends Transform {
47
- decoder = new StringDecoder('utf8');
48
- parser = new sax.SAXParser(true);
49
- rootTagName;
50
- currentTag = undefined;
51
- url = {};
49
+ #decoder = new StringDecoder('utf8');
50
+ #parser = new sax.SAXParser(true);
51
+ #rootTagName;
52
+ #currentTag = undefined;
53
+ #url = {};
52
54
  constructor() {
53
55
  super({
54
56
  readableObjectMode: true,
55
57
  transform: (chunk, _encoding, callback) => {
56
- this.parser.write(this.decoder.write(chunk));
58
+ this.#parser.write(this.#decoder.write(chunk));
57
59
  callback();
58
60
  },
59
61
  flush: (callback) => {
60
- const rest = this.decoder.end();
62
+ const rest = this.#decoder.end();
61
63
  if (rest.length > 0) {
62
- this.parser.write(rest);
64
+ this.#parser.write(rest);
63
65
  }
64
- this.parser.end();
66
+ this.#parser.end();
65
67
  callback();
66
68
  },
67
69
  });
68
- this.parser.onopentag = this.onOpenTag.bind(this);
69
- this.parser.onclosetag = this.onCloseTag.bind(this);
70
- this.parser.ontext = this.onText.bind(this);
71
- this.parser.oncdata = this.onText.bind(this);
72
- this.parser.onerror = this.destroy.bind(this);
70
+ this.#parser.onopentag = this.onOpenTag.bind(this);
71
+ this.#parser.onclosetag = this.onCloseTag.bind(this);
72
+ this.#parser.ontext = this.onText.bind(this);
73
+ this.#parser.oncdata = this.onText.bind(this);
74
+ this.#parser.onerror = this.destroy.bind(this);
73
75
  }
74
76
  onOpenTag(node) {
75
- if (this.rootTagName !== undefined) {
77
+ if (this.#rootTagName !== undefined) {
76
78
  if (node.name === 'loc' ||
77
79
  node.name === 'lastmod' ||
78
80
  node.name === 'priority' ||
79
81
  node.name === 'changefreq') {
80
- this.currentTag = node.name;
82
+ this.#currentTag = node.name;
81
83
  }
82
84
  }
83
85
  if (node.name === 'urlset') {
84
- this.rootTagName = 'urlset';
86
+ this.#rootTagName = 'urlset';
85
87
  }
86
88
  if (node.name === 'sitemapindex') {
87
- this.rootTagName = 'sitemapindex';
89
+ this.#rootTagName = 'sitemapindex';
88
90
  }
89
91
  }
90
92
  onCloseTag(name) {
91
93
  if (name === 'loc' || name === 'lastmod' || name === 'priority' || name === 'changefreq') {
92
- this.currentTag = undefined;
94
+ this.#currentTag = undefined;
93
95
  }
94
- if (name === 'url' && this.url.loc !== undefined) {
95
- this.push({ type: 'url', ...this.url, loc: this.url.loc });
96
- this.url = {};
96
+ if (name === 'url') {
97
+ if (this.#url.loc !== undefined) {
98
+ this.push({ type: 'url', ...this.#url, loc: this.#url.loc });
99
+ }
100
+ this.#url = {};
97
101
  }
98
102
  }
99
103
  onText(text) {
100
- if (this.currentTag === 'loc') {
101
- if (this.rootTagName === 'sitemapindex') {
104
+ if (this.#currentTag === 'loc') {
105
+ if (this.#rootTagName === 'sitemapindex') {
102
106
  this.push({ type: 'sitemapUrl', url: text.trim() });
103
107
  }
104
- if (this.rootTagName === 'urlset') {
105
- this.url ??= {};
106
- this.url.loc = text.trim();
108
+ if (this.#rootTagName === 'urlset') {
109
+ this.#url ??= {};
110
+ this.#url.loc = text.trim();
107
111
  }
108
112
  }
109
113
  text = text.trim();
110
- if (this.currentTag === 'lastmod') {
111
- this.url.lastmod = new Date(text);
114
+ if (this.#currentTag === 'lastmod') {
115
+ const lastmod = new Date(text);
116
+ if (!Number.isNaN(lastmod.getTime())) {
117
+ this.#url.lastmod = lastmod;
118
+ }
112
119
  }
113
- if (this.currentTag === 'priority') {
114
- this.url.priority = Number(text);
120
+ if (this.#currentTag === 'priority') {
121
+ this.#url.priority = Number(text);
115
122
  }
116
- if (this.currentTag === 'changefreq') {
123
+ if (this.#currentTag === 'changefreq') {
117
124
  if (['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'].includes(text)) {
118
- this.url.changefreq = text;
125
+ this.#url.changefreq = text;
119
126
  }
120
127
  }
121
128
  }
122
129
  }
123
130
  export async function* parseSitemap(initialSources, proxyUrl, options) {
124
- const { gotScraping } = await import('got-scraping');
125
- const { fileTypeStream } = await import('file-type');
126
- const { emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, networkTimeouts } = options ?? {};
131
+ const { httpClient = new FetchHttpClient(), emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, timeoutMillis: timeout = 30000, reportNetworkErrors = true, nestedSitemapFilter, logger, } = options ?? {};
127
132
  const sources = [...initialSources];
128
133
  const visitedSitemapUrls = new Set();
129
134
  const createParser = (contentType = '', url) => {
@@ -145,7 +150,6 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
145
150
  while (sources.length > 0) {
146
151
  const source = sources.shift();
147
152
  if ((source?.depth ?? 0) > maxDepth) {
148
- log.debug(`Skipping sitemap ${source.type === 'url' ? source.url : ''} because it reached max depth ${maxDepth}.`);
149
153
  continue;
150
154
  }
151
155
  let items = null;
@@ -155,23 +159,29 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
155
159
  let retriesLeft = sitemapRetries + 1;
156
160
  while (retriesLeft-- > 0) {
157
161
  try {
158
- const sitemapStream = await new Promise((resolve, reject) => {
159
- const request = gotScraping.stream({
160
- url: sitemapUrl,
161
- proxyUrl,
162
+ let sitemapResponse;
163
+ try {
164
+ sitemapResponse = await httpClient.sendRequest(new Request(sitemapUrl, {
162
165
  method: 'GET',
163
- timeout: networkTimeouts,
164
166
  headers: {
165
- accept: 'text/plain, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8',
167
+ accept: '*/*',
166
168
  },
169
+ }), {
170
+ proxyUrl,
171
+ timeoutMillis: timeout,
167
172
  });
168
- request.on('response', () => resolve(request));
169
- request.on('error', reject);
170
- });
173
+ }
174
+ catch (error) {
175
+ sitemapResponse = null;
176
+ }
171
177
  let error = null;
172
- if (sitemapStream.response.statusCode >= 200 && sitemapStream.response.statusCode < 300) {
173
- let contentType = sitemapStream.response.headers['content-type'];
174
- const streamWithType = await fileTypeStream(sitemapStream);
178
+ if (sitemapResponse && sitemapResponse.status >= 200 && sitemapResponse.status < 300) {
179
+ let contentType = sitemapResponse.headers.get('content-type');
180
+ if (sitemapResponse.body === null) {
181
+ break;
182
+ }
183
+ const { fileTypeStream } = await import('file-type');
184
+ const streamWithType = await fileTypeStream(Readable.fromWeb(sitemapResponse.body));
175
185
  if (streamWithType.fileType !== undefined) {
176
186
  contentType = streamWithType.fileType.mime;
177
187
  }
@@ -184,29 +194,37 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
184
194
  sitemapUrl.pathname = sitemapUrl.pathname.substring(0, sitemapUrl.pathname.length - 3);
185
195
  }
186
196
  }
187
- items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), createParser(contentType, sitemapUrl), (e) => {
188
- if (e !== undefined) {
189
- error = e;
197
+ items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), createParser(contentType ?? undefined, sitemapUrl), (e) => {
198
+ if (e !== undefined && e !== null) {
199
+ error = { type: 'parser', error: e };
190
200
  }
191
201
  });
192
202
  }
193
203
  else {
194
- error = new Error(`Failed to fetch sitemap: ${sitemapUrl}, status code: ${sitemapStream.response.statusCode}`);
204
+ error = {
205
+ type: 'fetch',
206
+ error: new Error(`Failed to fetch sitemap: ${sitemapUrl}, status code: ${sitemapResponse?.status}`),
207
+ };
195
208
  }
196
209
  if (error !== null) {
197
- throw error;
210
+ const shouldIgnoreError = error.type === 'fetch' && !reportNetworkErrors;
211
+ if (!shouldIgnoreError) {
212
+ throw error.error;
213
+ }
214
+ }
215
+ else {
216
+ break;
198
217
  }
199
- break;
200
218
  }
201
219
  catch (e) {
202
- log.warning(`Malformed sitemap content: ${sitemapUrl}, ${retriesLeft === 0 ? 'no retries left.' : 'retrying...'} (${e})`);
220
+ logger?.warning(`Malformed sitemap content: ${sitemapUrl}, ${retriesLeft === 0 ? 'no retries left.' : 'retrying...'} (${e})`);
203
221
  }
204
222
  }
205
223
  }
206
224
  else if (source.type === 'raw') {
207
225
  items = pipeline(Readable.from([source.content]), createParser('text/xml'), (error) => {
208
226
  if (error !== undefined) {
209
- log.warning(`Malformed sitemap content: ${error}`);
227
+ logger?.warning(`Malformed sitemap content: ${error}`);
210
228
  }
211
229
  });
212
230
  }
@@ -215,6 +233,10 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
215
233
  }
216
234
  for await (const item of items) {
217
235
  if (item.type === 'sitemapUrl' && !visitedSitemapUrls.has(item.url)) {
236
+ if (nestedSitemapFilter && !nestedSitemapFilter(item.url)) {
237
+ logger?.debug(`Skipping sitemap ${item.url} due to nestedSitemapFilter.`);
238
+ continue;
239
+ }
218
240
  sources.push({ type: 'url', url: item.url, depth: (source.depth ?? 0) + 1 });
219
241
  if (emitNestedSitemaps) {
220
242
  yield { loc: item.url, originSitemapUrl: null };
@@ -254,7 +276,7 @@ export class Sitemap {
254
276
  * @param url The domain URL to fetch the sitemap for.
255
277
  * @param proxyUrl A proxy to be used for fetching the sitemap file.
256
278
  */
257
- static async tryCommonNames(url, proxyUrl) {
279
+ static async tryCommonNames(url, proxyUrl, parseSitemapOptions) {
258
280
  const sitemapUrls = [];
259
281
  const sitemapUrl = new URL(url);
260
282
  sitemapUrl.search = '';
@@ -262,7 +284,7 @@ export class Sitemap {
262
284
  sitemapUrls.push(sitemapUrl.toString());
263
285
  sitemapUrl.pathname = '/sitemap.txt';
264
286
  sitemapUrls.push(sitemapUrl.toString());
265
- return Sitemap.load(sitemapUrls, proxyUrl);
287
+ return Sitemap.load(sitemapUrls, proxyUrl, { reportNetworkErrors: false, ...parseSitemapOptions });
266
288
  }
267
289
  /**
268
290
  * Fetch sitemap content from given URL or URLs and return URLs of referenced pages.
@@ -277,8 +299,8 @@ export class Sitemap {
277
299
  * @param content XML sitemap content
278
300
  * @param proxyUrl URL of a proxy to be used for fetching sitemap contents
279
301
  */
280
- static async fromXmlString(content, proxyUrl) {
281
- return await this.parse([{ type: 'raw', content }], proxyUrl);
302
+ static async fromXmlString(content, proxyUrl, parseSitemapOptions) {
303
+ return await this.parse([{ type: 'raw', content }], proxyUrl, parseSitemapOptions);
282
304
  }
283
305
  static async parse(sources, proxyUrl, parseSitemapOptions) {
284
306
  const urls = [];
@@ -287,10 +309,128 @@ export class Sitemap {
287
309
  urls.push(item.loc);
288
310
  }
289
311
  }
290
- catch {
312
+ catch (e) {
313
+ parseSitemapOptions?.logger?.warning(`Sitemap.load: Failed to load sitemap, returning empty result. (${e})`);
291
314
  return new Sitemap([]);
292
315
  }
293
316
  return new Sitemap(urls);
294
317
  }
295
318
  }
296
- //# sourceMappingURL=sitemap.js.map
319
+ /**
320
+ * Given a list of URLs, discover related sitemap files for these domains by checking the `robots.txt` file,
321
+ * the default `sitemap.xml` & `sitemap.txt` files and the URLs themselves.
322
+ * @param `urls` The list of URLs to discover sitemaps for.
323
+ * @param `options` Options for sitemap discovery
324
+ * @returns An async iterable with the discovered sitemap URLs.
325
+ */
326
+ export async function* discoverValidSitemaps(urls, options = {}) {
327
+ const { proxyUrl, timeoutMillis = 60_000, signal: externalSignal, requestTimeoutMillis = 20_000, httpClient = new FetchHttpClient(), logger, } = options;
328
+ const controller = new AbortController();
329
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMillis);
330
+ const onExternalAbort = () => controller.abort();
331
+ if (externalSignal) {
332
+ if (externalSignal.aborted) {
333
+ controller.abort();
334
+ }
335
+ else {
336
+ externalSignal.addEventListener('abort', onExternalAbort, { once: true });
337
+ }
338
+ }
339
+ const signal = controller.signal;
340
+ const sitemapUrls = new Set();
341
+ const addSitemapUrl = (url) => {
342
+ const sizeBefore = sitemapUrls.size;
343
+ sitemapUrls.add(url);
344
+ if (sitemapUrls.size > sizeBefore) {
345
+ return url;
346
+ }
347
+ return undefined;
348
+ };
349
+ const urlExists = async (url) => {
350
+ if (!httpClient) {
351
+ return false;
352
+ }
353
+ try {
354
+ const response = await httpClient.sendRequest(new Request(url, { method: 'HEAD' }), {
355
+ proxyUrl,
356
+ timeoutMillis: requestTimeoutMillis,
357
+ signal,
358
+ });
359
+ return response.status >= 200 && response.status < 400;
360
+ }
361
+ catch {
362
+ return false;
363
+ }
364
+ };
365
+ const discoverSitemapsForDomainUrls = async function* (hostname, domainUrls) {
366
+ if (!hostname) {
367
+ return;
368
+ }
369
+ try {
370
+ const robotsFile = await RobotsTxtFile.find(domainUrls[0], {
371
+ proxyUrl,
372
+ timeoutMillis: requestTimeoutMillis,
373
+ signal,
374
+ httpClient,
375
+ logger,
376
+ });
377
+ for (const sitemapUrl of robotsFile.getSitemaps()) {
378
+ if (addSitemapUrl(sitemapUrl)) {
379
+ yield sitemapUrl;
380
+ }
381
+ }
382
+ }
383
+ catch (err) {
384
+ logger?.warning(`Failed to fetch robots.txt file for ${hostname}`, { error: err });
385
+ }
386
+ const sitemapUrl = domainUrls.find((url) => /sitemap\.(?:xml|txt)(?:\.gz)?$/i.test(url));
387
+ if (sitemapUrl !== undefined) {
388
+ if (addSitemapUrl(sitemapUrl)) {
389
+ yield sitemapUrl;
390
+ }
391
+ }
392
+ else {
393
+ const firstUrl = new URL(domainUrls[0]);
394
+ const possibleSitemapPathnames = ['/sitemap.xml', '/sitemap.txt', '/sitemap_index.xml'];
395
+ const candidateSitemapUrls = possibleSitemapPathnames.map((pathname) => {
396
+ firstUrl.pathname = pathname;
397
+ return firstUrl.toString();
398
+ });
399
+ const candidateResults = await Promise.allSettled(candidateSitemapUrls.map(urlExists));
400
+ for (const [index, result] of candidateResults.entries()) {
401
+ const candidateSitemapUrl = candidateSitemapUrls[index];
402
+ if (result.status === 'fulfilled') {
403
+ if (result.value && addSitemapUrl(candidateSitemapUrl)) {
404
+ yield candidateSitemapUrl;
405
+ }
406
+ }
407
+ else {
408
+ logger?.debug(`Failed to check sitemap candidate ${candidateSitemapUrl} for ${hostname}`, {
409
+ error: result.reason,
410
+ });
411
+ }
412
+ }
413
+ }
414
+ };
415
+ const groupedUrls = urls.reduce((acc, url) => {
416
+ const hostname = new URL(url)?.hostname ?? '';
417
+ acc[hostname] ??= [];
418
+ acc[hostname].push(url);
419
+ return acc;
420
+ }, {});
421
+ const iterables = Object.entries(groupedUrls).map(([hostname, domainUrls]) => discoverSitemapsForDomainUrls(hostname, domainUrls));
422
+ const discoveredUrls = new Set();
423
+ try {
424
+ for await (const url of mergeAsyncIterables(...iterables)) {
425
+ if (discoveredUrls.has(url)) {
426
+ continue;
427
+ }
428
+ discoveredUrls.add(url);
429
+ yield url;
430
+ }
431
+ }
432
+ finally {
433
+ clearTimeout(timeoutHandle);
434
+ externalSignal?.removeEventListener('abort', onExternalAbort);
435
+ }
436
+ }