@crawlee/puppeteer 4.0.0-beta.2 → 4.0.0-beta.200

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.
package/README.md CHANGED
@@ -1,19 +1,23 @@
1
1
  <h1 align="center">
2
2
  <a href="https://crawlee.dev">
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/apify/crawlee/master/website/static/img/crawlee-dark.svg?sanitize=true">
5
- <img alt="Crawlee" src="https://raw.githubusercontent.com/apify/crawlee/master/website/static/img/crawlee-light.svg?sanitize=true" width="500">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/apify/crawlee/master/website/static/img/crawlee-dark.svg?sanitize=true" />
5
+ <img alt="Crawlee" src="https://raw.githubusercontent.com/apify/crawlee/master/website/static/img/crawlee-light.svg?sanitize=true" width="500" />
6
6
  </picture>
7
7
  </a>
8
- <br>
8
+ <br />
9
9
  <small>A web scraping and browser automation library</small>
10
10
  </h1>
11
11
 
12
- <p align=center>
13
- <a href="https://www.npmjs.com/package/@crawlee/core" rel="nofollow"><img src="https://img.shields.io/npm/v/@crawlee/core.svg" alt="NPM latest version" data-canonical-src="https://img.shields.io/npm/v/@crawlee/core/next.svg" style="max-width: 100%;"></a>
14
- <a href="https://www.npmjs.com/package/@crawlee/core" rel="nofollow"><img src="https://img.shields.io/npm/dm/@crawlee/core.svg" alt="Downloads" data-canonical-src="https://img.shields.io/npm/dm/@crawlee/core.svg" style="max-width: 100%;"></a>
15
- <a href="https://discord.gg/jyEM2PRvMU" rel="nofollow"><img src="https://img.shields.io/discord/801163717915574323?label=discord" alt="Chat on discord" data-canonical-src="https://img.shields.io/discord/801163717915574323?label=discord" style="max-width: 100%;"></a>
16
- <a href="https://github.com/apify/crawlee/actions/workflows/test-ci.yml"><img src="https://github.com/apify/crawlee/actions/workflows/test-ci.yml/badge.svg?branch=master" alt="Build Status" style="max-width: 100%;"></a>
12
+ <p align="center">
13
+ <a href="https://trendshift.io/repositories/5179" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5179" alt="apify%2Fcrawlee | Trendshift" width="250" height="55"/></a>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://www.npmjs.com/package/@crawlee/core" rel="nofollow"><img src="https://img.shields.io/npm/v/@crawlee/core.svg" alt="NPM latest version" data-canonical-src="https://img.shields.io/npm/v/@crawlee/core/next.svg" /></a>
18
+ <a href="https://www.npmjs.com/package/@crawlee/core" rel="nofollow"><img src="https://img.shields.io/npm/dm/@crawlee/core.svg" alt="Downloads" data-canonical-src="https://img.shields.io/npm/dm/@crawlee/core.svg" /></a>
19
+ <a href="https://discord.gg/jyEM2PRvMU" rel="nofollow"><img src="https://img.shields.io/discord/801163717915574323?label=discord" alt="Chat on discord" data-canonical-src="https://img.shields.io/discord/801163717915574323?label=discord" /></a>
20
+ <a href="https://github.com/apify/crawlee/actions/workflows/test-ci.yml"><img src="https://github.com/apify/crawlee/actions/workflows/test-ci.yml/badge.svg?branch=master" alt="Build Status" /></a>
17
21
  </p>
18
22
 
19
23
  Crawlee covers your crawling and scraping end-to-end and **helps you build reliable scrapers. Fast.**
@@ -24,7 +28,7 @@ Crawlee is available as the [`crawlee`](https://www.npmjs.com/package/crawlee) N
24
28
 
25
29
  > 👉 **View full documentation, guides and examples on the [Crawlee project website](https://crawlee.dev)** 👈
26
30
 
27
- > Crawlee for Python is open for early adopters. 🐍 [👉 Checkout the source code 👈](https://github.com/apify/crawlee-python).
31
+ > Do you prefer 🐍 Python instead of JavaScript? [👉 Checkout Crawlee for Python 👈](https://github.com/apify/crawlee-python).
28
32
 
29
33
  ## Installation
30
34
 
@@ -85,7 +89,7 @@ By default, Crawlee stores data to `./storage` in the current working directory.
85
89
  We provide automated beta builds for every merged code change in Crawlee. You can find them in the npm [list of releases](https://www.npmjs.com/package/crawlee?activeTab=versions). If you want to test new features or bug fixes before we release them, feel free to install a beta build like this:
86
90
 
87
91
  ```bash
88
- npm install crawlee@3.12.3-beta.13
92
+ npm install crawlee@next
89
93
  ```
90
94
 
91
95
  If you also use the [Apify SDK](https://github.com/apify/apify-sdk-js), you need to specify dependency overrides in your `package.json` file so that you don't end up with multiple versions of Crawlee installed:
@@ -94,9 +98,9 @@ If you also use the [Apify SDK](https://github.com/apify/apify-sdk-js), you need
94
98
  {
95
99
  "overrides": {
96
100
  "apify": {
97
- "@crawlee/core": "3.12.3-beta.13",
98
- "@crawlee/types": "3.12.3-beta.13",
99
- "@crawlee/utils": "3.12.3-beta.13"
101
+ "@crawlee/core": "$crawlee",
102
+ "@crawlee/types": "$crawlee",
103
+ "@crawlee/utils": "$crawlee"
100
104
  }
101
105
  }
102
106
  }
package/index.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  export * from '@crawlee/browser';
2
+ export * from './internals/puppeteer-browser-pool.js';
2
3
  export * from './internals/puppeteer-crawler.js';
3
4
  export * from './internals/puppeteer-launcher.js';
4
- export * as puppeteerRequestInterception from './internals/utils/puppeteer_request_interception.js';
5
5
  export type { InterceptHandler } from './internals/utils/puppeteer_request_interception.js';
6
6
  export * as puppeteerUtils from './internals/utils/puppeteer_utils.js';
7
- export type { BlockRequestsOptions, CompiledScriptFunction, CompiledScriptParams, DirectNavigationOptions as PuppeteerDirectNavigationOptions, InfiniteScrollOptions, InjectFileOptions, SaveSnapshotOptions, } from './internals/utils/puppeteer_utils.js';
8
- export * as puppeteerClickElements from './internals/enqueue-links/click-elements.js';
7
+ export type { DirectNavigationOptions as PuppeteerDirectNavigationOptions } from './internals/utils/puppeteer_utils.js';
9
8
  export type { EnqueueLinksByClickingElementsOptions } from './internals/enqueue-links/click-elements.js';
10
- //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,7 +1,5 @@
1
1
  export * from '@crawlee/browser';
2
+ export * from './internals/puppeteer-browser-pool.js';
2
3
  export * from './internals/puppeteer-crawler.js';
3
4
  export * from './internals/puppeteer-launcher.js';
4
- export * as puppeteerRequestInterception from './internals/utils/puppeteer_request_interception.js';
5
5
  export * as puppeteerUtils from './internals/utils/puppeteer_utils.js';
6
- export * as puppeteerClickElements from './internals/enqueue-links/click-elements.js';
7
- //# sourceMappingURL=index.js.map
@@ -1,16 +1,16 @@
1
- import type { GlobInput, PseudoUrlInput, RegExpInput, RequestProvider, RequestTransform } from '@crawlee/browser';
1
+ import type { IRequestManager, RequestTransform, SkippedRequestCallback, UrlPatternInput } from '@crawlee/browser';
2
2
  import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
3
3
  // @ts-ignore optional peer dependency or compatibility with es2022
4
- import type { ClickOptions, Page, Target } from 'puppeteer';
4
+ import type { ClickOptions, Page } from 'puppeteer';
5
5
  export interface EnqueueLinksByClickingElementsOptions {
6
6
  /**
7
7
  * Puppeteer [`Page`](https://pptr.dev/#?product=Puppeteer&show=api-class-page) object.
8
8
  */
9
9
  page: Page;
10
10
  /**
11
- * A request queue to which the URLs will be enqueued.
11
+ * * A request manager to which the URLs will be enqueued.
12
12
  */
13
- requestQueue: RequestProvider;
13
+ requestManager: IRequestManager;
14
14
  /**
15
15
  * A CSS selector matching elements to be clicked on. Unlike in {@link enqueueLinks}, there is no default
16
16
  * value. This is to prevent suboptimal use of this function by using it too broadly.
@@ -25,72 +25,50 @@ export interface EnqueueLinksByClickingElementsOptions {
25
25
  */
26
26
  clickOptions?: ClickOptions;
27
27
  /**
28
- * An array of glob pattern strings or plain objects
29
- * containing glob pattern strings matching the URLs to be enqueued.
28
+ * An array of URL patterns that URLs must match to be enqueued.
30
29
  *
31
- * The plain objects must include at least the `glob` property, which holds the glob pattern string.
32
- * All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
30
+ * Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
33
31
  *
34
- * The matching is always case-insensitive.
35
- * If you need case-sensitive matching, use `regexps` property directly.
32
+ * Glob matching is always case-insensitive.
33
+ * If you need case-sensitive matching, use a `RegExp`.
36
34
  *
37
- * If `globs` is an empty array or `undefined`, then the function
35
+ * If `include` is an empty array or `undefined`, then the function
38
36
  * enqueues all the intercepted navigation requests produced by the page
39
37
  * after clicking on elements matching the provided CSS selector.
40
38
  */
41
- globs?: GlobInput[];
39
+ include?: UrlPatternInput[];
42
40
  /**
43
- * An array of regular expressions or plain objects
44
- * containing regular expressions matching the URLs to be enqueued.
41
+ * An array of URL patterns. Matching URLs will **not** be enqueued.
45
42
  *
46
- * The plain objects must include at least the `regexp` property, which holds the regular expression.
47
- * All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
43
+ * Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
48
44
  *
49
- * If `regexps` is an empty array or `undefined`, then the function
50
- * enqueues all the intercepted navigation requests produced by the page
51
- * after clicking on elements matching the provided CSS selector.
52
- */
53
- regexps?: RegExpInput[];
54
- /**
55
- * *NOTE:* In future versions of SDK the options will be removed.
56
- * Please use `globs` or `regexps` instead.
57
- *
58
- * An array of {@link PseudoUrl} strings or plain objects
59
- * containing {@link PseudoUrl} strings matching the URLs to be enqueued.
60
- *
61
- * The plain objects must include at least the `purl` property, which holds the pseudo-URL pattern string.
62
- * All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
63
- *
64
- * With a pseudo-URL string, the matching is always case-insensitive.
65
- * If you need case-sensitive matching, use `regexps` property directly.
66
- *
67
- * If `pseudoUrls` is an empty array or `undefined`, then the function
68
- * enqueues all the intercepted navigation requests produced by the page
69
- * after clicking on elements matching the provided CSS selector.
70
- *
71
- * @deprecated prefer using `globs` or `regexps` instead
45
+ * Glob matching is always case-insensitive.
46
+ * If you need case-sensitive matching, use a `RegExp`.
72
47
  */
73
- pseudoUrls?: PseudoUrlInput[];
48
+ exclude?: readonly UrlPatternInput[];
74
49
  /**
75
- * Just before a new {@link Request} is constructed and enqueued to the {@link RequestQueue}, this function can be used
76
- * to remove it or modify its contents such as `userData`, `payload` or, most importantly `uniqueKey`. This is useful
77
- * when you need to enqueue multiple `Requests` to the queue that share the same URL, but differ in methods or payloads,
78
- * or to dynamically update or create `userData`.
79
- *
80
- * For example: by adding `useExtendedUniqueKey: true` to the `request` object, `uniqueKey` will be computed from
81
- * a combination of `url`, `method` and `payload` which enables crawling of websites that navigate using form submits
82
- * (POST requests).
50
+ * After request options are filtered by `include`/`exclude` patterns,
51
+ * this function can be used to remove them or modify their contents such as `userData`, `payload` or, most importantly
52
+ * `uniqueKey`. This is useful when you need to enqueue multiple `Requests` to the queue that share the same URL,
53
+ * but differ in methods or payloads, or to dynamically update or create `userData`.
83
54
  *
84
55
  * **Example:**
85
56
  * ```javascript
86
57
  * {
87
58
  * transformRequestFunction: (request) => {
88
59
  * request.userData.foo = 'bar';
89
- * request.useExtendedUniqueKey = true;
90
60
  * return request;
91
61
  * }
92
62
  * }
93
63
  * ```
64
+ *
65
+ * Note that `transformRequestFunction` has the highest priority and can overwrite
66
+ * the global `label` option.
67
+ *
68
+ * The function receives a {@link RequestOptions} object and can return either:
69
+ * - The modified {@link RequestOptions} object
70
+ * - `'unchanged'` to keep the original options as-is
71
+ * - A falsy value or `'skip'` to exclude the request from the queue
94
72
  */
95
73
  transformRequestFunction?: RequestTransform;
96
74
  /**
@@ -130,6 +108,12 @@ export interface EnqueueLinksByClickingElementsOptions {
130
108
  * @default false
131
109
  */
132
110
  skipNavigation?: boolean;
111
+ /**
112
+ * When a request is skipped for some reason, you can use this callback to act on it.
113
+ * This is fired for requests skipped because they don't match enqueueLinks filters
114
+ * or because they were removed by `transformRequestFunction`.
115
+ */
116
+ onSkippedRequest?: SkippedRequestCallback;
133
117
  }
134
118
  /**
135
119
  * The function finds elements matching a specific CSS selector in a Puppeteer page,
@@ -140,8 +124,7 @@ export interface EnqueueLinksByClickingElementsOptions {
140
124
  * in `href` elements, but rather navigations are triggered in click handlers.
141
125
  * If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
142
126
  *
143
- * Optionally, the function allows you to filter the target links' URLs using an array of {@link PseudoUrl} objects
144
- * and override settings of the enqueued {@link Request} objects.
127
+ * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
145
128
  *
146
129
  * **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
147
130
  * such as changing the Z-index of elements being clicked and their visibility. Therefore,
@@ -159,13 +142,13 @@ export interface EnqueueLinksByClickingElementsOptions {
159
142
  * **Example usage**
160
143
  *
161
144
  * ```javascript
162
- * await utils.puppeteer.enqueueLinksByClickingElements({
145
+ * await puppeteerUtils.enqueueLinksByClickingElements({
163
146
  * page,
164
- * requestQueue,
147
+ * requestManager,
165
148
  * selector: 'a.product-detail',
166
- * pseudoUrls: [
167
- * 'https://www.example.com/handbags/[.*]'
168
- * 'https://www.example.com/purses/[.*]'
149
+ * include: [
150
+ * 'https://www.example.com/handbags/*',
151
+ * 'https://www.example.com/purses/*',
169
152
  * ],
170
153
  * });
171
154
  * ```
@@ -186,22 +169,22 @@ interface ClickElementsAndInterceptNavigationRequestsOptions extends WaitForPage
186
169
  * Clicks all elements of given page matching given selector.
187
170
  * Catches and intercepts all initiated navigation requests and opened pages.
188
171
  * Returns a list of all target URLs.
172
+ *
173
+ * Not part of the public API — exported only so tests can import this module directly.
189
174
  * @ignore
190
175
  */
191
176
  export declare function clickElementsAndInterceptNavigationRequests(options: ClickElementsAndInterceptNavigationRequestsOptions): Promise<Dictionary[]>;
192
- /**
193
- * We're only interested in pages created by the page we're currently clicking in.
194
- * There will generally be a lot of other targets being created in the browser.
195
- */
196
- export declare function isTargetRelevant(page: Page, target: Target): boolean;
197
177
  /**
198
178
  * Click all elements matching the given selector. To be able to do this using
199
179
  * Puppeteer's `.click()` we need to make sure the elements are reachable by mouse,
200
180
  * so we first move them to the top of the page's stacking context and then click.
201
181
  * We do all in series to prevent elements from hiding one another. Therefore,
202
182
  * for large element sets, this will take considerable amount of time.
183
+ *
184
+ * Not part of the public API — exported only so tests can import this module directly.
203
185
  * @ignore
204
186
  */
205
- export declare function clickElements(page: Page, selector: string, clickOptions?: ClickOptions): Promise<void>;
187
+ export declare function clickElements(page: Page, selector: string, clickOptions?: ClickOptions & {
188
+ clickCount?: number;
189
+ }): Promise<void>;
206
190
  export {};
207
- //# sourceMappingURL=click-elements.d.ts.map
@@ -1,10 +1,26 @@
1
1
  import { URL } from 'node:url';
2
- import { constructGlobObjectsFromGlobs, constructRegExpObjectsFromPseudoUrls, constructRegExpObjectsFromRegExps, createRequestOptions, createRequests, } from '@crawlee/browser';
3
- import ow from 'ow';
4
- import log_ from '@apify/log';
2
+ import { applyRequestTransform, constructUrlPatternObjects, createRequestOptions, createSkippedRequestArgs, filterRequestOptionsByPatterns, urlPatternSchema, Request, serviceLocator, } from '@crawlee/browser';
3
+ import { parseArgument, schemas } from '@crawlee/utils/internal';
4
+ import { z } from 'zod';
5
5
  import { addInterceptRequestHandler, removeInterceptRequestHandler } from '../utils/puppeteer_request_interception.js';
6
6
  const STARTING_Z_INDEX = 2147400000;
7
- const log = log_.child({ prefix: 'Puppeteer Click Elements' });
7
+ const getLog = () => serviceLocator.getChildLog('Puppeteer Click Elements');
8
+ const enqueueLinksByClickingElementsOptionsSchema = z.strictObject({
9
+ page: schemas.objectWithKeys(['goto', 'evaluate']),
10
+ requestManager: schemas.objectWithKeys(['fetchNextRequest', 'addRequestsBatched']),
11
+ selector: z.string(),
12
+ userData: schemas.anyObject.optional(),
13
+ clickOptions: schemas.anyObject.optional(),
14
+ include: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
15
+ exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
16
+ transformRequestFunction: schemas.anyFunction.optional(),
17
+ waitForPageIdleSecs: schemas.anyNumber.default(1),
18
+ maxWaitForPageIdleSecs: schemas.anyNumber.default(5),
19
+ label: z.string().optional(),
20
+ forefront: z.boolean().optional(),
21
+ skipNavigation: z.boolean().optional(),
22
+ onSkippedRequest: schemas.anyFunction.optional(),
23
+ });
8
24
  /**
9
25
  * The function finds elements matching a specific CSS selector in a Puppeteer page,
10
26
  * clicks all those elements using a mouse move and a left mouse button click and intercepts
@@ -14,8 +30,7 @@ const log = log_.child({ prefix: 'Puppeteer Click Elements' });
14
30
  * in `href` elements, but rather navigations are triggered in click handlers.
15
31
  * If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
16
32
  *
17
- * Optionally, the function allows you to filter the target links' URLs using an array of {@link PseudoUrl} objects
18
- * and override settings of the enqueued {@link Request} objects.
33
+ * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
19
34
  *
20
35
  * **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
21
36
  * such as changing the Z-index of elements being clicked and their visibility. Therefore,
@@ -33,13 +48,13 @@ const log = log_.child({ prefix: 'Puppeteer Click Elements' });
33
48
  * **Example usage**
34
49
  *
35
50
  * ```javascript
36
- * await utils.puppeteer.enqueueLinksByClickingElements({
51
+ * await puppeteerUtils.enqueueLinksByClickingElements({
37
52
  * page,
38
- * requestQueue,
53
+ * requestManager,
39
54
  * selector: 'a.product-detail',
40
- * pseudoUrls: [
41
- * 'https://www.example.com/handbags/[.*]'
42
- * 'https://www.example.com/purses/[.*]'
55
+ * include: [
56
+ * 'https://www.example.com/handbags/*',
57
+ * 'https://www.example.com/purses/*',
43
58
  * ],
44
59
  * });
45
60
  * ```
@@ -47,36 +62,13 @@ const log = log_.child({ prefix: 'Puppeteer Click Elements' });
47
62
  * @returns Promise that resolves to {@link BatchAddRequestsResult} object.
48
63
  */
49
64
  export async function enqueueLinksByClickingElements(options) {
50
- ow(options, ow.object.exactShape({
51
- page: ow.object.hasKeys('goto', 'evaluate'),
52
- requestQueue: ow.object.hasKeys('fetchNextRequest', 'addRequest'),
53
- selector: ow.string,
54
- userData: ow.optional.object,
55
- clickOptions: ow.optional.object.hasKeys('clickCount', 'delay'),
56
- pseudoUrls: ow.optional.array.ofType(ow.any(ow.string, ow.object.hasKeys('purl'))),
57
- globs: ow.optional.array.ofType(ow.any(ow.string, ow.object.hasKeys('glob'))),
58
- regexps: ow.optional.array.ofType(ow.any(ow.regExp, ow.object.hasKeys('regexp'))),
59
- transformRequestFunction: ow.optional.function,
60
- waitForPageIdleSecs: ow.optional.number,
61
- maxWaitForPageIdleSecs: ow.optional.number,
62
- label: ow.optional.string,
63
- forefront: ow.optional.boolean,
64
- skipNavigation: ow.optional.boolean,
65
- }));
66
- const { page, requestQueue, selector, clickOptions, pseudoUrls, globs, regexps, transformRequestFunction, waitForPageIdleSecs = 1, maxWaitForPageIdleSecs = 5, forefront, } = options;
65
+ const parsedOptions = parseArgument(options, enqueueLinksByClickingElementsOptionsSchema, 'EnqueueLinksByClickingElementsOptions');
66
+ const { page, requestManager, selector, clickOptions, include, exclude, transformRequestFunction, waitForPageIdleSecs, maxWaitForPageIdleSecs, forefront, onSkippedRequest, } = parsedOptions;
67
67
  const waitForPageIdleMillis = waitForPageIdleSecs * 1000;
68
68
  const maxWaitForPageIdleMillis = maxWaitForPageIdleSecs * 1000;
69
- const urlPatternObjects = [];
70
- if (pseudoUrls?.length) {
71
- log.deprecated('`pseudoUrls` option is deprecated, use `globs` or `regexps` instead');
72
- urlPatternObjects.push(...constructRegExpObjectsFromPseudoUrls(pseudoUrls));
73
- }
74
- if (globs?.length) {
75
- urlPatternObjects.push(...constructGlobObjectsFromGlobs(globs));
76
- }
77
- if (regexps?.length) {
78
- urlPatternObjects.push(...constructRegExpObjectsFromRegExps(regexps));
79
- }
69
+ const hasOnSkippedRequest = onSkippedRequest !== undefined;
70
+ const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
71
+ const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
80
72
  const interceptedRequests = await clickElementsAndInterceptNavigationRequests({
81
73
  page,
82
74
  selector,
@@ -84,18 +76,29 @@ export async function enqueueLinksByClickingElements(options) {
84
76
  maxWaitForPageIdleMillis,
85
77
  clickOptions,
86
78
  });
87
- let requestOptions = createRequestOptions(interceptedRequests, options);
79
+ const requestOptions = createRequestOptions(interceptedRequests, parsedOptions);
80
+ const skippedByFilters = [];
81
+ let filteredOptions = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects.length > 0 ? urlPatternObjects : undefined, urlExcludePatternObjects, undefined, hasOnSkippedRequest ? (opts) => skippedByFilters.push(opts) : undefined);
82
+ if (onSkippedRequest && skippedByFilters.length > 0) {
83
+ await Promise.all(skippedByFilters.map(async (opts) => onSkippedRequest(createSkippedRequestArgs(opts, 'filters'))));
84
+ }
88
85
  if (transformRequestFunction) {
89
- requestOptions = requestOptions.map(transformRequestFunction).filter((r) => !!r);
86
+ const skippedByTransform = [];
87
+ filteredOptions = applyRequestTransform(filteredOptions, transformRequestFunction, hasOnSkippedRequest ? (r) => skippedByTransform.push(r) : undefined);
88
+ if (onSkippedRequest && skippedByTransform.length > 0) {
89
+ await Promise.all(skippedByTransform.map(async (r) => onSkippedRequest(createSkippedRequestArgs(r, 'transform'))));
90
+ }
90
91
  }
91
- const requests = createRequests(requestOptions, urlPatternObjects);
92
- const { addedRequests } = await requestQueue.addRequestsBatched(requests, { forefront });
92
+ const requests = filteredOptions.map((opts) => new Request(opts));
93
+ const { addedRequests } = await requestManager.addRequestsBatched(requests, { forefront });
93
94
  return { processedRequests: addedRequests, unprocessedRequests: [] };
94
95
  }
95
96
  /**
96
97
  * Clicks all elements of given page matching given selector.
97
98
  * Catches and intercepts all initiated navigation requests and opened pages.
98
99
  * Returns a list of all target URLs.
100
+ *
101
+ * Not part of the public API — exported only so tests can import this module directly.
99
102
  * @ignore
100
103
  */
101
104
  export async function clickElementsAndInterceptNavigationRequests(options) {
@@ -130,6 +133,7 @@ function createInterceptRequestHandler(page, requests) {
130
133
  url,
131
134
  headers: req.headers(),
132
135
  method: req.method(),
136
+ // oxlint-disable-next-line typescript/no-deprecated -- fetchPostData() is async and adds a CDP roundtrip per request; keep the sync page-cached read
133
137
  payload: req.postData(),
134
138
  }));
135
139
  if (req.redirectChain().length) {
@@ -163,7 +167,9 @@ function createTargetCreatedHandler(page, requests) {
163
167
  await createdPage.close();
164
168
  }
165
169
  catch (err) {
166
- log.debug('enqueueLinksByClickingElements: Could not close spawned page.', { error: err.stack });
170
+ getLog().debug('enqueueLinksByClickingElements: Could not close spawned page.', {
171
+ error: err.stack,
172
+ });
167
173
  }
168
174
  };
169
175
  }
@@ -171,7 +177,8 @@ function createTargetCreatedHandler(page, requests) {
171
177
  * We're only interested in pages created by the page we're currently clicking in.
172
178
  * There will generally be a lot of other targets being created in the browser.
173
179
  */
174
- export function isTargetRelevant(page, target) {
180
+ function isTargetRelevant(page, target) {
181
+ // oxlint-disable-next-line typescript/no-deprecated -- the non-deprecated replacement (opener.page()) is async and would force every call site to await, including EventEmitter callbacks
175
182
  return target.type() === 'page' && page.target() === target.opener();
176
183
  }
177
184
  /**
@@ -215,11 +222,17 @@ async function preventHistoryNavigation(page) {
215
222
  * so we first move them to the top of the page's stacking context and then click.
216
223
  * We do all in series to prevent elements from hiding one another. Therefore,
217
224
  * for large element sets, this will take considerable amount of time.
225
+ *
226
+ * Not part of the public API — exported only so tests can import this module directly.
218
227
  * @ignore
219
228
  */
220
229
  export async function clickElements(page, selector, clickOptions) {
230
+ // Puppeteer 25 removed the deprecated `clickCount` option in favor of `count`, older versions ignore `count`, so we pass both.
231
+ if (clickOptions?.clickCount !== undefined && clickOptions.count === undefined) {
232
+ clickOptions = { ...clickOptions, count: clickOptions.clickCount };
233
+ }
221
234
  const elementHandles = await page.$$(selector);
222
- log.debug(`enqueueLinksByClickingElements: There are ${elementHandles.length} elements to click.`);
235
+ getLog().debug(`enqueueLinksByClickingElements: There are ${elementHandles.length} elements to click.`);
223
236
  let clickedElementsCount = 0;
224
237
  let zIndex = STARTING_Z_INDEX;
225
238
  let shouldLogWarning = true;
@@ -232,15 +245,15 @@ export async function clickElements(page, selector, clickOptions) {
232
245
  catch (err) {
233
246
  const e = err;
234
247
  if (shouldLogWarning && e.stack.includes('is detached from document')) {
235
- log.warning(`An element with selector ${selector} that you're trying to click has been removed from the page. ` +
248
+ getLog().warning(`An element with selector ${selector} that you're trying to click has been removed from the page. ` +
236
249
  'This was probably caused by an earlier click which triggered some JavaScript on the page that caused it to change. ' +
237
250
  'If you\'re trying to enqueue pagination links, we suggest using the "next" button, if available and going one by one.');
238
251
  shouldLogWarning = false;
239
252
  }
240
- log.debug('enqueueLinksByClickingElements: Click failed.', { stack: e.stack });
253
+ getLog().debug('enqueueLinksByClickingElements: Click failed.', { stack: e.stack });
241
254
  }
242
255
  }
243
- log.debug(`enqueueLinksByClickingElements: Successfully clicked ${clickedElementsCount} elements out of ${elementHandles.length}`);
256
+ getLog().debug(`enqueueLinksByClickingElements: Successfully clicked ${clickedElementsCount} elements out of ${elementHandles.length}`);
244
257
  }
245
258
  /* istanbul ignore next */
246
259
  /**
@@ -277,7 +290,6 @@ function updateElementCssToEnableMouseClick(el, zIndex) {
277
290
  async function waitForPageIdle({ page, waitForPageIdleMillis, maxWaitForPageIdleMillis, }) {
278
291
  return new Promise((resolve) => {
279
292
  let timeout;
280
- let maxTimeout;
281
293
  const context = page.browserContext();
282
294
  function newTabTracker(target) {
283
295
  if (isTargetRelevant(page, target))
@@ -291,7 +303,7 @@ async function waitForPageIdle({ page, waitForPageIdleMillis, maxWaitForPageIdle
291
303
  }, waitForPageIdleMillis);
292
304
  }
293
305
  function maxTimeoutHandler() {
294
- log.debug(`enqueueLinksByClickingElements: Page still showed activity after ${maxWaitForPageIdleMillis}ms. ` +
306
+ getLog().debug(`enqueueLinksByClickingElements: Page still showed activity after ${maxWaitForPageIdleMillis}ms. ` +
295
307
  'This is probably due to the website itself dispatching requests, but some links may also have been missed.');
296
308
  finish();
297
309
  }
@@ -301,7 +313,7 @@ async function waitForPageIdle({ page, waitForPageIdleMillis, maxWaitForPageIdle
301
313
  context.off('targetcreated', newTabTracker);
302
314
  resolve();
303
315
  }
304
- maxTimeout = setTimeout(maxTimeoutHandler, maxWaitForPageIdleMillis);
316
+ const maxTimeout = setTimeout(maxTimeoutHandler, maxWaitForPageIdleMillis);
305
317
  activityHandler(); // We call this once manually in case there would be no requests at all.
306
318
  page.on('request', activityHandler);
307
319
  page.on('framenavigated', activityHandler);
@@ -325,8 +337,7 @@ async function restoreHistoryNavigationAndSaveCapturedUrls(page, requests) {
325
337
  requests.add(JSON.stringify({ url }));
326
338
  }
327
339
  catch (err) {
328
- log.debug('enqueueLinksByClickingElements: Failed to ', { error: err.stack });
340
+ getLog().debug('enqueueLinksByClickingElements: Failed to ', { error: err.stack });
329
341
  }
330
342
  });
331
343
  }
332
- //# sourceMappingURL=click-elements.js.map
@@ -0,0 +1,55 @@
1
+ import type { Configuration } from '@crawlee/browser';
2
+ import type { BrowserPool, BrowserPoolHooks, BrowserPoolOptions, PuppeteerPlugin, RemoteBrowserPool, RemoteBrowserPoolOptions } from '@crawlee/browser-pool';
3
+ // @ts-ignore optional peer dependency or compatibility with es2022
4
+ import type { Page } from 'puppeteer';
5
+ import type { PuppeteerLaunchContext } from './puppeteer-launcher.js';
6
+ /** A {@link BrowserPool} of Puppeteer browsers, as built by {@link puppeteerBrowserPool}. */
7
+ export type PuppeteerBrowserPool = BrowserPool<{
8
+ browserPlugins: [PuppeteerPlugin];
9
+ }, [PuppeteerPlugin]>;
10
+ export interface PuppeteerBrowserPoolOptions extends Omit<BrowserPoolOptions, 'browserPlugins'>, BrowserPoolHooks<ReturnType<PuppeteerPlugin['createController']>, ReturnType<PuppeteerPlugin['createLaunchContext']>, Page> {
11
+ /** How to launch the browser: proxy, user data dir, whether to use full Chrome, ... */
12
+ launchContext?: PuppeteerLaunchContext;
13
+ /**
14
+ * Whether to run the browser in headless mode. Shorthand for `launchContext.launchOptions.headless`.
15
+ * Defaults to `true`, and can also be set via {@link Configuration}.
16
+ */
17
+ headless?: boolean | 'new' | 'old';
18
+ /** Configuration to read the browser defaults from. Defaults to the global configuration. */
19
+ configuration?: Configuration;
20
+ }
21
+ export interface RemotePuppeteerBrowserPoolOptions extends Pick<PuppeteerBrowserPoolOptions, 'launchContext' | 'headless' | 'configuration'>, Omit<RemoteBrowserPoolOptions, 'browserPlugins'> {
22
+ }
23
+ /**
24
+ * Builds a {@link BrowserPool} of Puppeteer browsers to pass to a {@link PuppeteerCrawler} as its
25
+ * {@link BrowserCrawlerOptions.browserPool|`browserPool`}.
26
+ *
27
+ * It accepts every {@link BrowserPoolOptions|`BrowserPool` option} plus the crawler's own `launchContext` and
28
+ * `headless`, and derives the browser plugin from them - so a pool built here always matches the crawler it is
29
+ * given to, and configuring one never means assembling a {@link PuppeteerPlugin} by hand.
30
+ *
31
+ * **Example usage:**
32
+ *
33
+ * ```javascript
34
+ * const crawler = new PuppeteerCrawler({
35
+ * browserPool: puppeteerBrowserPool({ maxOpenPagesPerBrowser: 1 }),
36
+ * requestHandler: async ({ page }) => { ... },
37
+ * });
38
+ * ```
39
+ *
40
+ * The returned pool is *not* torn down by the crawler, which is what makes it shareable between crawlers.
41
+ *
42
+ * @category Browser management
43
+ */
44
+ export declare function puppeteerBrowserPool(options?: PuppeteerBrowserPoolOptions): PuppeteerBrowserPool;
45
+ /**
46
+ * The {@link RemoteBrowserPool} counterpart of {@link puppeteerBrowserPool}: connects to a remote browser
47
+ * service (Browserbase, Browserless, Steel, ...) with a Puppeteer plugin derived from `launchContext`.
48
+ *
49
+ * A {@link PuppeteerCrawler} accepts the same connection details directly via
50
+ * {@link BrowserCrawlerOptions.remoteBrowser|`remoteBrowser`}; reach for this factory when you also need to
51
+ * tune the wrapping pool, or to share one remote pool between crawlers.
52
+ *
53
+ * @category Browser management
54
+ */
55
+ export declare function remotePuppeteerBrowserPool(options: RemotePuppeteerBrowserPoolOptions): RemoteBrowserPool<Page>;
@@ -0,0 +1,48 @@
1
+ import { PuppeteerLauncher } from './puppeteer-launcher.js';
2
+ /**
3
+ * Builds a {@link BrowserPool} of Puppeteer browsers to pass to a {@link PuppeteerCrawler} as its
4
+ * {@link BrowserCrawlerOptions.browserPool|`browserPool`}.
5
+ *
6
+ * It accepts every {@link BrowserPoolOptions|`BrowserPool` option} plus the crawler's own `launchContext` and
7
+ * `headless`, and derives the browser plugin from them - so a pool built here always matches the crawler it is
8
+ * given to, and configuring one never means assembling a {@link PuppeteerPlugin} by hand.
9
+ *
10
+ * **Example usage:**
11
+ *
12
+ * ```javascript
13
+ * const crawler = new PuppeteerCrawler({
14
+ * browserPool: puppeteerBrowserPool({ maxOpenPagesPerBrowser: 1 }),
15
+ * requestHandler: async ({ page }) => { ... },
16
+ * });
17
+ * ```
18
+ *
19
+ * The returned pool is *not* torn down by the crawler, which is what makes it shareable between crawlers.
20
+ *
21
+ * @category Browser management
22
+ */
23
+ export function puppeteerBrowserPool(options = {}) {
24
+ const { launchContext, headless, configuration, ...poolOptions } = options;
25
+ return puppeteerLauncher(launchContext, headless, configuration).createBrowserPool(poolOptions);
26
+ }
27
+ /**
28
+ * The {@link RemoteBrowserPool} counterpart of {@link puppeteerBrowserPool}: connects to a remote browser
29
+ * service (Browserbase, Browserless, Steel, ...) with a Puppeteer plugin derived from `launchContext`.
30
+ *
31
+ * A {@link PuppeteerCrawler} accepts the same connection details directly via
32
+ * {@link BrowserCrawlerOptions.remoteBrowser|`remoteBrowser`}; reach for this factory when you also need to
33
+ * tune the wrapping pool, or to share one remote pool between crawlers.
34
+ *
35
+ * @category Browser management
36
+ */
37
+ export function remotePuppeteerBrowserPool(options) {
38
+ const { launchContext, headless, configuration, ...remoteOptions } = options;
39
+ return puppeteerLauncher(launchContext, headless, configuration).createRemoteBrowserPool(remoteOptions);
40
+ }
41
+ function puppeteerLauncher(launchContext = {}, headless, configuration) {
42
+ return new PuppeteerLauncher(headless == null
43
+ ? launchContext
44
+ : {
45
+ ...launchContext,
46
+ launchOptions: { ...launchContext.launchOptions, headless: headless },
47
+ }, configuration);
48
+ }