From a006b5205234b076df2f3a9ff753ea3fb5d4c2cb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 10 Dec 2020 16:01:55 +0000 Subject: [PATCH] Initial plugin runtime api implementation --- .../@node-red/editor-api/lib/admin/index.js | 6 + .../@node-red/editor-api/lib/admin/plugins.js | 38 +++ .../editor-client/locales/en-US/editor.json | 1 + .../@node-red/editor-client/src/js/i18n.js | 25 ++ .../@node-red/editor-client/src/js/red.js | 85 +++++- .../@node-red/registry/lib/index.js | 8 + .../@node-red/registry/lib/loader.js | 289 ++++++++++++++---- .../@node-red/registry/lib/localfilesystem.js | 58 ++-- .../@node-red/registry/lib/plugins.js | 100 ++++++ .../@node-red/registry/lib/registry.js | 57 ++-- .../@node-red/registry/lib/util.js | 11 + .../@node-red/runtime/lib/api/index.js | 2 + .../@node-red/runtime/lib/api/plugins.js | 57 ++++ .../@node-red/runtime/lib/index.js | 8 + .../@node-red/runtime/lib/plugins.js | 10 + .../locales/en-US/test-editor-plugin.json | 3 + .../resources/plugin/test-plugin/package.json | 12 + .../test-plugin/test-editor-plugin.html | 5 + .../plugin/test-plugin/test-runtime-plugin.js | 10 + test/resources/plugin/test-plugin/test.html | 4 + test/resources/plugin/test-plugin/test.js | 13 + .../editor-api/lib/admin/plugins_spec.js | 111 +++++++ .../@node-red/registry/lib/plugins_spec.js | 153 ++++++++++ .../@node-red/runtime/lib/api/plugins_spec.js | 68 +++++ .../@node-red/runtime/lib/plugins_spec.js | 13 + 25 files changed, 1026 insertions(+), 121 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-api/lib/admin/plugins.js create mode 100644 packages/node_modules/@node-red/registry/lib/plugins.js create mode 100644 packages/node_modules/@node-red/runtime/lib/api/plugins.js create mode 100644 packages/node_modules/@node-red/runtime/lib/plugins.js create mode 100644 test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json create mode 100644 test/resources/plugin/test-plugin/package.json create mode 100644 test/resources/plugin/test-plugin/test-editor-plugin.html create mode 100644 test/resources/plugin/test-plugin/test-runtime-plugin.js create mode 100644 test/resources/plugin/test-plugin/test.html create mode 100644 test/resources/plugin/test-plugin/test.js create mode 100644 test/unit/@node-red/editor-api/lib/admin/plugins_spec.js create mode 100644 test/unit/@node-red/registry/lib/plugins_spec.js create mode 100644 test/unit/@node-red/runtime/lib/api/plugins_spec.js create mode 100644 test/unit/@node-red/runtime/lib/plugins_spec.js diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 34c47b2cb..cf3505439 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -22,6 +22,7 @@ var flow = require("./flow"); var context = require("./context"); var auth = require("../auth"); var info = require("./settings"); +var plugins = require("./plugins"); var apiUtil = require("../util"); @@ -32,6 +33,7 @@ module.exports = { nodes.init(runtimeAPI); context.init(runtimeAPI); info.init(settings,runtimeAPI); + plugins.init(runtimeAPI); var needsPermission = auth.needsPermission; @@ -80,6 +82,10 @@ module.exports = { adminApp.get("/settings",needsPermission("settings.read"),info.runtimeSettings,apiUtil.errorHandler); + // Plugins + adminApp.get("/plugins", needsPermission("plugins.read"), plugins.getAll, apiUtil.errorHandler); + adminApp.get("/plugins/messages", needsPermission("plugins.read"), plugins.getCatalogs, apiUtil.errorHandler); + return adminApp; } } diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js new file mode 100644 index 000000000..15428f86f --- /dev/null +++ b/packages/node_modules/@node-red/editor-api/lib/admin/plugins.js @@ -0,0 +1,38 @@ +var apiUtils = require("../util"); + +var runtimeAPI; + +module.exports = { + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; + }, + getAll: function(req,res) { + var opts = { + user: req.user, + req: apiUtils.getRequestLogObject(req) + } + if (req.get("accept") == "application/json") { + runtimeAPI.plugins.getPluginList(opts).then(function(list) { + res.json(list); + }) + } else { + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + runtimeAPI.plugins.getPluginConfigs(opts).then(function(configs) { + res.send(configs); + }) + } + }, + getCatalogs: function(req,res) { + var opts = { + user: req.user, + lang: req.query.lng, + req: apiUtils.getRequestLogObject(req) + } + runtimeAPI.plugins.getPluginCatalogs(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + console.log(err.stack); + apiUtils.rejectHandler(req,res,err); + }) + } +}; diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 7894f1ed2..7cdfde99d 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -38,6 +38,7 @@ } }, "event": { + "loadPlugins": "Loading Plugins", "loadPalette": "Loading Palette", "loadNodeCatalogs": "Loading Node catalogs", "loadNodes": "Loading Nodes __count__", diff --git a/packages/node_modules/@node-red/editor-client/src/js/i18n.js b/packages/node_modules/@node-red/editor-client/src/js/i18n.js index f1d0edfaf..ac439ecbc 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/i18n.js +++ b/packages/node_modules/@node-red/editor-client/src/js/i18n.js @@ -108,6 +108,31 @@ RED.i18n = (function() { } }); }) + }, + + loadPluginCatalogs: function(done) { + var languageList = i18n.functions.toLanguages(localStorage.getItem("editor-language")||i18n.detectLanguage()); + var toLoad = languageList.length; + + languageList.forEach(function(lang) { + $.ajax({ + headers: { + "Accept":"application/json" + }, + cache: false, + url: apiRootUrl+'plugins/messages?lng='+lang, + success: function(data) { + var namespaces = Object.keys(data); + namespaces.forEach(function(ns) { + i18n.addResourceBundle(lang,ns,data[ns]); + }); + toLoad--; + if (toLoad === 0) { + done(); + } + } + }); + }) } } })(); 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 3a4f4781f..e3439c435 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 @@ -15,19 +15,66 @@ **/ var RED = (function() { - function appendNodeConfig(nodeConfig,done) { + + function loadPluginList() { + loader.reportProgress(RED._("event.loadPlugins"), 10) + $.ajax({ + headers: { + "Accept":"application/json" + }, + cache: false, + url: 'plugins', + success: function(data) { + console.log(data); + loader.reportProgress(RED._("event.loadPlugins"), 13) + RED.i18n.loadPluginCatalogs(function() { + loadPlugins(function() { + loadNodeList(); + }); + }); + } + }); + } + function loadPlugins(done) { + loader.reportProgress(RED._("event.loadPlugins",{count:""}), 17) + var lang = localStorage.getItem("editor-language")||i18n.detectLanguage(); + + $.ajax({ + headers: { + "Accept":"text/html", + "Accept-Language": lang + }, + cache: false, + url: 'plugins', + success: function(data) { + var configs = data.trim().split(/(?=)/); + var totalCount = configs.length; + var stepConfig = function() { + // loader.reportProgress(RED._("event.loadNodes",{count:(totalCount-configs.length)+"/"+totalCount}), 30 + ((totalCount-configs.length)/totalCount)*40 ) + if (configs.length === 0) { + done(); + } else { + var config = configs.shift(); + appendPluginConfig(config,stepConfig); + } + } + stepConfig(); + } + }); + } + + function appendConfig(config, moduleIdMatch, targetContainer, done) { done = done || function(){}; - var m = //.exec(nodeConfig.trim()); var moduleId; - if (m) { - moduleId = m[1]; + if (moduleIdMatch) { + moduleId = moduleIdMatch[1]; } else { moduleId = "unknown"; } try { - var hasDeferred = false; - var nodeConfigEls = $("
"+nodeConfig+"
"); + var hasDeferred = false; + var nodeConfigEls = $("
"+config+"
"); var scripts = nodeConfigEls.find("script"); var scriptCount = scripts.length; scripts.each(function(i,el) { @@ -38,14 +85,14 @@ var RED = (function() { newScript.onload = function() { scriptCount--; if (scriptCount === 0) { - $("#red-ui-editor-node-configs").append(nodeConfigEls); + $(targetContainer).append(nodeConfigEls); done() } } if ($(el).attr('type') === "module") { newScript.type = "module"; } - $("#red-ui-editor-node-configs").append(newScript); + $(targetContainer).append(newScript); newScript.src = RED.settings.apiRootUrl+srcUrl; hasDeferred = true; } else { @@ -61,7 +108,7 @@ var RED = (function() { } }) if (!hasDeferred) { - $("#red-ui-editor-node-configs").append(nodeConfigEls); + $(targetContainer).append(nodeConfigEls); done(); } } catch(err) { @@ -73,6 +120,23 @@ var RED = (function() { done(); } } + function appendPluginConfig(pluginConfig,done) { + appendConfig( + pluginConfig, + //.exec(pluginConfig.trim()), + "#red-ui-editor-plugin-configs", + done + ); + } + + function appendNodeConfig(nodeConfig,done) { + appendConfig( + nodeConfig, + //.exec(nodeConfig.trim()), + "#red-ui-editor-node-configs", + done + ); + } function loadNodeList() { loader.reportProgress(RED._("event.loadPalette"), 20) @@ -580,7 +644,7 @@ var RED = (function() { RED.actions.add("core:show-about", showAbout); - loadNodeList(); + loadPluginList(); } @@ -596,6 +660,7 @@ var RED = (function() { '
'+ '
'+ '').appendTo(options.target); + $('
').appendTo(options.target); $('
').appendTo(options.target); $('
').appendTo(options.target); diff --git a/packages/node_modules/@node-red/registry/lib/index.js b/packages/node_modules/@node-red/registry/lib/index.js index 03f979424..251b04971 100644 --- a/packages/node_modules/@node-red/registry/lib/index.js +++ b/packages/node_modules/@node-red/registry/lib/index.js @@ -28,6 +28,7 @@ var registry = require("./registry"); var loader = require("./loader"); var installer = require("./installer"); var library = require("./library"); +var plugins = require("./plugins"); /** * Initialise the registry with a reference to a runtime object @@ -40,6 +41,7 @@ function init(runtime) { // 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); + plugins.init(runtime.settings); registry.init(runtime.settings,loader); library.init(); } @@ -297,6 +299,12 @@ module.exports = { */ getNodeExampleFlowPath: library.getExampleFlowPath, + registerPlugin: plugins.registerPlugin, + getPlugin: plugins.getPlugin, + getPluginsByType: plugins.getPluginsByType, + getPluginList: plugins.getPluginList, + getPluginConfigs: plugins.getPluginConfigs, + deprecated: require("./deprecated") }; diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index f35987732..d29eadc66 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -36,78 +36,140 @@ function load(disableNodePathScan) { // To skip node scan, the following line will use the stored node list. // We should expose that as an option at some point, although the // performance gains are minimal. - //return loadNodeFiles(registry.getModuleList()); + //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((settings.version||"0.0.0").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 - log.warn("["+module+"] "+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("/"); - - 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 => {})); - } catch(err) { - // - } - } + if (modules[module].nodes) { + nodePromises = nodePromises.concat(loadModuleTypeFiles(modules[module], "nodes")); + } + if (modules[module].plugins) { + pluginPromises = pluginPromises.concat(loadModuleTypeFiles(modules[module], "plugins")); } } } } - return Promise.all(promises).then(function(results) { - for (var module in nodeFiles) { - if (nodeFiles.hasOwnProperty(module)) { - if (!nodeFiles[module].err) { - registry.addModule(nodeFiles[module]); + var pluginList; + var nodeList; + + 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]); + } } } } - return loadNodeSetList(nodes); + 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(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; }); } @@ -175,11 +237,12 @@ async function loadNodeLocales(node) { node.namespace = node.module; return node } - 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); }).catch(err => { node.namespace = node.module; @@ -204,6 +267,7 @@ async function loadNodeConfig(fileInfo) { } var node = { + type: "node", id: id, module: module, name: name, @@ -227,6 +291,58 @@ async function loadNodeConfig(fileInfo) { return node; } +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 +} + /** * Loads the specified node into the runtime * @param node a node info object - see loadNodeConfig @@ -236,8 +352,6 @@ async function loadNodeConfig(fileInfo) { * */ function loadNodeSet(node) { - var nodeDir = path.dirname(node.file); - var nodeFn = path.basename(node.file); if (!node.enabled) { return Promise.resolve(node); } else { @@ -284,11 +398,59 @@ function loadNodeSet(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) { var promises = []; nodes.forEach(function(node) { if (!node.err) { - promises.push(loadNodeSet(node).catch(err => {})); + if (node.type === "plugin") { + promises.push(loadPlugin(node).catch(err => {})); + } else { + promises.push(loadNodeSet(node).catch(err => {})); + } } else { promises.push(node); } @@ -316,6 +478,7 @@ function addModule(module) { return Promise.reject(e); } try { +<<<<<<< HEAD var moduleFiles = {}; var moduleStack = [module]; while(moduleStack.length > 0) { @@ -339,6 +502,10 @@ function addModule(module) { } } return loadNodeFiles(moduleFiles).then(() => module) +======= + var moduleFiles = localfilesystem.getModuleFiles(module); + return loadModuleFiles(moduleFiles); +>>>>>>> Initial plugin runtime api implementation } 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 ed4b81d8e..92e03fd62 100644 --- a/packages/node_modules/@node-red/registry/lib/localfilesystem.js +++ b/packages/node_modules/@node-red/registry/lib/localfilesystem.js @@ -220,33 +220,41 @@ function getModuleNodeFiles(module) { var moduleDir = module.dir; var pkg = module.package; - 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"); try { @@ -396,6 +404,7 @@ function convertModuleFileListToObject(moduleFiles) { local: moduleFile.local||false, user: moduleFile.user||false, nodes: {}, + plugins: {}, icons: nodeModuleFiles.icons, examples: nodeModuleFiles.examples }; @@ -408,11 +417,14 @@ function convertModuleFileListToObject(moduleFiles) { if (moduleFile.usedBy) { nodeList[moduleFile.package.name].usedBy = moduleFile.usedBy; } - nodeModuleFiles.files.forEach(function(node) { - node.local = moduleFile.local||false; + 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; + }); }); return nodeList; } diff --git a/packages/node_modules/@node-red/registry/lib/plugins.js b/packages/node_modules/@node-red/registry/lib/plugins.js new file mode 100644 index 000000000..c2bb2298d --- /dev/null +++ b/packages/node_modules/@node-red/registry/lib/plugins.js @@ -0,0 +1,100 @@ +const registry = require("./registry"); +const {events} = require("@node-red/util") + +var pluginConfigCache = {}; +var pluginToId = {}; +var plugins = {}; +var pluginsByType = {}; +var settings; + +function init(_settings) { + settings = _settings; + plugins = {}; + pluginConfigCache = {}; + pluginToId = {}; + pluginsByType = {}; +} + +function registerPlugin(nodeSetId,id,definition) { + var moduleId = registry.getModuleFromSetId(nodeSetId); + var pluginId = registry.getNodeFromSetId(nodeSetId); + + definition.id = id; + definition.module = moduleId; + pluginToId[id] = nodeSetId; + plugins[id] = definition; + var module = registry.getModule(moduleId); + module.plugins[pluginId].plugins.push(definition); + if (definition.type) { + pluginsByType[definition.type] = pluginsByType[definition.type] || []; + pluginsByType[definition.type].push(definition); + } + if (definition.onadd && typeof definition.onadd === 'function') { + definition.onadd(); + } + events.emit("registry:plugin-added",id); +} + +function getPlugin(id) { + return plugins[id] +} + +function getPluginsByType(type) { + return pluginsByType[type] || []; +} + +function getPluginConfigs(lang) { + if (!pluginConfigCache[lang]) { + var result = ""; + var script = ""; + var moduleConfigs = registry.getModuleList(); + for (var module in moduleConfigs) { + /* istanbul ignore else */ + if (moduleConfigs.hasOwnProperty(module)) { + var plugins = moduleConfigs[module].plugins; + for (var plugin in plugins) { + if (plugins.hasOwnProperty(plugin)) { + var config = plugins[plugin]; + if (config.enabled && !config.err && config.config) { + result += "\n\n"; + result += config.config; + } + } + } + } + } + pluginConfigCache[lang] = result; + } + return pluginConfigCache[lang]; +} +function getPluginList() { + var list = []; + var moduleConfigs = registry.getModuleList(); + for (var module in moduleConfigs) { + /* istanbul ignore else */ + if (moduleConfigs.hasOwnProperty(module)) { + var plugins = moduleConfigs[module].plugins; + for (var plugin in plugins) { + /* istanbul ignore else */ + if (plugins.hasOwnProperty(plugin)) { + var pluginInfo = registry.filterNodeInfo(plugins[plugin]); + pluginInfo.version = moduleConfigs[module].version; + // if (moduleConfigs[module].pending_version) { + // nodeInfo.pending_version = moduleConfigs[module].pending_version; + // } + list.push(pluginInfo); + } + } + } + } + return list; +} + +module.exports = { + init, + registerPlugin, + getPlugin, + getPluginsByType, + getPluginConfigs, + getPluginList +} \ No newline at end of file diff --git a/packages/node_modules/@node-red/registry/lib/registry.js b/packages/node_modules/@node-red/registry/lib/registry.js index 730280d72..96594fbef 100644 --- a/packages/node_modules/@node-red/registry/lib/registry.js +++ b/packages/node_modules/@node-red/registry/lib/registry.js @@ -67,17 +67,24 @@ function filterNodeInfo(n) { if (n.hasOwnProperty("err")) { r.err = n.err; } + if (n.hasOwnProperty("plugins")) { + r.plugins = n.plugins; + } + if (n.type === "plugin") { + r.editor = !!n.template; + r.runtime = !!n.file; + } return r; } -function getModule(id) { +function getModuleFromSetId(id) { var parts = id.split("/"); return parts.slice(0,parts.length-1).join("/"); } -function getNode(id) { +function getNodeFromSetId(id) { var parts = id.split("/"); return parts[parts.length-1]; } @@ -220,11 +227,11 @@ function addModule(module) { 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); if (i > -1) { nodeList.splice(i,1); @@ -294,9 +301,9 @@ function getNodeInfo(typeOrId) { } /* istanbul ignore else */ 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) { var info = filterNodeInfo(config); if (config.hasOwnProperty("loaded")) { @@ -323,9 +330,9 @@ function getFullNodeInfo(typeOrId) { } /* istanbul ignore else */ if (id) { - var module = moduleConfigs[getModule(id)]; + var module = moduleConfigs[getModuleFromSetId(id)]; if (module) { - return module.nodes[getNode(id)]; + return module.nodes[getNodeFromSetId(id)]; } } return null; @@ -359,16 +366,10 @@ function getNodeList(filter) { } 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]; } function getModuleInfo(module) { @@ -461,13 +462,11 @@ function getAllNodeConfigs(lang) { var script = ""; for (var i=0;i 0)) { continue; } - - var config = module.nodes[getNode(id)]; + var config = module.nodes[getNodeFromSetId(id)]; if (config.enabled && !config.err) { result += "\n\n"; result += config.config; @@ -486,11 +485,11 @@ function getAllNodeConfigs(lang) { } 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) { var result = "\n"+config.config; result += loader.getNodeHelp(config,lang||"en-US") @@ -511,7 +510,7 @@ function getNodeConstructor(type) { if (typeof id === "undefined") { config = undefined; } else { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; } if (!config || (config.enabled && !config.err)) { @@ -548,7 +547,7 @@ function enableNodeSet(typeOrId) { } var config; try { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; delete config.err; config.enabled = true; nodeConfigCache = {}; @@ -571,7 +570,7 @@ function disableNodeSet(typeOrId) { } var config; try { - config = moduleConfigs[getModule(id)].nodes[getNode(id)]; + config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; // TODO: persist setting config.enabled = false; nodeConfigCache = {}; @@ -710,6 +709,7 @@ var registry = module.exports = { getFullNodeInfo: getFullNodeInfo, getNodeList: getNodeList, getModuleList: getModuleList, + getModule: getModule, getModuleInfo: getModuleInfo, getNodeIconPath: getNodeIconPath, @@ -725,5 +725,8 @@ var registry = module.exports = { saveNodeList: saveNodeList, - cleanModuleList: cleanModuleList + cleanModuleList: cleanModuleList, + getModuleFromSetId: getModuleFromSetId, + getNodeFromSetId: getNodeFromSetId, + filterNodeInfo: filterNodeInfo }; diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index 94690d6cb..e6caa5d67 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -70,6 +70,17 @@ function createNodeApi(node) { }) } }, + 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: { register: function(type) { return runtime.library.register(node.id,type); diff --git a/packages/node_modules/@node-red/runtime/lib/api/index.js b/packages/node_modules/@node-red/runtime/lib/api/index.js index b131470b0..46d15d1e7 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/index.js +++ b/packages/node_modules/@node-red/runtime/lib/api/index.js @@ -28,6 +28,7 @@ var api = module.exports = { api.library.init(runtime); api.projects.init(runtime); api.context.init(runtime); + api.plugins.init(runtime); }, comms: require("./comms"), @@ -37,6 +38,7 @@ var api = module.exports = { settings: require("./settings"), projects: require("./projects"), context: require("./context"), + plugins: require("./plugins"), isStarted: async function(opts) { return runtime.isStarted(); diff --git a/packages/node_modules/@node-red/runtime/lib/api/plugins.js b/packages/node_modules/@node-red/runtime/lib/api/plugins.js new file mode 100644 index 000000000..ee35d445a --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/api/plugins.js @@ -0,0 +1,57 @@ +/** + * @mixin @node-red/runtime_plugins + */ + +var runtime; + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + + + /** + * Gets the editor content for an individual plugin + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the node information + * @memberof @node-red/runtime_nodes + */ + getPluginList: async function(opts) { + runtime.log.audit({event: "plugins.list.get"}, opts.req); + return runtime.plugins.getPluginList(); + }, + + getPluginConfigs: async function(opts) { + runtime.log.audit({event: "plugins.configs.get"}, opts.req); + return runtime.plugins.getPluginConfigs(opts.lang); + }, + /** + * Gets all registered module message catalogs + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.lang - the i18n language to return. If not set, uses runtime default (en-US) + * @param {Object} opts.req - the request to log (optional) + * @return {Promise} - the message catalogs + * @memberof @node-red/runtime_nodes + */ + getPluginCatalogs: async function(opts) { + var lang = opts.lang; + var prevLang = runtime.i18n.i.language; + // Trigger a load from disk of the language if it is not the default + return new Promise( (resolve,reject) => { + runtime.i18n.i.changeLanguage(lang, function(){ + var nodeList = runtime.plugins.getPluginList(); + var result = {}; + nodeList.forEach(function(n) { + if (n.module !== "node-red") { + result[n.id] = runtime.i18n.i.getResourceBundle(lang, n.id)||{}; + } + }); + runtime.i18n.i.changeLanguage(prevLang); + resolve(result); + }); + }); + }, +} diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index 40aea7e36..0be1c6511 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -21,6 +21,7 @@ var flows = require("./flows"); var storage = require("./storage"); var library = require("./library"); var hooks = require("./hooks"); +var plugins = require("./plugins"); var settings = require("./settings"); var express = require("express"); @@ -280,6 +281,7 @@ var runtime = { storage: storage, hooks: hooks, nodes: redNodes, + plugins: plugins, flows: flows, library: library, exec: exec, @@ -341,6 +343,12 @@ module.exports = { */ context: externalAPI.context, + /** + * @memberof @node-red/runtime + * @mixes @node-red/runtime_plugins + */ + plugins: externalAPI.plugins, + /** * Returns whether the runtime is started * @param {Object} opts diff --git a/packages/node_modules/@node-red/runtime/lib/plugins.js b/packages/node_modules/@node-red/runtime/lib/plugins.js new file mode 100644 index 000000000..738a64fa7 --- /dev/null +++ b/packages/node_modules/@node-red/runtime/lib/plugins.js @@ -0,0 +1,10 @@ +const registry = require("@node-red/registry"); + +module.exports = { + init: function() {}, + registerPlugin: registry.registerPlugin, + getPlugin: registry.getPlugin, + getPluginsByType: registry.getPluginsByType, + getPluginList: registry.getPluginList, + getPluginConfigs: registry.getPluginConfigs, +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json b/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json new file mode 100644 index 000000000..e990e2ec1 --- /dev/null +++ b/test/resources/plugin/test-plugin/locales/en-US/test-editor-plugin.json @@ -0,0 +1,3 @@ +{ + "plugin": "winning" +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/package.json b/test/resources/plugin/test-plugin/package.json new file mode 100644 index 000000000..1077042de --- /dev/null +++ b/test/resources/plugin/test-plugin/package.json @@ -0,0 +1,12 @@ +{ + "name": "test-plugin", + "version": "1.0.0", + "description": "", + "node-red": { + "plugins": { + "test": "test.js", + "test-editor-plugin": "test-editor-plugin.html", + "test-runtime-plugin": "test-runtime-plugin.js" + } + } +} diff --git a/test/resources/plugin/test-plugin/test-editor-plugin.html b/test/resources/plugin/test-plugin/test-editor-plugin.html new file mode 100644 index 000000000..067d09d3c --- /dev/null +++ b/test/resources/plugin/test-plugin/test-editor-plugin.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test-runtime-plugin.js b/test/resources/plugin/test-plugin/test-runtime-plugin.js new file mode 100644 index 000000000..d9e3ff3c7 --- /dev/null +++ b/test/resources/plugin/test-plugin/test-runtime-plugin.js @@ -0,0 +1,10 @@ +module.exports = function(RED) { + console.log("Loaded test-plugin/test-runtime-plugin") + + RED.plugins.registerPlugin("my-test-runtime-only-plugin", { + type: "bar", + onadd: function() { + console.log("my-test-runtime-only-plugin.onadd called") + } + }) +} \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test.html b/test/resources/plugin/test-plugin/test.html new file mode 100644 index 000000000..72d2979df --- /dev/null +++ b/test/resources/plugin/test-plugin/test.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/test/resources/plugin/test-plugin/test.js b/test/resources/plugin/test-plugin/test.js new file mode 100644 index 000000000..3a14fe9af --- /dev/null +++ b/test/resources/plugin/test-plugin/test.js @@ -0,0 +1,13 @@ +module.exports = function(RED) { + console.log("Loaded test-plugin/test") + + RED.plugins.registerPlugin("my-test-plugin", { + type: "foo", + onadd: function() { + console.log("my-test-plugin.onadd called") + RED.events.on("registry:plugin-added", function(id) { + console.log(`my-test-plugin: plugin-added event "${id}"`) + }); + } + }) +} \ No newline at end of file diff --git a/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js b/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js new file mode 100644 index 000000000..74584a1d8 --- /dev/null +++ b/test/unit/@node-red/editor-api/lib/admin/plugins_spec.js @@ -0,0 +1,111 @@ +const should = require("should"); +const request = require('supertest'); +const express = require('express'); +const bodyParser = require("body-parser"); + +var app; + +var NR_TEST_UTILS = require("nr-test-utils"); + +var plugins = NR_TEST_UTILS.require("@node-red/editor-api/lib/admin/plugins"); + +describe("api/editor/plugins", function() { + const pluginList = [ + { + "id": "test-module/test-set", + "enabled": true, + "local": false, + "plugins": [ + { + "type": "foo", + "id": "a-plugin", + "module": "test-module" + }, + { + "type": "bar", + "id": "a-plugin2", + "module": "test-module" + }, + { + "type": "foo", + "id": "a-plugin3", + "module": "test-module" + } + ] + }, + { + "id": "test-module/test-disabled-set", + "enabled": false, + "local": false, + "plugins": [] + } + ]; + const pluginConfigs = ` + +test-module-config`; + + const pluginCatalogs = { "test-module": {"foo": "bar"}}; + + before(function() { + app = express(); + app.use(bodyParser.json()); + app.get("/plugins",plugins.getAll); + app.get("/plugins/messages",plugins.getCatalogs); + + plugins.init({ + plugins: { + getPluginList: async function() { return pluginList }, + getPluginConfigs: async function() { return pluginConfigs }, + getPluginCatalogs: async function() { return pluginCatalogs } + } + }) + }); + + it('returns the list of plugins', function(done) { + request(app) + .get("/plugins") + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + try { + JSON.stringify(res.body).should.eql(JSON.stringify(pluginList)); + done(); + } catch(err) { + done(err) + } + }); + }); + it('returns the plugin configs', function(done) { + request(app) + .get("/plugins") + .set('Accept', 'text/html') + .expect(200) + .expect(pluginConfigs) + .end(function(err,res) { + if (err) { + return done(err); + } + done(); + }); + }); + it('returns the plugin catalogs', function(done) { + request(app) + .get("/plugins/messages") + .set('Accept', 'application/json') + .expect(200) + .end(function(err,res) { + if (err) { + return done(err); + } + try { + JSON.stringify(res.body).should.eql(JSON.stringify(pluginCatalogs)); + done(); + } catch(err) { + done(err) + } + }); + }); +}); diff --git a/test/unit/@node-red/registry/lib/plugins_spec.js b/test/unit/@node-red/registry/lib/plugins_spec.js new file mode 100644 index 000000000..430553fac --- /dev/null +++ b/test/unit/@node-red/registry/lib/plugins_spec.js @@ -0,0 +1,153 @@ + +const should = require("should"); +const sinon = require("sinon"); +const path = require("path"); + +const NR_TEST_UTILS = require("nr-test-utils"); + +const plugins = NR_TEST_UTILS.require("@node-red/registry/lib/plugins"); +const registry = NR_TEST_UTILS.require("@node-red/registry/lib/registry"); +const { events } = NR_TEST_UTILS.require("@node-red/util"); + +describe("red/nodes/registry/plugins",function() { + let receivedEvents = []; + let modules; + function handleEvent(evnt) { + receivedEvents.push(evnt); + } + beforeEach(function() { + plugins.init({}); + receivedEvents = []; + modules = { + "test-module": { + plugins: { + "test-set": { + id: "test-module/test-set", + enabled: true, + config: "test-module-config", + plugins: [] + }, + "test-disabled-set": { + id: "test-module/test-disabled-set", + enabled: false, + config: "disabled-plugin-config", + plugins: [] + } + } + } + } + events.on("registry:plugin-added",handleEvent); + sinon.stub(registry,"getModule", moduleId => modules[moduleId]); + sinon.stub(registry,"getModuleList", () => modules) + }); + afterEach(function() { + events.removeListener("registry:plugin-added",handleEvent); + registry.getModule.restore(); + registry.getModuleList.restore(); + }) + + describe("registerPlugin", function() { + it("registers a plugin", function() { + let pluginDef = {} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + receivedEvents.length.should.eql(1); + receivedEvents[0].should.eql("a-plugin"); + should.exist(modules['test-module'].plugins['test-set'].plugins[0]) + modules['test-module'].plugins['test-set'].plugins[0].should.equal(pluginDef) + }) + it("calls a plugins onadd function", function() { + let pluginDef = { onadd: sinon.stub() } + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + pluginDef.onadd.called.should.be.true(); + }) + }) + + describe("getPlugin", function() { + it("returns a registered plugin", function() { + let pluginDef = {} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + pluginDef.should.equal(plugins.getPlugin("a-plugin")); + }) + }) + describe("getPluginsByType", function() { + it("returns a plugins of a given type", function() { + let pluginDef = {type: "foo"} + let pluginDef2 = {type: "bar"} + let pluginDef3 = {type: "foo"} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + plugins.registerPlugin("test-module/test-set","a-plugin2",pluginDef2); + plugins.registerPlugin("test-module/test-set","a-plugin3",pluginDef3); + + let fooPlugins = plugins.getPluginsByType("foo"); + let barPlugins = plugins.getPluginsByType("bar"); + let noPlugins = plugins.getPluginsByType("none"); + + noPlugins.should.be.of.length(0); + + fooPlugins.should.be.of.length(2); + fooPlugins.should.containEql(pluginDef); + fooPlugins.should.containEql(pluginDef3); + + barPlugins.should.be.of.length(1); + barPlugins.should.containEql(pluginDef2); + + }) + }) + + describe("getPluginConfigs", function() { + it("gets all plugin configs", function() { + let configs = plugins.getPluginConfigs("en-US"); + configs.should.eql(` + +test-module-config`) + }) + }) + + + describe("getPluginList", function() { + it("returns a plugins of a given type", function() { + let pluginDef = {type: "foo"} + let pluginDef2 = {type: "bar"} + let pluginDef3 = {type: "foo"} + plugins.registerPlugin("test-module/test-set","a-plugin",pluginDef); + plugins.registerPlugin("test-module/test-set","a-plugin2",pluginDef2); + plugins.registerPlugin("test-module/test-set","a-plugin3",pluginDef3); + + let pluginList = plugins.getPluginList(); + JSON.stringify(pluginList).should.eql(JSON.stringify( + [ + { + "id": "test-module/test-set", + "enabled": true, + "local": false, + "plugins": [ + { + "type": "foo", + "id": "a-plugin", + "module": "test-module" + }, + { + "type": "bar", + "id": "a-plugin2", + "module": "test-module" + }, + { + "type": "foo", + "id": "a-plugin3", + "module": "test-module" + } + ] + }, + { + "id": "test-module/test-disabled-set", + "enabled": false, + "local": false, + "plugins": [] + } + ] + )) + }) + }) + + +}); \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/api/plugins_spec.js b/test/unit/@node-red/runtime/lib/api/plugins_spec.js new file mode 100644 index 000000000..7ae6d8286 --- /dev/null +++ b/test/unit/@node-red/runtime/lib/api/plugins_spec.js @@ -0,0 +1,68 @@ +const should = require("should"); +const sinon = require("sinon"); + +const NR_TEST_UTILS = require("nr-test-utils"); +const plugins = NR_TEST_UTILS.require("@node-red/runtime/lib/api/plugins") + +const mockLog = () => ({ + log: sinon.stub(), + debug: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub(), + info: sinon.stub(), + metric: sinon.stub(), + audit: sinon.stub(), + _: function() { return "abc"} +}) + +describe("runtime-api/plugins", function() { + const pluginList = [{id:"one",module:'test-module'},{id:"two",module:"node-red"}]; + const pluginConfigs = "123"; + + describe("getPluginList", function() { + it("gets the plugin list", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginList: function() { return pluginList} + } + }); + return plugins.getPluginList({}).then(function(result) { + result.should.eql(pluginList); + }) + }); + }); + describe("getPluginConfigs", function() { + it("gets the plugin configs", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginConfigs: function() { return pluginConfigs} + } + }); + return plugins.getPluginConfigs({}).then(function(result) { + result.should.eql(pluginConfigs); + }) + }); + }); + describe("getPluginCatalogs", function() { + it("gets the plugin catalogs", function() { + plugins.init({ + log: mockLog(), + plugins: { + getPluginList: function() { return pluginList} + }, + i18n: { + i: { + changeLanguage: function(lang,done) { done && done() }, + getResourceBundle: function(lang, id) { return {lang,id}} + } + } + }); + return plugins.getPluginCatalogs({lang: "en-US"}).then(function(result) { + JSON.stringify(result).should.eql(JSON.stringify({ one: { lang: "en-US", id: "one" } })) + }) + }); + }); + +}); \ No newline at end of file diff --git a/test/unit/@node-red/runtime/lib/plugins_spec.js b/test/unit/@node-red/runtime/lib/plugins_spec.js new file mode 100644 index 000000000..a78de643c --- /dev/null +++ b/test/unit/@node-red/runtime/lib/plugins_spec.js @@ -0,0 +1,13 @@ +const should = require("should"); +const sinon = require("sinon"); +const NR_TEST_UTILS = require("nr-test-utils"); + +const plugins = NR_TEST_UTILS.require("@node-red/runtime/lib/plugins"); + +describe("runtime/plugins",function() { + + it.skip("delegates all functions to registry module", function() { + // There's no easy way to test this as we can't stub the registry functions + // before the plugin module gets a reference to them + }) +});