2021-01-11 11:32:16 +01:00
|
|
|
/**
|
|
|
|
* 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";
|
|
|
|
|
2021-01-27 14:27:54 +01:00
|
|
|
var metadataFileName = "npm-modules.json";
|
|
|
|
|
2021-01-11 11:32:16 +01:00
|
|
|
var moduleProp = {};
|
|
|
|
var moduleBase = null;
|
|
|
|
|
|
|
|
var allowInstall = true;
|
|
|
|
var allowList = ["*"];
|
|
|
|
var denyList = [];
|
|
|
|
|
2021-01-27 14:27:54 +01:00
|
|
|
var inProgress = {};
|
|
|
|
|
2021-01-11 11:32:16 +01:00
|
|
|
/**
|
|
|
|
* Initialise npm install module.
|
|
|
|
* @param {Object} _runtime - runtime object
|
|
|
|
*/
|
|
|
|
function init(_runtime) {
|
|
|
|
runtime = _runtime;
|
|
|
|
settings = _runtime.settings;
|
|
|
|
exec = _runtime.exec;
|
|
|
|
log = _runtime.log;
|
|
|
|
|
|
|
|
moduleProp = {};
|
2021-01-27 14:27:54 +01:00
|
|
|
inProgress = {};
|
|
|
|
|
2021-01-11 11:32:16 +01:00
|
|
|
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;
|
|
|
|
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
|
2021-01-27 14:27:54 +01:00
|
|
|
* @return {Object} array [name, version], where name is name part and version is version part of the module
|
2021-01-11 11:32:16 +01:00
|
|
|
*/
|
|
|
|
function moduleName(module) {
|
2021-01-27 14:27:54 +01:00
|
|
|
var match = /^([^@]+)@(.+)/.exec(module);
|
2021-01-11 11:32:16 +01:00
|
|
|
if (match) {
|
|
|
|
return [match[1], match[2]];
|
|
|
|
}
|
|
|
|
return [module, undefined];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-01-27 14:27:54 +01:00
|
|
|
* Get NPM module info
|
2021-01-11 11:32:16 +01:00
|
|
|
* @param {string} name - module name
|
2021-01-27 14:27:54 +01:00
|
|
|
* @return {Object} package.json for specified NPM module
|
2021-01-11 11:32:16 +01:00
|
|
|
*/
|
2021-01-27 14:27:54 +01:00
|
|
|
function infoNPM(name) {
|
|
|
|
var path = modulePath("node_modules", name, "package.json");
|
2021-01-11 11:32:16 +01:00
|
|
|
try {
|
2021-01-27 14:27:54 +01:00
|
|
|
var pkg = fs.readFileSync(path);
|
|
|
|
return JSON.parse(pkg);
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-01-27 14:27:54 +01:00
|
|
|
* Load NPM module metadata
|
|
|
|
* @return {object} module metadata object
|
2021-01-11 11:32:16 +01:00
|
|
|
*/
|
2021-01-27 14:27:54 +01:00
|
|
|
function loadMetadata() {
|
|
|
|
var path = modulePath(metadataFileName);
|
|
|
|
try {
|
|
|
|
var pkg = fs.readFileSync(path);
|
|
|
|
return JSON.parse(pkg);
|
|
|
|
}
|
|
|
|
catch (e) {
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
2021-01-27 14:27:54 +01:00
|
|
|
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;
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Install NPM module
|
|
|
|
* @param {string} module - module specification
|
|
|
|
*/
|
|
|
|
function installNPM(module) {
|
|
|
|
var [name, ver] = moduleName(module);
|
2021-01-27 14:27:54 +01:00
|
|
|
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);
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
2021-01-27 14:27:54 +01:00
|
|
|
clearInProgress(name);
|
|
|
|
var spec = name +(pkg ? ("@"+pkg.version) : "");
|
|
|
|
log.info("already installed: "+spec);
|
|
|
|
}
|
|
|
|
resolve(true);
|
|
|
|
});
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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)) {
|
2021-01-27 14:27:54 +01:00
|
|
|
var [n, v] = moduleName(name);
|
|
|
|
setInProgress(name);
|
2021-01-11 11:32:16 +01:00
|
|
|
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);
|
2021-01-27 14:27:54 +01:00
|
|
|
var path = modulePath("node_modules", name);
|
2021-01-11 11:32:16 +01:00
|
|
|
var npm = require(path);
|
|
|
|
return npm;
|
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get list of installed modules
|
|
|
|
*/
|
|
|
|
function listModules() {
|
2021-01-27 14:27:54 +01:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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",
|
2021-01-11 11:32:16 +01:00
|
|
|
};
|
2021-01-27 14:27:54 +01:00
|
|
|
items.push(item);
|
|
|
|
meta.modules = items;
|
|
|
|
saveMetadata(meta);
|
|
|
|
clearInProgress(name);
|
|
|
|
resolve(true);
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
2021-01-27 14:27:54 +01:00
|
|
|
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);
|
|
|
|
});
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
2021-01-27 14:27:54 +01:00
|
|
|
else {
|
|
|
|
clearInProgress(name);
|
|
|
|
var msg = "not "+acted+": "+name;
|
|
|
|
log.info(msg);
|
|
|
|
reject(msg);
|
|
|
|
}
|
|
|
|
});
|
2021-01-11 11:32:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
api = {
|
|
|
|
init: init,
|
|
|
|
register: register,
|
|
|
|
checkInstall: checkInstall,
|
|
|
|
load: load,
|
2021-01-27 14:27:54 +01:00
|
|
|
list: listModules,
|
|
|
|
uninstall: uninstall,
|
|
|
|
update: update
|
2021-01-11 11:32:16 +01:00
|
|
|
};
|
|
|
|
module.exports = api;
|