diff --git a/core/lib/CorePlugins.ts b/core/lib/CorePlugins.ts index da4ff0a7d..69a592d9f 100644 --- a/core/lib/CorePlugins.ts +++ b/core/lib/CorePlugins.ts @@ -9,12 +9,14 @@ import ICorePlugins from '@ulixee/hero-interfaces/ICorePlugins'; import { PluginTypes } from '@ulixee/hero-interfaces/IPluginTypes'; import IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile'; import Agent from '@ulixee/unblocked-agent/lib/Agent'; +import { PluginConfigs } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin'; import Core from '../index'; interface IOptionsCreate { dependencyMap?: IDependencyMap; corePluginPaths?: string[]; getSessionSummary?: () => ISessionSummary; + pluginConfigs?: PluginConfigs; } interface IDependencyMap { @@ -34,6 +36,7 @@ export default class CorePlugins implements ICorePlugins { private readonly logger: IBoundLog; private agent: Agent; private getSessionSummary: IOptionsCreate['getSessionSummary']; + private pluginConfigs: PluginConfigs; constructor( agent: Agent, @@ -41,6 +44,7 @@ export default class CorePlugins implements ICorePlugins { private corePluginsById: { [id: string]: ICorePluginClass }, ) { const { dependencyMap, corePluginPaths, getSessionSummary } = options; + this.pluginConfigs = options.pluginConfigs ?? {}; this.agent = agent; if (getSessionSummary) this.getSessionSummary = getSessionSummary; @@ -53,9 +57,14 @@ export default class CorePlugins implements ICorePlugins { this.logger = agent.logger.createChild(module); for (const plugin of Object.values(corePluginsById)) { - const shouldActivate = - plugin.shouldActivate?.(agent.emulationProfile, getSessionSummary()) ?? true; - if (shouldActivate) this.use(plugin); + const config = this.pluginConfigs[plugin.id]; + // true shortcircuits and skips shouldActivate check, we also skip running the shouldActivate function. + const shouldActivate = (): boolean | undefined => + plugin.shouldActivate?.(agent.emulationProfile, getSessionSummary(), config); + if (config !== true && (config === false || shouldActivate() === false)) { + continue; + } + this.use(plugin); } if (Core.allowDynamicPluginLoading) { @@ -101,11 +110,14 @@ export default class CorePlugins implements ICorePlugins { public use(CorePlugin: ICorePluginClass): void { if (this.instanceById[CorePlugin.id]) return; + + const config = this.pluginConfigs[CorePlugin.id];; const corePlugin = new CorePlugin({ emulationProfile: this.agent.emulationProfile, logger: this.logger, corePlugins: this, sessionSummary: this.sessionSummary, + customConfig: typeof config === 'boolean' ? undefined : config, }); this.instances.push(corePlugin); this.instanceById[corePlugin.id] = corePlugin; diff --git a/core/lib/Session.ts b/core/lib/Session.ts index a7aa91b40..f3a15a252 100644 --- a/core/lib/Session.ts +++ b/core/lib/Session.ts @@ -582,6 +582,8 @@ export default class Session id: this.id, commandMarker: this.commands, userAgentOption: userProfile?.userAgent, + plugins: options.unblockedPlugins, + pluginConfigs: options.pluginConfigs, }); this.plugins = new CorePlugins( @@ -590,6 +592,7 @@ export default class Session corePluginPaths: options.corePluginPaths, dependencyMap: options.dependencyMap, getSessionSummary: this.getSummary.bind(this), + pluginConfigs: options.pluginConfigs, }, this.core.corePluginsById, ); diff --git a/core/test/connection.test.ts b/core/test/connection.test.ts index 3441faf91..21f4f2982 100644 --- a/core/test/connection.test.ts +++ b/core/test/connection.test.ts @@ -15,7 +15,7 @@ afterEach(Helpers.afterEach); describe('basic connection tests', () => { it('should throw an error informing how to install dependencies', async () => { class CustomEmulator extends DefaultBrowserEmulator { - public static id = 'emulate-test'; + public static override id = 'emulate-test'; public override onNewBrowser() { // don't change launch args so it doesn't reuse a previous one } diff --git a/core/test/domRecorder.test.ts b/core/test/domRecorder.test.ts index 3a42e1baa..c0c2e0746 100644 --- a/core/test/domRecorder.test.ts +++ b/core/test/domRecorder.test.ts @@ -12,6 +12,7 @@ let connectionToClient: ConnectionToHeroClient; beforeAll(async () => { Core.defaultUnblockedPlugins.push( class BasicHumanEmulator { + static id = 'BasicHumanEmulator'; async playInteractions(interactionGroups, runFn): Promise { for (const group of interactionGroups) { for (const step of group) { diff --git a/docs/basic-client/hero.md b/docs/basic-client/hero.md index 6f0230520..a60fcc79e 100644 --- a/docs/basic-client/hero.md +++ b/docs/basic-client/hero.md @@ -90,6 +90,7 @@ const Hero = require('@ulixee/hero-playground'); - proxyIp `string`. The optional IP address of your proxy, if known ahead of time. - publicIp `string`. The optional IP address of your host machine, if known ahead of time. - sessionPersistence `boolean`. Do not save the [Session](../advanced-concepts/sessions.md) database if set to `false`. Defaults to `true` so you can troubleshoot errors, and load/extract data from previous sessions. + - pluginConfigs `Record`: object use to configure hero core and unblocked plugins. Object is indexed with the id of a specific plugin. Storing `true` will always enable the plugin (if loaded) and skip shouldEnable function. Same for `false` but that will disable it instead. It also possible to storing a custom `object` in there that the plugin can then use to configure itself. ## Properties diff --git a/interfaces/ICorePlugin.ts b/interfaces/ICorePlugin.ts index 5ae61a55b..0e416cabb 100644 --- a/interfaces/ICorePlugin.ts +++ b/interfaces/ICorePlugin.ts @@ -1,5 +1,8 @@ import { IFrame } from '@ulixee/unblocked-specification/agent/browser/IFrame'; -import IUnblockedPlugin from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin'; +import IUnblockedPlugin, { + PluginCustomConfig, +} from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin'; + import IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile'; import { IPage } from '@ulixee/unblocked-specification/agent/browser/IPage'; import { PluginTypes } from './IPluginTypes'; @@ -11,13 +14,14 @@ export default interface ICorePlugin extends ICorePluginMethods, IUnblockedPlugi readonly sessionSummary?: ISessionSummary; } -export interface ICorePluginClass { +export interface ICorePluginClass { id: string; type: keyof typeof PluginTypes; new (createOptions: ICorePluginCreateOptions): ICorePlugin; shouldActivate?( emulationProfile: IEmulationProfile, sessionSummary: ISessionSummary, + customConfig?: PluginCustomConfig, ): boolean; } diff --git a/interfaces/ICorePluginCreateOptions.ts b/interfaces/ICorePluginCreateOptions.ts index 50bb181aa..7184529c9 100644 --- a/interfaces/ICorePluginCreateOptions.ts +++ b/interfaces/ICorePluginCreateOptions.ts @@ -1,11 +1,13 @@ import { IBoundLog } from '@ulixee/commons/interfaces/ILog'; import IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile'; +import { PluginCustomConfig } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin'; import ICorePlugins from './ICorePlugins'; import { ISessionSummary } from './ICorePlugin'; -export default interface ICorePluginCreateOptions { +export default interface ICorePluginCreateOptions { emulationProfile: IEmulationProfile; corePlugins: ICorePlugins; sessionSummary: ISessionSummary; logger: IBoundLog; + customConfig?: PluginCustomConfig } diff --git a/interfaces/ISessionCreateOptions.ts b/interfaces/ISessionCreateOptions.ts index e0ccb3c50..4d7cab85a 100644 --- a/interfaces/ISessionCreateOptions.ts +++ b/interfaces/ISessionCreateOptions.ts @@ -1,4 +1,5 @@ import { IEmulationOptions } from '@ulixee/unblocked-specification/plugin/IEmulationProfile'; +import { IUnblockedPluginClass, PluginConfigs } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin'; import IUserProfile from './IUserProfile'; import ISessionOptions from './ISessionOptions'; import IScriptInvocationMeta from './IScriptInvocationMeta'; @@ -24,4 +25,7 @@ export default interface ISessionCreateOptions extends ISessionOptions, IEmulati showChromeAlive?: boolean; desktopConnectionId?: string; showChromeInteractions?: boolean; + // Config use to configure all unblocked, and hero core plugins + pluginConfigs?: PluginConfigs; + unblockedPlugins?: IUnblockedPluginClass[]; } diff --git a/package.json b/package.json index b08ce2aaf..a350787f5 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "clean": "tsc -b --clean tsconfig.json", "test:build": "cross-env NODE_ENV=test ULX_DATA_DIR=.data-test jest", "test": "ulx-repo-after-build && cd build && yarn test:build", + "test:debug": "yarn build && yarn copy:build && cd ./build && cross-env ULX_DATA_DIR=.data-test NODE_ENV=test node --inspect node_modules/.bin/jest --runInBand", "lint": "eslint --cache ./", "version:check": "ulx-repo-version-check fix", "version:bump": "ulx-repo-version-bump" diff --git a/plugins/execute-js/package.json b/plugins/execute-js/package.json index adb8cead2..4374e15ad 100644 --- a/plugins/execute-js/package.json +++ b/plugins/execute-js/package.json @@ -12,6 +12,7 @@ "@ulixee/execute-js-plugin": "2.0.0-alpha.28", "@ulixee/hero": "2.0.0-alpha.28", "@ulixee/hero-core": "2.0.0-alpha.28", + "@ulixee/net": "2.0.0-alpha.28", "@ulixee/hero-testing": "2.0.0-alpha.28" } } diff --git a/plugins/execute-js/test/config.test.ts b/plugins/execute-js/test/config.test.ts new file mode 100644 index 000000000..96b070873 --- /dev/null +++ b/plugins/execute-js/test/config.test.ts @@ -0,0 +1,124 @@ +import { Helpers, Hero } from '@ulixee/hero-testing'; +import Core from '@ulixee/hero-core'; +import { CorePlugin } from '@ulixee/execute-js-plugin'; +import ICorePluginCreateOptions from '@ulixee/hero-interfaces/ICorePluginCreateOptions'; +import ICorePlugin, { ISessionSummary } from '@ulixee/hero-interfaces/ICorePlugin'; +import type IEmulationProfile from '@ulixee/unblocked-specification/plugin/IEmulationProfile'; +import type { PluginCustomConfig } from '@ulixee/unblocked-specification/plugin/IUnblockedPlugin'; +import { TransportBridge } from '@ulixee/net'; +import { ConnectionToHeroCore } from '@ulixee/hero'; + +let core: Core; +let connectionToCore: ConnectionToHeroCore; +afterAll(Helpers.afterAll); +afterEach(async () => { + await Helpers.afterEach(); + await core.close(); +}); + +beforeEach(async () => { + core = await Core.start(); + const bridge = new TransportBridge(); + core.addConnection(bridge.transportToClient); + connectionToCore = new ConnectionToHeroCore(bridge.transportToCore); +}); + +test('it should receive a custom config', async () => { + const testConfig = { test: 'testData' }; + const constructor = jest.fn(); + const shouldActivate = jest.fn(); + + class TestingExecuteJsCorePlugin1 extends CorePlugin { + static override id = 'TestingExecuteJsCorePlugin1'; + constructor(opts: ICorePluginCreateOptions) { + super(opts); + constructor(opts.customConfig); + } + + static shouldActivate?( + _emulationProfile: IEmulationProfile, + _sessionSummary: ISessionSummary, + customConfig?: PluginCustomConfig, + ): boolean { + shouldActivate(customConfig); + return true; + } + } + + core.use(TestingExecuteJsCorePlugin1); + const hero = new Hero({ + connectionToCore, + pluginConfigs: { + [TestingExecuteJsCorePlugin1.id]: testConfig, + }, + }); + + Helpers.onClose(() => hero.close(), true); + + await hero.sessionId; + expect(constructor).toHaveBeenCalledWith(testConfig); + expect(shouldActivate).toHaveBeenCalledWith(testConfig); + await hero.close(); +}); + +test('it should not activate if config === false', async () => { + const constructor = jest.fn(); + const shouldActivate = jest.fn(); + + class TestingExecuteJsCorePlugin2 extends CorePlugin implements ICorePlugin { + static override id = 'TestingExecuteJsCorePlugin2'; + constructor(opts: ICorePluginCreateOptions) { + super(opts); + constructor(); + } + + static shouldActivate(): boolean { + shouldActivate(); + return true; + } + } + core.use(TestingExecuteJsCorePlugin2); + + const hero = new Hero({ + pluginConfigs: { + [TestingExecuteJsCorePlugin2.id]: false, + }, + }); + Helpers.onClose(() => hero.close(), true); + + await hero.sessionId; + expect(shouldActivate).not.toHaveBeenCalled(); + expect(constructor).not.toHaveBeenCalled(); + await hero.close(); +}); + +test('it should skip shouldActivate if config === true', async () => { + const constructor = jest.fn(); + const shouldActivate = jest.fn(); + + class TestingExecuteJsCorePlugin3 extends CorePlugin implements ICorePlugin { + static override id = 'TestingExecuteJsCorePlugin3'; + constructor(opts: ICorePluginCreateOptions) { + super(opts); + constructor(); + } + + static shouldActivate(): boolean { + shouldActivate(); + return false; + } + } + core.use(TestingExecuteJsCorePlugin3); + + const hero = new Hero({ + pluginConfigs: { + [TestingExecuteJsCorePlugin3.id]: true, + }, + }); + Helpers.onClose(() => hero.close(), true); + + await hero.sessionId; + expect(shouldActivate).not.toHaveBeenCalled(); + expect(constructor).toHaveBeenCalled(); + await hero.close(); +}); diff --git a/testing/TestHero.ts b/testing/TestHero.ts index 483ea276e..5b72bb613 100644 --- a/testing/TestHero.ts +++ b/testing/TestHero.ts @@ -5,7 +5,7 @@ import TransportBridge from '@ulixee/net/lib/TransportBridge'; let core: Core; export default class TestHero extends DefaultHero { constructor(createOptions: IHeroCreateOptions = {}) { - createOptions.connectionToCore = TestHero.getDirectConnectionToCore(); + createOptions.connectionToCore ??= TestHero.getDirectConnectionToCore(); super(createOptions); } diff --git a/yarn.lock b/yarn.lock index 7251b8134..79490e9f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2186,17 +2186,17 @@ dependencies: "@ulixee/js-path" "^2.0.0-alpha.18" -"@ulixee/chrome-113-0@^5672.127.8": - version "5672.127.8" - resolved "https://registry.yarnpkg.com/@ulixee/chrome-113-0/-/chrome-113-0-5672.127.8.tgz#6e6ff2fb393e10ab5321ade6058a3aff8d0b6d23" - integrity sha512-zbVEf6ZZWUF10JhqVQFptN6do2VmiSywX/NW4ByDT6IuFbQUheLuw9jHpA9IvO+8e180hklMX+QOKFPUTBUJqg== +"@ulixee/chrome-115-0@^5790.114.8": + version "5790.114.8" + resolved "https://registry.yarnpkg.com/@ulixee/chrome-115-0/-/chrome-115-0-5790.114.8.tgz#b598ed0c972a68c5ebe931a087848b862509ef2a" + integrity sha512-Nl83GRrNb+vJ7GCPbajobwlIlRG7pvibcIOaL3pXHjRcIIcHxl9jVslIEPEOclBGPc8sKt2jWZcsB1szNbR72Q== dependencies: "@ulixee/chrome-app" "^1.0.3" -"@ulixee/chrome-121-0@^6167.86.8": - version "6167.86.8" - resolved "https://registry.yarnpkg.com/@ulixee/chrome-121-0/-/chrome-121-0-6167.86.8.tgz#433f59f75b9657b316f12b6c7d959c4aa0038f03" - integrity sha512-FcvH16fTBWJypsn2ODZueqp8I1gaq/4pzfeS/jTgids+FrivA4krvb74+1A+bh4R4shujTRyZYWMuC3NHmmREw== +"@ulixee/chrome-124-0@^6367.208.10": + version "6367.208.10" + resolved "https://registry.yarnpkg.com/@ulixee/chrome-124-0/-/chrome-124-0-6367.208.10.tgz#32a874efdc837487d14899da721ced4b51b3a463" + integrity sha512-3rn8vdd5m+coAZXgBMkerqN+cC9RaYiK1HwXC36lKkK2csIsl6Ekc+PNCZyyl/i33p6Y1+sXVzRwR26Npr+0OA== dependencies: "@ulixee/chrome-app" "^1.0.3" @@ -2209,10 +2209,10 @@ progress "^2.0.3" tar "^6.1.11" -"@ulixee/repo-tools@^1.0.29": - version "1.0.29" - resolved "https://registry.yarnpkg.com/@ulixee/repo-tools/-/repo-tools-1.0.29.tgz#aa90ea63b8bbfa7a84ac081988ae3d6acee00fe1" - integrity sha512-cga/4OfcTN5GxYSHhPdXHEZFykUt4uIqkl+T4QZ5rH4CXndt8pq4SvoQw1KSG9u3eNMu+yHmLc4iMZGYS/UavA== +"@ulixee/repo-tools@^1.0.31": + version "1.0.31" + resolved "https://registry.yarnpkg.com/@ulixee/repo-tools/-/repo-tools-1.0.31.tgz#53e261239af01d9ecc6443fa7be393f08b0f9419" + integrity sha512-jgIGgzIUTcPkTctmXE5jWUATcCkwQmxWk8uqpCIkUO72dg82MRYXNYhcycSDcxcbRte4JRoQ9wmTmjQW4V3LfA== dependencies: "@typescript-eslint/eslint-plugin" "^6.20.0" "@typescript-eslint/parser" "^6.20.0" @@ -2231,7 +2231,7 @@ eslint-plugin-promise "^6.1.1" prettier "^3.2.4" pretty-quick "^4.0.0" - typescript "^5.3.3" + typescript "^5.4.5" "@ungap/structured-clone@^1.2.0": version "1.2.0" @@ -2726,6 +2726,11 @@ big.js@^5.2.2: resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== +bignumber.js@^9.0.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + binary@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" @@ -3124,6 +3129,11 @@ commander@11.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== +commander@^9.5.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + compare-func@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" @@ -8864,10 +8874,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +typescript@^5.3.3, typescript@^5.4.5: + version "5.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" + integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== ua-parser-js@^0.7.18: version "0.7.37" @@ -9360,3 +9370,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.20.2: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==