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+="- "+msg.modules.map(function(m) { return RED.utils.sanitize(m.module)+(m.error?(" - "+RED.utils.sanitize(""+m.error)+""):"")}).join("
- ")+"
";
+ 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)
+ }
+ });
+ });
});