UNPKG

@eko-ai/eko-nodejs

Version:

Empowering language to transform human words into action.

1,582 lines (1,480 loc) 95.8 kB
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; };