configorama 0.6.6 → 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
 
@@ -829,6 +904,7 @@ class Configorama {
829
904
  throw new Error(errorMessage)
830
905
  }
831
906
  if (typeof rawValue === 'string') {
907
+ // console.log('rawValue', rawValue)
832
908
  /* Process inline functions like merge() */
833
909
  if (ENABLE_FUNCTIONS && rawValue.match(/> function /)) {
834
910
  // console.log('RAW FUNCTION', rawFunction)
@@ -888,7 +964,216 @@ class Configorama {
888
964
  })
889
965
  })
890
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
+ }
891
1175
  runFunction(variableString) {
1176
+ // console.log('runFunction', variableString)
892
1177
  /* If json object value return it */
893
1178
  if (variableString.match(/^\s*{/) && variableString.match(/}\s*$/)) {
894
1179
  return variableString
@@ -916,7 +1201,6 @@ class Configorama {
916
1201
  argsToPass = formatFunctionArgs(splitter)
917
1202
  }
918
1203
  // console.log('argsToPass runFunction', argsToPass)
919
-
920
1204
  // TODO check for camelCase version. | toUpperCase messes with function name
921
1205
  const theFunction = this.functions[functionName] || this.functions[functionName.toLowerCase()]
922
1206
 
@@ -1014,6 +1298,15 @@ class Configorama {
1014
1298
  }
1015
1299
  }
1016
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
+
1017
1310
  if (originalValue && isString(originalValue)) {
1018
1311
  const varString = cleanVariable(originalValue, this.variableSyntax, true, `getProperties ${this.callCount}`)
1019
1312
  if (varString.match(fileRefSyntax)) {
@@ -1025,7 +1318,6 @@ class Configorama {
1025
1318
  }
1026
1319
  return results
1027
1320
  }
1028
-
1029
1321
  /**
1030
1322
  * @typedef {TerminalProperty} TerminalPropertyPopulated
1031
1323
  * @property {Object} populated The populated value of the value at the path
@@ -1042,11 +1334,9 @@ class Configorama {
1042
1334
  // Initial check if value has variable string in it
1043
1335
  return isString(property.value) && property.value.match(this.variableSyntax)
1044
1336
  })
1045
-
1046
1337
  /*
1047
1338
  console.log(`variables at call count ${this.callCount}`, variables)
1048
1339
  /** */
1049
-
1050
1340
  /* Exclude git messages from being processed */
1051
1341
  // Was failing on git msgs like "xyz cron:pattern to cron(pattern) for improved clarity"
1052
1342
  if (this.callCount > 1) {
@@ -1058,7 +1348,6 @@ class Configorama {
1058
1348
  return true
1059
1349
  })
1060
1350
  }
1061
-
1062
1351
  return map(variables, (valueObject) => {
1063
1352
  // console.log('valueObject', valueObject)
1064
1353
  return this.populateValue(valueObject, false, '_populateVariables').then((populated) => {
@@ -1161,16 +1450,127 @@ class Configorama {
1161
1450
  */
1162
1451
  renderMatches(valueObject, matches, results) {
1163
1452
  /*
1453
+ console.log('valueObject', valueObject)
1164
1454
  console.log('RENDER', matches)
1165
1455
  console.log('RESULTS', results)
1166
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
+
1167
1567
  let result = valueObject.value
1168
1568
  for (let i = 0; i < matches.length; i += 1) {
1169
1569
  this.warnIfNotFound(matches[i].variable, results[i])
1170
1570
  // console.log('Render MATCHES', results[i])
1171
1571
  let valueToPop = results[i]
1172
1572
  // TODO refactor this. __internal_only_flag needed to stop clash with sync/async file resolution
1173
- 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)) {
1174
1574
  valueToPop = results[i].value
1175
1575
  }
1176
1576
  result = this.populateVariable(valueObject, matches[i].match, valueToPop)
@@ -1214,7 +1614,10 @@ class Configorama {
1214
1614
  .then((result) => {
1215
1615
  // console.log('renderMatches result', result)
1216
1616
  if (root && isArray(matches)) {
1217
- 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')
1218
1621
  }
1219
1622
  return result
1220
1623
  })
@@ -1252,11 +1655,11 @@ class Configorama {
1252
1655
 
1253
1656
  const parts = splitByComma(variable, this.variableSyntax)
1254
1657
  if (DEBUG) {
1255
- console.log('parts', parts)
1256
- console.log('parts variable:', variable)
1257
- console.log('parts originalVar:', originalVar)
1258
- console.log('parts property:', valueObject)
1259
- 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)
1260
1663
  console.log('-----')
1261
1664
  }
1262
1665
  if (parts.length <= 1) {
@@ -1277,16 +1680,20 @@ class Configorama {
1277
1680
  populateVariable(valueObject, matchedString, valueToPopulate) {
1278
1681
  let property = valueObject.value
1279
1682
  // console.log('init property', property)
1280
- let DEBUG_TYPE = false
1683
+
1281
1684
  if (DEBUG) {
1282
1685
  console.log('────────START populateVar──────────────')
1283
- console.log('populateVar: valueToPopulate', valueToPopulate)
1284
- console.log('populateVar: typeof valueToPopulate', typeof valueToPopulate)
1285
- console.log(`populateVar: path "${valueObject.path}"`)
1286
- console.log(`populateVar: value \`${valueObject.value}\``)
1287
- console.log(`populateVar: originalSource \`${valueObject.originalSource}\``)
1288
- console.log('populateVar: property', property)
1289
- 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
+ }
1290
1697
  }
1291
1698
 
1292
1699
  const originalSrc = valueObject.originalSource || ''
@@ -1305,9 +1712,35 @@ class Configorama {
1305
1712
  if (DEBUG_TYPE) console.log('DEBUG_TYPE total replacement')
1306
1713
  const v = valueObject.value || ''
1307
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
+ }
1308
1740
 
1309
1741
  /* Handle ${self:custom.ref, ''} with deep values */
1310
1742
  if (v.match(deepRefSyntax) && originalSrc.match(this.variableSyntax) && !v.match(/deep\:(\d*)\..*}$/)) {
1743
+ // console.log('deep ref syntax')
1311
1744
  // console.log('deep var', this.deep)
1312
1745
  // console.log('originalSrc', originalSrc)
1313
1746
  // console.log('value', v)
@@ -1410,7 +1843,12 @@ class Configorama {
1410
1843
  missingValue = this.deep[i]
1411
1844
  }
1412
1845
 
1413
- 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
+ )
1414
1852
  const cleanVarNoFilters = cleanVar.split('|')[0]
1415
1853
  const splitVars = splitByComma(cleanVarNoFilters)
1416
1854
  const nestedVar = findNestedVariable(splitVars, valueObject.originalSource)
@@ -1425,6 +1863,7 @@ class Configorama {
1425
1863
  value: fallbackStr,
1426
1864
  path: valueObject.path,
1427
1865
  originalSource: valueObject.originalSource,
1866
+ resolutionHistory: valueObject.resolutionHistory || [],
1428
1867
  // set __internal_only_flag to note this is object we make not a resolved value
1429
1868
  __internal_only_flag: true,
1430
1869
  caller: 'nestedVar',
@@ -1476,6 +1915,7 @@ Missing Value ${missingValue} - ${matchedString}
1476
1915
  value: finalProp, // prop to fix nested ¯\_(ツ)_/¯
1477
1916
  path: valueObject.path,
1478
1917
  originalSource: valueObject.originalSource,
1918
+ resolutionHistory: valueObject.resolutionHistory || [],
1479
1919
  // set __internal_only_flag to note this is object we make not a resolved value
1480
1920
  // __internal_only_flag: true
1481
1921
  }
@@ -1494,11 +1934,18 @@ Missing Value ${missingValue} - ${matchedString}
1494
1934
  }
1495
1935
  }
1496
1936
  */
1497
- // Does not match file refs with nested vars + args
1498
- // @TODO fix this for eval refs
1499
- // console.log('prop', prop)
1500
- // console.log('func', func)
1501
- if (!prop.match(fileRefSyntax) && !prop.match(getValueFromEval.match) && func) {
1937
+
1938
+ if (
1939
+ /* Not another variable reference */
1940
+ !prop.match(this.variableSyntax)
1941
+ &&
1942
+ /* Not file or text refs */
1943
+ !prop.match(fileRefSyntax)
1944
+ && !prop.match(textRefSyntax)
1945
+ /* Not eval refs */
1946
+ && !prop.match(getValueFromEval.match)
1947
+ // AND is not multiline value
1948
+ && (func && prop.split('\n').length < 3)) {
1502
1949
  // console.log('IS FUNCTION')
1503
1950
  /* if matches function signature like ${merge('foo', 'bar')}
1504
1951
  rewrite the variable to run the function after inputs resolved
@@ -1516,14 +1963,21 @@ Missing Value ${missingValue} - ${matchedString}
1516
1963
 
1517
1964
  // console.log('foundFilters', foundFilters)
1518
1965
 
1519
- /* Apply filters if found */
1520
- //console.log('> property', property)
1521
- if (
1522
- foundFilters.length > 0 &&
1523
- typeof valueToPopulate === 'string' &&
1524
- !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 &&
1525
1973
  !property.match(this.variableSyntax)
1526
1974
  ) {
1975
+ runFilters = true
1976
+ }
1977
+
1978
+ /* Apply filters if found */
1979
+ //console.log('> property', property)
1980
+ if (runFilters) {
1527
1981
  // If filter cache exists we need to remove filter that have already been run
1528
1982
  if (this.filterCache[valueObject.path]) {
1529
1983
  foundFilters = foundFilters.filter((filter) => {
@@ -1548,6 +2002,7 @@ Missing Value ${missingValue} - ${matchedString}
1548
2002
  value: property,
1549
2003
  path: valueObject.path,
1550
2004
  originalSource: valueObject.originalSource,
2005
+ resolutionHistory: valueObject.resolutionHistory || [],
1551
2006
  __internal_only_flag: true, // set __internal_only_flag to note this is object we make not a resolved value
1552
2007
  caller: 'end',
1553
2008
  count: this.callCount,
@@ -1589,15 +2044,22 @@ Missing Value ${missingValue} - ${matchedString}
1589
2044
  // console.log('propertyString', typeof propertyString)
1590
2045
  const variableValues = variableStrings.map((variableString) => {
1591
2046
  // This runs on nested variable resolution
1592
- return this.getValueFromSource(variableString, valueObject, 'overwrite')
2047
+ return this.getValueFromSource(variableString, valueObject, 'overwrite', valueObject.originalSource)
1593
2048
  })
1594
-
2049
+
1595
2050
  // console.log('variableValues', variableValues)
1596
2051
  return Promise.all(variableValues).then((values) => {
1597
2052
  let deepPropertyStr = propertyString
1598
2053
  let deepProperties = 0
1599
2054
  // console.log('overwrite values', valuesToUse)
1600
- 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) => {
1601
2063
  // console.log('───────────────────────────────> value', value)
1602
2064
  if (isString(value) && value.match(this.variableSyntax)) {
1603
2065
  deepProperties += 1
@@ -1613,7 +2075,7 @@ Missing Value ${missingValue} - ${matchedString}
1613
2075
  })
1614
2076
  return deepProperties > 0
1615
2077
  ? Promise.resolve(deepPropertyStr) // return deep variable replacement of original
1616
- : Promise.resolve(values.find(isValidValue)) // resolve first valid value, else undefined
2078
+ : Promise.resolve(extractedValues.find(isValidValue)) // resolve first valid value, else undefined
1617
2079
  })
1618
2080
  }
1619
2081
  /**
@@ -1625,6 +2087,25 @@ Missing Value ${missingValue} - ${matchedString}
1625
2087
  // console.log('getValueFromSrc caller', caller)
1626
2088
  const propertyString = valueObject.value
1627
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
+
1628
2109
  // console.log('getValueFromSrc propertyString', propertyString)
1629
2110
  // console.log(`tracker contains ${variableString}`, this.tracker.contains(variableString))
1630
2111
  if (this.tracker.contains(variableString)) {
@@ -1635,11 +2116,12 @@ Missing Value ${missingValue} - ${matchedString}
1635
2116
  let newHasFilter
1636
2117
  // Else lookup value from various sources
1637
2118
  if (DEBUG) {
1638
- console.log(`>>>>> getValueFromSrc() call - ${caller}`)
1639
- console.log('variableString:', variableString)
1640
- console.log('propertyString:', propertyString)
1641
- console.log('pathValue:', pathValue)
1642
- 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)
1643
2125
  console.log('-----')
1644
2126
  }
1645
2127
 
@@ -1667,6 +2149,8 @@ Missing Value ${missingValue} - ${matchedString}
1667
2149
  })
1668
2150
  .map((f) => {
1669
2151
  return trim(f)
2152
+ // TODO refactor this. This is a temp fix for filters with nested vars.
2153
+ .replace(/}$/, '')
1670
2154
  })
1671
2155
  // console.log('filters to run', _filter)
1672
2156
 
@@ -1723,6 +2207,21 @@ Missing Value ${missingValue} - ${matchedString}
1723
2207
  valueObject,
1724
2208
 
1725
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
+
1726
2225
  // console.log('VALUE', val)
1727
2226
  if (
1728
2227
  val === null ||
@@ -1756,7 +2255,7 @@ Missing Value ${missingValue} - ${matchedString}
1756
2255
  // console.log('valueCount', valueCount)
1757
2256
  // TODO throw on empty values?
1758
2257
  // No fallback value found AND this is undefined, throw error
1759
- const nestedVars = findNestedVariables(propertyString, this.variableSyntax)
2258
+ const nestedVars = findNestedVariables(propertyString, this.variableSyntax, this.variablesKnownTypes)
1760
2259
  // console.log('nestedVars', nestedVars)
1761
2260
  const noNestedVars = nestedVars.length < 2
1762
2261
  if (valueCount.length === 1 && noNestedVars) {
@@ -1785,7 +2284,19 @@ Unable to resolve configuration variable
1785
2284
  if (!newHasFilter) {
1786
2285
  // console.log('no newHasFilter', val, valueObject)
1787
2286
  // console.log('> RESOLVER RETURN newValue 3', val, originalVar)
1788
- 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
+ })
1789
2300
  }
1790
2301
 
1791
2302
  const newUse = newHasFilter.reduce((acc, currentFilter, i) => {
@@ -1798,6 +2309,8 @@ Unable to resolve configuration variable
1798
2309
  // args: argsToPass
1799
2310
  })
1800
2311
  }, [])
2312
+ // console.log('pathValue', pathValue)
2313
+ // console.log('propertyString', propertyString)
1801
2314
  // console.log('newUse', newUse)
1802
2315
 
1803
2316
  if (typeof val === 'string' && val.match(/deep:/)) {
@@ -1834,7 +2347,19 @@ Unable to resolve configuration variable
1834
2347
  }, val)
1835
2348
  // console.log('> RESOLVER RETURN newValue', newValue)
1836
2349
  // console.log('> RESOLVER RETURN newValue 5', newValue)
1837
- 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
+ })
1838
2363
  })
1839
2364
 
1840
2365
  // console.log('valuePromise', valuePromise)
@@ -1844,12 +2369,20 @@ Unable to resolve configuration variable
1844
2369
  return this.tracker.add(variableString, valuePromise, propertyString, newHasFilter, promiseKey)
1845
2370
  }
1846
2371
 
2372
+ // console.log('fall thru variableString', variableString)
2373
+
1847
2374
  /* fall through case with self refs */
1848
2375
  if (variableString) {
1849
2376
  // console.log('before clean propertyString', propertyString, variableString)
1850
- 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
+ )
1851
2383
  // TODO @DWELLS cleanVariable makes fallback values with spaces have no spaces
1852
2384
  // console.log('AFTER cleanVariable', clean)
2385
+ // console.log(typeof clean)
1853
2386
  const cleanClean = clean.split('|')[0]
1854
2387
  // console.log('cleanCleanVariable', cleanClean)
1855
2388
  if (funcRegex.exec(cleanClean)) {
@@ -1858,7 +2391,8 @@ Unable to resolve configuration variable
1858
2391
  }
1859
2392
 
1860
2393
  const split = splitByComma(cleanClean)
1861
-
2394
+ // console.log('split', split)
2395
+ // console.log('typeof split', typeof split)
1862
2396
  // @TODO refactor this. USE FILTER [ 'commas', 'split("-"' ] is wrong
1863
2397
  let fallbackValue
1864
2398
  if (split.length === 2 || split.length === 3) {
@@ -1867,7 +2401,9 @@ Unable to resolve configuration variable
1867
2401
  fallbackValue = split[0]
1868
2402
  }
1869
2403
 
2404
+ // TODO this should be new in memory resolutionHistory probably?
1870
2405
  const nestedVar = findNestedVariable(split, valueObject.originalSource)
2406
+ // console.log('nestedVar', nestedVar)
1871
2407
 
1872
2408
  if (nestedVar) {
1873
2409
  if (!this.opts.allowUnknownVars) {
@@ -1876,7 +2412,7 @@ Unable to resolve configuration variable
1876
2412
  const fallbackStr = getFallbackString(split, nestedVar)
1877
2413
  return this.getValueFromSource(variableString, {
1878
2414
  value: fallbackStr,
1879
- }, 'nestedVar')
2415
+ }, 'nestedVar', originalVar)
1880
2416
  }
1881
2417
 
1882
2418
  // TODO verify we need this still with file(file.js, param)
@@ -1888,7 +2424,7 @@ Unable to resolve configuration variable
1888
2424
  // recurse on fallback and check again
1889
2425
  return this.getValueFromSource(`${variableString})`, {
1890
2426
  value: propertyString,
1891
- }, 'cleanClean.match(fileRefSyntax)')
2427
+ }, 'cleanClean.match(fileRefSyntax)', originalVar)
1892
2428
  }
1893
2429
  }
1894
2430
  // const fallbackValue = split[1]
@@ -1900,7 +2436,10 @@ Unable to resolve configuration variable
1900
2436
  const valuePromise = Promise.resolve(fallbackValue)
1901
2437
  return this.tracker.add(fallbackValue, valuePromise, propertyString, newHasFilter)
1902
2438
  }
1903
-
2439
+ /*
2440
+ console.log('what is fallbackValue', fallbackValue)
2441
+ console.log('typeof fallbackValue', typeof fallbackValue)
2442
+ /** */
1904
2443
  // has fallback but needs deeper lookup. Call getValueFromSrc again
1905
2444
  if (fallbackValue) {
1906
2445
  if (DEBUG) console.log('fallbackValue', fallbackValue)
@@ -1908,11 +2447,21 @@ Unable to resolve configuration variable
1908
2447
  // recurse on fallback and check again
1909
2448
  return this.getValueFromSource(
1910
2449
  fallbackValue,
1911
- {
1912
- value: propertyString,
1913
- },
2450
+ valueObject,
2451
+ // Object.assign({}, valueObject, { value: propertyString }),
2452
+ // {
2453
+ // value: propertyString,
2454
+ // path: valueObject.path,
2455
+ // originalSource: valueObject.originalSource,
2456
+ // ahh:true
2457
+ // },
1914
2458
  'fallbackValue',
1915
- )
2459
+ originalVar,
2460
+ ).then((res) => {
2461
+ // console.log('res', res)
2462
+ // console.log('typeof res', typeof res)
2463
+ return res
2464
+ })
1916
2465
  }
1917
2466
  }
1918
2467
 
@@ -1925,7 +2474,9 @@ Unable to resolve configuration variable
1925
2474
  ]
1926
2475
 
1927
2476
  // Default value used for self variable
1928
- 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(/,/)) {
1929
2480
  errorMessage.push('\n Default values for self referenced values are not allowed')
1930
2481
  errorMessage.push(`\n Fix the ${propertyString} variable`)
1931
2482
  }
@@ -1933,23 +2484,30 @@ Unable to resolve configuration variable
1933
2484
  let allowSpecialCase = false
1934
2485
  /* handle special cases for cloudformation ${Sub} values */
1935
2486
  if (this.originalConfig && key.endsWith('Fn::Sub')) {
1936
- const params = this.originalConfig.Parameters || (this.originalConfig.parameters || {}).Parameters
1937
- const resources = this.originalConfig.Resources || (this.originalConfig.resources || {}).Resources
1938
- /* Cloudformation Resource References */
1939
- if (resources && resources[variableString]) {
1940
- allowSpecialCase = true
1941
- } else if (params && params[variableString]) {
1942
- allowSpecialCase = true
1943
- } else if (variableString === 'ApiGatewayRestApi') {
1944
- // Allow for "hidden" cloudformation variables, set by sls framework
1945
- allowSpecialCase = true
1946
- } else if (variableString === 'HttpApi') {
1947
- // 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
1948
2504
  allowSpecialCase = true
1949
2505
  }
1950
2506
  }
1951
2507
  /* Todo handle stage variables */
1952
2508
 
2509
+
2510
+
1953
2511
  /* Pass through unknown variables */
1954
2512
  if (this.opts.allowUnknownVars || allowSpecialCase) {
1955
2513
  // console.log('allowUnknownVars propertyString', propertyString)
@@ -2054,7 +2612,10 @@ Unable to resolve configuration variable
2054
2612
  } else if (resolvedPath.match(/\.\//)) {
2055
2613
  // TODO test higher parent refs
2056
2614
  const cleanName = path.basename(resolvedPath)
2057
- fullFilePath = findUp.sync(cleanName, { cwd: this.configPath })
2615
+ const findUpResult = findUp.sync(cleanName, { cwd: this.configPath })
2616
+ if (findUpResult) {
2617
+ fullFilePath = findUpResult
2618
+ }
2058
2619
  }
2059
2620
 
2060
2621
  let fileExtension = resolvedPath.split('.')
@@ -2063,18 +2624,39 @@ Unable to resolve configuration variable
2063
2624
 
2064
2625
  // Validate file exists
2065
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
2066
2644
  // console.log('NO FILE FOUND', fullFilePath)
2067
2645
  // console.log('variableString', variableString)
2068
- const errorMsg = `${logLines}
2069
- 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.
2070
2651
 
2071
2652
  File not found ${fullFilePath}
2072
2653
 
2073
2654
  Default fallback value will be used if provided.
2074
- ${logLines}
2075
- `
2076
2655
 
2077
- console.log(errorMsg)
2656
+ ${JSON.stringify(options.context, null, 2)}`,
2657
+ })
2658
+ console.log(errorMsg)
2659
+ }
2078
2660
  // TODO maybe reject. YAML does not allow for null/undefined values
2079
2661
  // return Promise.reject(new Error(errorMsg))
2080
2662
  return Promise.resolve(undefined)
@@ -2092,6 +2674,7 @@ ${logLines}
2092
2674
 
2093
2675
  // Process JS files
2094
2676
  if (fileExtension === 'js' || fileExtension === 'cjs') {
2677
+ // Possible alt importer tool https://github.com/humanwhocodes/module-importer
2095
2678
  const jsFile = require(fullFilePath)
2096
2679
  let returnValueFunction = jsFile
2097
2680
  // TODO change how exported functions are referenced
@@ -2191,6 +2774,7 @@ Check if your TypeScript is returning the correct data.`
2191
2774
  }
2192
2775
 
2193
2776
  if (fileExtension === 'mjs' || fileExtension === 'esm') {
2777
+ // Possible alt importer tool https://github.com/humanwhocodes/module-importer
2194
2778
  const { executeESMFile } = require('./parsers/esm')
2195
2779
  let returnValueFunction
2196
2780
  const variableArray = variableString.split(':')
@@ -2262,9 +2846,10 @@ Check if your ESM is returning the correct data.`
2262
2846
  // console.log('deep', variableString)
2263
2847
  // console.log('matchedFileString', matchedFileString)
2264
2848
  let deepProperties = variableString.replace(matchedFileString, '')
2849
+ // TODO 2025-11-12 add file.path.support instead of just :
2265
2850
  if (deepProperties.substring(0, 1) !== ':') {
2266
2851
  const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
2267
- Please use ":" to reference sub properties`
2852
+ Please use ":" to reference sub properties. ${deepProperties}`
2268
2853
  return Promise.reject(new Error(errorMessage))
2269
2854
  }
2270
2855
  deepProperties = deepProperties.slice(1).split('.')
@@ -2291,7 +2876,7 @@ Please use ":" to reference sub properties`
2291
2876
  return Promise.resolve(valueToPopulate)
2292
2877
  }
2293
2878
  }
2294
- console.log('fall thru', valueToPopulate)
2879
+ // console.log('fall thru', valueToPopulate)
2295
2880
  return Promise.resolve(valueToPopulate)
2296
2881
  }
2297
2882
  getVariableFromDeep(variableString) {
@@ -2303,15 +2888,22 @@ Please use ":" to reference sub properties`
2303
2888
  /** */
2304
2889
  return this.deep[index]
2305
2890
  }
2306
- getValueFromDeep(variableString) {
2891
+ getValueFromDeep(variableString, pathValue) {
2307
2892
  const variable = this.getVariableFromDeep(variableString)
2308
2893
  const deepRef = variableString.replace(deepPrefixReplacePattern, '')
2309
2894
  /*
2310
2895
  console.log("GET getValueFromDeep", variableString)
2311
- console.log('deepRef', deepRef)
2896
+ console.log('deepRef', (deepRef) ? deepRef : '- no deepRef')
2312
2897
  console.log('getValueFromDeep variable', variable)
2313
2898
  /** */
2314
- 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')
2315
2907
  if (deepRef.length) {
2316
2908
  // if there is a deep reference remaining
2317
2909
  ret = ret.then((result) => {
@@ -2335,7 +2927,12 @@ Please use ":" to reference sub properties`
2335
2927
  }
2336
2928
  // console.log("makeDeepVariable SET INDEX", index)
2337
2929
  const variableContainer = variable.match(this.variableSyntax)[0]
2338
- 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
+ )
2339
2936
  const deepVar = variableContainer.replace(variableString, `deep:${index}`)
2340
2937
  /*
2341
2938
  console.log('MAKE DEEP', variable, caller)