diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js index 187bd823f9..058053a29b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js @@ -60,6 +60,7 @@ module.exports = { runtimeAPI.nodes.addModule(opts).then(function(info) { res.json(info); }).catch(function(err) { + console.log(err.stack); apiUtils.rejectHandler(req,res,err); }) }, diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index ff8dad5bf4..51f864ab78 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -386,6 +386,7 @@ var RED = (function() { } }); RED.comms.subscribe("notification/node/#",function(topic,msg) { + console.log(topic,msg); var i,m; var typeList; var info; diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index 19cdd7e279..b88a95299c 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -73,19 +73,6 @@ function checkModulePath(folder) { }; } -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) { if (Buffer.isBuffer(module)) { return installTarball(module) @@ -96,6 +83,7 @@ function installModule(module,version,url) { return new Promise((resolve,reject) => { var installName = module; var isUpgrade = false; + var isExisting = false; try { if (url) { if (pkgurlRe.test(url) || localtgzRe.test(url)) { @@ -125,7 +113,20 @@ function installModule(module,version,url) { reject(e); return; } - isUpgrade = checkExistingModule(module,version); + 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; + } } catch(err) { return reject(err); } @@ -141,7 +142,17 @@ function installModule(module,version,url) { exec.run(npmCommand,args,{ cwd: installDir }, true).then(result => { - if (!isUpgrade) { + 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})); + resolve(require("./registry").setUserInstalled(module,true).then(reportAddedModules)); + } else if (!isUpgrade) { log.info(log._("server.install.installed",{name:module})); resolve(require("./index").addModule(module).then(reportAddedModules)); } else { @@ -182,7 +193,6 @@ function installModule(module,version,url) { } function reportAddedModules(info) { - //comms.publish("node/added",info.nodes,false); if (info.nodes.length > 0) { log.info(log._("server.added-types")); for (var i=0;i 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 module) } catch(err) { return Promise.reject(err); } diff --git a/packages/node_modules/@node-red/registry/lib/localfilesystem.js b/packages/node_modules/@node-red/registry/lib/localfilesystem.js index 7233d1da38..15e3e5e84b 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -26,6 +26,7 @@ var i18n = require("@node-red/util").i18n; var settings; var disableNodePathScan = false; var iconFileExtensions = [".png", ".gif", ".svg"]; +var packageList = {}; function init(runtime) { settings = runtime.settings; @@ -173,9 +174,17 @@ function scanTreeForNodesModules(moduleName) { var userDir; 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 /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]; + }); } if (dir) { @@ -275,20 +284,19 @@ function getNodeFiles(disableNodePathScan) { } } - var nodeList = { - "node-red": { - name: "node-red", - version: settings.version, - nodes: {}, - icons: iconList - } + var nodeList = {}; + var coreNodeEntry = { + name: "node-red", + version: settings.version, + nodes: {}, + icons: iconList } 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}; } if (!disableNodePathScan) { @@ -297,7 +305,6 @@ function getNodeFiles(disableNodePathScan) { // Filter the module list to ignore global modules // that have also been installed locally - allowing the user to // update a module they may not otherwise be able to touch - moduleFiles.sort(function(A,B) { if (A.local && !B.local) { return -1 @@ -310,7 +317,7 @@ function getNodeFiles(disableNodePathScan) { moduleFiles = moduleFiles.filter(function(mod) { var result; if (!knownModules[mod.package.name]) { - knownModules[mod.package.name] = true; + knownModules[mod.package.name] = mod; result = true; } else { result = false; @@ -320,48 +327,62 @@ function getNodeFiles(disableNodePathScan) { 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); } else { // console.log("node path scan disabled"); } + nodeList["node-red"] = coreNodeEntry; return nodeList; } function getModuleFiles(module) { - var nodeList = {}; - + // Update the package list var moduleFiles = scanTreeForNodesModules(module); if (moduleFiles.length === 0) { var err = new Error(log._("nodes.registry.localfilesystem.module-not-found", {module:module})); err.code = 'MODULE_NOT_FOUND'; throw err; } + // 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) { + const nodeList = {}; 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, + user: moduleFile.user||false, nodes: {}, icons: nodeModuleFiles.icons, examples: nodeModuleFiles.examples @@ -369,7 +390,14 @@ function getModuleFiles(module) { if (moduleFile.package['node-red'].version) { nodeList[moduleFile.package.name].redVersion = moduleFile.package['node-red'].version; } + 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.files.forEach(function(node) { + node.local = moduleFile.local||false; nodeList[moduleFile.package.name].nodes[node.name] = node; nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false; }); @@ -400,6 +428,23 @@ function scanIconDir(dir) { }) return iconList; } +/** + * 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; +} module.exports = { init: init, diff --git a/packages/node_modules/@node-red/registry/lib/registry.js b/packages/node_modules/@node-red/registry/lib/registry.js index 9078b0ee81..7efd3ce256 100644 --- a/packages/node_modules/@node-red/registry/lib/registry.js +++ b/packages/node_modules/@node-red/registry/lib/registry.js @@ -60,7 +60,8 @@ function filterNodeInfo(n) { name: n.name, types: n.types, enabled: n.enabled, - local: n.local||false + local: n.local||false, + user: n.user || false }; if (n.hasOwnProperty("module")) { r.module = n.module; @@ -96,6 +97,7 @@ function saveNodeList() { name: module, version: moduleConfigs[module].version, local: moduleConfigs[module].local||false, + user: moduleConfigs[module].user||false, nodes: {} }; if (moduleConfigs[module].hasOwnProperty('pending_version')) { @@ -181,6 +183,7 @@ function loadNodeConfigs() { function addModule(module) { moduleNodes[module.name] = []; 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) { if (module.nodes.hasOwnProperty(setName)) { var set = module.nodes[setName]; @@ -242,21 +245,47 @@ function removeNode(id) { return filterNodeInfo(config); } -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); + } + 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 m !== name); + if (moduleConfigs[dep].usedBy.length === 0) { + // Remove the dependency + removeModule(dep,true); + } + } + }); + } + for (var i=0;i 0)) { + continue; + } var nodes = moduleConfigs[module].nodes; for (var node in nodes) { /* istanbul ignore else */ @@ -348,9 +380,13 @@ function getModuleInfo(module) { name: module, version: moduleConfigs[module].version, 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) { m.pending_version = moduleConfigs[module].pending_version; } @@ -427,7 +463,13 @@ function getAllNodeConfigs(lang) { var script = ""; for (var i=0;i 0)) { + continue; + } + + var config = module.nodes[getNode(id)]; if (config.enabled && !config.err) { result += "\n\n"; result += config.config; @@ -589,6 +631,17 @@ function setModulePendingUpdated(module,version) { }); } +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 = { }; var iconCache = {}; @@ -650,6 +703,9 @@ var registry = module.exports = { disableNodeSet: disableNodeSet, setModulePendingUpdated: setModulePendingUpdated, + setUserInstalled: setUserInstalled, + addModuleDependency:addModuleDependency, + removeModule: removeModule, getNodeInfo: getNodeInfo, diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index a06cbed966..f686830d67 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -207,7 +207,7 @@ var api = module.exports = { } if (opts.module) { var existingModule = runtime.nodes.getModuleInfo(opts.module); - if (existingModule) { + if (existingModule && existingModule.user) { if (!opts.version || existingModule.version === opts.version) { runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"}, opts.req); var err = new Error("Module already loaded"); diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index f9a772ea3b..d998874474 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -213,7 +213,9 @@ function reinstallModules(moduleList) { if (results[i].state === 'rejected') { reinstallList.push(moduleList[i]); } else { - events.emit("runtime-event",{id:"node/added",retain:false,payload:results[i].value.nodes}); + results[i].value.forEach(info => { + events.emit("runtime-event",{id:"node/added",retain:false,payload:info.nodes}); + }); } } if (reinstallList.length > 0) { diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/index.js index acc81d719e..cafe0417f7 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -14,7 +14,6 @@ * limitations under the License. **/ -var when = require("when"); var path = require("path"); var fs = require("fs"); var clone = require("clone"); @@ -182,11 +181,12 @@ function installModule(module,version,url) { function uninstallModule(module) { var info = registry.getModuleInfo(module); - if (!info) { + if (!info || !info.user) { throw new Error(log._("nodes.index.unrecognised-module", {module:module})); } else { - for (var i=0;i `${module}/${n.name}`); + for (var i=0;i