configorama 0.6.7 → 0.6.8

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/src/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const os = require('os')
2
2
  const path = require('path')
3
3
  const fs = require('fs')
4
- /*
4
+ /* // disable logs to find broken tests
5
5
  console.log = () => {}
6
6
  // process.exit(1)
7
7
  /** */
@@ -91,7 +91,7 @@ const logLines = '────────────────────
91
91
  let DEBUG = process.argv.includes('--debug') ? true : false
92
92
  let VERBOSE = process.argv.includes('--verbose') ? true : false
93
93
  // DEBUG = true
94
-
94
+ let DEBUG_TYPE = false
95
95
  const ENABLE_FUNCTIONS = true
96
96
 
97
97
  function combineRegexes(regexes) {
@@ -104,6 +104,136 @@ function combineRegexes(regexes) {
104
104
  return new RegExp(`(${patterns.join('|')})`)
105
105
  }
106
106
 
107
+ /**
108
+ * Preprocess config to fix malformed fallback references
109
+ * @param {Object} configObject - The parsed configuration object
110
+ * @param {RegExp} variableSyntax - The variable syntax regex to use
111
+ * @returns {Object} The preprocessed configuration object
112
+ */
113
+ function preProcess(configObject, variableSyntax) {
114
+ // Known reference prefixes that should be wrapped in ${}
115
+ const refPrefixes = ['self:', 'opt:', 'env:', 'file:', 'text:', 'deep:']
116
+
117
+ /**
118
+ * Fix malformed fallback references in a string
119
+ * @param {string} str - String potentially containing variables
120
+ * @returns {string} String with fixed fallback references
121
+ */
122
+ function fixFallbacksInString(str) {
123
+ if (typeof str !== 'string') return str
124
+
125
+ let result = str
126
+ let changed = true
127
+
128
+ // Keep iterating until no more changes (to handle nested variables)
129
+ while (changed) {
130
+ changed = false
131
+
132
+ // Find innermost ${...} blocks (ones that don't contain other ${)
133
+ let i = 0
134
+ while (i < result.length) {
135
+ if (result[i] === '$' && result[i + 1] === '{') {
136
+ const start = i
137
+ let braceCount = 1
138
+ let j = i + 2
139
+
140
+ // Find the matching closing brace by counting { and }
141
+ while (j < result.length && braceCount > 0) {
142
+ if (result[j] === '{') {
143
+ braceCount++
144
+ } else if (result[j] === '}') {
145
+ braceCount--
146
+ }
147
+ j++
148
+ }
149
+
150
+ if (braceCount === 0) {
151
+ const end = j
152
+ const match = result.substring(start, end)
153
+ const content = result.substring(start + 2, end - 1)
154
+
155
+ // Only process if there's a comma (indicating fallback syntax)
156
+ if (content.includes(',')) {
157
+ // Split by comma
158
+ const parts = splitByComma(content, variableSyntax)
159
+
160
+ if (parts.length > 1) {
161
+ // Check if the first part has nested ${} - if so, skip this (process inner ones first)
162
+ const firstPart = parts[0]
163
+ if (firstPart.includes('${')) {
164
+ i = start + 2 // Move past ${ to find inner variables
165
+ continue
166
+ }
167
+
168
+ // Check each part after the first (these are fallback values)
169
+ const fixed = parts.map((part, index) => {
170
+ if (index === 0) {
171
+ return part // Keep the main reference as-is
172
+ }
173
+
174
+ const trimmed = part.trim()
175
+
176
+ // Check if this looks like a reference but is not wrapped
177
+ const looksLikeRef = refPrefixes.some(prefix => trimmed.startsWith(prefix))
178
+ const alreadyWrapped = trimmed.startsWith('${') && trimmed.endsWith('}')
179
+
180
+ if (looksLikeRef && !alreadyWrapped) {
181
+ return ` \${${trimmed}}`
182
+ }
183
+
184
+ return ` ${trimmed}`
185
+ })
186
+
187
+ const replacement = `\${${fixed.join(',')}}`
188
+ if (replacement !== match) {
189
+ result = result.substring(0, start) + replacement + result.substring(end)
190
+ changed = true
191
+ break // Restart search from beginning
192
+ }
193
+ }
194
+ }
195
+
196
+ i = start + 2 // Move past ${ to continue searching for nested variables
197
+ } else {
198
+ i++
199
+ }
200
+ } else {
201
+ i++
202
+ }
203
+ }
204
+ }
205
+
206
+ return result
207
+ }
208
+
209
+ /**
210
+ * Recursively traverse and fix the config object
211
+ */
212
+ function traverseAndFix(obj) {
213
+ if (typeof obj === 'string') {
214
+ return fixFallbacksInString(obj)
215
+ }
216
+
217
+ if (Array.isArray(obj)) {
218
+ return obj.map(item => traverseAndFix(item))
219
+ }
220
+
221
+ if (obj !== null && typeof obj === 'object') {
222
+ const result = {}
223
+ for (const key in obj) {
224
+ if (obj.hasOwnProperty(key)) {
225
+ result[key] = traverseAndFix(obj[key])
226
+ }
227
+ }
228
+ return result
229
+ }
230
+
231
+ return obj
232
+ }
233
+
234
+ return traverseAndFix(configObject)
235
+ }
236
+
107
237
  class Configorama {
108
238
  constructor(fileOrObject, opts) {
109
239
  /* attach sig events on async calls */
@@ -124,6 +254,9 @@ class Configorama {
124
254
 
125
255
  this.foundVariables = []
126
256
 
257
+ // Track variable resolutions for metadata (keyed by path)
258
+ this.resolutionTracking = {}
259
+
127
260
  const defaultSyntax = '\\${((?!AWS|stageVariables)[ ~:a-zA-Z0-9=+!@#%*<>?._\'",|\\-\\/\\(\\)\\\\]+?)}'
128
261
 
129
262
  const varSyntax = options.syntax || defaultSyntax
@@ -226,8 +359,7 @@ class Configorama {
226
359
  prefix: 'file',
227
360
  match: fileRefSyntax,
228
361
  resolver: (varString, o, x, pathValue) => {
229
- // console.log('pathValue getValueFromFile', pathValue)
230
- return this.getValueFromFile(varString)
362
+ return this.getValueFromFile(varString, { context: pathValue })
231
363
  },
232
364
  },
233
365
 
@@ -237,7 +369,7 @@ class Configorama {
237
369
  prefix: 'text',
238
370
  match: textRefSyntax,
239
371
  resolver: (varString, o, x, pathValue) => {
240
- return this.getValueFromFile(varString, { asRawText: true })
372
+ return this.getValueFromFile(varString, { asRawText: true, context: pathValue })
241
373
  },
242
374
  },
243
375
 
@@ -258,7 +390,7 @@ class Configorama {
258
390
  match: deepRefSyntax,
259
391
  resolver: (varString, o, x, pathValue) => {
260
392
  // console.log('>>>>>getValueFromDeep', varString)
261
- return this.getValueFromDeep(varString)
393
+ return this.getValueFromDeep(varString, pathValue)
262
394
  },
263
395
  },
264
396
  // Numbers
@@ -463,7 +595,7 @@ class Configorama {
463
595
 
464
596
  // If we have a file path but no config yet, parse it now
465
597
  if (this.configFilePath && !this.config) {
466
- const configObject = await parseFileContents(
598
+ let configObject = await parseFileContents(
467
599
  this.originalString,
468
600
  this.configFileType,
469
601
  this.configFilePath,
@@ -474,6 +606,16 @@ class Configorama {
474
606
  if (VERBOSE || showFoundVariables) {
475
607
  this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
476
608
  }
609
+ /*
610
+ console.log('before preprocess', configObject)
611
+ /** */
612
+ /* Preprocess step here */
613
+ configObject = preProcess(configObject, this.variableSyntax)
614
+ /*
615
+ console.log('after preprocess', configObject)
616
+ /** */
617
+ //process.exit(1)
618
+
477
619
  this.config = configObject
478
620
  this.originalConfig = cloneDeep(configObject)
479
621
  }
@@ -489,106 +631,14 @@ class Configorama {
489
631
  const variablesKnownTypes = this.variablesKnownTypes
490
632
 
491
633
  if (VERBOSE || showFoundVariables) {
492
- const foundVariables = []
493
- const variableData = {}
494
- let matchCount = 1
495
- // console.log('this.originalConfig', this.originalConfig)
496
- traverse(this.originalConfig).forEach(function (rawValue) {
497
- if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
498
- const configValuePath = this.path.join('.')
499
- if (configValuePath.endsWith('Fn::Sub')) {
500
- return
501
- }
502
-
503
- const nested = findNestedVariables(rawValue, variableSyntax, variablesKnownTypes, configValuePath)
504
- /*
505
- console.log('traverse nested result', nested)
506
- /** */
507
-
508
- // console.log(`▷ Path: ${configValuePath}`)
509
- // console.log('\n Key/value:')
510
- // console.log(` ${configValuePath}: ${rawValue}`)
511
- const lastItem = nested[nested.length - 1]
512
- const lastKeyPath = this.path[this.path.length - 1]
513
- const itemKey = (lastKeyPath.match(/[\d+]$/)) ? `${this.path[this.path.length - 2]}[${lastKeyPath}]` : lastKeyPath
514
- const key = lastItem.fullMatch
515
- const varData = {
516
- path: configValuePath,
517
- key: itemKey,
518
- value: rawValue,
519
- variable: lastItem.fullMatch,
520
- isRequired: false,
521
- defaultValue: undefined,
522
- matchIndex: matchCount++,
523
- // hasFallback: false,
524
- resolveOrder: [],
525
- resolveDetails: nested,
526
- }
527
- let defaultValueIsVar = false
528
- function calculateResolveOrder(item) {
529
- if (item && item.fallbackValues) {
530
- let hasResolvedFallback
531
- // console.log('item.fallbackValues', item.fallbackValues)
532
- const order = ([item.valueBeforeFallback]).concat(item.fallbackValues.map((f, i) => {
533
- // console.log('f', f)
534
- if (f.fallbackValues) {
535
- const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
536
- // console.log('nestedOrder', nestedOrder)
537
- // console.log('nestedResolvedFallback', nestedResolvedFallback)
538
- if (!hasResolvedFallback && nestedResolvedFallback) {
539
- hasResolvedFallback = nestedResolvedFallback
540
- }
541
- return nestedOrder // Return just the order part
542
- }
543
-
544
- if (!hasResolvedFallback && f.isResolvedFallback) {
545
- hasResolvedFallback = f.stringValue
546
- }
547
- if (f.isResolvedFallback) {
548
- hasResolvedFallback = f.stringValue
549
- }
550
-
551
- if (!hasResolvedFallback && f.isVariable) {
552
- defaultValueIsVar = f
553
- }
554
- // console.log('hasResolvedFallback', hasResolvedFallback)
555
- return `${f.stringValue || f.variable}${f.isResolvedFallback ? ' (Resolved default fallback)' : ''}`
556
- })).flat()
557
-
558
- return [order, hasResolvedFallback]
559
- }
560
- return [[item.variable], undefined] // Return array instead of just the value
561
- }
562
-
563
- const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
564
- varData.resolveOrder = resolveOrder
565
-
566
- if (defaultValueIsVar) {
567
- varData.defaultValueIsVar = defaultValueIsVar
568
- }
569
-
570
- // console.log('hasResolvedFallback', hasResolvedFallback)
571
- if (typeof hasResolvedFallback !== 'undefined') {
572
- varData.defaultValue = hasResolvedFallback
573
- }
574
-
575
- // console.log('varData.defaultValue', varData.defaultValue)
576
- if (typeof varData.defaultValue === 'undefined') {
577
- varData.isRequired = true
578
- }
579
-
580
- if (varData.resolveOrder.length > 1) {
581
- varData.hasFallback = true
582
- }
583
- //console.log('varData', varData)
584
-
585
- variableData[key] = (variableData[key] || []).concat(varData)
586
-
587
- foundVariables.push(rawValue)
588
- }
589
- })
590
-
591
- if (!foundVariables.length) {
634
+ // Use collectVariableMetadata to get variable info (DRY - don't duplicate logic)
635
+ const metadata = this.collectVariableMetadata()
636
+ deepLog('metadata', metadata)
637
+ process.exit(1)
638
+ const variableData = metadata.variables
639
+ const varKeys = Object.keys(variableData)
640
+
641
+ if (!varKeys.length) {
592
642
  logHeader('No Variables Found in Config')
593
643
  if (this.configFilePath) {
594
644
  console.log(`File: ${this.configFilePath}`)
@@ -611,10 +661,7 @@ class Configorama {
611
661
  console.log()
612
662
  }
613
663
 
614
- // make foundVariables array unique
615
- const finalFoundVariables = [...new Set(foundVariables)]
616
- if (finalFoundVariables.length > 0) {
617
- const varKeys = Object.keys(variableData)
664
+ if (varKeys.length > 0) {
618
665
  const fileName = this.configFilePath ? ` in ${this.configFilePath}` : ''
619
666
 
620
667
  logHeader(`Found ${varKeys.length} Variables${fileName}`)
@@ -626,9 +673,34 @@ class Configorama {
626
673
  const longestKey = varKeys.reduce((acc, k) => {
627
674
  return Math.max(acc, k.length)
628
675
  }, 0)
676
+ // Count all references including nested ones within other variables
677
+ const countAllReferences = (targetVariable) => {
678
+ // Start with direct references
679
+ let count = variableData[targetVariable].length
680
+
681
+ // Check all other variables for nested references to this variable
682
+ varKeys.forEach((otherKey) => {
683
+ if (otherKey === targetVariable) return
684
+
685
+ variableData[otherKey].forEach((instance) => {
686
+ if (instance.resolveDetails) {
687
+ instance.resolveDetails.forEach((detail) => {
688
+ // Check if this resolveDetail references our target variable
689
+ if (detail.fullMatch === targetVariable) {
690
+ count++
691
+ }
692
+ })
693
+ }
694
+ })
695
+ })
696
+
697
+ return count
698
+ }
699
+
629
700
  console.log(varKeys.map((k) => {
630
- const placesWord = variableData[k].length > 1 ? 'places' : 'place'
631
- return `- ${k.padEnd(longestKey).padEnd(longestKey + 10)} referenced ${variableData[k].length} ${placesWord}`
701
+ const refCount = countAllReferences(k)
702
+ const placesWord = refCount > 1 ? 'places' : 'place'
703
+ return `- ${k.padEnd(longestKey).padEnd(longestKey + 10)} referenced ${refCount} ${placesWord}`
632
704
  }).join('\n'))
633
705
  console.log()
634
706
  }
@@ -802,6 +874,7 @@ class Configorama {
802
874
  return this.populateObjectImpl(this.config).finally(() => {
803
875
  // TODO populate function values here?
804
876
  // console.log('Final Config', this.config)
877
+ // console.log(this.deep)
805
878
  const transform = this.runFunction.bind(this)
806
879
  const varSyntax = this.variableSyntax
807
880
  const leaves = this.leaves
@@ -812,7 +885,9 @@ class Configorama {
812
885
  /* Pass through unknown variables */
813
886
  if (!configoramaOpts.allowUndefinedValues && typeof rawValue === 'undefined') {
814
887
  const configValuePath = this.path.join('.')
888
+ /*
815
889
  console.log(this.path)
890
+ /** */
816
891
  const ogValue = dotProp.get(originalConfig, configValuePath)
817
892
  const varDisplay = ogValue ? `"${ogValue}" variable` : 'variable'
818
893
 
@@ -889,6 +964,214 @@ class Configorama {
889
964
  })
890
965
  })
891
966
  }
967
+
968
+ /**
969
+ * Collect metadata about all variables found in the configuration
970
+ * @returns {object} Metadata object containing variables, fileRefs, and summary
971
+ */
972
+ collectVariableMetadata() {
973
+ const variableSyntax = this.variableSyntax
974
+ const variablesKnownTypes = this.variablesKnownTypes
975
+ const foundVariables = []
976
+ const variableData = {}
977
+ const fileRefs = []
978
+ const fileGlobPatterns = []
979
+ let matchCount = 1
980
+
981
+ traverse(this.originalConfig).forEach(function (rawValue) {
982
+ if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
983
+ const configValuePath = this.path.join('.')
984
+ /* Skip Fn::Sub variables */
985
+ if (configValuePath.endsWith('Fn::Sub')) {
986
+ return
987
+ }
988
+
989
+ const nested = findNestedVariables(rawValue, variableSyntax, variablesKnownTypes, configValuePath)
990
+
991
+ const lastItem = nested[nested.length - 1]
992
+ const lastKeyPath = this.path[this.path.length - 1]
993
+ const itemKey = (lastKeyPath.match(/[\d+]$/)) ? `${this.path[this.path.length - 2]}[${lastKeyPath}]` : lastKeyPath
994
+ const key = lastItem.fullMatch
995
+ const varData = {
996
+ path: configValuePath,
997
+ key: itemKey,
998
+ value: rawValue,
999
+ variable: lastItem.fullMatch,
1000
+ isRequired: false,
1001
+ defaultValue: undefined,
1002
+ matchIndex: matchCount++,
1003
+ resolveOrder: [],
1004
+ resolveDetails: nested,
1005
+ }
1006
+ let defaultValueIsVar = false
1007
+
1008
+ function calculateResolveOrder(item) {
1009
+ if (item && item.fallbackValues) {
1010
+ let hasResolvedFallback
1011
+ const order = ([item.valueBeforeFallback]).concat(item.fallbackValues.map((f, i) => {
1012
+ if (f.fallbackValues) {
1013
+ const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
1014
+ if (!hasResolvedFallback && nestedResolvedFallback) {
1015
+ hasResolvedFallback = nestedResolvedFallback
1016
+ }
1017
+ return nestedOrder
1018
+ }
1019
+
1020
+ if (!hasResolvedFallback && f.isResolvedFallback) {
1021
+ hasResolvedFallback = f.stringValue
1022
+ }
1023
+ if (f.isResolvedFallback) {
1024
+ hasResolvedFallback = f.stringValue
1025
+ }
1026
+
1027
+ if (!hasResolvedFallback && f.isVariable) {
1028
+ defaultValueIsVar = f
1029
+ }
1030
+ return `${f.stringValue || f.variable}${f.isResolvedFallback ? ' (Resolved default fallback)' : ''}`
1031
+ })).flat()
1032
+
1033
+ return [order, hasResolvedFallback]
1034
+ }
1035
+ return [[item.variable], undefined]
1036
+ }
1037
+
1038
+ const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
1039
+ varData.resolveOrder = resolveOrder
1040
+
1041
+ if (defaultValueIsVar) {
1042
+ varData.defaultValueIsVar = defaultValueIsVar
1043
+ }
1044
+
1045
+ if (typeof hasResolvedFallback !== 'undefined') {
1046
+ varData.defaultValue = hasResolvedFallback
1047
+ }
1048
+
1049
+ if (typeof varData.defaultValue === 'undefined') {
1050
+ varData.isRequired = true
1051
+ }
1052
+
1053
+ if (varData.resolveOrder.length > 1) {
1054
+ varData.hasFallback = true
1055
+ }
1056
+
1057
+ // Extract file references
1058
+ nested.forEach((detail) => {
1059
+ if (detail.varType &&
1060
+ (detail.varType.startsWith('file(') || detail.varType.startsWith('text('))
1061
+ ) {
1062
+ const fileMatch = detail.varType.match(/^(?:file|text)\((.*?)\)/)
1063
+ if (fileMatch && fileMatch[1]) {
1064
+ let fileContent = fileMatch[1].trim()
1065
+
1066
+ // Split by comma to separate file path from parameters/fallback values
1067
+ const parts = splitCsv(fileContent)
1068
+ let filePath = parts[0].trim()
1069
+
1070
+ // Remove quotes if present
1071
+ filePath = filePath.replace(/^['"]|['"]$/g, '')
1072
+
1073
+ // Normalize path: ensure relative paths start with ./
1074
+ let normalizedPath = filePath
1075
+ if (
1076
+ !filePath.startsWith('./') &&
1077
+ !filePath.startsWith('../') &&
1078
+ !filePath.startsWith('/') &&
1079
+ !filePath.startsWith('~')
1080
+ ) {
1081
+ normalizedPath = './' + filePath
1082
+ }
1083
+
1084
+ // file .//
1085
+ if (normalizedPath.startsWith('.//')) {
1086
+ normalizedPath = normalizedPath.replace('.//', './')
1087
+ }
1088
+
1089
+ // Handle variables in file paths - just record the pattern
1090
+ if (!fileRefs.includes(normalizedPath)) {
1091
+ fileRefs.push(normalizedPath)
1092
+ }
1093
+
1094
+ // Check if path contains variables and create glob pattern
1095
+ if (normalizedPath.match(variableSyntax)) {
1096
+ // Replace variable syntax ${...} with * for glob pattern
1097
+ const globPattern = normalizedPath.replace(variableSyntax, '*')
1098
+ if (!fileGlobPatterns.includes(globPattern)) {
1099
+ fileGlobPatterns.push(globPattern)
1100
+ }
1101
+ }
1102
+ }
1103
+ }
1104
+ })
1105
+
1106
+ variableData[key] = (variableData[key] || []).concat(varData)
1107
+ foundVariables.push(rawValue)
1108
+ }
1109
+ })
1110
+
1111
+ // Make foundVariables array unique
1112
+ const finalFoundVariables = [...new Set(foundVariables)]
1113
+ const varKeys = Object.keys(variableData)
1114
+
1115
+ // Calculate summary using same logic as CLI display
1116
+ let requiredCount = 0
1117
+ let withDefaultsCount = 0
1118
+ varKeys.forEach((key) => {
1119
+ const instances = variableData[key]
1120
+ const firstInstance = instances[0]
1121
+
1122
+ // Check if truly required using same logic as display code
1123
+ let isTrulyRequired = false
1124
+ if (typeof firstInstance.defaultValue === 'undefined') {
1125
+ // Check for self-references that resolve to config values
1126
+ let dotPropArr = []
1127
+ if (firstInstance.defaultValueIsVar && (
1128
+ firstInstance.defaultValueIsVar.varType === 'self:' ||
1129
+ firstInstance.defaultValueIsVar.varType === 'dot.prop'
1130
+ )) {
1131
+ dotPropArr = [firstInstance.defaultValueIsVar]
1132
+ }
1133
+
1134
+ const hasDotPropOrSelf = instances.reduce((acc, v) => {
1135
+ const dotProp = v.resolveDetails.find((d) => d.varType === 'dot.prop')
1136
+ if (dotProp) {
1137
+ acc.push(dotProp)
1138
+ }
1139
+ if (v.resolveDetails && v.resolveDetails.length === 1 && v.resolveDetails[0].varType === 'self:') {
1140
+ acc.push(v.resolveDetails[0])
1141
+ }
1142
+ return acc
1143
+ }, dotPropArr)
1144
+
1145
+ if (!hasDotPropOrSelf.length) {
1146
+ isTrulyRequired = true
1147
+ } else {
1148
+ // Check if the self-reference resolves to a value
1149
+ const cleanPath = hasDotPropOrSelf[0].variable.replace('self:', '')
1150
+ const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1151
+ if (typeof dotPropValue === 'undefined') {
1152
+ isTrulyRequired = true
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ if (isTrulyRequired) {
1158
+ requiredCount++
1159
+ } else {
1160
+ withDefaultsCount++
1161
+ }
1162
+ })
1163
+
1164
+ return {
1165
+ variables: variableData,
1166
+ summary: {
1167
+ totalVariables: varKeys.length,
1168
+ requiredVariables: requiredCount,
1169
+ variablesWithDefaults: withDefaultsCount
1170
+ },
1171
+ fileRefs: fileRefs,
1172
+ fileGlobPatterns: fileGlobPatterns,
1173
+ }
1174
+ }
892
1175
  runFunction(variableString) {
893
1176
  // console.log('runFunction', variableString)
894
1177
  /* If json object value return it */
@@ -918,7 +1201,6 @@ class Configorama {
918
1201
  argsToPass = formatFunctionArgs(splitter)
919
1202
  }
920
1203
  // console.log('argsToPass runFunction', argsToPass)
921
-
922
1204
  // TODO check for camelCase version. | toUpperCase messes with function name
923
1205
  const theFunction = this.functions[functionName] || this.functions[functionName.toLowerCase()]
924
1206
 
@@ -1016,6 +1298,15 @@ class Configorama {
1016
1298
  }
1017
1299
  }
1018
1300
  leaf.originalSource = originalValue
1301
+
1302
+ // Check if we have existing resolution history from previous iterations
1303
+ const pathKey = thePath
1304
+ if (this.resolutionTracking[pathKey] && this.resolutionTracking[pathKey].resolutionHistory) {
1305
+ leaf.resolutionHistory = this.resolutionTracking[pathKey].resolutionHistory
1306
+ } else {
1307
+ leaf.resolutionHistory = []
1308
+ }
1309
+
1019
1310
  if (originalValue && isString(originalValue)) {
1020
1311
  const varString = cleanVariable(originalValue, this.variableSyntax, true, `getProperties ${this.callCount}`)
1021
1312
  if (varString.match(fileRefSyntax)) {
@@ -1027,7 +1318,6 @@ class Configorama {
1027
1318
  }
1028
1319
  return results
1029
1320
  }
1030
-
1031
1321
  /**
1032
1322
  * @typedef {TerminalProperty} TerminalPropertyPopulated
1033
1323
  * @property {Object} populated The populated value of the value at the path
@@ -1044,11 +1334,9 @@ class Configorama {
1044
1334
  // Initial check if value has variable string in it
1045
1335
  return isString(property.value) && property.value.match(this.variableSyntax)
1046
1336
  })
1047
-
1048
1337
  /*
1049
1338
  console.log(`variables at call count ${this.callCount}`, variables)
1050
1339
  /** */
1051
-
1052
1340
  /* Exclude git messages from being processed */
1053
1341
  // Was failing on git msgs like "xyz cron:pattern to cron(pattern) for improved clarity"
1054
1342
  if (this.callCount > 1) {
@@ -1060,7 +1348,6 @@ class Configorama {
1060
1348
  return true
1061
1349
  })
1062
1350
  }
1063
-
1064
1351
  return map(variables, (valueObject) => {
1065
1352
  // console.log('valueObject', valueObject)
1066
1353
  return this.populateValue(valueObject, false, '_populateVariables').then((populated) => {
@@ -1163,16 +1450,127 @@ class Configorama {
1163
1450
  */
1164
1451
  renderMatches(valueObject, matches, results) {
1165
1452
  /*
1453
+ console.log('valueObject', valueObject)
1166
1454
  console.log('RENDER', matches)
1167
1455
  console.log('RESULTS', results)
1168
1456
  /** */
1457
+
1458
+ /* Attach data to valueObject for parent details */
1459
+ if (matches.length === 1) {
1460
+ valueObject.currentVarDetails = matches[0]
1461
+ valueObject.currentVarDetails.result = results[0]
1462
+
1463
+ // Extract metadata from result if present
1464
+ let actualResult = results[0]
1465
+ let resolverType = undefined
1466
+ if (results[0] && typeof results[0] === 'object') {
1467
+ if (results[0].__internal_metadata) {
1468
+ actualResult = results[0].value
1469
+ resolverType = results[0].__resolverType
1470
+ } else if (results[0].__internal_only_flag) {
1471
+ // Don't extract value from __internal_only_flag objects, but grab resolverType if present
1472
+ actualResult = results[0]
1473
+ resolverType = results[0].__resolverType
1474
+ }
1475
+ }
1476
+ // valueObject.currentVarDetails.varType = results[0].__resolverType
1477
+
1478
+ // Track resolution history
1479
+ if (!valueObject.resolutionHistory) {
1480
+ valueObject.resolutionHistory = []
1481
+ }
1482
+
1483
+ // Extract clean result to avoid circular references
1484
+ // For __internal_only_flag objects (like deep resolver results), extract the value
1485
+ // For real data objects (like file contents), keep them as-is
1486
+ let cleanResult = actualResult
1487
+ if (actualResult && typeof actualResult === 'object' && actualResult.__internal_only_flag) {
1488
+ cleanResult = actualResult.value
1489
+ }
1490
+
1491
+ const historyEntry = {
1492
+ match: matches[0].match,
1493
+ variable: matches[0].variable,
1494
+ result: cleanResult,
1495
+ resultType: typeof cleanResult,
1496
+ valueBeforeResolution: valueObject.value,
1497
+ }
1498
+ if (resolverType) {
1499
+ historyEntry.varType = resolverType
1500
+ }
1501
+
1502
+ // Check if variable has fallback values (comma-separated)
1503
+ const variableParts = splitByComma(matches[0].variable)
1504
+ if (variableParts.length > 1) {
1505
+ historyEntry.hasFallback = true
1506
+ historyEntry.valueBeforeFallback = variableParts[0]
1507
+ historyEntry.fallbackValues = variableParts.slice(1).map((fallback) => {
1508
+ const trimmedFallback = fallback.trim()
1509
+ // Check if it's a variable reference
1510
+ const isVariable = trimmedFallback.match(this.variableSyntax) || trimmedFallback.match(this.variablesKnownTypes)
1511
+ const fallbackData = {
1512
+ isVariable: !!isVariable,
1513
+ fullMatch: trimmedFallback,
1514
+ variable: trimmedFallback,
1515
+ }
1516
+
1517
+ // If it's a literal string/number, parse it
1518
+ if (!isVariable) {
1519
+ // Check if it's a quoted string
1520
+ if (/^["'].*["']$/.test(trimmedFallback)) {
1521
+ fallbackData.stringValue = trimmedFallback.replace(/^["']|["']$/g, '')
1522
+ fallbackData.isResolvedFallback = true
1523
+ } else if (/^-?\d+(\.\d+)?$/.test(trimmedFallback)) {
1524
+ // It's a number
1525
+ fallbackData.numberValue = parseFloat(trimmedFallback)
1526
+ fallbackData.isResolvedFallback = true
1527
+ } else {
1528
+ fallbackData.stringValue = trimmedFallback
1529
+ fallbackData.isResolvedFallback = true
1530
+ }
1531
+ } else {
1532
+ // Extract varType from variable references
1533
+ const varTypeMatch = trimmedFallback.match(this.variablesKnownTypes)
1534
+ if (varTypeMatch && varTypeMatch[1]) {
1535
+ fallbackData.varType = varTypeMatch[1]
1536
+ }
1537
+ }
1538
+
1539
+ return fallbackData
1540
+ })
1541
+ }
1542
+
1543
+ // Only add to history if not a duplicate (same match + variable)
1544
+ const isDuplicate = valueObject.resolutionHistory.some(entry =>
1545
+ entry.match === historyEntry.match &&
1546
+ entry.variable === historyEntry.variable
1547
+ )
1548
+
1549
+ if (!isDuplicate) {
1550
+ valueObject.resolutionHistory.push(historyEntry)
1551
+ }
1552
+
1553
+ // Save resolution history to tracking map for persistence across iterations
1554
+ if (valueObject.path && valueObject.path.length) {
1555
+ const pathKey = valueObject.path.join('.')
1556
+ if (!this.resolutionTracking[pathKey]) {
1557
+ this.resolutionTracking[pathKey] = {
1558
+ path: pathKey,
1559
+ originalPropertyString: valueObject.originalSource,
1560
+ calls: []
1561
+ }
1562
+ }
1563
+ this.resolutionTracking[pathKey].resolutionHistory = valueObject.resolutionHistory
1564
+ }
1565
+ }
1566
+
1169
1567
  let result = valueObject.value
1170
1568
  for (let i = 0; i < matches.length; i += 1) {
1171
1569
  this.warnIfNotFound(matches[i].variable, results[i])
1172
1570
  // console.log('Render MATCHES', results[i])
1173
1571
  let valueToPop = results[i]
1174
1572
  // TODO refactor this. __internal_only_flag needed to stop clash with sync/async file resolution
1175
- if (results[i] && typeof results[i] === 'object' && results[i].__internal_only_flag) {
1573
+ if (results[i] && typeof results[i] === 'object' && (results[i].__internal_only_flag || results[i].__internal_metadata)) {
1176
1574
  valueToPop = results[i].value
1177
1575
  }
1178
1576
  result = this.populateVariable(valueObject, matches[i].match, valueToPop)
@@ -1216,7 +1614,10 @@ class Configorama {
1216
1614
  .then((result) => {
1217
1615
  // console.log('renderMatches result', result)
1218
1616
  if (root && isArray(matches)) {
1219
- return this.populateValue({ value: result.value }, root, 'self populateValue')
1617
+ return this.populateValue({
1618
+ value: result.value,
1619
+ resolutionHistory: result.resolutionHistory || valueObject.resolutionHistory || []
1620
+ }, root, 'self populateValue')
1220
1621
  }
1221
1622
  return result
1222
1623
  })
@@ -1254,11 +1655,11 @@ class Configorama {
1254
1655
 
1255
1656
  const parts = splitByComma(variable, this.variableSyntax)
1256
1657
  if (DEBUG) {
1257
- console.log('parts', parts)
1258
- console.log('parts variable:', variable)
1259
- console.log('parts originalVar:', originalVar)
1260
- console.log('parts property:', valueObject)
1261
- console.log('All parts:', parts)
1658
+ console.log('splitAndGet parts', parts)
1659
+ console.log('splitAndGet parts variable:', variable)
1660
+ console.log('splitAndGet parts originalVar:', originalVar)
1661
+ console.log('splitAndGet parts current valueObject:', valueObject)
1662
+ console.log('splitAndGet All parts:', parts)
1262
1663
  console.log('-----')
1263
1664
  }
1264
1665
  if (parts.length <= 1) {
@@ -1279,16 +1680,20 @@ class Configorama {
1279
1680
  populateVariable(valueObject, matchedString, valueToPopulate) {
1280
1681
  let property = valueObject.value
1281
1682
  // console.log('init property', property)
1282
- let DEBUG_TYPE = false
1683
+
1283
1684
  if (DEBUG) {
1284
1685
  console.log('────────START populateVar──────────────')
1285
- console.log('populateVar: valueToPopulate', valueToPopulate)
1286
- console.log('populateVar: typeof valueToPopulate', typeof valueToPopulate)
1287
- console.log(`populateVar: path "${valueObject.path}"`)
1288
- console.log(`populateVar: value \`${valueObject.value}\``)
1289
- console.log(`populateVar: originalSource \`${valueObject.originalSource}\``)
1290
- console.log('populateVar: property', property)
1291
- console.log('populateVar: matchedString', matchedString)
1686
+ console.log('populateVariable: valueObject', valueObject)
1687
+ console.log('populateVariable: valueToPopulate', valueToPopulate)
1688
+ console.log('populateVariable: typeof valueToPopulate', typeof valueToPopulate)
1689
+ console.log(`populateVariable: path "${valueObject.path}"`)
1690
+ console.log(`populateVariable: value \`${valueObject.value}\``)
1691
+ console.log(`populateVariable: originalSource \`${valueObject.originalSource}\``)
1692
+ console.log('populateVariable: property', property)
1693
+ console.log('populateVariable: matchedString', matchedString)
1694
+ if (valueObject.resolutionHistory && valueObject.resolutionHistory.length > 0) {
1695
+ console.log('populateVariable: resolutionHistory', JSON.stringify(valueObject.resolutionHistory, null, 2))
1696
+ }
1292
1697
  }
1293
1698
 
1294
1699
  const originalSrc = valueObject.originalSource || ''
@@ -1307,9 +1712,35 @@ class Configorama {
1307
1712
  if (DEBUG_TYPE) console.log('DEBUG_TYPE total replacement')
1308
1713
  const v = valueObject.value || ''
1309
1714
  property = valueToPopulate
1715
+ // console.log('hasFilters', hasFilters)
1716
+ // console.log('valueToPopulate', valueToPopulate)
1717
+ /* Check resolution history for parent details */
1718
+ if (valueObject.resolutionHistory && valueObject.resolutionHistory.length) {
1719
+ const currentDetails = valueObject.resolutionHistory[valueObject.resolutionHistory.length - 1]
1720
+
1721
+ // get 2nd to last item in resolution history
1722
+ const parentDetails = valueObject.resolutionHistory[valueObject.resolutionHistory.length - 2]
1723
+ /*
1724
+ console.log('currentDetails', currentDetails)
1725
+ console.log('parentDetails', parentDetails)
1726
+ /** */
1727
+
1728
+ /* Convert a fallback number to string */
1729
+ if (currentDetails &&
1730
+ currentDetails.resultType === 'number' &&
1731
+ parentDetails && parentDetails.resultType === 'string' &&
1732
+ parentDetails.result.match(/^\d+$/) && parentDetails.varType === 'env'
1733
+ ) {
1734
+ if (Number(parentDetails.result) === currentDetails.result) {
1735
+ property = String(valueToPopulate)
1736
+ }
1737
+ }
1738
+
1739
+ }
1310
1740
 
1311
1741
  /* Handle ${self:custom.ref, ''} with deep values */
1312
1742
  if (v.match(deepRefSyntax) && originalSrc.match(this.variableSyntax) && !v.match(/deep\:(\d*)\..*}$/)) {
1743
+ // console.log('deep ref syntax')
1313
1744
  // console.log('deep var', this.deep)
1314
1745
  // console.log('originalSrc', originalSrc)
1315
1746
  // console.log('value', v)
@@ -1412,7 +1843,12 @@ class Configorama {
1412
1843
  missingValue = this.deep[i]
1413
1844
  }
1414
1845
 
1415
- const cleanVar = cleanVariable(property, this.variableSyntax, true, `populateVariable fallback ${this.callCount}`)
1846
+ const cleanVar = cleanVariable(
1847
+ property,
1848
+ this.variableSyntax,
1849
+ true,
1850
+ `populateVariable fallback ${this.callCount}`
1851
+ )
1416
1852
  const cleanVarNoFilters = cleanVar.split('|')[0]
1417
1853
  const splitVars = splitByComma(cleanVarNoFilters)
1418
1854
  const nestedVar = findNestedVariable(splitVars, valueObject.originalSource)
@@ -1427,6 +1863,7 @@ class Configorama {
1427
1863
  value: fallbackStr,
1428
1864
  path: valueObject.path,
1429
1865
  originalSource: valueObject.originalSource,
1866
+ resolutionHistory: valueObject.resolutionHistory || [],
1430
1867
  // set __internal_only_flag to note this is object we make not a resolved value
1431
1868
  __internal_only_flag: true,
1432
1869
  caller: 'nestedVar',
@@ -1478,6 +1915,7 @@ Missing Value ${missingValue} - ${matchedString}
1478
1915
  value: finalProp, // prop to fix nested ¯\_(ツ)_/¯
1479
1916
  path: valueObject.path,
1480
1917
  originalSource: valueObject.originalSource,
1918
+ resolutionHistory: valueObject.resolutionHistory || [],
1481
1919
  // set __internal_only_flag to note this is object we make not a resolved value
1482
1920
  // __internal_only_flag: true
1483
1921
  }
@@ -1496,13 +1934,15 @@ Missing Value ${missingValue} - ${matchedString}
1496
1934
  }
1497
1935
  }
1498
1936
  */
1499
- // Does not match file refs with nested vars + args
1500
- // @TODO fix this for eval refs
1501
- // console.log('prop', prop)
1502
- // console.log('func', func)
1503
1937
 
1504
1938
  if (
1939
+ /* Not another variable reference */
1940
+ !prop.match(this.variableSyntax)
1941
+ &&
1942
+ /* Not file or text refs */
1505
1943
  !prop.match(fileRefSyntax)
1944
+ && !prop.match(textRefSyntax)
1945
+ /* Not eval refs */
1506
1946
  && !prop.match(getValueFromEval.match)
1507
1947
  // AND is not multiline value
1508
1948
  && (func && prop.split('\n').length < 3)) {
@@ -1523,14 +1963,21 @@ Missing Value ${missingValue} - ${matchedString}
1523
1963
 
1524
1964
  // console.log('foundFilters', foundFilters)
1525
1965
 
1526
- /* Apply filters if found */
1527
- //console.log('> property', property)
1528
- if (
1529
- foundFilters.length > 0 &&
1530
- typeof valueToPopulate === 'string' &&
1531
- !valueToPopulate.match(deepRefSyntax) &&
1966
+ let runFilters = false
1967
+ if (typeof valueToPopulate === 'number' && foundFilters.length) {
1968
+ runFilters = true
1969
+ } else if (
1970
+ typeof valueToPopulate === 'string' &&
1971
+ !valueToPopulate.match(deepRefSyntax) &&
1972
+ foundFilters.length &&
1532
1973
  !property.match(this.variableSyntax)
1533
1974
  ) {
1975
+ runFilters = true
1976
+ }
1977
+
1978
+ /* Apply filters if found */
1979
+ //console.log('> property', property)
1980
+ if (runFilters) {
1534
1981
  // If filter cache exists we need to remove filter that have already been run
1535
1982
  if (this.filterCache[valueObject.path]) {
1536
1983
  foundFilters = foundFilters.filter((filter) => {
@@ -1555,6 +2002,7 @@ Missing Value ${missingValue} - ${matchedString}
1555
2002
  value: property,
1556
2003
  path: valueObject.path,
1557
2004
  originalSource: valueObject.originalSource,
2005
+ resolutionHistory: valueObject.resolutionHistory || [],
1558
2006
  __internal_only_flag: true, // set __internal_only_flag to note this is object we make not a resolved value
1559
2007
  caller: 'end',
1560
2008
  count: this.callCount,
@@ -1596,15 +2044,22 @@ Missing Value ${missingValue} - ${matchedString}
1596
2044
  // console.log('propertyString', typeof propertyString)
1597
2045
  const variableValues = variableStrings.map((variableString) => {
1598
2046
  // This runs on nested variable resolution
1599
- return this.getValueFromSource(variableString, valueObject, 'overwrite')
2047
+ return this.getValueFromSource(variableString, valueObject, 'overwrite', valueObject.originalSource)
1600
2048
  })
1601
-
2049
+
1602
2050
  // console.log('variableValues', variableValues)
1603
2051
  return Promise.all(variableValues).then((values) => {
1604
2052
  let deepPropertyStr = propertyString
1605
2053
  let deepProperties = 0
1606
2054
  // console.log('overwrite values', valuesToUse)
1607
- values.forEach((value, index) => {
2055
+ // Extract actual values from metadata objects
2056
+ const extractedValues = values.map((value) => {
2057
+ if (value && typeof value === 'object' && (value.__internal_only_flag || value.__internal_metadata)) {
2058
+ return value.value
2059
+ }
2060
+ return value
2061
+ })
2062
+ extractedValues.forEach((value, index) => {
1608
2063
  // console.log('───────────────────────────────> value', value)
1609
2064
  if (isString(value) && value.match(this.variableSyntax)) {
1610
2065
  deepProperties += 1
@@ -1620,7 +2075,7 @@ Missing Value ${missingValue} - ${matchedString}
1620
2075
  })
1621
2076
  return deepProperties > 0
1622
2077
  ? Promise.resolve(deepPropertyStr) // return deep variable replacement of original
1623
- : Promise.resolve(values.find(isValidValue)) // resolve first valid value, else undefined
2078
+ : Promise.resolve(extractedValues.find(isValidValue)) // resolve first valid value, else undefined
1624
2079
  })
1625
2080
  }
1626
2081
  /**
@@ -1632,6 +2087,25 @@ Missing Value ${missingValue} - ${matchedString}
1632
2087
  // console.log('getValueFromSrc caller', caller)
1633
2088
  const propertyString = valueObject.value
1634
2089
  const pathValue = valueObject.path
2090
+
2091
+ // Track every call to getValueFromSource for metadata
2092
+ if (pathValue && pathValue.length) {
2093
+ const pathKey = pathValue.join('.')
2094
+ if (!this.resolutionTracking[pathKey]) {
2095
+ this.resolutionTracking[pathKey] = {
2096
+ path: pathKey,
2097
+ originalPropertyString: propertyString,
2098
+ calls: []
2099
+ }
2100
+ }
2101
+
2102
+ this.resolutionTracking[pathKey].calls.push({
2103
+ variableString: variableString,
2104
+ propertyString: propertyString,
2105
+ caller: caller
2106
+ })
2107
+ }
2108
+
1635
2109
  // console.log('getValueFromSrc propertyString', propertyString)
1636
2110
  // console.log(`tracker contains ${variableString}`, this.tracker.contains(variableString))
1637
2111
  if (this.tracker.contains(variableString)) {
@@ -1642,11 +2116,12 @@ Missing Value ${missingValue} - ${matchedString}
1642
2116
  let newHasFilter
1643
2117
  // Else lookup value from various sources
1644
2118
  if (DEBUG) {
1645
- console.log(`>>>>> getValueFromSrc() call - ${caller}`)
1646
- console.log('variableString:', variableString)
1647
- console.log('propertyString:', propertyString)
1648
- console.log('pathValue:', pathValue)
1649
- console.log('valueObject', valueObject)
2119
+ console.log(`>>>>> getValueFromSrc() caller - ${caller}`)
2120
+ console.log('getValueFromSource originalVar', originalVar)
2121
+ console.log('getValueFromSource variableString:', variableString)
2122
+ console.log('getValueFromSource propertyString:', propertyString)
2123
+ console.log('getValueFromSource pathValue:', valueObject.path)
2124
+ console.log('getValueFromSource valueObject:', valueObject)
1650
2125
  console.log('-----')
1651
2126
  }
1652
2127
 
@@ -1674,6 +2149,8 @@ Missing Value ${missingValue} - ${matchedString}
1674
2149
  })
1675
2150
  .map((f) => {
1676
2151
  return trim(f)
2152
+ // TODO refactor this. This is a temp fix for filters with nested vars.
2153
+ .replace(/}$/, '')
1677
2154
  })
1678
2155
  // console.log('filters to run', _filter)
1679
2156
 
@@ -1730,6 +2207,21 @@ Missing Value ${missingValue} - ${matchedString}
1730
2207
  valueObject,
1731
2208
 
1732
2209
  ).then((val) => {
2210
+ // Update the last call with the resolved value
2211
+ if (pathValue && pathValue.length) {
2212
+ const pathKey = pathValue.join('.')
2213
+ if (this.resolutionTracking[pathKey] && this.resolutionTracking[pathKey].calls.length) {
2214
+ // Find the most recent call for this variableString
2215
+ for (let i = this.resolutionTracking[pathKey].calls.length - 1; i >= 0; i--) {
2216
+ if (this.resolutionTracking[pathKey].calls[i].variableString === variableString) {
2217
+ this.resolutionTracking[pathKey].calls[i].resolvedValue = val
2218
+ this.resolutionTracking[pathKey].calls[i].resolverType = resolverType
2219
+ break
2220
+ }
2221
+ }
2222
+ }
2223
+ }
2224
+
1733
2225
  // console.log('VALUE', val)
1734
2226
  if (
1735
2227
  val === null ||
@@ -1763,7 +2255,7 @@ Missing Value ${missingValue} - ${matchedString}
1763
2255
  // console.log('valueCount', valueCount)
1764
2256
  // TODO throw on empty values?
1765
2257
  // No fallback value found AND this is undefined, throw error
1766
- const nestedVars = findNestedVariables(propertyString, this.variableSyntax)
2258
+ const nestedVars = findNestedVariables(propertyString, this.variableSyntax, this.variablesKnownTypes)
1767
2259
  // console.log('nestedVars', nestedVars)
1768
2260
  const noNestedVars = nestedVars.length < 2
1769
2261
  if (valueCount.length === 1 && noNestedVars) {
@@ -1792,7 +2284,19 @@ Unable to resolve configuration variable
1792
2284
  if (!newHasFilter) {
1793
2285
  // console.log('no newHasFilter', val, valueObject)
1794
2286
  // console.log('> RESOLVER RETURN newValue 3', val, originalVar)
1795
- return Promise.resolve(val)
2287
+ // Wrap value with resolverType metadata for resolution tracking
2288
+ // But don't wrap if it's already an internal flag object
2289
+ if (val && typeof val === 'object' && val.__internal_only_flag) {
2290
+ // Attach resolverType to existing internal object
2291
+ val.__resolverType = resolverType
2292
+ return Promise.resolve(val)
2293
+ }
2294
+ return Promise.resolve({
2295
+ value: val,
2296
+ __resolverType: resolverType,
2297
+ __variableString: variableString,
2298
+ __internal_metadata: true
2299
+ })
1796
2300
  }
1797
2301
 
1798
2302
  const newUse = newHasFilter.reduce((acc, currentFilter, i) => {
@@ -1805,6 +2309,8 @@ Unable to resolve configuration variable
1805
2309
  // args: argsToPass
1806
2310
  })
1807
2311
  }, [])
2312
+ // console.log('pathValue', pathValue)
2313
+ // console.log('propertyString', propertyString)
1808
2314
  // console.log('newUse', newUse)
1809
2315
 
1810
2316
  if (typeof val === 'string' && val.match(/deep:/)) {
@@ -1841,7 +2347,19 @@ Unable to resolve configuration variable
1841
2347
  }, val)
1842
2348
  // console.log('> RESOLVER RETURN newValue', newValue)
1843
2349
  // console.log('> RESOLVER RETURN newValue 5', newValue)
1844
- return Promise.resolve(newValue)
2350
+ // Wrap value with resolverType metadata for resolution tracking
2351
+ // But don't wrap if it's already an internal flag object
2352
+ if (newValue && typeof newValue === 'object' && newValue.__internal_only_flag) {
2353
+ // Attach resolverType to existing internal object
2354
+ newValue.__resolverType = resolverType
2355
+ return Promise.resolve(newValue)
2356
+ }
2357
+ return Promise.resolve({
2358
+ value: newValue,
2359
+ __resolverType: resolverType,
2360
+ __variableString: variableString,
2361
+ __internal_metadata: true
2362
+ })
1845
2363
  })
1846
2364
 
1847
2365
  // console.log('valuePromise', valuePromise)
@@ -1851,12 +2369,20 @@ Unable to resolve configuration variable
1851
2369
  return this.tracker.add(variableString, valuePromise, propertyString, newHasFilter, promiseKey)
1852
2370
  }
1853
2371
 
2372
+ // console.log('fall thru variableString', variableString)
2373
+
1854
2374
  /* fall through case with self refs */
1855
2375
  if (variableString) {
1856
2376
  // console.log('before clean propertyString', propertyString, variableString)
1857
- const clean = cleanVariable(propertyString, this.variableSyntax, true, `getValueFromSrc self ${this.callCount}`)
2377
+ const clean = cleanVariable(
2378
+ propertyString,
2379
+ this.variableSyntax,
2380
+ true,
2381
+ `getValueFromSrc self ${this.callCount}`
2382
+ )
1858
2383
  // TODO @DWELLS cleanVariable makes fallback values with spaces have no spaces
1859
2384
  // console.log('AFTER cleanVariable', clean)
2385
+ // console.log(typeof clean)
1860
2386
  const cleanClean = clean.split('|')[0]
1861
2387
  // console.log('cleanCleanVariable', cleanClean)
1862
2388
  if (funcRegex.exec(cleanClean)) {
@@ -1865,7 +2391,8 @@ Unable to resolve configuration variable
1865
2391
  }
1866
2392
 
1867
2393
  const split = splitByComma(cleanClean)
1868
-
2394
+ // console.log('split', split)
2395
+ // console.log('typeof split', typeof split)
1869
2396
  // @TODO refactor this. USE FILTER [ 'commas', 'split("-"' ] is wrong
1870
2397
  let fallbackValue
1871
2398
  if (split.length === 2 || split.length === 3) {
@@ -1874,7 +2401,9 @@ Unable to resolve configuration variable
1874
2401
  fallbackValue = split[0]
1875
2402
  }
1876
2403
 
2404
+ // TODO this should be new in memory resolutionHistory probably?
1877
2405
  const nestedVar = findNestedVariable(split, valueObject.originalSource)
2406
+ // console.log('nestedVar', nestedVar)
1878
2407
 
1879
2408
  if (nestedVar) {
1880
2409
  if (!this.opts.allowUnknownVars) {
@@ -1883,7 +2412,7 @@ Unable to resolve configuration variable
1883
2412
  const fallbackStr = getFallbackString(split, nestedVar)
1884
2413
  return this.getValueFromSource(variableString, {
1885
2414
  value: fallbackStr,
1886
- }, 'nestedVar')
2415
+ }, 'nestedVar', originalVar)
1887
2416
  }
1888
2417
 
1889
2418
  // TODO verify we need this still with file(file.js, param)
@@ -1895,7 +2424,7 @@ Unable to resolve configuration variable
1895
2424
  // recurse on fallback and check again
1896
2425
  return this.getValueFromSource(`${variableString})`, {
1897
2426
  value: propertyString,
1898
- }, 'cleanClean.match(fileRefSyntax)')
2427
+ }, 'cleanClean.match(fileRefSyntax)', originalVar)
1899
2428
  }
1900
2429
  }
1901
2430
  // const fallbackValue = split[1]
@@ -1907,7 +2436,10 @@ Unable to resolve configuration variable
1907
2436
  const valuePromise = Promise.resolve(fallbackValue)
1908
2437
  return this.tracker.add(fallbackValue, valuePromise, propertyString, newHasFilter)
1909
2438
  }
1910
-
2439
+ /*
2440
+ console.log('what is fallbackValue', fallbackValue)
2441
+ console.log('typeof fallbackValue', typeof fallbackValue)
2442
+ /** */
1911
2443
  // has fallback but needs deeper lookup. Call getValueFromSrc again
1912
2444
  if (fallbackValue) {
1913
2445
  if (DEBUG) console.log('fallbackValue', fallbackValue)
@@ -1915,11 +2447,21 @@ Unable to resolve configuration variable
1915
2447
  // recurse on fallback and check again
1916
2448
  return this.getValueFromSource(
1917
2449
  fallbackValue,
1918
- {
1919
- value: propertyString,
1920
- },
2450
+ valueObject,
2451
+ // Object.assign({}, valueObject, { value: propertyString }),
2452
+ // {
2453
+ // value: propertyString,
2454
+ // path: valueObject.path,
2455
+ // originalSource: valueObject.originalSource,
2456
+ // ahh:true
2457
+ // },
1921
2458
  'fallbackValue',
1922
- )
2459
+ originalVar,
2460
+ ).then((res) => {
2461
+ // console.log('res', res)
2462
+ // console.log('typeof res', typeof res)
2463
+ return res
2464
+ })
1923
2465
  }
1924
2466
  }
1925
2467
 
@@ -1932,7 +2474,9 @@ Unable to resolve configuration variable
1932
2474
  ]
1933
2475
 
1934
2476
  // Default value used for self variable
1935
- if (propertyString.match(/,/)) {
2477
+ // Only show this error if the variable itself (not a parent fallback) is a self-reference with a fallback
2478
+ const isSelfReference = !variableString.match(/^(env|opt|file|text|cron|eval|git):/)
2479
+ if (isSelfReference && variableString.match(/,/)) {
1936
2480
  errorMessage.push('\n Default values for self referenced values are not allowed')
1937
2481
  errorMessage.push(`\n Fix the ${propertyString} variable`)
1938
2482
  }
@@ -1940,23 +2484,30 @@ Unable to resolve configuration variable
1940
2484
  let allowSpecialCase = false
1941
2485
  /* handle special cases for cloudformation ${Sub} values */
1942
2486
  if (this.originalConfig && key.endsWith('Fn::Sub')) {
1943
- const params = this.originalConfig.Parameters || (this.originalConfig.parameters || {}).Parameters
1944
- const resources = this.originalConfig.Resources || (this.originalConfig.resources || {}).Resources
1945
- /* Cloudformation Resource References */
1946
- if (resources && resources[variableString]) {
1947
- allowSpecialCase = true
1948
- } else if (params && params[variableString]) {
1949
- allowSpecialCase = true
1950
- } else if (variableString === 'ApiGatewayRestApi') {
1951
- // Allow for "hidden" cloudformation variables, set by sls framework
1952
- allowSpecialCase = true
1953
- } else if (variableString === 'HttpApi') {
1954
- // Allow for "hidden" cloudformation variables, set by sls framework
2487
+ if (this.opts.verifySubReferences) {
2488
+ const params = this.originalConfig.Parameters || (this.originalConfig.resources || {}).Parameters
2489
+ const resources = this.originalConfig.Resources || (this.originalConfig.resources || {}).Resources
2490
+ /* Cloudformation Resource References */
2491
+ if (resources && resources[variableString]) {
2492
+ allowSpecialCase = true
2493
+ } else if (params && params[variableString]) {
2494
+ allowSpecialCase = true
2495
+ } else if (variableString === 'ApiGatewayRestApi') {
2496
+ // Allow for "hidden" cloudformation variables, set by sls framework
2497
+ allowSpecialCase = true
2498
+ } else if (variableString === 'HttpApi') {
2499
+ // Allow for "hidden" cloudformation variables, set by sls framework
2500
+ allowSpecialCase = true
2501
+ }
2502
+ } else {
2503
+ // Default let any sub references pass through
1955
2504
  allowSpecialCase = true
1956
2505
  }
1957
2506
  }
1958
2507
  /* Todo handle stage variables */
1959
2508
 
2509
+
2510
+
1960
2511
  /* Pass through unknown variables */
1961
2512
  if (this.opts.allowUnknownVars || allowSpecialCase) {
1962
2513
  // console.log('allowUnknownVars propertyString', propertyString)
@@ -2061,7 +2612,10 @@ Unable to resolve configuration variable
2061
2612
  } else if (resolvedPath.match(/\.\//)) {
2062
2613
  // TODO test higher parent refs
2063
2614
  const cleanName = path.basename(resolvedPath)
2064
- fullFilePath = findUp.sync(cleanName, { cwd: this.configPath })
2615
+ const findUpResult = findUp.sync(cleanName, { cwd: this.configPath })
2616
+ if (findUpResult) {
2617
+ fullFilePath = findUpResult
2618
+ }
2065
2619
  }
2066
2620
 
2067
2621
  let fileExtension = resolvedPath.split('.')
@@ -2070,18 +2624,39 @@ Unable to resolve configuration variable
2070
2624
 
2071
2625
  // Validate file exists
2072
2626
  if (!fs.existsSync(fullFilePath)) {
2627
+ const originalVar = options.context && options.context.originalSource
2628
+
2629
+ const findNestedResult = findNestedVariables(
2630
+ originalVar,
2631
+ this.variableSyntax,
2632
+ this.variablesKnownTypes,
2633
+ options.context.path
2634
+ )
2635
+ // console.log('findNestedResult', findNestedResult)
2636
+ let hasFallback = false
2637
+ if (findNestedResult) {
2638
+ const varDetails = findNestedResult[0]
2639
+ // console.log('varDetails', varDetails)
2640
+ hasFallback = varDetails.hasFallback
2641
+ }
2642
+
2643
+ // check if original var has fallback value
2073
2644
  // console.log('NO FILE FOUND', fullFilePath)
2074
2645
  // console.log('variableString', variableString)
2075
- const errorMsg = `${logLines}
2076
- Variable ${variableString} cannot resolve due to missing file.
2646
+
2647
+ if (!hasFallback) {
2648
+ const errorMsg = makeBox({
2649
+ title: `File Not Found in ${originalVar}`,
2650
+ text: `Variable ${variableString} cannot resolve due to missing file.
2077
2651
 
2078
2652
  File not found ${fullFilePath}
2079
2653
 
2080
2654
  Default fallback value will be used if provided.
2081
- ${logLines}
2082
- `
2083
2655
 
2084
- console.log(errorMsg)
2656
+ ${JSON.stringify(options.context, null, 2)}`,
2657
+ })
2658
+ console.log(errorMsg)
2659
+ }
2085
2660
  // TODO maybe reject. YAML does not allow for null/undefined values
2086
2661
  // return Promise.reject(new Error(errorMsg))
2087
2662
  return Promise.resolve(undefined)
@@ -2099,6 +2674,7 @@ ${logLines}
2099
2674
 
2100
2675
  // Process JS files
2101
2676
  if (fileExtension === 'js' || fileExtension === 'cjs') {
2677
+ // Possible alt importer tool https://github.com/humanwhocodes/module-importer
2102
2678
  const jsFile = require(fullFilePath)
2103
2679
  let returnValueFunction = jsFile
2104
2680
  // TODO change how exported functions are referenced
@@ -2198,6 +2774,7 @@ Check if your TypeScript is returning the correct data.`
2198
2774
  }
2199
2775
 
2200
2776
  if (fileExtension === 'mjs' || fileExtension === 'esm') {
2777
+ // Possible alt importer tool https://github.com/humanwhocodes/module-importer
2201
2778
  const { executeESMFile } = require('./parsers/esm')
2202
2779
  let returnValueFunction
2203
2780
  const variableArray = variableString.split(':')
@@ -2269,9 +2846,10 @@ Check if your ESM is returning the correct data.`
2269
2846
  // console.log('deep', variableString)
2270
2847
  // console.log('matchedFileString', matchedFileString)
2271
2848
  let deepProperties = variableString.replace(matchedFileString, '')
2849
+ // TODO 2025-11-12 add file.path.support instead of just :
2272
2850
  if (deepProperties.substring(0, 1) !== ':') {
2273
2851
  const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
2274
- Please use ":" to reference sub properties`
2852
+ Please use ":" to reference sub properties. ${deepProperties}`
2275
2853
  return Promise.reject(new Error(errorMessage))
2276
2854
  }
2277
2855
  deepProperties = deepProperties.slice(1).split('.')
@@ -2298,7 +2876,7 @@ Please use ":" to reference sub properties`
2298
2876
  return Promise.resolve(valueToPopulate)
2299
2877
  }
2300
2878
  }
2301
- console.log('fall thru', valueToPopulate)
2879
+ // console.log('fall thru', valueToPopulate)
2302
2880
  return Promise.resolve(valueToPopulate)
2303
2881
  }
2304
2882
  getVariableFromDeep(variableString) {
@@ -2310,15 +2888,22 @@ Please use ":" to reference sub properties`
2310
2888
  /** */
2311
2889
  return this.deep[index]
2312
2890
  }
2313
- getValueFromDeep(variableString) {
2891
+ getValueFromDeep(variableString, pathValue) {
2314
2892
  const variable = this.getVariableFromDeep(variableString)
2315
2893
  const deepRef = variableString.replace(deepPrefixReplacePattern, '')
2316
2894
  /*
2317
2895
  console.log("GET getValueFromDeep", variableString)
2318
- console.log('deepRef', deepRef)
2896
+ console.log('deepRef', (deepRef) ? deepRef : '- no deepRef')
2319
2897
  console.log('getValueFromDeep variable', variable)
2320
2898
  /** */
2321
- let ret = this.populateValue({ value: variable }, undefined, 'getValueFromDeep')
2899
+ // Preserve path and originalSource information from pathValue
2900
+ const valueObject = {
2901
+ value: variable,
2902
+ path: pathValue ? pathValue.path : undefined,
2903
+ originalSource: pathValue ? pathValue.originalSource : undefined,
2904
+ resolutionHistory: pathValue ? pathValue.resolutionHistory : []
2905
+ }
2906
+ let ret = this.populateValue(valueObject, undefined, 'getValueFromDeep')
2322
2907
  if (deepRef.length) {
2323
2908
  // if there is a deep reference remaining
2324
2909
  ret = ret.then((result) => {
@@ -2342,7 +2927,12 @@ Please use ":" to reference sub properties`
2342
2927
  }
2343
2928
  // console.log("makeDeepVariable SET INDEX", index)
2344
2929
  const variableContainer = variable.match(this.variableSyntax)[0]
2345
- const variableString = cleanVariable(variableContainer, this.variableSyntax, true, `makeDeepVariable ${this.callCount}`)
2930
+ const variableString = cleanVariable(
2931
+ variableContainer,
2932
+ this.variableSyntax,
2933
+ true,
2934
+ `makeDeepVariable ${this.callCount}`
2935
+ )
2346
2936
  const deepVar = variableContainer.replace(variableString, `deep:${index}`)
2347
2937
  /*
2348
2938
  console.log('MAKE DEEP', variable, caller)