scrapely 1.2.3 → 1.2.4
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 +119 -1
- package/dist/actor.d.ts +703 -0
- package/dist/actor.js +1441 -0
- package/dist/browser-configuration.d.ts +58 -0
- package/dist/browser-configuration.js +128 -0
- package/dist/browser-locations.d.ts +5 -0
- package/dist/browser-locations.js +5469 -0
- package/dist/charging.d.ts +153 -0
- package/dist/charging.js +782 -0
- package/dist/charging_client.d.ts +18 -0
- package/dist/charging_client.js +49 -0
- package/dist/configuration.d.ts +121 -0
- package/dist/configuration.js +128 -0
- package/dist/crypto/secrets.d.ts +44 -0
- package/dist/crypto/secrets.js +164 -0
- package/dist/events/events-client.d.ts +59 -0
- package/dist/events/events-client.js +120 -0
- package/dist/events/scrapely-event-manager.d.ts +13 -0
- package/dist/events/scrapely-event-manager.js +61 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +95 -0
- package/dist/log-env.d.ts +1 -0
- package/dist/log-env.js +12 -0
- package/dist/manifest.d.ts +30 -0
- package/dist/manifest.js +47 -0
- package/dist/patch-browser-pool.d.ts +66 -0
- package/dist/patch-browser-pool.js +827 -0
- package/dist/proxy-configuration.d.ts +118 -0
- package/dist/proxy-configuration.js +88 -0
- package/dist/request-list.d.ts +57 -0
- package/dist/request-list.js +79 -0
- package/dist/runner/docker-manager.d.ts +88 -0
- package/dist/runner/docker-manager.js +283 -0
- package/dist/runner/index.d.ts +2 -0
- package/dist/runner/index.js +7 -0
- package/dist/runner/timeout-manager.d.ts +43 -0
- package/dist/runner/timeout-manager.js +90 -0
- package/dist/scrapely-proxy-configuration.d.ts +99 -0
- package/dist/scrapely-proxy-configuration.js +559 -0
- package/dist/storage/api-storage-client.d.ts +199 -0
- package/dist/storage/api-storage-client.js +910 -0
- package/dist/storage/billing-tracker.d.ts +163 -0
- package/dist/storage/billing-tracker.js +260 -0
- package/dist/storage/dataset-client.d.ts +43 -0
- package/dist/storage/dataset-client.js +232 -0
- package/dist/storage/dataset-collection-client.d.ts +8 -0
- package/dist/storage/dataset-collection-client.js +75 -0
- package/dist/storage/dataset-push-script.d.ts +18 -0
- package/dist/storage/dataset-push-script.js +38 -0
- package/dist/storage/dragonfly-client.d.ts +94 -0
- package/dist/storage/dragonfly-client.js +167 -0
- package/dist/storage/index.d.ts +11 -0
- package/dist/storage/index.js +25 -0
- package/dist/storage/key-value-store-client.d.ts +61 -0
- package/dist/storage/key-value-store-client.js +306 -0
- package/dist/storage/kv-store-collection-client.d.ts +8 -0
- package/dist/storage/kv-store-collection-client.js +76 -0
- package/dist/storage/kvs-iteration.d.ts +17 -0
- package/dist/storage/kvs-iteration.js +126 -0
- package/dist/storage/log-manager.d.ts +114 -0
- package/dist/storage/log-manager.js +244 -0
- package/dist/storage/patch-storage.d.ts +11 -0
- package/dist/storage/patch-storage.js +177 -0
- package/dist/storage/request-queue-client.d.ts +93 -0
- package/dist/storage/request-queue-client.js +522 -0
- package/dist/storage/rq-collection-client.d.ts +8 -0
- package/dist/storage/rq-collection-client.js +89 -0
- package/dist/storage/rq-list-requests.d.ts +59 -0
- package/dist/storage/rq-list-requests.js +114 -0
- package/dist/storage/rq-lock-script.d.ts +56 -0
- package/dist/storage/rq-lock-script.js +127 -0
- package/dist/storage/run-metadata.d.ts +98 -0
- package/dist/storage/run-metadata.js +256 -0
- package/dist/storage/s3-client.d.ts +103 -0
- package/dist/storage/s3-client.js +302 -0
- package/dist/storage/ttl-manager.d.ts +70 -0
- package/dist/storage/ttl-manager.js +115 -0
- package/dist/storage/utils.d.ts +3 -0
- package/dist/storage/utils.js +19 -0
- package/dist/storage-identifiers.d.ts +20 -0
- package/dist/storage-identifiers.js +6 -0
- package/dist/utils.d.ts +54 -0
- package/dist/utils.js +63 -0
- package/dist/validator.d.ts +24 -0
- package/dist/validator.js +158 -0
- package/package.json +90 -9
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setCdpUrl = setCdpUrl;
|
|
4
|
+
exports.getCdpUrlForTests = getCdpUrlForTests;
|
|
5
|
+
exports.resetBrowserProxyWarningsForTests = resetBrowserProxyWarningsForTests;
|
|
6
|
+
exports.applyProxyInfoToCdpUrl = applyProxyInfoToCdpUrl;
|
|
7
|
+
exports.patchBrowserPoolModule = patchBrowserPoolModule;
|
|
8
|
+
exports.patchCrawleeCrawlerModule = patchCrawleeCrawlerModule;
|
|
9
|
+
exports.patchLoadedCrawleeCopies = patchLoadedCrawleeCopies;
|
|
10
|
+
exports.patchRawBrowserType = patchRawBrowserType;
|
|
11
|
+
/**
|
|
12
|
+
* Redirects every browser launch (Crawlee browser pool, raw playwright /
|
|
13
|
+
* puppeteer) to the Scrapely remote browser over CDP. The remote browser
|
|
14
|
+
* never takes a proxy: `proxy_url` is stripped from the CDP URL, custom
|
|
15
|
+
* (BYOP) proxies are warned about and ignored, and location comes from
|
|
16
|
+
* `browserConfiguration`, a platform ProxyConfiguration, or the run default
|
|
17
|
+
* (see applyProxyInfoToCdpUrl).
|
|
18
|
+
*/
|
|
19
|
+
const browser_configuration_js_1 = require("./browser-configuration.js");
|
|
20
|
+
const browser_locations_js_1 = require("./browser-locations.js");
|
|
21
|
+
let _cdpUrl;
|
|
22
|
+
/**
|
|
23
|
+
* Lazily get CDP URL from environment or cached value.
|
|
24
|
+
* This allows non-browser actors to work without CDP being set.
|
|
25
|
+
*/
|
|
26
|
+
function getCdpUrl() {
|
|
27
|
+
return _cdpUrl || process.env.CDP_URL;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set CDP URL explicitly (for testing or advanced use cases).
|
|
31
|
+
* Pass `undefined` to clear the override and fall back to `CDP_URL` env.
|
|
32
|
+
* Normally the CDP URL is read from CDP_URL env var lazily.
|
|
33
|
+
*/
|
|
34
|
+
function setCdpUrl(url) {
|
|
35
|
+
_cdpUrl = url;
|
|
36
|
+
}
|
|
37
|
+
/** @internal Exported for tests. */
|
|
38
|
+
function getCdpUrlForTests() {
|
|
39
|
+
return getCdpUrl();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether the process runs on the Scrapely platform (worker-injected env).
|
|
43
|
+
*/
|
|
44
|
+
function isAtHome() {
|
|
45
|
+
return !!(process.env.SCRAPELY_IS_AT_HOME || process.env.APIFY_IS_AT_HOME);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Unpatched builds (browser patch off at build time) keep their browser
|
|
49
|
+
* binaries; the worker sets SCRAPELY_BROWSER_PATCH=0 so every launch goes to
|
|
50
|
+
* the original local launcher instead of the remote CDP browser. Read lazily
|
|
51
|
+
* at launch time.
|
|
52
|
+
*/
|
|
53
|
+
function browserPatchDisabled() {
|
|
54
|
+
return process.env.SCRAPELY_BROWSER_PATCH === '0';
|
|
55
|
+
}
|
|
56
|
+
/** gVisor lacks crashpad support; ReadonlyRootfs + tmpfs /tmp loses crash dirs. */
|
|
57
|
+
const GVISOR_CHROME_ARGS = [
|
|
58
|
+
'--disable-breakpad',
|
|
59
|
+
'--disable-crash-reporter',
|
|
60
|
+
'--disable-dev-shm-usage',
|
|
61
|
+
'--crash-dumps-dir=/tmp',
|
|
62
|
+
];
|
|
63
|
+
/** Add gVisor-safe Chrome args to local launch options when running under runsc. */
|
|
64
|
+
function withGVisorArgs(options) {
|
|
65
|
+
const opts = { ...(options ?? {}) };
|
|
66
|
+
if (process.env.SCRAPELY_CONTAINER_RUNTIME !== 'runsc')
|
|
67
|
+
return opts;
|
|
68
|
+
const args = [...(opts.args ?? [])];
|
|
69
|
+
for (const arg of GVISOR_CHROME_ARGS) {
|
|
70
|
+
if (!args.includes(arg))
|
|
71
|
+
args.push(arg);
|
|
72
|
+
}
|
|
73
|
+
opts.args = args;
|
|
74
|
+
return opts;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Require CDP_URL for platform browser runs. Local launch is disabled on the
|
|
78
|
+
* platform unless the build is unpatched (SCRAPELY_BROWSER_PATCH=0) — otherwise
|
|
79
|
+
* a worker container without CDP_URL is a misconfiguration and must fail
|
|
80
|
+
* loudly instead of launching Chromium inside the (gVisor) container.
|
|
81
|
+
*/
|
|
82
|
+
function requireCdpUrl() {
|
|
83
|
+
const cdpUrl = getCdpUrl();
|
|
84
|
+
if (!cdpUrl) {
|
|
85
|
+
throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
|
|
86
|
+
}
|
|
87
|
+
return cdpUrl;
|
|
88
|
+
}
|
|
89
|
+
const BYOP_IGNORED_WARNING = '[scrapely] Custom proxies are ignored when using the Scrapely remote browser — it routes traffic through its own ' +
|
|
90
|
+
'integrated proxy. Set the location with browserConfiguration (Actor.createBrowserConfiguration({ countryCode, state, city })) ' +
|
|
91
|
+
'or ProxyConfiguration countryCode.';
|
|
92
|
+
const GROUPS_SESSION_INFO = '[scrapely] Proxy groups and session IDs do not apply to the Scrapely remote browser; routing is location-based ' +
|
|
93
|
+
'(proxy_country/proxy_state). Each browser connection is a fresh session.';
|
|
94
|
+
let byopWarned = false;
|
|
95
|
+
let groupsInfoLogged = false;
|
|
96
|
+
let unknownSubdivisionWarned = false;
|
|
97
|
+
/** @internal Reset the once-per-process browser proxy messages (tests only). */
|
|
98
|
+
function resetBrowserProxyWarningsForTests() {
|
|
99
|
+
byopWarned = false;
|
|
100
|
+
groupsInfoLogged = false;
|
|
101
|
+
unknownSubdivisionWarned = false;
|
|
102
|
+
}
|
|
103
|
+
/** Parse a proxy URL, accepting puppeteer-style scheme-less `host:port`. */
|
|
104
|
+
function parseProxyUrl(proxyUrl) {
|
|
105
|
+
for (const candidate of [proxyUrl, `http://${proxyUrl}`]) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = new URL(candidate);
|
|
108
|
+
if (parsed.hostname)
|
|
109
|
+
return parsed;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// try the next form
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Platform gateway hostname injected by the worker (same vars as
|
|
119
|
+
* getPlatformProxyEnv). Read directly: this module also loads in SDK-less
|
|
120
|
+
* processes (base image register hook) and must not pull in @crawlee/core.
|
|
121
|
+
*/
|
|
122
|
+
function platformProxyHostname() {
|
|
123
|
+
const hostname = (process.env.SCRAPELY_PROXY_HOSTNAME || process.env.APIFY_PROXY_HOSTNAME || '').trim().toLowerCase();
|
|
124
|
+
return hostname || undefined;
|
|
125
|
+
}
|
|
126
|
+
function isPlatformProxyUrl(proxyUrl) {
|
|
127
|
+
const platformHost = platformProxyHostname();
|
|
128
|
+
if (!platformHost)
|
|
129
|
+
return false;
|
|
130
|
+
return parseProxyUrl(proxyUrl)?.hostname.toLowerCase() === platformHost;
|
|
131
|
+
}
|
|
132
|
+
/** `scheme://***@host:port` — never log proxy credentials. */
|
|
133
|
+
function redactProxyUrl(proxyUrl) {
|
|
134
|
+
const parsed = parseProxyUrl(proxyUrl);
|
|
135
|
+
if (!parsed)
|
|
136
|
+
return '<unparseable proxy URL>';
|
|
137
|
+
const userinfo = parsed.username || parsed.password ? '***@' : '';
|
|
138
|
+
return `${parsed.protocol}//${userinfo}${parsed.host}`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Location from the gateway username grammar (`sub,groups-X,session-Y,country-US_CA`).
|
|
142
|
+
*/
|
|
143
|
+
function parseProxyUrlLocation(proxyUrl) {
|
|
144
|
+
if (!proxyUrl)
|
|
145
|
+
return {};
|
|
146
|
+
const parsed = parseProxyUrl(proxyUrl);
|
|
147
|
+
if (!parsed?.username)
|
|
148
|
+
return {};
|
|
149
|
+
let username;
|
|
150
|
+
try {
|
|
151
|
+
username = decodeURIComponent(parsed.username);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
username = parsed.username;
|
|
155
|
+
}
|
|
156
|
+
for (const fragment of username.split(',')) {
|
|
157
|
+
const match = /^country-([A-Za-z]{2})(?:_([A-Za-z0-9]{1,3}))?$/.exec(fragment.trim());
|
|
158
|
+
if (match)
|
|
159
|
+
return { country: match[1].toUpperCase(), subdivision: match[2]?.toUpperCase() };
|
|
160
|
+
}
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
163
|
+
/** Location requested by a platform-gateway proxyInfo; `{}` for none or BYOP. */
|
|
164
|
+
function resolveLaunchLocation(proxyInfo) {
|
|
165
|
+
const proxyUrl = proxyInfo?.url;
|
|
166
|
+
if (typeof proxyUrl !== 'string' || !proxyUrl)
|
|
167
|
+
return {};
|
|
168
|
+
if (!isPlatformProxyUrl(proxyUrl)) {
|
|
169
|
+
if (!byopWarned) {
|
|
170
|
+
byopWarned = true;
|
|
171
|
+
console.warn(`${BYOP_IGNORED_WARNING} (ignored: ${redactProxyUrl(proxyUrl)})`);
|
|
172
|
+
}
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
if ((proxyInfo?.groups?.length || proxyInfo?.sessionId != null) && !groupsInfoLogged) {
|
|
176
|
+
groupsInfoLogged = true;
|
|
177
|
+
console.info(GROUPS_SESSION_INFO);
|
|
178
|
+
}
|
|
179
|
+
const grammar = parseProxyUrlLocation(proxyUrl);
|
|
180
|
+
return {
|
|
181
|
+
country: proxyInfo?.countryCode ?? grammar.country,
|
|
182
|
+
subdivision: proxyInfo?.subdivisionCode ?? grammar.subdivision,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Build the remote browser CDP URL for one browser connection.
|
|
187
|
+
*
|
|
188
|
+
* The Scrapely remote browser never takes a proxy: it routes through its own
|
|
189
|
+
* integrated proxy, selected by location params. Semantics:
|
|
190
|
+
* - The worker-injected `proxy_country` is the run default; `proxy_url` is
|
|
191
|
+
* never sent and is removed if present.
|
|
192
|
+
* - A platform-gateway `proxyInfo` (crawler ProxyConfiguration) overrides
|
|
193
|
+
* `proxy_country` / `proxy_state` (ISO subdivision codes mapped to region
|
|
194
|
+
* names). A custom (BYOP) proxy is warned about once and ignored.
|
|
195
|
+
* - `browserConfiguration` params always win.
|
|
196
|
+
* - When nothing changes, the CDP URL is returned byte-identical.
|
|
197
|
+
*
|
|
198
|
+
* @internal Exported for tests.
|
|
199
|
+
*/
|
|
200
|
+
function applyProxyInfoToCdpUrl(cdpUrl, proxyInfo, browserConfiguration) {
|
|
201
|
+
if (browserConfiguration != null && !(0, browser_configuration_js_1.isBrowserConfiguration)(browserConfiguration)) {
|
|
202
|
+
throw new Error('browserConfiguration must be created with Actor.createBrowserConfiguration() or new BrowserConfiguration().');
|
|
203
|
+
}
|
|
204
|
+
const url = new URL(cdpUrl);
|
|
205
|
+
const params = url.searchParams;
|
|
206
|
+
let changed = false;
|
|
207
|
+
const setParam = (key, value) => {
|
|
208
|
+
if (params.get(key) === value)
|
|
209
|
+
return;
|
|
210
|
+
params.set(key, value);
|
|
211
|
+
changed = true;
|
|
212
|
+
};
|
|
213
|
+
if (params.has('proxy_url')) {
|
|
214
|
+
params.delete('proxy_url');
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
const location = resolveLaunchLocation(proxyInfo);
|
|
218
|
+
const country = location.country?.toUpperCase();
|
|
219
|
+
let state;
|
|
220
|
+
if (location.subdivision) {
|
|
221
|
+
state = (0, browser_locations_js_1.subdivisionToStateName)(country, location.subdivision);
|
|
222
|
+
if (!state && !unknownSubdivisionWarned) {
|
|
223
|
+
unknownSubdivisionWarned = true;
|
|
224
|
+
console.warn(`[scrapely] Subdivision "${country ?? '?'}-${location.subdivision}" has no Scrapely browser mapping; ` +
|
|
225
|
+
"routing by country only. Use browserConfiguration { state: '<name>' } to target a region.");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (country)
|
|
229
|
+
setParam('proxy_country', country);
|
|
230
|
+
if (state)
|
|
231
|
+
setParam('proxy_state', state);
|
|
232
|
+
if (browserConfiguration != null) {
|
|
233
|
+
for (const [key, value] of Object.entries(browserConfiguration.toCdpParams())) {
|
|
234
|
+
setParam(key, value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return changed ? url.toString() : cdpUrl;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @internal Normalize a browser-pool LaunchContext's proxy data into ProxyInfoLike.
|
|
241
|
+
*
|
|
242
|
+
* Crawlee's browser crawler sets both `launchContext.proxyInfo` (the ProxyInfo
|
|
243
|
+
* object, `.url` included) and `launchContext.proxyUrl` (the full proxy URL)
|
|
244
|
+
* per browser launch. The URL fallback covers callers that only provide the URL.
|
|
245
|
+
*/
|
|
246
|
+
function proxyInfoFromLaunchContext(launchContext) {
|
|
247
|
+
const info = launchContext?.proxyInfo;
|
|
248
|
+
if (typeof info?.url === 'string' && info.url) {
|
|
249
|
+
return {
|
|
250
|
+
url: info.url,
|
|
251
|
+
countryCode: info.countryCode,
|
|
252
|
+
subdivisionCode: info.subdivisionCode,
|
|
253
|
+
groups: info.groups,
|
|
254
|
+
sessionId: info.sessionId,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const proxyUrl = launchContext?.proxyUrl;
|
|
258
|
+
if (typeof proxyUrl === 'string' && proxyUrl)
|
|
259
|
+
return { url: proxyUrl };
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Resolve Playwright's chromium BrowserType for CDP connect.
|
|
264
|
+
*
|
|
265
|
+
* Crawlee's PlaywrightPlugin.library is a BrowserType (chromium|firefox|webkit).
|
|
266
|
+
* Remote browser is always Chromium CDP, so firefox/webkit/camoufox launchers
|
|
267
|
+
* are redirected to chromium.connectOverCDP — never denied, never local launch.
|
|
268
|
+
*/
|
|
269
|
+
function getPlaywrightChromium(library) {
|
|
270
|
+
// Full playwright module passed as library (has .chromium)
|
|
271
|
+
if (library?.chromium && typeof library.chromium.connectOverCDP === 'function') {
|
|
272
|
+
return library.chromium;
|
|
273
|
+
}
|
|
274
|
+
// BrowserType that is already chromium
|
|
275
|
+
if (typeof library?.connectOverCDP === 'function' &&
|
|
276
|
+
typeof library?.name === 'function' &&
|
|
277
|
+
library.name() === 'chromium') {
|
|
278
|
+
return library;
|
|
279
|
+
}
|
|
280
|
+
// firefox / webkit / unknown BrowserType → force chromium from playwright
|
|
281
|
+
// Prefer playwright, fall back to playwright-core (images may ship either).
|
|
282
|
+
try {
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
284
|
+
const pw = require('playwright');
|
|
285
|
+
if (pw?.chromium?.connectOverCDP) {
|
|
286
|
+
return pw.chromium;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// ignore
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
294
|
+
const pwCore = require('playwright-core');
|
|
295
|
+
if (pwCore?.chromium?.connectOverCDP) {
|
|
296
|
+
return pwCore.chromium;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// ignore
|
|
301
|
+
}
|
|
302
|
+
// Last resort: if library has connectOverCDP, use it (may be chromium-shaped)
|
|
303
|
+
if (typeof library?.connectOverCDP === 'function') {
|
|
304
|
+
return library;
|
|
305
|
+
}
|
|
306
|
+
throw new Error('Unable to resolve Playwright chromium for remote CDP connect. Install playwright or playwright-core.');
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Redirect a PlaywrightPlugin class's `_launch` to remote Chromium CDP
|
|
310
|
+
* (chromium / firefox / webkit / camoufox) on the platform; a normal local
|
|
311
|
+
* launch when running locally without CDP_URL. Idempotent per class.
|
|
312
|
+
*/
|
|
313
|
+
function patchPlaywrightPluginClass(PlaywrightPlugin) {
|
|
314
|
+
const proto = PlaywrightPlugin?.prototype;
|
|
315
|
+
if (!proto || typeof proto._launch !== 'function' || proto._scrapelyCdpPatched)
|
|
316
|
+
return;
|
|
317
|
+
const origPlaywrightLaunch = proto._launch;
|
|
318
|
+
proto._launch = async function (launchContext) {
|
|
319
|
+
if (browserPatchDisabled() || (!getCdpUrl() && !isAtHome())) {
|
|
320
|
+
return origPlaywrightLaunch.call(this, launchContext);
|
|
321
|
+
}
|
|
322
|
+
const cdpUrl = applyProxyInfoToCdpUrl(requireCdpUrl(), proxyInfoFromLaunchContext(launchContext), launchContext?.browserConfiguration);
|
|
323
|
+
const chromium = getPlaywrightChromium(this.library);
|
|
324
|
+
return chromium.connectOverCDP(cdpUrl);
|
|
325
|
+
};
|
|
326
|
+
proto._scrapelyCdpPatched = true;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Redirect a PuppeteerPlugin class's `_launch` to remote Chromium CDP via
|
|
330
|
+
* browserWSEndpoint on the platform; a normal local launch when running
|
|
331
|
+
* locally without CDP_URL. Idempotent per class.
|
|
332
|
+
*/
|
|
333
|
+
function patchPuppeteerPluginClass(PuppeteerPlugin) {
|
|
334
|
+
const proto = PuppeteerPlugin?.prototype;
|
|
335
|
+
if (!proto || typeof proto._launch !== 'function' || proto._scrapelyCdpPatched)
|
|
336
|
+
return;
|
|
337
|
+
const origPuppeteerLaunch = proto._launch;
|
|
338
|
+
proto._launch = async function (launchContext) {
|
|
339
|
+
if (browserPatchDisabled() || (!getCdpUrl() && !isAtHome())) {
|
|
340
|
+
return origPuppeteerLaunch.call(this, launchContext);
|
|
341
|
+
}
|
|
342
|
+
const cdpUrl = applyProxyInfoToCdpUrl(requireCdpUrl(), proxyInfoFromLaunchContext(launchContext), launchContext?.browserConfiguration);
|
|
343
|
+
const launcher = this.library;
|
|
344
|
+
if (!launcher || typeof launcher.connect !== 'function') {
|
|
345
|
+
throw new Error('Puppeteer library is missing connect(). Install puppeteer or puppeteer-core.');
|
|
346
|
+
}
|
|
347
|
+
return launcher.connect({ browserWSEndpoint: cdpUrl });
|
|
348
|
+
};
|
|
349
|
+
proto._scrapelyCdpPatched = true;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Patch the browser plugins exported by one loaded copy of
|
|
353
|
+
* `@crawlee/browser-pool` (or one of its plugin files). Used for the SDK's own
|
|
354
|
+
* copy (eager pass below) and for every actor-local copy (Module._load hook),
|
|
355
|
+
* since an actor that installs its own Crawlee resolves a different instance.
|
|
356
|
+
*
|
|
357
|
+
* @internal Exported for tests.
|
|
358
|
+
*/
|
|
359
|
+
function patchBrowserPoolModule(mod) {
|
|
360
|
+
if (!mod || (typeof mod !== 'object' && typeof mod !== 'function'))
|
|
361
|
+
return;
|
|
362
|
+
patchPlaywrightPluginClass(mod.PlaywrightPlugin);
|
|
363
|
+
patchPuppeteerPluginClass(mod.PuppeteerPlugin);
|
|
364
|
+
patchControllerProxyOptions(mod.PlaywrightController);
|
|
365
|
+
patchControllerProxyOptions(mod.PuppeteerController);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* With incognito pages Crawlee turns the launch proxy into per-context proxy
|
|
369
|
+
* options. On the remote browser that would route pages through a custom
|
|
370
|
+
* proxy (or double-proxy the gateway), so no per-page proxy is set while
|
|
371
|
+
* connected over CDP.
|
|
372
|
+
*/
|
|
373
|
+
function patchControllerProxyOptions(Controller) {
|
|
374
|
+
const proto = Controller?.prototype;
|
|
375
|
+
if (!proto || typeof proto.normalizeProxyOptions !== 'function' || proto._scrapelyProxyOptionsPatched)
|
|
376
|
+
return;
|
|
377
|
+
const origNormalize = proto.normalizeProxyOptions;
|
|
378
|
+
proto.normalizeProxyOptions = function (...args) {
|
|
379
|
+
if (!browserPatchDisabled() && getCdpUrl())
|
|
380
|
+
return {};
|
|
381
|
+
return origNormalize.apply(this, args);
|
|
382
|
+
};
|
|
383
|
+
proto._scrapelyProxyOptionsPatched = true;
|
|
384
|
+
}
|
|
385
|
+
// ─── Crawler `browserConfiguration` option ──────────────────────────────────
|
|
386
|
+
//
|
|
387
|
+
// Crawlee validates crawler options with ow.exactShape, and every crawler
|
|
388
|
+
// class copies its parent's optionsShape at module load, so the option is
|
|
389
|
+
// added to each class's own shape: BasicCrawler (browser crawlers forward
|
|
390
|
+
// unknown keys to it) and every browser crawler. HTTP crawlers never accept
|
|
391
|
+
// it — the key is removed from their (possibly copied) shapes so passing it
|
|
392
|
+
// there fails with Crawlee's normal unknown-option error.
|
|
393
|
+
//
|
|
394
|
+
// The predicate reuses the class's own `sessionPoolOptions` predicate
|
|
395
|
+
// (ow.optional.object): an ow predicate from a different ow copy is not
|
|
396
|
+
// recognized as a predicate by exactShape.
|
|
397
|
+
//
|
|
398
|
+
// Acceptance is never gated on CDP_URL / SCRAPELY_BROWSER_PATCH — only the
|
|
399
|
+
// `_launch` redirect is — so the same actor code runs locally unchanged.
|
|
400
|
+
function isCrawlerClass(value) {
|
|
401
|
+
return typeof value === 'function' && !!value.optionsShape && typeof value.optionsShape === 'object';
|
|
402
|
+
}
|
|
403
|
+
function patchCrawlerClass(CrawlerClass) {
|
|
404
|
+
const shape = CrawlerClass.optionsShape;
|
|
405
|
+
if (!Object.prototype.hasOwnProperty.call(CrawlerClass, 'optionsShape'))
|
|
406
|
+
return;
|
|
407
|
+
const isBrowserCrawler = 'launchContext' in shape;
|
|
408
|
+
const isBasicCrawler = CrawlerClass.name === 'BasicCrawler';
|
|
409
|
+
if (isBrowserCrawler || isBasicCrawler) {
|
|
410
|
+
if (!('browserConfiguration' in shape) && shape.sessionPoolOptions) {
|
|
411
|
+
shape.browserConfiguration = shape.sessionPoolOptions;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
else if ('browserConfiguration' in shape) {
|
|
415
|
+
delete shape.browserConfiguration;
|
|
416
|
+
}
|
|
417
|
+
const proto = CrawlerClass.prototype;
|
|
418
|
+
if (proto &&
|
|
419
|
+
Object.prototype.hasOwnProperty.call(proto, '_extendLaunchContext') &&
|
|
420
|
+
typeof proto._extendLaunchContext === 'function' &&
|
|
421
|
+
!proto._scrapelyBrowserConfigPatched) {
|
|
422
|
+
const origExtend = proto._extendLaunchContext;
|
|
423
|
+
proto._extendLaunchContext = async function (pageId, launchContext) {
|
|
424
|
+
await origExtend.call(this, pageId, launchContext);
|
|
425
|
+
const browserConfiguration = this.options?.browserConfiguration;
|
|
426
|
+
if (browserConfiguration != null && launchContext && launchContext.browserConfiguration === undefined) {
|
|
427
|
+
if (typeof launchContext.extend === 'function') {
|
|
428
|
+
launchContext.extend({ browserConfiguration });
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
launchContext.browserConfiguration = browserConfiguration;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
proto._scrapelyBrowserConfigPatched = true;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Add the `browserConfiguration` option to the crawler classes exported by
|
|
440
|
+
* one loaded Crawlee module (package index or crawler file). Idempotent.
|
|
441
|
+
*
|
|
442
|
+
* @internal Exported for tests.
|
|
443
|
+
*/
|
|
444
|
+
function patchCrawleeCrawlerModule(mod) {
|
|
445
|
+
if (!mod || (typeof mod !== 'object' && typeof mod !== 'function'))
|
|
446
|
+
return;
|
|
447
|
+
const candidates = isCrawlerClass(mod) ? [mod] : [];
|
|
448
|
+
for (const key of Object.keys(mod)) {
|
|
449
|
+
let value;
|
|
450
|
+
try {
|
|
451
|
+
value = mod[key];
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
if (isCrawlerClass(value))
|
|
457
|
+
candidates.push(value);
|
|
458
|
+
}
|
|
459
|
+
for (const CrawlerClass of candidates)
|
|
460
|
+
patchCrawlerClass(CrawlerClass);
|
|
461
|
+
}
|
|
462
|
+
const CRAWLER_PACKAGES = 'basic|browser|playwright|puppeteer|http|cheerio|jsdom|linkedom';
|
|
463
|
+
/** Requests that load (a part of) some copy of a Crawlee crawler package. */
|
|
464
|
+
const CRAWLER_REQUEST_RE = new RegExp(`^(@crawlee/(${CRAWLER_PACKAGES})|crawlee)$` +
|
|
465
|
+
`|[/\\\\]@crawlee[/\\\\](${CRAWLER_PACKAGES})[/\\\\]index\\.js$` +
|
|
466
|
+
`|[/\\\\]node_modules[/\\\\]crawlee[/\\\\]index\\.js$` +
|
|
467
|
+
'|(^|[/\\\\])internals[/\\\\][a-z-]+-crawler(\\.js)?$');
|
|
468
|
+
/** Cached module filenames of Crawlee crawler packages (any copy). */
|
|
469
|
+
const CRAWLER_FILENAME_RE = new RegExp(`[/\\\\](@crawlee[/\\\\](${CRAWLER_PACKAGES})|crawlee)[/\\\\](internals[/\\\\][a-z-]+-crawler\\.js|index\\.js)$`);
|
|
470
|
+
/**
|
|
471
|
+
* Patch Crawlee copies that loaded before this module (e.g. an actor that
|
|
472
|
+
* imports `crawlee` before `scrapely` in a local run). Copies loaded later are
|
|
473
|
+
* patched by the Module._load hook.
|
|
474
|
+
*
|
|
475
|
+
* @internal Exported for tests.
|
|
476
|
+
*/
|
|
477
|
+
function patchLoadedCrawleeCopies() {
|
|
478
|
+
const cache = require.cache ?? {};
|
|
479
|
+
for (const [filename, cached] of Object.entries(cache)) {
|
|
480
|
+
if (cached?.exports && CRAWLER_FILENAME_RE.test(filename)) {
|
|
481
|
+
patchCrawleeCrawlerModule(cached.exports);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
// Use Crawlee's ow (0.28.x CJS). Do not import top-level ow@1.x (ESM-only).
|
|
487
|
+
const owModule = require(require.resolve('ow', { paths: [require.resolve('@crawlee/browser')] }));
|
|
488
|
+
const ow = owModule.default ?? owModule;
|
|
489
|
+
// Playwright + Puppeteer plugins of the SDK's own @crawlee/browser-pool copy.
|
|
490
|
+
// Actor-local copies are patched by the Module._load hook further down.
|
|
491
|
+
try {
|
|
492
|
+
patchBrowserPoolModule(require('@crawlee/browser-pool'));
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
// @crawlee/browser-pool not installed — skip plugin patches
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
const launcherPath = require.resolve('@crawlee/playwright').replace(/[/\\][^/\\]+$/, '') +
|
|
499
|
+
'/internals/playwright-launcher.js';
|
|
500
|
+
const { PlaywrightLauncher } = require(launcherPath);
|
|
501
|
+
PlaywrightLauncher.optionsShape.cdpUrl = ow.optional.string;
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// @crawlee/playwright not installed — skip Playwright patches
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const puppeteerLauncherPath = require.resolve('@crawlee/puppeteer').replace(/[/\\][^/\\]+$/, '') +
|
|
508
|
+
'/internals/puppeteer-launcher.js';
|
|
509
|
+
const { PuppeteerLauncher } = require(puppeteerLauncherPath);
|
|
510
|
+
PuppeteerLauncher.optionsShape.cdpUrl = ow.optional.string;
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
// @crawlee/puppeteer not installed — plugin patch above is enough
|
|
514
|
+
}
|
|
515
|
+
// Patch @crawlee/browser BrowserLauncher when available
|
|
516
|
+
const browserPkg = require.resolve('@crawlee/browser');
|
|
517
|
+
const browserLauncherPath = browserPkg.replace(/[/\\][^/\\]+$/, '') + '/internals/browser-launcher.js';
|
|
518
|
+
const { BrowserLauncher } = require(browserLauncherPath);
|
|
519
|
+
BrowserLauncher.optionsShape.cdpUrl = ow.optional.string;
|
|
520
|
+
// Patch createLaunchOptions to inject gVisor-safe Chrome args for any residual
|
|
521
|
+
// local launch path. Playwright/Puppeteer pool launches use CDP and skip this.
|
|
522
|
+
// Only applies when SCRAPELY_CONTAINER_RUNTIME=runsc (gVisor sandbox).
|
|
523
|
+
const origCreateLaunchOptions = BrowserLauncher.prototype.createLaunchOptions;
|
|
524
|
+
BrowserLauncher.prototype.createLaunchOptions = function () {
|
|
525
|
+
const launchOptions = origCreateLaunchOptions.call(this);
|
|
526
|
+
// Inject crashpad-disabling args when running under gVisor
|
|
527
|
+
// gVisor doesn't support Chrome's crashpad_handler process
|
|
528
|
+
// Also needed with ReadonlyRootfs + tmpfs /tmp where /tmp/.X11-unix
|
|
529
|
+
// and crashpad database dir don't persist from the base image.
|
|
530
|
+
if (process.env.SCRAPELY_CONTAINER_RUNTIME === 'runsc') {
|
|
531
|
+
launchOptions.args = launchOptions.args || [];
|
|
532
|
+
for (const arg of GVISOR_CHROME_ARGS) {
|
|
533
|
+
if (!launchOptions.args.includes(arg)) {
|
|
534
|
+
launchOptions.args.push(arg);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return launchOptions;
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
// @crawlee/browser not installed — skip browser pool patches (e.g. cheerio-only actors)
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Split our `browserConfiguration` option off raw launch options. The options
|
|
546
|
+
* object is returned as-is when the key is absent.
|
|
547
|
+
*/
|
|
548
|
+
function takeBrowserConfiguration(options) {
|
|
549
|
+
if (!options || typeof options !== 'object' || !('browserConfiguration' in options)) {
|
|
550
|
+
return { options: options ?? {}, browserConfiguration: undefined };
|
|
551
|
+
}
|
|
552
|
+
const { browserConfiguration, ...rest } = options;
|
|
553
|
+
return { options: rest, browserConfiguration };
|
|
554
|
+
}
|
|
555
|
+
/** Options that only make sense for a local launch (no remote-CDP equivalent). */
|
|
556
|
+
const RAW_LAUNCH_ONLY_KEYS = new Set([
|
|
557
|
+
'headless',
|
|
558
|
+
'args',
|
|
559
|
+
'ignoreDefaultArgs',
|
|
560
|
+
'ignoreArgs',
|
|
561
|
+
'executablePath',
|
|
562
|
+
'channel',
|
|
563
|
+
'env',
|
|
564
|
+
'devtools',
|
|
565
|
+
'chromiumSandbox',
|
|
566
|
+
'firefoxUserPrefs',
|
|
567
|
+
'proxy',
|
|
568
|
+
'tracesDir',
|
|
569
|
+
'waitForExtensions',
|
|
570
|
+
'userDataDir',
|
|
571
|
+
'user_data_dir',
|
|
572
|
+
'downloadsPath',
|
|
573
|
+
]);
|
|
574
|
+
function composeRawProxyUrl(proxy) {
|
|
575
|
+
if (!proxy?.server)
|
|
576
|
+
return undefined;
|
|
577
|
+
if (proxy.username) {
|
|
578
|
+
try {
|
|
579
|
+
const url = new URL(proxy.server);
|
|
580
|
+
url.username = proxy.username;
|
|
581
|
+
url.password = proxy.password ?? '';
|
|
582
|
+
return url.toString();
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
return proxy.server;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return proxy.server;
|
|
589
|
+
}
|
|
590
|
+
function rawLaunchCdpUrl(options, browserConfiguration) {
|
|
591
|
+
const cdpUrl = getCdpUrl();
|
|
592
|
+
if (!cdpUrl)
|
|
593
|
+
return undefined;
|
|
594
|
+
const proxyUrl = composeRawProxyUrl(options?.proxy);
|
|
595
|
+
return applyProxyInfoToCdpUrl(cdpUrl, proxyUrl ? { url: proxyUrl } : undefined, browserConfiguration);
|
|
596
|
+
}
|
|
597
|
+
function rawConnectOptions(options) {
|
|
598
|
+
const out = {};
|
|
599
|
+
if (options.timeout !== undefined)
|
|
600
|
+
out.timeout = options.timeout;
|
|
601
|
+
if (options.slowMo !== undefined)
|
|
602
|
+
out.slowMo = options.slowMo;
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
function warnDroppedRawOptions(kind, name, options) {
|
|
606
|
+
const dropped = Object.keys(options).filter((k) => k !== 'timeout' && k !== 'slowMo');
|
|
607
|
+
if (dropped.length > 0) {
|
|
608
|
+
console.warn(`[scrapely] playwright ${name}.${kind} redirected to the remote CDP browser; ` +
|
|
609
|
+
`local launch options ignored: ${dropped.join(', ')} ` +
|
|
610
|
+
'(the remote browser manages headless mode, binary and flags).');
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
/** @internal Exported for tests. */
|
|
614
|
+
function patchRawBrowserType(chromium, browserType, name) {
|
|
615
|
+
if (!browserType || typeof browserType.launch !== 'function')
|
|
616
|
+
return;
|
|
617
|
+
if (browserType._scrapelyRawCdpPatched)
|
|
618
|
+
return;
|
|
619
|
+
const origLaunch = browserType.launch;
|
|
620
|
+
const origLpc = browserType.launchPersistentContext;
|
|
621
|
+
browserType.launch = async function rawLaunchPatched(launchOptions = {}) {
|
|
622
|
+
const { options, browserConfiguration } = takeBrowserConfiguration(launchOptions);
|
|
623
|
+
if (browserPatchDisabled())
|
|
624
|
+
return origLaunch.call(this, withGVisorArgs(options));
|
|
625
|
+
const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
|
|
626
|
+
if (!cdpUrl) {
|
|
627
|
+
if (!isAtHome())
|
|
628
|
+
return origLaunch.call(this, options);
|
|
629
|
+
throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
|
|
630
|
+
}
|
|
631
|
+
warnDroppedRawOptions('launch', name, options);
|
|
632
|
+
return chromium.connectOverCDP(cdpUrl, rawConnectOptions(options));
|
|
633
|
+
};
|
|
634
|
+
if (typeof origLpc === 'function') {
|
|
635
|
+
browserType.launchPersistentContext = async function rawLpcPatched(...lpcArgs) {
|
|
636
|
+
// Playwright signature: launchPersistentContext(userDataDir, options);
|
|
637
|
+
// an options-only call is accepted too.
|
|
638
|
+
const optionsIndex = typeof lpcArgs[0] === 'string' ? 1 : 0;
|
|
639
|
+
const userDataDir = optionsIndex === 1 ? lpcArgs[0] : undefined;
|
|
640
|
+
const { options, browserConfiguration } = takeBrowserConfiguration(lpcArgs[optionsIndex]);
|
|
641
|
+
const withOptions = (opts) => optionsIndex === 1 ? [userDataDir, opts] : [opts];
|
|
642
|
+
if (browserPatchDisabled())
|
|
643
|
+
return origLpc.call(this, ...withOptions(withGVisorArgs(options)));
|
|
644
|
+
const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
|
|
645
|
+
if (!cdpUrl) {
|
|
646
|
+
if (!isAtHome()) {
|
|
647
|
+
return origLpc.call(this, ...withOptions(lpcArgs[optionsIndex] === undefined ? undefined : options));
|
|
648
|
+
}
|
|
649
|
+
throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
|
|
650
|
+
}
|
|
651
|
+
const profileDir = userDataDir ?? options.userDataDir;
|
|
652
|
+
if (profileDir) {
|
|
653
|
+
console.warn(`[scrapely] playwright ${name}.launchPersistentContext(userDataDir=${profileDir}) redirected to the ` +
|
|
654
|
+
'remote CDP browser — persistent profiles have no remote equivalent; a fresh context is returned.');
|
|
655
|
+
}
|
|
656
|
+
warnDroppedRawOptions('launchPersistentContext', name, options);
|
|
657
|
+
const browser = await chromium.connectOverCDP(cdpUrl, rawConnectOptions(options));
|
|
658
|
+
const contextOptions = {};
|
|
659
|
+
for (const [k, v] of Object.entries(options)) {
|
|
660
|
+
if (!RAW_LAUNCH_ONLY_KEYS.has(k) && k !== 'timeout' && k !== 'slowMo')
|
|
661
|
+
contextOptions[k] = v;
|
|
662
|
+
}
|
|
663
|
+
return browser.newContext(contextOptions);
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
browserType._scrapelyRawCdpPatched = true;
|
|
667
|
+
}
|
|
668
|
+
/** Patch the chromium/firefox/webkit BrowserTypes of a playwright module. */
|
|
669
|
+
function patchRawPlaywrightModule(pw) {
|
|
670
|
+
if (!pw?.chromium || typeof pw.chromium.connectOverCDP !== 'function')
|
|
671
|
+
return;
|
|
672
|
+
for (const name of ['chromium', 'firefox', 'webkit']) {
|
|
673
|
+
patchRawBrowserType(pw.chromium, pw[name], name);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Patch a raw puppeteer launcher: the default export (also exposed as module
|
|
678
|
+
* props). Both the full puppeteer package and puppeteer-core ship the same
|
|
679
|
+
* launcher shape (launch/connect) — patch whichever was loaded.
|
|
680
|
+
*/
|
|
681
|
+
function patchRawPuppeteerModule(mod, moduleName) {
|
|
682
|
+
const launcher = mod?.default ?? mod;
|
|
683
|
+
if (typeof launcher?.launch !== 'function' || launcher._scrapelyRawCdpPatched) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const origLaunch = launcher.launch.bind(launcher);
|
|
687
|
+
launcher.launch = async function rawPuppeteerLaunchPatched(launchOptions = {}) {
|
|
688
|
+
const { options, browserConfiguration } = takeBrowserConfiguration(launchOptions);
|
|
689
|
+
if (browserPatchDisabled())
|
|
690
|
+
return origLaunch(withGVisorArgs(options));
|
|
691
|
+
const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
|
|
692
|
+
if (!cdpUrl) {
|
|
693
|
+
if (!isAtHome())
|
|
694
|
+
return origLaunch(options);
|
|
695
|
+
throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
|
|
696
|
+
}
|
|
697
|
+
const dropped = Object.keys(options).filter((k) => k !== 'timeout' && k !== 'slowMo' && k !== 'protocol');
|
|
698
|
+
if (dropped.length > 0) {
|
|
699
|
+
console.warn(`[scrapely] ${moduleName}.launch redirected to the remote CDP browser; ` +
|
|
700
|
+
`local launch options ignored: ${dropped.join(', ')}.`);
|
|
701
|
+
}
|
|
702
|
+
return launcher.connect({ browserWSEndpoint: cdpUrl, ...rawConnectOptions(options) });
|
|
703
|
+
};
|
|
704
|
+
launcher._scrapelyRawCdpPatched = true;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Eager pass: patch whatever playwright/puppeteer is resolvable from this
|
|
708
|
+
* module's own dependency tree right now (the SDK tree under
|
|
709
|
+
* /opt/scrapely/node_modules in the base image). Actor-local installs are
|
|
710
|
+
* covered by the Module._load hook below — a bare require() from this file
|
|
711
|
+
* can never resolve them.
|
|
712
|
+
*/
|
|
713
|
+
function patchSdkTreeRawModules() {
|
|
714
|
+
for (const pwModuleName of ['playwright', 'playwright-core']) {
|
|
715
|
+
try {
|
|
716
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
717
|
+
patchRawPlaywrightModule(require(pwModuleName));
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
// not installed in the SDK tree — actor-local copies still get the hook
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
// Raw puppeteer API: default export launcher (also exposed as module props).
|
|
724
|
+
// Both the full puppeteer package and puppeteer-core ship the same launcher
|
|
725
|
+
// shape (launch/connect) — patch whichever is installed.
|
|
726
|
+
for (const puppeteerModuleName of ['puppeteer', 'puppeteer-core']) {
|
|
727
|
+
try {
|
|
728
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
729
|
+
patchRawPuppeteerModule(require(puppeteerModuleName), puppeteerModuleName);
|
|
730
|
+
}
|
|
731
|
+
catch {
|
|
732
|
+
// not installed in the SDK tree — actor-local copies still get the hook
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Apply the raw patches to a module coming back from the hooked loader.
|
|
738
|
+
* `request` is the specifier as written by the caller, `loaded` is whatever
|
|
739
|
+
* instance it actually resolved to, and `loadRoot` resolves a package root
|
|
740
|
+
* through the original loader (no hook recursion) for subpath requests.
|
|
741
|
+
*/
|
|
742
|
+
/**
|
|
743
|
+
* Requests that load (a part of) some copy of @crawlee/browser-pool:
|
|
744
|
+
* - the bare package name (require from @crawlee/playwright etc.),
|
|
745
|
+
* - an absolute path to its index.js (ESM → CJS interop loads by filename),
|
|
746
|
+
* - the relative plugin files the package's own index.js requires.
|
|
747
|
+
*/
|
|
748
|
+
const BROWSER_POOL_REQUEST_RE = /^@crawlee\/browser-pool$|[/\\]@crawlee[/\\]browser-pool[/\\]index\.js$|(^|[/\\])(playwright|puppeteer)-plugin(\.js)?$/;
|
|
749
|
+
function patchLoadedRawModule(request, loaded, loadRoot) {
|
|
750
|
+
if (typeof request !== 'string')
|
|
751
|
+
return;
|
|
752
|
+
// Crawlee browser pool — the actor's own copy (e.g. /usr/src/app/node_modules)
|
|
753
|
+
// is a different instance from the SDK's, so its plugins need the CDP patch too.
|
|
754
|
+
if (BROWSER_POOL_REQUEST_RE.test(request)) {
|
|
755
|
+
patchBrowserPoolModule(loaded);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (CRAWLER_REQUEST_RE.test(request)) {
|
|
759
|
+
patchCrawleeCrawlerModule(loaded);
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (request === 'playwright' || request === 'playwright-core') {
|
|
763
|
+
patchRawPlaywrightModule(loaded);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (request === 'puppeteer' || request === 'puppeteer-core') {
|
|
767
|
+
patchRawPuppeteerModule(loaded, request);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
// BrowserType subpath loads, e.g. require('playwright/chromium') — the
|
|
771
|
+
// export is the BrowserType itself, not the package root. Resolve the root
|
|
772
|
+
// for its chromium (the connectOverCDP anchor), patch the root, then the
|
|
773
|
+
// subpath export. The launch() shape check ignores deep imports into the
|
|
774
|
+
// package that are not BrowserTypes.
|
|
775
|
+
if (!request.startsWith('playwright/') && !request.startsWith('playwright-core/'))
|
|
776
|
+
return;
|
|
777
|
+
const subpath = /^(playwright|playwright-core)\/(.+)$/.exec(request);
|
|
778
|
+
if (subpath && typeof loaded?.launch === 'function') {
|
|
779
|
+
try {
|
|
780
|
+
const root = loadRoot(subpath[1]);
|
|
781
|
+
patchRawPlaywrightModule(root);
|
|
782
|
+
if (root?.chromium)
|
|
783
|
+
patchRawBrowserType(root.chromium, loaded, subpath[2]);
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// package root unresolvable — no chromium anchor to connect through
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Hook the CommonJS loader so the raw patches reach actor-local installs.
|
|
792
|
+
*
|
|
793
|
+
* The eager pass above can only see this module's own dependency tree; an
|
|
794
|
+
* actor bundling playwright/puppeteer in its workdir resolves a different,
|
|
795
|
+
* unpatched instance. Module._load is the chokepoint every require() passes
|
|
796
|
+
* through AFTER resolution — including CJS packages imported from ESM, which
|
|
797
|
+
* Node funnels through the CJS loader — so patching here covers whichever
|
|
798
|
+
* copy (SDK tree or actor workdir) the request actually resolved to. Chained
|
|
799
|
+
* onto the current _load (composes with tsx's hook in dev) and idempotent.
|
|
800
|
+
*/
|
|
801
|
+
function installRawModuleLoadHook() {
|
|
802
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
803
|
+
const Module = require('module');
|
|
804
|
+
const origLoad = Module._load;
|
|
805
|
+
if (typeof origLoad !== 'function' || origLoad._scrapelyRawCdpHook)
|
|
806
|
+
return;
|
|
807
|
+
const hookedLoad = function (request, parent, isMain) {
|
|
808
|
+
const loaded = origLoad.apply(this, [request, parent, isMain]);
|
|
809
|
+
try {
|
|
810
|
+
patchLoadedRawModule(request, loaded, (name) => origLoad(name, parent, isMain));
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
// Never break the actor's module loading — warn and hand back the
|
|
814
|
+
// unpatched module rather than crashing the process.
|
|
815
|
+
console.warn(`[scrapely] CDP patch failed for '${String(request)}':`, err?.message);
|
|
816
|
+
}
|
|
817
|
+
return loaded;
|
|
818
|
+
};
|
|
819
|
+
hookedLoad._scrapelyRawCdpHook = true;
|
|
820
|
+
Module._load = hookedLoad;
|
|
821
|
+
}
|
|
822
|
+
function applyRawBrowserPatches() {
|
|
823
|
+
patchSdkTreeRawModules();
|
|
824
|
+
installRawModuleLoadHook();
|
|
825
|
+
patchLoadedCrawleeCopies();
|
|
826
|
+
}
|
|
827
|
+
applyRawBrowserPatches();
|