read-config-ng
Version:
Multi format configuration loader
110 lines • 3.81 kB
JavaScript
import * as path from 'path';
import { pick } from './deep.js';
import { ReadConfigError } from '../read-config-error.js';
/**
* Resolve a value that may contain variable expressions
*/
export function resolveValue(prop, value, marker, values, opts = {}) {
const escapedMarker = escapeRegExp(marker);
const fullFieldRegexp = new RegExp(`^\\ *${escapedMarker}{\\ *([^${escapedMarker}}]+)\\ *}\\ *$`);
const partialFieldRegexp = new RegExp(`${escapedMarker}{\\ *([^${escapedMarker}}]+)\\ *}`, 'g');
const expressionCheckRegexp = new RegExp(`${escapedMarker}{\\ *([^${escapedMarker}}]+)\\ *}`, 'g');
const partialFieldReplacer = (_match, expression) => {
const resolved = resolveExpression(prop, expression, values, opts);
if (typeof resolved === 'object') {
return JSON.stringify(resolved);
}
return String(resolved ?? '');
};
let result = value;
while (expressionCheckRegexp.test(String(result))) {
// Reset lastIndex after test
expressionCheckRegexp.lastIndex = 0;
const fullFieldMatch = fullFieldRegexp.exec(value);
if (fullFieldMatch !== null && fullFieldMatch.length > 1) {
result = resolveExpression(prop, fullFieldMatch[1], values, opts);
}
else {
result = value.replace(partialFieldRegexp, partialFieldReplacer);
}
}
return result;
}
/**
* Resolve a single expression (e.g., "path.to.value|defaultValue")
*/
function resolveExpression(prop, expression, values, opts) {
const exprChunks = expression.split('|');
const exprValue = exprChunks[0]?.trim() || '';
const exprDefValue = exprChunks.length > 1 && exprChunks[1] ? exprChunks[1] : null;
const resolvedProp = resolveRelativeProperty(prop, exprValue);
const pickResult = pick(values, resolvedProp);
if (pickResult) {
return pickResult.value;
}
else if (exprDefValue !== null) {
return castDefaultValue(exprDefValue);
}
else if (!opts.skipUnresolved) {
throw new ReadConfigError(`Unresolved configuration variable: ${expression}`, 'RESOLUTION_ERROR');
}
else {
return `NOTFOUND: ${resolvedProp}`;
}
}
/**
* Cast a default value string to its appropriate type
*/
function castDefaultValue(value) {
const trimmed = value.trim();
if (/^\d+$/.test(trimmed)) {
return parseInt(trimmed, 10);
}
else if (/^\d*\.\d+$/.test(trimmed)) {
return parseFloat(trimmed);
}
else if (trimmed.toLowerCase() === 'true') {
return true;
}
else if (trimmed.toLowerCase() === 'false') {
return false;
}
else if (trimmed === 'undefined') {
return undefined;
}
else if (trimmed === 'null') {
return null;
}
return value;
}
/**
* Resolve relative property paths (e.g., "./sibling", "../parent")
*/
function resolveRelativeProperty(propPath, relPath) {
// If not a relative path, return as-is
if (!relPath.startsWith('./') && !relPath.startsWith('../')) {
return relPath;
}
// Normalize the relative path
let normalizedPath = relPath;
if (relPath.startsWith('./')) {
normalizedPath = `../${relPath}`;
}
else if (relPath.startsWith('../')) {
normalizedPath = `../${relPath}`;
}
// Convert dots to slashes for path resolution
const propAsPath = propPath.replace(/\./g, '/');
const resolved = path.join(propAsPath, normalizedPath);
// Convert back to dot notation
return resolved
.replace(/\\/g, '.')
.replace(/\//g, '.');
}
/**
* Escape special regex characters in a string
*/
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}
//# sourceMappingURL=resolve-expression.js.map