node-red/packages/node_modules/@node-red/registry/lib/loader.js

566 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 loadModuleTypeFiles(module, type) {
const things = module[type];
var first = true;
var promises = [];
for (var thingName in things) {
/* istanbul ignore else */
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: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
}