initial support for npm module installation

This commit is contained in:
Hiroyasu Nishiyama
2021-01-11 19:32:16 +09:00
parent c40412d7c6
commit d51aefa156
6 changed files with 829 additions and 145 deletions

View File

@@ -1,3 +1,279 @@
<script type="text/html" data-template-name="library-config">
<div class="form-row">
<label for="node-config-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-config-input-name" data-i18n="[placeholder]common.label.name">
</div>
</div>
<div class="form-row" style="margin-bottom: 2px; margin-top: 15px;">
<label style="width: 65%;"><i class="fa fa-cog"></i> <span>Import Libraries</span></label>
<input type="checkbox" id="node-config-input-showAll" style="display: inline-block; width: auto; vertical-align: top;">
<label for="node-config-input-showAll" style="width: auto;">Show All Libraries</label>
</div>
<div class="form-row "">
<ol id="node-input-libs-container"></ol>
</div>
</script>
<script type="text/javascript">
// object that maps from library name to its descriptor
var allLibs = {};
/**
* Add library descriptor
* @param {string} name - library name
*/
function addLib(name) {
var item = allLibs[name]
if (item) {
item.count++;
}
else {
allLibs[name] = {
name: name,
count: 1
};
}
}
/**
* Remove library descriptor if reference count=0
* @param {string} name - library name
*/
function removeLib(name) {
var item = allLibs[name];
if (item) {
item.count--;
if (item.count === 0) {
delete allLibs[name];
}
}
}
function currentLibs() {
var result = [];
var libs = $("#node-input-libs-container").editableList("items");
libs.each(function(i) {
var item = $(this);
var n = item.find(".node-input-libs-val").val();
if (n && (n !== "")) {
result.push(n);
}
});
return result;
}
/**
* Validate library spec including conflicts with other specs
* @param {string} name - library name
*/
function checkLib(name) {
var m0 = name.match(/^([^@]+)(@.*)?$/);
if (m0) {
var lname0 = m0[1];
var ver0 = m0[2];
var ok = true;
var current = currentLibs(libs);
var libs = Object.keys(allLibs).concat(current);
libs.forEach(function(lib) {
if (name !== lib) {
var m1 = lib.match(/^([^@]+)(@.*)?$/);
if (m1) {
var lname1 = m1[1];
var ver1 = m1[2];
if ((lname1 === lname0) && (ver0 !== ver1)) {
ok = false;
return;
}
}
}
});
return ok;
}
return false;
}
/**
* Update library list
*/
function updateLib(done) {
allLibs = {};
RED.nodes.eachConfig(function(n) {
if (n.type === "library-config") {
var libs = n.libs || [];
libs.forEach(function(lib) { addLib(lib.name); });
}
});
var root = RED.settings.apiRootUrl || "";
$.ajax({
headers: {
"Accept":"application/json"
},
cache: false,
url: root + "/function/modules",
success: function(data) {
data.forEach(function(lib) {
var name = lib.name;
if (lib.version && (lib.version !== "")) {
name += "@" +lib.version;
}
addLib(name);
});
done();
}
});
}
/**
* Register NPM library configuration node
*/
RED.nodes.registerType("library-config", {
category: "config",
defaults: {
name: {value: ""},
libs: {value: []},
showAll: {value: false}
},
label: function() {
if (this.name && (this.name !== "")) {
return this.name;
}
return "Library Set:"+this.id;
},
oneditprepare: function() {
var node = this;
var popovers = {};
$("#node-input-libs-container").css('min-height','250px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var disabled = (opt && !!opt.unused);
var row = $("<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(row);
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: disabled
}).css({
width: "280px",
"margin-left": "5px"
}).appendTo(row);
fvar.on("change", function (e) {
opt.vname = $(this).val();
});
fmodule.on("change", function (e) {
var val = $(this).val();
if (!checkLib(val)) {
$(this).addClass("input-error");
var popover = RED.popover.tooltip($(this), "conflict in module spec");
popovers[i] = popover;
}
else {
$(this).removeClass("input-error");
var popover = popovers[i];
if (popover) {
popover.setContent(function () {
return null;
});
}
}
opt.name = val;
});
if (opt) {
if (opt.vname) {
fvar.val(opt.vname);
}
if (opt.name) {
fmodule.val(opt.name);
addLib(opt.name);
}
}
},
removeItem: function(item) {
removeLib(item.name);
},
removable: true,
sortable: true
});
var libs = node.libs ? node.libs : [];
libs.forEach(function(lib) {
var conf = {
vname: lib.vname,
name: lib.name
};
$("#node-input-libs-container").editableList("addItem", conf);
});
$("#node-config-input-showAll").on("change", function () {
var checked = $(this).prop("checked");
var fun = null;
if (checked) {
fun = null;
}
else {
fun = function(item) {
return (!item.unused) ||
((item.vname && (item.vname !== "")) &&
(item.name && (item.name !== "")));
};
}
$("#node-input-libs-container").editableList("filter", fun);
});
updateLib(function () {
Object.values(allLibs).forEach(function(lib) {
function pred(x) {
return (x.name === lib.name);
}
if (libs.find(pred)) {
return;
}
var conf = {
vname: "",
name: lib.name,
unused: true
};
$("#node-input-libs-container").editableList("addItem", conf);
});
$("#node-config-input-showAll").change();
});
},
oneditsave: function() {
var node = this;
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,
name: n
});
});
},
oneditresize: function(size) {
var height = size.height;
$("#node-input-libs-container").editableList("height", height -60);
}
});
</script>
<script type="text/html" data-template-name="function">
<div class="form-row">
@@ -50,6 +326,15 @@
</div>
</div>
<div id="func-tab-config" style="display:none">
<div class="form-row">
<label><i class="fa fa-wrench"></i> <span>Use Library</span></label>
<input type="text" id="node-input-libs">
</div>
<div class="form-row" style="height: 250px; min-height: 150px">
<ol id="node-input-require-container"></ol>
</div>
</div>
</div>
</script>
@@ -64,7 +349,8 @@
outputs: {value:1},
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""}
finalize: {value:""},
libs: {value:"", type:"library-config", required:false}
},
inputs:1,
outputs:1,
@@ -97,6 +383,11 @@
id: "func-tab-finalize",
label: that._("function.label.finalize")
});
tabs.addTab({
id: "func-tab-config",
label: "Config"
});
tabs.activateTab("func-tab-body");
$( "#node-input-outputs" ).spinner({
@@ -205,7 +496,56 @@
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"));
$("#node-input-require-container").css('min-height','250px').css('min-width','450px').editableList({
addItem: function(container,i,opt) {
var row = $("<div/>").appendTo(container);
var fvar = $("<input/>", {
class: "node-input-require-var",
placeholder: RED._("node-red:function.require.var"),
type: "text",
disabled: true
}).css({
width: "130px",
"margin-left": "5px"
}).appendTo(row);
var fmodule = $("<input/>", {
class: "node-input-require-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: true
}).css({
width: "390px",
"margin-left": "5px"
}).appendTo(row);
if (opt) {
if (opt.vname) {
fvar.val(opt.vname);
}
if (opt.name) {
fmodule.val(opt.name);
}
}
},
addButton: false,
removable: false
});
function updateLibs() {
var id = $("#node-input-libs").val();
var node = RED.nodes.node(id);
if (node && node.libs) {
var libs = node.libs;
$("#node-input-require-container").editableList("empty");
libs.forEach(function (r) {
$("#node-input-require-container").editableList("addItem", r);
});
}
}
updateLibs();
$("#node-input-libs").on("change", function () {
updateLibs();
});
},
oneditsave: function() {
var node = this;
@@ -271,6 +611,7 @@
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-require-container").css("height", (height -155)+"px");
}
});
</script>

View File

@@ -16,6 +16,19 @@
module.exports = function(RED) {
"use strict";
function LibsConfigNode(n) {
RED.nodes.createNode(this, n);
this.name = n.name;
this.libs = n.libs;
}
RED.nodes.registerType("library-config", LibsConfigNode);
RED.httpNode.get("/function/modules", function (req, res) {
var list = RED.nodes.listNPMModules();
res.send(list);
});
var util = require("util");
var vm = require("vm");
@@ -88,12 +101,16 @@ module.exports = function(RED) {
}
function FunctionNode(n) {
RED.nodes.createNode(this,n);
var libConf = RED.nodes.getNode(n.libs);
var libs = libConf ? libConf.libs : [];
n.modules = libs.map(x => x.name).filter(x => (x && (x !== "")));
var loadPromise = 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;
var handleNodeDoneCall = true;
@@ -105,23 +122,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,12 +283,47 @@ module.exports = function(RED) {
};
sandbox.promisify = util.promisify;
}
var context = vm.createContext(sandbox);
try {
var iniScript = null;
var iniOpt = null;
if (node.ini && (node.ini !== "")) {
var iniText = `
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);
}
});
// wait for module installation
loadPromise.then(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 lib = RED.require(module.name);
sandbox[vname] = lib;
}
catch (e) {
node.warn("failed to load library: "+ module.name);
}
}
});
}
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,
@@ -288,141 +340,130 @@ module.exports = function(RED) {
};
`+ 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);
}
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);
}
function processMessage(msg, send, done) {
var start = process.hrtime();
context.msg = msg;
context.__send__ = send;
context.__done__ = done;
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();
}
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/);
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;
//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++;
}
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 (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);
}
if (!errorMessage) {
errorMessage = err.toString();
else if (typeof err === "string") {
done(err);
}
else {
done(JSON.stringify(err));
}
});
}
node.on("close", function() {
if (finScript) {
try {
finScript.runInContext(context, finOpt);
}
catch (err) {
node.error(err);
}
done(errorMessage);
}
else if (typeof err === "string") {
done(err);
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop());
}
else {
done(JSON.stringify(err));
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
}
});
}
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 {
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;
promise.then(function (v) {
var msgs = messages;
messages = [];
}
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
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);
RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "modules"
});
RED.library.register("functions");
};