node-pluginsmanager
Version:
452 lines (451 loc) • 20.8 kB
JavaScript
"use strict";
// deps
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
// natives
const node_events_1 = __importDefault(require("node:events"));
const node_path_1 = require("node:path");
const node_os_1 = require("node:os");
const promises_1 = require("node:fs/promises");
// externals
const check_version_modules_1 = __importDefault(require("check-version-modules"));
// locals
const rmdirp_1 = __importDefault(require("./utils/rmdirp"));
const checkAbsoluteDirectory_1 = __importDefault(require("./checkers/checkAbsoluteDirectory"));
const checkFunction_1 = __importDefault(require("./checkers/checkFunction"));
const checkNonEmptyArray_1 = __importDefault(require("./checkers/checkNonEmptyArray"));
const checkNonEmptyString_1 = __importDefault(require("./checkers/checkNonEmptyString"));
const checkOrchestrator_1 = __importDefault(require("./checkers/checkOrchestrator"));
const createPluginByDirectory_1 = __importDefault(require("./createPluginByDirectory"));
const loadSortedPlugins_1 = __importDefault(require("./loadSortedPlugins"));
const initSortedPlugins_1 = __importDefault(require("./initSortedPlugins"));
const extractGithub_1 = __importDefault(require("./extractGithub"));
// git
const gitInstall_1 = __importDefault(require("./cmd/git/gitInstall"));
const gitUpdate_1 = __importDefault(require("./cmd/git/gitUpdate"));
// npm
const npmInstall_1 = __importDefault(require("./cmd/npm/npmInstall"));
const npmUpdate_1 = __importDefault(require("./cmd/npm/npmUpdate"));
// consts
const DEFAULT_PLUGINS_DIRECTORY = (0, node_path_1.join)((0, node_os_1.homedir)(), "node-pluginsmanager-plugins");
const DEFAULT_RESSOURCES_DIRECTORY = (0, node_path_1.join)((0, node_os_1.homedir)(), "node-pluginsmanager-resources");
// module
class PluginsManager extends node_events_1.default {
// constructor
constructor(options) {
super();
// protected
this._beforeLoadAll = null;
this._beforeInitAll = null;
this._orderedPluginsNames = [];
this._logger = options && "function" === typeof options.logger
? options.logger
: null;
// public
this.plugins = [];
this.directory = options && "undefined" !== typeof options.directory
? options.directory
: DEFAULT_PLUGINS_DIRECTORY;
this.externalRessourcesDirectory = options && "undefined" !== typeof options.externalRessourcesDirectory
? options.externalRessourcesDirectory
: DEFAULT_RESSOURCES_DIRECTORY;
}
// public
getPluginsNames() {
return [...this.plugins].map((plugin) => {
return plugin.name;
});
}
// create a forced order to synchronously initialize plugins. not ordered plugins are asynchronously initialized after.
setOrder(pluginsNames) {
return (0, checkNonEmptyArray_1.default)("setOrder/pluginsNames", pluginsNames).then(() => {
const errors = [];
for (let i = 0; i < pluginsNames.length; ++i) {
if ("string" !== typeof pluginsNames[i]) {
errors.push("The directory at index \"" + i + "\" must be a string");
}
else if ("" === pluginsNames[i].trim()) {
errors.push("The directory at index \"" + i + "\" must be not empty");
}
else if (1 < pluginsNames.filter((name) => {
return name === pluginsNames[i];
}).length) {
errors.push("The directory at index \"" + i + "\" is given twice or more");
}
}
return !errors.length ? Promise.resolve() : Promise.reject(new Error(errors.join("\r\n")));
}).then(() => {
this._orderedPluginsNames = pluginsNames;
return Promise.resolve();
});
}
getOrder() {
return [...this._orderedPluginsNames];
}
checkAllModules() {
const _checkPluginsModules = (i = 0) => {
return i < this.plugins.length ? this.checkModules(this.plugins[i]).then(() => {
return _checkPluginsModules(i + 1);
}) : Promise.resolve();
};
return _checkPluginsModules();
}
checkModules(plugin) {
return (0, checkAbsoluteDirectory_1.default)("checkModules/directory", this.directory).then(() => {
return (0, checkOrchestrator_1.default)("checkModules/plugin", plugin);
}).then(() => {
return (0, check_version_modules_1.default)((0, node_path_1.join)(this.directory, plugin.name, "package.json"), {
"failAtMajor": true,
"failAtMinor": true,
"failAtPatch": false,
"dev": false
}).then((analyze) => {
return analyze.result ? Promise.resolve() : Promise.reject(new Error("\"" + plugin.name + "\" plugin has obsoletes modules"));
});
});
}
// used for execute all plugins' middlewares in app (express or other)
appMiddleware(req, res, next) {
// limit to plugins which has registered middleware
const plugins = this.plugins.filter((plugin) => {
return "function" === typeof plugin.appMiddleware;
});
if (0 >= plugins.length) {
return next();
}
else {
const _recursiveNext = (i) => {
if (i < plugins.length) {
return () => {
return plugins[i].appMiddleware(req, res, _recursiveNext(i + 1));
};
}
// if no more plugin to try, pass the lead to the application
else {
return next;
}
};
return plugins[0].appMiddleware(req, res, _recursiveNext(1)); // must start at index "1", cause the "0" is executed here
}
}
// middleware for socket to add bilateral push events
socketMiddleware(server) {
this.plugins.filter((plugin) => {
return "function" === typeof plugin.socketMiddleware;
}).forEach((plugin) => {
plugin.socketMiddleware(server);
});
}
// add a function executed before loading all plugins
beforeLoadAll(callback) {
return (0, checkFunction_1.default)("beforeLoadAll/callback", callback).then(() => {
this._beforeLoadAll = callback;
return Promise.resolve();
});
}
// load all plugins asynchronously, using "data" in arguments for "load" plugin's Orchestrator method
loadAll(...data) {
// create dir if not exist
return (0, checkNonEmptyString_1.default)("initAll/directory", this.directory).then(() => {
return (0, promises_1.mkdir)(this.directory, {
"recursive": true
}).then(() => {
return (0, checkAbsoluteDirectory_1.default)("initAll/directory", this.directory);
});
// create dir if not exist
}).then(() => {
return (0, checkNonEmptyString_1.default)("initAll/externalRessourcesDirectory", this.externalRessourcesDirectory).then(() => {
return (0, promises_1.mkdir)(this.externalRessourcesDirectory, {
"recursive": true
}).then(() => {
return (0, checkAbsoluteDirectory_1.default)("initAll/externalRessourcesDirectory", this.externalRessourcesDirectory);
});
});
// ensure loop
}).then(() => {
return new Promise((resolve) => {
setImmediate(resolve);
});
// execute _beforeLoadAll
}).then(() => {
return "function" !== typeof this._beforeLoadAll ? Promise.resolve() : new Promise((resolve, reject) => {
const fn = this._beforeLoadAll(...data);
if (!(fn instanceof Promise)) {
resolve();
}
else {
fn.then(resolve).catch(reject);
}
});
// init plugins
}).then(() => {
return (0, promises_1.readdir)(this.directory);
// load all
}).then((files) => {
return (0, loadSortedPlugins_1.default)(this.directory, this.externalRessourcesDirectory, files, this.plugins, this._orderedPluginsNames, this.emit.bind(this), this._logger, ...data);
// sort plugins
}).then(() => {
this.plugins.sort((a, b) => {
if (a.name < b.name) {
return -1;
}
else if (a.name > b.name) {
return 1;
}
else {
return 0;
}
});
return Promise.resolve();
// end
}).then(() => {
this.emit("allloaded", ...data);
return Promise.resolve();
});
}
// after releasing, destroy packages data & free "plugins" list, using "data" in arguments for "destroy" plugin's Orchestrator method
destroyAll(...data) {
return Promise.resolve().then(() => {
const _destroyPlugin = (i = this.plugins.length - 1) => {
return -1 < i ? Promise.resolve().then(() => {
const pluginName = this.plugins[i].name;
// emit event
return this.plugins[i].destroy().then(() => {
this.emit("destroyed", pluginName, ...data);
return Promise.resolve();
// loop
}).then(() => {
return _destroyPlugin(i - 1);
});
}) : Promise.resolve();
};
return _destroyPlugin();
// end
}).then(() => {
this.plugins = [];
this.emit("alldestroyed", ...data);
return Promise.resolve();
// remove all external resources
}).then(() => {
return (0, rmdirp_1.default)(this.externalRessourcesDirectory);
});
}
// add a function executed before initializing all plugins
beforeInitAll(callback) {
return (0, checkFunction_1.default)("beforeInitAll/callback", callback).then(() => {
this._beforeInitAll = callback;
return Promise.resolve();
});
}
// initialize all plugins asynchronously, using "data" in arguments for "init" plugin's Orchestrator method
initAll(...data) {
return (0, checkAbsoluteDirectory_1.default)("initAll/directory", this.directory).then(() => {
return (0, checkAbsoluteDirectory_1.default)("initAll/externalRessourcesDirectory", this.externalRessourcesDirectory);
}).then(() => {
// execute _beforeInitAll
return "function" !== typeof this._beforeInitAll ? Promise.resolve() : new Promise((resolve, reject) => {
const fn = this._beforeInitAll(...data);
if (!(fn instanceof Promise)) {
resolve();
}
else {
fn.then(resolve).catch(reject);
}
});
// init plugins
}).then(() => {
return (0, initSortedPlugins_1.default)(this.plugins, this._orderedPluginsNames, this.emit.bind(this), ...data);
// end
}).then(() => {
this.emit("allinitialized", ...data);
return Promise.resolve();
});
}
// release a plugin (keep package but destroy Mediator & Server), using "data" in arguments for "release" plugin's Orchestrator method
releaseAll(...data) {
return Promise.resolve().then(() => {
const _releasePlugin = (i = this.plugins.length - 1) => {
return -1 < i ? Promise.resolve().then(() => {
return this.plugins[i].release(data);
// emit event
}).then(() => {
this.emit("released", this.plugins[i], ...data);
return Promise.resolve();
// loop
}).then(() => {
return _releasePlugin(i - 1);
}) : Promise.resolve();
};
return _releasePlugin();
// end
}).then(() => {
this.emit("allreleased", ...data);
return Promise.resolve();
});
}
// install a plugin via github repo, using "data" in arguments for "install" and "init" plugin's Orchestrator methods
installViaGithub(user, repo, ...data) {
return (0, checkAbsoluteDirectory_1.default)("installViaGithub/directory", this.directory).then(() => {
return (0, checkNonEmptyString_1.default)("installViaGithub/user", user);
}).then(() => {
return (0, checkNonEmptyString_1.default)("installViaGithub/repo", repo);
}).then(() => {
const directory = (0, node_path_1.join)(this.directory, repo);
return new Promise((resolve, reject) => {
(0, checkAbsoluteDirectory_1.default)("installViaGithub/plugindirectory", directory).then(() => {
return reject(new Error("\"" + repo + "\" plugin already exists"));
}).catch(() => {
return resolve(directory);
});
});
}).then((directory) => {
return Promise.resolve().then(() => {
return (0, gitInstall_1.default)(directory, user, repo);
}).then(() => {
// install dependencies & execute install script
return (0, createPluginByDirectory_1.default)(directory, this.externalRessourcesDirectory, this._logger, ...data);
// check plugin modules versions
}).then((plugin) => {
return this.checkModules(plugin).then(() => {
return Promise.resolve(plugin);
});
}).then((plugin) => {
return Promise.resolve().then(() => {
return !plugin.dependencies ? Promise.resolve() : (0, npmInstall_1.default)(directory);
}).then(() => {
return plugin.install(...data);
// execute init script
}).then(() => {
this.emit("installed", plugin, ...data);
return plugin.init(...data);
}).then(() => {
this.emit("initialized", plugin, ...data);
this.plugins.push(plugin);
return Promise.resolve(plugin);
}).catch((err) => {
return this.uninstall(plugin, ...data).then(() => {
return Promise.reject(err);
});
});
}).catch((err) => {
return new Promise((resolve, reject) => {
(0, checkAbsoluteDirectory_1.default)("installViaGithub/plugindirectory", directory).then(() => {
return (0, rmdirp_1.default)(directory).then(() => {
return err ? reject(err) : resolve();
});
}).catch(() => {
return err ? reject(err) : resolve();
});
});
});
});
}
// update a plugin via its github repo, using "data" in arguments for "release", "update" and "init" plugin's methods
updateViaGithub(plugin, ...data) {
let directory = "";
let key = -1;
// check plugin
return Promise.resolve().then(() => {
return (0, checkOrchestrator_1.default)("updateViaGithub/plugin", plugin).then(() => {
return (0, checkNonEmptyString_1.default)("updateViaGithub/github", (0, extractGithub_1.default)(plugin)).catch(() => {
return Promise.reject(new ReferenceError("Plugin \"" + plugin.name + "\" must be linked in the package to a github project to be updated"));
});
}).then(() => {
key = this.getPluginsNames().findIndex((pluginName) => {
return pluginName === plugin.name;
});
return -1 < key ? Promise.resolve() : Promise.reject(new Error("Plugin \"" + plugin.name + "\" is not registered"));
});
// check plugin directory
}).then(() => {
return (0, checkAbsoluteDirectory_1.default)("updateViaGithub/directory", this.directory).then(() => {
directory = (0, node_path_1.join)(this.directory, plugin.name);
return (0, checkAbsoluteDirectory_1.default)("updateViaGithub/plugindirectory", directory);
});
// release plugin
}).then(() => {
const pluginName = plugin.name;
return plugin.release(...data).then(() => {
this.emit("released", plugin, ...data);
return plugin.destroy();
}).then(() => {
this.emit("destroyed", pluginName, ...data);
this.plugins.splice(key, 1);
return Promise.resolve();
});
// download plugin
}).then(() => {
return (0, gitUpdate_1.default)(directory).then(() => {
return (0, createPluginByDirectory_1.default)(directory, this.externalRessourcesDirectory, this._logger, ...data);
});
// check plugin modules versions
}).then((_plugin) => {
return this.checkModules(_plugin).then(() => {
return Promise.resolve(_plugin);
});
}).then((_plugin) => {
// update dependencies & execute update script
return Promise.resolve().then(() => {
return !_plugin.dependencies ? Promise.resolve() : (0, npmUpdate_1.default)(directory);
}).then(() => {
return _plugin.update(...data);
}).then(() => {
this.emit("updated", _plugin, ...data);
return _plugin.init(...data);
// execute init script
}).then(() => {
this.emit("initialized", _plugin, ...data);
this.plugins[key] = _plugin;
return Promise.resolve(_plugin);
});
});
}
// uninstall a plugin, using "data" in arguments for "release" and "uninstall" plugin's methods
uninstall(plugin, ...data) {
let directory = "";
let key = -1;
let pluginName = "";
// check plugin
return Promise.resolve().then(() => {
return (0, checkOrchestrator_1.default)("uninstall/plugin", plugin).then(() => {
key = this.getPluginsNames().findIndex((name) => {
return name === plugin.name;
});
return -1 < key ? Promise.resolve() : Promise.reject(new Error("Plugin \"" + plugin.name + "\" is not registered"));
});
// check plugin directory
}).then(() => {
return (0, checkAbsoluteDirectory_1.default)("uninstall/directory", this.directory).then(() => {
pluginName = plugin.name;
directory = (0, node_path_1.join)(this.directory, pluginName);
return (0, checkAbsoluteDirectory_1.default)("uninstall/plugindirectory", directory);
});
// release plugin
}).then(() => {
return plugin.release(...data).then(() => {
return (0, rmdirp_1.default)((0, node_path_1.join)(this.externalRessourcesDirectory));
}).then(() => {
this.emit("released", plugin, ...data);
return plugin.destroy(...data);
}).then(() => {
this.emit("destroyed", pluginName, ...data);
this.plugins.splice(key, 1);
return Promise.resolve();
});
// update dependencies & execute update script
}).then(() => {
return Promise.resolve().then(() => {
return plugin.uninstall(...data);
}).then(() => {
this.emit("uninstalled", pluginName, ...data);
return (0, rmdirp_1.default)(directory);
});
}).then(() => {
return Promise.resolve(pluginName);
});
}
}
exports.default = PluginsManager;
;