scrapely 1.2.26 → 1.2.28
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/dist/actor.d.ts +14 -2
- package/dist/actor.js +58 -17
- package/dist/charging.d.ts +7 -2
- package/dist/charging.js +16 -4
- package/dist/charging_client.d.ts +1 -3
- package/dist/charging_client.js +3 -5
- package/dist/client-loader.d.ts +20 -0
- package/dist/client-loader.js +42 -0
- package/dist/esm-loader-hooks.d.mts +20 -0
- package/dist/esm-loader-hooks.mjs +21 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +33 -12
- package/dist/index.js +7 -29
- package/dist/index.mjs +39 -0
- package/dist/lazy-exports.d.ts +14 -0
- package/dist/lazy-exports.js +132 -0
- package/dist/node-wrapper.d.ts +58 -0
- package/dist/node-wrapper.js +270 -0
- package/dist/patch-browser-pool.d.ts +40 -0
- package/dist/patch-browser-pool.js +349 -114
- package/dist/proxy-configuration.js +1 -1
- package/package.json +16 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PUPPETEER_NODE_ESM_SUFFIX = exports.PUPPETEER_NODE_ESM_URL_RE = exports.PUPPETEER_NODE_PATCH_GLOBAL = void 0;
|
|
3
4
|
exports.setCdpUrl = setCdpUrl;
|
|
4
5
|
exports.getCdpUrlForTests = getCdpUrlForTests;
|
|
5
6
|
exports.resetBrowserProxyWarningsForTests = resetBrowserProxyWarningsForTests;
|
|
@@ -7,7 +8,11 @@ exports.applyProxyInfoToCdpUrl = applyProxyInfoToCdpUrl;
|
|
|
7
8
|
exports.patchBrowserPoolModule = patchBrowserPoolModule;
|
|
8
9
|
exports.patchCrawleeCrawlerModule = patchCrawleeCrawlerModule;
|
|
9
10
|
exports.patchLoadedCrawleeCopies = patchLoadedCrawleeCopies;
|
|
11
|
+
exports.patchBrowserLauncherModule = patchBrowserLauncherModule;
|
|
10
12
|
exports.patchRawBrowserType = patchRawBrowserType;
|
|
13
|
+
exports.patchPuppeteerNodeClass = patchPuppeteerNodeClass;
|
|
14
|
+
exports.patchLoadedModules = patchLoadedModules;
|
|
15
|
+
exports.esmPuppeteerInstalled = esmPuppeteerInstalled;
|
|
11
16
|
/**
|
|
12
17
|
* Redirects every browser launch (Crawlee browser pool, raw playwright /
|
|
13
18
|
* puppeteer) to the Scrapely remote browser over CDP. The remote browser
|
|
@@ -19,6 +24,7 @@ exports.patchRawBrowserType = patchRawBrowserType;
|
|
|
19
24
|
const browser_configuration_js_1 = require("./browser-configuration.js");
|
|
20
25
|
const browser_locations_js_1 = require("./browser-locations.js");
|
|
21
26
|
const cdp_retry_js_1 = require("./cdp-retry.js");
|
|
27
|
+
const node_wrapper_js_1 = require("./node-wrapper.js");
|
|
22
28
|
let _cdpUrl;
|
|
23
29
|
/**
|
|
24
30
|
* Lazily get CDP URL from environment or cached value.
|
|
@@ -485,52 +491,79 @@ function patchLoadedCrawleeCopies() {
|
|
|
485
491
|
}
|
|
486
492
|
}
|
|
487
493
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
494
|
+
// ─── Crawlee launchers: `cdpUrl` option + gVisor-safe local launch args ─────
|
|
495
|
+
//
|
|
496
|
+
// Applied when a launcher module loads (Module._load hook below), never by
|
|
497
|
+
// importing Crawlee up front: loading @crawlee/browser-pool and playwright
|
|
498
|
+
// eagerly cost ~2.4 s per Node process under gVisor.
|
|
499
|
+
/** Requests that load a Crawlee launcher file (`require('./internals/browser-launcher')`). */
|
|
500
|
+
const LAUNCHER_REQUEST_RE = /(^|[/\\])internals[/\\](browser|playwright|puppeteer)-launcher(\.js)?$/;
|
|
501
|
+
/** Cached module filenames of Crawlee launcher files (any copy). */
|
|
502
|
+
const LAUNCHER_FILENAME_RE = /[/\\]@crawlee[/\\](browser|playwright|puppeteer)[/\\]internals[/\\](browser|playwright|puppeteer)-launcher\.js$/;
|
|
503
|
+
/**
|
|
504
|
+
* An `ow.optional.string` predicate from the same ow copy as the launcher's
|
|
505
|
+
* own shape (a predicate from another ow copy is not recognized by
|
|
506
|
+
* exactShape). Reuses an existing optional-string entry, else loads ow next
|
|
507
|
+
* to the module that required the launcher.
|
|
508
|
+
*/
|
|
509
|
+
function optionalStringPredicate(shape, parentFilename) {
|
|
510
|
+
if (shape.userAgent)
|
|
511
|
+
return shape.userAgent;
|
|
512
|
+
if (shape.userDataDir)
|
|
513
|
+
return shape.userDataDir;
|
|
514
|
+
if (!parentFilename)
|
|
515
|
+
return undefined;
|
|
516
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
517
|
+
const { createRequire } = require('module');
|
|
518
|
+
const owModule = createRequire(parentFilename)('ow');
|
|
519
|
+
return (owModule.default ?? owModule).optional.string;
|
|
520
|
+
}
|
|
521
|
+
function addCdpUrlOption(Launcher, parentFilename) {
|
|
522
|
+
const shape = Launcher?.optionsShape;
|
|
523
|
+
if (!shape || typeof shape !== 'object' || 'cdpUrl' in shape)
|
|
524
|
+
return;
|
|
525
|
+
const predicate = optionalStringPredicate(shape, parentFilename);
|
|
526
|
+
if (predicate)
|
|
527
|
+
shape.cdpUrl = predicate;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Patch the launcher classes exported by one loaded Crawlee launcher file (or
|
|
531
|
+
* package index). Idempotent.
|
|
532
|
+
*
|
|
533
|
+
* - `cdpUrl` becomes a valid launcher option (PlaywrightLauncher and
|
|
534
|
+
* PuppeteerLauncher copy BrowserLauncher's shape when their file loads, so
|
|
535
|
+
* each class gets it; browser-launcher.js loads first, so the copies inherit it).
|
|
536
|
+
* - Local launches get gVisor-safe Chrome args under runsc. Playwright/Puppeteer
|
|
537
|
+
* pool launches use CDP and skip this.
|
|
538
|
+
*
|
|
539
|
+
* @internal Exported for tests.
|
|
540
|
+
*/
|
|
541
|
+
function patchBrowserLauncherModule(mod, parentFilename) {
|
|
542
|
+
if (!mod || (typeof mod !== 'object' && typeof mod !== 'function'))
|
|
543
|
+
return;
|
|
544
|
+
for (const key of ['BrowserLauncher', 'PlaywrightLauncher', 'PuppeteerLauncher']) {
|
|
545
|
+
let Launcher;
|
|
546
|
+
try {
|
|
547
|
+
Launcher = mod[key];
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (typeof Launcher !== 'function')
|
|
553
|
+
continue;
|
|
554
|
+
addCdpUrlOption(Launcher, parentFilename);
|
|
514
555
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const origCreateLaunchOptions = BrowserLauncher.prototype.createLaunchOptions;
|
|
527
|
-
BrowserLauncher.prototype.createLaunchOptions = function () {
|
|
528
|
-
const launchOptions = origCreateLaunchOptions.call(this);
|
|
529
|
-
// Inject crashpad-disabling args when running under gVisor
|
|
530
|
-
// gVisor doesn't support Chrome's crashpad_handler process
|
|
531
|
-
// Also needed with ReadonlyRootfs + tmpfs /tmp where /tmp/.X11-unix
|
|
532
|
-
// and crashpad database dir don't persist from the base image.
|
|
533
|
-
if (process.env.SCRAPELY_CONTAINER_RUNTIME === 'runsc') {
|
|
556
|
+
const BrowserLauncher = mod.BrowserLauncher;
|
|
557
|
+
const proto = BrowserLauncher?.prototype;
|
|
558
|
+
if (!proto || typeof proto.createLaunchOptions !== 'function' || proto._scrapelyGVisorArgsPatched)
|
|
559
|
+
return;
|
|
560
|
+
const origCreateLaunchOptions = proto.createLaunchOptions;
|
|
561
|
+
proto.createLaunchOptions = function (...args) {
|
|
562
|
+
const launchOptions = origCreateLaunchOptions.apply(this, args);
|
|
563
|
+
// gVisor doesn't support Chrome's crashpad_handler process. Also needed
|
|
564
|
+
// with ReadonlyRootfs + tmpfs /tmp where /tmp/.X11-unix and the crashpad
|
|
565
|
+
// database dir don't persist from the base image.
|
|
566
|
+
if (process.env.SCRAPELY_CONTAINER_RUNTIME === 'runsc' && launchOptions) {
|
|
534
567
|
launchOptions.args = launchOptions.args || [];
|
|
535
568
|
for (const arg of GVISOR_CHROME_ARGS) {
|
|
536
569
|
if (!launchOptions.args.includes(arg)) {
|
|
@@ -540,9 +573,7 @@ try {
|
|
|
540
573
|
}
|
|
541
574
|
return launchOptions;
|
|
542
575
|
};
|
|
543
|
-
|
|
544
|
-
catch {
|
|
545
|
-
// @crawlee/browser not installed — skip browser pool patches (e.g. cheerio-only actors)
|
|
576
|
+
proto._scrapelyGVisorArgsPatched = true;
|
|
546
577
|
}
|
|
547
578
|
/**
|
|
548
579
|
* Split our `browserConfiguration` option off raw launch options. The options
|
|
@@ -681,72 +712,87 @@ function patchRawPlaywrightModule(pw) {
|
|
|
681
712
|
}
|
|
682
713
|
}
|
|
683
714
|
/**
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
715
|
+
* The patched puppeteer `launch`: remote CDP connect on the platform, the
|
|
716
|
+
* original local launch otherwise. `connect` is the launcher's (retry-wrapped)
|
|
717
|
+
* connect; `origLaunch` the unpatched launch, both called with `self`.
|
|
718
|
+
*/
|
|
719
|
+
async function puppeteerLaunchPatched(self, origLaunch, moduleName, launchOptions = {}) {
|
|
720
|
+
const { options, browserConfiguration } = takeBrowserConfiguration(launchOptions);
|
|
721
|
+
if (browserPatchDisabled())
|
|
722
|
+
return origLaunch.call(self, withGVisorArgs(options));
|
|
723
|
+
const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
|
|
724
|
+
if (!cdpUrl) {
|
|
725
|
+
if (!isAtHome())
|
|
726
|
+
return origLaunch.call(self, options);
|
|
727
|
+
throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
|
|
728
|
+
}
|
|
729
|
+
const dropped = Object.keys(options).filter((k) => k !== 'timeout' && k !== 'slowMo' && k !== 'protocol');
|
|
730
|
+
if (dropped.length > 0) {
|
|
731
|
+
console.warn(`[scrapely] ${moduleName}.launch redirected to the remote CDP browser; ` +
|
|
732
|
+
`local launch options ignored: ${dropped.join(', ')}.`);
|
|
733
|
+
}
|
|
734
|
+
return self.connect({ browserWSEndpoint: cdpUrl, ...rawConnectOptions(options) });
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Patch puppeteer's PuppeteerNode class (puppeteer-core `node/PuppeteerNode.js`,
|
|
738
|
+
* shared by puppeteer and puppeteer-core, CJS and ESM builds). The package
|
|
739
|
+
* entry creates its default instance right after this class loads, and the
|
|
740
|
+
* constructor binds `launch`/`connect` from the prototype, so patching the
|
|
741
|
+
* prototype first also covers the named exports (`import { launch } from
|
|
742
|
+
* 'puppeteer'`), which are those bound methods. Idempotent per class.
|
|
743
|
+
*
|
|
744
|
+
* @internal Exported for tests.
|
|
745
|
+
*/
|
|
746
|
+
function patchPuppeteerNodeClass(PuppeteerNode) {
|
|
747
|
+
const proto = PuppeteerNode?.prototype;
|
|
748
|
+
if (!proto || typeof proto.launch !== 'function')
|
|
749
|
+
return;
|
|
750
|
+
if (Object.prototype.hasOwnProperty.call(proto, '_scrapelyRawCdpPatched'))
|
|
751
|
+
return;
|
|
752
|
+
(0, cdp_retry_js_1.wrapConnectWithRetry)(proto, 'connect', getCdpUrl);
|
|
753
|
+
const origLaunch = proto.launch;
|
|
754
|
+
proto.launch = function rawPuppeteerLaunchPatched(launchOptions) {
|
|
755
|
+
const moduleName = this?._isPuppeteerCore === false ? 'puppeteer' : 'puppeteer-core';
|
|
756
|
+
return puppeteerLaunchPatched(this, origLaunch, moduleName, launchOptions);
|
|
757
|
+
};
|
|
758
|
+
proto._scrapelyRawCdpPatched = true;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Patch a raw puppeteer launcher instance: the default export (also exposed as
|
|
762
|
+
* module props). Covers puppeteer copies whose instance already exists (loaded
|
|
763
|
+
* before this module) or whose class could not be patched first. Skipped when
|
|
764
|
+
* the instance's class is already patched.
|
|
687
765
|
*/
|
|
688
766
|
function patchRawPuppeteerModule(mod, moduleName) {
|
|
689
767
|
const launcher = mod?.default ?? mod;
|
|
690
768
|
if (typeof launcher?.launch !== 'function' || launcher._scrapelyRawCdpPatched) {
|
|
691
769
|
return;
|
|
692
770
|
}
|
|
771
|
+
const origBoundLaunch = launcher.launch;
|
|
772
|
+
const origBoundConnect = launcher.connect;
|
|
693
773
|
(0, cdp_retry_js_1.wrapConnectWithRetry)(launcher, 'connect', getCdpUrl);
|
|
694
774
|
const origLaunch = launcher.launch.bind(launcher);
|
|
695
775
|
launcher.launch = async function rawPuppeteerLaunchPatched(launchOptions = {}) {
|
|
696
|
-
|
|
697
|
-
if (browserPatchDisabled())
|
|
698
|
-
return origLaunch(withGVisorArgs(options));
|
|
699
|
-
const cdpUrl = rawLaunchCdpUrl(options, browserConfiguration);
|
|
700
|
-
if (!cdpUrl) {
|
|
701
|
-
if (!isAtHome())
|
|
702
|
-
return origLaunch(options);
|
|
703
|
-
throw new Error('cdpUrl is required. Local browser launch is disabled — all runs must use the remote browser.');
|
|
704
|
-
}
|
|
705
|
-
const dropped = Object.keys(options).filter((k) => k !== 'timeout' && k !== 'slowMo' && k !== 'protocol');
|
|
706
|
-
if (dropped.length > 0) {
|
|
707
|
-
console.warn(`[scrapely] ${moduleName}.launch redirected to the remote CDP browser; ` +
|
|
708
|
-
`local launch options ignored: ${dropped.join(', ')}.`);
|
|
709
|
-
}
|
|
710
|
-
return launcher.connect({ browserWSEndpoint: cdpUrl, ...rawConnectOptions(options) });
|
|
776
|
+
return puppeteerLaunchPatched(launcher, origLaunch, moduleName, launchOptions);
|
|
711
777
|
};
|
|
712
778
|
launcher._scrapelyRawCdpPatched = true;
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
catch {
|
|
728
|
-
// not installed in the SDK tree — actor-local copies still get the hook
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
// Raw puppeteer API: default export launcher (also exposed as module props).
|
|
732
|
-
// Both the full puppeteer package and puppeteer-core ship the same launcher
|
|
733
|
-
// shape (launch/connect) — patch whichever is installed.
|
|
734
|
-
for (const puppeteerModuleName of ['puppeteer', 'puppeteer-core']) {
|
|
735
|
-
try {
|
|
736
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
737
|
-
patchRawPuppeteerModule(require(puppeteerModuleName), puppeteerModuleName);
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
// not installed in the SDK tree — actor-local copies still get the hook
|
|
779
|
+
// Named exports (`exports.launch = puppeteer.launch`) hold the original
|
|
780
|
+
// bound methods; point them at the patched ones.
|
|
781
|
+
if (mod && mod !== launcher) {
|
|
782
|
+
for (const [key, original] of [
|
|
783
|
+
['launch', origBoundLaunch],
|
|
784
|
+
['connect', origBoundConnect],
|
|
785
|
+
]) {
|
|
786
|
+
try {
|
|
787
|
+
if (mod[key] === original && mod[key] !== launcher[key])
|
|
788
|
+
mod[key] = launcher[key];
|
|
789
|
+
}
|
|
790
|
+
catch {
|
|
791
|
+
// read-only export binding — the default export is still patched
|
|
792
|
+
}
|
|
741
793
|
}
|
|
742
794
|
}
|
|
743
795
|
}
|
|
744
|
-
/**
|
|
745
|
-
* Apply the raw patches to a module coming back from the hooked loader.
|
|
746
|
-
* `request` is the specifier as written by the caller, `loaded` is whatever
|
|
747
|
-
* instance it actually resolved to, and `loadRoot` resolves a package root
|
|
748
|
-
* through the original loader (no hook recursion) for subpath requests.
|
|
749
|
-
*/
|
|
750
796
|
/**
|
|
751
797
|
* Requests that load (a part of) some copy of @crawlee/browser-pool:
|
|
752
798
|
* - the bare package name (require from @crawlee/playwright etc.),
|
|
@@ -754,7 +800,22 @@ function patchSdkTreeRawModules() {
|
|
|
754
800
|
* - the relative plugin files the package's own index.js requires.
|
|
755
801
|
*/
|
|
756
802
|
const BROWSER_POOL_REQUEST_RE = /^@crawlee\/browser-pool$|[/\\]@crawlee[/\\]browser-pool[/\\]index\.js$|(^|[/\\])(playwright|puppeteer)-plugin(\.js)?$/;
|
|
757
|
-
|
|
803
|
+
/** Cached module filenames of @crawlee/browser-pool (index or plugin files, any copy). */
|
|
804
|
+
const BROWSER_POOL_FILENAME_RE = /[/\\]@crawlee[/\\]browser-pool[/\\](index|playwright[/\\]playwright-plugin|puppeteer[/\\]puppeteer-plugin)\.js$/;
|
|
805
|
+
/** playwright / playwright-core package entry, by filename (ESM imports and require.cache). */
|
|
806
|
+
const PLAYWRIGHT_ENTRY_FILENAME_RE = /[/\\]node_modules[/\\](playwright|playwright-core)[/\\]index\.js$/;
|
|
807
|
+
/** puppeteer / puppeteer-core CJS package entry (default instance), by filename. */
|
|
808
|
+
const PUPPETEER_ENTRY_FILENAME_RE = /[/\\]node_modules[/\\](puppeteer|puppeteer-core)[/\\]lib[/\\]cjs[/\\]puppeteer[/\\](puppeteer|puppeteer-core)\.js$/;
|
|
809
|
+
/** puppeteer-core's PuppeteerNode class file (CJS), as requested by node/node.js or cached. */
|
|
810
|
+
const PUPPETEER_NODE_REQUEST_RE = /(^|[/\\])PuppeteerNode(\.js)?$/;
|
|
811
|
+
/**
|
|
812
|
+
* Apply the patches to a module coming back from the hooked loader.
|
|
813
|
+
* `request` is the specifier as written by the caller (an absolute filename
|
|
814
|
+
* when ESM imports a CJS package), `loaded` is whatever instance it actually
|
|
815
|
+
* resolved to, and `loadRoot` resolves a package root through the original
|
|
816
|
+
* loader (no hook recursion) for subpath requests.
|
|
817
|
+
*/
|
|
818
|
+
function patchLoadedRawModule(request, loaded, loadRoot, parentFilename) {
|
|
758
819
|
if (typeof request !== 'string')
|
|
759
820
|
return;
|
|
760
821
|
// Crawlee browser pool — the actor's own copy (e.g. /usr/src/app/node_modules)
|
|
@@ -763,18 +824,31 @@ function patchLoadedRawModule(request, loaded, loadRoot) {
|
|
|
763
824
|
patchBrowserPoolModule(loaded);
|
|
764
825
|
return;
|
|
765
826
|
}
|
|
827
|
+
if (LAUNCHER_REQUEST_RE.test(request)) {
|
|
828
|
+
patchBrowserLauncherModule(loaded, parentFilename);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
766
831
|
if (CRAWLER_REQUEST_RE.test(request)) {
|
|
767
832
|
patchCrawleeCrawlerModule(loaded);
|
|
768
833
|
return;
|
|
769
834
|
}
|
|
770
|
-
if (request === 'playwright' || request === 'playwright-core') {
|
|
835
|
+
if (request === 'playwright' || request === 'playwright-core' || PLAYWRIGHT_ENTRY_FILENAME_RE.test(request)) {
|
|
771
836
|
patchRawPlaywrightModule(loaded);
|
|
772
837
|
return;
|
|
773
838
|
}
|
|
839
|
+
if (PUPPETEER_NODE_REQUEST_RE.test(request)) {
|
|
840
|
+
patchPuppeteerNodeClass(loaded?.PuppeteerNode);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
774
843
|
if (request === 'puppeteer' || request === 'puppeteer-core') {
|
|
775
844
|
patchRawPuppeteerModule(loaded, request);
|
|
776
845
|
return;
|
|
777
846
|
}
|
|
847
|
+
const puppeteerEntry = PUPPETEER_ENTRY_FILENAME_RE.exec(request);
|
|
848
|
+
if (puppeteerEntry) {
|
|
849
|
+
patchRawPuppeteerModule(loaded, puppeteerEntry[1]);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
778
852
|
// BrowserType subpath loads, e.g. require('playwright/chromium') — the
|
|
779
853
|
// export is the BrowserType itself, not the package root. Resolve the root
|
|
780
854
|
// for its chromium (the connectOverCDP anchor), patch the root, then the
|
|
@@ -796,15 +870,48 @@ function patchLoadedRawModule(request, loaded, loadRoot) {
|
|
|
796
870
|
}
|
|
797
871
|
}
|
|
798
872
|
/**
|
|
799
|
-
*
|
|
873
|
+
* `require('puppeteer')` of an ESM-only puppeteer (v25+) on Node without
|
|
874
|
+
* module.registerHooks (Node 20, 22.0-22.14): require(esm) bypasses both
|
|
875
|
+
* Module._load for its inner modules and the off-thread ESM hook, and the
|
|
876
|
+
* returned namespace is frozen, so the named `launch` export would keep the
|
|
877
|
+
* original. Load and patch the PuppeteerNode class first (same ESM module
|
|
878
|
+
* instance the entry imports), so the entry's default instance binds the
|
|
879
|
+
* patched methods. Older CJS builds are covered by the regular hook anyway.
|
|
880
|
+
*/
|
|
881
|
+
const puppeteerEntriesPreloaded = new Set();
|
|
882
|
+
function preloadPuppeteerNodeClass(request, parent, origLoad) {
|
|
883
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
884
|
+
const Module = require('module');
|
|
885
|
+
if (typeof Module.registerHooks === 'function')
|
|
886
|
+
return;
|
|
887
|
+
const parentDir = typeof parent?.path === 'string' ? parent.path : '';
|
|
888
|
+
const key = `${parentDir}\0${request}`;
|
|
889
|
+
if (puppeteerEntriesPreloaded.has(key))
|
|
890
|
+
return;
|
|
891
|
+
puppeteerEntriesPreloaded.add(key);
|
|
892
|
+
try {
|
|
893
|
+
// puppeteer's own puppeteer-core (may be nested under puppeteer).
|
|
894
|
+
const entry = Module._resolveFilename(request, parent, false);
|
|
895
|
+
const coreRequest = 'puppeteer-core/internal/node/PuppeteerNode.js';
|
|
896
|
+
const corePath = Module.createRequire(entry).resolve(coreRequest);
|
|
897
|
+
if (!/[/\\]lib[/\\]puppeteer[/\\]node[/\\]PuppeteerNode\.js$/.test(corePath))
|
|
898
|
+
return;
|
|
899
|
+
patchPuppeteerNodeClass(origLoad(corePath, parent, false)?.PuppeteerNode);
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
// not installed / different layout: the regular patches still apply
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Hook the CommonJS loader so the patches reach every copy of the browser
|
|
907
|
+
* libraries and Crawlee, whenever they load.
|
|
800
908
|
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
* onto the current _load (composes with tsx's hook in dev) and idempotent.
|
|
909
|
+
* Module._load is the chokepoint every require() passes through AFTER
|
|
910
|
+
* resolution — including CJS packages imported from ESM, which Node funnels
|
|
911
|
+
* through the CJS loader with the resolved filename — so patching here covers
|
|
912
|
+
* whichever copy (SDK tree or actor workdir) the request actually resolved to.
|
|
913
|
+
* Chained onto the current _load (composes with tsx's hook in dev) and
|
|
914
|
+
* idempotent.
|
|
808
915
|
*/
|
|
809
916
|
function installRawModuleLoadHook() {
|
|
810
917
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
@@ -813,9 +920,12 @@ function installRawModuleLoadHook() {
|
|
|
813
920
|
if (typeof origLoad !== 'function' || origLoad._scrapelyRawCdpHook)
|
|
814
921
|
return;
|
|
815
922
|
const hookedLoad = function (request, parent, isMain) {
|
|
923
|
+
if (request === 'puppeteer' || request === 'puppeteer-core') {
|
|
924
|
+
preloadPuppeteerNodeClass(request, parent, origLoad);
|
|
925
|
+
}
|
|
816
926
|
const loaded = origLoad.apply(this, [request, parent, isMain]);
|
|
817
927
|
try {
|
|
818
|
-
patchLoadedRawModule(request, loaded, (name) => origLoad(name, parent, isMain));
|
|
928
|
+
patchLoadedRawModule(request, loaded, (name) => origLoad(name, parent, isMain), typeof parent?.filename === 'string' ? parent.filename : undefined);
|
|
819
929
|
}
|
|
820
930
|
catch (err) {
|
|
821
931
|
// Never break the actor's module loading — warn and hand back the
|
|
@@ -827,9 +937,134 @@ function installRawModuleLoadHook() {
|
|
|
827
937
|
hookedLoad._scrapelyRawCdpHook = true;
|
|
828
938
|
Module._load = hookedLoad;
|
|
829
939
|
}
|
|
940
|
+
/**
|
|
941
|
+
* Patch copies that loaded before this module (e.g. a local run that imports
|
|
942
|
+
* crawlee or playwright before scrapely). Copies loaded later are patched by
|
|
943
|
+
* the loader hooks.
|
|
944
|
+
*
|
|
945
|
+
* @internal Exported for tests.
|
|
946
|
+
*/
|
|
947
|
+
function patchLoadedModules() {
|
|
948
|
+
const cache = require.cache ?? {};
|
|
949
|
+
for (const [filename, cached] of Object.entries(cache)) {
|
|
950
|
+
const mod = cached?.exports;
|
|
951
|
+
if (!mod || !cached?.loaded)
|
|
952
|
+
continue;
|
|
953
|
+
try {
|
|
954
|
+
if (CRAWLER_FILENAME_RE.test(filename))
|
|
955
|
+
patchCrawleeCrawlerModule(mod);
|
|
956
|
+
if (BROWSER_POOL_FILENAME_RE.test(filename))
|
|
957
|
+
patchBrowserPoolModule(mod);
|
|
958
|
+
if (LAUNCHER_FILENAME_RE.test(filename))
|
|
959
|
+
patchBrowserLauncherModule(mod, filename);
|
|
960
|
+
if (PLAYWRIGHT_ENTRY_FILENAME_RE.test(filename))
|
|
961
|
+
patchRawPlaywrightModule(mod);
|
|
962
|
+
const puppeteerEntry = PUPPETEER_ENTRY_FILENAME_RE.exec(filename);
|
|
963
|
+
// Existing instance first: once its class is patched, the instance
|
|
964
|
+
// reads as patched but still holds the original bound methods.
|
|
965
|
+
if (puppeteerEntry)
|
|
966
|
+
patchRawPuppeteerModule(mod, puppeteerEntry[1]);
|
|
967
|
+
}
|
|
968
|
+
catch (err) {
|
|
969
|
+
console.warn(`[scrapely] CDP patch failed for '${filename}':`, err?.message);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
for (const [filename, cached] of Object.entries(cache)) {
|
|
973
|
+
if (cached?.loaded && /[/\\]lib[/\\]cjs[/\\]puppeteer[/\\]node[/\\]PuppeteerNode\.js$/.test(filename)) {
|
|
974
|
+
patchPuppeteerNodeClass(cached.exports?.PuppeteerNode);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
// ─── Pure-ESM puppeteer ─────────────────────────────────────────────────────
|
|
979
|
+
//
|
|
980
|
+
// `import puppeteer from 'puppeteer'` loads puppeteer's ESM build, which never
|
|
981
|
+
// passes through Module._load. A module load hook appends one call to
|
|
982
|
+
// PuppeteerNode.js that hands the class to patchPuppeteerNodeClass (through a
|
|
983
|
+
// global, so it also works from Node 20's off-thread loader). The hook is only
|
|
984
|
+
// registered when puppeteer is installed where ESM code can import it (ESM
|
|
985
|
+
// resolution ignores NODE_PATH), so other actors pay nothing.
|
|
986
|
+
/** @internal Global the ESM load hook's appended code calls. */
|
|
987
|
+
exports.PUPPETEER_NODE_PATCH_GLOBAL = 'scrapely.patchPuppeteerNodeClass';
|
|
988
|
+
/** puppeteer-core's ESM PuppeteerNode.js (URL): lib/esm/puppeteer/... up to v24, lib/puppeteer/... (ESM only) from v25. */
|
|
989
|
+
exports.PUPPETEER_NODE_ESM_URL_RE = /\/puppeteer-core\/lib\/(esm\/)?puppeteer\/node\/PuppeteerNode\.js$/;
|
|
990
|
+
/** Appended to the ESM PuppeteerNode.js source; runs right after the class is defined. */
|
|
991
|
+
exports.PUPPETEER_NODE_ESM_SUFFIX = `\n;globalThis[Symbol.for(${JSON.stringify(exports.PUPPETEER_NODE_PATCH_GLOBAL)})]?.(PuppeteerNode);\n`;
|
|
992
|
+
/** Directories ESM code in this process resolves bare imports from: cwd and the main script's ancestors. */
|
|
993
|
+
function esmLookupDirs() {
|
|
994
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
995
|
+
const path = require('path');
|
|
996
|
+
const starts = [process.cwd()];
|
|
997
|
+
if (process.argv[1])
|
|
998
|
+
starts.push(path.dirname(path.resolve(process.argv[1])));
|
|
999
|
+
const dirs = new Set();
|
|
1000
|
+
for (const start of starts) {
|
|
1001
|
+
let dir = start;
|
|
1002
|
+
for (;;) {
|
|
1003
|
+
dirs.add(dir);
|
|
1004
|
+
const parent = path.dirname(dir);
|
|
1005
|
+
if (parent === dir)
|
|
1006
|
+
break;
|
|
1007
|
+
dir = parent;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return [...dirs];
|
|
1011
|
+
}
|
|
1012
|
+
/** @internal Whether an ESM import of puppeteer / puppeteer-core could succeed in this process. */
|
|
1013
|
+
function esmPuppeteerInstalled() {
|
|
1014
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1015
|
+
const fs = require('fs');
|
|
1016
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1017
|
+
const path = require('path');
|
|
1018
|
+
for (const dir of esmLookupDirs()) {
|
|
1019
|
+
for (const name of ['puppeteer-core', 'puppeteer']) {
|
|
1020
|
+
if (fs.existsSync(path.join(dir, 'node_modules', name, 'package.json')))
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
function installEsmPuppeteerHook() {
|
|
1027
|
+
globalThis[Symbol.for(exports.PUPPETEER_NODE_PATCH_GLOBAL)] = patchPuppeteerNodeClass;
|
|
1028
|
+
if (process.env.SCRAPELY_ESM_PUPPETEER_HOOK === '0' || !esmPuppeteerInstalled())
|
|
1029
|
+
return;
|
|
1030
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1031
|
+
const Module = require('module');
|
|
1032
|
+
if (Module._scrapelyEsmPuppeteerHook)
|
|
1033
|
+
return;
|
|
1034
|
+
Module._scrapelyEsmPuppeteerHook = true;
|
|
1035
|
+
try {
|
|
1036
|
+
if (typeof Module.registerHooks === 'function') {
|
|
1037
|
+
// Node 22.15+ / 23.5+: synchronous, in-thread.
|
|
1038
|
+
Module.registerHooks({
|
|
1039
|
+
load(url, context, nextLoad) {
|
|
1040
|
+
const result = nextLoad(url, context);
|
|
1041
|
+
if (result?.format === 'module' && result.source != null && exports.PUPPETEER_NODE_ESM_URL_RE.test(url)) {
|
|
1042
|
+
const source = typeof result.source === 'string' ? result.source : new TextDecoder().decode(result.source);
|
|
1043
|
+
return { ...result, source: source + exports.PUPPETEER_NODE_ESM_SUFFIX };
|
|
1044
|
+
}
|
|
1045
|
+
return result;
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
else if (typeof Module.register === 'function') {
|
|
1050
|
+
// Node 20.6+ / 22.0-22.14: off-thread loader (esm-loader-hooks.mjs).
|
|
1051
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1052
|
+
const { pathToFileURL } = require('url');
|
|
1053
|
+
Module.register('./esm-loader-hooks.mjs', pathToFileURL(__filename));
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
catch (err) {
|
|
1057
|
+
console.warn('[scrapely] ESM puppeteer hook unavailable:', err?.message);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
830
1060
|
function applyRawBrowserPatches() {
|
|
831
|
-
patchSdkTreeRawModules();
|
|
832
1061
|
installRawModuleLoadHook();
|
|
833
|
-
|
|
1062
|
+
installEsmPuppeteerHook();
|
|
1063
|
+
patchLoadedModules();
|
|
1064
|
+
}
|
|
1065
|
+
// Package-manager processes (npm/npx/yarn/pnpm) only spawn the actor; they
|
|
1066
|
+
// never run actor code, so they get no hooks. `npm start` whose script is a
|
|
1067
|
+
// plain `node <file>` runs that node process directly (see node-wrapper.ts).
|
|
1068
|
+
if (!(0, node_wrapper_js_1.handlePackageManagerProcess)()) {
|
|
1069
|
+
applyRawBrowserPatches();
|
|
834
1070
|
}
|
|
835
|
-
applyRawBrowserPatches();
|
|
@@ -27,7 +27,7 @@ function getPlatformProxyEnv() {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
const BYOP_WARNING = '[Scrapely] No proxy configured. Scrapely injects platform proxy credentials (APIFY_PROXY_*/SCRAPELY_PROXY_*) into runs when your account has proxy access. ' +
|
|
30
|
-
'Locally, either add proxyUrls to your actor input, or copy your proxy credentials from the Scrapely
|
|
30
|
+
'Locally, either add proxyUrls to your actor input, or copy your proxy credentials from the Scrapely dashboard (Proxy page) into the ' +
|
|
31
31
|
'SCRAPELY_PROXY_HOSTNAME, SCRAPELY_PROXY_PORT, SCRAPELY_PROXY_USERNAME and SCRAPELY_PROXY_PASSWORD environment variables. ' +
|
|
32
32
|
'Using the platform proxy from outside Scrapely requires an active plan or a prepaid (PAYG) balance of at least $1; ' +
|
|
33
33
|
'free accounts get run-scoped credentials injected into their on-platform runs only.';
|
package/package.json
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scrapely",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.28",
|
|
4
4
|
"description": "Scrapely SDK for building JavaScript and TypeScript Actors",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json",
|
|
15
|
+
"./dist/*.js": "./dist/*.js",
|
|
16
|
+
"./dist/*.mjs": "./dist/*.mjs",
|
|
17
|
+
"./dist/*": "./dist/*.js",
|
|
18
|
+
"./*": "./*"
|
|
19
|
+
},
|
|
7
20
|
"files": [
|
|
8
21
|
"dist",
|
|
9
22
|
"LICENSE",
|
|
@@ -48,7 +61,8 @@
|
|
|
48
61
|
"test:cdp-retry": "npx tsx --test tests/cdp-retry.test.ts",
|
|
49
62
|
"test:proxy-input-aliases": "npx tsx --test tests/proxy-input-aliases.test.ts",
|
|
50
63
|
"test:charge-retries": "npx tsx --test tests/charge-retries.test.ts",
|
|
51
|
-
"test:solver": "npx tsx --test tests/solver.test.ts"
|
|
64
|
+
"test:solver": "npx tsx --test tests/solver.test.ts",
|
|
65
|
+
"test:lazy-preload": "npx tsx --test tests/lazy-preload.test.ts"
|
|
52
66
|
},
|
|
53
67
|
"author": "SandeepDev1",
|
|
54
68
|
"license": "ISC AND Apache-2.0",
|