1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00
node-red/packages/node_modules/@node-red/registry/lib/loader.js

396 lines
13 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 loadNodeFiles(registry.getModuleList());
log.info(log._("server.loading"));
var nodeFiles = localfilesystem.getNodeFiles(disableNodePathScan);
return loadNodeFiles(nodeFiles);
}
function loadNodeFiles(nodeFiles) {
var promises = [];
var nodes = [];
for (var module in nodeFiles) {
/* 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)) {
//TODO: log it
log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:nodeFiles[module].redVersion}));
nodeFiles[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) {
//
}
}
}
}
}
}
return Promise.all(promises).then(function(results) {
for (var module in nodeFiles) {
if (nodeFiles.hasOwnProperty(module)) {
if (!nodeFiles[module].err) {
registry.addModule(nodeFiles[module]);
}
}
}
return loadNodeSetList(nodes);
});
}
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";
if (err.code !== 'ENOENT') {
node.types = [];
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
}
return fs.stat(path.join(path.dirname(node.file),"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")
.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;
}
var 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;
}
/**
* 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) {
var nodeDir = path.dirname(node.file);
var nodeFn = path.basename(node.file);
if (!node.enabled) {
return Promise.resolve(node);
} else {
}
try {
var loadPromise = null;
var r = require(node.file);
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 i = stack.indexOf(node.file);
if (i > -1) {
var excerpt = stack.substring(i+node.file.length+1,i+node.file.length+20);
var m = /^(\d+):(\d+)/.exec(excerpt);
if (m) {
node.err = err+" (line:"+m[1]+")";
}
}
}
return Promise.resolve(node);
}
}
function loadNodeSetList(nodes) {
var promises = [];
nodes.forEach(function(node) {
if (!node.err) {
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 loadNodeFiles(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
}