@crawlee/core 4.0.0-beta.120 → 4.0.0-beta.122

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.
@@ -1,8 +1,26 @@
1
- import ow from 'ow';
1
+ import { z } from 'zod';
2
2
  import { addTimeoutToPromise } from '@apify/timeout';
3
3
  import { betterClearInterval, betterSetInterval } from '@apify/utilities';
4
4
  import { CriticalError } from '../errors.js';
5
5
  import { serviceLocator } from '../service_locator.js';
6
+ import { parseArgument, schemas, validators } from '../validators.js';
7
+ // `schemas.anyObject` and `objectWithKeys`-based validators pass values through by reference
8
+ // (object schemas return a pruned plain copy), so class instances like loggers and the
9
+ // concurrency system keep their prototype.
10
+ const autoscaledPoolOptionsSchema = z.strictObject({
11
+ runTaskFunction: schemas.anyFunction,
12
+ isFinishedFunction: schemas.anyFunction,
13
+ isTaskReadyFunction: schemas.anyFunction,
14
+ maybeRunIntervalSecs: schemas.anyNumber
15
+ .refine((value) => value > 0, 'Expected a number greater than 0')
16
+ .default(0.5),
17
+ taskTimeoutSecs: schemas.anyNumber
18
+ .refine((value) => value >= 0, 'Expected a number greater than or equal to 0')
19
+ .default(0),
20
+ log: validators.logger.default(() => serviceLocator.getLogger()),
21
+ concurrencySystem: schemas.anyObject,
22
+ consumer: schemas.anyObject.refine((value) => typeof value.id === 'string' && value.id.length > 0, "Expected an object with a non-empty string 'id'"),
23
+ });
6
24
  /**
7
25
  * Manages a pool of asynchronous resource-intensive tasks that are executed in parallel.
8
26
  * The pool only starts new tasks while its {@link IConcurrencySystem|concurrency system} reports free capacity —
@@ -78,17 +96,7 @@ export class AutoscaledPool {
78
96
  */
79
97
  #ownConcurrency = 0;
80
98
  constructor(options) {
81
- ow(options, ow.object.exactShape({
82
- runTaskFunction: ow.function,
83
- isFinishedFunction: ow.function,
84
- isTaskReadyFunction: ow.function,
85
- maybeRunIntervalSecs: ow.optional.number.greaterThan(0),
86
- taskTimeoutSecs: ow.optional.number.greaterThanOrEqual(0),
87
- log: ow.optional.object,
88
- concurrencySystem: ow.object,
89
- consumer: ow.object.partialShape({ id: ow.string.nonEmpty }),
90
- }));
91
- const { runTaskFunction, isFinishedFunction, isTaskReadyFunction, maybeRunIntervalSecs = 0.5, taskTimeoutSecs = 0, log = serviceLocator.getLogger(), concurrencySystem, consumer, } = options;
99
+ const { runTaskFunction, isFinishedFunction, isTaskReadyFunction, maybeRunIntervalSecs, taskTimeoutSecs, log, concurrencySystem, consumer, } = parseArgument(options, autoscaledPoolOptionsSchema, 'AutoscaledPoolOptions');
92
100
  this.#log = log.child({ prefix: 'AutoscaledPool' });
93
101
  // Configurable properties.
94
102
  this.#maybeRunIntervalMillis = maybeRunIntervalSecs * 1000;
@@ -1,8 +1,34 @@
1
- import ow from 'ow';
1
+ import { z } from 'zod';
2
2
  import { betterClearInterval, betterSetInterval } from '@apify/utilities';
3
3
  import { serviceLocator } from '../service_locator.js';
4
+ import { parseArgument, schemas, validators } from '../validators.js';
4
5
  import { Snapshotter } from './snapshotter.js';
5
6
  import { SystemStatus } from './system_status.js';
7
+ const concurrencySchema = z.number().int().gte(1).optional();
8
+ // `schemas.anyObject` passes values through by reference, so the load signal instances inside
9
+ // `loadSignals` and class instances like loggers keep their prototype.
10
+ const concurrencySystemOptionsSchema = z.strictObject({
11
+ maxConcurrency: z.number().int().gte(1).default(200),
12
+ minConcurrency: z.number().int().gte(1).default(1),
13
+ desiredConcurrency: z.number().int().gte(1).optional(),
14
+ desiredConcurrencyRatio: z.number().gt(0).lt(1).default(0.9),
15
+ scaleUpStepRatio: z.number().gt(0).lt(1).default(0.05),
16
+ scaleDownStepRatio: z.number().gt(0).lt(1).default(0.05),
17
+ loggingIntervalSecs: schemas.anyNumber
18
+ .refine((value) => value > 0, 'Expected a number greater than 0')
19
+ .nullish()
20
+ .default(60),
21
+ autoscaleIntervalSecs: schemas.anyNumber
22
+ .refine((value) => value > 0, 'Expected a number greater than 0')
23
+ .default(10),
24
+ loadSignals: schemas.anyObject.default(() => ({})),
25
+ snapshotHistorySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
26
+ currentHistorySecs: schemas.anyNumber.refine((value) => value > 0, 'Expected a number greater than 0').optional(),
27
+ log: validators.logger.default(() => serviceLocator.getLogger()),
28
+ maxTasksPerMinute: z
29
+ .union([z.number().int().gte(1), z.literal(Number.POSITIVE_INFINITY)])
30
+ .default(Number.POSITIVE_INFINITY),
31
+ });
6
32
  /**
7
33
  * The shareable "governor" behind an {@link AutoscaledPool}: it decides whether there is free compute for one more
8
34
  * task by combining live system load (via an internal {@link Snapshotter}) with a concurrency budget it autoscales
@@ -44,22 +70,7 @@ export class ConcurrencySystem {
44
70
  /** Set once per session, so a pool outliving `stop()` is reported once rather than every half second. */
45
71
  #warnedAboutQueryWhileStopped = false;
46
72
  constructor(options = {}) {
47
- ow(options, ow.object.exactShape({
48
- maxConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
49
- minConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
50
- desiredConcurrency: ow.optional.number.integer.greaterThanOrEqual(1),
51
- desiredConcurrencyRatio: ow.optional.number.greaterThan(0).lessThan(1),
52
- scaleUpStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
53
- scaleDownStepRatio: ow.optional.number.greaterThan(0).lessThan(1),
54
- loggingIntervalSecs: ow.any(ow.number.greaterThan(0), ow.nullOrUndefined),
55
- autoscaleIntervalSecs: ow.optional.number.greaterThan(0),
56
- loadSignals: ow.optional.object,
57
- snapshotHistorySecs: ow.optional.number.greaterThan(0),
58
- currentHistorySecs: ow.optional.number.greaterThan(0),
59
- log: ow.optional.object,
60
- maxTasksPerMinute: ow.optional.number.integerOrInfinite.greaterThanOrEqual(1),
61
- }));
62
- const { maxConcurrency = 200, minConcurrency = 1, desiredConcurrency, desiredConcurrencyRatio = 0.9, scaleUpStepRatio = 0.05, scaleDownStepRatio = 0.05, loggingIntervalSecs = 60, autoscaleIntervalSecs = 10, loadSignals = {}, snapshotHistorySecs, currentHistorySecs, log = serviceLocator.getLogger(), maxTasksPerMinute = Infinity, } = options;
73
+ const { maxConcurrency, minConcurrency, desiredConcurrency, desiredConcurrencyRatio, scaleUpStepRatio, scaleDownStepRatio, loggingIntervalSecs, autoscaleIntervalSecs, loadSignals, snapshotHistorySecs, currentHistorySecs, log, maxTasksPerMinute, } = parseArgument(options, concurrencySystemOptionsSchema, 'ConcurrencySystemOptions');
63
74
  this.log = log.child({ prefix: 'ConcurrencySystem' });
64
75
  this.desiredConcurrencyRatio = desiredConcurrencyRatio;
65
76
  this.scaleUpStepRatio = scaleUpStepRatio;
@@ -99,7 +110,7 @@ export class ConcurrencySystem {
99
110
  * If you're not sure, just keep the default value and the concurrency will scale up automatically.
100
111
  */
101
112
  set minConcurrency(value) {
102
- ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
113
+ parseArgument(value, concurrencySchema);
103
114
  this.#minConcurrency = value;
104
115
  this.clampDesiredConcurrency();
105
116
  }
@@ -116,7 +127,7 @@ export class ConcurrencySystem {
116
127
  * limit as they settle).
117
128
  */
118
129
  set maxConcurrency(value) {
119
- ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
130
+ parseArgument(value, concurrencySchema);
120
131
  this.#maxConcurrency = value;
121
132
  this.clampDesiredConcurrency();
122
133
  }
@@ -132,7 +143,7 @@ export class ConcurrencySystem {
132
143
  * in parallel if there's large enough supply of tasks.
133
144
  */
134
145
  set desiredConcurrency(value) {
135
- ow(value, ow.optional.number.integer.greaterThanOrEqual(1));
146
+ parseArgument(value, concurrencySchema);
136
147
  this.#desiredConcurrency = value;
137
148
  this.clampDesiredConcurrency();
138
149
  }
@@ -6,47 +6,47 @@ export interface ConfigField<T extends z.ZodType = z.ZodType> {
6
6
  }
7
7
  export declare function field<T extends z.ZodType>(schema: T, envVar?: string | string[]): ConfigField<T>;
8
8
  /** Zod preprocessor treating `'0'` and `'false'` as falsy. */
9
- export declare const coerceBoolean: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>;
10
- export declare const coerceNumber: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
9
+ export declare const coerceBoolean: z.ZodPreprocess<z.ZodBoolean>;
10
+ export declare const coerceNumber: z.ZodPreprocess<z.ZodNumber>;
11
11
  export declare const crawleeConfigFields: {
12
12
  /** @default 'default' */
13
13
  defaultDatasetId: ConfigField<z.ZodDefault<z.ZodString>>;
14
14
  /** @default true */
15
- purgeOnStart: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>>;
15
+ purgeOnStart: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
16
16
  /** @default 'default' */
17
17
  defaultKeyValueStoreId: ConfigField<z.ZodDefault<z.ZodString>>;
18
18
  /** @default 'default' */
19
19
  defaultRequestQueueId: ConfigField<z.ZodDefault<z.ZodString>>;
20
20
  /** @default 0.95 */
21
- maxUsedCpuRatio: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
21
+ maxUsedCpuRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
22
22
  /** @default 0.25 */
23
- availableMemoryRatio: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
24
- memoryMbytes: ConfigField<z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
23
+ availableMemoryRatio: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
24
+ memoryMbytes: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber>>>;
25
25
  /** @default 60_000 */
26
- persistStateIntervalMillis: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
26
+ persistStateIntervalMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
27
27
  /**
28
28
  * Internal safety-net timeout for a single request, in milliseconds. When unset the crawler derives it from
29
29
  * the request handler timeout (twice it, and never below 5 minutes).
30
30
  */
31
- internalTimeoutMillis: ConfigField<z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
31
+ internalTimeoutMillis: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodNumber>>>;
32
32
  /** @default 1_000 */
33
- systemInfoIntervalMillis: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>>>;
33
+ systemInfoIntervalMillis: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodNumber>>>;
34
34
  /** @default 'INPUT' */
35
35
  inputKey: ConfigField<z.ZodDefault<z.ZodString>>;
36
36
  /** @default true */
37
- headless: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>>;
37
+ headless: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
38
38
  /** @default false */
39
- xvfb: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>>;
39
+ xvfb: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
40
40
  chromeExecutablePath: ConfigField<z.ZodOptional<z.ZodString>>;
41
41
  defaultBrowserPath: ConfigField<z.ZodOptional<z.ZodString>>;
42
42
  /** @default false */
43
- disableBrowserSandbox: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>>;
44
- logLevel: ConfigField<z.ZodOptional<z.ZodPipe<z.ZodTransform<{} | null | undefined, unknown>, z.ZodEnum<typeof LogLevel>>>>;
43
+ disableBrowserSandbox: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
44
+ logLevel: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodEnum<typeof LogLevel>>>>;
45
45
  /** @default true */
46
- persistStorage: ConfigField<z.ZodDefault<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>>;
46
+ persistStorage: ConfigField<z.ZodDefault<z.ZodPreprocess<z.ZodBoolean>>>;
47
47
  /** @default './storage' */
48
48
  storageDir: ConfigField<z.ZodDefault<z.ZodString>>;
49
- containerized: ConfigField<z.ZodOptional<z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>>>;
49
+ containerized: ConfigField<z.ZodOptional<z.ZodPreprocess<z.ZodBoolean>>>;
50
50
  };
51
51
  export type FieldsInput<F extends Record<string, ConfigField>> = {
52
52
  [K in keyof F]?: z.output<F[K]['schema']>;
package/configuration.js CHANGED
@@ -36,7 +36,7 @@ const logLevelSchema = z.preprocess((val) => {
36
36
  if (key in LogLevel)
37
37
  return LogLevel[key];
38
38
  return val;
39
- }, z.nativeEnum(LogLevel));
39
+ }, z.enum(LogLevel));
40
40
  // --- Crawlee config field definitions ---
41
41
  export const crawleeConfigFields = {
42
42
  /** @default 'default' */
@@ -196,8 +196,8 @@ export class Configuration {
196
196
  continue;
197
197
  }
198
198
  // 4. Schema default (by parsing undefined through the schema)
199
- const result = fieldDef.schema.safeParse(undefined);
200
- values[key] = result.success ? result.data : undefined;
199
+ const parsed = fieldDef.schema.safeParse(undefined);
200
+ values[key] = parsed.success ? parsed.data : undefined;
201
201
  }
202
202
  return values;
203
203
  }
@@ -46,7 +46,7 @@ export declare class ErrorSnapshotter {
46
46
  */
47
47
  contextCaptureSnapshot(context: BrowserCrawlingContext, fileName: string): Promise<SnapshotResult | undefined>;
48
48
  /**
49
- * Save the HTML snapshot of the page, and return the fileName with the extension.
49
+ * Save the HTML snapshot of the page, and return the key it was stored under.
50
50
  */
51
51
  saveHTMLSnapshot(html: string, keyValueStore: Pick<KeyValueStore, 'setValue'>, fileName: string): Promise<string | undefined>;
52
52
  /**
@@ -77,12 +77,14 @@ export class ErrorSnapshotter {
77
77
  }
78
78
  }
79
79
  /**
80
- * Save the HTML snapshot of the page, and return the fileName with the extension.
80
+ * Save the HTML snapshot of the page, and return the key it was stored under.
81
81
  */
82
82
  async saveHTMLSnapshot(html, keyValueStore, fileName) {
83
83
  try {
84
84
  await keyValueStore.setValue(fileName, html, { contentType: 'text/html' });
85
- return `${fileName}.html`;
85
+ // The record key is `fileName` - returning it with an `.html` suffix (as v3 did,
86
+ // where local storage put the extension in the key) would break `getPublicUrl`.
87
+ return fileName;
86
88
  }
87
89
  catch {
88
90
  return undefined;
@@ -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,7 +1,8 @@
1
- import ow from 'ow';
2
1
  import { getDomain } from 'tldts';
2
+ import { z } from 'zod';
3
3
  import { Request } from '../request.js';
4
- import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, filterRequestOptionsByPatterns, } from './shared.js';
4
+ import { parseArgument, schemas } from '../validators.js';
5
+ import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, filterRequestOptionsByPatterns, urlPatternSchema, } from './shared.js';
5
6
  /**
6
7
  * The different enqueueing strategies available.
7
8
  *
@@ -54,6 +55,28 @@ export var EnqueueStrategy;
54
55
  */
55
56
  EnqueueStrategy["SameOrigin"] = "same-origin";
56
57
  })(EnqueueStrategy || (EnqueueStrategy = {}));
58
+ // `schemas.anyObject` passes values through by reference (object schemas return a pruned plain
59
+ // copy), so `userData` keeps its identity for the enqueued requests.
60
+ const enqueueLinksOptionsSchema = z.strictObject({
61
+ urls: schemas.arrayOf(z.string(), 'strings'),
62
+ requestManager: schemas.objectWithKeys(['addRequestsBatched']),
63
+ robotsTxtFile: schemas.objectWithKeys(['isAllowed']).optional(),
64
+ respectRobotsTxtFile: z.union([z.boolean(), z.strictObject({ userAgent: z.string().optional() })]).optional(),
65
+ onSkippedRequest: schemas.anyFunction.optional(),
66
+ forefront: z.boolean().optional(),
67
+ skipNavigation: z.boolean().optional(),
68
+ sessionId: z.string().optional(),
69
+ limit: schemas.anyNumber.optional(),
70
+ selector: z.string().optional(),
71
+ baseUrl: z.string().optional(),
72
+ userData: schemas.anyObject.optional(),
73
+ label: z.string().optional(),
74
+ include: schemas.arrayOf(urlPatternSchema, 'URL patterns').min(1).optional(),
75
+ exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
76
+ transformRequestFunction: schemas.anyFunction.optional(),
77
+ strategy: z.enum(EnqueueStrategy).optional(),
78
+ waitForAllRequestsToBeAdded: z.boolean().optional(),
79
+ });
57
80
  /**
58
81
  * This function enqueues the urls provided to the {@link RequestQueue} provided. If you want to automatically find and enqueue links,
59
82
  * you should use the context-aware `enqueueLinks` function provided on the crawler contexts.
@@ -84,37 +107,17 @@ export async function enqueueLinks(options) {
84
107
  'Check out our guide on how to use enqueueLinks() here: https://crawlee.dev/js/docs/examples/crawl-relative-links',
85
108
  ].join('\n'));
86
109
  }
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;
110
+ const parsedOptions = parseArgument(options, enqueueLinksOptionsSchema, 'EnqueueLinksOptions');
111
+ const { requestManager, limit, urls, include, exclude, transformRequestFunction, forefront, waitForAllRequestsToBeAdded, robotsTxtFile, onSkippedRequest, } = parsedOptions;
109
112
  const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
110
113
  const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
111
114
  // The strategy always applies, even when `include` patterns are provided - the two are AND-ed together
112
115
  // (a URL must match an `include` pattern *and* satisfy the strategy). This mirrors crawlee-python.
113
- options.strategy ??= EnqueueStrategy.SameHostname;
116
+ parsedOptions.strategy ??= EnqueueStrategy.SameHostname;
114
117
  const enqueueStrategyPatterns = [];
115
- if (options.baseUrl) {
116
- const url = new URL(options.baseUrl);
117
- switch (options.strategy) {
118
+ if (parsedOptions.baseUrl) {
119
+ const url = new URL(parsedOptions.baseUrl);
120
+ switch (parsedOptions.strategy) {
118
121
  case EnqueueStrategy.SameHostname:
119
122
  // We need to get the origin of the passed in domain in the event someone sets baseUrl
120
123
  // to an url like https://example.com/deep/default/path and one of the found urls is an
@@ -157,9 +160,11 @@ export async function enqueueLinks(options) {
157
160
  }));
158
161
  }
159
162
  }
160
- let requestOptions = createRequestOptions(urls, options);
161
- if (robotsTxtFile && options.respectRobotsTxtFile !== false) {
162
- const robotsUserAgent = typeof options.respectRobotsTxtFile === 'object' ? (options.respectRobotsTxtFile.userAgent ?? '*') : '*';
163
+ let requestOptions = createRequestOptions(urls, parsedOptions);
164
+ if (robotsTxtFile && parsedOptions.respectRobotsTxtFile !== false) {
165
+ const robotsUserAgent = typeof parsedOptions.respectRobotsTxtFile === 'object'
166
+ ? (parsedOptions.respectRobotsTxtFile.userAgent ?? '*')
167
+ : '*';
163
168
  const skippedRequests = [];
164
169
  requestOptions = requestOptions.filter((request) => {
165
170
  if (robotsTxtFile.isAllowed(request.url, robotsUserAgent)) {
@@ -175,13 +180,13 @@ export async function enqueueLinks(options) {
175
180
  // Step 1: Filter request options by exclude patterns, user include patterns, and strategy patterns.
176
181
  let filteredOptions;
177
182
  if (urlPatternObjects.length === 0) {
178
- filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects, options.strategy, (url) => skippedRequests.push(url));
183
+ filteredOptions = filterRequestOptionsByPatterns(requestOptions, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
179
184
  }
180
185
  else {
181
186
  // Filter by user patterns first (with exclude)
182
- const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects, options.strategy, (url) => skippedRequests.push(url));
187
+ const afterUserPatterns = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects, urlExcludePatternObjects, parsedOptions.strategy, (url) => skippedRequests.push(url));
183
188
  // ...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));
189
+ filteredOptions = filterRequestOptionsByPatterns(afterUserPatterns, enqueueStrategyPatterns.length > 0 ? enqueueStrategyPatterns : undefined, [], parsedOptions.strategy, (url) => skippedRequests.push(url));
185
190
  }
186
191
  await reportSkippedRequests(skippedRequests.map((url) => ({ url })), 'filters');
187
192
  // Step 2: Apply transformRequestFunction on request options - it has the highest priority
@@ -1,4 +1,5 @@
1
1
  import type { Awaitable } from '@crawlee/types';
2
+ import { z } from 'zod';
2
3
  import type { RequestOptions } from '../request.js';
3
4
  import type { EnqueueLinksOptions } from './enqueue_links.js';
4
5
  export { tryAbsoluteURL } from '@crawlee/utils/internal';
@@ -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;
@@ -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
  */
@@ -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,
@@ -1,9 +1,25 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { s } from '@sapphire/shapeshift';
2
+ import { parseArgument, schemas } from '@crawlee/utils/internal';
3
+ import { z } from 'zod';
3
4
  import { isStream, toBuffer } from '../utils.js';
4
5
  import { BaseClient } from './common/base-client.js';
5
6
  import mime from 'mime-types';
6
7
  const DEFAULT_LOCAL_FILE_EXTENSION = 'bin';
8
+ const keySchema = z.string();
9
+ const inputRecordSchema = z.object({
10
+ key: z.string().min(1),
11
+ value: z.union([
12
+ z.null(),
13
+ z.string(),
14
+ z.number(),
15
+ z.instanceof(Buffer),
16
+ z.instanceof(ArrayBuffer),
17
+ schemas.typedArray,
18
+ // only checks the value is an actual object, not null, nor array
19
+ schemas.plainObject,
20
+ ]),
21
+ contentType: z.string().min(1).optional(),
22
+ });
7
23
  /**
8
24
  * Key under which a run's input is stored in the default key-value store. Matches Crawlee's default
9
25
  * `inputKey` (`CRAWLEE_INPUT_KEY`) and the `INPUT` files `FileSystemStorageBackend` preserves on purge.
@@ -58,13 +74,7 @@ export class KeyValueStoreBackend extends BaseClient {
58
74
  this.updateTimestamps(true);
59
75
  }
60
76
  async listKeys(options = {}) {
61
- const { prefix, exclusiveStartKey, limit } = s
62
- .object({
63
- prefix: s.string().optional(),
64
- exclusiveStartKey: s.string().optional(),
65
- limit: s.number().int().greaterThan(0).optional(),
66
- })
67
- .parse(options);
77
+ const { prefix, exclusiveStartKey, limit } = parseArgument(options, schemas.keyValueStoreListKeysOptions);
68
78
  const items = [];
69
79
  for (const record of this.#keyValueEntries.values()) {
70
80
  const size = Buffer.byteLength(record.value);
@@ -104,7 +114,7 @@ export class KeyValueStoreBackend extends BaseClient {
104
114
  * @param key The key of the record to generate the public URL for.
105
115
  */
106
116
  async getPublicUrl(key) {
107
- s.string().parse(key);
117
+ parseArgument(key, keySchema);
108
118
  return undefined;
109
119
  }
110
120
  /**
@@ -114,11 +124,11 @@ export class KeyValueStoreBackend extends BaseClient {
114
124
  * @returns `true` if the record exists, `false` if it does not.
115
125
  */
116
126
  async recordExists(key) {
117
- s.string().parse(key);
127
+ parseArgument(key, keySchema);
118
128
  return this.#keyValueEntries.has(key);
119
129
  }
120
130
  async getValue(key) {
121
- s.string().parse(key);
131
+ parseArgument(key, keySchema);
122
132
  const entry = this.#keyValueEntries.get(key);
123
133
  if (!entry) {
124
134
  return undefined;
@@ -136,20 +146,7 @@ export class KeyValueStoreBackend extends BaseClient {
136
146
  return record;
137
147
  }
138
148
  async setValue(record) {
139
- s.object({
140
- key: s.string().lengthGreaterThan(0),
141
- value: s.union([
142
- s.null(),
143
- s.string(),
144
- s.number(),
145
- s.instance(Buffer),
146
- s.instance(ArrayBuffer),
147
- s.typedArray(),
148
- // disabling validation will make shapeshift only check the object given is an actual object, not null, nor array
149
- s.object({}).setValidationEnabled(false),
150
- ]),
151
- contentType: s.string().lengthGreaterThan(0).optional(),
152
- }).parse(record);
149
+ parseArgument(record, inputRecordSchema);
153
150
  const { key } = record;
154
151
  let { value } = record;
155
152
  // The frontend (KeyValueStore codec) serializes the value and resolves its content type
@@ -180,7 +177,7 @@ export class KeyValueStoreBackend extends BaseClient {
180
177
  this.updateTimestamps(true);
181
178
  }
182
179
  async deleteValue(key) {
183
- s.string().parse(key);
180
+ parseArgument(key, keySchema);
184
181
  if (this.#keyValueEntries.has(key)) {
185
182
  this.#keyValueEntries.delete(key);
186
183
  this.updateTimestamps(true);