@crawlee/browser 4.0.0-beta.125 → 4.0.0-beta.127

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,6 +1,6 @@
1
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';
3
+ import type { CommonPage, CrawlerRemoteBrowserOptions } from '@crawlee/browser-pool';
4
4
  import type { Awaitable, Dictionary, IBrowserPool } from '@crawlee/types';
5
5
  import { z } from 'zod';
6
6
  import type { BrowserLaunchContext } from './browser-launcher.js';
@@ -9,6 +9,20 @@ interface BaseResponse {
9
9
  /** Optional because only Playwright and Puppeteer responses are guaranteed to carry it. */
10
10
  headers?(): Record<string, string>;
11
11
  }
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;
12
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
13
27
  GoToOptions extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
14
28
  /**
@@ -38,12 +52,20 @@ GoToOptions extends Dictionary = Dictionary> extends CrawlingContext<UserData> {
38
52
  enqueueLinks: (options?: EnqueueLinksOptions) => Promise<AddRequestsBatchedResult>;
39
53
  }
40
54
  export type BrowserHook<Context = BrowserCrawlingContext, ContextExtension = {}> = (crawlingContext: Context & ContextExtension) => Awaitable<void | Partial<Context>>;
41
- 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, InternalBrowserPoolOptions extends BrowserPoolOptions = BrowserPoolOptions, Routes extends Record<keyof Routes, Dictionary> = Record<string, GetUserDataFromRequest<Context['request']>>, StatisticStateExtension extends object = {}, __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, Routes, StatisticStateExtension>, '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'> {
42
56
  launchContext?: BrowserLaunchContext<any, any>;
43
57
  /**
44
- * An existing browser pool instance to use. When provided, the crawler will use this pool directly instead of
45
- * constructing a new one from `browserPoolOptions`, enabling browser sharing across multiple crawlers. The crawler
46
- * 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.
47
69
  */
48
70
  browserPool?: IBrowserPool<Page>;
49
71
  /**
@@ -54,8 +76,9 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
54
76
  * crawler. Supply the connection details only: a static `endpoint` URL, a function returning one per launch,
55
77
  * or a {@link RemoteBrowserProvider}.
56
78
  *
57
- * Ignored when `browserPool` is set. For sharing a remote pool across crawlers, construct a
58
- * {@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`.
59
82
  */
60
83
  remoteBrowser?: CrawlerRemoteBrowserOptions;
61
84
  /**
@@ -106,11 +129,6 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
106
129
  * represents the last error thrown during processing of the request.
107
130
  */
108
131
  failedRequestHandler?: ErrorHandler<CrawlingContext, ExtendedContext>;
109
- /**
110
- * Custom options passed to the underlying {@link BrowserPool} constructor.
111
- * We can tweak those to fine-tune browser management.
112
- */
113
- browserPoolOptions?: Partial<BrowserPoolOptions> & Partial<BrowserPoolHooks<__BrowserControllerReturn, __LaunchContextReturn>>;
114
132
  /**
115
133
  * Async functions that are sequentially evaluated before the navigation. Good for setting additional cookies
116
134
  * or browser properties before navigation. The function receives the `crawlingContext`; the options object
@@ -175,11 +193,6 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
175
193
  * Defines whether the cookies should be persisted for sessions. Enabled by default.
176
194
  */
177
195
  saveResponseCookies?: boolean;
178
- /**
179
- * Whether to run browser in headless mode. Defaults to `true`.
180
- * Can be also set via {@link Configuration}.
181
- */
182
- headless?: boolean | 'new' | 'old';
183
196
  /**
184
197
  * Whether to ignore custom elements (and their #shadow-roots) when processing the page content via `parseWithCheerio` helper.
185
198
  * By default, they are expanded automatically. Use this option to disable this behavior.
@@ -231,7 +244,7 @@ export interface BrowserCrawlerOptions<Page extends CommonPage = CommonPage, Res
231
244
  *
232
245
  * @category Crawlers
233
246
  */
234
- 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> = 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> {
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> {
235
248
  #private;
236
249
  /**
237
250
  * A reference to the underlying browser pool that manages the crawler's browsers. Typed as
@@ -246,10 +259,9 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
246
259
  preNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
247
260
  postNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
248
261
  launchContext: z.ZodDefault<z.ZodCustom<Dictionary, Dictionary>>;
249
- headless: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
250
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>>;
251
264
  remoteBrowser: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
252
- browserPoolOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
253
265
  saveResponseCookies: z.ZodDefault<z.ZodBoolean>;
254
266
  proxyConfiguration: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
255
267
  ignoreIframes: z.ZodDefault<z.ZodBoolean>;
@@ -304,10 +316,9 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
304
316
  preNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
305
317
  postNavigationHooks: z.ZodDefault<z.ZodCustom<unknown[], unknown[]>>;
306
318
  launchContext: z.ZodDefault<z.ZodCustom<Dictionary, Dictionary>>;
307
- headless: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
308
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>>;
309
321
  remoteBrowser: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
310
- browserPoolOptions: z.ZodOptional<z.ZodCustom<Dictionary, Dictionary>>;
311
322
  saveResponseCookies: z.ZodDefault<z.ZodBoolean>;
312
323
  proxyConfiguration: z.ZodOptional<z.ZodType<Dictionary<any>, unknown, z.core.$ZodTypeInternals<Dictionary<any>, unknown>>>;
313
324
  ignoreIframes: z.ZodDefault<z.ZodBoolean>;
@@ -360,8 +371,14 @@ export declare abstract class BrowserCrawler<Page extends CommonPage = CommonPag
360
371
  /**
361
372
  * All `BrowserCrawler` parameters are passed via an options object.
362
373
  */
363
- protected constructor(options: BrowserCrawlerOptions<Page, Response, Context, ContextExtension, ExtendedContext, BrowserPoolOptions, Routes, StatisticStateExtension> & {
374
+ protected constructor(options: BrowserCrawlerOptions<Page, Response, Context, ContextExtension, ExtendedContext, Routes, StatisticStateExtension> & {
364
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>;
365
382
  });
366
383
  protected getNavigationTimeoutMillis(): number;
367
384
  protected buildContextPipeline(): ContextPipeline<CrawlingContext, BrowserCrawlingContext<Page, Response, Dictionary>>;
@@ -1,9 +1,22 @@
1
1
  import { BasicCrawler, browserPoolCookieToToughCookie, ContextPipeline, cookieStringToToughCookie, EnqueueStrategy, NavigationSkippedError, OwnedOrInjected, parseArgument, remainingNavigationWindowMillis, RequestState, RequestThrottledError, resolveBaseUrlForEnqueueLinksFiltering, schemas, SessionError, toughCookieToBrowserPoolCookie, tryAbsoluteURL, validators, } from '@crawlee/basic';
2
- import { BrowserPool, RemoteBrowserPool } from '@crawlee/browser-pool';
3
2
  import { CLOUDFLARE_RETRY_CSS_SELECTORS, RETRY_CSS_SELECTORS } from '@crawlee/utils/internal';
4
3
  import { sleep } from '@crawlee/utils';
5
4
  import { z } from 'zod';
6
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
+ }
7
20
  const COOKIES_BEFORE_HOOKS = Symbol('cookiesBeforeHooks');
8
21
  const readContextField = (ctx, key) => ctx[key];
9
22
  /**
@@ -82,10 +95,9 @@ export class BrowserCrawler extends BasicCrawler {
82
95
  preNavigationHooks: schemas.anyArray.default(() => []),
83
96
  postNavigationHooks: schemas.anyArray.default(() => []),
84
97
  launchContext: schemas.anyObject.default(() => ({})),
85
- headless: z.union([z.boolean(), z.string()]).optional(),
86
98
  browserPool: validators.browserPool.optional(),
99
+ browserPoolBuilder: schemas.anyFunction.optional(),
87
100
  remoteBrowser: schemas.anyObject.optional(),
88
- browserPoolOptions: schemas.anyObject.optional(),
89
101
  saveResponseCookies: z.boolean().default(true),
90
102
  proxyConfiguration: validators.proxyConfiguration.optional(),
91
103
  ignoreIframes: z.boolean().default(false),
@@ -96,7 +108,10 @@ export class BrowserCrawler extends BasicCrawler {
96
108
  * All `BrowserCrawler` parameters are passed via an options object.
97
109
  */
98
110
  constructor(options) {
99
- const { navigationTimeoutSecs, saveResponseCookies, launchContext, browserPool, remoteBrowser, browserPoolOptions, preNavigationHooks, postNavigationHooks, headless, ignoreIframes, ignoreShadowRoots, contextPipelineBuilder, extendContext, ...basicCrawlerOptions } = parseArgument(options, BrowserCrawler.optionsSchema, 'BrowserCrawlerOptions');
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
+ }
100
115
  const skipGuard = (action) => ({
101
116
  action: async (ctx) => (ctx.request.skipNavigation ? {} : ((await action(ctx)) ?? {})),
102
117
  });
@@ -138,39 +153,8 @@ export class BrowserCrawler extends BasicCrawler {
138
153
  this.#postNavigationHooks = postNavigationHooks;
139
154
  this.ignoreIframes = ignoreIframes;
140
155
  this.ignoreShadowRoots = ignoreShadowRoots;
141
- if (headless != null) {
142
- this.launchContext.launchOptions ??= {};
143
- this.launchContext.launchOptions.headless = headless;
144
- }
145
156
  this.#saveResponseCookies = saveResponseCookies;
146
- // `browserPool` wins over `remoteBrowser` — a passed-in pool is used as-is (borrowed), the sugar is ignored.
147
- // The default is only built when no pool was injected, so all the option/launchContext fiddling below stays
148
- // inside the factory.
149
- this.#browserPoolDep = OwnedOrInjected.resolve(browserPool, () => {
150
- const resolvedBrowserPoolOptions = browserPoolOptions ?? {};
151
- if (launchContext?.userAgent) {
152
- if (resolvedBrowserPoolOptions.useFingerprints)
153
- this.log.info('Custom user agent provided, disabling automatic browser fingerprint injection!');
154
- resolvedBrowserPoolOptions.useFingerprints = false;
155
- }
156
- if (remoteBrowser) {
157
- // The crawler already built the right plugin for its browser — hand it to a RemoteBrowserPool so the
158
- // remote connection is always for the matching browser (no plugin to construct, no way to mismatch).
159
- const { browserPlugins, ...remoteBrowserPoolOptions } = resolvedBrowserPoolOptions;
160
- return new RemoteBrowserPool({
161
- browserPlugins: browserPlugins,
162
- ...remoteBrowser,
163
- browserPoolOptions: remoteBrowserPoolOptions,
164
- });
165
- }
166
- // Double cast: `BrowserPool` implements `IBrowserPool<PageReturn>`, where `PageReturn` is derived from the
167
- // plugin/controller generics and doesn't overlap with the crawler's free `Page` type param, so TS won't
168
- // narrow it directly. The concrete pool does satisfy the `Page`/`destroy` contract at runtime — this is the
169
- // long-standing `Page` variance gap, not a `destroy`-related hole.
170
- return new BrowserPool({
171
- ...resolvedBrowserPoolOptions,
172
- });
173
- });
157
+ this.#browserPoolDep = OwnedOrInjected.resolve(browserPool, () => browserPoolBuilder(remoteBrowser));
174
158
  }
175
159
  getNavigationTimeoutMillis() {
176
160
  return this.#navigationTimeoutMillis;
@@ -415,7 +399,7 @@ export class BrowserCrawler extends BasicCrawler {
415
399
  const { session, request, page } = crawlingContext;
416
400
  if (typeof response === 'object' && typeof response.status === 'function') {
417
401
  const status = response.status();
418
- this.stats.registerStatusCode(status);
402
+ this.statistics.registerStatusCode(status);
419
403
  // Ahead of the error-status throw below: a 429 the user opted into treating as an error is still a
420
404
  // rate limit the domain should back off from.
421
405
  if (status === 429) {
@@ -1,5 +1,6 @@
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';
4
5
  import { z } from 'zod';
5
6
  export interface BrowserLaunchContext<TOptions, Launcher> extends BrowserPluginOptions<TOptions> {
@@ -65,6 +66,19 @@ export interface BrowserLaunchContext<TOptions, Launcher> extends BrowserPluginO
65
66
  */
66
67
  launcher?: Launcher;
67
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'>;
68
82
  /**
69
83
  * Abstract class for creating browser launchers, such as `PlaywrightLauncher` and `PuppeteerLauncher`.
70
84
  * @ignore
@@ -107,6 +121,26 @@ export declare abstract class BrowserLauncher<Plugin extends BrowserPlugin, Laun
107
121
  * @ignore
108
122
  */
109
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;
110
144
  /**
111
145
  * Launches a browser instance based on the plugin.
112
146
  * @returns Browser instance.
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import { createRequire } from 'node:module';
3
3
  import os from 'node:os';
4
- import { Configuration, schemas } from '@crawlee/basic';
4
+ import { Configuration, schemas, serviceLocator } from '@crawlee/basic';
5
+ import { BrowserPool, RemoteBrowserPool } from '@crawlee/browser-pool';
5
6
  import { z } from 'zod';
6
7
  const DEFAULT_VIEWPORT = {
7
8
  width: 1366,
@@ -74,6 +75,49 @@ export class BrowserLauncher {
74
75
  ...this.otherLaunchContextProps,
75
76
  });
76
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
+ }
77
121
  /**
78
122
  * Launches a browser instance based on the plugin.
79
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.125",
3
+ "version": "4.0.0-beta.127",
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,10 +48,10 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@apify/timeout": "^0.4.4",
51
- "@crawlee/basic": "4.0.0-beta.125",
52
- "@crawlee/browser-pool": "4.0.0-beta.125",
53
- "@crawlee/types": "4.0.0-beta.125",
54
- "@crawlee/utils": "4.0.0-beta.125",
51
+ "@crawlee/basic": "4.0.0-beta.127",
52
+ "@crawlee/browser-pool": "4.0.0-beta.127",
53
+ "@crawlee/types": "4.0.0-beta.127",
54
+ "@crawlee/utils": "4.0.0-beta.127",
55
55
  "tslib": "^2.8.1",
56
56
  "type-fest": "^4.41.0",
57
57
  "zod": "^4.4.3"
@@ -75,5 +75,5 @@
75
75
  }
76
76
  }
77
77
  },
78
- "gitHead": "04a7212dd4abe4a5f94f5fc9632acb6089819c8b"
78
+ "gitHead": "4aa4a4d8d105bb530649ac5cd9197a3169106bd9"
79
79
  }