@crawlee/core 4.0.0-beta.121 → 4.0.0-beta.123

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 (45) hide show
  1. package/autoscaling/autoscaled_pool.js +20 -12
  2. package/autoscaling/concurrency_system.d.ts +2 -2
  3. package/autoscaling/concurrency_system.js +31 -20
  4. package/autoscaling/index.d.ts +1 -1
  5. package/autoscaling/index.js +1 -1
  6. package/autoscaling/load_signal.d.ts +7 -6
  7. package/autoscaling/load_signal.js +2 -1
  8. package/autoscaling/snapshotter.d.ts +6 -6
  9. package/autoscaling/snapshotter.js +9 -9
  10. package/autoscaling/{client_load_signal.d.ts → storage_backend_load_signal.d.ts} +13 -12
  11. package/autoscaling/{client_load_signal.js → storage_backend_load_signal.js} +11 -11
  12. package/autoscaling/system_status.d.ts +8 -8
  13. package/autoscaling/system_status.js +2 -2
  14. package/configuration.d.ts +15 -15
  15. package/configuration.js +3 -3
  16. package/crawlers/crawler_commons.d.ts +8 -54
  17. package/crawlers/statistics.d.ts +1 -1
  18. package/crawlers/statistics.js +14 -14
  19. package/debug.js +4 -4
  20. package/enqueue_links/enqueue_links.d.ts +33 -61
  21. package/enqueue_links/enqueue_links.js +35 -152
  22. package/enqueue_links/shared.d.ts +17 -4
  23. package/enqueue_links/shared.js +28 -1
  24. package/memory-storage/resource-clients/dataset.js +2 -8
  25. package/memory-storage/resource-clients/key-value-store.js +23 -26
  26. package/memory-storage/resource-clients/request-queue.js +9 -22
  27. package/package.json +7 -8
  28. package/proxy_configuration.js +10 -6
  29. package/request.d.ts +2 -2
  30. package/request.js +44 -31
  31. package/router.d.ts +5 -5
  32. package/serialization.js +6 -4
  33. package/session_pool/session.js +22 -20
  34. package/session_pool/session_pool.js +20 -17
  35. package/storages/dataset.js +11 -9
  36. package/storages/key_value_store.js +30 -27
  37. package/storages/request_list.d.ts +2 -1
  38. package/storages/request_list.js +26 -21
  39. package/storages/request_queue.js +64 -59
  40. package/storages/sitemap_request_loader.d.ts +1 -1
  41. package/storages/sitemap_request_loader.js +22 -22
  42. package/storages/throttling_request_manager.js +11 -9
  43. package/storages/utils.d.ts +2 -1
  44. package/validators.d.ts +22 -25
  45. package/validators.js +13 -25
@@ -1,12 +1,12 @@
1
1
  import type { Dictionary, HttpRequestOptions, ISession, ProxyInfo, SendRequestOptions } from '@crawlee/types';
2
- import type { ReadonlyDeep, SetRequired } from 'type-fest';
3
- import type { EnqueueLinksOptions } from '../enqueue_links/enqueue_links.js';
2
+ import type { ReadonlyDeep } from 'type-fest';
3
+ import type { EnqueueUrlsOptions } from '../enqueue_links/enqueue_links.js';
4
4
  import type { CrawleeLogger } from '../log.js';
5
5
  import type { Request, RequestOptions, Source } from '../request.js';
6
6
  import type { StorageIdentifier } from '../storages/storage_instance_manager.js';
7
7
  import type { Dataset } from '../storages/dataset.js';
8
8
  import type { KeyValueStore } from '../storages/key_value_store.js';
9
- import type { RequestQueueOperationOptions } from '../storages/request_queue.js';
9
+ import type { AddRequestsBatchedResult } from '../storages/request_queue.js';
10
10
  /** @internal */
11
11
  export type IsAny<T> = 0 extends 1 & T ? true : false;
12
12
  /**
@@ -37,7 +37,7 @@ export type TypedRequestsLike<Routes extends Record<keyof Routes, Dictionary>> =
37
37
  * The label-aware `addRequests` method signature exposed on a request handler's context when the crawler is
38
38
  * bound to a typed router. Mirrors {@link RestrictedCrawlingContext.addRequests} with typed sources.
39
39
  */
40
- export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<RequestQueueOperationOptions>) => Promise<void>;
40
+ export type TypedContextAddRequests<Routes extends Record<keyof Routes, Dictionary>> = (requestsLike: ReadonlyDeep<LabeledSource<Routes>[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
41
41
  /**
42
42
  * An `enqueueLinks`-options object with its `label`/`userData` retyped according to a router's route map: a
43
43
  * declared `label` requires the matching `userData` shape (unknown labels are rejected), while unlabeled
@@ -87,36 +87,15 @@ export interface RestrictedCrawlingContext<UserData extends Dictionary = Diction
87
87
  */
88
88
  pushData(data: ReadonlyDeep<Parameters<Dataset['pushData']>[0]>, datasetIdentifier?: string | StorageIdentifier): Promise<void>;
89
89
  /**
90
- * This function automatically finds and enqueues links from the current page, adding them to the {@link RequestQueue}
91
- * currently used by the crawler.
90
+ * Add requests directly to the request queue currently used by the crawler.
92
91
  *
93
- * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
94
- *
95
- * Check out the [Crawl a website with relative links](https://crawlee.dev/js/docs/examples/crawl-relative-links) example
96
- * for more details regarding its usage.
97
- *
98
- * **Example usage**
99
- *
100
- * ```ts
101
- * async requestHandler({ enqueueLinks }) {
102
- * await enqueueLinks({
103
- * include: [
104
- * 'https://www.example.com/handbags/*',
105
- * ],
106
- * });
107
- * },
108
- * ```
109
- *
110
- * @param [options] All `enqueueLinks()` parameters are passed via an options object.
111
- */
112
- enqueueLinks: (options: ReadonlyDeep<Omit<SetRequired<EnqueueLinksOptions, 'urls'>, 'requestManager' | 'robotsTxtFile'>>) => Promise<unknown>;
113
- /**
114
- * Add requests directly to the request queue.
92
+ * Optionally, the function allows you to filter the target URLs using an array of glob or regexp patterns,
93
+ * the same way {@link CrawlingContext.enqueueLinks|`enqueueLinks`} does for extracted links.
115
94
  *
116
95
  * @param requests The requests to add
117
96
  * @param options Options for the request queue
118
97
  */
119
- addRequests: (requestsLike: ReadonlyDeep<(string | Source)[]>, options?: ReadonlyDeep<RequestQueueOperationOptions>) => Promise<void>;
98
+ addRequests: (requestsLike: ReadonlyDeep<(string | Source)[]>, options?: ReadonlyDeep<EnqueueUrlsOptions>) => Promise<AddRequestsBatchedResult>;
120
99
  /**
121
100
  * Returns the state - a piece of mutable persistent data shared across all the request handler runs.
122
101
  */
@@ -131,31 +110,6 @@ export interface RestrictedCrawlingContext<UserData extends Dictionary = Diction
131
110
  log: CrawleeLogger;
132
111
  }
133
112
  export interface CrawlingContext<UserData extends Dictionary = Dictionary> extends RestrictedCrawlingContext<UserData> {
134
- /**
135
- * This function automatically finds and enqueues links from the current page, adding them to the {@link RequestQueue}
136
- * currently used by the crawler.
137
- *
138
- * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
139
- *
140
- * Check out the [Crawl a website with relative links](https://crawlee.dev/js/docs/examples/crawl-relative-links) example
141
- * for more details regarding its usage.
142
- *
143
- * **Example usage**
144
- *
145
- * ```ts
146
- * async requestHandler({ enqueueLinks }) {
147
- * await enqueueLinks({
148
- * include: [
149
- * 'https://www.example.com/handbags/*',
150
- * ],
151
- * });
152
- * },
153
- * ```
154
- *
155
- * @param [options] All `enqueueLinks()` parameters are passed via an options object.
156
- * @returns Promise that resolves to {@link BatchAddRequestsResult} object.
157
- */
158
- enqueueLinks(options: ReadonlyDeep<Omit<SetRequired<EnqueueLinksOptions, 'urls'>, 'requestManager' | 'robotsTxtFile'>> & Pick<EnqueueLinksOptions, 'requestManager' | 'robotsTxtFile'>): Promise<unknown>;
159
113
  /**
160
114
  * Fires HTTP request via the internal HTTP client, allowing to override the request options on the fly.
161
115
  *
@@ -1,5 +1,5 @@
1
1
  import type { CrawleeLogger } from '../log.js';
2
- import type { KeyValueStore } from '../storages/key_value_store.js';
2
+ import { KeyValueStore } from '../storages/key_value_store.js';
3
3
  import { ErrorTracker } from './error_tracker.js';
4
4
  /**
5
5
  * Persistence-related options to control how and when crawler's data gets persisted.
@@ -1,7 +1,8 @@
1
- import ow from 'ow';
2
1
  import { z } from 'zod';
3
2
  import { RecoverableState } from '../recoverable_state.js';
4
3
  import { serviceLocator } from '../service_locator.js';
4
+ import { KeyValueStore } from '../storages/key_value_store.js';
5
+ import { parseArgument, schemas, validators } from '../validators.js';
5
6
  import { ErrorTracker } from './error_tracker.js';
6
7
  /**
7
8
  * @ignore
@@ -17,6 +18,16 @@ class Job {
17
18
  return this.#durationMillis;
18
19
  }
19
20
  }
21
+ const statisticsOptionsSchema = z.strictObject({
22
+ logIntervalSecs: schemas.anyNumber.default(60),
23
+ logMessage: z.string().default('Statistics'),
24
+ log: validators.logger.optional(),
25
+ keyValueStore: z.instanceof(KeyValueStore).optional(),
26
+ // `schemas.anyObject` passes values through by reference (object schemas return a pruned plain copy).
27
+ persistenceOptions: schemas.anyObject.default(() => ({ enable: true })),
28
+ saveErrorSnapshots: z.boolean().default(false),
29
+ id: z.union([schemas.anyNumber, z.string()]).optional(),
30
+ });
20
31
  const errorTrackerConfig = {
21
32
  showErrorCode: true,
22
33
  showErrorName: true,
@@ -184,21 +195,10 @@ export class Statistics {
184
195
  * persistence or error snapshots, share it across sequential runs, or subclass it to track extra fields.
185
196
  */
186
197
  constructor(options = {}) {
187
- ow(options, ow.object.exactShape({
188
- logIntervalSecs: ow.optional.number,
189
- logMessage: ow.optional.string,
190
- log: ow.optional.object,
191
- keyValueStore: ow.optional.object,
192
- persistenceOptions: ow.optional.object,
193
- saveErrorSnapshots: ow.optional.boolean,
194
- id: ow.optional.any(ow.number, ow.string),
195
- }));
196
- const { logIntervalSecs = 60, logMessage = 'Statistics', keyValueStore, persistenceOptions = {
197
- enable: true,
198
- }, saveErrorSnapshots = false, id, } = options;
198
+ const { logIntervalSecs, logMessage, log, keyValueStore, persistenceOptions, saveErrorSnapshots, id } = parseArgument(options, statisticsOptionsSchema);
199
199
  this.id = id ?? String(Statistics.id++);
200
200
  this.persistStateKey = `CRAWLEE_CRAWLER_STATISTICS_${this.id}`;
201
- this.log = (options.log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
201
+ this.log = (log ?? serviceLocator.getLogger()).child({ prefix: 'Statistics' });
202
202
  this.errorTracker = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
203
203
  this.errorTrackerRetry = new ErrorTracker({ ...errorTrackerConfig, saveErrorSnapshots });
204
204
  this.#logIntervalMillis = logIntervalSecs * 1000;
package/debug.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { inspect } from 'node:util';
2
- import ow from 'ow';
2
+ import { parseArgument, schemas } from './validators.js';
3
3
  /**
4
4
  * Creates a standardized debug info from request and response. This info is usually added to dataset under the hidden `#debug` field.
5
5
  *
@@ -12,9 +12,9 @@ import ow from 'ow';
12
12
  * @internal
13
13
  */
14
14
  export function createRequestDebugInfo(request, response = {}, additionalFields = {}) {
15
- ow(request, ow.object);
16
- ow(response, ow.object);
17
- ow(additionalFields, ow.object);
15
+ parseArgument(request, schemas.anyObject);
16
+ parseArgument(response, schemas.anyObject);
17
+ parseArgument(additionalFields, schemas.anyObject);
18
18
  return {
19
19
  requestId: request.id,
20
20
  url: request.url,
@@ -1,19 +1,24 @@
1
- import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
2
- import { type RobotsTxtFile } from '@crawlee/utils';
3
- import type { SetRequired } from 'type-fest';
4
- import { Request } from '../request.js';
5
- import type { IRequestManager } from '../storages/request_manager.js';
6
- import type { AddRequestsBatchedOptions, AddRequestsBatchedResult, RequestQueueOperationOptions } from '../storages/request_queue.js';
7
- import type { RequestTransform, SkippedRequestCallback, UrlPatternInput } from './shared.js';
8
- export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
1
+ import type { Dictionary } from '@crawlee/types';
2
+ import type { RequestQueueOperationOptions } from '../storages/request_queue.js';
3
+ import type { RequestTransform, SkippedRequestCallback, UrlPatternInput, UrlPatternObject } from './shared.js';
4
+ /**
5
+ * Options shared by the `extractLinks()` context helper across crawler types.
6
+ */
7
+ export interface ExtractLinksOptions {
8
+ /** A CSS selector matching links to be extracted. */
9
+ selector?: string;
10
+ /**
11
+ * A base URL that will be used to resolve relative URLs when using Cheerio. Ignored when using Puppeteer,
12
+ * since the relative URL resolution is done inside the browser automatically.
13
+ */
14
+ baseUrl?: string;
15
+ }
16
+ /**
17
+ * Options accepted by the `enqueueUrls()` context helper exposed by `BasicCrawler`.
18
+ */
19
+ export interface EnqueueUrlsOptions extends RequestQueueOperationOptions {
9
20
  /** Limit the amount of actually enqueued URLs to this number. Useful for testing across the entire crawling scope. */
10
21
  limit?: number;
11
- /** An array of URLs to enqueue. */
12
- urls?: readonly string[];
13
- /** A request manager to which the URLs will be enqueued. */
14
- requestManager?: IRequestManager;
15
- /** A CSS selector matching links to be enqueued. */
16
- selector?: string;
17
22
  /** Sets {@link Request.userData} for newly enqueued requests. */
18
23
  userData?: Dictionary;
19
24
  /**
@@ -30,8 +35,7 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
30
35
  */
31
36
  skipNavigation?: boolean;
32
37
  /**
33
- * A base URL that will be used to resolve relative URLs when using Cheerio. Ignored when using Puppeteer,
34
- * since the relative URL resolution is done inside the browser automatically.
38
+ * A base URL that will be used to resolve relative URLs.
35
39
  */
36
40
  baseUrl?: string;
37
41
  /**
@@ -42,11 +46,11 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
42
46
  * Glob matching is always case-insensitive.
43
47
  * If you need case-sensitive matching, use a `RegExp`.
44
48
  *
45
- * The patterns are combined with the {@link EnqueueLinksOptions.strategy|`strategy`} using AND logic - a URL
49
+ * The patterns are combined with the {@link EnqueueUrlsOptions.strategy|`strategy`} using AND logic - a URL
46
50
  * must match at least one `include` pattern **and** satisfy the strategy to be enqueued. To match URLs across
47
51
  * hostnames, pass an explicit {@link EnqueueStrategy.All} strategy.
48
52
  *
49
- * If `undefined`, the links are enqueued based on the {@link EnqueueLinksOptions.strategy|`strategy`} alone.
53
+ * If `undefined`, the links are enqueued based on the {@link EnqueueUrlsOptions.strategy|`strategy`} alone.
50
54
  * Passing an empty array is not allowed.
51
55
  */
52
56
  include?: readonly UrlPatternInput[];
@@ -106,25 +110,12 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
106
110
  *
107
111
  * @default EnqueueStrategy.SameHostname
108
112
  */
109
- strategy?: EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
113
+ strategy?: EnqueueStrategyOption;
110
114
  /**
111
115
  * By default, only the first batch (1000) of found requests will be added to the queue before resolving the call.
112
116
  * You can use this option to wait for adding all of them.
113
117
  */
114
118
  waitForAllRequestsToBeAdded?: boolean;
115
- /**
116
- * RobotsTxtFile instance for the current request that triggered the `enqueueLinks`.
117
- * If provided, disallowed URLs will be ignored.
118
- */
119
- robotsTxtFile?: Pick<RobotsTxtFile, 'isAllowed'>;
120
- /**
121
- * Mirrors {@link BasicCrawlerOptions.respectRobotsTxtFile}: pass `false` to disable filtering or
122
- * `{ userAgent }` to evaluate rules for a specific user-agent. Defaults to `*` when
123
- * {@link EnqueueLinksOptions.robotsTxtFile|`robotsTxtFile`} is provided.
124
- */
125
- respectRobotsTxtFile?: boolean | {
126
- userAgent?: string;
127
- };
128
119
  /**
129
120
  * When a request is skipped for some reason, you can use this callback to act on it.
130
121
  * This is currently fired for requests skipped
@@ -134,6 +125,8 @@ export interface EnqueueLinksOptions extends RequestQueueOperationOptions {
134
125
  */
135
126
  onSkippedRequest?: SkippedRequestCallback;
136
127
  }
128
+ /** The combined options accepted by a crawler context's `enqueueLinks()` helper: `extractLinks()` + `enqueueUrls()`. */
129
+ export type EnqueueLinksOptions = ExtractLinksOptions & EnqueueUrlsOptions;
137
130
  /**
138
131
  * The different enqueueing strategies available.
139
132
  *
@@ -185,34 +178,8 @@ export declare enum EnqueueStrategy {
185
178
  */
186
179
  SameOrigin = "same-origin"
187
180
  }
188
- /**
189
- * This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
190
- * you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
191
- *
192
- * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
193
- *
194
- * **Example usage**
195
- *
196
- * ```javascript
197
- * await enqueueLinks({
198
- * urls: aListOfFoundUrls,
199
- * requestManager,
200
- * selector: 'a.product-detail',
201
- * include: [
202
- * 'https://www.example.com/handbags/*',
203
- * 'https://www.example.com/purses/*'
204
- * ],
205
- * });
206
- * ```
207
- *
208
- * @param options All `enqueueLinks()` parameters are passed via an options object.
209
- * @returns Promise that resolves to {@link BatchAddRequestsResult} object.
210
- */
211
- export declare function enqueueLinks(options: SetRequired<Omit<EnqueueLinksOptions, 'requestManager'>, 'urls'> & {
212
- requestManager: {
213
- addRequestsBatched: (requests: Request<Dictionary>[], options: AddRequestsBatchedOptions) => Promise<AddRequestsBatchedResult>;
214
- };
215
- }): Promise<BatchAddRequestsResult>;
181
+ /** The `strategy` option accepted by {@link ExtractLinksOptions} and {@link EnqueueUrlsOptions}. */
182
+ export type EnqueueStrategyOption = EnqueueStrategy | 'all' | 'same-domain' | 'same-hostname' | 'same-origin';
216
183
  /**
217
184
  * @internal
218
185
  * This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
@@ -227,7 +194,12 @@ export declare function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy
227
194
  */
228
195
  export interface ResolveBaseUrl {
229
196
  userProvidedBaseUrl?: string;
230
- enqueueStrategy?: EnqueueLinksOptions['strategy'];
197
+ enqueueStrategy?: EnqueueStrategyOption;
231
198
  originalRequestUrl: string;
232
199
  finalRequestUrl?: string;
233
200
  }
201
+ /**
202
+ * @internal
203
+ * Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
204
+ */
205
+ export declare function buildEnqueueStrategyPatterns(baseUrl: string, strategy: EnqueueStrategyOption): UrlPatternObject[];
@@ -1,7 +1,4 @@
1
- import ow from 'ow';
2
1
  import { getDomain } from 'tldts';
3
- import { Request } from '../request.js';
4
- import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, filterRequestOptionsByPatterns, } from './shared.js';
5
2
  /**
6
3
  * The different enqueueing strategies available.
7
4
  *
@@ -54,155 +51,6 @@ export var EnqueueStrategy;
54
51
  */
55
52
  EnqueueStrategy["SameOrigin"] = "same-origin";
56
53
  })(EnqueueStrategy || (EnqueueStrategy = {}));
57
- /**
58
- * This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
59
- * you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
60
- *
61
- * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
62
- *
63
- * **Example usage**
64
- *
65
- * ```javascript
66
- * await enqueueLinks({
67
- * urls: aListOfFoundUrls,
68
- * requestManager,
69
- * selector: 'a.product-detail',
70
- * include: [
71
- * 'https://www.example.com/handbags/*',
72
- * 'https://www.example.com/purses/*'
73
- * ],
74
- * });
75
- * ```
76
- *
77
- * @param options All `enqueueLinks()` parameters are passed via an options object.
78
- * @returns Promise that resolves to {@link BatchAddRequestsResult} object.
79
- */
80
- export async function enqueueLinks(options) {
81
- if (!options || Object.keys(options).length === 0) {
82
- throw new RangeError([
83
- 'enqueueLinks() was called without the required options. You can only do that when you use the `crawlingContext.enqueueLinks()` method in request handlers.',
84
- 'Check out our guide on how to use enqueueLinks() here: https://crawlee.dev/js/docs/examples/crawl-relative-links',
85
- ].join('\n'));
86
- }
87
- const urlPatternValidator = ow.any(ow.string, ow.regExp, ow.object.hasKeys('glob'), ow.object.hasKeys('regexp'));
88
- ow(options, ow.object.exactShape({
89
- urls: ow.array.ofType(ow.string),
90
- requestManager: ow.object.hasKeys('addRequestsBatched'),
91
- robotsTxtFile: ow.optional.object.hasKeys('isAllowed'),
92
- respectRobotsTxtFile: ow.optional.any(ow.boolean, ow.object.exactShape({ userAgent: ow.optional.string })),
93
- onSkippedRequest: ow.optional.function,
94
- forefront: ow.optional.boolean,
95
- skipNavigation: ow.optional.boolean,
96
- sessionId: ow.optional.string,
97
- limit: ow.optional.number,
98
- selector: ow.optional.string,
99
- baseUrl: ow.optional.string,
100
- userData: ow.optional.object,
101
- label: ow.optional.string,
102
- include: ow.optional.array.minLength(1).ofType(urlPatternValidator),
103
- exclude: ow.optional.array.ofType(urlPatternValidator),
104
- transformRequestFunction: ow.optional.function,
105
- strategy: ow.optional.string.oneOf(Object.values(EnqueueStrategy)),
106
- waitForAllRequestsToBeAdded: ow.optional.boolean,
107
- }));
108
- const { requestManager, limit, urls, include, exclude, transformRequestFunction, forefront, waitForAllRequestsToBeAdded, robotsTxtFile, onSkippedRequest, } = options;
109
- const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
110
- const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
111
- // The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
112
- // (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
113
- options.strategy ??= EnqueueStrategy.SameHostname;
114
- const enqueueStrategyPatterns = [];
115
- if (options.baseUrl) {
116
- const url = new URL(options.baseUrl);
117
- switch (options.strategy) {
118
- case EnqueueStrategy.SameHostname:
119
- // We need to get the origin of the passed in domain in the event someone sets baseUrl
120
- // to an url like https://example.com/deep/default/path and one of the found urls is an
121
- // absolute relative path (/path/to/page)
122
- enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
123
- break;
124
- case EnqueueStrategy.SameDomain: {
125
- // Get the actual hostname from the base url
126
- const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
127
- if (baseUrlHostname) {
128
- // We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
129
- url.hostname = baseUrlHostname;
130
- enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) }, { glob: ignoreHttpSchema(`${url.origin}/**`) });
131
- }
132
- else {
133
- // We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
134
- // as SameDomainAndSubdomain
135
- enqueueStrategyPatterns.push({ glob: ignoreHttpSchema(`${url.origin}/**`) });
136
- }
137
- break;
138
- }
139
- case EnqueueStrategy.SameOrigin: {
140
- // The same behavior as SameHostname, but respecting the protocol of the URL
141
- enqueueStrategyPatterns.push({ glob: `${url.origin}/**` });
142
- break;
143
- }
144
- case EnqueueStrategy.All:
145
- default:
146
- enqueueStrategyPatterns.push({ glob: `http{s,}://**` });
147
- break;
148
- }
149
- }
150
- async function reportSkippedRequests(skippedRequests, reason) {
151
- if (onSkippedRequest && skippedRequests.length > 0) {
152
- await Promise.all(skippedRequests.map((request) => {
153
- return onSkippedRequest({
154
- url: request.url,
155
- reason: request.skippedReason ?? reason,
156
- });
157
- }));
158
- }
159
- }
160
- let requestOptions = createRequestOptions(urls, options);
161
- if (robotsTxtFile && options.respectRobotsTxtFile !== false) {
162
- const robotsUserAgent = typeof options.respectRobotsTxtFile === 'object' ? (options.respectRobotsTxtFile.userAgent ?? '*') : '*';
163
- const skippedRequests = [];
164
- requestOptions = requestOptions.filter((request) => {
165
- if (robotsTxtFile.isAllowed(request.url, robotsUserAgent)) {
166
- return true;
167
- }
168
- skippedRequests.push(request);
169
- return false;
170
- });
171
- await reportSkippedRequests(skippedRequests, 'robotsTxt');
172
- }
173
- async function createFilteredRequests() {
174
- const skippedRequests = [];
175
- // Step 1: Filter request options by exclude patterns, user include patterns, and strategy patterns.
176
- let filteredOptions;
177
- if (urlPatternObjects.length === 0) {
178
- filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects, options.strategy, (url) => skippedRequests.push(url));
179
- }
180
- else {
181
- // Filter by user patterns first (with exclude)
182
- const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects, options.strategy, (url) => skippedRequests.push(url));
183
- // ...then filter by the enqueue links strategy (making this an AND check)
184
- filteredOptions = filterRequestOptionsByPatterns(afterUserPatterns, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], options.strategy, (url) => skippedRequests.push(url));
185
- }
186
- await reportSkippedRequests(skippedRequests.map((url) => ({ url })), 'filters');
187
- // Step 2: Apply transformRequestFunction on request options - it has the highest priority
188
- if (transformRequestFunction) {
189
- const skippedByTransform = [];
190
- filteredOptions = applyRequestTransform(filteredOptions, transformRequestFunction, (r) => skippedByTransform.push(r));
191
- await reportSkippedRequests(skippedByTransform, 'transform');
192
- }
193
- // Step 3: Create Request instances from the final request options
194
- return filteredOptions.map((opts) => new Request(opts));
195
- }
196
- const { addedRequests, requestsOverLimit } = await requestManager.addRequestsBatched(await createFilteredRequests(), {
197
- forefront,
198
- waitForAllRequestsToBeAdded,
199
- maxNewRequests: limit,
200
- });
201
- if (requestsOverLimit?.length !== undefined && requestsOverLimit.length > 0) {
202
- await reportSkippedRequests(requestsOverLimit.map((r) => ({ url: typeof r === 'string' ? r : r.url })), 'enqueueLimit');
203
- }
204
- return { processedRequests: addedRequests, unprocessedRequests: [] };
205
- }
206
54
  /**
207
55
  * @internal
208
56
  * This method helps resolve the baseUrl that will be used for filtering in {@link enqueueLinks}.
@@ -237,6 +85,41 @@ export function resolveBaseUrlForEnqueueLinksFiltering({ enqueueStrategy, finalR
237
85
  // before actually finding the urls
238
86
  return originalUrlOrigin;
239
87
  }
88
+ /**
89
+ * @internal
90
+ * Builds the glob patterns a URL must match to satisfy the given enqueue `strategy`, anchored at `baseUrl`.
91
+ */
92
+ export function buildEnqueueStrategyPatterns(baseUrl, strategy) {
93
+ const url = new URL(baseUrl);
94
+ switch (strategy) {
95
+ case EnqueueStrategy.SameHostname:
96
+ // We need to get the origin of the passed in domain in the event someone sets baseUrl
97
+ // to an url like https://example.com/deep/default/path and one of the found urls is an
98
+ // absolute relative path (/path/to/page)
99
+ return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
100
+ case EnqueueStrategy.SameDomain: {
101
+ // Get the actual hostname from the base url
102
+ const baseUrlHostname = getDomain(url.hostname, { mixedInputs: false });
103
+ if (baseUrlHostname) {
104
+ // We have a hostname, so we can use it to match all links on the page that point to it and any subdomains of it
105
+ url.hostname = baseUrlHostname;
106
+ return [
107
+ { glob: ignoreHttpSchema(`${url.origin.replace(baseUrlHostname, `*.${baseUrlHostname}`)}/**`) },
108
+ { glob: ignoreHttpSchema(`${url.origin}/**`) },
109
+ ];
110
+ }
111
+ // We don't have a hostname (can happen for ips for instance), so reproduce the same behavior
112
+ // as SameDomainAndSubdomain
113
+ return [{ glob: ignoreHttpSchema(`${url.origin}/**`) }];
114
+ }
115
+ case EnqueueStrategy.SameOrigin:
116
+ // The same behavior as SameHostname, but respecting the protocol of the URL
117
+ return [{ glob: `${url.origin}/**` }];
118
+ case EnqueueStrategy.All:
119
+ default:
120
+ return [{ glob: `http{s,}://**` }];
121
+ }
122
+ }
240
123
  /**
241
124
  * Internal function that changes the enqueue glob patterns to match both http and https
242
125
  */
@@ -1,6 +1,7 @@
1
- import type { Awaitable } from '@crawlee/types';
1
+ import type { Awaitable, Dictionary } from '@crawlee/types';
2
+ import { z } from 'zod';
2
3
  import type { RequestOptions } from '../request.js';
3
- import type { EnqueueLinksOptions } from './enqueue_links.js';
4
+ import type { EnqueueStrategyOption } from './enqueue_links.js';
4
5
  export { tryAbsoluteURL } from '@crawlee/utils/internal';
5
6
  export interface UrlPatternObject {
6
7
  glob?: string;
@@ -16,6 +17,11 @@ export interface RegExpObject {
16
17
  export type RegExpInput = RegExp | RegExpObject;
17
18
  /** Unified URL pattern input — accepts glob strings, glob objects, RegExp instances, or regexp objects. */
18
19
  export type UrlPatternInput = GlobInput | RegExpInput;
20
+ /**
21
+ * Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
22
+ * @internal
23
+ */
24
+ export declare const urlPatternSchema: z.ZodType<UrlPatternInput>;
19
25
  export type SkippedRequestReason = 'robotsTxt' | 'limit' | 'enqueueLimit' | 'filters' | 'transform' | 'redirect' | 'depth';
20
26
  export type SkippedRequestCallback = (args: {
21
27
  url: string;
@@ -53,11 +59,18 @@ export declare function constructUrlPatternObjects(patterns: readonly UrlPattern
53
59
  * When `includePatterns` is empty/undefined, all options pass through (only exclude filtering applies).
54
60
  * @ignore
55
61
  */
56
- export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?: EnqueueLinksOptions['strategy'], onSkippedUrl?: (url: string) => void): RequestOptions[];
62
+ export declare function filterRequestOptionsByPatterns(requestOptions: RequestOptions[], includePatterns: UrlPatternObject[] | undefined, excludePatterns?: UrlPatternObject[], strategy?: EnqueueStrategyOption, onSkippedUrl?: (url: string) => void): RequestOptions[];
57
63
  /**
58
64
  * @ignore
59
65
  */
60
- export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?: Pick<EnqueueLinksOptions, 'label' | 'userData' | 'baseUrl' | 'skipNavigation' | 'sessionId' | 'strategy'>): RequestOptions[];
66
+ export declare function createRequestOptions(sources: readonly (string | Record<string, unknown>)[], options?: {
67
+ label?: string;
68
+ userData?: Dictionary;
69
+ baseUrl?: string;
70
+ skipNavigation?: boolean;
71
+ sessionId?: string;
72
+ strategy?: EnqueueStrategyOption;
73
+ }): RequestOptions[];
61
74
  /**
62
75
  * Takes a {@link RequestOptions} object and changes its attributes in a desired way. This user-function is used
63
76
  * by {@link enqueueLinks} to modify request options before they are converted to {@link Request} instances.
@@ -1,5 +1,7 @@
1
1
  import { URL } from 'node:url';
2
2
  import { Minimatch } from 'minimatch';
3
+ import { z } from 'zod';
4
+ import { schemas } from '../validators.js';
3
5
  export { tryAbsoluteURL } from '@crawlee/utils/internal';
4
6
  const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
5
7
  /**
@@ -8,6 +10,16 @@ const MAX_ENQUEUE_LINKS_CACHE_SIZE = 1000;
8
10
  * @ignore
9
11
  */
10
12
  const enqueueLinksPatternCache = new Map();
13
+ /**
14
+ * Accepts one {@link UrlPatternInput} — a glob string, a RegExp instance, or a `{ glob }` / `{ regexp }` object.
15
+ * @internal
16
+ */
17
+ export const urlPatternSchema = z.union([
18
+ z.string(),
19
+ z.instanceof(RegExp),
20
+ schemas.objectWithKeys(['glob']),
21
+ schemas.objectWithKeys(['regexp']),
22
+ ]);
11
23
  /**
12
24
  * @ignore
13
25
  */
@@ -132,6 +144,16 @@ export function filterRequestOptionsByPatterns(requestOptions, includePatterns,
132
144
  })
133
145
  .filter((opts) => opts !== null);
134
146
  }
147
+ function isAbsoluteUrl(url) {
148
+ try {
149
+ // eslint-disable-next-line no-new
150
+ new URL(url);
151
+ return true;
152
+ }
153
+ catch {
154
+ return false;
155
+ }
156
+ }
135
157
  /**
136
158
  * @ignore
137
159
  */
@@ -149,7 +171,12 @@ export function createRequestOptions(sources, options = {}) {
149
171
  }
150
172
  })
151
173
  .map((requestOptions) => {
152
- requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
174
+ // Leave already-absolute URLs untouched - re-deriving them via `new URL()` would normalize them
175
+ // (e.g. adding a trailing slash to a bare domain), which is surprising for URLs that didn't need
176
+ // resolving against `baseUrl` in the first place.
177
+ if (!isAbsoluteUrl(requestOptions.url)) {
178
+ requestOptions.url = new URL(requestOptions.url, options.baseUrl).href;
179
+ }
153
180
  requestOptions.userData ??= options.userData ?? {};
154
181
  if (typeof options.label === 'string') {
155
182
  requestOptions.userData = {
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { s } from '@sapphire/shapeshift';
2
+ import { parseArgument, schemas } from '@crawlee/utils/internal';
3
3
  import { BaseClient } from './common/base-client.js';
4
4
  /**
5
5
  * This is what API returns in the x-apify-pagination-limit
@@ -49,13 +49,7 @@ export class DatasetBackend extends BaseClient {
49
49
  this.updateTimestamps(true);
50
50
  }
51
51
  getData(options = {}) {
52
- const { desc, limit, offset } = s
53
- .object({
54
- desc: s.boolean().optional(),
55
- limit: s.number().int().optional(),
56
- offset: s.number().int().optional(),
57
- })
58
- .parse(options);
52
+ const { desc, limit, offset } = parseArgument(options, schemas.datasetListItemsOptions);
59
53
  return this.getDataPage({
60
54
  desc,
61
55
  offset: offset ?? 0,