mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Merge pull request #2873 from node-red/function-modules
Function node external modules
This commit is contained in:
commit
3d23d1de4f
@ -106,7 +106,7 @@
|
|||||||
"marked": "2.0.0",
|
"marked": "2.0.0",
|
||||||
"minami": "1.2.3",
|
"minami": "1.2.3",
|
||||||
"mocha": "^5.2.0",
|
"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",
|
"node-sass": "^4.14.1",
|
||||||
"nodemon": "2.0.6",
|
"nodemon": "2.0.6",
|
||||||
"should": "13.2.3",
|
"should": "13.2.3",
|
||||||
|
@ -143,6 +143,7 @@
|
|||||||
"nodeActionDisabled": "node actions disabled",
|
"nodeActionDisabled": "node actions disabled",
|
||||||
"nodeActionDisabledSubflow": "node actions disabled within subflow",
|
"nodeActionDisabledSubflow": "node actions disabled within subflow",
|
||||||
"missing-types": "<p>Flows stopped due to missing node types.</p>",
|
"missing-types": "<p>Flows stopped due to missing node types.</p>",
|
||||||
|
"missing-modules": "<p>Flows stopped due to missing modules.</p>",
|
||||||
"safe-mode":"<p>Flows stopped in safe mode.</p><p>You can modify your flows and deploy the changes to restart.</p>",
|
"safe-mode":"<p>Flows stopped in safe mode.</p><p>You can modify your flows and deploy the changes to restart.</p>",
|
||||||
"restartRequired": "Node-RED must be restarted to enable upgraded modules",
|
"restartRequired": "Node-RED must be restarted to enable upgraded modules",
|
||||||
"credentials_load_failed": "<p>Flows stopped as the credentials could not be decrypted.</p><p>The flow credential file is encrypted, but the project's encryption key is missing or invalid.</p>",
|
"credentials_load_failed": "<p>Flows stopped as the credentials could not be decrypted.</p><p>The flow credential file is encrypted, but the project's encryption key is missing or invalid.</p>",
|
||||||
|
@ -315,6 +315,7 @@ var RED = (function() {
|
|||||||
id: notificationId
|
id: notificationId
|
||||||
}
|
}
|
||||||
if (notificationId === "runtime-state") {
|
if (notificationId === "runtime-state") {
|
||||||
|
RED.events.emit("runtime-state",msg);
|
||||||
if (msg.error === "safe-mode") {
|
if (msg.error === "safe-mode") {
|
||||||
options.buttons = [
|
options.buttons = [
|
||||||
{
|
{
|
||||||
@ -347,6 +348,16 @@ var RED = (function() {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
} else if (msg.error === "missing-modules") {
|
||||||
|
text+="<ul><li>"+msg.modules.map(function(m) { return RED.utils.sanitize(m.module)+(m.error?(" - <small>"+RED.utils.sanitize(""+m.error)+"</small>"):"")}).join("</li><li>")+"</li></ul>";
|
||||||
|
options.buttons = [
|
||||||
|
{
|
||||||
|
text: RED._("common.label.close"),
|
||||||
|
click: function() {
|
||||||
|
persistentNotifications[notificationId].hideNotification();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
} else if (msg.error === "credentials_load_failed") {
|
} else if (msg.error === "credentials_load_failed") {
|
||||||
if (RED.settings.theme("projects.enabled",false)) {
|
if (RED.settings.theme("projects.enabled",false)) {
|
||||||
// projects enabled
|
// projects enabled
|
||||||
@ -437,6 +448,9 @@ var RED = (function() {
|
|||||||
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
|
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
|
||||||
persistentNotifications[notificationId].close();
|
persistentNotifications[notificationId].close();
|
||||||
delete persistentNotifications[notificationId];
|
delete persistentNotifications[notificationId];
|
||||||
|
if (notificationId === 'runtime-state') {
|
||||||
|
RED.events.emit("runtime-state",msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
RED.comms.subscribe("status/#",function(topic,msg) {
|
RED.comms.subscribe("status/#",function(topic,msg) {
|
||||||
|
@ -29,6 +29,7 @@ RED.tabs = (function() {
|
|||||||
var currentTabWidth;
|
var currentTabWidth;
|
||||||
var currentActiveTabWidth = 0;
|
var currentActiveTabWidth = 0;
|
||||||
var collapsibleMenu;
|
var collapsibleMenu;
|
||||||
|
var mousedownTab;
|
||||||
var preferredOrder = options.order;
|
var preferredOrder = options.order;
|
||||||
var ul = options.element || $("#"+options.id);
|
var ul = options.element || $("#"+options.id);
|
||||||
var wrapper = ul.wrap( "<div>" ).parent();
|
var wrapper = ul.wrap( "<div>" ).parent();
|
||||||
@ -207,6 +208,11 @@ RED.tabs = (function() {
|
|||||||
if (dragActive) {
|
if (dragActive) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (evt.currentTarget !== mousedownTab) {
|
||||||
|
mousedownTab = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mousedownTab = null;
|
||||||
if (dblClickTime && Date.now()-dblClickTime < 400) {
|
if (dblClickTime && Date.now()-dblClickTime < 400) {
|
||||||
dblClickTime = 0;
|
dblClickTime = 0;
|
||||||
dblClickArmed = true;
|
dblClickArmed = true;
|
||||||
@ -445,6 +451,7 @@ RED.tabs = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul.find("li.red-ui-tab a")
|
ul.find("li.red-ui-tab a")
|
||||||
|
.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
|
||||||
.on("mouseup",onTabClick)
|
.on("mouseup",onTabClick)
|
||||||
.on("click", function(evt) {evt.preventDefault(); })
|
.on("click", function(evt) {evt.preventDefault(); })
|
||||||
.on("dblclick", function(evt) {evt.stopPropagation(); 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.attr('id',"red-ui-tab-"+(tab.id.replace(".","-")));
|
||||||
li.data("tabId",tab.id);
|
li.data("tabId",tab.id);
|
||||||
|
|
||||||
if (options.maximumTabWidth) {
|
if (options.maximumTabWidth || tab.maximumTabWidth) {
|
||||||
li.css("maxWidth",options.maximumTabWidth+"px");
|
li.css("maxWidth",(options.maximumTabWidth || tab.maximumTabWidth) +"px");
|
||||||
}
|
}
|
||||||
var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
|
var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
|
||||||
if (tab.icon) {
|
if (tab.icon) {
|
||||||
@ -636,6 +643,7 @@ RED.tabs = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
link.on("mousedown", function(evt) { mousedownTab = evt.currentTarget })
|
||||||
link.on("mouseup",onTabClick);
|
link.on("mouseup",onTabClick);
|
||||||
link.on("click", function(evt) { evt.preventDefault(); })
|
link.on("click", function(evt) { evt.preventDefault(); })
|
||||||
link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })
|
link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); })
|
||||||
|
@ -344,6 +344,16 @@
|
|||||||
that.element.val(that.value());
|
that.element.val(that.value());
|
||||||
that.element.trigger('change',[that.propertyType,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) {
|
this.input.on('keydown', function(evt) {
|
||||||
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
|
if (evt.keyCode >= 37 && evt.keyCode <= 40) {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
@ -369,7 +369,7 @@ RED.palette.editor = (function() {
|
|||||||
if (v.modules) {
|
if (v.modules) {
|
||||||
var a = false;
|
var a = false;
|
||||||
v.modules = v.modules.filter(function(m) {
|
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;
|
loadedIndex[m.id] = m;
|
||||||
m.index = [m.id];
|
m.index = [m.id];
|
||||||
if (m.keywords) {
|
if (m.keywords) {
|
||||||
@ -483,68 +483,6 @@ RED.palette.editor = (function() {
|
|||||||
var installAllowList = ['*'];
|
var installAllowList = ['*'];
|
||||||
var installDenyList = [];
|
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<list.length;i++) {
|
|
||||||
var rule = list[i];
|
|
||||||
if (rule.module.test(module)) {
|
|
||||||
// Without a full semver library in the editor,
|
|
||||||
// we skip the version check.
|
|
||||||
// Not ideal - but will get caught in the runtime
|
|
||||||
// if the user tries to install.
|
|
||||||
return rule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkModuleAllowed(module,version,allowList,denyList) {
|
|
||||||
if (!allowList && !denyList) {
|
|
||||||
// Default to allow
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (allowList.length === 0 && denyList.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowedRule = checkAgainstList(module,version,allowList);
|
|
||||||
var deniedRule = checkAgainstList(module,version,denyList);
|
|
||||||
// console.log("A",allowedRule)
|
|
||||||
// console.log("D",deniedRule)
|
|
||||||
|
|
||||||
if (allowedRule && !deniedRule) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!allowedRule && deniedRule) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!allowedRule && !deniedRule) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
|
|
||||||
return allowedRule.wildcardPos > 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() {
|
function init() {
|
||||||
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
|
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
|
||||||
return;
|
return;
|
||||||
@ -555,8 +493,8 @@ RED.palette.editor = (function() {
|
|||||||
installAllowList = settingsAllowList;
|
installAllowList = settingsAllowList;
|
||||||
installDenyList = settingsDenyList
|
installDenyList = settingsDenyList
|
||||||
}
|
}
|
||||||
installAllowList = parseModuleList(installAllowList);
|
installAllowList = RED.utils.parseModuleList(installAllowList);
|
||||||
installDenyList = parseModuleList(installDenyList);
|
installDenyList = RED.utils.parseModuleList(installDenyList);
|
||||||
|
|
||||||
createSettingsPane();
|
createSettingsPane();
|
||||||
|
|
||||||
|
@ -1171,6 +1171,67 @@ RED.utils = (function() {
|
|||||||
return '#'+'000000'.slice(0, 6-s.length)+s;
|
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<list.length;i++) {
|
||||||
|
var rule = list[i];
|
||||||
|
if (rule.module.test(module)) {
|
||||||
|
// Without a full semver library in the editor,
|
||||||
|
// we skip the version check.
|
||||||
|
// Not ideal - but will get caught in the runtime
|
||||||
|
// if the user tries to install.
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkModuleAllowed(module,version,allowList,denyList) {
|
||||||
|
if (!allowList && !denyList) {
|
||||||
|
// Default to allow
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (allowList.length === 0 && denyList.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedRule = checkAgainstList(module,version,allowList);
|
||||||
|
var deniedRule = checkAgainstList(module,version,denyList);
|
||||||
|
// console.log("A",allowedRule)
|
||||||
|
// console.log("D",deniedRule)
|
||||||
|
|
||||||
|
if (allowedRule && !deniedRule) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!allowedRule && deniedRule) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!allowedRule && !deniedRule) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
|
||||||
|
return allowedRule.wildcardPos > 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 {
|
return {
|
||||||
createObjectElement: buildMessageElement,
|
createObjectElement: buildMessageElement,
|
||||||
getMessageProperty: getMessageProperty,
|
getMessageProperty: getMessageProperty,
|
||||||
@ -1190,6 +1251,8 @@ RED.utils = (function() {
|
|||||||
sanitize: sanitize,
|
sanitize: sanitize,
|
||||||
renderMarkdown: renderMarkdown,
|
renderMarkdown: renderMarkdown,
|
||||||
createNodeIcon: createNodeIcon,
|
createNodeIcon: createNodeIcon,
|
||||||
getDarkerColor: getDarkerColor
|
getDarkerColor: getDarkerColor,
|
||||||
|
parseModuleList: parseModuleList,
|
||||||
|
checkModuleAllowed: checkModuleAllowed
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
@ -146,6 +146,13 @@ body {
|
|||||||
background-size: contain
|
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 {
|
code {
|
||||||
font-family: $monospace-font;
|
font-family: $monospace-font;
|
||||||
font-size: $primary-font-size;
|
font-size: $primary-font-size;
|
||||||
|
@ -174,8 +174,8 @@ button.red-ui-tray-resize-button {
|
|||||||
|
|
||||||
.red-ui-editor .red-ui-tray {
|
.red-ui-editor .red-ui-tray {
|
||||||
.dialog-form, #dialog-form, #node-config-dialog-edit-form {
|
.dialog-form, #dialog-form, #node-config-dialog-edit-form {
|
||||||
margin: 20px;
|
margin: 10px 20px;
|
||||||
height: calc(100% - 40px);
|
height: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,60 +1,318 @@
|
|||||||
|
|
||||||
<script type="text/html" data-template-name="function">
|
<script type="text/html" data-template-name="function">
|
||||||
|
<style>
|
||||||
|
.func-tabs-row {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
#node-input-libs-container-row .red-ui-editableList-container {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
#node-input-libs-container-row .red-ui-editableList-container li {
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
#node-input-libs-container-row .red-ui-editableList-item-remove {
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
.node-libs-entry {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.node-libs-entry .node-input-libs-var, .node-libs-entry .red-ui-typedInput-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.node-libs-entry > code,.node-libs-entry > span {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.node-libs-entry > input[type=text] {
|
||||||
|
border-radius: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-top: none;
|
||||||
|
border-right: none;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-libs-entry > span > i {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.node-libs-entry > span.input-error > i {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<input type="hidden" id="node-input-func">
|
||||||
|
<input type="hidden" id="node-input-noerr">
|
||||||
|
<input type="hidden" id="node-input-finalize">
|
||||||
|
<input type="hidden" id="node-input-initialize">
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||||
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
|
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
|
<div class="form-row func-tabs-row">
|
||||||
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
|
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
|
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
|
||||||
|
|
||||||
<div id="func-tab-init" style="display:none">
|
<div id="func-tab-config" style="display:none">
|
||||||
<div class="form-row" style="margin-bottom: 0px;">
|
<div class="form-row">
|
||||||
<input type="hidden" id="node-input-initialize" autofocus="autofocus">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row node-text-editor-row" style="position:relative">
|
|
||||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
|
||||||
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-init-editor" ></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="func-tab-body" style="display:none">
|
|
||||||
<div class="form-row" style="margin-bottom: 0px;">
|
|
||||||
<input type="hidden" id="node-input-func" autofocus="autofocus">
|
|
||||||
<input type="hidden" id="node-input-noerr">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row node-text-editor-row" style="position:relative">
|
|
||||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
|
||||||
<div style="height: 220px; min-height:120px; margin-top: 30px;" class="node-text-editor" id="node-input-func-editor" ></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row" style="margin-bottom: 0px">
|
|
||||||
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
||||||
<input id="node-input-outputs" style="width: 60px;" value="1">
|
<input id="node-input-outputs" style="width: 60px;" value="1">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
|
||||||
|
<label><i class="fa fa-cubes"></i> <span>Modules</span></label>
|
||||||
|
</div>
|
||||||
|
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
|
||||||
|
<ol id="node-input-libs-container"></ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="func-tab-init" style="display:none">
|
||||||
|
<div class="form-row node-text-editor-row" style="position:relative">
|
||||||
|
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-init-editor" ></div>
|
||||||
|
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="func-tab-body" style="display:none">
|
||||||
|
<div class="form-row node-text-editor-row" style="position:relative">
|
||||||
|
<div style="height: 220px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
|
||||||
|
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="func-tab-finalize" style="display:none">
|
<div id="func-tab-finalize" style="display:none">
|
||||||
<div class="form-row" style="margin-bottom: 0px;">
|
|
||||||
<input type="hidden" id="node-input-finalize" autofocus="autofocus">
|
|
||||||
</div>
|
|
||||||
<div class="form-row node-text-editor-row" style="position:relative">
|
<div class="form-row node-text-editor-row" style="position:relative">
|
||||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
|
||||||
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
|
<div style="position: absolute; right:0; bottom: calc(100% - 20px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
var invalidModuleVNames = [
|
||||||
|
'console',
|
||||||
|
'util',
|
||||||
|
'Buffer',
|
||||||
|
'Date',
|
||||||
|
'RED',
|
||||||
|
'node',
|
||||||
|
'__node__',
|
||||||
|
'context',
|
||||||
|
'flow',
|
||||||
|
'global',
|
||||||
|
'env',
|
||||||
|
'setTimeout',
|
||||||
|
'clearTimeout',
|
||||||
|
'setInterval',
|
||||||
|
'clearInterval',
|
||||||
|
'promisify'
|
||||||
|
]
|
||||||
|
|
||||||
|
var knownFunctionNodes = {};
|
||||||
|
RED.events.on("nodes:add", function(n) {
|
||||||
|
if (n.type === "function") {
|
||||||
|
knownFunctionNodes[n.id] = n;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
RED.events.on("nodes:remove", function(n) {
|
||||||
|
if (n.type === "function") {
|
||||||
|
delete knownFunctionNodes[n.id];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var missingModules = [];
|
||||||
|
var missingModuleReasons = {};
|
||||||
|
RED.events.on("runtime-state", function(event) {
|
||||||
|
if (event.error === "missing-modules") {
|
||||||
|
missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module });
|
||||||
|
for (var id in knownFunctionNodes) {
|
||||||
|
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
|
||||||
|
RED.editor.validateNode(knownFunctionNodes[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!event.text) {
|
||||||
|
missingModuleReasons = {};
|
||||||
|
missingModules = [];
|
||||||
|
for (var id in knownFunctionNodes) {
|
||||||
|
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
|
||||||
|
RED.editor.validateNode(knownFunctionNodes[id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RED.view.redraw();
|
||||||
|
});
|
||||||
|
|
||||||
|
var installAllowList = ['*'];
|
||||||
|
var installDenyList = [];
|
||||||
|
|
||||||
|
var modulesEnabled = true;
|
||||||
|
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
|
||||||
|
modulesEnabled = false;
|
||||||
|
}
|
||||||
|
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
|
||||||
|
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
|
||||||
|
if (settingsAllowList || settingsDenyList) {
|
||||||
|
installAllowList = settingsAllowList;
|
||||||
|
installDenyList = settingsDenyList
|
||||||
|
}
|
||||||
|
installAllowList = RED.utils.parseModuleList(installAllowList);
|
||||||
|
installDenyList = RED.utils.parseModuleList(installDenyList);
|
||||||
|
|
||||||
|
|
||||||
|
// object that maps from library name to its descriptor
|
||||||
|
var allLibs = [];
|
||||||
|
|
||||||
|
function moduleName(module) {
|
||||||
|
var match = /^([^@]+)@(.+)/.exec(module);
|
||||||
|
if (match) {
|
||||||
|
return [match[1], match[2]];
|
||||||
|
}
|
||||||
|
return [module, undefined];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllUsedModules() {
|
||||||
|
var moduleSet = new Set();
|
||||||
|
for (var id in knownFunctionNodes) {
|
||||||
|
if (knownFunctionNodes.hasOwnProperty(id)) {
|
||||||
|
if (knownFunctionNodes[id].libs) {
|
||||||
|
for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) {
|
||||||
|
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) {
|
||||||
|
moduleSet.add(knownFunctionNodes[id].libs[i].module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var modules = Array.from(moduleSet);
|
||||||
|
modules.sort();
|
||||||
|
return modules;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareLibraryConfig(node) {
|
||||||
|
$(".node-input-libs-row").show();
|
||||||
|
var usedModules = getAllUsedModules();
|
||||||
|
var typedModules = usedModules.map(function(l) {
|
||||||
|
return {icon:"fa fa-cube", value:l,label:l,hasValue:false}
|
||||||
|
})
|
||||||
|
typedModules.push({
|
||||||
|
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
|
||||||
|
})
|
||||||
|
|
||||||
|
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
|
||||||
|
addItem: function(container,i,opt) {
|
||||||
|
var parent = container.parent();
|
||||||
|
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
|
||||||
|
var fieldWidth = "260px";
|
||||||
|
$('<code>const </code>').appendTo(row0);
|
||||||
|
var fvar = $("<input/>", {
|
||||||
|
class: "node-input-libs-var red-ui-font-code",
|
||||||
|
placeholder: RED._("node-red:function.require.var"),
|
||||||
|
type: "text"
|
||||||
|
}).css({
|
||||||
|
width: "120px",
|
||||||
|
"margin-left": "5px"
|
||||||
|
}).appendTo(row0).val(opt.var);
|
||||||
|
var vnameWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
|
||||||
|
RED.popover.tooltip(vnameWarning.find("i"),function() {
|
||||||
|
var val = fvar.val();
|
||||||
|
if (invalidModuleVNames.indexOf(val) !== -1) {
|
||||||
|
return RED._("node-red:function.error.moduleNameReserved",{name:val})
|
||||||
|
} else {
|
||||||
|
return RED._("node-red:function.error.moduleNameError",{name:val})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$('<code> = require(</code>').appendTo(row0);
|
||||||
|
var fmodule = $("<input/>", {
|
||||||
|
class: "node-input-libs-val",
|
||||||
|
placeholder: RED._("node-red:function.require.module"),
|
||||||
|
type: "text"
|
||||||
|
}).css({
|
||||||
|
width: "180px",
|
||||||
|
}).appendTo(row0).typedInput({
|
||||||
|
types: typedModules,
|
||||||
|
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
|
||||||
|
});
|
||||||
|
if (usedModules.indexOf(opt.module) === -1) {
|
||||||
|
fmodule.typedInput('value', opt.module);
|
||||||
|
}
|
||||||
|
|
||||||
|
$('<code>)</code>').appendTo(row0);
|
||||||
|
|
||||||
|
var moduleWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
|
||||||
|
RED.popover.tooltip(moduleWarning.find("i"),function() {
|
||||||
|
var val = fmodule.typedInput("type");
|
||||||
|
if (val === "_custom_") {
|
||||||
|
val = fmodule.val();
|
||||||
|
}
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
|
||||||
|
return RED._("node-red:function.error.moduleNotAllowed",{module:val});
|
||||||
|
} else {
|
||||||
|
return RED._("node-red:function.error.moduleLoadError",{module:val,error:missingModuleReasons[val]});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fvar.on("change keyup paste", function (e) {
|
||||||
|
var v = $(this).val().trim();
|
||||||
|
if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) {
|
||||||
|
fvar.addClass("input-error");
|
||||||
|
vnameWarning.addClass("input-error");
|
||||||
|
} else {
|
||||||
|
fvar.removeClass("input-error");
|
||||||
|
vnameWarning.removeClass("input-error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fmodule.on("change keyup paste", function (e) {
|
||||||
|
var val = $(this).typedInput("type");
|
||||||
|
if (val === "_custom_") {
|
||||||
|
val = $(this).val();
|
||||||
|
}
|
||||||
|
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() });
|
||||||
|
fvar.val(varName);
|
||||||
|
fvar.trigger("change");
|
||||||
|
|
||||||
|
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
|
||||||
|
fmodule.removeClass("input-error");
|
||||||
|
moduleWarning.removeClass("input-error");
|
||||||
|
} else {
|
||||||
|
fmodule.addClass("input-error");
|
||||||
|
moduleWarning.addClass("input-error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
|
||||||
|
fmodule.removeClass("input-error");
|
||||||
|
moduleWarning.removeClass("input-error");
|
||||||
|
} else {
|
||||||
|
fmodule.addClass("input-error");
|
||||||
|
moduleWarning.addClass("input-error");
|
||||||
|
}
|
||||||
|
if (opt.var) {
|
||||||
|
fvar.trigger("change");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
var libs = node.libs || [];
|
||||||
|
for (var i=0,l=libs.length;i<l; i++) {
|
||||||
|
libList.editableList('addItem',libs[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
RED.nodes.registerType('function',{
|
RED.nodes.registerType('function',{
|
||||||
color:"#fdd0a2",
|
color:"#fdd0a2",
|
||||||
category: 'function',
|
category: 'function',
|
||||||
@ -64,7 +322,26 @@
|
|||||||
outputs: {value:1},
|
outputs: {value:1},
|
||||||
noerr: {value:0,required:true,validate:function(v) { return !v; }},
|
noerr: {value:0,required:true,validate:function(v) { return !v; }},
|
||||||
initialize: {value:""},
|
initialize: {value:""},
|
||||||
finalize: {value:""}
|
finalize: {value:""},
|
||||||
|
libs: {value: [], validate: function(v) {
|
||||||
|
if (!v) { return true; }
|
||||||
|
for (var i=0,l=v.length;i<l;i++) {
|
||||||
|
var m = v[i];
|
||||||
|
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (m.var === "" || / /.test(m.var)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (missingModules.indexOf(m.module) > -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (invalidModuleVNames.indexOf(m.var) !== -1){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
},
|
},
|
||||||
inputs:1,
|
inputs:1,
|
||||||
outputs:1,
|
outputs:1,
|
||||||
@ -85,6 +362,12 @@
|
|||||||
$("#" + tab.id).show();
|
$("#" + tab.id).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
tabs.addTab({
|
||||||
|
id: "func-tab-config",
|
||||||
|
iconClass: "fa fa-cog",
|
||||||
|
label: that._("function.label.setup")
|
||||||
|
});
|
||||||
|
|
||||||
tabs.addTab({
|
tabs.addTab({
|
||||||
id: "func-tab-init",
|
id: "func-tab-init",
|
||||||
label: that._("function.label.initialize")
|
label: that._("function.label.initialize")
|
||||||
@ -97,6 +380,7 @@
|
|||||||
id: "func-tab-finalize",
|
id: "func-tab-finalize",
|
||||||
label: that._("function.label.finalize")
|
label: that._("function.label.finalize")
|
||||||
});
|
});
|
||||||
|
|
||||||
tabs.activateTab("func-tab-body");
|
tabs.activateTab("func-tab-body");
|
||||||
|
|
||||||
$( "#node-input-outputs" ).spinner({
|
$( "#node-input-outputs" ).spinner({
|
||||||
@ -205,7 +489,9 @@
|
|||||||
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
|
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
|
||||||
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
|
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
|
||||||
|
|
||||||
|
if (RED.settings.functionExternalModules !== false) {
|
||||||
|
prepareLibraryConfig(that);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
oneditsave: function() {
|
oneditsave: function() {
|
||||||
var node = this;
|
var node = this;
|
||||||
@ -237,7 +523,28 @@
|
|||||||
|
|
||||||
$("#node-input-noerr").val(noerr);
|
$("#node-input-noerr").val(noerr);
|
||||||
this.noerr = noerr;
|
this.noerr = noerr;
|
||||||
|
if (RED.settings.functionExternalModules === true) {
|
||||||
|
var libs = $("#node-input-libs-container").editableList("items");
|
||||||
|
node.libs = [];
|
||||||
|
libs.each(function(i) {
|
||||||
|
var item = $(this);
|
||||||
|
var v = item.find(".node-input-libs-var").val();
|
||||||
|
var n = item.find(".node-input-libs-val").typedInput("type");
|
||||||
|
if (n === "_custom_") {
|
||||||
|
n = item.find(".node-input-libs-val").val();
|
||||||
|
}
|
||||||
|
if ((!v || (v === "")) ||
|
||||||
|
(!n || (n === ""))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node.libs.push({
|
||||||
|
var: v,
|
||||||
|
module: n
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
node.libs = [];
|
||||||
|
}
|
||||||
},
|
},
|
||||||
oneditcancel: function() {
|
oneditcancel: function() {
|
||||||
var node = this;
|
var node = this;
|
||||||
@ -259,18 +566,19 @@
|
|||||||
}
|
}
|
||||||
var editorRow = $("#dialog-form>div.node-text-editor-row");
|
var editorRow = $("#dialog-form>div.node-text-editor-row");
|
||||||
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
|
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
|
||||||
$(".node-text-editor").css("height",height+"px");
|
$("#dialog-form .node-text-editor").css("height",height+"px");
|
||||||
this.editor.resize();
|
|
||||||
|
|
||||||
var height = size.height;
|
var height = size.height;
|
||||||
$("#node-input-init-editor").css("height", (height -105)+"px");
|
$("#node-input-init-editor").css("height", (height -45-48)+"px");
|
||||||
$("#node-input-func-editor").css("height", (height -145)+"px");
|
$("#node-input-func-editor").css("height", (height -45-48)+"px");
|
||||||
$("#node-input-finalize-editor").css("height", (height -105)+"px");
|
$("#node-input-finalize-editor").css("height", (height -45-48)+"px");
|
||||||
|
|
||||||
this.initEditor.resize();
|
this.initEditor.resize();
|
||||||
this.editor.resize();
|
this.editor.resize();
|
||||||
this.finalizeEditor.resize();
|
this.finalizeEditor.resize();
|
||||||
|
|
||||||
|
$("#node-input-libs-container").css("height", (height - 185)+"px");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
module.exports = function(RED) {
|
module.exports = function(RED) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var util = require("util");
|
var util = require("util");
|
||||||
var vm = require("vm");
|
var vm = require("vm");
|
||||||
|
|
||||||
@ -94,6 +95,11 @@ module.exports = function(RED) {
|
|||||||
node.func = n.func;
|
node.func = n.func;
|
||||||
node.ini = n.initialize ? n.initialize.trim() : "";
|
node.ini = n.initialize ? n.initialize.trim() : "";
|
||||||
node.fin = n.finalize ? n.finalize.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;
|
var handleNodeDoneCall = true;
|
||||||
|
|
||||||
@ -266,6 +272,54 @@ module.exports = function(RED) {
|
|||||||
};
|
};
|
||||||
sandbox.promisify = util.promisify;
|
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);
|
var context = vm.createContext(sandbox);
|
||||||
try {
|
try {
|
||||||
var iniScript = null;
|
var iniScript = null;
|
||||||
@ -293,7 +347,21 @@ module.exports = function(RED) {
|
|||||||
}
|
}
|
||||||
node.script = vm.createScript(functionText, createVMOpt(node, ""));
|
node.script = vm.createScript(functionText, createVMOpt(node, ""));
|
||||||
if (node.fin && (node.fin !== "")) {
|
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");
|
finOpt = createVMOpt(node, " cleanup");
|
||||||
finScript = new vm.Script(finText, finOpt);
|
finScript = new vm.Script(finText, finOpt);
|
||||||
}
|
}
|
||||||
@ -303,7 +371,7 @@ module.exports = function(RED) {
|
|||||||
promise = iniScript.runInContext(context, iniOpt);
|
promise = iniScript.runInContext(context, iniOpt);
|
||||||
}
|
}
|
||||||
|
|
||||||
function processMessage(msg, send, done) {
|
processMessage = function (msg, send, done) {
|
||||||
var start = process.hrtime();
|
var start = process.hrtime();
|
||||||
context.msg = msg;
|
context.msg = msg;
|
||||||
context.__send__ = send;
|
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() {
|
node.on("close", function() {
|
||||||
if (finScript) {
|
if (finScript) {
|
||||||
try {
|
try {
|
||||||
@ -422,7 +476,12 @@ module.exports = function(RED) {
|
|||||||
node.error(err);
|
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");
|
RED.library.register("functions");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -209,16 +209,25 @@
|
|||||||
"function": {
|
"function": {
|
||||||
"function": "",
|
"function": "",
|
||||||
"label": {
|
"label": {
|
||||||
"function": "Function",
|
"setup": "Setup",
|
||||||
"initialize": "Setup",
|
"function": "On Message",
|
||||||
"finalize": "Close",
|
"initialize": "On Start",
|
||||||
|
"finalize": "On Stop",
|
||||||
"outputs": "Outputs"
|
"outputs": "Outputs"
|
||||||
},
|
},
|
||||||
"text": {
|
"text": {
|
||||||
"initialize": "// Code added here will be run once\n// whenever the node is deployed.\n",
|
"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"
|
"finalize": "// Code added here will be run when the\n// node is being stopped or re-deployed.\n"
|
||||||
},
|
},
|
||||||
|
"require": {
|
||||||
|
"var": "variable",
|
||||||
|
"module": "module"
|
||||||
|
},
|
||||||
"error": {
|
"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",
|
"inputListener":"Cannot add listener to 'input' event within Function",
|
||||||
"non-message-returned":"Function tried to send a message of type __type__"
|
"non-message-returned":"Function tried to send a message of type __type__"
|
||||||
}
|
}
|
||||||
|
223
packages/node_modules/@node-red/registry/lib/externalModules.js
vendored
Normal file
223
packages/node_modules/@node-red/registry/lib/externalModules.js
vendored
Normal file
@ -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
|
||||||
|
}
|
@ -28,6 +28,7 @@ var registry = require("./registry");
|
|||||||
var loader = require("./loader");
|
var loader = require("./loader");
|
||||||
var installer = require("./installer");
|
var installer = require("./installer");
|
||||||
var library = require("./library");
|
var library = require("./library");
|
||||||
|
const externalModules = require("./externalModules")
|
||||||
var plugins = require("./plugins");
|
var plugins = require("./plugins");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,6 +45,7 @@ function init(runtime) {
|
|||||||
plugins.init(runtime.settings);
|
plugins.init(runtime.settings);
|
||||||
registry.init(runtime.settings,loader);
|
registry.init(runtime.settings,loader);
|
||||||
library.init();
|
library.init();
|
||||||
|
externalModules.init(runtime.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -299,6 +301,8 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
getNodeExampleFlowPath: library.getExampleFlowPath,
|
getNodeExampleFlowPath: library.getExampleFlowPath,
|
||||||
|
|
||||||
|
checkFlowDependencies: externalModules.checkFlowDependencies,
|
||||||
|
|
||||||
registerPlugin: plugins.registerPlugin,
|
registerPlugin: plugins.registerPlugin,
|
||||||
getPlugin: plugins.getPlugin,
|
getPlugin: plugins.getPlugin,
|
||||||
getPluginsByType: plugins.getPluginsByType,
|
getPluginsByType: plugins.getPluginsByType,
|
||||||
|
@ -21,6 +21,7 @@ var fs = require("fs");
|
|||||||
var library = require("./library");
|
var library = require("./library");
|
||||||
const {events} = require("@node-red/util")
|
const {events} = require("@node-red/util")
|
||||||
var subflows = require("./subflow");
|
var subflows = require("./subflow");
|
||||||
|
var externalModules = require("./externalModules")
|
||||||
var settings;
|
var settings;
|
||||||
var loader;
|
var loader;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ var nodeConfigCache = {};
|
|||||||
var moduleConfigs = {};
|
var moduleConfigs = {};
|
||||||
var nodeList = [];
|
var nodeList = [];
|
||||||
var nodeConstructors = {};
|
var nodeConstructors = {};
|
||||||
|
var nodeOptions = {};
|
||||||
var subflowModules = {};
|
var subflowModules = {};
|
||||||
|
|
||||||
var nodeTypeToId = {};
|
var nodeTypeToId = {};
|
||||||
@ -36,12 +38,7 @@ var moduleNodes = {};
|
|||||||
function init(_settings,_loader) {
|
function init(_settings,_loader) {
|
||||||
settings = _settings;
|
settings = _settings;
|
||||||
loader = _loader;
|
loader = _loader;
|
||||||
moduleNodes = {};
|
clear();
|
||||||
nodeTypeToId = {};
|
|
||||||
nodeConstructors = {};
|
|
||||||
subflowModules = {};
|
|
||||||
nodeList = [];
|
|
||||||
nodeConfigCache = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
@ -241,6 +238,7 @@ function removeNode(id) {
|
|||||||
if (typeId === id) {
|
if (typeId === id) {
|
||||||
delete subflowModules[t];
|
delete subflowModules[t];
|
||||||
delete nodeConstructors[t];
|
delete nodeConstructors[t];
|
||||||
|
delete nodeOptions[t];
|
||||||
delete nodeTypeToId[t];
|
delete nodeTypeToId[t];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -412,7 +410,7 @@ function getCaller(){
|
|||||||
return stack[0].getFileName();
|
return stack[0].getFileName();
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerNodeConstructor(nodeSet,type,constructor) {
|
function registerNodeConstructor(nodeSet,type,constructor,options) {
|
||||||
if (nodeConstructors.hasOwnProperty(type)) {
|
if (nodeConstructors.hasOwnProperty(type)) {
|
||||||
throw new Error(type+" already registered");
|
throw new Error(type+" already registered");
|
||||||
}
|
}
|
||||||
@ -432,6 +430,12 @@ function registerNodeConstructor(nodeSet,type,constructor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nodeConstructors[type] = constructor;
|
nodeConstructors[type] = constructor;
|
||||||
|
nodeOptions[type] = options;
|
||||||
|
if (options) {
|
||||||
|
if (options.dynamicModuleList) {
|
||||||
|
externalModules.register(type,options.dynamicModuleList);
|
||||||
|
}
|
||||||
|
}
|
||||||
events.emit("type-registered",type);
|
events.emit("type-registered",type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,6 +456,9 @@ function registerSubflow(nodeSet, subflow) {
|
|||||||
nodeSetInfo.config = result.config;
|
nodeSetInfo.config = result.config;
|
||||||
}
|
}
|
||||||
subflowModules[result.type] = result;
|
subflowModules[result.type] = result;
|
||||||
|
externalModules.registerSubflow(result.type,subflow);
|
||||||
|
|
||||||
|
|
||||||
events.emit("type-registered",result.type);
|
events.emit("type-registered",result.type);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@ -524,6 +531,7 @@ function clear() {
|
|||||||
moduleConfigs = {};
|
moduleConfigs = {};
|
||||||
nodeList = [];
|
nodeList = [];
|
||||||
nodeConstructors = {};
|
nodeConstructors = {};
|
||||||
|
nodeOptions = {};
|
||||||
subflowModules = {};
|
subflowModules = {};
|
||||||
nodeTypeToId = {};
|
nodeTypeToId = {};
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const semver = require("semver");
|
const semver = require("semver");
|
||||||
const {events,i18n,log} = require("@node-red/util");
|
const {events,i18n,log} = require("@node-red/util");
|
||||||
|
|
||||||
var runtime;
|
var runtime;
|
||||||
|
|
||||||
function copyObjectProperties(src,dst,copyList,blockList) {
|
function copyObjectProperties(src,dst,copyList,blockList) {
|
||||||
@ -45,9 +46,8 @@ function requireModule(name) {
|
|||||||
var relPath = path.relative(__dirname, moduleInfo.path);
|
var relPath = path.relative(__dirname, moduleInfo.path);
|
||||||
return require(relPath);
|
return require(relPath);
|
||||||
} else {
|
} else {
|
||||||
var err = new Error(`Cannot find module '${name}'`);
|
// Require it here to avoid the circular dependency
|
||||||
err.code = "MODULE_NOT_FOUND";
|
return require("./externalModules").require(name);
|
||||||
throw err;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ function createNodeApi(node) {
|
|||||||
httpAdmin: runtime.adminApp,
|
httpAdmin: runtime.adminApp,
|
||||||
server: runtime.server
|
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) {
|
red.nodes.registerType = function(type,constructor,opts) {
|
||||||
runtime.nodes.registerType(node.id,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) {
|
function checkModuleAllowed(module,version,allowList,denyList) {
|
||||||
// console.log("checkModuleAllowed",module,version);//,allowList,denyList)
|
|
||||||
if (!allowList && !denyList) {
|
if (!allowList && !denyList) {
|
||||||
// Default to allow
|
// Default to allow
|
||||||
return true;
|
return true;
|
||||||
|
@ -187,8 +187,8 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return configSavePromise
|
|
||||||
.then(function(flowRevision) {
|
return configSavePromise.then(flowRevision => {
|
||||||
if (!isLoad) {
|
if (!isLoad) {
|
||||||
log.debug("saved flow revision: "+flowRevision);
|
log.debug("saved flow revision: "+flowRevision);
|
||||||
}
|
}
|
||||||
@ -246,7 +246,7 @@ function getFlows() {
|
|||||||
return activeConfig;
|
return activeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
function start(type,diff,muteLog) {
|
async function start(type,diff,muteLog) {
|
||||||
type = type||"full";
|
type = type||"full";
|
||||||
started = true;
|
started = true;
|
||||||
var i;
|
var i;
|
||||||
@ -271,7 +271,21 @@ function start(type,diff,muteLog) {
|
|||||||
log.info(" "+settings.userDir);
|
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});
|
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<err.length;i++) {
|
||||||
|
let errMessage = err[i].error.toString()
|
||||||
|
missingModules.push({module:err[i].module.module, error: err[i].error.code || err[i].error.toString()})
|
||||||
|
log.info(` - ${err[i].module.spec} [${err[i].error.code || "unknown_error"}]`);
|
||||||
|
}
|
||||||
|
events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In safe mode, don't actually start anything, emit the necessary runtime event and return
|
// In safe mode, don't actually start anything, emit the necessary runtime event and return
|
||||||
@ -280,7 +294,7 @@ function start(type,diff,muteLog) {
|
|||||||
log.info(log._("nodes.flows.safe-mode"));
|
log.info(log._("nodes.flows.safe-mode"));
|
||||||
log.info("*****************************************************************")
|
log.info("*****************************************************************")
|
||||||
events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true});
|
events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true});
|
||||||
return Promise.resolve();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!muteLog) {
|
if (!muteLog) {
|
||||||
@ -370,7 +384,7 @@ function start(type,diff,muteLog) {
|
|||||||
log.info(log._("nodes.flows.started-flows"));
|
log.info(log._("nodes.flows.started-flows"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stop(type,diff,muteLog) {
|
function stop(type,diff,muteLog) {
|
||||||
|
@ -271,6 +271,7 @@ function stop() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// This is the internal api
|
// This is the internal api
|
||||||
var runtime = {
|
var runtime = {
|
||||||
version: getVersion,
|
version: getVersion,
|
||||||
|
@ -81,7 +81,7 @@ function registerType(nodeSet,type,constructor,opts) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
registry.registerType(nodeSet,type,constructor);
|
registry.registerType(nodeSet,type,constructor,opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,5 +261,5 @@ module.exports = {
|
|||||||
// Contexts
|
// Contexts
|
||||||
loadContextsPlugin: context.load,
|
loadContextsPlugin: context.load,
|
||||||
closeContextsPlugin: context.close,
|
closeContextsPlugin: context.close,
|
||||||
listContextStores: context.listStores
|
listContextStores: context.listStores,
|
||||||
};
|
};
|
||||||
|
4
packages/node_modules/node-red/settings.js
vendored
4
packages/node_modules/node-red/settings.js
vendored
@ -247,6 +247,10 @@ module.exports = {
|
|||||||
// jfive:require("johnny-five"),
|
// jfive:require("johnny-five"),
|
||||||
// j5board:require("johnny-five").Board({repl:false})
|
// j5board:require("johnny-five").Board({repl:false})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Allow the Function node to load additional npm modules
|
||||||
|
functionExternalModules: false,
|
||||||
|
|
||||||
// `global.keys()` returns a list of all properties set in global context.
|
// `global.keys()` returns a list of all properties set in global context.
|
||||||
// This allows them to be displayed in the Context Sidebar within the editor.
|
// This allows them to be displayed in the Context Sidebar within the editor.
|
||||||
// In some circumstances it is not desirable to expose them to the editor. The
|
// In some circumstances it is not desirable to expose them to the editor. The
|
||||||
|
@ -93,9 +93,6 @@ describe('function node', function() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
it('should be loaded', function(done) {
|
it('should be loaded', function(done) {
|
||||||
var flow = [{id:"n1", type:"function", name: "function" }];
|
var flow = [{id:"n1", type:"function", name: "function" }];
|
||||||
helper.load(functionNode, flow, function() {
|
helper.load(functionNode, flow, function() {
|
||||||
@ -1417,6 +1414,86 @@ describe('function node', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('externalModules', function() {
|
||||||
|
|
||||||
|
it('should fail if using OS module without functionExternalModules set to true', 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.load(functionNode, flow, function() {
|
||||||
|
var n1 = helper.getNode("n1");
|
||||||
|
should.not.exist(n1);
|
||||||
|
done();
|
||||||
|
}).catch(err => 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 () {
|
describe('Logger', function () {
|
||||||
|
|
||||||
function testLog(initCode,funcCode,expectedLevel, done) {
|
function testLog(initCode,funcCode,expectedLevel, done) {
|
||||||
@ -1603,5 +1680,4 @@ describe('function node', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
*/
|
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "test-subflow-mod",
|
"name": "test-subflow-mod",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -13,6 +13,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-red-node-random": "*"
|
"node-red-node-random": "*",
|
||||||
|
"cowsay2": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,6 +189,7 @@
|
|||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
"finalize": "",
|
"finalize": "",
|
||||||
|
"libs": [ {"var":"cowsay2","module":"cowsay2"}],
|
||||||
"x": 240,
|
"x": 240,
|
||||||
"y": 100,
|
"y": 100,
|
||||||
"wires": [
|
"wires": [
|
||||||
|
Binary file not shown.
BIN
test/resources/subflow/test-subflow-mod-1.0.2.tgz
Normal file
BIN
test/resources/subflow/test-subflow-mod-1.0.2.tgz
Normal file
Binary file not shown.
302
test/unit/@node-red/registry/lib/externalModules_spec.js
Normal file
302
test/unit/@node-red/registry/lib/externalModules_spec.js
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
@ -40,7 +40,7 @@ describe('red/registry/index', function() {
|
|||||||
stubs.push(sinon.stub(loader,"init"));
|
stubs.push(sinon.stub(loader,"init"));
|
||||||
stubs.push(sinon.stub(typeRegistry,"init"));
|
stubs.push(sinon.stub(typeRegistry,"init"));
|
||||||
|
|
||||||
registry.init({});
|
registry.init({settings:{}});
|
||||||
installer.init.called.should.be.true();
|
installer.init.called.should.be.true();
|
||||||
loader.init.called.should.be.true();
|
loader.init.called.should.be.true();
|
||||||
typeRegistry.init.called.should.be.true();
|
typeRegistry.init.called.should.be.true();
|
||||||
|
@ -36,6 +36,7 @@ describe('flows/index', function() {
|
|||||||
|
|
||||||
var flowCreate;
|
var flowCreate;
|
||||||
var getType;
|
var getType;
|
||||||
|
var checkFlowDependencies;
|
||||||
|
|
||||||
var mockLog = {
|
var mockLog = {
|
||||||
log: sinon.stub(),
|
log: sinon.stub(),
|
||||||
@ -52,9 +53,16 @@ describe('flows/index', function() {
|
|||||||
getType = sinon.stub(typeRegistry,"get",function(type) {
|
getType = sinon.stub(typeRegistry,"get",function(type) {
|
||||||
return type.indexOf('missing') === -1;
|
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() {
|
after(function() {
|
||||||
getType.restore();
|
getType.restore();
|
||||||
|
checkFlowDependencies.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -306,7 +314,7 @@ describe('flows/index', function() {
|
|||||||
|
|
||||||
flows.init({log:mockLog, settings:{},storage:storage});
|
flows.init({log:mockLog, settings:{},storage:storage});
|
||||||
flows.load().then(function() {
|
flows.load().then(function() {
|
||||||
flows.startFlows();
|
return flows.startFlows();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('does not start if nodes missing', function(done) {
|
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.init({log:mockLog, settings:{},storage:storage});
|
||||||
flows.load().then(function() {
|
flows.load().then(function() {
|
||||||
flows.startFlows();
|
return flows.startFlows();
|
||||||
|
}).then(() => {
|
||||||
|
try {
|
||||||
flowCreate.called.should.be.false();
|
flowCreate.called.should.be.false();
|
||||||
done();
|
done();
|
||||||
|
} catch(err) {
|
||||||
|
done(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -339,9 +352,9 @@ describe('flows/index', function() {
|
|||||||
}
|
}
|
||||||
flows.init({log:mockLog, settings:{},storage:storage});
|
flows.init({log:mockLog, settings:{},storage:storage});
|
||||||
flows.load().then(function() {
|
flows.load().then(function() {
|
||||||
flows.startFlows();
|
return flows.startFlows();
|
||||||
|
}).then(() => {
|
||||||
flowCreate.called.should.be.false();
|
flowCreate.called.should.be.false();
|
||||||
|
|
||||||
events.emit("type-registered","missing");
|
events.emit("type-registered","missing");
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
flowCreate.called.should.be.false();
|
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)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user