Skip to content
This repository was archived by the owner on Nov 30, 2019. It is now read-only.

Commit a356928

Browse files
committed
feat(PluginManager): Added Plugin and PluginManager
The PluginManager loads plugins based on a given directory. Also added the plugin.json descriptor file
1 parent b7e0160 commit a356928

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Describes the structure of the plugin
3+
*
4+
* @export
5+
* @interface IPluginDescriptorFile
6+
* @since 0.0.1
7+
* @version 0.0.1
8+
* @author Yannick Fricke <yannickfricke@googlemail.com>
9+
* @license MIT
10+
* @copyright MedjaiBot https://github.com/MedjaiBot/server
11+
*/
12+
export interface IPluginDescriptorFile {
13+
/**
14+
* The main entry for the plugin
15+
* Can also be undefined
16+
*
17+
* @type {string}
18+
* @memberof IPluginDescriptorFile
19+
*/
20+
main?: string;
21+
}

src/plugin/Plugin.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* A basic plugin definition
3+
*
4+
* @export
5+
* @abstract
6+
* @class Plugin
7+
* @since 0.0.1
8+
* @version 0.0.1
9+
* @author Yannick Fricke <yannickfricke@googlemail.com>
10+
* @license MIT
11+
* @copyright MedjaiBot https://github.com/MedjaiBot/server
12+
*/
13+
export abstract class Plugin {
14+
/**
15+
* The id of the plugin in the followoing format
16+
* Github: "com.github.<You Username>.<Repo Name>"
17+
*
18+
* @type {string}
19+
* @memberof Plugin
20+
*/
21+
public id: string;
22+
23+
/**
24+
* The name of the plugin
25+
*
26+
* @type {string}
27+
* @memberof Plugin
28+
*/
29+
public name: string;
30+
31+
/**
32+
* The version of the plugin
33+
*
34+
* @type {string}
35+
* @memberof Plugin
36+
*/
37+
public version: string;
38+
39+
/**
40+
* The author(s) of the plugin
41+
*
42+
* @type {string}
43+
* @memberof Plugin
44+
*/
45+
public author: string;
46+
47+
/**
48+
* Creates an instance of Plugin.
49+
* @param {string} id The id iof the plugin
50+
* @param {string} name The name of the plugin
51+
* @param {string} version The version of the plugin
52+
* @param {string} author The author(s) of the plugin
53+
* @memberof Plugin
54+
*/
55+
constructor(
56+
id: string,
57+
name: string,
58+
version: string,
59+
author: string,
60+
) {
61+
this.id = id;
62+
this.name = name;
63+
this.version = version;
64+
this.author = author;
65+
}
66+
67+
/**
68+
* Lifecycle hook
69+
* Will be called when the plugin was loaded
70+
* Use this to do the initial work
71+
*
72+
* @abstract
73+
* @memberof Plugin
74+
*/
75+
public abstract onInit(): void;
76+
}

src/plugin/PluginManager.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// tslint:disable:forin
2+
3+
import { existsSync, readdirSync, readFileSync } from 'fs';
4+
import { inject, injectable } from 'inversify';
5+
import { resolve } from 'path';
6+
import { IsNullOrUndefined } from '../../Extras';
7+
import { Logger } from '../logger/Logger';
8+
import { IPluginDescriptorFile } from './IPluginDescriptorFile';
9+
import { Plugin } from './Plugin';
10+
11+
/**
12+
* The plugin manager manages plugins
13+
* The standard implementation is to load plugins
14+
* by a given directory
15+
*
16+
* @export
17+
* @class PluginManager
18+
* @since 0.0.1
19+
* @version 0.0.1
20+
* @author Yannick Fricke <yannickfricke@googlemail.com>
21+
* @license MIT
22+
* @copyright MedjaiBot https://github.com/MedjaiBot/server
23+
*/
24+
@injectable()
25+
export class PluginManager {
26+
/**
27+
* The plugins which will be managed by the plugin manager
28+
*
29+
* @type {Plugin[]}
30+
* @memberof PluginManager
31+
*/
32+
public plugins: Plugin[];
33+
34+
/**
35+
* The logger which will be used
36+
* Mostly loaded from the dependency injection container
37+
*
38+
* @private
39+
* @type {Logger}
40+
* @memberof PluginManager
41+
*/
42+
private logger: Logger;
43+
44+
/**
45+
* Creates an instance of PluginManager.
46+
* @param {Logger} logger The logger which should be used from the dependency injection container
47+
* @memberof PluginManager
48+
*/
49+
constructor(
50+
@inject(Symbol.for('Logger'))
51+
logger: Logger,
52+
) {
53+
// Sets the plugin property to an empty array
54+
this.plugins = [];
55+
56+
// Sets the logger property to the given logger
57+
this.logger = logger;
58+
}
59+
60+
/**
61+
* Loads all plugins from the given directory
62+
*
63+
* @param directory The directory where to load the plugins from
64+
* @memberof PluginManager
65+
*/
66+
public loadPlugins = (directory: string) => {
67+
// Checks if the given directory exists on the filesystem
68+
if (!existsSync(directory)) {
69+
throw new Error(`Directory ${directory} does not exists`);
70+
}
71+
72+
// Loads the contents of the directory
73+
const pluginDirectories = readdirSync(directory);
74+
75+
// Iterates over the contents of the directory
76+
for (const index in pluginDirectories) {
77+
// Gets the current entry from the contents
78+
const pluginDirectory = pluginDirectories[index];
79+
80+
// Resolves the given parts into a path as string
81+
const pluginJsonFile = resolve(directory, pluginDirectory, 'plugin.json');
82+
83+
// Checks if the plugin.json file exists
84+
if (!existsSync(pluginJsonFile)) {
85+
this.logger.warn(
86+
`Plugin directory ${pluginDirectory} does not contain a plugin.json. Skipping plugin.`,
87+
);
88+
89+
continue;
90+
}
91+
92+
// The loaded plugin.json file contents
93+
let pluginFileContents;
94+
95+
try {
96+
pluginFileContents = readFileSync(pluginJsonFile).toString();
97+
} catch (error) {
98+
this.logger.error('Could not read the contents of the plugin.json file', error);
99+
100+
continue;
101+
}
102+
103+
// Parses the the contents of plugin.json file as JSON
104+
const parsedPluginFile: IPluginDescriptorFile = JSON.parse(pluginFileContents);
105+
106+
// The main key of the parsed plugin file
107+
const mainEntry = parsedPluginFile.main;
108+
109+
// Checks if the main entry is null or undefined
110+
if (IsNullOrUndefined(mainEntry)) {
111+
this.logger.debug('The parsed config key "main" is undefined');
112+
113+
continue;
114+
}
115+
116+
// Check if the main entry wants try to include a file outside of the plugin directory
117+
if ((mainEntry as string).includes('..')) {
118+
this.logger.debug('The main entry must be located inside in the plugin directory');
119+
}
120+
121+
// The plugin structure of the file
122+
let plugin;
123+
124+
try {
125+
// Requires the file which is defined in the "main" key
126+
plugin = require(
127+
resolve(
128+
directory,
129+
pluginDirectory,
130+
mainEntry as string,
131+
));
132+
} catch (error) {
133+
this.logger.error(`Could not require plugin ${pluginDirectory}`, error);
134+
135+
continue;
136+
}
137+
138+
if (typeof plugin.default !== 'function') {
139+
this.logger.warn(`The default export of the plugin ${pluginDirectory} is not a function`);
140+
141+
continue;
142+
}
143+
144+
// The plugin instance
145+
const pluginInstance: Plugin = new plugin.default();
146+
147+
try {
148+
// Trying to call the onInit function of the plugin
149+
pluginInstance.onInit();
150+
} catch (error) {
151+
this.logger.error(`Could not call onInit for plugin "${pluginDirectory}"`, error);
152+
153+
continue;
154+
}
155+
156+
// Add the plugin to the managed plugins
157+
this.plugins.push(pluginInstance);
158+
}
159+
}
160+
}

0 commit comments

Comments
 (0)