diff --git a/package.json b/package.json index 206b18bed..8e41e5f66 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "marked": "2.0.0", "minami": "1.2.3", "mocha": "^5.2.0", - "node-red-node-test-helper": "^0.2.6", + "node-red-node-test-helper": "^0.2.7", "node-sass": "^4.14.1", "nodemon": "2.0.6", "should": "13.2.3", diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 8c393a3df..45d04e233 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -143,6 +143,7 @@ "nodeActionDisabled": "node actions disabled", "nodeActionDisabledSubflow": "node actions disabled within subflow", "missing-types": "

Flows stopped due to missing node types.

", + "missing-modules": "

Flows stopped due to missing modules.

", "safe-mode":"

Flows stopped in safe mode.

You can modify your flows and deploy the changes to restart.

", "restartRequired": "Node-RED must be restarted to enable upgraded modules", "credentials_load_failed": "

Flows stopped as the credentials could not be decrypted.

The flow credential file is encrypted, but the project's encryption key is missing or invalid.

", diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index 001776417..9839c3e62 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -315,6 +315,7 @@ var RED = (function() { id: notificationId } if (notificationId === "runtime-state") { + RED.events.emit("runtime-state",msg); if (msg.error === "safe-mode") { options.buttons = [ { @@ -347,6 +348,16 @@ var RED = (function() { } ] } + } else if (msg.error === "missing-modules") { + text+=""; + options.buttons = [ + { + text: RED._("common.label.close"), + click: function() { + persistentNotifications[notificationId].hideNotification(); + } + } + ] } else if (msg.error === "credentials_load_failed") { if (RED.settings.theme("projects.enabled",false)) { // projects enabled @@ -437,6 +448,9 @@ var RED = (function() { } else if (persistentNotifications.hasOwnProperty(notificationId)) { persistentNotifications[notificationId].close(); delete persistentNotifications[notificationId]; + if (notificationId === 'runtime-state') { + RED.events.emit("runtime-state",msg); + } } }); RED.comms.subscribe("status/#",function(topic,msg) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index 81cd407eb..baaac7a8b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -29,6 +29,7 @@ RED.tabs = (function() { var currentTabWidth; var currentActiveTabWidth = 0; var collapsibleMenu; + var mousedownTab; var preferredOrder = options.order; var ul = options.element || $("#"+options.id); var wrapper = ul.wrap( "
" ).parent(); @@ -207,6 +208,11 @@ RED.tabs = (function() { if (dragActive) { return } + if (evt.currentTarget !== mousedownTab) { + mousedownTab = null; + return; + } + mousedownTab = null; if (dblClickTime && Date.now()-dblClickTime < 400) { dblClickTime = 0; dblClickArmed = true; @@ -445,6 +451,7 @@ RED.tabs = (function() { } ul.find("li.red-ui-tab a") + .on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) .on("mouseup",onTabClick) .on("click", function(evt) {evt.preventDefault(); }) .on("dblclick", function(evt) {evt.stopPropagation(); evt.preventDefault(); }) @@ -509,8 +516,8 @@ RED.tabs = (function() { li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-"))); li.data("tabId",tab.id); - if (options.maximumTabWidth) { - li.css("maxWidth",options.maximumTabWidth+"px"); + if (options.maximumTabWidth || tab.maximumTabWidth) { + li.css("maxWidth",(options.maximumTabWidth || tab.maximumTabWidth) +"px"); } var link = $("",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li); if (tab.icon) { @@ -636,6 +643,7 @@ RED.tabs = (function() { } } + link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget }) link.on("mouseup",onTabClick); link.on("click", function(evt) { evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js index 5a6794732..3ae721483 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js @@ -344,6 +344,16 @@ that.element.val(that.value()); that.element.trigger('change',[that.propertyType,that.value()]); }); + this.input.on('keyup', function(evt) { + that.validate(); + that.element.val(that.value()); + that.element.trigger('keyup',evt); + }); + this.input.on('paste', function(evt) { + that.validate(); + that.element.val(that.value()); + that.element.trigger('paste',evt); + }); this.input.on('keydown', function(evt) { if (evt.keyCode >= 37 && evt.keyCode <= 40) { evt.stopPropagation(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index d50601bdc..c27146dec 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -369,7 +369,7 @@ RED.palette.editor = (function() { if (v.modules) { var a = false; v.modules = v.modules.filter(function(m) { - if (checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) { + if (RED.utils.checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) { loadedIndex[m.id] = m; m.index = [m.id]; if (m.keywords) { @@ -483,68 +483,6 @@ RED.palette.editor = (function() { var installAllowList = ['*']; var installDenyList = []; - function parseModuleList(list) { - list = list || ["*"]; - return list.map(function(rule) { - var m = /^(.+?)(?:@(.*))?$/.exec(rule); - var wildcardPos = m[1].indexOf("*"); - wildcardPos = wildcardPos===-1?Infinity:wildcardPos; - - return { - module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"), - version: m[2], - wildcardPos: wildcardPos - } - }) - } - - function checkAgainstList(module,version,list) { - for (var i=0;i deniedRule.wildcardPos - } else { - // First wildcard in same position. - // Go with the longer matching rule. This isn't going to be 100% - // right, but we are deep into edge cases at this point. - return allowedRule.module.toString().length > deniedRule.module.toString().length - } - return false; - } - function init() { if (RED.settings.get('externalModules.palette.allowInstall', true) === false) { return; @@ -555,8 +493,8 @@ RED.palette.editor = (function() { installAllowList = settingsAllowList; installDenyList = settingsDenyList } - installAllowList = parseModuleList(installAllowList); - installDenyList = parseModuleList(installDenyList); + installAllowList = RED.utils.parseModuleList(installAllowList); + installDenyList = RED.utils.parseModuleList(installDenyList); createSettingsPane(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js index 599b5dd8e..fd84ea70d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/utils.js @@ -1171,6 +1171,67 @@ RED.utils = (function() { return '#'+'000000'.slice(0, 6-s.length)+s; } + function parseModuleList(list) { + list = list || ["*"]; + return list.map(function(rule) { + var m = /^(.+?)(?:@(.*))?$/.exec(rule); + var wildcardPos = m[1].indexOf("*"); + wildcardPos = wildcardPos===-1?Infinity:wildcardPos; + + return { + module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"), + version: m[2], + wildcardPos: wildcardPos + } + }) + } + + function checkAgainstList(module,version,list) { + for (var i=0;i deniedRule.wildcardPos + } else { + // First wildcard in same position. + // Go with the longer matching rule. This isn't going to be 100% + // right, but we are deep into edge cases at this point. + return allowedRule.module.toString().length > deniedRule.module.toString().length + } + return false; + } return { createObjectElement: buildMessageElement, getMessageProperty: getMessageProperty, @@ -1190,6 +1251,8 @@ RED.utils = (function() { sanitize: sanitize, renderMarkdown: renderMarkdown, createNodeIcon: createNodeIcon, - getDarkerColor: getDarkerColor + getDarkerColor: getDarkerColor, + parseModuleList: parseModuleList, + checkModuleAllowed: checkModuleAllowed } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/base.scss b/packages/node_modules/@node-red/editor-client/src/sass/base.scss index ca1a5807f..e9c4dcb16 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/base.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/base.scss @@ -146,6 +146,13 @@ body { background-size: contain } + .red-ui-font-code { + font-family: $monospace-font; + font-size: $primary-font-size; + color: $info-text-code-color; + white-space: nowrap; + } + code { font-family: $monospace-font; font-size: $primary-font-size; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss index aefd617c0..869f8930d 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss @@ -174,8 +174,8 @@ button.red-ui-tray-resize-button { .red-ui-editor .red-ui-tray { .dialog-form, #dialog-form, #node-config-dialog-edit-form { - margin: 20px; - height: calc(100% - 40px); + margin: 10px 20px; + height: calc(100% - 20px); } } diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.html b/packages/node_modules/@node-red/nodes/core/function/10-function.html index f0e5c7d68..76bcc9556 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.html +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.html @@ -1,60 +1,318 @@ - diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index eb07c319d..ee54cca50 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -16,6 +16,7 @@ module.exports = function(RED) { "use strict"; + var util = require("util"); var vm = require("vm"); @@ -94,6 +95,11 @@ module.exports = function(RED) { node.func = n.func; node.ini = n.initialize ? n.initialize.trim() : ""; node.fin = n.finalize ? n.finalize.trim() : ""; + node.libs = n.libs || []; + + if (RED.settings.functionExternalModules !== true && node.libs.length > 0) { + throw new Error("Function node not allowed to load external modules"); + } var handleNodeDoneCall = true; @@ -105,23 +111,23 @@ module.exports = function(RED) { } var functionText = "var results = null;"+ - "results = (async function(msg,__send__,__done__){ "+ - "var __msgid__ = msg._msgid;"+ - "var node = {"+ - "id:__node__.id,"+ - "name:__node__.name,"+ - "log:__node__.log,"+ - "error:__node__.error,"+ - "warn:__node__.warn,"+ - "debug:__node__.debug,"+ - "trace:__node__.trace,"+ - "on:__node__.on,"+ - "status:__node__.status,"+ - "send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+ - "done:__done__"+ - "};\n"+ - node.func+"\n"+ - "})(msg,__send__,__done__);"; + "results = (async function(msg,__send__,__done__){ "+ + "var __msgid__ = msg._msgid;"+ + "var node = {"+ + "id:__node__.id,"+ + "name:__node__.name,"+ + "log:__node__.log,"+ + "error:__node__.error,"+ + "warn:__node__.warn,"+ + "debug:__node__.debug,"+ + "trace:__node__.trace,"+ + "on:__node__.on,"+ + "status:__node__.status,"+ + "send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},"+ + "done:__done__"+ + "};\n"+ + node.func+"\n"+ + "})(msg,__send__,__done__);"; var finScript = null; var finOpt = null; node.topic = n.topic; @@ -266,34 +272,96 @@ module.exports = function(RED) { }; sandbox.promisify = util.promisify; } + + if (node.hasOwnProperty("libs")) { + let moduleErrors = false; + var modules = node.libs; + modules.forEach(module => { + var vname = module.hasOwnProperty("var") ? module.var : null; + if (vname && (vname !== "")) { + if (sandbox.hasOwnProperty(vname) || vname === 'node') { + node.error(RED._("function.error.moduleNameError",{name:vname})) + moduleErrors = true; + return; + } + sandbox[vname] = null; + try { + var spec = module.module; + if (spec && (spec !== "")) { + var lib = RED.require(module.module); + sandbox[vname] = lib; + } + } catch (e) { + //TODO: NLS error message + node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:e.toString()})) + moduleErrors = true; + } + } + }); + if (moduleErrors) { + throw new Error("Function node failed to load external modules"); + } + } + + + const RESOLVING = 0; + const RESOLVED = 1; + const ERROR = 2; + var state = RESOLVING; + var messages = []; + var processMessage = (() => {}); + + node.on("input", function(msg,send,done) { + if(state === RESOLVING) { + messages.push({msg:msg, send:send, done:done}); + } + else if(state === RESOLVED) { + processMessage(msg, send, done); + } + }); + var context = vm.createContext(sandbox); try { var iniScript = null; var iniOpt = null; if (node.ini && (node.ini !== "")) { var iniText = ` - (async function(__send__) { - var node = { - id:__node__.id, - name:__node__.name, - log:__node__.log, - error:__node__.error, - warn:__node__.warn, - debug:__node__.debug, - trace:__node__.trace, - status:__node__.status, - send: function(msgs, cloneMsg) { - __node__.send(__send__, RED.util.generateId(), msgs, cloneMsg); - } - }; - `+ node.ini +` - })(__initSend__);`; + (async function(__send__) { + var node = { + id:__node__.id, + name:__node__.name, + log:__node__.log, + error:__node__.error, + warn:__node__.warn, + debug:__node__.debug, + trace:__node__.trace, + status:__node__.status, + send: function(msgs, cloneMsg) { + __node__.send(__send__, RED.util.generateId(), msgs, cloneMsg); + } + }; + `+ node.ini +` + })(__initSend__);`; iniOpt = createVMOpt(node, " setup"); iniScript = new vm.Script(iniText, iniOpt); } node.script = vm.createScript(functionText, createVMOpt(node, "")); if (node.fin && (node.fin !== "")) { - var finText = "(function () {\n"+node.fin +"\n})();"; + var finText = `(function () { + var node = { + id:__node__.id, + name:__node__.name, + log:__node__.log, + error:__node__.error, + warn:__node__.warn, + debug:__node__.debug, + trace:__node__.trace, + status:__node__.status, + send: function(msgs, cloneMsg) { + __node__.error("Cannot send from close function"); + } + }; + `+node.fin +`})();`; finOpt = createVMOpt(node, " cleanup"); finScript = new vm.Script(finText, finOpt); } @@ -303,7 +371,7 @@ module.exports = function(RED) { promise = iniScript.runInContext(context, iniOpt); } - function processMessage(msg, send, done) { + processMessage = function (msg, send, done) { var start = process.hrtime(); context.msg = msg; context.__send__ = send; @@ -363,20 +431,6 @@ module.exports = function(RED) { }); } - const RESOLVING = 0; - const RESOLVED = 1; - const ERROR = 2; - var state = RESOLVING; - var messages = []; - - node.on("input", function(msg,send,done) { - if(state === RESOLVING) { - messages.push({msg:msg, send:send, done:done}); - } - else if(state === RESOLVED) { - processMessage(msg, send, done); - } - }); node.on("close", function() { if (finScript) { try { @@ -422,7 +476,12 @@ module.exports = function(RED) { node.error(err); } } - RED.nodes.registerType("function",FunctionNode); + RED.nodes.registerType("function",FunctionNode, { + dynamicModuleList: "libs", + settings: { + functionExternalModules: { value: false, exportable: true } + } + }); RED.library.register("functions"); }; diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index b6c8e77ae..13f8800cb 100755 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -209,16 +209,25 @@ "function": { "function": "", "label": { - "function": "Function", - "initialize": "Setup", - "finalize": "Close", + "setup": "Setup", + "function": "On Message", + "initialize": "On Start", + "finalize": "On Stop", "outputs": "Outputs" }, "text": { "initialize": "// Code added here will be run once\n// whenever the node is deployed.\n", "finalize": "// Code added here will be run when the\n// node is being stopped or re-deployed.\n" }, + "require": { + "var": "variable", + "module": "module" + }, "error": { + "moduleNotAllowed": "Module __module__ not allowed", + "moduleLoadError": "Failed to load module __module__: __error__", + "moduleNameError": "Invalid module variable name: __name__", + "moduleNameReserved": "Reserved variable name: __name__", "inputListener":"Cannot add listener to 'input' event within Function", "non-message-returned":"Function tried to send a message of type __type__" } diff --git a/packages/node_modules/@node-red/registry/lib/externalModules.js b/packages/node_modules/@node-red/registry/lib/externalModules.js new file mode 100644 index 000000000..ddbac24ae --- /dev/null +++ b/packages/node_modules/@node-red/registry/lib/externalModules.js @@ -0,0 +1,223 @@ +// This module handles the management of modules required by the runtime and flows. +// Essentially this means keeping track of what extra modules a flow requires, +// ensuring those modules are installed and providing a standard way for nodes +// to require those modules safely. + +const fs = require("fs-extra"); +const registryUtil = require("./util"); +const path = require("path"); +const clone = require("clone"); +const exec = require("@node-red/util").exec; +const log = require("@node-red/util").log; + +const BUILTIN_MODULES = require('module').builtinModules; +const EXTERNAL_MODULES_DIR = "externalModules"; + +// TODO: outsource running npm to a plugin +const NPM_COMMAND = (process.platform === "win32") ? "npm.cmd" : "npm"; + +let registeredTypes = {}; +let subflowTypes = {}; +let settings; + +let knownExternalModules = {}; + +let installEnabled = true; +let installAllowList = ['*']; +let installDenyList = []; + +function getInstallDir() { + return path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", "externalModules")); +} + +async function refreshExternalModules() { + const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR)); + try { + const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8")); + knownExternalModules = pkgFile.dependencies; + } catch(err) { + } +} + +function init(_settings) { + settings = _settings; + knownExternalModules = {}; + installEnabled = true; + if (settings.externalModules && settings.externalModules.modules) { + if (settings.externalModules.modules.allowList || settings.externalModules.modules.denyList) { + installAllowList = settings.externalModules.modules.allowList; + installDenyList = settings.externalModules.modules.denyList; + } + if (settings.externalModules.modules.hasOwnProperty("allowInstall")) { + installEnabled = settings.externalModules.modules.allowInstall + } + } + installAllowList = registryUtil.parseModuleList(installAllowList); + installDenyList = registryUtil.parseModuleList(installDenyList); +} + +function register(type, dynamicModuleListProperty) { + registeredTypes[type] = dynamicModuleListProperty; +} + +function registerSubflow(type, subflowConfig) { + subflowTypes[type] = subflowConfig; +} + +function requireModule(module) { + if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) { + const e = new Error("Module not allowed"); + e.code = "module_not_allowed"; + throw e; + } + if (BUILTIN_MODULES.indexOf(module) !== -1) { + return require(module); + } + if (!knownExternalModules[module]) { + const e = new Error("Module not allowed"); + e.code = "module_not_allowed"; + throw e; + } + const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR)); + const moduleDir = path.join(externalModuleDir,"node_modules",module); + return require(moduleDir); +} + +function parseModuleName(module) { + var match = /((?:@[^/]+\/)?[^/@]+)(?:@([\s\S]+))?/.exec(module); + if (match) { + return { + spec: module, + module: match[1], + version: match[2], + builtin: BUILTIN_MODULES.indexOf(match[1]) !== -1, + known: !!knownExternalModules[match[1]] + } + } + return null; +} + +function isInstalled(moduleDetails) { + return moduleDetails.builtin || moduleDetails.known; +} + + +async function checkFlowDependencies(flowConfig) { + let nodes = clone(flowConfig); + await refreshExternalModules(); + + const checkedModules = {}; + const promises = []; + const errors = []; + const checkedSubflows = {}; + while (nodes.length > 0) { + let n = nodes.shift(); + if (subflowTypes[n.type] && !checkedSubflows[n.type]) { + checkedSubflows[n.type] = true; + nodes = nodes.concat(subflowTypes[n.type].flow) + } else if (registeredTypes[n.type]) { + let nodeModules = n[registeredTypes[n.type]] || []; + if (!Array.isArray(nodeModules)) { + nodeModules = [nodeModules] + } + nodeModules.forEach(module => { + if (typeof module !== 'string') { + module = module.module || ""; + } + if (module) { + let moduleDetails = parseModuleName(module) + if (moduleDetails && checkedModules[moduleDetails.module] === undefined) { + checkedModules[moduleDetails.module] = isInstalled(moduleDetails) + if (!checkedModules[moduleDetails.module]) { + if (installEnabled) { + promises.push(installModule(moduleDetails).catch(err => { + errors.push({module: moduleDetails,error:err}); + })) + } else if (!installEnabled) { + const e = new Error("Module install disabled - externalModules.modules.allowInstall=false"); + e.code = "install_not_allowed"; + errors.push({module: moduleDetails,error:e}); + } + } else if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) { + const e = new Error("Module not allowed"); + e.code = "module_not_allowed"; + errors.push({module: moduleDetails,error:e}); + } + } + } + }) + } + } + return Promise.all(promises).then(refreshExternalModules).then(() => { + if (errors.length > 0) { + throw errors; + } + }) +} + + +async function ensureModuleDir() { + const installDir = getInstallDir(); + + if (!fs.existsSync(installDir)) { + await fs.ensureDir(installDir); + } + const pkgFile = path.join(installDir,"package.json"); + if (!fs.existsSync(pkgFile)) { + await fs.writeFile(path.join(installDir,"package.json"),`{ + "name": "Node-RED-External-Modules", + "description": "These modules are automatically installed by Node-RED to use in Function nodes.", + "version": "1.0.0", + "private": true, + "dependencies": {} +}`) + } +} + +async function installModule(moduleDetails) { + let installSpec = moduleDetails.module; + if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) { + const e = new Error("Install not allowed"); + e.code = "install_not_allowed"; + throw e; + } + if (moduleDetails.version) { + installSpec = installSpec+"@"+moduleDetails.version; + } + log.info(log._("server.install.installing",{name: moduleDetails.module,version: moduleDetails.version||"latest"})); + const installDir = getInstallDir(); + + await ensureModuleDir(); + + var args = ["install", installSpec, "--production"]; + return exec.run(NPM_COMMAND, args, { + cwd: installDir + },true).then(result => { + log.info("successfully installed: "+installSpec); + }).catch(result => { + var output = result.stderr; + var e; + if (/E404/.test(output) || /ETARGET/.test(output)) { + log.error(log._("server.install.install-failed-not-found",{name:installSpec})); + e = new Error("Module not found"); + e.code = 404; + throw e; + } else { + log.error(log._("server.install.install-failed-long",{name:installSpec})); + log.error("------------------------------------------"); + log.error(output); + log.error("------------------------------------------"); + e = new Error(log._("server.install.install-failed")); + e.code = "unexpected_error"; + throw e; + } + }) +} + +module.exports = { + init: init, + register: register, + registerSubflow: registerSubflow, + checkFlowDependencies: checkFlowDependencies, + require: requireModule +} \ No newline at end of file diff --git a/packages/node_modules/@node-red/registry/lib/index.js b/packages/node_modules/@node-red/registry/lib/index.js index 18c30f0fc..a620d9ad2 100644 --- a/packages/node_modules/@node-red/registry/lib/index.js +++ b/packages/node_modules/@node-red/registry/lib/index.js @@ -28,6 +28,7 @@ var registry = require("./registry"); var loader = require("./loader"); var installer = require("./installer"); var library = require("./library"); +const externalModules = require("./externalModules") var plugins = require("./plugins"); /** @@ -44,6 +45,7 @@ function init(runtime) { plugins.init(runtime.settings); registry.init(runtime.settings,loader); library.init(); + externalModules.init(runtime.settings); } /** @@ -299,6 +301,8 @@ module.exports = { */ getNodeExampleFlowPath: library.getExampleFlowPath, + checkFlowDependencies: externalModules.checkFlowDependencies, + registerPlugin: plugins.registerPlugin, getPlugin: plugins.getPlugin, getPluginsByType: plugins.getPluginsByType, diff --git a/packages/node_modules/@node-red/registry/lib/registry.js b/packages/node_modules/@node-red/registry/lib/registry.js index 96594fbef..82de4e442 100644 --- a/packages/node_modules/@node-red/registry/lib/registry.js +++ b/packages/node_modules/@node-red/registry/lib/registry.js @@ -21,6 +21,7 @@ var fs = require("fs"); var library = require("./library"); const {events} = require("@node-red/util") var subflows = require("./subflow"); +var externalModules = require("./externalModules") var settings; var loader; @@ -28,6 +29,7 @@ var nodeConfigCache = {}; var moduleConfigs = {}; var nodeList = []; var nodeConstructors = {}; +var nodeOptions = {}; var subflowModules = {}; var nodeTypeToId = {}; @@ -36,12 +38,7 @@ var moduleNodes = {}; function init(_settings,_loader) { settings = _settings; loader = _loader; - moduleNodes = {}; - nodeTypeToId = {}; - nodeConstructors = {}; - subflowModules = {}; - nodeList = []; - nodeConfigCache = {}; + clear(); } function load() { @@ -241,6 +238,7 @@ function removeNode(id) { if (typeId === id) { delete subflowModules[t]; delete nodeConstructors[t]; + delete nodeOptions[t]; delete nodeTypeToId[t]; } }); @@ -412,7 +410,7 @@ function getCaller(){ return stack[0].getFileName(); } -function registerNodeConstructor(nodeSet,type,constructor) { +function registerNodeConstructor(nodeSet,type,constructor,options) { if (nodeConstructors.hasOwnProperty(type)) { throw new Error(type+" already registered"); } @@ -432,6 +430,12 @@ function registerNodeConstructor(nodeSet,type,constructor) { } nodeConstructors[type] = constructor; + nodeOptions[type] = options; + if (options) { + if (options.dynamicModuleList) { + externalModules.register(type,options.dynamicModuleList); + } + } events.emit("type-registered",type); } @@ -452,6 +456,9 @@ function registerSubflow(nodeSet, subflow) { nodeSetInfo.config = result.config; } subflowModules[result.type] = result; + externalModules.registerSubflow(result.type,subflow); + + events.emit("type-registered",result.type); return result; } @@ -524,6 +531,7 @@ function clear() { moduleConfigs = {}; nodeList = []; nodeConstructors = {}; + nodeOptions = {}; subflowModules = {}; nodeTypeToId = {}; } diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index e6caa5d67..0a5d579ab 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -17,6 +17,7 @@ const path = require("path"); const semver = require("semver"); const {events,i18n,log} = require("@node-red/util"); + var runtime; function copyObjectProperties(src,dst,copyList,blockList) { @@ -45,9 +46,8 @@ function requireModule(name) { var relPath = path.relative(__dirname, moduleInfo.path); return require(relPath); } else { - var err = new Error(`Cannot find module '${name}'`); - err.code = "MODULE_NOT_FOUND"; - throw err; + // Require it here to avoid the circular dependency + return require("./externalModules").require(name); } } @@ -90,7 +90,7 @@ function createNodeApi(node) { httpAdmin: runtime.adminApp, server: runtime.server } - copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials" ]); + copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials"]); red.nodes.registerType = function(type,constructor,opts) { runtime.nodes.registerType(node.id,type,constructor,opts); } @@ -136,7 +136,6 @@ function checkAgainstList(module,version,list) { } function checkModuleAllowed(module,version,allowList,denyList) { - // console.log("checkModuleAllowed",module,version);//,allowList,denyList) if (!allowList && !denyList) { // Default to allow return true; diff --git a/packages/node_modules/@node-red/runtime/lib/flows/index.js b/packages/node_modules/@node-red/runtime/lib/flows/index.js index af5663771..8d6fe1873 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/index.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/index.js @@ -187,35 +187,35 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) { }); } - return configSavePromise - .then(function(flowRevision) { - if (!isLoad) { - log.debug("saved flow revision: "+flowRevision); - } - activeConfig = { - flows:config, - rev:flowRevision - }; - activeFlowConfig = newFlowConfig; - if (forceStart || started) { - // Flows are running (or should be) - // Stop the active flows (according to deploy type and the diff) - return stop(type,diff,muteLog).then(() => { - // Once stopped, allow context to remove anything no longer needed - return context.clean(activeFlowConfig) - }).then(() => { - // Start the active flows - start(type,diff,muteLog).then(() => { - events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); - }); - // Return the new revision asynchronously to the actual start - return flowRevision; - }).catch(function(err) { }) - } else { - events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); - } - }); + return configSavePromise.then(flowRevision => { + if (!isLoad) { + log.debug("saved flow revision: "+flowRevision); + } + activeConfig = { + flows:config, + rev:flowRevision + }; + activeFlowConfig = newFlowConfig; + if (forceStart || started) { + // Flows are running (or should be) + + // Stop the active flows (according to deploy type and the diff) + return stop(type,diff,muteLog).then(() => { + // Once stopped, allow context to remove anything no longer needed + return context.clean(activeFlowConfig) + }).then(() => { + // Start the active flows + start(type,diff,muteLog).then(() => { + events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); + }); + // Return the new revision asynchronously to the actual start + return flowRevision; + }).catch(function(err) { }) + } else { + events.emit("runtime-event",{id:"runtime-deploy",payload:{revision:flowRevision},retain: true}); + } + }); } function getNode(id) { @@ -246,7 +246,7 @@ function getFlows() { return activeConfig; } -function start(type,diff,muteLog) { +async function start(type,diff,muteLog) { type = type||"full"; started = true; var i; @@ -271,7 +271,21 @@ function start(type,diff,muteLog) { log.info(" "+settings.userDir); } events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true}); - return Promise.resolve(); + return; + } + + try { + await typeRegistry.checkFlowDependencies(activeConfig.flows); + } catch(err) { + log.info("Failed to load external modules required by this flow:"); + const missingModules = []; + for (i=0;i done(err)); + }) + + it('should fail if using OS module without it listed in libs', function(done) { + var flow = [ + {id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = os.type(); return msg;"}, + {id:"n2", type:"helper"} + ]; + helper.settings({ + functionExternalModules: true + }) + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + var messageReceived = false; + n2.on("input", function(msg) { + messageReceived = true; + }); + n1.receive({payload:"foo",topic: "bar"}); + setTimeout(function() { + try { + messageReceived.should.be.false(); + done(); + } catch(err) { + done(err); + } + },20); + }).catch(err => done(err)); + }) + it('should require the OS module', function(done) { + var flow = [ + {id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = os.type(); return msg;", "libs": [{var:"os", module:"os"}]}, + {id:"n2", type:"helper"} + ]; + helper.settings({ + functionExternalModules: true + }) + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function(msg) { + try { + msg.should.have.property('topic', 'bar'); + msg.should.have.property('payload', require('os').type()); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo",topic: "bar"}); + }).catch(err => done(err)); + }) + it('should fail if module variable name clashes with sandbox builtin', function(done) { + var flow = [ + {id:"n1",type:"function",wires:[["n2"]],func:"msg.payload = os.type(); return msg;", "libs": [{var:"flow", module:"os"}]}, + {id:"n2", type:"helper"} + ]; + helper.settings({ + functionExternalModules: true + }) + helper.load(functionNode, flow, function() { + var n1 = helper.getNode("n1"); + should.not.exist(n1); + done(); + }).catch(err => done(err)); + }) + }) + + describe('Logger', function () { function testLog(initCode,funcCode,expectedLevel, done) { @@ -1603,5 +1680,4 @@ describe('function node', function() { }); }) - */ }); diff --git a/test/resources/subflow/package/package.json b/test/resources/subflow/package/package.json index 92ff33e2b..831428c6a 100644 --- a/test/resources/subflow/package/package.json +++ b/test/resources/subflow/package/package.json @@ -1,6 +1,6 @@ { "name": "test-subflow-mod", - "version": "1.0.1", + "version": "1.0.2", "description": "", "keywords": [], "license": "ISC", @@ -13,6 +13,7 @@ ] }, "dependencies": { - "node-red-node-random": "*" + "node-red-node-random": "*", + "cowsay2": "*" } } diff --git a/test/resources/subflow/package/subflow.json b/test/resources/subflow/package/subflow.json index e5c6b25ab..db43cdff4 100644 --- a/test/resources/subflow/package/subflow.json +++ b/test/resources/subflow/package/subflow.json @@ -189,6 +189,7 @@ "noerr": 0, "initialize": "", "finalize": "", + "libs": [ {"var":"cowsay2","module":"cowsay2"}], "x": 240, "y": 100, "wires": [ diff --git a/test/resources/subflow/test-subflow-mod-1.0.1.tgz b/test/resources/subflow/test-subflow-mod-1.0.1.tgz deleted file mode 100644 index daa53b77a..000000000 Binary files a/test/resources/subflow/test-subflow-mod-1.0.1.tgz and /dev/null differ diff --git a/test/resources/subflow/test-subflow-mod-1.0.2.tgz b/test/resources/subflow/test-subflow-mod-1.0.2.tgz new file mode 100644 index 000000000..093e12f92 Binary files /dev/null and b/test/resources/subflow/test-subflow-mod-1.0.2.tgz differ diff --git a/test/unit/@node-red/registry/lib/externalModules_spec.js b/test/unit/@node-red/registry/lib/externalModules_spec.js new file mode 100644 index 000000000..1cd274561 --- /dev/null +++ b/test/unit/@node-red/registry/lib/externalModules_spec.js @@ -0,0 +1,302 @@ + // init: init, + // register: register, + // registerSubflow: registerSubflow, + // checkFlowDependencies: checkFlowDependencies, + // require: requireModule + // + +const should = require("should"); +const sinon = require("sinon"); +const fs = require("fs-extra"); +const path = require("path"); +const os = require("os"); + +const NR_TEST_UTILS = require("nr-test-utils"); +const externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules"); +const exec = NR_TEST_UTILS.require("@node-red/util/lib/exec"); + +let homeDir; + +async function createUserDir() { + if (!homeDir) { + homeDir = path.join(os.tmpdir(),"nr-test-"+Math.floor(Math.random()*100000)); + } + await fs.ensureDir(homeDir); +} + +async function setupExternalModulesPackage(dependencies) { + await fs.ensureDir(path.join(homeDir,"externalModules")) + await fs.writeFile(path.join(homeDir,"externalModules","package.json"),`{ +"name": "Node-RED-External-Modules", +"description": "These modules are automatically installed by Node-RED to use in Function nodes.", +"version": "1.0.0", +"private": true, +"dependencies": ${JSON.stringify(dependencies)} +}`) +} + +describe("externalModules api", function() { + beforeEach(async function() { + await createUserDir() + }) + afterEach(async function() { + await fs.remove(homeDir); + }) + describe("checkFlowDependencies", function() { + beforeEach(function() { + sinon.stub(exec,"run", async function(cmd, args, options) { + let error; + if (args[1] === "moduleNotFound") { + error = new Error(); + error.stderr = "E404"; + } else if (args[1] === "moduleVersionNotFound") { + error = new Error(); + error.stderr = "ETARGET"; + } else if (args[1] === "moduleFail") { + error = new Error(); + error.stderr = "Some unexpected install error"; + } + if (error) { + throw error; + } + }) + }) + afterEach(function() { + exec.run.restore(); + }) + it("does nothing when no types are registered",async function() { + externalModules.init({userDir: homeDir}); + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "foo"}]} + ]) + exec.run.called.should.be.false(); + }); + + it("skips install for modules already installed", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + await setupExternalModulesPackage({"foo": "1.2.3", "bar":"2.3.4"}); + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "foo"}]} + ]) + exec.run.called.should.be.false(); + }) + + it("skips install for built-in modules", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "fs"}]} + ]) + exec.run.called.should.be.false(); + }) + + it("installs missing modules", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + fs.existsSync(path.join(homeDir,"externalModules")).should.be.false(); + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "foo"}]} + ]) + exec.run.called.should.be.true(); + fs.existsSync(path.join(homeDir,"externalModules")).should.be.true(); + }) + + it("installs missing modules from inside subflow module", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + externalModules.registerSubflow("sf", {"flow":[{type: "function", libs:[{module: "foo"}]}]}); + await externalModules.checkFlowDependencies([ + {type: "sf"} + ]) + exec.run.called.should.be.true(); + }) + + it("reports install fail - 404", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + try { + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "moduleNotFound"}]} + ]) + throw new Error("checkFlowDependencies did not reject after install fail") + } catch(err) { + exec.run.called.should.be.true(); + Array.isArray(err).should.be.true(); + err.should.have.length(1); + err[0].should.have.property("module"); + err[0].module.should.have.property("module","moduleNotFound"); + err[0].should.have.property("error"); + err[0].error.should.have.property("code",404); + + } + }) + it("reports install fail - target", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + try { + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "moduleVersionNotFound"}]} + ]) + throw new Error("checkFlowDependencies did not reject after install fail") + } catch(err) { + exec.run.called.should.be.true(); + Array.isArray(err).should.be.true(); + err.should.have.length(1); + err[0].should.have.property("module"); + err[0].module.should.have.property("module","moduleVersionNotFound"); + err[0].should.have.property("error"); + err[0].error.should.have.property("code",404); + } + }) + + it("reports install fail - unexpected", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + try { + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "moduleFail"}]} + ]) + throw new Error("checkFlowDependencies did not reject after install fail") + } catch(err) { + exec.run.called.should.be.true(); + Array.isArray(err).should.be.true(); + err.should.have.length(1); + err[0].should.have.property("module"); + err[0].module.should.have.property("module","moduleFail"); + err[0].should.have.property("error"); + err[0].error.should.have.property("code","unexpected_error"); + } + }) + it("reports install fail - multiple", async function() { + externalModules.init({userDir: homeDir}); + externalModules.register("function", "libs"); + try { + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "moduleNotFound"},{module: "moduleFail"}]} + ]) + throw new Error("checkFlowDependencies did not reject after install fail") + } catch(err) { + exec.run.called.should.be.true(); + Array.isArray(err).should.be.true(); + err.should.have.length(2); + // Sort the array so we know the order to test for + err.sort(function(A,B) { + return A.module.module.localeCompare(B.module.module); + }) + err[1].should.have.property("module"); + err[1].module.should.have.property("module","moduleNotFound"); + err[1].should.have.property("error"); + err[1].error.should.have.property("code",404); + err[0].should.have.property("module"); + err[0].module.should.have.property("module","moduleFail"); + err[0].should.have.property("error"); + err[0].error.should.have.property("code","unexpected_error"); + + } + }) + it("reports install fail - install disabled", async function() { + externalModules.init({userDir: homeDir, externalModules: { + modules: { + allowInstall: false + } + }}); + externalModules.register("function", "libs"); + try { + await externalModules.checkFlowDependencies([ + {type: "function", libs:[{module: "foo"}]} + ]) + throw new Error("checkFlowDependencies did not reject after install fail") + } catch(err) { + // Should not try to install + exec.run.called.should.be.false(); + Array.isArray(err).should.be.true(); + err.should.have.length(1); + err[0].should.have.property("module"); + err[0].module.should.have.property("module","foo"); + err[0].should.have.property("error"); + err[0].error.should.have.property("code","install_not_allowed"); + } + }) + + it("reports install fail - module disallowed", async function() { + externalModules.init({userDir: homeDir, externalModules: { + modules: { + denyList: ['foo'] + } + }}); + externalModules.register("function", "libs"); + try { + await externalModules.checkFlowDependencies([ + // foo disallowed + // bar allowed + {type: "function", libs:[{module: "foo"},{module: "bar"}]} + ]) + throw new Error("checkFlowDependencies did not reject after install fail") + } catch(err) { + exec.run.calledOnce.should.be.true(); + Array.isArray(err).should.be.true(); + err.should.have.length(1); + err[0].should.have.property("module"); + err[0].module.should.have.property("module","foo"); + err[0].should.have.property("error"); + err[0].error.should.have.property("code","install_not_allowed"); + } + }) + + it("reports install fail - built-in module disallowed", async function() { + externalModules.init({userDir: homeDir, externalModules: { + modules: { + denyList: ['fs'] + } + }}); + externalModules.register("function", "libs"); + try { + await externalModules.checkFlowDependencies([ + // foo disallowed + // bar allowed + {type: "function", libs:[{module: "fs"},{module: "bar"}]} + ]) + throw new Error("checkFlowDependencies did not reject after install fail") + } catch(err) { + exec.run.calledOnce.should.be.true(); + Array.isArray(err).should.be.true(); + err.should.have.length(1); + err[0].should.have.property("module"); + err[0].module.should.have.property("module","fs"); + err[0].should.have.property("error"); + err[0].error.should.have.property("code","module_not_allowed"); + } + }) + }) + describe("require", async function() { + it("requires built-in modules", async function() { + externalModules.init({userDir: homeDir}); + const result = externalModules.require("fs") + result.should.eql(require("fs")); + }) + it("rejects unknown modules", async function() { + externalModules.init({userDir: homeDir}); + try { + externalModules.require("foo") + throw new Error("require did not reject after fail") + } catch(err) { + err.should.have.property("code","module_not_allowed"); + } + }) + + it("rejects disallowed modules", async function() { + externalModules.init({userDir: homeDir, externalModules: { + modules: { + denyList: ['fs'] + } + }}); + try { + externalModules.require("fs") + throw new Error("require did not reject after fail") + } catch(err) { + err.should.have.property("code","module_not_allowed"); + } + }) + }) +}); \ No newline at end of file diff --git a/test/unit/@node-red/registry/lib/index_spec.js b/test/unit/@node-red/registry/lib/index_spec.js index 75a057730..03ce5d509 100644 --- a/test/unit/@node-red/registry/lib/index_spec.js +++ b/test/unit/@node-red/registry/lib/index_spec.js @@ -40,7 +40,7 @@ describe('red/registry/index', function() { stubs.push(sinon.stub(loader,"init")); stubs.push(sinon.stub(typeRegistry,"init")); - registry.init({}); + registry.init({settings:{}}); installer.init.called.should.be.true(); loader.init.called.should.be.true(); typeRegistry.init.called.should.be.true(); diff --git a/test/unit/@node-red/runtime/lib/flows/index_spec.js b/test/unit/@node-red/runtime/lib/flows/index_spec.js index e230d2407..2e645df0d 100644 --- a/test/unit/@node-red/runtime/lib/flows/index_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/index_spec.js @@ -36,6 +36,7 @@ describe('flows/index', function() { var flowCreate; var getType; + var checkFlowDependencies; var mockLog = { log: sinon.stub(), @@ -52,9 +53,16 @@ describe('flows/index', function() { getType = sinon.stub(typeRegistry,"get",function(type) { return type.indexOf('missing') === -1; }); + checkFlowDependencies = sinon.stub(typeRegistry, "checkFlowDependencies", async function(flow) { + if (flow[0].id === "node-with-missing-modules") { + throw new Error("Missing module"); + } + }); }); + after(function() { getType.restore(); + checkFlowDependencies.restore(); }); @@ -306,7 +314,7 @@ describe('flows/index', function() { flows.init({log:mockLog, settings:{},storage:storage}); flows.load().then(function() { - flows.startFlows(); + return flows.startFlows(); }); }); it('does not start if nodes missing', function(done) { @@ -321,9 +329,14 @@ describe('flows/index', function() { flows.init({log:mockLog, settings:{},storage:storage}); flows.load().then(function() { - flows.startFlows(); - flowCreate.called.should.be.false(); - done(); + return flows.startFlows(); + }).then(() => { + try { + flowCreate.called.should.be.false(); + done(); + } catch(err) { + done(err); + } }); }); @@ -339,9 +352,9 @@ describe('flows/index', function() { } flows.init({log:mockLog, settings:{},storage:storage}); flows.load().then(function() { - flows.startFlows(); + return flows.startFlows(); + }).then(() => { flowCreate.called.should.be.false(); - events.emit("type-registered","missing"); setTimeout(function() { flowCreate.called.should.be.false(); @@ -354,7 +367,44 @@ describe('flows/index', function() { }); }); + it('does not start if external modules missing', function(done) { + var originalConfig = [ + {id:"node-with-missing-modules",x:10,y:10,z:"t1",type:"test",wires:[]}, + {id:"t1",type:"tab"} + ]; + storage.getFlows = function() { + return Promise.resolve({flows:originalConfig}); + } + var receivedEvent = null; + var handleEvent = function(payload) { + receivedEvent = payload; + } + + events.on("runtime-event",handleEvent); + + //{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});" + + + flows.init({log:mockLog, settings:{},storage:storage}); + flows.load().then(flows.startFlows).then(() => { + events.removeListener("runtime-event",handleEvent); + try { + flowCreate.called.should.be.false(); + receivedEvent.should.have.property('id','runtime-state'); + receivedEvent.should.have.property('payload', + { error: 'missing-modules', + type: 'warning', + text: 'notification.warnings.missing-modules', + modules: [] } + ); + + done(); + }catch(err) { + done(err) + } + }); + }); });