@crawlee/utils 4.0.0-beta.99 → 4.0.0-rc.1

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.
@@ -0,0 +1,115 @@
1
+ import { BaseHttpClient } from '@crawlee/http-client';
2
+ import { z } from 'zod';
3
+ /**
4
+ * Accepts any object (including arrays and functions).
5
+ * @internal
6
+ */
7
+ export const anyObject = z.custom((value) => (typeof value === 'object' && value !== null) || typeof value === 'function', { message: 'Invalid input: expected object' });
8
+ /**
9
+ * Accepts any array without validating its items (cheap for huge arrays).
10
+ * @internal
11
+ */
12
+ export const anyArray = z.custom(Array.isArray, { message: 'Invalid input: expected array' });
13
+ /**
14
+ * Accepts any function.
15
+ * @internal
16
+ */
17
+ export const anyFunction = z.custom((value) => typeof value === 'function', {
18
+ message: 'Invalid input: expected function',
19
+ });
20
+ /**
21
+ * Mirrors `ow.number`: `Infinity` is a valid number, `NaN` is not.
22
+ * @internal
23
+ */
24
+ export const anyNumber = z.custom((value) => typeof value === 'number' && !Number.isNaN(value), {
25
+ message: 'Invalid input: expected number',
26
+ });
27
+ /**
28
+ * Accepts any object (including functions) that has all the given keys, own or inherited.
29
+ * @internal
30
+ */
31
+ export function objectWithKeys(keys, message) {
32
+ return z.custom((value) => ((typeof value === 'object' && value !== null) || typeof value === 'function') &&
33
+ keys.every((key) => key in value), {
34
+ message: message ?? `Invalid input: expected an object with keys ${keys.map((key) => `'${key}'`).join(', ')}`,
35
+ });
36
+ }
37
+ /**
38
+ * Accepts only instances of {@link BaseHttpClient} (all Crawlee HTTP clients extend it).
39
+ * @internal
40
+ */
41
+ export const httpClient = z.instanceof(BaseHttpClient);
42
+ /**
43
+ * Accepts any object implementing the CrawleeLogger interface.
44
+ * @internal
45
+ */
46
+ export const logger = objectWithKeys(['child', 'info', 'error', 'warning'], "Expected an object implementing the CrawleeLogger interface (missing one of 'child', 'info', 'error', 'warning'), got something else.");
47
+ /**
48
+ * Accepts any typed array (`Uint8Array`, `Float64Array`, ...), but not a `DataView`.
49
+ * @internal
50
+ */
51
+ export const typedArray = z.custom((value) => ArrayBuffer.isView(value) && !(value instanceof DataView), { message: 'Invalid input: expected a typed array' });
52
+ /**
53
+ * Accepts any non-null, non-array object.
54
+ * @internal
55
+ */
56
+ export const plainObject = z.custom((value) => typeof value === 'object' && value !== null && !Array.isArray(value), { message: 'Invalid input: expected an object' });
57
+ const storageRequestShape = z.looseObject({
58
+ id: z.string(),
59
+ url: z.url({ protocol: /^https?$/ }),
60
+ uniqueKey: z.string(),
61
+ method: z.string().optional(),
62
+ retryCount: z.number().int().optional(),
63
+ handledAt: z.union([z.string(), z.date()]).optional(),
64
+ });
65
+ /**
66
+ * Shape of a request stored in a request queue. Compiled: storage clients validate every request with it.
67
+ * @internal
68
+ */
69
+ export const storageRequest = z.compile(storageRequestShape);
70
+ /**
71
+ * {@link storageRequest} before an id is assigned.
72
+ * @internal
73
+ */
74
+ export const storageRequestWithoutId = z.compile(storageRequestShape.omit({ id: true }));
75
+ /**
76
+ * `z.array(item)` whose top-level type error names the element type — ``expected an array of numbers`` —
77
+ * instead of zod's bare `expected array`. Element failures keep zod's per-index messages, and `elements`
78
+ * is a human-readable plural (`'numbers'`, `'URL patterns'`), since element types cannot be introspected.
79
+ * @internal
80
+ */
81
+ export function arrayOf(item, elements) {
82
+ return z.array(item, {
83
+ error: (issue) => issue.code === 'invalid_type' ? `Invalid input: expected an array of ${elements}` : undefined,
84
+ });
85
+ }
86
+ /**
87
+ * Batch of {@link storageRequestWithoutId}.
88
+ * @internal
89
+ */
90
+ export const storageRequestBatch = z.compile(arrayOf(storageRequestWithoutId, 'requests'));
91
+ /**
92
+ * Options of request queue add/update operations.
93
+ * @internal
94
+ */
95
+ export const requestQueueOperationOptions = z.object({
96
+ forefront: z.boolean().optional(),
97
+ });
98
+ /**
99
+ * Options of key-value store `listKeys`.
100
+ * @internal
101
+ */
102
+ export const keyValueStoreListKeysOptions = z.object({
103
+ prefix: z.string().optional(),
104
+ exclusiveStartKey: z.string().optional(),
105
+ limit: z.number().int().gt(0).optional(),
106
+ });
107
+ /**
108
+ * Options of dataset item listing.
109
+ * @internal
110
+ */
111
+ export const datasetListItemsOptions = z.object({
112
+ desc: z.boolean().optional(),
113
+ limit: z.number().int().optional(),
114
+ offset: z.number().int().optional(),
115
+ });
@@ -1,4 +1,6 @@
1
- import type { BaseHttpClient, CrawleeLogger } from '@crawlee/types';
1
+ import type { BaseHttpClient } from '@crawlee/http-client';
2
+ import type { CrawleeLogger } from '@crawlee/types';
3
+ import { type EnqueueStrategy } from './url.js';
2
4
  interface SitemapUrlData {
3
5
  loc: string;
4
6
  lastmod?: Date;
@@ -54,6 +56,13 @@ export interface ParseSitemapOptions {
54
56
  * If not provided, all nested sitemaps are followed.
55
57
  */
56
58
  nestedSitemapFilter?: (sitemapUrl: string) => boolean;
59
+ /**
60
+ * Keep only sitemap-derived URLs (nested `<sitemap>` and `<url>` entries) matching this strategy
61
+ * relative to the parent sitemap URL; non-`http(s)` schemes are always dropped. Skipped for raw string
62
+ * sources (no parent URL). Pass `'all'` to disable host filtering.
63
+ * @default 'same-hostname'
64
+ */
65
+ enqueueStrategy?: EnqueueStrategy | `${EnqueueStrategy}`;
57
66
  /**
58
67
  * Optional logger for reporting warnings during sitemap parsing.
59
68
  */
@@ -73,6 +82,7 @@ export declare function parseSitemap<T extends ParseSitemapOptions>(initialSourc
73
82
  * ```
74
83
  */
75
84
  export declare class Sitemap {
85
+ #private;
76
86
  readonly urls: string[];
77
87
  constructor(urls: string[]);
78
88
  /**
@@ -94,7 +104,6 @@ export declare class Sitemap {
94
104
  * @param proxyUrl URL of a proxy to be used for fetching sitemap contents
95
105
  */
96
106
  static fromXmlString(content: string, proxyUrl?: string, parseSitemapOptions?: ParseSitemapOptions): Promise<Sitemap>;
97
- private static parse;
98
107
  }
99
108
  /**
100
109
  * Given a list of URLs, discover related sitemap files for these domains by checking the `robots.txt` file,
@@ -3,31 +3,30 @@ import { PassThrough, pipeline, Readable, Transform } from 'node:stream';
3
3
  import { StringDecoder } from 'node:string_decoder';
4
4
  import { createGunzip } from 'node:zlib';
5
5
  import { FetchHttpClient } from '@crawlee/http-client';
6
- import { fileTypeStream } from 'file-type';
7
- import sax from 'sax';
8
- import MIMEType from 'whatwg-mimetype';
6
+ import { MIMEType } from 'whatwg-mimetype';
9
7
  import { mergeAsyncIterables } from './iterables.js';
10
8
  import { RobotsTxtFile } from './robots.js';
9
+ import { filterUrl } from './url.js';
11
10
  class SitemapTxtParser extends Transform {
12
- decoder = new StringDecoder('utf8');
13
- buffer = '';
11
+ #decoder = new StringDecoder('utf8');
12
+ #buffer = '';
14
13
  constructor() {
15
14
  super({
16
15
  readableObjectMode: true,
17
16
  transform: (chunk, _encoding, callback) => {
18
- this.processBuffer(this.decoder.write(chunk), false);
17
+ this.#processBuffer(this.#decoder.write(chunk), false);
19
18
  callback();
20
19
  },
21
20
  flush: (callback) => {
22
- this.processBuffer(this.decoder.end(), true);
21
+ this.#processBuffer(this.#decoder.end(), true);
23
22
  callback();
24
23
  },
25
24
  });
26
25
  }
27
- processBuffer(input, finalize) {
28
- this.buffer += input;
29
- if (finalize || this.buffer.includes('\n')) {
30
- const parts = this.buffer
26
+ #processBuffer(input, finalize) {
27
+ this.#buffer += input;
28
+ if (finalize || this.#buffer.includes('\n')) {
29
+ const parts = this.#buffer
31
30
  .split('\n')
32
31
  .map((part) => part.trim())
33
32
  .filter((part) => part.length > 0);
@@ -35,104 +34,109 @@ class SitemapTxtParser extends Transform {
35
34
  for (const url of parts) {
36
35
  this.push({ type: 'url', loc: url });
37
36
  }
38
- this.buffer = '';
37
+ this.#buffer = '';
39
38
  }
40
39
  else if (parts.length > 0) {
41
40
  for (const url of parts.slice(0, -1)) {
42
41
  this.push({ type: 'url', loc: url });
43
42
  }
44
- this.buffer = parts.at(-1);
43
+ this.#buffer = parts.at(-1);
45
44
  }
46
45
  }
47
46
  }
48
47
  }
49
48
  class SitemapXmlParser extends Transform {
50
- decoder = new StringDecoder('utf8');
51
- parser = new sax.SAXParser(true);
52
- rootTagName;
53
- currentTag = undefined;
54
- url = {};
55
- constructor() {
49
+ #decoder = new StringDecoder('utf8');
50
+ #parser;
51
+ #rootTagName;
52
+ #currentTag = undefined;
53
+ #url = {};
54
+ static async create() {
55
+ const { SAXParser } = (await import('sax')).default;
56
+ return new SitemapXmlParser(new SAXParser(true));
57
+ }
58
+ constructor(parser) {
56
59
  super({
57
60
  readableObjectMode: true,
58
61
  transform: (chunk, _encoding, callback) => {
59
- this.parser.write(this.decoder.write(chunk));
62
+ this.#parser.write(this.#decoder.write(chunk));
60
63
  callback();
61
64
  },
62
65
  flush: (callback) => {
63
- const rest = this.decoder.end();
66
+ const rest = this.#decoder.end();
64
67
  if (rest.length > 0) {
65
- this.parser.write(rest);
68
+ this.#parser.write(rest);
66
69
  }
67
- this.parser.end();
70
+ this.#parser.end();
68
71
  callback();
69
72
  },
70
73
  });
71
- this.parser.onopentag = this.onOpenTag.bind(this);
72
- this.parser.onclosetag = this.onCloseTag.bind(this);
73
- this.parser.ontext = this.onText.bind(this);
74
- this.parser.oncdata = this.onText.bind(this);
75
- this.parser.onerror = this.destroy.bind(this);
74
+ this.#parser = parser;
75
+ this.#parser.onopentag = this.#onOpenTag.bind(this);
76
+ this.#parser.onclosetag = this.#onCloseTag.bind(this);
77
+ this.#parser.ontext = this.#onText.bind(this);
78
+ this.#parser.oncdata = this.#onText.bind(this);
79
+ this.#parser.onerror = this.destroy.bind(this);
76
80
  }
77
- onOpenTag(node) {
78
- if (this.rootTagName !== undefined) {
81
+ #onOpenTag(node) {
82
+ if (this.#rootTagName !== undefined) {
79
83
  if (node.name === 'loc' ||
80
84
  node.name === 'lastmod' ||
81
85
  node.name === 'priority' ||
82
86
  node.name === 'changefreq') {
83
- this.currentTag = node.name;
87
+ this.#currentTag = node.name;
84
88
  }
85
89
  }
86
90
  if (node.name === 'urlset') {
87
- this.rootTagName = 'urlset';
91
+ this.#rootTagName = 'urlset';
88
92
  }
89
93
  if (node.name === 'sitemapindex') {
90
- this.rootTagName = 'sitemapindex';
94
+ this.#rootTagName = 'sitemapindex';
91
95
  }
92
96
  }
93
- onCloseTag(name) {
97
+ #onCloseTag(name) {
94
98
  if (name === 'loc' || name === 'lastmod' || name === 'priority' || name === 'changefreq') {
95
- this.currentTag = undefined;
99
+ this.#currentTag = undefined;
96
100
  }
97
101
  if (name === 'url') {
98
- if (this.url.loc !== undefined) {
99
- this.push({ type: 'url', ...this.url, loc: this.url.loc });
102
+ if (this.#url.loc !== undefined) {
103
+ this.push({ type: 'url', ...this.#url, loc: this.#url.loc });
100
104
  }
101
- this.url = {};
105
+ this.#url = {};
102
106
  }
103
107
  }
104
- onText(text) {
105
- if (this.currentTag === 'loc') {
106
- if (this.rootTagName === 'sitemapindex') {
108
+ #onText(text) {
109
+ if (this.#currentTag === 'loc') {
110
+ if (this.#rootTagName === 'sitemapindex') {
107
111
  this.push({ type: 'sitemapUrl', url: text.trim() });
108
112
  }
109
- if (this.rootTagName === 'urlset') {
110
- this.url ??= {};
111
- this.url.loc = text.trim();
113
+ if (this.#rootTagName === 'urlset') {
114
+ this.#url ??= {};
115
+ this.#url.loc = text.trim();
112
116
  }
113
117
  }
114
118
  text = text.trim();
115
- if (this.currentTag === 'lastmod') {
119
+ if (this.#currentTag === 'lastmod') {
116
120
  const lastmod = new Date(text);
117
121
  if (!Number.isNaN(lastmod.getTime())) {
118
- this.url.lastmod = lastmod;
122
+ this.#url.lastmod = lastmod;
119
123
  }
120
124
  }
121
- if (this.currentTag === 'priority') {
122
- this.url.priority = Number(text);
125
+ if (this.#currentTag === 'priority') {
126
+ this.#url.priority = Number(text);
123
127
  }
124
- if (this.currentTag === 'changefreq') {
128
+ if (this.#currentTag === 'changefreq') {
125
129
  if (['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'].includes(text)) {
126
- this.url.changefreq = text;
130
+ this.#url.changefreq = text;
127
131
  }
128
132
  }
129
133
  }
130
134
  }
131
135
  export async function* parseSitemap(initialSources, proxyUrl, options) {
132
- const { httpClient = new FetchHttpClient(), emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, timeoutMillis: timeout = 30000, reportNetworkErrors = true, nestedSitemapFilter, logger, } = options ?? {};
136
+ const { httpClient = new FetchHttpClient(), emitNestedSitemaps = false, maxDepth = Infinity, sitemapRetries = 3, timeoutMillis: timeout = 30000, reportNetworkErrors = true, nestedSitemapFilter, enqueueStrategy = 'same-hostname', logger, } = options ?? {};
133
137
  const sources = [...initialSources];
134
138
  const visitedSitemapUrls = new Set();
135
- const createParser = (contentType = '', url) => {
139
+ const createParser = async (contentType = '', url) => {
136
140
  let mimeType;
137
141
  try {
138
142
  mimeType = new MIMEType(contentType);
@@ -141,7 +145,7 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
141
145
  mimeType = null;
142
146
  }
143
147
  if (mimeType?.isXML() || url?.pathname.endsWith('.xml')) {
144
- return new SitemapXmlParser();
148
+ return SitemapXmlParser.create();
145
149
  }
146
150
  if (mimeType?.essence === 'text/plain' || url?.pathname.endsWith('.txt')) {
147
151
  return new SitemapTxtParser();
@@ -154,8 +158,10 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
154
158
  continue;
155
159
  }
156
160
  let items = null;
161
+ // Parent URL, parsed once and reused as the origin for the strategy checks below.
162
+ let sitemapUrl;
157
163
  if (source.type === 'url') {
158
- const sitemapUrl = new URL(source.url);
164
+ sitemapUrl = new URL(source.url);
159
165
  visitedSitemapUrls.add(sitemapUrl.toString());
160
166
  let retriesLeft = sitemapRetries + 1;
161
167
  while (retriesLeft-- > 0) {
@@ -181,6 +187,7 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
181
187
  if (sitemapResponse.body === null) {
182
188
  break;
183
189
  }
190
+ const { fileTypeStream } = await import('file-type');
184
191
  const streamWithType = await fileTypeStream(Readable.fromWeb(sitemapResponse.body));
185
192
  if (streamWithType.fileType !== undefined) {
186
193
  contentType = streamWithType.fileType.mime;
@@ -194,7 +201,7 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
194
201
  sitemapUrl.pathname = sitemapUrl.pathname.substring(0, sitemapUrl.pathname.length - 3);
195
202
  }
196
203
  }
197
- items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), createParser(contentType ?? undefined, sitemapUrl), (e) => {
204
+ items = pipeline(streamWithType, isGzipped ? createGunzip() : new PassThrough(), await createParser(contentType ?? undefined, sitemapUrl), (e) => {
198
205
  if (e !== undefined && e !== null) {
199
206
  error = { type: 'parser', error: e };
200
207
  }
@@ -222,7 +229,7 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
222
229
  }
223
230
  }
224
231
  else if (source.type === 'raw') {
225
- items = pipeline(Readable.from([source.content]), createParser('text/xml'), (error) => {
232
+ items = pipeline(Readable.from([source.content]), await createParser('text/xml'), (error) => {
226
233
  if (error !== undefined) {
227
234
  logger?.warning(`Malformed sitemap content: ${error}`);
228
235
  }
@@ -231,18 +238,39 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
231
238
  if (items === null) {
232
239
  continue;
233
240
  }
241
+ // URL entries dropped by the enqueue strategy filter, reported in one warning per sitemap after
242
+ // the loop (per-entry warnings could flood the log; individual drops are logged at debug level).
243
+ let droppedUrlEntries = 0;
234
244
  for await (const item of items) {
235
245
  if (item.type === 'sitemapUrl' && !visitedSitemapUrls.has(item.url)) {
236
246
  if (nestedSitemapFilter && !nestedSitemapFilter(item.url)) {
237
247
  logger?.debug(`Skipping sitemap ${item.url} due to nestedSitemapFilter.`);
238
248
  continue;
239
249
  }
250
+ // Keep only nested sitemaps matching the strategy (and using http(s)) relative to the
251
+ // parent. Raw string sources have no parent URL, so the check is skipped.
252
+ if (source.type === 'url') {
253
+ const { allowed, reason } = filterUrl(item.url, sitemapUrl, enqueueStrategy);
254
+ if (!allowed) {
255
+ logger?.warning(`Skipping nested sitemap ${item.url} (parent ${source.url}): ${reason}.`);
256
+ continue;
257
+ }
258
+ }
240
259
  sources.push({ type: 'url', url: item.url, depth: (source.depth ?? 0) + 1 });
241
260
  if (emitNestedSitemaps) {
242
261
  yield { loc: item.url, originSitemapUrl: null };
243
262
  }
244
263
  }
245
264
  if (item.type === 'url') {
265
+ // Keep only URL entries that match the enqueue strategy relative to the parent (see above).
266
+ if (source.type === 'url') {
267
+ const { allowed, reason } = filterUrl(item.loc, sitemapUrl, enqueueStrategy);
268
+ if (!allowed) {
269
+ droppedUrlEntries++;
270
+ logger?.debug(`Skipping sitemap URL ${item.loc} (parent ${source.url}): ${reason}.`);
271
+ continue;
272
+ }
273
+ }
246
274
  yield {
247
275
  ...item,
248
276
  originSitemapUrl: source.type === 'url'
@@ -251,6 +279,9 @@ export async function* parseSitemap(initialSources, proxyUrl, options) {
251
279
  };
252
280
  }
253
281
  }
282
+ if (droppedUrlEntries > 0 && source.type === 'url') {
283
+ logger?.warning(`Skipped ${droppedUrlEntries} URL(s) from sitemap ${source.url} not matching enqueue strategy '${enqueueStrategy}' (or using a non-http(s) scheme). Enable debug logs to see each skipped URL.`);
284
+ }
254
285
  }
255
286
  }
256
287
  /**
@@ -292,7 +323,7 @@ export class Sitemap {
292
323
  * @param proxyUrl URL of a proxy to be used for fetching sitemap contents
293
324
  */
294
325
  static async load(urls, proxyUrl, parseSitemapOptions) {
295
- return await this.parse((Array.isArray(urls) ? urls : [urls]).map((url) => ({ type: 'url', url })), proxyUrl, parseSitemapOptions);
326
+ return await this.#parse((Array.isArray(urls) ? urls : [urls]).map((url) => ({ type: 'url', url })), proxyUrl, parseSitemapOptions);
296
327
  }
297
328
  /**
298
329
  * Parse XML sitemap content from a string and return URLs of referenced pages. If the sitemap references other sitemaps, they will be loaded via HTTP.
@@ -300,9 +331,9 @@ export class Sitemap {
300
331
  * @param proxyUrl URL of a proxy to be used for fetching sitemap contents
301
332
  */
302
333
  static async fromXmlString(content, proxyUrl, parseSitemapOptions) {
303
- return await this.parse([{ type: 'raw', content }], proxyUrl, parseSitemapOptions);
334
+ return await this.#parse([{ type: 'raw', content }], proxyUrl, parseSitemapOptions);
304
335
  }
305
- static async parse(sources, proxyUrl, parseSitemapOptions) {
336
+ static async #parse(sources, proxyUrl, parseSitemapOptions) {
306
337
  const urls = [];
307
338
  try {
308
339
  for await (const item of parseSitemap(sources, proxyUrl, parseSitemapOptions)) {
@@ -374,7 +405,8 @@ export async function* discoverValidSitemaps(urls, options = {}) {
374
405
  httpClient,
375
406
  logger,
376
407
  });
377
- for (const sitemapUrl of robotsFile.getSitemaps()) {
408
+ // Surface all referenced sitemaps, including cross-host; scoping happens at load time.
409
+ for (const sitemapUrl of robotsFile.getSitemaps({ enqueueStrategy: 'all' })) {
378
410
  if (addSitemapUrl(sitemapUrl)) {
379
411
  yield sitemapUrl;
380
412
  }
@@ -383,7 +415,7 @@ export async function* discoverValidSitemaps(urls, options = {}) {
383
415
  catch (err) {
384
416
  logger?.warning(`Failed to fetch robots.txt file for ${hostname}`, { error: err });
385
417
  }
386
- const sitemapUrl = domainUrls.find((url) => /sitemap\.(?:xml|txt)(?:\.gz)?$/i.test(url));
418
+ const sitemapUrl = domainUrls.find((url) => /sitemap(?:_index)?\.(?:xml|txt)(?:\.gz)?$/i.test(url));
387
419
  if (sitemapUrl !== undefined) {
388
420
  if (addSitemapUrl(sitemapUrl)) {
389
421
  yield sitemapUrl;
@@ -391,6 +423,11 @@ export async function* discoverValidSitemaps(urls, options = {}) {
391
423
  }
392
424
  else {
393
425
  const firstUrl = new URL(domainUrls[0]);
426
+ // The guessed candidates are well-known paths on the same origin, so the query string
427
+ // and fragment of the input URL are not part of them - carrying them over probes (and
428
+ // yields) URLs like `/sitemap.xml?page=2`, which servers may well answer with a 404.
429
+ firstUrl.search = '';
430
+ firstUrl.hash = '';
394
431
  const possibleSitemapPathnames = ['/sitemap.xml', '/sitemap.txt', '/sitemap_index.xml'];
395
432
  const candidateSitemapUrls = possibleSitemapPathnames.map((pathname) => {
396
433
  firstUrl.pathname = pathname;
@@ -225,7 +225,7 @@ export declare const TWITTER_REGEX: RegExp;
225
225
  * ```
226
226
  * import { social } from 'crawlee';
227
227
  *
228
- * const matches = text.match(social.TWITTER_REGEX_STRING);
228
+ * const matches = text.match(social.TWITTER_REGEX_GLOBAL);
229
229
  * if (matches) console.log(`${matches.length} Twitter profiles found!`);
230
230
  * ```
231
231
  */
@@ -326,7 +326,7 @@ export const TWITTER_REGEX = new RegExp(`^${TWITTER_REGEX_STRING}$`, 'i');
326
326
  * ```
327
327
  * import { social } from 'crawlee';
328
328
  *
329
- * const matches = text.match(social.TWITTER_REGEX_STRING);
329
+ * const matches = text.match(social.TWITTER_REGEX_GLOBAL);
330
330
  * if (matches) console.log(`${matches.length} Twitter profiles found!`);
331
331
  * ```
332
332
  */
@@ -590,7 +590,7 @@ export const DISCORD_REGEX_GLOBAL = new RegExp(DISCORD_REGEX_STRING, 'ig');
590
590
  * @return An object with the social handles.
591
591
  */
592
592
  export async function parseHandlesFromHtml(html, data = null) {
593
- const cheerio = await import('cheerio');
593
+ const cheerio = await import('cheerio/slim');
594
594
  const result = {
595
595
  emails: [],
596
596
  phones: [],
@@ -1,4 +1,73 @@
1
1
  import type { SearchParams } from '@crawlee/types';
2
+ /**
3
+ * The different enqueueing strategies available.
4
+ *
5
+ * Depending on the strategy you select, we will only check certain parts of the URLs found. Here is a diagram of each URL part and their name:
6
+ *
7
+ * ```md
8
+ * Protocol Domain
9
+ * ┌────┐ ┌─────────┐
10
+ * https://example.crawlee.dev/...
11
+ * │ └─────────────────┤
12
+ * │ Hostname │
13
+ * │ │
14
+ * └─────────────────────────┘
15
+ * Origin
16
+ *```
17
+ *
18
+ * - The `Protocol` is usually `http` or `https`
19
+ * - The `Domain` represents the path without any possible subdomains to a website. For example, `crawlee.dev` is the domain of `https://example.crawlee.dev/`
20
+ * - The `Hostname` is the full path to a website, including any subdomains. For example, `example.crawlee.dev` is the hostname of `https://example.crawlee.dev/`
21
+ * - The `Origin` is the combination of the `Protocol` and `Hostname`. For example, `https://example.crawlee.dev` is the origin of `https://example.crawlee.dev/`
22
+ */
23
+ export declare enum EnqueueStrategy {
24
+ /**
25
+ * Matches any URLs found
26
+ */
27
+ All = "all",
28
+ /**
29
+ * Matches any URLs that have the same hostname.
30
+ * For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
31
+ * `https://example.com/hello` will not be matched.
32
+ *
33
+ * > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
34
+ */
35
+ SameHostname = "same-hostname",
36
+ /**
37
+ * Matches any URLs that have the same domain as the base URL.
38
+ * For example, `https://wow.an.example.com` and `https://example.com` will both be matched for a base url of
39
+ * `https://example.com`.
40
+ *
41
+ * > This strategy will match both `http` and `https` protocols regardless of the base URL protocol.
42
+ */
43
+ SameDomain = "same-domain",
44
+ /**
45
+ * Matches any URLs that have the same hostname and protocol.
46
+ * For example, `https://wow.example.com/hello` will be matched for a base url of `https://wow.example.com/`, but
47
+ * `http://wow.example.com/hello` will not be matched.
48
+ *
49
+ * > This strategy will ensure the protocol of the base URL is the same as the protocol of the URL to be enqueued.
50
+ */
51
+ SameOrigin = "same-origin"
52
+ }
53
+ /** Reusable suffix for log messages explaining why a non-`http(s)` URL was rejected. */
54
+ export declare const UNSUPPORTED_SCHEME_MESSAGE = "unsupported URL scheme (only http and https are allowed)";
55
+ /**
56
+ * Check whether `target` matches `origin` under the given enqueue `strategy`. The URL scheme is not
57
+ * considered here (use {@link filterUrl} for the combined scheme + strategy check).
58
+ *
59
+ * The `enqueueLinks` implementation in `@crawlee/core` matches the same strategies via glob patterns
60
+ * (see `packages/core/src/enqueue_links/enqueue_links.ts`) — keep the two in sync when changing either.
61
+ */
62
+ export declare function matchesEnqueueStrategy(strategy: EnqueueStrategy | `${EnqueueStrategy}`, target: URL, origin: URL): boolean;
63
+ /**
64
+ * Check whether `target` may be enqueued under `strategy` relative to `origin`: it must use an `http(s)`
65
+ * scheme and match the strategy. On rejection, `reason` is a human-readable message for log output.
66
+ */
67
+ export declare function filterUrl(target: string | URL, origin: string | URL, strategy: EnqueueStrategy | `${EnqueueStrategy}`): {
68
+ allowed: boolean;
69
+ reason?: string;
70
+ };
2
71
  /**
3
72
  * Appends search (query string) parameters to a URL, replacing the original value (if any).
4
73
  *