1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge pull request #3064 from node-red/revert-external-modules-dir

Move externalModules back into the user dir
This commit is contained in:
Nick O'Leary 2021-07-15 09:56:23 +01:00 committed by GitHub
commit eb4625a0b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 341 additions and 243 deletions

View File

@ -7,36 +7,53 @@
padding: 0px; padding: 0px;
} }
#node-input-libs-container-row .red-ui-editableList-container li { #node-input-libs-container-row .red-ui-editableList-container li {
padding:5px; padding:0px;
} }
#node-input-libs-container-row .red-ui-editableList-item-remove { #node-input-libs-container-row .red-ui-editableList-item-remove {
right: 5px; right: 5px;
} }
#node-input-libs-container-row .red-ui-editableList-header {
display: flex;
background: var(--red-ui-tertiary-background);
padding-right: 75px;
}
#node-input-libs-container-row .red-ui-editableList-header > div {
flex-grow: 1;
}
.node-libs-entry { .node-libs-entry {
display: flex; display: flex;
} }
.node-libs-entry .node-input-libs-var, .node-libs-entry .red-ui-typedInput-container {
flex-grow: 1;
}
.node-libs-entry > code,.node-libs-entry > span {
line-height: 30px;
}
.node-libs-entry > input[type=text] {
border-radius: 0;
border-left: none;
border-top: none;
border-right: none;
padding-top: 2px;
padding-bottom: 2px;
margin-top: 4px;
margin-bottom: 2px;
height: 26px;
}
.node-libs-entry > span > i { .node-libs-entry .red-ui-typedInput-container {
border-radius: 0;
border: none;
}
.node-libs-entry .red-ui-typedInput-type-select {
border-radius: 0 !important;
height: 34px;
}
.node-libs-entry > span > input[type=text] {
border-radius: 0;
border-top-color: var(--red-ui-form-background);
border-bottom-color: var(--red-ui-form-background);
border-right-color: var(--red-ui-form-background);
}
.node-libs-entry > span > input[type=text].input-error {
}
.node-libs-entry > span {
flex-grow: 1;
width: 50%;
position: relative;
}
.node-libs-entry span .node-input-libs-var, .node-libs-entry span .red-ui-typedInput-container {
width: 100%;
}
.node-libs-entry > span > span > i {
display: none; display: none;
} }
.node-libs-entry > span.input-error > i { .node-libs-entry > span > span.input-error > i {
display: inline; display: inline;
} }
@ -209,47 +226,24 @@
}) })
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({ var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
header: $('<div><div data-i18n="node-red:function.require.moduleName"></div><div data-i18n="node-red:function.require.importAs"></div></div>'),
addItem: function(container,i,opt) { addItem: function(container,i,opt) {
var parent = container.parent(); var parent = container.parent();
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container); var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
var fieldWidth = "260px"; var fmoduleSpan = $("<span>").appendTo(row0);
$('<code>const </code>').appendTo(row0);
var fvar = $("<input/>", {
class: "node-input-libs-var red-ui-font-code",
placeholder: RED._("node-red:function.require.var"),
type: "text"
}).css({
width: "120px",
"margin-left": "5px"
}).appendTo(row0).val(opt.var);
var vnameWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(vnameWarning.find("i"),function() {
var val = fvar.val();
if (invalidModuleVNames.indexOf(val) !== -1) {
return RED._("node-red:function.error.moduleNameReserved",{name:val})
} else {
return RED._("node-red:function.error.moduleNameError",{name:val})
}
})
$('<code> = require(</code>').appendTo(row0);
var fmodule = $("<input/>", { var fmodule = $("<input/>", {
class: "node-input-libs-val", class: "node-input-libs-val",
placeholder: RED._("node-red:function.require.module"), placeholder: RED._("node-red:function.require.module"),
type: "text" type: "text"
}).css({ }).css({
width: "180px", }).appendTo(fmoduleSpan).typedInput({
}).appendTo(row0).typedInput({
types: typedModules, types: typedModules,
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_" default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
}); });
if (usedModules.indexOf(opt.module) === -1) { if (usedModules.indexOf(opt.module) === -1) {
fmodule.typedInput('value', opt.module); fmodule.typedInput('value', opt.module);
} }
var moduleWarning = $('<span style="position: absolute;right:2px;top:7px; display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fmoduleSpan);
$('<code>)</code>').appendTo(row0);
var moduleWarning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
RED.popover.tooltip(moduleWarning.find("i"),function() { RED.popover.tooltip(moduleWarning.find("i"),function() {
var val = fmodule.typedInput("type"); var val = fmodule.typedInput("type");
if (val === "_custom_") { if (val === "_custom_") {
@ -264,6 +258,26 @@
} }
}) })
var fvarSpan = $("<span>").appendTo(row0);
var fvar = $("<input/>", {
class: "node-input-libs-var red-ui-font-code",
placeholder: RED._("node-red:function.require.var"),
type: "text"
}).css({
}).appendTo(fvarSpan).val(opt.var);
var vnameWarning = $('<span style="position: absolute; right:2px;top:7px;display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(fvarSpan);
RED.popover.tooltip(vnameWarning.find("i"),function() {
var val = fvar.val();
if (invalidModuleVNames.indexOf(val) !== -1) {
return RED._("node-red:function.error.moduleNameReserved",{name:val})
} else {
return RED._("node-red:function.error.moduleNameError",{name:val})
}
})
fvar.on("change keyup paste", function (e) { fvar.on("change keyup paste", function (e) {
var v = $(this).val().trim(); var v = $(this).val().trim();
if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) { if (v === "" || / /.test(v) || invalidModuleVNames.indexOf(v) !== -1) {
@ -280,7 +294,7 @@
if (val === "_custom_") { if (val === "_custom_") {
val = $(this).val(); val = $(this).val();
} }
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() }); var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/].?/g, function(v) { return v[1]?v[1].toUpperCase():"" });
fvar.val(varName); fvar.val(varName);
fvar.trigger("change"); fvar.trigger("change");

View File

@ -290,6 +290,7 @@ module.exports = function(RED) {
}; };
sandbox.promisify = util.promisify; sandbox.promisify = util.promisify;
} }
const moduleLoadPromises = [];
if (node.hasOwnProperty("libs")) { if (node.hasOwnProperty("libs")) {
let moduleErrors = false; let moduleErrors = false;
@ -303,25 +304,21 @@ module.exports = function(RED) {
return; return;
} }
sandbox[vname] = null; sandbox[vname] = null;
try { var spec = module.module;
var spec = module.module; if (spec && (spec !== "")) {
if (spec && (spec !== "")) { moduleLoadPromises.push(RED.import(module.module).then(lib => {
var lib = RED.require(module.module);
sandbox[vname] = lib; sandbox[vname] = lib;
} }).catch(err => {
} catch (e) { node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:err.toString()}))
//TODO: NLS error message throw err;
node.error(RED._("function.error.moduleLoadError",{module:module.spec, error:e.toString()})) }));
moduleErrors = true;
} }
} }
}); });
if (moduleErrors) { if (moduleErrors) {
throw new Error(RED._("function.error.externalModuleLoadError")); throw new Error(RED._("function.error.externalModuleLoadError"));
} }
} }
const RESOLVING = 0; const RESOLVING = 0;
const RESOLVED = 1; const RESOLVED = 1;
const ERROR = 2; const ERROR = 2;
@ -337,165 +334,168 @@ module.exports = function(RED) {
processMessage(msg, send, done); processMessage(msg, send, done);
} }
}); });
Promise.all(moduleLoadPromises).then(() => {
var context = vm.createContext(sandbox); var context = vm.createContext(sandbox);
try { try {
var iniScript = null; var iniScript = null;
var iniOpt = null; var iniOpt = null;
if (node.ini && (node.ini !== "")) { if (node.ini && (node.ini !== "")) {
var iniText = ` var iniText = `
(async function(__send__) { (async function(__send__) {
var node = { var node = {
id:__node__.id, id:__node__.id,
name:__node__.name, name:__node__.name,
outputCount:__node__.outputCount, outputCount:__node__.outputCount,
log:__node__.log, log:__node__.log,
error:__node__.error, error:__node__.error,
warn:__node__.warn, warn:__node__.warn,
debug:__node__.debug, debug:__node__.debug,
trace:__node__.trace, trace:__node__.trace,
status:__node__.status, status:__node__.status,
send: function(msgs, cloneMsg) { send: function(msgs, cloneMsg) {
__node__.send(__send__, RED.util.generateId(), 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++;
} }
};
`+ 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) { processMessage = function (msg, send, done) {
errorMessage = stack[line]; var start = process.hrtime();
var m = /:(\d+):(\d+)$/.exec(stack[line+1]); context.msg = msg;
if (m) { context.__send__ = send;
var lineno = Number(m[1])-1; context.__done__ = done;
var cha = m[2];
errorMessage += " (line "+lineno+", col "+cha+")"; 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) { else if (typeof err === "string") {
errorMessage = err.toString(); 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") { while (node.outstandingTimers.length > 0) {
done(err); clearTimeout(node.outstandingTimers.pop());
} }
else { while (node.outstandingIntervals.length > 0) {
done(JSON.stringify(err)); clearInterval(node.outstandingIntervals.pop());
}
if (node.clearStatus) {
node.status({});
} }
}); });
}
node.on("close", function() { promise.then(function (v) {
if (finScript) { var msgs = messages;
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;
messages = []; messages = [];
} while (msgs.length > 0) {
state = RESOLVED; msgs.forEach(function (s) {
}).catch((error) => { processMessage(s.msg, s.send, s.done);
messages = []; });
state = ERROR; msgs = messages;
node.error(error); messages = [];
}); }
state = RESOLVED;
}).catch((error) => {
messages = [];
state = ERROR;
node.error(error);
});
} }
catch(err) { catch(err) {
// eg SyntaxError - which v8 doesn't include line number information // eg SyntaxError - which v8 doesn't include line number information
// so we can't do better than this // so we can't do better than this
updateErrorInfo(err); updateErrorInfo(err);
node.error(err); node.error(err);
} }
}).catch(err => {
throw new Error(RED._("function.error.externalModuleLoadError"));
});
} }
RED.nodes.registerType("function",FunctionNode, { RED.nodes.registerType("function",FunctionNode, {
dynamicModuleList: "libs", dynamicModuleList: "libs",

View File

@ -226,7 +226,9 @@
}, },
"require": { "require": {
"var": "variable", "var": "variable",
"module": "module" "module": "module",
"moduleName": "Module name",
"importAs": "Import as"
}, },
"error": { "error": {
"externalModuleNotAllowed": "Function node not allowed to load external modules", "externalModuleNotAllowed": "Function node not allowed to load external modules",

View File

@ -12,7 +12,6 @@ const log = require("@node-red/util").log;
const hooks = require("@node-red/util").hooks; const hooks = require("@node-red/util").hooks;
const BUILTIN_MODULES = require('module').builtinModules; const BUILTIN_MODULES = require('module').builtinModules;
const EXTERNAL_MODULES_DIR = "externalModules";
// TODO: outsource running npm to a plugin // TODO: outsource running npm to a plugin
const NPM_COMMAND = (process.platform === "win32") ? "npm.cmd" : "npm"; const NPM_COMMAND = (process.platform === "win32") ? "npm.cmd" : "npm";
@ -28,15 +27,30 @@ let installAllowList = ['*'];
let installDenyList = []; let installDenyList = [];
function getInstallDir() { function getInstallDir() {
return path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", "externalModules")); return path.resolve(settings.userDir || process.env.NODE_RED_HOME || ".");
} }
let loggedLegacyWarning = false;
async function refreshExternalModules() { async function refreshExternalModules() {
const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR));
if (!loggedLegacyWarning) {
loggedLegacyWarning = true;
const oldExternalModulesDir = path.join(path.resolve(settings.userDir || process.env.NODE_RED_HOME || "."),"externalModules");
if (fs.existsSync(oldExternalModulesDir)) {
try {
log.warn(log._("server.install.old-ext-mod-dir-warning",{oldDir:oldExternalModulesDir, newDir:getInstallDir()}))
} catch(err) {console.log(err)}
}
}
const externalModuleDir = getInstallDir();
try { try {
const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8")); const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8"));
knownExternalModules = pkgFile.dependencies; knownExternalModules = pkgFile.dependencies || {};
} catch(err) { } catch(err) {
knownExternalModules = {};
} }
} }
@ -44,6 +58,7 @@ function init(_settings) {
settings = _settings; settings = _settings;
knownExternalModules = {}; knownExternalModules = {};
installEnabled = true; installEnabled = true;
if (settings.externalModules && settings.externalModules.modules) { if (settings.externalModules && settings.externalModules.modules) {
if (settings.externalModules.modules.allowList || settings.externalModules.modules.denyList) { if (settings.externalModules.modules.allowList || settings.externalModules.modules.denyList) {
installAllowList = settings.externalModules.modules.allowList; installAllowList = settings.externalModules.modules.allowList;
@ -82,10 +97,33 @@ function requireModule(module) {
e.code = "module_not_allowed"; e.code = "module_not_allowed";
throw e; throw e;
} }
const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR)); const externalModuleDir = getInstallDir();
const moduleDir = path.join(externalModuleDir,"node_modules",module); const moduleDir = path.join(externalModuleDir,"node_modules",module);
return require(moduleDir); 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) { function parseModuleName(module) {
var match = /((?:@[^/]+\/)?[^/@]+)(?:@([\s\S]+))?/.exec(module); var match = /((?:@[^/]+\/)?[^/@]+)(?:@([\s\S]+))?/.exec(module);
@ -214,6 +252,9 @@ async function installModule(moduleDetails) {
return hooks.trigger("postInstall", triggerPayload) return hooks.trigger("postInstall", triggerPayload)
}).then(() => { }).then(() => {
log.info(log._("server.install.installed", { name: installSpec })); log.info(log._("server.install.installed", { name: installSpec }));
const runtimeInstalledModules = settings.get("modules") || {};
runtimeInstalledModules[moduleDetails.module] = moduleDetails;
settings.set("modules",runtimeInstalledModules)
}).catch(result => { }).catch(result => {
var output = result.stderr || result.toString(); var output = result.stderr || result.toString();
var e; var e;
@ -235,9 +276,10 @@ async function installModule(moduleDetails) {
} }
module.exports = { module.exports = {
init: init, init,
register: register, register,
registerSubflow: registerSubflow, registerSubflow,
checkFlowDependencies: checkFlowDependencies, checkFlowDependencies,
require: requireModule require: requireModule,
import: importModule
} }

View File

@ -50,6 +50,16 @@ function requireModule(name) {
return require("./externalModules").require(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) { function createNodeApi(node) {
var red = { var red = {
@ -61,6 +71,7 @@ function createNodeApi(node) {
util: runtime.util, util: runtime.util,
version: runtime.version, version: runtime.version,
require: requireModule, require: requireModule,
import: importModule,
comms: { comms: {
publish: function(topic,data,retain) { publish: function(topic,data,retain) {
events.emit("comms",{ events.emit("comms",{

View File

@ -20,7 +20,7 @@ const fspath = require("path");
const log = require("@node-red/util").log; const log = require("@node-red/util").log;
const util = require("./util"); const util = require("./util");
const configSections = ['nodes','users','projects']; const configSections = ['nodes','users','projects','modules'];
const settingsCache = {}; const settingsCache = {};
@ -59,6 +59,7 @@ async function migrateToMultipleConfigFiles() {
* - .config.nodes.json - the node registry * - .config.nodes.json - the node registry
* - .config.users.json - user specific settings (eg editor settings) * - .config.users.json - user specific settings (eg editor settings)
* - .config.projects.json - project settings, including the active project * - .config.projects.json - project settings, including the active project
* - .config.modules.json - external modules installed by the runtime
* - .config.runtime.json - everything else - most notable _credentialSecret * - .config.runtime.json - everything else - most notable _credentialSecret
*/ */
function writeSettings(data) { function writeSettings(data) {

View File

@ -41,7 +41,8 @@
"uninstalling": "Uninstalling module: __name__", "uninstalling": "Uninstalling module: __name__",
"uninstall-failed": "Uninstall failed", "uninstall-failed": "Uninstall failed",
"uninstall-failed-long": "Uninstall of module __name__ failed:", "uninstall-failed-long": "Uninstall of module __name__ failed:",
"uninstalled": "Uninstalled module: __name__" "uninstalled": "Uninstalled module: __name__",
"old-ext-mod-dir-warning": "\n\n---------------------------------------------------------------------\nNode-RED 1.3 external modules directory detected:\n __oldDir__\nThis directory is no longer used. External Modules will be\nreinstalled in your Node-RED user directory:\n __newDir__\nDelete the old externalModules directory to stop this message.\n---------------------------------------------------------------------\n"
}, },
"deprecatedOption": "Use of __old__ is DEPRECATED. Use __new__ instead", "deprecatedOption": "Use of __old__ is DEPRECATED. Use __new__ instead",
"unable-to-listen": "Unable to listen on __listenpath__", "unable-to-listen": "Unable to listen on __listenpath__",

View File

@ -26,8 +26,7 @@ async function createUserDir() {
} }
async function setupExternalModulesPackage(dependencies) { async function setupExternalModulesPackage(dependencies) {
await fs.ensureDir(path.join(homeDir,"externalModules")) await fs.writeFile(path.join(homeDir,"package.json"),`{
await fs.writeFile(path.join(homeDir,"externalModules","package.json"),`{
"name": "Node-RED-External-Modules", "name": "Node-RED-External-Modules",
"description": "These modules are automatically installed by Node-RED to use in Function nodes.", "description": "These modules are automatically installed by Node-RED to use in Function nodes.",
"version": "1.0.0", "version": "1.0.0",
@ -68,7 +67,7 @@ describe("externalModules api", function() {
exec.run.restore(); exec.run.restore();
}) })
it("does nothing when no types are registered",async function() { it("does nothing when no types are registered",async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]} {type: "function", libs:[{module: "foo"}]}
]) ])
@ -76,7 +75,7 @@ describe("externalModules api", function() {
}); });
it("skips install for modules already installed", async function() { it("skips install for modules already installed", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
await setupExternalModulesPackage({"foo": "1.2.3", "bar":"2.3.4"}); await setupExternalModulesPackage({"foo": "1.2.3", "bar":"2.3.4"});
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
@ -86,7 +85,7 @@ describe("externalModules api", function() {
}) })
it("skips install for built-in modules", async function() { it("skips install for built-in modules", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "fs"}]} {type: "function", libs:[{module: "fs"}]}
@ -95,19 +94,17 @@ describe("externalModules api", function() {
}) })
it("installs missing modules", async function() { it("installs missing modules", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
fs.existsSync(path.join(homeDir,"externalModules")).should.be.false();
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
{type: "function", libs:[{module: "foo"}]} {type: "function", libs:[{module: "foo"}]}
]) ])
exec.run.called.should.be.true(); exec.run.called.should.be.true();
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
}) })
it("calls pre/postInstall hooks", async function() { it("calls pre/postInstall hooks", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
let receivedPreEvent,receivedPostEvent; let receivedPreEvent,receivedPostEvent;
hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; }) hooks.add("preInstall", function(event) { event.args = ["a"]; receivedPreEvent = event; })
@ -122,11 +119,10 @@ describe("externalModules api", function() {
receivedPreEvent.should.have.property("version") receivedPreEvent.should.have.property("version")
receivedPreEvent.should.have.property("dir") receivedPreEvent.should.have.property("dir")
receivedPreEvent.should.eql(receivedPostEvent) receivedPreEvent.should.eql(receivedPostEvent)
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
}) })
it("skips npm install if preInstall returns false", async function() { it("skips npm install if preInstall returns false", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
let receivedPreEvent,receivedPostEvent; let receivedPreEvent,receivedPostEvent;
hooks.add("preInstall", function(event) { receivedPreEvent = event; return false }) hooks.add("preInstall", function(event) { receivedPreEvent = event; return false })
@ -140,12 +136,11 @@ describe("externalModules api", function() {
receivedPreEvent.should.have.property("version") receivedPreEvent.should.have.property("version")
receivedPreEvent.should.have.property("dir") receivedPreEvent.should.have.property("dir")
receivedPreEvent.should.eql(receivedPostEvent) receivedPreEvent.should.eql(receivedPostEvent)
fs.existsSync(path.join(homeDir,"externalModules")).should.be.true();
}) })
it("installs missing modules from inside subflow module", async function() { it("installs missing modules from inside subflow module", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
externalModules.registerSubflow("sf", {"flow":[{type: "function", libs:[{module: "foo"}]}]}); externalModules.registerSubflow("sf", {"flow":[{type: "function", libs:[{module: "foo"}]}]});
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
@ -155,7 +150,7 @@ describe("externalModules api", function() {
}) })
it("reports install fail - 404", async function() { it("reports install fail - 404", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
try { try {
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
@ -174,7 +169,7 @@ describe("externalModules api", function() {
} }
}) })
it("reports install fail - target", async function() { it("reports install fail - target", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
try { try {
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
@ -193,7 +188,7 @@ describe("externalModules api", function() {
}) })
it("reports install fail - unexpected", async function() { it("reports install fail - unexpected", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
try { try {
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
@ -211,7 +206,7 @@ describe("externalModules api", function() {
} }
}) })
it("reports install fail - multiple", async function() { it("reports install fail - multiple", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
externalModules.register("function", "libs"); externalModules.register("function", "libs");
try { try {
await externalModules.checkFlowDependencies([ await externalModules.checkFlowDependencies([
@ -238,7 +233,7 @@ describe("externalModules api", function() {
} }
}) })
it("reports install fail - install disabled", async function() { it("reports install fail - install disabled", async function() {
externalModules.init({userDir: homeDir, externalModules: { externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}, externalModules: {
modules: { modules: {
allowInstall: false allowInstall: false
} }
@ -262,7 +257,7 @@ describe("externalModules api", function() {
}) })
it("reports install fail - module disallowed", async function() { it("reports install fail - module disallowed", async function() {
externalModules.init({userDir: homeDir, externalModules: { externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}, externalModules: {
modules: { modules: {
denyList: ['foo'] denyList: ['foo']
} }
@ -287,7 +282,7 @@ describe("externalModules api", function() {
}) })
it("reports install fail - built-in module disallowed", async function() { it("reports install fail - built-in module disallowed", async function() {
externalModules.init({userDir: homeDir, externalModules: { externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}, externalModules: {
modules: { modules: {
denyList: ['fs'] denyList: ['fs']
} }
@ -313,12 +308,12 @@ describe("externalModules api", function() {
}) })
describe("require", async function() { describe("require", async function() {
it("requires built-in modules", async function() { it("requires built-in modules", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
const result = externalModules.require("fs") const result = externalModules.require("fs")
result.should.eql(require("fs")); result.should.eql(require("fs"));
}) })
it("rejects unknown modules", async function() { it("rejects unknown modules", async function() {
externalModules.init({userDir: homeDir}); externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
try { try {
externalModules.require("foo") externalModules.require("foo")
throw new Error("require did not reject after fail") throw new Error("require did not reject after fail")
@ -328,7 +323,7 @@ describe("externalModules api", function() {
}) })
it("rejects disallowed modules", async function() { it("rejects disallowed modules", async function() {
externalModules.init({userDir: homeDir, externalModules: { externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}, externalModules: {
modules: { modules: {
denyList: ['fs'] denyList: ['fs']
} }
@ -341,4 +336,36 @@ describe("externalModules api", function() {
} }
}) })
}) })
describe("import", async function() {
it("import built-in modules", async function() {
externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
const result = await externalModules.import("fs")
// `result` won't have the `should` property
should.exist(result);
should.exist(result.existsSync);
})
it("rejects unknown modules", async function() {
externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}});
try {
await externalModules.import("foo")
throw new Error("import did not reject after fail")
} catch(err) {
err.should.have.property("code","module_not_allowed");
}
})
it("rejects disallowed modules", async function() {
externalModules.init({userDir: homeDir, get:()=>{}, set:()=>{}, externalModules: {
modules: {
denyList: ['fs']
}
}});
try {
await externalModules.import("fs")
throw new Error("import did not reject after fail")
} catch(err) {
err.should.have.property("code","module_not_allowed");
}
})
})
}); });