@node-red/registry
Advanced tools
Comparing version 1.2.9 to 1.3.0-beta.1
228
lib/index.js
@@ -1,2 +0,2 @@ | ||
/** | ||
/*! | ||
* Copyright JS Foundation and other contributors, http://js.foundation | ||
@@ -31,13 +31,28 @@ * | ||
var library = require("./library"); | ||
const externalModules = require("./externalModules") | ||
var plugins = require("./plugins"); | ||
var settings; | ||
/** | ||
* Initialise the registry with a reference to a runtime object | ||
* @param {Object} runtime - a runtime object | ||
* @memberof @node-red/registry | ||
*/ | ||
function init(runtime) { | ||
settings = runtime.settings; | ||
installer.init(runtime); | ||
installer.init(runtime.settings); | ||
// Loader requires the full runtime object because it initialises | ||
// the util module it. The Util module is responsible for constructing the | ||
// RED object passed to node modules when they are loaded. | ||
loader.init(runtime); | ||
registry.init(settings,loader,runtime.events); | ||
plugins.init(runtime.settings); | ||
registry.init(runtime.settings,loader); | ||
library.init(); | ||
externalModules.init(runtime.settings); | ||
} | ||
/** | ||
* Triggers the intial discovery and loading of all Node-RED node modules. | ||
* found on the node path. | ||
* @return {Promise} - resolves when the registry has finised discovering node modules. | ||
* @memberof @node-red/registry | ||
*/ | ||
function load() { | ||
@@ -70,34 +85,233 @@ registry.load(); | ||
clear: registry.clear, | ||
/** | ||
* Register a node constructor function. | ||
* | ||
* @param {Object} nodeSet - the Node Set object the constructor is for | ||
* @param {String} type - the node type | ||
* @param {Function} constructor - the node constructor function | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
registerType: registry.registerNodeConstructor, | ||
/** | ||
* Get a node constructor function. | ||
* | ||
* @param {String} type - the node type | ||
* @return {Function} the node constructor function | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
get: registry.getNodeConstructor, | ||
registerSubflow: registry.registerSubflow, | ||
/** | ||
* Get a node's set information. | ||
* | ||
* @param {String} type - the node type or set identifier | ||
* @return {Object} the node set information | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeInfo: registry.getNodeInfo, | ||
/** | ||
* Get a list of all nodes in the registry. | ||
* | ||
* @return {Object} the node list | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeList: registry.getNodeList, | ||
/** | ||
* Get a modules's information. | ||
* | ||
* @param {String} type - the module identifier | ||
* @return {Object} the module information | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getModuleInfo: registry.getModuleInfo, | ||
/** | ||
* Get a list of all moduless in the registry. | ||
* | ||
* @return {Object} the module list | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getModuleList: registry.getModuleList, | ||
/** | ||
* Get the HTML configs for all nodes in the registry. | ||
* | ||
* @param {String} lang - the language to return, default `en-US` | ||
* @return {String} the node configs | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeConfigs: registry.getAllNodeConfigs, | ||
/** | ||
* Get the HTML config for a single node set. | ||
* | ||
* @param {String} id - the node identifier | ||
* @param {String} lang - the language to return, default `en-US` | ||
* @return {String} the node config | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeConfig: registry.getNodeConfig, | ||
/** | ||
* Get the local path to a node's icon file. | ||
* | ||
* @param {String} module - the module that provides the icon | ||
* @param {String} icon - the name of the icon | ||
* @return {String} the local path to the icon | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeIconPath: registry.getNodeIconPath, | ||
/** | ||
* Get the full list of all icons available. | ||
* | ||
* @return {String} the icon list | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeIcons: registry.getNodeIcons, | ||
/** | ||
* Enables a node set, making it available for use. | ||
* | ||
* @param {String} type - the node type or set identifier | ||
* @return {Promise} A promise that resolves when the node set has been enabled | ||
* @throws if the identifier is not recognised or runtime settings are unavailable | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
enableNode: enableNodeSet, | ||
/** | ||
* Disables a node set, making it unavailable for use. | ||
* | ||
* @param {String} type - the node type or set identifier | ||
* @return {Promise} A promise that resolves when the node set has been disabled | ||
* @throws if the identifier is not recognised or runtime settings are unavailable | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
disableNode: registry.disableNodeSet, | ||
/** | ||
* Loads a new module into the registry. | ||
* | ||
* This will rescan the node module paths looking for this module. | ||
* | ||
* @param {String} module - the name of the module to add | ||
* @return {Promise<Object>} A promise that resolves with the module information once it has been added | ||
* @throws if the module has already been added or the runtime settings are unavailable | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
addModule: addModule, | ||
/** | ||
* Removes a module from the registry. | ||
* | ||
* @param {String} module - the name of the module to remove | ||
* @return {Promise<Array>} A promise that resolves with the list of removed node sets | ||
* @throws if the module is not found or the runtime settings are unavailable | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
removeModule: registry.removeModule, | ||
/** | ||
* Installs a new node module using npm and then add to the registry | ||
* | ||
* @param {String|Buffer} module - the name of the module to install, or a Buffer containing a module tar file | ||
* @param {String} version - the version of the module to install, default: `latest` | ||
* @param {String} url - (optional) a url to install the module from | ||
* @return {Promise<Array>} A promise that resolves with the module information once it has been installed | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
installModule: installer.installModule, | ||
/** | ||
* Uninstalls a module using npm | ||
* | ||
* @param {String} module - the name of the module to uninstall | ||
* @return {Promise<Array>} A promise that resolves when the module has been removed | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
uninstallModule: installer.uninstallModule, | ||
/** | ||
* Update to internal list of available modules based on what has been actually | ||
* loaded. | ||
* | ||
* The `externalModules.autoInstall` (previously `autoInstallModules`) | ||
* runtime option means the runtime may try to install | ||
* missing modules after the initial load is complete. If that flag is not set | ||
* this function is used to remove the modules from the registry's saved list. | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
cleanModuleList: registry.cleanModuleList, | ||
paletteEditorEnabled: installer.paletteEditorEnabled, | ||
/** | ||
* Check if the regisrty is able to install/remove modules. | ||
* | ||
* This is based on whether it has found `npm` on the command-line. | ||
* @return {Boolean} whether the installer is enabled | ||
* | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
installerEnabled: installer.installerEnabled, | ||
/** | ||
* Get a list of all example flows provided by nodes in the registry. | ||
* @return {Object} an object, indexed by module, listing all example flows | ||
* | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeExampleFlows: library.getExampleFlows, | ||
/** | ||
* Gets the full path to a node example | ||
* @param {String} module - the name of the module providing the example | ||
* @param {String} path - the relative path of the example | ||
* @return {String} the full path to the example | ||
* | ||
* @function | ||
* @memberof @node-red/registry | ||
*/ | ||
getNodeExampleFlowPath: library.getExampleFlowPath, | ||
checkFlowDependencies: externalModules.checkFlowDependencies, | ||
registerPlugin: plugins.registerPlugin, | ||
getPlugin: plugins.getPlugin, | ||
getPluginsByType: plugins.getPluginsByType, | ||
getPluginList: plugins.getPluginList, | ||
getPluginConfigs: plugins.getPluginConfigs, | ||
exportPluginSettings: plugins.exportPluginSettings, | ||
deprecated: require("./deprecated") | ||
}; |
@@ -18,19 +18,16 @@ /** | ||
var path = require("path"); | ||
var os = require("os"); | ||
var fs = require("fs-extra"); | ||
var tar = require("tar"); | ||
const path = require("path"); | ||
const os = require("os"); | ||
const fs = require("fs-extra"); | ||
const tar = require("tar"); | ||
var registry = require("./registry"); | ||
var library = require("./library"); | ||
var log; | ||
var exec; | ||
const registry = require("./registry"); | ||
const registryUtil = require("./util"); | ||
const library = require("./library"); | ||
const {exec,log,events} = require("@node-red/util"); | ||
const child_process = require('child_process'); | ||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; | ||
let installerEnabled = false; | ||
var events; | ||
var child_process = require('child_process'); | ||
var npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; | ||
var paletteEditorEnabled = false; | ||
var settings; | ||
let settings; | ||
const moduleRe = /^(@[^/@]+?[/])?[^/@]+?$/; | ||
@@ -41,7 +38,32 @@ const slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; | ||
function init(runtime) { | ||
events = runtime.events; | ||
settings = runtime.settings; | ||
log = runtime.log; | ||
exec = runtime.exec; | ||
// Default allow/deny lists | ||
let installAllowList = ['*']; | ||
let installDenyList = []; | ||
let installAllAllowed = true; | ||
let installVersionRestricted = false; | ||
function init(_settings) { | ||
settings = _settings; | ||
// TODO: This is duplicated in localfilesystem.js | ||
// Should it *all* be managed by util? | ||
if (settings.externalModules && settings.externalModules.palette) { | ||
if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) { | ||
installAllowList = settings.externalModules.palette.allowList; | ||
installDenyList = settings.externalModules.palette.denyList; | ||
} | ||
} | ||
installAllowList = registryUtil.parseModuleList(installAllowList); | ||
installDenyList = registryUtil.parseModuleList(installDenyList); | ||
installAllAllowed = installDenyList.length === 0; | ||
if (!installAllAllowed) { | ||
installAllowList.forEach(function(rule) { | ||
installVersionRestricted = installVersionRestricted || (!!rule.version); | ||
}) | ||
if (!installVersionRestricted) { | ||
installDenyList.forEach(function(rule) { | ||
installVersionRestricted = installVersionRestricted || (!!rule.version); | ||
}) | ||
} | ||
} | ||
} | ||
@@ -77,17 +99,3 @@ | ||
} | ||
function checkExistingModule(module,version) { | ||
var info = registry.getModuleInfo(module); | ||
if (info) { | ||
if (!version || info.version === version) { | ||
var err = new Error("Module already loaded"); | ||
err.code = "module_already_loaded"; | ||
throw err; | ||
} | ||
return true; | ||
} | ||
return false; | ||
} | ||
function installModule(module,version,url) { | ||
async function installModule(module,version,url) { | ||
if (Buffer.isBuffer(module)) { | ||
@@ -97,84 +105,117 @@ return installTarball(module) | ||
module = module || ""; | ||
activePromise = activePromise.then(() => { | ||
activePromise = activePromise.then(async function() { | ||
//TODO: ensure module is 'safe' | ||
return new Promise((resolve,reject) => { | ||
var installName = module; | ||
var isUpgrade = false; | ||
try { | ||
if (url) { | ||
if (pkgurlRe.test(url) || localtgzRe.test(url)) { | ||
// Git remote url or Tarball url - check the valid package url | ||
installName = url; | ||
} else { | ||
log.warn(log._("server.install.install-failed-url",{name:module,url:url})); | ||
const e = new Error("Invalid url"); | ||
e.code = "invalid_module_url"; | ||
reject(e); | ||
return; | ||
} | ||
} else if (moduleRe.test(module)) { | ||
// Simple module name - assume it can be npm installed | ||
if (version) { | ||
installName += "@"+version; | ||
} | ||
} else if (slashRe.test(module)) { | ||
// A path - check if there's a valid package.json | ||
installName = module; | ||
let info = checkModulePath(module); | ||
module = info.name; | ||
} else { | ||
log.warn(log._("server.install.install-failed-name",{name:module})); | ||
const e = new Error("Invalid module name"); | ||
e.code = "invalid_module_name"; | ||
reject(e); | ||
return; | ||
} | ||
isUpgrade = checkExistingModule(module,version); | ||
} catch(err) { | ||
return reject(err); | ||
var installName = module; | ||
let isRegistryPackage = true; | ||
var isUpgrade = false; | ||
var isExisting = false; | ||
if (url) { | ||
if (pkgurlRe.test(url) || localtgzRe.test(url)) { | ||
// Git remote url or Tarball url - check the valid package url | ||
installName = url; | ||
isRegistryPackage = false; | ||
} else { | ||
log.warn(log._("server.install.install-failed-url",{name:module,url:url})); | ||
const e = new Error("Invalid url"); | ||
e.code = "invalid_module_url"; | ||
throw e; | ||
} | ||
if (!isUpgrade) { | ||
log.info(log._("server.install.installing",{name: module,version: version||"latest"})); | ||
} else if (moduleRe.test(module)) { | ||
// Simple module name - assume it can be npm installed | ||
if (version) { | ||
installName += "@"+version; | ||
} | ||
} else if (slashRe.test(module)) { | ||
// A path - check if there's a valid package.json | ||
installName = module; | ||
let info = checkModulePath(module); | ||
module = info.name; | ||
isRegistryPackage = false; | ||
} else { | ||
log.warn(log._("server.install.install-failed-name",{name:module})); | ||
const e = new Error("Invalid module name"); | ||
e.code = "invalid_module_name"; | ||
throw e; | ||
} | ||
if (!installAllAllowed) { | ||
let installVersion = version; | ||
if (installVersionRestricted && isRegistryPackage) { | ||
installVersion = await getModuleVersionFromNPM(module, version); | ||
} | ||
if (!registryUtil.checkModuleAllowed(module,installVersion,installAllowList,installDenyList)) { | ||
const e = new Error("Install not allowed"); | ||
e.code = "install_not_allowed"; | ||
throw e; | ||
} | ||
} | ||
var info = registry.getModuleInfo(module); | ||
if (info) { | ||
if (!info.user) { | ||
log.debug(`Installing existing module: ${module}`) | ||
isExisting = true; | ||
} else if (!version || info.version === version) { | ||
var err = new Error("Module already loaded"); | ||
err.code = "module_already_loaded"; | ||
throw err; | ||
} | ||
isUpgrade = true; | ||
} else { | ||
isUpgrade = false; | ||
} | ||
if (!isUpgrade) { | ||
log.info(log._("server.install.installing",{name: module,version: version||"latest"})); | ||
} else { | ||
log.info(log._("server.install.upgrading",{name: module,version: version||"latest"})); | ||
} | ||
var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; | ||
var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production',installName]; | ||
log.trace(npmCommand + JSON.stringify(args)); | ||
return exec.run(npmCommand,args,{ | ||
cwd: installDir | ||
}, true).then(result => { | ||
if (isExisting) { | ||
// This is a module we already have installed as a non-user module. | ||
// That means it was discovered when loading, but was not listed | ||
// in package.json and has been hidden from the editor. | ||
// The user has requested to install this module. Having run | ||
// the npm install above, it will now be listed in package.json. | ||
// Update the registry to mark it as a user module so it will | ||
// be available to the editor. | ||
log.info(log._("server.install.installed",{name:module})); | ||
return require("./registry").setUserInstalled(module,true).then(reportAddedModules); | ||
} else if (!isUpgrade) { | ||
log.info(log._("server.install.installed",{name:module})); | ||
return require("./index").addModule(module).then(reportAddedModules); | ||
} else { | ||
log.info(log._("server.install.upgrading",{name: module,version: version||"latest"})); | ||
log.info(log._("server.install.upgraded",{name:module, version:version})); | ||
events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true}); | ||
return require("./registry").setModulePendingUpdated(module,version); | ||
} | ||
var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; | ||
var args = ['install','--no-audit','--no-update-notifier','--no-fund','--save','--save-prefix=~','--production',installName]; | ||
log.trace(npmCommand + JSON.stringify(args)); | ||
exec.run(npmCommand,args,{ | ||
cwd: installDir | ||
}, true).then(result => { | ||
if (!isUpgrade) { | ||
log.info(log._("server.install.installed",{name:module})); | ||
resolve(require("./index").addModule(module).then(reportAddedModules)); | ||
} else { | ||
log.info(log._("server.install.upgraded",{name:module, version:version})); | ||
events.emit("runtime-event",{id:"restart-required",payload:{type:"warning",text:"notification.warnings.restartRequired"},retain:true}); | ||
resolve(require("./registry").setModulePendingUpdated(module,version)); | ||
} | ||
}).catch(result => { | ||
var output = result.stderr; | ||
var e; | ||
var lookFor404 = new RegExp(" 404 .*"+module,"m"); | ||
var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m"); | ||
if (lookFor404.test(output)) { | ||
log.warn(log._("server.install.install-failed-not-found",{name:module})); | ||
e = new Error("Module not found"); | ||
e.code = 404; | ||
reject(e); | ||
} else if (isUpgrade && lookForVersionNotFound.test(output)) { | ||
log.warn(log._("server.install.upgrade-failed-not-found",{name:module})); | ||
e = new Error("Module not found"); | ||
e.code = 404; | ||
reject(e); | ||
} else { | ||
log.warn(log._("server.install.install-failed-long",{name:module})); | ||
log.warn("------------------------------------------"); | ||
log.warn(output); | ||
log.warn("------------------------------------------"); | ||
reject(new Error(log._("server.install.install-failed"))); | ||
} | ||
}) | ||
}); | ||
}).catch(result => { | ||
var output = result.stderr; | ||
var e; | ||
var lookFor404 = new RegExp(" 404 .*"+module,"m"); | ||
var lookForVersionNotFound = new RegExp("version not found: "+module+"@"+version,"m"); | ||
if (lookFor404.test(output)) { | ||
log.warn(log._("server.install.install-failed-not-found",{name:module})); | ||
e = new Error("Module not found"); | ||
e.code = 404; | ||
throw e; | ||
} else if (isUpgrade && lookForVersionNotFound.test(output)) { | ||
log.warn(log._("server.install.upgrade-failed-not-found",{name:module})); | ||
e = new Error("Module not found"); | ||
e.code = 404; | ||
throw e; | ||
} else { | ||
log.warn(log._("server.install.install-failed-long",{name:module})); | ||
log.warn("------------------------------------------"); | ||
log.warn(output); | ||
log.warn("------------------------------------------"); | ||
throw new Error(log._("server.install.install-failed")); | ||
} | ||
}) | ||
}).catch(err => { | ||
@@ -189,3 +230,2 @@ // In case of error, reset activePromise to be resolvable | ||
function reportAddedModules(info) { | ||
//comms.publish("node/added",info.nodes,false); | ||
if (info.nodes.length > 0) { | ||
@@ -229,3 +269,59 @@ log.info(log._("server.added-types")); | ||
async function getModuleVersionFromNPM(module, version) { | ||
let installName = module; | ||
if (version) { | ||
installName += "@" + version; | ||
} | ||
return new Promise((resolve, reject) => { | ||
child_process.execFile(npmCommand,['info','--json',installName],function(err,stdout,stderr) { | ||
try { | ||
if (!stdout) { | ||
log.warn(log._("server.install.install-failed-not-found",{name:module})); | ||
e = new Error("Version not found"); | ||
e.code = 404; | ||
reject(e); | ||
return; | ||
} | ||
const response = JSON.parse(stdout); | ||
if (response.error) { | ||
if (response.error.code === "E404") { | ||
log.warn(log._("server.install.install-failed-not-found",{name:module})); | ||
e = new Error("Module not found"); | ||
e.code = 404; | ||
reject(e); | ||
} else { | ||
log.warn(log._("server.install.install-failed-long",{name:module})); | ||
log.warn("------------------------------------------"); | ||
log.warn(response.error.summary); | ||
log.warn("------------------------------------------"); | ||
reject(new Error(log._("server.install.install-failed"))); | ||
} | ||
return; | ||
} else { | ||
resolve(response.version); | ||
} | ||
} catch(err) { | ||
log.warn(log._("server.install.install-failed-long",{name:module})); | ||
log.warn("------------------------------------------"); | ||
if (stdout) { | ||
log.warn(stdout); | ||
} | ||
if (stderr) { | ||
log.warn(stderr); | ||
} | ||
log.warn(err); | ||
log.warn("------------------------------------------"); | ||
reject(new Error(log._("server.install.install-failed"))); | ||
} | ||
}); | ||
}) | ||
} | ||
async function installTarball(tarball) { | ||
if (settings.externalModules && settings.externalModules.palette && settings.externalModules.palette.allowUpload === false) { | ||
throw new Error("Module upload disabled") | ||
} | ||
// Check this tarball contains a valid node-red module. | ||
@@ -351,3 +447,28 @@ // Get its module name/version | ||
function checkPrereq() { | ||
async function checkPrereq() { | ||
if (settings.editorTheme && settings.editorTheme.palette) { | ||
if (settings.editorTheme.palette.hasOwnProperty("editable")) { | ||
log.warn(log._("server.deprecatedOption",{old:"editorTheme.palette.editable", new:"externalModules.palette.allowInstall"})); | ||
} | ||
if (settings.editorTheme.palette.hasOwnProperty("upload")) { | ||
log.warn(log._("server.deprecatedOption",{old:"editorTheme.palette.upload", new:"externalModules.palette.allowUpload"})); | ||
} | ||
} | ||
try { | ||
if (settings.editorTheme.palette.editable === false) { | ||
log.info(log._("server.palette-editor.disabled")); | ||
installerEnabled = false; | ||
return | ||
} | ||
} catch(err) {} | ||
try { | ||
if (settings.externalModules.palette.allowInstall === false) { | ||
log.info(log._("server.palette-editor.disabled")); | ||
installerEnabled = false; | ||
return | ||
} | ||
} catch(err) {} | ||
if (settings.hasOwnProperty('editorTheme') && | ||
@@ -359,4 +480,3 @@ settings.editorTheme.hasOwnProperty('palette') && | ||
log.info(log._("server.palette-editor.disabled")); | ||
paletteEditorEnabled = false; | ||
return Promise.resolve(); | ||
installerEnabled = false; | ||
} else { | ||
@@ -367,9 +487,9 @@ return new Promise(resolve => { | ||
log.info(log._("server.palette-editor.npm-not-found")); | ||
paletteEditorEnabled = false; | ||
installerEnabled = false; | ||
} else { | ||
if (parseInt(stdout.split(".")[0]) < 3) { | ||
log.info(log._("server.palette-editor.npm-too-old")); | ||
paletteEditorEnabled = false; | ||
installerEnabled = false; | ||
} else { | ||
paletteEditorEnabled = true; | ||
installerEnabled = true; | ||
} | ||
@@ -388,5 +508,5 @@ } | ||
uninstallModule: uninstallModule, | ||
paletteEditorEnabled: function() { | ||
return paletteEditorEnabled | ||
installerEnabled: function() { | ||
return installerEnabled | ||
} | ||
} |
@@ -17,3 +17,3 @@ /** | ||
var fs = require('fs'); | ||
var fs = require('fs-extra'); | ||
var fspath = require('path'); | ||
@@ -26,36 +26,32 @@ | ||
function getFlowsFromPath(path) { | ||
return new Promise(function(resolve,reject) { | ||
var result = {}; | ||
fs.readdir(path,function(err,files) { | ||
var promises = []; | ||
var validFiles = []; | ||
if (files) { | ||
files.forEach(function(file) { | ||
var fullPath = fspath.join(path,file); | ||
var stats = fs.lstatSync(fullPath); | ||
if (stats.isDirectory()) { | ||
validFiles.push(file); | ||
promises.push(getFlowsFromPath(fullPath)); | ||
} else if (/\.json$/.test(file)){ | ||
validFiles.push(file); | ||
promises.push(Promise.resolve(file.split(".")[0])) | ||
} | ||
}) | ||
async function getFlowsFromPath(path) { | ||
var result = {}; | ||
var validFiles = []; | ||
return fs.readdir(path).then(files => { | ||
var promises = []; | ||
if (files) { | ||
files.forEach(function(file) { | ||
var fullPath = fspath.join(path,file); | ||
var stats = fs.lstatSync(fullPath); | ||
if (stats.isDirectory()) { | ||
validFiles.push(file); | ||
promises.push(getFlowsFromPath(fullPath)); | ||
} else if (/\.json$/.test(file)){ | ||
validFiles.push(file); | ||
promises.push(Promise.resolve(file.split(".")[0])) | ||
} | ||
}) | ||
} | ||
return Promise.all(promises) | ||
}).then(results => { | ||
results.forEach(function(r,i) { | ||
if (typeof r === 'string') { | ||
result.f = result.f||[]; | ||
result.f.push(r); | ||
} else { | ||
result.d = result.d||{}; | ||
result.d[validFiles[i]] = r; | ||
} | ||
var i=0; | ||
Promise.all(promises).then(function(results) { | ||
results.forEach(function(r) { | ||
if (typeof r === 'string') { | ||
result.f = result.f||[]; | ||
result.f.push(r); | ||
} else { | ||
result.d = result.d||{}; | ||
result.d[validFiles[i]] = r; | ||
} | ||
i++; | ||
}) | ||
resolve(result); | ||
}) | ||
}); | ||
}) | ||
return result; | ||
}) | ||
@@ -62,0 +58,0 @@ } |
@@ -17,3 +17,2 @@ /** | ||
var when = require("when"); | ||
var fs = require("fs-extra"); | ||
@@ -27,11 +26,10 @@ var path = require("path"); | ||
var i18n = require("@node-red/util").i18n; | ||
var log = require("@node-red/util").log; | ||
var settings; | ||
var runtime; | ||
function init(_runtime) { | ||
runtime = _runtime; | ||
settings = runtime.settings; | ||
localfilesystem.init(runtime); | ||
registryUtil.init(runtime); | ||
settings = _runtime.settings; | ||
localfilesystem.init(settings); | ||
registryUtil.init(_runtime); | ||
} | ||
@@ -43,60 +41,102 @@ | ||
// performance gains are minimal. | ||
//return loadNodeFiles(registry.getModuleList()); | ||
runtime.log.info(runtime.log._("server.loading")); | ||
//return loadModuleFiles(registry.getModuleList()); | ||
log.info(log._("server.loading")); | ||
var nodeFiles = localfilesystem.getNodeFiles(disableNodePathScan); | ||
return loadNodeFiles(nodeFiles); | ||
var modules = localfilesystem.getNodeFiles(disableNodePathScan); | ||
return loadModuleFiles(modules); | ||
} | ||
function loadNodeFiles(nodeFiles) { | ||
function loadModuleTypeFiles(module, type) { | ||
const things = module[type]; | ||
var first = true; | ||
var promises = []; | ||
var nodes = []; | ||
for (var module in nodeFiles) { | ||
for (var thingName in things) { | ||
/* istanbul ignore else */ | ||
if (nodeFiles.hasOwnProperty(module)) { | ||
if (nodeFiles[module].redVersion && | ||
!semver.satisfies(runtime.version().replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), nodeFiles[module].redVersion)) { | ||
if (things.hasOwnProperty(thingName)) { | ||
if (module.name != "node-red" && first) { | ||
// Check the module directory exists | ||
first = false; | ||
var fn = things[thingName].file; | ||
var parts = fn.split("/"); | ||
var i = parts.length-1; | ||
for (;i>=0;i--) { | ||
if (parts[i] == "node_modules") { | ||
break; | ||
} | ||
} | ||
var moduleFn = parts.slice(0,i+2).join("/"); | ||
try { | ||
var stat = fs.statSync(moduleFn); | ||
} catch(err) { | ||
// Module not found, don't attempt to load its nodes | ||
break; | ||
} | ||
} | ||
try { | ||
var promise; | ||
if (type === "nodes") { | ||
promise = loadNodeConfig(things[thingName]); | ||
} else if (type === "plugins") { | ||
promise = loadPluginConfig(things[thingName]); | ||
} | ||
promises.push( | ||
promise.then( | ||
(function() { | ||
var m = module.name; | ||
var n = thingName; | ||
return function(nodeSet) { | ||
things[n] = nodeSet; | ||
return nodeSet; | ||
} | ||
})() | ||
).catch(err => {console.log(err)}) | ||
); | ||
} catch(err) { | ||
console.log(err) | ||
// | ||
} | ||
} | ||
} | ||
return promises; | ||
} | ||
function loadModuleFiles(modules) { | ||
var pluginPromises = []; | ||
var nodePromises = []; | ||
for (var module in modules) { | ||
/* istanbul ignore else */ | ||
if (modules.hasOwnProperty(module)) { | ||
if (modules[module].redVersion && | ||
!semver.satisfies((settings.version||"0.0.0").replace(/(\-[1-9A-Za-z-][0-9A-Za-z-\.]*)?(\+[0-9A-Za-z-\.]+)?$/,""), modules[module].redVersion)) { | ||
//TODO: log it | ||
runtime.log.warn("["+module+"] "+runtime.log._("server.node-version-mismatch",{version:nodeFiles[module].redVersion})); | ||
nodeFiles[module].err = "version_mismatch"; | ||
log.warn("["+module+"] "+log._("server.node-version-mismatch",{version:modules[module].redVersion})); | ||
modules[module].err = "version_mismatch"; | ||
continue; | ||
} | ||
if (module == "node-red" || !registry.getModuleInfo(module)) { | ||
var first = true; | ||
for (var node in nodeFiles[module].nodes) { | ||
/* istanbul ignore else */ | ||
if (nodeFiles[module].nodes.hasOwnProperty(node)) { | ||
if (module != "node-red" && first) { | ||
// Check the module directory exists | ||
first = false; | ||
var fn = nodeFiles[module].nodes[node].file; | ||
var parts = fn.split("/"); | ||
var i = parts.length-1; | ||
for (;i>=0;i--) { | ||
if (parts[i] == "node_modules") { | ||
break; | ||
} | ||
} | ||
var moduleFn = parts.slice(0,i+2).join("/"); | ||
if (modules[module].nodes) { | ||
nodePromises = nodePromises.concat(loadModuleTypeFiles(modules[module], "nodes")); | ||
} | ||
if (modules[module].plugins) { | ||
pluginPromises = pluginPromises.concat(loadModuleTypeFiles(modules[module], "plugins")); | ||
} | ||
} | ||
} | ||
} | ||
var pluginList; | ||
var nodeList; | ||
try { | ||
var stat = fs.statSync(moduleFn); | ||
} catch(err) { | ||
// Module not found, don't attempt to load its nodes | ||
break; | ||
} | ||
} | ||
try { | ||
promises.push(loadNodeConfig(nodeFiles[module].nodes[node]).then((function() { | ||
var m = module; | ||
var n = node; | ||
return function(nodeSet) { | ||
nodeFiles[m].nodes[n] = nodeSet; | ||
nodes.push(nodeSet); | ||
} | ||
})())); | ||
} catch(err) { | ||
// | ||
} | ||
return Promise.all(pluginPromises).then(function(results) { | ||
pluginList = results.filter(r => !!r); | ||
// Initial plugin load has happened. Ensure modules that provide | ||
// plugins are in the registry now. | ||
for (var module in modules) { | ||
if (modules.hasOwnProperty(module)) { | ||
if (modules[module].plugins && Object.keys(modules[module].plugins).length > 0) { | ||
// Add the modules for plugins | ||
if (!modules[module].err) { | ||
registry.addModule(modules[module]); | ||
} | ||
@@ -106,15 +146,35 @@ } | ||
} | ||
} | ||
return when.settle(promises).then(function(results) { | ||
for (var module in nodeFiles) { | ||
if (nodeFiles.hasOwnProperty(module)) { | ||
if (!nodeFiles[module].err) { | ||
registry.addModule(nodeFiles[module]); | ||
return loadNodeSetList(pluginList); | ||
}).then(function() { | ||
return Promise.all(nodePromises); | ||
}).then(function(results) { | ||
nodeList = results.filter(r => !!r); | ||
// Initial node load has happened. Ensure remaining modules are in the registry | ||
for (var module in modules) { | ||
if (modules.hasOwnProperty(module)) { | ||
if (!modules[module].plugins || Object.keys(modules[module].plugins).length === 0) { | ||
if (!modules[module].err) { | ||
registry.addModule(modules[module]); | ||
} | ||
} | ||
} | ||
} | ||
return loadNodeSetList(nodes); | ||
return loadNodeSetList(nodeList); | ||
}); | ||
} | ||
async function loadPluginTemplate(plugin) { | ||
return fs.readFile(plugin.template,'utf8').then(content => { | ||
plugin.config = content; | ||
return plugin; | ||
}).catch(err => { | ||
if (err.code === 'ENOENT') { | ||
plugin.err = "Error: "+plugin.template+" does not exist"; | ||
} else { | ||
plugin.err = err.toString(); | ||
} | ||
return plugin; | ||
}); | ||
} | ||
async function loadNodeTemplate(node) { | ||
@@ -167,9 +227,6 @@ return fs.readFile(node.template,'utf8').then(content => { | ||
}).catch(err => { | ||
node.types = []; | ||
if (err.code === 'ENOENT') { | ||
if (!node.types) { | ||
node.types = []; | ||
} | ||
node.err = "Error: "+node.template+" does not exist"; | ||
} else { | ||
// ENOENT means no html file. We can live with that. But any other error | ||
// should be fatal | ||
// node.err = "Error: "+node.template+" does not exist"; | ||
if (err.code !== 'ENOENT') { | ||
node.types = []; | ||
@@ -188,7 +245,8 @@ node.err = err.toString(); | ||
} | ||
return fs.stat(path.join(path.dirname(node.file),"locales")).then(stat => { | ||
const baseFile = node.file||node.template; | ||
return fs.stat(path.join(path.dirname(baseFile),"locales")).then(stat => { | ||
node.namespace = node.id; | ||
return i18n.registerMessageCatalog(node.id, | ||
path.join(path.dirname(node.file),"locales"), | ||
path.basename(node.file,".js")+".json") | ||
path.join(path.dirname(baseFile),"locales"), | ||
path.basename(baseFile).replace(/\.[^.]+$/,".json")) | ||
.then(() => node); | ||
@@ -218,2 +276,3 @@ }).catch(err => { | ||
var node = { | ||
type: "node", | ||
id: id, | ||
@@ -242,2 +301,54 @@ module: module, | ||
async function loadPluginConfig(fileInfo) { | ||
var file = fileInfo.file; | ||
var module = fileInfo.module; | ||
var name = fileInfo.name; | ||
var version = fileInfo.version; | ||
var id = module + "/" + name; | ||
var isEnabled = true; | ||
// TODO: registry.getPluginInfo | ||
// var info = registry.getPluginInfo(id); | ||
// if (info) { | ||
// if (info.hasOwnProperty("loaded")) { | ||
// throw new Error(file+" already loaded"); | ||
// } | ||
// isEnabled = info.enabled; | ||
// } | ||
if (!fs.existsSync(jsFile)) { | ||
} | ||
var plugin = { | ||
type: "plugin", | ||
id: id, | ||
module: module, | ||
name: name, | ||
enabled: isEnabled, | ||
loaded:false, | ||
version: version, | ||
local: fileInfo.local, | ||
plugins: [], | ||
config: "", | ||
help: {} | ||
}; | ||
var jsFile = file.replace(/\.[^.]+$/,".js"); | ||
var htmlFile = file.replace(/\.[^.]+$/,".html"); | ||
if (fs.existsSync(jsFile)) { | ||
plugin.file = jsFile; | ||
} | ||
if (fs.existsSync(htmlFile)) { | ||
plugin.template = htmlFile; | ||
} | ||
await loadNodeLocales(plugin) | ||
if (plugin.template && !settings.disableEditor) { | ||
return loadPluginTemplate(plugin); | ||
} | ||
return plugin | ||
} | ||
/** | ||
@@ -252,4 +363,2 @@ * Loads the specified node into the runtime | ||
function loadNodeSet(node) { | ||
var nodeDir = path.dirname(node.file); | ||
var nodeFn = path.basename(node.file); | ||
if (!node.enabled) { | ||
@@ -301,2 +410,46 @@ return Promise.resolve(node); | ||
async function loadPlugin(plugin) { | ||
if (!plugin.file) { | ||
// No runtime component - nothing to load | ||
return plugin; | ||
} | ||
try { | ||
var r = require(plugin.file); | ||
if (typeof r === "function") { | ||
var red = registryUtil.createNodeApi(plugin); | ||
var promise = r(red); | ||
if (promise != null && typeof promise.then === "function") { | ||
return promise.then(function() { | ||
plugin.enabled = true; | ||
plugin.loaded = true; | ||
return plugin; | ||
}).catch(function(err) { | ||
plugin.err = err; | ||
return plugin; | ||
}); | ||
} | ||
} | ||
plugin.enabled = true; | ||
plugin.loaded = true; | ||
return plugin; | ||
} catch(err) { | ||
console.log(err); | ||
plugin.err = err; | ||
var stack = err.stack; | ||
var message; | ||
if (stack) { | ||
var i = stack.indexOf(plugin.file); | ||
if (i > -1) { | ||
var excerpt = stack.substring(i+node.file.length+1,i+plugin.file.length+20); | ||
var m = /^(\d+):(\d+)/.exec(excerpt); | ||
if (m) { | ||
plugin.err = err+" (line:"+m[1]+")"; | ||
} | ||
} | ||
} | ||
return plugin; | ||
} | ||
} | ||
function loadNodeSetList(nodes) { | ||
@@ -306,3 +459,7 @@ var promises = []; | ||
if (!node.err) { | ||
promises.push(loadNodeSet(node)); | ||
if (node.type === "plugin") { | ||
promises.push(loadPlugin(node).catch(err => {})); | ||
} else { | ||
promises.push(loadNodeSet(node).catch(err => {})); | ||
} | ||
} else { | ||
@@ -313,3 +470,3 @@ promises.push(node); | ||
return when.settle(promises).then(function() { | ||
return Promise.all(promises).then(function() { | ||
if (settings.available()) { | ||
@@ -328,3 +485,4 @@ return registry.saveNodeList(); | ||
var nodes = []; | ||
if (registry.getModuleInfo(module)) { | ||
var existingInfo = registry.getModuleInfo(module); | ||
if (existingInfo) { | ||
// TODO: nls | ||
@@ -336,4 +494,25 @@ var e = new Error("module_already_loaded"); | ||
try { | ||
var moduleFiles = localfilesystem.getModuleFiles(module); | ||
return loadNodeFiles(moduleFiles); | ||
var moduleFiles = {}; | ||
var moduleStack = [module]; | ||
while(moduleStack.length > 0) { | ||
var moduleToLoad = moduleStack.shift(); | ||
var files = localfilesystem.getModuleFiles(moduleToLoad); | ||
if (files[moduleToLoad]) { | ||
moduleFiles[moduleToLoad] = files[moduleToLoad]; | ||
if (moduleFiles[moduleToLoad].dependencies) { | ||
log.debug(`Loading dependencies for ${module}`) | ||
for (var i=0; i<moduleFiles[moduleToLoad].dependencies.length; i++) { | ||
var dep = moduleFiles[moduleToLoad].dependencies[i] | ||
if (!registry.getModuleInfo(dep)) { | ||
log.debug(` - load ${dep}`) | ||
moduleStack.push(dep); | ||
} else { | ||
log.debug(` - already loaded ${dep}`) | ||
registry.addModuleDependency(dep,moduleToLoad) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return loadModuleFiles(moduleFiles).then(() => module) | ||
} catch(err) { | ||
@@ -340,0 +519,0 @@ return Promise.reject(err); |
@@ -17,10 +17,12 @@ /** | ||
var fs = require("fs"); | ||
var path = require("path"); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const log = require("@node-red/util").log; | ||
const i18n = require("@node-red/util").i18n; | ||
const registryUtil = require("./util"); | ||
var events; | ||
var log; | ||
// Default allow/deny lists | ||
let loadAllowList = ['*']; | ||
let loadDenyList = []; | ||
var log = require("@node-red/util").log; | ||
var i18n = require("@node-red/util").i18n; | ||
@@ -30,6 +32,16 @@ var settings; | ||
var iconFileExtensions = [".png", ".gif", ".svg"]; | ||
var packageList = {}; | ||
function init(runtime) { | ||
settings = runtime.settings; | ||
events = runtime.events; | ||
function init(_settings) { | ||
settings = _settings; | ||
// TODO: This is duplicated in installer.js | ||
// Should it *all* be managed by util? | ||
if (settings.externalModules && settings.externalModules.palette) { | ||
if (settings.externalModules.palette.allowList || settings.externalModules.palette.denyList) { | ||
loadAllowList = settings.externalModules.palette.allowList; | ||
loadDenyList = settings.externalModules.palette.denyList; | ||
} | ||
} | ||
loadAllowList = registryUtil.parseModuleList(loadAllowList); | ||
loadDenyList = registryUtil.parseModuleList(loadDenyList); | ||
} | ||
@@ -80,3 +92,2 @@ | ||
* Synchronously walks the directory looking for node files. | ||
* Emits 'node-icon-dir' events for an icon dirs found | ||
* @param dir the directory to search | ||
@@ -149,4 +160,8 @@ * @return an array of fully-qualified paths to .js files | ||
if (pkg['node-red']) { | ||
var moduleDir = path.join(dir,fn); | ||
results.push({dir:moduleDir,package:pkg}); | ||
if (!registryUtil.checkModuleAllowed(pkg.name,pkg.version,loadAllowList,loadDenyList)) { | ||
log.debug("! Module: "+pkg.name+" "+pkg.version+ " *ignored due to denyList*"); | ||
} else { | ||
var moduleDir = path.join(dir,fn); | ||
results.push({dir:moduleDir,package:pkg}); | ||
} | ||
} | ||
@@ -180,5 +195,13 @@ } catch(err) { | ||
if (settings.userDir) { | ||
packageList = getPackageList(); | ||
userDir = path.join(settings.userDir,"node_modules"); | ||
results = scanDirForNodesModules(userDir,moduleName); | ||
results.forEach(function(r) { r.local = true; }); | ||
results.forEach(function(r) { | ||
// If it was found in <userDir>/node_modules then it is considered | ||
// a local module. | ||
// Also check to see if it is listed in the package.json file as a user-installed | ||
// module. This distinguishes modules installed as a dependency | ||
r.local = true; | ||
r.user = !!packageList[r.package.name]; | ||
}); | ||
} | ||
@@ -205,30 +228,38 @@ | ||
var nodes = pkg['node-red'].nodes||{}; | ||
var results = []; | ||
var iconDirs = []; | ||
var iconList = []; | ||
for (var n in nodes) { | ||
/* istanbul ignore else */ | ||
if (nodes.hasOwnProperty(n)) { | ||
var file = path.join(moduleDir,nodes[n]); | ||
results.push({ | ||
file: file, | ||
module: pkg.name, | ||
name: n, | ||
version: pkg.version | ||
}); | ||
var iconDir = path.join(moduleDir,path.dirname(nodes[n]),"icons"); | ||
if (iconDirs.indexOf(iconDir) == -1) { | ||
try { | ||
fs.statSync(iconDir); | ||
var icons = scanIconDir(iconDir); | ||
iconList.push({path:iconDir,icons:icons}); | ||
iconDirs.push(iconDir); | ||
} catch(err) { | ||
function scanTypes(types) { | ||
const files = []; | ||
for (var n in types) { | ||
/* istanbul ignore else */ | ||
if (types.hasOwnProperty(n)) { | ||
var file = path.join(moduleDir,types[n]); | ||
files.push({ | ||
file: file, | ||
module: pkg.name, | ||
name: n, | ||
version: pkg.version | ||
}); | ||
var iconDir = path.join(moduleDir,path.dirname(types[n]),"icons"); | ||
if (iconDirs.indexOf(iconDir) == -1) { | ||
try { | ||
fs.statSync(iconDir); | ||
var icons = scanIconDir(iconDir); | ||
iconList.push({path:iconDir,icons:icons}); | ||
iconDirs.push(iconDir); | ||
} catch(err) { | ||
} | ||
} | ||
} | ||
} | ||
return files; | ||
} | ||
var result = {files:results,icons:iconList}; | ||
var result = { | ||
nodeFiles:scanTypes(pkg['node-red'].nodes||{}), | ||
pluginFiles:scanTypes(pkg['node-red'].plugins||{}), | ||
icons:iconList | ||
}; | ||
var examplesDir = path.join(moduleDir,"examples"); | ||
@@ -238,3 +269,2 @@ try { | ||
result.examples = {path:examplesDir}; | ||
// events.emit("node-examples-dir",{name:pkg.name,path:examplesDir}); | ||
} catch(err) { | ||
@@ -285,16 +315,17 @@ } | ||
var coreNodeEntry = { | ||
name: "node-red", | ||
version: settings.version, | ||
nodes: {}, | ||
icons: iconList | ||
} | ||
var nodeList = { | ||
"node-red": { | ||
name: "node-red", | ||
version: settings.version, | ||
nodes: {}, | ||
icons: iconList | ||
} | ||
} | ||
"node-red": coreNodeEntry | ||
}; | ||
nodeFiles.forEach(function(node) { | ||
nodeList["node-red"].nodes[node.name] = node; | ||
coreNodeEntry.nodes[node.name] = node; | ||
}); | ||
if (settings.coreNodesDir) { | ||
var examplesDir = path.join(settings.coreNodesDir,"examples"); | ||
nodeList["node-red"].examples = {path: examplesDir}; | ||
coreNodeEntry.examples = {path: examplesDir}; | ||
} | ||
@@ -308,3 +339,2 @@ | ||
// update a module they may not otherwise be able to touch | ||
moduleFiles.sort(function(A,B) { | ||
@@ -322,3 +352,3 @@ if (A.local && !B.local) { | ||
if (!knownModules[mod.package.name]) { | ||
knownModules[mod.package.name] = true; | ||
knownModules[mod.package.name] = mod; | ||
result = true; | ||
@@ -328,27 +358,30 @@ } else { | ||
} | ||
log.debug("Module: "+mod.package.name+" "+mod.package.version+(result?"":" *ignored due to local copy*")); | ||
log.debug(" "+mod.dir); | ||
log.debug((result?"":"! ")+"Module: "+mod.package.name+" "+mod.package.version+" "+mod.dir+(result?"":" *ignored due to local copy*")); | ||
return result; | ||
}); | ||
moduleFiles.forEach(function(moduleFile) { | ||
var nodeModuleFiles = getModuleNodeFiles(moduleFile); | ||
nodeList[moduleFile.package.name] = { | ||
name: moduleFile.package.name, | ||
version: moduleFile.package.version, | ||
path: moduleFile.dir, | ||
local: moduleFile.local||false, | ||
nodes: {}, | ||
icons: nodeModuleFiles.icons, | ||
examples: nodeModuleFiles.examples | ||
}; | ||
if (moduleFile.package['node-red'].version) { | ||
nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version; | ||
// Do a second pass to check we have all the declared node dependencies | ||
// As this is only done as part of the initial palette load, `knownModules` will | ||
// contain a list of everything discovered during this phase. This means | ||
// we can check for missing dependencies here. | ||
moduleFiles = moduleFiles.filter(function(mod) { | ||
if (Array.isArray(mod.package["node-red"].dependencies)) { | ||
const deps = mod.package["node-red"].dependencies; | ||
const missingDeps = mod.package["node-red"].dependencies.filter(dep => { | ||
if (knownModules[dep]) { | ||
knownModules[dep].usedBy = knownModules[dep].usedBy || []; | ||
knownModules[dep].usedBy.push(mod.package.name) | ||
} else { | ||
return true; | ||
} | ||
}) | ||
if (missingDeps.length > 0) { | ||
log.error(`Module: ${mod.package.name} missing dependencies:`); | ||
missingDeps.forEach(m => { log.error(` - ${m}`)}); | ||
return false; | ||
} | ||
} | ||
nodeModuleFiles.files.forEach(function(node) { | ||
node.local = moduleFile.local||false; | ||
nodeList[moduleFile.package.name].nodes[node.name] = node; | ||
}); | ||
nodeFiles = nodeFiles.concat(nodeModuleFiles.files); | ||
return true; | ||
}); | ||
nodeList = convertModuleFileListToObject(moduleFiles, nodeList); | ||
} else { | ||
@@ -361,4 +394,3 @@ // console.log("node path scan disabled"); | ||
function getModuleFiles(module) { | ||
var nodeList = {}; | ||
// Update the package list | ||
var moduleFiles = scanTreeForNodesModules(module); | ||
@@ -370,4 +402,12 @@ if (moduleFiles.length === 0) { | ||
} | ||
// Unlike when doing the initial palette load, this call cannot verify the | ||
// dependencies of the new module as it doesn't have visiblity of what | ||
// is in the registry. That will have to be done be the caller in loader.js | ||
return convertModuleFileListToObject(moduleFiles); | ||
} | ||
function convertModuleFileListToObject(moduleFiles,seedObject) { | ||
const nodeList = seedObject || {}; | ||
moduleFiles.forEach(function(moduleFile) { | ||
var nodeModuleFiles = getModuleNodeFiles(moduleFile); | ||
@@ -378,3 +418,6 @@ nodeList[moduleFile.package.name] = { | ||
path: moduleFile.dir, | ||
local: moduleFile.local||false, | ||
user: moduleFile.user||false, | ||
nodes: {}, | ||
plugins: {}, | ||
icons: nodeModuleFiles.icons, | ||
@@ -386,6 +429,16 @@ examples: nodeModuleFiles.examples | ||
} | ||
nodeModuleFiles.files.forEach(function(node) { | ||
if (moduleFile.package['node-red'].dependencies) { | ||
nodeList[moduleFile.package.name].dependencies = moduleFile.package['node-red'].dependencies; | ||
} | ||
if (moduleFile.usedBy) { | ||
nodeList[moduleFile.package.name].usedBy = moduleFile.usedBy; | ||
} | ||
nodeModuleFiles.nodeFiles.forEach(function(node) { | ||
nodeList[moduleFile.package.name].nodes[node.name] = node; | ||
nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false; | ||
}); | ||
nodeModuleFiles.pluginFiles.forEach(function(plugin) { | ||
nodeList[moduleFile.package.name].plugins[plugin.name] = plugin; | ||
nodeList[moduleFile.package.name].plugins[plugin.name].local = moduleFile.local || false; | ||
}); | ||
}); | ||
@@ -418,2 +471,19 @@ return nodeList; | ||
} | ||
/** | ||
* Gets the list of modules installed in this runtime as reported by package.json | ||
* Note: these may include non-Node-RED modules | ||
*/ | ||
function getPackageList() { | ||
var list = {}; | ||
if (settings.userDir) { | ||
try { | ||
var userPackage = path.join(settings.userDir,"package.json"); | ||
var pkg = JSON.parse(fs.readFileSync(userPackage,"utf-8")); | ||
return pkg.dependencies; | ||
} catch(err) { | ||
log.error(err); | ||
} | ||
} | ||
return list; | ||
} | ||
@@ -420,0 +490,0 @@ module.exports = { |
@@ -22,4 +22,5 @@ /** | ||
var library = require("./library"); | ||
var events; | ||
const {events} = require("@node-red/util") | ||
var subflows = require("./subflow"); | ||
var externalModules = require("./externalModules") | ||
var settings; | ||
@@ -32,14 +33,12 @@ var loader; | ||
var nodeConstructors = {}; | ||
var nodeOptions = {}; | ||
var subflowModules = {}; | ||
var nodeTypeToId = {}; | ||
var moduleNodes = {}; | ||
function init(_settings,_loader, _events) { | ||
function init(_settings,_loader) { | ||
settings = _settings; | ||
loader = _loader; | ||
events = _events; | ||
moduleNodes = {}; | ||
nodeTypeToId = {}; | ||
nodeConstructors = {}; | ||
nodeList = []; | ||
nodeConfigCache = {}; | ||
clear(); | ||
} | ||
@@ -61,3 +60,4 @@ | ||
enabled: n.enabled, | ||
local: n.local||false | ||
local: n.local||false, | ||
user: n.user || false | ||
}; | ||
@@ -70,2 +70,9 @@ if (n.hasOwnProperty("module")) { | ||
} | ||
if (n.hasOwnProperty("plugins")) { | ||
r.plugins = n.plugins; | ||
} | ||
if (n.type === "plugin") { | ||
r.editor = !!n.template; | ||
r.runtime = !!n.file; | ||
} | ||
return r; | ||
@@ -76,3 +83,3 @@ } | ||
function getModule(id) { | ||
function getModuleFromSetId(id) { | ||
var parts = id.split("/"); | ||
@@ -82,3 +89,3 @@ return parts.slice(0,parts.length-1).join("/"); | ||
function getNode(id) { | ||
function getNodeFromSetId(id) { | ||
var parts = id.split("/"); | ||
@@ -101,2 +108,3 @@ return parts[parts.length-1]; | ||
local: moduleConfigs[module].local||false, | ||
user: moduleConfigs[module].user||false, | ||
nodes: {} | ||
@@ -187,2 +195,3 @@ }; | ||
moduleConfigs[module.name] = module; | ||
// console.log("registry.js.addModule",module.name,"user?",module.user,"usedBy",module.usedBy,"dependencies",module.dependencies) | ||
for (var setName in module.nodes) { | ||
@@ -226,7 +235,7 @@ if (module.nodes.hasOwnProperty(setName)) { | ||
function removeNode(id) { | ||
var config = moduleConfigs[getModule(id)].nodes[getNode(id)]; | ||
var config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; | ||
if (!config) { | ||
throw new Error("Unrecognised id: "+id); | ||
} | ||
delete moduleConfigs[getModule(id)].nodes[getNode(id)]; | ||
delete moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; | ||
var i = nodeList.indexOf(id); | ||
@@ -239,3 +248,5 @@ if (i > -1) { | ||
if (typeId === id) { | ||
delete subflowModules[t]; | ||
delete nodeConstructors[t]; | ||
delete nodeOptions[t]; | ||
delete nodeTypeToId[t]; | ||
@@ -250,17 +261,43 @@ } | ||
function removeModule(module) { | ||
function removeModule(name,skipSave) { | ||
if (!settings.available()) { | ||
throw new Error("Settings unavailable"); | ||
} | ||
var nodes = moduleNodes[module]; | ||
var infoList = []; | ||
var module = moduleConfigs[name]; | ||
var nodes = moduleNodes[name]; | ||
if (!nodes) { | ||
throw new Error("Unrecognised module: "+module); | ||
throw new Error("Unrecognised module: "+name); | ||
} | ||
var infoList = []; | ||
for (var i=0;i<nodes.length;i++) { | ||
infoList.push(removeNode(module+"/"+nodes[i])); | ||
if (module.usedBy && module.usedBy > 0) { | ||
// We are removing a module that is used by other modules... so whilst | ||
// this module should be removed from the editor palette, it needs to | ||
// stay in the runtime... for now. | ||
module.user = false; | ||
for (var i=0;i<nodes.length;i++) { | ||
infoList.push(filterNodeInfo(nodes[i])); | ||
} | ||
} else { | ||
if (module.dependencies) { | ||
module.dependencies.forEach(function(dep) { | ||
// Check each dependency of this module to see if it is a non-user-installed | ||
// module that we can expect to disappear once npm uninstall is run | ||
if (!moduleConfigs[dep].user) { | ||
moduleConfigs[dep].usedBy = moduleConfigs[dep].usedBy.filter(m => m !== name); | ||
if (moduleConfigs[dep].usedBy.length === 0) { | ||
// Remove the dependency | ||
removeModule(dep,true); | ||
} | ||
} | ||
}); | ||
} | ||
for (var i=0;i<nodes.length;i++) { | ||
infoList.push(removeNode(name+"/"+nodes[i])); | ||
} | ||
delete moduleNodes[name]; | ||
delete moduleConfigs[name]; | ||
} | ||
delete moduleNodes[module]; | ||
delete moduleConfigs[module]; | ||
saveNodeList(); | ||
if (!skipSave) { | ||
saveNodeList(); | ||
} | ||
return infoList; | ||
@@ -276,5 +313,5 @@ } | ||
if (id) { | ||
var module = moduleConfigs[getModule(id)]; | ||
var module = moduleConfigs[getModuleFromSetId(id)]; | ||
if (module) { | ||
var config = module.nodes[getNode(id)]; | ||
var config = module.nodes[getNodeFromSetId(id)]; | ||
if (config) { | ||
@@ -306,5 +343,5 @@ var info = filterNodeInfo(config); | ||
if (id) { | ||
var module = moduleConfigs[getModule(id)]; | ||
var module = moduleConfigs[getModuleFromSetId(id)]; | ||
if (module) { | ||
return module.nodes[getNode(id)]; | ||
return module.nodes[getNodeFromSetId(id)]; | ||
} | ||
@@ -320,2 +357,5 @@ } | ||
if (moduleConfigs.hasOwnProperty(module)) { | ||
if (!moduleConfigs[module].user && (moduleConfigs[module].usedBy && moduleConfigs[module].usedBy.length > 0)) { | ||
continue; | ||
} | ||
var nodes = moduleConfigs[module].nodes; | ||
@@ -341,13 +381,7 @@ for (var node in nodes) { | ||
function getModuleList() { | ||
//var list = []; | ||
//for (var module in moduleNodes) { | ||
// /* istanbul ignore else */ | ||
// if (moduleNodes.hasOwnProperty(module)) { | ||
// list.push(registry.getModuleInfo(module)); | ||
// } | ||
//} | ||
//return list; | ||
return moduleConfigs; | ||
} | ||
function getModule(id) { | ||
return moduleConfigs[id]; | ||
} | ||
@@ -361,5 +395,9 @@ function getModuleInfo(module) { | ||
local: moduleConfigs[module].local, | ||
user: moduleConfigs[module].user, | ||
path: moduleConfigs[module].path, | ||
nodes: [] | ||
}; | ||
if (moduleConfigs[module].dependencies) { | ||
m.dependencies = moduleConfigs[module].dependencies; | ||
} | ||
if (moduleConfigs[module] && moduleConfigs[module].pending_version) { | ||
@@ -391,3 +429,3 @@ m.pending_version = moduleConfigs[module].pending_version; | ||
function registerNodeConstructor(nodeSet,type,constructor) { | ||
function registerNodeConstructor(nodeSet,type,constructor,options) { | ||
if (nodeConstructors.hasOwnProperty(type)) { | ||
@@ -412,5 +450,35 @@ throw new Error(type+" already registered"); | ||
nodeConstructors[type] = constructor; | ||
nodeOptions[type] = options; | ||
if (options) { | ||
if (options.dynamicModuleList) { | ||
externalModules.register(type,options.dynamicModuleList); | ||
} | ||
} | ||
events.emit("type-registered",type); | ||
} | ||
function registerSubflow(nodeSet, subflow) { | ||
var nodeSetInfo = getFullNodeInfo(nodeSet); | ||
const result = subflows.register(nodeSet,subflow); | ||
if (subflowModules.hasOwnProperty(result.type)) { | ||
throw new Error(result.type+" already registered"); | ||
} | ||
if (nodeSetInfo) { | ||
if (nodeSetInfo.types.indexOf(result.type) === -1) { | ||
nodeSetInfo.types.push(result.type); | ||
nodeTypeToId[result.type] = nodeSetInfo.id; | ||
} | ||
nodeSetInfo.config = result.config; | ||
} | ||
subflowModules[result.type] = result; | ||
externalModules.registerSubflow(result.type,subflow); | ||
events.emit("type-registered",result.type); | ||
return result; | ||
} | ||
function getAllNodeConfigs(lang) { | ||
@@ -422,3 +490,7 @@ if (!nodeConfigCache[lang]) { | ||
var id = nodeList[i]; | ||
var config = moduleConfigs[getModule(id)].nodes[getNode(id)]; | ||
var module = moduleConfigs[getModuleFromSetId(id)] | ||
if (!module.user && (module.usedBy && module.usedBy.length > 0)) { | ||
continue; | ||
} | ||
var config = module.nodes[getNodeFromSetId(id)]; | ||
if (config.enabled && !config.err) { | ||
@@ -442,7 +514,7 @@ result += "\n<!-- --- [red-module:"+id+"] --- -->\n"; | ||
function getNodeConfig(id,lang) { | ||
var config = moduleConfigs[getModule(id)]; | ||
var config = moduleConfigs[getModuleFromSetId(id)]; | ||
if (!config) { | ||
return null; | ||
} | ||
config = config.nodes[getNode(id)]; | ||
config = config.nodes[getNodeFromSetId(id)]; | ||
if (config) { | ||
@@ -468,7 +540,7 @@ var result = "<!-- --- [red-module:"+id+"] --- -->\n"+config.config; | ||
} else { | ||
config = moduleConfigs[getModule(id)].nodes[getNode(id)]; | ||
config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; | ||
} | ||
if (!config || (config.enabled && !config.err)) { | ||
return nodeConstructors[type]; | ||
return nodeConstructors[type] || subflowModules[type]; | ||
} | ||
@@ -483,2 +555,4 @@ return null; | ||
nodeConstructors = {}; | ||
nodeOptions = {}; | ||
subflowModules = {}; | ||
nodeTypeToId = {}; | ||
@@ -506,3 +580,3 @@ } | ||
try { | ||
config = moduleConfigs[getModule(id)].nodes[getNode(id)]; | ||
config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; | ||
delete config.err; | ||
@@ -530,3 +604,3 @@ config.enabled = true; | ||
try { | ||
config = moduleConfigs[getModule(id)].nodes[getNode(id)]; | ||
config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; | ||
// TODO: persist setting | ||
@@ -589,2 +663,13 @@ config.enabled = false; | ||
function setUserInstalled(module,userInstalled) { | ||
moduleConfigs[module].user = userInstalled; | ||
return saveNodeList().then(function() { | ||
return getModuleInfo(module); | ||
}); | ||
} | ||
function addModuleDependency(module,usedBy) { | ||
moduleConfigs[module].usedBy = moduleConfigs[module].usedBy || []; | ||
moduleConfigs[module].usedBy.push(usedBy); | ||
} | ||
var icon_paths = { }; | ||
@@ -643,2 +728,3 @@ var iconCache = {}; | ||
registerSubflow: registerSubflow, | ||
@@ -651,2 +737,5 @@ addModule: addModule, | ||
setModulePendingUpdated: setModulePendingUpdated, | ||
setUserInstalled: setUserInstalled, | ||
addModuleDependency:addModuleDependency, | ||
removeModule: removeModule, | ||
@@ -658,2 +747,3 @@ | ||
getModuleList: getModuleList, | ||
getModule: getModule, | ||
getModuleInfo: getModuleInfo, | ||
@@ -674,3 +764,6 @@ | ||
cleanModuleList: cleanModuleList | ||
cleanModuleList: cleanModuleList, | ||
getModuleFromSetId: getModuleFromSetId, | ||
getNodeFromSetId: getNodeFromSetId, | ||
filterNodeInfo: filterNodeInfo | ||
}; |
107
lib/util.js
@@ -17,5 +17,6 @@ /** | ||
var path = require("path"); | ||
var i18n = require("@node-red/util").i18n; | ||
var registry; | ||
const path = require("path"); | ||
const semver = require("semver"); | ||
const {events,i18n,log} = require("@node-red/util"); | ||
var runtime; | ||
@@ -44,3 +45,3 @@ | ||
function requireModule(name) { | ||
var moduleInfo = registry.getModuleInfo(name); | ||
var moduleInfo = require("./index").getModuleInfo(name); | ||
if (moduleInfo && moduleInfo.path) { | ||
@@ -50,5 +51,4 @@ var relPath = path.relative(__dirname, moduleInfo.path); | ||
} else { | ||
var err = new Error(`Cannot find module '${name}'`); | ||
err.code = "MODULE_NOT_FOUND"; | ||
throw err; | ||
// Require it here to avoid the circular dependency | ||
return require("./externalModules").require(name); | ||
} | ||
@@ -62,3 +62,3 @@ } | ||
settings: {}, | ||
events: runtime.events, | ||
events: events, | ||
hooks: runtime.hooks, | ||
@@ -70,3 +70,3 @@ util: runtime.util, | ||
publish: function(topic,data,retain) { | ||
runtime.events.emit("comms",{ | ||
events.emit("comms",{ | ||
topic: topic, | ||
@@ -78,2 +78,13 @@ data: data, | ||
}, | ||
plugins: { | ||
registerPlugin: function(id,definition) { | ||
return runtime.plugins.registerPlugin(node.id,id,definition); | ||
}, | ||
get: function(id) { | ||
return runtime.plugins.getPlugin(id); | ||
}, | ||
getByType: function(type) { | ||
return runtime.plugins.getPluginsByType(type); | ||
} | ||
}, | ||
library: { | ||
@@ -88,7 +99,10 @@ register: function(type) { | ||
} | ||
copyObjectProperties(runtime.nodes,red.nodes,["createNode","getNode","eachNode","addCredentials","getCredentials","deleteCredentials" ]); | ||
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); | ||
} | ||
copyObjectProperties(runtime.log,red.log,null,["init"]); | ||
red.nodes.registerSubflow = function(subflowDef) { | ||
runtime.nodes.registerSubflow(node.id,subflowDef) | ||
} | ||
copyObjectProperties(log,red.log,null,["init"]); | ||
copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]); | ||
@@ -115,8 +129,75 @@ if (runtime.adminApi) { | ||
function checkAgainstList(module,version,list) { | ||
for (let i=0;i<list.length;i++) { | ||
let rule = list[i]; | ||
if (rule.module.test(module)) { | ||
if (version && rule.version) { | ||
if (semver.satisfies(version,rule.version)) { | ||
return rule; | ||
} | ||
} else { | ||
return rule; | ||
} | ||
} | ||
} | ||
} | ||
function checkModuleAllowed(module,version,allowList,denyList) { | ||
if (!allowList && !denyList) { | ||
// Default to allow | ||
return true; | ||
} | ||
if (allowList.length === 0 && denyList.length === 0) { | ||
return true; | ||
} | ||
var allowedRule = checkAgainstList(module,version,allowList); | ||
var deniedRule = checkAgainstList(module,version,denyList); | ||
// console.log("A",allowedRule) | ||
// console.log("D",deniedRule) | ||
if (allowedRule && !deniedRule) { | ||
return true; | ||
} | ||
if (!allowedRule && deniedRule) { | ||
return false; | ||
} | ||
if (!allowedRule && !deniedRule) { | ||
return true; | ||
} | ||
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) { | ||
return allowedRule.wildcardPos > deniedRule.wildcardPos | ||
} else { | ||
// First wildcard in same position. | ||
// Go with the longer matching rule. This isn't going to be 100% | ||
// right, but we are deep into edge cases at this point. | ||
return allowedRule.module.toString().length > deniedRule.module.toString().length | ||
} | ||
return false; | ||
} | ||
function parseModuleList(list) { | ||
list = list || ["*"]; | ||
return list.map(rule => { | ||
let m = /^(.+?)(?:@(.*))?$/.exec(rule); | ||
let wildcardPos = m[1].indexOf("*"); | ||
wildcardPos = wildcardPos===-1?Infinity:wildcardPos; | ||
return { | ||
module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"), | ||
version: m[2], | ||
wildcardPos: wildcardPos | ||
} | ||
}) | ||
} | ||
module.exports = { | ||
init: function(_runtime) { | ||
runtime = _runtime; | ||
registry = require("@node-red/registry/lib"); | ||
}, | ||
createNodeApi: createNodeApi | ||
createNodeApi: createNodeApi, | ||
parseModuleList: parseModuleList, | ||
checkModuleAllowed: checkModuleAllowed | ||
} |
{ | ||
"name": "@node-red/registry", | ||
"version": "1.2.9", | ||
"version": "1.3.0-beta.1", | ||
"license": "Apache-2.0", | ||
@@ -19,8 +19,7 @@ "main": "./lib/index.js", | ||
"dependencies": { | ||
"@node-red/util": "1.2.9", | ||
"@node-red/util": "1.3.0-beta.1", | ||
"semver": "6.3.0", | ||
"tar": "6.0.5", | ||
"uglify-js": "3.12.4", | ||
"when": "3.7.8" | ||
"uglify-js": "3.12.4" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
127079
4
14
3149
2
22
+ Added@node-red/util@1.3.0-beta.1(transitive)
- Removedwhen@3.7.8
- Removed@node-red/util@1.2.9(transitive)
- Removedwhen@3.7.8(transitive)
Updated@node-red/util@1.3.0-beta.1