@eko-ai/eko-nodejs
Version:
Empowering language to transform human words into action.
1,582 lines (1,480 loc) • 95.8 kB
JavaScript
import 'fs';
import 'os';
import 'path';
import { Log, BaseBrowserLabelsAgent, uuidv4 } from '@eko-ai/eko';
import { spawn } from 'child_process';
async function getCdpWsEndpoint(port) {
// Example => ws://localhost:9222/devtools/browser/{session-id}
const response = await fetch(`http://localhost:${port}/json/version`);
const browserInfo = await response.json();
Log.info("browserInfo: ", browserInfo);
return browserInfo.webSocketDebuggerUrl;
}
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
function getAugmentedNamespace(n) {
if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n;
var f = n.default;
if (typeof f == "function") {
var a = function a () {
if (this instanceof a) {
return Reflect.construct(f, arguments, this.constructor);
}
return f.apply(this, arguments);
};
a.prototype = f.prototype;
} else a = {};
Object.defineProperty(a, '__esModule', {value: true});
Object.keys(n).forEach(function (k) {
var d = Object.getOwnPropertyDescriptor(n, k);
Object.defineProperty(a, k, d.get ? d : {
enumerable: true,
get: function () {
return n[k];
}
});
});
return a;
}
var browser = {exports: {}};
/**
* Helpers.
*/
var ms;
var hasRequiredMs;
function requireMs () {
if (hasRequiredMs) return ms;
hasRequiredMs = 1;
var s = 1000;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var w = d * 7;
var y = d * 365.25;
/**
* Parse or format the given `val`.
*
* Options:
*
* - `long` verbose formatting [false]
*
* @param {String|Number} val
* @param {Object} [options]
* @throws {Error} throw an error if val is not a non-empty string or a number
* @return {String|Number}
* @api public
*/
ms = function (val, options) {
options = options || {};
var type = typeof val;
if (type === 'string' && val.length > 0) {
return parse(val);
} else if (type === 'number' && isFinite(val)) {
return options.long ? fmtLong(val) : fmtShort(val);
}
throw new Error(
'val is not a non-empty string or a valid number. val=' +
JSON.stringify(val)
);
};
/**
* Parse the given `str` and return milliseconds.
*
* @param {String} str
* @return {Number}
* @api private
*/
function parse(str) {
str = String(str);
if (str.length > 100) {
return;
}
var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
str
);
if (!match) {
return;
}
var n = parseFloat(match[1]);
var type = (match[2] || 'ms').toLowerCase();
switch (type) {
case 'years':
case 'year':
case 'yrs':
case 'yr':
case 'y':
return n * y;
case 'weeks':
case 'week':
case 'w':
return n * w;
case 'days':
case 'day':
case 'd':
return n * d;
case 'hours':
case 'hour':
case 'hrs':
case 'hr':
case 'h':
return n * h;
case 'minutes':
case 'minute':
case 'mins':
case 'min':
case 'm':
return n * m;
case 'seconds':
case 'second':
case 'secs':
case 'sec':
case 's':
return n * s;
case 'milliseconds':
case 'millisecond':
case 'msecs':
case 'msec':
case 'ms':
return n;
default:
return undefined;
}
}
/**
* Short format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtShort(ms) {
var msAbs = Math.abs(ms);
if (msAbs >= d) {
return Math.round(ms / d) + 'd';
}
if (msAbs >= h) {
return Math.round(ms / h) + 'h';
}
if (msAbs >= m) {
return Math.round(ms / m) + 'm';
}
if (msAbs >= s) {
return Math.round(ms / s) + 's';
}
return ms + 'ms';
}
/**
* Long format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtLong(ms) {
var msAbs = Math.abs(ms);
if (msAbs >= d) {
return plural(ms, msAbs, d, 'day');
}
if (msAbs >= h) {
return plural(ms, msAbs, h, 'hour');
}
if (msAbs >= m) {
return plural(ms, msAbs, m, 'minute');
}
if (msAbs >= s) {
return plural(ms, msAbs, s, 'second');
}
return ms + ' ms';
}
/**
* Pluralization helper.
*/
function plural(ms, msAbs, n, name) {
var isPlural = msAbs >= n * 1.5;
return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : '');
}
return ms;
}
var common;
var hasRequiredCommon;
function requireCommon () {
if (hasRequiredCommon) return common;
hasRequiredCommon = 1;
/**
* This is the common logic for both the Node.js and web browser
* implementations of `debug()`.
*/
function setup(env) {
createDebug.debug = createDebug;
createDebug.default = createDebug;
createDebug.coerce = coerce;
createDebug.disable = disable;
createDebug.enable = enable;
createDebug.enabled = enabled;
createDebug.humanize = requireMs();
createDebug.destroy = destroy;
Object.keys(env).forEach(key => {
createDebug[key] = env[key];
});
/**
* The currently active debug mode names, and names to skip.
*/
createDebug.names = [];
createDebug.skips = [];
/**
* Map of special "%n" handling functions, for the debug "format" argument.
*
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
*/
createDebug.formatters = {};
/**
* Selects a color for a debug namespace
* @param {String} namespace The namespace string for the debug instance to be colored
* @return {Number|String} An ANSI color code for the given namespace
* @api private
*/
function selectColor(namespace) {
let hash = 0;
for (let i = 0; i < namespace.length; i++) {
hash = ((hash << 5) - hash) + namespace.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
}
createDebug.selectColor = selectColor;
/**
* Create a debugger with the given `namespace`.
*
* @param {String} namespace
* @return {Function}
* @api public
*/
function createDebug(namespace) {
let prevTime;
let enableOverride = null;
let namespacesCache;
let enabledCache;
function debug(...args) {
// Disabled?
if (!debug.enabled) {
return;
}
const self = debug;
// Set `diff` timestamp
const curr = Number(new Date());
const ms = curr - (prevTime || curr);
self.diff = ms;
self.prev = prevTime;
self.curr = curr;
prevTime = curr;
args[0] = createDebug.coerce(args[0]);
if (typeof args[0] !== 'string') {
// Anything else let's inspect with %O
args.unshift('%O');
}
// Apply any `formatters` transformations
let index = 0;
args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => {
// If we encounter an escaped % then don't increase the array index
if (match === '%%') {
return '%';
}
index++;
const formatter = createDebug.formatters[format];
if (typeof formatter === 'function') {
const val = args[index];
match = formatter.call(self, val);
// Now we need to remove `args[index]` since it's inlined in the `format`
args.splice(index, 1);
index--;
}
return match;
});
// Apply env-specific formatting (colors, etc.)
createDebug.formatArgs.call(self, args);
const logFn = self.log || createDebug.log;
logFn.apply(self, args);
}
debug.namespace = namespace;
debug.useColors = createDebug.useColors();
debug.color = createDebug.selectColor(namespace);
debug.extend = extend;
debug.destroy = createDebug.destroy; // XXX Temporary. Will be removed in the next major release.
Object.defineProperty(debug, 'enabled', {
enumerable: true,
configurable: false,
get: () => {
if (enableOverride !== null) {
return enableOverride;
}
if (namespacesCache !== createDebug.namespaces) {
namespacesCache = createDebug.namespaces;
enabledCache = createDebug.enabled(namespace);
}
return enabledCache;
},
set: v => {
enableOverride = v;
}
});
// Env-specific initialization logic for debug instances
if (typeof createDebug.init === 'function') {
createDebug.init(debug);
}
return debug;
}
function extend(namespace, delimiter) {
const newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace);
newDebug.log = this.log;
return newDebug;
}
/**
* Enables a debug mode by namespaces. This can include modes
* separated by a colon and wildcards.
*
* @param {String} namespaces
* @api public
*/
function enable(namespaces) {
createDebug.save(namespaces);
createDebug.namespaces = namespaces;
createDebug.names = [];
createDebug.skips = [];
const split = (typeof namespaces === 'string' ? namespaces : '')
.trim()
.replace(/\s+/g, ',')
.split(',')
.filter(Boolean);
for (const ns of split) {
if (ns[0] === '-') {
createDebug.skips.push(ns.slice(1));
} else {
createDebug.names.push(ns);
}
}
}
/**
* Checks if the given string matches a namespace template, honoring
* asterisks as wildcards.
*
* @param {String} search
* @param {String} template
* @return {Boolean}
*/
function matchesTemplate(search, template) {
let searchIndex = 0;
let templateIndex = 0;
let starIndex = -1;
let matchIndex = 0;
while (searchIndex < search.length) {
if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === '*')) {
// Match character or proceed with wildcard
if (template[templateIndex] === '*') {
starIndex = templateIndex;
matchIndex = searchIndex;
templateIndex++; // Skip the '*'
} else {
searchIndex++;
templateIndex++;
}
} else if (starIndex !== -1) { // eslint-disable-line no-negated-condition
// Backtrack to the last '*' and try to match more characters
templateIndex = starIndex + 1;
matchIndex++;
searchIndex = matchIndex;
} else {
return false; // No match
}
}
// Handle trailing '*' in template
while (templateIndex < template.length && template[templateIndex] === '*') {
templateIndex++;
}
return templateIndex === template.length;
}
/**
* Disable debug output.
*
* @return {String} namespaces
* @api public
*/
function disable() {
const namespaces = [
...createDebug.names,
...createDebug.skips.map(namespace => '-' + namespace)
].join(',');
createDebug.enable('');
return namespaces;
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*
* @param {String} name
* @return {Boolean}
* @api public
*/
function enabled(name) {
for (const skip of createDebug.skips) {
if (matchesTemplate(name, skip)) {
return false;
}
}
for (const ns of createDebug.names) {
if (matchesTemplate(name, ns)) {
return true;
}
}
return false;
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) {
return val.stack || val.message;
}
return val;
}
/**
* XXX DO NOT USE. This is a temporary stub function.
* XXX It WILL be removed in the next major release.
*/
function destroy() {
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.');
}
createDebug.enable(createDebug.load());
return createDebug;
}
common = setup;
return common;
}
/* eslint-env browser */
var hasRequiredBrowser;
function requireBrowser () {
if (hasRequiredBrowser) return browser.exports;
hasRequiredBrowser = 1;
(function (module, exports) {
/**
* This is the web browser implementation of `debug()`.
*/
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;
exports.storage = localstorage();
exports.destroy = (() => {
let warned = false;
return () => {
if (!warned) {
warned = true;
console.warn('Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`.');
}
};
})();
/**
* Colors.
*/
exports.colors = [
'#0000CC',
'#0000FF',
'#0033CC',
'#0033FF',
'#0066CC',
'#0066FF',
'#0099CC',
'#0099FF',
'#00CC00',
'#00CC33',
'#00CC66',
'#00CC99',
'#00CCCC',
'#00CCFF',
'#3300CC',
'#3300FF',
'#3333CC',
'#3333FF',
'#3366CC',
'#3366FF',
'#3399CC',
'#3399FF',
'#33CC00',
'#33CC33',
'#33CC66',
'#33CC99',
'#33CCCC',
'#33CCFF',
'#6600CC',
'#6600FF',
'#6633CC',
'#6633FF',
'#66CC00',
'#66CC33',
'#9900CC',
'#9900FF',
'#9933CC',
'#9933FF',
'#99CC00',
'#99CC33',
'#CC0000',
'#CC0033',
'#CC0066',
'#CC0099',
'#CC00CC',
'#CC00FF',
'#CC3300',
'#CC3333',
'#CC3366',
'#CC3399',
'#CC33CC',
'#CC33FF',
'#CC6600',
'#CC6633',
'#CC9900',
'#CC9933',
'#CCCC00',
'#CCCC33',
'#FF0000',
'#FF0033',
'#FF0066',
'#FF0099',
'#FF00CC',
'#FF00FF',
'#FF3300',
'#FF3333',
'#FF3366',
'#FF3399',
'#FF33CC',
'#FF33FF',
'#FF6600',
'#FF6633',
'#FF9900',
'#FF9933',
'#FFCC00',
'#FFCC33'
];
/**
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
* and the Firebug extension (any Firefox version) are known
* to support "%c" CSS customizations.
*
* TODO: add a `localStorage` variable to explicitly enable/disable colors
*/
// eslint-disable-next-line complexity
function useColors() {
// NB: In an Electron preload script, document will be defined but not fully
// initialized. Since we know we're in Chrome, we'll just detect this case
// explicitly
if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) {
return true;
}
// Internet Explorer and Edge do not support colors.
if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
return false;
}
let m;
// Is webkit? http://stackoverflow.com/a/16459606/376773
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
// eslint-disable-next-line no-return-assign
return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) ||
// Is firebug? http://stackoverflow.com/a/398120/376773
(typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) ||
// Is firefox >= v31?
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
(typeof navigator !== 'undefined' && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31) ||
// Double check webkit in userAgent just in case we are in a worker
(typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/));
}
/**
* Colorize log arguments if enabled.
*
* @api public
*/
function formatArgs(args) {
args[0] = (this.useColors ? '%c' : '') +
this.namespace +
(this.useColors ? ' %c' : ' ') +
args[0] +
(this.useColors ? '%c ' : ' ') +
'+' + module.exports.humanize(this.diff);
if (!this.useColors) {
return;
}
const c = 'color: ' + this.color;
args.splice(1, 0, c, 'color: inherit');
// The final "%c" is somewhat tricky, because there could be other
// arguments passed either before or after the %c, so we need to
// figure out the correct index to insert the CSS into
let index = 0;
let lastC = 0;
args[0].replace(/%[a-zA-Z%]/g, match => {
if (match === '%%') {
return;
}
index++;
if (match === '%c') {
// We only are interested in the *last* %c
// (the user may have provided their own)
lastC = index;
}
});
args.splice(lastC, 0, c);
}
/**
* Invokes `console.debug()` when available.
* No-op when `console.debug` is not a "function".
* If `console.debug` is not available, falls back
* to `console.log`.
*
* @api public
*/
exports.log = console.debug || console.log || (() => {});
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
try {
if (namespaces) {
exports.storage.setItem('debug', namespaces);
} else {
exports.storage.removeItem('debug');
}
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
let r;
try {
r = exports.storage.getItem('debug') || exports.storage.getItem('DEBUG') ;
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG
if (!r && typeof process !== 'undefined' && 'env' in process) {
r = process.env.DEBUG;
}
return r;
}
/**
* Localstorage attempts to return the localstorage.
*
* This is necessary because safari throws
* when a user disables cookies/localstorage
* and you attempt to access it.
*
* @return {LocalStorage}
* @api private
*/
function localstorage() {
try {
// TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context
// The Browser also has localStorage in the global context.
return localStorage;
} catch (error) {
// Swallow
// XXX (@Qix-) should we be logging these?
}
}
module.exports = requireCommon()(exports);
const {formatters} = module.exports;
/**
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
*/
formatters.j = function (v) {
try {
return JSON.stringify(v);
} catch (error) {
return '[UnexpectedJSONParseError]: ' + error.message;
}
};
} (browser, browser.exports));
return browser.exports;
}
var browserExports = requireBrowser();
var debug$3 = /*@__PURE__*/getDefaultExportFromCjs(browserExports);
/*!
* playwright-extra v4.3.5 by berstend
* https://github.com/berstend/puppeteer-extra/tree/master/packages/playwright-extra#readme
* @license MIT
*/
/** Node.js module loader helper */
class Loader {
constructor(moduleName, packageNames) {
this.moduleName = moduleName;
this.packageNames = packageNames;
}
/**
* Lazy load a top level export from another module by wrapping it in a JS proxy.
*
* This allows us to re-export e.g. `devices` from `playwright` while redirecting direct calls
* to it to the module version the user has installed, rather than shipping with a hardcoded version.
*
* If we don't do this and the user doesn't have the target module installed we'd throw immediately when our code is imported.
*
* We use a "super" Proxy defining all traps, so calls like `Object.keys(playwright.devices).length` will return the correct value.
*/
lazyloadExportOrDie(exportName) {
const that = this;
const trapHandler = Object.fromEntries(Object.getOwnPropertyNames(Reflect).map((name) => [
name,
function (target, ...args) {
const moduleExport = that.loadModuleOrDie()[exportName];
const customTarget = moduleExport;
const result = Reflect[name](customTarget || target, ...args);
return result;
}
]));
return new Proxy({}, trapHandler);
}
/** Load the module if possible */
loadModule() {
return requirePackages(this.packageNames);
}
/** Load the module if possible or throw */
loadModuleOrDie() {
const module = requirePackages(this.packageNames);
if (module) {
return module;
}
throw this.requireError;
}
get requireError() {
const moduleNamePretty = this.moduleName.charAt(0).toUpperCase() + this.moduleName.slice(1);
return new Error(`
${moduleNamePretty} is missing. :-)
I've tried loading ${this.packageNames
.map(p => `"${p}"`)
.join(', ')} - no luck.
Make sure you install one of those packages or use the named 'addExtra' export,
to patch a specific (and maybe non-standard) implementation of ${moduleNamePretty}.
To get the latest stable version of ${moduleNamePretty} run:
'yarn add ${this.moduleName}' or 'npm i ${this.moduleName}'
`);
}
}
function requirePackages(packageNames) {
for (const name of packageNames) {
try {
return require(name);
}
catch (_) {
continue; // noop
}
}
return;
}
/** Playwright specific module loader */
const playwrightLoader = new Loader('playwright', [
'playwright-core',
'playwright'
]);
const debug = debug$3('playwright-extra:puppeteer-compat');
const isPlaywrightPage = (obj) => {
return 'unroute' in obj;
};
const isPlaywrightFrame = (obj) => {
return ['parentFrame', 'frameLocator'].every(x => x in obj);
};
const isPlaywrightBrowser = (obj) => {
return 'newContext' in obj;
};
const isPuppeteerCompat = (obj) => {
return !!obj && typeof obj === 'object' && !!obj.isCompatShim;
};
const cache = {
objectToShim: new Map(),
cdpSession: {
page: new Map(),
browser: new Map()
}
};
/** Augment a Playwright object with compatibility with certain Puppeteer methods */
function addPuppeteerCompat(object) {
if (!object || typeof object !== 'object') {
return object;
}
if (cache.objectToShim.has(object)) {
return cache.objectToShim.get(object);
}
if (isPuppeteerCompat(object)) {
return object;
}
debug('addPuppeteerCompat', cache.objectToShim.size);
if (isPlaywrightPage(object) || isPlaywrightFrame(object)) {
const shim = createPageShim(object);
cache.objectToShim.set(object, shim);
return shim;
}
if (isPlaywrightBrowser(object)) {
const shim = createBrowserShim(object);
cache.objectToShim.set(object, shim);
return shim;
}
debug('Received unknown object:', Reflect.ownKeys(object));
return object;
}
// Only chromium browsers support CDP
const dummyCDPClient = {
send: async (...args) => {
debug('dummy CDP client called', 'send', args);
},
on: (...args) => {
debug('dummy CDP client called', 'on', args);
}
};
async function getPageCDPSession(page) {
let session = cache.cdpSession.page.get(page);
if (session) {
debug('getPageCDPSession: use existing');
return session;
}
debug('getPageCDPSession: use new');
const context = isPlaywrightFrame(page)
? page.page().context()
: page.context();
try {
session = await context.newCDPSession(page);
cache.cdpSession.page.set(page, session);
return session;
}
catch (err) {
debug('getPageCDPSession: error while creating session:', err.message);
debug('getPageCDPSession: Unable create CDP session (most likely a different browser than chromium) - returning a dummy');
}
return dummyCDPClient;
}
async function getBrowserCDPSession(browser) {
let session = cache.cdpSession.browser.get(browser);
if (session) {
debug('getBrowserCDPSession: use existing');
return session;
}
debug('getBrowserCDPSession: use new');
try {
session = await browser.newBrowserCDPSession();
cache.cdpSession.browser.set(browser, session);
return session;
}
catch (err) {
debug('getBrowserCDPSession: error while creating session:', err.message);
debug('getBrowserCDPSession: Unable create CDP session (most likely a different browser than chromium) - returning a dummy');
}
return dummyCDPClient;
}
function createPageShim(page) {
const objId = Math.random().toString(36).substring(2, 7);
const shim = new Proxy(page, {
get(target, prop) {
if (prop === 'isCompatShim' || prop === 'isPlaywright') {
return true;
}
debug('page - get', objId, prop);
if (prop === '_client') {
return () => ({
send: async (method, params) => {
const session = await getPageCDPSession(page);
return await session.send(method, params);
},
on: (event, listener) => {
getPageCDPSession(page).then(session => {
session.on(event, listener);
});
}
});
}
if (prop === 'setBypassCSP') {
return async (enabled) => {
const session = await getPageCDPSession(page);
return await session.send('Page.setBypassCSP', {
enabled
});
};
}
if (prop === 'setUserAgent') {
return async (userAgent, userAgentMetadata) => {
const session = await getPageCDPSession(page);
return await session.send('Emulation.setUserAgentOverride', {
userAgent,
userAgentMetadata
});
};
}
if (prop === 'browser') {
if (isPlaywrightPage(page)) {
return () => {
let browser = page.context().browser();
if (!browser) {
debug('page.browser() - not available, most likely due to launchPersistentContext');
// Use a page shim as quick drop-in (so browser.userAgent() still works)
browser = page;
}
return addPuppeteerCompat(browser);
};
}
}
if (prop === 'evaluateOnNewDocument') {
if (isPlaywrightPage(page)) {
return async function (pageFunction, ...args) {
return await page.addInitScript(pageFunction, args[0]);
};
}
}
// Only relevant when page is being used a pseudo stand-in for the browser object (launchPersistentContext)
if (prop === 'userAgent') {
return async (enabled) => {
const session = await getPageCDPSession(page);
const data = await session.send('Browser.getVersion');
return data.userAgent;
};
}
return Reflect.get(target, prop);
}
});
return shim;
}
function createBrowserShim(browser) {
const objId = Math.random().toString(36).substring(2, 7);
const shim = new Proxy(browser, {
get(target, prop) {
if (prop === 'isCompatShim' || prop === 'isPlaywright') {
return true;
}
debug('browser - get', objId, prop);
if (prop === 'pages') {
return () => browser
.contexts()
.flatMap(c => c.pages().map(page => addPuppeteerCompat(page)));
}
if (prop === 'userAgent') {
return async () => {
const session = await getBrowserCDPSession(browser);
const data = await session.send('Browser.getVersion');
return data.userAgent;
};
}
return Reflect.get(target, prop);
}
});
return shim;
}
const debug$1 = debug$3('playwright-extra:plugins');
class PluginList {
constructor() {
this._plugins = [];
this._dependencyDefaults = new Map();
this._dependencyResolution = new Map();
}
/**
* Get a list of all registered plugins.
*/
get list() {
return this._plugins;
}
/**
* Get the names of all registered plugins.
*/
get names() {
return this._plugins.map(p => p.name);
}
/**
* Add a new plugin to the list (after checking if it's well-formed).
*
* @param plugin
* @internal
*/
add(plugin) {
var _a;
if (!this.isValidPluginInstance(plugin)) {
return false;
}
if (!!plugin.onPluginRegistered) {
plugin.onPluginRegistered({ framework: 'playwright' });
}
// PuppeteerExtraPlugin: Populate `_childClassMembers` list containing methods defined by the plugin
if (!!plugin._registerChildClassMembers) {
plugin._registerChildClassMembers(Object.getPrototypeOf(plugin));
}
if ((_a = plugin.requirements) === null || _a === void 0 ? void 0 : _a.has('dataFromPlugins')) {
plugin.getDataFromPlugins = this.getData.bind(this);
}
this._plugins.push(plugin);
return true;
}
/** Check if the shape of a plugin is correct or warn */
isValidPluginInstance(plugin) {
if (!plugin ||
typeof plugin !== 'object' ||
!plugin._isPuppeteerExtraPlugin) {
console.error(`Warning: Plugin is not derived from PuppeteerExtraPlugin, ignoring.`, plugin);
return false;
}
if (!plugin.name) {
console.error(`Warning: Plugin with no name registering, ignoring.`, plugin);
return false;
}
return true;
}
/** Error callback in case calling a plugin method throws an error. Can be overwritten. */
onPluginError(plugin, method, err) {
console.warn(`An error occured while executing "${method}" in plugin "${plugin.name}":`, err);
}
/**
* Define default values for plugins implicitly required through the `dependencies` plugin stanza.
*
* @param dependencyPath - The string by which the dependency is listed (not the plugin name)
*
* @example
* chromium.use(stealth)
* chromium.plugins.setDependencyDefaults('stealth/evasions/webgl.vendor', { vendor: 'Bob', renderer: 'Alice' })
*/
setDependencyDefaults(dependencyPath, opts) {
this._dependencyDefaults.set(dependencyPath, opts);
return this;
}
/**
* Define custom plugin modules for plugins implicitly required through the `dependencies` plugin stanza.
*
* Using this will prevent dynamic imports from being used, which JS bundlers often have issues with.
*
* @example
* chromium.use(stealth)
* chromium.plugins.setDependencyResolution('stealth/evasions/webgl.vendor', VendorPlugin)
*/
setDependencyResolution(dependencyPath, pluginModule) {
this._dependencyResolution.set(dependencyPath, pluginModule);
return this;
}
/**
* Prepare plugins to be used (resolve dependencies, ordering)
* @internal
*/
prepare() {
this.resolveDependencies();
this.order();
}
/** Return all plugins using the supplied method */
filterByMethod(methodName) {
return this._plugins.filter(plugin => {
// PuppeteerExtraPlugin: The base class will already define all methods, hence we need to do a different check
if (!!plugin._childClassMembers &&
Array.isArray(plugin._childClassMembers)) {
return plugin._childClassMembers.includes(methodName);
}
return methodName in plugin;
});
}
/** Conditionally add puppeteer compatibility to values provided to the plugins */
_addPuppeteerCompatIfNeeded(plugin, method, args) {
const canUseShim = plugin._isPuppeteerExtraPlugin && !plugin.noPuppeteerShim;
const methodWhitelist = [
'onBrowser',
'onPageCreated',
'onPageClose',
'afterConnect',
'afterLaunch'
];
const shouldUseShim = methodWhitelist.includes(method);
if (!canUseShim || !shouldUseShim) {
return args;
}
debug$1('add puppeteer compatibility', plugin.name, method);
return [...args.map(arg => addPuppeteerCompat(arg))];
}
/**
* Dispatch plugin lifecycle events in a typesafe way.
* Only Plugins that expose the supplied property will be called.
*
* Will not await results to dispatch events as fast as possible to all plugins.
*
* @param method - The lifecycle method name
* @param args - Optional: Any arguments to be supplied to the plugin methods
* @internal
*/
dispatch(method, ...args) {
var _a, _b;
const plugins = this.filterByMethod(method);
debug$1('dispatch', method, {
all: this._plugins.length,
filteredByMethod: plugins.length
});
for (const plugin of plugins) {
try {
args = this._addPuppeteerCompatIfNeeded.bind(this)(plugin, method, args);
const fnType = (_b = (_a = plugin[method]) === null || _a === void 0 ? void 0 : _a.constructor) === null || _b === void 0 ? void 0 : _b.name;
debug$1('dispatch to plugin', {
plugin: plugin.name,
method,
fnType
});
if (fnType === 'AsyncFunction') {
;
plugin[method](...args).catch((err) => this.onPluginError(plugin, method, err));
}
else {
;
plugin[method](...args);
}
}
catch (err) {
this.onPluginError(plugin, method, err);
}
}
}
/**
* Dispatch plugin lifecycle events in a typesafe way.
* Only Plugins that expose the supplied property will be called.
*
* Can also be used to get a definite return value after passing it to plugins:
* Calls plugins sequentially and passes on a value (waterfall style).
*
* The plugins can either modify the value or return an updated one.
* Will return the latest, updated value which ran through all plugins.
*
* By convention only the first argument will be used as the updated value.
*
* @param method - The lifecycle method name
* @param args - Optional: Any arguments to be supplied to the plugin methods
* @internal
*/
async dispatchBlocking(method, ...args) {
const plugins = this.filterByMethod(method);
debug$1('dispatchBlocking', method, {
all: this._plugins.length,
filteredByMethod: plugins.length
});
let retValue = null;
for (const plugin of plugins) {
try {
args = this._addPuppeteerCompatIfNeeded.bind(this)(plugin, method, args);
retValue = await plugin[method](...args);
// In case we got a return value use that as new first argument for followup function calls
if (retValue !== undefined) {
args[0] = retValue;
}
}
catch (err) {
this.onPluginError(plugin, method, err);
return retValue;
}
}
return retValue;
}
/**
* Order plugins that have expressed a special placement requirement.
*
* This is useful/necessary for e.g. plugins that depend on the data from other plugins.
*
* @private
*/
order() {
debug$1('order:before', this.names);
const runLast = this._plugins
.filter(p => { var _a; return (_a = p.requirements) === null || _a === void 0 ? void 0 : _a.has('runLast'); })
.map(p => p.name);
for (const name of runLast) {
const index = this._plugins.findIndex(p => p.name === name);
this._plugins.push(this._plugins.splice(index, 1)[0]);
}
debug$1('order:after', this.names);
}
/**
* Collects the exposed `data` property of all registered plugins.
* Will be reduced/flattened to a single array.
*
* Can be accessed by plugins that listed the `dataFromPlugins` requirement.
*
* Implemented mainly for plugins that need data from other plugins (e.g. `user-preferences`).
*
* @see [PuppeteerExtraPlugin]/data
* @param name - Filter data by optional name
*
* @private
*/
getData(name) {
const data = this._plugins
.filter((p) => !!p.data)
.map((p) => (Array.isArray(p.data) ? p.data : [p.data]))
.reduce((acc, arr) => [...acc, ...arr], []);
return name ? data.filter((d) => d.name === name) : data;
}
/**
* Handle `plugins` stanza (already instantiated plugins that don't require dynamic imports)
*/
resolvePluginsStanza() {
debug$1('resolvePluginsStanza');
const pluginNames = new Set(this.names);
this._plugins
.filter(p => !!p.plugins && p.plugins.length)
.filter(p => !pluginNames.has(p.name)) // TBD: Do we want to filter out existing?
.forEach(parent => {
(parent.plugins || []).forEach(p => {
debug$1(parent.name, 'adding missing plugin', p.name);
this.add(p);
});
});
}
/**
* Handle `dependencies` stanza (which requires dynamic imports)
*
* Plugins can define `dependencies` as a Set or Array of dependency paths, or a Map with additional opts
*
* @note
* - The default opts for implicit dependencies can be defined using `setDependencyDefaults()`
* - Dynamic imports can be avoided by providing plugin modules with `setDependencyResolution()`
*/
resolveDependenciesStanza() {
debug$1('resolveDependenciesStanza');
/** Attempt to dynamically require a plugin module */
const requireDependencyOrDie = (parentName, dependencyPath) => {
// If the user provided the plugin module already we use that
if (this._dependencyResolution.has(dependencyPath)) {
return this._dependencyResolution.get(dependencyPath);
}
const possiblePrefixes = ['puppeteer-extra-plugin-']; // could be extended later
const isAlreadyPrefixed = possiblePrefixes.some(prefix => dependencyPath.startsWith(prefix));
const packagePaths = [];
// If the dependency is not already prefixed we attempt to require all possible combinations to find one that works
if (!isAlreadyPrefixed) {
packagePaths.push(...possiblePrefixes.map(prefix => prefix + dependencyPath));
}
// We always attempt to require the path verbatim (as a last resort)
packagePaths.push(dependencyPath);
const pluginModule = requirePackages(packagePaths);
if (pluginModule) {
return pluginModule;
}
const explanation = `
The plugin '${parentName}' listed '${dependencyPath}' as dependency,
which could not be found. Please install it:
${packagePaths
.map(packagePath => `yarn add ${packagePath.split('/')[0]}`)
.join(`\n or:\n`)}
Note: You don't need to require the plugin yourself,
unless you want to modify it's default settings.
If your bundler has issues with dynamic imports take a look at '.plugins.setDependencyResolution()'.
`;
console.warn(explanation);
throw new Error('Plugin dependency not found');
};
const existingPluginNames = new Set(this.names);
const recursivelyLoadMissingDependencies = ({ name: parentName, dependencies }) => {
if (!dependencies) {
return;
}
const processDependency = (dependencyPath, opts) => {
const pluginModule = requireDependencyOrDie(parentName, dependencyPath);
opts = opts || this._dependencyDefaults.get(dependencyPath) || {};
const plugin = pluginModule(opts);
if (existingPluginNames.has(plugin.name)) {
debug$1(parentName, '=> dependency already exists:', plugin.name);
return;
}
existingPluginNames.add(plugin.name);
debug$1(parentName, '=> adding new dependency:', plugin.name, opts);
this.add(plugin);
return recursivelyLoadMissingDependencies(plugin);
};
if (dependencies instanceof Set || Array.isArray(dependencies)) {
return [...dependencies].forEach(dependencyPath => processDependency(dependencyPath));
}
if (dependencies instanceof Map) {
// Note: `k,v => v,k` (Map + forEach will reverse the order)
return dependencies.forEach((v, k) => processDependency(k, v));
}
};
this.list.forEach(recursivelyLoadMissingDependencies);
}
/**
* Lightweight plugin dependency management to require plugins and code mods on demand.
* @private
*/
resolveDependencies() {
debug$1('resolveDependencies');
this.resolvePluginsStanza();
this.resolveDependenciesStanza();
}
}
const debug$2 = debug$3('playwright-extra');
/**
* Modular plugin framework to teach `playwright` new tricks.
*/
class PlaywrightExtraClass {
constructor(_launcher) {
this._launcher = _launcher;
this.plugins = new PluginList();
}
/**
* The **main interface** to register plugins.
*
* Can be called multiple times to enable multiple plugins.
*
* Plugins derived from `PuppeteerExtraPlugin` will be used with a compatiblity layer.
*
* @example
* chromium.use(plugin1).use(plugin2)
* firefox.use(plugin1).use(plugin2)
*
* @see [PuppeteerExtraPlugin]
*
* @return The same `PlaywrightExtra` instance (for optional chaining)
*/
use(plugin) {
const isValid = plugin && 'name' in plugin;
if (!isValid) {
throw new Error('A plugin must be provided to .use()');
}
if (this.plugins.add(plugin)) {
debug$2('Plugin registered', plugin.name);
}
return this;
}
/**
* In order to support a default export which will require vanilla playwright automatically,
* as well as `addExtra` to patch a provided launcher, we need to so some gymnastics here.
*
* Otherwise this would throw immediately, even when only using the `addExtra` export with an arbitrary compatible launcher.
*
* The solution is to make the vanilla launcher optional and only throw once we try to effectively use and can't find it.
*
* @internal
*/
get launcher() {
if (!this._launcher) {
throw playwrightLoader.requireError;
}
return this._launcher;
}
async launch(...args) {
if (!this.launcher.launch) {
throw new Error('Launcher does not support "launch"');
}
let [options] = args;
options = Object.assign({ args: [] }, (options || {})); // Initialize args array
debug$2('launch', options);
this.plugins.prepare();
// Give plugins the chance to modify the options before continuing
options =
(await this.plugins.dispatchBlocking('beforeLaunch', options)) || options;
debug$2('launch with options', options);
if ('userDataDir' in options) {
debug$2("A plugin defined userDataDir during .launch, which isn't supported by playwright - ignoring");
delete options.userDataDir;
}
const browser = await this.launcher['launch'](options);
await this.plugins.dispatchBlocking('onBrowser', browser);
await this._bindBrowserEvents(browser);
await this.plugins.dispatchBlocking('afterLaunch', browser);
return browser;
}
async launchPersistentContext(...args) {
if (!this.launcher.launchPersistentContext) {
throw new Error('Launcher does not support "launchPersistentContext"');
}
let [userDataDir, options] = args;
options = Object.assign({ args: [] }, (options || {})); // Initialize args array
debug$2('launchPersistentContext', options);
this.plugins.prepare();
// Give plugins the chance to modify the options before continuing
options =
(await this.plugins.dispatchBlocking('beforeLaunch', options)) || options;
const context = await this.launcher['launchPersistentContext'](userDataDir, options);
await this.plugins.dispatchBlocking('afterLaunch', context);
this._bindBrowserContextEvents(context);
return context;
}
async connect(wsEndpointOrOptions, wsOptions = {}) {
if (!this.launcher.connect) {
throw new Error('Launcher does not support "connect"');
}
this.plugins.prepare();
// Playwright currently supports two function signatures for .connect
let options = {};
let wsEndpointAsString = false;
if (typeof wsEndpointOrOptions === 'object') {
options = Object.assign(Object.assign({}, wsEndpointOrOptions), wsOptions);
}
else {
wsEndpointAsString = true;
options = Object.assign({ wsEndpoint: wsEndpointOrOptions }, wsOptions);
}
debug$2('connect', options);
// Give plugins the chance to modify the options before launch/connect
options =
(await this.plugins.dispatchBlocking('beforeConnect', options)) || options;
// Follow call signature of end user
const args = [];
const wsEndpoint = options.wsEndpoint;
if (wsEndpointAsString) {
delete options.wsEndpoint;
args.push(wsEndpoint, options);
}
else {
args.push(options);
}
const browser = (await this.launcher['connect'](...args));
await this.plugins.dispatchBlocking('onBrowser', browser);
await this._bindBrowserEvents(browser);
await this.plugins.dispatchBlocking('afterConnect', browser);
return browser;
}
async connectOverCDP(wsEndpointOrOptions, wsOptions = {}) {
if (!this.launcher.connectOverCDP) {
throw new Error(`Launcher does not implement 'connectOverCDP'`);
}
this.plugins.prepare();
// Playwright currently supports two function signatures for .connectOverCDP
let options = {};
let wsEndpointAsString = false;
if (typeof wsEndpointOrOptions === 'object') {
options = Object.assign(Object.assign({}, wsEndpointOrOptions), wsOptions);
}
else {
wsEndpointAsString = true;
options = Object.assign({ endpointURL: wsEndpointOrOptions }, wsOptions);
}
debug$2('connectOverCDP');
// Give plugins the chance to modify the options before launch/connect
options =
(await this.plugins.dispatchBlocking('beforeConnect', options)) || options;
// Follow call signature of end user
const args = [];
const endpointURL = options.endpointURL;
if (wsEndpointAsString) {
delete options.endpointURL;
args.push(endpointURL, options);
}
else {
args.push(options);
}
const browser = (await this.launcher['connectOverCDP'](...args));
await this.plugins.dispatchBlocking('onBrowser', browser);
await this._bindBrowserEvents(browser);
await this.plugins.dispatchBlocking('afterConnect', browser);
return browser;
}
async _bindBrowserContextEvents(context, contextOptions) {
debug$2('_bindBrowserContextEvents');
this.plugins.dispatch('onContextCreated', context, contextOptions);
// Make sure things like `addInitScript` show an effect on the very first page as well
context.newPage = ((originalMethod, ctx) => {
return async () => {
const page = await originalMethod.call(ctx);
await page.goto('about:blank');
return page;
};