From c520cebc6e0c028a5e2e2979dc369c42cc8c0f4e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 11 Sep 2024 16:13:10 +0100 Subject: [PATCH 1/3] refactor contrib nodes loading to support multiple sources and unit testing --- nodes/config/ui_base.js | 70 +++++++++++++++++++++++------------------ nodes/utils/index.js | 47 ++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 32 deletions(-) diff --git a/nodes/config/ui_base.js b/nodes/config/ui_base.js index dd03283e4..b12a048a7 100644 --- a/nodes/config/ui_base.js +++ b/nodes/config/ui_base.js @@ -1,10 +1,9 @@ -const fs = require('fs') const path = require('path') const v = require('../../package.json').version const datastore = require('../store/data.js') const statestore = require('../store/state.js') -const { appendTopic, addConnectionCredentials } = require('../utils/index.js') +const { appendTopic, addConnectionCredentials, getThirdPartyWidgets } = require('../utils/index.js') // from: https://stackoverflow.com/a/28592528/3016654 function join (...paths) { @@ -90,36 +89,8 @@ module.exports = function (RED) { /** * Load in third party widgets */ - let packagePath, packageJson - if (RED.settings?.userDir) { - packagePath = path.join(RED.settings.userDir, 'package.json') - packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')) - } else { - node.log('Cannot import third party widgets. No access to Node-RED package.json') - } - if (packageJson && packageJson.dependencies) { - Object.entries(packageJson.dependencies)?.filter(([packageName, _packageVersion]) => { - return packageName.includes('node-red-dashboard-2-') - }).map(([packageName, _packageVersion]) => { - const modulePath = path.join(RED.settings.userDir, 'node_modules', packageName) - const packagePath = path.join(modulePath, 'package.json') - // get third party package.json - const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')) - if (packageJson?.['node-red-dashboard-2']) { - // loop over object of widgets - Object.entries(packageJson['node-red-dashboard-2'].widgets).forEach(([widgetName, widgetConfig]) => { - uiShared.contribs[widgetName] = { - package: packageName, - name: widgetName, - src: widgetConfig.output, - component: widgetConfig.component - } - }) - } - return packageJson - }) - } + uiShared.contribs = loadContribs(node) /** * Configure Web Server to handle UI traffic @@ -217,6 +188,43 @@ module.exports = function (RED) { } } + function loadContribs (node) { + // from nodesDir + let contribs = { ...uiShared.contribs } + if (RED.settings?.nodesDir) { + const nodesDir = Array.isArray(RED.settings.nodesDir) ? RED.settings.nodesDir : [RED.settings.nodesDir] + for (const dir of nodesDir) { + try { + if (!dir || typeof dir !== 'string') { continue } + const _contribs = getThirdPartyWidgets(dir) + contribs = { ...contribs, ..._contribs } + } catch (error) { + node.log(`Cannot import third party widgets from nodes directory '${dir}}' package.json`) + } + } + } + + // from user directory package.json + if (RED.settings?.userDir) { + try { + const _contribs = getThirdPartyWidgets(RED.settings.userDir) + contribs = { ...contribs, ..._contribs } + } catch (error) { + node.log('Cannot import third party widgets from user directory package.json') + } + } + + // from main Node-RED package.json + try { + const appRoot = path.join(require.main.paths?.[0]?.split('node_modules')[0], '..') + const _contribs = getThirdPartyWidgets(appRoot) + contribs = { ...contribs, ..._contribs } + } catch (error) { + node.log('Cannot import third party widgets from main application root package.json') + } + return contribs + } + /** * Close the SocketIO Server */ diff --git a/nodes/utils/index.js b/nodes/utils/index.js index 6c5465669..5e288a1f2 100644 --- a/nodes/utils/index.js +++ b/nodes/utils/index.js @@ -1,3 +1,6 @@ +const fs = require('fs') +const path = require('path') + function asyncEvaluateNodeProperty (RED, value, type, node, msg) { return new Promise(function (resolve, reject) { RED.util.evaluateNodeProperty(value, type, node, msg, function (e, r) { @@ -54,8 +57,50 @@ function addConnectionCredentials (RED, msg, conn, config) { return msg } +function getThirdPartyWidgets (directory) { + const contribs = {} + const packagePath = path.join(directory, 'package.json') + if (!fs.existsSync(packagePath)) { + return contribs + } + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')) + const getWidgets = (packageJson) => { + if (packageJson?.['node-red-dashboard-2']) { + // loop over object of widgets & add to contribs object + Object.entries(packageJson['node-red-dashboard-2'].widgets).forEach(([widgetName, widgetConfig]) => { + contribs[widgetName] = { + package: packageJson.name, + name: widgetName, + src: widgetConfig.output, + path: path.resolve(path.join(directory)), + component: widgetConfig.component + } + }) + } + } + if (packageJson?.['node-red-dashboard-2']) { + // this _is_ a dashboard node! get its widgets. + getWidgets(packageJson) + } else if (packageJson && packageJson.dependencies) { + // get widgets from dependencies of this package + Object.entries(packageJson.dependencies)?.filter(([packageName, _packageVersion]) => { + return packageName.includes('node-red-dashboard-2-') + }).forEach(([packageName, _packageVersion]) => { + const modulePath = path.join(directory, 'node_modules', packageName) + const packagePath = path.join(modulePath, 'package.json') + // get third party package.json + const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')) + if (packageJson?.['node-red-dashboard-2']) { + getWidgets(packageJson) + } + }) + } + return contribs +} + module.exports = { asyncEvaluateNodeProperty, appendTopic, - addConnectionCredentials + addConnectionCredentials, + getThirdPartyWidgets } From 12d43a293d116cf8ba94d3d6ea9da9b4b5e5c4a3 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Wed, 11 Sep 2024 16:13:46 +0100 Subject: [PATCH 2/3] add unit tests for contrib loading utils routine --- test/nodes/fixtures/contrib-node/package.json | 20 +++++++++ .../package.json | 16 +++++++ .../package.json | 16 +++++++ .../nodes/fixtures/contrib-nodes/package.json | 11 +++++ test/nodes/utils.spec.js | 42 +++++++++++++++++++ 5 files changed, 105 insertions(+) create mode 100644 test/nodes/fixtures/contrib-node/package.json create mode 100644 test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-a/package.json create mode 100644 test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-b/package.json create mode 100644 test/nodes/fixtures/contrib-nodes/package.json create mode 100644 test/nodes/utils.spec.js diff --git a/test/nodes/fixtures/contrib-node/package.json b/test/nodes/fixtures/contrib-node/package.json new file mode 100644 index 000000000..618d66a16 --- /dev/null +++ b/test/nodes/fixtures/contrib-node/package.json @@ -0,0 +1,20 @@ +{ + "name": "@me/node-red-dashboard-2-two-widgets", + "version": "1.0.0", + "description": "My dashboard 2 test node package", + "node-red-dashboard-2": { + "version": "1.0.0", + "widgets": { + "ui-widget-1": { + "output": "ui-widget-1.js", + "component": "ui-widget-1" + }, + "ui-widget-2": { + "output": "ui-widget-2.js", + "component": "ui-widget-2" + } + } + }, + "author": "Your Name", + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-a/package.json b/test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-a/package.json new file mode 100644 index 000000000..8593c14f0 --- /dev/null +++ b/test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-a/package.json @@ -0,0 +1,16 @@ +{ + "name": "@me/node-red-dashboard-2-widget-a", + "version": "1.0.0", + "description": "My dashboard 2 dependant contrib 1", + "node-red-dashboard-2": { + "version": "1.0.0", + "widgets": { + "widget-a": { + "output": "ui-widget.js", + "component": "ui-widget-a" + } + } + }, + "author": "Your Name", + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-b/package.json b/test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-b/package.json new file mode 100644 index 000000000..8575ded8a --- /dev/null +++ b/test/nodes/fixtures/contrib-nodes/node_modules/@me/node-red-dashboard-2-widget-b/package.json @@ -0,0 +1,16 @@ +{ + "name": "@me/node-red-dashboard-2-widget-b", + "version": "1.0.0", + "description": "My dashboard 2 dependant contrib 2", + "node-red-dashboard-2": { + "version": "1.0.0", + "widgets": { + "widget-b": { + "output": "ui-widget.js", + "component": "ui-widget-b" + } + } + }, + "author": "Your Name", + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/test/nodes/fixtures/contrib-nodes/package.json b/test/nodes/fixtures/contrib-nodes/package.json new file mode 100644 index 000000000..16c2f6017 --- /dev/null +++ b/test/nodes/fixtures/contrib-nodes/package.json @@ -0,0 +1,11 @@ +{ + "name": "@me/my-2-widgets", + "version": "1.0.0", + "description": "My dashboard 2 test node", + "dependencies": { + "@me/node-red-dashboard-2-widget-a": "1.0.0", + "@me/node-red-dashboard-2-widget-b": "1.0.0" + }, + "author": "Your Name", + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/test/nodes/utils.spec.js b/test/nodes/utils.spec.js new file mode 100644 index 000000000..c4d266d2d --- /dev/null +++ b/test/nodes/utils.spec.js @@ -0,0 +1,42 @@ +const should = require('should') // eslint-disable-line no-unused-vars + +const utils = require('../../nodes/utils/index.js') + +describe('utils', function () { + describe('getThirdPartyWidgets', function () { + it('should load single node package', function () { + // this covers loading from a nodesDir source + const widgets = utils.getThirdPartyWidgets('test/nodes/fixtures/contrib-node') + widgets.should.be.an.Object() + widgets.should.have.properties(['ui-widget-1', 'ui-widget-2']) + widgets['ui-widget-1'].should.have.properties(['component', 'name', 'package', 'path', 'src']) + widgets['ui-widget-1'].component.should.equal('ui-widget-1') + widgets['ui-widget-1'].name.should.equal('ui-widget-1') + widgets['ui-widget-1'].package.should.equal('@me/node-red-dashboard-2-two-widgets') + widgets['ui-widget-1'].src.should.equal('ui-widget-1.js') + + widgets['ui-widget-2'].should.have.properties(['component', 'name', 'package', 'path', 'src']) + widgets['ui-widget-2'].component.should.equal('ui-widget-2') + widgets['ui-widget-2'].name.should.equal('ui-widget-2') + widgets['ui-widget-2'].package.should.equal('@me/node-red-dashboard-2-two-widgets') + widgets['ui-widget-2'].src.should.equal('ui-widget-2.js') + }) + it('should load nodes from a package dependencies', function () { + // this covers loading from node-red src package and from userDir package + const widgets = utils.getThirdPartyWidgets('test/nodes/fixtures/contrib-nodes') + widgets.should.be.an.Object() + widgets.should.have.properties(['widget-a', 'widget-b']) + widgets['widget-a'].should.have.properties(['component', 'name', 'package', 'path', 'src']) + widgets['widget-a'].component.should.equal('ui-widget-a') + widgets['widget-a'].name.should.equal('widget-a') + widgets['widget-a'].package.should.equal('@me/node-red-dashboard-2-widget-a') + widgets['widget-a'].src.should.equal('ui-widget.js') + + widgets['widget-b'].should.have.properties(['component', 'name', 'package', 'path', 'src']) + widgets['widget-b'].component.should.equal('ui-widget-b') + widgets['widget-b'].name.should.equal('widget-b') + widgets['widget-b'].package.should.equal('@me/node-red-dashboard-2-widget-b') + widgets['widget-b'].src.should.equal('ui-widget.js') + }) + }) +}) From 6e8600d1d156bb7f3520f0dd28d7770ca1fcdf7c Mon Sep 17 00:00:00 2001 From: Stephen McLaughlin <44235289+Steve-Mcl@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:39:45 +0100 Subject: [PATCH 3/3] Update nodes/utils/index.js --- nodes/utils/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes/utils/index.js b/nodes/utils/index.js index 5e288a1f2..c16ebafd3 100644 --- a/nodes/utils/index.js +++ b/nodes/utils/index.js @@ -72,7 +72,7 @@ function getThirdPartyWidgets (directory) { package: packageJson.name, name: widgetName, src: widgetConfig.output, - path: path.resolve(path.join(directory)), + path: path.resolve(directory), component: widgetConfig.component } })