helpers/jrequire.js

/**
 * @module helpers/jrequire
 * @author jesse reichler <mouser@donationcoder.com>
 * @copyright 11/16/19
 * @description
 * Service locator to make module dependencies more flexible..
 * The basic idea is that for a framework, one wants to be able to decouple dependencies so that one could swap in a replacement module easily.
 * For example, imagine a framework with different classes for users, resources, etc., which are tightly coupled and co-dependent, where
 * one would like to use the framework but swap out a class for a replacement (derived) class.
 * The approach here is to use a singleton centralized registry of modules used by a framework, and allow an initial setup to change the mappings from names to modules.
 * Use as a replacement for require, after modules/paths have been registered.
 * This is meant to be used as a singleton -- a global central requirement loader for our classes.
 * It is only meant to be used for modules that one might want to swap out with replacements when using a given framework.
 *
 * @see <a href="https://www.amazon.com/Node-js-Design-Patterns-server-side-applications/dp/1785885588">NodeJs Patterns book</a>
 */

"use strict";





//---------------------------------------------------------------------------
// module global
const requires = {};
const requirePaths = {};
const plugins = {};
const addonModules = {};
const addonModuleCategories = {};

// we normally used deferred loading, which is better if we might replace a path before it's needed; this can be overridden with call to setDeferredLoading(boolean)
let flagDeferredLoading = true;
//---------------------------------------------------------------------------






//---------------------------------------------------------------------------
/**
 * Register a new module dependency by its path.
 * @example jrequire.registerPath("helper", require.path("accessories/helper"));
 * @example jrequire.registerPath("coremod", "coremodule"); // where require(path) will resolve
 *
 * @param {string} name - the same to store the requirement under (can include / . etc)
 * @param {string} requirePath - full path to pass to the require module
 * ##### Notes
 *  * the requirePath can be a full path obtained via require.resolve() OR a path relative to where the jrequire.js module is, OR an npm registered module (might be useful for npm registered plugins)
 */
function registerPath(name, requirePath) {

	// save the path for this name
	requirePaths[name] = requirePath;

	if (require[name]) {
		// already exists, so it is being replaced for subsequent jrequire(name)
		console.log("Warning: In jrequire.registerPath, replacing requirement of " + name + ".");
		// delete any current cached require
		delete require[name];
	}

	if (!flagDeferredLoading) {
		// we normally defer loading, especially useful if we might replace requirement modules after a default init; but for testing we might do it now
		// console.log("In registerPath, immediate load of: " + name);
		requires[name] = require(fixRequirePath(requirePath));
	}
}


/**
 * Register a new module dependency by directly passing in the result of require()
 * @example jrequire.registerRequire("helper", require("accessories/helper"));
 *
 * @param {string} name - the same to store the requirement under (can include / . etc)
 * @param {object} requireResult - result of a require statement on a module
 */
function registerRequire(name, requireResult) {
	requires[name] = requireResult;
	requirePaths[name] = "path unknown";
}
//---------------------------------------------------------------------------

































//---------------------------------------------------------------------------
function registerAddonModule(collectionName, name, obj) {
	// register a new generic thing

	// console.log("in registerAddonModule with collection = " + collectionName + ", name = " + pluginName + " from category '" + pluginCategory + "' at path " + pluginPath);

	// add it to our registerPath normal registry
	const addonNameRegistered = calcAddonRegisteredName(collectionName, name);
	registerPath(addonNameRegistered, fixRequirePath(obj.path));

	// add it to our array of addons by collectionName
	// create category if it doesn't exist
	if (!addonModules[collectionName]) {
		addonModules[collectionName] = {};
	}
	addonModules[collectionName][name] = {
		...obj,
	};

	// and now we also store it in category tree under the collectionName
	let objCat = obj.category;
	if (!objCat) {
		objCat = "_UNCATEGORIZED_";
	}
	if (objCat) {
		if (!addonModuleCategories[collectionName]) {
			addonModuleCategories[collectionName] = {};
		}
		if (!addonModuleCategories[collectionName][objCat]) {
			addonModuleCategories[collectionName][objCat] = {};
		}
		addonModuleCategories[collectionName][objCat][name] = {
			...obj,
		};
	}
}


function getAllAddonModulesForCollectionName(collectionName) {
	return addonModules[collectionName];
}


function getAllAddonCategoriesForCollectionName(collectionName) {
	const catObjs = addonModuleCategories[collectionName];
	if (catObjs) {
		return catObjs;
	}
	return {};
}


function getAllAddonModulesInCategoryForCollectionName(collectionName, category) {
	const catObjs = addonModuleCategories[collectionName];
	if (catObjs[category]) {
		return catObjs[category];
	}
	return {};
}


function requireAddonModule(collectionName, name) {
	const addonNameRegistered = calcAddonRegisteredName(collectionName, name);
	return jrequire(addonNameRegistered);
}


function calcAddonRegisteredName(collectionName, name) {
	return collectionName + "/" + name;
}
//---------------------------------------------------------------------------























//---------------------------------------------------------------------------
/**
 * Substitute for the normal cached require() statement
 *
 * @param {string} name - name used to register previously
 * @returns result of previous require statement
 */
function jrequire(name) {
	// console.log("In requires for " + name);
	if (requires[name]) {
		// console.log("Returning cached1");
		return requires[name];
	}
	if (requirePaths[name]) {
		// deferred, so require it now
		// console.log("Deferred loading of module: " + name);
		const obj = require(fixRequirePath(requirePaths[name]));
		// console.log("Returning cached2");
		if (obj) {
			requires[name] = obj;
			return obj;
		}
	}

	// not found
	let emsg = "In jrequire: The following module was requested to be loaded but was not registered with jrequire: " + name;
	if (Object.keys(requires).length === 0 && Object.keys(requirePaths).length === 0) {
		emsg += ".  ATTENTION: No modules were registered with jrequire -- did you forget to register your module paths?";
	}
	throw new Error(emsg);
}



/**
 * Private function that we could use to do some special replacements, like replacing %APPROOT% with root path of project, etc.
 *
 * @param {string} path
 * @returns path with any special replacements
 */
function fixRequirePath(path) {
	// lets also try resolving it now
	path = require.resolve(path);
	return path;
}
//---------------------------------------------------------------------------






//---------------------------------------------------------------------------
/**
 * Just return an object with debugging information suitable for display
 *
 * @returns debug object with info on named and paths
 */
function calcDebugInfo() {
	return {
		requirePaths,
		plugins,
	};
}


/**
 * Set the deferred loading flag
 *
 * @param {boolean} val
 */
function setDeferredLoading(val) {
	flagDeferredLoading = val;
}
//---------------------------------------------------------------------------










//---------------------------------------------------------------------------
function checkCircularRequireFailures() {
	// ATTN: THIS DOES NOT WORK
	// walk all names and see if any are empty
	console.log("IN checkCircularRequireFailures.");
	let obj;
	for (const rname in requires) {
		console.log("CHECKING VALIDITY OF REQUIRES NAME: " + rname + " TYPE = " + typeof requires[rname]);
		obj = requires[rname];
		if (typeof obj === "object") {
			if (!obj || (Object.entries(obj).length === 0 && obj.constructor === Object)) {
				console.log("LOOKS BAD.");
			} else {
				console.log("LEN = " + Object.entries(obj).length);
			}
		}
	}
}
//---------------------------------------------------------------------------











//---------------------------------------------------------------------------
// set these on top of the main function for access to them
// this is an unusual nodejs pattern that makes it easy to run the main function, and possible to call others
// in this way we can export just the one main function (jrequire), but the other functions can be invoked by doing jrequire.registerPath etc...
jrequire.registerPath = registerPath;
jrequire.registerRequire = registerRequire;
jrequire.calcDebugInfo = calcDebugInfo;
jrequire.setDeferredLoading = setDeferredLoading;
//
jrequire.registerAddonModule = registerAddonModule;
jrequire.getAllAddonModulesForCollectionName = getAllAddonModulesForCollectionName;
jrequire.getAllAddonCategoriesForCollectionName = getAllAddonCategoriesForCollectionName;
jrequire.getAllAddonModulesInCategoryForCollectionName = getAllAddonModulesInCategoryForCollectionName;
//
jrequire.requireAddonModule = requireAddonModule;
//
jrequire.checkCircularRequireFailures = checkCircularRequireFailures;
//---------------------------------------------------------------------------




// our sole export is the main function
module.exports = jrequire;