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 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
- deepLog(rest)
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.4",
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 testlib && uvu tests \".*\\.test.js$\" ",
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
- "testlib": "uvu src \".*\\.test.js$\"",
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 variableSyntax = options.syntax || defaultSyntax
118
+ const varSyntax = options.syntax || defaultSyntax
107
119
  let varRegex
108
- if (typeof variableSyntax === 'string') {
109
- varRegex = new RegExp(variableSyntax, 'g')
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 (variableSyntax instanceof RegExp) {
112
- varRegex = variableSyntax
123
+ } else if (varSyntax instanceof RegExp) {
124
+ varRegex = varSyntax
113
125
  }
114
126
  // console.log('varRegex', varRegex)
115
- this.variableSyntax = varRegex
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
- this.variablesKnownTypes = new RegExp(`^(${this.variableTypes.map((v) => v.prefix || v.type).join('|')}):`)
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
- // console.log('variables', variables)
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${valueObject.path}: ${valueObject.originalSource}
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: "${valueObject.path ? valueObject.path.join('.') : 'na'}"`,
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
@@ -7,6 +7,7 @@ function getValueFromString(variableString) {
7
7
  }
8
8
 
9
9
  module.exports = {
10
+ type: 'string',
10
11
  match: stringRefSyntax,
11
12
  resolver: getValueFromString
12
13
  }
@@ -1,63 +1,7 @@
1
- /**
2
- * Processes nested variable interpolations in a string and collects all matches
3
- * @param {string} input - The input string containing variable interpolations
4
- * @param {boolean} debug - Whether to print debug information
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 workingString = input
83
- // console.log('workingString', workingString)
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(workingString)
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
- order: iteration,
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
- workingString = workingString.substring(0, match.index) +
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: ${workingString}`)
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
- // First match, the variable is the same as varString
147
- if (i === 0) {
148
- matches[i].varString = matches[i].variable
149
- } else {
150
- // For other matches, we need to copy the original variable with placeholders
151
- matches[i].varString = matches[i].variable
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].order, 1);
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, true);
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
- console.log('result', result)
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
+ })