configorama 0.6.17 → 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
|
|
|
@@ -2566,7 +2608,6 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2566
2608
|
|
|
2567
2609
|
// console.log('variableValues', variableValues)
|
|
2568
2610
|
return Promise.all(variableValues).then((values) => {
|
|
2569
|
-
let deepPropertyStr = propertyString
|
|
2570
2611
|
let deepProperties = 0
|
|
2571
2612
|
// console.log('overwrite values', valuesToUse)
|
|
2572
2613
|
// Extract actual values from metadata objects
|
|
@@ -2576,6 +2617,10 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2576
2617
|
}
|
|
2577
2618
|
return value
|
|
2578
2619
|
})
|
|
2620
|
+
|
|
2621
|
+
// Build deep variable parts for reconstruction
|
|
2622
|
+
const deepVariableParts = variableStrings.slice()
|
|
2623
|
+
|
|
2579
2624
|
extractedValues.forEach((value, index) => {
|
|
2580
2625
|
// console.log('───────────────────────────────> value', value)
|
|
2581
2626
|
if (isString(value) && value.match(this.variableSyntax)) {
|
|
@@ -2585,14 +2630,17 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2585
2630
|
// console.log('deepVariable', deepVariable)
|
|
2586
2631
|
const newValue = cleanVariable(deepVariable, this.variableSyntax, true, `overwrite ${this.callCount}`)
|
|
2587
2632
|
// console.log(`overwrite newValue ${variableStrings[index]}`, newValue)
|
|
2588
|
-
//
|
|
2589
|
-
|
|
2590
|
-
// console.log('deepPropertyString', deepPropertyStr)
|
|
2633
|
+
// Store the deep ref for this part
|
|
2634
|
+
deepVariableParts[index] = newValue
|
|
2591
2635
|
}
|
|
2592
2636
|
})
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2637
|
+
|
|
2638
|
+
if (deepProperties > 0) {
|
|
2639
|
+
// Reconstruct a minimal variable string with deep refs, not the full outer string
|
|
2640
|
+
const reconstructed = this.varPrefix + deepVariableParts.join(', ') + this.varSuffix
|
|
2641
|
+
return Promise.resolve(reconstructed)
|
|
2642
|
+
}
|
|
2643
|
+
return Promise.resolve(extractedValues.find(isValidValue)) // resolve first valid value, else undefined
|
|
2596
2644
|
})
|
|
2597
2645
|
}
|
|
2598
2646
|
|
|
@@ -2688,7 +2736,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2688
2736
|
.map((f) => {
|
|
2689
2737
|
return trim(f)
|
|
2690
2738
|
// TODO refactor this. This is a temp fix for filters with nested vars.
|
|
2691
|
-
.replace(
|
|
2739
|
+
.replace(this.varSuffixPattern, '')
|
|
2692
2740
|
})
|
|
2693
2741
|
// console.log('filters to run', _filter)
|
|
2694
2742
|
|
|
@@ -2803,6 +2851,16 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2803
2851
|
}
|
|
2804
2852
|
|
|
2805
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
|
+
}
|
|
2806
2864
|
// Encode unresolved variable to pass through resolution
|
|
2807
2865
|
return Promise.resolve(encodeUnknown(propertyString))
|
|
2808
2866
|
}
|
|
@@ -2879,7 +2937,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2879
2937
|
if (typeof val === 'string' && val.match(/deep:/)) {
|
|
2880
2938
|
// TODO refactor the deep filter logic here. match | filter | filter..
|
|
2881
2939
|
const allFilters = propertyString
|
|
2882
|
-
.replace(
|
|
2940
|
+
.replace(this.varSuffixPattern, '')
|
|
2883
2941
|
.split('|')
|
|
2884
2942
|
.reduce((acc, currentFilter, i) => {
|
|
2885
2943
|
if (i === 0) {
|
|
@@ -2889,7 +2947,7 @@ Missing Value ${missingValue} - ${matchedString}
|
|
|
2889
2947
|
return acc
|
|
2890
2948
|
}, '')
|
|
2891
2949
|
// add filters to deep references if filter is used
|
|
2892
|
-
const deepValueWithFilters = newHasFilter[1] ? val.replace(
|
|
2950
|
+
const deepValueWithFilters = newHasFilter[1] ? val.replace(this.varSuffixPattern, ` ${allFilters}${this.varSuffix}`) : val
|
|
2893
2951
|
// console.log('deepValueWithFilters', deepValueWithFilters)
|
|
2894
2952
|
// console.log('RESOLVER RETURN newValue 4', deepValueWithFilters)
|
|
2895
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()
|