diff --git a/packages/library/src/base/plugin.test.ts b/packages/library/src/base/plugin.test.ts new file mode 100644 index 000000000..ce85cb1e8 --- /dev/null +++ b/packages/library/src/base/plugin.test.ts @@ -0,0 +1,63 @@ +import { Component } from './component' +import { Plugin, PluginAPI } from './plugin' + +it('Calls plugin for registration', () => { + const p = new Plugin() + const spy = jest.spyOn(p, 'handle') + const c = new Component({ id: 'c', plugins: [p] }) + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith(c, 'pluginAdd') +}) + +it('Calls plugin handle method on component event', () => { + const p = new Plugin() + const spy = jest.spyOn(p, 'handle') + const c = new Component({ id: 'c', plugins: [p] }) + + c.internals.emitter.trigger('foo', 'bar') + + expect(spy).toHaveBeenCalledTimes(2) + expect(spy).toHaveBeenLastCalledWith(c, 'foo', 'bar') +}) + +it('Default plugin calls handler methods by name', () => { + const p = new Plugin() + + const spyHandle = jest.spyOn(p, 'handle') + //@ts-ignore + p.onPluginAdd = () => {} + //@ts-ignore + const spyPluginAdd = jest.spyOn(p, 'onPluginAdd') + //@ts-ignore + p.onFoo = () => {} + //@ts-ignore + const spyFoo = jest.spyOn(p, 'onFoo') + + const c = new Component({ id: 'c', plugins: [p] }) + expect(spyHandle).toHaveBeenCalledTimes(1) + expect(spyPluginAdd).toHaveBeenCalledTimes(1) + expect(spyPluginAdd).toHaveBeenLastCalledWith(c) + + c.internals.emitter.trigger('foo', 'bar') + expect(spyHandle).toHaveBeenCalledTimes(2) + expect(spyFoo).toHaveBeenCalledTimes(1) + expect(spyFoo).toHaveBeenLastCalledWith(c, 'bar') +}) + +it('Calls plugin handler with appropriate context', () => { + const p = new Plugin() + const c = new Component({ id: 'c', plugins: [p] }) + + let context = undefined + + //@ts-ignore + p.onFoo = function () { + context = this + } + //@ts-ignore + const spy = jest.spyOn(p, 'onFoo') + c.internals.emitter.trigger('foo', 'bar') + + expect(context).toEqual(p) +}) diff --git a/packages/library/src/base/plugin.ts b/packages/library/src/base/plugin.ts index d52b45988..75e68b760 100644 --- a/packages/library/src/base/plugin.ts +++ b/packages/library/src/base/plugin.ts @@ -1,13 +1,25 @@ -import { without } from 'lodash' +import { capitalize, without } from 'lodash' import { Component } from './component' -type PluginEvent = 'plugin:add' | 'plugin:remove' +type PluginEvent = 'pluginAdd' | 'pluginRemove' +type EventHandler = { + [key in `on${Capitalize}`]?: (context: C, ...params: P[]) => Promise +} -export class Plugin { - async handle(context: C, event: E | PluginEvent, data?: any) {} +//@ts-expect-error TS2359: No methods in common with EventHandler type +export class Plugin + implements EventHandler +{ + async handle(context: C, event: E | PluginEvent, ...params: any[]) { + //@ts-ignore Dynamic dispatch is too much for TS + this[`on${capitalize(event)}`]?.call(this, context, ...params) + } } -export class PluginAPI { +export class PluginAPI< + C extends Component = Component, + E extends string = string, +> { plugins: Array> context: C @@ -16,7 +28,7 @@ export class PluginAPI { this.plugins = plugins // Initialize existing plugins - this.plugins.forEach(p => p.handle(this.context, 'plugin:add')) + this.plugins.forEach(p => p.handle(this.context, 'pluginAdd')) // Setup event handlers this.handle = this.handle.bind(this) @@ -25,17 +37,17 @@ export class PluginAPI { add(plugin: Plugin) { this.plugins.push(plugin) - plugin.handle(this.context, 'plugin:add') + plugin.handle(this.context, 'pluginAdd') } remove(plugin: Plugin) { - plugin.handle(this.context, 'plugin:remove') + plugin.handle(this.context, 'pluginRemove') this.plugins = without(this.plugins, plugin) } - async handle(event: E, data: any) { + async handle(event: E, ...data: any[]) { await Promise.all( - this.plugins.map(p => p.handle(this.context, event, data)), + this.plugins.map(p => p.handle(this.context, event, ...data)), ) } } diff --git a/packages/library/src/base/util/emitter.ts b/packages/library/src/base/util/emitter.ts index 5921c74a9..9e2b15ebe 100644 --- a/packages/library/src/base/util/emitter.ts +++ b/packages/library/src/base/util/emitter.ts @@ -2,6 +2,8 @@ // by Jason Miller, see https://github.com/developit/mitt . // Any mistakes, of course, are entirely my own. +import { getEventMethodName } from './eventName' + export type EventHandler = (...payload: any[]) => void type WildCardEventHandler = (event: T, ...payload: any[]) => void @@ -21,15 +23,6 @@ export type EmitterOptions = { context?: object } -// Event name splitter functions -const splitter = /(^|:)(\w)/gi -const getEventName = function (_m: string, _pre: string, eventName: string) { - return eventName.toUpperCase() -} -const getMethodName = function (event: string) { - return `on${event.replace(splitter, getEventName)}` -} - export class Emitter { id?: string options: EmitterOptions @@ -52,7 +45,7 @@ export class Emitter { } // Trigger local method, if available - const methodName = getMethodName(event) + const methodName = getEventMethodName(event) const method = (this.#context as any)[methodName] if (method && typeof method === 'function') { await method.apply(this.#context, payload) diff --git a/packages/library/src/base/util/eventName.test.ts b/packages/library/src/base/util/eventName.test.ts new file mode 100644 index 000000000..f7994b6dd --- /dev/null +++ b/packages/library/src/base/util/eventName.test.ts @@ -0,0 +1,9 @@ +import { getEventMethodName } from './eventName' + +it('Generates basic method names', () => { + expect(getEventMethodName('foo')).toBe('onFoo') +}) + +it('Handles split method names', () => { + expect(getEventMethodName('foo:bar')).toBe('onFooBar') +}) diff --git a/packages/library/src/base/util/eventName.ts b/packages/library/src/base/util/eventName.ts new file mode 100644 index 000000000..44d17e7f2 --- /dev/null +++ b/packages/library/src/base/util/eventName.ts @@ -0,0 +1,7 @@ +// Event name splitter functions +const splitter = /(^|:)(\w)/gi +const getEventName = (_m: string, _pre: string, eventName: string) => + eventName.toUpperCase() + +export const getEventMethodName = (event: string) => + `on${event.replace(splitter, getEventName)}` diff --git a/packages/library/src/plugins/debug.ts b/packages/library/src/plugins/debug.ts index c861c9c02..81c6e52b1 100644 --- a/packages/library/src/plugins/debug.ts +++ b/packages/library/src/plugins/debug.ts @@ -366,7 +366,7 @@ export default class Debug { async handle(context: Component, event: string) { switch (event) { - case 'plugin:add': + case 'pluginAdd': return this.onInit(context) case 'prepare': return await this.onPrepare() diff --git a/packages/library/test/core.js b/packages/library/test/core.js index 2cf1f7868..e1c713b70 100644 --- a/packages/library/test/core.js +++ b/packages/library/test/core.js @@ -1055,7 +1055,7 @@ describe('Core', () => { // Check result assert.deepEqual(c.internals.plugins.plugins, [plugin]) assert.ok( - spy.calledWith(c, 'plugin:add') + spy.calledWith(c, 'pluginAdd') ) })