mirror of https://github.com/node-red/node-red.git
				
				
				
			
		
			
				
	
	
		
			396 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			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
 | 
						|
}
 |