Skip to content

Commit

Permalink
feat(plugins): add plugin configuration (#385) (#385)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aracturat authored Jun 11, 2021
1 parent 6115ada commit 5e7714b
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 13 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ directory.
name: 'plugin-name',
component: 'PluginReactComponentName',
point: 'extension-point-name',
position: 'wrap'
position: 'wrap',
config: { param: 'value'}
},
{
name: 'plugin-name',
Expand All @@ -177,13 +178,14 @@ directory.

, where:

* **name** (required) - a name of an html-reporter plugin _package_. It expected to be `require`-resolvable from your project.
* **component** (optional) - React component name from the plugin.
* **point** (optional) - html-reporter's extension point name. Sets specific place within the html-reporter UI where to place the specified component. [More on extension points](#extension-points).
* **position** (optional) - specifies the way the component is going to be applied to the html-reporter UI extension point. Possible values are:
* **name** (required) `String` - a name of an html-reporter plugin _package_. It expected to be `require`-resolvable from your project.
* **component** (optional) `String` - React component name from the plugin.
* **point** (optional) `String` - html-reporter's extension point name. Sets specific place within the html-reporter UI where to place the specified component. [More on extension points](#extension-points).
* **position** (optional) `String` - specifies the way the component is going to be applied to the html-reporter UI extension point. Possible values are:
* `wrap` - to wrap the extension point UI
* `before` - to place the component before the extension point
* `after` - to place the component after the extension point
* **config** (optional) `Object` - plugin configuration

A plugin with only **name** specified may be used to redefine existing gui-server middleware.

Expand Down Expand Up @@ -257,7 +259,7 @@ directory.
The routes then can be called from the plugin React components defined in the `plugin.js`. For convenience the plugin name is always passed with options when function- or array-returning form is used to export plugin as the function options property `pluginName`:

```js
export default ['react', 'axios', function(React, axios, {pluginName, actions}) {
export default ['react', 'axios', function(React, axios, {pluginName, pluginConfig, actions}) {
class PluginComponent extends React.Component {
// ... somewhere inside the component ...
const result = await axios.get(`/plugin-routes/${pluginName}/plugin-route`);
Expand All @@ -270,7 +272,9 @@ directory.
}
```

In the example you can also see another convenience property `actions` with all the html-reporter **Redux** actions.
In the example you can also see another convenient properties:
- `actions` - all the html-reporter **Redux** actions;
- `pluginConfig` - plugin configuration.

#### Extension points

Expand Down
6 changes: 5 additions & 1 deletion lib/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const option = configParser.option;
const ENV_PREFIX = 'html_reporter_';
const CLI_PREFIX = '--html-reporter-';

const ALLOWED_PLUGIN_DESCRIPTION_FIELDS = new Set(['name', 'component', 'point', 'position']);
const ALLOWED_PLUGIN_DESCRIPTION_FIELDS = new Set(['name', 'component', 'point', 'position', 'config']);

const {config: configDefaults} = require('../constants/defaults');
const saveFormats = require('../constants/save-formats');
Expand Down Expand Up @@ -100,6 +100,10 @@ const assertPluginDescription = (description) => {
throw new Error(`"plugins.position" option got an unexpected value "${description.position}"`);
}

if (description.config && !_.isPlainObject(description.config)) {
throw new Error(`plugin configuration expected to be an object but got ${typeof description.config}`);
}

_.forOwn(description, (value, key) => {
if (!ALLOWED_PLUGIN_DESCRIPTION_FIELDS.has(key)) {
throw new Error(`a "plugins" item has unexpected field "${key}" of type ${typeof value}`);
Expand Down
8 changes: 4 additions & 4 deletions lib/static/modules/load-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const whitelistedDeps = {

const pendingPlugins = {};

export default async function loadPlugin(pluginName) {
export default async function loadPlugin(pluginName, pluginConfig) {
if (pendingPlugins[pluginName]) {
return pendingPlugins[pluginName];
}
Expand All @@ -55,14 +55,14 @@ export default async function loadPlugin(pluginName) {
return pendingPlugins[pluginName] = Promise.resolve(pluginScriptPath)
.then(getScriptText)
.then(executePluginCode)
.then(plugin => initPlugin(plugin, pluginName))
.then(plugin => initPlugin(plugin, pluginName, pluginConfig))
.then(null, err => {
console.error(`Plugin "${pluginName}" failed to load.`, err);
return null;
});
}

async function initPlugin(plugin, pluginName) {
async function initPlugin(plugin, pluginName, pluginConfig) {
try {
if (!_.isObject(plugin)) {
return null;
Expand All @@ -80,7 +80,7 @@ async function initPlugin(plugin, pluginName) {
const depArgs = deps.map(dep => whitelistedDeps[dep]);
// cyclic dep, resolve it dynamically
const actions = await import('./actions');
return plugin(...depArgs, {pluginName, actions});
return plugin(...depArgs, {pluginName, pluginConfig, actions});
}

return plugin;
Expand Down
2 changes: 1 addition & 1 deletion lib/static/modules/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function loadAll(config) {
}

const pluginConfigs = await Promise.all(config.plugins.map(async pluginConfig => {
const plugin = await loadPlugin(pluginConfig.name);
const plugin = await loadPlugin(pluginConfig.name, pluginConfig.config);
if (plugin) {
plugins[pluginConfig.name] = plugin;
return pluginConfig;
Expand Down
65 changes: 65 additions & 0 deletions test/unit/lib/static/modules/load-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

import axios from 'axios';
import loadPlugin from 'lib/static/modules/load-plugin';
import * as actions from 'lib/static/modules/actions';

describe('static/modules/load-plugin', () => {
const sandbox = sinon.sandbox.create();
let plugin;
let pluginFactory;

beforeEach(() => {
plugin = sinon.stub();
pluginFactory = [plugin];
global.pluginFactory = pluginFactory;

sandbox.stub(axios, 'get');
axios.get.resolves({
status: 200,
data: `__hermione_html_reporter_register_plugin__(pluginFactory)`
});
});

afterEach(() => {
sandbox.restore();
delete global.pluginFactory;
});

describe('loadPlugin', () => {
it('should call plugin constructor with plugin name', async () => {
await loadPlugin('plugin-a');

assert.deepStrictEqual(plugin.args, [
[{actions, pluginName: 'plugin-a', pluginConfig: undefined}]
]);
});

it('should call plugin constructor with plugin config', async () => {
const config = {param: 'value'};
await loadPlugin('plugin-b', config);

assert.deepStrictEqual(plugin.args, [
[{actions, pluginName: 'plugin-b', pluginConfig: config}]
]);
});

it('should call plugin constructor with plugin dependencies', async () => {
pluginFactory.unshift('axios');
await loadPlugin('plugin-c');

assert.deepStrictEqual(plugin.args, [
[axios, {actions, pluginName: 'plugin-c', pluginConfig: undefined}]
]);
});

it('should skip denied plugin dependencies', async () => {
pluginFactory.unshift('dependency');
await loadPlugin('plugin-d');

assert.deepStrictEqual(plugin.args, [
[undefined, {actions, pluginName: 'plugin-d', pluginConfig: undefined}]
]);
});
});
});
19 changes: 19 additions & 0 deletions test/unit/lib/static/modules/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ describe('static/modules/plugins', () => {

afterEach(() => sandbox.restore());

describe('loadAll', () => {
it('should call loadPlugin function with configuration', async () => {
const config = {param: 'value'};

await plugins.loadAll({
pluginsEnabled: true,
plugins: [
{name: 'plugin-a'},
{name: 'plugin-b', config}
]
});

assert.deepStrictEqual(loadPluginStub.args, [
['plugin-a', undefined],
['plugin-b', config]
]);
});
});

describe('getLoadedConfigs', () => {
it('should return empty array when no plugins are configured', async () => {
await plugins.loadAll();
Expand Down

0 comments on commit 5e7714b

Please sign in to comment.