configorama 0.5.3 → 0.5.5
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 +33 -3
- package/package.json +4 -3
- package/src/main.js +92 -9
- 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
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const minimist = require('minimist')
|
|
5
5
|
const Configorama = require('./src/main')
|
|
6
|
+
const deepLog = require('./src/utils/deep-log')
|
|
6
7
|
|
|
7
8
|
// Parse command line arguments
|
|
8
9
|
const argv = minimist(process.argv.slice(2), {
|
|
9
10
|
string: ['output', 'o', 'format', 'f'],
|
|
10
|
-
boolean: ['help', 'h', 'version', 'v', 'debug', '
|
|
11
|
+
boolean: ['help', 'h', 'version', 'v', 'debug', 'allow-unknown', 'allow-undefined'],
|
|
11
12
|
alias: {
|
|
12
13
|
h: 'help',
|
|
13
14
|
v: 'version',
|
|
14
15
|
o: 'output',
|
|
15
16
|
f: 'format',
|
|
16
|
-
d: 'debug'
|
|
17
17
|
},
|
|
18
18
|
default: {
|
|
19
19
|
format: 'json'
|
|
@@ -74,6 +74,34 @@ const options = {
|
|
|
74
74
|
dynamicArgs: argv
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
if (options.dynamicArgs.verbose) {
|
|
78
|
+
console.log('───────────── Input Options ──────────────────────')
|
|
79
|
+
const dynamicArgs = options.dynamicArgs || {}
|
|
80
|
+
const {
|
|
81
|
+
_,
|
|
82
|
+
verbose,
|
|
83
|
+
v,
|
|
84
|
+
debug,
|
|
85
|
+
d,
|
|
86
|
+
help,
|
|
87
|
+
h,
|
|
88
|
+
version,
|
|
89
|
+
f,
|
|
90
|
+
format,
|
|
91
|
+
'allow-unknown': allowUnknown,
|
|
92
|
+
'allow-undefined': allowUndefined,
|
|
93
|
+
...rest
|
|
94
|
+
} = dynamicArgs
|
|
95
|
+
|
|
96
|
+
console.log()
|
|
97
|
+
if (Object.keys(rest).length) {
|
|
98
|
+
deepLog(rest)
|
|
99
|
+
} else {
|
|
100
|
+
console.log('No flag options provided. Set flags like --flag value')
|
|
101
|
+
}
|
|
102
|
+
console.log()
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
// Create Configorama instance
|
|
78
106
|
const configorama = new Configorama(inputFile, options)
|
|
79
107
|
// console.log('configorama', configorama)
|
|
@@ -104,7 +132,9 @@ configorama.init(argv)
|
|
|
104
132
|
fs.writeFileSync(argv.output, output)
|
|
105
133
|
console.log(`Configuration written to ${argv.output}`)
|
|
106
134
|
} else {
|
|
107
|
-
|
|
135
|
+
if (!argv.verbose) {
|
|
136
|
+
console.log(output)
|
|
137
|
+
}
|
|
108
138
|
}
|
|
109
139
|
})
|
|
110
140
|
.catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "configorama",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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)
|
|
@@ -79,10 +79,21 @@ const base64WrapperRegex = /\[_\[([A-Za-z0-9+/=\s]*)\]_\]/g
|
|
|
79
79
|
const logLines = '─────────────────────────────────────────────────'
|
|
80
80
|
|
|
81
81
|
let DEBUG = process.argv.includes('--debug') ? true : false
|
|
82
|
+
let VERBOSE = process.argv.includes('--verbose') ? true : false
|
|
82
83
|
// DEBUG = true
|
|
83
84
|
|
|
84
85
|
const ENABLE_FUNCTIONS = true
|
|
85
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
|
+
|
|
86
97
|
class Configorama {
|
|
87
98
|
constructor(fileOrObject, opts) {
|
|
88
99
|
/* attach sig events on async calls */
|
|
@@ -100,18 +111,21 @@ class Configorama {
|
|
|
100
111
|
|
|
101
112
|
this.filterCache = {}
|
|
102
113
|
|
|
114
|
+
this.foundVariables = []
|
|
115
|
+
|
|
103
116
|
const defaultSyntax = '\\${((?!AWS|stageVariables)[ ~:a-zA-Z0-9=+!@#%*<>?._\'",|\\-\\/\\(\\)\\\\]+?)}'
|
|
104
117
|
|
|
105
|
-
const
|
|
118
|
+
const varSyntax = options.syntax || defaultSyntax
|
|
106
119
|
let varRegex
|
|
107
|
-
if (typeof
|
|
108
|
-
varRegex = new RegExp(
|
|
120
|
+
if (typeof varSyntax === 'string') {
|
|
121
|
+
varRegex = new RegExp(varSyntax, 'g')
|
|
109
122
|
// this.variableSyntax = /\${((?!AWS)([ ~:a-zA-Z0-9=+!@#%*<>?._'",|\-\/\(\)\\]+?|(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*))}/
|
|
110
|
-
} else if (
|
|
111
|
-
varRegex =
|
|
123
|
+
} else if (varSyntax instanceof RegExp) {
|
|
124
|
+
varRegex = varSyntax
|
|
112
125
|
}
|
|
113
126
|
// console.log('varRegex', varRegex)
|
|
114
|
-
|
|
127
|
+
const variableSyntax = varRegex
|
|
128
|
+
this.variableSyntax = variableSyntax
|
|
115
129
|
|
|
116
130
|
// Set initial config object to populate
|
|
117
131
|
if (typeof fileOrObject === 'object') {
|
|
@@ -266,7 +280,13 @@ class Configorama {
|
|
|
266
280
|
/* attach self matcher last */
|
|
267
281
|
this.variableTypes = this.variableTypes.concat(fallThroughSelfMatcher)
|
|
268
282
|
|
|
269
|
-
|
|
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)
|
|
270
290
|
// console.log('this.variablesKnownTypes', this.variablesKnownTypes)
|
|
271
291
|
// process.exit(1)
|
|
272
292
|
// Additional filters on values. ${thing | filterFunction}
|
|
@@ -383,6 +403,63 @@ class Configorama {
|
|
|
383
403
|
this.functions = Object.assign({}, this.functions, options.functions)
|
|
384
404
|
}
|
|
385
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
|
+
if (!loggedHeader) {
|
|
417
|
+
console.log('───────────── Variables Detected ──────────────────────')
|
|
418
|
+
console.log()
|
|
419
|
+
loggedHeader = true
|
|
420
|
+
}
|
|
421
|
+
const configValuePath = this.path.join('.')
|
|
422
|
+
const nested = findNestedVariables(rawValue, variableSyntax, variablesKnownTypes, configValuePath)
|
|
423
|
+
/*
|
|
424
|
+
console.log(nested)
|
|
425
|
+
/** */
|
|
426
|
+
|
|
427
|
+
console.log(`▷ Path: ${configValuePath}`)
|
|
428
|
+
console.log('\n Key/value:')
|
|
429
|
+
console.log(` ${configValuePath}: ${rawValue}`)
|
|
430
|
+
if (nested.length > 0) {
|
|
431
|
+
const nestedCount = nested.length - 1
|
|
432
|
+
console.log('\n Variable:')
|
|
433
|
+
console.log(` ${nested[nested.length - 1].fullMatch}`)
|
|
434
|
+
|
|
435
|
+
if (nestedCount) {
|
|
436
|
+
console.log(`\n Contains ${nestedCount} nested values.`)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// non mutate remove last
|
|
440
|
+
const removeLast = (nested.length > 1) ? nested.slice(0, -1) : nested
|
|
441
|
+
console.log()
|
|
442
|
+
removeLast.forEach((v) => {
|
|
443
|
+
if (v.hasFallback) {
|
|
444
|
+
console.log(' Resolve order:')
|
|
445
|
+
console.log(` 1. ${v.valueBeforeFallback}`)
|
|
446
|
+
v.fallbackValues.forEach((f, i) => {
|
|
447
|
+
console.log(` ${i + 2}. ${f.fullMatch}${f.isFallback ? ' (Fallback string value)' : ''}`)
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
console.log()
|
|
453
|
+
foundVariables.push(rawValue)
|
|
454
|
+
}
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
console.log(`───────────── Found ${foundVariables.length} Variables ──────────────────────`)
|
|
458
|
+
console.log()
|
|
459
|
+
deepLog(foundVariables)
|
|
460
|
+
console.log()
|
|
461
|
+
}
|
|
462
|
+
|
|
386
463
|
this.deep = []
|
|
387
464
|
this.callCount = 0
|
|
388
465
|
}
|
|
@@ -478,6 +555,12 @@ class Configorama {
|
|
|
478
555
|
if (this.mergeKeys && this.config) {
|
|
479
556
|
this.config = mergeByKeys(this.config, '', this.mergeKeys)
|
|
480
557
|
}
|
|
558
|
+
if (VERBOSE) {
|
|
559
|
+
console.log('───────────── Resolved Config ───────────────────')
|
|
560
|
+
console.log()
|
|
561
|
+
deepLog(this.config)
|
|
562
|
+
console.log()
|
|
563
|
+
}
|
|
481
564
|
return this.config
|
|
482
565
|
})
|
|
483
566
|
})
|
|
@@ -625,7 +708,7 @@ class Configorama {
|
|
|
625
708
|
// Initial check if value has variable string in it
|
|
626
709
|
return isString(property.value) && property.value.match(this.variableSyntax)
|
|
627
710
|
})
|
|
628
|
-
|
|
711
|
+
|
|
629
712
|
return map(variables, (valueObject) => {
|
|
630
713
|
// console.log('valueObject', valueObject)
|
|
631
714
|
return this.populateValue(valueObject, false, '_populateVariables').then((populated) => {
|
|
@@ -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
|
+
})
|