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

@@ -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;