From 4c7d455b49c87cb6cc1fd6a484ea4933f54931d7 Mon Sep 17 00:00:00 2001 From: Paulo Cesar Date: Mon, 15 Mar 2021 01:20:51 -0300 Subject: [PATCH 1/2] Typescript refactoring --- .eslintrc | 19 +- api-extractor.json | 364 +++++++++++++++ babel.config.js | 6 + jest.config.js | 1 + package.json | 38 +- ...er-controller.js => browser-controller.ts} | 161 ++++--- src/abstract-classes/browser-plugin.js | 206 -------- src/abstract-classes/browser-plugin.ts | 211 +++++++++ src/abstract-classes/utils.js | 7 - src/abstract-classes/utils.ts | 3 + src/{browser-pool.js => browser-pool.ts} | 438 +++++++++++------- src/{events.js => events.ts} | 9 +- src/index.js | 36 -- src/index.ts | 56 +++ src/launch-context.js | 96 ---- src/launch-context.ts | 116 +++++ src/{logger.js => logger.ts} | 4 +- src/playwright/{browser.js => browser.ts} | 43 +- src/playwright/playwright-controller.js | 43 -- src/playwright/playwright-controller.ts | 40 ++ src/playwright/playwright-plugin.js | 78 ---- src/playwright/playwright-plugin.ts | 88 ++++ ...-controller.js => puppeteer-controller.ts} | 24 +- src/puppeteer/puppeteer-plugin.js | 71 --- src/puppeteer/puppeteer-plugin.ts | 73 +++ src/{utils.js => utils.ts} | 6 +- .../{plugins.test.js => plugins.test.ts} | 18 +- ...wser-pool.test.js => browser-pool.test.ts} | 13 +- test/{index.test.js => index.test.ts} | 8 +- tsconfig.json | 43 ++ tsconfig.test.json | 6 + 31 files changed, 1481 insertions(+), 844 deletions(-) create mode 100644 api-extractor.json create mode 100644 babel.config.js rename src/abstract-classes/{browser-controller.js => browser-controller.ts} (50%) delete mode 100644 src/abstract-classes/browser-plugin.js create mode 100644 src/abstract-classes/browser-plugin.ts delete mode 100644 src/abstract-classes/utils.js create mode 100644 src/abstract-classes/utils.ts rename src/{browser-pool.js => browser-pool.ts} (54%) rename src/{events.js => events.ts} (60%) delete mode 100644 src/index.js create mode 100644 src/index.ts delete mode 100644 src/launch-context.js create mode 100644 src/launch-context.ts rename src/{logger.js => logger.ts} (59%) rename src/playwright/{browser.js => browser.ts} (72%) delete mode 100644 src/playwright/playwright-controller.js create mode 100644 src/playwright/playwright-controller.ts delete mode 100644 src/playwright/playwright-plugin.js create mode 100644 src/playwright/playwright-plugin.ts rename src/puppeteer/{puppeteer-controller.js => puppeteer-controller.ts} (70%) delete mode 100644 src/puppeteer/puppeteer-plugin.js create mode 100644 src/puppeteer/puppeteer-plugin.ts rename src/{utils.js => utils.ts} (68%) rename test/browser-plugins/{plugins.test.js => plugins.test.ts} (96%) rename test/{browser-pool.test.js => browser-pool.test.ts} (98%) rename test/{index.test.js => index.test.ts} (52%) create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json diff --git a/.eslintrc b/.eslintrc index e0b7c18..86c0ff3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,20 @@ { - "extends": "@apify" + "extends": [ + "@apify", + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "max-len": 0, + "import/extensions": 0, + "@typescript-eslint/ban-types": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/explicit-module-boundary-types": 0 + } } diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000..1aeeb5b --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,364 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + "mainEntryPointFilePath": "/lib/index.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + // "reportFolder": "/etc/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/" + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": true + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "ae-extra-release-tag": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..faa86eb --- /dev/null +++ b/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', + ], +}; diff --git a/jest.config.js b/jest.config.js index e05082b..f6a03e3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,4 +6,5 @@ module.exports = { rootDir: path.join(__dirname, './'), testTimeout: 20000, testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], + preset: 'ts-jest', }; diff --git a/package.json b/package.json index ce2df6f..403f310 100644 --- a/package.json +++ b/package.json @@ -2,31 +2,43 @@ "name": "browser-pool", "version": "1.1.1", "description": "Rotate multiple browsers using popular automation libraries such as Playwright or Puppeteer.", - "main": "src/index.js", + "main": "lib/index.js", "dependencies": { - "apify-shared": "^0.6.0", + "apify-shared": "^0.6.3", "fs-extra": "^9.1.0", - "lodash": "^4.17.20", - "nanoid": "^3.1.16", + "nanoid": "^3.1.21", + "lodash.merge": "^4.6.2", + "lodash.noop": "^3.0.1", "ow": "^0.23.0", - "proxy-chain": "^0.4.5" + "proxy-chain": "^0.4.7" }, "devDependencies": { - "@apify/eslint-config": "^0.1.0", - "@types/jest": "^26.0.16", + "@apify/eslint-config": "^0.1.3", + "@babel/core": "^7.13.10", + "@babel/preset-env": "^7.13.10", + "@babel/preset-typescript": "^7.13.0", + "@microsoft/api-extractor": "^7.13.2", + "@types/fs-extra": "^9.0.8", + "@types/jest": "^26.0.20", + "@types/node": "^14.14.34", + "@typescript-eslint/eslint-plugin": "^4.17.0", + "@typescript-eslint/parser": "^4.17.0", + "babel-jest": "^26.6.3", "eslint": "^7.10.0", "jest": "^26.6.3", - "jsdoc-to-markdown": "^6.0.1", + "ts-jest": "^26.5.3", + "jsdoc-to-markdown": "^7.0.0", "markdown-toc": "^1.2.0", "playwright": "^1.9.1", - "puppeteer": "^7.1.0" + "puppeteer": "^7.1.0", + "typescript": "^4.2.3" }, "scripts": { - "build-docs": "npm run build-toc && node docs/build_docs.js", + "build": "tsc -p .", + "build-docs": "npm run build && npm run build-toc && node docs/build_docs.js", "build-toc": "markdown-toc docs/README.md -i", - "start": "node src/main.js", - "lint": "./node_modules/.bin/eslint ./src --ext .js,.jsx", - "lint:fix": "./node_modules/.bin/eslint ./src --ext .js,.jsx --fix", + "lint": "./node_modules/.bin/eslint ./src --ext .js,.ts,.jsx", + "lint:fix": "./node_modules/.bin/eslint ./src --ext .js,.ts,.jsx --fix", "test": "jest --maxWorkers=3 --forceExit" }, "author": { diff --git a/src/abstract-classes/browser-controller.js b/src/abstract-classes/browser-controller.ts similarity index 50% rename from src/abstract-classes/browser-controller.js rename to src/abstract-classes/browser-controller.ts index 14a93e4..85aaad9 100644 --- a/src/abstract-classes/browser-controller.js +++ b/src/abstract-classes/browser-controller.ts @@ -1,37 +1,108 @@ -const EventEmitter = require('events'); -const { nanoid } = require('nanoid'); -const log = require('../logger'); -const { throwImplementationNeeded } = require('./utils'); +import EventEmitter from 'events'; +import { nanoid } from 'nanoid'; +import type { Protocol } from 'puppeteer'; +import log from '../logger'; +import { throwImplementationNeeded } from './utils'; +import type BrowserPlugin from './browser-plugin'; // eslint-disable-line import/no-duplicates +import type { Browser } from './browser-plugin'; // eslint-disable-line import/no-duplicates +import type LaunchContext from '../launch-context'; + const { BROWSER_CONTROLLER_EVENTS: { BROWSER_CLOSED } } = require('../events'); const PROCESS_KILL_TIMEOUT_MILLIS = 5000; +/** + * Common interface for browser cookies + */ +export interface BrowserControllerCookie { + name: string; + value: string; + /** + * either url or domain / path are required. Optional. + */ + url?: string; + /** + * either url or domain / path are required Optional. + */ + domain?: string; + /** + * either url or domain / path are required Optional. + */ + path?: string; + /** + * Unix time in seconds. Optional. + */ + expires?: number; + /** + * Optional. + */ + httpOnly?: boolean; + /** + * Optional. + */ + secure?: boolean; + /** + * Optional. + */ + sameSite?: Protocol.Network.CookieSameSite; +} + /** * The `BrowserController` serves two purposes. First, it is the base class that * specialized controllers like `PuppeteerController` or `PlaywrightController` * extend. Second, it defines the public interface of the specialized classes * which provide only private methods. Therefore, we do not keep documentation * for the specialized classes, because it's the same for all of them. - * @property {string} id - * @property {BrowserPlugin} browserPlugin - * The `BrowserPlugin` instance used to launch the browser. - * @property {Browser} browser - * Browser representation of the underlying automation library. - * @property {LaunchContext} launchContext - * The configuration the browser was launched with. * @hideconstructor */ -class BrowserController extends EventEmitter { +export default class BrowserController< + BrowserLibrary extends Browser, + Page extends object, + LaunchOptions extends Record, + PageOptions extends Record, +> extends EventEmitter { + id: string; + /** - * @param {BrowserPlugin} browserPlugin + * The `BrowserPlugin` instance used to launch the browser. */ - constructor(browserPlugin) { + browserPlugin: BrowserPlugin + + /** + * Browser representation of the underlying automation library. + */ + browser: BrowserLibrary; + + /** + * The configuration the browser was launched with. + */ + launchContext: LaunchContext; + + isActive: boolean; + + supportsPageOptions: boolean; + + isActivePromise: Promise; + + hasBrowserPromise: Promise; + + protected _activate!: () => any; + + protected commitBrowser!: () => any; + + activePages: number; + + totalPages: number; + + lastPageOpenedAt: number; + + constructor(browserPlugin: BrowserPlugin) { super(); this.id = nanoid(); this.browserPlugin = browserPlugin; - this.browser = undefined; - this.launchContext = undefined; + this.browser = undefined as any; + this.launchContext = undefined as any; this.isActive = false; this.supportsPageOptions = false; @@ -53,7 +124,7 @@ class BrowserController extends EventEmitter { * activate is called. * @ignore */ - activate() { + activate(): void { if (!this.browser) { throw new Error('Cannot activate BrowserController without an assigned browser.'); } @@ -62,11 +133,9 @@ class BrowserController extends EventEmitter { } /** - * @param {Browser} browser - * @param {LaunchContext} launchContext * @ignore */ - assignBrowser(browser, launchContext) { + assignBrowser(browser: BrowserLibrary, launchContext: LaunchContext): void { if (this.browser) { throw new Error('BrowserController already has a browser instance assigned.'); } @@ -80,9 +149,8 @@ class BrowserController extends EventEmitter { * there will be no lingering browser processes. * * Emits 'browserClosed' event. - * @return {Promise} */ - async close() { + async close(): Promise { await this.hasBrowserPromise; await this._close().catch((err) => { log.debug(`Could not close browser.\nCause: ${err.message}`, { id: this.id }); @@ -99,9 +167,8 @@ class BrowserController extends EventEmitter { * Immediately kills the browser process. * * Emits 'browserClosed' event. - * @return {Promise} */ - async kill() { + async kill(): Promise { await this.hasBrowserPromise; await this._kill(); this.emit(BROWSER_CLOSED, this); @@ -109,11 +176,10 @@ class BrowserController extends EventEmitter { /** * Opens new browser page. - * @param {object} pageOptions - * @return {Promise} + * * @ignore */ - async newPage(pageOptions) { + async newPage(pageOptions?: PageOptions): Promise { this.activePages++; this.totalPages++; await this.isActivePromise; @@ -123,66 +189,49 @@ class BrowserController extends EventEmitter { } /** - * @param page {Object} - * @param cookies {Array} - * @return {Promise} */ - async setCookies(page, cookies) { + async setCookies(page: Page, cookies: BrowserControllerCookie[]): Promise { return this._setCookies(page, cookies); } /** - * - * @param page {Object} - * @return {Promise>} */ - async getCookies(page) { + async getCookies(page: Page): Promise { return this._getCookies(page); } /** - * @return {Promise} - * @private + * @protected */ - async _close() { + async _close(): Promise { throwImplementationNeeded('_close'); } /** - * @return {Promise} - * @private + * @protected */ - async _kill() { + async _kill(): Promise { throwImplementationNeeded('_kill'); } /** - * @param {object} pageOptions - * @return {Promise} - * @private + * @protected */ - async _newPage(pageOptions) { // eslint-disable-line no-unused-vars + async _newPage(_pageOptions?: PageOptions): Promise { // eslint-disable-line @typescript-eslint/no-unused-vars throwImplementationNeeded('_newPage'); } /** - * @param {Page} page - * @param {object[]} cookies - * @return {Promise} - * @private + * @protected */ - async _setCookies(page, cookies) { // eslint-disable-line no-unused-vars + async _setCookies(_page: Page, _cookies: BrowserControllerCookie[]): Promise { // eslint-disable-line @typescript-eslint/no-unused-vars throwImplementationNeeded('_setCookies'); } /** - * @param {Page} page - * @return {Promise>} - * @private + * @protected */ - async _getCookies(page) { // eslint-disable-line no-unused-vars + async _getCookies(_page: Page): Promise { // eslint-disable-line @typescript-eslint/no-unused-vars throwImplementationNeeded('_getCookies'); } } - -module.exports = BrowserController; diff --git a/src/abstract-classes/browser-plugin.js b/src/abstract-classes/browser-plugin.js deleted file mode 100644 index a52a9c4..0000000 --- a/src/abstract-classes/browser-plugin.js +++ /dev/null @@ -1,206 +0,0 @@ -const fs = require('fs-extra'); -const _ = require('lodash'); -const proxyChain = require('proxy-chain'); -const LaunchContext = require('../launch-context'); -const log = require('../logger'); -const { throwImplementationNeeded } = require('./utils'); - -/** - * The `BrowserPlugin` serves two purposes. First, it is the base class that - * specialized controllers like `PuppeteerPlugin` or `PlaywrightPlugin` extend. - * Second, it allows the user to configure the automation libraries and - * feed them to {@link BrowserPool} for use. - */ -class BrowserPlugin { - /** - * @param {object} library - * Each plugin expects an instance of the object with the `.launch()` property. - * For Puppeteer, it is the `puppeteer` module itself, whereas for Playwright - * it is one of the browser types, such as `puppeteer.chromium`. - * `BrowserPlugin` does not include the library. You can choose any version - * or fork of the library. It also keeps `browser-pool` installation small. - * @param {object} [options] - * @param {object} [options.launchOptions] - * Options that will be passed down to the automation library. E.g. - * `puppeteer.launch(launchOptions);`. This is a good place to set - * options that you want to apply as defaults. To dynamically override - * those options per-browser, see the `preLaunchHooks` of {@link BrowserPool}. - * @param {string} [options.proxyUrl] - * Automation libraries configure proxies differently. This helper allows you - * to set a proxy URL without worrying about specific implementations. - * It also allows you use an authenticated proxy without extra code. - * @property {boolean} [useIncognitoPages=false] - * By default pages share the same browser context. - * If set to true each page uses its own context that is destroyed once the page is closed or crashes. - * @property {object} [userDataDir] - * Path to a User Data Directory, which stores browser session data like cookies and local storage. - */ - constructor(library, options = {}) { - const { - launchOptions = {}, - proxyUrl, - useIncognitoPages = false, - userDataDir, - } = options; - - this.name = this.constructor.name; - this.library = library; - this.launchOptions = launchOptions; - this.proxyUrl = proxyUrl && new URL(proxyUrl).href; - this.userDataDir = userDataDir; - this.useIncognitoPages = useIncognitoPages; - } - - /** - * Creates a `LaunchContext` with all the information needed - * to launch a browser. Aside from library specific launch options, - * it also includes internal properties used by `BrowserPool` for - * management of the pool and extra features. - * - * @param {object} [options] - * @param {string} [options.id] - * @param {object} [options.launchOptions] - * @param {string} [options.proxyUrl] - * @property {boolean} [useIncognitoPages] - * If set to false pages use share the same browser context. - * If set to true each page uses its own context that is destroyed once the page is closed or crashes. - * @property {object} [userDataDir] - * Path to a User Data Directory, which stores browser session data like cookies and local storage. - * @return {LaunchContext} - * @ignore - */ - createLaunchContext(options = {}) { - const { - id, - launchOptions = {}, - proxyUrl = this.proxyUrl, - useIncognitoPages = this.useIncognitoPages, - userDataDir = this.userDataDir, - } = options; - - return new LaunchContext({ - id, - launchOptions: _.merge({}, this.launchOptions, launchOptions), - browserPlugin: this, - proxyUrl, - useIncognitoPages, - userDataDir, - }); - } - - /** - * @return {BrowserController} - * @ignore - */ - createController() { - return this._createController(); - } - - /** - * Launches the browser using provided launch context. - * - * @param {LaunchContext} [launchContext] - * @return {Promise} - * @ignore - */ - async launch(launchContext = this.createLaunchContext()) { - const { proxyUrl, useIncognitoPages, userDataDir } = launchContext; - - if (proxyUrl) { - await this._addProxyToLaunchOptions(launchContext); - } - - if (!useIncognitoPages) { - await this._ensureDir(userDataDir); - } - - return this._launch(launchContext); - } - - /** - * @param {LaunchContext} launchContext - * @return {Promise} - * @private - */ - async _addProxyToLaunchOptions(launchContext) { // eslint-disable-line - throwImplementationNeeded('_addProxyToLaunchOptions'); - } - - /** - * @param {LaunchContext} launchContext - * @return {Promise} - * @private - */ - async _launch(launchContext) { // eslint-disable-line - throwImplementationNeeded('_launch'); - } - - /** - * @return {BrowserController} - * @private - */ - _createController() { - throwImplementationNeeded('_createController'); - } - - /** - * Starts proxy-chain server - https://www.npmjs.com/package/proxy-chain#anonymizeproxyproxyurl-callback - * @param {string} proxyUrl - * Proxy URL with username and password. - * @return {Promise} - * URL of the anonymization proxy server that needs to be closed after the proxy is not used anymore. - * @private - */ - async _getAnonymizedProxyUrl(proxyUrl) { - let anonymizedProxyUrl; - try { - anonymizedProxyUrl = await proxyChain.anonymizeProxy(proxyUrl); - } catch (e) { - throw new Error(`BrowserPool: Could not anonymize proxyUrl: ${proxyUrl}. Reason: ${e.message}.`); - } - - return anonymizedProxyUrl; - } - - /** - * - * @param {string} proxyUrl - * Anonymized proxy URL of a running proxy server. - * @return {Promise} - * @private - */ - async _closeAnonymizedProxy(proxyUrl) { - return proxyChain.closeAnonymizedProxy(proxyUrl, true).catch((err) => { - log.debug(`Could not close anonymized proxy server.\nCause:${err.message}`); - }); - } - - /** - * Checks if proxy URL should be anonymized. - * @param {string} proxyUrl - * @return {boolean} - * @private - */ - _shouldAnonymizeProxy(proxyUrl) { - const parsedProxyUrl = proxyChain.parseUrl(proxyUrl); - if (parsedProxyUrl.username || parsedProxyUrl.password) { - if (parsedProxyUrl.scheme !== 'http') { - throw new Error('Invalid "proxyUrl" option: authentication is only supported for HTTP proxy type.'); - } - return true; - } - - return false; - } - - /** - * - * @param {string} dir - Absolute path to the directory. - * @returns {Promise} - */ - async _ensureDir(dir) { - return fs.ensureDir(dir); - } -} - -module.exports = BrowserPlugin; diff --git a/src/abstract-classes/browser-plugin.ts b/src/abstract-classes/browser-plugin.ts new file mode 100644 index 0000000..e096851 --- /dev/null +++ b/src/abstract-classes/browser-plugin.ts @@ -0,0 +1,211 @@ +import fs from 'fs-extra'; +import LaunchContext, { LaunchContextOptions } from '../launch-context'; +import type BrowserController from './browser-controller'; +import log from '../logger'; +import { throwImplementationNeeded } from './utils'; + +const merge = require('lodash.merge'); +const proxyChain = require('proxy-chain'); + +/** + * Each plugin expects an instance of the object with the `.launch()` property. + * For Puppeteer, it is the `puppeteer` module itself, whereas for Playwright + * it is one of the browser types, such as `puppeteer.chromium`. + * `BrowserPlugin` does not include the library. You can choose any version + * or fork of the library. It also keeps `browser-pool` installation small. + */ +export type Browser = any; + +export interface BrowserPluginOptions> { + /** + * Options that will be passed down to the automation library. E.g. + * `puppeteer.launch(launchOptions);`. This is a good place to set + * options that you want to apply as defaults. To dynamically override + * those options per-browser, see the `preLaunchHooks` of {@link BrowserPool}. + */ + launchOptions?: LaunchOptions; + /** + * Automation libraries configure proxies differently. This helper allows you + * to set a proxy URL without worrying about specific implementations. + * It also allows you use an authenticated proxy without extra code. + */ + proxyUrl?: string; + /** + * By default pages share the same browser context. + * If set to true each page uses its own context that is destroyed once the page is closed or crashes. + */ + useIncognitoPages?: boolean; + /** + * Path to a User Data Directory, which stores browser session data like cookies and local storage. + */ + userDataDir?: string; +} + +/** + * The `BrowserPlugin` serves two purposes. First, it is the base class that + * specialized controllers like `PuppeteerPlugin` or `PlaywrightPlugin` extend. + * Second, it allows the user to configure the automation libraries and + * feed them to {@link BrowserPool} for use. + */ +export default class BrowserPlugin< + BrowserLibrary extends Browser, + Page extends object, + LaunchOptions extends Record, + PageOptions extends Record, +> { + name: string; + + library: BrowserLibrary; + + launchOptions: NonNullable; + + proxyUrl: BrowserPluginOptions['proxyUrl']; + + useIncognitoPages: NonNullable['useIncognitoPages']>; + + userDataDir: BrowserPluginOptions['userDataDir']; + + constructor(library: BrowserLibrary, options: BrowserPluginOptions) { + const { + launchOptions = {}, + proxyUrl, + useIncognitoPages = false, + userDataDir, + } = options; + + this.name = this.constructor.name; + this.library = library; + this.launchOptions = launchOptions as any; + this.proxyUrl = proxyUrl && new URL(proxyUrl).href; + this.userDataDir = userDataDir; + this.useIncognitoPages = useIncognitoPages; + } + + /** + * Creates a `LaunchContext` with all the information needed + * to launch a browser. Aside from library specific launch options, + * it also includes internal properties used by `BrowserPool` for + * management of the pool and extra features. + * @ignore + */ + createLaunchContext(options: Partial>): LaunchContext { + const { + id, + launchOptions = {}, + proxyUrl = this.proxyUrl, + useIncognitoPages = this.useIncognitoPages, + userDataDir = this.userDataDir, + } = options; + + return new LaunchContext({ + id, + launchOptions: merge({}, this.launchOptions, launchOptions), + browserPlugin: this, + proxyUrl, + useIncognitoPages, + userDataDir, + }); + } + + /** + * @ignore + */ + createController(): BrowserController { + return this._createController(); + } + + /** + * Launches the browser using provided launch context. + * + * @ignore + */ + async launch(launchContext: LaunchContext): Promise { + const { proxyUrl, useIncognitoPages, userDataDir } = launchContext; + + if (proxyUrl) { + await this._addProxyToLaunchOptions(launchContext); + } + + if (!useIncognitoPages) { + await this._ensureDir(userDataDir); + } + + return this._launch(launchContext); + } + + /** + * @private + */ + async _addProxyToLaunchOptions(_launchContext: LaunchContext): Promise { // eslint-disable-line + throwImplementationNeeded('_addProxyToLaunchOptions'); + } + + /** + * @private + */ + async _launch(_launchContext: LaunchContext): Promise { // eslint-disable-line + throwImplementationNeeded('_launch'); + } + + /** + * @private + */ + _createController(): BrowserController { + throwImplementationNeeded('_createController'); + } + + /** + * Starts proxy-chain server - https://www.npmjs.com/package/proxy-chain#anonymizeproxyproxyurl-callback + * @param {string} proxyUrl + * Proxy URL with username and password. + * @return + * URL of the anonymization proxy server that needs to be closed after the proxy is not used anymore. + * @private + */ + async _getAnonymizedProxyUrl(proxyUrl: string): Promise { + let anonymizedProxyUrl; + try { + anonymizedProxyUrl = await proxyChain.anonymizeProxy(proxyUrl); + } catch (e) { + throw new Error(`BrowserPool: Could not anonymize proxyUrl: ${proxyUrl}. Reason: ${e.message}.`); + } + + return anonymizedProxyUrl; + } + + /** + * @param {string} proxyUrl + * Anonymized proxy URL of a running proxy server. + * @private + */ + async _closeAnonymizedProxy(proxyUrl: string): Promise { + return proxyChain.closeAnonymizedProxy(proxyUrl, true).catch((err: Error) => { + log.debug(`Could not close anonymized proxy server.\nCause:${err.message}`); + }); + } + + /** + * Checks if proxy URL should be anonymized. + * @private + */ + _shouldAnonymizeProxy(proxyUrl?: string): boolean { + if (proxyUrl) { + const parsedProxyUrl = proxyChain.parseUrl(proxyUrl); + if (parsedProxyUrl.username || parsedProxyUrl.password) { + if (parsedProxyUrl.scheme !== 'http') { + throw new Error('Invalid "proxyUrl" option: authentication is only supported for HTTP proxy type.'); + } + return true; + } + } + + return false; + } + + /** + * @param {string} dir - Absolute path to the directory. + */ + async _ensureDir(dir: string): Promise { + return fs.ensureDir(dir); + } +} diff --git a/src/abstract-classes/utils.js b/src/abstract-classes/utils.js deleted file mode 100644 index f76aca5..0000000 --- a/src/abstract-classes/utils.js +++ /dev/null @@ -1,7 +0,0 @@ -function throwImplementationNeeded(methodName) { - throw new Error(`You need to implement method ${methodName}.`); -} - -module.exports = { - throwImplementationNeeded, -}; diff --git a/src/abstract-classes/utils.ts b/src/abstract-classes/utils.ts new file mode 100644 index 0000000..d0e996d --- /dev/null +++ b/src/abstract-classes/utils.ts @@ -0,0 +1,3 @@ +export function throwImplementationNeeded(methodName: string): never { + throw new Error(`You need to implement method ${methodName}.`); +} diff --git a/src/browser-pool.js b/src/browser-pool.ts similarity index 54% rename from src/browser-pool.js rename to src/browser-pool.ts index e64f721..f0b2cac 100644 --- a/src/browser-pool.js +++ b/src/browser-pool.ts @@ -1,8 +1,12 @@ -const EventEmitter = require('events'); -const ow = require('ow').default; -const { nanoid } = require('nanoid'); -const log = require('./logger'); -const { addTimeoutToPromise } = require('./utils'); +import EventEmitter from 'events'; +import { nanoid } from 'nanoid'; +import ow from 'ow'; +import log from './logger'; +import { addTimeoutToPromise } from './utils'; +import type BrowserController from './abstract-classes/browser-controller'; +import type BrowserPlugin from './abstract-classes/browser-plugin'; // eslint-disable-line import/no-duplicates +import type { Browser } from './abstract-classes/browser-plugin'; // eslint-disable-line import/no-duplicates +import type LaunchContext from './launch-context'; const { BROWSER_POOL_EVENTS: { @@ -16,6 +20,152 @@ const { const PAGE_CLOSE_KILL_TIMEOUT_MILLIS = 1000; const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000; +export interface BrowserPoolNewPageOptions { + /** + * Assign a custom ID to the page. If you don't a random string ID + * will be generated. + */ + id?: string; + /** + * Some libraries (Playwright) allow you to open new pages with specific + * options. Use this property to set those options. + */ + pageOptions?: PageOptions; + /** + * Options that will be used to launch the new browser. + */ + launchOptions?: LaunchOptions; + /** + * Provide a plugin to launch the browser. If none is provided, + * one of the pool's available plugins will be used. + * + * If you configured `BrowserPool` to rotate multiple libraries, + * such as both Puppeteer and Playwright, you should always set + * the `browserPlugin` when using the `launchOptions` option. + * + * The plugin will not be added to the list of plugins used by + * the pool. You can either use one of those, to launch a specific + * browser, or provide a completely new configuration. + */ + browserPlugin?: BrowserPlugin; +} + +// extract the types here so it's easier to assign them to the code as a consumer + +export type BrowserPoolPreLaunchHook = ( + pageId: string, + launchContext: LaunchContext +) => Promise; + +export type BrowserPoolPostLaunchHook = ( + pageId: string, + browserController: BrowserController +) => Promise; + +export type BrowserPoolPrePageCreateHook = ( + pageId: string, + browserController: BrowserController, + pageOptions: PageOptions, +) => Promise; + +export type BrowserPoolPostPageCreateHook = ( + page: Page, + browserController: BrowserController, +) => Promise; + +export type BrowserPoolPrePageCloseHook = ( + page: Page, + browserController: BrowserController, +) => Promise; + +export type BrowserPoolPostPageCloseHook = ( + pageId: string, + browserController: BrowserController, +) => Promise; + +export interface BrowserPoolOptions { + /** + * Browser plugins are wrappers of browser automation libraries that + * allow `BrowserPool` to control browsers with those libraries. + * `browser-pool` comes with a `PuppeteerPlugin` and a `PlaywrightPlugin`. + */ + browserPlugins: BrowserPlugin[]; + /** + * Sets the maximum number of pages that can be open in a browser at the + * same time. Once reached, a new browser will be launched to handle the excess. + */ + maxOpenPagesPerBrowser?: number; + /** + * Browsers tend to get bloated after processing a lot of pages. This option + * configures the number of processed pages after which the browser will + * automatically retire and close. A new browser will launch in its place. + */ + retireBrowserAfterPageCount?: number; + /** + * As we know from experience, async operations of the underlying libraries, + * such as launching a browser or opening a new page, can get stuck. + * To prevent `BrowserPool` from getting stuck, we add a timeout + * to those operations and you can configure it with this option. + */ + operationTimeoutSecs?: number; + /** + * Browsers normally close immediately after their last page is processed. + * However, there could be situations where this does not happen. Browser Pool + * makes sure all inactive browsers are closed regularly, to free resources. + */ + closeInactiveBrowserAfterSecs?: number; + /** + * Pre-launch hooks are executed just before a browser is launched and provide + * a good opportunity to dynamically change the launch options. + * The hooks are called with two arguments: + * `pageId`: `string` and `launchContext`: {@link LaunchContext} + */ + preLaunchHooks?: Array>; + /** + * Post-launch hooks are executed as soon as a browser is launched. + * The hooks are called with two arguments: + * `pageId`: `string` and `browserController`: {@link BrowserController} + * To guarantee order of execution before other hooks in the same browser, + * the {@link BrowserController} methods cannot be used until the post-launch + * hooks complete. If you attempt to call `await browserController.close()` from + * a post-launch hook, it will deadlock the process. This API is subject to change. + */ + postLaunchHooks?: Array>; + /** + * Pre-page-create hooks are executed just before a new page is created. They + * are useful to make dynamic changes to the browser before opening a page. + * The hooks are called with two arguments: + * `pageId`: `string`, `browserController`: {@link BrowserController} and + * `pageOptions`: `object|undefined` - This only works if the underlying `BrowserController` supports new page options. + * So far, new page options are only supported by `PlaywrightController`. + * If the page options are not supported by `BrowserController` the `pageOptions` argument is `undefined`. + */ + prePageCreateHooks?: Array>; + /** + * Post-page-create hooks are called right after a new page is created + * and all internal actions of Browser Pool are completed. This is the + * place to make changes to a page that you would like to apply to all + * pages. Such as injecting a JavaScript library into all pages. + * The hooks are called with two arguments: + * `page`: `Page` and `browserController`: {@link BrowserController} + */ + postPageCreateHooks?: Array>; + /** + * Pre-page-close hooks give you the opportunity to make last second changes + * in a page that's about to be closed, such as saving a snapshot or updating + * state. + * The hooks are called with two arguments: + * `page`: `Page` and `browserController`: {@link BrowserController} + */ + prePageCloseHooks?: Array>; + /** + * Post-page-close hooks allow you to do page related clean up. + * The hooks are called with two arguments: + * `pageId`: `string` and `browserController`: {@link BrowserController} + */ + postPageCloseHooks?: Array>; +} + /** * The `BrowserPool` class is the most important class of the `browser-pool` module. * It manages opening and closing of browsers and their pages and its constructor @@ -62,69 +212,51 @@ const BROWSER_KILLER_INTERVAL_MILLIS = 10 * 1000; * }] * }); * ``` - * - * @param {object} options - * @param {BrowserPlugin[]} options.browserPlugins - * Browser plugins are wrappers of browser automation libraries that - * allow `BrowserPool` to control browsers with those libraries. - * `browser-pool` comes with a `PuppeteerPlugin` and a `PlaywrightPlugin`. - * @param {number} [options.maxOpenPagesPerBrowser=20] - * Sets the maximum number of pages that can be open in a browser at the - * same time. Once reached, a new browser will be launched to handle the excess. - * @param {number} [options.retireBrowserAfterPageCount=100] - * Browsers tend to get bloated after processing a lot of pages. This option - * configures the number of processed pages after which the browser will - * automatically retire and close. A new browser will launch in its place. - * @param {number} [options.operationTimeoutSecs=15] - * As we know from experience, async operations of the underlying libraries, - * such as launching a browser or opening a new page, can get stuck. - * To prevent `BrowserPool` from getting stuck, we add a timeout - * to those operations and you can configure it with this option. - * @param {number} [options.closeInactiveBrowserAfterSecs=300] - * Browsers normally close immediately after their last page is processed. - * However, there could be situations where this does not happen. Browser Pool - * makes sure all inactive browsers are closed regularly, to free resources. - * @param {function[]} [options.preLaunchHooks] - * Pre-launch hooks are executed just before a browser is launched and provide - * a good opportunity to dynamically change the launch options. - * The hooks are called with two arguments: - * `pageId`: `string` and `launchContext`: {@link LaunchContext} - * @param {function[]} [options.postLaunchHooks] - * Post-launch hooks are executed as soon as a browser is launched. - * The hooks are called with two arguments: - * `pageId`: `string` and `browserController`: {@link BrowserController} - * To guarantee order of execution before other hooks in the same browser, - * the {@link BrowserController} methods cannot be used until the post-launch - * hooks complete. If you attempt to call `await browserController.close()` from - * a post-launch hook, it will deadlock the process. This API is subject to change. - * @param {function[]} [options.prePageCreateHooks] - * Pre-page-create hooks are executed just before a new page is created. They - * are useful to make dynamic changes to the browser before opening a page. - * The hooks are called with two arguments: - * `pageId`: `string`, `browserController`: {@link BrowserController} and - * `pageOptions`: `object|undefined` - This only works if the underlying `BrowserController` supports new page options. - * So far, new page options are only supported by `PlaywrightController`. - * If the page options are not supported by `BrowserController` the `pageOptions` argument is `undefined`. - * @param {function[]} [options.postPageCreateHooks] - * Post-page-create hooks are called right after a new page is created - * and all internal actions of Browser Pool are completed. This is the - * place to make changes to a page that you would like to apply to all - * pages. Such as injecting a JavaScript library into all pages. - * The hooks are called with two arguments: - * `page`: `Page` and `browserController`: {@link BrowserController} - * @param {function[]} [options.prePageCloseHooks] - * Pre-page-close hooks give you the opportunity to make last second changes - * in a page that's about to be closed, such as saving a snapshot or updating - * state. - * The hooks are called with two arguments: - * `page`: `Page` and `browserController`: {@link BrowserController} - * @param {function[]} [options.postPageCloseHooks] - * Post-page-close hooks allow you to do page related clean up. - * The hooks are called with two arguments: - * `pageId`: `string` and `browserController`: {@link BrowserController} */ -class BrowserPool extends EventEmitter { - constructor(options = {}) { +export default class BrowserPool< + BrowserLibrary extends Browser, + Page extends object, + LaunchOptions extends Record, + PageOptions extends Record, +> extends EventEmitter { + browserPlugins: BrowserPoolOptions['browserPlugins']; + + maxOpenPagesPerBrowser: NonNullable['maxOpenPagesPerBrowser']>; + + retireBrowserAfterPageCount: NonNullable['retireBrowserAfterPageCount']>; + + operationTimeoutMillis: number; + + closeInactiveBrowserAfterMillis: number; + + // hooks + preLaunchHooks: BrowserPoolPreLaunchHook[]; + + postLaunchHooks: BrowserPoolPostLaunchHook[]; + + prePageCreateHooks: BrowserPoolPrePageCreateHook[]; + + postPageCreateHooks: BrowserPoolPostPageCreateHook[]; + + prePageCloseHooks: BrowserPoolPrePageCloseHook[]; + + postPageCloseHooks: BrowserPoolPostPageCloseHook[]; + + pageCounter: number; + + pages: Map; + + pageIds: WeakMap; + + activeBrowserControllers: Set>; + + retiredBrowserControllers: Set>; + + pageToBrowserController: WeakMap>; + + browserKillerInterval: NodeJS.Timeout | void; + + constructor(options: BrowserPoolOptions) { ow(options, ow.object.exactShape({ browserPlugins: ow.array.minLength(1), maxOpenPagesPerBrowser: ow.optional.number, @@ -186,24 +318,8 @@ class BrowserPool extends EventEmitter { * Opens a new page in one of the running browsers or launches * a new browser and opens a page there, if no browsers are active, * or their page limits have been exceeded. - * - * @param {object} options - * @param {string} [options.id] - * Assign a custom ID to the page. If you don't a random string ID - * will be generated. - * @param {object} [options.pageOptions] - * Some libraries (Playwright) allow you to open new pages with specific - * options. Use this property to set those options. - * @param {BrowserPlugin} [options.browserPlugin] - * Choose a plugin to open the page with. If none is provided, - * one of the pool's available plugins will be used. - * - * It must be one of the plugins browser pool was created with. - * If you wish to start a browser with a different configuration, - * see the `newPageInNewBrowser` function. - * @return {Promise} */ - async newPage(options = {}) { + async newPage(options: BrowserPoolNewPageOptions = {}): Promise { const { id = nanoid(), pageOptions, @@ -220,7 +336,9 @@ class BrowserPool extends EventEmitter { let browserController = this._pickBrowserWithFreeCapacity(browserPlugin); - if (!browserController) browserController = await this._launchBrowser(id, { browserPlugin }); + if (!browserController) { + browserController = await this._launchBrowser(id, { browserPlugin }); + } return this._createPageForBrowser(id, browserController, pageOptions); } @@ -228,30 +346,8 @@ class BrowserPool extends EventEmitter { * Unlike {@link newPage}, `newPageInNewBrowser` always launches a new * browser to open the page in. Use the `launchOptions` option to * configure the new browser. - * - * @param {object} options - * @param {string} [options.id] - * Assign a custom ID to the page. If you don't a random string ID - * will be generated. - * @param {object} [options.pageOptions] - * Some libraries (Playwright) allow you to open new pages with specific - * options. Use this property to set those options. - * @param {object} [options.launchOptions] - * Options that will be used to launch the new browser. - * @param {BrowserPlugin} [options.browserPlugin] - * Provide a plugin to launch the browser. If none is provided, - * one of the pool's available plugins will be used. - * - * If you configured `BrowserPool` to rotate multiple libraries, - * such as both Puppeteer and Playwright, you should always set - * the `browserPlugin` when using the `launchOptions` option. - * - * The plugin will not be added to the list of plugins used by - * the pool. You can either use one of those, to launch a specific - * browser, or provide a completely new configuration. - * @return {Promise} */ - async newPageInNewBrowser(options = {}) { + async newPageInNewBrowser(options: BrowserPoolNewPageOptions = {}): Promise { const { id = nanoid(), pageOptions, @@ -287,18 +383,17 @@ class BrowserPool extends EventEmitter { * const pages = await browserPool.newPageWithEachPlugin(); * const [chromiumPage, firefoxPage, webkitPage, puppeteerPage] = pages; * ``` - * - * @param {object[]} optionsList - * @return {Promise} */ - async newPageWithEachPlugin(optionsList = []) { + async newPageWithEachPlugin(optionsList: any[] = []): Promise { const pagePromises = this.browserPlugins.map((browserPlugin, idx) => { const userOptions = optionsList[idx] || {}; + return this.newPage({ ...userOptions, browserPlugin, }); }); + return Promise.all(pagePromises); } @@ -312,11 +407,8 @@ class BrowserPool extends EventEmitter { * cause weird things to happen, so please always use `BrowserController` * to control your browsers. The function returns `undefined` if the * browser is closed. - * - * @param page {Page} - Browser plugin page - * @return {?BrowserController} */ - getBrowserControllerByPage(page) { + getBrowserControllerByPage(page: Page): BrowserController | undefined { return this.pageToBrowserController.get(page); } @@ -325,11 +417,8 @@ class BrowserPool extends EventEmitter { * randomly generated one, you can use this function to retrieve * the page. If the page is no longer open, the function will * return `undefined`. - * - * @param {string} id - * @return {?Page} */ - getPage(id) { + getPage(id: string): Page | undefined { return this.pages.get(id); } @@ -338,22 +427,19 @@ class BrowserPool extends EventEmitter { * events. You can use a page ID to track the full lifecycle of the page. * It is created even before a browser is launched and stays with the page * until it's closed. - * - * @param {Page} page - * @return {string} */ - getPageId(page) { + getPageId(page: Page): string | undefined { return this.pageIds.get(page); } /** - * @param {string} pageId - * @param {BrowserController} browserController - * @param {object} pageOptions - * @return {Promise} * @private */ - async _createPageForBrowser(pageId, browserController, pageOptions = {}) { + async _createPageForBrowser( + pageId: string, + browserController: BrowserController, + pageOptions: PageOptions = {} as PageOptions, + ): Promise { // TODO This is needed for concurrent newPage calls to wait for the browser launch. // It's not ideal though, we need to come up with a better API. await browserController.isActivePromise; @@ -391,10 +477,10 @@ class BrowserPool extends EventEmitter { /** * Removes a browser controller from the pool. The underlying * browser will be closed after all its pages are closed. - * @param {BrowserController} browserController + * @param {} browserController * */ - retireBrowserController(browserController) { + retireBrowserController(browserController: BrowserController) { const hasBeenRetiredOrKilled = !this.activeBrowserControllers.has(browserController); if (hasBeenRetiredOrKilled) return; @@ -406,11 +492,12 @@ class BrowserPool extends EventEmitter { /** * Removes a browser from the pool. It will be * closed after all its pages are closed. - * @param {Page} page */ - retireBrowserByPage(page) { + retireBrowserByPage(page: Page) { const browserController = this.getBrowserControllerByPage(page); - this.retireBrowserController(browserController); + if (browserController) { + this.retireBrowserController(browserController); + } } /** @@ -425,11 +512,10 @@ class BrowserPool extends EventEmitter { /** * Closes all managed browsers without waiting for pages to close. - * @return {Promise} */ - async closeAllBrowsers() { + async closeAllBrowsers(): Promise { const controllers = this._getAllBrowserControllers(); - const promises = []; + const promises: Promise[] = []; controllers.forEach((controller) => { promises.push(controller.close()); }); @@ -438,14 +524,18 @@ class BrowserPool extends EventEmitter { /** * Closes all managed browsers and tears down the pool. - * @return {Promise} */ - async destroy() { - this.browserKillerInterval = clearInterval(this.browserKillerInterval); + async destroy(): Promise { + this.browserKillerInterval = this.browserKillerInterval ? clearInterval(this.browserKillerInterval) : undefined; await this.closeAllBrowsers(); this._teardown(); } + /** + * @internal + * @private + * @ignore + */ _teardown() { this.activeBrowserControllers.clear(); this.retiredBrowserControllers.clear(); @@ -454,22 +544,23 @@ class BrowserPool extends EventEmitter { } /** - * @return {Set} + * @internal * @private + * @ignore */ - _getAllBrowserControllers() { + _getAllBrowserControllers(): Set> { return new Set([...this.activeBrowserControllers, ...this.retiredBrowserControllers]); } /** - * @param {string} pageId - * @param {object} options - * @param {BrowserPlugin} options.browserPlugin - * @param {object} [options.launchOptions] - * @return {Promise} + * @internal * @private + * @ignore */ - async _launchBrowser(pageId, options = {}) { + async _launchBrowser(pageId: string, options: { + browserPlugin: BrowserPlugin, + launchOptions?: LaunchOptions, + }): Promise> { const { browserPlugin, launchOptions, @@ -497,10 +588,11 @@ class BrowserPool extends EventEmitter { /** * Picks plugins round robin. - * @return {BrowserPlugin} + * @internal * @private + * @ignore */ - _pickBrowserPlugin() { + _pickBrowserPlugin(): BrowserPlugin { const pluginIndex = this.pageCounter % this.browserPlugins.length; this.pageCounter++; @@ -508,11 +600,11 @@ class BrowserPool extends EventEmitter { } /** - * @param {BrowserPlugin} browserPlugin - * @return {BrowserController} + * @ignore + * @internal * @private */ - _pickBrowserWithFreeCapacity(browserPlugin) { + _pickBrowserWithFreeCapacity(browserPlugin: BrowserPlugin): BrowserController | undefined { return Array.from(this.activeBrowserControllers.values()) .find((controller) => { // TODO if you synchronously trigger a lot of page launches, controller.activePages @@ -524,8 +616,13 @@ class BrowserPool extends EventEmitter { }); } + /** + * @internal + * @ignore + * @private + */ async _closeInactiveRetiredBrowsers() { - const closedBrowserIds = []; + const closedBrowserIds: string[] = []; this.retiredBrowserControllers.forEach((controller) => { const millisSinceLastPageOpened = Date.now() - controller.lastPageOpenedAt; @@ -550,44 +647,51 @@ class BrowserPool extends EventEmitter { } /** - * @param {Page} page + * @ignore * @private */ - _overridePageClose(page) { - const originalPageClose = page.close; + _overridePageClose(page: Page) { + const originalPageClose = (page as any).close; const browserController = this.pageToBrowserController.get(page); + const pageId = this.getPageId(page); + if (!browserController || !pageId) { + return; + } - page.close = async (...args) => { + (page as any).close = async (...args: any) => { await this._executeHooks(this.prePageCloseHooks, page, browserController); await originalPageClose.apply(page, args) - .catch((err) => { + .catch((err: Error) => { log.debug(`Could not close page.\nCause:${err.message}`, { id: browserController.id }); }); await this._executeHooks(this.postPageCloseHooks, pageId, browserController); - this.pages.delete(this.getPageId(page)); + const nPageId = this.getPageId(page); + if (nPageId) { + this.pages.delete(nPageId); + } this._closeRetiredBrowserWithNoPages(browserController); this.emit(PAGE_CLOSED, page); }; } /** - * @param {function[]} hooks - * @param {...*} args - * @return {Promise} + * @ignore * @private + * @internal */ - async _executeHooks(hooks, ...args) { + async _executeHooks(hooks: Array<(...args: any) => Promise>, ...args: any): Promise { for (const hook of hooks) { await hook(...args); } } /** - * @param {BrowserController} browserController + * @ignore * @private + * @internal */ - _closeRetiredBrowserWithNoPages(browserController) { + _closeRetiredBrowserWithNoPages(browserController: BrowserController): void { if (browserController.activePages === 0 && this.retiredBrowserControllers.has(browserController)) { // Run this with a delay, otherwise page.close() // might fail with "Protocol error (Target.closeTarget): Target closed." @@ -599,5 +703,3 @@ class BrowserPool extends EventEmitter { } } } - -module.exports = BrowserPool; diff --git a/src/events.js b/src/events.ts similarity index 60% rename from src/events.js rename to src/events.ts index 9b5c5c3..43a1289 100644 --- a/src/events.js +++ b/src/events.ts @@ -1,4 +1,4 @@ -const BROWSER_POOL_EVENTS = { +export const BROWSER_POOL_EVENTS = { BROWSER_LAUNCHED: 'browserLaunched', BROWSER_RETIRED: 'browserRetired', BROWSER_CLOSED: 'browserClosed', @@ -7,11 +7,6 @@ const BROWSER_POOL_EVENTS = { PAGE_CLOSED: 'pageClosed', }; -const BROWSER_CONTROLLER_EVENTS = { +export const BROWSER_CONTROLLER_EVENTS = { BROWSER_CLOSED: 'browserClosed', }; - -module.exports = { - BROWSER_POOL_EVENTS, - BROWSER_CONTROLLER_EVENTS, -}; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index fccd713..0000000 --- a/src/index.js +++ /dev/null @@ -1,36 +0,0 @@ -const BrowserPool = require('./browser-pool'); -const PuppeteerPlugin = require('./puppeteer/puppeteer-plugin'); -const PlaywrightPlugin = require('./playwright/playwright-plugin'); - -/** - * The `browser-pool` module exports three constructors. One for `BrowserPool` - * itself and two for the included Puppeteer and Playwright plugins. - * - * **Example:** - * ```js - * const { - * BrowserPool, - * PuppeteerPlugin, - * PlaywrightPlugin - * } = require('browser-pool'); - * const puppeteer = require('puppeteer'); - * const playwright = require('playwright'); - * - * const browserPool = new BrowserPool({ - * browserPlugins: [ - * new PuppeteerPlugin(puppeteer), - * new PlaywrightPlugin(playwright.chromium), - * ] - * }); - * ``` - * - * @property {BrowserPool} BrowserPool - * @property {PuppeteerPlugin} PuppeteerPlugin - * @property {PlaywrightPlugin} PlaywrightPlugin - * @module browser-pool - */ -module.exports = { - BrowserPool, - PuppeteerPlugin, - PlaywrightPlugin, -}; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..9304fcd --- /dev/null +++ b/src/index.ts @@ -0,0 +1,56 @@ +import BrowserPool, { + BrowserPoolOptions, + BrowserPoolNewPageOptions, + BrowserPoolPostLaunchHook, + BrowserPoolPostPageCloseHook, + BrowserPoolPostPageCreateHook, + BrowserPoolPreLaunchHook, + BrowserPoolPrePageCloseHook, + BrowserPoolPrePageCreateHook, +} from './browser-pool'; +import BrowserPlugin, { BrowserPluginOptions, Browser } from './abstract-classes/browser-plugin'; +import BrowserController, { BrowserControllerCookie } from './abstract-classes/browser-controller'; +import PuppeteerPlugin from './puppeteer/puppeteer-plugin'; +import PlaywrightPlugin, { PlaywrightLaunchContext } from './playwright/playwright-plugin'; + +/** + * The `browser-pool` module exports three constructors. One for `BrowserPool` + * itself and two for the included Puppeteer and Playwright plugins. + * + * **Example:** + * ```js + * const { + * BrowserPool, + * PuppeteerPlugin, + * PlaywrightPlugin + * } = require('browser-pool'); + * const puppeteer = require('puppeteer'); + * const playwright = require('playwright'); + * + * const browserPool = new BrowserPool({ + * browserPlugins: [ + * new PuppeteerPlugin(puppeteer), + * new PlaywrightPlugin(playwright.chromium), + * ] + * }); + * ``` + */ +export { + Browser, + BrowserPool, + BrowserPlugin, + BrowserController, + BrowserPoolOptions, + BrowserPluginOptions, + BrowserPoolNewPageOptions, + BrowserPoolPostLaunchHook, + BrowserPoolPostPageCloseHook, + BrowserPoolPostPageCreateHook, + BrowserPoolPreLaunchHook, + BrowserPoolPrePageCloseHook, + BrowserPoolPrePageCreateHook, + BrowserControllerCookie, + PlaywrightPlugin, + PlaywrightLaunchContext, + PuppeteerPlugin, +}; diff --git a/src/launch-context.js b/src/launch-context.js deleted file mode 100644 index 6d66b7e..0000000 --- a/src/launch-context.js +++ /dev/null @@ -1,96 +0,0 @@ -const path = require('path'); -const os = require('os'); -const { nanoid } = require('nanoid'); - -/** - * `LaunchContext` holds information about the launched browser. It's useful - * to retrieve the `launchOptions`, the proxy the browser was launched with - * or any other information user chose to add to the `LaunchContext` by calling - * its `extend` function. This is very useful to keep track of browser-scoped - * values, such as session IDs. - * @property {string} id - * To make identification of `LaunchContext` easier, `BrowserPool` assigns - * the `LaunchContext` an `id` that's equal to the `id` of the page that - * triggered the browser launch. This is useful, because many pages share - * a single launch context (single browser). - * @property {BrowserPlugin} browserPlugin - * The `BrowserPlugin` instance used to launch the browser. - * @property {object} launchOptions - * The actual options the browser was launched with, after changes. - * Those changes would be typically made in pre-launch hooks. - * @property {boolean} [useIncognitoPages] - * By default pages share the same browser context. - * If set to true each page uses its own context that is destroyed once the page is closed or crashes. - * @property {object} [userDataDir] - * Path to a User Data Directory, which stores browser session data like cookies and local storage. - * @hideconstructor - */ -class LaunchContext { - /** - * @param {object} options - * @param {BrowserPlugin} options.browserPlugin - * @param {object} options.launchOptions - * @param {string} [options.id] - * @param {string} [options.proxyUrl] - * @param {boolean} [options.useIncognitoPages] - * @param {string} [options.userDataDir] - */ - constructor(options) { - const { - id, - browserPlugin, - launchOptions, - proxyUrl, - useIncognitoPages, - userDataDir = path.join(os.tmpdir(), nanoid()), - } = options; - - this.id = id; - this.browserPlugin = browserPlugin; - this.launchOptions = launchOptions; - this.useIncognitoPages = useIncognitoPages; - this.userDataDir = userDataDir; - - this._proxyUrl = proxyUrl; - this._reservedFieldNames = Reflect.ownKeys(this); - } - - /** - * Extend the launch context with any extra fields. - * This is useful to keep state information relevant - * to the browser being launched. It ensures that - * no internal fields are overridden and should be - * used instead of property assignment. - * - * @param {object} fields - */ - extend(fields) { - Object.entries(fields).forEach(([key, value]) => { - if (this._reservedFieldNames.includes(key)) { - throw new Error(`Cannot extend LaunchContext with key: ${key}, because it's reserved.`); - } else { - this[key] = value; - } - }); - } - - /** - * Sets a proxy URL for the browser. - * Use `undefined` to unset existing proxy URL. - * - * @param {?string} url - */ - set proxyUrl(url) { - this._proxyUrl = url && new URL(url).href; - } - - /** - * Returns the proxy URL of the browser. - * @return {string} - */ - get proxyUrl() { - return this._proxyUrl; - } -} - -module.exports = LaunchContext; diff --git a/src/launch-context.ts b/src/launch-context.ts new file mode 100644 index 0000000..b3cf5cd --- /dev/null +++ b/src/launch-context.ts @@ -0,0 +1,116 @@ +import path from 'path'; +import os from 'os'; +import { nanoid } from 'nanoid'; +import type BrowserPlugin from './abstract-classes/browser-plugin'; // eslint-disable-line import/no-duplicates +import type { Browser } from './abstract-classes/browser-plugin'; // eslint-disable-line import/no-duplicates + +export interface LaunchContextOptions extends LaunchContextDynamicProps { + /** + * The `BrowserPlugin` instance used to launch the browser. + */ + browserPlugin: BrowserPlugin; + /** + * To make identification of `LaunchContext` easier, `BrowserPool` assigns + * the `LaunchContext` an `id` that's equal to the `id` of the page that + * triggered the browser launch. This is useful, because many pages share + * a single launch context (single browser). + */ + id?: string; + launchOptions?: LaunchOptions; + proxyUrl?: string; + /** + * By default pages share the same browser context. + * If set to true each page uses its own context that is destroyed once the page is closed or crashes. + */ + useIncognitoPages?: boolean; + /** + * Path to a User Data Directory, which stores browser session data like cookies and local storage. + */ + userDataDir?: string; +} + +export interface LaunchContextDynamicProps { + [key: string]: any; +} + +/** + * `LaunchContext` holds information about the launched browser. It's useful + * to retrieve the `launchOptions`, the proxy the browser was launched with + * or any other information user chose to add to the `LaunchContext` by calling + * its `extend` function. This is very useful to keep track of browser-scoped + * values, such as session IDs. + * + * @hideconstructor + */ +export default class LaunchContext< + BrowserLibrary extends Browser, + Page extends object, // eslint-disable-line + LaunchOptions extends Record, + PageOptions extends Record, +> implements LaunchContextDynamicProps { + id: NonNullable['id']>; + + launchOptions: NonNullable['launchOptions']>; + + browserPlugin: LaunchContextOptions['browserPlugin']; + + useIncognitoPages: LaunchContextOptions['useIncognitoPages']; + + userDataDir: NonNullable['userDataDir']>; + + protected _proxyUrl?: string; + + protected _reservedFieldNames: (string|symbol)[]; + + constructor(options: LaunchContextOptions) { + const { + id = nanoid(), + browserPlugin, + launchOptions, + proxyUrl, + useIncognitoPages, + userDataDir = path.join(os.tmpdir(), nanoid()), + } = options; + + this.id = id; + this.browserPlugin = browserPlugin; + this.launchOptions = launchOptions || {} as any; + this.useIncognitoPages = useIncognitoPages; + this.userDataDir = userDataDir; + + this._proxyUrl = proxyUrl; + this._reservedFieldNames = Reflect.ownKeys(this); + } + + /** + * Extend the launch context with any extra fields. + * This is useful to keep state information relevant + * to the browser being launched. It ensures that + * no internal fields are overridden and should be + * used instead of property assignment. + */ + extend(fields: Record): void { + Object.entries(fields).forEach(([key, value]) => { + if (this._reservedFieldNames.includes(key)) { + throw new Error(`Cannot extend LaunchContext with key: ${key}, because it's reserved.`); + } else { + (this as any)[key] = value; + } + }); + } + + /** + * Sets a proxy URL for the browser. + * Use `undefined` to unset existing proxy URL. + */ + set proxyUrl(url: string | undefined) { + this._proxyUrl = url && new URL(url).href; + } + + /** + * Returns the proxy URL of the browser. + */ + get proxyUrl(): string | undefined { + return this._proxyUrl; + } +} diff --git a/src/logger.js b/src/logger.ts similarity index 59% rename from src/logger.js rename to src/logger.ts index d201ba8..5625ae2 100644 --- a/src/logger.js +++ b/src/logger.ts @@ -1,7 +1,5 @@ const defaultLog = require('apify-shared/log'); -const log = defaultLog.child({ +export default defaultLog.child({ prefix: 'BrowserPool', }); - -module.exports = log; diff --git a/src/playwright/browser.js b/src/playwright/browser.ts similarity index 72% rename from src/playwright/browser.js rename to src/playwright/browser.ts index 5d0d590..bf8b783 100644 --- a/src/playwright/browser.js +++ b/src/playwright/browser.ts @@ -1,20 +1,22 @@ -const EventEmitter = require('events'); -/** - * @typedef BrowserOptions - * @param {import('playwright').BrowserContext} browserContext - * @param {string} version - * -*/ +import EventEmitter from 'events'; +import type { BrowserContext } from 'playwright'; + +export interface BrowserOptions { + browserContext: BrowserContext; + version: string; +} /** * Browser wrapper created to have consistent API with persistent and non-persistent contexts. */ -class Browser extends EventEmitter { - /** - * - * @param {BrowserOptions} options - */ - constructor(options = {}) { +export default class Browser extends EventEmitter { + browserContext: BrowserContext; + + _version: string; + + _isConnected: boolean; + + constructor(options: BrowserOptions) { super(); const { browserContext, version } = options; @@ -38,17 +40,14 @@ class Browser extends EventEmitter { /** * Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts. - * @returns {Array} */ - contexts() { + contexts(): BrowserContext[] { return [this.browserContext]; } /** * Indicates that the browser is connected. - * @returns {boolean} */ - isConnected() { return this._isConnected; } @@ -66,17 +65,15 @@ class Browser extends EventEmitter { * Creates a new page in a new browser context. Closing this page will close the context as well. * @param {...any} args - New Page options. See https://playwright.dev/docs/next/api/class-browser#browsernewpageoptions. */ - async newPage(...args) { - return this.browserContext.newPage(...args); + async newPage(...args: any) { + // TODO: BrowserContext.newPage doesn't allow arguments + return (this.browserContext.newPage as any)(...args); } /** * Returns the browser version. - * @returns {string} browser version. */ - version() { + version(): string { return this._version; } } - -module.exports = Browser; diff --git a/src/playwright/playwright-controller.js b/src/playwright/playwright-controller.js deleted file mode 100644 index 6911e2c..0000000 --- a/src/playwright/playwright-controller.js +++ /dev/null @@ -1,43 +0,0 @@ -const BrowserController = require('../abstract-classes/browser-controller'); - -/** - * playwright - * @extends BrowserController - */ -class PlaywrightController extends BrowserController { - constructor(options) { - super(options); - this.supportsPageOptions = true; - } - - async _newPage(pageOptions) { - const page = await this.browser.newPage(pageOptions); - - page.once('close', async () => { - this.activePages--; - }); - - return page; - } - - async _close() { - await this.browser.close(); - } - - async _kill() { - // TODO We need to be absolutely sure the browser dies. - await this.browser.close(); // Playwright does not have the browser child process attached to normal browser server - } - - async _getCookies(page) { - const context = page.context(); - return context.cookies(); - } - - async _setCookies(page, cookies) { - const context = page.context(); - return context.addCookies(cookies); - } -} - -module.exports = PlaywrightController; diff --git a/src/playwright/playwright-controller.ts b/src/playwright/playwright-controller.ts new file mode 100644 index 0000000..3a87b02 --- /dev/null +++ b/src/playwright/playwright-controller.ts @@ -0,0 +1,40 @@ +import type { Page, LaunchOptions, Browser, FirefoxBrowser, ChromiumBrowser, WebKitBrowser } from 'playwright'; +import BrowserController, { BrowserControllerCookie } from '../abstract-classes/browser-controller'; + +export type PlaywrightControllerPageOptions = NonNullable[0]>; + +/** + * playwright + */ +export default class PlaywrightController extends BrowserController { + supportsPageOptions = true; + + async _newPage(pageOptions: PlaywrightControllerPageOptions) { + const page = await this.browser.newPage(pageOptions); + + page.once('close', async () => { + this.activePages--; + }); + + return page; + } + + async _close() { + await this.browser.close(); + } + + async _kill() { + // TODO We need to be absolutely sure the browser dies. + await this.browser.close(); // Playwright does not have the browser child process attached to normal browser server + } + + async _getCookies(page: Page): Promise { + const context = page.context(); + return context.cookies() as any; + } + + async _setCookies(page: Page, cookies: BrowserControllerCookie[]) { + const context = page.context(); + return context.addCookies(cookies); + } +} diff --git a/src/playwright/playwright-plugin.js b/src/playwright/playwright-plugin.js deleted file mode 100644 index 127b087..0000000 --- a/src/playwright/playwright-plugin.js +++ /dev/null @@ -1,78 +0,0 @@ -const _ = require('lodash'); -const BrowserPlugin = require('../abstract-classes/browser-plugin'); -const PlaywrightController = require('./playwright-controller'); -const Browser = require('./browser'); - -/** - * playwright - */ -class PlaywrightPlugin extends BrowserPlugin { - /** - * @param {LaunchContext} launchContext - * @return {Promise} - * @private - */ - async _launch(launchContext) { - const { - launchOptions, - anonymizedProxyUrl, - useIncognitoPages, - userDataDir, - } = launchContext; - let browser; - - if (useIncognitoPages) { - browser = await this.library.launch(launchOptions); - } else { - const browserContext = await this.library.launchPersistentContext(userDataDir, launchOptions); - - if (!this._browserVersion) { - // Launches unused browser just to get the browser version. - - const inactiveBrowser = await this.library.launch(launchOptions); - this._browserVersion = inactiveBrowser.version(); - - inactiveBrowser.close().catch(_.noop); - } - - browser = new Browser({ browserContext, version: this._browserVersion }); - } - - if (anonymizedProxyUrl) { - browser.once('disconnected', () => { - this._closeAnonymizedProxy(anonymizedProxyUrl); - }); - } - - return browser; - } - - /** - * @return {PlaywrightController} - * @private - */ - _createController() { - return new PlaywrightController(this); - } - - /** - * - * @param {LaunchContext} launchContext - * @return {Promise} - * @private - */ - async _addProxyToLaunchOptions(launchContext) { - const { launchOptions, proxyUrl } = launchContext; - - if (this._shouldAnonymizeProxy(proxyUrl)) { - const anonymizedProxyUrl = await this._getAnonymizedProxyUrl(proxyUrl); - launchContext.anonymizedProxyUrl = anonymizedProxyUrl; - launchOptions.proxy = { server: anonymizedProxyUrl }; - return; - } - - launchOptions.proxy = { server: proxyUrl }; - } -} - -module.exports = PlaywrightPlugin; diff --git a/src/playwright/playwright-plugin.ts b/src/playwright/playwright-plugin.ts new file mode 100644 index 0000000..d37f43b --- /dev/null +++ b/src/playwright/playwright-plugin.ts @@ -0,0 +1,88 @@ +import type { FirefoxBrowser, ChromiumBrowser, Page, WebKitBrowser, Browser as PlaywrightBrowser, BrowserType, LaunchOptions } from 'playwright'; +import BrowserPlugin from '../abstract-classes/browser-plugin'; +import PlaywrightController, { PlaywrightControllerPageOptions } from './playwright-controller'; +import type { LaunchContextOptions } from '../launch-context'; +import Browser from './browser'; + +const noop = require('lodash.noop'); + +export interface PlaywrightLaunchContext extends LaunchContextOptions { + anonymizedProxyUrl?: string; +} + +/** + * playwright + */ +export default class PlaywrightPlugin + extends BrowserPlugin, Page, LaunchOptions, PlaywrightControllerPageOptions> { + _browserVersion: string | null = null; + + /** + * TODO: find a way to make this work to remove any + * @private + */ + async _launch(launchContext: PlaywrightLaunchContext): Promise { + const { + launchOptions, + anonymizedProxyUrl, + useIncognitoPages, + userDataDir, + } = launchContext; + let browser!: T; + + if (useIncognitoPages) { + browser = await this.library.launch(launchOptions); + } else { + // TODO: userDataDir must be provided! + const browserContext = await this.library.launchPersistentContext(userDataDir as any, launchOptions); + + if (!this._browserVersion) { + // Launches unused browser just to get the browser version. + + const inactiveBrowser = await this.library.launch(launchOptions); + this._browserVersion = inactiveBrowser.version(); + + inactiveBrowser.close().catch(noop); + } + + browser = new Browser({ browserContext, version: this._browserVersion }) as unknown as T; + } + // @TODO: Rework the disconnected events once the browser context vs browser issue is fixed + if (anonymizedProxyUrl) { + browser.once('disconnected', () => { + this._closeAnonymizedProxy(anonymizedProxyUrl); + }); + } + + return browser; + } + + /** + * TODO: find a way to make this work to remove any + * @private + */ + _createController(): PlaywrightController { + return new PlaywrightController(this as any); + } + + /** + * TODO: find a way to make this work to remove any + * @private + */ + async _addProxyToLaunchOptions(launchContext: PlaywrightLaunchContext) { + const { launchOptions, proxyUrl } = launchContext; + + if (launchOptions) { + if (proxyUrl) { + if (this._shouldAnonymizeProxy(proxyUrl)) { + const anonymizedProxyUrl = await this._getAnonymizedProxyUrl(proxyUrl); + launchContext.anonymizedProxyUrl = anonymizedProxyUrl; + launchOptions.proxy = { server: anonymizedProxyUrl }; + return; + } + + launchOptions.proxy = { server: proxyUrl }; + } + } + } +} diff --git a/src/puppeteer/puppeteer-controller.js b/src/puppeteer/puppeteer-controller.ts similarity index 70% rename from src/puppeteer/puppeteer-controller.js rename to src/puppeteer/puppeteer-controller.ts index fc8b187..bbd2c43 100644 --- a/src/puppeteer/puppeteer-controller.js +++ b/src/puppeteer/puppeteer-controller.ts @@ -1,17 +1,19 @@ -const _ = require('lodash'); +import type { Page, Browser, ChromeArgOptions, BrowserContext } from 'puppeteer'; +import log from '../logger'; +import BrowserController, { BrowserControllerCookie } from '../abstract-classes/browser-controller'; -const BrowserController = require('../abstract-classes/browser-controller'); +const noop = require('lodash.noop'); const PROCESS_KILL_TIMEOUT_MILLIS = 5000; /** * puppeteer */ -class PuppeteerController extends BrowserController { +export default class PuppeteerController extends BrowserController { async _newPage() { const { useIncognitoPages } = this.launchContext; - let page; - let context; + let page: Page; + let context: BrowserContext; if (useIncognitoPages) { context = await this.browser.createIncognitoBrowserContext(); @@ -24,13 +26,13 @@ class PuppeteerController extends BrowserController { this.activePages--; if (useIncognitoPages) { - context.close().catch(_.noop); + context.close().catch(noop); } }); page.once('error', (error) => { - this.log.exception(error, 'Page crashed.'); - page.close().catch(_.noop); + log.exception(error, 'Page crashed.'); + page.close().catch(noop); }); return page; @@ -63,13 +65,11 @@ class PuppeteerController extends BrowserController { } } - async _getCookies(page) { + async _getCookies(page: Page) { return page.cookies(); } - async _setCookies(page, cookies) { + async _setCookies(page: Page, cookies: BrowserControllerCookie[]) { return page.setCookie(...cookies); } } - -module.exports = PuppeteerController; diff --git a/src/puppeteer/puppeteer-plugin.js b/src/puppeteer/puppeteer-plugin.js deleted file mode 100644 index 8bdb39f..0000000 --- a/src/puppeteer/puppeteer-plugin.js +++ /dev/null @@ -1,71 +0,0 @@ -const BrowserPlugin = require('../abstract-classes/browser-plugin'); -const PuppeteerController = require('./puppeteer-controller'); - -const PROXY_SERVER_ARG = '--proxy-server='; - -/** - * puppeteer - */ -class PuppeteerPlugin extends BrowserPlugin { - /** - * @param {LaunchContext} launchContext - * @return {Promise} - * @private - */ - async _launch(launchContext) { - const { - launchOptions, - anonymizedProxyUrl, - userDataDir, - } = launchContext; - - const finalLaunchOptions = { - ...launchOptions, - userDataDir: launchOptions.userDataDir || userDataDir, - }; - - const browser = await this.library.launch(finalLaunchOptions); - - if (anonymizedProxyUrl) { - browser.once('disconnected', () => { - this._closeAnonymizedProxy(anonymizedProxyUrl); - }); - } - - return browser; - } - - /** - * @return {PuppeteerController} - * @private - */ - _createController() { - return new PuppeteerController(this); - } - - /** - * - * @param launchContext {object} - * @return {Promise} - * @private - */ - async _addProxyToLaunchOptions(launchContext) { - const { launchOptions, proxyUrl } = launchContext; - let finalProxyUrl = proxyUrl; - - if (this._shouldAnonymizeProxy(proxyUrl)) { - finalProxyUrl = await this._getAnonymizedProxyUrl(proxyUrl); - launchContext.anonymizedProxyUrl = finalProxyUrl; - } - - const proxyArg = `${PROXY_SERVER_ARG}${finalProxyUrl}`; - - if (Array.isArray(launchOptions.args)) { - launchOptions.args.push(proxyArg); - } else { - launchOptions.args = [proxyArg]; - } - } -} - -module.exports = PuppeteerPlugin; diff --git a/src/puppeteer/puppeteer-plugin.ts b/src/puppeteer/puppeteer-plugin.ts new file mode 100644 index 0000000..7806af8 --- /dev/null +++ b/src/puppeteer/puppeteer-plugin.ts @@ -0,0 +1,73 @@ +import type { Browser, Page, ChromeArgOptions } from 'puppeteer'; +import BrowserPlugin from '../abstract-classes/browser-plugin'; +import PuppeteerController from './puppeteer-controller'; +import type { LaunchContextOptions } from '../launch-context'; + +const PROXY_SERVER_ARG = '--proxy-server='; + +export interface PuppeteerLaunchContext extends LaunchContextOptions { + anonymizedProxyUrl?: string; +} +/** + * puppeteer + */ +export default class PuppeteerPlugin extends BrowserPlugin { + /** + * @private + */ + async _launch(launchContext: PuppeteerLaunchContext): Promise { + const { + launchOptions, + anonymizedProxyUrl, + userDataDir, + } = launchContext; + + const finalLaunchOptions = { + ...launchOptions, + userDataDir: launchOptions?.userDataDir || userDataDir, + }; + + const browser = await (this.library as any).launch(finalLaunchOptions); + + if (anonymizedProxyUrl) { + browser.once('disconnected', () => { + this._closeAnonymizedProxy(anonymizedProxyUrl); + }); + } + + return browser; + } + + /** + * @private + */ + _createController(): PuppeteerController { + return new PuppeteerController(this); + } + + /** + * + * @param launchContext {object} + * @return {Promise} + * @private + */ + async _addProxyToLaunchOptions(launchContext: PuppeteerLaunchContext) { + const { launchOptions, proxyUrl } = launchContext; + let finalProxyUrl = proxyUrl; + + if (proxyUrl && this._shouldAnonymizeProxy(proxyUrl)) { + finalProxyUrl = await this._getAnonymizedProxyUrl(proxyUrl); + launchContext.anonymizedProxyUrl = finalProxyUrl; + } + + const proxyArg = `${PROXY_SERVER_ARG}${finalProxyUrl}`; + + if (launchOptions) { + if (Array.isArray(launchOptions.args)) { + launchOptions.args.push(proxyArg); + } else { + launchOptions.args = [proxyArg]; + } + } + } +} diff --git a/src/utils.js b/src/utils.ts similarity index 68% rename from src/utils.js rename to src/utils.ts index 5d0f5f5..d4707c6 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,4 +1,4 @@ -const addTimeoutToPromise = (promise, timeoutMillis, errorMessage) => { +export const addTimeoutToPromise = >(promise: T, timeoutMillis: number, errorMessage: string): Promise ? U : any> => { return new Promise(async (resolve, reject) => { // eslint-disable-line const timeout = setTimeout(() => reject(new Error(errorMessage)), timeoutMillis); try { @@ -11,7 +11,3 @@ const addTimeoutToPromise = (promise, timeoutMillis, errorMessage) => { } }); }; - -module.exports = { - addTimeoutToPromise, -}; diff --git a/test/browser-plugins/plugins.test.js b/test/browser-plugins/plugins.test.ts similarity index 96% rename from test/browser-plugins/plugins.test.js rename to test/browser-plugins/plugins.test.ts index dc7d9d5..dbb38fc 100644 --- a/test/browser-plugins/plugins.test.js +++ b/test/browser-plugins/plugins.test.ts @@ -1,14 +1,14 @@ -const puppeteer = require('puppeteer'); -const playwright = require('playwright'); -const fs = require('fs'); +import puppeteer from 'puppeteer'; +import playwright from 'playwright'; +import fs from 'fs'; -const PuppeteerPlugin = require('../../src/puppeteer/puppeteer-plugin'); -const PuppeteerController = require('../../src/puppeteer/puppeteer-controller'); +import PuppeteerPlugin from '../../src/puppeteer/puppeteer-plugin'; +import PuppeteerController from '../../src/puppeteer/puppeteer-controller'; -const PlaywrightPlugin = require('../../src/playwright/playwright-plugin.js'); -const PlaywrightController = require('../../src/playwright/playwright-controller'); -const Browser = require('../../src/playwright/browser'); -const LaunchContext = require('../../src/launch-context'); +import PlaywrightPlugin from '../../src/playwright/playwright-plugin'; +import PlaywrightController from '../../src/playwright/playwright-controller'; +import Browser from '../../src/playwright/browser'; +import LaunchContext from '../../src/launch-context'; jest.setTimeout(120000); diff --git a/test/browser-pool.test.js b/test/browser-pool.test.ts similarity index 98% rename from test/browser-pool.test.js rename to test/browser-pool.test.ts index c946022..479d7dc 100644 --- a/test/browser-pool.test.js +++ b/test/browser-pool.test.ts @@ -1,8 +1,9 @@ -const puppeteer = require('puppeteer'); -const playwright = require('playwright'); -const BrowserPool = require('../src/browser-pool'); -const PuppeteerPlugin = require('../src/puppeteer/puppeteer-plugin'); -const PlaywrightPlugin = require('../src/playwright/playwright-plugin'); +import puppeteer from 'puppeteer'; +import playwright from 'playwright'; +import BrowserPool from '../src/browser-pool'; +import PuppeteerPlugin from '../src/puppeteer/puppeteer-plugin'; +import PlaywrightPlugin from '../src/playwright/playwright-plugin'; + const { BROWSER_POOL_EVENTS: { BROWSER_LAUNCHED, @@ -215,7 +216,7 @@ describe('BrowserPool', () => { expect(browserPool.retiredBrowserControllers.size).toBe(1); await page.close(); - await new Promise((resolve) => setTimeout(() => { + await new Promise((resolve) => setTimeout(() => { resolve(); }, 1000)); diff --git a/test/index.test.js b/test/index.test.ts similarity index 52% rename from test/index.test.js rename to test/index.test.ts index 605ee56..e2f3f87 100644 --- a/test/index.test.js +++ b/test/index.test.ts @@ -1,8 +1,8 @@ -const BrowserPool = require('../src/browser-pool'); -const PuppeteerPlugin = require('../src/puppeteer/puppeteer-plugin'); -const PlaywrightPlugin = require('../src/playwright/playwright-plugin.js'); +import BrowserPool from '../src/browser-pool'; +import PuppeteerPlugin from '../src/puppeteer/puppeteer-plugin'; +import PlaywrightPlugin from '../src/playwright/playwright-plugin'; -const modules = require('../src/index'); +import * as modules from '../src/index'; describe('Exports', () => { test('Modules', () => { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..5cff9f6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2018", + "module": "commonjs", + "lib": [ + "DOM", + "DOM.Iterable", + "ES6", + "ES2019.Symbol", + "ES2019.String", + "ES2019.Object", + "ES2020.String" + ], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./lib", + "removeComments": false, + "noEmit": false, + "noEmitOnError": true, + "downlevelIteration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..7d06d80 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "test/**/*.ts" + ] +} From 1b12eebb28fbfae0398e8eb323ecb0c90b7908fc Mon Sep 17 00:00:00 2001 From: Paulo Cesar Date: Mon, 29 Mar 2021 00:47:21 -0300 Subject: [PATCH 2/2] add default objects for contexts --- .eslintrc | 15 ++++++++------- package.json | 6 +++--- src/abstract-classes/browser-controller.ts | 8 ++++---- src/abstract-classes/browser-plugin.ts | 4 ++-- src/browser-pool.ts | 14 +++++++------- src/launch-context.ts | 2 +- src/playwright/playwright-plugin.ts | 2 +- test/.eslintrc | 6 ++++++ test/browser-plugins/plugins.test.ts | 10 +++++----- test/browser-pool.test.ts | 10 ++++++---- tsconfig.eslint.json | 7 +++++++ 11 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 tsconfig.eslint.json diff --git a/.eslintrc b/.eslintrc index 86c0ff3..06ca9b0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,17 +1,18 @@ { "extends": [ - "@apify", - "eslint:recommended", - "plugin:@typescript-eslint/recommended" + "@apify/ts" ], "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], + "env": { + "node": true + }, + "parserOptions": { + "project": "./tsconfig.eslint.json" + }, "rules": { "max-len": 0, "import/extensions": 0, + "@typescript-eslint/await-thenable": 1, "@typescript-eslint/ban-types": 0, "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-var-requires": 0, diff --git a/package.json b/package.json index f39d2ff..c81af9e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "proxy-chain": "^1.0.0" }, "devDependencies": { - "@apify/eslint-config": "^0.1.3", + "@apify/eslint-config-ts": "^0.1.1", "@babel/core": "^7.13.10", "@babel/preset-env": "^7.13.10", "@babel/preset-typescript": "^7.13.0", @@ -39,8 +39,8 @@ "build-docs": "npm run build && npm run build-toc && node docs/build_docs.js", "build-toc": "markdown-toc docs/README.md -i", "api-documenter": "api-extractor run && api-documenter markdown", - "lint": "./node_modules/.bin/eslint ./src --ext .js,.ts,.jsx", - "lint:fix": "./node_modules/.bin/eslint ./src --ext .js,.ts,.jsx --fix", + "lint": "eslint ./src ./test --ext .js,.ts,.jsx && tsc --noemit", + "lint:fix": "eslint ./src ./test --ext .js,.ts,.jsx --fix", "test": "jest --maxWorkers=3 --forceExit" }, "author": { diff --git a/src/abstract-classes/browser-controller.ts b/src/abstract-classes/browser-controller.ts index 8c9d4e3..0c72640 100644 --- a/src/abstract-classes/browser-controller.ts +++ b/src/abstract-classes/browser-controller.ts @@ -87,9 +87,9 @@ export default class BrowserController< hasBrowserPromise: Promise; - protected _activate!: () => any; + protected _activate!: () => void; - protected commitBrowser!: () => any; + protected commitBrowser!: () => void; activePages: number; @@ -138,7 +138,7 @@ export default class BrowserController< */ assignBrowser( browser: BrowserLibrary, - launchContext: LaunchContext, + launchContext: LaunchContext = {} as any, ): void { if (this.browser) { throw new Error('BrowserController already has a browser instance assigned.'); @@ -185,7 +185,7 @@ export default class BrowserController< * * @ignore */ - async newPage(pageOptions?: PageOptions): Promise { + async newPage(pageOptions: PageOptions = {} as PageOptions): Promise { this.activePages++; this.totalPages++; await this.isActivePromise; diff --git a/src/abstract-classes/browser-plugin.ts b/src/abstract-classes/browser-plugin.ts index 7420990..2e8d061 100644 --- a/src/abstract-classes/browser-plugin.ts +++ b/src/abstract-classes/browser-plugin.ts @@ -15,7 +15,7 @@ const proxyChain = require('proxy-chain'); * or fork of the library. It also keeps `browser-pool` installation small. */ export interface Launcher { - launch(object: any): any; + launch(object: unknown): unknown; name?: () => string; } @@ -182,7 +182,7 @@ export default class BrowserPlugin< * Anonymized proxy URL of a running proxy server. * @private */ - async _closeAnonymizedProxy(proxyUrl: string): Promise { + async _closeAnonymizedProxy(proxyUrl: string): Promise { return proxyChain.closeAnonymizedProxy(proxyUrl, true).catch((err: Error) => { log.debug(`Could not close anonymized proxy server.\nCause:${err.message}`); }); diff --git a/src/browser-pool.ts b/src/browser-pool.ts index 5619036..f0a34dc 100644 --- a/src/browser-pool.ts +++ b/src/browser-pool.ts @@ -52,33 +52,33 @@ export interface BrowserPoolNewPageOptions = ( +export type BrowserPoolPreLaunchHook = ( pageId: string, launchContext: LaunchContext ) => Promise; -export type BrowserPoolPostLaunchHook = ( +export type BrowserPoolPostLaunchHook = ( pageId: string, browserController: BrowserController ) => Promise; -export type BrowserPoolPrePageCreateHook = ( +export type BrowserPoolPrePageCreateHook = ( pageId: string, browserController: BrowserController, pageOptions: PageOptions, ) => Promise; -export type BrowserPoolPostPageCreateHook = ( +export type BrowserPoolPostPageCreateHook = ( page: Page, browserController: BrowserController, ) => Promise; -export type BrowserPoolPrePageCloseHook = ( +export type BrowserPoolPrePageCloseHook = ( page: Page, browserController: BrowserController, ) => Promise; -export type BrowserPoolPostPageCloseHook = ( +export type BrowserPoolPostPageCloseHook = ( pageId: string, browserController: BrowserController, ) => Promise; @@ -388,7 +388,7 @@ export default class BrowserPool< * const [chromiumPage, firefoxPage, webkitPage, puppeteerPage] = pages; * ``` */ - async newPageWithEachPlugin(optionsList: any[] = []): Promise { + async newPageWithEachPlugin(optionsList: Record[] = []): Promise { const pagePromises = this.browserPlugins.map((browserPlugin, idx) => { const userOptions = optionsList[idx] || {}; diff --git a/src/launch-context.ts b/src/launch-context.ts index fd1fe63..09d3751 100644 --- a/src/launch-context.ts +++ b/src/launch-context.ts @@ -67,7 +67,7 @@ export default class LaunchContext< anonymizedProxyUrl?: string; - constructor(options: LaunchContextOptions) { + constructor(options: LaunchContextOptions = {}) { const { id = nanoid(), browserPlugin, diff --git a/src/playwright/playwright-plugin.ts b/src/playwright/playwright-plugin.ts index 5eb5174..1c69dff 100644 --- a/src/playwright/playwright-plugin.ts +++ b/src/playwright/playwright-plugin.ts @@ -6,7 +6,7 @@ import Browser from './browser'; const noop = require('lodash.noop'); -export interface PlaywrightLaunchContext> extends LaunchContextOptions { +export interface PlaywrightLaunchContext> extends LaunchContextOptions { anonymizedProxyUrl?: string; } diff --git a/test/.eslintrc b/test/.eslintrc index 1e65838..4947baa 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -12,5 +12,11 @@ "expect": false, "test": false, "jest": false + }, + "parserOptions": { + "project": "./tsconfig.json" + }, + "rules": { + "@typescript-eslint/no-non-null-assertion": 0 } } diff --git a/test/browser-plugins/plugins.test.ts b/test/browser-plugins/plugins.test.ts index 78d7d67..11ae245 100644 --- a/test/browser-plugins/plugins.test.ts +++ b/test/browser-plugins/plugins.test.ts @@ -92,7 +92,7 @@ const runPluginTest = { const proxyUrl = 'http://10.10.10.0:8080'; const plugin = new PuppeteerPlugin(puppeteer as any); jest.spyOn(plugin, '_getAnonymizedProxyUrl'); - const context = await plugin.createLaunchContext({ proxyUrl }); + const context = plugin.createLaunchContext({ proxyUrl }); browser = await plugin.launch(context) as any; const argWithProxy = context.launchOptions?.args?.find((arg) => arg.includes('--proxy-server=')); @@ -216,7 +216,7 @@ describe('Plugins', () => { const proxyUrl = 'http://10.10.10.0:8080'; const plugin = new PlaywrightPlugin(playwright[browserName]); jest.spyOn(plugin, '_getAnonymizedProxyUrl'); - const context = await plugin.createLaunchContext({ proxyUrl }); + const context = plugin.createLaunchContext({ proxyUrl }); browser = await plugin.launch(context); expect(context?.launchOptions?.proxy?.server).toEqual(proxyUrl); @@ -227,7 +227,7 @@ describe('Plugins', () => { const proxyUrl = 'http://apify1234:password@10.10.10.0:8080'; const plugin = new PlaywrightPlugin(playwright[browserName]); jest.spyOn(plugin, '_getAnonymizedProxyUrl'); - const context = await plugin.createLaunchContext({ proxyUrl }); + const context = plugin.createLaunchContext({ proxyUrl }); browser = await plugin.launch(context); expect(context?.launchOptions?.proxy?.server).toEqual(context.anonymizedProxyUrl); @@ -262,7 +262,7 @@ describe('Plugins', () => { browserController.activate(); const page = await browserController.newPage(); - const context = await page.context(); + const context = page.context(); await browserController.newPage(); expect(context.pages()).toHaveLength(3); // 3 pages because of the about:blank. diff --git a/test/browser-pool.test.ts b/test/browser-pool.test.ts index 6bf9aaa..1d13ab0 100644 --- a/test/browser-pool.test.ts +++ b/test/browser-pool.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import puppeteer from 'puppeteer'; import playwright from 'playwright'; import BrowserPool, { BrowserPoolPrePageCreateHook } from '../src/browser-pool'; @@ -37,7 +36,7 @@ describe('BrowserPool', () => { test('should retire browsers', async () => { await browserPool.newPage(); - await browserPool.retireAllBrowsers(); + browserPool.retireAllBrowsers(); expect(browserPool.activeBrowserControllers.size).toBe(0); expect(browserPool.retiredBrowserControllers.size).toBe(1); }); @@ -238,7 +237,10 @@ describe('BrowserPool', () => { for (let i = 0; i < hooks.length; i++) { hooks[i] = createAsyncHookReturningIndex(i); } - await browserPool._executeHooks(hooks); // eslint-disable-line + + // eslint-disable-next-line no-underscore-dangle + await browserPool._executeHooks(hooks); + expect(indexArray).toHaveLength(10); indexArray.forEach((v, index) => expect(v).toEqual(index)); }); @@ -250,7 +252,7 @@ describe('BrowserPool', () => { jest.spyOn(browserPool, '_executeHooks'); const page = await browserPool.newPage(); - const pageId = await browserPool.getPageId(page); + const pageId = browserPool.getPageId(page); const { launchContext } = browserPool.getBrowserControllerByPage(page)!; expect(browserPool._executeHooks).toHaveBeenNthCalledWith(1, browserPool.preLaunchHooks, pageId, launchContext); // eslint-disable-line }); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..f420eed --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./src/**/*.ts", + "./test/**/*.ts" + ] +}