mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
d6bb3a558f
fixes #3861
567 lines
18 KiB
JavaScript
567 lines
18 KiB
JavaScript
/**
|
|
* Copyright JS Foundation and other contributors, http://js.foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
**/
|
|
|
|
var fs = require("fs-extra");
|
|
var path = require("path");
|
|
var semver = require("semver");
|
|
|
|
var localfilesystem = require("./localfilesystem");
|
|
var registry = require("./registry");
|
|
var registryUtil = require("./util")
|
|
var i18n = require("@node-red/util").i18n;
|
|
var log = require("@node-red/util").log;
|
|
|
|
var settings;
|
|
|
|
function init(_runtime) {
|
|
settings = _runtime.settings;
|
|
localfilesystem.init(settings);
|
|
registryUtil.init(_runtime);
|
|
}
|
|
|
|
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 loadModuleFiles(registry.getModuleList());
|
|
log.info(log._("server.loading"));
|
|
|
|
var modules = localfilesystem.getNodeFiles(disableNodePathScan);
|
|
return loadModuleFiles(modules);
|
|
}
|
|
|
|
function splitPath(p) {
|
|
return path.posix.normalize((p || '').replace(/\\/g, path.sep)).split(path.sep)
|
|
}
|
|
|
|
function loadModuleTypeFiles(module, type) {
|
|
const things = module[type];
|
|
let first = true;
|
|
const promises = [];
|
|
for (let thingName in things) {
|
|
/* istanbul ignore else */
|
|
if (things.hasOwnProperty(thingName)) {
|
|
if (module.name != "node-red" && first) {
|
|
// Check the module directory exists
|
|
first = false;
|
|
let moduleFn = module.path
|
|
const fn = things[thingName].file
|
|
const parts = splitPath(fn)
|
|
const nmi = parts.indexOf('node_modules')
|
|
if(nmi > -1) {
|
|
moduleFn = parts.slice(0,nmi+2).join(path.sep);
|
|
}
|
|
if (!moduleFn) {
|
|
// shortcut - skip calling statSync on empty string
|
|
break; // Module not found, don't attempt to load its nodes
|
|
}
|
|
try {
|
|
const stat = fs.statSync(moduleFn);
|
|
} catch(err) {
|
|
break; // Module not found, don't attempt to load its nodes
|
|
}
|
|
}
|
|
|
|
try {
|
|
let promise;
|
|
if (type === "nodes") {
|
|
promise = loadNodeConfig(things[thingName]);
|
|
} else if (type === "plugins") {
|
|
promise = loadPluginConfig(things[thingName]);
|
|
}
|
|
promises.push(
|
|
promise.then(
|
|
(function() {
|
|
const 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:modules[module].redVersion}));
|
|
modules[module].err = "version_mismatch";
|
|
continue;
|
|
}
|
|
if (module == "node-red" || !registry.getModuleInfo(module)) {
|
|
if (modules[module].nodes) {
|
|
nodePromises = nodePromises.concat(loadModuleTypeFiles(modules[module], "nodes"));
|
|
}
|
|
if (modules[module].plugins) {
|
|
pluginPromises = pluginPromises.concat(loadModuleTypeFiles(modules[module], "plugins"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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(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;
|
|
});
|
|
}
|
|
|
|
async function loadNodeTemplate(node) {
|
|
return fs.readFile(node.template,'utf8').then(content => {
|
|
var types = [];
|
|
|
|
var regExp = /<script (?:[^>]*)data-template-name\s*=\s*['"]([^'"]*)['"]/gi;
|
|
var match = null;
|
|
|
|
while ((match = regExp.exec(content)) !== null) {
|
|
types.push(match[1]);
|
|
}
|
|
node.types = types;
|
|
|
|
var langRegExp = /^<script[^>]* data-lang\s*=\s*['"](.+?)['"]/i;
|
|
regExp = /(<script[^>]* data-help-name=[\s\S]*?<\/script>)/gi;
|
|
match = null;
|
|
var mainContent = "";
|
|
var helpContent = {};
|
|
var index = 0;
|
|
while ((match = regExp.exec(content)) !== null) {
|
|
mainContent += content.substring(index,regExp.lastIndex-match[1].length);
|
|
index = regExp.lastIndex;
|
|
var help = content.substring(regExp.lastIndex-match[1].length,regExp.lastIndex);
|
|
|
|
var lang = i18n.defaultLang;
|
|
if ((match = langRegExp.exec(help)) !== null) {
|
|
lang = match[1];
|
|
}
|
|
if (!helpContent.hasOwnProperty(lang)) {
|
|
helpContent[lang] = "";
|
|
}
|
|
|
|
helpContent[lang] += help;
|
|
}
|
|
mainContent += content.substring(index);
|
|
|
|
node.config = mainContent;
|
|
node.help = helpContent;
|
|
// TODO: parse out the javascript portion of the template
|
|
//node.script = "";
|
|
for (var i=0;i<node.types.length;i++) {
|
|
if (registry.getTypeId(node.types[i])) {
|
|
node.err = node.types[i]+" already registered";
|
|
break;
|
|
}
|
|
}
|
|
return node
|
|
}).catch(err => {
|
|
// ENOENT means no html file. We can live with that. But any other error
|
|
// should be fatal
|
|
// node.err = "Error: "+node.template+" does not exist";
|
|
node.types = node.types || [];
|
|
if (err.code !== 'ENOENT') {
|
|
node.err = err.toString();
|
|
}
|
|
return node;
|
|
});
|
|
}
|
|
|
|
async function loadNodeLocales(node) {
|
|
if (node.module === 'node-red') {
|
|
// do not look up locales directory for core nodes
|
|
node.namespace = node.module;
|
|
return node
|
|
}
|
|
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(baseFile),"locales"),
|
|
path.basename(baseFile).replace(/\.[^.]+$/,".json"))
|
|
.then(() => node);
|
|
}).catch(err => {
|
|
node.namespace = node.module;
|
|
return node;
|
|
});
|
|
}
|
|
|
|
async function loadNodeConfig(fileInfo) {
|
|
var file = fileInfo.file;
|
|
var module = fileInfo.module;
|
|
var name = fileInfo.name;
|
|
var version = fileInfo.version;
|
|
|
|
var id = module + "/" + name;
|
|
var info = registry.getNodeInfo(id);
|
|
var isEnabled = true;
|
|
if (info) {
|
|
if (info.hasOwnProperty("loaded")) {
|
|
throw new Error(file+" already loaded");
|
|
}
|
|
isEnabled = !(info.enabled === false);
|
|
}
|
|
|
|
var node = {
|
|
type: "node",
|
|
id: id,
|
|
module: module,
|
|
name: name,
|
|
file: file,
|
|
template: file.replace(/\.js$/,".html"),
|
|
enabled: isEnabled,
|
|
loaded:false,
|
|
version: version,
|
|
local: fileInfo.local,
|
|
types: [],
|
|
config: "",
|
|
help: {}
|
|
};
|
|
if (fileInfo.hasOwnProperty("types")) {
|
|
node.types = fileInfo.types;
|
|
}
|
|
await loadNodeLocales(node)
|
|
if (!settings.disableEditor) {
|
|
return loadNodeTemplate(node);
|
|
}
|
|
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
|
|
* @return a promise that resolves to an update node info object. The object
|
|
* has the following properties added:
|
|
* err: any error encountered whilst loading the node
|
|
*
|
|
*/
|
|
function loadNodeSet(node) {
|
|
if (!node.enabled) {
|
|
return Promise.resolve(node);
|
|
} else {
|
|
}
|
|
try {
|
|
var loadPromise = null;
|
|
var r = require(node.file);
|
|
r = r.__esModule ? r.default : r
|
|
if (typeof r === "function") {
|
|
|
|
var red = registryUtil.createNodeApi(node);
|
|
var promise = r(red);
|
|
if (promise != null && typeof promise.then === "function") {
|
|
loadPromise = promise.then(function() {
|
|
node.enabled = true;
|
|
node.loaded = true;
|
|
return node;
|
|
}).catch(function(err) {
|
|
node.err = err;
|
|
return node;
|
|
});
|
|
}
|
|
}
|
|
if (loadPromise == null) {
|
|
node.enabled = true;
|
|
node.loaded = true;
|
|
loadPromise = Promise.resolve(node);
|
|
}
|
|
return loadPromise;
|
|
} catch(err) {
|
|
node.err = err;
|
|
var stack = err.stack;
|
|
var message;
|
|
if (stack) {
|
|
var filePath = node.file;
|
|
try {
|
|
filePath = fs.realpathSync(filePath);
|
|
}
|
|
catch (e) {
|
|
// ignore canonicalization error
|
|
}
|
|
var i = stack.indexOf(filePath);
|
|
if (i > -1) {
|
|
var excerpt = stack.substring(i+filePath.length+1,i+filePath.length+20);
|
|
var m = /^(\d+)/.exec(excerpt);
|
|
if (m) {
|
|
node.err = err+" (line:"+m[1]+")";
|
|
}
|
|
}
|
|
}
|
|
return Promise.resolve(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) {
|
|
if (node.type === "plugin") {
|
|
promises.push(loadPlugin(node).catch(err => {}));
|
|
} else {
|
|
promises.push(loadNodeSet(node).catch(err => {}));
|
|
}
|
|
} else {
|
|
promises.push(node);
|
|
}
|
|
});
|
|
|
|
return Promise.all(promises).then(function() {
|
|
if (settings.available()) {
|
|
return registry.saveNodeList();
|
|
} else {
|
|
return;
|
|
}
|
|
});
|
|
}
|
|
|
|
function addModule(module) {
|
|
if (!settings.available()) {
|
|
throw new Error("Settings unavailable");
|
|
}
|
|
var nodes = [];
|
|
var existingInfo = registry.getModuleInfo(module);
|
|
if (existingInfo) {
|
|
// TODO: nls
|
|
var e = new Error("module_already_loaded");
|
|
e.code = "module_already_loaded";
|
|
return Promise.reject(e);
|
|
}
|
|
try {
|
|
var moduleFiles = {};
|
|
var moduleStack = [module];
|
|
while(moduleStack.length > 0) {
|
|
var moduleToLoad = moduleStack.shift();
|
|
var files = localfilesystem.getModuleFiles(moduleToLoad);
|
|
if (files[moduleToLoad]) {
|
|
moduleFiles[moduleToLoad] = files[moduleToLoad];
|
|
if (moduleFiles[moduleToLoad].dependencies) {
|
|
log.debug(`Loading dependencies for ${module}`)
|
|
for (var i=0; i<moduleFiles[moduleToLoad].dependencies.length; i++) {
|
|
var dep = moduleFiles[moduleToLoad].dependencies[i]
|
|
if (!registry.getModuleInfo(dep)) {
|
|
log.debug(` - load ${dep}`)
|
|
moduleStack.push(dep);
|
|
} else {
|
|
log.debug(` - already loaded ${dep}`)
|
|
registry.addModuleDependency(dep,moduleToLoad)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return loadModuleFiles(moduleFiles).then(() => module)
|
|
} catch(err) {
|
|
return Promise.reject(err);
|
|
}
|
|
}
|
|
|
|
function loadNodeHelp(node,lang) {
|
|
var base = path.basename(node.template);
|
|
var localePath;
|
|
if (node.module === 'node-red') {
|
|
var cat_dir = path.dirname(node.template);
|
|
var cat = path.basename(cat_dir);
|
|
var dir = path.dirname(cat_dir);
|
|
localePath = path.join(dir, "..", "locales", lang, cat, base)
|
|
}
|
|
else {
|
|
var dir = path.dirname(node.template);
|
|
localePath = path.join(dir,"locales",lang,base);
|
|
}
|
|
try {
|
|
// TODO: make this async
|
|
var content = fs.readFileSync(localePath, "utf8")
|
|
return content;
|
|
} catch(err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getNodeHelp(node,lang) {
|
|
if (!node.help[lang]) {
|
|
var help = loadNodeHelp(node,lang);
|
|
if (help == null) {
|
|
var langParts = lang.split("-");
|
|
if (langParts.length == 2) {
|
|
help = loadNodeHelp(node,langParts[0]);
|
|
}
|
|
}
|
|
if (help) {
|
|
node.help[lang] = help;
|
|
} else if (lang === i18n.defaultLang) {
|
|
return null;
|
|
} else {
|
|
node.help[lang] = getNodeHelp(node, i18n.defaultLang);
|
|
}
|
|
}
|
|
return node.help[lang];
|
|
}
|
|
|
|
module.exports = {
|
|
init: init,
|
|
load: load,
|
|
addModule: addModule,
|
|
loadNodeSet: loadNodeSet,
|
|
getNodeHelp: getNodeHelp
|
|
}
|