diff --git a/lib/launcher.js b/lib/launcher.js index e658330..4362991 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -3,6 +3,7 @@ const { existsSync } = require('fs') const fs = require('fs/promises') const path = require('path') const { info, debug, warn, NRlog } = require('./logging/log') +const utils = require('./utils') const MIN_RESTART_TIME = 10000 // 10 seconds const MAX_RESTART_COUNT = 5 @@ -194,13 +195,12 @@ class Launcher { path.join(__dirname, 'plugins', 'node_modules', '@flowforge', 'flowforge-library-plugin').replace(/\\/g, '/') ] // Check to see if we're in the dev-env - if so, we need to set a different - // path to the `@flowforge/nr-project-nodes` for development purposes - const devEnvPath = path.join(__dirname, '..', '..', '..', 'packages', 'nr-project-nodes').replace(/\\/g, '/') - if (existsSync(devEnvPath)) { - settings.nodesDir.push(devEnvPath) - } else { - settings.nodesDir.push(path.join(__dirname, '..', 'node_modules', '@flowfuse', 'nr-project-nodes').replace(/\\/g, '/')) + // path to the project-nodes for development purposes + let projectNodesPackage = '@flowfuse/nr-project-nodes' // runtime default + if (utils.isDevEnv()) { + projectNodesPackage = 'nr-project-nodes' } + settings.nodesDir.push(utils.getPackagePath(projectNodesPackage)) const sharedLibraryConfig = { id: 'flowfuse-team-library', type: 'flowfuse-team-library', diff --git a/lib/utils.js b/lib/utils.js index 2dc8ca7..e47a4db 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,8 +1,13 @@ +const path = require('path') +const existsSync = require('fs').existsSync + module.exports = { compareNodeRedData, compareObjects, isObject, - hasProperty + hasProperty, + isDevEnv, + getPackagePath } /** @@ -77,3 +82,54 @@ function isObject (object) { function hasProperty (object, property) { return !!(object && Object.prototype.hasOwnProperty.call(object, property)) } + +const devPackages = path.join(__dirname, '..', '..', '..', 'packages') +const runtimePackages = path.join(__dirname, '..', 'node_modules') + +/** + * Test if the runtime is running in a development environment. + * Development environment is defined as: + * * `NODE_ENV` is set to 'development' + * - OR + * * 'packages' directory exists AND another "known" package exists (/packages/nr-project-nodes) + * @returns {boolean} true if the runtime is running in a development environment + */ +function isDevEnv () { + // if NODE_ENV is set, use that + if (process.env.NODE_ENV) { + return process.env.NODE_ENV === 'development' + } + const devEnvTestPath = path.join(devPackages, 'nr-project-nodes').replace(/\\/g, '/') + if (existsSync(devEnvTestPath)) { + return true + } + return false +} + +/** + * Get the full path to a package. + * + * When running in a development environment, the path to the package in the dev-env is returned. + * + * When running in a runtime environment, the path to the package in the device-agent node_modules is returned. + * @example + * // process.env.NODE_ENV = 'development' + * getPackagePath('nr-project-nodes') + * // returns '/path/to/dev-env/packages/nr-project-nodes' + * @example + * // process.env.NODE_ENV = '' && 'nr-project-nodes' exists in `dev-env/packages` + * getPackagePath('nr-project-nodes') + * // returns '/path/to/dev-env/packages/nr-project-nodes' + * @example + * // process.env.NODE_ENV = 'production' || 'nr-project-nodes' does not exist in `dev-env/packages` + * getPackagePath('@flowfuse/nr-project-nodes') + * // returns '/path/to/device-agent/node_modules/@flowfuse/nr-project-nodes' + * @param {...string} packageName Name of the package to get the path for + * @returns {string} The full path to the package + */ +function getPackagePath (...packageName) { + if (isDevEnv()) { + return path.join(devPackages, ...packageName).replace(/\\/g, '/') + } + return path.join(runtimePackages, ...packageName).replace(/\\/g, '/') +} diff --git a/test/unit/lib/launcher_spec.js b/test/unit/lib/launcher_spec.js index 146fea6..c9e72be 100644 --- a/test/unit/lib/launcher_spec.js +++ b/test/unit/lib/launcher_spec.js @@ -1,4 +1,6 @@ const should = require('should') +const sinon = require('sinon') +const utils = require('../../../lib/utils') const { newLauncher } = require('../../../lib/launcher') const setup = require('../setup') const fs = require('fs/promises') @@ -15,7 +17,7 @@ describe('Launcher', function () { dir: '', verbose: true } - + const nodeEnv = process.env.NODE_ENV beforeEach(async function () { config.dir = await fs.mkdtemp(path.join(os.tmpdir(), 'ff-launcher-')) await fs.mkdir(path.join(config.dir, 'project')) @@ -23,6 +25,8 @@ describe('Launcher', function () { afterEach(async function () { await fs.rm(config.dir, { recursive: true, force: true }) + process.env.NODE_ENV = nodeEnv // restore NODE_ENV + sinon.restore() }) it('Create Snapshot Flow/Creds Files, instance bound device', async function () { @@ -157,4 +161,68 @@ describe('Launcher', function () { settings.editorTheme.should.have.property('palette') settings.editorTheme.palette.should.not.have.a.property('catalogue') }) + it('Uses flowfuse project nodes from dev-env when detected', async function () { + // Summary: if dev-env is detected, then the launcher should use the project nodes from dev-env + const licensedConfig = { + ...config, + licenseType: 'ee', + licensed: true + } + // set NODE_ENV to 'development' to simulate dev-env + process.env.NODE_ENV = 'development' + // spy utils.getPackagePath to ensure it is called with 'nr-project-nodes' + sinon.spy(utils, 'getPackagePath') + const launcher = newLauncher(licensedConfig, null, 'projectId', setup.snapshot) + await launcher.writeSettings() + + // check that utils.getPackagePath was called with 'nr-project-nodes' + utils.getPackagePath.calledWith('nr-project-nodes').should.be.true() + + // check that settings.nodesDir contains the dev path to the project nodes + const setFile = await fs.readFile(path.join(config.dir, 'project', 'settings.json')) + const settings = JSON.parse(setFile) + settings.should.have.property('nodesDir') + + // because we are in dev-env, settings.nodesDir should contain the dev path + 'packages/nr-project-nodes' + settings.nodesDir.filter((dir) => dir.endsWith('packages/nr-project-nodes')).should.have.a.lengthOf(1) + }) + it('Uses flowfuse project nodes from device node_modules is runtime', async function () { + // Summary: if dev-env is not detected, then the launcher should use the project nodes from device-agent node_modules + const licensedConfig = { + ...config, + licenseType: 'ee', + licensed: true + } + // set NODE_ENV to 'production' to simulate runtime + process.env.NODE_ENV = 'production' + // spy utils.getPackagePath to ensure it is called with '@flowfuse/nr-project-nodes' + sinon.spy(utils, 'getPackagePath') + const launcher = newLauncher(licensedConfig, null, 'projectId', setup.snapshot) + await launcher.writeSettings() + + // check that utils.getPackagePath was called with '@flowfuse/nr-project-nodes' + utils.getPackagePath.calledWith('@flowfuse/nr-project-nodes').should.be.true() + + // check that settings.nodesDir contains the dev path to the project nodes + const setFile = await fs.readFile(path.join(config.dir, 'project', 'settings.json')) + const settings = JSON.parse(setFile) + settings.should.have.property('nodesDir') + + // because we are in runtime, settings.nodesDir should contain the agent path + 'node_modules/@flowfuse/nr-project-nodes' + settings.nodesDir.filter((dir) => dir.endsWith('node_modules/@flowfuse/nr-project-nodes')).should.have.a.lengthOf(1) + }) + it('Does not add path for project nodes when unlicensed', async function () { + // Summary: if NON EE, then nodesDir should either be empty OR should NOT contain 'nr-project-nodes' + const unlicensedConfig = { + ...config, + licensed: false + } + const launcher = newLauncher(unlicensedConfig, null, 'projectId', setup.snapshot) + await launcher.writeSettings() + const setFile = await fs.readFile(path.join(config.dir, 'project', 'settings.json')) + const settings = JSON.parse(setFile) + if (settings.nodesDir && Array.isArray(settings.nodesDir) && settings.nodesDir.length > 0) { + settings.nodesDir.filter((dir) => dir.endsWith('nr-project-nodes')).should.have.a.lengthOf(0) + } + }) }) diff --git a/test/unit/lib/util_spec.js b/test/unit/lib/util_spec.js index b7c5662..a56f749 100644 --- a/test/unit/lib/util_spec.js +++ b/test/unit/lib/util_spec.js @@ -1,5 +1,6 @@ const should = require('should') // eslint-disable-line const utils = require('../../../lib/utils.js') +const path = require('path') /* Ensure utils used throughout agent are tested @@ -103,4 +104,28 @@ describe('utils', function () { utils.compareNodeRedData({ modules: { 'node-red': 'latest' } }, { modules: undefined }).should.be.false() }) }) + describe('getPackagePath', function () { + const devPackages = path.join(__dirname, '..', '..', '..', '..', '..', 'packages') + const runtimePackages = path.join(__dirname, '..', '..', '..', 'node_modules') + describe('developer environment', function () { + beforeEach(function () { + process.env.NODE_ENV = 'development' // simulate dev environment + }) + it('should return path of the specified packageName in the dev-env path', function () { + const pkgPath = utils.getPackagePath('nr-project-nodes') + pkgPath.should.be.a.String() + pkgPath.should.eql(path.join(devPackages, 'nr-project-nodes').replace(/\\/g, '/')) + }) + }) + describe('runtime environment', function () { + beforeEach(function () { + process.env.NODE_ENV = 'production' // simulate runtime environment + }) + it('should return path of the specified packageName in the device-agent node_modules', function () { + const pkgPath = utils.getPackagePath('@flowfuse/nr-project-nodes') + pkgPath.should.be.a.String() + pkgPath.should.eql(path.join(runtimePackages, '@flowfuse', 'nr-project-nodes').replace(/\\/g, '/')) + }) + }) + }) })