Skip to content

Commit

Permalink
feat: Plugins helpers (#4274)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbatiste authored Nov 23, 2023
1 parent ec7a0f5 commit d7d018e
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 0 deletions.
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 `tryGetPluginByKey` and specifying the plugin set key and plugin key.

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

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

// Call tryGetPluginByKey to get a specific plugin by key
const plugin = tryGetPluginByKey('foo-plugins', 'key-1');
```

## 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
46 changes: 46 additions & 0 deletions helpers/plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const pluginSets = new Map();

function getPluginSet(setKey) {
let pluginSet = pluginSets.get(setKey);
if (pluginSet) return pluginSet;

pluginSet = { plugins: [], requested: false, requiresSorting: false };
pluginSets.set(setKey, pluginSet);
return pluginSet;
}

export function getPlugins(setKey) {
const pluginSet = getPluginSet(setKey);
pluginSet.requested = true;
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) {
const pluginSet = getPluginSet(setKey);

if (pluginSet.requested) {
throw new Error(`Plugin Set "${setKey}" has already been requested. Additional plugin registrations would result in stale consumer plugins.`);
} else if (options?.key !== undefined) {
if (pluginSet.plugins.find(registeredPlugin => registeredPlugin.options.key === options?.key)) {
throw new Error(`Plugin Set "${setKey}" already has a plugin with the key "${options.key}".`);
}
}

pluginSet.plugins.push({ plugin, options: Object.assign({ key: undefined, sort: 0 }, options) });
pluginSet.requiresSorting = pluginSet.requiresSorting || (options?.sort !== undefined);
}

// Do not import! Testing only!!
export function resetPlugins() {
pluginSets.clear();
}

export function tryGetPluginByKey(setKey, pluginKey) {
const pluginSet = pluginSets.get(setKey);
const plugin = pluginSet?.plugins.find(plugin => plugin.options.key === pluginKey)?.plugin;
return plugin || null;
}
103 changes: 103 additions & 0 deletions helpers/test/plugins.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { getPlugins, registerPlugin, resetPlugins, tryGetPluginByKey } from '../plugins.js';
import { expect } from '@brightspace-ui/testing';

describe('plugins', () => {

afterEach(() => {
resetPlugins();
});

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);
});

it('registerPlugin should throw when called after a consumer has called getPlugins for the same Set key', () => {
getPlugins('test-plugins');
expect(() => {
registerPlugin('test-plugins', { prop1: 'candy apple' });
}).to.throw();
});

it('registerPlugin should not throw when called after a consumer has called getPlugins for a different Set key', () => {
getPlugins('test-plugins');
expect(() => {
registerPlugin('test-plugins-other', { prop1: 'candy apple' });
}).to.not.throw();
});

});

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');
});

});

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 = tryGetPluginByKey('invalid-plugin-set-key', 'plugin1');
expect(plugin).to.be.null;
});

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

it('getPlugin should return plugin for specified keys', () => {
const plugin = tryGetPluginByKey('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();
});

});

});

0 comments on commit d7d018e

Please sign in to comment.