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

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