configorama 0.6.18 → 0.6.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -67,7 +67,7 @@ const { warnIfNotFound, isValidValue } = require('./utils/validation/warnIfNotFo
|
|
|
67
67
|
/* Utils - variables */
|
|
68
68
|
const cleanVariable = require('./utils/variables/cleanVariable')
|
|
69
69
|
const appendDeepVariable = require('./utils/variables/appendDeepVariable')
|
|
70
|
-
const { getFallbackString, verifyVariable } = require('./utils/variables/variableUtils')
|
|
70
|
+
const { extractVariableWrapper, getFallbackString, verifyVariable } = require('./utils/variables/variableUtils')
|
|
71
71
|
const { findNestedVariables } = require('./utils/variables/findNestedVariables')
|
|
72
72
|
|
|
73
73
|
/* Resolvers */
|
|
@@ -174,6 +174,15 @@ class Configorama {
|
|
|
174
174
|
const variableSyntax = varRegex
|
|
175
175
|
this.variableSyntax = variableSyntax
|
|
176
176
|
|
|
177
|
+
// Extract variable prefix/suffix from syntax regex for reconstructing variables
|
|
178
|
+
const syntaxWrapper = extractVariableWrapper(variableSyntax.source)
|
|
179
|
+
this.varPrefix = syntaxWrapper.prefix
|
|
180
|
+
this.varSuffix = syntaxWrapper.suffix
|
|
181
|
+
const escapedSuffix = this.varSuffix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
182
|
+
this.varPrefixPattern = new RegExp('^' + this.varPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
183
|
+
this.varSuffixPattern = new RegExp(escapedSuffix + '$')
|
|
184
|
+
this.varSuffixWithSpacePattern = new RegExp('\\s+' + escapedSuffix + '$')
|
|
185
|
+
|
|
177
186
|
// Set initial config object to populate
|
|
178
187
|
if (typeof fileOrObject === 'object') {
|
|
179
188
|
// set config objects
|
|
@@ -566,7 +575,7 @@ class Configorama {
|
|
|
566
575
|
this.rawOriginalConfig = cloneDeep(configObject)
|
|
567
576
|
|
|
568
577
|
/* Preprocess step here - escapes ${} in help() args, fixes malformed fallbacks */
|
|
569
|
-
configObject = preProcess(configObject, this.variableSyntax)
|
|
578
|
+
configObject = preProcess(configObject, this.variableSyntax, this.variableTypes)
|
|
570
579
|
/*
|
|
571
580
|
console.log('after preprocess', configObject)
|
|
572
581
|
/** */
|
|
@@ -654,7 +663,7 @@ class Configorama {
|
|
|
654
663
|
const fileName = this.configFilePath ? ` in ${this.configFilePath}` : ''
|
|
655
664
|
|
|
656
665
|
// Extract base variable name from varMatch key (e.g., '${env:FOO, default}' -> 'env:FOO')
|
|
657
|
-
const getBaseVarName = (key) => key.replace(
|
|
666
|
+
const getBaseVarName = (key) => key.replace(this.varPrefixPattern, '').replace(this.varSuffixPattern, '').split(',')[0].trim()
|
|
658
667
|
|
|
659
668
|
logHeader(`Found ${varKeys.length} Variables${fileName}`)
|
|
660
669
|
|
|
@@ -1309,9 +1318,9 @@ class Configorama {
|
|
|
1309
1318
|
.map((filter) => filter.trim())
|
|
1310
1319
|
.filter(Boolean)
|
|
1311
1320
|
|
|
1312
|
-
// Remove filters from the key (replace "| String}" with
|
|
1321
|
+
// Remove filters from the key (replace "| String}" with suffix)
|
|
1313
1322
|
// Also clean up any trailing whitespace before the closing brace
|
|
1314
|
-
keyWithoutFilters = originalSrc.replace(filterMatch,
|
|
1323
|
+
keyWithoutFilters = originalSrc.replace(filterMatch, this.varSuffix).replace(this.varSuffixWithSpacePattern, this.varSuffix)
|
|
1315
1324
|
}
|
|
1316
1325
|
|
|
1317
1326
|
const key = keyWithoutFilters
|
|
@@ -1337,7 +1346,7 @@ class Configorama {
|
|
|
1337
1346
|
if (cleaned.varMatch && filterMatch) {
|
|
1338
1347
|
const match = cleaned.varMatch.match(filterMatch)
|
|
1339
1348
|
if (match) {
|
|
1340
|
-
cleaned.varMatch = cleaned.varMatch.replace(filterMatch, '').replace(/\s+$/, '') +
|
|
1349
|
+
cleaned.varMatch = cleaned.varMatch.replace(filterMatch, '').replace(/\s+$/, '') + this.varSuffix
|
|
1341
1350
|
}
|
|
1342
1351
|
}
|
|
1343
1352
|
if (cleaned.variable && filterMatch) {
|
|
@@ -2202,7 +2211,7 @@ class Configorama {
|
|
|
2202
2211
|
let foundFilters = []
|
|
2203
2212
|
if (hasFilters) {
|
|
2204
2213
|
foundFilters = hasFilters[0]
|
|
2205
|
-
.replace(
|
|
2214
|
+
.replace(this.varSuffixPattern, '')
|
|
2206
2215
|
.split('|')
|
|
2207
2216
|
.map((filter) => filter.trim())
|
|
2208
2217
|
.filter(Boolean)
|
|
@@ -2287,13 +2296,13 @@ class Configorama {
|
|
|
2287
2296
|
console.log('isString currentMatchedString', currentMatchedString)
|
|
2288
2297
|
console.log('>------')
|
|
2289
2298
|
/** */
|
|
2290
|
-
// Handle comma ${opt:stage, dev} and remove extra
|
|
2299
|
+
// Handle comma ${opt:stage, dev} and remove extra suffix
|
|
2291
2300
|
if (
|
|
2292
2301
|
currentMatchedString.match(this.variableSyntax) &&
|
|
2293
2302
|
!valueToPopulate.match(this.variableSyntax) &&
|
|
2294
|
-
valueToPopulate.match(
|
|
2303
|
+
valueToPopulate.match(this.varSuffixPattern)
|
|
2295
2304
|
) {
|
|
2296
|
-
valueToPopulate = valueToPopulate.replace(
|
|
2305
|
+
valueToPopulate = valueToPopulate.replace(this.varSuffixPattern, '')
|
|
2297
2306
|
}
|
|
2298
2307
|
|
|
2299
2308
|
property = replaceAll(currentMatchedString, valueToPopulate, property)
|
|
@@ -2340,7 +2349,7 @@ class Configorama {
|
|
|
2340
2349
|
let missingValue = matchedString
|
|
2341
2350
|
|
|
2342
2351
|
if (matchedString.match(deepRefSyntax)) {
|
|
2343
|
-
const deepIndex = matchedString.split(':')[1].replace(
|
|
2352
|
+
const deepIndex = matchedString.split(':')[1].replace(this.varSuffixPattern, '')
|
|
2344
2353
|
const i = Number(deepIndex)
|
|
2345
2354
|
missingValue = this.deep[i]
|
|
2346
2355
|
}
|
|
@@ -2372,6 +2381,39 @@ class Configorama {
|
|
|
2372
2381
|
}
|
|
2373
2382
|
}
|
|
2374
2383
|
|
|
2384
|
+
// If allowUnresolvedVariables and there are fallbacks, use the fallback
|
|
2385
|
+
if (this.opts.allowUnresolvedVariables && splitVars.length > 1) {
|
|
2386
|
+
const nextFallback = splitVars[1].trim()
|
|
2387
|
+
// Strip trailing variable suffix (handles }, }}, >, ]], etc.)
|
|
2388
|
+
const nextFallbackClean = nextFallback.replace(this.varSuffixPattern, '')
|
|
2389
|
+
const isQuotedString = /^['"].*['"]$/.test(nextFallbackClean)
|
|
2390
|
+
const isNumeric = /^-?\d+(\.\d+)?$/.test(nextFallbackClean)
|
|
2391
|
+
if (isQuotedString || isNumeric) {
|
|
2392
|
+
let staticValue = nextFallbackClean.replace(/^['"]|['"]$/g, '')
|
|
2393
|
+
// Convert to number if it's a numeric fallback
|
|
2394
|
+
if (isNumeric) {
|
|
2395
|
+
staticValue = Number(staticValue)
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
value: staticValue,
|
|
2399
|
+
path: valueObject.path,
|
|
2400
|
+
originalSource: valueObject.originalSource,
|
|
2401
|
+
resolutionHistory: valueObject.resolutionHistory || [],
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
// Next fallback is another variable
|
|
2405
|
+
const remainingContent = splitVars.slice(1).join(', ').replace(this.varSuffixPattern, '')
|
|
2406
|
+
const remainingFallbacks = this.varPrefix + remainingContent + this.varSuffix
|
|
2407
|
+
return {
|
|
2408
|
+
value: remainingFallbacks,
|
|
2409
|
+
path: valueObject.path,
|
|
2410
|
+
originalSource: valueObject.originalSource,
|
|
2411
|
+
resolutionHistory: valueObject.resolutionHistory || [],
|
|
2412
|
+
__internal_only_flag: true,
|
|
2413
|
+
caller: 'allowUnresolvedVariables-fallback',
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2375
2417
|
const currentPath = valueObject.path.join('.')
|
|
2376
2418
|
|
|
2377
2419
|
const errorMessage = `
|
|
@@ -2394,7 +2436,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2394
2436
|
)
|
|
2395
2437
|
|
|
2396
2438
|
// Double processing needed for `${eval(${self:three} > ${self:four})}`
|
|
2397
|
-
if (prop.startsWith(
|
|
2439
|
+
if (prop.startsWith(this.varPrefix)) {
|
|
2398
2440
|
prop = cleanVariable(prop, this.variableSyntax, true, `populateVariable string ${this.callCount}`)
|
|
2399
2441
|
}
|
|
2400
2442
|
|
|
@@ -2595,7 +2637,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2595
2637
|
|
|
2596
2638
|
if (deepProperties > 0) {
|
|
2597
2639
|
// Reconstruct a minimal variable string with deep refs, not the full outer string
|
|
2598
|
-
const reconstructed =
|
|
2640
|
+
const reconstructed = this.varPrefix + deepVariableParts.join(', ') + this.varSuffix
|
|
2599
2641
|
return Promise.resolve(reconstructed)
|
|
2600
2642
|
}
|
|
2601
2643
|
return Promise.resolve(extractedValues.find(isValidValue)) // resolve first valid value, else undefined
|
|
@@ -2694,7 +2736,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2694
2736
|
.map((f) => {
|
|
2695
2737
|
return trim(f)
|
|
2696
2738
|
// TODO refactor this. This is a temp fix for filters with nested vars.
|
|
2697
|
-
.replace(
|
|
2739
|
+
.replace(this.varSuffixPattern, '')
|
|
2698
2740
|
})
|
|
2699
2741
|
// console.log('filters to run', _filter)
|
|
2700
2742
|
|
|
@@ -2809,6 +2851,16 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2809
2851
|
}
|
|
2810
2852
|
|
|
2811
2853
|
if (this.opts.allowUnresolvedVariables) {
|
|
2854
|
+
// Check if outer expression has fallbacks we can use
|
|
2855
|
+
// valueCount[0] is the primary var, valueCount[1+] are fallbacks
|
|
2856
|
+
if (valueCount.length > 1) {
|
|
2857
|
+
const primaryVar = valueCount[0]
|
|
2858
|
+
// If the unresolvable variableString is used INSIDE the primary var,
|
|
2859
|
+
// return undefined to trigger the outer fallback mechanism
|
|
2860
|
+
if (primaryVar.includes(variableString)) {
|
|
2861
|
+
return Promise.resolve(undefined)
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2812
2864
|
// Encode unresolved variable to pass through resolution
|
|
2813
2865
|
return Promise.resolve(encodeUnknown(propertyString))
|
|
2814
2866
|
}
|
|
@@ -2885,7 +2937,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2885
2937
|
if (typeof val === 'string' && val.match(/deep:/)) {
|
|
2886
2938
|
// TODO refactor the deep filter logic here. match | filter | filter..
|
|
2887
2939
|
const allFilters = propertyString
|
|
2888
|
-
.replace(
|
|
2940
|
+
.replace(this.varSuffixPattern, '')
|
|
2889
2941
|
.split('|')
|
|
2890
2942
|
.reduce((acc, currentFilter, i) => {
|
|
2891
2943
|
if (i === 0) {
|
|
@@ -2895,7 +2947,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2895
2947
|
return acc
|
|
2896
2948
|
}, '')
|
|
2897
2949
|
// add filters to deep references if filter is used
|
|
2898
|
-
const deepValueWithFilters = newHasFilter[1] ? val.replace(
|
|
2950
|
+
const deepValueWithFilters = newHasFilter[1] ? val.replace(this.varSuffixPattern, ` ${allFilters}${this.varSuffix}`) : val
|
|
2899
2951
|
// console.log('deepValueWithFilters', deepValueWithFilters)
|
|
2900
2952
|
// console.log('RESOLVER RETURN newValue 4', deepValueWithFilters)
|
|
2901
2953
|
return Promise.resolve(deepValueWithFilters)
|
|
@@ -3,16 +3,27 @@
|
|
|
3
3
|
* and escape variables inside help() filter arguments
|
|
4
4
|
*/
|
|
5
5
|
const { splitByComma } = require('../strings/splitByComma')
|
|
6
|
+
const { extractVariableWrapper } = require('../variables/variableUtils')
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Preprocess config to fix malformed fallback references
|
|
9
10
|
* @param {Object} configObject - The parsed configuration object
|
|
10
11
|
* @param {RegExp} variableSyntax - The variable syntax regex to use
|
|
12
|
+
* @param {Array} [variableTypes] - Array of variable type definitions with type/prefix fields
|
|
11
13
|
* @returns {Object} The preprocessed configuration object
|
|
12
14
|
*/
|
|
13
|
-
function preProcess(configObject, variableSyntax) {
|
|
14
|
-
//
|
|
15
|
-
const
|
|
15
|
+
function preProcess(configObject, variableSyntax, variableTypes) {
|
|
16
|
+
// Extract prefix/suffix from variable syntax for reconstructing variables
|
|
17
|
+
const { prefix: varPrefix, suffix: varSuffix } = variableSyntax
|
|
18
|
+
? extractVariableWrapper(variableSyntax.source)
|
|
19
|
+
: { prefix: '${', suffix: '}' }
|
|
20
|
+
|
|
21
|
+
// Extract reference prefixes from variable types, or use defaults
|
|
22
|
+
const refPrefixes = variableTypes && variableTypes.length > 0
|
|
23
|
+
? variableTypes
|
|
24
|
+
.map(v => (v.prefix || v.type) + ':')
|
|
25
|
+
.filter(p => p !== 'dot.prop:' && p !== 'string:' && p !== 'number:')
|
|
26
|
+
: ['self:', 'opt:', 'env:', 'file:', 'text:', 'deep:']
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* Escape variables inside help() filter arguments so main resolver skips them
|
|
@@ -52,31 +63,37 @@ function preProcess(configObject, variableSyntax) {
|
|
|
52
63
|
let changed = true
|
|
53
64
|
|
|
54
65
|
// Keep iterating until no more changes (to handle nested variables)
|
|
66
|
+
const prefixLen = varPrefix.length
|
|
67
|
+
const suffixLen = varSuffix.length
|
|
68
|
+
|
|
55
69
|
while (changed) {
|
|
56
70
|
changed = false
|
|
57
71
|
|
|
58
|
-
// Find innermost
|
|
72
|
+
// Find innermost variable blocks (ones that don't contain other variables)
|
|
59
73
|
let i = 0
|
|
60
74
|
while (i < result.length) {
|
|
61
|
-
if (result
|
|
75
|
+
if (result.substring(i, i + prefixLen) === varPrefix) {
|
|
62
76
|
const start = i
|
|
63
|
-
let
|
|
64
|
-
let j = i +
|
|
65
|
-
|
|
66
|
-
// Find the matching
|
|
67
|
-
while (j < result.length &&
|
|
68
|
-
if (result
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
let depth = 1
|
|
78
|
+
let j = i + prefixLen
|
|
79
|
+
|
|
80
|
+
// Find the matching suffix by counting full prefix/suffix occurrences
|
|
81
|
+
while (j < result.length && depth > 0) {
|
|
82
|
+
if (result.substring(j, j + prefixLen) === varPrefix) {
|
|
83
|
+
depth++
|
|
84
|
+
j += prefixLen
|
|
85
|
+
} else if (result.substring(j, j + suffixLen) === varSuffix) {
|
|
86
|
+
depth--
|
|
87
|
+
if (depth > 0) j += suffixLen
|
|
88
|
+
} else {
|
|
89
|
+
j++
|
|
72
90
|
}
|
|
73
|
-
j++
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
if (
|
|
77
|
-
const end = j
|
|
93
|
+
if (depth === 0) {
|
|
94
|
+
const end = j + suffixLen
|
|
78
95
|
const match = result.substring(start, end)
|
|
79
|
-
const content = result.substring(start +
|
|
96
|
+
const content = result.substring(start + prefixLen, end - suffixLen)
|
|
80
97
|
|
|
81
98
|
// Only process if there's a comma (indicating fallback syntax)
|
|
82
99
|
if (content.includes(',')) {
|
|
@@ -84,10 +101,10 @@ function preProcess(configObject, variableSyntax) {
|
|
|
84
101
|
const parts = splitByComma(content, variableSyntax)
|
|
85
102
|
|
|
86
103
|
if (parts.length > 1) {
|
|
87
|
-
// Check if the first part has nested
|
|
104
|
+
// Check if the first part has nested variables - if so, skip this (process inner ones first)
|
|
88
105
|
const firstPart = parts[0]
|
|
89
|
-
if (firstPart.includes(
|
|
90
|
-
i = start +
|
|
106
|
+
if (firstPart.includes(varPrefix)) {
|
|
107
|
+
i = start + prefixLen // Move past prefix to find inner variables
|
|
91
108
|
continue
|
|
92
109
|
}
|
|
93
110
|
|
|
@@ -101,16 +118,16 @@ function preProcess(configObject, variableSyntax) {
|
|
|
101
118
|
|
|
102
119
|
// Check if this looks like a reference but is not wrapped
|
|
103
120
|
const looksLikeRef = refPrefixes.some(prefix => trimmed.startsWith(prefix))
|
|
104
|
-
const alreadyWrapped = trimmed.startsWith(
|
|
121
|
+
const alreadyWrapped = trimmed.startsWith(varPrefix) && trimmed.endsWith(varSuffix)
|
|
105
122
|
|
|
106
123
|
if (looksLikeRef && !alreadyWrapped) {
|
|
107
|
-
return `
|
|
124
|
+
return ` ${varPrefix}${trimmed}${varSuffix}`
|
|
108
125
|
}
|
|
109
126
|
|
|
110
127
|
return ` ${trimmed}`
|
|
111
128
|
})
|
|
112
129
|
|
|
113
|
-
const replacement =
|
|
130
|
+
const replacement = `${varPrefix}${fixed.join(',')}${varSuffix}`
|
|
114
131
|
if (replacement !== match) {
|
|
115
132
|
result = result.substring(0, start) + replacement + result.substring(end)
|
|
116
133
|
changed = true
|
|
@@ -119,7 +136,7 @@ function preProcess(configObject, variableSyntax) {
|
|
|
119
136
|
}
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
i = start +
|
|
139
|
+
i = start + prefixLen // Move past prefix to continue searching for nested variables
|
|
123
140
|
} else {
|
|
124
141
|
i++
|
|
125
142
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Tests for preProcess utility (fixFallbacksInString)
|
|
2
|
+
|
|
3
|
+
const { test } = require('uvu')
|
|
4
|
+
const assert = require('uvu/assert')
|
|
5
|
+
const preProcess = require('./preProcess')
|
|
6
|
+
|
|
7
|
+
// Default ${} syntax
|
|
8
|
+
const defaultSyntax = /\$\{([ ~:a-zA-Z0-9._\\'",\-\/\(\)]+?)\}/g
|
|
9
|
+
|
|
10
|
+
// Custom syntaxes
|
|
11
|
+
const doubleBraceSyntax = /\$\{\{([ ~:a-zA-Z0-9._\\'",\-\/\(\)]+?)\}\}/g
|
|
12
|
+
const hashSyntax = /\#\{([ ~:a-zA-Z0-9._\\'",\-\/\(\)]+?)\}/g
|
|
13
|
+
const angleSyntax = /\<([ ~:a-zA-Z0-9._\\'",\-\/\(\)]+?)\>/g
|
|
14
|
+
|
|
15
|
+
// Tests for fixFallbacksInString with default ${} syntax
|
|
16
|
+
test('fixFallbacksInString - wraps unwrapped self: fallback', () => {
|
|
17
|
+
const input = { key: '${opt:missing, self:fallback}' }
|
|
18
|
+
const result = preProcess(input, defaultSyntax)
|
|
19
|
+
assert.is(result.key, '${opt:missing, ${self:fallback}}')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test('fixFallbacksInString - wraps unwrapped env: fallback', () => {
|
|
23
|
+
const input = { key: '${opt:missing, env:FALLBACK}' }
|
|
24
|
+
const result = preProcess(input, defaultSyntax)
|
|
25
|
+
assert.is(result.key, '${opt:missing, ${env:FALLBACK}}')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('fixFallbacksInString - wraps unwrapped opt: fallback', () => {
|
|
29
|
+
const input = { key: '${self:missing, opt:fallback}' }
|
|
30
|
+
const result = preProcess(input, defaultSyntax)
|
|
31
|
+
assert.is(result.key, '${self:missing, ${opt:fallback}}')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('fixFallbacksInString - wraps unwrapped file: fallback', () => {
|
|
35
|
+
const input = { key: '${opt:missing, file:./config.json}' }
|
|
36
|
+
const result = preProcess(input, defaultSyntax)
|
|
37
|
+
assert.is(result.key, '${opt:missing, ${file:./config.json}}')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('fixFallbacksInString - leaves already wrapped fallback alone', () => {
|
|
41
|
+
const input = { key: '${opt:missing, ${self:fallback}}' }
|
|
42
|
+
const result = preProcess(input, defaultSyntax)
|
|
43
|
+
assert.is(result.key, '${opt:missing, ${self:fallback}}')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('fixFallbacksInString - leaves string fallback alone', () => {
|
|
47
|
+
const input = { key: '${opt:missing, "default"}' }
|
|
48
|
+
const result = preProcess(input, defaultSyntax)
|
|
49
|
+
assert.is(result.key, '${opt:missing, "default"}')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('fixFallbacksInString - leaves numeric fallback alone', () => {
|
|
53
|
+
const input = { key: '${opt:missing, 42}' }
|
|
54
|
+
const result = preProcess(input, defaultSyntax)
|
|
55
|
+
assert.is(result.key, '${opt:missing, 42}')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('fixFallbacksInString - handles multiple fallbacks', () => {
|
|
59
|
+
const input = { key: '${opt:missing, self:first, env:second}' }
|
|
60
|
+
const result = preProcess(input, defaultSyntax)
|
|
61
|
+
assert.is(result.key, '${opt:missing, ${self:first}, ${env:second}}')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('fixFallbacksInString - handles nested variables in primary', () => {
|
|
65
|
+
const input = { key: '${file(./config.${opt:stage}.json), "default"}' }
|
|
66
|
+
const result = preProcess(input, defaultSyntax)
|
|
67
|
+
assert.is(result.key, '${file(./config.${opt:stage}.json), "default"}')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Tests for ${{}} syntax
|
|
71
|
+
test('fixFallbacksInString - ${{}} syntax wraps unwrapped fallback', () => {
|
|
72
|
+
const input = { key: '${{opt:missing, self:fallback}}' }
|
|
73
|
+
const result = preProcess(input, doubleBraceSyntax)
|
|
74
|
+
assert.is(result.key, '${{opt:missing, ${{self:fallback}}}}')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('fixFallbacksInString - ${{}} syntax leaves wrapped fallback alone', () => {
|
|
78
|
+
const input = { key: '${{opt:missing, ${{self:fallback}}}}' }
|
|
79
|
+
const result = preProcess(input, doubleBraceSyntax)
|
|
80
|
+
assert.is(result.key, '${{opt:missing, ${{self:fallback}}}}')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('fixFallbacksInString - ${{}} syntax leaves string fallback alone', () => {
|
|
84
|
+
const input = { key: '${{opt:missing, "default"}}' }
|
|
85
|
+
const result = preProcess(input, doubleBraceSyntax)
|
|
86
|
+
assert.is(result.key, '${{opt:missing, "default"}}')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Tests for #{} syntax
|
|
90
|
+
test('fixFallbacksInString - #{} syntax wraps unwrapped fallback', () => {
|
|
91
|
+
const input = { key: '#{opt:missing, self:fallback}' }
|
|
92
|
+
const result = preProcess(input, hashSyntax)
|
|
93
|
+
assert.is(result.key, '#{opt:missing, #{self:fallback}}')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('fixFallbacksInString - #{} syntax leaves wrapped fallback alone', () => {
|
|
97
|
+
const input = { key: '#{opt:missing, #{self:fallback}}' }
|
|
98
|
+
const result = preProcess(input, hashSyntax)
|
|
99
|
+
assert.is(result.key, '#{opt:missing, #{self:fallback}}')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('fixFallbacksInString - #{} syntax handles multiple fallbacks', () => {
|
|
103
|
+
const input = { key: '#{opt:missing, env:FIRST, self:second}' }
|
|
104
|
+
const result = preProcess(input, hashSyntax)
|
|
105
|
+
assert.is(result.key, '#{opt:missing, #{env:FIRST}, #{self:second}}')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Tests for <> syntax
|
|
109
|
+
test('fixFallbacksInString - <> syntax wraps unwrapped fallback', () => {
|
|
110
|
+
const input = { key: '<opt:missing, self:fallback>' }
|
|
111
|
+
const result = preProcess(input, angleSyntax)
|
|
112
|
+
assert.is(result.key, '<opt:missing, <self:fallback>>')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('fixFallbacksInString - <> syntax leaves wrapped fallback alone', () => {
|
|
116
|
+
const input = { key: '<opt:missing, <self:fallback>>' }
|
|
117
|
+
const result = preProcess(input, angleSyntax)
|
|
118
|
+
assert.is(result.key, '<opt:missing, <self:fallback>>')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('fixFallbacksInString - <> syntax leaves string fallback alone', () => {
|
|
122
|
+
const input = { key: '<opt:missing, "default">' }
|
|
123
|
+
const result = preProcess(input, angleSyntax)
|
|
124
|
+
assert.is(result.key, '<opt:missing, "default">')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Edge cases
|
|
128
|
+
test('fixFallbacksInString - handles deeply nested objects', () => {
|
|
129
|
+
const input = {
|
|
130
|
+
level1: {
|
|
131
|
+
level2: {
|
|
132
|
+
key: '${opt:missing, self:fallback}'
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const result = preProcess(input, defaultSyntax)
|
|
137
|
+
assert.is(result.level1.level2.key, '${opt:missing, ${self:fallback}}')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('fixFallbacksInString - handles arrays', () => {
|
|
141
|
+
const input = {
|
|
142
|
+
items: [
|
|
143
|
+
'${opt:one, self:fallback1}',
|
|
144
|
+
'${opt:two, self:fallback2}'
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
const result = preProcess(input, defaultSyntax)
|
|
148
|
+
assert.is(result.items[0], '${opt:one, ${self:fallback1}}')
|
|
149
|
+
assert.is(result.items[1], '${opt:two, ${self:fallback2}}')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('fixFallbacksInString - handles no variable syntax', () => {
|
|
153
|
+
const input = { key: 'just a plain string' }
|
|
154
|
+
const result = preProcess(input, defaultSyntax)
|
|
155
|
+
assert.is(result.key, 'just a plain string')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('fixFallbacksInString - handles null variableSyntax', () => {
|
|
159
|
+
const input = { key: '${opt:missing, self:fallback}' }
|
|
160
|
+
const result = preProcess(input, null)
|
|
161
|
+
// Without syntax, should use defaults
|
|
162
|
+
assert.is(result.key, '${opt:missing, ${self:fallback}}')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Tests with explicit variableTypes
|
|
166
|
+
const defaultVariableTypes = [
|
|
167
|
+
{ type: 'env' },
|
|
168
|
+
{ type: 'options', prefix: 'opt' },
|
|
169
|
+
{ type: 'self', prefix: 'self' },
|
|
170
|
+
{ type: 'file', prefix: 'file' },
|
|
171
|
+
{ type: 'text', prefix: 'text' },
|
|
172
|
+
{ type: 'deep' },
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
test('fixFallbacksInString - uses variableTypes to determine prefixes', () => {
|
|
176
|
+
const input = { key: '${opt:missing, self:fallback}' }
|
|
177
|
+
const result = preProcess(input, defaultSyntax, defaultVariableTypes)
|
|
178
|
+
assert.is(result.key, '${opt:missing, ${self:fallback}}')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('fixFallbacksInString - custom variableTypes with custom prefix', () => {
|
|
182
|
+
const customTypes = [
|
|
183
|
+
{ type: 'ssm', prefix: 'ssm' },
|
|
184
|
+
{ type: 'secret', prefix: 'secret' },
|
|
185
|
+
]
|
|
186
|
+
const input = { key: '${opt:missing, ssm:/path/to/param}' }
|
|
187
|
+
const result = preProcess(input, defaultSyntax, customTypes)
|
|
188
|
+
assert.is(result.key, '${opt:missing, ${ssm:/path/to/param}}')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('fixFallbacksInString - custom variableTypes wraps secret: fallback', () => {
|
|
192
|
+
const customTypes = [
|
|
193
|
+
{ type: 'ssm', prefix: 'ssm' },
|
|
194
|
+
{ type: 'secret', prefix: 'secret' },
|
|
195
|
+
{ type: 'self', prefix: 'self' },
|
|
196
|
+
]
|
|
197
|
+
const input = { key: '${self:missing, secret:API_KEY}' }
|
|
198
|
+
const result = preProcess(input, defaultSyntax, customTypes)
|
|
199
|
+
assert.is(result.key, '${self:missing, ${secret:API_KEY}}')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('fixFallbacksInString - ignores dot.prop and string types in prefixes', () => {
|
|
203
|
+
const typesWithDotProp = [
|
|
204
|
+
{ type: 'env' },
|
|
205
|
+
{ type: 'dot.prop' },
|
|
206
|
+
{ type: 'string' },
|
|
207
|
+
{ type: 'self', prefix: 'self' },
|
|
208
|
+
]
|
|
209
|
+
const input = { key: '${env:MISSING, self:fallback}' }
|
|
210
|
+
const result = preProcess(input, defaultSyntax, typesWithDotProp)
|
|
211
|
+
assert.is(result.key, '${env:MISSING, ${self:fallback}}')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test.run()
|
|
@@ -1,3 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract variable prefix/suffix from regex source
|
|
3
|
+
* @param {string} syntaxSource - The regex source string (e.g., '\\$\\{(...)\\}')
|
|
4
|
+
* @returns {{ prefix: string, suffix: string }} The unescaped prefix and suffix
|
|
5
|
+
*/
|
|
6
|
+
function extractVariableWrapper(syntaxSource) {
|
|
7
|
+
// Find first capturing group ( that's not escaped and not a special group (?:, (?=, etc.)
|
|
8
|
+
let openParen = -1
|
|
9
|
+
for (let i = 0; i < syntaxSource.length; i++) {
|
|
10
|
+
if (syntaxSource[i] === '(' && (i === 0 || syntaxSource[i - 1] !== '\\')) {
|
|
11
|
+
// Check if it's a special group like (?:, (?=, (?!, (?<
|
|
12
|
+
if (syntaxSource[i + 1] !== '?') {
|
|
13
|
+
openParen = i
|
|
14
|
+
break
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Find last ) that's not escaped
|
|
20
|
+
let closeParen = -1
|
|
21
|
+
for (let i = syntaxSource.length - 1; i >= 0; i--) {
|
|
22
|
+
if (syntaxSource[i] === ')' && (i === 0 || syntaxSource[i - 1] !== '\\')) {
|
|
23
|
+
closeParen = i
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let escapedPrefix = openParen > 0 ? syntaxSource.substring(0, openParen) : ''
|
|
29
|
+
const escapedSuffix = closeParen >= 0 ? syntaxSource.substring(closeParen + 1) : ''
|
|
30
|
+
|
|
31
|
+
// Strip any leading non-capturing groups like (?:...) from prefix
|
|
32
|
+
escapedPrefix = escapedPrefix.replace(/^\(\?:[^)]*\)/g, '')
|
|
33
|
+
|
|
34
|
+
// Unescape regex escapes: \$ -> $, \{ -> {, \[ -> [, etc.
|
|
35
|
+
const unescape = (s) => s.replace(/\\(.)/g, '$1')
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
prefix: unescape(escapedPrefix) || '${',
|
|
39
|
+
suffix: unescape(escapedSuffix) || '}'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
1
43
|
/**
|
|
2
44
|
* Get fallback variable string
|
|
3
45
|
* @param {string[]} split - Array from split at comma
|
|
@@ -47,6 +89,7 @@ Remove or update the \${${variableString}} to fix
|
|
|
47
89
|
}
|
|
48
90
|
|
|
49
91
|
module.exports = {
|
|
92
|
+
extractVariableWrapper,
|
|
50
93
|
getFallbackString,
|
|
51
94
|
verifyVariable
|
|
52
95
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { test } = require('uvu')
|
|
2
2
|
const assert = require('uvu/assert')
|
|
3
|
-
const { getFallbackString, verifyVariable } = require('./variableUtils')
|
|
3
|
+
const { extractVariableWrapper, getFallbackString, verifyVariable } = require('./variableUtils')
|
|
4
4
|
|
|
5
5
|
// Tests for getFallbackString
|
|
6
6
|
test('getFallbackString - should reconstruct variable from split array', () => {
|
|
@@ -113,5 +113,42 @@ test('verifyVariable - should match with function and config param', () => {
|
|
|
113
113
|
assert.is(result, true)
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
+
// Tests for extractVariableWrapper
|
|
117
|
+
test('extractVariableWrapper - standard ${} syntax', () => {
|
|
118
|
+
const result = extractVariableWrapper('\\$\\{([^}]+)\\}')
|
|
119
|
+
assert.equal(result.prefix, '${')
|
|
120
|
+
assert.equal(result.suffix, '}')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('extractVariableWrapper - double brace ${{}} syntax', () => {
|
|
124
|
+
const result = extractVariableWrapper('\\$\\{\\{([^}]+)\\}\\}')
|
|
125
|
+
assert.equal(result.prefix, '${{')
|
|
126
|
+
assert.equal(result.suffix, '}}')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('extractVariableWrapper - hash #{} syntax', () => {
|
|
130
|
+
const result = extractVariableWrapper('\\#\\{([^}]+)\\}')
|
|
131
|
+
assert.equal(result.prefix, '#{')
|
|
132
|
+
assert.equal(result.suffix, '}')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('extractVariableWrapper - angle bracket <> syntax', () => {
|
|
136
|
+
const result = extractVariableWrapper('\\<([^>]+)\\>')
|
|
137
|
+
assert.equal(result.prefix, '<')
|
|
138
|
+
assert.equal(result.suffix, '>')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('extractVariableWrapper - double bracket [[]] syntax', () => {
|
|
142
|
+
const result = extractVariableWrapper('\\[\\[([^\\]]+)\\]\\]')
|
|
143
|
+
assert.equal(result.prefix, '[[')
|
|
144
|
+
assert.equal(result.suffix, ']]')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('extractVariableWrapper - strips non-capturing group prefix', () => {
|
|
148
|
+
const result = extractVariableWrapper('(?:prefix)\\$\\{([^}]+)\\}')
|
|
149
|
+
assert.equal(result.prefix, '${')
|
|
150
|
+
assert.equal(result.suffix, '}')
|
|
151
|
+
})
|
|
152
|
+
|
|
116
153
|
// Run all tests
|
|
117
154
|
test.run()
|