const MenuUtils = require('./menuUtils'); /** * Server-side helper for exposing MenuUtils to the browser via HTTP endpoints. */ class EndpointUtils { /** * @param {Object} options * @param {Function} options.MenuUtilsClass the MenuUtils constructor/function */ constructor({ MenuUtilsClass = MenuUtils } = {}) { this.MenuUtils = MenuUtilsClass; } /** * Registers an HTTP GET endpoint that serves the client-side MenuUtils code * @param {object} RED the Node-RED API object * @param {string} nodeName the name of the node (used in the URL) * @param {object} customHelpers additional helper functions to inject */ createMenuUtilsEndpoint(RED, nodeName, customHelpers = {}) { RED.httpAdmin.get(`/${nodeName}/resources/menuUtils.js`, (req, res) => { console.log(`Serving menuUtils.js for ${nodeName} node`); res.set('Content-Type', 'application/javascript'); const browserCode = this.generateMenuUtilsCode(nodeName, customHelpers); res.send(browserCode); }); } /** * Generates the browser-side JavaScript that redefines MenuUtils and helper fns * @param {string} nodeName * @param {object} customHelpers map of name: functionString pairs * @returns {string} a JS snippet to run in the browser */ generateMenuUtilsCode(nodeName, customHelpers = {}) { // Default helper implementations to expose alongside MenuUtils const defaultHelpers = { validateRequired: `function(value) { return value != null && value.toString().trim() !== ''; }`, formatDisplayValue: `function(value, unit) { return \`${'${'}value} ${'${'}unit || ''}\`.trim(); }`, validateScaling: `function(min, max) { return !isNaN(min) && !isNaN(max) && Number(min) < Number(max); }`, validateUnit: `function(unit) { return typeof unit === 'string' && unit.trim() !== ''; }`, }; // Merge any custom overrides const allHelpers = { ...defaultHelpers, ...customHelpers }; // Build the helpers code block const helpersCode = Object.entries(allHelpers) .map(([name, fnBody]) => ` ${name}: ${fnBody}`) .join(',\n'); // Introspect MenuUtils prototype to extract method definitions const proto = this.MenuUtils.prototype; const browserMethods = Object.getOwnPropertyNames(proto) .filter(key => key !== 'constructor') .map(methodName => { const fn = proto[methodName]; const src = fn.toString(); const isAsync = fn.constructor.name === 'AsyncFunction'; // extract signature and body const signature = src.slice(src.indexOf('(')); const prefix = isAsync ? 'async ' : ''; return ` ${prefix}${methodName}${signature}`; }) .join('\n\n'); // Return a complete JS snippet for the browser return ` // Auto-generated MenuUtils for node: ${nodeName} window.EVOLV = window.EVOLV || {}; window.EVOLV.nodes = window.EVOLV.nodes || {}; window.EVOLV.nodes.${nodeName} = window.EVOLV.nodes.${nodeName} || {}; class MenuUtils { constructor(opts) { // Allow same options API as server-side this.useCloud = opts.useCloud || false; this.projectSettings = opts.projectSettings || {}; // any other client-side initialization... } ${browserMethods} } window.EVOLV.nodes.${nodeName}.utils = { menuUtils: new MenuUtils({}), helpers: { ${helpersCode} } }; console.log('Loaded EVOLV.nodes.${nodeName}.utils.menuUtils'); `; } } module.exports = EndpointUtils;