mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
Rework Function node module integration
This commit is contained in:
parent
4a1d66f210
commit
9c09ee3b71
@ -67,11 +67,6 @@ module.exports = {
|
||||
adminApp.get(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.read"),nodes.getSet,apiUtil.errorHandler);
|
||||
adminApp.put(/^\/nodes\/((@[^\/]+\/)?[^\/]+)\/([^\/]+)$/,needsPermission("nodes.write"),nodes.putSet,apiUtil.errorHandler);
|
||||
|
||||
// NPM Modules
|
||||
adminApp.get("/modules", needsPermission("nodes.read"), nodes.listNPMModules, apiUtil.errorHandler);
|
||||
adminApp.delete("/modules/:spec", needsPermission("nodes.write"), nodes.uninstallNPMModule, apiUtil.errorHandler);
|
||||
adminApp.post("/modules", needsPermission("nodes.write"), nodes.updateNPMModule, apiUtil.errorHandler);
|
||||
|
||||
// Context
|
||||
adminApp.get("/context/:scope(global)",needsPermission("context.read"),context.get,apiUtil.errorHandler);
|
||||
adminApp.get("/context/:scope(global)/*",needsPermission("context.read"),context.get,apiUtil.errorHandler);
|
||||
|
@ -191,45 +191,5 @@ module.exports = {
|
||||
runtimeAPI.nodes.getIconList(opts).then(function(list) {
|
||||
res.json(list);
|
||||
});
|
||||
},
|
||||
|
||||
listNPMModules: function(req, res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.nodes.listNPMModules(opts).then(function(list) {
|
||||
res.json(list);
|
||||
}).catch(err => {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});;
|
||||
},
|
||||
|
||||
uninstallNPMModule: function(req, res) {
|
||||
var opts = {
|
||||
user: req.user,
|
||||
spec: req.params.spec,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.nodes.uninstallNPMModule(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(err => {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});
|
||||
},
|
||||
|
||||
updateNPMModule: function(req, res) {
|
||||
var body = req.body;
|
||||
var opts = {
|
||||
user: req.user,
|
||||
spec: body.spec,
|
||||
update: body.update,
|
||||
req: apiUtils.getRequestLogObject(req)
|
||||
}
|
||||
runtimeAPI.nodes.updateNPMModule(opts).then(function(result) {
|
||||
res.json(result);
|
||||
}).catch(err => {
|
||||
apiUtils.rejectHandler(req,res,err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -142,6 +142,7 @@
|
||||
"nodeActionDisabled": "node actions disabled",
|
||||
"nodeActionDisabledSubflow": "node actions disabled within subflow",
|
||||
"missing-types": "<p>Flows stopped due to missing node types.</p>",
|
||||
"missing-modules": "<p>Flows stopped due to missing modules.</p>",
|
||||
"safe-mode":"<p>Flows stopped in safe mode.</p><p>You can modify your flows and deploy the changes to restart.</p>",
|
||||
"restartRequired": "Node-RED must be restarted to enable upgraded modules",
|
||||
"credentials_load_failed": "<p>Flows stopped as the credentials could not be decrypted.</p><p>The flow credential file is encrypted, but the project's encryption key is missing or invalid.</p>",
|
||||
|
@ -248,6 +248,7 @@ var RED = (function() {
|
||||
id: notificationId
|
||||
}
|
||||
if (notificationId === "runtime-state") {
|
||||
RED.events.emit("runtime-state",msg);
|
||||
if (msg.error === "safe-mode") {
|
||||
options.buttons = [
|
||||
{
|
||||
@ -280,6 +281,16 @@ var RED = (function() {
|
||||
}
|
||||
]
|
||||
}
|
||||
} else if (msg.error === "missing-modules") {
|
||||
text+="<ul><li>"+msg.modules.map(function(m) { return RED.utils.sanitize(m.module)+(m.error?(" - <small>"+RED.utils.sanitize(""+m.error)+"</small>"):"")}).join("</li><li>")+"</li></ul>";
|
||||
options.buttons = [
|
||||
{
|
||||
text: RED._("common.label.close"),
|
||||
click: function() {
|
||||
persistentNotifications[notificationId].hideNotification();
|
||||
}
|
||||
}
|
||||
]
|
||||
} else if (msg.error === "credentials_load_failed") {
|
||||
if (RED.settings.theme("projects.enabled",false)) {
|
||||
// projects enabled
|
||||
@ -370,6 +381,9 @@ var RED = (function() {
|
||||
} else if (persistentNotifications.hasOwnProperty(notificationId)) {
|
||||
persistentNotifications[notificationId].close();
|
||||
delete persistentNotifications[notificationId];
|
||||
if (notificationId === 'runtime-state') {
|
||||
RED.events.emit("runtime-state",msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
RED.comms.subscribe("status/#",function(topic,msg) {
|
||||
|
@ -509,8 +509,8 @@ RED.tabs = (function() {
|
||||
li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-")));
|
||||
li.data("tabId",tab.id);
|
||||
|
||||
if (options.maximumTabWidth) {
|
||||
li.css("maxWidth",options.maximumTabWidth+"px");
|
||||
if (options.maximumTabWidth || tab.maximumTabWidth) {
|
||||
li.css("maxWidth",(options.maximumTabWidth || tab.maximumTabWidth) +"px");
|
||||
}
|
||||
var link = $("<a/>",{href:"#"+tab.id, class:"red-ui-tab-label"}).appendTo(li);
|
||||
if (tab.icon) {
|
||||
|
@ -331,7 +331,7 @@ RED.palette.editor = (function() {
|
||||
if (v.modules) {
|
||||
var a = false;
|
||||
v.modules = v.modules.filter(function(m) {
|
||||
if (checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) {
|
||||
if (RED.utils.checkModuleAllowed(m.id,m.version,installAllowList,installDenyList)) {
|
||||
loadedIndex[m.id] = m;
|
||||
m.index = [m.id];
|
||||
if (m.keywords) {
|
||||
@ -445,68 +445,6 @@ RED.palette.editor = (function() {
|
||||
var installAllowList = ['*'];
|
||||
var installDenyList = [];
|
||||
|
||||
function parseModuleList(list) {
|
||||
list = list || ["*"];
|
||||
return list.map(function(rule) {
|
||||
var m = /^(.+?)(?:@(.*))?$/.exec(rule);
|
||||
var wildcardPos = m[1].indexOf("*");
|
||||
wildcardPos = wildcardPos===-1?Infinity:wildcardPos;
|
||||
|
||||
return {
|
||||
module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"),
|
||||
version: m[2],
|
||||
wildcardPos: wildcardPos
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function checkAgainstList(module,version,list) {
|
||||
for (var i=0;i<list.length;i++) {
|
||||
var rule = list[i];
|
||||
if (rule.module.test(module)) {
|
||||
// Without a full semver library in the editor,
|
||||
// we skip the version check.
|
||||
// Not ideal - but will get caught in the runtime
|
||||
// if the user tries to install.
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkModuleAllowed(module,version,allowList,denyList) {
|
||||
if (!allowList && !denyList) {
|
||||
// Default to allow
|
||||
return true;
|
||||
}
|
||||
if (allowList.length === 0 && denyList.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var allowedRule = checkAgainstList(module,version,allowList);
|
||||
var deniedRule = checkAgainstList(module,version,denyList);
|
||||
// console.log("A",allowedRule)
|
||||
// console.log("D",deniedRule)
|
||||
|
||||
if (allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (!allowedRule && deniedRule) {
|
||||
return false;
|
||||
}
|
||||
if (!allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
|
||||
return allowedRule.wildcardPos > deniedRule.wildcardPos
|
||||
} else {
|
||||
// First wildcard in same position.
|
||||
// Go with the longer matching rule. This isn't going to be 100%
|
||||
// right, but we are deep into edge cases at this point.
|
||||
return allowedRule.module.toString().length > deniedRule.module.toString().length
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (RED.settings.get('externalModules.palette.allowInstall', true) === false) {
|
||||
return;
|
||||
@ -517,8 +455,8 @@ RED.palette.editor = (function() {
|
||||
installAllowList = settingsAllowList;
|
||||
installDenyList = settingsDenyList
|
||||
}
|
||||
installAllowList = parseModuleList(installAllowList);
|
||||
installDenyList = parseModuleList(installDenyList);
|
||||
installAllowList = RED.utils.parseModuleList(installAllowList);
|
||||
installDenyList = RED.utils.parseModuleList(installDenyList);
|
||||
|
||||
createSettingsPane();
|
||||
|
||||
|
@ -1122,6 +1122,67 @@ RED.utils = (function() {
|
||||
return '#'+'000000'.slice(0, 6-s.length)+s;
|
||||
}
|
||||
|
||||
function parseModuleList(list) {
|
||||
list = list || ["*"];
|
||||
return list.map(function(rule) {
|
||||
var m = /^(.+?)(?:@(.*))?$/.exec(rule);
|
||||
var wildcardPos = m[1].indexOf("*");
|
||||
wildcardPos = wildcardPos===-1?Infinity:wildcardPos;
|
||||
|
||||
return {
|
||||
module: new RegExp("^"+m[1].replace(/\*/g,".*")+"$"),
|
||||
version: m[2],
|
||||
wildcardPos: wildcardPos
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function checkAgainstList(module,version,list) {
|
||||
for (var i=0;i<list.length;i++) {
|
||||
var rule = list[i];
|
||||
if (rule.module.test(module)) {
|
||||
// Without a full semver library in the editor,
|
||||
// we skip the version check.
|
||||
// Not ideal - but will get caught in the runtime
|
||||
// if the user tries to install.
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkModuleAllowed(module,version,allowList,denyList) {
|
||||
if (!allowList && !denyList) {
|
||||
// Default to allow
|
||||
return true;
|
||||
}
|
||||
if (allowList.length === 0 && denyList.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var allowedRule = checkAgainstList(module,version,allowList);
|
||||
var deniedRule = checkAgainstList(module,version,denyList);
|
||||
// console.log("A",allowedRule)
|
||||
// console.log("D",deniedRule)
|
||||
|
||||
if (allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (!allowedRule && deniedRule) {
|
||||
return false;
|
||||
}
|
||||
if (!allowedRule && !deniedRule) {
|
||||
return true;
|
||||
}
|
||||
if (allowedRule.wildcardPos !== deniedRule.wildcardPos) {
|
||||
return allowedRule.wildcardPos > deniedRule.wildcardPos
|
||||
} else {
|
||||
// First wildcard in same position.
|
||||
// Go with the longer matching rule. This isn't going to be 100%
|
||||
// right, but we are deep into edge cases at this point.
|
||||
return allowedRule.module.toString().length > deniedRule.module.toString().length
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
createObjectElement: buildMessageElement,
|
||||
getMessageProperty: getMessageProperty,
|
||||
@ -1141,6 +1202,8 @@ RED.utils = (function() {
|
||||
sanitize: sanitize,
|
||||
renderMarkdown: renderMarkdown,
|
||||
createNodeIcon: createNodeIcon,
|
||||
getDarkerColor: getDarkerColor
|
||||
getDarkerColor: getDarkerColor,
|
||||
parseModuleList: parseModuleList,
|
||||
checkModuleAllowed: checkModuleAllowed
|
||||
}
|
||||
})();
|
||||
|
@ -143,6 +143,13 @@
|
||||
background-size: contain
|
||||
}
|
||||
|
||||
.red-ui-font-code {
|
||||
font-family: $monospace-font;
|
||||
font-size: $primary-font-size;
|
||||
color: $info-text-code-color;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: $monospace-font;
|
||||
font-size: $primary-font-size;
|
||||
|
@ -1,20 +1,67 @@
|
||||
<script type="text/html" data-template-name="function">
|
||||
<style>
|
||||
#node-input-libs-container-row .red-ui-editableList-container {
|
||||
padding: 0px;
|
||||
}
|
||||
#node-input-libs-container-row .red-ui-editableList-container li {
|
||||
padding:5px;
|
||||
}
|
||||
#node-input-libs-container-row .red-ui-editableList-item-remove {
|
||||
right: 5px;
|
||||
}
|
||||
.node-libs-entry {
|
||||
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;
|
||||
}
|
||||
|
||||
.node-libs-entry > span > i {
|
||||
display: none;
|
||||
}
|
||||
.node-libs-entry > span.input-error > i {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
</style>
|
||||
<input type="hidden" id="node-input-func">
|
||||
<input type="hidden" id="node-input-noerr">
|
||||
<input type="hidden" id="node-input-finalize">
|
||||
<input type="hidden" id="node-input-initialize">
|
||||
|
||||
<div class="form-row" style="margin-top: -20px">
|
||||
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
|
||||
</div>
|
||||
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
|
||||
|
||||
<div id="func-tab-config" style="display:none">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
|
||||
<div style="display: inline-block; width: calc(100% - 105px)"><input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<ul style="min-width: 600px; margin-bottom: 20px;" id="func-tabs"></ul>
|
||||
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
||||
<input id="node-input-outputs" style="width: 60px;" value="1">
|
||||
</div>
|
||||
|
||||
<div id="func-tabs-content" style="min-height: calc(100% - 95px);">
|
||||
<div class="form-row node-input-libs-row hide" style="margin-bottom: 0px;">
|
||||
<label><i class="fa fa-cubes"></i> <span>Modules</span></label>
|
||||
</div>
|
||||
<div class="form-row node-input-libs-row hide" id="node-input-libs-container-row">
|
||||
<ol id="node-input-libs-container"></ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="func-tab-init" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<input type="hidden" id="node-input-initialize" autofocus="autofocus">
|
||||
</div>
|
||||
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-init-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-init-editor" ></div>
|
||||
@ -22,46 +69,77 @@
|
||||
</div>
|
||||
|
||||
<div id="func-tab-body" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<input type="hidden" id="node-input-func" autofocus="autofocus">
|
||||
<input type="hidden" id="node-input-noerr">
|
||||
</div>
|
||||
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-function-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="height: 220px; min-height:120px; margin-top: 30px;" class="node-text-editor" id="node-input-func-editor" ></div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-bottom: 0px">
|
||||
<label for="node-input-outputs"><i class="fa fa-random"></i> <span data-i18n="function.label.outputs"></span></label>
|
||||
<input id="node-input-outputs" style="width: 60px;" value="1">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="func-tab-finalize" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<input type="hidden" id="node-input-finalize" autofocus="autofocus">
|
||||
</div>
|
||||
<div class="form-row node-text-editor-row" style="position:relative">
|
||||
<div style="position: absolute; right:0; bottom: calc(100% + 3px);"><button id="node-finalize-expand-js" class="red-ui-button red-ui-button-small"><i class="fa fa-expand"></i></button></div>
|
||||
<div style="height: 250px; min-height:150px; margin-top: 30px;" class="node-text-editor" id="node-input-finalize-editor" ></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="func-tab-config" style="display:none">
|
||||
<div class="form-row" style="margin-bottom: 0px;">
|
||||
<label><i class="fa fa-wrench"></i> <span>Use Library</span></label>
|
||||
</div>
|
||||
<div class="form-row" style="height: 250px; min-height: 150px">
|
||||
<ol id="node-input-libs-container"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
(function() {
|
||||
|
||||
var knownFunctionNodes = {};
|
||||
RED.events.on("nodes:add", function(n) {
|
||||
if (n.type === "function") {
|
||||
knownFunctionNodes[n.id] = n;
|
||||
}
|
||||
})
|
||||
RED.events.on("nodes:remove", function(n) {
|
||||
if (n.type === "function") {
|
||||
delete knownFunctionNodes[n.id];
|
||||
}
|
||||
})
|
||||
|
||||
var missingModules = [];
|
||||
var missingModuleReasons = {};
|
||||
RED.events.on("runtime-state", function(event) {
|
||||
if (event.error === "missing-modules") {
|
||||
missingModules = event.modules.map(function(m) { missingModuleReasons[m.module] = m.error; return m.module });
|
||||
for (var id in knownFunctionNodes) {
|
||||
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
|
||||
RED.editor.validateNode(knownFunctionNodes[id])
|
||||
}
|
||||
}
|
||||
} else if (!event.text) {
|
||||
missingModuleReasons = {};
|
||||
missingModules = [];
|
||||
for (var id in knownFunctionNodes) {
|
||||
if (knownFunctionNodes.hasOwnProperty(id) && knownFunctionNodes[id].libs && knownFunctionNodes[id].libs.length > 0) {
|
||||
RED.editor.validateNode(knownFunctionNodes[id])
|
||||
}
|
||||
}
|
||||
}
|
||||
RED.view.redraw();
|
||||
});
|
||||
|
||||
var installAllowList = ['*'];
|
||||
var installDenyList = [];
|
||||
|
||||
var modulesEnabled = true;
|
||||
if (RED.settings.get('externalModules.modules.allowInstall', true) === false) {
|
||||
modulesEnabled = false;
|
||||
}
|
||||
var settingsAllowList = RED.settings.get("externalModules.modules.allowList")
|
||||
var settingsDenyList = RED.settings.get("externalModules.modules.denyList")
|
||||
if (settingsAllowList || settingsDenyList) {
|
||||
installAllowList = settingsAllowList;
|
||||
installDenyList = settingsDenyList
|
||||
}
|
||||
installAllowList = RED.utils.parseModuleList(installAllowList);
|
||||
installDenyList = RED.utils.parseModuleList(installDenyList);
|
||||
|
||||
|
||||
// object that maps from library name to its descriptor
|
||||
var allLibs = [];
|
||||
|
||||
@ -73,523 +151,123 @@
|
||||
return [module, undefined];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add library descriptor
|
||||
* @param {string} name - library name
|
||||
*/
|
||||
function addLib(info) {
|
||||
allLibs.push(info);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate library spec including conflicts with other specs
|
||||
* @param {string} name - library name
|
||||
*/
|
||||
function checkLib(spec) {
|
||||
var [name, ver] = moduleName(spec);
|
||||
var libs = $("#node-input-libs-container").editableList("items");
|
||||
var count = 0;
|
||||
libs.each(function(i) {
|
||||
if (count > 1) {
|
||||
return;
|
||||
}
|
||||
var item = $(this);
|
||||
var n = item.find(".node-input-libs-val").val();
|
||||
if (n && (n !== "")) {
|
||||
var [name1, ver1] = moduleName(n);
|
||||
if (name1 === name) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
});
|
||||
return (count <= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get library list
|
||||
*/
|
||||
function getModules() {
|
||||
var root = RED.settings.apiRootUrl || "";
|
||||
var promise = $.ajax({
|
||||
url: root + "/modules",
|
||||
type: "GET",
|
||||
cache: false
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and update in progress state of modules
|
||||
*/
|
||||
function checkInProgress(allLibs) {
|
||||
var waitTime = 1000;
|
||||
|
||||
function update() {
|
||||
map = {};
|
||||
allLibs.forEach(function (lib) {
|
||||
if (lib.inProgress) {
|
||||
map[lib.name] = lib;
|
||||
}
|
||||
});
|
||||
getModules().then(function (data,textStatus,xhr) {
|
||||
var count = 0;
|
||||
data.forEach(lib => {
|
||||
var name = lib.name;
|
||||
if (lib.inProgress) {
|
||||
count++;
|
||||
}
|
||||
else if (name in map) {
|
||||
var lib2 = map[name];
|
||||
if (lib2) {
|
||||
delete map[name];
|
||||
var cb = lib2.onReady;
|
||||
if (cb) {
|
||||
lib2.version = lib.version;
|
||||
lib2.status["installed"] = true;
|
||||
lib2.inProgress = false;
|
||||
cb();
|
||||
function getAllUsedModules() {
|
||||
var moduleSet = new Set();
|
||||
for (var id in knownFunctionNodes) {
|
||||
if (knownFunctionNodes.hasOwnProperty(id)) {
|
||||
if (knownFunctionNodes[id].libs) {
|
||||
for (var i=0, l=knownFunctionNodes[id].libs.length; i<l; i++) {
|
||||
if (RED.utils.checkModuleAllowed(knownFunctionNodes[id].libs[i].module,null,installAllowList,installDenyList)) {
|
||||
moduleSet.add(knownFunctionNodes[id].libs[i].module);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (count > 0) {
|
||||
setTimeout(update, waitTime);
|
||||
}
|
||||
});
|
||||
}
|
||||
setTimeout(update, waitTime);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update library list
|
||||
*/
|
||||
function updateLib(node, done) {
|
||||
var names = {};
|
||||
var specs = {};
|
||||
var libs = node.libs || [];
|
||||
allLibs = [];
|
||||
libs.forEach(lib => {
|
||||
var vname = lib.vname;
|
||||
var spec = lib.spec;
|
||||
var [name, ver] = moduleName(spec);
|
||||
var item = {
|
||||
vname: vname,
|
||||
name: name,
|
||||
spec: spec,
|
||||
status: { "edit":true }
|
||||
};
|
||||
addLib(item);
|
||||
specs[spec] = true;
|
||||
names[name] = true;
|
||||
});
|
||||
RED.nodes.eachNode(function(n) {
|
||||
if ((n.type === "function") && (n !== node)) {
|
||||
var libs = n.libs || [];
|
||||
libs.forEach(lib => {
|
||||
var spec = lib.spec;
|
||||
var [name, ver] = moduleName(spec);
|
||||
if (name in names) {
|
||||
var items = allLibs.filter(item => (item.name === name));
|
||||
items.forEach(item => {
|
||||
item.status["used"] = true;
|
||||
});
|
||||
if (spec in specs) {
|
||||
// olready found
|
||||
return;
|
||||
}
|
||||
// spec conflict
|
||||
}
|
||||
var item = {
|
||||
name: name,
|
||||
spec: spec,
|
||||
status: { "used": true }
|
||||
};
|
||||
addLib(item);
|
||||
names[name] = true;
|
||||
specs[spec] = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
var inProgressCount = 0;
|
||||
getModules().then(function(data,textStatus,xhr) {
|
||||
data.forEach(lib => {
|
||||
var name = lib.name;
|
||||
var version = lib.version;
|
||||
var preinstalled = (lib.status == "preinstalled");
|
||||
var spec = lib.spec;
|
||||
var items = allLibs.filter(item => (item.name === name));
|
||||
if (items.length > 0) {
|
||||
items.forEach(item => {
|
||||
item.version = version;
|
||||
item.status["installed"] = true;
|
||||
item.status["preinstalled"] = preinstalled;
|
||||
item.inProgress = lib.inProgress;
|
||||
});
|
||||
}
|
||||
else {
|
||||
var item = {
|
||||
name: name,
|
||||
spec: spec,
|
||||
version: version,
|
||||
status: {
|
||||
"installed": true,
|
||||
"preinstalled": preinstalled
|
||||
},
|
||||
inProgress: lib.inProgress
|
||||
};
|
||||
addLib(item);
|
||||
names[name] = true;
|
||||
specs[spec] = true;
|
||||
}
|
||||
if (lib.inProgress) {
|
||||
inProgressCount++;
|
||||
}
|
||||
});
|
||||
$("#node-input-libs-container").editableList("empty");
|
||||
allLibs.forEach(function(lib) {
|
||||
$("#node-input-libs-container").editableList("addItem", lib);
|
||||
});
|
||||
if (inProgressCount > 0) {
|
||||
checkInProgress(allLibs);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall NPM module
|
||||
*/
|
||||
function uninstallModule(node, spec, name, version,
|
||||
moduleInfo, installButton,
|
||||
installInfoRow, installedInfo,
|
||||
removeButton,
|
||||
progressRow,
|
||||
done) {
|
||||
var root = RED.settings.apiRootUrl || "";
|
||||
progressRow.show();
|
||||
$.ajax({
|
||||
url: root + "/modules/" +spec,
|
||||
type: "DELETE",
|
||||
}).then(function(data,textStatus,xhr) {
|
||||
moduleInfo.attr("disabled", false);
|
||||
installButton.attr("disabled", false);
|
||||
installInfoRow.hide();
|
||||
installedInfo.val("");
|
||||
removeButton.show();
|
||||
progressRow.hide();
|
||||
|
||||
RED.notify("Successfully uninstalled:" +name);
|
||||
if(done) {
|
||||
done();
|
||||
}
|
||||
}).fail(function(xhr,textStatus,err) {
|
||||
progressRow.hide();
|
||||
RED.notify("Failed to uninstall: " +name);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updaate or Install NPM module
|
||||
*/
|
||||
function updateModule(node, spec, update,
|
||||
moduleInfo, installButton,
|
||||
installInfoRow, installedInfo, removeButton,
|
||||
progressRow,
|
||||
done) {
|
||||
var [name, version] = moduleName(spec);
|
||||
var root = RED.settings.apiRootUrl || "";
|
||||
var errorMessage = "failed to "+(update ? "update": "install") +": " +name;
|
||||
progressRow.show();
|
||||
$.ajax({
|
||||
url: root + "/modules",
|
||||
type: "POST",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
cache: false,
|
||||
data: JSON.stringify({
|
||||
spec: spec,
|
||||
update: update
|
||||
})
|
||||
}).then(function(data) {
|
||||
getModules().then(function(data,textStatus,xhr) {
|
||||
var item = data.find(lib => (lib.name === name));
|
||||
progressRow.hide();
|
||||
if (item) {
|
||||
moduleInfo.attr("disabled", true);
|
||||
installButton.attr("disabled", true);
|
||||
installInfoRow.show();
|
||||
installedInfo.val(item.name+"@"+item.version);
|
||||
removeButton.hide();
|
||||
|
||||
var msg = "Successfully "
|
||||
+(update ? "updated": "installed")
|
||||
+": " +name;
|
||||
|
||||
RED.notify(msg);
|
||||
}
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
}).fail(function(xhr,textStatus,err) {
|
||||
progressRow.hide();
|
||||
RED.notify(errorMessage);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}).fail(function(xhr,textStatus,err) {
|
||||
progressRow.hide();
|
||||
RED.notify(errorMessage);
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function withNotification(msg, text, cb) {
|
||||
var notification = RED.notify(msg, {
|
||||
modal: true,
|
||||
fixed: true,
|
||||
buttons: [
|
||||
{
|
||||
text: RED._("common.label.cancel"),
|
||||
click: function() {
|
||||
notification.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: text,
|
||||
class: "primary red-ui-palette-module-install-confirm-button-update",
|
||||
click: function() {
|
||||
notification.close();
|
||||
cb();
|
||||
}
|
||||
},
|
||||
]
|
||||
});
|
||||
var modules = Array.from(moduleSet);
|
||||
modules.sort();
|
||||
return modules;
|
||||
}
|
||||
|
||||
function prepareLibraryConfig(node) {
|
||||
$("#node-input-libs-container").css('min-height','250px').css('min-width','450px').editableList({
|
||||
$(".node-input-libs-row").show();
|
||||
var usedModules = getAllUsedModules();
|
||||
var typedModules = usedModules.map(function(l) {
|
||||
return {icon:"fa fa-cube", value:l,label:l,hasValue:false}
|
||||
})
|
||||
typedModules.push({
|
||||
value:"_custom_", label:RED._("editor:subflow.licenseOther"), icon:"red/images/typedInput/az.svg"
|
||||
})
|
||||
|
||||
var libList = $("#node-input-libs-container").css('min-height','100px').css('min-width','450px').editableList({
|
||||
addItem: function(container,i,opt) {
|
||||
var parent = container.parent();
|
||||
var removeButton = parent.find(".red-ui-editableList-item-remove");
|
||||
var disabled = (opt && !!opt.unused);
|
||||
var row0 = $("<div/>").appendTo(container);
|
||||
var fvar = $("<input/>", {
|
||||
class: "node-input-libs-var",
|
||||
placeholder: RED._("node-red:function.require.var"),
|
||||
type: "text",
|
||||
disabled: disabled
|
||||
}).css({
|
||||
width: "90px",
|
||||
"margin-left": "5px"
|
||||
}).appendTo(row0);
|
||||
var row0 = $("<div/>").addClass("node-libs-entry").appendTo(container);
|
||||
var fieldWidth = "260px";
|
||||
$('<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);
|
||||
$('<code> = require(</code>').appendTo(row0);
|
||||
var fmodule = $("<input/>", {
|
||||
class: "node-input-libs-val",
|
||||
placeholder: RED._("node-red:function.require.module"),
|
||||
type: "text",
|
||||
disabled: disabled
|
||||
type: "text"
|
||||
}).css({
|
||||
width: fieldWidth,
|
||||
"margin-left": "5px"
|
||||
}).appendTo(row0);
|
||||
width: "180px",
|
||||
}).appendTo(row0).typedInput({
|
||||
types: typedModules,
|
||||
default: usedModules.indexOf(opt.module) > -1 ? opt.module : "_custom_"
|
||||
});
|
||||
if (usedModules.indexOf(opt.module) === -1) {
|
||||
fmodule.typedInput('value', opt.module);
|
||||
}
|
||||
|
||||
var buttonWidth = "70px";
|
||||
var install = $("<button/>", {
|
||||
class: "red-ui-button red-ui-button-small"
|
||||
}).css({
|
||||
"margin-left": "10px",
|
||||
width: buttonWidth
|
||||
}).appendTo(row0);;
|
||||
$("<i/>", {
|
||||
class: "fa fa-cube"
|
||||
}).appendTo(install);
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text("Install").appendTo(install);
|
||||
$('<code>)</code>').appendTo(row0);
|
||||
|
||||
var warn = $("<span/>").css({
|
||||
"margin-left": "10px"
|
||||
}).appendTo(row0);
|
||||
$("<i/>", {
|
||||
class: "fa fa-warning"
|
||||
}).appendTo(warn);
|
||||
RED.popover.tooltip(warn, "module spec conflict");
|
||||
warn.hide();
|
||||
|
||||
var row1 = $("<div/>").css({
|
||||
"margin-top": "5px"
|
||||
}).appendTo(container);
|
||||
|
||||
var linstalled = $("<label/>").css({
|
||||
width: "90px",
|
||||
"text-align": "right",
|
||||
"margin-right": "10px"
|
||||
}).appendTo(row1);
|
||||
$("<i/>", {
|
||||
class: "fa fa-cube"
|
||||
}).appendTo(linstalled);
|
||||
var status = "Installed";
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text(status).appendTo(linstalled);
|
||||
|
||||
var finstalled = $("<input/>", {
|
||||
type: "text",
|
||||
disabled: true
|
||||
}).css({
|
||||
width: fieldWidth
|
||||
}).appendTo(row1);
|
||||
|
||||
var uninstall = $("<button/>", {
|
||||
class: "red-ui-button red-ui-button-small"
|
||||
}).css({
|
||||
"margin-left": "10px",
|
||||
width: buttonWidth
|
||||
}).appendTo(row1);
|
||||
$("<i/>", {
|
||||
class: "fa fa-trash"
|
||||
}).appendTo(uninstall);
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text("Uninstall").appendTo(uninstall);
|
||||
|
||||
var update = $("<button/>", {
|
||||
class: "red-ui-button red-ui-button-small"
|
||||
}).css({
|
||||
"margin-left": "10px",
|
||||
width: buttonWidth
|
||||
}).appendTo(row1);
|
||||
$("<i/>", {
|
||||
class: "fa fa-refresh"
|
||||
}).appendTo(update);
|
||||
$("<span/>").css({
|
||||
"margin-left": "5px"
|
||||
}).text("Update").appendTo(update);
|
||||
|
||||
var row2 = $("<div/>", {
|
||||
class: "red-ui-palette-module-shade"
|
||||
}).appendTo(container);
|
||||
$("<img/>", {
|
||||
src: "red/images/spin.svg",
|
||||
class: "red-ui-palette-spinner"
|
||||
}).appendTo(row2);
|
||||
row2.hide();
|
||||
var warning = $('<span style="display:inline-block; width: 16px;"><i class="fa fa-warning"></i></span>').appendTo(row0);
|
||||
RED.popover.tooltip(warning.find("i"),function() {
|
||||
var val = fmodule.typedInput("type");
|
||||
if (val === "_custom_") {
|
||||
val = fmodule.val();
|
||||
}
|
||||
if (!RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList)) {
|
||||
return "Module not allowed"
|
||||
} else {
|
||||
return "Module not installed: "+missingModuleReasons[val]
|
||||
}
|
||||
})
|
||||
|
||||
fvar.on("change", function (e) {
|
||||
opt.vname = $(this).val();
|
||||
var v = $(this).val().trim();
|
||||
if (v === "" || / /.test(v)) {
|
||||
fvar.addClass("input-error");
|
||||
} else {
|
||||
fvar.removeClass("input-error");
|
||||
}
|
||||
});
|
||||
|
||||
fmodule.on("change", function (e) {
|
||||
var val = $(this).val();
|
||||
if (!checkLib(val, opt.id)) {
|
||||
$(this).addClass("input-error");
|
||||
warn.show();
|
||||
var val = $(this).typedInput("type");
|
||||
if (val === "_custom_") {
|
||||
val = $(this).val();
|
||||
}
|
||||
else {
|
||||
$(this).removeClass("input-error");
|
||||
warn.hide();
|
||||
if (val && (val !== "") &&
|
||||
(!opt.version || (opt.version === ""))) {
|
||||
install.attr("disabled", false);
|
||||
}
|
||||
}
|
||||
opt.spec = val;
|
||||
});
|
||||
var varName = val.trim().replace(/^@/,"").replace(/@.*$/,"").replace(/[-_/]./g, function(v) { return v[1].toUpperCase() });
|
||||
fvar.val(varName);
|
||||
fvar.trigger("change");
|
||||
|
||||
install.on("click", function (e) {
|
||||
withNotification(
|
||||
"Install NPM Module " +opt.spec,
|
||||
"Install",
|
||||
function (cb) {
|
||||
updateModule(
|
||||
node, opt.spec, false,
|
||||
fmodule, install,
|
||||
row1, finstalled, removeButton,
|
||||
row2,
|
||||
cb);
|
||||
if (RED.utils.checkModuleAllowed(val,null,installAllowList,installDenyList) && (missingModules.indexOf(val) === -1)) {
|
||||
fmodule.removeClass("input-error");
|
||||
warning.removeClass("input-error");
|
||||
} else {
|
||||
fmodule.addClass("input-error");
|
||||
warning.addClass("input-error");
|
||||
}
|
||||
});
|
||||
});
|
||||
uninstall.on("click", function (e) {
|
||||
withNotification(
|
||||
"Uninstall NPM Module "+opt.spec,
|
||||
"Uninstall",
|
||||
function (cb) {
|
||||
uninstallModule(
|
||||
node, opt.spec, opt.name, opt.version,
|
||||
fmodule, install,
|
||||
row1, finstalled,
|
||||
removeButton,
|
||||
row2,
|
||||
cb);
|
||||
});
|
||||
});
|
||||
update.on("click", function (e) {
|
||||
withNotification(
|
||||
"Update NPM Module " +opt.spec,
|
||||
"Update",
|
||||
function (cb) {
|
||||
updateModule(
|
||||
node, opt.spec, true,
|
||||
fmodule, install,
|
||||
row1, finstalled, removeButton,
|
||||
row2,
|
||||
cb);
|
||||
});
|
||||
});
|
||||
|
||||
install.attr("disabled", true);
|
||||
if (opt) {
|
||||
function updateData(data) {
|
||||
if (data.vname && (data.vname !== "")) {
|
||||
fvar.val(data.vname);
|
||||
}
|
||||
if (data.spec && (data.spec !== "")) {
|
||||
fmodule.val(data.spec);
|
||||
}
|
||||
if (data.version && (data.version !== "")) {
|
||||
fmodule.prop("disabled", true);
|
||||
row1.show();
|
||||
removeButton.hide();
|
||||
finstalled.val(data.name+"@"+data.version)
|
||||
if (data.status["preinstalled"]) {
|
||||
uninstall.attr("disabled", true);
|
||||
update.attr("disabled", true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
install.attr("disabled", false);
|
||||
row1.hide();
|
||||
removeButton.show();
|
||||
}
|
||||
}
|
||||
updateData(opt);
|
||||
if (opt.inProgress) {
|
||||
row2.show();
|
||||
opt.onReady = function () {
|
||||
row2.hide();
|
||||
updateData(opt);
|
||||
opt.onReady = null;
|
||||
};
|
||||
if (RED.utils.checkModuleAllowed(opt.module,null,installAllowList,installDenyList) && (missingModules.indexOf(opt.module) === -1)) {
|
||||
fmodule.removeClass("input-error");
|
||||
warning.removeClass("input-error");
|
||||
} else {
|
||||
fmodule.addClass("input-error");
|
||||
warning.addClass("input-error");
|
||||
}
|
||||
if (opt.var) {
|
||||
fvar.trigger("change");
|
||||
}
|
||||
},
|
||||
removable: true,
|
||||
sortable: true
|
||||
});
|
||||
updateLib(node, function () {
|
||||
removable: true
|
||||
});
|
||||
|
||||
var libs = node.libs || [];
|
||||
for (var i=0,l=libs.length;i<l; i++) {
|
||||
libList.editableList('addItem',libs[i])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RED.nodes.registerType('function',{
|
||||
@ -602,7 +280,22 @@
|
||||
noerr: {value:0,required:true,validate:function(v) { return !v; }},
|
||||
initialize: {value:""},
|
||||
finalize: {value:""},
|
||||
libs: {value: []}
|
||||
libs: {value: [], validate: function(v) {
|
||||
if (!v) { return true; }
|
||||
for (var i=0,l=v.length;i<l;i++) {
|
||||
var m = v[i];
|
||||
if (!RED.utils.checkModuleAllowed(m.module,null,installAllowList,installDenyList)) {
|
||||
return false
|
||||
}
|
||||
if (m.var === "" || / /.test(m.var)) {
|
||||
return false;
|
||||
}
|
||||
if (missingModules.indexOf(m.module) > -1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
},
|
||||
inputs:1,
|
||||
outputs:1,
|
||||
@ -623,21 +316,24 @@
|
||||
$("#" + tab.id).show();
|
||||
}
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-config",
|
||||
iconClass: "fa fa-cog",
|
||||
maximumTabWidth: 38
|
||||
|
||||
});
|
||||
|
||||
tabs.addTab({
|
||||
id: "func-tab-init",
|
||||
label: that._("function.label.initialize")
|
||||
label: "On Start", //that._("function.label.initialize")
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-body",
|
||||
label: that._("function.label.function")
|
||||
label: "On Message"//that._("function.label.function")
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-finalize",
|
||||
label: that._("function.label.finalize")
|
||||
});
|
||||
tabs.addTab({
|
||||
id: "func-tab-config",
|
||||
label: "Config"
|
||||
label: "On Stop"//that._("function.label.finalize")
|
||||
});
|
||||
|
||||
tabs.activateTab("func-tab-body");
|
||||
@ -748,7 +444,9 @@
|
||||
RED.popover.tooltip($("#node-function-expand-js"), RED._("node-red:common.label.expand"));
|
||||
RED.popover.tooltip($("#node-finalize-expand-js"), RED._("node-red:common.label.expand"));
|
||||
|
||||
if (RED.settings.functionExternalModules !== false) {
|
||||
prepareLibraryConfig(that);
|
||||
}
|
||||
},
|
||||
oneditsave: function() {
|
||||
var node = this;
|
||||
@ -780,24 +478,28 @@
|
||||
|
||||
$("#node-input-noerr").val(noerr);
|
||||
this.noerr = noerr;
|
||||
|
||||
if (RED.settings.functionExternalModules !== false) {
|
||||
var libs = $("#node-input-libs-container").editableList("items");
|
||||
var oldLibs = node.libs || [];
|
||||
var newLibs = [];
|
||||
node.libs = newLibs;
|
||||
node.libs = [];
|
||||
libs.each(function(i) {
|
||||
var item = $(this);
|
||||
var v = item.find(".node-input-libs-var").val();
|
||||
var n = item.find(".node-input-libs-val").val();
|
||||
var n = item.find(".node-input-libs-val").typedInput("type");
|
||||
if (n === "_custom_") {
|
||||
n = item.find(".node-input-libs-val").val();
|
||||
}
|
||||
if ((!v || (v === "")) ||
|
||||
(!n || (n === ""))) {
|
||||
return;
|
||||
}
|
||||
newLibs.push({
|
||||
vname: v,
|
||||
spec: n
|
||||
node.libs.push({
|
||||
var: v,
|
||||
module: n
|
||||
});
|
||||
});
|
||||
} else {
|
||||
node.libs = [];
|
||||
}
|
||||
},
|
||||
oneditcancel: function() {
|
||||
var node = this;
|
||||
@ -823,15 +525,16 @@
|
||||
this.editor.resize();
|
||||
|
||||
var height = size.height;
|
||||
$("#node-input-init-editor").css("height", (height -105)+"px");
|
||||
$("#node-input-func-editor").css("height", (height -145)+"px");
|
||||
$("#node-input-finalize-editor").css("height", (height -105)+"px");
|
||||
$("#node-input-init-editor").css("height", (height -45)+"px");
|
||||
$("#node-input-func-editor").css("height", (height -45)+"px");
|
||||
$("#node-input-finalize-editor").css("height", (height -45)+"px");
|
||||
|
||||
this.initEditor.resize();
|
||||
this.editor.resize();
|
||||
this.finalizeEditor.resize();
|
||||
|
||||
$("#node-input-libs-container").css("height", (height -155)+"px");
|
||||
$("#node-input-libs-container").css("height", (height - 185)+"px");
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
@ -91,13 +91,17 @@ module.exports = function(RED) {
|
||||
function FunctionNode(n) {
|
||||
var libs = n.libs || [];
|
||||
n.modules = libs.map(x => x.spec).filter(x => (x && (x !== "")));
|
||||
var loadPromise = RED.nodes.createNode(this,n);
|
||||
RED.nodes.createNode(this,n);
|
||||
var node = this;
|
||||
node.name = n.name;
|
||||
node.func = n.func;
|
||||
node.ini = n.initialize ? n.initialize.trim() : "";
|
||||
node.fin = n.finalize ? n.finalize.trim() : "";
|
||||
node.libs = libs;
|
||||
node.libs = libs || [];
|
||||
|
||||
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
|
||||
throw new Error("Function node not allowed to load external modules");
|
||||
}
|
||||
|
||||
var handleNodeDoneCall = true;
|
||||
|
||||
@ -287,13 +291,10 @@ module.exports = function(RED) {
|
||||
}
|
||||
});
|
||||
|
||||
// wait for module installation
|
||||
loadPromise.catch(()=>{
|
||||
}).finally(function () {
|
||||
if (node.hasOwnProperty("libs")) {
|
||||
var modules = node.libs;
|
||||
modules.forEach(module => {
|
||||
var vname = module.hasOwnProperty("vname") ? module.vname : null;
|
||||
var vname = module.hasOwnProperty("var") ? module.var : null;
|
||||
if (vname && (vname !== "")) {
|
||||
sandbox[vname] = null;
|
||||
try {
|
||||
@ -304,6 +305,7 @@ module.exports = function(RED) {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
node.warn("failed to load library: "+ module.spec);
|
||||
}
|
||||
}
|
||||
@ -450,10 +452,12 @@ module.exports = function(RED) {
|
||||
updateErrorInfo(err);
|
||||
node.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
RED.nodes.registerType("function",FunctionNode, {
|
||||
dynamicModuleList: "modules"
|
||||
dynamicModuleList: "libs",
|
||||
settings: {
|
||||
functionExternalModules: { value: true, exportable: true }
|
||||
}
|
||||
});
|
||||
RED.library.register("functions");
|
||||
};
|
||||
|
@ -220,7 +220,7 @@
|
||||
"finalize": "// Code added here will be run when the\n// node is being stopped or re-deployed.\n"
|
||||
},
|
||||
"require": {
|
||||
"var": "name",
|
||||
"var": "variable",
|
||||
"module": "module"
|
||||
},
|
||||
"error": {
|
||||
|
209
packages/node_modules/@node-red/registry/lib/externalModules.js
vendored
Normal file
209
packages/node_modules/@node-red/registry/lib/externalModules.js
vendored
Normal file
@ -0,0 +1,209 @@
|
||||
// This module handles the management of modules required by the runtime and flows.
|
||||
// Essentially this means keeping track of what extra modules a flow requires,
|
||||
// ensuring those modules are installed and providing a standard way for nodes
|
||||
// to require those modules safely.
|
||||
|
||||
const fs = require("fs-extra");
|
||||
const registryUtil = require("./util");
|
||||
const path = require("path");
|
||||
const exec = require("@node-red/util").exec;
|
||||
const log = require("@node-red/util").log;
|
||||
|
||||
const BUILTIN_MODULES = require('module').builtinModules;
|
||||
const EXTERNAL_MODULES_DIR = "externalModules";
|
||||
|
||||
// TODO: outsource running npm to a plugin
|
||||
const NPM_COMMAND = (process.platform === "win32") ? "npm.cmd" : "npm";
|
||||
|
||||
let registeredTypes = {};
|
||||
let settings;
|
||||
|
||||
let knownExternalModules = {};
|
||||
|
||||
let installEnabled = true;
|
||||
let installAllowList = ['*'];
|
||||
let installDenyList = [];
|
||||
|
||||
function getInstallDir() {
|
||||
return path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", "externalModules"));
|
||||
}
|
||||
|
||||
async function refreshExternalModules() {
|
||||
const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR));
|
||||
try {
|
||||
const pkgFile = JSON.parse(await fs.readFile(path.join(externalModuleDir,"package.json"),"utf-8"));
|
||||
knownExternalModules = pkgFile.dependencies;
|
||||
} catch(err) {
|
||||
}
|
||||
}
|
||||
|
||||
function init(_settings) {
|
||||
settings = _settings;
|
||||
path.resolve(settings.userDir || process.env.NODE_RED_HOME || ".");
|
||||
|
||||
if (settings.externalModules && settings.externalModules.modules) {
|
||||
if (settings.externalModules.modules.allowList || settings.externalModules.modules.denyList) {
|
||||
installAllowList = settings.externalModules.modules.allowList;
|
||||
installDenyList = settings.externalModules.modules.denyList;
|
||||
}
|
||||
if (settings.externalModules.modules.hasOwnProperty("allowInstall")) {
|
||||
installEnabled = settings.externalModules.modules.allowInstall
|
||||
}
|
||||
}
|
||||
installAllowList = registryUtil.parseModuleList(installAllowList);
|
||||
installDenyList = registryUtil.parseModuleList(installDenyList);
|
||||
}
|
||||
|
||||
function register(type, dynamicModuleListProperty) {
|
||||
registeredTypes[type] = dynamicModuleListProperty;
|
||||
}
|
||||
|
||||
function requireModule(module) {
|
||||
if (!registryUtil.checkModuleAllowed( module, null,installAllowList,installDenyList)) {
|
||||
const e = new Error("Module not allowed");
|
||||
e.code = "module_not_allowed";
|
||||
throw e;
|
||||
}
|
||||
if (BUILTIN_MODULES.indexOf(module) !== -1) {
|
||||
return require(module);
|
||||
}
|
||||
if (!knownExternalModules[module]) {
|
||||
const e = new Error("Module not allowed");
|
||||
e.code = "module_not_allowed";
|
||||
throw e;
|
||||
}
|
||||
const externalModuleDir = path.resolve(path.join(settings.userDir || process.env.NODE_RED_HOME || ".", EXTERNAL_MODULES_DIR));
|
||||
const moduleDir = path.join(externalModuleDir,"node_modules",module);
|
||||
return require(moduleDir);
|
||||
}
|
||||
|
||||
function parseModuleName(module) {
|
||||
var match = /((?:@[^/]+\/)?[^/@]+)(?:@([\s\S]+))?/.exec(module);
|
||||
if (match) {
|
||||
return {
|
||||
spec: module,
|
||||
module: match[1],
|
||||
version: match[2],
|
||||
builtin: BUILTIN_MODULES.indexOf(match[1]) !== -1,
|
||||
known: !!knownExternalModules[match[1]]
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isInstalled(moduleDetails) {
|
||||
return moduleDetails.builtin || moduleDetails.known;
|
||||
}
|
||||
|
||||
async function checkFlowDependencies(flowConfig) {
|
||||
await refreshExternalModules();
|
||||
|
||||
const checkedModules = {};
|
||||
const promises = [];
|
||||
const errors = [];
|
||||
|
||||
flowConfig.forEach(n => {
|
||||
if (registeredTypes[n.type]) {
|
||||
let nodeModules = n[registeredTypes[n.type]] || [];
|
||||
if (!Array.isArray(nodeModules)) {
|
||||
nodeModules = [nodeModules]
|
||||
}
|
||||
nodeModules.forEach(module => {
|
||||
if (typeof module !== 'string') {
|
||||
module = module.module || "";
|
||||
}
|
||||
if (module) {
|
||||
let moduleDetails = parseModuleName(module)
|
||||
if (moduleDetails && checkedModules[moduleDetails.module] === undefined) {
|
||||
checkedModules[moduleDetails.module] = isInstalled(moduleDetails)
|
||||
if (!checkedModules[moduleDetails.module]) {
|
||||
if (installEnabled) {
|
||||
promises.push(installModule(moduleDetails).catch(err => {
|
||||
errors.push({module: moduleDetails,error:err});
|
||||
}))
|
||||
} else if (!installEnabled) {
|
||||
const e = new Error("Module install disabled - externalModules.modules.allowInstall=false");
|
||||
e.code = "install_not_allowed";
|
||||
errors.push({module: moduleDetails,error:e});
|
||||
}
|
||||
} else if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) {
|
||||
const e = new Error("Module not allowed");
|
||||
e.code = "module_not_allowed";
|
||||
errors.push({module: moduleDetails,error:e});
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async function ensureModuleDir() {
|
||||
const installDir = getInstallDir();
|
||||
|
||||
if (!fs.existsSync(installDir)) {
|
||||
await fs.ensureDir(installDir);
|
||||
}
|
||||
const pkgFile = path.join(installDir,"package.json");
|
||||
if (!fs.existsSync(pkgFile)) {
|
||||
await fs.writeFile(path.join(installDir,"package.json"),`{
|
||||
"name": "Node-RED External Modules",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {}
|
||||
}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function installModule(moduleDetails) {
|
||||
let installSpec = moduleDetails.module;
|
||||
if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) {
|
||||
const e = new Error("Install not allowed");
|
||||
e.code = "install_not_allowed";
|
||||
throw e;
|
||||
}
|
||||
if (moduleDetails.version) {
|
||||
installSpec = installSpec+"@"+moduleDetails.version;
|
||||
}
|
||||
log.info(log._("server.install.installing",{name: moduleDetails.module,version: moduleDetails.version||"latest"}));
|
||||
const installDir = getInstallDir();
|
||||
|
||||
await ensureModuleDir();
|
||||
|
||||
var args = ["install", installSpec, "--production"];
|
||||
return exec.run(NPM_COMMAND, args, {
|
||||
cwd: installDir
|
||||
},true).then(result => {
|
||||
log.info("successfully installed: "+installSpec);
|
||||
}).catch(result => {
|
||||
var output = result.stderr;
|
||||
var e;
|
||||
var lookForVersionNotFound = new RegExp("version not found: ","m");
|
||||
if (/E404/.test(output) || /ETARGET/.test(output)) {
|
||||
log.error(log._("server.install.install-failed-not-found",{name:installSpec}));
|
||||
e = new Error("Module not found");
|
||||
e.code = 404;
|
||||
throw e;
|
||||
} else {
|
||||
log.error(log._("server.install.install-failed-long",{name:installSpec}));
|
||||
log.error("------------------------------------------");
|
||||
log.error(output);
|
||||
log.error("------------------------------------------");
|
||||
throw new Error(log._("server.install.install-failed"));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
register: register,
|
||||
checkFlowDependencies: checkFlowDependencies,
|
||||
require: requireModule
|
||||
}
|
@ -28,6 +28,7 @@ var registry = require("./registry");
|
||||
var loader = require("./loader");
|
||||
var installer = require("./installer");
|
||||
var library = require("./library");
|
||||
const externalModules = require("./externalModules")
|
||||
|
||||
/**
|
||||
* Initialise the registry with a reference to a runtime object
|
||||
@ -42,6 +43,7 @@ function init(runtime) {
|
||||
loader.init(runtime);
|
||||
registry.init(runtime.settings,loader);
|
||||
library.init();
|
||||
externalModules.init(runtime.settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -297,6 +299,8 @@ module.exports = {
|
||||
*/
|
||||
getNodeExampleFlowPath: library.getExampleFlowPath,
|
||||
|
||||
checkFlowDependencies: externalModules.checkFlowDependencies,
|
||||
|
||||
deprecated: require("./deprecated")
|
||||
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ var fs = require("fs");
|
||||
var library = require("./library");
|
||||
const {events} = require("@node-red/util")
|
||||
var subflows = require("./subflow");
|
||||
var externalModules = require("./externalModules")
|
||||
var settings;
|
||||
var loader;
|
||||
|
||||
@ -28,6 +29,7 @@ var nodeConfigCache = {};
|
||||
var moduleConfigs = {};
|
||||
var nodeList = [];
|
||||
var nodeConstructors = {};
|
||||
var nodeOptions = {};
|
||||
var subflowModules = {};
|
||||
|
||||
var nodeTypeToId = {};
|
||||
@ -36,12 +38,7 @@ var moduleNodes = {};
|
||||
function init(_settings,_loader) {
|
||||
settings = _settings;
|
||||
loader = _loader;
|
||||
moduleNodes = {};
|
||||
nodeTypeToId = {};
|
||||
nodeConstructors = {};
|
||||
subflowModules = {};
|
||||
nodeList = [];
|
||||
nodeConfigCache = {};
|
||||
clear();
|
||||
}
|
||||
|
||||
function load() {
|
||||
@ -234,6 +231,7 @@ function removeNode(id) {
|
||||
if (typeId === id) {
|
||||
delete subflowModules[t];
|
||||
delete nodeConstructors[t];
|
||||
delete nodeOptions[t];
|
||||
delete nodeTypeToId[t];
|
||||
}
|
||||
});
|
||||
@ -411,7 +409,7 @@ function getCaller(){
|
||||
return stack[0].getFileName();
|
||||
}
|
||||
|
||||
function registerNodeConstructor(nodeSet,type,constructor) {
|
||||
function registerNodeConstructor(nodeSet,type,constructor,options) {
|
||||
if (nodeConstructors.hasOwnProperty(type)) {
|
||||
throw new Error(type+" already registered");
|
||||
}
|
||||
@ -431,6 +429,12 @@ function registerNodeConstructor(nodeSet,type,constructor) {
|
||||
}
|
||||
|
||||
nodeConstructors[type] = constructor;
|
||||
nodeOptions[type] = options;
|
||||
if (options) {
|
||||
if (options.dynamicModuleList) {
|
||||
externalModules.register(type,options.dynamicModuleList);
|
||||
}
|
||||
}
|
||||
events.emit("type-registered",type);
|
||||
}
|
||||
|
||||
@ -525,6 +529,7 @@ function clear() {
|
||||
moduleConfigs = {};
|
||||
nodeList = [];
|
||||
nodeConstructors = {};
|
||||
nodeOptions = {};
|
||||
subflowModules = {};
|
||||
nodeTypeToId = {};
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
const path = require("path");
|
||||
const semver = require("semver");
|
||||
const {events,i18n,log} = require("@node-red/util");
|
||||
|
||||
var runtime;
|
||||
|
||||
function copyObjectProperties(src,dst,copyList,blockList) {
|
||||
@ -45,13 +46,8 @@ function requireModule(name) {
|
||||
var relPath = path.relative(__dirname, moduleInfo.path);
|
||||
return require(relPath);
|
||||
} else {
|
||||
var npm = runtime.nodes.loadNPMModule(name);
|
||||
if (npm) {
|
||||
return npm;
|
||||
}
|
||||
var err = new Error(`Cannot find module '${name}'`);
|
||||
err.code = "MODULE_NOT_FOUND";
|
||||
throw err;
|
||||
// Require it here to avoid the circular dependency
|
||||
return require("./externalModules").require(name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,7 +125,6 @@ function checkAgainstList(module,version,list) {
|
||||
}
|
||||
|
||||
function checkModuleAllowed(module,version,allowList,denyList) {
|
||||
// console.log("checkModuleAllowed",module,version);//,allowList,denyList)
|
||||
if (!allowList && !denyList) {
|
||||
// Default to allow
|
||||
return true;
|
||||
|
@ -447,47 +447,5 @@ var api = module.exports = {
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets list of NPM modules
|
||||
* @param {Object} opts
|
||||
* @param {User} opts.user - the user calling the api
|
||||
* @param {Object} opts.req - the request to log (optional)
|
||||
* @return {Promise<Buffer>} - list of installed NPM modules
|
||||
* @memberof @node-red/runtime_nodes
|
||||
*/
|
||||
listNPMModules: async function(opts) {
|
||||
var promise = runtime.nodes.listNPMModules();
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Uninstall NPM modules
|
||||
* @param {Object} opts
|
||||
* @param {User} opts.user - the user calling the api
|
||||
* @param {Object} opts.req - the request to log (optional)
|
||||
* @return {Promise<Object>} - object for request result
|
||||
* @memberof @node-red/runtime_nodes
|
||||
*/
|
||||
uninstallNPMModule: async function(opts) {
|
||||
var spec = opts.spec;
|
||||
var promise = runtime.nodes.uninstallNPMModule(spec);
|
||||
return promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update NPM modules
|
||||
* @param {Object} opts
|
||||
* @param {User} opts.user - the user calling the api
|
||||
* @param {Object} opts.req - the request to log (optional)
|
||||
* @return {Promise<Object>} - object for request result
|
||||
* @memberof @node-red/runtime_nodes
|
||||
*/
|
||||
updateNPMModule: async function(opts) {
|
||||
var spec = opts.spec;
|
||||
var isUpdate = opts.update;
|
||||
var promise = runtime.nodes.updateNPMModule(spec, isUpdate);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
@ -187,8 +187,8 @@ function setFlows(_config,_credentials,type,muteLog,forceStart,user) {
|
||||
});
|
||||
}
|
||||
|
||||
return configSavePromise
|
||||
.then(function(flowRevision) {
|
||||
|
||||
return configSavePromise.then(flowRevision => {
|
||||
if (!isLoad) {
|
||||
log.debug("saved flow revision: "+flowRevision);
|
||||
}
|
||||
@ -246,7 +246,7 @@ function getFlows() {
|
||||
return activeConfig;
|
||||
}
|
||||
|
||||
function start(type,diff,muteLog) {
|
||||
async function start(type,diff,muteLog) {
|
||||
type = type||"full";
|
||||
started = true;
|
||||
var i;
|
||||
@ -271,7 +271,21 @@ function start(type,diff,muteLog) {
|
||||
log.info(" "+settings.userDir);
|
||||
}
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-types", type:"warning",text:"notification.warnings.missing-types",types:activeFlowConfig.missingTypes},retain:true});
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await typeRegistry.checkFlowDependencies(activeConfig.flows);
|
||||
} catch(err) {
|
||||
log.info("Failed to load external modules required by this flow:");
|
||||
const missingModules = [];
|
||||
for (i=0;i<err.length;i++) {
|
||||
let errMessage = err[i].error.toString()
|
||||
missingModules.push({module:err[i].module.module, error: err[i].error.code || err[i].error.toString()})
|
||||
log.info(` - ${err[i].module.spec} [${err[i].error.code || "unknown_error"}]`);
|
||||
}
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{error:"missing-modules", type:"warning",text:"notification.warnings.missing-modules",modules:missingModules},retain:true});
|
||||
return;
|
||||
}
|
||||
|
||||
// In safe mode, don't actually start anything, emit the necessary runtime event and return
|
||||
@ -280,7 +294,7 @@ function start(type,diff,muteLog) {
|
||||
log.info(log._("nodes.flows.safe-mode"));
|
||||
log.info("*****************************************************************")
|
||||
events.emit("runtime-event",{id:"runtime-state",payload:{error:"safe-mode", type:"warning",text:"notification.warnings.safe-mode"},retain:true});
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!muteLog) {
|
||||
@ -370,7 +384,7 @@ function start(type,diff,muteLog) {
|
||||
log.info(log._("nodes.flows.started-flows"));
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
function stop(type,diff,muteLog) {
|
||||
|
@ -26,7 +26,6 @@ var flows = require("../flows");
|
||||
var flowUtil = require("../flows/util")
|
||||
var context = require("./context");
|
||||
var Node = require("./Node");
|
||||
var npmModule = require("./npmModule");
|
||||
var log;
|
||||
|
||||
const events = require("@node-red/util").events;
|
||||
@ -50,7 +49,6 @@ function registerType(nodeSet,type,constructor,opts) {
|
||||
type = nodeSet;
|
||||
nodeSet = "";
|
||||
}
|
||||
var dynModule = null;
|
||||
if (opts) {
|
||||
if (opts.credentials) {
|
||||
credentials.register(type,opts.credentials);
|
||||
@ -62,11 +60,7 @@ function registerType(nodeSet,type,constructor,opts) {
|
||||
log.warn("["+type+"] "+err.message);
|
||||
}
|
||||
}
|
||||
if (opts.dynamicModuleList) {
|
||||
dynModule = opts.dynamicModuleList;
|
||||
}
|
||||
}
|
||||
npmModule.register(type, dynModule);
|
||||
if(!(constructor.prototype instanceof Node)) {
|
||||
if(Object.getPrototypeOf(constructor.prototype) === Object.prototype) {
|
||||
util.inherits(constructor,Node);
|
||||
@ -87,7 +81,7 @@ function registerType(nodeSet,type,constructor,opts) {
|
||||
}
|
||||
}
|
||||
}
|
||||
registry.registerType(nodeSet,type,constructor);
|
||||
registry.registerType(nodeSet,type,constructor,opts);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -116,7 +110,6 @@ function createNode(node,def) {
|
||||
} else if (credentials.getDefinition(node.type)) {
|
||||
node.credentials = {};
|
||||
}
|
||||
return npmModule.checkInstall(def);
|
||||
}
|
||||
|
||||
function registerSubflow(nodeSet, subflow) {
|
||||
@ -145,7 +138,6 @@ function init(runtime) {
|
||||
flows.init(runtime);
|
||||
registry.init(runtime);
|
||||
context.init(runtime.settings);
|
||||
npmModule.init(runtime);
|
||||
}
|
||||
|
||||
function disableNode(id) {
|
||||
@ -270,10 +262,4 @@ module.exports = {
|
||||
loadContextsPlugin: context.load,
|
||||
closeContextsPlugin: context.close,
|
||||
listContextStores: context.listStores,
|
||||
|
||||
// NPM modules
|
||||
listNPMModules: npmModule.list,
|
||||
uninstallNPMModule: npmModule.uninstall,
|
||||
updateNPMModule: npmModule.update,
|
||||
loadNPMModule: npmModule.load
|
||||
};
|
||||
|
@ -1,460 +0,0 @@
|
||||
/**
|
||||
* Copyright JS Foundation and other contributors, http://js.foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var path = require("path");
|
||||
var fs = require("fs-extra");
|
||||
var os = require("os");
|
||||
var util = require("@node-red/registry/lib/util");
|
||||
|
||||
var api;
|
||||
|
||||
var runtime;
|
||||
var settings;
|
||||
var exec;
|
||||
var log;
|
||||
|
||||
var npmCommand = (process.platform === "win32") ? "npm.cmd" : "npm";
|
||||
|
||||
var metadataFileName = "npm-modules.json";
|
||||
|
||||
var moduleProp = {};
|
||||
var moduleBase = null;
|
||||
|
||||
var allowInstall = true;
|
||||
var allowList = ["*"];
|
||||
var denyList = [];
|
||||
|
||||
var inProgress = {};
|
||||
|
||||
/**
|
||||
* Initialise npm install module.
|
||||
* @param {Object} _runtime - runtime object
|
||||
*/
|
||||
function init(_runtime) {
|
||||
runtime = _runtime;
|
||||
settings = _runtime.settings;
|
||||
exec = _runtime.exec;
|
||||
log = _runtime.log;
|
||||
|
||||
moduleProp = {};
|
||||
inProgress = {};
|
||||
|
||||
moduleBase = settings.userDir || process.env.NODE_RED_HOME || ".";
|
||||
|
||||
if (settings.hasOwnProperty("externalModules")) {
|
||||
var em = settings.externalModules;
|
||||
if (em && em.hasOwnProperty("modules")) {
|
||||
var mod = em.modules;
|
||||
if (mod.hasOwnProperty("allowInstall")) {
|
||||
allowInstall = mod.allowInstall;
|
||||
}
|
||||
if (mod.hasOwnProperty("allowList")) {
|
||||
var alist = mod.allowList;
|
||||
if (Array.isArray(alist)) {
|
||||
allowList = alist;
|
||||
}
|
||||
else {
|
||||
log.warn("unexpected value of externalModule.allowList in settings.js");
|
||||
}
|
||||
}
|
||||
if (mod.hasOwnProperty("denyList")) {
|
||||
var dlist = mod.denyList;
|
||||
if (Array.isArray(dlist)) {
|
||||
denyList = dlist;
|
||||
}
|
||||
else {
|
||||
log.warn("unexpected value of externalModule.denyList in settings.js");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register dynamic module installation property.
|
||||
* @param {string} type - node type
|
||||
* @param {string} prop - property name
|
||||
*/
|
||||
function register(type, prop) {
|
||||
if (prop) {
|
||||
moduleProp[type] = prop;
|
||||
}
|
||||
else {
|
||||
delete moduleProp[prop]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to install modules
|
||||
*/
|
||||
function modulePath() { // takes variable length arguments in `arguments`
|
||||
var result = moduleBase;
|
||||
for(var i = 0; i < arguments.length; i++) {
|
||||
result = path.join(result, arguments[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decompose NPM module specification string
|
||||
* @param {string} module - module specification
|
||||
* @return {Object} array [name, version], where name is name part and version is version part of the module
|
||||
*/
|
||||
function moduleName(module) {
|
||||
var match = /^([^@]+)@(.+)/.exec(module);
|
||||
if (match) {
|
||||
return [match[1], match[2]];
|
||||
}
|
||||
return [module, undefined];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get NPM module info
|
||||
* @param {string} name - module name
|
||||
* @return {Object} package.json for specified NPM module
|
||||
*/
|
||||
function infoNPM(name) {
|
||||
var path = modulePath("node_modules", name, "package.json");
|
||||
try {
|
||||
var pkg = fs.readFileSync(path);
|
||||
return JSON.parse(pkg);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load NPM module metadata
|
||||
* @return {object} module metadata object
|
||||
*/
|
||||
function loadMetadata() {
|
||||
var path = modulePath(metadataFileName);
|
||||
try {
|
||||
var pkg = fs.readFileSync(path);
|
||||
return JSON.parse(pkg);
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return {
|
||||
modules: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save NPM module metadata
|
||||
* @param {string} data - module metadata object
|
||||
*/
|
||||
function saveMetadata(data) {
|
||||
var path = modulePath(metadataFileName);
|
||||
var str = JSON.stringify(data, null, 4);
|
||||
fs.writeFileSync(path, str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find item in metadata
|
||||
* @param {Object} meta - metadata
|
||||
* @param {string} name - module name
|
||||
* @return {object} metadata item
|
||||
*/
|
||||
function findModule(meta, name) {
|
||||
var modules = meta.modules;
|
||||
var item = modules.find(item => (item.name === name));
|
||||
return item;
|
||||
}
|
||||
|
||||
function setInProgress(name) {
|
||||
inProgress[name] = true;
|
||||
}
|
||||
|
||||
function clearInProgress(name) {
|
||||
inProgress[name] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install NPM module
|
||||
* @param {string} module - module specification
|
||||
*/
|
||||
function installNPM(module) {
|
||||
var [name, ver] = moduleName(module);
|
||||
setInProgress(name);
|
||||
return new Promise((resolve, reject) => {
|
||||
var pkg = infoNPM(name);
|
||||
if (!pkg) {
|
||||
var args = ["install", module];
|
||||
var dir = modulePath();
|
||||
return exec.run(npmCommand, args, {
|
||||
cwd: dir
|
||||
}, true).then(result => {
|
||||
if (result && (result.code === 0)) {
|
||||
pkg = infoNPM(name);
|
||||
var spec = name +(pkg ? "@"+pkg.version : "");
|
||||
log.info("successfully installed: "+spec);
|
||||
var meta = loadMetadata();
|
||||
var item = {
|
||||
name: name,
|
||||
spec: module,
|
||||
status: "installed",
|
||||
};
|
||||
meta.modules.push(item);
|
||||
saveMetadata(meta);
|
||||
clearInProgress(name);
|
||||
resolve(true);
|
||||
}
|
||||
else {
|
||||
clearInProgress(name);
|
||||
var msg = "failed to install: "+name;
|
||||
log.warn(msg);
|
||||
reject(msg);
|
||||
}
|
||||
}).catch(e => {
|
||||
clearInProgress(name);
|
||||
var msg = "failed to install: "+name
|
||||
log.warn(msg);
|
||||
reject(msg);
|
||||
});
|
||||
}
|
||||
else {
|
||||
var meta = loadMetadata();
|
||||
if (!findModule(meta, name)) {
|
||||
var item = {
|
||||
name: name,
|
||||
spec: module,
|
||||
status: "preinstalled",
|
||||
};
|
||||
meta.modules.push(item);
|
||||
saveMetadata(meta);
|
||||
}
|
||||
clearInProgress(name);
|
||||
var spec = name +(pkg ? ("@"+pkg.version) : "");
|
||||
log.info("already installed: "+spec);
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check allowance of NPM module installation
|
||||
* @param {string} name - module specification
|
||||
*/
|
||||
function isAllowed(name) {
|
||||
if (!allowInstall) {
|
||||
return false;
|
||||
}
|
||||
var [module, ver] = moduleName(name);
|
||||
var aList = util.parseModuleList(allowList);
|
||||
var dList = util.parseModuleList(denyList);
|
||||
return util.checkModuleAllowed(module, ver, aList, dList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and install NPM module according to dynamic module specification
|
||||
* @param {Object} node - node object
|
||||
*/
|
||||
function checkInstall(node) {
|
||||
var name = null;
|
||||
if(moduleProp.hasOwnProperty(node.type)) {
|
||||
name = moduleProp[node.type];
|
||||
}
|
||||
var promises = [];
|
||||
if (name && node.hasOwnProperty(name)) {
|
||||
var modules = node[name];
|
||||
modules.forEach(module => {
|
||||
var name = module;
|
||||
if ((typeof module === "object") &&
|
||||
module &&
|
||||
module.hasOwnProperty("name")) {
|
||||
name = module.name;
|
||||
}
|
||||
if (isAllowed(name)) {
|
||||
var [n, v] = moduleName(name);
|
||||
setInProgress(name);
|
||||
promises.push(installNPM(name));
|
||||
}
|
||||
else {
|
||||
log.info("installation not allowed: "+name);
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load NPM module
|
||||
* @param {string} module - module to load
|
||||
*/
|
||||
function load(module) {
|
||||
try {
|
||||
var [name, ver] = moduleName(module);
|
||||
var path = modulePath("node_modules", name);
|
||||
var npm = require(path);
|
||||
return npm;
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of installed modules
|
||||
*/
|
||||
function listModules() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var meta = loadMetadata();
|
||||
var modules = meta.modules;
|
||||
modules.forEach(item => {
|
||||
var name = item.name;
|
||||
var info = infoNPM(name);
|
||||
if (info) {
|
||||
item.version = info.version;
|
||||
}
|
||||
item.inProgress = ((name in inProgress) && inProgress[name]);
|
||||
});
|
||||
Object.keys(inProgress).forEach(name => {
|
||||
if (inProgress[name] &&
|
||||
!modules.find(item => (item.name === name))) {
|
||||
modules.push({
|
||||
name: name,
|
||||
spec: name,
|
||||
state: "inprogress",
|
||||
inProgress: true
|
||||
});
|
||||
}
|
||||
});
|
||||
resolve(meta.modules);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall NPM modules
|
||||
*/
|
||||
function uninstall(module) {
|
||||
var [name, ver] = moduleName(module);
|
||||
setInProgress(name);
|
||||
return new Promise((resolve, reject) => {
|
||||
var pkg = infoNPM(name);
|
||||
var meta = loadMetadata();
|
||||
var item = findModule(meta, name);
|
||||
if (pkg && item) {
|
||||
if (item.status === "preinstalled") {
|
||||
clearInProgress(name);
|
||||
var msg = "can't uninstall preinstalled: "+name;
|
||||
log.warn(msg);
|
||||
reject(msg);
|
||||
}
|
||||
else {
|
||||
var args = ["uninstall", module];
|
||||
var dir = modulePath();
|
||||
return exec.run(npmCommand, args, {
|
||||
cwd: dir
|
||||
}, true).then(result => {
|
||||
if (result && (result.code === 0)) {
|
||||
log.info("successfully uninstalled: "+name);
|
||||
var meta = loadMetadata();
|
||||
var items = meta.modules.filter(item => (item.name !== name));
|
||||
meta.modules = items;
|
||||
saveMetadata(meta);
|
||||
clearInProgress(name);
|
||||
resolve(true);
|
||||
}
|
||||
else {
|
||||
clearInProgress(name);
|
||||
var msg = "failed to uninstall: "+name;
|
||||
log.warn(msg);
|
||||
reject(msg);
|
||||
}
|
||||
}).catch(e => {
|
||||
clearInProgress(name);
|
||||
var msg = "failed to uninstall: "+name;
|
||||
log.warn(msg);
|
||||
reject(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
clearInProgress(name);
|
||||
var msg = "module not installed: "+name;
|
||||
log.info(msg);
|
||||
reject(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update NPM modules
|
||||
*/
|
||||
function update(module, isUpdate) {
|
||||
var act = (isUpdate ? "updated": "install")
|
||||
var acted = (isUpdate ? "updated": "installed")
|
||||
var [name, ver] = moduleName(module);
|
||||
setInProgress(name);
|
||||
return new Promise((resolve, reject) => {
|
||||
var pkg = infoNPM(name);
|
||||
if (!pkg || isUpdate) {
|
||||
var args = ["install", module];
|
||||
var dir = modulePath();
|
||||
return exec.run(npmCommand, args, {
|
||||
cwd: dir
|
||||
}, true).then(result => {
|
||||
if (result && (result.code === 0)) {
|
||||
pkg = infoNPM(name);
|
||||
var spec = name +(pkg ? "@"+pkg.version : "");
|
||||
log.info("successfully "+acted+": "+spec);
|
||||
var meta = loadMetadata();
|
||||
var items = meta.modules.filter(item => (item.name !== name));
|
||||
var item = {
|
||||
name: name,
|
||||
spec: module,
|
||||
status: "installed",
|
||||
};
|
||||
items.push(item);
|
||||
meta.modules = items;
|
||||
saveMetadata(meta);
|
||||
clearInProgress(name);
|
||||
resolve(true);
|
||||
}
|
||||
else {
|
||||
clearInProgress(name);
|
||||
var msg = "failed to "+act+": "+name;
|
||||
log.warn(msg);
|
||||
reject(msg);
|
||||
}
|
||||
}).catch(e => {
|
||||
clearInProgress(name);
|
||||
var msg = "failed to "+act+": "+name;
|
||||
log.warn(msg);
|
||||
reject(msg);
|
||||
});
|
||||
}
|
||||
else {
|
||||
clearInProgress(name);
|
||||
var msg = "not "+acted+": "+name;
|
||||
log.info(msg);
|
||||
reject(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
api = {
|
||||
init: init,
|
||||
register: register,
|
||||
checkInstall: checkInstall,
|
||||
load: load,
|
||||
list: listModules,
|
||||
uninstall: uninstall,
|
||||
update: update
|
||||
};
|
||||
module.exports = api;
|
20
test/unit/@node-red/registry/lib/externalModules_spec.js
Normal file
20
test/unit/@node-red/registry/lib/externalModules_spec.js
Normal file
@ -0,0 +1,20 @@
|
||||
// init: init,
|
||||
// register: register,
|
||||
// checkFlowDependencies: checkFlowDependencies,
|
||||
// require: requireModule
|
||||
//
|
||||
|
||||
const should = require("should");
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
var NR_TEST_UTILS = require("nr-test-utils");
|
||||
|
||||
var externalModules = NR_TEST_UTILS.require("@node-red/registry/lib/externalModules");
|
||||
|
||||
|
||||
describe("externalModules api", function() {
|
||||
|
||||
|
||||
});
|
Loading…
Reference in New Issue
Block a user