configorama 0.9.15 → 0.9.17

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
@@ -10,7 +10,6 @@ console.log = () => {}
10
10
  const findUp = require('find-up')
11
11
  const traverse = require('traverse')
12
12
  const dotProp = require('dot-prop')
13
- const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
14
13
  /* Utils - root */
15
14
  const {
16
15
  isArray, isString, isNumber, isObject, isDate, isRegExp, isFunction,
@@ -30,8 +29,7 @@ const { parseFileContents } = require('./utils/parsing/parse')
30
29
  const { mergeByKeys } = require('./utils/parsing/mergeByKeys')
31
30
  const { arrayToJsonPath } = require('./utils/parsing/arrayToJsonPath')
32
31
  /* Utils - paths */
33
- const { normalizePath, extractFilePath, resolveInnerVariables } = require('./utils/paths/filePathUtils')
34
- const { findLineForKey } = require('./utils/paths/findLineForKey')
32
+ const { findLineByPath } = require('./utils/paths/findLineForKey')
35
33
  /* Utils - regex */
36
34
  const { combineRegexes, funcRegex, fileRefSyntax, textRefSyntax } = require('./utils/regex')
37
35
  /* Utils - strings */
@@ -46,8 +44,11 @@ const { splitOnPipe } = require('./utils/strings/splitOnPipe')
46
44
  const chalk = require('./utils/ui/chalk')
47
45
  const deepLog = require('./utils/ui/deep-log')
48
46
  const { logHeader } = require('./utils/ui/logs')
49
- const { createEditorLink } = require('./utils/ui/createEditorLink')
50
- const { runConfigWizard, isSensitiveVariable } = require('./utils/ui/configWizard')
47
+ const { runConfigWizard } = require('./utils/ui/configWizard')
48
+ /* Display */
49
+ const { displayNoVariablesFound, displayVariableDetails, displayUniqueVariables, displayConfigurableVariables } = require('./display')
50
+ /* Metadata */
51
+ const { collectVariableMetadata: collectMetadata } = require('./metadata')
51
52
  /* Utils - validation */
52
53
  const { warnIfNotFound, isValidValue } = require('./utils/validation/warnIfNotFound')
53
54
  /* Utils - variables */
@@ -127,6 +128,10 @@ class Configorama {
127
128
  returnMetadata: false,
128
129
  // Return preResolvedVariableDetails
129
130
  returnPreResolvedVariableDetails: false,
131
+ // Suppress env-stage-loader's normal dotenv loading logs by default.
132
+ // CLI users can still see them with --verbose or dotEnvSilent: false.
133
+ dotEnvSilent: !VERBOSE,
134
+ dotEnvDebug: false,
130
135
  }, options)
131
136
 
132
137
  // Backward compat: allowUnknownVars -> allowUnknownVariableTypes
@@ -159,6 +164,8 @@ class Configorama {
159
164
 
160
165
  // Track variable resolutions for metadata (keyed by path)
161
166
  this.resolutionTracking = {}
167
+ // Only track per-call metadata when returnMetadata is requested
168
+ this._trackCalls = !!(this.settings.returnMetadata)
162
169
 
163
170
  // Detect file type early to determine default syntax
164
171
  let detectedFileType = null
@@ -739,457 +746,25 @@ class Configorama {
739
746
  }
740
747
 
741
748
  if (!varKeys.length) {
742
- logHeader('No Variables Found in Config')
743
- if (this.configFilePath) {
744
- console.log(`File: ${this.configFilePath}`)
745
- }
746
-
747
- console.log(`\nVariable syntax: `, variableSyntax)
748
-
749
- const varTypes = Object.keys(this.variableTypes)
750
- if (varTypes.length) {
751
- const exclude = ['dot.prop', 'deep']
752
- console.log('\nAllowed variable types:')
753
- varTypes.forEach((v) => {
754
- const vData = this.variableTypes[v]
755
- if (exclude.includes(vData.type)) {
756
- return
757
- }
758
- console.log(` - ${vData.type}: `, vData.match)
759
- })
760
- }
761
- console.log()
749
+ displayNoVariablesFound(this.configFilePath, variableSyntax, this.variableTypes)
762
750
  }
763
751
 
764
752
  const lines = this.configFileContents ? this.configFileContents.split('\n') : []
765
753
  const fileType = this.configFileType
766
754
  const configFilePath = this.configFilePath
767
755
 
768
- if (varKeys.length > 0) {
769
- const fileName = this.configFilePath ? ` in ${this.configFilePath}` : ''
770
-
771
- // Extract base variable name from varMatch key (e.g., '${env:FOO, default}' -> 'env:FOO')
772
- const getBaseVarName = (key) => key.replace(this.varPrefixPattern, '').replace(this.varSuffixPattern, '').split(',')[0].trim()
773
-
774
- logHeader(`Found ${varKeys.length} Variables${fileName}`)
775
-
776
- // deepLog('variableData', variableData)
777
-
778
- if (varKeys.length) {
779
- console.log()
780
- const longestKey = varKeys.reduce((acc, k) => {
781
- return Math.max(acc, k.length)
782
- }, 0)
783
-
784
- // Use uniqueVariables for simpler reference counting
785
- const referenceData = varKeys.map((k) => {
786
- const varName = getBaseVarName(k)
787
- const uniqueVar = uniqueVariables[varName]
788
- const refCount = uniqueVar ? uniqueVar.occurrences.length : variableData[k].length
789
- const placesWord = refCount > 1 ? 'places' : 'place'
790
- return `- ${k.padEnd(longestKey).padEnd(longestKey + 10)} referenced ${refCount} ${placesWord}`
791
- }).join('\n')
792
-
793
- console.log(`${referenceData}\n`)
794
- }
795
-
796
- logHeader('Variable Details')
797
-
798
- const indent = ''
799
- const boxes = varKeys.map((key, i) => {
800
- const variableInstances = variableData[key]
801
- // console.log('variableInstances', variableInstances)
802
- const firstInstance = variableInstances[0]
803
-
804
- // Get uniqueVariable data for description and other metadata
805
- const varName = getBaseVarName(key)
806
- const uniqueVar = uniqueVariables[varName]
807
-
808
- // Build display message from enriched metadata
809
- const spacing = ' '
810
- const titleText = `Variable:${spacing}`
811
- const VALUE_HEX = '#899499'
812
- const keyChalk = chalk.whiteBright
813
- const valueChalk = chalk.hex(VALUE_HEX)
814
-
815
- let varMsg = ''
816
- let requiredMessage = ''
817
-
818
- // Show required status from metadata
819
- if (firstInstance.isRequired) {
820
- requiredMessage = `${chalk.red.bold('[Required]')}`
821
- }
822
-
823
- // Show type filter if present (Boolean, String, Number, etc.)
824
- if (uniqueVar && uniqueVar.types && uniqueVar.types.length > 0) {
825
- const typeLabel = `${indent}${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
826
- varMsg += `${typeLabel} ${valueChalk(uniqueVar.types.join(', '))}\n`
827
- }
828
-
829
- // Show description from uniqueVariables if available
830
- if (uniqueVar && uniqueVar.descriptions && uniqueVar.descriptions.length > 0) {
831
- const descText = `${indent}${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
832
- const combinedDesc = uniqueVar.descriptions.join('. ')
833
- varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
834
- }
835
-
836
- // Show resolve order from metadata
837
- if (firstInstance.resolveOrder.length > 1) {
838
- varMsg += `${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
839
- const resolveOrder = firstInstance.resolveOrder.join(', ')
840
- varMsg += ` ${valueChalk(resolveOrder)}\n`
841
- }
842
-
843
- // Show default value from metadata
844
- if (typeof firstInstance.defaultValue !== 'undefined') {
845
- const defaultValueRender = firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue
846
- const defaultValueText = `${indent}${keyChalk('Default value:'.padEnd(titleText.length, ' '))}`
847
- varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}\n`
848
- }
849
-
850
- // Show default value source path from metadata
851
- if (firstInstance.defaultValueSrc) {
852
- varMsg += `${indent}${keyChalk('Default path:'.padEnd(titleText.length, ' '))} `
853
- const defaultPathLine = findLineForKey(firstInstance.defaultValueSrc, lines, fileType)
854
- if (defaultPathLine) {
855
- varMsg += `${createEditorLink(configFilePath, defaultPathLine, 1, firstInstance.defaultValueSrc, 'gray')}\n`
856
- } else {
857
- varMsg += `${valueChalk(firstInstance.defaultValueSrc)}\n`
858
- }
859
- }
860
-
861
- // Show path(s) from metadata
862
- const configPathLine = findLineForKey(variableInstances[0].path, lines, fileType)
863
- let locationRender = configPathLine
864
- ? createEditorLink(configFilePath, configPathLine, 1, variableInstances[0].path, 'gray')
865
- : valueChalk(variableInstances[0].path)
866
- let locationLabel = `${indent}${keyChalk('Config Path:'.padEnd(titleText.length, ' '))}`
867
- let typeText = ''
868
- if (variableInstances.length > 1) {
869
- const pathIndent = ' '.repeat(titleText.length + 1)
870
- const pathItems = variableInstances.map((v, idx) => {
871
- const pathLine = findLineForKey(v.path, lines, fileType)
872
- const pathLink = pathLine
873
- ? createEditorLink(configFilePath, pathLine, 1, `- ${v.path}`, 'gray')
874
- : valueChalk(`- ${v.path}`)
875
- // Show type filter per path if different
876
- if (uniqueVar && uniqueVar.occurrences.length > 1) {
877
- const occurrence = uniqueVar.occurrences.find(occ => occ.path === v.path)
878
- const pathType = occurrence && occurrence.type
879
- typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
880
- const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
881
- return `${prefix}${pathLink}${typeText}`
882
- }
883
- const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
884
- return `${prefix}${pathLink}${typeText}`
885
- })
886
- locationRender = pathItems.join('\n')
887
- locationLabel = `${indent}${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
888
- } else {
889
- const pathType = firstInstance.type
890
- typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
891
- }
892
- varMsg += `${locationLabel} ${locationRender}`
893
-
894
- const lineNumber = findLineForKey(firstInstance.key, lines, fileType)
895
-
896
- return {
897
- content: {
898
- left: varMsg,
899
- backgroundColor: 'red',
900
- width: '100%',
901
- },
902
- title: {
903
- left: `▷ ${lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, key) : key}`,
904
- right: lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, `${requiredMessage} ${lineNumber ? `Line: ${lineNumber.toString().padEnd(2, ' ')}` : ''}`, 'gray') : '',
905
- center: typeText,
906
- paddingBottom: 1,
907
- paddingTop: (i === 0) ? 1 : 0,
908
- truncate: true,
909
- },
910
- width: '100%',
911
- }
912
- })
913
-
914
- console.log(makeStackedBoxes(boxes, {
915
- borderText: 'Variable Details. Click on titles to open in editor.',
916
- borderColor: 'gray',
917
- minWidth: '96%',
918
- borderStyle: 'bold',
919
- disableTitleSeparator: true,
920
- }))
921
- // process.exit(1)
922
- }
923
-
924
- // New unique variable makeStackedBoxes display
925
- const uniqueBoxes = uniqueVarKeys.map((varName, i) => {
926
- const uniqueVar = uniqueVariables[varName]
927
- const occurrences = uniqueVar.occurrences || []
928
- const firstOcc = occurrences[0] || {}
929
-
930
- const spacing = ' '
931
- const titleText = `Variable:${spacing}`
932
- const VALUE_HEX = '#899499'
933
- const keyChalk = chalk.whiteBright
934
- const valueChalk = chalk.hex(VALUE_HEX)
935
-
936
- let varMsg = ''
937
- let requiredMessage = ''
938
-
939
- // Show required status from computed isRequired (accounts for resolved self-refs)
940
- const isRequired = occurrences.some(occ => occ.isRequired)
941
- if (isRequired) {
942
- requiredMessage = `${chalk.red.bold('[Required]')}`
943
- }
944
-
945
- // Show type filter if present
946
- if (uniqueVar.types && uniqueVar.types.length > 0) {
947
- const typeLabel = `${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
948
- varMsg += `${typeLabel} ${valueChalk(uniqueVar.types.join(', '))}\n`
949
- }
950
-
951
- // Show description
952
- if (uniqueVar.descriptions && uniqueVar.descriptions.length > 0) {
953
- const descText = `${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
954
- const combinedDesc = uniqueVar.descriptions.join('. ')
955
- varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
956
- }
957
-
958
- // Show default value only if it's a true fallback, not a pre-resolved value
959
- // Redact sensitive values like API keys, secrets, tokens
960
- const isSensitive = isSensitiveVariable(varName)
961
- const hasActualDefault = firstOcc.hasFallback && typeof firstOcc.defaultValue !== 'undefined'
962
- if (hasActualDefault) {
963
- const defaultValueRender = isSensitive ? '********' : (firstOcc.defaultValue === '' ? '""' : firstOcc.defaultValue)
964
- const defaultValueText = `${keyChalk('Default value:'.padEnd(titleText.length, ' '))}`
965
- varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}\n`
966
- } else if (uniqueVar.resolvedValue !== undefined) {
967
- // Show pre-resolved current value (e.g., from env, git)
968
- const resolvedRender = isSensitive ? '********' : (uniqueVar.resolvedValue === '' ? '""' : uniqueVar.resolvedValue)
969
- const resolvedText = `${keyChalk('Current value:'.padEnd(titleText.length, ' '))}`
970
- const envIndicator = uniqueVar.variableType === 'env' ? ` ${chalk.red('(currently set env var)')}` : ''
971
- varMsg += `${resolvedText} ${valueChalk(resolvedRender)}${envIndicator}\n`
972
- }
973
-
974
- // Show default value source path
975
- if (firstOcc.defaultValueSrc) {
976
- varMsg += `${keyChalk('Default path:'.padEnd(titleText.length, ' '))} `
977
- const defaultPathLine = findLineForKey(firstOcc.defaultValueSrc, lines, fileType)
978
- if (defaultPathLine) {
979
- varMsg += `${createEditorLink(configFilePath, defaultPathLine, 1, firstOcc.defaultValueSrc, 'gray')}\n`
980
- } else {
981
- varMsg += `${valueChalk(firstOcc.defaultValueSrc)}\n`
982
- }
983
- }
984
-
985
- // Show config path(s) from occurrences
986
- let locationRender
987
- let locationLabel
988
- if (occurrences.length > 1) {
989
- const pathIndent = ' '.repeat(titleText.length + 1)
990
- const pathItems = occurrences.map((occ, idx) => {
991
- const pathLine = findLineForKey(occ.path, lines, fileType)
992
- const pathLink = pathLine
993
- ? createEditorLink(configFilePath, pathLine, 1, `- ${occ.path}`, 'gray')
994
- : valueChalk(`- ${occ.path}`)
995
- const typeText = occ.type ? ` ${chalk.dim(`Type: ${occ.type}`)}` : ''
996
- const prefix = idx === 0 ? '' : `${pathIndent}`
997
- return `${prefix}${pathLink}${typeText}`
998
- })
999
- locationRender = pathItems.join('\n')
1000
- locationLabel = `${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
1001
- } else {
1002
- const pathLine = findLineForKey(firstOcc.path, lines, fileType)
1003
- locationRender = pathLine
1004
- ? createEditorLink(configFilePath, pathLine, 1, firstOcc.path, 'gray')
1005
- : valueChalk(firstOcc.path)
1006
- locationLabel = `${keyChalk('Config Path:'.padEnd(titleText.length, ' '))}`
1007
- }
1008
- varMsg += `${locationLabel} ${locationRender}`
756
+ const displayParams = { lines, fileType, configFilePath, uniqueVariables, uniqueVarKeys }
1009
757
 
1010
- // Find first line number for title
1011
- const lineNumber = findLineForKey(firstOcc.path, lines, fileType)
1012
-
1013
- return {
1014
- content: {
1015
- left: varMsg,
1016
- backgroundColor: 'red',
1017
- width: '100%',
1018
- },
1019
- title: {
1020
- left: `▷ ${firstOcc.varMatch}`,
1021
- right: `${requiredMessage} ${lineNumber ? `Line: ${lineNumber.toString().padEnd(2, ' ')}` : ''}`,
1022
- paddingBottom: 1,
1023
- paddingTop: (i === 0) ? 1 : 0,
1024
- truncate: true,
1025
- },
1026
- width: '100%',
1027
- }
758
+ displayVariableDetails({
759
+ varKeys, variableData, uniqueVariables,
760
+ varPrefixPattern: this.varPrefixPattern,
761
+ varSuffixPattern: this.varSuffixPattern,
762
+ lines, fileType, configFilePath,
1028
763
  })
1029
764
 
1030
- console.log(makeStackedBoxes(uniqueBoxes, {
1031
- borderText: 'Unique Variables',
1032
- borderColor: 'gray',
1033
- minWidth: '96%',
1034
- borderStyle: 'bold',
1035
- disableTitleSeparator: true,
1036
- }))
1037
- console.log()
1038
-
1039
-
1040
- // Unique variables that require setup (excludes readonly source types)
1041
- const CONFIGURABLE_SOURCES = ['user', 'config', 'remote']
1042
- const configurableVariables = {}
1043
- const configurableVarKeys = []
1044
-
1045
- for (const varName of uniqueVarKeys) {
1046
- const uniqueVar = uniqueVariables[varName]
1047
- // Include if source type is user, config, or remote (not readonly)
1048
- if (CONFIGURABLE_SOURCES.includes(uniqueVar.variableSourceType)) {
1049
- configurableVariables[varName] = uniqueVar
1050
- configurableVarKeys.push(varName)
1051
- }
1052
- }
1053
-
1054
- // Display configurable variables by source type
1055
- if (configurableVarKeys.length > 0) {
1056
- const spacing = ' '
1057
- const titleText = `Variable:${spacing}`
1058
- const VALUE_HEX = '#899499'
1059
- const keyChalk = chalk.whiteBright
1060
- const valueChalk = chalk.hex(VALUE_HEX)
1061
-
1062
- // Group by source type
1063
- const bySource = {
1064
- user: [],
1065
- config: [],
1066
- remote: [],
1067
- }
1068
-
1069
- for (const varName of configurableVarKeys) {
1070
- const v = configurableVariables[varName]
1071
- const sourceType = v.variableSourceType || 'user'
1072
- if (bySource[sourceType]) {
1073
- bySource[sourceType].push({ varName, ...v })
1074
- }
1075
- }
1076
-
1077
- const sourceLabels = {
1078
- user: 'User Input Required',
1079
- config: 'Config References',
1080
- remote: 'Remote Services',
1081
- }
1082
-
1083
- const sourceColors = {
1084
- user: 'yellow',
1085
- config: 'cyan',
1086
- remote: 'magenta',
1087
- }
1088
-
1089
- const configurableBoxes = []
1090
-
1091
- for (const [sourceType, vars] of Object.entries(bySource)) {
1092
- if (vars.length === 0) continue
765
+ displayUniqueVariables(displayParams)
1093
766
 
1094
- for (let i = 0; i < vars.length; i++) {
1095
- const v = vars[i]
1096
- const occurrences = v.occurrences || []
1097
- const firstOcc = occurrences[0] || {}
1098
-
1099
- let varMsg = ''
1100
- let requiredMessage = ''
1101
-
1102
- // Show required status from computed isRequired (accounts for resolved self-refs)
1103
- const isRequired = occurrences.some(occ => occ.isRequired)
1104
- if (isRequired) {
1105
- requiredMessage = `${chalk.red.bold('[Required]')}`
1106
- }
1107
-
1108
- // Show description if present (directly under title, not as key/value)
1109
- if (v.descriptions && v.descriptions.length > 0) {
1110
- varMsg += `${chalk.dim(v.descriptions.join('. '))}\n\n`
1111
- }
1112
-
1113
- // Show type filter if defined (String, Number, etc.)
1114
- const varType = (v.types && v.types[0]) || firstOcc.type
1115
- if (varType) {
1116
- varMsg += `${keyChalk('Type:'.padEnd(titleText.length, ' '))} ${valueChalk(varType)}\n`
1117
- }
1118
-
1119
- // Show current/default value (redact sensitive values)
1120
- const isSensitive = isSensitiveVariable(v.varName)
1121
- if (v.resolvedValue !== undefined) {
1122
- const resolvedRender = isSensitive ? '********' : (v.resolvedValue === '' ? '""' : v.resolvedValue)
1123
- varMsg += `${keyChalk('Current value:'.padEnd(titleText.length, ' '))} ${valueChalk(resolvedRender)}\n`
1124
- } else if (firstOcc.hasFallback && firstOcc.defaultValue !== undefined) {
1125
- const defaultRender = isSensitive ? '********' : (firstOcc.defaultValue === '' ? '""' : firstOcc.defaultValue)
1126
- varMsg += `${keyChalk('Default value:'.padEnd(titleText.length, ' '))} ${valueChalk(defaultRender)}\n`
1127
- }
1128
-
1129
- // Show config path(s)
1130
- let locationRender
1131
- let locationLabel
1132
- if (occurrences.length > 1) {
1133
- const pathIndent = ' '.repeat(titleText.length + 1)
1134
- const pathItems = occurrences.map((occ, idx) => {
1135
- const pathLine = findLineForKey(occ.path, lines, fileType)
1136
- const pathLink = pathLine
1137
- ? createEditorLink(configFilePath, pathLine, 1, `- ${occ.path}`, VALUE_HEX)
1138
- : valueChalk(`- ${occ.path}`)
1139
- const prefix = idx === 0 ? '' : `${pathIndent}`
1140
- return `${prefix}${pathLink}`
1141
- })
1142
- locationRender = pathItems.join('\n')
1143
- locationLabel = 'Config Paths:'
1144
- } else {
1145
- const pathLine = findLineForKey(firstOcc.path, lines, fileType)
1146
- locationRender = pathLine
1147
- ? createEditorLink(configFilePath, pathLine, 1, firstOcc.path, VALUE_HEX)
1148
- : valueChalk(firstOcc.path)
1149
- locationLabel = 'Config Path:'
1150
- }
1151
- varMsg += `${keyChalk(locationLabel.padEnd(titleText.length, ' '))} ${locationRender}`
1152
-
1153
- // Get type for center heading (reuse varType from above)
1154
- const typeText = varType ? chalk.dim(`Type: ${varType}`) : ''
1155
-
1156
- // Get line number for first occurrence
1157
- const firstOccLine = findLineForKey(firstOcc.path, lines, fileType)
1158
- const varTitle = firstOcc.varMatch || v.varName
1159
- const requiredSuffix = requiredMessage ? ` - ${requiredMessage}` : ''
1160
- const titleLink = firstOccLine
1161
- ? createEditorLink(configFilePath, firstOccLine, 1, `▷ ${varTitle}`) + requiredSuffix
1162
- : `▷ ${varTitle}${requiredSuffix}`
1163
-
1164
- configurableBoxes.push({
1165
- content: {
1166
- left: varMsg,
1167
- width: '100%',
1168
- },
1169
- title: {
1170
- left: titleLink,
1171
- // center: typeText,
1172
- right: chalk.dim(`${v.variableType}`),
1173
- paddingBottom: 1,
1174
- paddingTop: (configurableBoxes.length === 0) ? 1 : 0,
1175
- truncate: true,
1176
- },
1177
- width: '100%',
1178
- })
1179
- }
1180
- }
1181
-
1182
- if (configurableBoxes.length > 0) {
1183
- console.log(makeStackedBoxes(configurableBoxes, {
1184
- borderText: `Configurable Variables (${configurableVarKeys.length})`,
1185
- borderColor: 'yellow',
1186
- minWidth: '96%',
1187
- borderStyle: 'bold',
1188
- disableTitleSeparator: true,
1189
- }))
1190
- console.log()
1191
- }
1192
- }
767
+ displayConfigurableVariables(displayParams)
1193
768
 
1194
769
 
1195
770
  // WALK through CLI prompt if --setup flag is set
@@ -1261,8 +836,8 @@ class Configorama {
1261
836
  const stage = cliOpts.stage || providerStage || process.env.NODE_ENV || 'dev'
1262
837
  /* Load env variables into process.env */
1263
838
  require('env-stage-loader')({
1264
- // silent: true,
1265
- // debug: true,
839
+ silent: this.settings.dotEnvSilent,
840
+ debug: this.settings.dotEnvDebug,
1266
841
  env: stage,
1267
842
  // defaultEnv: 'prod',
1268
843
  // ignoreFiles: ['.env']
@@ -1423,426 +998,19 @@ class Configorama {
1423
998
  return this._cachedMetadata
1424
999
  }
1425
1000
 
1426
- const variableSyntax = this.variableSyntax
1427
- const variablesKnownTypes = this.variablesKnownTypes
1428
- const variableTypes = this.variableTypes
1429
- const filterMatch = this.filterMatch
1430
- const configFilePath = this.configFilePath
1431
- // Use rawOriginalConfig for metadata display (truly original, no escaping)
1432
- const originalConfig = this.rawOriginalConfig || this.originalConfig
1433
- const foundVariables = []
1434
- const variableData = {}
1435
- const fileRefs = []
1436
- const fileGlobPatterns = []
1437
- const preResolvedPaths = new Set()
1438
- const byConfigPath = []
1439
- const referencesMap = new Map()
1440
- let matchCount = 1
1441
-
1442
- traverse(originalConfig).forEach(function (rawValue) {
1443
- if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
1444
- const configValuePath = this.path.join('.')
1445
- /* Skip Fn::Sub variables */
1446
- if (configValuePath.endsWith('Fn::Sub')) {
1447
- return
1448
- }
1449
-
1450
- const nested = findNestedVariables(
1451
- rawValue,
1452
- variableSyntax,
1453
- variablesKnownTypes,
1454
- configValuePath,
1455
- variableTypes
1456
- )
1457
-
1458
- const lastItem = nested[nested.length - 1]
1459
- const lastKeyPath = this.path[this.path.length - 1]
1460
- const itemKey = (lastKeyPath.match(/[\d+]$/)) ? `${this.path[this.path.length - 2]}[${lastKeyPath}]` : lastKeyPath
1461
-
1462
- // Extract filters from varMatch
1463
- const originalSrc = lastItem.varMatch || ''
1464
- const hasFilters = filterMatch && originalSrc.match(filterMatch)
1465
- let foundFilters = []
1466
- let keyWithoutFilters = originalSrc
1467
-
1468
- if (hasFilters) {
1469
- // Extract filter names from the match (e.g., "| String}" -> ["String"])
1470
- const filterPart = hasFilters[0].replace(/}?$/, '') // Remove trailing }
1471
- foundFilters = splitOnPipe(filterPart)
1472
- .map((filter) => filter.trim())
1473
- .filter(Boolean)
1474
-
1475
- // Remove filters from the key (replace "| String}" with suffix)
1476
- // Also clean up any trailing whitespace before the closing brace
1477
- keyWithoutFilters = originalSrc.replace(filterMatch, this.varSuffix).replace(this.varSuffixWithSpacePattern, this.varSuffix)
1478
- }
1479
-
1480
- const key = keyWithoutFilters
1481
-
1482
- // Helper to pre-resolve a variable from config
1483
- const preResolveFromConfig = (varString, varType) => {
1484
- if (!varString) return undefined
1485
- // Handle self: prefix
1486
- const varPath = varString.startsWith('self:') ? varString.slice(5) : varString
1487
- // Only pre-resolve dot.prop and self references
1488
- if (varType === 'dot.prop' || varType === 'self') {
1489
- const value = dotProp.get(originalConfig, varPath)
1490
- if (value !== undefined && typeof value !== 'object') {
1491
- return { resolved: value, path: varPath }
1492
- }
1493
- }
1494
- return undefined
1495
- }
1496
-
1497
- // Strip filters from resolveDetails
1498
- const cleanedResolveDetails = nested.map(detail => {
1499
- const cleaned = { ...detail }
1500
- if (cleaned.varMatch && filterMatch) {
1501
- const match = cleaned.varMatch.match(filterMatch)
1502
- if (match) {
1503
- cleaned.varMatch = cleaned.varMatch.replace(filterMatch, '').replace(/\s+$/, '') + this.varSuffix
1504
- }
1505
- }
1506
- if (cleaned.variable && filterMatch) {
1507
- const match = cleaned.variable.match(filterMatch)
1508
- if (match) {
1509
- cleaned.variable = cleaned.variable.replace(filterMatch, '').replace(/\s+$/, '')
1510
- }
1511
- }
1512
- if (cleaned.varString && filterMatch) {
1513
- const match = cleaned.varString.match(filterMatch)
1514
- if (match) {
1515
- cleaned.varString = cleaned.varString.replace(filterMatch, '').trim()
1516
- }
1517
- }
1518
-
1519
- // Pre-resolve dot.prop and self references
1520
- const preResolved = preResolveFromConfig(cleaned.varString || cleaned.variable, cleaned.variableType)
1521
- if (preResolved) {
1522
- cleaned.varResolved = preResolved.resolved
1523
- cleaned.varResolvedPath = preResolved.path
1524
- }
1525
-
1526
- // Also clean fallbackValues if present
1527
- if (cleaned.fallbackValues && Array.isArray(cleaned.fallbackValues)) {
1528
- cleaned.fallbackValues = cleaned.fallbackValues.map(fb => {
1529
- const cleanedFb = { ...fb }
1530
- if (cleanedFb.varMatch && filterMatch) {
1531
- const match = cleanedFb.varMatch.match(filterMatch)
1532
- if (match) {
1533
- cleanedFb.varMatch = cleanedFb.varMatch.replace(filterMatch, '').trim()
1534
- }
1535
- }
1536
- if (cleanedFb.variable && filterMatch) {
1537
- const match = cleanedFb.variable.match(filterMatch)
1538
- if (match) {
1539
- cleanedFb.variable = cleanedFb.variable.replace(filterMatch, '').trim()
1540
- }
1541
- }
1542
- if (cleanedFb.stringValue && filterMatch) {
1543
- const match = cleanedFb.stringValue.match(filterMatch)
1544
- if (match) {
1545
- cleanedFb.stringValue = cleanedFb.stringValue.replace(filterMatch, '').trim()
1546
- }
1547
- }
1548
-
1549
- // Pre-resolve fallback variable references
1550
- if (cleanedFb.stringValue && cleanedFb.stringValue.match(/^\$\{[^}]+\}$/)) {
1551
- const innerVar = cleanedFb.stringValue.slice(2, -1)
1552
- const fbPreResolved = preResolveFromConfig(innerVar, 'dot.prop')
1553
- if (fbPreResolved) {
1554
- cleanedFb.varResolved = fbPreResolved.resolved
1555
- cleanedFb.varResolvedPath = fbPreResolved.path
1556
- }
1557
- }
1558
-
1559
- return cleanedFb
1560
- })
1561
- }
1562
- return cleaned
1563
- })
1564
-
1565
- const varData = {
1566
- filters: foundFilters.length > 0 ? foundFilters : undefined,
1567
- path: configValuePath,
1568
- key: itemKey,
1569
- originalStringValue: rawValue,
1570
- variable: keyWithoutFilters,
1571
- variableWithFilters: originalSrc,
1572
- isRequired: false,
1573
- defaultValue: undefined,
1574
- defaultValueIsVar: undefined,
1575
- defaultValueSrc: undefined,
1576
- hasFallback: false,
1577
- matchIndex: matchCount++,
1578
- resolveOrder: [],
1579
- resolveDetails: cleanedResolveDetails,
1580
- }
1581
- let defaultValueIsVar = false
1582
-
1583
- function calculateResolveOrder(item) {
1584
- // Helper to strip filters from variable strings
1585
- const stripFilters = (str) => {
1586
- if (!str || !filterMatch) return str
1587
- const match = str.match(filterMatch)
1588
- if (match) {
1589
- return str.replace(filterMatch, '').trim()
1590
- }
1591
- return str
1592
- }
1593
-
1594
- if (item && item.fallbackValues) {
1595
- let hasResolvedFallback
1596
- let defaultValueSrc
1597
- const isSingleFallback = item.fallbackValues.length === 1
1598
- const order = ([stripFilters(item.valueBeforeFallback)]).concat(item.fallbackValues.map((f, i) => {
1599
- if (f.fallbackValues) {
1600
- const [nestedOrder, nestedResolvedFallback, nestedDefaultSrc] = calculateResolveOrder(f)
1601
- if (!hasResolvedFallback && nestedResolvedFallback) {
1602
- hasResolvedFallback = nestedResolvedFallback
1603
- defaultValueSrc = nestedDefaultSrc
1604
- }
1605
- return nestedOrder
1606
- }
1607
-
1608
- const valueStr = stripFilters(f.stringValue || f.variable)
1609
-
1610
- // Only set default from first resolvable fallback
1611
- if (!hasResolvedFallback && f.isResolvedFallback) {
1612
- if (f.varResolved !== undefined) {
1613
- hasResolvedFallback = f.varResolved
1614
- defaultValueSrc = f.varResolvedPath
1615
- } else if (!valueStr.match(/^\$\{[^}]+\}$/)) {
1616
- // Literal value - use as default
1617
- hasResolvedFallback = valueStr
1618
- }
1619
- // If variable can't resolve, don't set - let next fallback try
1620
- }
1621
-
1622
- if (!hasResolvedFallback && f.isVariable) {
1623
- defaultValueIsVar = f
1624
- }
1625
-
1626
- if (f.isResolvedFallback) {
1627
- if (isSingleFallback) {
1628
- // Single fallback: show "value (default)"
1629
- return `${valueStr} (default)`
1630
- } else {
1631
- // Multiple fallbacks: show resolved value if available
1632
- if (f.varResolved !== undefined) {
1633
- return `${valueStr} = ${f.varResolved}`
1634
- }
1635
- // If can't resolve, just show the value without annotation
1636
- return valueStr
1637
- }
1638
- }
1639
- return valueStr
1640
- })).flat()
1641
-
1642
- return [order, hasResolvedFallback, defaultValueSrc]
1643
- }
1644
- return [[stripFilters(item.variable)], undefined, undefined]
1645
- }
1646
-
1647
- const lastCleanedItem = cleanedResolveDetails[cleanedResolveDetails.length - 1]
1648
- const [resolveOrder, hasResolvedFallback, defaultValueSrc] = calculateResolveOrder(lastCleanedItem)
1649
- varData.resolveOrder = resolveOrder
1650
-
1651
- if (defaultValueIsVar) {
1652
- varData.defaultValueIsVar = defaultValueIsVar
1653
- }
1654
-
1655
- if (typeof hasResolvedFallback !== 'undefined') {
1656
- varData.defaultValue = hasResolvedFallback
1657
- }
1658
-
1659
- if (defaultValueSrc) {
1660
- varData.defaultValueSrc = defaultValueSrc
1661
- }
1662
-
1663
- if (typeof varData.defaultValue === 'undefined') {
1664
- varData.isRequired = true
1665
- }
1666
-
1667
- if (varData.resolveOrder.length > 1) {
1668
- varData.hasFallback = true
1669
- }
1670
-
1671
- // Extract file references
1672
- nested.forEach((detail) => {
1673
- // console.log('detail', detail)
1674
- if (detail.variableType && (detail.variableType === 'file' || detail.variableType === 'text')) {
1675
- const extracted = extractFilePath(detail.variable)
1676
- if (extracted) {
1677
- const normalizedPath = normalizePath(extracted.filePath)
1678
- if (!normalizedPath) return
1679
-
1680
- // Handle variables in file paths - just record the pattern
1681
- if (!fileRefs.includes(normalizedPath)) {
1682
- fileRefs.push(normalizedPath)
1683
- }
1684
-
1685
- // Check if path contains variables and create glob pattern
1686
- const containsVariables = !!normalizedPath.match(variableSyntax)
1687
- let globPattern
1688
- if (containsVariables) {
1689
- // Replace variable syntax ${...} with * for glob pattern
1690
- globPattern = normalizedPath.replace(variableSyntax, '*')
1691
- if (!fileGlobPatterns.includes(globPattern)) {
1692
- fileGlobPatterns.push(globPattern)
1693
- }
1694
- }
1695
-
1696
- // Try to pre-resolve inner variables from originalConfig
1697
- let resolvedPath = normalizedPath
1698
- let resolvedVarString = rawValue
1699
- if (containsVariables) {
1700
- const pathResult = resolveInnerVariables(normalizedPath, variableSyntax, originalConfig, dotProp.get)
1701
- const varStringResult = resolveInnerVariables(rawValue, variableSyntax, originalConfig, dotProp.get)
1702
-
1703
- if (pathResult.didResolve) {
1704
- resolvedPath = normalizePath(pathResult.resolved) || pathResult.resolved
1705
- resolvedVarString = varStringResult.resolved
1706
- preResolvedPaths.add(resolvedPath)
1707
- }
1708
- }
1709
-
1710
- // Build byConfigPath entry
1711
- const absolutePath = configFilePath
1712
- ? path.resolve(path.dirname(configFilePath), resolvedPath)
1713
- : resolvedPath
1714
- const fileExists = configFilePath ? fs.existsSync(absolutePath) : false
1715
-
1716
- const configPathEntry = {
1717
- location: configValuePath,
1718
- filePath: absolutePath,
1719
- relativePath: resolvedPath,
1720
- originalVariableString: rawValue,
1721
- resolvedVariableString: resolvedVarString,
1722
- containsVariables,
1723
- exists: fileExists,
1724
- }
1725
- if (globPattern) {
1726
- configPathEntry.pattern = globPattern
1727
- }
1728
- byConfigPath.push(configPathEntry)
1729
-
1730
- // Build references entry (use resolvedPath as key when available)
1731
- const refKey = resolvedPath
1732
- if (!referencesMap.has(refKey)) {
1733
- referencesMap.set(refKey, {
1734
- resolvedPath: refKey,
1735
- refs: [],
1736
- })
1737
- }
1738
- const refEntry = referencesMap.get(refKey)
1739
- refEntry.refs.push({
1740
- location: configValuePath,
1741
- value: normalizedPath,
1742
- originalVariableString: rawValue,
1743
- })
1744
- }
1745
- }
1746
- })
1747
-
1748
- variableData[key] = (variableData[key] || []).concat(varData)
1749
- foundVariables.push(rawValue)
1750
- }
1751
- })
1752
-
1753
- // Make foundVariables array unique
1754
- const finalFoundVariables = [...new Set(foundVariables)]
1755
- const varKeys = Object.keys(variableData)
1756
-
1757
- // Calculate summary using same logic as CLI display
1758
- let requiredCount = 0
1759
- let withDefaultsCount = 0
1760
- varKeys.forEach((key) => {
1761
- const instances = variableData[key]
1762
- const firstInstance = instances[0]
1763
-
1764
- // Extract variable name from key (e.g. "${self:service}" -> "self:service")
1765
- const keyVarName = key.slice(2, -1).split(',')[0].trim()
1766
-
1767
- // Find the resolveDetail that matches THIS variable (not any self-ref in the string)
1768
- let matchingDetail = null
1769
- for (const instance of instances) {
1770
- if (instance.resolveDetails && instance.resolveDetails.length > 0) {
1771
- const found = instance.resolveDetails.find((detail) => {
1772
- const detailVar = detail.valueBeforeFallback || detail.variable
1773
- return detailVar === keyVarName
1774
- })
1775
- if (found && (found.variableType === 'dot.prop' || found.variableType === 'self')) {
1776
- matchingDetail = found
1777
- break
1778
- }
1779
- }
1780
- }
1781
-
1782
- // Also check defaultValueIsVar
1783
- if (!matchingDetail && firstInstance.defaultValueIsVar && (
1784
- firstInstance.defaultValueIsVar.variableType === 'self:' ||
1785
- firstInstance.defaultValueIsVar.variableType === 'dot.prop'
1786
- )) {
1787
- matchingDetail = firstInstance.defaultValueIsVar
1788
- }
1789
-
1790
- // Check if truly required
1791
- let isTrulyRequired = false
1792
- if (matchingDetail) {
1793
- // Check if the self-reference resolves to a value
1794
- // Use valueBeforeFallback if present (strips inline fallback like ", false")
1795
- const varPath = matchingDetail.valueBeforeFallback || matchingDetail.variable
1796
- const cleanPath = varPath.replace('self:', '')
1797
- const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1798
- if (typeof dotPropValue === 'undefined') {
1799
- isTrulyRequired = true
1800
- } else {
1801
- // Enrich ALL instances with resolved self-reference value (overrides inline fallbacks)
1802
- instances.forEach((instance) => {
1803
- instance.defaultValueSrc = cleanPath
1804
- instance.defaultValue = dotPropValue
1805
- instance.isRequired = false
1806
- })
1807
- }
1808
- } else if (typeof firstInstance.defaultValue === 'undefined') {
1809
- isTrulyRequired = true
1810
- }
1811
-
1812
- // Update isRequired based on computed isTrulyRequired
1813
- instances.forEach((instance) => {
1814
- instance.isRequired = isTrulyRequired
1815
- })
1816
-
1817
- if (isTrulyRequired) {
1818
- requiredCount++
1819
- } else {
1820
- withDefaultsCount++
1821
- }
1001
+ this._cachedMetadata = collectMetadata({
1002
+ variableSyntax: this.variableSyntax,
1003
+ variablesKnownTypes: this.variablesKnownTypes,
1004
+ variableTypes: this.variableTypes,
1005
+ filterMatch: this.filterMatch,
1006
+ configFilePath: this.configFilePath,
1007
+ // Use rawOriginalConfig for metadata display (truly original, no escaping)
1008
+ displayConfig: this.rawOriginalConfig || this.originalConfig,
1009
+ originalConfig: this.originalConfig,
1010
+ varSuffix: this.varSuffix,
1011
+ varSuffixWithSpacePattern: this.varSuffixWithSpacePattern,
1822
1012
  })
1823
1013
 
1824
- this._cachedMetadata = {
1825
- variables: variableData,
1826
- uniqueVariables: {},
1827
- fileDependencies: {
1828
- globPatterns: fileGlobPatterns,
1829
- // all: fileRefs,
1830
- dynamicPaths: fileRefs.filter(ref => ref.indexOf('*') !== -1 || ref.match(variableSyntax)),
1831
- // Resolved paths: static paths + pre-resolved dynamic paths
1832
- resolvedPaths: [
1833
- ...fileRefs.filter(ref => ref.indexOf('*') === -1 && !ref.match(variableSyntax)),
1834
- ...preResolvedPaths
1835
- ],
1836
- byConfigPath,
1837
- references: Array.from(referencesMap.values()),
1838
- },
1839
- summary: {
1840
- totalVariables: varKeys.length,
1841
- requiredVariables: requiredCount,
1842
- variablesWithDefaults: withDefaultsCount
1843
- },
1844
- }
1845
-
1846
1014
  return this._cachedMetadata
1847
1015
  }
1848
1016
  /**
@@ -1917,7 +1085,7 @@ class Configorama {
1917
1085
  if (!context) context = []
1918
1086
  let results = _results
1919
1087
  if (!results) results = []
1920
-
1088
+
1921
1089
  const addContext = (value, key) => {
1922
1090
  return this.getProperties(root, false, value, context.concat(key), results)
1923
1091
  }
@@ -2877,10 +2045,12 @@ Missing Value ${missingValue} - ${matchedString}
2877
2045
  // console.log('getValueFromSrc caller', caller)
2878
2046
  const propertyString = valueObject.value
2879
2047
  const pathValue = valueObject.path
2048
+ // Cache joined path to avoid repeated array.join('.') calls
2049
+ const pathJoined = pathValue && pathValue.length ? pathValue.join('.') : null
2880
2050
 
2881
2051
  // Track every call to getValueFromSource for metadata
2882
- if (pathValue && pathValue.length) {
2883
- const pathKey = pathValue.join('.')
2052
+ if (this._trackCalls && pathJoined) {
2053
+ const pathKey = pathJoined
2884
2054
  if (!this.resolutionTracking[pathKey]) {
2885
2055
  this.resolutionTracking[pathKey] = {
2886
2056
  path: pathKey,
@@ -2892,7 +2062,7 @@ Missing Value ${missingValue} - ${matchedString}
2892
2062
 
2893
2063
  // this.resolutionTracking[pathKey].resolutionHistory = this.resolutionTracking[pathKey].resolutionHistory || []
2894
2064
 
2895
- // const isDuplicate = this.resolutionTracking[pathKey].resolutionHistory.some(entry =>
2065
+ // const isDuplicate = this.resolutionTracking[pathKey].resolutionHistory.some(entry =>
2896
2066
  // entry.variableString === variableString
2897
2067
  // )
2898
2068
 
@@ -2917,7 +2087,7 @@ Missing Value ${missingValue} - ${matchedString}
2917
2087
  // console.log(`tracker contains ${variableString}`, this.tracker.contains(variableString))
2918
2088
 
2919
2089
  // Cycle detection: track dependencies and check for cycles
2920
- const fromPath = valueObject.path ? valueObject.path.join('.') : null
2090
+ const fromPath = pathJoined
2921
2091
  // Extract target path from variableString (e.g., 'self:b' → 'b', 'b.c' → 'b.c')
2922
2092
  let toPath = variableString
2923
2093
  if (variableString.startsWith('self:')) {
@@ -3049,8 +2219,8 @@ Missing Value ${missingValue} - ${matchedString}
3049
2219
  valueObject,
3050
2220
  ).then((val) => {
3051
2221
  // Update the last call with the resolved value
3052
- if (pathValue && pathValue.length) {
3053
- const pathKey = pathValue.join('.')
2222
+ if (this._trackCalls && pathJoined) {
2223
+ const pathKey = pathJoined
3054
2224
  if (this.resolutionTracking[pathKey] && this.resolutionTracking[pathKey].calls.length) {
3055
2225
  // Find the most recent call for this variableString
3056
2226
  for (let i = this.resolutionTracking[pathKey].calls.length - 1; i >= 0; i--) {
@@ -3131,11 +2301,18 @@ Missing Value ${missingValue} - ${matchedString}
3131
2301
  }
3132
2302
 
3133
2303
  if (valueCount.length === 1 && noNestedVars) {
3134
- const configFilePathMsg = (this.configFilePath) ? `\nIn file ${this.configFilePath} ` : ''
2304
+ let lineInfo = ''
2305
+ if (this.originalString && this.configFilePath && valueObject.path) {
2306
+ const ext = path.extname(this.configFilePath)
2307
+ if (ext === '.yml' || ext === '.yaml' || ext === '.json') {
2308
+ const rawLines = this.originalString.split('\n')
2309
+ const lineNum = findLineByPath(arrayToJsonPath(valueObject.path), rawLines, ext)
2310
+ if (lineNum) lineInfo = ` at line ${lineNum},`
2311
+ }
2312
+ }
2313
+ const configFilePathMsg = (this.configFilePath) ? `\nIn file ${this.configFilePath}${lineInfo} ` : ''
3135
2314
  const fromLine = (propertyString !== valueObject.originalSource) ? `\n From "${valueObject.originalSource}"\n` : ''
3136
2315
 
3137
-
3138
-
3139
2316
  throw new Error(`Unable to resolve config variable "${propertyString}".\n${configFilePathMsg}at location ${valueObject.path ? `"${arrayToJsonPath(valueObject.path)}"` : 'n/a'}${fromLine}
3140
2317
  \nFix this reference, your inputs and/or provide a valid fallback value.
3141
2318
  \nExample of setting a fallback value: \${${variableString}, "fallbackValue"\}\n`)
@@ -3351,7 +2528,7 @@ Missing Value ${missingValue} - ${matchedString}
3351
2528
  }
3352
2529
 
3353
2530
  // Variable NOT FOUND. Warn user
3354
- const key = valueObject.path ? valueObject.path.join('.') : 'na'
2531
+ const key = pathJoined || 'na'
3355
2532
  const errorMessage = [
3356
2533
  `Invalid variable reference syntax`,
3357
2534
  `Key: "${key}"`,
@@ -3395,21 +2572,11 @@ Missing Value ${missingValue} - ${matchedString}
3395
2572
 
3396
2573
  /* Pass through unknown variable types */
3397
2574
  if (allowSpecialCase || this.isUnknownTypeAllowed(propertyString)) {
3398
- // console.log('allowUnknownVars propertyString', propertyString)
3399
- const varMatches = propertyString.match(this.variableSyntax)
3400
- let allowUnknownVars = propertyString
3401
- /* Only encode variables that are actually unknown, not all of them */
3402
- if (varMatches && varMatches.length) {
3403
- varMatches.forEach((m) => {
3404
- // Only encode this variable if IT is unknown (not just because the string contains unknowns)
3405
- if (this.isUnknownTypeAllowed(m)) {
3406
- allowUnknownVars = allowUnknownVars.replace(m, encodeUnknown(m))
3407
- }
3408
- })
3409
- }
3410
- // console.log('allowUnknownVars propertyString:', propertyString)
3411
- // console.log('allowUnknownVars:', allowUnknownVars)
3412
- return Promise.resolve(allowUnknownVars)
2575
+ // Return only the encoded current variable, not the whole propertyString.
2576
+ // The caller substitutes this value at the matched position; returning the
2577
+ // full property would re-insert the surrounding context (including this
2578
+ // variable) and cause exponential string growth on subsequent passes.
2579
+ return Promise.resolve(encodeUnknown(this.varPrefix + variableString + this.varSuffix))
3413
2580
  }
3414
2581
 
3415
2582
  const message = errorMessage.join('\n')