configorama 0.5.4 → 0.5.6
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/cli.js +6 -1
- package/package.json +4 -3
- package/src/main.js +111 -19
- package/src/resolvers/valueFromString.js +1 -0
- package/src/utils/find-nested-variables.js +117 -83
- package/src/utils/find-nested-variables.test.js +31 -11
- package/src/utils/x.js +173 -0
package/cli.js
CHANGED
|
@@ -92,8 +92,13 @@ if (options.dynamicArgs.verbose) {
|
|
|
92
92
|
'allow-undefined': allowUndefined,
|
|
93
93
|
...rest
|
|
94
94
|
} = dynamicArgs
|
|
95
|
+
|
|
95
96
|
console.log()
|
|
96
|
-
|
|
97
|
+
if (Object.keys(rest).length) {
|
|
98
|
+
deepLog(rest)
|
|
99
|
+
} else {
|
|
100
|
+
console.log('No flag options provided. Set flags like --flag value')
|
|
101
|
+
}
|
|
97
102
|
console.log()
|
|
98
103
|
}
|
|
99
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configorama",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Variable support for configuration files",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"docs": "node ./scripts/docs.js",
|
|
19
|
-
"test": "npm run
|
|
19
|
+
"test": "npm run test:lib && uvu tests \".*\\.test.js$\" ",
|
|
20
|
+
"test:tests": "uvu tests \".*\\.test.js$\" ",
|
|
20
21
|
"test:api": "uvu tests/api api.test.js",
|
|
21
|
-
"
|
|
22
|
+
"test:lib": "uvu src \".*\\.test.js$\"",
|
|
22
23
|
"watch": "watchlist tests -- npm test",
|
|
23
24
|
"publish": "git push origin && git push origin --tags",
|
|
24
25
|
"release:patch": "npm version patch && npm publish",
|
package/src/main.js
CHANGED
|
@@ -66,7 +66,7 @@ const deepRefSyntax = RegExp(/(\${)?deep:\d+(\.[^}]+)*()}?/)
|
|
|
66
66
|
const deepIndexReplacePattern = new RegExp(/^deep:|(\.[^}]+)*$/g)
|
|
67
67
|
const deepIndexPattern = /deep\:(\d*)/
|
|
68
68
|
const deepPrefixReplacePattern = /(?:^deep:)\d+\.?/g
|
|
69
|
-
const fileRefSyntax = RegExp(/^file\((~?[a-zA-Z0-9._\-\/,'" ]+?)\)/g)
|
|
69
|
+
const fileRefSyntax = RegExp(/^file\((~?[\{\}\:\$a-zA-Z0-9._\-\/,'" ]+?)\)/g)
|
|
70
70
|
// TODO update file regex ^file\((~?[a-zA-Z0-9._\-\/, ]+?)\)
|
|
71
71
|
// To match file(asyncValue.js, lol) input params
|
|
72
72
|
const envRefSyntax = RegExp(/^env:/g)
|
|
@@ -84,6 +84,16 @@ let VERBOSE = process.argv.includes('--verbose') ? true : false
|
|
|
84
84
|
|
|
85
85
|
const ENABLE_FUNCTIONS = true
|
|
86
86
|
|
|
87
|
+
function combineRegexes(regexes) {
|
|
88
|
+
// Extract the pattern from each RegExp and join with OR operator
|
|
89
|
+
const patterns = regexes.map(regex => {
|
|
90
|
+
// Get source pattern string without flags
|
|
91
|
+
return regex.source
|
|
92
|
+
}).filter(Boolean)
|
|
93
|
+
// Join patterns with the OR operator and create new RegExp
|
|
94
|
+
return new RegExp(`(${patterns.join('|')})`)
|
|
95
|
+
}
|
|
96
|
+
|
|
87
97
|
class Configorama {
|
|
88
98
|
constructor(fileOrObject, opts) {
|
|
89
99
|
/* attach sig events on async calls */
|
|
@@ -101,18 +111,21 @@ class Configorama {
|
|
|
101
111
|
|
|
102
112
|
this.filterCache = {}
|
|
103
113
|
|
|
114
|
+
this.foundVariables = []
|
|
115
|
+
|
|
104
116
|
const defaultSyntax = '\\${((?!AWS|stageVariables)[ ~:a-zA-Z0-9=+!@#%*<>?._\'",|\\-\\/\\(\\)\\\\]+?)}'
|
|
105
117
|
|
|
106
|
-
const
|
|
118
|
+
const varSyntax = options.syntax || defaultSyntax
|
|
107
119
|
let varRegex
|
|
108
|
-
if (typeof
|
|
109
|
-
varRegex = new RegExp(
|
|
120
|
+
if (typeof varSyntax === 'string') {
|
|
121
|
+
varRegex = new RegExp(varSyntax, 'g')
|
|
110
122
|
// this.variableSyntax = /\${((?!AWS)([ ~:a-zA-Z0-9=+!@#%*<>?._'",|\-\/\(\)\\]+?|(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*))}/
|
|
111
|
-
} else if (
|
|
112
|
-
varRegex =
|
|
123
|
+
} else if (varSyntax instanceof RegExp) {
|
|
124
|
+
varRegex = varSyntax
|
|
113
125
|
}
|
|
114
126
|
// console.log('varRegex', varRegex)
|
|
115
|
-
|
|
127
|
+
const variableSyntax = varRegex
|
|
128
|
+
this.variableSyntax = variableSyntax
|
|
116
129
|
|
|
117
130
|
// Set initial config object to populate
|
|
118
131
|
if (typeof fileOrObject === 'object') {
|
|
@@ -137,13 +150,6 @@ class Configorama {
|
|
|
137
150
|
this.opts
|
|
138
151
|
)
|
|
139
152
|
|
|
140
|
-
if (VERBOSE) {
|
|
141
|
-
console.log('───────────── Input Config ──────────────────────')
|
|
142
|
-
console.log()
|
|
143
|
-
deepLog(configObject)
|
|
144
|
-
console.log()
|
|
145
|
-
}
|
|
146
|
-
|
|
147
153
|
this.configFilePath = fileOrObject
|
|
148
154
|
// set config objects
|
|
149
155
|
this.config = configObject
|
|
@@ -274,7 +280,13 @@ class Configorama {
|
|
|
274
280
|
/* attach self matcher last */
|
|
275
281
|
this.variableTypes = this.variableTypes.concat(fallThroughSelfMatcher)
|
|
276
282
|
|
|
277
|
-
|
|
283
|
+
// const variablesKnownTypes = new RegExp(`^(${this.variableTypes.map((v) => v.prefix || v.type).join('|')}):`)
|
|
284
|
+
const variablesKnownTypes = combineRegexes(this.variableTypes.filter((v) => v.type !== 'string').map((v) => v.match))
|
|
285
|
+
// console.log('variablesKnownTypes', variablesKnownTypes)
|
|
286
|
+
this.variablesKnownTypes = variablesKnownTypes
|
|
287
|
+
|
|
288
|
+
// this.allPatterns = combineRegexes(...this.variableTypes.map((v) => v.match))
|
|
289
|
+
// console.log('this.allPatterns', this.allPatterns)
|
|
278
290
|
// console.log('this.variablesKnownTypes', this.variablesKnownTypes)
|
|
279
291
|
// process.exit(1)
|
|
280
292
|
// Additional filters on values. ${thing | filterFunction}
|
|
@@ -391,6 +403,69 @@ class Configorama {
|
|
|
391
403
|
this.functions = Object.assign({}, this.functions, options.functions)
|
|
392
404
|
}
|
|
393
405
|
|
|
406
|
+
if (VERBOSE) {
|
|
407
|
+
console.log('───────────── Input Config ──────────────────────')
|
|
408
|
+
console.log()
|
|
409
|
+
deepLog(this.originalConfig)
|
|
410
|
+
console.log()
|
|
411
|
+
|
|
412
|
+
const foundVariables = []
|
|
413
|
+
let loggedHeader = false
|
|
414
|
+
traverse(this.originalConfig).forEach(function (rawValue) {
|
|
415
|
+
if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
|
|
416
|
+
const configValuePath = this.path.join('.')
|
|
417
|
+
if (configValuePath.endsWith('Fn::Sub')) {
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
if (!loggedHeader) {
|
|
423
|
+
console.log('───────────── Variables Detected ──────────────────────')
|
|
424
|
+
console.log()
|
|
425
|
+
loggedHeader = true
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const nested = findNestedVariables(rawValue, variableSyntax, variablesKnownTypes, configValuePath)
|
|
429
|
+
/*
|
|
430
|
+
console.log(nested)
|
|
431
|
+
/** */
|
|
432
|
+
|
|
433
|
+
console.log(`▷ Path: ${configValuePath}`)
|
|
434
|
+
console.log('\n Key/value:')
|
|
435
|
+
console.log(` ${configValuePath}: ${rawValue}`)
|
|
436
|
+
if (nested.length > 0) {
|
|
437
|
+
const nestedCount = nested.length - 1
|
|
438
|
+
console.log('\n Variable:')
|
|
439
|
+
console.log(` ${nested[nested.length - 1].fullMatch}`)
|
|
440
|
+
|
|
441
|
+
if (nestedCount) {
|
|
442
|
+
console.log(`\n Contains ${nestedCount} nested values.`)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// non mutate remove last
|
|
446
|
+
const removeLast = (nested.length > 1) ? nested.slice(0, -1) : nested
|
|
447
|
+
console.log()
|
|
448
|
+
removeLast.forEach((v) => {
|
|
449
|
+
if (v.hasFallback) {
|
|
450
|
+
console.log(' Resolve order:')
|
|
451
|
+
console.log(` 1. ${v.valueBeforeFallback}`)
|
|
452
|
+
v.fallbackValues.forEach((f, i) => {
|
|
453
|
+
console.log(` ${i + 2}. ${f.fullMatch}${f.isFallback ? ' (Fallback string value)' : ''}`)
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
console.log()
|
|
459
|
+
foundVariables.push(rawValue)
|
|
460
|
+
}
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
console.log(`───────────── Found ${foundVariables.length} Variables ──────────────────────`)
|
|
464
|
+
console.log()
|
|
465
|
+
deepLog(foundVariables)
|
|
466
|
+
console.log()
|
|
467
|
+
}
|
|
468
|
+
|
|
394
469
|
this.deep = []
|
|
395
470
|
this.callCount = 0
|
|
396
471
|
}
|
|
@@ -639,7 +714,7 @@ class Configorama {
|
|
|
639
714
|
// Initial check if value has variable string in it
|
|
640
715
|
return isString(property.value) && property.value.match(this.variableSyntax)
|
|
641
716
|
})
|
|
642
|
-
|
|
717
|
+
|
|
643
718
|
return map(variables, (valueObject) => {
|
|
644
719
|
// console.log('valueObject', valueObject)
|
|
645
720
|
return this.populateValue(valueObject, false, '_populateVariables').then((populated) => {
|
|
@@ -1011,11 +1086,13 @@ class Configorama {
|
|
|
1011
1086
|
}
|
|
1012
1087
|
}
|
|
1013
1088
|
|
|
1089
|
+
const currentPath = valueObject.path.join('.')
|
|
1090
|
+
|
|
1014
1091
|
const errorMessage = `
|
|
1015
1092
|
Missing Value ${missingValue} - ${matchedString}
|
|
1016
1093
|
\nMake sure the property is being passed in correctly
|
|
1017
1094
|
\nFor variable:
|
|
1018
|
-
\n${
|
|
1095
|
+
\n${currentPath}: ${valueObject.originalSource}
|
|
1019
1096
|
`
|
|
1020
1097
|
throw new Error(errorMessage)
|
|
1021
1098
|
}
|
|
@@ -1480,9 +1557,10 @@ Unable to resolve configuration variable
|
|
|
1480
1557
|
}
|
|
1481
1558
|
|
|
1482
1559
|
// Variable NOT FOUND. Warn user
|
|
1560
|
+
const key = valueObject.path ? valueObject.path.join('.') : 'na'
|
|
1483
1561
|
const errorMessage = [
|
|
1484
1562
|
`Invalid variable reference syntax`,
|
|
1485
|
-
`Key: "${
|
|
1563
|
+
`Key: "${key}"`,
|
|
1486
1564
|
`Variable: "${variableString}" from ${propertyString} not found`,
|
|
1487
1565
|
]
|
|
1488
1566
|
|
|
@@ -1491,9 +1569,23 @@ Unable to resolve configuration variable
|
|
|
1491
1569
|
errorMessage.push('\n Default values for self referenced values are not allowed')
|
|
1492
1570
|
errorMessage.push(`\n Fix the ${propertyString} variable`)
|
|
1493
1571
|
}
|
|
1572
|
+
|
|
1573
|
+
let allowSpecialCase = false
|
|
1574
|
+
/* handle special cases for cloudformation ${Sub} values */
|
|
1575
|
+
if (this.originalConfig && key.endsWith('Fn::Sub')) {
|
|
1576
|
+
const params = this.originalConfig.Parameters || (this.originalConfig.parameters || {}).Parameters
|
|
1577
|
+
const resources = this.originalConfig.Resources || (this.originalConfig.resources || {}).Resources
|
|
1578
|
+
/* Cloudformation Resource References */
|
|
1579
|
+
if (resources && resources[variableString]) {
|
|
1580
|
+
allowSpecialCase = true
|
|
1581
|
+
} else if (params && params[variableString]) {
|
|
1582
|
+
allowSpecialCase = true
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
/* Todo handle stage variables */
|
|
1494
1586
|
|
|
1495
1587
|
/* Pass through unknown variables */
|
|
1496
|
-
if (this.opts.allowUnknownVars) {
|
|
1588
|
+
if (this.opts.allowUnknownVars || allowSpecialCase) {
|
|
1497
1589
|
// console.log('allowUnknownVars propertyString', propertyString)
|
|
1498
1590
|
const varMatches = propertyString.match(this.variableSyntax)
|
|
1499
1591
|
let allowUnknownVars = propertyString
|
|
@@ -1,63 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* @returns {Array} Array of match objects containing full match and captured group
|
|
6
|
-
*/
|
|
7
|
-
function findNestedVariablesx(input, regex, debug = false) {
|
|
8
|
-
let str = input
|
|
9
|
-
let matches = []
|
|
10
|
-
let match
|
|
11
|
-
let iteration = 0
|
|
12
|
-
|
|
13
|
-
console.log('input', input)
|
|
14
|
-
|
|
15
|
-
if (debug) console.log(`Initial string: ${str}`)
|
|
16
|
-
|
|
17
|
-
// Process string until no more matches are found
|
|
18
|
-
while (true) {
|
|
19
|
-
iteration++
|
|
20
|
-
if (debug) console.log(`\nIteration ${iteration}:`)
|
|
21
|
-
|
|
22
|
-
// Reset regex index
|
|
23
|
-
regex.lastIndex = 0
|
|
24
|
-
|
|
25
|
-
// Find the next match
|
|
26
|
-
match = regex.exec(str)
|
|
27
|
-
if (!match) break
|
|
28
|
-
|
|
29
|
-
// Log match details if in debug mode
|
|
30
|
-
if (debug) {
|
|
31
|
-
console.log(`Match: ${match[0]}`)
|
|
32
|
-
console.log(`Captured group: ${match[1]}`)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Store the match
|
|
36
|
-
matches.push({
|
|
37
|
-
fullMatch: match[0],
|
|
38
|
-
variable: match[1],
|
|
39
|
-
order: iteration
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// Replace the match with placeholder
|
|
43
|
-
str = str.replace(regex, `__REPLACED_${iteration - 1}__`)
|
|
44
|
-
if (debug) console.log(`After replacement: ${str}`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Replace the `__REPLACED_${iteration - 1}__` with the original match
|
|
48
|
-
matches = matches.map((match, index) => {
|
|
49
|
-
const indexOfReplaced = match.fullMatch.match(/__REPLACED_(\d+)__/)
|
|
50
|
-
if (indexOfReplaced) {
|
|
51
|
-
const replacedIndex = parseInt(indexOfReplaced[1])
|
|
52
|
-
match.fullMatch = match.fullMatch.replace(`__REPLACED_${replacedIndex}__`, matches[replacedIndex].variable)
|
|
53
|
-
match.variable = match.variable.replace(`__REPLACED_${replacedIndex}__`, matches[replacedIndex].variable)
|
|
54
|
-
}
|
|
55
|
-
return match
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
if (debug) console.log(`\nTotal matches found: ${matches.length}`)
|
|
59
|
-
return matches
|
|
60
|
-
}
|
|
1
|
+
const { splitByComma } = require('./splitByComma')
|
|
2
|
+
const trimQuotes = require('./trimSurroundingQuotes')
|
|
3
|
+
const FALLBACK_REGEX = /,\s*/
|
|
4
|
+
const VAR_MATCH_REGEX = /__VAR_\d+__/
|
|
61
5
|
|
|
62
6
|
/**
|
|
63
7
|
* Finds all nested variable interpolations in a string while preserving original syntax
|
|
@@ -77,10 +21,10 @@ function findNestedVariablesx(input, regex, debug = false) {
|
|
|
77
21
|
* @param {boolean} debug - Whether to print debug information
|
|
78
22
|
* @returns {Array} Array of match objects with fullMatch, variable, varString and other properties
|
|
79
23
|
*/
|
|
80
|
-
function findNestedVariables(input, regex, debug = false) {
|
|
24
|
+
function findNestedVariables(input, regex, variablesKnownTypes, location, debug = false) {
|
|
81
25
|
// Create a copy of the input for replacement tracking
|
|
82
|
-
let
|
|
83
|
-
// console.log('
|
|
26
|
+
let current = input
|
|
27
|
+
// console.log('current', current)
|
|
84
28
|
// Store matches with their positions in the original string
|
|
85
29
|
let matches = []
|
|
86
30
|
// Track original positions and replacements
|
|
@@ -99,7 +43,7 @@ function findNestedVariables(input, regex, debug = false) {
|
|
|
99
43
|
regex.lastIndex = 0
|
|
100
44
|
|
|
101
45
|
// Find the next match in the working string
|
|
102
|
-
match = regex.exec(
|
|
46
|
+
match = regex.exec(current)
|
|
103
47
|
if (!match) break
|
|
104
48
|
|
|
105
49
|
// Generate a unique placeholder
|
|
@@ -107,12 +51,15 @@ function findNestedVariables(input, regex, debug = false) {
|
|
|
107
51
|
|
|
108
52
|
// Store match details
|
|
109
53
|
const matchInfo = {
|
|
54
|
+
location,
|
|
55
|
+
value: input,
|
|
110
56
|
fullMatch: match[0],
|
|
111
|
-
variable: match[1],
|
|
112
|
-
|
|
57
|
+
variable: match[1].trim(),
|
|
58
|
+
varString: match[1],
|
|
59
|
+
resolveOrder: iteration,
|
|
113
60
|
start: match.index,
|
|
114
61
|
end: match.index + match[0].length,
|
|
115
|
-
placeholder
|
|
62
|
+
placeholder,
|
|
116
63
|
}
|
|
117
64
|
|
|
118
65
|
if (debug) {
|
|
@@ -132,29 +79,49 @@ function findNestedVariables(input, regex, debug = false) {
|
|
|
132
79
|
})
|
|
133
80
|
|
|
134
81
|
// Replace in working string (to find next match)
|
|
135
|
-
|
|
136
|
-
placeholder +
|
|
137
|
-
workingString.substring(match.index + match[0].length)
|
|
82
|
+
current = current.substring(0, match.index) + placeholder + current.substring(match.index + match[0].length)
|
|
138
83
|
|
|
139
|
-
if (debug) console.log(`After replacement: ${
|
|
84
|
+
if (debug) console.log(`After replacement: ${current}`)
|
|
140
85
|
}
|
|
141
86
|
|
|
142
87
|
if (debug) console.log(`\nTotal matches found: ${matches.length}`)
|
|
143
88
|
|
|
144
89
|
// We need to store varString - the variable string with placeholders
|
|
145
90
|
for (let i = 0; i < matches.length; i++) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
matches[i].varString
|
|
91
|
+
matches[i].varString = matches[i].variable
|
|
92
|
+
/* Save additional meta data about the variable */
|
|
93
|
+
// console.log('matches[i].varString', matches[i].varString)
|
|
94
|
+
if (variablesKnownTypes && variablesKnownTypes.test(matches[i].varString)) {
|
|
95
|
+
matches[i].varType = matches[i].varString.match(variablesKnownTypes)[1]
|
|
96
|
+
if (FALLBACK_REGEX.test(matches[i].varString)) {
|
|
97
|
+
const split = splitByComma(matches[i].varString, regex)
|
|
98
|
+
matches[i].hasFallback = true
|
|
99
|
+
|
|
100
|
+
matches[i].valueBeforeFallback = split[0]
|
|
101
|
+
// remove first element from split
|
|
102
|
+
matches[i].fallbackValues = split.slice(1).map((item) => {
|
|
103
|
+
// console.log('item', item)
|
|
104
|
+
const isVariable = variablesKnownTypes.test(item) || VAR_MATCH_REGEX.test(item)
|
|
105
|
+
const fallbackData = {
|
|
106
|
+
isVariable,
|
|
107
|
+
fullMatch: item,
|
|
108
|
+
variable: item
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!isVariable && typeof item === 'string') {
|
|
112
|
+
fallbackData.stringValue = trimQuotes(item)
|
|
113
|
+
fallbackData.isFallback = true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fallbackData
|
|
117
|
+
})
|
|
118
|
+
}
|
|
152
119
|
}
|
|
153
120
|
}
|
|
154
121
|
|
|
155
122
|
// Second pass: Reconstruct each variable with original nested syntax
|
|
156
123
|
// We need to do this recursively to ensure all placeholders are replaced properly
|
|
157
|
-
function replaceAllPlaceholders(text, matchesArray) {
|
|
124
|
+
function replaceAllPlaceholders(text = '', matchesArray, key = 'fullMatch') {
|
|
158
125
|
let result = text
|
|
159
126
|
let needsAnotherPass = false
|
|
160
127
|
|
|
@@ -162,17 +129,14 @@ function findNestedVariables(input, regex, debug = false) {
|
|
|
162
129
|
for (let i = 0; i < matchesArray.length; i++) {
|
|
163
130
|
const m = matchesArray[i]
|
|
164
131
|
if (result.includes(m.placeholder)) {
|
|
165
|
-
result = result.replace(
|
|
166
|
-
new RegExp(m.placeholder, 'g'),
|
|
167
|
-
m.fullMatch
|
|
168
|
-
)
|
|
132
|
+
result = result.replace(new RegExp(m.placeholder, 'g'), m[key])
|
|
169
133
|
needsAnotherPass = true
|
|
170
134
|
}
|
|
171
135
|
}
|
|
172
136
|
|
|
173
137
|
// If we made replacements, we might need another pass to handle nested placeholders
|
|
174
138
|
if (needsAnotherPass) {
|
|
175
|
-
return replaceAllPlaceholders(result, matchesArray)
|
|
139
|
+
return replaceAllPlaceholders(result, matchesArray, key)
|
|
176
140
|
}
|
|
177
141
|
|
|
178
142
|
return result
|
|
@@ -186,10 +150,19 @@ function findNestedVariables(input, regex, debug = false) {
|
|
|
186
150
|
if (!currentMatch.fullMatch.includes('__VAR_') && !currentMatch.variable.includes('__VAR_')) {
|
|
187
151
|
continue
|
|
188
152
|
}
|
|
153
|
+
|
|
154
|
+
if (currentMatch.hasFallback) {
|
|
155
|
+
currentMatch.fallbackValues.forEach((item) => {
|
|
156
|
+
item.fullMatch = replaceAllPlaceholders(item.fullMatch, matches, 'fullMatch')
|
|
157
|
+
item.variable = replaceAllPlaceholders(item.variable, matches, 'variable')
|
|
158
|
+
})
|
|
159
|
+
}
|
|
189
160
|
|
|
190
161
|
// Reconstruct with all nested variables
|
|
191
162
|
currentMatch.fullMatch = replaceAllPlaceholders(currentMatch.fullMatch, matches)
|
|
192
163
|
currentMatch.variable = replaceAllPlaceholders(currentMatch.variable, matches)
|
|
164
|
+
|
|
165
|
+
|
|
193
166
|
}
|
|
194
167
|
|
|
195
168
|
if (debug) {
|
|
@@ -207,6 +180,67 @@ function findNestedVariables(input, regex, debug = false) {
|
|
|
207
180
|
}
|
|
208
181
|
|
|
209
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Processes nested variable interpolations in a string and collects all matches
|
|
185
|
+
* @param {string} input - The input string containing variable interpolations
|
|
186
|
+
* @param {boolean} debug - Whether to print debug information
|
|
187
|
+
* @returns {Array} Array of match objects containing full match and captured group
|
|
188
|
+
*/
|
|
189
|
+
function findNestedVariablesx(input, regex, variablesKnownTypes, debug = false) {
|
|
190
|
+
let str = input
|
|
191
|
+
let matches = []
|
|
192
|
+
let match
|
|
193
|
+
let iteration = 0
|
|
194
|
+
|
|
195
|
+
console.log('input', input)
|
|
196
|
+
|
|
197
|
+
if (debug) console.log(`Initial string: ${str}`)
|
|
198
|
+
|
|
199
|
+
// Process string until no more matches are found
|
|
200
|
+
while (true) {
|
|
201
|
+
iteration++
|
|
202
|
+
if (debug) console.log(`\nIteration ${iteration}:`)
|
|
203
|
+
|
|
204
|
+
// Reset regex index
|
|
205
|
+
regex.lastIndex = 0
|
|
206
|
+
|
|
207
|
+
// Find the next match
|
|
208
|
+
match = regex.exec(str)
|
|
209
|
+
if (!match) break
|
|
210
|
+
|
|
211
|
+
// Log match details if in debug mode
|
|
212
|
+
if (debug) {
|
|
213
|
+
console.log(`Match: ${match[0]}`)
|
|
214
|
+
console.log(`Captured group: ${match[1]}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Store the match
|
|
218
|
+
matches.push({
|
|
219
|
+
fullMatch: match[0],
|
|
220
|
+
variable: match[1],
|
|
221
|
+
order: iteration
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
// Replace the match with placeholder
|
|
225
|
+
str = str.replace(regex, `__REPLACED_${iteration - 1}__`)
|
|
226
|
+
if (debug) console.log(`After replacement: ${str}`)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Replace the `__REPLACED_${iteration - 1}__` with the original match
|
|
230
|
+
matches = matches.map((match, index) => {
|
|
231
|
+
const indexOfReplaced = match.fullMatch.match(/__REPLACED_(\d+)__/)
|
|
232
|
+
if (indexOfReplaced) {
|
|
233
|
+
const replacedIndex = parseInt(indexOfReplaced[1])
|
|
234
|
+
match.fullMatch = match.fullMatch.replace(`__REPLACED_${replacedIndex}__`, matches[replacedIndex].variable)
|
|
235
|
+
match.variable = match.variable.replace(`__REPLACED_${replacedIndex}__`, matches[replacedIndex].variable)
|
|
236
|
+
}
|
|
237
|
+
return match
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
if (debug) console.log(`\nTotal matches found: ${matches.length}`)
|
|
241
|
+
return matches
|
|
242
|
+
}
|
|
243
|
+
|
|
210
244
|
// // Test with the example
|
|
211
245
|
// const regex = /\${((?!AWS|stageVariables)[ ~:a-zA-Z0-9=+!@#%*<>?._'",|\-\/\(\)\\]+?)}/g
|
|
212
246
|
// const input = '${file(./config.${opt:stage, ${defaultStage}}.json):CREDS}'
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
const { test } = require('uvu');
|
|
2
2
|
const assert = require('uvu/assert');
|
|
3
3
|
const { findNestedVariables } = require('./find-nested-variables');
|
|
4
|
+
const deepLog = require('./deep-log')
|
|
4
5
|
|
|
5
6
|
// Define the regex pattern as used in the main function
|
|
6
7
|
const regex = /\${((?!AWS|stageVariables)[ ~:a-zA-Z0-9=+!@#%*<>?._'",|\-\/\(\)\\]+?)}/g;
|
|
8
|
+
const variablesKnownTypes = /(^env:|^opt:|^self:|^file\((~?[\{\}\:\$a-zA-Z0-9._\-\/,'" ]+?)\)|^git:|(\${)?deep:\d+(\.[^}]+)*()}?)/
|
|
7
9
|
|
|
8
10
|
test('findNestedVariables - simple variables', () => {
|
|
9
11
|
const input = '${simple}';
|
|
10
|
-
const result = findNestedVariables(input, regex);
|
|
12
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes, 'key');
|
|
13
|
+
deepLog('result', result)
|
|
11
14
|
|
|
12
15
|
assert.equal(result.length, 1);
|
|
13
16
|
assert.equal(result[0].fullMatch, '${simple}');
|
|
14
17
|
assert.equal(result[0].variable, 'simple');
|
|
15
|
-
assert.equal(result[0].
|
|
18
|
+
assert.equal(result[0].resolveOrder, 1, 'order should be 1');
|
|
16
19
|
});
|
|
17
20
|
|
|
18
21
|
test('findNestedVariables - complex variable with colon syntax', () => {
|
|
19
22
|
const input = '${opt:stage, dev}';
|
|
20
|
-
const result = findNestedVariables(input, regex);
|
|
23
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
21
24
|
|
|
22
25
|
assert.equal(result.length, 1);
|
|
23
26
|
assert.equal(result[0].fullMatch, '${opt:stage, dev}');
|
|
@@ -26,7 +29,7 @@ test('findNestedVariables - complex variable with colon syntax', () => {
|
|
|
26
29
|
|
|
27
30
|
test('findNestedVariables - one level nesting', () => {
|
|
28
31
|
const input = '${file(./config.${stage}.json)}';
|
|
29
|
-
const result = findNestedVariables(input, regex);
|
|
32
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
30
33
|
|
|
31
34
|
assert.equal(result.length, 2);
|
|
32
35
|
// The innermost variable should be found first
|
|
@@ -39,7 +42,7 @@ test('findNestedVariables - one level nesting', () => {
|
|
|
39
42
|
|
|
40
43
|
test('findNestedVariables - two levels of nesting', () => {
|
|
41
44
|
const input = '${file(./config.${opt:stage, ${defaultStage}}.json):CREDS}';
|
|
42
|
-
const result = findNestedVariables(input, regex);
|
|
45
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
43
46
|
|
|
44
47
|
assert.equal(result.length, 3);
|
|
45
48
|
// Innermost first
|
|
@@ -55,7 +58,7 @@ test('findNestedVariables - two levels of nesting', () => {
|
|
|
55
58
|
|
|
56
59
|
test('findNestedVariables - multiple separate variables', () => {
|
|
57
60
|
const input = 'Hello ${name}, welcome to ${service}!';
|
|
58
|
-
const result = findNestedVariables(input, regex);
|
|
61
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
59
62
|
|
|
60
63
|
assert.equal(result.length, 2);
|
|
61
64
|
assert.equal(result[0].fullMatch, '${name}');
|
|
@@ -64,7 +67,7 @@ test('findNestedVariables - multiple separate variables', () => {
|
|
|
64
67
|
|
|
65
68
|
test('findNestedVariables - complex mixed case', () => {
|
|
66
69
|
const input = '${db.${envOne}.host}:${db.${envTwo}.port} using ${credentials.${user.role}}';
|
|
67
|
-
const result = findNestedVariables(input, regex,
|
|
70
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
68
71
|
console.log('result', result)
|
|
69
72
|
assert.equal(result.length, 6);
|
|
70
73
|
// Check the correct nesting order
|
|
@@ -77,23 +80,40 @@ test('findNestedVariables - complex mixed case', () => {
|
|
|
77
80
|
|
|
78
81
|
test('findNestedVariables - empty string', () => {
|
|
79
82
|
const input = '';
|
|
80
|
-
const result = findNestedVariables(input, regex);
|
|
83
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
81
84
|
assert.equal(result.length, 0);
|
|
82
85
|
});
|
|
83
86
|
|
|
84
87
|
test('findNestedVariables - string with no variables', () => {
|
|
85
88
|
const input = 'This is a string with no variables';
|
|
86
|
-
const result = findNestedVariables(input, regex);
|
|
89
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
87
90
|
assert.equal(result.length, 0);
|
|
88
91
|
});
|
|
89
92
|
|
|
90
93
|
test('findNestedVariables - varString property for nested variables', () => {
|
|
91
94
|
const input = '${file(./config.${opt:stage, ${defaultStage}}.json)}';
|
|
92
|
-
const result = findNestedVariables(input, regex);
|
|
93
|
-
|
|
95
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
96
|
+
deepLog('result', result)
|
|
94
97
|
// Check varString property for the outermost variable
|
|
95
98
|
assert.equal(result[2].variable, 'file(./config.${opt:stage, ${defaultStage}}.json)');
|
|
96
99
|
});
|
|
97
100
|
|
|
101
|
+
test('findNestedVariables - mutliple fallback items', () => {
|
|
102
|
+
const input = '${file(./config.${opt:stage, ${opt:stageOne}, ${opt:stageTwo}, "three"}.json)}';
|
|
103
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes);
|
|
104
|
+
deepLog('result', result)
|
|
105
|
+
// Check varString property for the outermost variable
|
|
106
|
+
assert.equal(result[result.length - 1].variable, 'file(./config.${opt:stage, ${opt:stageOne}, ${opt:stageTwo}, "three"}.json)');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test.skip('findNestedVariables - deep', () => {
|
|
110
|
+
const input = '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}';
|
|
111
|
+
const result = findNestedVariables(input, regex, variablesKnownTypes, 'xyz');
|
|
112
|
+
deepLog('result', result)
|
|
113
|
+
// Check varString property for the outermost variable
|
|
114
|
+
assert.equal(result[result.length - 1].variable, 'file(./config.${opt:stage, ${opt:stageOne}, ${opt:stageTwo}, "three"}.json)');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
|
|
98
118
|
// Run all tests
|
|
99
119
|
test.run();
|
package/src/utils/x.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
process.env.foo = 'foo'
|
|
2
|
+
process.env.opt_stage = 'stage'
|
|
3
|
+
process.env.opt_stageOne = 'stageOne'
|
|
4
|
+
process.env.opt_stageTwo = 'stageTwo'
|
|
5
|
+
|
|
6
|
+
function getResolvers() {
|
|
7
|
+
return {
|
|
8
|
+
'file': async function fileResolver(arg) {
|
|
9
|
+
return 'filevalue'
|
|
10
|
+
},
|
|
11
|
+
'env:': async function envResolver(key) {
|
|
12
|
+
return process.env[key]
|
|
13
|
+
},
|
|
14
|
+
'opt:': async function optResolver(key) {
|
|
15
|
+
return process.env[`opt_${key}`] || null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function resolveVariables(variableArray) {
|
|
21
|
+
// Sort by resolveOrder to ensure inner variables are resolved first
|
|
22
|
+
const sortedArray = [...variableArray].sort((a, b) => a.resolveOrder - b.resolveOrder)
|
|
23
|
+
const resolvedValues = {}
|
|
24
|
+
const resolvers = getResolvers()
|
|
25
|
+
|
|
26
|
+
for (const item of sortedArray) {
|
|
27
|
+
let resolvedValue = null
|
|
28
|
+
|
|
29
|
+
if (item.hasFallback) {
|
|
30
|
+
// Try to resolve the primary value
|
|
31
|
+
const resolver = resolvers[item.varType]
|
|
32
|
+
const primaryKey = item.valueBeforeFallback.replace(`${item.varType}`, '')
|
|
33
|
+
resolvedValue = await resolver(primaryKey)
|
|
34
|
+
|
|
35
|
+
// If primary value is null, try fallbacks in order
|
|
36
|
+
if (resolvedValue === null) {
|
|
37
|
+
for (const fallback of item.fallbackValues) {
|
|
38
|
+
if (fallback.isVariable) {
|
|
39
|
+
// This is a reference to another variable that should be already resolved
|
|
40
|
+
const placeholderMatch = fallback.variable.match(/__VAR_(\d+)__/)
|
|
41
|
+
if (placeholderMatch) {
|
|
42
|
+
resolvedValue = resolvedValues[`__VAR_${placeholderMatch[1]}__`]
|
|
43
|
+
if (resolvedValue !== null) break
|
|
44
|
+
} else {
|
|
45
|
+
// It's a direct variable reference
|
|
46
|
+
const varType = fallback.variable.split(':')[0] + ':'
|
|
47
|
+
const varKey = fallback.variable.replace(`${varType}`, '')
|
|
48
|
+
resolvedValue = await resolvers[varType](varKey)
|
|
49
|
+
if (resolvedValue !== null) break
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// It's a literal value
|
|
53
|
+
resolvedValue = fallback.variable.replace(/"/g, '')
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} else if (item.varType.startsWith('file')) {
|
|
59
|
+
// Handle file type specially as it requires the resolved path
|
|
60
|
+
let filePath = item.varString
|
|
61
|
+
for (const [placeholder, value] of Object.entries(resolvedValues)) {
|
|
62
|
+
filePath = filePath.replace(placeholder, value)
|
|
63
|
+
}
|
|
64
|
+
resolvedValue = await resolvers['file'](filePath)
|
|
65
|
+
} else {
|
|
66
|
+
// Simple variable resolution
|
|
67
|
+
const resolver = resolvers[item.varType]
|
|
68
|
+
const key = item.variable.replace(`${item.varType}`, '')
|
|
69
|
+
resolvedValue = await resolver(key)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
resolvedValues[item.placeholder] = resolvedValue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return resolvedValues
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
const array = [
|
|
80
|
+
{
|
|
81
|
+
location: 'xyz',
|
|
82
|
+
value: '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}',
|
|
83
|
+
fullMatch: '${env:foo}',
|
|
84
|
+
variable: 'env:foo',
|
|
85
|
+
varString: 'env:foo',
|
|
86
|
+
resolveOrder: 1,
|
|
87
|
+
start: 45,
|
|
88
|
+
end: 55,
|
|
89
|
+
placeholder: '__VAR_0__',
|
|
90
|
+
varType: 'env:'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
location: 'xyz',
|
|
94
|
+
value: '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}',
|
|
95
|
+
fullMatch: '${opt:stageOne, ${env:foo}}',
|
|
96
|
+
variable: 'opt:stageOne, ${env:foo}',
|
|
97
|
+
varString: 'opt:stageOne, __VAR_0__',
|
|
98
|
+
resolveOrder: 2,
|
|
99
|
+
start: 29,
|
|
100
|
+
end: 55,
|
|
101
|
+
placeholder: '__VAR_1__',
|
|
102
|
+
varType: 'opt:',
|
|
103
|
+
hasFallback: true,
|
|
104
|
+
valueBeforeFallback: 'opt:stageOne',
|
|
105
|
+
fallbackValues: [
|
|
106
|
+
{
|
|
107
|
+
isVariable: true,
|
|
108
|
+
variable: 'env:foo',
|
|
109
|
+
fullMatch: '${env:foo}'
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
location: 'xyz',
|
|
115
|
+
value: '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}',
|
|
116
|
+
fullMatch: '${opt:stageTwo}',
|
|
117
|
+
variable: 'opt:stageTwo',
|
|
118
|
+
varString: 'opt:stageTwo',
|
|
119
|
+
resolveOrder: 3,
|
|
120
|
+
start: 40,
|
|
121
|
+
end: 55,
|
|
122
|
+
placeholder: '__VAR_2__',
|
|
123
|
+
varType: 'opt:'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
location: 'xyz',
|
|
127
|
+
value: '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}',
|
|
128
|
+
fullMatch: '${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }',
|
|
129
|
+
variable: 'opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three"',
|
|
130
|
+
varString: 'opt:stage, __VAR_1__, __VAR_2__, "three"',
|
|
131
|
+
resolveOrder: 4,
|
|
132
|
+
start: 16,
|
|
133
|
+
end: 60,
|
|
134
|
+
placeholder: '__VAR_3__',
|
|
135
|
+
varType: 'opt:',
|
|
136
|
+
hasFallback: true,
|
|
137
|
+
valueBeforeFallback: 'opt:stage',
|
|
138
|
+
fallbackValues: [
|
|
139
|
+
{
|
|
140
|
+
isVariable: true,
|
|
141
|
+
variable: 'opt:stageOne, ${env:foo}',
|
|
142
|
+
fullMatch: '${opt:stageOne, ${env:foo}}'
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
isVariable: true,
|
|
146
|
+
variable: 'opt:stageTwo',
|
|
147
|
+
fullMatch: '${opt:stageTwo}'
|
|
148
|
+
},
|
|
149
|
+
{ isVariable: false, variable: '"three"', fullMatch: '"three"' }
|
|
150
|
+
]
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
location: 'xyz',
|
|
154
|
+
value: '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}',
|
|
155
|
+
fullMatch: '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}',
|
|
156
|
+
variable: 'file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)',
|
|
157
|
+
varString: 'file(./config.__VAR_3__.json)',
|
|
158
|
+
resolveOrder: 5,
|
|
159
|
+
start: 0,
|
|
160
|
+
end: 32,
|
|
161
|
+
placeholder: '__VAR_4__',
|
|
162
|
+
varType: 'file(./config.__VAR_3__.json)'
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
resolveVariables(array)
|
|
168
|
+
.then((res) => {
|
|
169
|
+
console.log('res', res)
|
|
170
|
+
})
|
|
171
|
+
.catch((err) => {
|
|
172
|
+
console.log('err', err)
|
|
173
|
+
})
|