Rework Function node module integration

This commit is contained in:
Nick O'Leary
2021-02-12 18:14:13 +00:00
parent 4a1d66f210
commit 9c09ee3b71
20 changed files with 800 additions and 1384 deletions

View File

@@ -1,20 +1,67 @@
<script type="text/html" data-template-name="function">
<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>
<style>
#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;
}
<div class="form-row">
.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" style="margin-top: -20px">
<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 id="func-tab-config" style="display:none">
<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">
<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="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>
@@ -22,46 +69,77 @@
</div>
<div id="func-tab-body" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<input type="hidden" id="node-input-func" autofocus="autofocus">
<input type="hidden" id="node-input-noerr">
</div>
<div class="form-row node-text-editor-row" style="position:relative">
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
<div style="height: 220px; min-height:120px; margin-top: 30px;" class="node-text-editor" id="node-input-func-editor" ></div>
</div>
<div class="form-row" style="margin-bottom: 0px">
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
<input id="node-input-outputs" style="width: 60px;" value="1">
</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>
</div>
<div id="func-tab-config" style="display:none">
<div class="form-row" style="margin-bottom: 0px;">
<label><i class="fa fa-wrench"></i> <span>Use Library</span></label>
</div>
<div class="form-row" style="height: 250px; min-height: 150px">
<ol id="node-input-libs-container"></ol>
</div>
</div>
</div>
</script>
<script type="text/javascript">
(function() {
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 = [];
@@ -73,523 +151,123 @@
return [module, undefined];
}
/**
* Add library descriptor
* @param {string} name - library name
*/
function addLib(info) {
allLibs.push(info);
}
/**
* Validate library spec including conflicts with other specs
* @param {string} name - library name
*/
function checkLib(spec) {
var [name, ver] = moduleName(spec);
var libs = $("#node-input-libs-container").editableList("items");
var count = 0;
libs.each(function(i) {
if (count > 1) {
return;
}
var item = $(this);
var n = item.find(".node-input-libs-val").val();
if (n && (n !== "")) {
var [name1, ver1] = moduleName(n);
if (name1 === name) {
count++;
}
}
});
return (count <= 1);
}
/**
* Get library list
*/
function getModules() {
var root = RED.settings.apiRootUrl || "";
var promise = $.ajax({
url: root + "/modules",
type: "GET",
cache: false
});
return promise;
}
/**
* Check and update in progress state of modules
*/
function checkInProgress(allLibs) {
var waitTime = 1000;
function update() {
map = {};
allLibs.forEach(function (lib) {
if (lib.inProgress) {
map[lib.name] = lib;
}
});
getModules().then(function (data,textStatus,xhr) {
var count = 0;
data.forEach(lib => {
var name = lib.name;
if (lib.inProgress) {
count++;
}
else if (name in map) {
var lib2 = map[name];
if (lib2) {
delete map[name];
var cb = lib2.onReady;
if (cb) {
lib2.version = lib.version;
lib2.status["installed"] = true;
lib2.inProgress = false;
cb();
}
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);
}
}
});
if (count > 0) {
setTimeout(update, waitTime);
}
});
}
}
setTimeout(update, waitTime);
}
/**
* Update library list
*/
function updateLib(node, done) {
var names = {};
var specs = {};
var libs = node.libs || [];
allLibs = [];
libs.forEach(lib => {
var vname = lib.vname;
var spec = lib.spec;
var [name, ver] = moduleName(spec);
var item = {
vname: vname,
name: name,
spec: spec,
status: { "edit":true }
};
addLib(item);
specs[spec] = true;
names[name] = true;
});
RED.nodes.eachNode(function(n) {
if ((n.type === "function") && (n !== node)) {
var libs = n.libs || [];
libs.forEach(lib => {
var spec = lib.spec;
var [name, ver] = moduleName(spec);
if (name in names) {
var items = allLibs.filter(item => (item.name === name));
items.forEach(item => {
item.status["used"] = true;
});
if (spec in specs) {
// olready found
return;
}
// spec conflict
}
var item = {
name: name,
spec: spec,
status: { "used": true }
};
addLib(item);
names[name] = true;
specs[spec] = true;
});
}
});
var inProgressCount = 0;
getModules().then(function(data,textStatus,xhr) {
data.forEach(lib => {
var name = lib.name;
var version = lib.version;
var preinstalled = (lib.status == "preinstalled");
var spec = lib.spec;
var items = allLibs.filter(item => (item.name === name));
if (items.length > 0) {
items.forEach(item => {
item.version = version;
item.status["installed"] = true;
item.status["preinstalled"] = preinstalled;
item.inProgress = lib.inProgress;
});
}
else {
var item = {
name: name,
spec: spec,
version: version,
status: {
"installed": true,
"preinstalled": preinstalled
},
inProgress: lib.inProgress
};
addLib(item);
names[name] = true;
specs[spec] = true;
}
if (lib.inProgress) {
inProgressCount++;
}
});
$("#node-input-libs-container").editableList("empty");
allLibs.forEach(function(lib) {
$("#node-input-libs-container").editableList("addItem", lib);
});
if (inProgressCount > 0) {
checkInProgress(allLibs);
}
if (done) {
done();
}
});
}
/**
* Uninstall NPM module
*/
function uninstallModule(node, spec, name, version,
moduleInfo, installButton,
installInfoRow, installedInfo,
removeButton,
progressRow,
done) {
var root = RED.settings.apiRootUrl || "";
progressRow.show();
$.ajax({
url: root + "/modules/" +spec,
type: "DELETE",
}).then(function(data,textStatus,xhr) {
moduleInfo.attr("disabled", false);
installButton.attr("disabled", false);
installInfoRow.hide();
installedInfo.val("");
removeButton.show();
progressRow.hide();
RED.notify("Successfully uninstalled:" +name);
if(done) {
done();
}
}).fail(function(xhr,textStatus,err) {
progressRow.hide();
RED.notify("Failed to uninstall: " +name);
if (done) {
done();
}
});
}
/**
* Updaate or Install NPM module
*/
function updateModule(node, spec, update,
moduleInfo, installButton,
installInfoRow, installedInfo, removeButton,
progressRow,
done) {
var [name, version] = moduleName(spec);
var root = RED.settings.apiRootUrl || "";
var errorMessage = "failed to "+(update ? "update": "install") +": " +name;
progressRow.show();
$.ajax({
url: root + "/modules",
type: "POST",
contentType: "application/json; charset=utf-8",
cache: false,
data: JSON.stringify({
spec: spec,
update: update
})
}).then(function(data) {
getModules().then(function(data,textStatus,xhr) {
var item = data.find(lib => (lib.name === name));
progressRow.hide();
if (item) {
moduleInfo.attr("disabled", true);
installButton.attr("disabled", true);
installInfoRow.show();
installedInfo.val(item.name+"@"+item.version);
removeButton.hide();
var msg = "Successfully "
+(update ? "updated": "installed")
+": " +name;
RED.notify(msg);
}
if (done) {
done();
}
}).fail(function(xhr,textStatus,err) {
progressRow.hide();
RED.notify(errorMessage);
if (done) {
done();
}
});
}).fail(function(xhr,textStatus,err) {
progressRow.hide();
RED.notify(errorMessage);
if (done) {
done();
}
});
}
/**
*
*/
function withNotification(msg, text, cb) {
var notification = RED.notify(msg, {
modal: true,
fixed: true,
buttons: [
{
text: RED._("common.label.cancel"),
click: function() {
notification.close();
}
},
{
text: text,
class: "primary red-ui-palette-module-install-confirm-button-update",
click: function() {
notification.close();
cb();
}
},
]
});
var modules = Array.from(moduleSet);
modules.sort();
return modules;
}
function prepareLibraryConfig(node) {
$("#node-input-libs-container").css('min-height','250px').css('min-width','450px').editableList({
$(".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 removeButton = parent.find(".red-ui-editableList-item-remove");
var disabled = (opt && !!opt.unused);
var row0 = $("<div/>").appendTo(container);
var fvar = $("<input/>", {
class: "node-input-libs-var",
placeholder: RED._("node-red:function.require.var"),
type: "text",
disabled: disabled
}).css({
width: "90px",
"margin-left": "5px"
}).appendTo(row0);
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);
$('<code> = require(</code>').appendTo(row0);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: disabled
type: "text"
}).css({
width: fieldWidth,
"margin-left": "5px"
}).appendTo(row0);
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);
}
var buttonWidth = "70px";
var install = $("<button/>", {
class: "red-ui-button red-ui-button-small"
}).css({
"margin-left": "10px",
width: buttonWidth
}).appendTo(row0);;
$("<i/>", {
class: "fa fa-cube"
}).appendTo(install);
$("<span/>").css({
"margin-left": "5px"
}).text("Install").appendTo(install);
$('<code>)</code>').appendTo(row0);
var warn = $("<span/>").css({
"margin-left": "10px"
}).appendTo(row0);
$("<i/>", {
class: "fa fa-warning"
}).appendTo(warn);
RED.popover.tooltip(warn, "module spec conflict");
warn.hide();
var row1 = $("<div/>").css({
"margin-top": "5px"
}).appendTo(container);
var linstalled = $("<label/>").css({
width: "90px",
"text-align": "right",
"margin-right": "10px"
}).appendTo(row1);
$("<i/>", {
class: "fa fa-cube"
}).appendTo(linstalled);
var status = "Installed";
$("<span/>").css({
"margin-left": "5px"
}).text(status).appendTo(linstalled);
var finstalled = $("<input/>", {
type: "text",
disabled: true
}).css({
width: fieldWidth
}).appendTo(row1);
var uninstall = $("<button/>", {
class: "red-ui-button red-ui-button-small"
}).css({
"margin-left": "10px",
width: buttonWidth
}).appendTo(row1);
$("<i/>", {
class: "fa fa-trash"
}).appendTo(uninstall);
$("<span/>").css({
"margin-left": "5px"
}).text("Uninstall").appendTo(uninstall);
var update = $("<button/>", {
class: "red-ui-button red-ui-button-small"
}).css({
"margin-left": "10px",
width: buttonWidth
}).appendTo(row1);
$("<i/>", {
class: "fa fa-refresh"
}).appendTo(update);
$("<span/>").css({
"margin-left": "5px"
}).text("Update").appendTo(update);
var row2 = $("<div/>", {
class: "red-ui-palette-module-shade"
}).appendTo(container);
$("<img/>", {
src: "red/images/spin.svg",
class: "red-ui-palette-spinner"
}).appendTo(row2);
row2.hide();
var warning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(warning.find("i"),function() {
var val = fmodule.typedInput("type");
if (val === "_custom_") {
val = fmodule.val();
}
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
return "Module not allowed"
} else {
return "Module not installed: "+missingModuleReasons[val]
}
})
fvar.on("change", function (e) {
opt.vname = $(this).val();
var v = $(this).val().trim();
if (v === "" || / /.test(v)) {
fvar.addClass("input-error");
} else {
fvar.removeClass("input-error");
}
});
fmodule.on("change", function (e) {
var val = $(this).val();
if (!checkLib(val, opt.id)) {
$(this).addClass("input-error");
warn.show();
var val = $(this).typedInput("type");
if (val === "_custom_") {
val = $(this).val();
}
else {
$(this).removeClass("input-error");
warn.hide();
if (val && (val !== "") &&
(!opt.version || (opt.version === ""))) {
install.attr("disabled", false);
}
}
opt.spec = val;
});
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() });
fvar.val(varName);
fvar.trigger("change");
install.on("click", function (e) {
withNotification(
"Install NPM Module " +opt.spec,
"Install",
function (cb) {
updateModule(
node, opt.spec, false,
fmodule, install,
row1, finstalled, removeButton,
row2,
cb);
});
});
uninstall.on("click", function (e) {
withNotification(
"Uninstall NPM Module "+opt.spec,
"Uninstall",
function (cb) {
uninstallModule(
node, opt.spec, opt.name, opt.version,
fmodule, install,
row1, finstalled,
removeButton,
row2,
cb);
});
});
update.on("click", function (e) {
withNotification(
"Update NPM Module " +opt.spec,
"Update",
function (cb) {
updateModule(
node, opt.spec, true,
fmodule, install,
row1, finstalled, removeButton,
row2,
cb);
});
});
install.attr("disabled", true);
if (opt) {
function updateData(data) {
if (data.vname && (data.vname !== "")) {
fvar.val(data.vname);
}
if (data.spec && (data.spec !== "")) {
fmodule.val(data.spec);
}
if (data.version && (data.version !== "")) {
fmodule.prop("disabled", true);
row1.show();
removeButton.hide();
finstalled.val(data.name+"@"+data.version)
if (data.status["preinstalled"]) {
uninstall.attr("disabled", true);
update.attr("disabled", true);
}
}
else {
install.attr("disabled", false);
row1.hide();
removeButton.show();
}
}
updateData(opt);
if (opt.inProgress) {
row2.show();
opt.onReady = function () {
row2.hide();
updateData(opt);
opt.onReady = null;
};
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
fmodule.removeClass("input-error");
warning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
warning.addClass("input-error");
}
});
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
fmodule.removeClass("input-error");
warning.removeClass("input-error");
} else {
fmodule.addClass("input-error");
warning.addClass("input-error");
}
if (opt.var) {
fvar.trigger("change");
}
},
removable: true,
sortable: true
});
updateLib(node, function () {
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',{
@@ -602,7 +280,22 @@
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""},
libs: {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;
}
}
return true;
}}
},
inputs:1,
outputs:1,
@@ -623,21 +316,24 @@
$("#" + tab.id).show();
}
});
tabs.addTab({
id: "func-tab-config",
iconClass: "fa fa-cog",
maximumTabWidth: 38
});
tabs.addTab({
id: "func-tab-init",
label: that._("function.label.initialize")
label: "On Start", //that._("function.label.initialize")
});
tabs.addTab({
id: "func-tab-body",
label: that._("function.label.function")
label: "On Message"//that._("function.label.function")
});
tabs.addTab({
id: "func-tab-finalize",
label: that._("function.label.finalize")
});
tabs.addTab({
id: "func-tab-config",
label: "Config"
label: "On Stop"//that._("function.label.finalize")
});
tabs.activateTab("func-tab-body");
@@ -748,7 +444,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"));
prepareLibraryConfig(that);
if (RED.settings.functionExternalModules !== false) {
prepareLibraryConfig(that);
}
},
oneditsave: function() {
var node = this;
@@ -780,24 +478,28 @@
$("#node-input-noerr").val(noerr);
this.noerr = noerr;
var libs = $("#node-input-libs-container").editableList("items");
var oldLibs = node.libs || [];
var newLibs = [];
node.libs = newLibs;
libs.each(function(i) {
var item = $(this);
var v = item.find(".node-input-libs-var").val();
var n = item.find(".node-input-libs-val").val();
if ((!v || (v === "")) ||
(!n || (n === ""))) {
return;
}
newLibs.push({
vname: v,
spec: n
if (RED.settings.functionExternalModules !== false) {
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;
@@ -823,15 +525,16 @@
this.editor.resize();
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)+"px");
$("#node-input-func-editor").css("height", (height -45)+"px");
$("#node-input-finalize-editor").css("height", (height -45)+"px");
this.initEditor.resize();
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-libs-container").css("height", (height -155)+"px");
$("#node-input-libs-container").css("height", (height - 185)+"px");
}
});
})();
</script>

View File

@@ -91,13 +91,17 @@ module.exports = function(RED) {
function FunctionNode(n) {
var libs = n.libs || [];
n.modules = libs.map(x => x.spec).filter(x => (x && (x !== "")));
var loadPromise = RED.nodes.createNode(this,n);
RED.nodes.createNode(this,n);
var node = this;
node.name = n.name;
node.func = n.func;
node.ini = n.initialize ? n.initialize.trim() : "";
node.fin = n.finalize ? n.finalize.trim() : "";
node.libs = libs;
node.libs = libs || [];
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
throw new Error("Function node not allowed to load external modules");
}
var handleNodeDoneCall = true;
@@ -287,173 +291,173 @@ module.exports = function(RED) {
}
});
// wait for module installation
loadPromise.catch(()=>{
}).finally(function () {
if (node.hasOwnProperty("libs")) {
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("vname") ? module.vname : null;
if (vname && (vname !== "")) {
sandbox[vname] = null;
try {
var spec = module.spec;
if (spec && (spec !== "")) {
var lib = RED.require(module.spec);
sandbox[vname] = lib;
if (node.hasOwnProperty("libs")) {
var modules = node.libs;
modules.forEach(module => {
var vname = module.hasOwnProperty("var") ? module.var : null;
if (vname && (vname !== "")) {
sandbox[vname] = null;
try {
var spec = module.spec;
if (spec && (spec !== "")) {
var lib = RED.require(module.spec);
sandbox[vname] = lib;
}
}
catch (e) {
console.log(e);
node.warn("failed to load library: "+ module.spec);
}
}
});
}
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__);`;
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})();";
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
}
var promise = Promise.resolve();
if (iniScript) {
context.__initSend__ = function(msgs) { node.send(msgs); };
promise = iniScript.runInContext(context, iniOpt);
}
processMessage = function (msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
node.script.runInContext(context);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {
done();
}
var duration = process.hrtime(start);
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
node.metric("duration", msg, converted);
if (process.env.NODE_RED_FUNCTION_TIME) {
node.status({fill:"yellow",shape:"dot",text:""+converted});
}
}).catch(err => {
if ((typeof err === "object") && err.hasOwnProperty("stack")) {
//remove unwanted part
var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/);
err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n');
var stack = err.stack.split(/\r?\n/);
//store the error in msg to be used in flows
msg.error = err;
var line = 0;
var errorMessage;
if (stack.length > 0) {
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
line++;
}
if (line < stack.length) {
errorMessage = stack[line];
var m = /:(\d+):(\d+)$/.exec(stack[line+1]);
if (m) {
var lineno = Number(m[1])-1;
var cha = m[2];
errorMessage += " (line "+lineno+", col "+cha+")";
}
}
}
catch (e) {
node.warn("failed to load library: "+ module.spec);
if (!errorMessage) {
errorMessage = err.toString();
}
done(errorMessage);
}
else if (typeof err === "string") {
done(err);
}
else {
done(JSON.stringify(err));
}
});
}
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__);`;
iniOpt = createVMOpt(node, " setup");
iniScript = new vm.Script(iniText, iniOpt);
node.on("close", function() {
if (finScript) {
try {
finScript.runInContext(context, finOpt);
}
catch (err) {
node.error(err);
}
}
node.script = vm.createScript(functionText, createVMOpt(node, ""));
if (node.fin && (node.fin !== "")) {
var finText = "(function () {\n"+node.fin +"\n})();";
finOpt = createVMOpt(node, " cleanup");
finScript = new vm.Script(finText, finOpt);
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop());
}
var promise = Promise.resolve();
if (iniScript) {
context.__initSend__ = function(msgs) { node.send(msgs); };
promise = iniScript.runInContext(context, iniOpt);
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
}
});
processMessage = function (msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
node.script.runInContext(context);
context.results.then(function(results) {
sendResults(node,send,msg._msgid,results,false);
if (handleNodeDoneCall) {
done();
}
var duration = process.hrtime(start);
var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100;
node.metric("duration", msg, converted);
if (process.env.NODE_RED_FUNCTION_TIME) {
node.status({fill:"yellow",shape:"dot",text:""+converted});
}
}).catch(err => {
if ((typeof err === "object") && err.hasOwnProperty("stack")) {
//remove unwanted part
var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/);
err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n');
var stack = err.stack.split(/\r?\n/);
//store the error in msg to be used in flows
msg.error = err;
var line = 0;
var errorMessage;
if (stack.length > 0) {
while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) {
line++;
}
if (line < stack.length) {
errorMessage = stack[line];
var m = /:(\d+):(\d+)$/.exec(stack[line+1]);
if (m) {
var lineno = Number(m[1])-1;
var cha = m[2];
errorMessage += " (line "+lineno+", col "+cha+")";
}
}
}
if (!errorMessage) {
errorMessage = err.toString();
}
done(errorMessage);
}
else if (typeof err === "string") {
done(err);
}
else {
done(JSON.stringify(err));
}
promise.then(function (v) {
var msgs = messages;
messages = [];
while (msgs.length > 0) {
msgs.forEach(function (s) {
processMessage(s.msg, s.send, s.done);
});
msgs = messages;
messages = [];
}
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
node.on("close", function() {
if (finScript) {
try {
finScript.runInContext(context, finOpt);
}
catch (err) {
node.error(err);
}
}
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop());
}
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
}
});
promise.then(function (v) {
var msgs = messages;
messages = [];
while (msgs.length > 0) {
msgs.forEach(function (s) {
processMessage(s.msg, s.send, s.done);
});
msgs = messages;
messages = [];
}
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
}
catch(err) {
// eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this
updateErrorInfo(err);
node.error(err);
}
});
}
catch(err) {
// eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this
updateErrorInfo(err);
node.error(err);
}
}
RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "modules"
dynamicModuleList: "libs",
settings: {
functionExternalModules: { value: true, exportable: true }
}
});
RED.library.register("functions");
};