configorama 0.6.7 → 0.6.9

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,17 @@ 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
- })
634
+ // Use collectVariableMetadata to get variable info (DRY - don't duplicate logic)
635
+ const metadata = this.collectVariableMetadata()
636
+ /*
637
+ deepLog('metadata', metadata)
638
+ process.exit(1)
639
+ /** */
640
+
641
+ const variableData = metadata.variables
642
+ const varKeys = Object.keys(variableData)
590
643
 
591
- if (!foundVariables.length) {
644
+ if (!varKeys.length) {
592
645
  logHeader('No Variables Found in Config')
593
646
  if (this.configFilePath) {
594
647
  console.log(`File: ${this.configFilePath}`)
@@ -611,10 +664,7 @@ class Configorama {
611
664
  console.log()
612
665
  }
613
666
 
614
- // make foundVariables array unique
615
- const finalFoundVariables = [...new Set(foundVariables)]
616
- if (finalFoundVariables.length > 0) {
617
- const varKeys = Object.keys(variableData)
667
+ if (varKeys.length > 0) {
618
668
  const fileName = this.configFilePath ? ` in ${this.configFilePath}` : ''
619
669
 
620
670
  logHeader(`Found ${varKeys.length} Variables${fileName}`)
@@ -626,9 +676,34 @@ class Configorama {
626
676
  const longestKey = varKeys.reduce((acc, k) => {
627
677
  return Math.max(acc, k.length)
628
678
  }, 0)
679
+ // Count all references including nested ones within other variables
680
+ const countAllReferences = (targetVariable) => {
681
+ // Start with direct references
682
+ let count = variableData[targetVariable].length
683
+
684
+ // Check all other variables for nested references to this variable
685
+ varKeys.forEach((otherKey) => {
686
+ if (otherKey === targetVariable) return
687
+
688
+ variableData[otherKey].forEach((instance) => {
689
+ if (instance.resolveDetails) {
690
+ instance.resolveDetails.forEach((detail) => {
691
+ // Check if this resolveDetail references our target variable
692
+ if (detail.fullMatch === targetVariable) {
693
+ count++
694
+ }
695
+ })
696
+ }
697
+ })
698
+ })
699
+
700
+ return count
701
+ }
702
+
629
703
  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}`
704
+ const refCount = countAllReferences(k)
705
+ const placesWord = refCount > 1 ? 'places' : 'place'
706
+ return `- ${k.padEnd(longestKey).padEnd(longestKey + 10)} referenced ${refCount} ${placesWord}`
632
707
  }).join('\n'))
633
708
  console.log()
634
709
  }
@@ -697,7 +772,9 @@ class Configorama {
697
772
  firstInstance.defaultValue = truncatedString
698
773
  } else {
699
774
  deepLog('Missing default var', firstInstance)
700
- throw new Error(`Variable misconfiguration at ${firstInstance.variable}\n\n"${hasDotPropOrSelf[0].variable}" resolves to undefined value.\n`)
775
+ throw new Error(
776
+ `Variable misconfiguration at ${firstInstance.variable}\n\n"${hasDotPropOrSelf[0].variable}" resolves to undefined value.\n`
777
+ )
701
778
  }
702
779
  }
703
780
  //this.originalConfig[key] = undefined
@@ -802,6 +879,7 @@ class Configorama {
802
879
  return this.populateObjectImpl(this.config).finally(() => {
803
880
  // TODO populate function values here?
804
881
  // console.log('Final Config', this.config)
882
+ // console.log(this.deep)
805
883
  const transform = this.runFunction.bind(this)
806
884
  const varSyntax = this.variableSyntax
807
885
  const leaves = this.leaves
@@ -812,7 +890,9 @@ class Configorama {
812
890
  /* Pass through unknown variables */
813
891
  if (!configoramaOpts.allowUndefinedValues && typeof rawValue === 'undefined') {
814
892
  const configValuePath = this.path.join('.')
893
+ /*
815
894
  console.log(this.path)
895
+ /** */
816
896
  const ogValue = dotProp.get(originalConfig, configValuePath)
817
897
  const varDisplay = ogValue ? `"${ogValue}" variable` : 'variable'
818
898
 
@@ -889,6 +969,214 @@ class Configorama {
889
969
  })
890
970
  })
891
971
  }
972
+
973
+ /**
974
+ * Collect metadata about all variables found in the configuration
975
+ * @returns {object} Metadata object containing variables, fileRefs, and summary
976
+ */
977
+ collectVariableMetadata() {
978
+ const variableSyntax = this.variableSyntax
979
+ const variablesKnownTypes = this.variablesKnownTypes
980
+ const foundVariables = []
981
+ const variableData = {}
982
+ const fileRefs = []
983
+ const fileGlobPatterns = []
984
+ let matchCount = 1
985
+
986
+ traverse(this.originalConfig).forEach(function (rawValue) {
987
+ if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
988
+ const configValuePath = this.path.join('.')
989
+ /* Skip Fn::Sub variables */
990
+ if (configValuePath.endsWith('Fn::Sub')) {
991
+ return
992
+ }
993
+
994
+ const nested = findNestedVariables(rawValue, variableSyntax, variablesKnownTypes, configValuePath)
995
+
996
+ const lastItem = nested[nested.length - 1]
997
+ const lastKeyPath = this.path[this.path.length - 1]
998
+ const itemKey = (lastKeyPath.match(/[\d+]$/)) ? `${this.path[this.path.length - 2]}[${lastKeyPath}]` : lastKeyPath
999
+ const key = lastItem.fullMatch
1000
+ const varData = {
1001
+ path: configValuePath,
1002
+ key: itemKey,
1003
+ value: rawValue,
1004
+ variable: lastItem.fullMatch,
1005
+ isRequired: false,
1006
+ defaultValue: undefined,
1007
+ matchIndex: matchCount++,
1008
+ resolveOrder: [],
1009
+ resolveDetails: nested,
1010
+ }
1011
+ let defaultValueIsVar = false
1012
+
1013
+ function calculateResolveOrder(item) {
1014
+ if (item && item.fallbackValues) {
1015
+ let hasResolvedFallback
1016
+ const order = ([item.valueBeforeFallback]).concat(item.fallbackValues.map((f, i) => {
1017
+ if (f.fallbackValues) {
1018
+ const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
1019
+ if (!hasResolvedFallback && nestedResolvedFallback) {
1020
+ hasResolvedFallback = nestedResolvedFallback
1021
+ }
1022
+ return nestedOrder
1023
+ }
1024
+
1025
+ if (!hasResolvedFallback && f.isResolvedFallback) {
1026
+ hasResolvedFallback = f.stringValue
1027
+ }
1028
+ if (f.isResolvedFallback) {
1029
+ hasResolvedFallback = f.stringValue
1030
+ }
1031
+
1032
+ if (!hasResolvedFallback && f.isVariable) {
1033
+ defaultValueIsVar = f
1034
+ }
1035
+ return `${f.stringValue || f.variable}${f.isResolvedFallback ? ' (Resolved default fallback)' : ''}`
1036
+ })).flat()
1037
+
1038
+ return [order, hasResolvedFallback]
1039
+ }
1040
+ return [[item.variable], undefined]
1041
+ }
1042
+
1043
+ const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
1044
+ varData.resolveOrder = resolveOrder
1045
+
1046
+ if (defaultValueIsVar) {
1047
+ varData.defaultValueIsVar = defaultValueIsVar
1048
+ }
1049
+
1050
+ if (typeof hasResolvedFallback !== 'undefined') {
1051
+ varData.defaultValue = hasResolvedFallback
1052
+ }
1053
+
1054
+ if (typeof varData.defaultValue === 'undefined') {
1055
+ varData.isRequired = true
1056
+ }
1057
+
1058
+ if (varData.resolveOrder.length > 1) {
1059
+ varData.hasFallback = true
1060
+ }
1061
+
1062
+ // Extract file references
1063
+ nested.forEach((detail) => {
1064
+ if (detail.varType &&
1065
+ (detail.varType.startsWith('file(') || detail.varType.startsWith('text('))
1066
+ ) {
1067
+ const fileMatch = detail.varType.match(/^(?:file|text)\((.*?)\)/)
1068
+ if (fileMatch && fileMatch[1]) {
1069
+ let fileContent = fileMatch[1].trim()
1070
+
1071
+ // Split by comma to separate file path from parameters/fallback values
1072
+ const parts = splitCsv(fileContent)
1073
+ let filePath = parts[0].trim()
1074
+
1075
+ // Remove quotes if present
1076
+ filePath = filePath.replace(/^['"]|['"]$/g, '')
1077
+
1078
+ // Normalize path: ensure relative paths start with ./
1079
+ let normalizedPath = filePath
1080
+ if (
1081
+ !filePath.startsWith('./') &&
1082
+ !filePath.startsWith('../') &&
1083
+ !filePath.startsWith('/') &&
1084
+ !filePath.startsWith('~')
1085
+ ) {
1086
+ normalizedPath = './' + filePath
1087
+ }
1088
+
1089
+ // file .//
1090
+ if (normalizedPath.startsWith('.//')) {
1091
+ normalizedPath = normalizedPath.replace('.//', './')
1092
+ }
1093
+
1094
+ // Handle variables in file paths - just record the pattern
1095
+ if (!fileRefs.includes(normalizedPath)) {
1096
+ fileRefs.push(normalizedPath)
1097
+ }
1098
+
1099
+ // Check if path contains variables and create glob pattern
1100
+ if (normalizedPath.match(variableSyntax)) {
1101
+ // Replace variable syntax ${...} with * for glob pattern
1102
+ const globPattern = normalizedPath.replace(variableSyntax, '*')
1103
+ if (!fileGlobPatterns.includes(globPattern)) {
1104
+ fileGlobPatterns.push(globPattern)
1105
+ }
1106
+ }
1107
+ }
1108
+ }
1109
+ })
1110
+
1111
+ variableData[key] = (variableData[key] || []).concat(varData)
1112
+ foundVariables.push(rawValue)
1113
+ }
1114
+ })
1115
+
1116
+ // Make foundVariables array unique
1117
+ const finalFoundVariables = [...new Set(foundVariables)]
1118
+ const varKeys = Object.keys(variableData)
1119
+
1120
+ // Calculate summary using same logic as CLI display
1121
+ let requiredCount = 0
1122
+ let withDefaultsCount = 0
1123
+ varKeys.forEach((key) => {
1124
+ const instances = variableData[key]
1125
+ const firstInstance = instances[0]
1126
+
1127
+ // Check if truly required using same logic as display code
1128
+ let isTrulyRequired = false
1129
+ if (typeof firstInstance.defaultValue === 'undefined') {
1130
+ // Check for self-references that resolve to config values
1131
+ let dotPropArr = []
1132
+ if (firstInstance.defaultValueIsVar && (
1133
+ firstInstance.defaultValueIsVar.varType === 'self:' ||
1134
+ firstInstance.defaultValueIsVar.varType === 'dot.prop'
1135
+ )) {
1136
+ dotPropArr = [firstInstance.defaultValueIsVar]
1137
+ }
1138
+
1139
+ const hasDotPropOrSelf = instances.reduce((acc, v) => {
1140
+ const dotProp = v.resolveDetails.find((d) => d.varType === 'dot.prop')
1141
+ if (dotProp) {
1142
+ acc.push(dotProp)
1143
+ }
1144
+ if (v.resolveDetails && v.resolveDetails.length === 1 && v.resolveDetails[0].varType === 'self:') {
1145
+ acc.push(v.resolveDetails[0])
1146
+ }
1147
+ return acc
1148
+ }, dotPropArr)
1149
+
1150
+ if (!hasDotPropOrSelf.length) {
1151
+ isTrulyRequired = true
1152
+ } else {
1153
+ // Check if the self-reference resolves to a value
1154
+ const cleanPath = hasDotPropOrSelf[0].variable.replace('self:', '')
1155
+ const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1156
+ if (typeof dotPropValue === 'undefined') {
1157
+ isTrulyRequired = true
1158
+ }
1159
+ }
1160
+ }
1161
+
1162
+ if (isTrulyRequired) {
1163
+ requiredCount++
1164
+ } else {
1165
+ withDefaultsCount++
1166
+ }
1167
+ })
1168
+
1169
+ return {
1170
+ variables: variableData,
1171
+ summary: {
1172
+ totalVariables: varKeys.length,
1173
+ requiredVariables: requiredCount,
1174
+ variablesWithDefaults: withDefaultsCount
1175
+ },
1176
+ fileRefs: fileRefs,
1177
+ fileGlobPatterns: fileGlobPatterns,
1178
+ }
1179
+ }
892
1180
  runFunction(variableString) {
893
1181
  // console.log('runFunction', variableString)
894
1182
  /* If json object value return it */
@@ -918,7 +1206,6 @@ class Configorama {
918
1206
  argsToPass = formatFunctionArgs(splitter)
919
1207
  }
920
1208
  // console.log('argsToPass runFunction', argsToPass)
921
-
922
1209
  // TODO check for camelCase version. | toUpperCase messes with function name
923
1210
  const theFunction = this.functions[functionName] || this.functions[functionName.toLowerCase()]
924
1211
 
@@ -1016,6 +1303,15 @@ class Configorama {
1016
1303
  }
1017
1304
  }
1018
1305
  leaf.originalSource = originalValue
1306
+
1307
+ // Check if we have existing resolution history from previous iterations
1308
+ const pathKey = thePath
1309
+ if (this.resolutionTracking[pathKey] && this.resolutionTracking[pathKey].resolutionHistory) {
1310
+ leaf.resolutionHistory = this.resolutionTracking[pathKey].resolutionHistory
1311
+ } else {
1312
+ leaf.resolutionHistory = []
1313
+ }
1314
+
1019
1315
  if (originalValue && isString(originalValue)) {
1020
1316
  const varString = cleanVariable(originalValue, this.variableSyntax, true, `getProperties ${this.callCount}`)
1021
1317
  if (varString.match(fileRefSyntax)) {
@@ -1027,7 +1323,6 @@ class Configorama {
1027
1323
  }
1028
1324
  return results
1029
1325
  }
1030
-
1031
1326
  /**
1032
1327
  * @typedef {TerminalProperty} TerminalPropertyPopulated
1033
1328
  * @property {Object} populated The populated value of the value at the path
@@ -1044,11 +1339,9 @@ class Configorama {
1044
1339
  // Initial check if value has variable string in it
1045
1340
  return isString(property.value) && property.value.match(this.variableSyntax)
1046
1341
  })
1047
-
1048
1342
  /*
1049
1343
  console.log(`variables at call count ${this.callCount}`, variables)
1050
1344
  /** */
1051
-
1052
1345
  /* Exclude git messages from being processed */
1053
1346
  // Was failing on git msgs like "xyz cron:pattern to cron(pattern) for improved clarity"
1054
1347
  if (this.callCount > 1) {
@@ -1060,7 +1353,6 @@ class Configorama {
1060
1353
  return true
1061
1354
  })
1062
1355
  }
1063
-
1064
1356
  return map(variables, (valueObject) => {
1065
1357
  // console.log('valueObject', valueObject)
1066
1358
  return this.populateValue(valueObject, false, '_populateVariables').then((populated) => {
@@ -1163,16 +1455,127 @@ class Configorama {
1163
1455
  */
1164
1456
  renderMatches(valueObject, matches, results) {
1165
1457
  /*
1458
+ console.log('valueObject', valueObject)
1166
1459
  console.log('RENDER', matches)
1167
1460
  console.log('RESULTS', results)
1168
1461
  /** */
1462
+
1463
+ /* Attach data to valueObject for parent details */
1464
+ if (matches.length === 1) {
1465
+ valueObject.currentVarDetails = matches[0]
1466
+ valueObject.currentVarDetails.result = results[0]
1467
+
1468
+ // Extract metadata from result if present
1469
+ let actualResult = results[0]
1470
+ let resolverType = undefined
1471
+ if (results[0] && typeof results[0] === 'object') {
1472
+ if (results[0].__internal_metadata) {
1473
+ actualResult = results[0].value
1474
+ resolverType = results[0].__resolverType
1475
+ } else if (results[0].__internal_only_flag) {
1476
+ // Don't extract value from __internal_only_flag objects, but grab resolverType if present
1477
+ actualResult = results[0]
1478
+ resolverType = results[0].__resolverType
1479
+ }
1480
+ }
1481
+ // valueObject.currentVarDetails.varType = results[0].__resolverType
1482
+
1483
+ // Track resolution history
1484
+ if (!valueObject.resolutionHistory) {
1485
+ valueObject.resolutionHistory = []
1486
+ }
1487
+
1488
+ // Extract clean result to avoid circular references
1489
+ // For __internal_only_flag objects (like deep resolver results), extract the value
1490
+ // For real data objects (like file contents), keep them as-is
1491
+ let cleanResult = actualResult
1492
+ if (actualResult && typeof actualResult === 'object' && actualResult.__internal_only_flag) {
1493
+ cleanResult = actualResult.value
1494
+ }
1495
+
1496
+ const historyEntry = {
1497
+ match: matches[0].match,
1498
+ variable: matches[0].variable,
1499
+ result: cleanResult,
1500
+ resultType: typeof cleanResult,
1501
+ valueBeforeResolution: valueObject.value,
1502
+ }
1503
+ if (resolverType) {
1504
+ historyEntry.varType = resolverType
1505
+ }
1506
+
1507
+ // Check if variable has fallback values (comma-separated)
1508
+ const variableParts = splitByComma(matches[0].variable)
1509
+ if (variableParts.length > 1) {
1510
+ historyEntry.hasFallback = true
1511
+ historyEntry.valueBeforeFallback = variableParts[0]
1512
+ historyEntry.fallbackValues = variableParts.slice(1).map((fallback) => {
1513
+ const trimmedFallback = fallback.trim()
1514
+ // Check if it's a variable reference
1515
+ const isVariable = trimmedFallback.match(this.variableSyntax) || trimmedFallback.match(this.variablesKnownTypes)
1516
+ const fallbackData = {
1517
+ isVariable: !!isVariable,
1518
+ fullMatch: trimmedFallback,
1519
+ variable: trimmedFallback,
1520
+ }
1521
+
1522
+ // If it's a literal string/number, parse it
1523
+ if (!isVariable) {
1524
+ // Check if it's a quoted string
1525
+ if (/^["'].*["']$/.test(trimmedFallback)) {
1526
+ fallbackData.stringValue = trimmedFallback.replace(/^["']|["']$/g, '')
1527
+ fallbackData.isResolvedFallback = true
1528
+ } else if (/^-?\d+(\.\d+)?$/.test(trimmedFallback)) {
1529
+ // It's a number
1530
+ fallbackData.numberValue = parseFloat(trimmedFallback)
1531
+ fallbackData.isResolvedFallback = true
1532
+ } else {
1533
+ fallbackData.stringValue = trimmedFallback
1534
+ fallbackData.isResolvedFallback = true
1535
+ }
1536
+ } else {
1537
+ // Extract varType from variable references
1538
+ const varTypeMatch = trimmedFallback.match(this.variablesKnownTypes)
1539
+ if (varTypeMatch && varTypeMatch[1]) {
1540
+ fallbackData.varType = varTypeMatch[1]
1541
+ }
1542
+ }
1543
+
1544
+ return fallbackData
1545
+ })
1546
+ }
1547
+
1548
+ // Only add to history if not a duplicate (same match + variable)
1549
+ const isDuplicate = valueObject.resolutionHistory.some(entry =>
1550
+ entry.match === historyEntry.match &&
1551
+ entry.variable === historyEntry.variable
1552
+ )
1553
+
1554
+ if (!isDuplicate) {
1555
+ valueObject.resolutionHistory.push(historyEntry)
1556
+ }
1557
+
1558
+ // Save resolution history to tracking map for persistence across iterations
1559
+ if (valueObject.path && valueObject.path.length) {
1560
+ const pathKey = valueObject.path.join('.')
1561
+ if (!this.resolutionTracking[pathKey]) {
1562
+ this.resolutionTracking[pathKey] = {
1563
+ path: pathKey,
1564
+ originalPropertyString: valueObject.originalSource,
1565
+ calls: []
1566
+ }
1567
+ }
1568
+ this.resolutionTracking[pathKey].resolutionHistory = valueObject.resolutionHistory
1569
+ }
1570
+ }
1571
+
1169
1572
  let result = valueObject.value
1170
1573
  for (let i = 0; i < matches.length; i += 1) {
1171
1574
  this.warnIfNotFound(matches[i].variable, results[i])
1172
1575
  // console.log('Render MATCHES', results[i])
1173
1576
  let valueToPop = results[i]
1174
1577
  // 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) {
1578
+ if (results[i] && typeof results[i] === 'object' && (results[i].__internal_only_flag || results[i].__internal_metadata)) {
1176
1579
  valueToPop = results[i].value
1177
1580
  }
1178
1581
  result = this.populateVariable(valueObject, matches[i].match, valueToPop)
@@ -1216,7 +1619,10 @@ class Configorama {
1216
1619
  .then((result) => {
1217
1620
  // console.log('renderMatches result', result)
1218
1621
  if (root && isArray(matches)) {
1219
- return this.populateValue({ value: result.value }, root, 'self populateValue')
1622
+ return this.populateValue({
1623
+ value: result.value,
1624
+ resolutionHistory: result.resolutionHistory || valueObject.resolutionHistory || []
1625
+ }, root, 'self populateValue')
1220
1626
  }
1221
1627
  return result
1222
1628
  })
@@ -1254,11 +1660,11 @@ class Configorama {
1254
1660
 
1255
1661
  const parts = splitByComma(variable, this.variableSyntax)
1256
1662
  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)
1663
+ console.log('splitAndGet parts', parts)
1664
+ console.log('splitAndGet parts variable:', variable)
1665
+ console.log('splitAndGet parts originalVar:', originalVar)
1666
+ console.log('splitAndGet parts current valueObject:', valueObject)
1667
+ console.log('splitAndGet All parts:', parts)
1262
1668
  console.log('-----')
1263
1669
  }
1264
1670
  if (parts.length <= 1) {
@@ -1279,16 +1685,20 @@ class Configorama {
1279
1685
  populateVariable(valueObject, matchedString, valueToPopulate) {
1280
1686
  let property = valueObject.value
1281
1687
  // console.log('init property', property)
1282
- let DEBUG_TYPE = false
1688
+
1283
1689
  if (DEBUG) {
1284
1690
  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)
1691
+ console.log('populateVariable: valueObject', valueObject)
1692
+ console.log('populateVariable: valueToPopulate', valueToPopulate)
1693
+ console.log('populateVariable: typeof valueToPopulate', typeof valueToPopulate)
1694
+ console.log(`populateVariable: path "${valueObject.path}"`)
1695
+ console.log(`populateVariable: value \`${valueObject.value}\``)
1696
+ console.log(`populateVariable: originalSource \`${valueObject.originalSource}\``)
1697
+ console.log('populateVariable: property', property)
1698
+ console.log('populateVariable: matchedString', matchedString)
1699
+ if (valueObject.resolutionHistory && valueObject.resolutionHistory.length > 0) {
1700
+ console.log('populateVariable: resolutionHistory', JSON.stringify(valueObject.resolutionHistory, null, 2))
1701
+ }
1292
1702
  }
1293
1703
 
1294
1704
  const originalSrc = valueObject.originalSource || ''
@@ -1307,9 +1717,35 @@ class Configorama {
1307
1717
  if (DEBUG_TYPE) console.log('DEBUG_TYPE total replacement')
1308
1718
  const v = valueObject.value || ''
1309
1719
  property = valueToPopulate
1720
+ // console.log('hasFilters', hasFilters)
1721
+ // console.log('valueToPopulate', valueToPopulate)
1722
+ /* Check resolution history for parent details */
1723
+ if (valueObject.resolutionHistory && valueObject.resolutionHistory.length) {
1724
+ const currentDetails = valueObject.resolutionHistory[valueObject.resolutionHistory.length - 1]
1725
+
1726
+ // get 2nd to last item in resolution history
1727
+ const parentDetails = valueObject.resolutionHistory[valueObject.resolutionHistory.length - 2]
1728
+ /*
1729
+ console.log('currentDetails', currentDetails)
1730
+ console.log('parentDetails', parentDetails)
1731
+ /** */
1732
+
1733
+ /* Convert a fallback number to string */
1734
+ if (currentDetails &&
1735
+ currentDetails.resultType === 'number' &&
1736
+ parentDetails && parentDetails.resultType === 'string' &&
1737
+ parentDetails.result.match(/^\d+$/) && parentDetails.varType === 'env'
1738
+ ) {
1739
+ if (Number(parentDetails.result) === currentDetails.result) {
1740
+ property = String(valueToPopulate)
1741
+ }
1742
+ }
1743
+
1744
+ }
1310
1745
 
1311
1746
  /* Handle ${self:custom.ref, ''} with deep values */
1312
1747
  if (v.match(deepRefSyntax) && originalSrc.match(this.variableSyntax) && !v.match(/deep\:(\d*)\..*}$/)) {
1748
+ // console.log('deep ref syntax')
1313
1749
  // console.log('deep var', this.deep)
1314
1750
  // console.log('originalSrc', originalSrc)
1315
1751
  // console.log('value', v)
@@ -1412,7 +1848,12 @@ class Configorama {
1412
1848
  missingValue = this.deep[i]
1413
1849
  }
1414
1850
 
1415
- const cleanVar = cleanVariable(property, this.variableSyntax, true, `populateVariable fallback ${this.callCount}`)
1851
+ const cleanVar = cleanVariable(
1852
+ property,
1853
+ this.variableSyntax,
1854
+ true,
1855
+ `populateVariable fallback ${this.callCount}`
1856
+ )
1416
1857
  const cleanVarNoFilters = cleanVar.split('|')[0]
1417
1858
  const splitVars = splitByComma(cleanVarNoFilters)
1418
1859
  const nestedVar = findNestedVariable(splitVars, valueObject.originalSource)
@@ -1427,6 +1868,7 @@ class Configorama {
1427
1868
  value: fallbackStr,
1428
1869
  path: valueObject.path,
1429
1870
  originalSource: valueObject.originalSource,
1871
+ resolutionHistory: valueObject.resolutionHistory || [],
1430
1872
  // set __internal_only_flag to note this is object we make not a resolved value
1431
1873
  __internal_only_flag: true,
1432
1874
  caller: 'nestedVar',
@@ -1478,6 +1920,7 @@ Missing Value ${missingValue} - ${matchedString}
1478
1920
  value: finalProp, // prop to fix nested ¯\_(ツ)_/¯
1479
1921
  path: valueObject.path,
1480
1922
  originalSource: valueObject.originalSource,
1923
+ resolutionHistory: valueObject.resolutionHistory || [],
1481
1924
  // set __internal_only_flag to note this is object we make not a resolved value
1482
1925
  // __internal_only_flag: true
1483
1926
  }
@@ -1496,13 +1939,15 @@ Missing Value ${missingValue} - ${matchedString}
1496
1939
  }
1497
1940
  }
1498
1941
  */
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
1942
 
1504
1943
  if (
1944
+ /* Not another variable reference */
1945
+ !prop.match(this.variableSyntax)
1946
+ &&
1947
+ /* Not file or text refs */
1505
1948
  !prop.match(fileRefSyntax)
1949
+ && !prop.match(textRefSyntax)
1950
+ /* Not eval refs */
1506
1951
  && !prop.match(getValueFromEval.match)
1507
1952
  // AND is not multiline value
1508
1953
  && (func && prop.split('\n').length < 3)) {
@@ -1523,14 +1968,21 @@ Missing Value ${missingValue} - ${matchedString}
1523
1968
 
1524
1969
  // console.log('foundFilters', foundFilters)
1525
1970
 
1526
- /* Apply filters if found */
1527
- //console.log('> property', property)
1528
- if (
1529
- foundFilters.length > 0 &&
1530
- typeof valueToPopulate === 'string' &&
1531
- !valueToPopulate.match(deepRefSyntax) &&
1971
+ let runFilters = false
1972
+ if (typeof valueToPopulate === 'number' && foundFilters.length) {
1973
+ runFilters = true
1974
+ } else if (
1975
+ typeof valueToPopulate === 'string' &&
1976
+ !valueToPopulate.match(deepRefSyntax) &&
1977
+ foundFilters.length &&
1532
1978
  !property.match(this.variableSyntax)
1533
1979
  ) {
1980
+ runFilters = true
1981
+ }
1982
+
1983
+ /* Apply filters if found */
1984
+ //console.log('> property', property)
1985
+ if (runFilters) {
1534
1986
  // If filter cache exists we need to remove filter that have already been run
1535
1987
  if (this.filterCache[valueObject.path]) {
1536
1988
  foundFilters = foundFilters.filter((filter) => {
@@ -1555,6 +2007,7 @@ Missing Value ${missingValue} - ${matchedString}
1555
2007
  value: property,
1556
2008
  path: valueObject.path,
1557
2009
  originalSource: valueObject.originalSource,
2010
+ resolutionHistory: valueObject.resolutionHistory || [],
1558
2011
  __internal_only_flag: true, // set __internal_only_flag to note this is object we make not a resolved value
1559
2012
  caller: 'end',
1560
2013
  count: this.callCount,
@@ -1596,15 +2049,22 @@ Missing Value ${missingValue} - ${matchedString}
1596
2049
  // console.log('propertyString', typeof propertyString)
1597
2050
  const variableValues = variableStrings.map((variableString) => {
1598
2051
  // This runs on nested variable resolution
1599
- return this.getValueFromSource(variableString, valueObject, 'overwrite')
2052
+ return this.getValueFromSource(variableString, valueObject, 'overwrite', valueObject.originalSource)
1600
2053
  })
1601
-
2054
+
1602
2055
  // console.log('variableValues', variableValues)
1603
2056
  return Promise.all(variableValues).then((values) => {
1604
2057
  let deepPropertyStr = propertyString
1605
2058
  let deepProperties = 0
1606
2059
  // console.log('overwrite values', valuesToUse)
1607
- values.forEach((value, index) => {
2060
+ // Extract actual values from metadata objects
2061
+ const extractedValues = values.map((value) => {
2062
+ if (value && typeof value === 'object' && (value.__internal_only_flag || value.__internal_metadata)) {
2063
+ return value.value
2064
+ }
2065
+ return value
2066
+ })
2067
+ extractedValues.forEach((value, index) => {
1608
2068
  // console.log('───────────────────────────────> value', value)
1609
2069
  if (isString(value) && value.match(this.variableSyntax)) {
1610
2070
  deepProperties += 1
@@ -1620,7 +2080,7 @@ Missing Value ${missingValue} - ${matchedString}
1620
2080
  })
1621
2081
  return deepProperties > 0
1622
2082
  ? Promise.resolve(deepPropertyStr) // return deep variable replacement of original
1623
- : Promise.resolve(values.find(isValidValue)) // resolve first valid value, else undefined
2083
+ : Promise.resolve(extractedValues.find(isValidValue)) // resolve first valid value, else undefined
1624
2084
  })
1625
2085
  }
1626
2086
  /**
@@ -1632,6 +2092,25 @@ Missing Value ${missingValue} - ${matchedString}
1632
2092
  // console.log('getValueFromSrc caller', caller)
1633
2093
  const propertyString = valueObject.value
1634
2094
  const pathValue = valueObject.path
2095
+
2096
+ // Track every call to getValueFromSource for metadata
2097
+ if (pathValue && pathValue.length) {
2098
+ const pathKey = pathValue.join('.')
2099
+ if (!this.resolutionTracking[pathKey]) {
2100
+ this.resolutionTracking[pathKey] = {
2101
+ path: pathKey,
2102
+ originalPropertyString: propertyString,
2103
+ calls: []
2104
+ }
2105
+ }
2106
+
2107
+ this.resolutionTracking[pathKey].calls.push({
2108
+ variableString: variableString,
2109
+ propertyString: propertyString,
2110
+ caller: caller
2111
+ })
2112
+ }
2113
+
1635
2114
  // console.log('getValueFromSrc propertyString', propertyString)
1636
2115
  // console.log(`tracker contains ${variableString}`, this.tracker.contains(variableString))
1637
2116
  if (this.tracker.contains(variableString)) {
@@ -1642,11 +2121,12 @@ Missing Value ${missingValue} - ${matchedString}
1642
2121
  let newHasFilter
1643
2122
  // Else lookup value from various sources
1644
2123
  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)
2124
+ console.log(`>>>>> getValueFromSrc() caller - ${caller}`)
2125
+ console.log('getValueFromSource originalVar', originalVar)
2126
+ console.log('getValueFromSource variableString:', variableString)
2127
+ console.log('getValueFromSource propertyString:', propertyString)
2128
+ console.log('getValueFromSource pathValue:', valueObject.path)
2129
+ console.log('getValueFromSource valueObject:', valueObject)
1650
2130
  console.log('-----')
1651
2131
  }
1652
2132
 
@@ -1674,6 +2154,8 @@ Missing Value ${missingValue} - ${matchedString}
1674
2154
  })
1675
2155
  .map((f) => {
1676
2156
  return trim(f)
2157
+ // TODO refactor this. This is a temp fix for filters with nested vars.
2158
+ .replace(/}$/, '')
1677
2159
  })
1678
2160
  // console.log('filters to run', _filter)
1679
2161
 
@@ -1730,6 +2212,21 @@ Missing Value ${missingValue} - ${matchedString}
1730
2212
  valueObject,
1731
2213
 
1732
2214
  ).then((val) => {
2215
+ // Update the last call with the resolved value
2216
+ if (pathValue && pathValue.length) {
2217
+ const pathKey = pathValue.join('.')
2218
+ if (this.resolutionTracking[pathKey] && this.resolutionTracking[pathKey].calls.length) {
2219
+ // Find the most recent call for this variableString
2220
+ for (let i = this.resolutionTracking[pathKey].calls.length - 1; i >= 0; i--) {
2221
+ if (this.resolutionTracking[pathKey].calls[i].variableString === variableString) {
2222
+ this.resolutionTracking[pathKey].calls[i].resolvedValue = val
2223
+ this.resolutionTracking[pathKey].calls[i].resolverType = resolverType
2224
+ break
2225
+ }
2226
+ }
2227
+ }
2228
+ }
2229
+
1733
2230
  // console.log('VALUE', val)
1734
2231
  if (
1735
2232
  val === null ||
@@ -1763,7 +2260,7 @@ Missing Value ${missingValue} - ${matchedString}
1763
2260
  // console.log('valueCount', valueCount)
1764
2261
  // TODO throw on empty values?
1765
2262
  // No fallback value found AND this is undefined, throw error
1766
- const nestedVars = findNestedVariables(propertyString, this.variableSyntax)
2263
+ const nestedVars = findNestedVariables(propertyString, this.variableSyntax, this.variablesKnownTypes)
1767
2264
  // console.log('nestedVars', nestedVars)
1768
2265
  const noNestedVars = nestedVars.length < 2
1769
2266
  if (valueCount.length === 1 && noNestedVars) {
@@ -1792,7 +2289,19 @@ Unable to resolve configuration variable
1792
2289
  if (!newHasFilter) {
1793
2290
  // console.log('no newHasFilter', val, valueObject)
1794
2291
  // console.log('> RESOLVER RETURN newValue 3', val, originalVar)
1795
- return Promise.resolve(val)
2292
+ // Wrap value with resolverType metadata for resolution tracking
2293
+ // But don't wrap if it's already an internal flag object
2294
+ if (val && typeof val === 'object' && val.__internal_only_flag) {
2295
+ // Attach resolverType to existing internal object
2296
+ val.__resolverType = resolverType
2297
+ return Promise.resolve(val)
2298
+ }
2299
+ return Promise.resolve({
2300
+ value: val,
2301
+ __resolverType: resolverType,
2302
+ __variableString: variableString,
2303
+ __internal_metadata: true
2304
+ })
1796
2305
  }
1797
2306
 
1798
2307
  const newUse = newHasFilter.reduce((acc, currentFilter, i) => {
@@ -1805,6 +2314,8 @@ Unable to resolve configuration variable
1805
2314
  // args: argsToPass
1806
2315
  })
1807
2316
  }, [])
2317
+ // console.log('pathValue', pathValue)
2318
+ // console.log('propertyString', propertyString)
1808
2319
  // console.log('newUse', newUse)
1809
2320
 
1810
2321
  if (typeof val === 'string' && val.match(/deep:/)) {
@@ -1841,7 +2352,19 @@ Unable to resolve configuration variable
1841
2352
  }, val)
1842
2353
  // console.log('> RESOLVER RETURN newValue', newValue)
1843
2354
  // console.log('> RESOLVER RETURN newValue 5', newValue)
1844
- return Promise.resolve(newValue)
2355
+ // Wrap value with resolverType metadata for resolution tracking
2356
+ // But don't wrap if it's already an internal flag object
2357
+ if (newValue && typeof newValue === 'object' && newValue.__internal_only_flag) {
2358
+ // Attach resolverType to existing internal object
2359
+ newValue.__resolverType = resolverType
2360
+ return Promise.resolve(newValue)
2361
+ }
2362
+ return Promise.resolve({
2363
+ value: newValue,
2364
+ __resolverType: resolverType,
2365
+ __variableString: variableString,
2366
+ __internal_metadata: true
2367
+ })
1845
2368
  })
1846
2369
 
1847
2370
  // console.log('valuePromise', valuePromise)
@@ -1851,12 +2374,20 @@ Unable to resolve configuration variable
1851
2374
  return this.tracker.add(variableString, valuePromise, propertyString, newHasFilter, promiseKey)
1852
2375
  }
1853
2376
 
2377
+ // console.log('fall thru variableString', variableString)
2378
+
1854
2379
  /* fall through case with self refs */
1855
2380
  if (variableString) {
1856
2381
  // console.log('before clean propertyString', propertyString, variableString)
1857
- const clean = cleanVariable(propertyString, this.variableSyntax, true, `getValueFromSrc self ${this.callCount}`)
2382
+ const clean = cleanVariable(
2383
+ propertyString,
2384
+ this.variableSyntax,
2385
+ true,
2386
+ `getValueFromSrc self ${this.callCount}`
2387
+ )
1858
2388
  // TODO @DWELLS cleanVariable makes fallback values with spaces have no spaces
1859
2389
  // console.log('AFTER cleanVariable', clean)
2390
+ // console.log(typeof clean)
1860
2391
  const cleanClean = clean.split('|')[0]
1861
2392
  // console.log('cleanCleanVariable', cleanClean)
1862
2393
  if (funcRegex.exec(cleanClean)) {
@@ -1865,7 +2396,8 @@ Unable to resolve configuration variable
1865
2396
  }
1866
2397
 
1867
2398
  const split = splitByComma(cleanClean)
1868
-
2399
+ // console.log('split', split)
2400
+ // console.log('typeof split', typeof split)
1869
2401
  // @TODO refactor this. USE FILTER [ 'commas', 'split("-"' ] is wrong
1870
2402
  let fallbackValue
1871
2403
  if (split.length === 2 || split.length === 3) {
@@ -1874,7 +2406,9 @@ Unable to resolve configuration variable
1874
2406
  fallbackValue = split[0]
1875
2407
  }
1876
2408
 
2409
+ // TODO this should be new in memory resolutionHistory probably?
1877
2410
  const nestedVar = findNestedVariable(split, valueObject.originalSource)
2411
+ // console.log('nestedVar', nestedVar)
1878
2412
 
1879
2413
  if (nestedVar) {
1880
2414
  if (!this.opts.allowUnknownVars) {
@@ -1883,7 +2417,7 @@ Unable to resolve configuration variable
1883
2417
  const fallbackStr = getFallbackString(split, nestedVar)
1884
2418
  return this.getValueFromSource(variableString, {
1885
2419
  value: fallbackStr,
1886
- }, 'nestedVar')
2420
+ }, 'nestedVar', originalVar)
1887
2421
  }
1888
2422
 
1889
2423
  // TODO verify we need this still with file(file.js, param)
@@ -1895,7 +2429,7 @@ Unable to resolve configuration variable
1895
2429
  // recurse on fallback and check again
1896
2430
  return this.getValueFromSource(`${variableString})`, {
1897
2431
  value: propertyString,
1898
- }, 'cleanClean.match(fileRefSyntax)')
2432
+ }, 'cleanClean.match(fileRefSyntax)', originalVar)
1899
2433
  }
1900
2434
  }
1901
2435
  // const fallbackValue = split[1]
@@ -1907,7 +2441,10 @@ Unable to resolve configuration variable
1907
2441
  const valuePromise = Promise.resolve(fallbackValue)
1908
2442
  return this.tracker.add(fallbackValue, valuePromise, propertyString, newHasFilter)
1909
2443
  }
1910
-
2444
+ /*
2445
+ console.log('what is fallbackValue', fallbackValue)
2446
+ console.log('typeof fallbackValue', typeof fallbackValue)
2447
+ /** */
1911
2448
  // has fallback but needs deeper lookup. Call getValueFromSrc again
1912
2449
  if (fallbackValue) {
1913
2450
  if (DEBUG) console.log('fallbackValue', fallbackValue)
@@ -1915,11 +2452,21 @@ Unable to resolve configuration variable
1915
2452
  // recurse on fallback and check again
1916
2453
  return this.getValueFromSource(
1917
2454
  fallbackValue,
1918
- {
1919
- value: propertyString,
1920
- },
2455
+ valueObject,
2456
+ // Object.assign({}, valueObject, { value: propertyString }),
2457
+ // {
2458
+ // value: propertyString,
2459
+ // path: valueObject.path,
2460
+ // originalSource: valueObject.originalSource,
2461
+ // ahh:true
2462
+ // },
1921
2463
  'fallbackValue',
1922
- )
2464
+ originalVar,
2465
+ ).then((res) => {
2466
+ // console.log('res', res)
2467
+ // console.log('typeof res', typeof res)
2468
+ return res
2469
+ })
1923
2470
  }
1924
2471
  }
1925
2472
 
@@ -1932,7 +2479,9 @@ Unable to resolve configuration variable
1932
2479
  ]
1933
2480
 
1934
2481
  // Default value used for self variable
1935
- if (propertyString.match(/,/)) {
2482
+ // Only show this error if the variable itself (not a parent fallback) is a self-reference with a fallback
2483
+ const isSelfReference = !variableString.match(/^(env|opt|file|text|cron|eval|git):/)
2484
+ if (isSelfReference && variableString.match(/,/)) {
1936
2485
  errorMessage.push('\n Default values for self referenced values are not allowed')
1937
2486
  errorMessage.push(`\n Fix the ${propertyString} variable`)
1938
2487
  }
@@ -1940,23 +2489,30 @@ Unable to resolve configuration variable
1940
2489
  let allowSpecialCase = false
1941
2490
  /* handle special cases for cloudformation ${Sub} values */
1942
2491
  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
2492
+ if (this.opts.verifySubReferences) {
2493
+ const params = this.originalConfig.Parameters || (this.originalConfig.resources || {}).Parameters
2494
+ const resources = this.originalConfig.Resources || (this.originalConfig.resources || {}).Resources
2495
+ /* Cloudformation Resource References */
2496
+ if (resources && resources[variableString]) {
2497
+ allowSpecialCase = true
2498
+ } else if (params && params[variableString]) {
2499
+ allowSpecialCase = true
2500
+ } else if (variableString === 'ApiGatewayRestApi') {
2501
+ // Allow for "hidden" cloudformation variables, set by sls framework
2502
+ allowSpecialCase = true
2503
+ } else if (variableString === 'HttpApi') {
2504
+ // Allow for "hidden" cloudformation variables, set by sls framework
2505
+ allowSpecialCase = true
2506
+ }
2507
+ } else {
2508
+ // Default let any sub references pass through
1955
2509
  allowSpecialCase = true
1956
2510
  }
1957
2511
  }
1958
2512
  /* Todo handle stage variables */
1959
2513
 
2514
+
2515
+
1960
2516
  /* Pass through unknown variables */
1961
2517
  if (this.opts.allowUnknownVars || allowSpecialCase) {
1962
2518
  // console.log('allowUnknownVars propertyString', propertyString)
@@ -2061,7 +2617,10 @@ Unable to resolve configuration variable
2061
2617
  } else if (resolvedPath.match(/\.\//)) {
2062
2618
  // TODO test higher parent refs
2063
2619
  const cleanName = path.basename(resolvedPath)
2064
- fullFilePath = findUp.sync(cleanName, { cwd: this.configPath })
2620
+ const findUpResult = findUp.sync(cleanName, { cwd: this.configPath })
2621
+ if (findUpResult) {
2622
+ fullFilePath = findUpResult
2623
+ }
2065
2624
  }
2066
2625
 
2067
2626
  let fileExtension = resolvedPath.split('.')
@@ -2070,18 +2629,39 @@ Unable to resolve configuration variable
2070
2629
 
2071
2630
  // Validate file exists
2072
2631
  if (!fs.existsSync(fullFilePath)) {
2632
+ const originalVar = options.context && options.context.originalSource
2633
+
2634
+ const findNestedResult = findNestedVariables(
2635
+ originalVar,
2636
+ this.variableSyntax,
2637
+ this.variablesKnownTypes,
2638
+ options.context.path
2639
+ )
2640
+ // console.log('findNestedResult', findNestedResult)
2641
+ let hasFallback = false
2642
+ if (findNestedResult) {
2643
+ const varDetails = findNestedResult[0]
2644
+ // console.log('varDetails', varDetails)
2645
+ hasFallback = varDetails.hasFallback
2646
+ }
2647
+
2648
+ // check if original var has fallback value
2073
2649
  // console.log('NO FILE FOUND', fullFilePath)
2074
2650
  // console.log('variableString', variableString)
2075
- const errorMsg = `${logLines}
2076
- Variable ${variableString} cannot resolve due to missing file.
2651
+
2652
+ if (!hasFallback) {
2653
+ const errorMsg = makeBox({
2654
+ title: `File Not Found in ${originalVar}`,
2655
+ text: `Variable ${variableString} cannot resolve due to missing file.
2077
2656
 
2078
2657
  File not found ${fullFilePath}
2079
2658
 
2080
2659
  Default fallback value will be used if provided.
2081
- ${logLines}
2082
- `
2083
2660
 
2084
- console.log(errorMsg)
2661
+ ${JSON.stringify(options.context, null, 2)}`,
2662
+ })
2663
+ console.log(errorMsg)
2664
+ }
2085
2665
  // TODO maybe reject. YAML does not allow for null/undefined values
2086
2666
  // return Promise.reject(new Error(errorMsg))
2087
2667
  return Promise.resolve(undefined)
@@ -2099,6 +2679,7 @@ ${logLines}
2099
2679
 
2100
2680
  // Process JS files
2101
2681
  if (fileExtension === 'js' || fileExtension === 'cjs') {
2682
+ // Possible alt importer tool https://github.com/humanwhocodes/module-importer
2102
2683
  const jsFile = require(fullFilePath)
2103
2684
  let returnValueFunction = jsFile
2104
2685
  // TODO change how exported functions are referenced
@@ -2198,6 +2779,7 @@ Check if your TypeScript is returning the correct data.`
2198
2779
  }
2199
2780
 
2200
2781
  if (fileExtension === 'mjs' || fileExtension === 'esm') {
2782
+ // Possible alt importer tool https://github.com/humanwhocodes/module-importer
2201
2783
  const { executeESMFile } = require('./parsers/esm')
2202
2784
  let returnValueFunction
2203
2785
  const variableArray = variableString.split(':')
@@ -2269,9 +2851,10 @@ Check if your ESM is returning the correct data.`
2269
2851
  // console.log('deep', variableString)
2270
2852
  // console.log('matchedFileString', matchedFileString)
2271
2853
  let deepProperties = variableString.replace(matchedFileString, '')
2854
+ // TODO 2025-11-12 add file.path.support instead of just :
2272
2855
  if (deepProperties.substring(0, 1) !== ':') {
2273
2856
  const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
2274
- Please use ":" to reference sub properties`
2857
+ Please use ":" to reference sub properties. ${deepProperties}`
2275
2858
  return Promise.reject(new Error(errorMessage))
2276
2859
  }
2277
2860
  deepProperties = deepProperties.slice(1).split('.')
@@ -2298,7 +2881,7 @@ Please use ":" to reference sub properties`
2298
2881
  return Promise.resolve(valueToPopulate)
2299
2882
  }
2300
2883
  }
2301
- console.log('fall thru', valueToPopulate)
2884
+ // console.log('fall thru', valueToPopulate)
2302
2885
  return Promise.resolve(valueToPopulate)
2303
2886
  }
2304
2887
  getVariableFromDeep(variableString) {
@@ -2310,15 +2893,22 @@ Please use ":" to reference sub properties`
2310
2893
  /** */
2311
2894
  return this.deep[index]
2312
2895
  }
2313
- getValueFromDeep(variableString) {
2896
+ getValueFromDeep(variableString, pathValue) {
2314
2897
  const variable = this.getVariableFromDeep(variableString)
2315
2898
  const deepRef = variableString.replace(deepPrefixReplacePattern, '')
2316
2899
  /*
2317
2900
  console.log("GET getValueFromDeep", variableString)
2318
- console.log('deepRef', deepRef)
2901
+ console.log('deepRef', (deepRef) ? deepRef : '- no deepRef')
2319
2902
  console.log('getValueFromDeep variable', variable)
2320
2903
  /** */
2321
- let ret = this.populateValue({ value: variable }, undefined, 'getValueFromDeep')
2904
+ // Preserve path and originalSource information from pathValue
2905
+ const valueObject = {
2906
+ value: variable,
2907
+ path: pathValue ? pathValue.path : undefined,
2908
+ originalSource: pathValue ? pathValue.originalSource : undefined,
2909
+ resolutionHistory: pathValue ? pathValue.resolutionHistory : []
2910
+ }
2911
+ let ret = this.populateValue(valueObject, undefined, 'getValueFromDeep')
2322
2912
  if (deepRef.length) {
2323
2913
  // if there is a deep reference remaining
2324
2914
  ret = ret.then((result) => {
@@ -2342,7 +2932,12 @@ Please use ":" to reference sub properties`
2342
2932
  }
2343
2933
  // console.log("makeDeepVariable SET INDEX", index)
2344
2934
  const variableContainer = variable.match(this.variableSyntax)[0]
2345
- const variableString = cleanVariable(variableContainer, this.variableSyntax, true, `makeDeepVariable ${this.callCount}`)
2935
+ const variableString = cleanVariable(
2936
+ variableContainer,
2937
+ this.variableSyntax,
2938
+ true,
2939
+ `makeDeepVariable ${this.callCount}`
2940
+ )
2346
2941
  const deepVar = variableContainer.replace(variableString, `deep:${index}`)
2347
2942
  /*
2348
2943
  console.log('MAKE DEEP', variable, caller)