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

Plugins helpers #4274

Merged
merged 13 commits into from
Nov 23, 2023
39 changes: 39 additions & 0 deletions helpers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,45 @@ import { getLegacyOffsetParent } from '@brightspace-ui/core/helpers/offsetParent
const offsetParent = getLegacyOffsetParent(element);
```

## Plugins

Plugin helpers provide a way for modules to implement and register objects that can be plugged into other project's modules without requiring the plugin consumer to import those modules or objects directly. A higher order module (ex. in BSI) is responsible for importing the plugin registrations.

The plugin implementor uses the `registerPlugin` helper method to make its implementation available to interested consumers. The implementor provides a key for the set of plugins in which to register and the plugin implementation.

Optionally, an object to specify a `key` for the plugin and/or the `sort` value may be provided. The `key` is useful if consumers intend to request a specific plugin, while the `sort` is useful in cases where the order of plugins is important to consumers. If `sort` is not specified for at least one plugin, they will be provided to consumers in registration order.

**Important!** plugin registrations should defer loading their dependencies using dynamic imports. They should **not** be synchronously imported in the registration module.

```js
import { registerPlugin } from '@brightspace-ui/core/helpers/plugins.js';

// Provide plugin set key, plugin
registerPlugin('foo-plugins', { prop1: 'some value' });
registerPlugin('foo-plugins', { prop1: 'other value' });

// Optionally provide key and/or sort value
registerPlugin('foo-plugins', { prop1: 'some value' }, { key: 'key-1', sort: 1 });
registerPlugin('foo-plugins', { prop1: 'other value' }, { key: 'key-2', sort: 2 });

// Defer loading dependencies until needed
registerPlugin('foo-plugins', { getRenderer: async () => {
return (await import('./some-module.js')).renderer
}});
```

The plugin consumer uses the `getPlugins` helper method to get references to the registered plugins by providing a key for the set of plugins. If the consumer knows the key of the plugin it needs, it can request the plugin by using `getPlugin` and specifying the plugin set key and plugin key.

```js
import { getPlugin, getPlugins } from '@brightspace-ui/core/helpers/plugins.js';

// Call getPlugins to get plugins
const plugins = getPlugins('foo-plugins');

// Call getPlugin to get a specific plugin by key
const plugin = getPlugin('foo-plugins', 'key-1');
dbatiste marked this conversation as resolved.
Show resolved Hide resolved
```

## queueMicrotask

A polyfill for [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/queueMicrotask). For more information on microtasks, read [this article from Mozilla](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide).
Expand Down
36 changes: 36 additions & 0 deletions helpers/plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const pluginSets = new Map();

export function getPlugin(setKey, pluginKey) {
dlockhart marked this conversation as resolved.
Show resolved Hide resolved
const pluginSet = pluginSets.get(setKey);
dbatiste marked this conversation as resolved.
Show resolved Hide resolved
return pluginSet?.plugins.find(plugin => plugin.options.key === pluginKey)?.plugin;
}

export function getPlugins(setKey) {
const pluginSet = pluginSets.get(setKey);
if (!pluginSet) return [];
dbatiste marked this conversation as resolved.
Show resolved Hide resolved
if (pluginSet.requiresSorting) {
pluginSet.plugins.sort((item1, item2) => item1.options.sort - item2.options.sort);
pluginSet.requiresSorting = false;
}
return pluginSet.plugins.map(item => item.plugin);
}

export function registerPlugin(setKey, plugin, options) {
let pluginSet = pluginSets.get(setKey);
if (!pluginSet) {
pluginSet = { plugins: [], requiresSorting: false };
dbatiste marked this conversation as resolved.
Show resolved Hide resolved
pluginSets.set(setKey, pluginSet);
} else if (options?.key !== undefined) {
if (pluginSet.plugins.find(registeredPlugin => registeredPlugin.options.key === options?.key)) {
throw new Error(`Plugin Set "${setKey}" already has plugin with defined key "${options.key}".`);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: not sure if this is better or not, but another option that's 1 less line!

if (!pluginSets.has(setKey)) {
  pluginSets.set({ plugins: [], requiresSorting: false });
}
const pluginSet = pluginSets.get(setKey);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah... I think I might had that initially, but then switched it to only do the single look-up. The difference is probably negligible for the number of plugin sets we'll have though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be O(1) lookup on the Map anyway.


pluginSet.plugins.push({ plugin, options: Object.assign({ key: undefined, sort: 0 }, options) });
pluginSet.requiresSorting = pluginSet.requiresSorting || (options?.sort !== undefined);
dlockhart marked this conversation as resolved.
Show resolved Hide resolved
}

// Do not import! Testing only!!
export function resetPlugins() {
pluginSets.clear();
}
104 changes: 104 additions & 0 deletions helpers/test/plugins.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { getPlugin, getPlugins, registerPlugin, resetPlugins } from '../plugins.js';
import { expect } from '@brightspace-ui/testing';

describe('plugins', () => {

afterEach(() => {
resetPlugins();
});
dlockhart marked this conversation as resolved.
Show resolved Hide resolved

describe('default', () => {

beforeEach(() => {
registerPlugin('test-plugins', { prop1: 'beer' });
registerPlugin('test-plugins', { prop1: 'donuts' });
});

it('getPlugins should return empty array for invalid plugin set key', () => {
const plugins = getPlugins('invalid-plugin-set-key');
expect(plugins.length).to.equal(0);
});

it('getPlugins should return array of plugins in registration order', () => {
const plugins = getPlugins('test-plugins');
expect(plugins.length).to.equal(2);
expect(plugins[0].prop1).to.equal('beer');
expect(plugins[1].prop1).to.equal('donuts');
});

it('getPlugins should return copy of the array for each consumer', () => {
const plugins1 = getPlugins('test-plugins');
const plugins2 = getPlugins('test-plugins');
expect(plugins1).not.to.equal(plugins2);
});

});

describe('sorted', () => {

beforeEach(() => {
registerPlugin('test-plugins', { prop1: 'beer' }, { sort: 3 });
registerPlugin('test-plugins', { prop1: 'donuts' }, { sort: 1 });
});

it('getPlugins should return array of plugins in sort order', () => {
const plugins = getPlugins('test-plugins');
expect(plugins.length).to.equal(2);
expect(plugins[0].prop1).to.equal('donuts');
expect(plugins[1].prop1).to.equal('beer');
});

it('getPlugins should return array of plugins in new sort order if additional plugins are registered', () => {
let plugins = getPlugins('test-plugins');
expect(plugins.length).to.equal(2);
expect(plugins[0].prop1).to.equal('donuts');
expect(plugins[1].prop1).to.equal('beer');

registerPlugin('test-plugins', { prop1: 'candy apple' }, { sort: 2 });

plugins = getPlugins('test-plugins');
expect(plugins.length).to.equal(3);
expect(plugins[0].prop1).to.equal('donuts');
expect(plugins[1].prop1).to.equal('candy apple');
expect(plugins[2].prop1).to.equal('beer');
});

});

describe('keyed', () => {

beforeEach(() => {
registerPlugin('test-plugins', { prop1: 'beer' }, { key: 'plugin1' });
registerPlugin('test-plugins', { prop1: 'donuts' }, { key: 'plugin2' });
});

it('getPlugin should return undefined for invalid plugin set key', () => {
const plugin = getPlugin('invalid-plugin-set-key', 'plugin1');
expect(plugin).to.equal(undefined);
dbatiste marked this conversation as resolved.
Show resolved Hide resolved
});

it('getPlugin should return undefined for invalid plugin key', () => {
const plugin = getPlugin('test-plugins', 'pluginx');
expect(plugin).to.equal(undefined);
});

it('getPlugin should return plugin for specified keys', () => {
const plugin = getPlugin('test-plugins', 'plugin1');
expect(plugin.prop1).to.equal('beer');
});

it('registerPlugin should not throw when adding a plugin with key not used within the set', () => {
expect(() => {
registerPlugin('test-plugins-other', { prop1: 'candy apple' }, { key: 'plugin1' });
}).to.not.throw();
});

it('registerPlugin should throw when adding a plugin with key already used within the set', () => {
expect(() => {
registerPlugin('test-plugins', { prop1: 'candy apple' }, { key: 'plugin1' });
}).to.throw();
});

});
dlockhart marked this conversation as resolved.
Show resolved Hide resolved

});