@crawlee/playwright 4.0.0-beta.98 → 4.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { BrowserCrawler, RequestState, Router, serviceLocator } from '@crawlee/browser';
2
- import ow from 'ow';
3
- import { PlaywrightLauncher } from './playwright-launcher.js';
1
+ import { assertBrowserPoolNotConfigured, BrowserCrawler, parseArgument, RequestState, Router, schemas, serviceLocator, } from '@crawlee/browser';
2
+ import { z } from 'zod';
3
+ import { playwrightBrowserPool, remotePlaywrightBrowserPool } from './playwright-browser-pool.js';
4
4
  import { gotoExtended, playwrightUtils } from './utils/playwright-utils.js';
5
5
  /**
6
6
  * Provides a simple framework for parallel crawling of web pages
@@ -70,46 +70,41 @@ import { gotoExtended, playwrightUtils } from './utils/playwright-utils.js';
70
70
  export class PlaywrightCrawler extends BrowserCrawler {
71
71
  static optionsShape = {
72
72
  ...BrowserCrawler.optionsShape,
73
- browserPoolOptions: ow.optional.object,
74
- launcher: ow.optional.object,
75
- ignoreIframes: ow.optional.boolean,
76
- ignoreShadowRoots: ow.optional.boolean,
73
+ headless: z.boolean().optional(),
74
+ launcher: schemas.anyObject.optional(),
77
75
  };
76
+ static optionsSchema = z.strictObject(PlaywrightCrawler.optionsShape);
78
77
  /**
79
78
  * All `PlaywrightCrawler` parameters are passed via an options object.
80
79
  */
81
80
  constructor(options = {}) {
82
- ow(options, 'PlaywrightCrawlerOptions', ow.object.exactShape(PlaywrightCrawler.optionsShape));
83
- const { launchContext = {}, headless, contextPipelineBuilder, ...browserCrawlerOptions } = options;
84
- const browserPoolOptions = {
85
- ...options.browserPoolOptions,
86
- };
81
+ const parsedOptions = parseArgument(options, PlaywrightCrawler.optionsSchema, 'PlaywrightCrawlerOptions');
82
+ const { launchContext, headless, configuration, contextPipelineBuilder, ...browserCrawlerOptions } = parsedOptions;
87
83
  if (launchContext.proxyUrl) {
88
84
  throw new Error('PlaywrightCrawlerOptions.launchContext.proxyUrl is not allowed in PlaywrightCrawler.' +
89
85
  'Use PlaywrightCrawlerOptions.proxyConfiguration');
90
86
  }
91
- // `browserPlugins` is working when it's not overridden by `launchContext`,
92
- // which for crawlers it is always overridden. Hence the error to use the other option.
93
- if (browserPoolOptions.browserPlugins) {
94
- throw new Error('browserPoolOptions.browserPlugins is disallowed. Use launchContext.launcher instead.');
95
- }
96
- if (headless != null) {
97
- launchContext.launchOptions ??= {};
98
- launchContext.launchOptions.headless = headless;
87
+ if (options.browserPool) {
88
+ // The raw options, not the parsed ones: `launchContext` has a default, so by now it is always set.
89
+ assertBrowserPoolNotConfigured(new.target.name, {
90
+ launchContext: options.launchContext,
91
+ headless: options.headless,
92
+ });
99
93
  }
100
- const playwrightLauncher = new PlaywrightLauncher(launchContext, options.configuration);
101
- browserPoolOptions.browserPlugins = [playwrightLauncher.createBrowserPlugin()];
102
94
  super({
103
95
  ...browserCrawlerOptions,
104
96
  launchContext,
105
- browserPoolOptions,
97
+ configuration,
98
+ browserPoolBuilder: (remoteBrowser) => remoteBrowser
99
+ ? remotePlaywrightBrowserPool({ ...remoteBrowser, launchContext, headless, configuration })
100
+ : playwrightBrowserPool({ launchContext, headless, configuration }),
106
101
  contextPipelineBuilder: contextPipelineBuilder ?? (() => this.buildContextPipeline()),
107
102
  });
108
103
  }
109
104
  buildContextPipeline() {
110
105
  return super.buildContextPipeline().compose({ action: this.enhanceContext.bind(this) });
111
106
  }
112
- async _navigationHandler(crawlingContext, gotoOptions) {
107
+ async navigationHandler(crawlingContext, gotoOptions) {
113
108
  return gotoExtended(crawlingContext.page, crawlingContext.request, gotoOptions);
114
109
  }
115
110
  async enhanceContext(context) {
@@ -3,6 +3,7 @@ import { BrowserLauncher, Configuration } from '@crawlee/browser';
3
3
  import { PlaywrightPlugin } from '@crawlee/browser-pool';
4
4
  // @ts-ignore optional peer dependency or compatibility with es2022
5
5
  import type { Browser, BrowserType, LaunchOptions } from 'playwright';
6
+ import { z } from 'zod';
6
7
  /**
7
8
  * Apify extends the launch options of Playwright.
8
9
  * You can use any of the Playwright compatible
@@ -73,26 +74,34 @@ export declare class PlaywrightLauncher extends BrowserLauncher<PlaywrightPlugin
73
74
  readonly configuration: Configuration;
74
75
  protected static optionsShape: {
75
76
  // @ts-ignore optional peer dependency or compatibility with es2022
76
- launcher: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
77
+ launcher: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
77
78
  // @ts-ignore optional peer dependency or compatibility with es2022
78
- launchContextOptions: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
79
+ launchContextOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
80
+ proxyUrl: z.ZodOptional<z.ZodURL>;
81
+ useChrome: z.ZodOptional<z.ZodBoolean>;
82
+ useIncognitoPages: z.ZodOptional<z.ZodBoolean>;
83
+ browserPerProxy: z.ZodOptional<z.ZodBoolean>;
84
+ ignoreProxyCertificate: z.ZodOptional<z.ZodBoolean>;
85
+ userDataDir: z.ZodOptional<z.ZodString>;
79
86
  // @ts-ignore optional peer dependency or compatibility with es2022
80
- proxyUrl: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
81
- // @ts-ignore optional peer dependency or compatibility with es2022
82
- useChrome: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
83
- // @ts-ignore optional peer dependency or compatibility with es2022
84
- useIncognitoPages: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
85
- // @ts-ignore optional peer dependency or compatibility with es2022
86
- browserPerProxy: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
87
- // @ts-ignore optional peer dependency or compatibility with es2022
88
- ignoreProxyCertificate: import("ow").BooleanPredicate & import("ow").BasePredicate<boolean | undefined>;
87
+ launchOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
88
+ userAgent: z.ZodOptional<z.ZodString>;
89
+ };
90
+ protected static optionsSchema: z.ZodObject<{
89
91
  // @ts-ignore optional peer dependency or compatibility with es2022
90
- userDataDir: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
92
+ launcher: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
91
93
  // @ts-ignore optional peer dependency or compatibility with es2022
92
- launchOptions: import("ow").ObjectPredicate<object> & import("ow").BasePredicate<object | undefined>;
94
+ launchContextOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
95
+ proxyUrl: z.ZodOptional<z.ZodURL>;
96
+ useChrome: z.ZodOptional<z.ZodBoolean>;
97
+ useIncognitoPages: z.ZodOptional<z.ZodBoolean>;
98
+ browserPerProxy: z.ZodOptional<z.ZodBoolean>;
99
+ ignoreProxyCertificate: z.ZodOptional<z.ZodBoolean>;
100
+ userDataDir: z.ZodOptional<z.ZodString>;
93
101
  // @ts-ignore optional peer dependency or compatibility with es2022
94
- userAgent: import("ow").StringPredicate & import("ow").BasePredicate<string | undefined>;
95
- };
102
+ launchOptions: z.ZodOptional<z.ZodCustom<import("@crawlee/types").Dictionary, import("@crawlee/types").Dictionary>>;
103
+ userAgent: z.ZodOptional<z.ZodString>;
104
+ }, z.core.$strict>;
96
105
  /**
97
106
  * All `PlaywrightLauncher` parameters are passed via this launchContext object.
98
107
  */
@@ -1,6 +1,6 @@
1
- import { BrowserLauncher, Configuration } from '@crawlee/browser';
1
+ import { BrowserLauncher, Configuration, parseArgument, schemas } from '@crawlee/browser';
2
2
  import { PlaywrightPlugin } from '@crawlee/browser-pool';
3
- import ow from 'ow';
3
+ import { z } from 'zod';
4
4
  /**
5
5
  * `PlaywrightLauncher` is based on the `BrowserLauncher`. It launches `playwright` browser instance.
6
6
  * @ignore
@@ -9,21 +9,23 @@ export class PlaywrightLauncher extends BrowserLauncher {
9
9
  configuration;
10
10
  static optionsShape = {
11
11
  ...BrowserLauncher.optionsShape,
12
- launcher: ow.optional.object,
13
- launchContextOptions: ow.optional.object,
12
+ // Passthrough schemas — the launcher module object must keep its prototype through parsing.
13
+ launcher: schemas.anyObject.optional(),
14
+ launchContextOptions: schemas.anyObject.optional(),
14
15
  };
16
+ static optionsSchema = z.strictObject(PlaywrightLauncher.optionsShape);
15
17
  /**
16
18
  * All `PlaywrightLauncher` parameters are passed via this launchContext object.
17
19
  */
18
20
  constructor(launchContext = {}, configuration = Configuration.getGlobalConfiguration()) {
19
- ow(launchContext, 'PlaywrightLauncherOptions', ow.object.exactShape(PlaywrightLauncher.optionsShape));
20
- const { launcher = BrowserLauncher.requireLauncherOrThrow('playwright', 'apify/actor-node-playwright-*').chromium, } = launchContext;
21
- const { launchOptions = {}, ...rest } = launchContext;
21
+ const parsedContext = parseArgument(launchContext, PlaywrightLauncher.optionsSchema, 'PlaywrightLaunchContext');
22
+ const { launcher = BrowserLauncher.requireLauncherOrThrow('playwright', 'apify/actor-node-playwright-*').chromium, } = parsedContext;
23
+ const { launchOptions = {}, ...rest } = parsedContext;
22
24
  super({
23
25
  ...rest,
24
26
  launchOptions: {
25
27
  ...launchOptions,
26
- executablePath: getDefaultExecutablePath(launchContext, configuration),
28
+ executablePath: getDefaultExecutablePath(parsedContext, configuration),
27
29
  },
28
30
  launcher,
29
31
  }, configuration);
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { Configuration, type Request } from '@crawlee/browser';
21
21
  import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
22
- import { type CheerioRoot } from '@crawlee/utils';
22
+ import { type CheerioRoot } from '@crawlee/utils/internal';
23
23
  // @ts-ignore optional peer dependency or compatibility with es2022
24
24
  import type { Download, Page, Response } from 'playwright';
25
25
  import type { EnqueueLinksByClickingElementsOptions } from '../enqueue-links/click-elements.js';
@@ -336,7 +336,6 @@ export interface HandleCloudflareChallengeOptions {
336
336
  * @param [options]
337
337
  */
338
338
  declare function handleCloudflareChallenge(page: Page, url: string, options?: HandleCloudflareChallengeOptions): Promise<Response | undefined>;
339
- /** @internal */
340
339
  export interface PlaywrightContextUtils {
341
340
  /**
342
341
  * Injects a JavaScript file into current `page`.
@@ -455,8 +454,7 @@ export interface PlaywrightContextUtils {
455
454
  * in `href` elements, but rather navigations are triggered in click handlers.
456
455
  * If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
457
456
  *
458
- * Optionally, the function allows you to filter the target links' URLs using an array of {@link PseudoUrl} objects
459
- * and override settings of the enqueued {@link Request} objects.
457
+ * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
460
458
  *
461
459
  * **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
462
460
  * such as changing the Z-index of elements being clicked and their visibility. Therefore,
@@ -477,9 +475,9 @@ export interface PlaywrightContextUtils {
477
475
  * async requestHandler({ enqueueLinksByClickingElements }) {
478
476
  * await enqueueLinksByClickingElements({
479
477
  * selector: 'a.product-detail',
480
- * globs: [
481
- * 'https://www.example.com/handbags/**'
482
- * 'https://www.example.com/purses/**'
478
+ * include: [
479
+ * 'https://www.example.com/handbags/**',
480
+ * 'https://www.example.com/purses/**',
483
481
  * ],
484
482
  * });
485
483
  * });
@@ -20,10 +20,9 @@
20
20
  import { readFile } from 'node:fs/promises';
21
21
  import { createRequire } from 'node:module';
22
22
  import vm from 'node:vm';
23
- import { Configuration, KeyValueStore, serviceLocator, SessionError, validators } from '@crawlee/browser';
23
+ import { Configuration, KeyValueStore, parseArgument, schemas, serviceLocator, SessionError, validators, } from '@crawlee/browser';
24
24
  import { expandShadowRoots, sleep } from '@crawlee/utils';
25
- import * as cheerio from 'cheerio';
26
- import ow from 'ow';
25
+ import { z } from 'zod';
27
26
  import { LruCache } from '@apify/datastructures';
28
27
  import { enqueueLinksByClickingElements } from '../enqueue-links/click-elements.js';
29
28
  import { RenderingTypePredictor } from './rendering-type-prediction.js';
@@ -32,6 +31,36 @@ const require = createRequire(import.meta.url);
32
31
  const jqueryPath = require.resolve('jquery');
33
32
  const MAX_INJECT_FILE_CACHE_SIZE = 10;
34
33
  const DEFAULT_BLOCK_REQUEST_URL_PATTERNS = ['.css', '.jpg', '.jpeg', '.png', '.svg', '.gif', '.woff', '.pdf', '.zip'];
34
+ const filePathSchema = z.string();
35
+ const injectFileOptionsSchema = z.strictObject({
36
+ surviveNavigations: z.boolean().optional(),
37
+ });
38
+ const gotoExtendedRequestSchema = z.looseObject({
39
+ url: z.url(),
40
+ method: z.string().optional(),
41
+ headers: schemas.anyObject.optional(),
42
+ payload: z.union([z.string(), z.instanceof(Uint8Array)]).optional(),
43
+ });
44
+ const blockRequestsOptionsSchema = z.strictObject({
45
+ urlPatterns: schemas.arrayOf(z.string(), 'strings').default(DEFAULT_BLOCK_REQUEST_URL_PATTERNS),
46
+ extraUrlPatterns: schemas.arrayOf(z.string(), 'strings').default(() => []),
47
+ });
48
+ const infiniteScrollOptionsSchema = z.strictObject({
49
+ timeoutSecs: schemas.anyNumber.default(0),
50
+ maxScrollHeight: schemas.anyNumber.default(0),
51
+ waitForSecs: schemas.anyNumber.default(4),
52
+ scrollDownAndUp: z.boolean().default(false),
53
+ buttonSelector: z.string().optional(),
54
+ stopScrollCallback: schemas.anyFunction.optional(),
55
+ });
56
+ const saveSnapshotOptionsSchema = z.strictObject({
57
+ key: z.string().min(1).default('SNAPSHOT'),
58
+ screenshotQuality: schemas.anyNumber.default(50),
59
+ saveScreenshot: z.boolean().default(true),
60
+ saveHtml: z.boolean().default(true),
61
+ keyValueStoreName: z.string().optional(),
62
+ configuration: schemas.anyObject.optional(),
63
+ });
35
64
  /**
36
65
  * Cache contents of previously injected files to limit file system access.
37
66
  */
@@ -48,18 +77,16 @@ const injectedFilesCache = new LruCache({ maxLength: MAX_INJECT_FILE_CACHE_SIZE
48
77
  * @param [options]
49
78
  */
50
79
  export async function injectFile(page, filePath, options = {}) {
51
- ow(page, ow.object.validate(validators.browserPage));
52
- ow(filePath, ow.string);
53
- ow(options, ow.object.exactShape({
54
- surviveNavigations: ow.optional.boolean,
55
- }));
80
+ parseArgument(page, validators.browserPage);
81
+ parseArgument(filePath, filePathSchema);
82
+ const { surviveNavigations } = parseArgument(options, injectFileOptionsSchema);
56
83
  let contents = injectedFilesCache.get(filePath);
57
84
  if (!contents) {
58
85
  contents = await readFile(filePath, 'utf8');
59
86
  injectedFilesCache.add(filePath, contents);
60
87
  }
61
88
  const evalP = page.evaluate(contents);
62
- if (options.surviveNavigations) {
89
+ if (surviveNavigations) {
63
90
  page.on('framenavigated', async () => page
64
91
  .evaluate(contents)
65
92
  .catch((error) => getLog().warning('An error occurred during the script injection!', { error })));
@@ -93,7 +120,7 @@ export async function injectFile(page, filePath, options = {}) {
93
120
  * @param [options.surviveNavigations] Opt-out option to disable the JQuery reinjection after navigation.
94
121
  */
95
122
  export async function injectJQuery(page, options) {
96
- ow(page, ow.object.validate(validators.browserPage));
123
+ parseArgument(page, validators.browserPage);
97
124
  return injectFile(page, jqueryPath, { surviveNavigations: options?.surviveNavigations ?? true });
98
125
  }
99
126
  /**
@@ -109,14 +136,9 @@ export async function injectJQuery(page, options) {
109
136
  * @param [gotoOptions] Custom options for `page.goto()`.
110
137
  */
111
138
  export async function gotoExtended(page, request, gotoOptions = {}) {
112
- ow(page, ow.object.validate(validators.browserPage));
113
- ow(request, ow.object.partialShape({
114
- url: ow.string.url,
115
- method: ow.optional.string,
116
- headers: ow.optional.object,
117
- payload: ow.optional.any(ow.string, ow.uint8Array),
118
- }));
119
- ow(gotoOptions, ow.object);
139
+ parseArgument(page, validators.browserPage);
140
+ parseArgument(request, gotoExtendedRequestSchema);
141
+ parseArgument(gotoOptions, schemas.anyObject);
120
142
  const { url, method, headers, payload } = request;
121
143
  const isEmpty = (o) => !o || Object.keys(o).length === 0;
122
144
  if (method !== 'GET' || payload) {
@@ -201,12 +223,8 @@ export async function gotoExtended(page, request, gotoOptions = {}) {
201
223
  * @param [options]
202
224
  */
203
225
  export async function blockRequests(page, options = {}) {
204
- ow(page, ow.object.validate(validators.browserPage));
205
- ow(options, ow.object.exactShape({
206
- urlPatterns: ow.optional.array.ofType(ow.string),
207
- extraUrlPatterns: ow.optional.array.ofType(ow.string),
208
- }));
209
- const { urlPatterns = DEFAULT_BLOCK_REQUEST_URL_PATTERNS, extraUrlPatterns = [] } = options;
226
+ parseArgument(page, validators.browserPage);
227
+ const { urlPatterns, extraUrlPatterns } = parseArgument(options, blockRequestsOptionsSchema);
210
228
  const patternsToBlock = [...urlPatterns, ...extraUrlPatterns];
211
229
  try {
212
230
  const client = await page.context().newCDPSession(page);
@@ -264,16 +282,8 @@ export function compileScript(scriptString, context = Object.create(null)) {
264
282
  * @param [options]
265
283
  */
266
284
  export async function infiniteScroll(page, options = {}) {
267
- ow(page, ow.object.validate(validators.browserPage));
268
- ow(options, ow.object.exactShape({
269
- timeoutSecs: ow.optional.number,
270
- maxScrollHeight: ow.optional.number,
271
- waitForSecs: ow.optional.number,
272
- scrollDownAndUp: ow.optional.boolean,
273
- buttonSelector: ow.optional.string,
274
- stopScrollCallback: ow.optional.function,
275
- }));
276
- const { timeoutSecs = 0, maxScrollHeight = 0, waitForSecs = 4, scrollDownAndUp = false, buttonSelector, stopScrollCallback, } = options;
285
+ parseArgument(page, validators.browserPage);
286
+ const { timeoutSecs, maxScrollHeight, waitForSecs, scrollDownAndUp, buttonSelector, stopScrollCallback } = parseArgument(options, infiniteScrollOptionsSchema);
277
287
  let finished;
278
288
  const startTime = Date.now();
279
289
  const CHECK_INTERVAL_MILLIS = 1000;
@@ -350,16 +360,8 @@ export async function infiniteScroll(page, options = {}) {
350
360
  * @param [options]
351
361
  */
352
362
  export async function saveSnapshot(page, options = {}) {
353
- ow(page, ow.object.validate(validators.browserPage));
354
- ow(options, ow.object.exactShape({
355
- key: ow.optional.string.nonEmpty,
356
- screenshotQuality: ow.optional.number,
357
- saveScreenshot: ow.optional.boolean,
358
- saveHtml: ow.optional.boolean,
359
- keyValueStoreName: ow.optional.string,
360
- configuration: ow.optional.object,
361
- }));
362
- const { key = 'SNAPSHOT', screenshotQuality = 50, saveScreenshot = true, saveHtml = true, keyValueStoreName, configuration, } = options;
363
+ parseArgument(page, validators.browserPage);
364
+ const { key, screenshotQuality, saveScreenshot, saveHtml, keyValueStoreName, configuration } = parseArgument(options, saveSnapshotOptionsSchema);
363
365
  try {
364
366
  const store = await KeyValueStore.open(keyValueStoreName ? { name: keyValueStoreName } : null, {
365
367
  configuration: configuration ?? Configuration.getGlobalConfiguration(),
@@ -397,12 +399,13 @@ export async function saveSnapshot(page, options = {}) {
397
399
  * @param ignoreShadowRoots
398
400
  */
399
401
  export async function parseWithCheerio(page, ignoreShadowRoots = false, ignoreIframes = false) {
400
- ow(page, ow.object.validate(validators.browserPage));
402
+ parseArgument(page, validators.browserPage);
401
403
  const html = ignoreShadowRoots
402
404
  ? null
403
405
  : (await page.evaluate(`(${expandShadowRoots.toString()})(document)`));
404
406
  const pageContent = html || (await page.content());
405
- const $ = cheerio.load(pageContent);
407
+ const { load } = await import('cheerio');
408
+ const $ = load(pageContent);
406
409
  if (page.frames().length > 1 && !ignoreIframes) {
407
410
  const frames = await page.$$('iframe');
408
411
  const cheerioIframes = $('iframe').toArray();
@@ -457,7 +460,7 @@ ${error.message}
457
460
  return idcacPlaywright;
458
461
  }
459
462
  export async function closeCookieModals(page) {
460
- ow(page, ow.object.validate(validators.browserPage));
463
+ parseArgument(page, validators.browserPage);
461
464
  const idcac = await getIdcacPlaywright();
462
465
  if (idcac?.getInjectableScript()) {
463
466
  await page.evaluate(idcac.getInjectableScript());
@@ -496,9 +499,9 @@ async function handleCloudflareChallenge(page, url, options = {}) {
496
499
  };
497
500
  options.isChallengeCallback ??= async () => {
498
501
  return await page.evaluate(async () => {
499
- // Cloudflare nests the ray ID under varying wrapper elements, so we match by descendants
500
- // instead of a direct-child chain (e.g. a `.footer-wrapper` was inserted in between).
501
- return !!document.querySelector('.footer .footer-inner .diagnostic-wrapper .ray-id');
502
+ // Cloudflare keeps reshuffling the wrapper elements between `.footer-inner` and `.ray-id`,
503
+ // so only the stable outer classes are matched.
504
+ return !!document.querySelector('.footer .footer-inner .ray-id');
502
505
  });
503
506
  };
504
507
  const retryBlocked = async () => {
@@ -26,7 +26,7 @@ export interface IRenderingTypePredictor {
26
26
  * @experimental
27
27
  */
28
28
  export declare class RenderingTypePredictor implements IRenderingTypePredictor {
29
- private detectionRatio;
29
+ #private;
30
30
  private state;
31
31
  constructor({ detectionRatio, persistenceOptions }: RenderingTypePredictorOptions);
32
32
  /**
@@ -2,6 +2,7 @@ import { RecoverableState } from '@crawlee/core';
2
2
  import LogisticRegression from 'ml-logistic-regression';
3
3
  import { Matrix } from 'ml-matrix';
4
4
  import stringComparison from 'string-comparison';
5
+ import { z } from 'zod';
5
6
  const urlComponents = (url) => {
6
7
  return [url.hostname, ...url.pathname.split('/')];
7
8
  };
@@ -24,41 +25,57 @@ const calculateUrlSimilarity = (a, b) => {
24
25
  };
25
26
  const sum = (values) => values.reduce((acc, value) => acc + value);
26
27
  const mean = (values) => (values.length > 0 ? sum(values) / values.length : undefined);
28
+ const renderingType = z.enum(['clientOnly', 'static']);
29
+ const predictorState = z.object({
30
+ logreg: z.instanceof(LogisticRegression),
31
+ detectionResults: z.map(renderingType, z.map(z.string().optional(), z.array(z.array(z.string())))),
32
+ });
33
+ const persistedState = z.object({
34
+ logreg: z
35
+ .record(z.string(), z.unknown())
36
+ .prefault(() => new LogisticRegression({ numSteps: 1000, learningRate: 0.05 }).toJSON()),
37
+ detectionResults: z
38
+ .array(z.object({
39
+ renderingType,
40
+ urlPartsByLabel: z.array(z.object({
41
+ label: z.string().optional(),
42
+ urlParts: z.array(z.array(z.string())),
43
+ })),
44
+ }))
45
+ .prefault([]),
46
+ });
47
+ const stateCodec = z.codec(persistedState, predictorState, {
48
+ decode: ({ logreg, detectionResults }) => ({
49
+ logreg: LogisticRegression.load(logreg),
50
+ detectionResults: new Map(detectionResults.map(({ renderingType, urlPartsByLabel }) => [
51
+ renderingType,
52
+ new Map(urlPartsByLabel.map(({ label, urlParts }) => [label, urlParts])),
53
+ ])),
54
+ }),
55
+ encode: ({ logreg, detectionResults }) => ({
56
+ logreg: logreg.toJSON(),
57
+ detectionResults: Array.from(detectionResults.entries()).map(([renderingType, urlPartsByLabel]) => ({
58
+ renderingType,
59
+ urlPartsByLabel: Array.from(urlPartsByLabel.entries()).map(([label, urlParts]) => ({ label, urlParts })),
60
+ })),
61
+ }),
62
+ });
27
63
  /**
28
64
  * Stores rendering type information for previously crawled URLs and predicts the rendering type for URLs that have yet to be crawled and recommends when rendering type detection should be performed.
29
65
  *
30
66
  * @experimental
31
67
  */
32
68
  export class RenderingTypePredictor {
33
- detectionRatio;
69
+ #detectionRatio;
70
+ // kept as TS-private: tests reach for it at runtime
34
71
  state;
35
72
  constructor({ detectionRatio, persistenceOptions }) {
36
- this.detectionRatio = detectionRatio;
73
+ this.#detectionRatio = detectionRatio;
37
74
  this.state = new RecoverableState({
38
- defaultState: {
39
- logreg: new LogisticRegression({ numSteps: 1000, learningRate: 0.05 }),
40
- detectionResults: new Map(),
41
- },
42
- serialize: (state) => JSON.stringify({
43
- logreg: state.logreg.toJSON(),
44
- detectionResults: Array.from(state.detectionResults.entries()).map(([renderingType, urlPartsByLabel]) => ({
45
- renderingType,
46
- urlPartsByLabel: Array.from(urlPartsByLabel.entries()).map(([label, urlParts]) => ({
47
- label,
48
- urlParts,
49
- })),
50
- })),
51
- }),
52
- deserialize: (serializedState) => {
53
- const { logreg, detectionResults = [] } = JSON.parse(serializedState);
54
- return {
55
- logreg: LogisticRegression.load(logreg),
56
- detectionResults: new Map(detectionResults.map((serializedItem) => [
57
- serializedItem.renderingType,
58
- new Map(serializedItem.urlPartsByLabel.map((item) => [item.label, item.urlParts])),
59
- ])),
60
- };
61
- },
75
+ defaultState: () => stateCodec.decode({}),
76
+ // The codec validates in the decode direction, so it is a Standard Schema as-is; encoding needs a call.
77
+ deserialize: stateCodec,
78
+ serialize: (state) => stateCodec.encode(state),
62
79
  persistStateKey: 'rendering-type-predictor-state',
63
80
  persistenceEnabled: true,
64
81
  ...persistenceOptions,
@@ -86,7 +103,7 @@ export class RenderingTypePredictor {
86
103
  renderingType: prediction === 1 ? 'static' : 'clientOnly',
87
104
  detectionProbabilityRecommendation: Math.abs(scores[0] - scores[1]) < 0.1
88
105
  ? 1
89
- : this.detectionRatio * Math.max(1, 5 - this.resultCount(label)),
106
+ : this.#detectionRatio * Math.max(1, 5 - this.resultCount(label)),
90
107
  };
91
108
  }
92
109
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crawlee/playwright",
3
- "version": "4.0.0-beta.98",
3
+ "version": "4.0.0-rc.0",
4
4
  "description": "The scalable web crawling and scraping library for JavaScript/Node.js. Enables development of data extraction and web automation jobs (not only) with headless Chrome and Puppeteer.",
5
5
  "engines": {
6
6
  "node": ">=22.0.0"
@@ -49,22 +49,21 @@
49
49
  "dependencies": {
50
50
  "@apify/datastructures": "^2.0.3",
51
51
  "@apify/timeout": "^0.4.4",
52
- "@crawlee/basic": "4.0.0-beta.98",
53
- "@crawlee/browser": "4.0.0-beta.98",
54
- "@crawlee/browser-pool": "4.0.0-beta.98",
55
- "@crawlee/cheerio": "4.0.0-beta.98",
56
- "@crawlee/core": "4.0.0-beta.98",
57
- "@crawlee/types": "4.0.0-beta.98",
58
- "@crawlee/utils": "4.0.0-beta.98",
52
+ "@crawlee/basic": "4.0.0-rc.0",
53
+ "@crawlee/browser": "4.0.0-rc.0",
54
+ "@crawlee/browser-pool": "4.0.0-rc.0",
55
+ "@crawlee/cheerio": "4.0.0-rc.0",
56
+ "@crawlee/core": "4.0.0-rc.0",
57
+ "@crawlee/types": "4.0.0-rc.0",
58
+ "@crawlee/utils": "4.0.0-rc.0",
59
59
  "cheerio": "^1.0.0",
60
- "idcac-playwright": "^0.1.3",
61
60
  "jquery": "^3.7.1",
62
61
  "ml-logistic-regression": "^2.0.0",
63
62
  "ml-matrix": "^6.12.1",
64
- "ow": "^2.0.0",
65
63
  "string-comparison": "^1.3.0",
66
64
  "tslib": "^2.8.1",
67
- "type-fest": "^4.0.0"
65
+ "type-fest": "^4.0.0",
66
+ "zod": "^4.4.3"
68
67
  },
69
68
  "peerDependencies": {
70
69
  "idcac-playwright": "^0.2.0",
@@ -85,5 +84,5 @@
85
84
  }
86
85
  }
87
86
  },
88
- "gitHead": "3b8cd86b13e253ab5fc71e631e12a68f7465cee5"
87
+ "gitHead": "79ab33dacdacb83e0197e6516d145f3aceef80c7"
89
88
  }