Initial plugin runtime api implementation

This commit is contained in:
Nick O'Leary
2020-12-10 16:01:55 +00:00
parent bebebaa3dd
commit a006b52052
25 changed files with 1026 additions and 121 deletions

View File

@@ -36,78 +36,140 @@ function load(disableNodePathScan) {
// To skip node scan, the following line will use the stored node list.
// We should expose that as an option at some point, although the
// performance gains are minimal.
//return loadNodeFiles(registry.getModuleList());
//return loadModuleFiles(registry.getModuleList());
log.info(log._("server.loading"));
var nodeFiles = localfilesystem.getNodeFiles(disableNodePathScan);
return loadNodeFiles(nodeFiles);
var modules = localfilesystem.getNodeFiles(disableNodePathScan);
return loadModuleFiles(modules);
}
function loadNodeFiles(nodeFiles) {
function loadModuleTypeFiles(module, type) {
const things = module[type];
var first = true;
var promises = [];
var nodes = [];
for (var module in nodeFiles) {
for (var thingName in things) {
/* istanbul ignore else */
if (nodeFiles.hasOwnProperty(module)) {
if (nodeFiles[module].redVersion &&
!semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), nodeFiles[module].redVersion)) {
if (things.hasOwnProperty(thingName)) {
if (module.name != "node-red" && first) {
// Check the module directory exists
first = false;
var fn = things[thingName].file;
var parts = fn.split("/");
var i = parts.length-1;
for (;i>=0;i--) {
if (parts[i] == "node_modules") {
break;
}
}
var moduleFn = parts.slice(0,i+2).join("/");
try {
var stat = fs.statSync(moduleFn);
} catch(err) {
// Module not found, don't attempt to load its nodes
break;
}
}
try {
var promise;
if (type === "nodes") {
promise = loadNodeConfig(things[thingName]);
} else if (type === "plugins") {
promise = loadPluginConfig(things[thingName]);
}
promises.push(
promise.then(
(function() {
var m = module.name;
var n = thingName;
return function(nodeSet) {
things[n] = nodeSet;
return nodeSet;
}
})()
).catch(err => {console.log(err)})
);
} catch(err) {
console.log(err)
//
}
}
}
return promises;
}
function loadModuleFiles(modules) {
var pluginPromises = [];
var nodePromises = [];
for (var module in modules) {
/* istanbul ignore else */
if (modules.hasOwnProperty(module)) {
if (modules[module].redVersion &&
!semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), modules[module].redVersion)) {
//TODO: log it
log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:nodeFiles[module].redVersion}));
nodeFiles[module].err = "version_mismatch";
log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:modules[module].redVersion}));
modules[module].err = "version_mismatch";
continue;
}
if (module == "node-red" || !registry.getModuleInfo(module)) {
var first = true;
for (var node in nodeFiles[module].nodes) {
/* istanbul ignore else */
if (nodeFiles[module].nodes.hasOwnProperty(node)) {
if (module != "node-red" && first) {
// Check the module directory exists
first = false;
var fn = nodeFiles[module].nodes[node].file;
var parts = fn.split("/");
var i = parts.length-1;
for (;i>=0;i--) {
if (parts[i] == "node_modules") {
break;
}
}
var moduleFn = parts.slice(0,i+2).join("/");
try {
var stat = fs.statSync(moduleFn);
} catch(err) {
// Module not found, don't attempt to load its nodes
break;
}
}
try {
promises.push(loadNodeConfig(nodeFiles[module].nodes[node]).then((function() {
var m = module;
var n = node;
return function(nodeSet) {
nodeFiles[m].nodes[n] = nodeSet;
nodes.push(nodeSet);
}
})()).catch(err => {}));
} catch(err) {
//
}
}
if (modules[module].nodes) {
nodePromises = nodePromises.concat(loadModuleTypeFiles(modules[module], "nodes"));
}
if (modules[module].plugins) {
pluginPromises = pluginPromises.concat(loadModuleTypeFiles(modules[module], "plugins"));
}
}
}
}
return Promise.all(promises).then(function(results) {
for (var module in nodeFiles) {
if (nodeFiles.hasOwnProperty(module)) {
if (!nodeFiles[module].err) {
registry.addModule(nodeFiles[module]);
var pluginList;
var nodeList;
return Promise.all(pluginPromises).then(function(results) {
pluginList = results.filter(r => !!r);
// Initial plugin load has happened. Ensure modules that provide
// plugins are in the registry now.
for (var module in modules) {
if (modules.hasOwnProperty(module)) {
if (modules[module].plugins && Object.keys(modules[module].plugins).length > 0) {
// Add the modules for plugins
if (!modules[module].err) {
registry.addModule(modules[module]);
}
}
}
}
return loadNodeSetList(nodes);
return loadNodeSetList(pluginList);
}).then(function() {
return Promise.all(nodePromises);
}).then(function(results) {
nodeList = results.filter(r => !!r);
// Initial node load has happened. Ensure remaining modules are in the registry
for (var module in modules) {
if (modules.hasOwnProperty(module)) {
if (!modules[module].plugins || Object.keys(modules[module].plugins).length === 0) {
if (!modules[module].err) {
registry.addModule(modules[module]);
}
}
}
}
return loadNodeSetList(nodeList);
});
}
async function loadPluginTemplate(plugin) {
return fs.readFile(plugin.template,'utf8').then(content => {
plugin.config = content;
return plugin;
}).catch(err => {
if (err.code === 'ENOENT') {
plugin.err = "Error: "+plugin.template+" does not exist";
} else {
plugin.err = err.toString();
}
return plugin;
});
}
@@ -175,11 +237,12 @@ async function loadNodeLocales(node) {
node.namespace = node.module;
return node
}
return fs.stat(path.join(path.dirname(node.file),"locales")).then(stat => {
const baseFile = node.file||node.template;
return fs.stat(path.join(path.dirname(baseFile),"locales")).then(stat => {
node.namespace = node.id;
return i18n.registerMessageCatalog(node.id,
path.join(path.dirname(node.file),"locales"),
path.basename(node.file,".js")+".json")
path.join(path.dirname(baseFile),"locales"),
path.basename(baseFile).replace(/\.[^.]+$/,".json"))
.then(() => node);
}).catch(err => {
node.namespace = node.module;
@@ -204,6 +267,7 @@ async function loadNodeConfig(fileInfo) {
}
var node = {
type: "node",
id: id,
module: module,
name: name,
@@ -227,6 +291,58 @@ async function loadNodeConfig(fileInfo) {
return node;
}
async function loadPluginConfig(fileInfo) {
var file = fileInfo.file;
var module = fileInfo.module;
var name = fileInfo.name;
var version = fileInfo.version;
var id = module + "/" + name;
var isEnabled = true;
// TODO: registry.getPluginInfo
// var info = registry.getPluginInfo(id);
// if (info) {
// if (info.hasOwnProperty("loaded")) {
// throw new Error(file+" already loaded");
// }
// isEnabled = info.enabled;
// }
if (!fs.existsSync(jsFile)) {
}
var plugin = {
type: "plugin",
id: id,
module: module,
name: name,
enabled: isEnabled,
loaded:false,
version: version,
local: fileInfo.local,
plugins: [],
config: "",
help: {}
};
var jsFile = file.replace(/\.[^.]+$/,".js");
var htmlFile = file.replace(/\.[^.]+$/,".html");
if (fs.existsSync(jsFile)) {
plugin.file = jsFile;
}
if (fs.existsSync(htmlFile)) {
plugin.template = htmlFile;
}
await loadNodeLocales(plugin)
if (plugin.template && !settings.disableEditor) {
return loadPluginTemplate(plugin);
}
return plugin
}
/**
* Loads the specified node into the runtime
* @param node a node info object - see loadNodeConfig
@@ -236,8 +352,6 @@ async function loadNodeConfig(fileInfo) {
*
*/
function loadNodeSet(node) {
var nodeDir = path.dirname(node.file);
var nodeFn = path.basename(node.file);
if (!node.enabled) {
return Promise.resolve(node);
} else {
@@ -284,11 +398,59 @@ function loadNodeSet(node) {
}
}
async function loadPlugin(plugin) {
if (!plugin.file) {
// No runtime component - nothing to load
return plugin;
}
try {
var r = require(plugin.file);
if (typeof r === "function") {
var red = registryUtil.createNodeApi(plugin);
var promise = r(red);
if (promise != null && typeof promise.then === "function") {
return promise.then(function() {
plugin.enabled = true;
plugin.loaded = true;
return plugin;
}).catch(function(err) {
plugin.err = err;
return plugin;
});
}
}
plugin.enabled = true;
plugin.loaded = true;
return plugin;
} catch(err) {
console.log(err);
plugin.err = err;
var stack = err.stack;
var message;
if (stack) {
var i = stack.indexOf(plugin.file);
if (i > -1) {
var excerpt = stack.substring(i+node.file.length+1,i+plugin.file.length+20);
var m = /^(\d+):(\d+)/.exec(excerpt);
if (m) {
plugin.err = err+" (line:"+m[1]+")";
}
}
}
return plugin;
}
}
function loadNodeSetList(nodes) {
var promises = [];
nodes.forEach(function(node) {
if (!node.err) {
promises.push(loadNodeSet(node).catch(err => {}));
if (node.type === "plugin") {
promises.push(loadPlugin(node).catch(err => {}));
} else {
promises.push(loadNodeSet(node).catch(err => {}));
}
} else {
promises.push(node);
}
@@ -316,6 +478,7 @@ function addModule(module) {
return Promise.reject(e);
}
try {
<<<<<<< HEAD
var moduleFiles = {};
var moduleStack = [module];
while(moduleStack.length > 0) {
@@ -339,6 +502,10 @@ function addModule(module) {
}
}
return loadNodeFiles(moduleFiles).then(() => module)
=======
var moduleFiles = localfilesystem.getModuleFiles(module);
return loadModuleFiles(moduleFiles);
>>>>>>> Initial plugin runtime api implementation
} catch(err) {
return Promise.reject(err);
}