@gregoriusrippenstein/node-red-contrib-nodedev
Version:
Support the development of Node-RED nodes within Node-RED.
571 lines (509 loc) • 20.4 kB
JavaScript
module.exports = function (RED) {
"use strict";
var fs = require('fs');
var path = require('path');
var mustache = require("mustache");
var spcRepDict = {
"ocb2": "{{",
"ocb3": "{{{",
"ccb3": "}}}",
"ccb2": "}}"
};
function extractTokens(tokens, set) {
set = set || new Set();
tokens.forEach(function (token) {
if (token[0] !== 'text') {
set.add(token[1]);
if (token.length > 4) {
extractTokens(token[4], set);
}
}
});
return set;
}
function parseContext(key) {
var match = /^(flow|global)(\[(\w+)\])?\.(.+)/.exec(key);
if (match) {
var parts = {};
parts.type = match[1];
parts.store = (match[3] === '') ? "default" : match[3];
parts.field = match[4];
return parts;
}
return undefined;
}
function parseEnv(key) {
var match = /^env\.(.+)/.exec(key);
if (match) {
return match[1];
}
return undefined;
}
function parseIntern(key) {
var match = /^__\.(.+)$/.exec(key);
if (match) {
return match[1];
}
return undefined;
}
/**
* Custom Mustache Context capable to collect message property and node
* flow and global context
*/
function NodeContext(msg, nodeContext, parent, escapeStrings, cachedContextTokens) {
this.msgContext = new mustache.Context(msg, parent);
this.nodeContext = nodeContext;
this.escapeStrings = escapeStrings;
this.cachedContextTokens = cachedContextTokens;
}
NodeContext.prototype = new mustache.Context();
NodeContext.prototype.lookup = function (name) {
// try message first:
try {
var value = this.msgContext.lookup(name);
if (value !== undefined) {
if (this.escapeStrings && typeof value === "string") {
value = value.replace(/\\/g, "\\\\");
value = value.replace(/\n/g, "\\n");
value = value.replace(/\t/g, "\\t");
value = value.replace(/\r/g, "\\r");
value = value.replace(/\f/g, "\\f");
value = value.replace(/[\b]/g, "\\b");
}
return value;
}
var spcRep = parseIntern(name)
if ( spcRep ) {
return spcRepDict[spcRep] || ("UNFOUND - " + spcRep);
}
// try env
if (parseEnv(name)) {
return this.cachedContextTokens[name];
}
// try flow/global context:
var context = parseContext(name);
if (context) {
var type = context.type;
var store = context.store;
var field = context.field;
var target = this.nodeContext[type];
if (target) {
return this.cachedContextTokens[name];
}
}
return '';
}
catch (err) {
throw err;
}
}
NodeContext.prototype.push = function push(view) {
return new NodeContext(view, this.nodeContext, this.msgContext, undefined, this.cachedContextTokens);
};
function handleTemplate(msg, node, template) {
var promises = [];
var tokens = extractTokens(mustache.parse(template));
var resolvedTokens = {};
tokens.forEach(function (name) {
var env_name = parseEnv(name);
if (env_name) {
var promise = new Promise((resolve, reject) => {
var val = RED.util.evaluateNodeProperty(env_name, 'env', node)
resolvedTokens[name] = val;
resolve();
});
promises.push(promise);
return;
}
var context = parseContext(name);
if (context) {
var type = context.type;
var store = context.store;
var field = context.field;
var target = node.context()[type];
if (target) {
var promise = new Promise((resolve, reject) => {
target.get(field, store, (err, val) => {
if (err) {
reject(err);
} else {
resolvedTokens[name] = val;
resolve();
}
});
});
promises.push(promise);
return;
}
}
});
return Promise.all(promises).then(function () {
return mustache.render(template, new NodeContext(msg, node.context(), null, false, resolvedTokens));
});
}
function createPluginNodeDefintions(msg, node) {
var sidebarHtmlPath = path.join(__dirname, 'templates', 'tmplsidebar.html');
var nodeJsPath = path.join(__dirname, 'templates', 'tmplsidebarnode.js');
var nodeHtmlPath = path.join(__dirname, 'templates', 'tmplsidebarnode.html');
var content = {};
var promises = [
handleTemplate(msg, node, fs.readFileSync(sidebarHtmlPath, 'utf8')).then((c) => { content["sbht"] = c }),
handleTemplate(msg, node, fs.readFileSync(nodeJsPath, 'utf8')).then((c) => { content["ndjs"] = c }),
handleTemplate(msg, node, fs.readFileSync(nodeHtmlPath, 'utf8')).then((c) => { content["ndht"] = c }),
];
return Promise.all(promises).then(() => {
var secondId = RED.util.generateId();
var thirdId = RED.util.generateId();
return [{
id: RED.util.generateId(),
type: "PkgFile",
name: msg.node.name + "Cfg.js",
filename: "nodes/" + msg.node.namelwr + ".js",
template: content["ndjs"],
syntax: "mustache",
format: "javascript",
output: "str",
x: 100,
y: 50,
wires: [[secondId]]
},
{
id: secondId,
type: "PkgFile",
name: "Sidebar: " + msg.node.name + ".html",
filename: "plugins/" + msg.node.namelwr + ".html",
template: content["sbht"],
syntax: "mustache",
format: "html",
output: "str",
x: 100,
y: 100,
wires: [[thirdId]]
},
{
id: thirdId,
type: "PkgFile",
name: msg.node.name + "Cfg.html",
filename: "nodes/" + msg.node.namelwr + ".html",
template: content["ndht"],
syntax: "mustache",
format: "html",
output: "str",
x: 100,
y: 150,
wires: [[]]
}]
})
}
function createRealPluginNodeDefintions(msg,node) {
var pluginJsPath = path.join(__dirname, 'templates', 'tmplplugin.js');
var content = {};
var promises = [
handleTemplate(msg, node, fs.readFileSync(pluginJsPath, 'utf8')).then((c) => { content["jsac"] = c }),
];
return Promise.all(promises).then(() => {
return [{
id: RED.util.generateId(),
type: "PkgFile",
name: msg.node.name + "Plugin.js",
filename: "plugins/" + msg.node.namelwr + ".js",
template: content["jsac"],
syntax: "mustache",
format: "javascript",
output: "str",
x: 100,
y: 50,
wires: [[]]
}]
})
}
function createSimpleNodeDefintions(msg,node) {
var htmlPath = path.join(__dirname, 'templates', 'tmpl.html');
var htmlJsPath = path.join(__dirname, 'templates', 'tmpl.html.js');
var jsPath = path.join(__dirname, 'templates', 'tmpl.js');
var localeJsonPath = path.join(__dirname, 'templates', 'locale', 'en-US', 'tmpl.json')
var localeHtmlPath = path.join(__dirname, 'templates', 'locale', 'en-US', 'tmpl.html')
var content = {};
var promises = [
handleTemplate(msg, node, fs.readFileSync(htmlPath, 'utf8')).then((c) => { content["html"] = c }),
handleTemplate(msg, node, fs.readFileSync(htmlJsPath, 'utf8')).then((c) => { content["htjs"] = c }),
handleTemplate(msg, node, fs.readFileSync(jsPath, 'utf8')).then((c) => { content["jasc"] = c }),
handleTemplate(msg, node, fs.readFileSync(localeJsonPath, 'utf8')).then((c) => { content["ljsn"] = c }),
handleTemplate(msg, node, fs.readFileSync(localeHtmlPath, 'utf8')).then((c) => { content["lhtm"] = c }),
];
return Promise.all(promises).then(() => {
var secondId = RED.util.generateId();
var secondIdTempJs = RED.util.generateId();
var thirdId = RED.util.generateId();
var fourthId = RED.util.generateId();
return [{
id: RED.util.generateId(),
type: "PkgFile",
name: msg.node.name + ".js",
filename: "nodes/" + msg.node.namelwr + ".js",
template: content["jasc"],
syntax: "mustache",
format: "javascript",
output: "str",
x: 100,
y: 50,
wires: [[secondIdTempJs]]
},
{
"id": secondIdTempJs,
"type": "template",
"name": msg.node.name + ".html.js",
"field": msg.node.name+ "TmplJs",
"fieldType": "msg",
"format": "javascript",
"syntax": "mustache",
"template": content["htjs"],
"x": 100,
"y": 100,
"wires": [
[
secondId
]
]
},
{
id: secondId,
type: "PkgFile",
name: msg.node.name + ".html",
filename: "nodes/" + msg.node.namelwr + ".html",
template: content["html"],
syntax: "mustache",
format: "html",
output: "str",
x: 100,
y: 150,
wires: [[thirdId]]
},
{
id: thirdId,
type: "PkgFile",
name: "Locale: " + msg.node.name + ".json",
filename: "nodes/locales/en-US/" + msg.node.namelwr + ".json",
template: content["ljsn"],
syntax: "mustache",
format: "json",
output: "str",
x: 120,
y: 200,
wires: [[fourthId]]
},
{
id: fourthId,
type: "PkgFile",
name: "Locale: " + msg.node.name + ".html",
filename: "nodes/locales/en-US/" + msg.node.namelwr + ".html",
template: content["lhtm"],
syntax: "mustache",
format: "html",
output: "str",
x: 120,
y: 250,
wires: [[]]
}
]
})
}
function createNodeDefintions(msg, node) {
if ( msg.node.isplugin ) {
return createPluginNodeDefintions(msg, node)
} else if (msg.node.isrealplugin ) {
return createRealPluginNodeDefintions(msg, node)
} else {
return createSimpleNodeDefintions(msg, node)
}
}
function createManifestFiles(msg, node, nodeDefinitions) {
var packageJsonPath = path.join(__dirname, 'templates', 'tmplpackage.json');
var readmePath = path.join(__dirname, 'templates', 'tmplreadme.md');
var licensePath = path.join(__dirname, 'templates', 'tmpllicense');
var changelogPath = path.join(__dirname, 'templates', 'tmplchangelog.md');
/* ASSUMPTION: assume the .js file is defined first and
then the .html file in the node definitions */
spcRepDict["nodestanza"] = " \"" + msg.node.namelwr + "\": \"" + nodeDefinitions[0].filename + "\""
if ( msg.node.isplugin ) {
spcRepDict["pluginstanza"] = " \"sidebar-plugin\": \"" + nodeDefinitions[1].filename + "\""
}
if (msg.node.isrealplugin) {
spcRepDict["pluginstanza"] = " \"" + msg.node.namelwr + "\": \"" + nodeDefinitions[0].filename + "\""
}
var content = {};
var promises = [
handleTemplate(msg, node, fs.readFileSync(packageJsonPath, 'utf8')).then((c) => { content["pkjs"] = c }),
handleTemplate(msg, node, fs.readFileSync(readmePath, 'utf8')).then((c) => { content["rdme"] = c }),
handleTemplate(msg, node, fs.readFileSync(licensePath, 'utf8')).then((c) => { content["lcns"] = c }),
handleTemplate(msg, node, fs.readFileSync(changelogPath, 'utf8')).then((c) => { content["chlg"] = c }),
];
return Promise.all(promises).then( () => {
var secondId = RED.util.generateId();
var thirdId = RED.util.generateId();
var fourthId = RED.util.generateId();
nodeDefinitions.push({
id: RED.util.generateId(),
type: "PkgFile",
name: "LICENSE",
filename: "LICENSE",
template: content["lcns"],
syntax: "mustache",
format: "text",
output: "str",
x: 100,
y: -150,
wires: [[ secondId ]]
})
nodeDefinitions.push({
id: secondId,
type: "PkgFile",
name: "README.md",
filename: "README.md",
template: content["rdme"],
syntax: "mustache",
format: "markdown",
output: "str",
x: 100,
y: -100,
wires: [[thirdId]]
})
nodeDefinitions.push({
id: thirdId,
type: "PkgFile",
name: "CHANGELOG.md",
filename: "CHANGELOG.md",
template: content["chlg"],
syntax: "mustache",
format: "markdown",
output: "str",
x: 100,
y: -50,
wires: [[fourthId]]
})
nodeDefinitions.push({
id: fourthId,
type: "PkgFile",
name: "package.json",
filename: "package.json",
template: content["pkjs"],
syntax: "mustache",
format: "json",
output: "str",
x: 100,
y: 0,
wires: [[nodeDefinitions[0].id]]
})
return nodeDefinitions;
})
}
function NodeFactoryFunctionality(config) {
RED.nodes.createNode(this, config);
var node = this;
var cfg = config;
node.on('close', function () {
node.status({});
});
node.on("input", function (msg, send, done) {
send({ payload: msg.node })
if (msg.node && msg.node.__task == "generate_from_templates") {
try {
createNodeDefintions(msg,node).then( (nodeDef) => {
if (msg.node.createmanifest ) {
/* create the package.json, readme and license files */
createManifestFiles(msg, node, nodeDef).then( (data) => {
var nodeImpStr = JSON.stringify(data);
send({ payload: nodeImpStr })
RED.comms.publish(
"nodedev:perform-autoimport-nodes",
RED.util.encodeObject({
msg: "autoimport",
payload: nodeImpStr,
topic: msg.topic,
nodeid: node.id,
_msg: msg
})
);
}).catch( (err) => {
msg.error = err
done(err.message, msg)
})
} else {
var nodeImpStr = JSON.stringify(nodeDef);
send({ payload: nodeImpStr })
RED.comms.publish(
"nodedev:perform-autoimport-nodes",
RED.util.encodeObject({
msg: "autoimport",
payload: nodeImpStr,
topic: msg.topic,
nodeid: node.id,
_msg: msg
})
);
}
done()
}).catch((err) => {
msg.error = err
done(err.message, msg)
})
} catch (err) {
msg.error = err
done(err.message, msg)
}
} else {
/* assume that payload is a buffer with a .tgz if not, error out */
try {
const onError = (err) => {
msg.error = err;
done("extraction error", msg)
}
const onFinish = (allFiles) => {
msg.payload = JSON.stringify(allFiles);
send(msg)
RED.comms.publish(
"nodedev:perform-autoimport-nodes",
RED.util.encodeObject({
msg: "autoimport",
payload: msg.payload,
topic: msg.topic,
nodeid: node.id,
_msg: msg
})
);
done()
}
require('./lib/tarhelpers.js').convertTarFile(RED, msg.payload, onFinish, onError)
} catch (err) {
msg.error = err
done(err.message, msg)
}
}
});
}
RED.nodes.registerType("NodeFactory", NodeFactoryFunctionality);
RED.httpAdmin.post("/NodeFactory/:id",
RED.auth.needsPermission("NodeFactory.write"),
(req, res) => {
var node = RED.nodes.getNode(req.params.id);
if (node != null) {
try {
if (req.body && req.params.id) {
var nde = RED.nodes.getNode(req.params.id)
if (nde && nde.type == "NodeFactory") {
node.receive(req.body);
}
} else {
res.sendStatus(404);
}
res.sendStatus(200);
} catch (err) {
res.sendStatus(500);
node.error("FlowHub: Submission failed: " +
err.toString())
}
} else {
res.sendStatus(404);
}
});
}