update UI, Runtime API, metadata handling, and others

This commit is contained in:
Hiroyasu Nishiyama 2021-01-27 22:27:54 +09:00
parent d51aefa156
commit 4a1d66f210
9 changed files with 900 additions and 420 deletions

View File

@ -67,6 +67,11 @@ module.exports = {
adminApp.get(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler);
adminApp.put(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler);
// NPM Modules
adminApp.get("/modules", needsPermission("nodes.read"), nodes.listNPMModules, apiUtil.errorHandler);
adminApp.delete("/modules/:spec", needsPermission("nodes.write"), nodes.uninstallNPMModule, apiUtil.errorHandler);
adminApp.post("/modules", needsPermission("nodes.write"), nodes.updateNPMModule, apiUtil.errorHandler);
// Context
adminApp.get("/context/:scope(global)",needsPermission("context.read"),context.get,apiUtil.errorHandler);
adminApp.get("/context/:scope(global)/*",needsPermission("context.read"),context.get,apiUtil.errorHandler);

View File

@ -191,5 +191,45 @@ module.exports = {
runtimeAPI.nodes.getIconList(opts).then(function(list) {
res.json(list);
});
},
listNPMModules: function(req, res) {
var opts = {
user: req.user,
req: apiUtils.getRequestLogObject(req)
}
runtimeAPI.nodes.listNPMModules(opts).then(function(list) {
res.json(list);
}).catch(err => {
apiUtils.rejectHandler(req,res,err);
});;
},
uninstallNPMModule: function(req, res) {
var opts = {
user: req.user,
spec: req.params.spec,
req: apiUtils.getRequestLogObject(req)
}
runtimeAPI.nodes.uninstallNPMModule(opts).then(function(result) {
res.json(result);
}).catch(err => {
apiUtils.rejectHandler(req,res,err);
});
},
updateNPMModule: function(req, res) {
var body = req.body;
var opts = {
user: req.user,
spec: body.spec,
update: body.update,
req: apiUtils.getRequestLogObject(req)
}
runtimeAPI.nodes.updateNPMModule(opts).then(function(result) {
res.json(result);
}).catch(err => {
apiUtils.rejectHandler(req,res,err);
});
}
};

View File

@ -1,280 +1,3 @@
<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">
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
@ -327,12 +50,11 @@
</div>
<div id="func-tab-config" style="display:none">
<div class="form-row">
<div class="form-row" style="margin-bottom: 0px;">
<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>
<ol id="node-input-libs-container"></ol>
</div>
</div>
</div>
@ -340,6 +62,536 @@
</script>
<script type="text/javascript">
// 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];
}
/**
* 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();
}
}
}
});
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();
}
},
]
});
}
function prepareLibraryConfig(node) {
$("#node-input-libs-container").css('min-height','250px').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 fieldWidth = "260px";
var fmodule = $("<input/>", {
class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"),
type: "text",
disabled: disabled
}).css({
width: fieldWidth,
"margin-left": "5px"
}).appendTo(row0);
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);
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();
fvar.on("change", function (e) {
opt.vname = $(this).val();
});
fmodule.on("change", function (e) {
var val = $(this).val();
if (!checkLib(val, opt.id)) {
$(this).addClass("input-error");
warn.show();
}
else {
$(this).removeClass("input-error");
warn.hide();
if (val && (val !== "") &&
(!opt.version || (opt.version === ""))) {
install.attr("disabled", false);
}
}
opt.spec = val;
});
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;
};
}
}
},
removable: true,
sortable: true
});
updateLib(node, function () {
});
}
RED.nodes.registerType('function',{
color:"#fdd0a2",
category: 'function',
@ -350,7 +602,7 @@
noerr: {value:0,required:true,validate:function(v) { return !v; }},
initialize: {value:""},
finalize: {value:""},
libs: {value:"", type:"library-config", required:false}
libs: {value: []}
},
inputs:1,
outputs:1,
@ -496,56 +748,7 @@
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();
});
prepareLibraryConfig(that);
},
oneditsave: function() {
var node = this;
@ -578,6 +781,23 @@
$("#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
});
});
},
oneditcancel: function() {
var node = this;
@ -611,7 +831,7 @@
this.editor.resize();
this.finalizeEditor.resize();
$("#node-input-require-container").css("height", (height -155)+"px");
$("#node-input-libs-container").css("height", (height -155)+"px");
}
});
</script>

View File

@ -17,18 +17,6 @@
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");
@ -101,9 +89,8 @@ module.exports = function(RED) {
}
function FunctionNode(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 libs = n.libs || [];
n.modules = libs.map(x => x.spec).filter(x => (x && (x !== "")));
var loadPromise = RED.nodes.createNode(this,n);
var node = this;
node.name = n.name;
@ -301,7 +288,8 @@ module.exports = function(RED) {
});
// wait for module installation
loadPromise.then(function () {
loadPromise.catch(()=>{
}).finally(function () {
if (node.hasOwnProperty("libs")) {
var modules = node.libs;
modules.forEach(module => {
@ -309,11 +297,14 @@ module.exports = function(RED) {
if (vname && (vname !== "")) {
sandbox[vname] = null;
try {
var lib = RED.require(module.name);
sandbox[vname] = lib;
var spec = module.spec;
if (spec && (spec !== "")) {
var lib = RED.require(module.spec);
sandbox[vname] = lib;
}
}
catch (e) {
node.warn("failed to load library: "+ module.name);
node.warn("failed to load library: "+ module.spec);
}
}
});

View File

@ -83,7 +83,7 @@ function createNodeApi(node) {
httpAdmin: runtime.adminApp,
server: runtime.server
}
copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials", "listNPMModules"]);
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);
}

View File

@ -447,5 +447,47 @@ var api = module.exports = {
} else {
return null
}
},
/**
* Gets list of NPM modules
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<Buffer>} - list of installed NPM modules
* @memberof @node-red/runtime_nodes
*/
listNPMModules: async function(opts) {
var promise = runtime.nodes.listNPMModules();
return promise;
},
/**
* Uninstall NPM modules
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<Object>} - object for request result
* @memberof @node-red/runtime_nodes
*/
uninstallNPMModule: async function(opts) {
var spec = opts.spec;
var promise = runtime.nodes.uninstallNPMModule(spec);
return promise;
},
/**
* Update NPM modules
* @param {Object} opts
* @param {User} opts.user - the user calling the api
* @param {Object} opts.req - the request to log (optional)
* @return {Promise<Object>} - object for request result
* @memberof @node-red/runtime_nodes
*/
updateNPMModule: async function(opts) {
var spec = opts.spec;
var isUpdate = opts.update;
var promise = runtime.nodes.updateNPMModule(spec, isUpdate);
return promise;
}
}

View File

@ -270,6 +270,7 @@ function stop() {
});
}
// This is the internal api
var runtime = {
version: getVersion,

View File

@ -273,5 +273,7 @@ module.exports = {
// NPM modules
listNPMModules: npmModule.list,
uninstallNPMModule: npmModule.uninstall,
updateNPMModule: npmModule.update,
loadNPMModule: npmModule.load
};

View File

@ -28,6 +28,8 @@ var log;
var npmCommand = (process.platform === "win32") ? "npm.cmd" : "npm";
var metadataFileName = "npm-modules.json";
var moduleProp = {};
var moduleBase = null;
@ -35,6 +37,8 @@ var allowInstall = true;
var allowList = ["*"];
var denyList = [];
var inProgress = {};
/**
* Initialise npm install module.
* @param {Object} _runtime - runtime object
@ -46,6 +50,8 @@ function init(_runtime) {
log = _runtime.log;
moduleProp = {};
inProgress = {};
moduleBase = settings.userDir || process.env.NODE_RED_HOME || ".";
if (settings.hasOwnProperty("externalModules")) {
@ -96,7 +102,6 @@ function register(type, prop) {
*/
function modulePath() { // takes variable length arguments in `arguments`
var result = moduleBase;
result = path.join(result, "lib", "node_modules");
for(var i = 0; i < arguments.length; i++) {
result = path.join(result, arguments[i]);
}
@ -106,9 +111,10 @@ function modulePath() { // takes variable length arguments in `arguments`
/**
* Decompose NPM module specification string
* @param {string} module - module specification
* @return {Object} array [name, version], where name is name part and version is version part of the module
*/
function moduleName(module) {
var match = /^([^@]+@.+)/.exec(module);
var match = /^([^@]+)@(.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
@ -116,15 +122,15 @@ function moduleName(module) {
}
/**
* Get NPM module package info
* Get NPM module info
* @param {string} name - module name
* @param {string} name - module version
* @return {Object} package.json for specified NPM module
*/
function infoNPM(name, ver) {
var path = modulePath(name, "package.json");
function infoNPM(name) {
var path = modulePath("node_modules", name, "package.json");
try {
var pkg = require(path);
return pkg;
var pkg = fs.readFileSync(path);
return JSON.parse(pkg);
}
catch (e) {
}
@ -132,17 +138,50 @@ function infoNPM(name, ver) {
}
/**
* Ensure existance of module installation directory
* Load NPM module metadata
* @return {object} module metadata object
*/
function ensureLibDirectory() {
var path = modulePath();
if (!fs.existsSync(path)) {
fs.mkdirSync(path, {
recursive: true
});
return fs.existsSync(path);
function loadMetadata() {
var path = modulePath(metadataFileName);
try {
var pkg = fs.readFileSync(path);
return JSON.parse(pkg);
}
return true;
catch (e) {
}
return {
modules: []
};
}
/**
* Save NPM module metadata
* @param {string} data - module metadata object
*/
function saveMetadata(data) {
var path = modulePath(metadataFileName);
var str = JSON.stringify(data, null, 4);
fs.writeFileSync(path, str);
}
/**
* Find item in metadata
* @param {Object} meta - metadata
* @param {string} name - module name
* @return {object} metadata item
*/
function findModule(meta, name) {
var modules = meta.modules;
var item = modules.find(item => (item.name === name));
return item;
}
function setInProgress(name) {
inProgress[name] = true;
}
function clearInProgress(name) {
inProgress[name] = false;
}
/**
@ -151,32 +190,60 @@ function ensureLibDirectory() {
*/
function installNPM(module) {
var [name, ver] = moduleName(module);
if (!ensureLibDirectory()) {
log.warn("failed to install: "+name);
return;
}
var pkg = infoNPM(name, ver);
if (!pkg) {
var args = ["install", module];
var dir = modulePath();
return exec.run(npmCommand, args, {
cwd: dir
}, true).then(result => {
if (result && (result.code === 0)) {
log.info("successfully installed: "+name);
setInProgress(name);
return new Promise((resolve, reject) => {
var pkg = infoNPM(name);
if (!pkg) {
var args = ["install", module];
var dir = modulePath();
return exec.run(npmCommand, args, {
cwd: dir
}, true).then(result => {
if (result && (result.code === 0)) {
pkg = infoNPM(name);
var spec = name +(pkg ? "@"+pkg.version : "");
log.info("successfully installed: "+spec);
var meta = loadMetadata();
var item = {
name: name,
spec: module,
status: "installed",
};
meta.modules.push(item);
saveMetadata(meta);
clearInProgress(name);
resolve(true);
}
else {
clearInProgress(name);
var msg = "failed to install: "+name;
log.warn(msg);
reject(msg);
}
}).catch(e => {
clearInProgress(name);
var msg = "failed to install: "+name
log.warn(msg);
reject(msg);
});
}
else {
var meta = loadMetadata();
if (!findModule(meta, name)) {
var item = {
name: name,
spec: module,
status: "preinstalled",
};
meta.modules.push(item);
saveMetadata(meta);
}
else {
log.warn("failed to install: "+name);
}
}).catch(e => {
var msg = e.hasOwnProperty("stderr") ? e.stderr : e;
log.warn("failed to install: "+name);
});
}
else {
log.info("already installed: "+name);
}
return Promise.resolve();
clearInProgress(name);
var spec = name +(pkg ? ("@"+pkg.version) : "");
log.info("already installed: "+spec);
}
resolve(true);
});
}
/**
@ -213,6 +280,8 @@ function checkInstall(node) {
name = module.name;
}
if (isAllowed(name)) {
var [n, v] = moduleName(name);
setInProgress(name);
promises.push(installNPM(name));
}
else {
@ -230,7 +299,7 @@ function checkInstall(node) {
function load(module) {
try {
var [name, ver] = moduleName(module);
var path = modulePath(name);
var path = modulePath("node_modules", name);
var npm = require(path);
return npm;
}
@ -243,32 +312,140 @@ function load(module) {
* Get list of installed modules
*/
function listModules() {
var modPath = modulePath();
if (!fs.existsSync(modPath)) {
return [];
}
var dir = fs.opendirSync(modPath);
var modules = [];
if (dir) {
var ent = null;
while (ent = dir.readSync()) {
var name = ent.name;
if (ent.isDirectory() &&
(name[0] !== ".")) {
var pkgPath = path.join(modPath, name, "package.json");
if (fs.existsSync(pkgPath)) {
var pkg = fs.readJSONSync(pkgPath);
var info = {
name: pkg.name,
version: pkg.version
};
modules.push(info);
}
return new Promise((resolve, reject) => {
var meta = loadMetadata();
var modules = meta.modules;
modules.forEach(item => {
var name = item.name;
var info = infoNPM(name);
if (info) {
item.version = info.version;
}
item.inProgress = ((name in inProgress) && inProgress[name]);
});
Object.keys(inProgress).forEach(name => {
if (inProgress[name] &&
!modules.find(item => (item.name === name))) {
modules.push({
name: name,
spec: name,
state: "inprogress",
inProgress: true
});
}
});
resolve(meta.modules);
});
}
/**
* Uninstall NPM modules
*/
function uninstall(module) {
var [name, ver] = moduleName(module);
setInProgress(name);
return new Promise((resolve, reject) => {
var pkg = infoNPM(name);
var meta = loadMetadata();
var item = findModule(meta, name);
if (pkg && item) {
if (item.status === "preinstalled") {
clearInProgress(name);
var msg = "can't uninstall preinstalled: "+name;
log.warn(msg);
reject(msg);
}
else {
var args = ["uninstall", module];
var dir = modulePath();
return exec.run(npmCommand, args, {
cwd: dir
}, true).then(result => {
if (result && (result.code === 0)) {
log.info("successfully uninstalled: "+name);
var meta = loadMetadata();
var items = meta.modules.filter(item => (item.name !== name));
meta.modules = items;
saveMetadata(meta);
clearInProgress(name);
resolve(true);
}
else {
clearInProgress(name);
var msg = "failed to uninstall: "+name;
log.warn(msg);
reject(msg);
}
}).catch(e => {
clearInProgress(name);
var msg = "failed to uninstall: "+name;
log.warn(msg);
reject(msg);
});
}
}
dir.closeSync();
}
return modules;
else {
clearInProgress(name);
var msg = "module not installed: "+name;
log.info(msg);
reject(msg);
}
});
}
/**
* Update NPM modules
*/
function update(module, isUpdate) {
var act = (isUpdate ? "updated": "install")
var acted = (isUpdate ? "updated": "installed")
var [name, ver] = moduleName(module);
setInProgress(name);
return new Promise((resolve, reject) => {
var pkg = infoNPM(name);
if (!pkg || isUpdate) {
var args = ["install", module];
var dir = modulePath();
return exec.run(npmCommand, args, {
cwd: dir
}, true).then(result => {
if (result && (result.code === 0)) {
pkg = infoNPM(name);
var spec = name +(pkg ? "@"+pkg.version : "");
log.info("successfully "+acted+": "+spec);
var meta = loadMetadata();
var items = meta.modules.filter(item => (item.name !== name));
var item = {
name: name,
spec: module,
status: "installed",
};
items.push(item);
meta.modules = items;
saveMetadata(meta);
clearInProgress(name);
resolve(true);
}
else {
clearInProgress(name);
var msg = "failed to "+act+": "+name;
log.warn(msg);
reject(msg);
}
}).catch(e => {
clearInProgress(name);
var msg = "failed to "+act+": "+name;
log.warn(msg);
reject(msg);
});
}
else {
clearInProgress(name);
var msg = "not "+acted+": "+name;
log.info(msg);
reject(msg);
}
});
}
api = {
@ -276,6 +453,8 @@ api = {
register: register,
checkInstall: checkInstall,
load: load,
list: listModules
list: listModules,
uninstall: uninstall,
update: update
};
module.exports = api;