"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PuppeteerExtraPlugin = void 0;
const debug_1 = __importDefault(require("debug"));
/** @private */
const merge = require('merge-deep');
/**
 * Base class for `puppeteer-extra` plugins.
 *
 * Provides convenience methods to avoid boilerplate.
 *
 * All common `puppeteer` browser events will be bound to
 * the plugin instance, if a respectively named class member is found.
 *
 * Please refer to the [puppeteer API documentation](https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md) as well.
 *
 * @example
 * // hello-world-plugin.js
 * const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin')
 *
 * class Plugin extends PuppeteerExtraPlugin {
 *   constructor (opts = { }) { super(opts) }
 *
 *   get name () { return 'hello-world' }
 *
 *   async onPageCreated (page) {
 *     this.debug('page created', page.url())
 *     const ua = await page.browser().userAgent()
 *     this.debug('user agent', ua)
 *   }
 * }
 *
 * module.exports = function (pluginConfig) { return new Plugin(pluginConfig) }
 *
 *
 * // foo.js
 * const puppeteer = require('puppeteer-extra')
 * puppeteer.use(require('./hello-world-plugin')())
 *
 * ;(async () => {
 *   const browser = await puppeteer.launch({headless: false})
 *   const page = await browser.newPage()
 *   await page.goto('http://example.com', {waitUntil: 'domcontentloaded'})
 *   await browser.close()
 * })()
 *
 */
class PuppeteerExtraPlugin {
    constructor(opts) {
        this._debugBase = (0, debug_1.default)(`puppeteer-extra-plugin:base:${this.name}`);
        this._childClassMembers = [];
        this._opts = merge(this.defaults, opts || {});
        this._debugBase('Initialized.');
    }
    /**
     * Plugin name (required).
     *
     * Convention:
     * - Package: `puppeteer-extra-plugin-anonymize-ua`
     * - Name: `anonymize-ua`
     *
     * @example
     * get name () { return 'anonymize-ua' }
     */
    get name() {
        throw new Error('Plugin must override "name"');
    }
    /**
     * Plugin defaults (optional).
     *
     * If defined will be ([deep-](https://github.com/jonschlinkert/merge-deep))merged with the (optional) user supplied options (supplied during plugin instantiation).
     *
     * The result of merging defaults with user supplied options can be accessed through `this.opts`.
     *
     * @see [[opts]]
     *
     * @example
     * get defaults () {
     *   return {
     *     stripHeadless: true,
     *     makeWindows: true,
     *     customFn: null
     *   }
     * }
     *
     * // Users can overwrite plugin defaults during instantiation:
     * puppeteer.use(require('puppeteer-extra-plugin-foobar')({ makeWindows: false }))
     */
    get defaults() {
        return {};
    }
    /**
     * Plugin requirements (optional).
     *
     * Signal certain plugin requirements to the base class and the user.
     *
     * Currently supported:
     * - `launch`
     *   - If the plugin only supports locally created browser instances (no `puppeteer.connect()`),
     *     will output a warning to the user.
     * - `headful`
     *   - If the plugin doesn't work in `headless: true` mode,
     *     will output a warning to the user.
     * - `dataFromPlugins`
     *   - In case the plugin requires data from other plugins.
     *     will enable usage of `this.getDataFromPlugins()`.
     * - `runLast`
     *   - In case the plugin prefers to run after the others.
     *     Useful when the plugin needs data from others.
     *
     * @example
     * get requirements () {
     *   return new Set(['runLast', 'dataFromPlugins'])
     * }
     */
    get requirements() {
        return new Set([]);
    }
    /**
     * Plugin dependencies (optional).
     *
     * Missing plugins will be required() by puppeteer-extra.
     *
     * @example
     * get dependencies () {
     *   return new Set(['user-preferences'])
     * }
     * // Will ensure the 'puppeteer-extra-plugin-user-preferences' plugin is loaded.
     */
    get dependencies() {
        return new Set([]);
    }
    /**
     * Plugin data (optional).
     *
     * Plugins can expose data (an array of objects), which in turn can be consumed by other plugins,
     * that list the `dataFromPlugins` requirement (by using `this.getDataFromPlugins()`).
     *
     * Convention: `[ {name: 'Any name', value: 'Any value'} ]`
     *
     * @see [[getDataFromPlugins]]
     *
     * @example
     * // plugin1.js
     * get data () {
     *   return [
     *     {
     *       name: 'userPreferences',
     *       value: { foo: 'bar' }
     *     },
     *     {
     *       name: 'userPreferences',
     *       value: { hello: 'world' }
     *     }
     *   ]
     *
     * // plugin2.js
     * get requirements () { return new Set(['dataFromPlugins']) }
     *
     * async beforeLaunch () {
     *   const prefs = this.getDataFromPlugins('userPreferences').map(d => d.value)
     *   this.debug(prefs) // => [ { foo: 'bar' }, { hello: 'world' } ]
     * }
     */
    get data() {
        return [];
    }
    /**
     * Access the plugin options (usually the `defaults` merged with user defined options)
     *
     * To skip the auto-merging of defaults with user supplied opts don't define a `defaults`
     * property and set the `this._opts` Object in your plugin constructor directly.
     *
     * @see [[defaults]]
     *
     * @example
     * get defaults () { return { foo: "bar" } }
     *
     * async onPageCreated (page) {
     *   this.debug(this.opts.foo) // => bar
     * }
     */
    get opts() {
        return this._opts;
    }
    /**
     *  Convenience debug logger based on the [debug] module.
     *  Will automatically namespace the logging output to the plugin package name.
     *  [debug]: https://www.npmjs.com/package/debug
     *
     *  ```bash
     *  # toggle output using environment variables
     *  DEBUG=puppeteer-extra-plugin:<plugin_name> node foo.js
     *  # to debug all the things:
     *  DEBUG=puppeteer-extra,puppeteer-extra-plugin:* node foo.js
     *  ```
     *
     * @example
     * this.debug('hello world')
     * // will output e.g. 'puppeteer-extra-plugin:anonymize-ua hello world'
     */
    get debug() {
        return (0, debug_1.default)(`puppeteer-extra-plugin:${this.name}`);
    }
    /**
     * Before a new browser instance is created/launched.
     *
     * Can be used to modify the puppeteer launch options by modifying or returning them.
     *
     * Plugins using this method will be called in sequence to each
     * be able to update the launch options.
     *
     * @example
     * async beforeLaunch (options) {
     *   if (this.opts.flashPluginPath) {
     *     options.args.push(`--ppapi-flash-path=${this.opts.flashPluginPath}`)
     *   }
     * }
     *
     * @param options - Puppeteer launch options
     */
    async beforeLaunch(options) {
        // noop
    }
    /**
     * After the browser has launched.
     *
     * Note: Don't assume that there will only be a single browser instance during the lifecycle of a plugin.
     * It's possible that `pupeeteer.launch` will be  called multiple times and more than one browser created.
     * In order to make the plugins as stateless as possible don't store a reference to the browser instance
     * in the plugin but rather consider alternatives.
     *
     * E.g. when using `onPageCreated` you can get a browser reference by using `page.browser()`.
     *
     * Alternatively you could expose a class method that takes a browser instance as a parameter to work with:
     *
     * ```es6
     * const fancyPlugin = require('puppeteer-extra-plugin-fancy')()
     * puppeteer.use(fancyPlugin)
     * const browser = await puppeteer.launch()
     * await fancyPlugin.killBrowser(browser)
     * ```
     *
     * @param  browser - The `puppeteer` browser instance.
     * @param  opts.options - Puppeteer launch options used.
     *
     * @example
     * async afterLaunch (browser, opts) {
     *   this.debug('browser has been launched', opts.options)
     * }
     */
    async afterLaunch(browser, opts = { options: {} }) {
        // noop
    }
    /**
     * Before connecting to an existing browser instance.
     *
     * Can be used to modify the puppeteer connect options by modifying or returning them.
     *
     * Plugins using this method will be called in sequence to each
     * be able to update the launch options.
     *
     * @param  {Object} options - Puppeteer connect options
     * @return {Object=}
     */
    async beforeConnect(options) {
        // noop
    }
    /**
     * After connecting to an existing browser instance.
     *
     * > Note: Don't assume that there will only be a single browser instance during the lifecycle of a plugin.
     *
     * @param browser - The `puppeteer` browser instance.
     * @param  {Object} opts
     * @param  {Object} opts.options - Puppeteer connect options used.
     *
     */
    async afterConnect(browser, opts = {}) {
        // noop
    }
    /**
     * Called when a browser instance is available.
     *
     * This applies to both `puppeteer.launch()` and `puppeteer.connect()`.
     *
     * Convenience method created for plugins that need access to a browser instance
     * and don't mind if it has been created through `launch` or `connect`.
     *
     * > Note: Don't assume that there will only be a single browser instance during the lifecycle of a plugin.
     *
     * @param browser - The `puppeteer` browser instance.
     */
    async onBrowser(browser, opts) {
        // noop
    }
    /**
     * Called when a target is created, for example when a new page is opened by window.open or browser.newPage.
     *
     * > Note: This includes target creations in incognito browser contexts.
     *
     * > Note: This includes browser instances created through `.launch()` as well as `.connect()`.
     *
     * @param  {Puppeteer.Target} target
     */
    async onTargetCreated(target) {
        // noop
    }
    /**
     * Same as `onTargetCreated` but prefiltered to only contain Pages, for convenience.
     *
     * > Note: This includes page creations in incognito browser contexts.
     *
     * > Note: This includes browser instances created through `.launch()` as well as `.connect()`.
     *
     * @param  {Puppeteer.Target} target
     *
     * @example
     * async onPageCreated (page) {
     *   let ua = await page.browser().userAgent()
     *   if (this.opts.stripHeadless) {
     *     ua = ua.replace('HeadlessChrome/', 'Chrome/')
     *   }
     *   this.debug('new ua', ua)
     *   await page.setUserAgent(ua)
     * }
     */
    async onPageCreated(page) {
        // noop
    }
    /**
     * Called when the url of a target changes.
     *
     * > Note: This includes target changes in incognito browser contexts.
     *
     * > Note: This includes browser instances created through `.launch()` as well as `.connect()`.
     *
     * @param  {Puppeteer.Target} target
     */
    async onTargetChanged(target) {
        // noop
    }
    /**
     * Called when a target is destroyed, for example when a page is closed.
     *
     * > Note: This includes target destructions in incognito browser contexts.
     *
     * > Note: This includes browser instances created through `.launch()` as well as `.connect()`.
     *
     * @param  {Puppeteer.Target} target
     */
    async onTargetDestroyed(target) {
        // noop
    }
    /**
     * Called when Puppeteer gets disconnected from the Chromium instance.
     *
     * This might happen because of one of the following:
     * - Chromium is closed or crashed
     * - The `browser.disconnect` method was called
     */
    async onDisconnected() {
        // noop
    }
    /**
     * **Deprecated:** Since puppeteer v1.6.0 `onDisconnected` has been improved
     * and should be used instead of `onClose`.
     *
     * In puppeteer < v1.6.0 `onDisconnected` was not catching all exit scenarios.
     * In order for plugins to clean up properly (e.g. deleting temporary files)
     * the `onClose` method had been introduced.
     *
     * > Note: Might be called multiple times on exit.
     *
     * > Note: This only includes browser instances created through `.launch()`.
     */
    async onClose() {
        // noop
    }
    /**
     * After the plugin has been registered in `puppeteer-extra`.
     *
     * Normally right after `puppeteer.use(plugin)` is called
     */
    async onPluginRegistered() {
        // noop
    }
    /**
     * Helper method to retrieve `data` objects from other plugins.
     *
     * A plugin needs to state the `dataFromPlugins` requirement
     * in order to use this method. Will be mapped to `puppeteer.getPluginData`.
     *
     * @param name - Filter data by `name` property
     *
     * @see [data]
     * @see [requirements]
     */
    getDataFromPlugins(name) {
        return [];
    }
    /**
     * Will match plugin dependencies against all currently registered plugins.
     * Is being called by `puppeteer-extra` and used to require missing dependencies.
     *
     * @param  {Array<Object>} plugins
     * @return {Set} - list of missing plugin names
     *
     * @private
     */
    _getMissingDependencies(plugins) {
        const pluginNames = new Set(plugins.map((p) => p.name));
        const missing = new Set(Array.from(this.dependencies.values()).filter(x => !pluginNames.has(x)));
        return missing;
    }
    /**
     * Conditionally bind browser/process events to class members.
     * The idea is to reduce event binding boilerplate in plugins.
     *
     * For efficiency we make sure the plugin is using the respective event
     * by checking the child class members before registering the listener.
     *
     * @param  {<Puppeteer.Browser>} browser
     * @param  {Object} opts - Options
     * @param  {string} opts.context - Puppeteer context (launch/connect)
     * @param  {Object} [opts.options] - Puppeteer launch or connect options
     * @param  {Array<string>} [opts.defaultArgs] - The default flags that Chromium will be launched with
     *
     * @private
     */
    async _bindBrowserEvents(browser, opts = {}) {
        if (this._hasChildClassMember('onTargetCreated') ||
            this._hasChildClassMember('onPageCreated')) {
            browser.on('targetcreated', this._onTargetCreated.bind(this));
        }
        if (this._hasChildClassMember('onTargetChanged') && this.onTargetChanged) {
            browser.on('targetchanged', this.onTargetChanged.bind(this));
        }
        if (this._hasChildClassMember('onTargetDestroyed') &&
            this.onTargetDestroyed) {
            browser.on('targetdestroyed', this.onTargetDestroyed.bind(this));
        }
        if (this._hasChildClassMember('onDisconnected') && this.onDisconnected) {
            browser.on('disconnected', this.onDisconnected.bind(this));
        }
        if (opts.context === 'launch' && this._hasChildClassMember('onClose')) {
            // The disconnect event has been improved since puppeteer v1.6.0
            // onClose is being kept mostly for legacy reasons
            if (this.onClose) {
                process.on('exit', this.onClose.bind(this));
                browser.on('disconnected', this.onClose.bind(this));
                if (opts.options.handleSIGINT !== false) {
                    process.on('SIGINT', this.onClose.bind(this));
                }
                if (opts.options.handleSIGTERM !== false) {
                    process.on('SIGTERM', this.onClose.bind(this));
                }
                if (opts.options.handleSIGHUP !== false) {
                    process.on('SIGHUP', this.onClose.bind(this));
                }
            }
        }
        if (opts.context === 'launch' && this.afterLaunch) {
            await this.afterLaunch(browser, opts);
        }
        if (opts.context === 'connect' && this.afterConnect) {
            await this.afterConnect(browser, opts);
        }
        if (this.onBrowser)
            await this.onBrowser(browser, opts);
    }
    /**
     * @private
     */
    async _onTargetCreated(target) {
        if (this.onTargetCreated)
            await this.onTargetCreated(target);
        // Pre filter pages for plugin developers convenience
        if (target.type() === 'page') {
            try {
                const page = await target.page();
                if (!page) {
                    return;
                }
                const validPage = 'isClosed' in page && !page.isClosed();
                if (this.onPageCreated && validPage) {
                    await this.onPageCreated(page);
                }
            }
            catch (err) {
                console.error(err);
            }
        }
    }
    /**
     * @private
     */
    _register(prototype) {
        this._registerChildClassMembers(prototype);
        if (this.onPluginRegistered)
            this.onPluginRegistered();
    }
    /**
     * @private
     */
    _registerChildClassMembers(prototype) {
        this._childClassMembers = Object.getOwnPropertyNames(prototype);
    }
    /**
     * @private
     */
    _hasChildClassMember(name) {
        return !!this._childClassMembers.includes(name);
    }
    /**
     * @private
     */
    get _isPuppeteerExtraPlugin() {
        return true;
    }
}
exports.PuppeteerExtraPlugin = PuppeteerExtraPlugin;
//# sourceMappingURL=index.js.map