2 * Copyright 2014 IBM Corp.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 var util = require("util");
18 var when = require("when");
19 var whenNode = require('when/node');
20 var fs = require("fs");
21 var path = require("path");
22 var crypto = require("crypto");
23 var UglifyJS = require("uglify-js");
25 var events = require("../events");
30 function filterNodeInfo(n) {
37 if (n.hasOwnProperty("loaded")) {
40 if (n.hasOwnProperty("module")) {
43 if (n.hasOwnProperty("err")) {
44 r.err = n.err.toString();
49 var registry = (function() {
50 var nodeConfigCache = null;
53 var nodeConstructors = {};
54 var nodeTypeToId = {};
57 function saveNodeList() {
60 for (var i in nodeConfigs) {
61 if (nodeConfigs.hasOwnProperty(i)) {
62 var nodeConfig = nodeConfigs[i];
63 var n = filterNodeInfo(nodeConfig);
64 n.file = nodeConfig.file;
72 if (settings.available()) {
73 return settings.set("nodes",nodeList);
75 return when.reject("Settings unavailable");
81 if (settings.available()) {
82 nodeConfigs = settings.get("nodes")||{};
83 // Restore the node id property to individual entries
84 for (var id in nodeConfigs) {
85 if (nodeConfigs.hasOwnProperty(id)) {
86 nodeConfigs[id].id = id;
94 nodeConstructors = {};
96 nodeConfigCache = null;
99 addNodeSet: function(id,set) {
101 set.types.forEach(function(t) {
102 nodeTypeToId[t] = id;
107 nodeModules[set.module] = nodeModules[set.module]||{nodes:[]};
108 nodeModules[set.module].nodes.push(id);
111 nodeConfigs[id] = set;
113 nodeConfigCache = null;
115 removeNode: function(id) {
116 var config = nodeConfigs[id];
118 throw new Error("Unrecognised id: "+id);
120 delete nodeConfigs[id];
121 var i = nodeList.indexOf(id);
123 nodeList.splice(i,1);
125 config.types.forEach(function(t) {
126 delete nodeConstructors[t];
127 delete nodeTypeToId[t];
129 config.enabled = false;
130 config.loaded = false;
131 nodeConfigCache = null;
132 return filterNodeInfo(config);
134 removeModule: function(module) {
135 if (!settings.available()) {
136 throw new Error("Settings unavailable");
138 var nodes = nodeModules[module];
140 throw new Error("Unrecognised module: "+module);
143 for (var i=0;i<nodes.nodes.length;i++) {
144 infoList.push(registry.removeNode(nodes.nodes[i]));
146 delete nodeModules[module];
150 getNodeInfo: function(typeOrId) {
151 if (nodeTypeToId[typeOrId]) {
152 return filterNodeInfo(nodeConfigs[nodeTypeToId[typeOrId]]);
153 } else if (nodeConfigs[typeOrId]) {
154 return filterNodeInfo(nodeConfigs[typeOrId]);
158 getNodeList: function() {
160 for (var id in nodeConfigs) {
161 if (nodeConfigs.hasOwnProperty(id)) {
162 list.push(filterNodeInfo(nodeConfigs[id]))
167 registerNodeConstructor: function(type,constructor) {
168 if (nodeConstructors[type]) {
169 throw new Error(type+" already registered");
171 //TODO: Ensure type is known - but doing so will break some tests
172 // that don't have a way to register a node template ahead
173 // of registering the constructor
174 util.inherits(constructor,Node);
175 nodeConstructors[type] = constructor;
176 events.emit("type-registered",type);
181 * Gets all of the node template configs
182 * @return all of the node templates in a single string
184 getAllNodeConfigs: function() {
185 if (!nodeConfigCache) {
188 for (var i=0;i<nodeList.length;i++) {
189 var config = nodeConfigs[nodeList[i]];
190 if (config.enabled && !config.err) {
191 result += config.config;
192 script += config.script;
195 if (script.length > 0) {
196 result += '<script type="text/javascript">';
197 result += UglifyJS.minify(script, {fromString: true}).code;
198 result += '</script>';
200 nodeConfigCache = result;
202 return nodeConfigCache;
205 getNodeConfig: function(id) {
206 var config = nodeConfigs[id];
208 var result = config.config;
210 result += '<script type="text/javascript">'+config.script+'</script>';
218 getNodeConstructor: function(type) {
219 var config = nodeConfigs[nodeTypeToId[type]];
220 if (!config || (config.enabled && !config.err)) {
221 return nodeConstructors[type];
227 nodeConfigCache = null;
230 nodeConstructors = {};
234 getTypeId: function(type) {
235 return nodeTypeToId[type];
238 getModuleInfo: function(type) {
239 return nodeModules[type];
242 enableNodeSet: function(id) {
243 if (!settings.available()) {
244 throw new Error("Settings unavailable");
246 var config = nodeConfigs[id];
249 config.enabled = true;
250 if (!config.loaded) {
251 // TODO: honour the promise this returns
252 loadNodeModule(config);
254 nodeConfigCache = null;
257 throw new Error("Unrecognised id: "+id);
259 return filterNodeInfo(config);
262 disableNodeSet: function(id) {
263 if (!settings.available()) {
264 throw new Error("Settings unavailable");
266 var config = nodeConfigs[id];
268 // TODO: persist setting
269 config.enabled = false;
270 nodeConfigCache = null;
273 throw new Error("Unrecognised id: "+id);
275 return filterNodeInfo(config);
278 saveNodeList: saveNodeList,
280 cleanNodeList: function() {
282 for (var id in nodeConfigs) {
283 if (nodeConfigs.hasOwnProperty(id)) {
284 if (nodeConfigs[id].module && !nodeModules[nodeConfigs[id].module]) {
285 registry.removeNode(id);
299 function init(_settings) {
300 Node = require("./Node");
301 settings = _settings;
306 * Synchronously walks the directory looking for node files.
307 * Emits 'node-icon-dir' events for an icon dirs found
308 * @param dir the directory to search
309 * @return an array of fully-qualified paths to .js files
311 function getNodeFiles(dir) {
315 files = fs.readdirSync(dir);
320 files.forEach(function(fn) {
321 var stats = fs.statSync(path.join(dir,fn));
322 if (stats.isFile()) {
323 if (/\.js$/.test(fn)) {
325 if (settings.nodesExcludes) {
326 for (var i=0;i<settings.nodesExcludes.length;i++) {
327 if (settings.nodesExcludes[i] == fn) {
333 valid = valid && fs.existsSync(path.join(dir,fn.replace(/\.js$/,".html")))
336 result.push(path.join(dir,fn));
339 } else if (stats.isDirectory()) {
340 // Ignore /.dirs/, /lib/ /node_modules/
341 if (!/^(\..*|lib|icons|node_modules|test)$/.test(fn)) {
342 result = result.concat(getNodeFiles(path.join(dir,fn)));
343 } else if (fn === "icons") {
344 events.emit("node-icon-dir",path.join(dir,fn));
352 * Scans the node_modules path for nodes
353 * @param moduleName the name of the module to be found
354 * @return a list of node modules: {dir,package}
356 function scanTreeForNodesModules(moduleName) {
357 var dir = __dirname+"/../../nodes";
359 var up = path.resolve(path.join(dir,".."));
361 var pm = path.join(dir,"node_modules");
363 var files = fs.readdirSync(pm);
364 for (var i=0;i<files.length;i++) {
366 if (!registry.getModuleInfo(fn)) {
367 if (!moduleName || fn == moduleName) {
368 var pkgfn = path.join(pm,fn,"package.json");
370 var pkg = require(pkgfn);
371 if (pkg['node-red']) {
372 var moduleDir = path.join(pm,fn);
373 results.push({dir:moduleDir,package:pkg});
376 if (err.code != "MODULE_NOT_FOUND") {
377 // TODO: handle unexpected error
380 if (fn == moduleName) {
390 up = path.resolve(path.join(dir,".."));
396 * Loads the nodes provided in an npm package.
397 * @param moduleDir the root directory of the package
398 * @param pkg the module's package.json object
400 function loadNodesFromModule(moduleDir,pkg) {
401 var nodes = pkg['node-red'].nodes||{};
404 for (var n in nodes) {
405 if (nodes.hasOwnProperty(n)) {
406 var file = path.join(moduleDir,nodes[n]);
408 results.push(loadNodeConfig(file,pkg.name,n));
411 var iconDir = path.join(moduleDir,path.dirname(nodes[n]),"icons");
412 if (iconDirs.indexOf(iconDir) == -1) {
413 if (fs.existsSync(iconDir)) {
414 events.emit("node-icon-dir",iconDir);
415 iconDirs.push(iconDir);
425 * Loads a node's configuration
426 * @param file the fully qualified path of the node's .js file
427 * @param name the name of the node
428 * @return the node object
430 * id: a unqiue id for the node file
431 * name: the name of the node file, or label from the npm module
432 * file: the fully qualified path to the node's .js file
433 * template: the fully qualified path to the node's .html file
434 * config: the non-script parts of the node's .html file
435 * script: the script part of the node's .html file
436 * types: an array of node type names in this file
439 function loadNodeConfig(file,module,name) {
440 var id = crypto.createHash('sha1').update(file).digest("hex");
441 if (module && name) {
442 var newid = crypto.createHash('sha1').update(module+":"+name).digest("hex");
443 var existingInfo = registry.getNodeInfo(id);
445 // For a brief period, id for modules were calculated incorrectly.
446 // To prevent false-duplicates, this removes the old id entry
447 registry.removeNode(id);
448 registry.saveNodeList();
453 var info = registry.getNodeInfo(id);
455 var isEnabled = true;
458 if (info.hasOwnProperty("loaded")) {
459 throw new Error(file+" already loaded");
461 isEnabled = info.enabled;
467 template: file.replace(/\.js$/,".html"),
473 node.name = module+":"+name;
474 node.module = module;
476 node.name = path.basename(file)
479 var content = fs.readFileSync(node.template,'utf8');
483 var regExp = /<script ([^>]*)data-template-name=['"]([^'"]*)['"]/gi;
486 while((match = regExp.exec(content)) !== null) {
487 types.push(match[2]);
490 node.config = content;
492 // TODO: parse out the javascript portion of the template
495 for (var i=0;i<node.types.length;i++) {
496 if (registry.getTypeId(node.types[i])) {
497 node.err = node.types[i]+" already registered";
503 if (err.code === 'ENOENT') {
504 node.err = "Error: "+file+" does not exist";
506 node.err = err.toString();
509 registry.addNodeSet(id,node);
514 * Loads all palette nodes
515 * @param defaultNodesDir optional parameter, when set, it overrides the default
516 * location of nodeFiles - used by the tests
517 * @return a promise that resolves on completion of loading
519 function load(defaultNodesDir,disableNodePathScan) {
520 return when.promise(function(resolve,reject) {
521 // Find all of the nodes to load
523 if(defaultNodesDir) {
524 nodeFiles = getNodeFiles(path.resolve(defaultNodesDir));
526 nodeFiles = getNodeFiles(__dirname+"/../../nodes");
529 if (settings.nodesDir) {
530 var dir = settings.nodesDir;
531 if (typeof settings.nodesDir == "string") {
534 for (var i=0;i<dir.length;i++) {
535 nodeFiles = nodeFiles.concat(getNodeFiles(dir[i]));
539 nodeFiles.forEach(function(file) {
541 nodes.push(loadNodeConfig(file));
547 // TODO: disabling npm module loading if defaultNodesDir set
548 // This indicates a test is being run - don't want to pick up
551 if (!disableNodePathScan) {
552 // Find all of the modules containing nodes
553 var moduleFiles = scanTreeForNodesModules();
554 moduleFiles.forEach(function(moduleFile) {
555 nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
559 nodes.forEach(function(node) {
561 promises.push(loadNodeModule(node));
566 when.settle(promises).then(function(results) {
567 // Trigger a load of the configs to get it precached
568 registry.getAllNodeConfigs();
570 if (settings.available()) {
571 resolve(registry.saveNodeList());
580 * Loads the specified node into the runtime
581 * @param node a node info object - see loadNodeConfig
582 * @return a promise that resolves to an update node info object. The object
583 * has the following properties added:
584 * err: any error encountered whilst loading the node
587 function loadNodeModule(node) {
588 var nodeDir = path.dirname(node.file);
589 var nodeFn = path.basename(node.file);
591 return when.resolve(node);
594 var loadPromise = null;
595 var r = require(node.file);
596 if (typeof r === "function") {
597 var promise = r(require('../red'));
598 if (promise != null && typeof promise.then === "function") {
599 loadPromise = promise.then(function() {
603 }).otherwise(function(err) {
609 if (loadPromise == null) {
612 loadPromise = when.resolve(node);
617 return when.resolve(node);
621 function loadNodeList(nodes) {
623 nodes.forEach(function(node) {
625 promises.push(loadNodeModule(node));
631 return when.settle(promises).then(function(results) {
632 return registry.saveNodeList().then(function() {
633 var list = results.map(function(r) {
634 return filterNodeInfo(r.value);
641 function addNode(file) {
642 if (!settings.available()) {
643 throw new Error("Settings unavailable");
647 nodes.push(loadNodeConfig(file));
649 return when.reject(err);
651 return loadNodeList(nodes);
654 function addModule(module) {
655 if (!settings.available()) {
656 throw new Error("Settings unavailable");
659 if (registry.getModuleInfo(module)) {
660 return when.reject(new Error("Module already loaded"));
662 var moduleFiles = scanTreeForNodesModules(module);
663 if (moduleFiles.length === 0) {
664 var err = new Error("Cannot find module '" + module + "'");
665 err.code = 'MODULE_NOT_FOUND';
666 return when.reject(err);
668 moduleFiles.forEach(function(moduleFile) {
669 nodes = nodes.concat(loadNodesFromModule(moduleFile.dir,moduleFile.package));
671 return loadNodeList(nodes);
677 clear: registry.clear,
678 registerType: registry.registerNodeConstructor,
679 get: registry.getNodeConstructor,
680 getNodeInfo: registry.getNodeInfo,
681 getNodeModuleInfo: registry.getModuleInfo,
682 getNodeList: registry.getNodeList,
683 getNodeConfigs: registry.getAllNodeConfigs,
684 getNodeConfig: registry.getNodeConfig,
686 removeNode: registry.removeNode,
687 enableNode: registry.enableNodeSet,
688 disableNode: registry.disableNodeSet,
690 addModule: addModule,
691 removeModule: registry.removeModule,
692 cleanNodeList: registry.cleanNodeList