wj-config
Version:
Javascript configuration module for NodeJS and browser frameworks such as React that works like ASP.net configuration where data sources are specified (usually JSON files) and environment variables can contribute/overwrite values by following a naming con
146 lines (145 loc) • 5.65 kB
JavaScript
import { forEachProperty, isArray, isConfigNode } from "./helpers.js";
const noop = (x) => '';
const rootPathFn = '_rootPath';
const rootUrlObjectProps = ['host', 'rootPath'];
const rootUrlObjectPropsForBrowser = ['host', 'rootPath', 'scheme', 'port'];
function buildUrlImpl(path, routeValues, routeRegex, queryString) {
let routeValuesFn = undefined;
let index = 0;
if (routeValues) {
if (typeof routeValues === 'function') {
routeValuesFn = routeValues;
}
else if (isArray(routeValues)) {
routeValuesFn = n => routeValues[index++];
}
else {
routeValuesFn = n => routeValues[n];
}
}
const rp = (this[rootPathFn] ?? noop)();
let url = `${rp}${(path ?? '')}`;
// Replace any replaceable route values.
if (routeRegex && routeValuesFn && routeRegex.test(url)) {
url = url.replace(routeRegex, (m, g1) => encodeURIComponent((routeValuesFn ?? noop)(g1)));
}
// Add query string values, if provided.
if (queryString) {
const qmPos = url.indexOf('?');
if (qmPos < 0) {
url += '?';
}
else if (url.length - qmPos - 1) {
url += '&';
}
let qsValue;
if (typeof queryString === 'function') {
qsValue = queryString();
}
else {
qsValue = queryString;
}
if (typeof qsValue === 'string') {
// Prepared query string. Just add to the url.
url += qsValue;
}
else {
let qs = '';
forEachProperty(qsValue, (key, value) => {
qs += `${encodeURIComponent(key)}=${encodeURIComponent(value)}&`;
});
if (qs.length > 0) {
url += qs.substring(0, qs.length - 1);
}
}
}
return url;
}
/**
* Builds a relative, absolute or full URL using the information found in the root node and taking into account if the
* code is running in the browser.
* @param this URL root node holding the URL information.
* @param isBrowser A Boolean value that indicates if code is running in the browser.
* @returns The relative, absolute or full URL, product of the information in the root node.
*/
function parentRootPath(isBrowser) {
if ((!this.host && !isBrowser) || (!this.host && !this.port && !this.scheme)) {
// When no host outside the browser, or no host, port or scheme in the browser,
// build a relative URL starting with the root path.
return this.rootPath ?? '';
}
return `${(this.scheme ?? 'http')}://${(this.host ?? globalThis.window?.location?.hostname ?? '')}${(this.port ? `:${this.port}` : '')}${(this.rootPath ?? '')}`;
}
/**
* Builds a relative, absolute or full URL by appending path information to the generated URL from the parent node.
* @param this URL node holding path information.
* @param parent Parent node.
* @returns The relative, absolute or full URL built by appending path information to the parent's generated URL.
*/
function pathRootPath(parent) {
const rp = (parent[rootPathFn] ?? noop)();
return `${rp}${(this.rootPath ?? '')}`;
}
function makeWsUrlFunctions(ws, routeValuesRegExp, isBrowser, parent) {
if (!ws) {
return;
}
if (!isConfigNode(ws)) {
throw new Error(`Cannot operate on a non-object value (provided value is of type ${typeof ws}).`);
}
const shouldConvert = (name) => {
// Any property in this list or whose name starts with underscore (_) are skipped.
const exceptions = [
'host',
'port',
'scheme',
'rootPath',
'buildUrl'
];
return !name.toString().startsWith('_') && !exceptions.includes(name.toString());
};
const isUrlRoot = (obj) => {
// An object is a root object if it has host or rootPath, or if code is running in a browser, an object is a
// root object if it has any of the reserved properties.
let yes = false;
forEachProperty(obj, k => yes = rootUrlObjectProps.includes(k) || (isBrowser && rootUrlObjectPropsForBrowser.includes(k)));
return yes;
};
const isUrlNode = (obj, parent) => {
return !!parent?.[rootPathFn] || !!obj._rootPath;
};
// Add the _rootPath() function.
if (isUrlRoot(ws) && (!parent?.buildUrl)) {
ws[rootPathFn] = function () {
return parentRootPath.bind(ws)(isBrowser);
};
}
else if (isUrlNode(ws, parent)) {
ws[rootPathFn] = function () {
return pathRootPath.bind(ws)(parent);
};
}
if (isUrlNode(ws, parent)) {
// Add the buildUrl function.
ws.buildUrl = function (path, routeValues, queryString) {
return buildUrlImpl.bind(ws)(path, routeValues, routeValuesRegExp, queryString);
};
}
const canServeAsParent = isUrlNode(ws, undefined);
// For every non-object property in the object, make it a function.
// Properties that have an object are recursively configured.
forEachProperty(ws, (key, value) => {
const sc = shouldConvert(key);
if (sc && canServeAsParent && typeof value === 'string') {
ws[key] = function (routeValues, queryString) {
return (ws.buildUrl ?? noop)(value, routeValues, queryString);
};
}
else if (sc && isConfigNode(value)) {
// Object value.
makeWsUrlFunctions(value, routeValuesRegExp, isBrowser, canServeAsParent ? ws : undefined);
}
});
}
;
export default makeWsUrlFunctions;