From 7fd17b4ec04839246b5c3c11d2513ac6206f3bcd Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 14 Jul 2021 19:18:39 +0100 Subject: [PATCH] Add RED.import to support importing ES6 modules --- .../nodes/core/function/10-function.js | 345 +++++++++--------- .../@node-red/registry/lib/externalModules.js | 30 +- .../@node-red/registry/lib/util.js | 11 + 3 files changed, 211 insertions(+), 175 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/10-function.js b/packages/node_modules/@node-red/nodes/core/function/10-function.js index c1c20f266..28e560005 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-function.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-function.js @@ -290,6 +290,7 @@ module.exports = function(RED) { }; sandbox.promisify = util.promisify; } + const moduleLoadPromises = []; if (node.hasOwnProperty("libs")) { let moduleErrors = false; @@ -303,199 +304,197 @@ module.exports = function(RED) { return; } sandbox[vname] = null; - try { - var spec = module.module; - if (spec && (spec !== "")) { - var lib = RED.require(module.module); + var spec = module.module; + if (spec && (spec !== "")) { + RED.import(module.module).then(lib => { sandbox[vname] = lib; - } - } catch (e) { - //TODO: NLS error message - node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:e.toString()})) - moduleErrors = true; + }).catch(err => { + node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:err.toString()})) + moduleErrors = true; + throw err; + }); } } }); - if (moduleErrors) { - throw new Error(RED._("function.error.externalModuleLoadError")); - } } + Promise.all(moduleLoadPromises).then(() => { + const RESOLVING = 0; + const RESOLVED = 1; + const ERROR = 2; + var state = RESOLVING; + var messages = []; + var processMessage = (() => {}); + node.on("input", function(msg,send,done) { + if(state === RESOLVING) { + messages.push({msg:msg, send:send, done:done}); + } + else if(state === RESOLVED) { + processMessage(msg, send, done); + } + }); - const RESOLVING = 0; - const RESOLVED = 1; - const ERROR = 2; - var state = RESOLVING; - var messages = []; - var processMessage = (() => {}); - - node.on("input", function(msg,send,done) { - if(state === RESOLVING) { - messages.push({msg:msg, send:send, done:done}); - } - else if(state === RESOLVED) { - processMessage(msg, send, done); - } - }); - - var context = vm.createContext(sandbox); - try { - var iniScript = null; - var iniOpt = null; - if (node.ini && (node.ini !== "")) { - var iniText = ` - (async function(__send__) { - var node = { - id:__node__.id, - name:__node__.name, - outputCount:__node__.outputCount, - log:__node__.log, - error:__node__.error, - warn:__node__.warn, - debug:__node__.debug, - trace:__node__.trace, - status:__node__.status, - send: function(msgs, cloneMsg) { - __node__.send(__send__, RED.util.generateId(), msgs, cloneMsg); - } - }; - `+ node.ini +` - })(__initSend__);`; - iniOpt = createVMOpt(node, " setup"); - iniScript = new vm.Script(iniText, iniOpt); - } - node.script = vm.createScript(functionText, createVMOpt(node, "")); - if (node.fin && (node.fin !== "")) { - var finText = `(function () { - var node = { - id:__node__.id, - name:__node__.name, - outputCount:__node__.outputCount, - log:__node__.log, - error:__node__.error, - warn:__node__.warn, - debug:__node__.debug, - trace:__node__.trace, - status:__node__.status, - send: function(msgs, cloneMsg) { - __node__.error("Cannot send from close function"); - } - }; - `+node.fin +` - })();`; - finOpt = createVMOpt(node, " cleanup"); - finScript = new vm.Script(finText, finOpt); - } - var promise = Promise.resolve(); - if (iniScript) { - context.__initSend__ = function(msgs) { node.send(msgs); }; - promise = iniScript.runInContext(context, iniOpt); - } - - processMessage = function (msg, send, done) { - var start = process.hrtime(); - context.msg = msg; - context.__send__ = send; - context.__done__ = done; - - node.script.runInContext(context); - context.results.then(function(results) { - sendResults(node,send,msg._msgid,results,false); - if (handleNodeDoneCall) { - done(); - } - - var duration = process.hrtime(start); - var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100; - node.metric("duration", msg, converted); - if (process.env.NODE_RED_FUNCTION_TIME) { - node.status({fill:"yellow",shape:"dot",text:""+converted}); - } - }).catch(err => { - if ((typeof err === "object") && err.hasOwnProperty("stack")) { - //remove unwanted part - var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/); - err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n'); - var stack = err.stack.split(/\r?\n/); - - //store the error in msg to be used in flows - msg.error = err; - - var line = 0; - var errorMessage; - if (stack.length > 0) { - while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { - line++; + var context = vm.createContext(sandbox); + try { + var iniScript = null; + var iniOpt = null; + if (node.ini && (node.ini !== "")) { + var iniText = ` + (async function(__send__) { + var node = { + id:__node__.id, + name:__node__.name, + outputCount:__node__.outputCount, + log:__node__.log, + error:__node__.error, + warn:__node__.warn, + debug:__node__.debug, + trace:__node__.trace, + status:__node__.status, + send: function(msgs, cloneMsg) { + __node__.send(__send__, RED.util.generateId(), msgs, cloneMsg); } + }; + `+ node.ini +` + })(__initSend__);`; + iniOpt = createVMOpt(node, " setup"); + iniScript = new vm.Script(iniText, iniOpt); + } + node.script = vm.createScript(functionText, createVMOpt(node, "")); + if (node.fin && (node.fin !== "")) { + var finText = `(function () { + var node = { + id:__node__.id, + name:__node__.name, + outputCount:__node__.outputCount, + log:__node__.log, + error:__node__.error, + warn:__node__.warn, + debug:__node__.debug, + trace:__node__.trace, + status:__node__.status, + send: function(msgs, cloneMsg) { + __node__.error("Cannot send from close function"); + } + }; + `+node.fin +` + })();`; + finOpt = createVMOpt(node, " cleanup"); + finScript = new vm.Script(finText, finOpt); + } + var promise = Promise.resolve(); + if (iniScript) { + context.__initSend__ = function(msgs) { node.send(msgs); }; + promise = iniScript.runInContext(context, iniOpt); + } - if (line < stack.length) { - errorMessage = stack[line]; - var m = /:(\d+):(\d+)$/.exec(stack[line+1]); - if (m) { - var lineno = Number(m[1])-1; - var cha = m[2]; - errorMessage += " (line "+lineno+", col "+cha+")"; + processMessage = function (msg, send, done) { + var start = process.hrtime(); + context.msg = msg; + context.__send__ = send; + context.__done__ = done; + + node.script.runInContext(context); + context.results.then(function(results) { + sendResults(node,send,msg._msgid,results,false); + if (handleNodeDoneCall) { + done(); + } + + var duration = process.hrtime(start); + var converted = Math.floor((duration[0] * 1e9 + duration[1])/10000)/100; + node.metric("duration", msg, converted); + if (process.env.NODE_RED_FUNCTION_TIME) { + node.status({fill:"yellow",shape:"dot",text:""+converted}); + } + }).catch(err => { + if ((typeof err === "object") && err.hasOwnProperty("stack")) { + //remove unwanted part + var index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/); + err.stack = err.stack.slice(0, index).split('\n').slice(0,-1).join('\n'); + var stack = err.stack.split(/\r?\n/); + + //store the error in msg to be used in flows + msg.error = err; + + var line = 0; + var errorMessage; + if (stack.length > 0) { + while (line < stack.length && stack[line].indexOf("ReferenceError") !== 0) { + line++; + } + + if (line < stack.length) { + errorMessage = stack[line]; + var m = /:(\d+):(\d+)$/.exec(stack[line+1]); + if (m) { + var lineno = Number(m[1])-1; + var cha = m[2]; + errorMessage += " (line "+lineno+", col "+cha+")"; + } } } + if (!errorMessage) { + errorMessage = err.toString(); + } + done(errorMessage); } - if (!errorMessage) { - errorMessage = err.toString(); + else if (typeof err === "string") { + done(err); + } + else { + done(JSON.stringify(err)); + } + }); + } + + node.on("close", function() { + if (finScript) { + try { + finScript.runInContext(context, finOpt); + } + catch (err) { + node.error(err); } - done(errorMessage); } - else if (typeof err === "string") { - done(err); + while (node.outstandingTimers.length > 0) { + clearTimeout(node.outstandingTimers.pop()); } - else { - done(JSON.stringify(err)); + while (node.outstandingIntervals.length > 0) { + clearInterval(node.outstandingIntervals.pop()); + } + if (node.clearStatus) { + node.status({}); } }); - } - node.on("close", function() { - if (finScript) { - try { - finScript.runInContext(context, finOpt); - } - catch (err) { - node.error(err); - } - } - while (node.outstandingTimers.length > 0) { - clearTimeout(node.outstandingTimers.pop()); - } - while (node.outstandingIntervals.length > 0) { - clearInterval(node.outstandingIntervals.pop()); - } - if (node.clearStatus) { - node.status({}); - } - }); - - promise.then(function (v) { - var msgs = messages; - messages = []; - while (msgs.length > 0) { - msgs.forEach(function (s) { - processMessage(s.msg, s.send, s.done); - }); - msgs = messages; + promise.then(function (v) { + var msgs = messages; messages = []; - } - state = RESOLVED; - }).catch((error) => { - messages = []; - state = ERROR; - node.error(error); - }); + while (msgs.length > 0) { + msgs.forEach(function (s) { + processMessage(s.msg, s.send, s.done); + }); + msgs = messages; + messages = []; + } + state = RESOLVED; + }).catch((error) => { + messages = []; + state = ERROR; + node.error(error); + }); - } - catch(err) { - // eg SyntaxError - which v8 doesn't include line number information - // so we can't do better than this - updateErrorInfo(err); - node.error(err); - } + } + catch(err) { + // eg SyntaxError - which v8 doesn't include line number information + // so we can't do better than this + updateErrorInfo(err); + node.error(err); + } + }).catch(err => { + throw new Error(RED._("function.error.externalModuleLoadError")); + }); } RED.nodes.registerType("function",FunctionNode, { dynamicModuleList: "libs", diff --git a/packages/node_modules/@node-red/registry/lib/externalModules.js b/packages/node_modules/@node-red/registry/lib/externalModules.js index 14845b5cf..3d1ba3ba5 100644 --- a/packages/node_modules/@node-red/registry/lib/externalModules.js +++ b/packages/node_modules/@node-red/registry/lib/externalModules.js @@ -48,7 +48,7 @@ async function refreshExternalModules() { const externalModuleDir = getInstallDir(); try { const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8")); - knownExternalModules = pkgFile.dependencies; + knownExternalModules = pkgFile.dependencies || {}; } catch(err) { knownExternalModules = {}; } @@ -101,6 +101,31 @@ function requireModule(module) { const moduleDir = path.join(externalModuleDir,"node_modules",module); return require(moduleDir); } +function importModule(module) { + if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) { + const e = new Error("Module not allowed"); + e.code = "module_not_allowed"; + throw e; + } + + const parsedModule = parseModuleName(module); + + if (BUILTIN_MODULES.indexOf(parsedModule.module) !== -1) { + return import(parsedModule.module); + } + if (!knownExternalModules[parsedModule.module]) { + const e = new Error("Module not allowed"); + e.code = "module_not_allowed"; + throw e; + } + const externalModuleDir = getInstallDir(); + const moduleDir = path.join(externalModuleDir,"node_modules",module); + // Import needs the full path to the module's main .js file + const moduleFile = require.resolve(moduleDir); + return import(moduleFile); +} + + function parseModuleName(module) { var match = /((?:@[^/]+\/)?[^/@]+)(?:@([\s\S]+))?/.exec(module); @@ -254,5 +279,6 @@ module.exports = { register: register, registerSubflow: registerSubflow, checkFlowDependencies: checkFlowDependencies, - require: requireModule + require: requireModule, + import: importModule } diff --git a/packages/node_modules/@node-red/registry/lib/util.js b/packages/node_modules/@node-red/registry/lib/util.js index 0a5d579ab..1f3aef630 100644 --- a/packages/node_modules/@node-red/registry/lib/util.js +++ b/packages/node_modules/@node-red/registry/lib/util.js @@ -50,6 +50,16 @@ function requireModule(name) { return require("./externalModules").require(name); } } +function importModule(name) { + var moduleInfo = require("./index").getModuleInfo(name); + if (moduleInfo && moduleInfo.path) { + var relPath = path.relative(__dirname, moduleInfo.path); + return import(relPath); + } else { + // Require it here to avoid the circular dependency + return require("./externalModules").import(name); + } +} function createNodeApi(node) { var red = { @@ -61,6 +71,7 @@ function createNodeApi(node) { util: runtime.util, version: runtime.version, require: requireModule, + import: importModule, comms: { publish: function(topic,data,retain) { events.emit("comms",{