A modular plugin framework for Playwright to enable enhanced browser automation through plugins.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Interfaces and types for creating compatible plugins that work with both Playwright Extra and puppeteer-extra. The plugin system provides comprehensive lifecycle hooks and compatibility features for extending browser automation capabilities.
The core interface that all plugins must implement to be compatible with Playwright Extra.
/**
* PuppeteerExtraPlugin interface, strongly typed for internal use
*/
interface PuppeteerExtraPlugin extends Partial<PluginLifecycleMethods> {
/** Plugin identification flag - must be true */
_isPuppeteerExtraPlugin: boolean;
/** Unique plugin name */
name: string;
/** Disable the puppeteer compatibility shim for this plugin */
noPuppeteerShim?: boolean;
/** Plugin requirements and capabilities */
requirements?: PluginRequirements;
/** Plugin dependencies that should be auto-loaded */
dependencies?: PluginDependencies;
/** Data that can be shared with other plugins */
data?: PluginData[];
/** Access data from other plugins (if dataFromPlugins requirement is set) */
getDataFromPlugins?(name?: string): void;
/** Method for registering child class members (PuppeteerExtraPlugin compatibility) */
_registerChildClassMembers?(prototype: any): void;
/** List of child class members (PuppeteerExtraPlugin compatibility) */
_childClassMembers?: string[];
/** Sub-plugins that should be registered automatically */
plugins?: CompatiblePlugin[];
}Abstract class defining all available lifecycle hooks that plugins can implement.
/**
* Strongly typed plugin lifecycle events for internal use
*/
abstract class PluginLifecycleMethods {
/** Called when plugin is registered with the framework */
async onPluginRegistered(env?: PluginEnv): Promise<void> {}
/** Called before browser launch, can modify launch options */
async beforeLaunch(options: LaunchOptions): Promise<LaunchOptions | void> {}
/** Called after browser launch, receives browser or context instance */
async afterLaunch(browserOrContext?: Browser | BrowserContext): Promise<void> {}
/** Called before connecting to existing browser, can modify connect options */
async beforeConnect(options: ConnectOptions): Promise<ConnectOptions | void> {}
/** Called after connecting to existing browser */
async afterConnect(browser: Browser): Promise<void> {}
/** Called when browser instance is available */
async onBrowser(browser: Browser): Promise<void> {}
/** Called when a new page is created */
async onPageCreated(page: Page): Promise<void> {}
/** Called when a page is closed */
async onPageClose(page: Page): Promise<void> {}
/** Called when browser is disconnected */
async onDisconnected(browser?: Browser): Promise<void> {}
/** Called before creating browser context, can modify context options */
async beforeContext(
options?: BrowserContextOptions,
browser?: Browser
): Promise<BrowserContextOptions | void> {}
/** Called when a new browser context is created */
async onContextCreated(
context?: BrowserContext,
options?: BrowserContextOptions
): Promise<void> {}
}Simple plugin implementation demonstrating the core structure:
import { PuppeteerExtraPlugin } from 'puppeteer-extra-plugin';
class MyPlugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
}
get name() {
return 'my-plugin';
}
async onPluginRegistered() {
console.log('Plugin registered with Playwright Extra');
}
async beforeLaunch(options) {
console.log('Before launch:', options);
// Modify launch options
options.args = options.args || [];
options.args.push('--custom-flag');
return options;
}
async onPageCreated(page) {
console.log('New page created');
// Add custom functionality to pages
await page.addInitScript(() => {
console.log('Custom script injected');
});
}
}
module.exports = (pluginConfig) => new MyPlugin(pluginConfig);Plugins can declare requirements that affect their behavior and execution order:
type PluginRequirements = Set<
| 'launch' // Plugin requires browser launch
| 'headful' // Plugin requires headful (non-headless) mode
| 'dataFromPlugins' // Plugin needs access to data from other plugins
| 'runLast' // Plugin should run after all other plugins
>;Usage Examples:
class DataCollectorPlugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
this.requirements = new Set(['dataFromPlugins', 'runLast']);
}
get name() {
return 'data-collector';
}
async onPluginRegistered() {
// This plugin will have access to getDataFromPlugins method
const allData = this.getDataFromPlugins();
console.log('Data from other plugins:', allData);
}
}Plugins can declare dependencies that will be automatically loaded:
type PluginDependencies = Set<string> | Map<string, any> | string[];Usage Examples:
class CompositePlugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
// Declare dependencies as Set
this.dependencies = new Set([
'stealth/evasions/webgl.vendor',
'stealth/evasions/user-agent-override'
]);
}
get name() {
return 'composite-plugin';
}
}
class ConfigurablePlugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
// Declare dependencies with options as Map
this.dependencies = new Map([
['stealth/evasions/webgl.vendor', {
vendor: 'Custom',
renderer: 'Custom'
}],
['stealth/evasions/user-agent-override', {
userAgent: 'Custom User Agent'
}]
]);
}
get name() {
return 'configurable-plugin';
}
}Plugins can expose data that other plugins can access:
interface PluginData {
name: string | { [key: string]: any };
value: { [key: string]: any };
}Usage Examples:
class DataProviderPlugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
this.data = [
{
name: 'config',
value: { apiKey: 'secret-key', endpoint: 'https://api.example.com' }
},
{
name: 'cache',
value: { results: [], timestamps: [] }
}
];
}
get name() {
return 'data-provider';
}
}
class DataConsumerPlugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
this.requirements = new Set(['dataFromPlugins']);
}
get name() {
return 'data-consumer';
}
async onPluginRegistered() {
// Access data from other plugins
const configData = this.getDataFromPlugins('config');
const cacheData = this.getDataFromPlugins('cache');
console.log('Config:', configData);
console.log('Cache:', cacheData);
}
}Complex plugin with multiple lifecycle hooks and error handling:
class AdvancedPlugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts);
this.options = { timeout: 30000, retry: 3, ...opts };
}
get name() {
return 'advanced-plugin';
}
async beforeLaunch(options) {
try {
// Pre-launch setup
console.log('Configuring browser launch');
options.args = options.args || [];
options.args.push(`--timeout=${this.options.timeout}`);
return options;
} catch (error) {
console.error('Error in beforeLaunch:', error);
throw error;
}
}
async onBrowser(browser) {
// Set up browser-level monitoring
browser.on('disconnected', () => {
console.log('Browser disconnected');
});
}
async beforeContext(options, browser) {
// Configure context options
options = options || {};
options.userAgent = 'Custom User Agent';
return options;
}
async onPageCreated(page) {
// Set up page-level functionality
await page.addInitScript(() => {
// Inject custom functionality
window.customAPI = {
version: '1.0.0',
initialized: true
};
});
// Add error handling
page.on('pageerror', error => {
console.error('Page error:', error);
});
}
}
module.exports = (pluginConfig) => new AdvancedPlugin(pluginConfig);type PluginMethodName = keyof PluginLifecycleMethods;
interface PluginEnv {
framework: 'playwright';
}
type CompatiblePlugin =
| CompatiblePuppeteerPlugin
| CompatiblePlaywrightPlugin
| CompatibleExtraPlugin;
interface CompatiblePuppeteerPlugin {
_isPuppeteerExtraPlugin: boolean;
name?: string;
}
interface CompatiblePlaywrightPlugin {
_isPlaywrightExtraPlugin: boolean;
name?: string;
}
interface CompatibleExtraPlugin {
_isExtraPlugin: boolean;
name?: string;
}Install with Tessl CLI
npx tessl i tessl/npm-playwright-extra