| /** |
| * Copyright 2014 IBM Corp. |
| * |
| * 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 util = require("util"); |
| var when = require("when"); |
| var whenNode = require('when/node'); |
| var fs = require("fs"); |
| var path = require("path"); |
| var crypto = require("crypto"); |
| var UglifyJS = require("uglify-js"); |
| |
| var events = require("../events"); |
| |
| var Node; |
| var settings; |
| |
| function filterNodeInfo(n) { |
| var r = { |
| id: n.id, |
| name: n.name, |
| types: n.types, |
| enabled: n.enabled |
| } |
| if (n.hasOwnProperty("loaded")) { |
| r.loaded = n.loaded; |
| } |
| if (n.hasOwnProperty("module")) { |
| r.module = n.module; |
| } |
| if (n.hasOwnProperty("err")) { |
| r.err = n.err.toString(); |
| } |
| return r; |
| } |
| |
| var registry = (function() { |
| var nodeConfigCache = null; |
| var nodeConfigs = {}; |
| var nodeList = []; |
| var nodeConstructors = {}; |
| var nodeTypeToId = {}; |
| var nodeModules = {}; |
| |
| function saveNodeList() { |
| var nodeList = {}; |
| |
| for (var i in nodeConfigs) { |
| if (nodeConfigs.hasOwnProperty(i)) { |
| var nodeConfig = nodeConfigs[i]; |
| var n = filterNodeInfo(nodeConfig); |
| n.file = nodeConfig.file; |
| delete n.loaded; |
| delete n.err; |
| delete n.file; |
| delete n.id; |
| nodeList[i] = n; |
| } |
| } |
| if (settings.available()) { |
| return settings.set("nodes",nodeList); |
| } else { |
| return when.reject("Settings unavailable"); |
| } |
| } |
| |
| return { |
| init: function() { |
| if (settings.available()) { |
| nodeConfigs = settings.get("nodes")||{}; |
| // Restore the node id property to individual entries |
| for (var id in nodeConfigs) { |
| if (nodeConfigs.hasOwnProperty(id)) { |
| nodeConfigs[id].id = id; |
| } |
| } |
| } else { |
| nodeConfigs = {}; |
| } |
| nodeModules = {}; |
| nodeTypeToId = {}; |
| nodeConstructors = {}; |
| nodeList = []; |
| nodeConfigCache = null; |
| }, |
| |
| addNodeSet: function(id,set) { |
| if (!set.err) { |
| set.types.forEach(function(t) { |
| nodeTypeToId[t] = id; |
| }); |
| } |
| |
| if (set.module) { |
| nodeModules[set.module] = nodeModules[set.module]||{nodes:[]}; |
| nodeModules[set.module].nodes.push(id); |
| } |
| |
| nodeConfigs[id] = set; |
| nodeList.push(id); |
| nodeConfigCache = null; |
| }, |
| removeNode: function(id) { |
| var config = nodeConfigs[id]; |
| if (!config) { |
| throw new Error("Unrecognised id: "+id); |
| } |
| delete nodeConfigs[id]; |
| var i = nodeList.indexOf(id); |
| if (i > -1) { |
| nodeList.splice(i,1); |
| } |
| config.types.forEach(function(t) { |
| delete nodeConstructors[t]; |
| delete nodeTypeToId[t]; |
| }); |
| config.enabled = false; |
| config.loaded = false; |
| nodeConfigCache = null; |
| return filterNodeInfo(config); |
| }, |
| removeModule: function(module) { |
| if (!settings.available()) { |
| throw new Error("Settings unavailable"); |
| } |
| var nodes = nodeModules[module]; |
| if (!nodes) { |
| throw new Error("Unrecognised module: "+module); |
| } |
| var infoList = []; |
| for (var i=0;i<nodes.nodes.length;i++) { |
| infoList.push(registry.removeNode(nodes.nodes[i])); |
| } |
| delete nodeModules[module]; |
| saveNodeList(); |
| return infoList; |
| }, |
| getNodeInfo: function(typeOrId) { |
| if (nodeTypeToId[typeOrId]) { |
| return filterNodeInfo(nodeConfigs[nodeTypeToId[typeOrId]]); |
| } else if (nodeConfigs[typeOrId]) { |
| return filterNodeInfo(nodeConfigs[typeOrId]); |
| } |
| return null; |
| }, |
| getNodeList: function() { |
| var list = []; |
| for (var id in nodeConfigs) { |
| if (nodeConfigs.hasOwnProperty(id)) { |
| list.push(filterNodeInfo(nodeConfigs[id])) |
| } |
| } |
| return list; |
| }, |
| registerNodeConstructor: function(type,constructor) { |
| if (nodeConstructors[type]) { |
| throw new Error(type+" already registered"); |
| } |
| //TODO: Ensure type is known - but doing so will break some tests |
| // that don't have a way to register a node template ahead |
| // of registering the constructor |
| util.inherits(constructor,Node); |
| nodeConstructors[type] = constructor; |
| events.emit("type-registered",type); |
| }, |
| |
| |
| /** |
| * Gets all of the node template configs |
| * @return all of the node templates in a single string |
| */ |
| getAllNodeConfigs: function() { |
| if (!nodeConfigCache) { |
| var result = ""; |
| var script = ""; |
| for (var i=0;i<nodeList.length;i++) { |
| var config = nodeConfigs[nodeList[i]]; |
| if (config.enabled && !config.err) { |
| result += config.config; |
| script += config.script; |
| } |
| } |
| if (script.length > 0) { |
| result += '<script type="text/javascript">'; |
| result += UglifyJS.minify(script, {fromString: true}).code; |
| result += '</script>'; |
| } |
| nodeConfigCache = result; |
| } |
| return nodeConfigCache; |
| }, |
| |
| getNodeConfig: function(id) { |
| var config = nodeConfigs[id]; |
| if (config) { |
| var result = config.config; |
| if (config.script) { |
| result += '<script type="text/javascript">'+config.script+'</script>'; |
| } |
| return result; |
| } else { |
| return null; |
| } |
| }, |
| |
| getNodeConstructor: function(type) { |
| var config = nodeConfigs[nodeTypeToId[type]]; |
| if (!config || (config.enabled && !config.err)) { |
| return nodeConstructors[type]; |
| } |
| return null; |
| }, |
| |
| clear: function() { |
| nodeConfigCache = null; |
| nodeConfigs = {}; |
| nodeList = []; |
| nodeConstructors = {}; |
| nodeTypeToId = {}; |
| }, |
| |
| getTypeId: function(type) { |
| return nodeTypeToId[type]; |
| }, |
| |
| getModuleInfo: function(type) { |
| return nodeModules[type]; |
| }, |
| |
| enableNodeSet: function(id) { |
| if (!settings.available()) { |
| throw new Error("Settings unavailable"); |
| } |
| var config = nodeConfigs[id]; |
| if (config) { |
| delete config.err; |
| config.enabled = true; |
| if (!config.loaded) { |
| // TODO: honour the promise this returns |
| loadNodeModule(config); |
| } |
| nodeConfigCache = null; |
| saveNodeList(); |
| } else { |
| throw new Error("Unrecognised id: "+id); |
| } |
| return filterNodeInfo(config); |
| }, |
| |
| disableNodeSet: function(id) { |
| if (!settings.available()) { |
| throw new Error("Settings unavailable"); |
| } |
| var config = nodeConfigs[id]; |
| if (config) { |
| // TODO: persist setting |
| config.enabled = false; |
| nodeConfigCache = null; |
| saveNodeList(); |
| } else { |
| throw new Error("Unrecognised id: "+id); |
| } |
| return filterNodeInfo(config); |
| }, |
| |
| saveNodeList: saveNodeList, |
| |
| cleanNodeList: function() { |
| var removed = false; |
| for (var id in nodeConfigs) { |
| if (nodeConfigs.hasOwnProperty(id)) { |
| if (nodeConfigs[id].module && !nodeModules[nodeConfigs[id].module]) { |
| registry.removeNode(id); |
| removed = true; |
| } |
| } |
| } |
| if (removed) { |
| saveNodeList(); |
| } |
| } |
| } |
| })(); |
| |
| |
| |
| function init(_settings) { |
| Node = require("./Node"); |
| settings = _settings; |
| registry.init(); |
| } |
| |
| /** |
| * Synchronously walks the directory looking for node files. |
| * Emits 'node-icon-dir' events for an icon dirs found |
| * @param dir the directory to search |
| * @return an array of fully-qualified paths to .js files |
| */ |
| function getNodeFiles(dir) { |
| var result = []; |
| var files = []; |
| try { |
| files = fs.readdirSync(dir); |
| } catch(err) { |
| return result; |
| } |
| files.sort(); |
| files.forEach(function(fn) { |
| var stats = fs.statSync(path.join(dir,fn)); |
| if (stats.isFile()) { |
| if (/\.js$/.test(fn)) { |
| var valid = true; |
| if (settings.nodesExcludes) { |
| for (var i=0;i<settings.nodesExcludes.length;i++) { |
| if (settings.nodesExcludes[i] == fn) { |
| valid = false; |
| break; |
| } |
| } |
| } |
| valid = valid && fs.existsSync(path.join(dir,fn.replace(/\.js$/,".html"))) |
| |
| if (valid) { |
| result.push(path.join(dir,fn)); |
| } |
| } |
| } else if (stats.isDirectory()) { |
| // Ignore /.dirs/, /lib/ /node_modules/ |
| if (!/^(\..*|lib|icons|node_modules|test)$/.test(fn)) { |
| result = result.concat(getNodeFiles(path.join(dir,fn))); |
| } else if (fn === "icons") { |
| events.emit("node-icon-dir",path.join(dir,fn)); |
| } |
| } |
| }); |
| return result; |
| } |
| |
| /** |
| * Scans the node_modules path for nodes |
| * @param moduleName the name of the module to be found |
| * @return a list of node modules: {dir,package} |
| */ |
| function scanTreeForNodesModules(moduleName) { |
| var dir = __dirname+"/../../nodes"; |
| var results = []; |
| var up = path.resolve(path.join(dir,"..")); |
| while (up !== dir) { |
| var pm = path.join(dir,"node_modules"); |
| try { |
| var files = fs.readdirSync(pm); |
| for (var i=0;i<files.length;i++) { |
| var fn = files[i]; |
| if (!registry.getModuleInfo(fn)) { |
| if (!moduleName || fn == moduleName) { |
| var pkgfn = path.join(pm,fn,"package.json"); |
| try { |
| var pkg = require(pkgfn); |
| if (pkg['node-red']) { |
| var moduleDir = path.join(pm,fn); |
| results.push({dir:moduleDir,package:pkg}); |
| } |
| } catch(err) { |
| if (err.code != "MODULE_NOT_FOUND") { |
| // TODO: handle unexpected error |
| } |
| } |
| if (fn == moduleName) { |
| break; |
| } |
| } |
| } |
| } |
| } catch(err) { |
| } |
| |
| dir = up; |
| up = path.resolve(path.join(dir,"..")); |
| } |
| return results; |
| } |
| |
| /** |
| * Loads the nodes provided in an npm package. |
| * @param moduleDir the root directory of the package |
| * @param pkg the module's package.json object |
| */ |
| function loadNodesFromModule(moduleDir,pkg) { |
| var nodes = pkg['node-red'].nodes||{}; |
| var results = []; |
| var iconDirs = []; |
| for (var n in nodes) { |
| if (nodes.hasOwnProperty(n)) { |
| var file = path.join(moduleDir,nodes[n]); |
| try { |
| results.push(loadNodeConfig(file,pkg.name,n)); |
| } catch(err) { |
| } |
| var iconDir = path.join(moduleDir,path.dirname(nodes[n]),"icons"); |
| if (iconDirs.indexOf(iconDir) == -1) { |
| if (fs.existsSync(iconDir)) { |
| events.emit("node-icon-dir",iconDir); |
| iconDirs.push(iconDir); |
| } |
| } |
| } |
| } |
| return results; |
| } |
| |
| |
| /** |
| * Loads a node's configuration |
| * @param file the fully qualified path of the node's .js file |
| * @param name the name of the node |
| * @return the node object |
| * { |
| * id: a unqiue id for the node file |
| * name: the name of the node file, or label from the npm module |
| * file: the fully qualified path to the node's .js file |
| * template: the fully qualified path to the node's .html file |
| * config: the non-script parts of the node's .html file |
| * script: the script part of the node's .html file |
| * types: an array of node type names in this file |
| * } |
| */ |
| function loadNodeConfig(file,module,name) { |
| var id = crypto.createHash('sha1').update(file).digest("hex"); |
| if (module && name) { |
| var newid = crypto.createHash('sha1').update(module+":"+name).digest("hex"); |
| var existingInfo = registry.getNodeInfo(id); |
| if (existingInfo) { |
| // For a brief period, id for modules were calculated incorrectly. |
| // To prevent false-duplicates, this removes the old id entry |
| registry.removeNode(id); |
| registry.saveNodeList(); |
| } |
| id = newid; |
| |
| } |
| 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, |
| file: file, |
| template: file.replace(/\.js$/,".html"), |
| enabled: isEnabled, |
| loaded:false |
| } |
| |
| if (module) { |
| node.name = module+":"+name; |
| node.module = module; |
| } else { |
| node.name = path.basename(file) |
| } |
| try { |
| var content = fs.readFileSync(node.template,'utf8'); |
| |
| var types = []; |
| |
| var regExp = /<script ([^>]*)data-template-name=['"]([^'"]*)['"]/gi; |
| var match = null; |
| |
| while((match = regExp.exec(content)) !== null) { |
| types.push(match[2]); |
| } |
| node.types = types; |
| node.config = content; |
| |
| // 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; |
| } |
| } |
| } catch(err) { |
| node.types = []; |
| if (err.code === 'ENOENT') { |
| node.err = "Error: "+file+" does not exist"; |
| } else { |
| node.err = err.toString(); |
| } |
| } |
| registry.addNodeSet(id,node); |
| return node; |
| } |
| |
| /** |
| * Loads all palette nodes |
| * @param defaultNodesDir optional parameter, when set, it overrides the default |
| * location of nodeFiles - used by the tests |
| * @return a promise that resolves on completion of loading |
| */ |
| function load(defaultNodesDir,disableNodePathScan) { |
| return when.promise(function(resolve,reject) { |
| // Find all of the nodes to load |
| var nodeFiles; |
| if(defaultNodesDir) { |
| nodeFiles = getNodeFiles(path.resolve(defaultNodesDir)); |
| } else { |
| nodeFiles = getNodeFiles(__dirname+"/../../nodes"); |
| } |
| |
| if (settings.nodesDir) { |
| var dir = settings.nodesDir; |
| if (typeof settings.nodesDir == "string") { |
| dir = [dir]; |
| } |
| for (var i=0;i<dir.length;i++) { |
| nodeFiles = nodeFiles.concat(getNodeFiles(dir[i])); |
| } |
| } |
| var nodes = []; |
| nodeFiles.forEach(function(file) { |
| try { |
| nodes.push(loadNodeConfig(file)); |
| } catch(err) { |
| // |
| } |
| }); |
| |
| // TODO: disabling npm module loading if defaultNodesDir set |
| // This indicates a test is being run - don't want to pick up |
| // unexpected nodes. |
| // Urgh. |
| if (!disableNodePathScan) { |
| // Find all of the modules containing nodes |
| var moduleFiles = scanTreeForNodesModules(); |
| moduleFiles.forEach(function(moduleFile) { |
| nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package)); |
| }); |
| } |
| var promises = []; |
| nodes.forEach(function(node) { |
| if (!node.err) { |
| promises.push(loadNodeModule(node)); |
| } |
| }); |
| |
| //resolve([]); |
| when.settle(promises).then(function(results) { |
| // Trigger a load of the configs to get it precached |
| registry.getAllNodeConfigs(); |
| |
| if (settings.available()) { |
| resolve(registry.saveNodeList()); |
| } else { |
| resolve(); |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * 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 loadNodeModule(node) { |
| var nodeDir = path.dirname(node.file); |
| var nodeFn = path.basename(node.file); |
| if (!node.enabled) { |
| return when.resolve(node); |
| } |
| try { |
| var loadPromise = null; |
| var r = require(node.file); |
| if (typeof r === "function") { |
| var promise = r(require('../red')); |
| if (promise != null && typeof promise.then === "function") { |
| loadPromise = promise.then(function() { |
| node.enabled = true; |
| node.loaded = true; |
| return node; |
| }).otherwise(function(err) { |
| node.err = err; |
| return node; |
| }); |
| } |
| } |
| if (loadPromise == null) { |
| node.enabled = true; |
| node.loaded = true; |
| loadPromise = when.resolve(node); |
| } |
| return loadPromise; |
| } catch(err) { |
| node.err = err; |
| return when.resolve(node); |
| } |
| } |
| |
| function loadNodeList(nodes) { |
| var promises = []; |
| nodes.forEach(function(node) { |
| if (!node.err) { |
| promises.push(loadNodeModule(node)); |
| } else { |
| promises.push(node); |
| } |
| }); |
| |
| return when.settle(promises).then(function(results) { |
| return registry.saveNodeList().then(function() { |
| var list = results.map(function(r) { |
| return filterNodeInfo(r.value); |
| }); |
| return list; |
| }); |
| }); |
| } |
| |
| function addNode(file) { |
| if (!settings.available()) { |
| throw new Error("Settings unavailable"); |
| } |
| var nodes = []; |
| try { |
| nodes.push(loadNodeConfig(file)); |
| } catch(err) { |
| return when.reject(err); |
| } |
| return loadNodeList(nodes); |
| } |
| |
| function addModule(module) { |
| if (!settings.available()) { |
| throw new Error("Settings unavailable"); |
| } |
| var nodes = []; |
| if (registry.getModuleInfo(module)) { |
| return when.reject(new Error("Module already loaded")); |
| } |
| var moduleFiles = scanTreeForNodesModules(module); |
| if (moduleFiles.length === 0) { |
| var err = new Error("Cannot find module '" + module + "'"); |
| err.code = 'MODULE_NOT_FOUND'; |
| return when.reject(err); |
| } |
| moduleFiles.forEach(function(moduleFile) { |
| nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package)); |
| }); |
| return loadNodeList(nodes); |
| } |
| |
| module.exports = { |
| init:init, |
| load:load, |
| clear: registry.clear, |
| registerType: registry.registerNodeConstructor, |
| get: registry.getNodeConstructor, |
| getNodeInfo: registry.getNodeInfo, |
| getNodeModuleInfo: registry.getModuleInfo, |
| getNodeList: registry.getNodeList, |
| getNodeConfigs: registry.getAllNodeConfigs, |
| getNodeConfig: registry.getNodeConfig, |
| addNode: addNode, |
| removeNode: registry.removeNode, |
| enableNode: registry.enableNodeSet, |
| disableNode: registry.disableNodeSet, |
| |
| addModule: addModule, |
| removeModule: registry.removeModule, |
| cleanNodeList: registry.cleanNodeList |
| } |