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",
|
||||
"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",
|
||||
|
@ -143,6 +143,7 @@
|
||||
"nodeActionDisabled": "node actions disabled",
|
||||
"nodeActionDisabledSubflow": "node actions disabled within subflow",
|
||||
"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>",
|
||||
"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>",
|
||||
|
@ -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+="<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") {
|
||||
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) {
|
||||
|
@ -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( "<div>" ).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 = $("<a/>",{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(); })
|
||||
|
@ -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();
|
||||
|
@ -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<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() {
|
||||
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();
|
||||
|
||||
|
@ -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<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 {
|
||||
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
|
||||
}
|
||||
})();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,60 +1,318 @@
|
||||
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="form-row">
|
||||
|
||||
<div class="form-row func-tabs-row">
|
||||
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
|
||||
</div>
|
||||
|
||||
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
|
||||
|
||||
<div id="func-tab-init" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<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">
|
||||
<div id="func-tab-config" style="display:none">
|
||||
<div class="form-row">
|
||||
<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">
|
||||
</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 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 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; margin-top: 30px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
|
||||
<div style="height: 250px; min-height:150px;" 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>
|
||||
|
||||
</script>
|
||||
|
||||
<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',{
|
||||
color:"#fdd0a2",
|
||||
category: 'function',
|
||||
@ -64,7 +322,26 @@
|
||||
outputs: {value:1},
|
||||
noerr: {value:0,required:true,validate:function(v) { return !v; }},
|
||||
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,
|
||||
outputs:1,
|
||||
@ -85,6 +362,12 @@
|
||||
$("#" + tab.id).show();
|
||||
}
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-config",
|
||||
iconClass: "fa fa-cog",
|
||||
label: that._("function.label.setup")
|
||||
});
|
||||
|
||||
tabs.addTab({
|
||||
id: "func-tab-init",
|
||||
label: that._("function.label.initialize")
|
||||
@ -97,6 +380,7 @@
|
||||
id: "func-tab-finalize",
|
||||
label: that._("function.label.finalize")
|
||||
});
|
||||
|
||||
tabs.activateTab("func-tab-body");
|
||||
|
||||
$( "#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-finalize-expand-js"), RED._("node-red:common.label.expand"));
|
||||
|
||||
|
||||
if (RED.settings.functionExternalModules !== false) {
|
||||
prepareLibraryConfig(that);
|
||||
}
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
@ -237,7 +523,28 @@
|
||||
|
||||
$("#node-input-noerr").val(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() {
|
||||
var node = this;
|
||||
@ -259,18 +566,19 @@
|
||||
}
|
||||
var editorRow = $("#dialog-form>div.node-text-editor-row");
|
||||
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
|
||||
$(".node-text-editor").css("height",height+"px");
|
||||
this.editor.resize();
|
||||
$("#dialog-form .node-text-editor").css("height",height+"px");
|
||||
|
||||
var height = size.height;
|
||||
$("#node-input-init-editor").css("height", (height -105)+"px");
|
||||
$("#node-input-func-editor").css("height", (height -145)+"px");
|
||||
$("#node-input-finalize-editor").css("height", (height -105)+"px");
|
||||
$("#node-input-init-editor").css("height", (height -45-48)+"px");
|
||||
$("#node-input-func-editor").css("height", (height -45-48)+"px");
|
||||
$("#node-input-finalize-editor").css("height", (height -45-48)+"px");
|
||||
|
||||
this.initEditor.resize();
|
||||
this.editor.resize();
|
||||
this.finalizeEditor.resize();
|
||||
|
||||
$("#node-input-libs-container").css("height", (height - 185)+"px");
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -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");
|
||||
};
|
||||
|
||||
|
@ -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__"
|
||||
}
|
||||
|
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 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,
|
||||
|
@ -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 = {};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<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
|
||||
@ -280,7 +294,7 @@ function start(type,diff,muteLog) {
|
||||
log.info(log._("nodes.flows.safe-mode"));
|
||||
log.info("*****************************************************************")
|
||||
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) {
|
||||
@ -370,7 +384,7 @@ function start(type,diff,muteLog) {
|
||||
log.info(log._("nodes.flows.started-flows"));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
function stop(type,diff,muteLog) {
|
||||
|
@ -271,6 +271,7 @@ function stop() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// This is the internal api
|
||||
var runtime = {
|
||||
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
|
||||
loadContextsPlugin: context.load,
|
||||
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"),
|
||||
// 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.
|
||||
// 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
|
||||
|
@ -93,9 +93,6 @@ describe('function node', function() {
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
it('should be loaded', function(done) {
|
||||
var flow = [{id:"n1", type:"function", name: "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 () {
|
||||
|
||||
function testLog(initCode,funcCode,expectedLevel, done) {
|
||||
@ -1603,5 +1680,4 @@ describe('function node', function() {
|
||||
});
|
||||
|
||||
})
|
||||
*/
|
||||
});
|
||||
|
@ -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": "*"
|
||||
}
|
||||
}
|
||||
|
@ -189,6 +189,7 @@
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [ {"var":"cowsay2","module":"cowsay2"}],
|
||||
"x": 240,
|
||||
"y": 100,
|
||||
"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(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();
|
||||
|
@ -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)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user