From a006b5205234b076df2f3a9ff753ea3fb5d4c2cb Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 10 Dec 2020 16:01:55 +0000 Subject: [PATCH 1/6] 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 + }) +}); From 7531314e3feed1fab2235527c4b7e72724806899 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 14 Dec 2020 10:40:06 +0000 Subject: [PATCH 2/6] Add RED.plugins module to editor --- Gruntfile.js | 1 + .../@node-red/editor-client/src/js/plugins.js | 29 +++++++++++++++++++ .../test-plugin/test-editor-plugin.html | 8 +++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 packages/node_modules/@node-red/editor-client/src/js/plugins.js diff --git a/Gruntfile.js b/Gruntfile.js index 2bdc2e3aa..23481f793 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -142,6 +142,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/text/bidi.js", "packages/node_modules/@node-red/editor-client/src/js/text/format.js", "packages/node_modules/@node-red/editor-client/src/js/ui/state.js", + "packages/node_modules/@node-red/editor-client/src/js/plugins.js", "packages/node_modules/@node-red/editor-client/src/js/nodes.js", "packages/node_modules/@node-red/editor-client/src/js/font-awesome.js", "packages/node_modules/@node-red/editor-client/src/js/history.js", diff --git a/packages/node_modules/@node-red/editor-client/src/js/plugins.js b/packages/node_modules/@node-red/editor-client/src/js/plugins.js new file mode 100644 index 000000000..25d6acf8a --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/plugins.js @@ -0,0 +1,29 @@ +RED.plugins = (function() { + var plugins = {}; + var pluginsByType = {}; + + function registerPlugin(id,definition) { + plugins[id] = definition; + if (definition.type) { + pluginsByType[definition.type] = pluginsByType[definition.type] || []; + pluginsByType[definition.type].push(definition); + } + if (definition.onadd && typeof definition.onadd === 'function') { + definition.onadd(); + } + RED.events.emit("registry:plugin-added",id); + } + + function getPlugin(id) { + return plugins[id] + } + + function getPluginsByType(type) { + return pluginsByType[type] || []; + } + return { + registerPlugin: registerPlugin, + getPlugin: getPlugin, + getPluginsByType: getPluginsByType + } +})(); diff --git a/test/resources/plugin/test-plugin/test-editor-plugin.html b/test/resources/plugin/test-plugin/test-editor-plugin.html index 067d09d3c..177813526 100644 --- a/test/resources/plugin/test-plugin/test-editor-plugin.html +++ b/test/resources/plugin/test-plugin/test-editor-plugin.html @@ -1,5 +1,9 @@ \ No newline at end of file From 9f71dbb0060c9070fc99f683cf71c5f0f8427679 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 18 Jan 2021 15:11:24 +0000 Subject: [PATCH 3/6] Fixup merge --- packages/node_modules/@node-red/editor-client/src/js/red.js | 1 - packages/node_modules/@node-red/registry/lib/loader.js | 5 ----- 2 files changed, 6 deletions(-) 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 e3439c435..229147b10 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 @@ -25,7 +25,6 @@ var RED = (function() { cache: false, url: 'plugins', success: function(data) { - console.log(data); loader.reportProgress(RED._("event.loadPlugins"), 13) RED.i18n.loadPluginCatalogs(function() { loadPlugins(function() { diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index d29eadc66..74eaa05c1 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -478,7 +478,6 @@ function addModule(module) { return Promise.reject(e); } try { -<<<<<<< HEAD var moduleFiles = {}; var moduleStack = [module]; while(moduleStack.length > 0) { @@ -502,10 +501,6 @@ 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); } From 9e179170ee082da3d3bb30085916e0dfebbaae78 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 20 Jan 2021 15:35:44 +0000 Subject: [PATCH 4/6] Add i18n function to editor plugins when they are registered Adds a `_` function to the plugin definition object that will automatically prepend the plugin's module namespace to any call. This saves the plugin from having to prepend its namespace all of the time. --- .../@node-red/editor-client/src/js/plugins.js | 17 +++++++++++++++++ .../@node-red/editor-client/src/js/red.js | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/plugins.js b/packages/node_modules/@node-red/editor-client/src/js/plugins.js index 25d6acf8a..e6f41517d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/plugins.js +++ b/packages/node_modules/@node-red/editor-client/src/js/plugins.js @@ -8,6 +8,23 @@ RED.plugins = (function() { pluginsByType[definition.type] = pluginsByType[definition.type] || []; pluginsByType[definition.type].push(definition); } + if (RED._loadingModule) { + definition.module = RED._loadingModule; + definition["_"] = function() { + var args = Array.prototype.slice.call(arguments); + var originalKey = args[0]; + if (!/:/.test(args[0])) { + args[0] = definition.module+":"+args[0]; + } + var result = RED._.apply(null,args); + if (result === args[0]) { + return originalKey; + } + return result; + } + } else { + definition["_"] = RED["_"] + } if (definition.onadd && typeof definition.onadd === 'function') { definition.onadd(); } 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 229147b10..a2bcb0e7d 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 @@ -67,11 +67,11 @@ var RED = (function() { var moduleId; if (moduleIdMatch) { moduleId = moduleIdMatch[1]; + RED._loadingModule = moduleId; } else { moduleId = "unknown"; } try { - var hasDeferred = false; var nodeConfigEls = $("
"+config+"
"); var scripts = nodeConfigEls.find("script"); @@ -85,6 +85,7 @@ var RED = (function() { scriptCount--; if (scriptCount === 0) { $(targetContainer).append(nodeConfigEls); + delete RED._loadingModule; done() } } @@ -108,6 +109,7 @@ var RED = (function() { }) if (!hasDeferred) { $(targetContainer).append(nodeConfigEls); + delete RED._loadingModule; done(); } } catch(err) { @@ -116,6 +118,7 @@ var RED = (function() { timeout: 10000 }); console.log("["+moduleId+"] "+err.toString()); + delete RED._loadingModule; done(); } } From 6e718ca77211221abdc31754c3638ca3ad60ca47 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 26 Jan 2021 13:44:38 +0000 Subject: [PATCH 5/6] Fix merge of dev --- packages/node_modules/@node-red/registry/lib/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index 74eaa05c1..bd5a7aae4 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -500,7 +500,7 @@ function addModule(module) { } } } - return loadNodeFiles(moduleFiles).then(() => module) + return loadModuleFiles(moduleFiles).then(() => module) } catch(err) { return Promise.reject(err); } From 8e7a230dbc9078047aa725813355c3a0ee884891 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 26 Jan 2021 13:49:13 +0000 Subject: [PATCH 6/6] Fix plugin test to expect user flag --- test/unit/@node-red/registry/lib/plugins_spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/unit/@node-red/registry/lib/plugins_spec.js b/test/unit/@node-red/registry/lib/plugins_spec.js index 430553fac..8f32d3802 100644 --- a/test/unit/@node-red/registry/lib/plugins_spec.js +++ b/test/unit/@node-red/registry/lib/plugins_spec.js @@ -120,6 +120,7 @@ test-module-config`) "id": "test-module/test-set", "enabled": true, "local": false, + "user": false, "plugins": [ { "type": "foo", @@ -142,6 +143,7 @@ test-module-config`) "id": "test-module/test-disabled-set", "enabled": false, "local": false, + "user": false, "plugins": [] } ]