1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

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;
@ -266,6 +283,41 @@ module.exports = function(RED) {
};
sandbox.promisify = util.promisify;
}
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;
@ -303,7 +355,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 +415,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 {
@ -421,8 +459,11 @@ module.exports = function(RED) {
updateErrorInfo(err);
node.error(err);
}
});
}
RED.nodes.registerType("function",FunctionNode);
RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "modules"
});
RED.library.register("functions");
};

View File

@ -212,12 +212,17 @@
"function": "Function",
"initialize": "Setup",
"finalize": "Close",
"outputs": "Outputs"
"outputs": "Outputs",
"require": "Require"
},
"text": {
"initialize": "// Code added here will be run once\n// whenever the node is deployed.\n",
"finalize": "// Code added here will be run when the\n// node is being stopped or re-deployed.\n"
},
"require": {
"var": "name",
"module": "module"
},
"error": {
"inputListener":"Cannot add listener to 'input' event within Function",
"non-message-returned":"Function tried to send a message of type __type__"

View File

@ -45,6 +45,10 @@ function requireModule(name) {
var relPath = path.relative(__dirname, moduleInfo.path);
return require(relPath);
} else {
var npm = runtime.nodes.loadNPMModule(name);
if (npm) {
return npm;
}
var err = new Error(`Cannot find module '${name}'`);
err.code = "MODULE_NOT_FOUND";
throw err;
@ -79,7 +83,7 @@ function createNodeApi(node) {
httpAdmin: runtime.adminApp,
server: runtime.server
}
copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials" ]);
copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials", "listNPMModules"]);
red.nodes.registerType = function(type,constructor,opts) {
runtime.nodes.registerType(node.id,type,constructor,opts);
}

View File

@ -26,6 +26,7 @@ var flows = require("../flows");
var flowUtil = require("../flows/util")
var context = require("./context");
var Node = require("./Node");
var npmModule = require("./npmModule");
var log;
const events = require("@node-red/util").events;
@ -49,6 +50,7 @@ function registerType(nodeSet,type,constructor,opts) {
type = nodeSet;
nodeSet = "";
}
var dynModule = null;
if (opts) {
if (opts.credentials) {
credentials.register(type,opts.credentials);
@ -60,7 +62,11 @@ function registerType(nodeSet,type,constructor,opts) {
log.warn("["+type+"] "+err.message);
}
}
if (opts.dynamicModuleList) {
dynModule = opts.dynamicModuleList;
}
}
npmModule.register(type, dynModule);
if(!(constructor.prototype instanceof Node)) {
if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
util.inherits(constructor,Node);
@ -110,6 +116,7 @@ function createNode(node,def) {
} else if (credentials.getDefinition(node.type)) {
node.credentials = {};
}
return npmModule.checkInstall(def);
}
function registerSubflow(nodeSet, subflow) {
@ -138,6 +145,7 @@ function init(runtime) {
flows.init(runtime);
registry.init(runtime);
context.init(runtime.settings);
npmModule.init(runtime);
}
function disableNode(id) {
@ -261,5 +269,9 @@ module.exports = {
// Contexts
loadContextsPlugin: context.load,
closeContextsPlugin: context.close,
listContextStores: context.listStores
listContextStores: context.listStores,
// NPM modules
listNPMModules: npmModule.list,
loadNPMModule: npmModule.load
};

View File

@ -0,0 +1,281 @@
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var path = require("path");
var fs = require("fs-extra");
var os = require("os");
var util = require("@node-red/registry/lib/util");
var api;
var runtime;
var settings;
var exec;
var log;
var npmCommand = (process.platform === "win32") ? "npm.cmd" : "npm";
var moduleProp = {};
var moduleBase = null;
var allowInstall = true;
var allowList = ["*"];
var denyList = [];
/**
* Initialise npm install module.
* @param {Object} _runtime - runtime object
*/
function init(_runtime) {
runtime = _runtime;
settings = _runtime.settings;
exec = _runtime.exec;
log = _runtime.log;
moduleProp = {};
moduleBase = settings.userDir || process.env.NODE_RED_HOME || ".";
if (settings.hasOwnProperty("externalModules")) {
var em = settings.externalModules;
if (em && em.hasOwnProperty("modules")) {
var mod = em.modules;
if (mod.hasOwnProperty("allowInstall")) {
allowInstall = mod.allowInstall;
}
if (mod.hasOwnProperty("allowList")) {
var alist = mod.allowList;
if (Array.isArray(alist)) {
allowList = alist;
}
else {
log.warn("unexpected value of externalModule.allowList in settings.js");
}
}
if (mod.hasOwnProperty("denyList")) {
var dlist = mod.denyList;
if (Array.isArray(dlist)) {
denyList = dlist;
}
else {
log.warn("unexpected value of externalModule.denyList in settings.js");
}
}
}
}
}
/**
* Register dynamic module installation property.
* @param {string} type - node type
* @param {string} prop - property name
*/
function register(type, prop) {
if (prop) {
moduleProp[type] = prop;
}
else {
delete moduleProp[prop]
}
}
/**
* Get path to install modules
*/
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]);
}
return result;
}
/**
* Decompose NPM module specification string
* @param {string} module - module specification
*/
function moduleName(module) {
var match = /^([^@]+@.+)/.exec(module);
if (match) {
return [match[1], match[2]];
}
return [module, undefined];
}
/**
* Get NPM module package info
* @param {string} name - module name
* @param {string} name - module version
*/
function infoNPM(name, ver) {
var path = modulePath(name, "package.json");
try {
var pkg = require(path);
return pkg;
}
catch (e) {
}
return null;
}
/**
* Ensure existance of module installation directory
*/
function ensureLibDirectory() {
var path = modulePath();
if (!fs.existsSync(path)) {
fs.mkdirSync(path, {
recursive: true
});
return fs.existsSync(path);
}
return true;
}
/**
* Install NPM module
* @param {string} module - module specification
*/
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);
}
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();
}
/**
* Check allowance of NPM module installation
* @param {string} name - module specification
*/
function isAllowed(name) {
if (!allowInstall) {
return false;
}
var [module, ver] = moduleName(name);
var aList = util.parseModuleList(allowList);
var dList = util.parseModuleList(denyList);
return util.checkModuleAllowed(module, ver, aList, dList);
}
/**
* Check and install NPM module according to dynamic module specification
* @param {Object} node - node object
*/
function checkInstall(node) {
var name = null;
if(moduleProp.hasOwnProperty(node.type)) {
name = moduleProp[node.type];
}
var promises = [];
if (name && node.hasOwnProperty(name)) {
var modules = node[name];
modules.forEach(module => {
var name = module;
if ((typeof module === "object") &&
module &&
module.hasOwnProperty("name")) {
name = module.name;
}
if (isAllowed(name)) {
promises.push(installNPM(name));
}
else {
log.info("installation not allowed: "+name);
}
});
}
return Promise.all(promises);
}
/**
* Load NPM module
* @param {string} module - module to load
*/
function load(module) {
try {
var [name, ver] = moduleName(module);
var path = modulePath(name);
var npm = require(path);
return npm;
}
catch (e) {
return null;
}
}
/**
* 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);
}
}
}
dir.closeSync();
}
return modules;
}
api = {
init: init,
register: register,
checkInstall: checkInstall,
load: load,
list: listModules
};
module.exports = api;