352 lines
14 KiB
JavaScript
352 lines
14 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.PluginList = void 0;
|
|
const debug_1 = __importDefault(require("debug"));
|
|
const debug = (0, debug_1.default)('playwright-extra:plugins');
|
|
const loader_1 = require("./helper/loader");
|
|
const puppeteer_compatiblity_shim_1 = require("./puppeteer-compatiblity-shim");
|
|
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('add puppeteer compatibility', plugin.name, method);
|
|
return [...args.map(arg => (0, puppeteer_compatiblity_shim_1.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('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('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('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('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('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('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(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('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 = (0, loader_1.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(parentName, '=> dependency already exists:', plugin.name);
|
|
return;
|
|
}
|
|
existingPluginNames.add(plugin.name);
|
|
debug(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('resolveDependencies');
|
|
this.resolvePluginsStanza();
|
|
this.resolveDependenciesStanza();
|
|
}
|
|
}
|
|
exports.PluginList = PluginList;
|
|
//# sourceMappingURL=plugins.js.map
|