Skip to content

Commit 900be79

Browse files
author
Ovidiu Barabula
committed
feat(core): improve plugin manager and add support for node modules plugin loading
1 parent d1a026f commit 900be79

File tree

2 files changed

+169
-20
lines changed

2 files changed

+169
-20
lines changed

src/plugin-manager/index.spec.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ describe('PluginManager', () => {
1818
hooks: ['before', 'midway', 'after'],
1919
});
2020
configWizard = ConfigWizard(await ConfigManager());
21-
validPlugin = { install: () => true };
21+
validPlugin = {
22+
install: () => true,
23+
name: 'my-valid-plugin',
24+
};
2225
});
2326

2427

2528
it('instantiates', () => {
2629
const pluginManager = PluginManager(taskManager, configWizard, logger);
27-
expect(pluginManager).to.be.an('object')
28-
.to.contain.keys('use');
30+
expect(pluginManager).to.be.an('object').to.contain.keys('use');
2931
});
3032

3133

@@ -43,6 +45,76 @@ describe('PluginManager', () => {
4345
});
4446

4547

48+
describe('private method loadPlugin()', () => {
49+
let pluginManager;
50+
51+
beforeEach(() => {
52+
pluginManager = PluginManager(taskManager, configWizard, logger);
53+
});
54+
55+
56+
it('throws if plugin name is not a string', () => {
57+
assert.throws(
58+
() => pluginManager.loadPlugin(1),
59+
ERRORS.PLUGIN_NAME_SHOULD_BE_STRING,
60+
);
61+
});
62+
63+
64+
it('throws if plugin is not found', () => {
65+
assert.throws(
66+
() => pluginManager.loadPlugin('non-existent-plugin'),
67+
ERRORS.PLUGIN_NOT_FOUND,
68+
);
69+
});
70+
});
71+
72+
73+
describe('private method parsePlugins()', () => {
74+
let pluginManager;
75+
76+
beforeEach(() => {
77+
pluginManager = PluginManager(taskManager, configWizard, logger);
78+
});
79+
80+
81+
it('returns a promise', () => {
82+
expect(pluginManager.parsePlugins(['myplugin1', 'myplugin2']))
83+
.to.satisfy(promise => promise instanceof Promise);
84+
});
85+
86+
87+
it('returns empty array if no valid plugins are found or loaded', () => {
88+
return expect(pluginManager.parsePlugins(['myplugin1', 'myplugin2']))
89+
.to.eventually.be.an('array').to.be.empty;
90+
});
91+
92+
93+
it('returns an array with installable plugin when installable object is passed', () => {
94+
return expect(pluginManager.parsePlugins([{
95+
hook: 'myhook',
96+
name: 'mytask',
97+
taskFn: () => true,
98+
}]))
99+
.to.eventually.be.an('array')
100+
.to.eventually.have.lengthOf(1)
101+
.that.eventually.satisfies(array => array[0].hasOwnProperty('install'));
102+
});
103+
104+
105+
it('returns an array with the same installable plugin that was passed in', () => {
106+
return expect(pluginManager.parsePlugins([{
107+
description: 'myPlugin\'s description',
108+
install: () => true,
109+
name: 'myPlugin',
110+
}]))
111+
.to.eventually.be.an('array')
112+
.to.eventually.have.lengthOf(1)
113+
.that.eventually.satisfies(array => array[0].hasOwnProperty('install'));
114+
});
115+
});
116+
117+
46118
it('calls taskManager.getSubscribers() method if plugin is valid', async () => {
47119
let called = false;
48120

@@ -74,6 +146,7 @@ describe('PluginManager', () => {
74146

75147
it('passes taskSubscribers and configSubscriber arguments to plugin.install() method', async () => {
76148
const pluginStub = {
149+
name: 'my-valid-plugin',
77150
install(taskSubscribers, configSubscriber) {
78151
expect(taskSubscribers)
79152
.to.be.be.an('object')

src/plugin-manager/index.ts

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
/**
22
* Name: index.ts
3-
* Description: Plugin manager and validator
3+
* Description: Plugin loader, manager and validator
44
* Author: Ovidiu Barabula <lectii2008@gmail.com>
55
* @since 0.1.0
66
*/
77

8+
import * as path from 'path';
89
import { IConfigWizard, QuestionnaireSubscriber } from '../config-wizard';
910
import { TaskManager, TaskSubscriber } from '../task-manager';
1011
import Logger, { ILogger } from '../util/logger';
12+
import { dynamicRequire, flattenArray, pluginName } from '../util/utility-functions';
1113
import Installable, { InstallableObject } from './installable';
1214

1315
export interface Plugin {
@@ -17,17 +19,23 @@ export interface Plugin {
1719
}
1820

1921
export interface PluginManager {
20-
use(plugin: Plugin | InstallableObject): Promise<void>;
21-
validate?(plugin: Plugin): boolean;
22+
use(...plugin: Array<string|Plugin|InstallableObject>): Promise<void>;
23+
/* test:start */
24+
loadPlugin?(name: string): any;
25+
parsePlugins?(plugins: PluginsArray): Promise<Plugin[]>;
26+
/* test:end */
2227
}
2328

29+
export type PluginsArray = Array<string|Plugin|InstallableObject>;
2430
export type PluginSubscribers = TaskSubscriber | QuestionnaireSubscriber;
2531

2632

2733
// Custom error messages
2834
export const ERRORS = {
2935
NO_CONFIG_WIZARD: 'PluginManager() requires second argument to be a ConfigWizard instance',
3036
NO_TASK_MANAGER: 'PluginManager() requires first argument to be a TaskManager instance',
37+
PLUGIN_NAME_SHOULD_BE_STRING: 'PluginManager() passed in plugin name should be a string',
38+
PLUGIN_NOT_FOUND: 'PluginManager> plugin could not be loaded',
3139
};
3240

3341

@@ -56,6 +64,57 @@ function PluginManager(
5664
}
5765

5866

67+
/**
68+
* Parse list of plugins and return installable plugin objects
69+
* @param plugins Array of plugins, plugin names or installable objects
70+
*/
71+
async function parsePlugins(plugins: PluginsArray): Promise<Plugin[]> {
72+
return await Promise.all(
73+
plugins.reduce((pluginsArray: PluginsArray, item) => {
74+
let plugin: PluginsArray|Plugin|InstallableObject;
75+
76+
// Try to load the plugin if current item is a plugin name
77+
if (typeof item === 'string') {
78+
try {
79+
plugin = loadPlugin(item);
80+
} catch (error) {
81+
// Log the error and return the array without the bad plugin
82+
logger.error(error.message);
83+
return pluginsArray;
84+
}
85+
// If it's not a string, then it must be a Plugin or InstallableObject
86+
} else {
87+
plugin = item;
88+
}
89+
90+
// Flatten everything so we have a one dimentional array
91+
// Loaded plugins could be arrays of installable objects
92+
return flattenArray([pluginsArray, plugin]);
93+
}, [])
94+
95+
// Convert all to installable plugins
96+
.map((plugin: Plugin | InstallableObject) => Installable(plugin)),
97+
);
98+
}
99+
100+
101+
/**
102+
* Get plugin from node_modules and return it
103+
* @param name Plugin name
104+
*/
105+
function loadPlugin(name: string): any {
106+
if (typeof name !== 'string') {
107+
throw new Error(ERRORS.PLUGIN_NAME_SHOULD_BE_STRING);
108+
}
109+
110+
try {
111+
return dynamicRequire(pluginName(name));
112+
} catch (error) {
113+
throw new Error(`${ERRORS.PLUGIN_NOT_FOUND}: ${error.message}`);
114+
}
115+
}
116+
117+
59118
/**
60119
* Provides an array of plugin subscribers
61120
*/
@@ -70,27 +129,44 @@ function PluginManager(
70129

71130

72131
/**
73-
* Register plugin
74-
* @param plugin Plugin object
132+
* Convert to installable plugin and call the plugin .install() method
133+
* @param plugin Plugin or Installable object
75134
*/
76-
async function use(plugin: Plugin | InstallableObject): Promise<void> {
77-
// TODO: Add support for plugin as string
78-
// TODO: When plugin is of type string, look for plugin in node_modules
79-
135+
async function install(plugin: Plugin): Promise<void> {
136+
// Get the array of plugin subscribers (tasks, questionnaires, etc.)
80137
const subscribers: PluginSubscribers[] = getPluginSubscribers();
138+
// Call the plugin's .install() method and provide plugin subscriber objects
139+
await plugin.install(...subscribers);
140+
}
81141

82-
try {
83-
await Installable(plugin).install(...subscribers);
84-
} catch (error) {
85-
logger.error(error.message);
86-
}
142+
143+
/**
144+
* Register plugin(s)
145+
* @param plugins Plugin object(s)
146+
*/
147+
async function use(...plugins: PluginsArray): Promise<void> {
148+
// Parse plugins array and get installable plugins
149+
const installablePlugins = await parsePlugins(plugins);
150+
// Install each plugin
151+
await installablePlugins.map(async (plugin: Plugin) => await install(plugin));
87152
}
88153

89154

90-
// Return public API
91-
return Object.freeze({
155+
// Public API
156+
let publicApi: PluginManager = {
92157
use,
93-
});
158+
};
159+
160+
/* test:start */
161+
// Add private methods to API, for testing only
162+
publicApi = {...publicApi,
163+
loadPlugin,
164+
parsePlugins,
165+
};
166+
/* test:end */
167+
168+
// Return public API
169+
return Object.freeze(publicApi);
94170
}
95171

96172
export default PluginManager;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy