@ralphwetzel/node-red-mcu-plugin
Version:
Plugin to integrate Node-RED MCU Edition into the Node-RED Editor
641 lines (519 loc) • 20.6 kB
JavaScript
const clone = require("clone")
const path = require("path");
const fs = require("fs-extra");
// const pkgContents = require('@npmcli/installed-package-contents')
// RDW 20220821: https://github.com/ai/nanoid/issues/364
// There's a BREAKING CHANGE @nanoid.v4 supporting only ES6. Thus we stick to v<4!
const { customAlphabet } = require('nanoid');
const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const nanoid = customAlphabet(alphabet, 16);
// https://github.com/stefanpenner/resolve-package-path
const resolve_package_path = require('resolve-package-path')
class manifest_builder {
// supported options: {
// preload: Add module to preload list
// }
constructor(library, mcu_modules_path, options) {
if (!library || !mcu_modules_path) {
throw ("manifest_builder: Mandatory constructor arguments missing.")
}
this.nodes_library = library
this.mcu_modules_path = mcu_modules_path;
this.manifest = {};
this.resolver_paths = [];
this.options = options ?? {};
this.initialize();
}
initialize(init) {
if (typeof(init) === "string") {
this.manifest = JSON.parse(init);
} else if (typeof(init) === "object"){
this.manifest = clone(init);
}
return true;
}
get_manifest_of_module(module, optional_path, node_type) {
// console.log(`Trying to find 'manifest.json' for module "${module}":`);
if (typeof(optional_path) !== "string") {
throw 'typeof(optional_path) has to be "string"!'
}
let package_path;
for (let i=0; i<this.resolver_paths.length; i+=1) {
package_path = resolve_package_path(module, this.resolver_paths[i]);
if (package_path) {
break;
}
}
if (!package_path) {
throw `Unable to resolve path for module "${module}".`;
}
let module_path = path.dirname(package_path);
let paths_to_check = [];
// If a node_type is given: Let's ask the module first!
if (node_type) {
let pckge = require(package_path);
if (!pckge) {
// Shall never happen!
return;
}
// check if there's a node-red section in package.json?
let nds = pckge["node-red"]?.nodes;
if (nds) {
let _entry;
// if so, check if there's an entry declared for this node type!
if (nds[node_type]) {
_entry = nds[node_type];
} else {
let ndsk = Object.keys(nds);
// if there's only one entry we assume this is the correct one!
if (ndsk.length === 1) {
_entry = nds[ndsk[0]];
}
}
if (_entry) {
// try to check for the given path
let _path = path.join(module_path, _entry);
try {
if (fs.lstatSync(_path).isFile()) {
_path = path.dirname(_path);
}
} catch {
// path does not exist?
_path = undefined;
}
if (_path && _path.length > 0) {
// we wish to have the manifest.json in the /mcu subdirectory
paths_to_check.push(path.join(_path, "mcu"));
// ... but accept it as well in the root
paths_to_check.push(_path);
}
}
}
}
// A very convenient situation: there is a manifest for this node type!
paths_to_check.push(path.join(module_path, "mcu"))
// This is deprecated: accept as well a "manifest.json" in the nodes root directory
paths_to_check.push(module_path);
// Next best: We've a manifest template provided predefined in our mcu_modules folder
let scoped_module = module.split("/");
paths_to_check.push(path.join(this.mcu_modules_path, ...scoped_module));
// Perhaps there's already a manifest.json in the (optionally) provided path
if (optional_path) {
paths_to_check.push(path.join(optional_path, ...scoped_module))
}
for (let i=0; i<paths_to_check.length; i+=1) {
let p = path.join(paths_to_check[i], "manifest.json");
if (fs.existsSync(p)) {
let mnfst = require(p);
if (mnfst["//"]?.template !== undefined) {
// don't accept templates
continue;
} else {
// console.log(`"manifest.json" found @ ${p}`);
return p;
}
}
}
return;
}
include_manifest(path) {
if (!this.manifest) {
throw "Missing base manifest @ include_manifest"
}
if (!this.manifest.include) {
this.manifest.include = [];
}
if (this.manifest.include.indexOf(path) < 0) {
this.manifest.include.push(path);
return true;
}
return false;
}
create_manifests_for_module(module, destination, node_type) {
// console.log(`Creating 'manifest.json' for module "${module}":`);
let package_path;
for (let i=0; i<this.resolver_paths.length; i+=1) {
package_path = resolve_package_path(module, this.resolver_paths[i]);
if (package_path) {
break;
}
}
if (!package_path) {
throw `Unable to resolve path for module "${module}".`;
}
let pckge = require(package_path);
// ToDo: Send sth to console
if (!pckge) return;
// console.log(pckge);
// split the module name to get its scope
let scoped_module = module.split("/");
// check if there is a template in mcu_nodes
let template_path = path.join(this.mcu_modules_path, ...scoped_module, "manifest.json");
let mnfst_template;
let template;
if (fs.existsSync(template_path)) {
mnfst_template = require(template_path);
template = mnfst_template["//"]?.template;
if (template === undefined) {
// sorry... this is not a template!
mnfst_template = undefined;
}
}
// console.log(mnfst_template);
// This is the name of the module that we need to make available
// We could use module here as well ... ??
let _module = pckge.name;
let _file;
// #0: if we got a node_type defined:
if (node_type) {
// check if there's a node-red section in package.json?
let nds = pckge["node-red"]?.nodes;
if (nds) {
// if so, check if there's a file declared for this node type!
if (nds[node_type]) {
_file = nds[node_type];
} else {
let ndsk = Object.keys(nds);
// if there's only one entry we assume this is the correct file!
if (ndsk.length === 1) {
_file = nds[ndsk[0]];
}
}
}
}
// *****
// ToDo & Attention!
// There's an edge case when there're several node_types defined in one module
// & we try to create a (separate) manifest.json for more than one of them.
// This will - most probably - not work as intended & lead to a runtime error
// ... as only the first manifest.json will be generated.
// *****
// #1: exports.import (as we prefer to be "import"ed modules)
// #2: exports.require (despite this will create issues...)
// #2: main - which is most likely == exports.require
// default acc. doc: "./index.js" if main not defined
_file = _file
?? pckge.exports?.import
?? pckge.exports?.require
?? pckge.main
?? "./index.js";
// Few modules define more than one entry point.
// In this case, try to get the 'default' entry
if (_file instanceof Object) {
_file = _file.default;
}
// No _file found
if (_file === undefined) {
throw Error(`Could not determine entry point for module ${module}.`)
}
// _file was defined w/ "". Treat this as "there's no entry point"!
// This may be the case for "@types" files.
if (_file === "") {
console.log(`${_module}: Skipped as package entry point voided.`)
return;
}
let _path = path.resolve(path.dirname(package_path), _file);
if (fs.pathExistsSync(_path)) {
// check if it is a dir
if (fs.lstatSync(_path).isDirectory()) {
_path = path.resolve(_path, "./index.js")
}
} else {
if (path.extname(_path).length < 1) {
_path += ".js";
}
}
if (fs.pathExistsSync(_path) !== true) {
console.log("Path not found: " + _path);
return;
}
// prepare the dir for this manifest
let mnfst_path = path.join(destination, ...scoped_module, "manifest.json");
fs.ensureDirSync(path.dirname(mnfst_path));
/* template: {
* "modules": ['name of module to be resolved & included', 'another name', '* == all']
* }
*/
function check_template(section, key) {
if (!template) return true;
let keys = template[section] ?? []
for (let i = 0; i < keys.length; i++) {
if (keys[i] === key || keys[i] == "*") {
return true;
}
}
return false;
}
// make module path & create symlink if necessary
if (check_template("modules", _module)) {
let _ext = "";
let _p = _path
let _pp;
let _name;
do {
_name = _pp?.name ?? "";
_pp = path.parse(_p)
// console.log(_pp);
_ext = _pp.ext + _ext;
_p = _p.slice(0, -_ext.length);
} while (_pp.ext !== "")
// Moddable will only resolve ".js" files
// In case the extension is sth else, create a symlink
if (_ext !== ".js") {
let _link;
do {
_link = `${_name}-${_ext.replace(/\./g, "")}-${nanoid()}.js`
_link = path.join(path.dirname(mnfst_path), _link)
} while (fs.existsSync(_link));
fs.symlinkSync(_path, _link);
_path = _link;
}
}
let mnfst = {
"//": {
"***": "https://github.com/ralphwetzel/node-red-mcu-plugin",
"npm": `${module}`,
"xs": "manifest.json",
"@": `${new Date(Date.now()).toJSON()}`,
"ref": "https://github.com/Moddable-OpenSource/moddable"
},
"build": {},
"include": [],
"modules": {
"*": [],
}
}
let bldr = new manifest_builder(this.nodes_library, this.mcu_modules_path, this.options);
// console.log(bldr);
if (mnfst_template) {
// if we don't clone here, we'll get the modified mnfst @ the next require call!
let mt = clone(mnfst_template)
// this eliminates the "template" property of mt/mnfst_template!
mt["//"] = clone(mnfst["//"]);
// to be sure...
mt.build = mt.build || {};
mt.include = mt.include || [];
mt.modules = mt.modules || { "*": [] };
bldr.initialize(mt);
} else {
bldr.initialize(clone(mnfst));
}
bldr.resolver_paths = this.resolver_paths;
let _MCUMODULES = false
if (check_template("build", "MCUMODULES")){
// first: define MCUMODULES
bldr.add_build("MCUMODULES", this.mcu_modules_path);
_MCUMODULES = true;
}
if (check_template("build", "REDNODES")){
// resolve core nodes directory => "@node-red/nodes"
for (let i=0; i<this.resolver_paths.length; i+=1) {
let pp = resolve_package_path("@node-red/nodes", this.resolver_paths[i]);
if (pp) {
bldr.add_build("REDNODES", path.dirname(pp));
}
}
}
if (check_template("include", "require")){
// Make "require" available
let _require = _MCUMODULES ? "$(MCUMODULES)" : this.mcu_modules_path
bldr.include_manifest(`${_require}/require/manifest.json`);
}
if (check_template("modules", _module)) {
// explicitely add with the import name and the path (or symlink)
let _pp = path.parse(_path);
if (_pp.ext.length > 0) {
_path = _path.slice(0, -_pp.ext.length);
}
bldr.add_module(_path, _module);
if (this.options?.preload === true) {
bldr.add_preload(_module);
}
}
// Write this initial manifest to disc
// to ensure that it's found on further iterations
// thus to stop the iteration!
// console.log(mnfst_path);
// console.log(bldr.get());
fs.writeFileSync(mnfst_path, bldr.get(), (err) => {
if (err) {
throw err;
}
});
let changed = false;
// console.log(`Checking dependencies of module "${module}":`);
let deps = pckge.dependencies;
if (deps) {
for (let key in deps) {
if (check_template("include", key)) {
let mnfst = this.get_manifest_of_module(key, destination);
if (mnfst && typeof (mnfst) === "string") {
bldr.include_manifest(mnfst);
changed = true;
continue;
}
mnfst = this.create_manifests_for_module(key, destination);
if (mnfst && typeof(mnfst) === "string") {
bldr.include_manifest(mnfst);
changed = true;
}
}
}
}
if (changed === true) {
fs.ensureDirSync(path.dirname(mnfst_path));
fs.writeFileSync(mnfst_path, bldr.get(), (err) => {
if (err) {
throw err;
}
});
}
return mnfst_path;
}
from_template(module, destination) {
// ToDo: Merge w/ outer functinality
function check_template(t, section, key) {
if (!t) return false;
let keys = t[section] ?? []
for (let i = 0; i < keys.length; i++) {
if (keys[i] === key || keys[i] == "*") {
return true;
}
}
return false;
}
let self = this;
// split the module name to get its scope
let scoped_module = module.split("/");
// prepare the dir for this manifest
let mnfst_path = path.join(destination, ...scoped_module, "manifest.json");
fs.ensureDirSync(path.dirname(mnfst_path));
// check if there is a template in mcu_nodes
let template_path = path.join(self.mcu_modules_path, ...scoped_module, "manifest.json");
if (!fs.existsSync(template_path))
return;
let mnfst_template = require(template_path);
let template = mnfst_template["//"]?.template;
if (template === undefined) {
// sorry... this is not a template!
return;
}
let mt = clone(mnfst_template);
delete mt["//"].template;
let bldr = new manifest_builder(self.nodes_library, self.mcu_modules_path, this.options);
bldr.initialize(mt);
let _MCUMODULES = false
if (check_template(template, "build", "MCUMODULES")){
// first: define MCUMODULES
bldr.add_build("MCUMODULES", self.mcu_modules_path);
_MCUMODULES = true;
}
if (check_template(template, "build", "REDNODES")){
// resolve core nodes directory => "@node-red/nodes"
for (let i=0; i<this.resolver_paths.length; i+=1) {
let pp = resolve_package_path("@node-red/nodes", self.resolver_paths[i]);
if (pp) {
bldr.add_build("REDNODES", path.dirname(pp));
}
}
}
if (check_template(template, "build", "MCUROOT")){
let rmp = path.resolve(__dirname, "./../node-red-mcu");
manifest.add_build("MCUROOT", rmp);
}
fs.writeFileSync(mnfst_path, bldr.get(), (err) => {
if (err) {
throw err;
}
});
let changed = false;
let deps = template.include ?? [];
for (let key of deps) {
let mnfst = this.get_manifest_of_module(key, destination);
if (mnfst && typeof (mnfst) === "string") {
bldr.include_manifest(mnfst);
changed = true;
continue;
}
mnfst = this.create_manifests_for_module(key, destination);
if (mnfst && typeof(mnfst) === "string") {
bldr.include_manifest(mnfst);
changed = true;
}
}
if (changed === true) {
fs.ensureDirSync(path.dirname(mnfst_path));
fs.writeFileSync(mnfst_path, bldr.get(), (err) => {
if (err) {
throw err;
}
});
}
for (let file of template.copy ?? []) {
let src = path.resolve(path.dirname(template_path), file);
let to = path.resolve(path.dirname(mnfst_path), file);
fs.copyFileSync(src, to)
}
return mnfst_path;
}
add_build(key, value) {
if (!this.manifest.build) {
this.manifest.build = {}
}
this.manifest.build[key] = value;
}
add_module(_path, key) {
key = key ?? "*";
if (typeof(key) !== "string") throw("typeof(key) must be string.")
if (!this.manifest) {
throw "Missing manifest @ add_module"
}
if (!this.manifest.modules) {
this.manifest.modules = {
"*": [],
"~": []
};
}
if (!this.manifest.modules[key]) {
this.manifest.modules[key] = _path;
return true;
}
let mms = this.manifest.modules[key];
if (Array.isArray(mms)) {
if (mms.indexOf(_path) < 0) {
mms.push(_path);
return true;
}
} else if (_path !== mms) {
this.manifest.modules[key] = [ mms, _path]
return true;
}
return false;
}
add_preload(module) {
if (!this.manifest.preload) {
this.manifest.preload = []
}
if (this.manifest.preload.indexOf(module) < 0) {
this.manifest.preload.push(module);
}
}
// create_manifests_from_package
get() {
return JSON.stringify(this.manifest, null, " ");
}
add(object, key) {
// this.manifest[key] ??= {}; // node 15+
if (!this.manifest[key]) {
this.manifest[key] = {};
}
let slot = this.manifest[key];
let obj = clone(object);
for (let k in obj) {
slot[k] = obj[k];
}
}
}
module.exports = {
builder: manifest_builder
}