Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat configure plugins #281

Merged
merged 6 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions core/lib/CorePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,13 +36,15 @@ export default class CorePlugins implements ICorePlugins {
private readonly logger: IBoundLog;
private agent: Agent;
private getSessionSummary: IOptionsCreate['getSessionSummary'];
private pluginConfigs: PluginConfigs;

constructor(
agent: Agent,
options: IOptionsCreate,
private corePluginsById: { [id: string]: ICorePluginClass },
) {
const { dependencyMap, corePluginPaths, getSessionSummary } = options;
this.pluginConfigs = options.pluginConfigs ?? {};
this.agent = agent;

if (getSessionSummary) this.getSessionSummary = getSessionSummary;
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions core/lib/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
);
Expand Down
2 changes: 1 addition & 1 deletion core/test/connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions core/test/domRecorder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ let connectionToClient: ConnectionToHeroClient;
beforeAll(async () => {
Core.defaultUnblockedPlugins.push(
class BasicHumanEmulator {
static id = 'BasicHumanEmulator';
async playInteractions(interactionGroups, runFn): Promise<void> {
for (const group of interactionGroups) {
for (const step of group) {
Expand Down
1 change: 1 addition & 0 deletions docs/basic-client/hero.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<PluginId=string, boolean | object>`: 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

Expand Down
8 changes: 6 additions & 2 deletions interfaces/ICorePlugin.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,13 +14,14 @@ export default interface ICorePlugin extends ICorePluginMethods, IUnblockedPlugi
readonly sessionSummary?: ISessionSummary;
}

export interface ICorePluginClass {
export interface ICorePluginClass<C extends object = any> {
id: string;
type: keyof typeof PluginTypes;
new (createOptions: ICorePluginCreateOptions): ICorePlugin;
shouldActivate?(
emulationProfile: IEmulationProfile<unknown>,
sessionSummary: ISessionSummary,
customConfig?: PluginCustomConfig<C>,
): boolean;
}

Expand Down
4 changes: 3 additions & 1 deletion interfaces/ICorePluginCreateOptions.ts
Original file line number Diff line number Diff line change
@@ -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<C extends object = any> {
emulationProfile: IEmulationProfile;
corePlugins: ICorePlugins;
sessionSummary: ISessionSummary;
logger: IBoundLog;
customConfig?: PluginCustomConfig<C>
}
4 changes: 4 additions & 0 deletions interfaces/ISessionCreateOptions.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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[];
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions plugins/execute-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
124 changes: 124 additions & 0 deletions plugins/execute-js/test/config.test.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>,
_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();
});
2 changes: 1 addition & 1 deletion testing/TestHero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading
Loading