From 8461b1bae74fb7f29e72dce88b172854b8dd52cf Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 7 Jul 2026 15:10:06 +0100 Subject: [PATCH 1/2] Remove dependency on @node-red/util and implement local red_util module --- forge/containers/stub/index.js | 4 +- forge/containers/stub/red_util.js | 322 ++++++++++++++++++++++++++++++ package-lock.json | 246 ----------------------- package.json | 1 - 4 files changed, 325 insertions(+), 248 deletions(-) create mode 100644 forge/containers/stub/red_util.js diff --git a/forge/containers/stub/index.js b/forge/containers/stub/index.js index 0eb64c2e3c..99124e10cb 100644 --- a/forge/containers/stub/index.js +++ b/forge/containers/stub/index.js @@ -11,7 +11,9 @@ */ const { normalize } = require('path') -const nrUtil = require('@node-red/util') // eslint-disable-line +const nrUtil = { + util: require('./red_util') +} const forgeUtils = require('../../db/utils') diff --git a/forge/containers/stub/red_util.js b/forge/containers/stub/red_util.js new file mode 100644 index 0000000000..5bb9f17c3d --- /dev/null +++ b/forge/containers/stub/red_util.js @@ -0,0 +1,322 @@ +/* eslint-disable no-useless-escape */ + +// Extracted from @node-red/util/util.js to avoid dependency on @node-red/util package + +function createError (code, message) { + const e = new Error(message) + e.code = code + return e +} + +/** + * Parses a property expression, such as `msg.foo.bar[3]` to validate it + * and convert it to a canonical version expressed as an Array of property + * names. + * + * For example, `a["b"].c` returns `['a','b','c']` + * + * If `msg` is provided, any internal cross-references will be evaluated against that + * object. Otherwise, it will return a nested set of properties + * + * For example, without msg set, 'a[msg.foo]' returns `['a', [ 'msg', 'foo'] ]` + * But if msg is set to '{"foo": "bar"}', 'a[msg.foo]' returns `['a', 'bar' ]` + * + * @param {String} str - the property expression + * @return {Array} the normalised expression + * @memberof @node-red/util_util + */ +function normalisePropertyExpression (str, msg, toString) { + // This must be kept in sync with validatePropertyExpression + // in editor/js/ui/utils.js + + const length = str.length + if (length === 0) { + throw createError('INVALID_EXPR', 'Invalid property expression: zero-length') + } + const parts = [] + let start = 0 + let inString = false + let inBox = false + let quoteChar + let v + for (let i = 0; i < length; i++) { + const c = str[i] + if (!inString) { + if (c === "'" || c === '"') { + if (i !== start) { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i) + } + inString = true + quoteChar = c + start = i + 1 + } else if (c === '.') { + if (i === 0) { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected . at position 0') + } + if (start !== i) { + v = str.substring(start, i) + if (/^\d+$/.test(v)) { + parts.push(parseInt(v)) + } else { + parts.push(v) + } + } + if (i === length - 1) { + throw createError('INVALID_EXPR', 'Invalid property expression: unterminated expression') + } + // Next char is first char of an identifier: a-z 0-9 $ _ + if (!/[a-z0-9\$\_]/i.test(str[i + 1])) { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' at position ' + (i + 1)) + } + start = i + 1 + } else if (c === '[') { + if (i === 0) { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i) + } + if (start !== i) { + parts.push(str.substring(start, i)) + } + if (i === length - 1) { + throw createError('INVALID_EXPR', 'Invalid property expression: unterminated expression') + } + // Start of a new expression. If it starts with msg it is a nested expression + // Need to scan ahead to find the closing bracket + if (/^msg[.\[]/.test(str.substring(i + 1))) { + let depth = 1 + let inLocalString = false + let localStringQuote + for (let j = i + 1; j < length; j++) { + if (/["']/.test(str[j])) { + if (inLocalString) { + if (str[j] === localStringQuote) { + inLocalString = false + } + } else { + inLocalString = true + localStringQuote = str[j] + } + } + if (str[j] === '[') { + depth++ + } else if (str[j] === ']') { + depth-- + } + if (depth === 0) { + try { + if (msg) { + const crossRefProp = getMessageProperty(msg, str.substring(i + 1, j)) + if (crossRefProp === undefined) { + throw createError('INVALID_EXPR', 'Invalid expression: undefined reference at position ' + (i + 1) + ' : ' + str.substring(i + 1, j)) + } + parts.push(crossRefProp) + } else { + parts.push(normalisePropertyExpression(str.substring(i + 1, j), msg)) + } + inBox = false + i = j + start = j + 1 + break + } catch (err) { + throw createError('INVALID_EXPR', 'Invalid expression started at position ' + (i + 1)) + } + } + } + if (depth > 0) { + throw createError('INVALID_EXPR', "Invalid property expression: unmatched '[' at position " + i) + } + continue + } else if (!/["'\d]/.test(str[i + 1])) { + // Next char is either a quote or a number + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' at position ' + (i + 1)) + } + start = i + 1 + inBox = true + } else if (c === ']') { + if (!inBox) { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i) + } + if (start !== i) { + v = str.substring(start, i) + if (/^\d+$/.test(v)) { + parts.push(parseInt(v)) + } else { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected array expression at position ' + start) + } + } + start = i + 1 + inBox = false + } else if (c === ' ') { + throw createError('INVALID_EXPR', "Invalid property expression: unexpected ' ' at position " + i) + } + } else { + if (c === quoteChar) { + if (i - start === 0) { + throw createError('INVALID_EXPR', 'Invalid property expression: zero-length string at position ' + start) + } + parts.push(str.substring(start, i)) + // If inBox, next char must be a ]. Otherwise it may be [ or . + if (inBox && !/\]/.test(str[i + 1])) { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected array expression at position ' + start) + } else if (!inBox && i + 1 !== length && !/[\[\.]/.test(str[i + 1])) { + throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' expression at position ' + (i + 1)) + } + start = i + 1 + inString = false + } + } + } + if (inBox || inString) { + // eslint-disable-next-line new-cap + throw new createError('INVALID_EXPR', 'Invalid property expression: unterminated expression') + } + if (start < length) { + parts.push(str.substring(start)) + } + + if (toString) { + let result = parts.shift() + while (parts.length > 0) { + let p = parts.shift() + if (typeof p === 'string') { + if (/"/.test(p)) { + p = "'" + p + "'" + } else { + p = '"' + p + '"' + } + } + result = result + '[' + p + ']' + } + return result + } + + return parts +} + +/** + * Gets a property of a message object. + * + * Unlike {@link @node-red/util-util.getObjectProperty}, this function will strip `msg.` from the + * front of the property expression if present. + * + * @param {Object} msg - the message object + * @param {String} expr - the property expression + * @return {any} the message property, or undefined if it does not exist + * @throws Will throw an error if the *parent* of the property does not exist + * @memberof @node-red/util_util + */ +function getMessageProperty (msg, expr) { + if (expr.indexOf('msg.') === 0) { + expr = expr.substring(4) + } + return getObjectProperty(msg, expr) +} + +/** + * Gets a property of an object. + * + * Given the object: + * + * { + * "pet": { + * "type": "cat" + * } + * } + * + * - `pet.type` will return `"cat"`. + * - `pet.name` will return `undefined` + * - `car` will return `undefined` + * - `car.type` will throw an Error (as `car` does not exist) + * + * @param {Object} msg - the object + * @param {String} expr - the property expression + * @return {any} the object property, or undefined if it does not exist + * @throws Will throw an error if the *parent* of the property does not exist + * @memberof @node-red/util_util + */ +function getObjectProperty (msg, expr) { + let result = null + const msgPropParts = normalisePropertyExpression(expr, msg) + msgPropParts.reduce(function (obj, key) { + result = (typeof obj[key] !== 'undefined' ? obj[key] : undefined) + return result + }, msg) + return result +} + +/** + * Sets a property of an object. + * + * @param {Object} msg - the object + * @param {String} prop - the property expression + * @param {any} value - the value to set + * @param {boolean} createMissing - whether to create missing parent properties + * @memberof @node-red/util_util + */ +function setObjectProperty (msg, prop, value, createMissing) { + if (typeof createMissing === 'undefined') { + createMissing = (typeof value !== 'undefined') + } + const msgPropParts = normalisePropertyExpression(prop, msg) + const length = msgPropParts.length + let obj = msg + let key + for (let i = 0; i < length - 1; i++) { + key = msgPropParts[i] + if (typeof key === 'string' || (typeof key === 'number' && !Array.isArray(obj))) { + if (hasOwnProperty.call(obj, key)) { + if (length > 1 && ((typeof obj[key] !== 'object' && typeof obj[key] !== 'function') || obj[key] === null)) { + // Break out early as we cannot create a property beneath + // this type of value + return false + } + obj = obj[key] + } else if (createMissing) { + if (typeof msgPropParts[i + 1] === 'string') { + obj[key] = {} + } else { + obj[key] = [] + } + obj = obj[key] + } else { + return false + } + } else if (typeof key === 'number') { + // obj is an array + if (obj[key] === undefined) { + if (createMissing) { + if (typeof msgPropParts[i + 1] === 'string') { + obj[key] = {} + } else { + obj[key] = [] + } + obj = obj[key] + } else { + return false + } + } else { + obj = obj[key] + } + } + } + key = msgPropParts[length - 1] + if (typeof value === 'undefined') { + if (typeof key === 'number' && Array.isArray(obj)) { + obj.splice(key, 1) + } else { + delete obj[key] + } + } else { + if (typeof obj === 'object' && obj !== null) { + obj[key] = value + } else { + // Cannot set a property of a non-object/array + return false + } + } + return true +} + +module.exports = { + getObjectProperty, + setObjectProperty +} diff --git a/package-lock.json b/package-lock.json index 41b1e2b822..03497e8f15 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "@headlessui/vue": "1.7.23", "@heroicons/vue": "2.1.5", "@levminer/speakeasy": "^1.4.2", - "@node-red/util": "^5.0.0", "@node-saml/passport-saml": "^5.0.0", "@redis/client": "^6.0.0", "@sentry/node": "^10.54.0", @@ -5077,147 +5076,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@node-red/util": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@node-red/util/-/util-5.0.1.tgz", - "integrity": "sha512-w382JAYDFl6ui4J5FkVDLC6gCXXI2TiFLnx/YwgJMWgoq7YKLHj2CxKjYA3PJ5Sk24N83uq7GlR5C0qltKzOtw==", - "dependencies": { - "chalk": "^4.1.2", - "fs-extra": "11.3.0", - "i18next": "25.8.14", - "json-stringify-safe": "5.0.1", - "jsonata": "2.0.6", - "lodash.clonedeep": "^4.5.0", - "moment": "2.30.1", - "moment-timezone": "0.5.48" - } - }, - "node_modules/@node-red/util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@node-red/util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@node-red/util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@node-red/util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@node-red/util/node_modules/fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@node-red/util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@node-red/util/node_modules/i18next": { - "version": "25.8.14", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.14.tgz", - "integrity": "sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "dependencies": { - "@babel/runtime": "^7.28.4" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@node-red/util/node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, - "node_modules/@node-red/util/node_modules/moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@node-red/util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@node-saml/node-saml": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.1.0.tgz", @@ -15936,14 +15794,6 @@ "node": ">=6" } }, - "node_modules/jsonata": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.6.tgz", - "integrity": "sha512-WhQB5tXQ32qjkx2GYHFw2XbL90u+LLzjofAYwi+86g6SyZeXHz9F1Q0amy3dWRYczshOC3Haok9J4pOCgHtwyQ==", - "engines": { - "node": ">= 8" - } - }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -28418,97 +28268,6 @@ "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "dev": true }, - "@node-red/util": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@node-red/util/-/util-5.0.1.tgz", - "integrity": "sha512-w382JAYDFl6ui4J5FkVDLC6gCXXI2TiFLnx/YwgJMWgoq7YKLHj2CxKjYA3PJ5Sk24N83uq7GlR5C0qltKzOtw==", - "requires": { - "chalk": "^4.1.2", - "fs-extra": "11.3.0", - "i18next": "25.8.14", - "json-stringify-safe": "5.0.1", - "jsonata": "2.0.6", - "lodash.clonedeep": "^4.5.0", - "moment": "2.30.1", - "moment-timezone": "0.5.48" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "fs-extra": { - "version": "11.3.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", - "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" - }, - "i18next": { - "version": "25.8.14", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.14.tgz", - "integrity": "sha512-paMUYkfWJMsWPeE/Hejcw+XLhHrQPehem+4wMo+uELnvIwvCG019L9sAIljwjCmEMtFQQO3YeitJY8Kctei3iA==", - "requires": { - "@babel/runtime": "^7.28.4" - } - }, - "moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" - }, - "moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", - "requires": { - "moment": "^2.29.4" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "@node-saml/node-saml": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-5.1.0.tgz", @@ -35910,11 +35669,6 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, - "jsonata": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.6.tgz", - "integrity": "sha512-WhQB5tXQ32qjkx2GYHFw2XbL90u+LLzjofAYwi+86g6SyZeXHz9F1Q0amy3dWRYczshOC3Haok9J4pOCgHtwyQ==" - }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", diff --git a/package.json b/package.json index 99fc3556ce..aa11c6f983 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@headlessui/vue": "1.7.23", "@heroicons/vue": "2.1.5", "@levminer/speakeasy": "^1.4.2", - "@node-red/util": "^5.0.0", "@node-saml/passport-saml": "^5.0.0", "@redis/client": "^6.0.0", "@sentry/node": "^10.54.0", From ab54f180efb7e3a6396f3dcb07433dcd13be9b4e Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Tue, 7 Jul 2026 15:10:50 +0100 Subject: [PATCH 2/2] fix stub driver tests on windows by using posix normalise --- forge/containers/stub/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/forge/containers/stub/index.js b/forge/containers/stub/index.js index 99124e10cb..8a1d455947 100644 --- a/forge/containers/stub/index.js +++ b/forge/containers/stub/index.js @@ -9,7 +9,7 @@ * @memberof forge.containers.drivers * */ -const { normalize } = require('path') +const { posix: { normalize } } = require('path') const nrUtil = { util: require('./red_util')