'use strict';
/**
* A framework for all $wider bundles but also a tracker for different modules of the same name being loaded by different packages of the same
*
* creates
* ```javascript
* global.$wider = {
* // container for other wider globals
* }
* ```
* @module @wider/registry
*/
import path from "path";
import fs from "fs-extra";
import Module from "module";
import Require from "@wider/utils_require";
import whereAmI from "@wider/utils_where-am-i";
fs.$moduleName = "fs-extra";
fs.$objectName = "fs";
path.$moduleName = "path";
const $moduleName = "@wider/registry";
const $objectName = $moduleName.split(/\//)[1];
let localRequire;
const root = "<<root>>";
const ROOT = Symbol(root); //moduleName for the root of the @wider cache
const modulesLoaded = {};
modulesLoaded[ROOT] = {
created: Date.now(),
objectName: ROOT,
path: "",
section: undefined,
type: "hierarchy"
};
const dictionary = "DICTIONARY";
const DICTIONARY = Symbol(dictionary); // if used in a bundle or the first entry of a merge then overrides the default logic on documentation
// makes a normally hidden non-enumerable member of global - we must always be the first to set global.$wider
const widerGlobal = global.$wider || {
CONFIG: {
$register: DICTIONARY,
customPaths: []
},
CONSTANTS: {
$register: DICTIONARY,
/*
ENGINE: "nodeJS", >> where-am-i > engine gives node not nodejs
IS_AT_SERVER: true, >> where-am-i > genre === server
*/
DELIMITER: "\uffff", // a general purpose delimiter
ENVIRONMENT: process.env.NODE_ENV || 'development', // could also be production or test or staging
INVALID: Symbol("invalid"), // a symbol to mark that a value is invalid
LANGUAGE: (process.env.LANG || "en_GB").split(/\./)[0],
MACHINE: process.env.COMPUTERNAME,
MACHINE_USER: process.env.USERDOMAIN + "\\" + process.env.USERNAME,
MISSING: Symbol("missing"), // a symbol to mark something required to be an object but is not being provided and that can be detected by `.isMissing()`
missing: () => {}, // a function with no return value to provide where a function is required but no action is required and that can be detected by `.isMissing()`
NOTHING: "\u2913\u2913", // pretty string for output to user when a field is blank - two enDashes - hyphen
RECOMPUTE: "++", // what a user types into a field to make the system see if there is a default value
SERVER_STARTED_ENGINE: Date.now(), // the dateSerial of the time the current cluster/thread started
UNDER_CONSTRUCTION: Symbol("UNDER_CONSTRUCTION")
},
DYNAMIC: {
$register: DICTIONARY,
loaded: [
DICTIONARY,
{
"registry": Date.now() // extends with moduleNames of things added to $wider]} - the NTH position will always be hhe absent or the same module name - sp eg if @wider/bundle_core-full is present it will always be `DYNAMIC[2]
}
]
}
};
if (!global.$wider)
Object.defineProperties(global, {
"$wider": {
get: () => {
return widerGlobal;
}
},
});
/**
* Extends `Error - with its additional properties and also logs the error to the console
* @property {String} message - the error message
* @property {NCName} code a code word for optional programmatic use by code that traps the error
* @property {id|Object} moduleName the name of the module affected. If provided as an object, then the object should have a `.moduleName` property
*/
class loggedError extends Error {
constructor(message, code, moduleName) {
super(`${$moduleName || 'anonymousModule'} - ${message}`);
this.code = (code || "UNSPECIFIED");
this.moduleName = moduleName.$moduleName || moduleName;
console.error(this);
}
}
loggedError.$moduleName = `${$moduleName}[throw loggedError]`;
/* if a section is used, make sure it is created if it does not yet exist */
function validateSection(section) {
if (section && !widerGlobal[section]) {
widerGlobal[section] = {};
modulesLoaded[section] = {
created: Date.now(),
objectName: section,
path: "",
section: undefined,
type: "section"
};
}
return section ? widerGlobal[section] : widerGlobal;
}
/** submit each entry of the dictionary
* @private
*/
function nameCleanerInner(value) {
return (value[1] || "").toUpperCase();
}
function nameCleaner(value) {
return value
.replace(/^[\.\/\.]*/, "")
.replace(/[\-\.]./g, nameCleanerInner)
.replace(/[\/\\]./g, nameCleanerInner);
}
/**
* Registers an object typically as it is being constructed via require or import. The purpose being to ensure that multiple packages within your runtime
* on a given cluster do not load the same module more than once - as can happen when a package is a dependency of several other packages. This function should *only*
* only be called from within the module level code of the module that is registering itself
*
* @param {String} [moduleName] - the name of the item to be registered. The string should be the same as used as might be used as id in `require`
* or `import`, or as used in `npm install`
*
* If moduleName is an object it is treated as a dictionary of registrations where the keyName is the **moduleName** and the associated value
* is an object with the properties ***value*** and ***objectName***. It may also contain ***section^^^ which if present will override the section parameter of the call to `.register()`
*
* If *moduleName* is absent then nothing is registered and the only action is to return the output Dictionary bundle
*
* @param {*} [value] - if present then the ***moduleName*** must be supplied and this ***value*** is added to the bundle.
*
* If absent, then the ***moduleName***, if provided, is registered in the dictionary and nothing is added to the bundle.
*
* If this is an object (class, function, json, plain object) then it should have a `$moduleName` static property which is the same as the parameter `moduleName`. It may also have a property **$objectName**
* - if this is a string then it must be the same as the **objectName** in this request
* - if this is the value of `registry.MERGE` then **moduleName** must already have been registered and the value shall be a dictionary of entries according to **section**, none of which may pre-exist
* @param {NCName} [objectName] if objectName is present, then ***value*** must be supplied and the value is stored in the response of this `@wider/registry`
* response under this name available for direct use by any other module without use of `require` or `import`. If ***objectName*** is not provided then it will
* be assumed to be the same as ***moduleName***
* @param {NCName} [section] if absent then the value is saved under ***objectName*** at the top level of the output Dictionary, otherwise this defines the section
* name in the dictionary under which the value is filed
*
* @returns {Dictionary} containing all registered items under their objectname and section.
* @throws {loggedError} an extension of the javascript `Error`` object. It is thrown when a registration is rejected
*/
function register(moduleName, value, objectName, section) {
switch (typeof moduleName) {
case "string":
if (!/^[\w_\-\d\@\/\.]+$/.test(moduleName))
throw new loggedError(`module name '${moduleName}' is not a valid simple module id`, "INVALID_MODULE_NAME", $moduleName);
else if (objectName && !/^\w[\w\d_]*$/)
throw new loggedError(`${moduleName} - Invalid format for name of objectName ${objectName}`, "BAD_OBJECTNAME", $moduleName);
else if (section && !/^\w[\w\d_]*$/)
throw new loggedError(`${moduleName} - Invalid format for name of objectName ${section}`, "BAD_SECTION", $moduleName);
else if (modulesLoaded[moduleName])
throw new loggedError(`${moduleName} - You have multiple packages attempting to load versions of the same moduleName`, "DUPLICATE_CALL", $moduleName);
else {
const target = validateSection(section);
// always add to modulesLoaded
objectName = objectName || (value && value.$objectName) || nameCleaner(moduleName);
modulesLoaded[moduleName] = {
created: Date.now(),
objectName: objectName
};
if (value !== undefined) {
if (target[objectName])
throw new loggedError(`Duplicate attempt to save member ${objectName} into $wider`, "DUPLICATE_GLOBAL_MEMBER", $moduleName);
else if (value.$moduleName != moduleName && value.$objectName != moduleName)
throw new loggedError(`The submitted object does not possess a $moduleName which is the same as the name of the registry name or a matching alias defined by $objectName `, "$MODULENAME_INVALID", $moduleName);
else {
Object.defineProperty(target, objectName, {
get() {
return value;
},
enumerable: true
});
Object.assign(modulesLoaded[moduleName], {
section: section,
path: (section ? section + "." : "") + objectName,
});
}
}
}
break;
//"undefined" :
default:
if (moduleName)
throw new loggedError(`Permitted types for the value of moduleName in CAll to register '${moduleName}]' must be a string or an array`, "MODULENAME_MISSING", register.$moduleName);
}
return widerGlobal;
}
register.$moduleName = `${$moduleName}[register]`;
/**
* Allow you to include large numbers of rarely used modules in your `$wider` object on a deferred loading scheme, so that your app's start up and run time does get filled with wasteful bloat. Your entries appear in `$wider`. Used exactly as `.bundle{}`, entries appear in `$wider` and are used in the same way. The only difference is that the module is not loaded/required unless and until it is actually used. Loading is always synchronous so as to honour your existing code without change and occurs just in time and will occur if you destructure the item from $wider or refer to it directly.
*
* As with other items in the registry, you may only load a given module once, but you can change your program to place items either in `bundle()` and `defer()`. Apart from changing the timing of loading into memory, there is no other impact.
*
* You can put `.merge()` items within a `defer()` object but that will have the effect of un-deferring the object.
*
* When debugging, viewing the container of the deferral, this will cause the all deferrals to be loaded/required.
*
* If a module does not self register, then this function will register it for you.
* @param {Array} entries one entry for each member whose loading is to be deferred until the first time of use. Each entry may be either a string module name or an object containing at least the `.$moduleName` property and optionally `.$objectName`. `$moduleName` that are relative paths are not supported, if from local files always used resolved paths. Other properties of the object are ignored
* @param {NCName} [section] if provided then the name of the section where the entries are to be lodged
*/
function defer(entries, section) {
if (!localRequire) {
localRequire = (widerGlobal.Require) ? new widerGlobal.Require(new Module()) : require;
if (!Array.isArray(entries))
entries = [entries];
const target = validateSection(section);
entries.forEach((deferItem, index) => {
if (typeof deferItem === "string")
deferItem = {
$moduleName: deferItem
};
else if (typeof deferItem != "object" || !deferItem.$moduleName)
throw new loggedError(`Entry submitted for '${section || deferItem.$moduleName}[${index}]' is neither a string moduleName nor does not have property $moduleName`, "MODULENAME_INVALID", defer.$moduleName);
if (deferItem.$moduleName[0] === ".")
throw new loggedError(`Relative path for moduleName not supported`, "MODULENAME_RELATIVE_NOT_SUPPORTED", defer.$moduleName);
const objectName = deferItem.$objectName ||
nameCleaner(deferItem.$moduleName);
if (target[objectName] || modulesLoaded[deferItem.$moduleName]) {
// with defer it is not always possible for you program logic readily to check if another program path has already deferred an item - for to check if it were so would be to un-defer it - so provided the new defer is compatible with a previous registration, we dont do anything - allowing the existing entry to cause the impact desired - otherwise we throw an error
const precursor = modulesLoaded[deferItem.$moduleName]||{};
if (precursor.objectName !== objectName ||
precursor.section != section
)
throw new loggedError(`Duplicate attempt to save member ${objectName} into $wider`, "DUPLICATE_GLOBAL_MEMBER", defer.$moduleName);
} else
modulesLoaded[deferItem.$moduleName] = {
created: Date.now(),
objectName: objectName,
section: section,
path: (section ? section + "." : "") + objectName,
type: "deferDormant"
};
let value;
Object.defineProperty(target, objectName, {
get() {
if (value === undefined)
try {
value = localRequire(deferItem.$moduleName);
modulesLoaded[deferItem.$moduleName].type = "deferActivated";
}
catch (err) {
value = err;
modulesLoaded[deferItem.$moduleName].type = "deferFailed";
}
if (modulesLoaded[deferItem.$moduleName].type === "deferFailed")
throw value;
else
return value;
},
enumerable: true
});
});
}
}
defer.$moduleName = `${$moduleName}[defer]`;
/**
* Merge one or more objects that **do not register themselves**, for example because they are a third party package
*
* Care needs to be taken when you act as proxy to register other packages in case different parts of your solution try to register the same third party package. If the risks remain despite your care, check in `global.$wider` before calling register.
* @param {Array|Object} entries A single object will be treated as if it were the first member of an Array. The array is a collection of objects, each to be registered individually. Each object must contain the property `$moduleName`, and optionally may contain `$objectName`, both as defined in `registry.register()`. If you are registering as proxy, you will need to add these properties to each object before registering them.
*
* If `.$moduleName` or `.$objectName` are missing, an attempted will be made to generate these and an error will be thrown if usable values cannot be created.
* @param {NCName} [section] if provided then the name of the section where the entries are to be lodged
* @returns {Dictionary} the $wider object that has been extended by the given entries
* @throws {loggedError} if any of the modules self registers or if it has not been possible to determine valid moduleName or objectName for any of the modules
*/
function bundle(entries, section) {
if (!Array.isArray(entries))
entries = [entries];
/*if (section) {
if (section && typeof section != "string")
throw new loggedError("bundle - Section name missing or not a string", "SECTION_MISSING", register.$moduleName);
// create the section if it does not yet exist
if (false && !modulesLoaded[section])
register(section, {
$moduleName: section
}, section);
}*/
// submit each entry of the dictionary
entries.forEach((element, index) => {
const objectName = element.$objectName || element.$moduleName || element.name;
if (!objectName)
throw new loggedError(`Entry submitted for '${section || root}[${index}]' does not have property $moduleName`, "MODULENAME_MISSING", bundle.$moduleName);
else {
if (!element.$moduleName)
element.$moduleName = objectName;
if (!element.$objectName)
element.$objectName = nameCleaner(objectName);
}
register(element.$moduleName, element, element.$objectName || element.$moduleName, element.$registrySection || section);
});
return widerGlobal;
}
bundle.$moduleName = `${register.$moduleName}[bundle]`;
/**
* Merges designated properties in a pre-existing registered object. Often used to add CONSTANTS or CONFIG values to an existing container
* @param {String} [moduleName] as per `registry.register()` - if the name is null, then the entries are added at the root of `$wider` or the section if provided
* @param {Dictionary} entries properties and methods to be added to the named `$moduleName`
* @param {NCName} [mergeId] optional name of the merge for documentary purposes
* @returns {Dictionary} the $wider object that has been extended by the given entries
* @throws {loggedError} if either the existing module does not exist or if you try to revise an entry that already exists
*/
function merge(moduleName, entries, mergeId) {
const loaded = modulesLoaded[moduleName];
const section = loaded.section;
if (!loaded)
throw new loggedError(`${$moduleName} not found to merge into`, "INVALID_PARAMS", merge.$moduleName);
else {
let target = (section ? widerGlobal[section] : widerGlobal);
if (loaded.objectName !== ROOT)
target = target[loaded.objectName];
// register the merge
if (!loaded.extensions)
loaded.extensions = [];
loaded.extensions.push({
name: mergeId || "Unnamed " + (loaded.extensions.length + 1),
created: Date.now()
});
//register the bundle entries
for (const key in entries) {
if (Object.hasOwnProperty.call(entries, key)) {
if (target[key])
throw new loggedError("Attempting to modify existing entry not allowed during merge", "MERGE_DUPLICATE", merge.$moduleName);
Object.defineProperty(target, key, {
/* jshint -W083 */
get() {
return entries[key];
},
/* jshint +W083 */
enumerable: true
});
}
}
}
}
merge.$moduleName = `${register.$moduleName}[merge]`;
/**
* Relate the given moduleName to the entry in $wider
* @param {String} moduleName the module name to be found
* @returns {*} the result of the search being undefined if the moduleName is unknown, or the element within $wider that was registered with the given moduleName
*/
function find(moduleName) {
try {
let result = widerGlobal;
modulesLoaded[moduleName].path.split(/\./).forEach(element => {
result = result[element];
});
return result;
} catch (e) {}
}
/**
* Generate a report of the items that have been loaded and how long into the run session it was
* when they are loaded. NB not all loaded items are published.
* @returns {String} a tabular report of items loaded
*/
function loaded() {
const result = ["\n## Modules loaded in chronological order of loading\n"];
const paddingName = 40;
const paddingWait = 10;
const paddingPath = 30;
const started = (modulesLoaded.CONFIG || modulesLoaded[$moduleName]).created;
for (const key in modulesLoaded) {
if (Object.hasOwnProperty.call(modulesLoaded, key)) {
const element = modulesLoaded[key];
const extensions = [];
(loaded.extensions || []).forEach(element => {
extensions.push(element);
});
if (extensions.length)
extensions.unshift("");
result.push(key.padEnd(paddingName, " ") + " " +
((element.created - started) / 1000).toFixed(3).padStart(paddingWait, " ") + " " +
(element.path || "").padEnd(paddingPath) + " " +
(element.type ? `<<${element.type}>>` : "") +
extensions.join("\n extension : ")
);
}
}
return result.join("\n");
}
/**
* A report of the structure in the object that is published via the common object
* @returns {String} a tabular report of items published
*/
function published() {
const result = ["\n## Modules published in global.$wider alphabetical order\n"];
const paddingName = 40;
const pusher = (key, element2) => {
const elementStr = (/^(string|number)$/.test(typeof element2) || Object.hasOwnProperty.call(element2 || {}, "toString")) ? element2 + "" : "";
result.push(
key.padEnd(paddingName, " ") + " " +
(typeof element2) +
(elementStr ?
` : ${elementStr.substr(0, 40)} ${(elementStr.length > 40) ? "\u2026" : " "}` :
"")
);
if (element2 && typeof element2 === "object") {
if (Array.isArray(element2) && element2[0] === DICTIONARY) {
element2.forEach((element2, index) => {
pusher(`${key}.[${index}]`, element2);
});
}
} else if (element2[DICTIONARY]) {
for (const key2 in element2) {
if (Object.hasOwnProperty.call(element2, key2)) {
driller(element2[key2], key + "." + key2);
}
}
}
};
const driller = (object, prefix = "") => {
for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
const target = widerGlobal[key];
const element = modulesLoaded[target.$moduleName];
if (!element) {
result.push(prefix, `${key.padEnd( paddingName, " " )} <<${typeof target}>>`);
if (target.$register && (target.$register === DICTIONARY || target.$register === "DICTIONARY"))
for (const key2 in target) {
if (Object.hasOwnProperty.call(target, key2))
pusher((key + "." + key2), target[key2], prefix);
}
} else {
result.push(prefix, key.padEnd(paddingName, " ") + " " +
(element.path || ""));
if (target.$moduleName)
for (const key2 in target) {
if (Object.hasOwnProperty.call(target, key2))
pusher((key + "." + key2), target[key2], prefix);
}
}
}
}
};
driller(widerGlobal);
return result.sort().join("\n");
}
// register myself
bundle(
[{
$moduleName: $moduleName,
$objectName: $objectName,
describe_loaded: loaded,
describe_published: published,
register: register,
loggedError: loggedError,
bundle: bundle,
merge: merge,
defer: defer,
find: find,
DICTIONARY: DICTIONARY,
ROOT: ROOT
}]
);
// there is a risk that this module itself is being multiple loaded so we have to use the version in global even if we didn't create it
//register($moduleName); // to stop ourselves being loaded multiple times
export default global.$wider;
global.$wider.registry.bundle([Module, Require, fs, path, whereAmI]);