From 4a1d66f210d563253ebf9423148dee6198e096db Mon Sep 17 00:00:00 2001 From: Hiroyasu Nishiyama Date: Wed, 27 Jan 2021 22:27:54 +0900 Subject: [PATCH] update UI, Runtime API, metadata handling, and others --- .../@node-red/editor-api/lib/admin/index.js | 5 + .../@node-red/editor-api/lib/admin/nodes.js | 40 + .../nodes/core/function/10-function.html | 884 +++++++++++------- .../nodes/core/function/10-function.js | 29 +- .../@node-red/registry/lib/util.js | 2 +- .../@node-red/runtime/lib/api/nodes.js | 42 + .../@node-red/runtime/lib/index.js | 1 + .../@node-red/runtime/lib/nodes/index.js | 2 + .../@node-red/runtime/lib/nodes/npmModule.js | 315 +++++-- 9 files changed, 900 insertions(+), 420 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 34c47b2cb..5de09acad 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -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); diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js index 058053a29..77efedd11 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js @@ -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); + }); } }; 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 74f2c0f75..e28413790 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,280 +1,3 @@ - - - - - - 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 708cdba0b..1b53374c2 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 @@ -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); } } }); diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index 8dda6ece5..15eca575b 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -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); } diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index 556e57df9..3e74e3a97 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -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} - 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 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 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; } } diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 40aea7e36..86c9156fa 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -270,6 +270,7 @@ function stop() { }); } + // This is the internal api var runtime = { version: getVersion, 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 f9323be18..cade54d55 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -273,5 +273,7 @@ module.exports = { // NPM modules listNPMModules: npmModule.list, + uninstallNPMModule: npmModule.uninstall, + updateNPMModule: npmModule.update, 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 index ae976de48..b4d4f64a7 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/npmModule.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/npmModule.js @@ -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;