diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.html b/packages/node_modules/@node-red/nodes/core/function/10-function.html index f0e5c7d68..74f2c0f75 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.html +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.html @@ -1,3 +1,279 @@ + + + + + @@ -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 = $("
").appendTo(container); + var fvar = $("", { + 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 = $("", { + 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"); } }); diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index eb07c319d..708cdba0b 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -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"); }; diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index eb623ce20..a2e529d16 100755 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -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__" diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index 94690d6cb..8dda6ece5 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -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); } diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/index.js index 73e7baee8..f9323be18 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -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 }; diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/npmModule.js b/packages/node_modules/@node-red/runtime/lib/nodes/npmModule.js new file mode 100644 index 000000000..ae976de48 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/nodes/npmModule.js @@ -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;