@crawlee/playwright 4.0.0-beta.14 → 4.0.0-beta.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +14 -14
  2. package/index.d.ts +2 -2
  3. package/index.js +1 -1
  4. package/internals/adaptive-playwright-crawler.d.ts +116 -63
  5. package/internals/adaptive-playwright-crawler.js +312 -264
  6. package/internals/enqueue-links/click-elements.d.ts +36 -64
  7. package/internals/enqueue-links/click-elements.js +64 -67
  8. package/internals/playwright-browser-pool.d.ts +71 -0
  9. package/internals/playwright-browser-pool.js +61 -0
  10. package/internals/playwright-crawler.d.ts +178 -125
  11. package/internals/playwright-crawler.js +68 -62
  12. package/internals/playwright-launcher.d.ts +32 -18
  13. package/internals/playwright-launcher.js +23 -17
  14. package/internals/utils/playwright-utils.d.ts +54 -26
  15. package/internals/utils/playwright-utils.js +112 -93
  16. package/internals/utils/rendering-type-prediction.d.ts +25 -10
  17. package/internals/utils/rendering-type-prediction.js +77 -22
  18. package/package.json +15 -15
  19. package/index.d.ts.map +0 -1
  20. package/index.js.map +0 -1
  21. package/internals/adaptive-playwright-crawler.d.ts.map +0 -1
  22. package/internals/adaptive-playwright-crawler.js.map +0 -1
  23. package/internals/enqueue-links/click-elements.d.ts.map +0 -1
  24. package/internals/enqueue-links/click-elements.js.map +0 -1
  25. package/internals/playwright-crawler.d.ts.map +0 -1
  26. package/internals/playwright-crawler.js.map +0 -1
  27. package/internals/playwright-launcher.d.ts.map +0 -1
  28. package/internals/playwright-launcher.js.map +0 -1
  29. package/internals/utils/playwright-utils.d.ts.map +0 -1
  30. package/internals/utils/playwright-utils.js.map +0 -1
  31. package/internals/utils/rendering-type-prediction.d.ts.map +0 -1
  32. package/internals/utils/rendering-type-prediction.js.map +0 -1
@@ -1,4 +1,4 @@
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
4
  import type { Page } from 'playwright';
@@ -9,9 +9,9 @@ export interface EnqueueLinksByClickingElementsOptions {
9
9
  */
10
10
  page: Page;
11
11
  /**
12
- * A request queue to which the URLs will be enqueued.
12
+ * * A request manager to which the URLs will be enqueued.
13
13
  */
14
- requestQueue: RequestProvider;
14
+ requestManager: IRequestManager;
15
15
  /**
16
16
  * A CSS selector matching elements to be clicked on. Unlike in {@link enqueueLinks}, there is no default
17
17
  * value. This is to prevent suboptimal use of this function by using it too broadly.
@@ -26,82 +26,50 @@ export interface EnqueueLinksByClickingElementsOptions {
26
26
  */
27
27
  clickOptions?: ClickOptions;
28
28
  /**
29
- * An array of glob pattern strings or plain objects
30
- * containing glob pattern strings matching the URLs to be enqueued.
29
+ * An array of URL patterns that URLs must match to be enqueued.
31
30
  *
32
- * The plain objects must include at least the `glob` property, which holds the glob pattern string.
33
- * All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
34
- *
35
- * The matching is always case-insensitive.
36
- * If you need case-sensitive matching, use `regexps` property directly.
37
- *
38
- * If `globs` is an empty array or `undefined`, then the function
39
- * enqueues all the intercepted navigation requests produced by the page
40
- * after clicking on elements matching the provided CSS selector.
41
- */
42
- globs?: GlobInput[];
43
- /**
44
- * An array of glob pattern strings, regexp patterns or plain objects
45
- * containing patterns matching URLs that will **never** be enqueued.
46
- *
47
- * The plain objects must include either the `glob` property or the `regexp` property.
31
+ * Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
48
32
  *
49
33
  * Glob matching is always case-insensitive.
50
- * If you need case-sensitive matching, provide a regexp.
51
- */
52
- exclude?: readonly (GlobInput | RegExpInput)[];
53
- /**
54
- * An array of regular expressions or plain objects
55
- * containing regular expressions matching the URLs to be enqueued.
34
+ * If you need case-sensitive matching, use a `RegExp`.
56
35
  *
57
- * The plain objects must include at least the `regexp` property, which holds the regular expression.
58
- * All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
59
- *
60
- * If `regexps` is an empty array or `undefined`, then the function
36
+ * If `include` is an empty array or `undefined`, then the function
61
37
  * enqueues all the intercepted navigation requests produced by the page
62
38
  * after clicking on elements matching the provided CSS selector.
63
39
  */
64
- regexps?: RegExpInput[];
40
+ include?: UrlPatternInput[];
65
41
  /**
66
- * *NOTE:* In future versions of SDK the options will be removed.
67
- * Please use `globs` or `regexps` instead.
68
- *
69
- * An array of {@link PseudoUrl} strings or plain objects
70
- * containing {@link PseudoUrl} strings matching the URLs to be enqueued.
42
+ * An array of URL patterns. Matching URLs will **not** be enqueued.
71
43
  *
72
- * The plain objects must include at least the `purl` property, which holds the pseudo-URL pattern string.
73
- * All remaining keys will be used as request options for the corresponding enqueued {@link Request} objects.
44
+ * Accepts glob pattern strings, `{ glob: string }` objects, `RegExp` instances, or `{ regexp: RegExp }` objects.
74
45
  *
75
- * With a pseudo-URL string, the matching is always case-insensitive.
76
- * If you need case-sensitive matching, use `regexps` property directly.
77
- *
78
- * If `pseudoUrls` is an empty array or `undefined`, then the function
79
- * enqueues all the intercepted navigation requests produced by the page
80
- * after clicking on elements matching the provided CSS selector.
81
- *
82
- * @deprecated prefer using `globs` or `regexps` instead
46
+ * Glob matching is always case-insensitive.
47
+ * If you need case-sensitive matching, use a `RegExp`.
83
48
  */
84
- pseudoUrls?: PseudoUrlInput[];
49
+ exclude?: readonly UrlPatternInput[];
85
50
  /**
86
- * Just before a new {@link Request} is constructed and enqueued to the {@link RequestQueue}, this function can be used
87
- * to remove it or modify its contents such as `userData`, `payload` or, most importantly `uniqueKey`. This is useful
88
- * when you need to enqueue multiple `Requests` to the queue that share the same URL, but differ in methods or payloads,
89
- * or to dynamically update or create `userData`.
90
- *
91
- * For example: by adding `useExtendedUniqueKey: true` to the `request` object, `uniqueKey` will be computed from
92
- * a combination of `url`, `method` and `payload` which enables crawling of websites that navigate using form submits
93
- * (POST requests).
51
+ * After request options are filtered by `include`/`exclude` patterns,
52
+ * this function can be used to remove them or modify their contents such as `userData`, `payload` or, most importantly
53
+ * `uniqueKey`. This is useful when you need to enqueue multiple `Requests` to the queue that share the same URL,
54
+ * but differ in methods or payloads, or to dynamically update or create `userData`.
94
55
  *
95
56
  * **Example:**
96
57
  * ```javascript
97
58
  * {
98
59
  * transformRequestFunction: (request) => {
99
60
  * request.userData.foo = 'bar';
100
- * request.useExtendedUniqueKey = true;
101
61
  * return request;
102
62
  * }
103
63
  * }
104
64
  * ```
65
+ *
66
+ * Note that `transformRequestFunction` has the highest priority and can overwrite
67
+ * the global `label` option.
68
+ *
69
+ * The function receives a {@link RequestOptions} object and can return either:
70
+ * - The modified {@link RequestOptions} object
71
+ * - `'unchanged'` to keep the original options as-is
72
+ * - A falsy value or `'skip'` to exclude the request from the queue
105
73
  */
106
74
  transformRequestFunction?: RequestTransform;
107
75
  /**
@@ -141,6 +109,12 @@ export interface EnqueueLinksByClickingElementsOptions {
141
109
  * @default false
142
110
  */
143
111
  skipNavigation?: boolean;
112
+ /**
113
+ * When a request is skipped for some reason, you can use this callback to act on it.
114
+ * This is fired for requests skipped because they don't match enqueueLinks filters
115
+ * or because they were removed by `transformRequestFunction`.
116
+ */
117
+ onSkippedRequest?: SkippedRequestCallback;
144
118
  }
145
119
  /**
146
120
  * The function finds elements matching a specific CSS selector in a Playwright page,
@@ -151,8 +125,7 @@ export interface EnqueueLinksByClickingElementsOptions {
151
125
  * in `href` elements, but rather navigations are triggered in click handlers.
152
126
  * If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
153
127
  *
154
- * Optionally, the function allows you to filter the target links' URLs using an array of {@link PseudoUrl} objects
155
- * and override settings of the enqueued {@link Request} objects.
128
+ * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
156
129
  *
157
130
  * **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
158
131
  * such as changing the Z-index of elements being clicked and their visibility. Therefore,
@@ -172,11 +145,11 @@ export interface EnqueueLinksByClickingElementsOptions {
172
145
  * ```javascript
173
146
  * await playwrightUtils.enqueueLinksByClickingElements({
174
147
  * page,
175
- * requestQueue,
148
+ * requestManager,
176
149
  * selector: 'a.product-detail',
177
- * pseudoUrls: [
178
- * 'https://www.example.com/handbags/[.*]'
179
- * 'https://www.example.com/purses/[.*]'
150
+ * include: [
151
+ * 'https://www.example.com/handbags/*',
152
+ * 'https://www.example.com/purses/*',
180
153
  * ],
181
154
  * });
182
155
  * ```
@@ -210,4 +183,3 @@ export declare function clickElementsAndInterceptNavigationRequests(options: Cli
210
183
  */
211
184
  export declare function clickElements(page: Page, selector: string, clickOptions?: ClickOptions): Promise<void>;
212
185
  export {};
213
- //# sourceMappingURL=click-elements.d.ts.map
@@ -1,9 +1,25 @@
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, filterRequestOptionsByPatterns, urlPatternSchema, Request as CrawleeRequest, serviceLocator, } from '@crawlee/browser';
3
+ import { parseArgument, schemas } from '@crawlee/utils/internal';
4
+ import { z } from 'zod';
5
5
  const STARTING_Z_INDEX = 2147400000;
6
- const log = log_.child({ prefix: 'Playwright Click Elements' });
6
+ const getLog = () => serviceLocator.getChildLog('Playwright Click Elements');
7
+ const enqueueLinksByClickingElementsOptionsSchema = z.strictObject({
8
+ page: schemas.objectWithKeys(['goto', 'evaluate']),
9
+ requestManager: schemas.objectWithKeys(['fetchNextRequest', 'addRequestsBatched']),
10
+ selector: z.string(),
11
+ userData: schemas.anyObject.optional(),
12
+ clickOptions: schemas.anyObject.optional(),
13
+ include: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
14
+ exclude: schemas.arrayOf(urlPatternSchema, 'URL patterns').optional(),
15
+ transformRequestFunction: schemas.anyFunction.optional(),
16
+ waitForPageIdleSecs: schemas.anyNumber.default(1),
17
+ maxWaitForPageIdleSecs: schemas.anyNumber.default(5),
18
+ label: z.string().optional(),
19
+ forefront: z.boolean().optional(),
20
+ skipNavigation: z.boolean().optional(),
21
+ onSkippedRequest: schemas.anyFunction.optional(),
22
+ });
7
23
  /**
8
24
  * The function finds elements matching a specific CSS selector in a Playwright page,
9
25
  * clicks all those elements using a mouse move and a left mouse button click and intercepts
@@ -13,8 +29,7 @@ const log = log_.child({ prefix: 'Playwright Click Elements' });
13
29
  * in `href` elements, but rather navigations are triggered in click handlers.
14
30
  * If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
15
31
  *
16
- * Optionally, the function allows you to filter the target links' URLs using an array of {@link PseudoUrl} objects
17
- * and override settings of the enqueued {@link Request} objects.
32
+ * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
18
33
  *
19
34
  * **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
20
35
  * such as changing the Z-index of elements being clicked and their visibility. Therefore,
@@ -34,11 +49,11 @@ const log = log_.child({ prefix: 'Playwright Click Elements' });
34
49
  * ```javascript
35
50
  * await playwrightUtils.enqueueLinksByClickingElements({
36
51
  * page,
37
- * requestQueue,
52
+ * requestManager,
38
53
  * selector: 'a.product-detail',
39
- * pseudoUrls: [
40
- * 'https://www.example.com/handbags/[.*]'
41
- * 'https://www.example.com/purses/[.*]'
54
+ * include: [
55
+ * 'https://www.example.com/handbags/*',
56
+ * 'https://www.example.com/purses/*',
42
57
  * ],
43
58
  * });
44
59
  * ```
@@ -46,48 +61,12 @@ const log = log_.child({ prefix: 'Playwright Click Elements' });
46
61
  * @returns Promise that resolves to {@link BatchAddRequestsResult} object.
47
62
  */
48
63
  export async function enqueueLinksByClickingElements(options) {
49
- ow(options, ow.object.exactShape({
50
- page: ow.object.hasKeys('goto', 'evaluate'),
51
- requestQueue: ow.object.hasKeys('fetchNextRequest', 'addRequest'),
52
- selector: ow.string,
53
- userData: ow.optional.object,
54
- clickOptions: ow.optional.object.hasKeys('clickCount', 'delay'),
55
- pseudoUrls: ow.optional.array.ofType(ow.any(ow.string, ow.object.hasKeys('purl'))),
56
- globs: ow.optional.array.ofType(ow.any(ow.string, ow.object.hasKeys('glob'))),
57
- regexps: ow.optional.array.ofType(ow.any(ow.regExp, ow.object.hasKeys('regexp'))),
58
- exclude: ow.optional.array.ofType(ow.any(ow.string, ow.regExp, ow.object.hasKeys('glob'), 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, exclude, } = options;
64
+ const parsedOptions = parseArgument(options, enqueueLinksByClickingElementsOptionsSchema, 'EnqueueLinksByClickingElementsOptions');
65
+ const { page, requestManager, selector, clickOptions, include, exclude, transformRequestFunction, waitForPageIdleSecs, maxWaitForPageIdleSecs, forefront, onSkippedRequest, } = parsedOptions;
67
66
  const waitForPageIdleMillis = waitForPageIdleSecs * 1000;
68
67
  const maxWaitForPageIdleMillis = maxWaitForPageIdleSecs * 1000;
69
- const urlExcludePatternObjects = [];
70
- const urlPatternObjects = [];
71
- if (exclude?.length) {
72
- for (const excl of exclude) {
73
- if (typeof excl === 'string' || 'glob' in excl) {
74
- urlExcludePatternObjects.push(...constructGlobObjectsFromGlobs([excl]));
75
- }
76
- else if (excl instanceof RegExp || 'regexp' in excl) {
77
- urlExcludePatternObjects.push(...constructRegExpObjectsFromRegExps([excl]));
78
- }
79
- }
80
- }
81
- if (pseudoUrls?.length) {
82
- log.deprecated('`pseudoUrls` option is deprecated, use `globs` or `regexps` instead');
83
- urlPatternObjects.push(...constructRegExpObjectsFromPseudoUrls(pseudoUrls));
84
- }
85
- if (globs?.length) {
86
- urlPatternObjects.push(...constructGlobObjectsFromGlobs(globs));
87
- }
88
- if (regexps?.length) {
89
- urlPatternObjects.push(...constructRegExpObjectsFromRegExps(regexps));
90
- }
68
+ const urlExcludePatternObjects = exclude?.length ? constructUrlPatternObjects(exclude) : [];
69
+ const urlPatternObjects = include?.length ? constructUrlPatternObjects(include) : [];
91
70
  const interceptedRequests = await clickElementsAndInterceptNavigationRequests({
92
71
  page,
93
72
  selector,
@@ -95,12 +74,21 @@ export async function enqueueLinksByClickingElements(options) {
95
74
  maxWaitForPageIdleMillis,
96
75
  clickOptions,
97
76
  });
98
- let requestOptions = createRequestOptions(interceptedRequests, options);
77
+ const requestOptions = createRequestOptions(interceptedRequests, parsedOptions);
78
+ const skippedByFilters = [];
79
+ let filteredOptions = filterRequestOptionsByPatterns(requestOptions, urlPatternObjects.length > 0 ? urlPatternObjects : undefined, urlExcludePatternObjects, undefined, (url) => skippedByFilters.push(url));
80
+ if (onSkippedRequest && skippedByFilters.length > 0) {
81
+ await Promise.all(skippedByFilters.map(async (url) => onSkippedRequest({ url, reason: 'filters' })));
82
+ }
99
83
  if (transformRequestFunction) {
100
- requestOptions = requestOptions.map(transformRequestFunction).filter((r) => !!r);
84
+ const skippedByTransform = [];
85
+ filteredOptions = applyRequestTransform(filteredOptions, transformRequestFunction, (r) => skippedByTransform.push(r));
86
+ if (onSkippedRequest && skippedByTransform.length > 0) {
87
+ await Promise.all(skippedByTransform.map(async (r) => onSkippedRequest({ url: r.url, reason: 'transform' })));
88
+ }
101
89
  }
102
- const requests = createRequests(requestOptions, urlPatternObjects, urlExcludePatternObjects);
103
- const { addedRequests } = await requestQueue.addRequestsBatched(requests, { forefront });
90
+ const requests = filteredOptions.map((opts) => new CrawleeRequest(opts));
91
+ const { addedRequests } = await requestManager.addRequestsBatched(requests, { forefront });
104
92
  return { processedRequests: addedRequests, unprocessedRequests: [] };
105
93
  }
106
94
  /**
@@ -126,7 +114,7 @@ export async function clickElementsAndInterceptNavigationRequests(options) {
126
114
  await restoreHistoryNavigationAndSaveCapturedUrls(page, uniqueRequests);
127
115
  // browser.off(BrowserEmittedEvents.TargetCreated, onTargetCreated);
128
116
  page.off('framenavigated', onFrameNavigated);
129
- await context.unroute('*', onInterceptedRequest);
117
+ await context.unroute('**', onInterceptedRequest);
130
118
  const serializedRequests = Array.from(uniqueRequests);
131
119
  return serializedRequests.map((r) => JSON.parse(r));
132
120
  }
@@ -162,7 +150,9 @@ function createTargetCreatedHandler(requests) {
162
150
  await popup.close();
163
151
  }
164
152
  catch (err) {
165
- log.debug('enqueueLinksByClickingElements: Could not close spawned page.', { error: err.stack });
153
+ getLog().debug('enqueueLinksByClickingElements: Could not close spawned page.', {
154
+ error: err.stack,
155
+ });
166
156
  }
167
157
  };
168
158
  }
@@ -170,7 +160,16 @@ function createTargetCreatedHandler(requests) {
170
160
  * @ignore
171
161
  */
172
162
  function isTopFrameNavigationRequest(page, req) {
173
- return req.isNavigationRequest() && req.frame() === page.mainFrame();
163
+ try {
164
+ return req.isNavigationRequest() && req.frame() === page.mainFrame();
165
+ }
166
+ catch {
167
+ // `req.frame()` throws when the owning frame is unavailable - e.g. the request was
168
+ // issued by a service worker, or before/after its frame existed (see #3216). Such a
169
+ // request is not a top-frame navigation, so swallow the throw and let it pass through
170
+ // instead of crashing the route handler (which would leave the route unhandled).
171
+ return false;
172
+ }
174
173
  }
175
174
  /**
176
175
  * @ignore
@@ -235,7 +234,7 @@ function updateElementCssToEnableMouseClick(el, zIndex) {
235
234
  */
236
235
  export async function clickElements(page, selector, clickOptions) {
237
236
  const elementHandles = await page.$$(selector);
238
- log.debug(`enqueueLinksByClickingElements: There are ${elementHandles.length} elements to click.`);
237
+ getLog().debug(`enqueueLinksByClickingElements: There are ${elementHandles.length} elements to click.`);
239
238
  let clickedElementsCount = 0;
240
239
  let zIndex = STARTING_Z_INDEX;
241
240
  let shouldLogWarning = true;
@@ -248,15 +247,15 @@ export async function clickElements(page, selector, clickOptions) {
248
247
  catch (err) {
249
248
  const e = err;
250
249
  if (shouldLogWarning && e.stack.includes('is detached from document')) {
251
- log.warning(`An element with selector ${selector} that you're trying to click has been removed from the page. ` +
250
+ getLog().warning(`An element with selector ${selector} that you're trying to click has been removed from the page. ` +
252
251
  'This was probably caused by an earlier click which triggered some JavaScript on the page that caused it to change. ' +
253
252
  'If you\'re trying to enqueue pagination links, we suggest using the "next" button, if available and going one by one.');
254
253
  shouldLogWarning = false;
255
254
  }
256
- log.debug('enqueueLinksByClickingElements: Click failed.', { stack: e.stack });
255
+ getLog().debug('enqueueLinksByClickingElements: Click failed.', { stack: e.stack });
257
256
  }
258
257
  }
259
- log.debug(`enqueueLinksByClickingElements: Successfully clicked ${clickedElementsCount} elements out of ${elementHandles.length}`);
258
+ getLog().debug(`enqueueLinksByClickingElements: Successfully clicked ${clickedElementsCount} elements out of ${elementHandles.length}`);
260
259
  }
261
260
  /**
262
261
  * This function tracks whether any requests, frame navigations or targets were emitted
@@ -275,8 +274,6 @@ export async function clickElements(page, selector, clickOptions) {
275
274
  async function waitForPageIdle({ page, waitForPageIdleMillis, maxWaitForPageIdleMillis, }) {
276
275
  return new Promise((resolve) => {
277
276
  let timeout;
278
- let maxTimeout;
279
- page.on('popup', activityHandler);
280
277
  function activityHandler() {
281
278
  clearTimeout(timeout);
282
279
  timeout = setTimeout(() => {
@@ -285,7 +282,7 @@ async function waitForPageIdle({ page, waitForPageIdleMillis, maxWaitForPageIdle
285
282
  }, waitForPageIdleMillis);
286
283
  }
287
284
  function maxTimeoutHandler() {
288
- log.debug(`enqueueLinksByClickingElements: Page still showed activity after ${maxWaitForPageIdleMillis}ms. ` +
285
+ getLog().debug(`enqueueLinksByClickingElements: Page still showed activity after ${maxWaitForPageIdleMillis}ms. ` +
289
286
  'This is probably due to the website itself dispatching requests, but some links may also have been missed.');
290
287
  finish();
291
288
  }
@@ -293,7 +290,8 @@ async function waitForPageIdle({ page, waitForPageIdleMillis, maxWaitForPageIdle
293
290
  page.off('request', activityHandler).off('framenavigated', activityHandler).off('popup', activityHandler);
294
291
  resolve();
295
292
  }
296
- maxTimeout = setTimeout(maxTimeoutHandler, maxWaitForPageIdleMillis);
293
+ const maxTimeout = setTimeout(maxTimeoutHandler, maxWaitForPageIdleMillis);
294
+ page.on('popup', activityHandler);
297
295
  activityHandler(); // We call this once manually in case there would be no requests at all.
298
296
  page.on('request', activityHandler);
299
297
  page.on('framenavigated', activityHandler);
@@ -316,8 +314,7 @@ async function restoreHistoryNavigationAndSaveCapturedUrls(page, requests) {
316
314
  requests.add(JSON.stringify({ url }));
317
315
  }
318
316
  catch (err) {
319
- log.debug('enqueueLinksByClickingElements: Failed to ', { error: err.stack });
317
+ getLog().debug('enqueueLinksByClickingElements: Failed to ', { error: err.stack });
320
318
  }
321
319
  });
322
320
  }
323
- //# sourceMappingURL=click-elements.js.map
@@ -0,0 +1,71 @@
1
+ import type { Configuration } from '@crawlee/browser';
2
+ import type { BrowserPool, BrowserPoolHooks, BrowserPoolOptions, PlaywrightPlugin, RemoteBrowserPool, RemoteBrowserPoolOptions } from '@crawlee/browser-pool';
3
+ // @ts-ignore optional peer dependency or compatibility with es2022
4
+ import type { Page } from 'playwright';
5
+ import type { PlaywrightLaunchContext } from './playwright-launcher.js';
6
+ /** A {@link BrowserPool} of Playwright browsers, as built by {@link playwrightBrowserPool}. */
7
+ export type PlaywrightBrowserPool = BrowserPool<{
8
+ browserPlugins: [PlaywrightPlugin];
9
+ }, [PlaywrightPlugin]>;
10
+ export interface PlaywrightBrowserPoolOptions extends Omit<BrowserPoolOptions, 'browserPlugins'>, BrowserPoolHooks<ReturnType<PlaywrightPlugin['createController']>, ReturnType<PlaywrightPlugin['createLaunchContext']>, Page> {
11
+ /** How to launch the browser: which Playwright browser type, proxy, user data dir, ... */
12
+ launchContext?: PlaywrightLaunchContext;
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;
18
+ /** Configuration to read the browser defaults from. Defaults to the global configuration. */
19
+ configuration?: Configuration;
20
+ }
21
+ export interface RemotePlaywrightBrowserPoolOptions extends Pick<PlaywrightBrowserPoolOptions, 'launchContext' | 'headless' | 'configuration'>, Omit<RemoteBrowserPoolOptions, 'browserPlugins'> {
22
+ }
23
+ /**
24
+ * Builds a {@link BrowserPool} of Playwright browsers to pass to a {@link PlaywrightCrawler} 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 PlaywrightPlugin} by hand.
30
+ *
31
+ * **Example usage:**
32
+ *
33
+ * ```javascript
34
+ * const crawler = new PlaywrightCrawler({
35
+ * browserPool: playwrightBrowserPool({
36
+ * maxOpenPagesPerBrowser: 1,
37
+ * launchContext: { launcher: firefox },
38
+ * }),
39
+ * requestHandler: async ({ page }) => { ... },
40
+ * });
41
+ * ```
42
+ *
43
+ * The returned pool is *not* torn down by the crawler, which is what makes it shareable between crawlers.
44
+ *
45
+ * @category Browser management
46
+ */
47
+ export declare function playwrightBrowserPool(options?: PlaywrightBrowserPoolOptions): PlaywrightBrowserPool;
48
+ /**
49
+ * The {@link RemoteBrowserPool} counterpart of {@link playwrightBrowserPool}: connects to a remote browser
50
+ * service (Browserbase, Browserless, Steel, ...) with a Playwright plugin derived from `launchContext`.
51
+ *
52
+ * A {@link PlaywrightCrawler} accepts the same connection details directly via
53
+ * {@link BrowserCrawlerOptions.remoteBrowser|`remoteBrowser`}; reach for this factory when you also need to
54
+ * tune the wrapping pool, or to share one remote pool between crawlers.
55
+ *
56
+ * **Example usage:**
57
+ *
58
+ * ```javascript
59
+ * const crawler = new PlaywrightCrawler({
60
+ * browserPool: remotePlaywrightBrowserPool({
61
+ * endpoint: 'wss://production-sfo.browserless.io?token=xxx',
62
+ * maxOpenBrowsers: 2,
63
+ * browserPoolOptions: { useFingerprints: false },
64
+ * }),
65
+ * requestHandler: async ({ page }) => { ... },
66
+ * });
67
+ * ```
68
+ *
69
+ * @category Browser management
70
+ */
71
+ export declare function remotePlaywrightBrowserPool(options: RemotePlaywrightBrowserPoolOptions): RemoteBrowserPool<Page>;
@@ -0,0 +1,61 @@
1
+ import { PlaywrightLauncher } from './playwright-launcher.js';
2
+ /**
3
+ * Builds a {@link BrowserPool} of Playwright browsers to pass to a {@link PlaywrightCrawler} 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 PlaywrightPlugin} by hand.
9
+ *
10
+ * **Example usage:**
11
+ *
12
+ * ```javascript
13
+ * const crawler = new PlaywrightCrawler({
14
+ * browserPool: playwrightBrowserPool({
15
+ * maxOpenPagesPerBrowser: 1,
16
+ * launchContext: { launcher: firefox },
17
+ * }),
18
+ * requestHandler: async ({ page }) => { ... },
19
+ * });
20
+ * ```
21
+ *
22
+ * The returned pool is *not* torn down by the crawler, which is what makes it shareable between crawlers.
23
+ *
24
+ * @category Browser management
25
+ */
26
+ export function playwrightBrowserPool(options = {}) {
27
+ const { launchContext, headless, configuration, ...poolOptions } = options;
28
+ return playwrightLauncher(launchContext, headless, configuration).createBrowserPool(poolOptions);
29
+ }
30
+ /**
31
+ * The {@link RemoteBrowserPool} counterpart of {@link playwrightBrowserPool}: connects to a remote browser
32
+ * service (Browserbase, Browserless, Steel, ...) with a Playwright plugin derived from `launchContext`.
33
+ *
34
+ * A {@link PlaywrightCrawler} accepts the same connection details directly via
35
+ * {@link BrowserCrawlerOptions.remoteBrowser|`remoteBrowser`}; reach for this factory when you also need to
36
+ * tune the wrapping pool, or to share one remote pool between crawlers.
37
+ *
38
+ * **Example usage:**
39
+ *
40
+ * ```javascript
41
+ * const crawler = new PlaywrightCrawler({
42
+ * browserPool: remotePlaywrightBrowserPool({
43
+ * endpoint: 'wss://production-sfo.browserless.io?token=xxx',
44
+ * maxOpenBrowsers: 2,
45
+ * browserPoolOptions: { useFingerprints: false },
46
+ * }),
47
+ * requestHandler: async ({ page }) => { ... },
48
+ * });
49
+ * ```
50
+ *
51
+ * @category Browser management
52
+ */
53
+ export function remotePlaywrightBrowserPool(options) {
54
+ const { launchContext, headless, configuration, ...remoteOptions } = options;
55
+ return playwrightLauncher(launchContext, headless, configuration).createRemoteBrowserPool(remoteOptions);
56
+ }
57
+ function playwrightLauncher(launchContext = {}, headless, configuration) {
58
+ return new PlaywrightLauncher(headless == null
59
+ ? launchContext
60
+ : { ...launchContext, launchOptions: { ...launchContext.launchOptions, headless } }, configuration);
61
+ }