Merge pull request #2873 from node-red/function-modules

Function node external modules
This commit is contained in:
Nick O'Leary
2021-03-01 21:35:31 +00:00
committed by GitHub
28 changed files with 1323 additions and 223 deletions

View File

@@ -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>

View File

@@ -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");
};