@crawlee/browser 4.0.0-beta.99 → 4.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,30 @@
1
- import type { BasicCrawlerOptions, BasicCrawlingContext, CrawlingContext, EnqueueLinksOptions, ErrorHandler, GetUserDataFromRequest, IRequestManager, LoadedRequest, Request, RequestHandler, RouterHandler, SkippedRequestCallback } from '@crawlee/basic';
1
+ import type { AddRequestsBatchedResult, BasicCrawlerOptions, CrawlingContext, EnqueueLinksOptions, ErrorHandler, ExtractLinksOptions, GetUserDataFromRequest, LoadedRequest, Request, RequestHandler, RouterHandler } from '@crawlee/basic';
2
2
  import { BasicCrawler, ContextPipeline } from '@crawlee/basic';
3
- import type { BrowserController, BrowserPlugin, BrowserPoolHooks, BrowserPoolOptions, CommonPage, CrawlerRemoteBrowserOptions, InferBrowserPluginArray, LaunchContext } from '@crawlee/browser-pool';
4
- import type { Awaitable, BatchAddRequestsResult, Dictionary, IBrowserPool } from '@crawlee/types';
5
- import type { RobotsTxtFile } from '@crawlee/utils';
6
- import type { ReadonlyDeep } from 'type-fest';
3
+ import type { CommonPage, CrawlerRemoteBrowserOptions } from '@crawlee/browser-pool';
4
+ import type { Awaitable, Dictionary, IBrowserPool } from '@crawlee/types';
5
+ import { z } from 'zod';
7
6
  import type { BrowserLaunchContext } from './browser-launcher.js';
8
7
  interface BaseResponse {
9
8
  status(): number;
9
+ /** Optional because only Playwright and Puppeteer responses are guaranteed to carry it. */
10
+ headers?(): Record<string, string>;
10
11
  }
11
- export interface BrowserCrawlingContext<Page extends CommonPage = CommonPage, Response extends BaseResponse = BaseResponse, UserData extends Dictionary = Dictionary, GoToOptions extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
12
+ /**
13
+ * The type of a browser pool the crawler builds (and therefore owns) for itself. It's an {@link IBrowserPool} that
14
+ * additionally exposes `destroy()` — the crawler only ever tears down pools it created, which is why {@link IBrowserPool}
15
+ * itself intentionally omits `destroy`.
16
+ */
17
+ export type OwnedBrowserPool<Page> = IBrowserPool<Page> & {
18
+ destroy: () => Promise<void>;
19
+ };
20
+ /**
21
+ * Rejects options that exist only to configure the browser pool the crawler would have built for itself.
22
+ * Accepting them alongside a pre-built `browserPool` and quietly ignoring them is how `browserPoolOptions` grew
23
+ * into a second, half-working way of configuring the same pool.
24
+ */
25
+ export declare function assertBrowserPoolNotConfigured(crawlerName: string, ignoredOptions: Dictionary): void;
26
+ export interface BrowserCrawlingContext<Page extends CommonPage = CommonPage, Response extends BaseResponse = BaseResponse, UserData extends Dictionary = any, // with default to Dictionary we cant use a typed router in untyped crawler
27
+ GoToOptions extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
12
28
  /**
13
29
  * The browser page object where the web page is loaded and rendered.
14
30
  */
@@ -26,18 +42,30 @@ export interface BrowserCrawlingContext<Page extends CommonPage = CommonPage, Re
26
42
  * object (or return `{ gotoOptions: ... }`) to influence the navigation.
27
43
  */
28
44
  gotoOptions: GoToOptions;
45
+ /**
46
+ * Extracts URLs from the current page, without adding them to the request queue.
47
+ */
48
+ extractLinks: (options?: ExtractLinksOptions) => Promise<string[]>;
29
49
  /**
30
50
  * Helper function for extracting URLs from the current page and adding them to the request queue.
31
51
  */
32
- enqueueLinks: (options?: EnqueueLinksOptions) => Promise<BatchAddRequestsResult>;
52
+ enqueueLinks: (options?: EnqueueLinksOptions) => Promise<AddRequestsBatchedResult>;
33
53
  }
34
54
  export type BrowserHook<Context = BrowserCrawlingContext, ContextExtension = {}> = (crawlingContext: Context & ContextExtension) => Awaitable<void | Partial<Context>>;
35
- export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Response extends BaseResponse = BaseResponse, Context extends BrowserCrawlingContext<Page, Response, Dictionary> = BrowserCrawlingContext<Page, Response, Dictionary>, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, InternalBrowserPoolOptions extends BrowserPoolOptions = BrowserPoolOptions, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, __BrowserPlugins extends BrowserPlugin[] = InferBrowserPluginArray<InternalBrowserPoolOptions['browserPlugins']>, __BrowserControllerReturn extends BrowserController = ReturnType<__BrowserPlugins[number]['createController']>, __LaunchContextReturn extends LaunchContext = ReturnType<__BrowserPlugins[number]['createLaunchContext']>> extends Omit<BasicCrawlerOptions<Context, ContextExtension, ExtendedContext>, 'requestHandler' | 'failedRequestHandler' | 'errorHandler'> {
55
+ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Response extends BaseResponse = BaseResponse, Context extends BrowserCrawlingContext<Page, Response> = BrowserCrawlingContext<Page, Response>, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, StatisticStateExtension extends object = {}> extends Omit<BasicCrawlerOptions<Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension>, 'requestHandler' | 'failedRequestHandler' | 'errorHandler'> {
36
56
  launchContext?: BrowserLaunchContext<any, any>;
37
57
  /**
38
- * An existing browser pool instance to use. When provided, the crawler will use this pool directly instead of
39
- * constructing a new one from `browserPoolOptions`, enabling browser sharing across multiple crawlers. The crawler
40
- * will not tear down a shared pool — the caller is responsible for its lifecycle.
58
+ * The browser pool the crawler should serve its pages from. This is the single way to run a pool with
59
+ * non-default options: build one with the factory that matches your crawler
60
+ * ({@link playwrightBrowserPool}, {@link puppeteerBrowserPool}, {@link stagehandBrowserPool}) - it
61
+ * accepts every {@link BrowserPoolOptions|`BrowserPool` option} and supplies the correct browser plugin
62
+ * itself, so the pool can never mismatch the crawler.
63
+ *
64
+ * A pool passed in this way is borrowed, not owned: the crawler will not tear it down, which is what makes it
65
+ * shareable across crawlers. Since the crawler then builds nothing itself, the options that configure its own
66
+ * pool (`launchContext`, `headless`, `remoteBrowser`) are rejected rather than silently ignored.
67
+ *
68
+ * When omitted, the crawler builds - and tears down - a default pool for its own browser.
41
69
  */
42
70
  browserPool?: IBrowserPool<Page>;
43
71
  /**
@@ -48,8 +76,9 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
48
76
  * crawler. Supply the connection details only: a static `endpoint` URL, a function returning one per launch,
49
77
  * or a {@link RemoteBrowserProvider}.
50
78
  *
51
- * Ignored when `browserPool` is set. For sharing a remote pool across crawlers, construct a
52
- * {@link RemoteBrowserPool} yourself and pass it as `browserPool` instead.
79
+ * Cannot be combined with `browserPool`. To tune the pool wrapping the remote connection, or to share it
80
+ * across crawlers, build it with the remote factory for your crawler ({@link remotePlaywrightBrowserPool},
81
+ * {@link remotePuppeteerBrowserPool}, {@link remoteStagehandBrowserPool}) and pass it as `browserPool`.
53
82
  */
54
83
  remoteBrowser?: CrawlerRemoteBrowserOptions;
55
84
  /**
@@ -100,11 +129,6 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
100
129
  * represents the last error thrown during processing of the request.
101
130
  */
102
131
  failedRequestHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
103
- /**
104
- * Custom options passed to the underlying {@link BrowserPool} constructor.
105
- * We can tweak those to fine-tune browser management.
106
- */
107
- browserPoolOptions?: Partial<BrowserPoolOptions> & Partial<BrowserPoolHooks<__BrowserControllerReturn, __LaunchContextReturn>>;
108
132
  /**
109
133
  * Async functions that are sequentially evaluated before the navigation. Good for setting additional cookies
110
134
  * or browser properties before navigation. The function receives the `crawlingContext`; the options object
@@ -169,11 +193,6 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
169
193
  * Defines whether the cookies should be persisted for sessions. Enabled by default.
170
194
  */
171
195
  saveResponseCookies?: boolean;
172
- /**
173
- * Whether to run browser in headless mode. Defaults to `true`.
174
- * Can be also set via {@link Configuration}.
175
- */
176
- headless?: boolean | 'new' | 'old';
177
196
  /**
178
197
  * Whether to ignore custom elements (and their #shadow-roots) when processing the page content via `parseWithCheerio` helper.
179
198
  * By default, they are expanded automatically. Use this option to disable this behavior.
@@ -225,9 +244,8 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
225
244
  *
226
245
  * @category Crawlers
227
246
  */
228
- export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPage, Response extends BaseResponse = BaseResponse, InternalBrowserPoolOptions extends BrowserPoolOptions = BrowserPoolOptions, LaunchOptions extends Dictionary | undefined = Dictionary, Context extends BrowserCrawlingContext<Page, Response, Dictionary> = BrowserCrawlingContext<Page, Response, Dictionary>, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, GoToOptions extends Dictionary = Dictionary> extends BasicCrawler<Context, ContextExtension, ExtendedContext, Routes> {
229
- /** Backs the {@link BrowserCrawler.browserPool|`browserPool`} getter. */
230
- private browserPoolDep;
247
+ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPage, Response extends BaseResponse = BaseResponse, LaunchOptions extends Dictionary | undefined = Dictionary, Context extends BrowserCrawlingContext<Page, Response> = BrowserCrawlingContext<Page, Response>, ContextExtension = Dictionary<never>, ExtendedContext extends Context = Context & ContextExtension, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, StatisticStateExtension extends object = {}, GoToOptions extends Dictionary = Dictionary> extends BasicCrawler<Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> {
248
+ #private;
231
249
  /**
232
250
  * A reference to the underlying browser pool that manages the crawler's browsers. Typed as
233
251
  * {@link IBrowserPool} so custom implementations can be plugged in via the `browserPool` constructor option.
@@ -236,105 +254,131 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
236
254
  launchContext: BrowserLaunchContext<LaunchOptions, unknown>;
237
255
  protected readonly ignoreShadowRoots: boolean;
238
256
  protected readonly ignoreIframes: boolean;
239
- private readonly navigationTimeoutMillis;
240
- private readonly preNavigationHooks;
241
- private readonly postNavigationHooks;
242
- private readonly saveResponseCookies;
243
257
  protected static optionsShape: {
244
- // @ts-ignore optional peer dependency or compatibility with es2022
245
- navigationTimeoutSecs: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
246
- // @ts-ignore optional peer dependency or compatibility with es2022
247
- preNavigationHooks: import("ow").ArrayPredicate<unknown> & import("ow").BasePredicate<unknown[] | undefined>;
248
- // @ts-ignore optional peer dependency or compatibility with es2022
249
- postNavigationHooks: import("ow").ArrayPredicate<unknown> & import("ow").BasePredicate<unknown[] | undefined>;
250
- // @ts-ignore optional peer dependency or compatibility with es2022
251
- launchContext: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
252
- // @ts-ignore optional peer dependency or compatibility with es2022
253
- headless: import("ow").AnyPredicate<string | boolean>;
254
- // @ts-ignore optional peer dependency or compatibility with es2022
255
- browserPool: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
256
- // @ts-ignore optional peer dependency or compatibility with es2022
257
- remoteBrowser: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
258
- // @ts-ignore optional peer dependency or compatibility with es2022
259
- browserPoolOptions: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
260
- // @ts-ignore optional peer dependency or compatibility with es2022
261
- saveResponseCookies: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
262
- // @ts-ignore optional peer dependency or compatibility with es2022
263
- proxyConfiguration: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
264
- // @ts-ignore optional peer dependency or compatibility with es2022
265
- contextPipelineBuilder: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
266
- // @ts-ignore optional peer dependency or compatibility with es2022
267
- extendContext: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
268
- // @ts-ignore optional peer dependency or compatibility with es2022
269
- requestList: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
270
- // @ts-ignore optional peer dependency or compatibility with es2022
271
- requestQueue: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
272
- // @ts-ignore optional peer dependency or compatibility with es2022
273
- requestHandler: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
274
- // @ts-ignore optional peer dependency or compatibility with es2022
275
- requestHandlerTimeoutSecs: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
276
- // @ts-ignore optional peer dependency or compatibility with es2022
277
- errorHandler: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
278
- // @ts-ignore optional peer dependency or compatibility with es2022
279
- failedRequestHandler: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
280
- // @ts-ignore optional peer dependency or compatibility with es2022
281
- maxRequestRetries: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
282
- // @ts-ignore optional peer dependency or compatibility with es2022
283
- sameDomainDelaySecs: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
284
- // @ts-ignore optional peer dependency or compatibility with es2022
285
- maxRequestsPerCrawl: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
286
- // @ts-ignore optional peer dependency or compatibility with es2022
287
- maxCrawlDepth: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
288
- // @ts-ignore optional peer dependency or compatibility with es2022
289
- taskLoopOptions: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
290
- // @ts-ignore optional peer dependency or compatibility with es2022
291
- concurrencySystem: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
292
- // @ts-ignore optional peer dependency or compatibility with es2022
293
- sessionPool: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
294
- // @ts-ignore optional peer dependency or compatibility with es2022
295
- statusMessageLoggingInterval: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
296
- // @ts-ignore optional peer dependency or compatibility with es2022
297
- statusMessageCallback: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
298
- // @ts-ignore optional peer dependency or compatibility with es2022
299
- additionalHttpErrorStatusCodes: import("ow").ArrayPredicate<number>;
300
- // @ts-ignore optional peer dependency or compatibility with es2022
301
- ignoreHttpErrorStatusCodes: import("ow").ArrayPredicate<number>;
302
- // @ts-ignore optional peer dependency or compatibility with es2022
303
- blockedStatusCodes: import("ow").ArrayPredicate<number>;
304
- // @ts-ignore optional peer dependency or compatibility with es2022
305
- retryOnBlocked: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
306
- // @ts-ignore optional peer dependency or compatibility with es2022
307
- respectRobotsTxtFile: import("ow").AnyPredicate<boolean | object>;
308
- // @ts-ignore optional peer dependency or compatibility with es2022
309
- onSkippedRequest: import("ow").Predicate<Function> & import("ow").BasePredicate<Function | undefined>;
310
- // @ts-ignore optional peer dependency or compatibility with es2022
311
- httpClient: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
312
- // @ts-ignore optional peer dependency or compatibility with es2022
313
- configuration: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
314
- // @ts-ignore optional peer dependency or compatibility with es2022
315
- storageBackend: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
316
- // @ts-ignore optional peer dependency or compatibility with es2022
317
- eventManager: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
318
- // @ts-ignore optional peer dependency or compatibility with es2022
319
- logger: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
320
- // @ts-ignore optional peer dependency or compatibility with es2022
321
- minConcurrency: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
322
- // @ts-ignore optional peer dependency or compatibility with es2022
323
- maxConcurrency: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
324
- // @ts-ignore optional peer dependency or compatibility with es2022
325
- maxRequestsPerMinute: import("ow").NumberPredicate & import("ow").BasePredicate<number | undefined>;
326
- // @ts-ignore optional peer dependency or compatibility with es2022
327
- keepAlive: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
328
- // @ts-ignore optional peer dependency or compatibility with es2022
329
- statisticsOptions: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
330
- // @ts-ignore optional peer dependency or compatibility with es2022
331
- id: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
258
+ navigationTimeoutSecs: z.ZodDefault<z.ZodCustom<number, number>>;
259
+ preNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
260
+ postNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
261
+ launchContext: z.ZodDefault<z.ZodCustom<Dictionary, Dictionary>>;
262
+ browserPool: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
263
+ browserPoolBuilder: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
264
+ remoteBrowser: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
265
+ saveResponseCookies: z.ZodDefault<z.ZodBoolean>;
266
+ proxyConfiguration: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
267
+ ignoreIframes: z.ZodDefault<z.ZodBoolean>;
268
+ ignoreShadowRoots: z.ZodDefault<z.ZodBoolean>;
269
+ contextPipelineBuilder: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
270
+ extendContext: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
271
+ requestList: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
272
+ requestQueue: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
273
+ requestManager: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
274
+ requestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
275
+ requestHandlerTimeoutSecs: z.ZodOptional<z.ZodCustom<number, number>>;
276
+ errorHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
277
+ failedRequestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
278
+ maxRequestRetries: z.ZodDefault<z.ZodCustom<number, number>>;
279
+ sameDomainDelaySecs: z.ZodDefault<z.ZodCustom<number, number>>;
280
+ maxRequestsPerCrawl: z.ZodOptional<z.ZodCustom<number, number>>;
281
+ maxCrawlDepth: z.ZodOptional<z.ZodCustom<number, number>>;
282
+ taskLoopOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
283
+ concurrencySystem: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
284
+ sessionPool: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
285
+ statusMessageLoggingInterval: z.ZodDefault<z.ZodCustom<number, number>>;
286
+ statusMessageCallback: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
287
+ additionalHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
288
+ ignoreHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
289
+ blockedStatusCodes: z.ZodOptional<z.ZodArray<z.ZodCustom<number, number>>>;
290
+ retryOnBlocked: z.ZodDefault<z.ZodBoolean>;
291
+ respectRobotsTxtFile: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCustom<Dictionary, Dictionary>]>>;
292
+ transactionalStorage: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
293
+ requestQueue: z.ZodOptional<z.ZodEnum<{
294
+ deferred: "deferred";
295
+ writeThrough: "writeThrough";
296
+ }>>;
297
+ }, z.core.$strict>]>>;
298
+ onSkippedRequest: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
299
+ // @ts-ignore optional peer dependency or compatibility with es2022
300
+ httpClient: z.ZodOptional<z.ZodCustom<import("@crawlee/http-client").BaseHttpClient, import("@crawlee/http-client").BaseHttpClient>>;
301
+ // @ts-ignore optional peer dependency or compatibility with es2022
302
+ configuration: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").Configuration, import("@crawlee/basic").Configuration>>;
303
+ storageBackend: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
304
+ // @ts-ignore optional peer dependency or compatibility with es2022
305
+ eventManager: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").EventManager, import("@crawlee/basic").EventManager>>;
306
+ logger: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
307
+ minConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
308
+ maxConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
309
+ maxRequestsPerMinute: z.ZodOptional<z.ZodCustom<number, number>>;
310
+ keepAlive: z.ZodOptional<z.ZodBoolean>;
311
+ statistics: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
312
+ id: z.ZodOptional<z.ZodString>;
332
313
  };
314
+ protected static optionsSchema: z.ZodObject<{
315
+ navigationTimeoutSecs: z.ZodDefault<z.ZodCustom<number, number>>;
316
+ preNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
317
+ postNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
318
+ launchContext: z.ZodDefault<z.ZodCustom<Dictionary, Dictionary>>;
319
+ browserPool: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
320
+ browserPoolBuilder: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
321
+ remoteBrowser: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
322
+ saveResponseCookies: z.ZodDefault<z.ZodBoolean>;
323
+ proxyConfiguration: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
324
+ ignoreIframes: z.ZodDefault<z.ZodBoolean>;
325
+ ignoreShadowRoots: z.ZodDefault<z.ZodBoolean>;
326
+ contextPipelineBuilder: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
327
+ extendContext: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
328
+ requestList: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
329
+ requestQueue: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
330
+ requestManager: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
331
+ requestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
332
+ requestHandlerTimeoutSecs: z.ZodOptional<z.ZodCustom<number, number>>;
333
+ errorHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
334
+ failedRequestHandler: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
335
+ maxRequestRetries: z.ZodDefault<z.ZodCustom<number, number>>;
336
+ sameDomainDelaySecs: z.ZodDefault<z.ZodCustom<number, number>>;
337
+ maxRequestsPerCrawl: z.ZodOptional<z.ZodCustom<number, number>>;
338
+ maxCrawlDepth: z.ZodOptional<z.ZodCustom<number, number>>;
339
+ taskLoopOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
340
+ concurrencySystem: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
341
+ sessionPool: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
342
+ statusMessageLoggingInterval: z.ZodDefault<z.ZodCustom<number, number>>;
343
+ statusMessageCallback: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
344
+ additionalHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
345
+ ignoreHttpErrorStatusCodes: z.ZodDefault<z.ZodArray<z.ZodCustom<number, number>>>;
346
+ blockedStatusCodes: z.ZodOptional<z.ZodArray<z.ZodCustom<number, number>>>;
347
+ retryOnBlocked: z.ZodDefault<z.ZodBoolean>;
348
+ respectRobotsTxtFile: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodCustom<Dictionary, Dictionary>]>>;
349
+ transactionalStorage: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodObject<{
350
+ requestQueue: z.ZodOptional<z.ZodEnum<{
351
+ deferred: "deferred";
352
+ writeThrough: "writeThrough";
353
+ }>>;
354
+ }, z.core.$strict>]>>;
355
+ onSkippedRequest: z.ZodOptional<z.ZodCustom<(...args: any[]) => unknown, (...args: any[]) => unknown>>;
356
+ // @ts-ignore optional peer dependency or compatibility with es2022
357
+ httpClient: z.ZodOptional<z.ZodCustom<import("@crawlee/http-client").BaseHttpClient, import("@crawlee/http-client").BaseHttpClient>>;
358
+ // @ts-ignore optional peer dependency or compatibility with es2022
359
+ configuration: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").Configuration, import("@crawlee/basic").Configuration>>;
360
+ storageBackend: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
361
+ // @ts-ignore optional peer dependency or compatibility with es2022
362
+ eventManager: z.ZodOptional<z.ZodCustom<import("@crawlee/basic").EventManager, import("@crawlee/basic").EventManager>>;
363
+ logger: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
364
+ minConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
365
+ maxConcurrency: z.ZodOptional<z.ZodCustom<number, number>>;
366
+ maxRequestsPerMinute: z.ZodOptional<z.ZodCustom<number, number>>;
367
+ keepAlive: z.ZodOptional<z.ZodBoolean>;
368
+ statistics: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
369
+ id: z.ZodOptional<z.ZodString>;
370
+ }, z.core.$strict>;
333
371
  /**
334
372
  * All `BrowserCrawler` parameters are passed via an options object.
335
373
  */
336
- protected constructor(options: BrowserCrawlerOptions<Page, Response, Context, ContextExtension, ExtendedContext> & {
374
+ protected constructor(options: BrowserCrawlerOptions<Page, Response, Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> & {
337
375
  contextPipelineBuilder: () => ContextPipeline<CrawlingContext, Context>;
376
+ /**
377
+ * Builds the pool the crawler owns, used only when the user injected no `browserPool`. Supplied by the
378
+ * concrete crawler, which is the only place that knows which browser plugin to run - that is also why
379
+ * `remoteBrowser` is handed to it rather than acted upon here.
380
+ */
381
+ browserPoolBuilder: (remoteBrowser?: CrawlerRemoteBrowserOptions) => OwnedBrowserPool<Page>;
338
382
  });
339
383
  protected getNavigationTimeoutMillis(): number;
340
384
  protected buildContextPipeline(): ContextPipeline<CrawlingContext, BrowserCrawlingContext<Page, Response, Dictionary>>;
@@ -364,7 +408,7 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
364
408
  * Transforms proxy-related errors to `SessionError`.
365
409
  */
366
410
  private throwIfProxyError;
367
- protected abstract _navigationHandler(crawlingContext: BrowserCrawlingContext<Page, Response>, gotoOptions: GoToOptions): Promise<Context['response'] | null | undefined>;
411
+ protected abstract navigationHandler(crawlingContext: BrowserCrawlingContext<Page, Response>, gotoOptions: GoToOptions): Promise<Context['response'] | null | undefined>;
368
412
  private processResponse;
369
413
  /**
370
414
  * Function for cleaning up after all requests are processed.
@@ -372,26 +416,6 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
372
416
  */
373
417
  teardown(): Promise<void>;
374
418
  }
375
- /** @internal */
376
- interface EnqueueLinksInternalOptions {
377
- options?: ReadonlyDeep<Omit<EnqueueLinksOptions, 'requestManager'>> & Pick<EnqueueLinksOptions, 'requestManager'>;
378
- page: CommonPage;
379
- requestManager: IRequestManager;
380
- robotsTxtFile?: RobotsTxtFile;
381
- onSkippedRequest?: SkippedRequestCallback;
382
- originalRequestUrl: string;
383
- finalRequestUrl?: string;
384
- }
385
- /** @internal */
386
- interface BoundEnqueueLinksInternalOptions {
387
- enqueueLinks: BasicCrawlingContext['enqueueLinks'];
388
- options?: ReadonlyDeep<Omit<EnqueueLinksOptions, 'requestManager'>> & Pick<EnqueueLinksOptions, 'requestManager'>;
389
- originalRequestUrl: string;
390
- finalRequestUrl?: string;
391
- page: CommonPage;
392
- }
393
- /** @internal */
394
- export declare function browserCrawlerEnqueueLinks(options: EnqueueLinksInternalOptions | BoundEnqueueLinksInternalOptions): Promise<unknown>;
395
419
  /**
396
420
  * Extracts URLs from a given page.
397
421
  * @ignore
@@ -1,8 +1,22 @@
1
- import { BasicCrawler, browserPoolCookieToToughCookie, ContextPipeline, cookieStringToToughCookie, enqueueLinks, NavigationSkippedError, OwnedOrInjected, remainingNavigationWindowMillis, RequestState, resolveBaseUrlForEnqueueLinksFiltering, SessionError, toughCookieToBrowserPoolCookie, tryAbsoluteURL, validators, } from '@crawlee/basic';
2
- import { BrowserPool, RemoteBrowserPool } from '@crawlee/browser-pool';
3
- import { CLOUDFLARE_RETRY_CSS_SELECTORS, RETRY_CSS_SELECTORS, sleep } from '@crawlee/utils';
4
- import ow from 'ow';
1
+ import { BasicCrawler, browserPoolCookieToToughCookie, ContextPipeline, cookieStringToToughCookie, EnqueueStrategy, NavigationSkippedError, OwnedOrInjected, parseArgument, remainingNavigationWindowMillis, RequestState, RequestThrottledError, resolveBaseUrlForEnqueueLinksFiltering, schemas, SessionError, toughCookieToBrowserPoolCookie, tryAbsoluteURL, validators, } from '@crawlee/basic';
2
+ import { CLOUDFLARE_RETRY_CSS_SELECTORS, RETRY_CSS_SELECTORS } from '@crawlee/utils/internal';
3
+ import { sleep } from '@crawlee/utils';
4
+ import { z } from 'zod';
5
5
  import { addTimeoutToPromise, TimeoutError, tryCancel } from '@apify/timeout';
6
+ /**
7
+ * Rejects options that exist only to configure the browser pool the crawler would have built for itself.
8
+ * Accepting them alongside a pre-built `browserPool` and quietly ignoring them is how `browserPoolOptions` grew
9
+ * into a second, half-working way of configuring the same pool.
10
+ */
11
+ export function assertBrowserPoolNotConfigured(crawlerName, ignoredOptions) {
12
+ const names = Object.keys(ignoredOptions).filter((name) => ignoredOptions[name] !== undefined);
13
+ if (names.length === 0) {
14
+ return;
15
+ }
16
+ throw new Error(`${crawlerName}: ${names.map((name) => `\`${name}\``).join(', ')} cannot be combined with \`browserPool\`, ` +
17
+ `${names.length > 1 ? 'they configure' : 'it configures'} the browser pool the crawler would build for ` +
18
+ 'itself. Configure the pool you pass in instead.');
19
+ }
6
20
  const COOKIES_BEFORE_HOOKS = Symbol('cookiesBeforeHooks');
7
21
  const readContextField = (ctx, key) => ctx[key];
8
22
  /**
@@ -58,40 +72,46 @@ function isNavigationTimeoutError(error) {
58
72
  */
59
73
  export class BrowserCrawler extends BasicCrawler {
60
74
  /** Backs the {@link BrowserCrawler.browserPool|`browserPool`} getter. */
61
- browserPoolDep;
75
+ #browserPoolDep;
62
76
  /**
63
77
  * A reference to the underlying browser pool that manages the crawler's browsers. Typed as
64
78
  * {@link IBrowserPool} so custom implementations can be plugged in via the `browserPool` constructor option.
65
79
  */
66
80
  get browserPool() {
67
- return this.browserPoolDep.value;
81
+ return this.#browserPoolDep.value;
68
82
  }
69
83
  launchContext;
70
84
  ignoreShadowRoots;
71
85
  ignoreIframes;
72
- navigationTimeoutMillis;
73
- preNavigationHooks;
74
- postNavigationHooks;
75
- saveResponseCookies;
86
+ #navigationTimeoutMillis;
87
+ #preNavigationHooks;
88
+ #postNavigationHooks;
89
+ #saveResponseCookies;
76
90
  static optionsShape = {
77
91
  ...BasicCrawler.optionsShape,
78
- navigationTimeoutSecs: ow.optional.number.greaterThan(0),
79
- preNavigationHooks: ow.optional.array,
80
- postNavigationHooks: ow.optional.array,
81
- launchContext: ow.optional.object,
82
- headless: ow.optional.any(ow.boolean, ow.string),
83
- browserPool: ow.optional.object.validate(validators.browserPool),
84
- remoteBrowser: ow.optional.object,
85
- browserPoolOptions: ow.optional.object,
86
- saveResponseCookies: ow.optional.boolean,
87
- proxyConfiguration: ow.optional.object.validate(validators.proxyConfiguration),
92
+ navigationTimeoutSecs: schemas.anyNumber
93
+ .refine((value) => value > 0, 'Expected a number greater than 0')
94
+ .default(60),
95
+ preNavigationHooks: schemas.anyArray.default(() => []),
96
+ postNavigationHooks: schemas.anyArray.default(() => []),
97
+ launchContext: schemas.anyObject.default(() => ({})),
98
+ browserPool: validators.browserPool.optional(),
99
+ browserPoolBuilder: schemas.anyFunction.optional(),
100
+ remoteBrowser: schemas.anyObject.optional(),
101
+ saveResponseCookies: z.boolean().default(true),
102
+ proxyConfiguration: validators.proxyConfiguration.optional(),
103
+ ignoreIframes: z.boolean().default(false),
104
+ ignoreShadowRoots: z.boolean().default(false),
88
105
  };
106
+ static optionsSchema = z.strictObject(BrowserCrawler.optionsShape);
89
107
  /**
90
108
  * All `BrowserCrawler` parameters are passed via an options object.
91
109
  */
92
110
  constructor(options) {
93
- ow(options, 'BrowserCrawlerOptions', ow.object.exactShape(BrowserCrawler.optionsShape));
94
- const { navigationTimeoutSecs = 60, saveResponseCookies = true, launchContext = {}, browserPool, remoteBrowser, browserPoolOptions, preNavigationHooks = [], postNavigationHooks = [], headless, ignoreIframes = false, ignoreShadowRoots = false, contextPipelineBuilder, extendContext, ...basicCrawlerOptions } = options;
111
+ const { navigationTimeoutSecs, saveResponseCookies, launchContext, browserPool, remoteBrowser, preNavigationHooks, postNavigationHooks, ignoreIframes, ignoreShadowRoots, contextPipelineBuilder, browserPoolBuilder, extendContext, ...basicCrawlerOptions } = parseArgument(options, BrowserCrawler.optionsSchema, 'BrowserCrawlerOptions');
112
+ if (browserPool) {
113
+ assertBrowserPoolNotConfigured(new.target.name, { remoteBrowser });
114
+ }
95
115
  const skipGuard = (action) => ({
96
116
  action: async (ctx) => (ctx.request.skipNavigation ? {} : ((await action(ctx)) ?? {})),
97
117
  });
@@ -103,18 +123,18 @@ export class BrowserCrawler extends BasicCrawler {
103
123
  // hook eats into the same window the navigation uses. The navigation itself is bounded by
104
124
  // capping its `gotoOptions.timeout` to the remaining budget.
105
125
  const windowGuard = (step) => skipGuard(async (ctx) => {
106
- const remaining = remainingNavigationWindowMillis(ctx, this.navigationTimeoutMillis);
126
+ const remaining = remainingNavigationWindowMillis(ctx, this.#navigationTimeoutMillis);
107
127
  if (remaining <= 0) {
108
- throw new TimeoutError(`Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
128
+ throw new TimeoutError(`Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
109
129
  }
110
- return addTimeoutToPromise(async () => step(ctx), remaining, `Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
130
+ return addTimeoutToPromise(async () => step(ctx), remaining, `Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
111
131
  });
112
132
  let pipeline = contextPipelineBuilder().compose({ action: this.prepareNavigation.bind(this) });
113
- for (const hook of this.preNavigationHooks) {
133
+ for (const hook of this.#preNavigationHooks) {
114
134
  pipeline = pipeline.compose(windowGuard(hook));
115
135
  }
116
136
  pipeline = pipeline.compose(skipGuard(this.navigate.bind(this)));
117
- for (const hook of this.postNavigationHooks) {
137
+ for (const hook of this.#postNavigationHooks) {
118
138
  pipeline = pipeline.compose(windowGuard(hook));
119
139
  }
120
140
  return pipeline
@@ -125,50 +145,19 @@ export class BrowserCrawler extends BasicCrawler {
125
145
  extendContext,
126
146
  });
127
147
  this.launchContext = launchContext;
128
- this.navigationTimeoutMillis = navigationTimeoutSecs * 1000;
148
+ this.#navigationTimeoutMillis = navigationTimeoutSecs * 1000;
129
149
  // The public option hooks are extension-aware; internal storage uses the base context type
130
150
  // (the pipeline composes hooks against the concrete context, which does not statically carry
131
151
  // `ContextExtension`). The extension members are present at runtime regardless.
132
- this.preNavigationHooks = preNavigationHooks;
133
- this.postNavigationHooks = postNavigationHooks;
152
+ this.#preNavigationHooks = preNavigationHooks;
153
+ this.#postNavigationHooks = postNavigationHooks;
134
154
  this.ignoreIframes = ignoreIframes;
135
155
  this.ignoreShadowRoots = ignoreShadowRoots;
136
- if (headless != null) {
137
- this.launchContext.launchOptions ??= {};
138
- this.launchContext.launchOptions.headless = headless;
139
- }
140
- this.saveResponseCookies = saveResponseCookies;
141
- // `browserPool` wins over `remoteBrowser` — a passed-in pool is used as-is (borrowed), the sugar is ignored.
142
- // The default is only built when no pool was injected, so all the option/launchContext fiddling below stays
143
- // inside the factory.
144
- this.browserPoolDep = OwnedOrInjected.resolve(browserPool, () => {
145
- const resolvedBrowserPoolOptions = browserPoolOptions ?? {};
146
- if (launchContext?.userAgent) {
147
- if (resolvedBrowserPoolOptions.useFingerprints)
148
- this.log.info('Custom user agent provided, disabling automatic browser fingerprint injection!');
149
- resolvedBrowserPoolOptions.useFingerprints = false;
150
- }
151
- if (remoteBrowser) {
152
- // The crawler already built the right plugin for its browser — hand it to a RemoteBrowserPool so the
153
- // remote connection is always for the matching browser (no plugin to construct, no way to mismatch).
154
- const { browserPlugins, ...remoteBrowserPoolOptions } = resolvedBrowserPoolOptions;
155
- return new RemoteBrowserPool({
156
- browserPlugins: browserPlugins,
157
- ...remoteBrowser,
158
- browserPoolOptions: remoteBrowserPoolOptions,
159
- });
160
- }
161
- // Double cast: `BrowserPool` implements `IBrowserPool<PageReturn>`, where `PageReturn` is derived from the
162
- // plugin/controller generics and doesn't overlap with the crawler's free `Page` type param, so TS won't
163
- // narrow it directly. The concrete pool does satisfy the `Page`/`destroy` contract at runtime — this is the
164
- // long-standing `Page` variance gap, not a `destroy`-related hole.
165
- return new BrowserPool({
166
- ...resolvedBrowserPoolOptions,
167
- });
168
- });
156
+ this.#saveResponseCookies = saveResponseCookies;
157
+ this.#browserPoolDep = OwnedOrInjected.resolve(browserPool, () => browserPoolBuilder(remoteBrowser));
169
158
  }
170
159
  getNavigationTimeoutMillis() {
171
- return this.navigationTimeoutMillis;
160
+ return this.#navigationTimeoutMillis;
172
161
  }
173
162
  buildContextPipeline() {
174
163
  return ContextPipeline.create().compose({
@@ -217,7 +206,10 @@ export class BrowserCrawler extends BasicCrawler {
217
206
  session: crawlingContext.session,
218
207
  });
219
208
  tryCancel();
220
- const contextEnqueueLinks = crawlingContext.enqueueLinks;
209
+ const addRequests = crawlingContext.addRequests;
210
+ const extractLinks = async (options) => {
211
+ return extractUrlsFromPage(page, options?.selector ?? 'a', options?.baseUrl ?? crawlingContext.request.loadedUrl ?? crawlingContext.request.url);
212
+ };
221
213
  return {
222
214
  page,
223
215
  get response() {
@@ -226,20 +218,20 @@ export class BrowserCrawler extends BasicCrawler {
226
218
  get gotoOptions() {
227
219
  throw new Error('The `gotoOptions` property is not available until `prepareNavigation` runs.');
228
220
  },
229
- enqueueLinks: async (enqueueOptions = {}) => {
230
- return (await browserCrawlerEnqueueLinks({
231
- options: {
232
- ...enqueueOptions,
233
- limit: await this.calculateEnqueuedRequestLimit(enqueueOptions?.limit),
234
- },
235
- page,
236
- requestManager: await this.getRequestManager(),
237
- robotsTxtFile: await this.getRobotsTxtFileForUrl(crawlingContext.request.url),
238
- onSkippedRequest: this.handleSkippedRequest,
239
- originalRequestUrl: crawlingContext.request.url,
221
+ extractLinks,
222
+ enqueueLinks: async (options = {}) => {
223
+ const baseUrl = resolveBaseUrlForEnqueueLinksFiltering({
224
+ enqueueStrategy: options.strategy,
240
225
  finalRequestUrl: crawlingContext.request.loadedUrl,
241
- enqueueLinks: contextEnqueueLinks,
242
- })); // TODO make this type safe
226
+ originalRequestUrl: crawlingContext.request.url,
227
+ userProvidedBaseUrl: options.baseUrl,
228
+ });
229
+ const urls = await extractLinks(options);
230
+ return addRequests(urls, {
231
+ ...options,
232
+ baseUrl,
233
+ strategy: options.strategy ?? EnqueueStrategy.SameHostname,
234
+ });
243
235
  },
244
236
  };
245
237
  }
@@ -263,31 +255,31 @@ export class BrowserCrawler extends BasicCrawler {
263
255
  return {
264
256
  // Default to the full navigation timeout so a pre-navigation hook can read it; `navigate` narrows it
265
257
  // to the remaining shared window unless a hook overrode it (see there).
266
- gotoOptions: { timeout: this.navigationTimeoutMillis },
267
- [COOKIES_BEFORE_HOOKS]: this._getCookieHeaderFromRequest(crawlingContext.request),
258
+ gotoOptions: { timeout: this.#navigationTimeoutMillis },
259
+ [COOKIES_BEFORE_HOOKS]: this.getCookieHeaderFromRequest(crawlingContext.request),
268
260
  };
269
261
  }
270
262
  async navigate(crawlingContext) {
271
263
  tryCancel();
272
264
  const gotoOptions = crawlingContext.gotoOptions;
273
- const remaining = remainingNavigationWindowMillis(crawlingContext, this.navigationTimeoutMillis);
265
+ const remaining = remainingNavigationWindowMillis(crawlingContext, this.#navigationTimeoutMillis);
274
266
  if (remaining <= 0) {
275
- throw new TimeoutError(`Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
267
+ throw new TimeoutError(`Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
276
268
  }
277
269
  // If a hook left the default `navigationTimeoutMillis` in place, bound the goto to whatever is left of the
278
270
  // shared navigation window. If it overrode the value - including `0`, Playwright's "no timeout" - honour
279
271
  // that verbatim as the goto's own timeout. The driver enforces this natively (so a timed-out goto is
280
272
  // aborted, not left lingering) and `handleNavigationTimeout` turns its error into our own message.
281
273
  const gotoTimeout = gotoOptions;
282
- if (gotoTimeout.timeout === this.navigationTimeoutMillis) {
274
+ if (gotoTimeout.timeout === this.#navigationTimeoutMillis) {
283
275
  gotoTimeout.timeout = remaining;
284
276
  }
285
277
  const cookiesBeforeHooks = readContextField(crawlingContext, COOKIES_BEFORE_HOOKS);
286
- const cookiesAfterHooks = this._getCookieHeaderFromRequest(crawlingContext.request);
278
+ const cookiesAfterHooks = this.getCookieHeaderFromRequest(crawlingContext.request);
287
279
  await this.applyCookies(crawlingContext, cookiesBeforeHooks, cookiesAfterHooks);
288
280
  let response;
289
281
  try {
290
- response = (await this._navigationHandler(crawlingContext, gotoOptions)) ?? undefined;
282
+ response = (await this.navigationHandler(crawlingContext, gotoOptions)) ?? undefined;
291
283
  }
292
284
  catch (error) {
293
285
  await this.handleNavigationTimeout(crawlingContext, error);
@@ -320,7 +312,7 @@ export class BrowserCrawler extends BasicCrawler {
320
312
  * Copies cookies from the live browser page into the session cookie jar.
321
313
  */
322
314
  async persistCookiesFromPage(crawlingContext) {
323
- if (!this.saveResponseCookies || !crawlingContext.session) {
315
+ if (!this.#saveResponseCookies || !crawlingContext.session) {
324
316
  return;
325
317
  }
326
318
  const { cookies } = await this.browserPool.extractPageState(crawlingContext.page);
@@ -329,7 +321,7 @@ export class BrowserCrawler extends BasicCrawler {
329
321
  const url = (await crawlingContext.page.url()) || crawlingContext.request.loadedUrl || crawlingContext.request.url;
330
322
  for (const cookie of cookies) {
331
323
  try {
332
- crawlingContext.session.cookieJar.setCookieSync(browserPoolCookieToToughCookie(cookie), url, {
324
+ await crawlingContext.session.cookieJar.setCookie(browserPoolCookieToToughCookie(cookie), url, {
333
325
  ignoreError: false,
334
326
  });
335
327
  }
@@ -370,7 +362,9 @@ export class BrowserCrawler extends BasicCrawler {
370
362
  return {};
371
363
  }
372
364
  async applyCookies({ session, request, page }, preHooksCookies, postHooksCookies) {
373
- const sessionCookie = session?.cookieJar.getCookiesSync(request.url).map(toughCookieToBrowserPoolCookie) ?? [];
365
+ const sessionCookie = session
366
+ ? (await session.cookieJar.getCookies(request.url)).map(toughCookieToBrowserPoolCookie)
367
+ : [];
374
368
  const parsedPreHooksCookies = preHooksCookies.split(/ *; */).map((c) => cookieStringToToughCookie(c));
375
369
  const parsedPostHooksCookies = postHooksCookies.split(/ *; */).map((c) => cookieStringToToughCookie(c));
376
370
  const cookies = [...sessionCookie, ...parsedPreHooksCookies, ...parsedPostHooksCookies]
@@ -390,7 +384,7 @@ export class BrowserCrawler extends BasicCrawler {
390
384
  session?.markBad();
391
385
  // The driver was handed the remaining window (usually shorter than `navigationTimeoutSecs` once the
392
386
  // hooks have run), so it names that value in its own error; report the configured window instead.
393
- throw new TimeoutError(`Navigation timed out after ${this.navigationTimeoutMillis / 1000} seconds.`);
387
+ throw new TimeoutError(`Navigation timed out after ${this.#navigationTimeoutMillis / 1000} seconds.`);
394
388
  }
395
389
  }
396
390
  /**
@@ -398,14 +392,23 @@ export class BrowserCrawler extends BasicCrawler {
398
392
  */
399
393
  throwIfProxyError(error) {
400
394
  if (this.isProxyError(error)) {
401
- throw new SessionError(this._getMessageFromError(error));
395
+ throw new SessionError(this.getMessageFromError(error));
402
396
  }
403
397
  }
404
398
  async processResponse(response, crawlingContext) {
405
399
  const { session, request, page } = crawlingContext;
406
400
  if (typeof response === 'object' && typeof response.status === 'function') {
407
401
  const status = response.status();
408
- this.stats.registerStatusCode(status);
402
+ this.statistics.registerStatusCode(status);
403
+ // Ahead of the error-status throw below: a 429 the user opted into treating as an error is still a
404
+ // rate limit the domain should back off from.
405
+ if (status === 429) {
406
+ // Both drivers lower-case header names and join duplicates, so a plain lookup is enough.
407
+ const retryAfter = response.headers?.()['retry-after'];
408
+ if (this.recordDomainRateLimit(crawlingContext.request.url, retryAfter)) {
409
+ throw new RequestThrottledError(`${crawlingContext.request.url} responded with 429.`);
410
+ }
411
+ }
409
412
  if (this.isErrorStatusCode(status)) {
410
413
  if (this.additionalHttpErrorStatusCodes.has(status)) {
411
414
  throw new Error(`${status} - Error status code was set by user.`);
@@ -415,7 +418,7 @@ export class BrowserCrawler extends BasicCrawler {
415
418
  }
416
419
  if (this.sessionPool && response && session) {
417
420
  if (typeof response === 'object' && typeof response.status === 'function') {
418
- this._throwOnBlockedRequest(response.status());
421
+ this.throwOnBlockedRequest(response.status());
419
422
  }
420
423
  else {
421
424
  this.log.debug('Got a malformed Browser response.', { request, response });
@@ -428,40 +431,10 @@ export class BrowserCrawler extends BasicCrawler {
428
431
  * @ignore
429
432
  */
430
433
  async teardown() {
431
- await this.browserPoolDep.ifOwned((pool) => pool.destroy());
434
+ await this.#browserPoolDep.ifOwned((pool) => pool.destroy());
432
435
  await super.teardown();
433
436
  }
434
437
  }
435
- /** @internal */
436
- function containsEnqueueLinks(options) {
437
- return !!options.enqueueLinks;
438
- }
439
- /** @internal */
440
- export async function browserCrawlerEnqueueLinks(options) {
441
- const { options: enqueueLinksOptions, finalRequestUrl, originalRequestUrl, page } = options;
442
- const baseUrl = resolveBaseUrlForEnqueueLinksFiltering({
443
- enqueueStrategy: enqueueLinksOptions?.strategy,
444
- finalRequestUrl,
445
- originalRequestUrl,
446
- userProvidedBaseUrl: enqueueLinksOptions?.baseUrl,
447
- });
448
- const urls = await extractUrlsFromPage(page, enqueueLinksOptions?.selector ?? 'a', enqueueLinksOptions?.baseUrl ?? finalRequestUrl ?? originalRequestUrl);
449
- if (containsEnqueueLinks(options)) {
450
- return options.enqueueLinks({
451
- urls,
452
- baseUrl,
453
- ...enqueueLinksOptions,
454
- });
455
- }
456
- return enqueueLinks({
457
- requestManager: options.requestManager,
458
- robotsTxtFile: options.robotsTxtFile,
459
- onSkippedRequest: options.onSkippedRequest,
460
- urls,
461
- baseUrl,
462
- ...enqueueLinksOptions,
463
- });
464
- }
465
438
  /**
466
439
  * Extracts URLs from a given page.
467
440
  * @ignore
@@ -1,6 +1,8 @@
1
1
  import { Configuration } from '@crawlee/basic';
2
- import type { BrowserPlugin, BrowserPluginOptions } from '@crawlee/browser-pool';
2
+ import type { BrowserPlugin, BrowserPluginOptions, BrowserPoolHooks, BrowserPoolOptions, RemoteBrowserPoolOptions } from '@crawlee/browser-pool';
3
+ import { BrowserPool, RemoteBrowserPool } from '@crawlee/browser-pool';
3
4
  import type { Constructor, Dictionary } from '@crawlee/types';
5
+ import { z } from 'zod';
4
6
  export interface BrowserLaunchContext<TOptions, Launcher> extends BrowserPluginOptions<TOptions> {
5
7
  /**
6
8
  * URL to an HTTP proxy server. It must define the port number,
@@ -64,6 +66,19 @@ export interface BrowserLaunchContext<TOptions, Launcher> extends BrowserPluginO
64
66
  */
65
67
  launcher?: Launcher;
66
68
  }
69
+ /**
70
+ * The {@link BrowserPool} options a launcher-built pool accepts: everything the pool itself takes except
71
+ * `browserPlugins`, which the launcher derives from its launch context. The hooks are deliberately unconstrained -
72
+ * the browser they run against is only known to the concrete `*BrowserPool()` factory, which is where the
73
+ * caller-facing types are pinned down.
74
+ */
75
+ export type LauncherBrowserPoolOptions = Omit<BrowserPoolOptions, 'browserPlugins'> & {
76
+ [Hook in keyof BrowserPoolHooks<any, any, any>]?: readonly ((...args: any[]) => unknown)[];
77
+ };
78
+ /**
79
+ * The {@link RemoteBrowserPool} counterpart of {@link LauncherBrowserPoolOptions}.
80
+ */
81
+ export type LauncherRemoteBrowserPoolOptions = Omit<RemoteBrowserPoolOptions, 'browserPlugins'>;
67
82
  /**
68
83
  * Abstract class for creating browser launchers, such as `PlaywrightLauncher` and `PuppeteerLauncher`.
69
84
  * @ignore
@@ -78,23 +93,25 @@ export declare abstract class BrowserLauncher<Plugin extends BrowserPlugin, Laun
78
93
  Plugin: T;
79
94
  userAgent?: string;
80
95
  protected static optionsShape: {
81
- // @ts-ignore optional peer dependency or compatibility with es2022
82
- proxyUrl: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
83
- // @ts-ignore optional peer dependency or compatibility with es2022
84
- useChrome: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
85
- // @ts-ignore optional peer dependency or compatibility with es2022
86
- useIncognitoPages: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
87
- // @ts-ignore optional peer dependency or compatibility with es2022
88
- browserPerProxy: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
89
- // @ts-ignore optional peer dependency or compatibility with es2022
90
- ignoreProxyCertificate: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
91
- // @ts-ignore optional peer dependency or compatibility with es2022
92
- userDataDir: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
93
- // @ts-ignore optional peer dependency or compatibility with es2022
94
- launchOptions: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
95
- // @ts-ignore optional peer dependency or compatibility with es2022
96
- userAgent: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
96
+ proxyUrl: z.ZodOptional<z.ZodURL>;
97
+ useChrome: z.ZodOptional<z.ZodBoolean>;
98
+ useIncognitoPages: z.ZodOptional<z.ZodBoolean>;
99
+ browserPerProxy: z.ZodOptional<z.ZodBoolean>;
100
+ ignoreProxyCertificate: z.ZodOptional<z.ZodBoolean>;
101
+ userDataDir: z.ZodOptional<z.ZodString>;
102
+ launchOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
103
+ userAgent: z.ZodOptional<z.ZodString>;
97
104
  };
105
+ protected static optionsSchema: z.ZodObject<{
106
+ proxyUrl: z.ZodOptional<z.ZodURL>;
107
+ useChrome: z.ZodOptional<z.ZodBoolean>;
108
+ useIncognitoPages: z.ZodOptional<z.ZodBoolean>;
109
+ browserPerProxy: z.ZodOptional<z.ZodBoolean>;
110
+ ignoreProxyCertificate: z.ZodOptional<z.ZodBoolean>;
111
+ userDataDir: z.ZodOptional<z.ZodString>;
112
+ launchOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
113
+ userAgent: z.ZodOptional<z.ZodString>;
114
+ }, z.core.$strict>;
98
115
  static requireLauncherOrThrow<T>(launcher: string, apifyImageName: string): T;
99
116
  /**
100
117
  * All `BrowserLauncher` parameters are passed via an launchContext object.
@@ -104,6 +121,26 @@ export declare abstract class BrowserLauncher<Plugin extends BrowserPlugin, Laun
104
121
  * @ignore
105
122
  */
106
123
  createBrowserPlugin(): Plugin;
124
+ /**
125
+ * Builds a {@link BrowserPool} running a single plugin for this launcher's browser. Shared body of the
126
+ * per-library `*BrowserPool()` factories, which exist so that configuring a pool never requires assembling
127
+ * a plugin by hand — and therefore never lets the plugin drift away from the crawler it is used with.
128
+ * @internal
129
+ */
130
+ createBrowserPool(options?: LauncherBrowserPoolOptions): BrowserPool<{
131
+ browserPlugins: [Plugin];
132
+ }, [Plugin]>;
133
+ /**
134
+ * The {@link RemoteBrowserPool} counterpart of {@link BrowserLauncher.createBrowserPool}: the launcher
135
+ * supplies the plugin, the caller supplies the remote connection details.
136
+ * @internal
137
+ */
138
+ createRemoteBrowserPool<Page>(options: LauncherRemoteBrowserPoolOptions): RemoteBrowserPool<Page>;
139
+ /**
140
+ * A custom `userAgent` and Crawlee's fingerprint injection would both write the same headers, so an
141
+ * explicitly requested user agent wins.
142
+ */
143
+ private resolveFingerprinting;
107
144
  /**
108
145
  * Launches a browser instance based on the plugin.
109
146
  * @returns Browser instance.
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import os from 'node:os';
4
- import { Configuration } from '@crawlee/basic';
5
- import ow from 'ow';
4
+ import { Configuration, schemas, serviceLocator } from '@crawlee/basic';
5
+ import { BrowserPool, RemoteBrowserPool } from '@crawlee/browser-pool';
6
+ import { z } from 'zod';
6
7
  const DEFAULT_VIEWPORT = {
7
8
  width: 1366,
8
9
  height: 768,
@@ -23,15 +24,16 @@ export class BrowserLauncher {
23
24
  Plugin;
24
25
  userAgent;
25
26
  static optionsShape = {
26
- proxyUrl: ow.optional.string.url,
27
- useChrome: ow.optional.boolean,
28
- useIncognitoPages: ow.optional.boolean,
29
- browserPerProxy: ow.optional.boolean,
30
- ignoreProxyCertificate: ow.optional.boolean,
31
- userDataDir: ow.optional.string,
32
- launchOptions: ow.optional.object,
33
- userAgent: ow.optional.string,
27
+ proxyUrl: z.url().optional(),
28
+ useChrome: z.boolean().optional(),
29
+ useIncognitoPages: z.boolean().optional(),
30
+ browserPerProxy: z.boolean().optional(),
31
+ ignoreProxyCertificate: z.boolean().optional(),
32
+ userDataDir: z.string().optional(),
33
+ launchOptions: schemas.anyObject.optional(),
34
+ userAgent: z.string().optional(),
34
35
  };
36
+ static optionsSchema = z.strictObject(BrowserLauncher.optionsShape);
35
37
  static requireLauncherOrThrow(launcher, apifyImageName) {
36
38
  try {
37
39
  return require(launcher); // eslint-disable-line
@@ -73,6 +75,49 @@ export class BrowserLauncher {
73
75
  ...this.otherLaunchContextProps,
74
76
  });
75
77
  }
78
+ /**
79
+ * Builds a {@link BrowserPool} running a single plugin for this launcher's browser. Shared body of the
80
+ * per-library `*BrowserPool()` factories, which exist so that configuring a pool never requires assembling
81
+ * a plugin by hand — and therefore never lets the plugin drift away from the crawler it is used with.
82
+ * @internal
83
+ */
84
+ createBrowserPool(options = {}) {
85
+ // The hook types `BrowserPool` derives from `Plugin` are unresolvable while `Plugin` is still a free type
86
+ // parameter, so the argument cannot be checked here. The concrete `*BrowserPool()` factories are where the
87
+ // caller-facing hook types get pinned down.
88
+ return new BrowserPool({
89
+ ...this.resolveFingerprinting(options),
90
+ browserPlugins: [this.createBrowserPlugin()],
91
+ });
92
+ }
93
+ /**
94
+ * The {@link RemoteBrowserPool} counterpart of {@link BrowserLauncher.createBrowserPool}: the launcher
95
+ * supplies the plugin, the caller supplies the remote connection details.
96
+ * @internal
97
+ */
98
+ createRemoteBrowserPool(options) {
99
+ return new RemoteBrowserPool({
100
+ ...options,
101
+ browserPlugins: [this.createBrowserPlugin()],
102
+ browserPoolOptions: this.resolveFingerprinting(options.browserPoolOptions ?? {}),
103
+ });
104
+ }
105
+ /**
106
+ * A custom `userAgent` and Crawlee's fingerprint injection would both write the same headers, so an
107
+ * explicitly requested user agent wins.
108
+ */
109
+ resolveFingerprinting(options) {
110
+ if (!this.userAgent) {
111
+ return options;
112
+ }
113
+ if (options.useFingerprints) {
114
+ serviceLocator
115
+ .getLogger()
116
+ .child({ prefix: 'BrowserLauncher' })
117
+ .info('Custom user agent provided, disabling automatic browser fingerprint injection!');
118
+ }
119
+ return { ...options, useFingerprints: false };
120
+ }
76
121
  /**
77
122
  * Launches a browser instance based on the plugin.
78
123
  * @returns Browser instance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/browser",
3
- "version": "4.0.0-beta.99",
3
+ "version": "4.0.0-rc.0",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -48,13 +48,13 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@apify/timeout": "^0.4.4",
51
- "@crawlee/basic": "4.0.0-beta.99",
52
- "@crawlee/browser-pool": "4.0.0-beta.99",
53
- "@crawlee/types": "4.0.0-beta.99",
54
- "@crawlee/utils": "4.0.0-beta.99",
55
- "ow": "^2.0.0",
51
+ "@crawlee/basic": "4.0.0-rc.0",
52
+ "@crawlee/browser-pool": "4.0.0-rc.0",
53
+ "@crawlee/types": "4.0.0-rc.0",
54
+ "@crawlee/utils": "4.0.0-rc.0",
56
55
  "tslib": "^2.8.1",
57
- "type-fest": "^4.41.0"
56
+ "type-fest": "^4.41.0",
57
+ "zod": "^4.4.3"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "playwright": "*",
@@ -75,5 +75,5 @@
75
75
  }
76
76
  }
77
77
  },
78
- "gitHead": "ad2748380941842bb10cff100f4b4caad92049e3"
78
+ "gitHead": "79ab33dacdacb83e0197e6516d145f3aceef80c7"
79
79
  }