configorama 0.6.12 → 0.6.14

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.
Files changed (66) hide show
  1. package/README.md +196 -24
  2. package/cli.js +3 -3
  3. package/package.json +1 -1
  4. package/src/index.js +22 -32
  5. package/src/main.js +690 -778
  6. package/src/parsers/yaml.js +3 -47
  7. package/src/resolvers/valueFromCron.js +3 -1
  8. package/src/resolvers/valueFromEnv.js +1 -0
  9. package/src/resolvers/valueFromEval.js +1 -0
  10. package/src/resolvers/valueFromFile.js +423 -0
  11. package/src/resolvers/valueFromGit.js +3 -2
  12. package/src/resolvers/valueFromOptions.js +1 -0
  13. package/src/resolvers/valueFromString.js +2 -1
  14. package/src/sync.js +12 -5
  15. package/src/utils/parsing/arrayToJsonPath.test.js +56 -0
  16. package/src/utils/{enrichMetadata.js → parsing/enrichMetadata.js} +210 -18
  17. package/src/utils/{parse.js → parsing/parse.js} +13 -13
  18. package/src/utils/parsing/preProcess.js +165 -0
  19. package/src/utils/{filePathUtils.js → paths/filePathUtils.js} +3 -2
  20. package/src/utils/paths/findLineForKey.js +47 -0
  21. package/src/utils/paths/findLineForKey.test.js +126 -0
  22. package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +24 -26
  23. package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
  24. package/src/utils/regex/index.js +23 -1
  25. package/src/utils/resolution/preResolveVariable.js +260 -0
  26. package/src/utils/resolution/preResolveVariable.test.js +98 -0
  27. package/src/utils/strings/bracketMatcher.js +86 -0
  28. package/src/utils/strings/bracketMatcher.test.js +135 -0
  29. package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
  30. package/src/utils/strings/formatFunctionArgs.test.js +77 -0
  31. package/src/utils/strings/quoteUtils.js +89 -0
  32. package/src/utils/strings/quoteUtils.test.js +217 -0
  33. package/src/utils/strings/replaceAll.test.js +82 -0
  34. package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
  35. package/src/utils/strings/splitCsv.js +38 -0
  36. package/src/utils/strings/splitCsv.test.js +96 -0
  37. package/src/utils/strings/textUtils.test.js +86 -0
  38. package/src/utils/{configWizard.js → ui/configWizard.js} +177 -38
  39. package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
  40. package/src/utils/{logs.js → ui/logs.js} +3 -3
  41. package/src/utils/validation/isValidValue.test.js +64 -0
  42. package/src/utils/validation/warnIfNotFound.js +52 -0
  43. package/src/utils/variables/appendDeepVariable.test.js +41 -0
  44. package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
  45. package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
  46. package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
  47. package/src/utils/variables/getVariableType.test.js +109 -0
  48. package/src/utils/variables/variableUtils.test.js +117 -0
  49. package/src/utils/isValidValue.js +0 -8
  50. package/src/utils/splitCsv.js +0 -29
  51. package/src/utils/trimSurroundingQuotes.js +0 -5
  52. /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
  53. /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
  54. /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.js} +0 -0
  55. /package/src/utils/{filePathUtils.test.js → paths/filePathUtils.test.js} +0 -0
  56. /package/src/utils/{find-project-root.js → paths/findProjectRoot.js} +0 -0
  57. /package/src/utils/{resolveAlias.test.js → paths/resolveAlias.test.js} +0 -0
  58. /package/src/utils/{replaceAll.js → strings/replaceAll.js} +0 -0
  59. /package/src/utils/{splitByComma.test.js → strings/splitByComma.test.js} +0 -0
  60. /package/src/utils/{textUtils.js → strings/textUtils.js} +0 -0
  61. /package/src/utils/{chalk.js → ui/chalk.js} +0 -0
  62. /package/src/utils/{deep-log.js → ui/deep-log.js} +0 -0
  63. /package/src/utils/{appendDeepVariable.js → variables/appendDeepVariable.js} +0 -0
  64. /package/src/utils/{cleanVariable.test.js → variables/cleanVariable.test.js} +0 -0
  65. /package/src/utils/{getVariableType.js → variables/getVariableType.js} +0 -0
  66. /package/src/utils/{variableUtils.js → variables/variableUtils.js} +0 -0
package/src/main.js CHANGED
@@ -1,24 +1,76 @@
1
+ /* Node built-ins */
1
2
  const os = require('os')
2
3
  const path = require('path')
3
4
  const fs = require('fs')
4
- const enrichMetadata = require('./utils/enrichMetadata')
5
- const { normalizePath, extractFilePath, resolveInnerVariables } = require('./utils/filePathUtils')
5
+
6
6
  /* // disable logs to find broken tests
7
7
  console.log = () => {}
8
8
  // process.exit(1)
9
9
  /** */
10
10
 
11
+ /* External dependencies */
11
12
  const promiseFinallyShim = require('promise.prototype.finally').shim()
12
- // @TODO only import lodash we need
13
-
14
13
  const findUp = require('find-up')
15
14
  const traverse = require('traverse')
16
15
  const dotProp = require('dot-prop')
17
- const chalk = require('./utils/chalk')
18
- const { resolveAlias } = require('./utils/resolveAlias')
19
- const { resolveFilePathFromMatch } = require('./utils/getFullFilePath')
16
+ const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
17
+
18
+ /* Utils - root */
19
+ const {
20
+ isArray, isString, isNumber, isObject, isDate, isRegExp, isFunction,
21
+ isEmpty, trim, camelCase, kebabCase, capitalize, split, map, mapValues,
22
+ assign, set, cloneDeep
23
+ } = require('./utils/lodash')
24
+ const PromiseTracker = require('./utils/PromiseTracker')
25
+ const handleSignalEvents = require('./utils/handleSignalEvents')
26
+
27
+ /* Utils - encoders */
28
+ const { encodeUnknown, decodeUnknown } = require('./utils/encoders/unknown-values')
29
+ const { decodeEncodedValue } = require('./utils/encoders')
30
+ const { encodeJsSyntax, decodeJsSyntax, hasParenthesesPlaceholder } = require('./utils/encoders/js-fixes')
20
31
 
21
- /* Default Value resolvers */
32
+ /* Utils - parsing */
33
+ const enrichMetadata = require('./utils/parsing/enrichMetadata')
34
+ const preProcess = require('./utils/parsing/preProcess')
35
+ const { parseFileContents } = require('./utils/parsing/parse')
36
+ const { mergeByKeys } = require('./utils/parsing/mergeByKeys')
37
+ const { arrayToJsonPath } = require('./utils/parsing/arrayToJsonPath')
38
+
39
+ /* Utils - paths */
40
+ const { normalizePath, extractFilePath, resolveInnerVariables } = require('./utils/paths/filePathUtils')
41
+ const { resolveAlias } = require('./utils/paths/resolveAlias')
42
+ const { resolveFilePathFromMatch } = require('./utils/paths/getFullFilePath')
43
+ const { findLineForKey } = require('./utils/paths/findLineForKey')
44
+
45
+ /* Utils - regex */
46
+ const { combineRegexes, funcRegex, funcStartOfLineRegex, subFunctionRegex } = require('./utils/regex')
47
+
48
+ /* Utils - strings */
49
+ const formatFunctionArgs = require('./utils/strings/formatFunctionArgs')
50
+
51
+ const { splitByComma } = require('./utils/strings/splitByComma')
52
+ const { splitCsv } = require('./utils/strings/splitCsv')
53
+ const { replaceAll } = require('./utils/strings/replaceAll')
54
+ const { getTextAfterOccurrence, findNestedVariable } = require('./utils/strings/textUtils')
55
+ const { trimSurroundingQuotes, ensureQuote, isSurroundedByQuotes, startsWithQuotedPipe } = require('./utils/strings/quoteUtils')
56
+
57
+ /* Utils - ui */
58
+ const chalk = require('./utils/ui/chalk')
59
+ const deepLog = require('./utils/ui/deep-log')
60
+ const { logHeader } = require('./utils/ui/logs')
61
+ const { createEditorLink } = require('./utils/ui/createEditorLink')
62
+ const { runConfigWizard, isSensitiveVariable } = require('./utils/ui/configWizard')
63
+
64
+ /* Utils - validation */
65
+ const { warnIfNotFound, isValidValue } = require('./utils/validation/warnIfNotFound')
66
+
67
+ /* Utils - variables */
68
+ const cleanVariable = require('./utils/variables/cleanVariable')
69
+ const appendDeepVariable = require('./utils/variables/appendDeepVariable')
70
+ const { getFallbackString, verifyVariable } = require('./utils/variables/variableUtils')
71
+ const { findNestedVariables } = require('./utils/variables/findNestedVariables')
72
+
73
+ /* Resolvers */
22
74
  const getValueFromString = require('./resolvers/valueFromString')
23
75
  const getValueFromNumber = require('./resolvers/valueFromNumber')
24
76
  const getValueFromEnv = require('./resolvers/valueFromEnv')
@@ -26,44 +78,16 @@ const getValueFromOptions = require('./resolvers/valueFromOptions')
26
78
  const getValueFromCron = require('./resolvers/valueFromCron')
27
79
  const getValueFromEval = require('./resolvers/valueFromEval')
28
80
  const createGitResolver = require('./resolvers/valueFromGit')
29
- /* Default File Parsers */
81
+ const { getValueFromFile: getValueFromFileResolver } = require('./resolvers/valueFromFile')
82
+
83
+ /* Parsers */
30
84
  const YAML = require('./parsers/yaml')
31
85
  const TOML = require('./parsers/toml')
32
86
  const INI = require('./parsers/ini')
33
87
  const JSON5 = require('./parsers/json5')
34
- /* functions */
35
- const md5Function = require('./functions/md5')
36
88
 
37
- /* Utility/helpers */
38
- const cleanVariable = require('./utils/cleanVariable')
39
- const appendDeepVariable = require('./utils/appendDeepVariable')
40
- const isValidValue = require('./utils/isValidValue')
41
- const PromiseTracker = require('./utils/PromiseTracker')
42
- const handleSignalEvents = require('./utils/handleSignalEvents')
43
- const formatFunctionArgs = require('./utils/formatFunctionArgs')
44
- const trimSurroundingQuotes = require('./utils/trimSurroundingQuotes')
45
- const deepLog = require('./utils/deep-log')
46
- const { splitByComma } = require('./utils/splitByComma')
47
- const {
48
- isArray, isString, isNumber, isObject, isDate, isRegExp, isFunction,
49
- isEmpty, trim, camelCase, kebabCase, capitalize, split, map, mapValues,
50
- assign, set, cloneDeep
51
- } = require('./utils/lodash')
52
- const { parseFileContents } = require('./utils/parse')
53
- const { splitCsv } = require('./utils/splitCsv')
54
- const { replaceAll } = require('./utils/replaceAll')
55
- const { getTextAfterOccurrence, findNestedVariable } = require('./utils/textUtils')
56
- const { getFallbackString, verifyVariable } = require('./utils/variableUtils')
57
- const { encodeUnknown, decodeUnknown } = require('./utils/encoders/unknown-values')
58
- const { decodeEncodedValue } = require('./utils/encoders')
59
- const { encodeJsSyntax, decodeJsSyntax, hasParenthesesPlaceholder } = require('./utils/encoders/js-fixes')
60
- const { mergeByKeys } = require('./utils/mergeByKeys')
61
- const { arrayToJsonPath } = require('./utils/arrayToJsonPath')
62
- const { findNestedVariables } = require('./utils/find-nested-variables')
63
- const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
64
- const { logHeader } = require('./utils/logs')
65
- const { createEditorLink } = require('./utils/createEditorLink')
66
- const { runConfigWizard } = require('./utils/configWizard')
89
+ /* Functions */
90
+ const md5Function = require('./functions/md5')
67
91
  /**
68
92
  * Maintainer's notes:
69
93
  *
@@ -89,9 +113,6 @@ const textRefSyntax = RegExp(/^text\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" ]+?)\)/g)
89
113
  const envRefSyntax = RegExp(/^env:/g)
90
114
  const optRefSyntax = RegExp(/^opt:/g)
91
115
  const selfRefSyntax = RegExp(/^self:/g)
92
- const funcRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
93
- const funcStartOfLineRegex = /^(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
94
- const subFunctionRegex = /(\w+):(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
95
116
  const base64WrapperRegex = /\[_\[([A-Za-z0-9+/=\s]*)\]_\]/g
96
117
  const logLines = '─────────────────────────────────────────────────'
97
118
 
@@ -102,147 +123,6 @@ let SETUP_MODE = process.argv.includes('--setup') ? true : false
102
123
  let DEBUG_TYPE = false
103
124
  const ENABLE_FUNCTIONS = true
104
125
 
105
- function combineRegexes(regexes) {
106
- // Extract the pattern from each RegExp and join with OR operator
107
- const patterns = regexes.map(regex => {
108
- // Get source pattern string without flags
109
- return regex.source
110
- }).filter(Boolean)
111
- // Join patterns with the OR operator and create new RegExp
112
- return new RegExp(`(${patterns.join('|')})`)
113
- }
114
-
115
- /**
116
- * Preprocess config to fix malformed fallback references
117
- * @param {Object} configObject - The parsed configuration object
118
- * @param {RegExp} variableSyntax - The variable syntax regex to use
119
- * @returns {Object} The preprocessed configuration object
120
- */
121
- function preProcess(configObject, variableSyntax) {
122
- // Known reference prefixes that should be wrapped in ${}
123
- const refPrefixes = ['self:', 'opt:', 'env:', 'file:', 'text:', 'deep:']
124
-
125
- /**
126
- * Fix malformed fallback references in a string
127
- * @param {string} str - String potentially containing variables
128
- * @returns {string} String with fixed fallback references
129
- */
130
- function fixFallbacksInString(str) {
131
- if (typeof str !== 'string') return str
132
-
133
- let result = str
134
- // result = result.replace(/\$\{self:/g, '${')
135
- let changed = true
136
-
137
- // Keep iterating until no more changes (to handle nested variables)
138
- while (changed) {
139
- changed = false
140
-
141
- // Find innermost ${...} blocks (ones that don't contain other ${)
142
- let i = 0
143
- while (i < result.length) {
144
- if (result[i] === '$' && result[i + 1] === '{') {
145
- const start = i
146
- let braceCount = 1
147
- let j = i + 2
148
-
149
- // Find the matching closing brace by counting { and }
150
- while (j < result.length && braceCount > 0) {
151
- if (result[j] === '{') {
152
- braceCount++
153
- } else if (result[j] === '}') {
154
- braceCount--
155
- }
156
- j++
157
- }
158
-
159
- if (braceCount === 0) {
160
- const end = j
161
- const match = result.substring(start, end)
162
- const content = result.substring(start + 2, end - 1)
163
-
164
- // Only process if there's a comma (indicating fallback syntax)
165
- if (content.includes(',')) {
166
- // Split by comma
167
- const parts = splitByComma(content, variableSyntax)
168
-
169
- if (parts.length > 1) {
170
- // Check if the first part has nested ${} - if so, skip this (process inner ones first)
171
- const firstPart = parts[0]
172
- if (firstPart.includes('${')) {
173
- i = start + 2 // Move past ${ to find inner variables
174
- continue
175
- }
176
-
177
- // Check each part after the first (these are fallback values)
178
- const fixed = parts.map((part, index) => {
179
- if (index === 0) {
180
- return part // Keep the main reference as-is
181
- }
182
-
183
- const trimmed = part.trim()
184
-
185
- // Check if this looks like a reference but is not wrapped
186
- const looksLikeRef = refPrefixes.some(prefix => trimmed.startsWith(prefix))
187
- const alreadyWrapped = trimmed.startsWith('${') && trimmed.endsWith('}')
188
-
189
- if (looksLikeRef && !alreadyWrapped) {
190
- return ` \${${trimmed}}`
191
- }
192
-
193
- return ` ${trimmed}`
194
- })
195
-
196
- const replacement = `\${${fixed.join(',')}}`
197
- if (replacement !== match) {
198
- result = result.substring(0, start) + replacement + result.substring(end)
199
- changed = true
200
- break // Restart search from beginning
201
- }
202
- }
203
- }
204
-
205
- i = start + 2 // Move past ${ to continue searching for nested variables
206
- } else {
207
- i++
208
- }
209
- } else {
210
- i++
211
- }
212
- }
213
- }
214
-
215
- return result
216
- }
217
-
218
- /**
219
- * Recursively traverse and fix the config object
220
- */
221
- function traverseAndFix(obj) {
222
- if (typeof obj === 'string') {
223
- return fixFallbacksInString(obj)
224
- }
225
-
226
- if (Array.isArray(obj)) {
227
- return obj.map(item => traverseAndFix(item))
228
- }
229
-
230
- if (obj !== null && typeof obj === 'object') {
231
- const result = {}
232
- for (const key in obj) {
233
- if (obj.hasOwnProperty(key)) {
234
- result[key] = traverseAndFix(obj[key])
235
- }
236
- }
237
- return result
238
- }
239
-
240
- return obj
241
- }
242
-
243
- return traverseAndFix(configObject)
244
- }
245
-
246
126
  class Configorama {
247
127
  constructor(fileOrObject, opts) {
248
128
  /* attach sig events on async calls */
@@ -254,17 +134,24 @@ class Configorama {
254
134
  // Set opts to pass into JS file calls
255
135
  this.opts = Object.assign({}, {
256
136
  // Allow for unknown variable syntax to pass through without throwing errors
257
- allowUnknownVars: false,
137
+ allowUnknownVariables: false,
258
138
  // Allow undefined to be an end result.
259
139
  allowUndefinedValues: false,
260
140
  // Allow unknown file refs to pass through without throwing errors
261
141
  allowUnknownFileRefs: false,
142
+ // Allow known variable types that can't be resolved to pass through
143
+ allowUnresolvedVariables: false,
262
144
  // Return metadata
263
145
  returnMetadata: false,
264
146
  // Return preResolvedVariableDetails
265
147
  returnPreResolvedVariableDetails: false,
266
148
  }, options)
267
149
 
150
+ // Backward compat: allowUnknownVars -> allowUnknownVariables
151
+ if (options.allowUnknownVars !== undefined && options.allowUnknownVariables === undefined) {
152
+ this.opts.allowUnknownVariables = options.allowUnknownVars
153
+ }
154
+
268
155
  this.filterCache = {}
269
156
 
270
157
  this.foundVariables = []
@@ -358,6 +245,7 @@ class Configorama {
358
245
  */
359
246
  {
360
247
  type: 'self',
248
+ source: 'config',
361
249
  prefix: 'self',
362
250
  syntax: '${self:pathToKeyInConfig}',
363
251
  description: `Resolves values from the current config object. Supports sub-properties via :key lookup.`,
@@ -374,9 +262,10 @@ class Configorama {
374
262
  */
375
263
  {
376
264
  type: 'file',
265
+ source: 'config',
377
266
  prefix: 'file',
378
267
  syntax: '${file(pathToFile.json)}',
379
- description: `Resolves values from files. Supports sub-properties via :key lookup.`,
268
+ description: `Resolves values from files. Supports sub-properties via :key or .key lookup.`,
380
269
  match: fileRefSyntax,
381
270
  resolver: (varString, o, x, pathValue) => {
382
271
  return this.getValueFromFile(varString, { context: pathValue })
@@ -386,6 +275,7 @@ class Configorama {
386
275
 
387
276
  {
388
277
  type: 'text',
278
+ source: 'config',
389
279
  prefix: 'text',
390
280
  match: textRefSyntax,
391
281
  resolver: (varString, o, x, pathValue) => {
@@ -421,6 +311,7 @@ class Configorama {
421
311
  /* Nicer self: references. Match key in object */
422
312
  const fallThroughSelfMatcher = {
423
313
  type: 'dot.prop',
314
+ source: 'config',
424
315
  match: (varString, fullObject, valueObject) => {
425
316
  /*
426
317
  console.log('fallThroughSelfMatcher varString', varString)
@@ -542,8 +433,8 @@ class Configorama {
542
433
  Boolean: (value) => {
543
434
  if (typeof value === 'boolean') return value
544
435
  const v = String(value).toLowerCase()
545
- if (['true', '1', 'yes', 'on'].includes(v)) return true
546
- if (['false', '0', 'no', 'off'].includes(v)) return false
436
+ if (['true', '1', 'yes', 'on', 'enabled'].includes(v)) return true
437
+ if (['false', '0', 'no', 'off', 'disabled'].includes(v)) return false
547
438
  throw new Error(`Configorama Error: Expected Boolean, got "${value}"`)
548
439
  },
549
440
  String: (value) => {
@@ -573,7 +464,9 @@ class Configorama {
573
464
  // (\|\s*(toUpperCase|toLowerCase|toCamelCase|toKebabCase|capitalize)\s*)+$
574
465
  // Updated to support function-style filters like help('text') with nested parens
575
466
  // Use a more permissive pattern that matches anything between parens including nested parens
576
- this.filterMatch = new RegExp(`(\\|\\s*(${Object.keys(this.filters).join('|')})(?:\\s*\\([^)]*(?:\\([^)]*\\))?[^)]*\\))?\\s*)+}?$`)
467
+ this.filterMatch = new RegExp(
468
+ `(\\|\\s*(${Object.keys(this.filters).join('|')})(?:\\s*\\([^)]*(?:\\([^)]*\\))?[^)]*\\))?\\s*)+}?$`
469
+ )
577
470
  // console.log('this.filterMatch', this.filterMatch)
578
471
 
579
472
  this.functions = {
@@ -638,15 +531,9 @@ class Configorama {
638
531
  this.callCount = 0
639
532
  }
640
533
 
641
- initialCall(func) {
642
- this.deep = []
643
- this.tracker.start()
644
- return func().finally(() => {
645
- this.tracker.stop()
646
- this.deep = []
647
- })
648
- }
649
-
534
+ // ################
535
+ // ## PUBLIC API ##
536
+ // ################
650
537
  /**
651
538
  * Populate all variables in the service, conveniently remove and restore the service attributes
652
539
  * that confuse the population methods.
@@ -658,6 +545,7 @@ class Configorama {
658
545
  const configoramaOpts = this.opts
659
546
 
660
547
  const showFoundVariables = configoramaOpts && configoramaOpts.dynamicArgs && (configoramaOpts.dynamicArgs.list || configoramaOpts.dynamicArgs.info)
548
+
661
549
 
662
550
  // If we have a file path but no config yet, parse it now
663
551
  if (this.configFilePath && !this.config) {
@@ -669,13 +557,16 @@ class Configorama {
669
557
  this.opts
670
558
  )
671
559
  this.configFileContents = ''
672
- if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
560
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails || SETUP_MODE) {
673
561
  this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
674
562
  }
675
563
  /*
676
564
  console.log('before preprocess', configObject)
677
565
  /** */
678
- /* Preprocess step here */
566
+ // Store truly raw config before any preprocessing (for metadata display)
567
+ this.rawOriginalConfig = cloneDeep(configObject)
568
+
569
+ /* Preprocess step here - escapes ${} in help() args, fixes malformed fallbacks */
679
570
  configObject = preProcess(configObject, this.variableSyntax)
680
571
  /*
681
572
  console.log('after preprocess', configObject)
@@ -696,22 +587,24 @@ class Configorama {
696
587
  const variableSyntax = this.variableSyntax
697
588
  const variablesKnownTypes = this.variablesKnownTypes
698
589
 
699
- if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
700
- // Use collectVariableMetadata to get variable info (DRY - don't duplicate logic)
590
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails || SETUP_MODE) {
701
591
  const metadata = this.collectVariableMetadata()
702
592
 
703
- const enrich = enrichMetadata(
593
+ const enrich = await enrichMetadata(
704
594
  metadata,
705
595
  this.resolutionTracking,
706
596
  this.variableSyntax,
707
597
  this.fileRefsFound,
708
598
  this.originalConfig,
709
599
  this.configFilePath,
710
- Object.keys(this.filters)
600
+ Object.keys(this.filters),
601
+ undefined, // resolvedConfig not available yet
602
+ this.opts.options,
603
+ this.variableTypes
711
604
  )
712
605
 
713
606
  if (showFoundVariables) {
714
- /*
607
+ //*
715
608
  deepLog('metadata', metadata)
716
609
  fs.writeFileSync(`metadata-${path.basename(this.configFilePath)}.json`, JSON.stringify(metadata, null, 2))
717
610
  deepLog('enrich', enrich)
@@ -725,7 +618,10 @@ class Configorama {
725
618
  const uniqueVarKeys = Object.keys(uniqueVariables)
726
619
 
727
620
  if (this.opts.returnPreResolvedVariableDetails) {
728
- return enrich
621
+ return Object.assign({}, {
622
+ resolved: false,
623
+ originalConfig: this.originalConfig
624
+ }, enrich)
729
625
  }
730
626
 
731
627
  if (!varKeys.length) {
@@ -751,6 +647,10 @@ class Configorama {
751
647
  console.log()
752
648
  }
753
649
 
650
+ const lines = this.configFileContents ? this.configFileContents.split('\n') : []
651
+ const fileType = this.configFileType
652
+ const configFilePath = this.configFilePath
653
+
754
654
  if (varKeys.length > 0) {
755
655
  const fileName = this.configFilePath ? ` in ${this.configFilePath}` : ''
756
656
 
@@ -781,8 +681,6 @@ class Configorama {
781
681
 
782
682
  logHeader('Variable Details')
783
683
 
784
- const lines = this.configFileContents ? this.configFileContents.split('\n') : []
785
-
786
684
  const indent = ''
787
685
  const boxes = varKeys.map((key, i) => {
788
686
  const variableInstances = variableData[key]
@@ -821,7 +719,12 @@ class Configorama {
821
719
  varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
822
720
  }
823
721
 
824
-
722
+ // Show resolve order from metadata
723
+ if (firstInstance.resolveOrder.length > 1) {
724
+ varMsg += `${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
725
+ const resolveOrder = firstInstance.resolveOrder.join(', ')
726
+ varMsg += ` ${valueChalk(resolveOrder)}\n`
727
+ }
825
728
 
826
729
  // Show default value from metadata
827
730
  if (typeof firstInstance.defaultValue !== 'undefined') {
@@ -832,34 +735,39 @@ class Configorama {
832
735
 
833
736
  // Show default value source path from metadata
834
737
  if (firstInstance.defaultValueSrc) {
835
- varMsg += `${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
836
- varMsg += `${valueChalk(firstInstance.defaultValueSrc)}\n`
837
- }
838
-
839
- // Show resolve order from metadata
840
- if (firstInstance.resolveOrder.length > 1) {
841
- varMsg += `${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
842
- const resolveOrder = firstInstance.resolveOrder.join(', ')
843
- varMsg += ` ${valueChalk(resolveOrder)}\n`
738
+ varMsg += `${indent}${keyChalk('Default path:'.padEnd(titleText.length, ' '))} `
739
+ const defaultPathLine = findLineForKey(firstInstance.defaultValueSrc, lines, fileType)
740
+ if (defaultPathLine) {
741
+ varMsg += `${createEditorLink(configFilePath, defaultPathLine, 1, firstInstance.defaultValueSrc, 'gray')}\n`
742
+ } else {
743
+ varMsg += `${valueChalk(firstInstance.defaultValueSrc)}\n`
744
+ }
844
745
  }
845
746
 
846
747
  // Show path(s) from metadata
847
- let locationRender = valueChalk(variableInstances[0].path)
748
+ const configPathLine = findLineForKey(variableInstances[0].path, lines, fileType)
749
+ let locationRender = configPathLine
750
+ ? createEditorLink(configFilePath, configPathLine, 1, variableInstances[0].path, 'gray')
751
+ : valueChalk(variableInstances[0].path)
848
752
  let locationLabel = `${indent}${keyChalk('Config Path:'.padEnd(titleText.length, ' '))}`
849
753
  let typeText = ''
850
754
  if (variableInstances.length > 1) {
851
755
  const pathIndent = ' '.repeat(titleText.length + 1)
852
756
  const pathItems = variableInstances.map((v, idx) => {
757
+ const pathLine = findLineForKey(v.path, lines, fileType)
758
+ const pathLink = pathLine
759
+ ? createEditorLink(configFilePath, pathLine, 1, `- ${v.path}`, 'gray')
760
+ : valueChalk(`- ${v.path}`)
853
761
  // Show type filter per path if different
854
762
  if (uniqueVar && uniqueVar.occurrences.length > 1) {
855
763
  const occurrence = uniqueVar.occurrences.find(occ => occ.path === v.path)
856
764
  const pathType = occurrence && occurrence.type
857
765
  typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
858
766
  const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
859
- return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
767
+ return `${prefix}${pathLink}${typeText}`
860
768
  }
861
769
  const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
862
- return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
770
+ return `${prefix}${pathLink}${typeText}`
863
771
  })
864
772
  locationRender = pathItems.join('\n')
865
773
  locationLabel = `${indent}${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
@@ -869,36 +777,7 @@ class Configorama {
869
777
  }
870
778
  varMsg += `${locationLabel} ${locationRender}`
871
779
 
872
- // Find line number in config file based on format (YAML, TOML, JSON, INI)
873
- const configKey = firstInstance.key
874
- const line = lines.findIndex((line) => {
875
- const fileType = this.configFileType
876
- const escapedKey = configKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
877
- // YAML: key: or key :
878
- if (fileType === '.yml' || fileType === '.yaml') {
879
- return new RegExp(`^\\s*${escapedKey}\\s*:`).test(line)
880
- }
881
- // TOML: key = or key=
882
- if (fileType === '.toml') {
883
- return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
884
- }
885
- // JSON: "key": or "key" :
886
- if (fileType === '.json' || fileType === '.json5') {
887
- return new RegExp(`"${escapedKey}"\\s*:`).test(line)
888
- }
889
- // INI: key = or key=
890
- if (fileType === '.ini') {
891
- return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
892
- }
893
- // JS/TS/ESM: key: or "key": or 'key': or `key`: or [`key`]:
894
- if (['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(fileType)) {
895
- return new RegExp(`(?:${escapedKey}|"${escapedKey}"|'${escapedKey}'|\`${escapedKey}\`|\\[\`${escapedKey}\`\\])\\s*:`).test(line)
896
- }
897
- // Default fallback: try YAML-style
898
- return line.includes(`${configKey}:`)
899
- })
900
- const lineNumber = line !== -1 ? line + 1 : 0
901
-
780
+ const lineNumber = findLineForKey(firstInstance.key, lines, fileType)
902
781
 
903
782
  return {
904
783
  content: {
@@ -928,6 +807,276 @@ class Configorama {
928
807
  // process.exit(1)
929
808
  }
930
809
 
810
+ // New unique variable makeStackedBoxes display
811
+ const uniqueBoxes = uniqueVarKeys.map((varName, i) => {
812
+ const uniqueVar = uniqueVariables[varName]
813
+ const occurrences = uniqueVar.occurrences || []
814
+ const firstOcc = occurrences[0] || {}
815
+
816
+ const spacing = ' '
817
+ const titleText = `Variable:${spacing}`
818
+ const VALUE_HEX = '#899499'
819
+ const keyChalk = chalk.whiteBright
820
+ const valueChalk = chalk.hex(VALUE_HEX)
821
+
822
+ let varMsg = ''
823
+ let requiredMessage = ''
824
+
825
+ // Show required status from computed isRequired (accounts for resolved self-refs)
826
+ const isRequired = occurrences.some(occ => occ.isRequired)
827
+ if (isRequired) {
828
+ requiredMessage = `${chalk.red.bold('[Required]')}`
829
+ }
830
+
831
+ // Show type filter if present
832
+ if (uniqueVar.types && uniqueVar.types.length > 0) {
833
+ const typeLabel = `${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
834
+ varMsg += `${typeLabel} ${valueChalk(uniqueVar.types.join(', '))}\n`
835
+ }
836
+
837
+ // Show description
838
+ if (uniqueVar.descriptions && uniqueVar.descriptions.length > 0) {
839
+ const descText = `${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
840
+ const combinedDesc = uniqueVar.descriptions.join('. ')
841
+ varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
842
+ }
843
+
844
+ // Show default value only if it's a true fallback, not a pre-resolved value
845
+ // Redact sensitive values like API keys, secrets, tokens
846
+ const isSensitive = isSensitiveVariable(varName)
847
+ const hasActualDefault = firstOcc.hasFallback && typeof firstOcc.defaultValue !== 'undefined'
848
+ if (hasActualDefault) {
849
+ const defaultValueRender = isSensitive ? '********' : (firstOcc.defaultValue === '' ? '""' : firstOcc.defaultValue)
850
+ const defaultValueText = `${keyChalk('Default value:'.padEnd(titleText.length, ' '))}`
851
+ varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}\n`
852
+ } else if (uniqueVar.resolvedValue !== undefined) {
853
+ // Show pre-resolved current value (e.g., from env, git)
854
+ const resolvedRender = isSensitive ? '********' : (uniqueVar.resolvedValue === '' ? '""' : uniqueVar.resolvedValue)
855
+ const resolvedText = `${keyChalk('Current value:'.padEnd(titleText.length, ' '))}`
856
+ const envIndicator = uniqueVar.variableType === 'env' ? ` ${chalk.red('(currently set env var)')}` : ''
857
+ varMsg += `${resolvedText} ${valueChalk(resolvedRender)}${envIndicator}\n`
858
+ }
859
+
860
+ // Show default value source path
861
+ if (firstOcc.defaultValueSrc) {
862
+ varMsg += `${keyChalk('Default path:'.padEnd(titleText.length, ' '))} `
863
+ const defaultPathLine = findLineForKey(firstOcc.defaultValueSrc, lines, fileType)
864
+ if (defaultPathLine) {
865
+ varMsg += `${createEditorLink(configFilePath, defaultPathLine, 1, firstOcc.defaultValueSrc, 'gray')}\n`
866
+ } else {
867
+ varMsg += `${valueChalk(firstOcc.defaultValueSrc)}\n`
868
+ }
869
+ }
870
+
871
+ // Show config path(s) from occurrences
872
+ let locationRender
873
+ let locationLabel
874
+ if (occurrences.length > 1) {
875
+ const pathIndent = ' '.repeat(titleText.length + 1)
876
+ const pathItems = occurrences.map((occ, idx) => {
877
+ const pathLine = findLineForKey(occ.path, lines, fileType)
878
+ const pathLink = pathLine
879
+ ? createEditorLink(configFilePath, pathLine, 1, `- ${occ.path}`, 'gray')
880
+ : valueChalk(`- ${occ.path}`)
881
+ const typeText = occ.type ? ` ${chalk.dim(`Type: ${occ.type}`)}` : ''
882
+ const prefix = idx === 0 ? '' : `${pathIndent}`
883
+ return `${prefix}${pathLink}${typeText}`
884
+ })
885
+ locationRender = pathItems.join('\n')
886
+ locationLabel = `${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
887
+ } else {
888
+ const pathLine = findLineForKey(firstOcc.path, lines, fileType)
889
+ locationRender = pathLine
890
+ ? createEditorLink(configFilePath, pathLine, 1, firstOcc.path, 'gray')
891
+ : valueChalk(firstOcc.path)
892
+ locationLabel = `${keyChalk('Config Path:'.padEnd(titleText.length, ' '))}`
893
+ }
894
+ varMsg += `${locationLabel} ${locationRender}`
895
+
896
+ // Find first line number for title
897
+ const lineNumber = findLineForKey(firstOcc.path, lines, fileType)
898
+
899
+ return {
900
+ content: {
901
+ left: varMsg,
902
+ backgroundColor: 'red',
903
+ width: '100%',
904
+ },
905
+ title: {
906
+ left: `▷ ${firstOcc.varMatch}`,
907
+ right: `${requiredMessage} ${lineNumber ? `Line: ${lineNumber.toString().padEnd(2, ' ')}` : ''}`,
908
+ paddingBottom: 1,
909
+ paddingTop: (i === 0) ? 1 : 0,
910
+ truncate: true,
911
+ },
912
+ width: '100%',
913
+ }
914
+ })
915
+
916
+ console.log(makeStackedBoxes(uniqueBoxes, {
917
+ borderText: 'Unique Variables',
918
+ borderColor: 'gray',
919
+ minWidth: '96%',
920
+ borderStyle: 'bold',
921
+ disableTitleSeparator: true,
922
+ }))
923
+ console.log()
924
+
925
+
926
+ // Unique variables that require setup (excludes readonly source types)
927
+ const CONFIGURABLE_SOURCES = ['user', 'config', 'remote']
928
+ const configurableVariables = {}
929
+ const configurableVarKeys = []
930
+
931
+ for (const varName of uniqueVarKeys) {
932
+ const uniqueVar = uniqueVariables[varName]
933
+ // Include if source type is user, config, or remote (not readonly)
934
+ if (CONFIGURABLE_SOURCES.includes(uniqueVar.variableSourceType)) {
935
+ configurableVariables[varName] = uniqueVar
936
+ configurableVarKeys.push(varName)
937
+ }
938
+ }
939
+
940
+ // Display configurable variables by source type
941
+ if (configurableVarKeys.length > 0) {
942
+ const spacing = ' '
943
+ const titleText = `Variable:${spacing}`
944
+ const VALUE_HEX = '#899499'
945
+ const keyChalk = chalk.whiteBright
946
+ const valueChalk = chalk.hex(VALUE_HEX)
947
+
948
+ // Group by source type
949
+ const bySource = {
950
+ user: [],
951
+ config: [],
952
+ remote: [],
953
+ }
954
+
955
+ for (const varName of configurableVarKeys) {
956
+ const v = configurableVariables[varName]
957
+ const sourceType = v.variableSourceType || 'user'
958
+ if (bySource[sourceType]) {
959
+ bySource[sourceType].push({ varName, ...v })
960
+ }
961
+ }
962
+
963
+ const sourceLabels = {
964
+ user: 'User Input Required',
965
+ config: 'Config References',
966
+ remote: 'Remote Services',
967
+ }
968
+
969
+ const sourceColors = {
970
+ user: 'yellow',
971
+ config: 'cyan',
972
+ remote: 'magenta',
973
+ }
974
+
975
+ const configurableBoxes = []
976
+
977
+ for (const [sourceType, vars] of Object.entries(bySource)) {
978
+ if (vars.length === 0) continue
979
+
980
+ for (let i = 0; i < vars.length; i++) {
981
+ const v = vars[i]
982
+ const occurrences = v.occurrences || []
983
+ const firstOcc = occurrences[0] || {}
984
+
985
+ let varMsg = ''
986
+ let requiredMessage = ''
987
+
988
+ // Show required status from computed isRequired (accounts for resolved self-refs)
989
+ const isRequired = occurrences.some(occ => occ.isRequired)
990
+ if (isRequired) {
991
+ requiredMessage = `${chalk.red.bold('[Required]')}`
992
+ }
993
+
994
+ // Show description if present (directly under title, not as key/value)
995
+ if (v.descriptions && v.descriptions.length > 0) {
996
+ varMsg += `${chalk.dim(v.descriptions.join('. '))}\n\n`
997
+ }
998
+
999
+ // Show type filter if defined (String, Number, etc.)
1000
+ const varType = (v.types && v.types[0]) || firstOcc.type
1001
+ if (varType) {
1002
+ varMsg += `${keyChalk('Type:'.padEnd(titleText.length, ' '))} ${valueChalk(varType)}\n`
1003
+ }
1004
+
1005
+ // Show current/default value (redact sensitive values)
1006
+ const isSensitive = isSensitiveVariable(v.varName)
1007
+ if (v.resolvedValue !== undefined) {
1008
+ const resolvedRender = isSensitive ? '********' : (v.resolvedValue === '' ? '""' : v.resolvedValue)
1009
+ varMsg += `${keyChalk('Current value:'.padEnd(titleText.length, ' '))} ${valueChalk(resolvedRender)}\n`
1010
+ } else if (firstOcc.hasFallback && firstOcc.defaultValue !== undefined) {
1011
+ const defaultRender = isSensitive ? '********' : (firstOcc.defaultValue === '' ? '""' : firstOcc.defaultValue)
1012
+ varMsg += `${keyChalk('Default value:'.padEnd(titleText.length, ' '))} ${valueChalk(defaultRender)}\n`
1013
+ }
1014
+
1015
+ // Show config path(s)
1016
+ let locationRender
1017
+ let locationLabel
1018
+ if (occurrences.length > 1) {
1019
+ const pathIndent = ' '.repeat(titleText.length + 1)
1020
+ const pathItems = occurrences.map((occ, idx) => {
1021
+ const pathLine = findLineForKey(occ.path, lines, fileType)
1022
+ const pathLink = pathLine
1023
+ ? createEditorLink(configFilePath, pathLine, 1, `- ${occ.path}`, VALUE_HEX)
1024
+ : valueChalk(`- ${occ.path}`)
1025
+ const prefix = idx === 0 ? '' : `${pathIndent}`
1026
+ return `${prefix}${pathLink}`
1027
+ })
1028
+ locationRender = pathItems.join('\n')
1029
+ locationLabel = 'Config Paths:'
1030
+ } else {
1031
+ const pathLine = findLineForKey(firstOcc.path, lines, fileType)
1032
+ locationRender = pathLine
1033
+ ? createEditorLink(configFilePath, pathLine, 1, firstOcc.path, VALUE_HEX)
1034
+ : valueChalk(firstOcc.path)
1035
+ locationLabel = 'Config Path:'
1036
+ }
1037
+ varMsg += `${keyChalk(locationLabel.padEnd(titleText.length, ' '))} ${locationRender}`
1038
+
1039
+ // Get type for center heading (reuse varType from above)
1040
+ const typeText = varType ? chalk.dim(`Type: ${varType}`) : ''
1041
+
1042
+ // Get line number for first occurrence
1043
+ const firstOccLine = findLineForKey(firstOcc.path, lines, fileType)
1044
+ const varTitle = firstOcc.varMatch || v.varName
1045
+ const requiredSuffix = requiredMessage ? ` - ${requiredMessage}` : ''
1046
+ const titleLink = firstOccLine
1047
+ ? createEditorLink(configFilePath, firstOccLine, 1, `▷ ${varTitle}`) + requiredSuffix
1048
+ : `▷ ${varTitle}${requiredSuffix}`
1049
+
1050
+ configurableBoxes.push({
1051
+ content: {
1052
+ left: varMsg,
1053
+ width: '100%',
1054
+ },
1055
+ title: {
1056
+ left: titleLink,
1057
+ // center: typeText,
1058
+ right: chalk.dim(`${v.variableType}`),
1059
+ paddingBottom: 1,
1060
+ paddingTop: (configurableBoxes.length === 0) ? 1 : 0,
1061
+ truncate: true,
1062
+ },
1063
+ width: '100%',
1064
+ })
1065
+ }
1066
+ }
1067
+
1068
+ if (configurableBoxes.length > 0) {
1069
+ console.log(makeStackedBoxes(configurableBoxes, {
1070
+ borderText: `Configurable Variables (${configurableVarKeys.length})`,
1071
+ borderColor: 'yellow',
1072
+ minWidth: '96%',
1073
+ borderStyle: 'bold',
1074
+ disableTitleSeparator: true,
1075
+ }))
1076
+ console.log()
1077
+ }
1078
+ }
1079
+
931
1080
 
932
1081
  // WALK through CLI prompt if --setup flag is set
933
1082
  if (SETUP_MODE) {
@@ -935,20 +1084,29 @@ class Configorama {
935
1084
  // deepLog('enrich', enrich)
936
1085
  const userInputs = await runConfigWizard(enrich, this.originalConfig, this.configFilePath)
937
1086
 
938
- console.log('\n')
939
1087
  logHeader('User Inputs Summary')
1088
+ console.log()
940
1089
  console.log(JSON.stringify(userInputs, null, 2))
941
1090
 
942
1091
  // TODO set values
943
1092
 
944
1093
  // Apply user inputs to options and environment
945
1094
  if (userInputs.options) {
946
- Object.assign(this.opts, userInputs.options)
1095
+ Object.assign(this.options, userInputs.options)
947
1096
  }
948
1097
  if (userInputs.env) {
949
1098
  Object.assign(process.env, userInputs.env)
950
1099
  }
951
- // Note: self references are in the config, so no need to apply them
1100
+
1101
+ if (userInputs.self) {
1102
+ Object.assign(this.config, userInputs.self)
1103
+ }
1104
+
1105
+ if (userInputs.dotProp) {
1106
+ for (const [key, value] of Object.entries(userInputs.dotProp)) {
1107
+ dotProp.set(this.config, key, value)
1108
+ }
1109
+ }
952
1110
 
953
1111
  console.log()
954
1112
  logHeader('Resolving Configuration')
@@ -962,7 +1120,8 @@ class Configorama {
962
1120
 
963
1121
  /* Exit early if list or info flag is set */
964
1122
  if (showFoundVariables) {
965
- process.exit(0)
1123
+ // TODO re-enable this
1124
+ // process.exit(0)
966
1125
  }
967
1126
  }
968
1127
 
@@ -1096,12 +1255,18 @@ class Configorama {
1096
1255
  * @returns {object} Metadata object containing variables, fileRefs, and summary
1097
1256
  */
1098
1257
  collectVariableMetadata() {
1258
+ // Return cached metadata if already computed
1259
+ if (this._cachedMetadata) {
1260
+ return this._cachedMetadata
1261
+ }
1262
+
1099
1263
  const variableSyntax = this.variableSyntax
1100
1264
  const variablesKnownTypes = this.variablesKnownTypes
1101
1265
  const variableTypes = this.variableTypes
1102
1266
  const filterMatch = this.filterMatch
1103
1267
  const configFilePath = this.configFilePath
1104
- const originalConfig = this.originalConfig
1268
+ // Use rawOriginalConfig for metadata display (truly original, no escaping)
1269
+ const originalConfig = this.rawOriginalConfig || this.originalConfig
1105
1270
  const foundVariables = []
1106
1271
  const variableData = {}
1107
1272
  const fileRefs = []
@@ -1111,7 +1276,7 @@ class Configorama {
1111
1276
  const referencesMap = new Map()
1112
1277
  let matchCount = 1
1113
1278
 
1114
- traverse(this.originalConfig).forEach(function (rawValue) {
1279
+ traverse(originalConfig).forEach(function (rawValue) {
1115
1280
  if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
1116
1281
  const configValuePath = this.path.join('.')
1117
1282
  /* Skip Fn::Sub variables */
@@ -1152,6 +1317,21 @@ class Configorama {
1152
1317
 
1153
1318
  const key = keyWithoutFilters
1154
1319
 
1320
+ // Helper to pre-resolve a variable from config
1321
+ const preResolveFromConfig = (varString, varType) => {
1322
+ if (!varString) return undefined
1323
+ // Handle self: prefix
1324
+ const varPath = varString.startsWith('self:') ? varString.slice(5) : varString
1325
+ // Only pre-resolve dot.prop and self references
1326
+ if (varType === 'dot.prop' || varType === 'self') {
1327
+ const value = dotProp.get(originalConfig, varPath)
1328
+ if (value !== undefined && typeof value !== 'object') {
1329
+ return { resolved: value, path: varPath }
1330
+ }
1331
+ }
1332
+ return undefined
1333
+ }
1334
+
1155
1335
  // Strip filters from resolveDetails
1156
1336
  const cleanedResolveDetails = nested.map(detail => {
1157
1337
  const cleaned = { ...detail }
@@ -1173,6 +1353,14 @@ class Configorama {
1173
1353
  cleaned.varString = cleaned.varString.replace(filterMatch, '').trim()
1174
1354
  }
1175
1355
  }
1356
+
1357
+ // Pre-resolve dot.prop and self references
1358
+ const preResolved = preResolveFromConfig(cleaned.varString || cleaned.variable, cleaned.variableType)
1359
+ if (preResolved) {
1360
+ cleaned.varResolved = preResolved.resolved
1361
+ cleaned.varResolvedPath = preResolved.path
1362
+ }
1363
+
1176
1364
  // Also clean fallbackValues if present
1177
1365
  if (cleaned.fallbackValues && Array.isArray(cleaned.fallbackValues)) {
1178
1366
  cleaned.fallbackValues = cleaned.fallbackValues.map(fb => {
@@ -1195,6 +1383,17 @@ class Configorama {
1195
1383
  cleanedFb.stringValue = cleanedFb.stringValue.replace(filterMatch, '').trim()
1196
1384
  }
1197
1385
  }
1386
+
1387
+ // Pre-resolve fallback variable references
1388
+ if (cleanedFb.stringValue && cleanedFb.stringValue.match(/^\$\{[^}]+\}$/)) {
1389
+ const innerVar = cleanedFb.stringValue.slice(2, -1)
1390
+ const fbPreResolved = preResolveFromConfig(innerVar, 'dot.prop')
1391
+ if (fbPreResolved) {
1392
+ cleanedFb.varResolved = fbPreResolved.resolved
1393
+ cleanedFb.varResolvedPath = fbPreResolved.path
1394
+ }
1395
+ }
1396
+
1198
1397
  return cleanedFb
1199
1398
  })
1200
1399
  }
@@ -1229,35 +1428,59 @@ class Configorama {
1229
1428
 
1230
1429
  if (item && item.fallbackValues) {
1231
1430
  let hasResolvedFallback
1431
+ let defaultValueSrc
1432
+ const isSingleFallback = item.fallbackValues.length === 1
1232
1433
  const order = ([stripFilters(item.valueBeforeFallback)]).concat(item.fallbackValues.map((f, i) => {
1233
1434
  if (f.fallbackValues) {
1234
- const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
1435
+ const [nestedOrder, nestedResolvedFallback, nestedDefaultSrc] = calculateResolveOrder(f)
1235
1436
  if (!hasResolvedFallback && nestedResolvedFallback) {
1236
1437
  hasResolvedFallback = nestedResolvedFallback
1438
+ defaultValueSrc = nestedDefaultSrc
1237
1439
  }
1238
1440
  return nestedOrder
1239
1441
  }
1240
1442
 
1443
+ const valueStr = stripFilters(f.stringValue || f.variable)
1444
+
1445
+ // Only set default from first resolvable fallback
1241
1446
  if (!hasResolvedFallback && f.isResolvedFallback) {
1242
- hasResolvedFallback = stripFilters(f.stringValue)
1243
- }
1244
- if (f.isResolvedFallback) {
1245
- hasResolvedFallback = stripFilters(f.stringValue)
1447
+ if (f.varResolved !== undefined) {
1448
+ hasResolvedFallback = f.varResolved
1449
+ defaultValueSrc = f.varResolvedPath
1450
+ } else if (!valueStr.match(/^\$\{[^}]+\}$/)) {
1451
+ // Literal value - use as default
1452
+ hasResolvedFallback = valueStr
1453
+ }
1454
+ // If variable can't resolve, don't set - let next fallback try
1246
1455
  }
1247
1456
 
1248
1457
  if (!hasResolvedFallback && f.isVariable) {
1249
1458
  defaultValueIsVar = f
1250
1459
  }
1251
- const valueStr = stripFilters(f.stringValue || f.variable)
1252
- return `${valueStr}${f.isResolvedFallback ? ' (default)' : ''}`
1460
+
1461
+ if (f.isResolvedFallback) {
1462
+ if (isSingleFallback) {
1463
+ // Single fallback: show "value (default)"
1464
+ return `${valueStr} (default)`
1465
+ } else {
1466
+ // Multiple fallbacks: show resolved value if available
1467
+ if (f.varResolved !== undefined) {
1468
+ return `${valueStr} = ${f.varResolved}`
1469
+ }
1470
+ // If can't resolve, just show the value without annotation
1471
+ return valueStr
1472
+ }
1473
+ }
1474
+ return valueStr
1253
1475
  })).flat()
1254
1476
 
1255
- return [order, hasResolvedFallback]
1477
+ return [order, hasResolvedFallback, defaultValueSrc]
1256
1478
  }
1257
- return [[stripFilters(item.variable)], undefined]
1479
+ return [[stripFilters(item.variable)], undefined, undefined]
1258
1480
  }
1259
1481
 
1260
- const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
1482
+ const lastCleanedItem = cleanedResolveDetails[cleanedResolveDetails.length - 1]
1483
+ const [resolveOrder, hasResolvedFallback, defaultValueSrc] = calculateResolveOrder(lastCleanedItem)
1261
1484
  varData.resolveOrder = resolveOrder
1262
1485
 
1263
1486
  if (defaultValueIsVar) {
@@ -1268,6 +1491,10 @@ class Configorama {
1268
1491
  varData.defaultValue = hasResolvedFallback
1269
1492
  }
1270
1493
 
1494
+ if (defaultValueSrc) {
1495
+ varData.defaultValueSrc = defaultValueSrc
1496
+ }
1497
+
1271
1498
  if (typeof varData.defaultValue === 'undefined') {
1272
1499
  varData.isRequired = true
1273
1500
  }
@@ -1369,50 +1596,58 @@ class Configorama {
1369
1596
  const instances = variableData[key]
1370
1597
  const firstInstance = instances[0]
1371
1598
 
1372
- // Check if truly required using same logic as display code
1373
- let isTrulyRequired = false
1374
- if (typeof firstInstance.defaultValue === 'undefined') {
1375
- // Check for self-references that resolve to config values
1376
- let dotPropArr = []
1377
- if (firstInstance.defaultValueIsVar && (
1378
- firstInstance.defaultValueIsVar.variableType === 'self:' ||
1379
- firstInstance.defaultValueIsVar.variableType === 'dot.prop'
1380
- )) {
1381
- dotPropArr = [firstInstance.defaultValueIsVar]
1382
- }
1383
-
1384
- const hasDotPropOrSelf = instances.reduce((acc, v) => {
1385
- // Only check the outermost variable (last in resolveDetails)
1386
- if (v.resolveDetails && v.resolveDetails.length > 0) {
1387
- const outermostDetail = v.resolveDetails[v.resolveDetails.length - 1]
1388
- if (outermostDetail.variableType === 'dot.prop' || outermostDetail.variableType === 'self') {
1389
- acc.push(outermostDetail)
1390
- }
1599
+ // Extract variable name from key (e.g. "${self:service}" -> "self:service")
1600
+ const keyVarName = key.slice(2, -1).split(',')[0].trim()
1601
+
1602
+ // Find the resolveDetail that matches THIS variable (not any self-ref in the string)
1603
+ let matchingDetail = null
1604
+ for (const instance of instances) {
1605
+ if (instance.resolveDetails && instance.resolveDetails.length > 0) {
1606
+ const found = instance.resolveDetails.find((detail) => {
1607
+ const detailVar = detail.valueBeforeFallback || detail.variable
1608
+ return detailVar === keyVarName
1609
+ })
1610
+ if (found && (found.variableType === 'dot.prop' || found.variableType === 'self')) {
1611
+ matchingDetail = found
1612
+ break
1391
1613
  }
1392
- return acc
1393
- }, dotPropArr)
1614
+ }
1615
+ }
1616
+
1617
+ // Also check defaultValueIsVar
1618
+ if (!matchingDetail && firstInstance.defaultValueIsVar && (
1619
+ firstInstance.defaultValueIsVar.variableType === 'self:' ||
1620
+ firstInstance.defaultValueIsVar.variableType === 'dot.prop'
1621
+ )) {
1622
+ matchingDetail = firstInstance.defaultValueIsVar
1623
+ }
1394
1624
 
1395
- if (!hasDotPropOrSelf.length) {
1625
+ // Check if truly required
1626
+ let isTrulyRequired = false
1627
+ if (matchingDetail) {
1628
+ // Check if the self-reference resolves to a value
1629
+ // Use valueBeforeFallback if present (strips inline fallback like ", false")
1630
+ const varPath = matchingDetail.valueBeforeFallback || matchingDetail.variable
1631
+ const cleanPath = varPath.replace('self:', '')
1632
+ const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1633
+ if (typeof dotPropValue === 'undefined') {
1396
1634
  isTrulyRequired = true
1397
1635
  } else {
1398
- // Check if the self-reference resolves to a value
1399
- const cleanPath = hasDotPropOrSelf[0].variable.replace('self:', '')
1400
- const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1401
- if (typeof dotPropValue === 'undefined') {
1402
- isTrulyRequired = true
1403
- } else {
1404
- // Enrich with default value from self-reference
1405
- firstInstance.defaultValueSrc = cleanPath
1406
- const niceString = typeof dotPropValue === 'object' ? JSON.stringify(dotPropValue) : dotPropValue
1407
- const truncatedString = niceString.length > 100 ? niceString.substring(0, 90) + '...' : niceString
1408
- firstInstance.defaultValue = truncatedString
1409
- firstInstance.isRequired = false
1410
- }
1636
+ // Enrich ALL instances with resolved self-reference value (overrides inline fallbacks)
1637
+ instances.forEach((instance) => {
1638
+ instance.defaultValueSrc = cleanPath
1639
+ instance.defaultValue = dotPropValue
1640
+ instance.isRequired = false
1641
+ })
1411
1642
  }
1643
+ } else if (typeof firstInstance.defaultValue === 'undefined') {
1644
+ isTrulyRequired = true
1412
1645
  }
1413
1646
 
1414
1647
  // Update isRequired based on computed isTrulyRequired
1415
- firstInstance.isRequired = isTrulyRequired
1648
+ instances.forEach((instance) => {
1649
+ instance.isRequired = isTrulyRequired
1650
+ })
1416
1651
 
1417
1652
  if (isTrulyRequired) {
1418
1653
  requiredCount++
@@ -1421,7 +1656,7 @@ class Configorama {
1421
1656
  }
1422
1657
  })
1423
1658
 
1424
- return {
1659
+ this._cachedMetadata = {
1425
1660
  variables: variableData,
1426
1661
  uniqueVariables: {},
1427
1662
  fileDependencies: {
@@ -1442,61 +1677,44 @@ class Configorama {
1442
1677
  variablesWithDefaults: withDefaultsCount
1443
1678
  },
1444
1679
  }
1680
+
1681
+ return this._cachedMetadata
1445
1682
  }
1446
- runFunction(variableString) {
1447
- // console.log('runFunction', variableString)
1448
- /* If json object value return it */
1449
- if (variableString.match(/^\s*{/) && variableString.match(/}\s*$/)) {
1450
- return variableString
1451
- }
1452
- // console.log('runFunction', variableString)
1453
- var hasFunc = funcRegex.exec(variableString)
1454
- // TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
1455
- // console.log('hasFunc', hasFunc)
1456
- if (!hasFunc || hasFunc && (hasFunc[1] === 'cron' || hasFunc[1] === 'eval')) {
1457
- return variableString
1458
- }
1459
- // test for object
1460
- const functionName = hasFunc[1]
1461
- const rawArgs = hasFunc[2]
1462
- // TODO @DWELLS. Loop through all raw args and parse to correct datatype
1463
- // argument is object
1464
- let argsToPass
1465
- if (rawArgs && rawArgs.match(/^{([^}]+)}$/)) {
1466
- // console.log('OBJECT', hasFunc[2])
1467
- // TODO use JSON5
1468
- argsToPass = [JSON.parse(rawArgs)]
1469
- } else {
1470
- // TODO fix how commas + spaces are ned
1471
- const splitter = splitCsv(rawArgs, ', ')
1472
- // console.log('splitter', splitter)
1473
- argsToPass = formatFunctionArgs(splitter)
1683
+ /**
1684
+ * Populate the variables in the given object.
1685
+ * @param objectToPopulate The object to populate variables within.
1686
+ * @returns {Promise.<TResult>|*} A promise resolving to the in-place populated object.
1687
+ */
1688
+ populateObject(objectToPopulate) {
1689
+ return this.initialCall(() => this.populateObjectImpl(objectToPopulate))
1690
+ }
1691
+ populateObjectImpl(objectToPopulate) {
1692
+ this.callCount = this.callCount + 1
1693
+
1694
+ if (DEBUG) {
1695
+ deepLog(`objectToPopulate called ${this.callCount} times`, objectToPopulate)
1696
+ // process.exit(0)
1474
1697
  }
1475
- // console.log('argsToPass runFunction', argsToPass)
1476
- // TODO check for camelCase version. | toUpperCase messes with function name
1477
- const theFunction = this.functions[functionName] || this.functions[functionName.toLowerCase()]
1478
1698
 
1479
- if (!theFunction) throw new Error(`Function "${functionName}" not found`)
1699
+ const leaves = this.getProperties(objectToPopulate, true, objectToPopulate)
1700
+ this.leaves = leaves
1701
+ // console.log('leaves', leaves)
1702
+ const populations = this.populateVariables(leaves)
1703
+ // console.log("FILL LEAVES", populations)
1480
1704
 
1481
- const funcValue = theFunction(...argsToPass)
1482
- // console.log('funcValue', funcValue)
1483
- // console.log('typeof funcValue', typeof funcValue)
1484
- let replaceVal = funcValue
1485
- if (typeof funcValue === 'string') {
1486
- const replaceIt = variableString.replace(hasFunc[0], funcValue)
1487
- replaceVal = cleanVariable(replaceIt, this.variableSyntax, true, `runFunction ${this.callCount}`)
1705
+ if (populations.length === 0) {
1706
+ if (DEBUG) console.log('Config Population Finished')
1707
+ return Promise.resolve(objectToPopulate)
1488
1708
  }
1489
1709
 
1490
- // If wrapped in outer function, recurse
1491
- const hasMoreFunctions = funcRegex.exec(replaceVal)
1492
- if (hasMoreFunctions) {
1493
- return this.runFunction(replaceVal)
1494
- }
1495
- return replaceVal
1710
+ return this.assignProperties(objectToPopulate, populations).then(() => {
1711
+ return this.populateObjectImpl(objectToPopulate)
1712
+ })
1496
1713
  }
1497
- // ############
1498
- // ## OBJECT ##
1499
- // ############
1714
+
1715
+ // #######################
1716
+ // ## PROPERTY HANDLING ##
1717
+ // #######################
1500
1718
  /**
1501
1719
  * The declaration of a terminal property. This declaration includes the path and value of the
1502
1720
  * property.
@@ -1644,40 +1862,9 @@ class Configorama {
1644
1862
  })
1645
1863
  })
1646
1864
  }
1647
- /**
1648
- * Populate the variables in the given object.
1649
- * @param objectToPopulate The object to populate variables within.
1650
- * @returns {Promise.<TResult>|*} A promise resolving to the in-place populated object.
1651
- */
1652
- populateObject(objectToPopulate) {
1653
- return this.initialCall(() => this.populateObjectImpl(objectToPopulate))
1654
- }
1655
- populateObjectImpl(objectToPopulate) {
1656
- this.callCount = this.callCount + 1
1657
-
1658
- if (DEBUG) {
1659
- deepLog(`objectToPopulate called ${this.callCount} times`, objectToPopulate)
1660
- // process.exit(0)
1661
- }
1662
-
1663
- const leaves = this.getProperties(objectToPopulate, true, objectToPopulate)
1664
- this.leaves = leaves
1665
- // console.log('leaves', leaves)
1666
- const populations = this.populateVariables(leaves)
1667
- // console.log("FILL LEAVES", populations)
1668
-
1669
- if (populations.length === 0) {
1670
- if (DEBUG) console.log('Config Population Finished')
1671
- return Promise.resolve(objectToPopulate)
1672
- }
1673
-
1674
- return this.assignProperties(objectToPopulate, populations).then(() => {
1675
- return this.populateObjectImpl(objectToPopulate)
1676
- })
1677
- }
1678
- // ##############
1679
- // ## PROPERTY ##
1680
- // ##############
1865
+ // ##################
1866
+ // ## MATCH/RENDER ##
1867
+ // ##################
1681
1868
  /**
1682
1869
  * @typedef {Object} MatchResult
1683
1870
  * @property {String} match The original property value that matched the variable syntax
@@ -1740,7 +1927,17 @@ class Configorama {
1740
1927
 
1741
1928
  let result = valueObject.value
1742
1929
  for (let i = 0; i < matches.length; i += 1) {
1743
- this.warnIfNotFound(matches[i].variable, results[i])
1930
+ warnIfNotFound(matches[i].variable, results[i], {
1931
+ patterns: {
1932
+ env: envRefSyntax,
1933
+ opt: optRefSyntax,
1934
+ self: selfRefSyntax,
1935
+ file: fileRefSyntax,
1936
+ deep: deepRefSyntax,
1937
+ text: textRefSyntax
1938
+ },
1939
+ debug: DEBUG
1940
+ })
1744
1941
 
1745
1942
  // Extract metadata from result if present
1746
1943
  let actualResult = results[i]
@@ -1884,6 +2081,10 @@ class Configorama {
1884
2081
 
1885
2082
  return result
1886
2083
  }
2084
+
2085
+ // ######################
2086
+ // ## VALUE RESOLUTION ##
2087
+ // ######################
1887
2088
  /**
1888
2089
  * Populate the given value, recursively if root is true
1889
2090
  * @param valueObject The value to populate variables within
@@ -2001,7 +2202,8 @@ class Configorama {
2001
2202
  const hasFilters = originalSrc.match(this.filterMatch)
2002
2203
  let foundFilters = []
2003
2204
  if (hasFilters) {
2004
- foundFilters = hasFilters[1]
2205
+ foundFilters = hasFilters[0]
2206
+ .replace(/}$/, '') // remove trailing }
2005
2207
  .split('|')
2006
2208
  .map((filter) => filter.trim())
2007
2209
  .filter(Boolean)
@@ -2156,7 +2358,7 @@ class Configorama {
2156
2358
 
2157
2359
  if (nestedVar) {
2158
2360
  const fallbackStr = getFallbackString(splitVars, nestedVar)
2159
- if (!this.opts.allowUnknownVars) {
2361
+ if (!this.opts.allowUnknownVariables) {
2160
2362
  verifyVariable(nestedVar, valueObject, this.variableTypes, this.config)
2161
2363
  }
2162
2364
 
@@ -2268,14 +2470,13 @@ Missing Value ${missingValue} - ${matchedString}
2268
2470
  if (typeof valueToPopulate === 'number' && foundFilters.length) {
2269
2471
  runFilters = true
2270
2472
  } else if (
2271
- typeof valueToPopulate === 'string' &&
2272
- !valueToPopulate.match(deepRefSyntax) &&
2273
- foundFilters.length &&
2473
+ typeof valueToPopulate === 'string' &&
2474
+ !valueToPopulate.match(deepRefSyntax) &&
2475
+ foundFilters.length &&
2274
2476
  !property.match(this.variableSyntax)
2275
2477
  ) {
2276
2478
  runFilters = true
2277
2479
  }
2278
-
2279
2480
  /* Apply filters if found */
2280
2481
  //console.log('> property', property)
2281
2482
  if (runFilters) {
@@ -2395,6 +2596,10 @@ Missing Value ${missingValue} - ${matchedString}
2395
2596
  : Promise.resolve(extractedValues.find(isValidValue)) // resolve first valid value, else undefined
2396
2597
  })
2397
2598
  }
2599
+
2600
+ // ####################
2601
+ // ## SOURCE GETTERS ##
2602
+ // ####################
2398
2603
  /**
2399
2604
  * Given any variable string, return the value it should be populated with.
2400
2605
  * @param variableString The variable string to retrieve a value for.
@@ -2598,6 +2803,11 @@ Missing Value ${missingValue} - ${matchedString}
2598
2803
  return Promise.resolve(encodeUnknown(propertyString))
2599
2804
  }
2600
2805
 
2806
+ if (this.opts.allowUnresolvedVariables) {
2807
+ // Encode unresolved variable to pass through resolution
2808
+ return Promise.resolve(encodeUnknown(propertyString))
2809
+ }
2810
+
2601
2811
  if (valueCount.length === 1 && noNestedVars) {
2602
2812
  const configFilePathMsg = (this.configFilePath) ? `\nIn file ${this.configFilePath} ` : ''
2603
2813
  const fromLine = (propertyString !== valueObject.originalSource) ? `\n From "${valueObject.originalSource}"\n` : ''
@@ -2760,7 +2970,7 @@ Missing Value ${missingValue} - ${matchedString}
2760
2970
  // console.log('nestedVar', nestedVar)
2761
2971
 
2762
2972
  if (nestedVar) {
2763
- if (!this.opts.allowUnknownVars) {
2973
+ if (!this.opts.allowUnknownVariables) {
2764
2974
  verifyVariable(nestedVar, valueObject, this.variableTypes, this.config)
2765
2975
  }
2766
2976
  const fallbackStr = getFallbackString(split, nestedVar)
@@ -2863,7 +3073,7 @@ Missing Value ${missingValue} - ${matchedString}
2863
3073
 
2864
3074
 
2865
3075
  /* Pass through unknown variables */
2866
- if (this.opts.allowUnknownVars || allowSpecialCase) {
3076
+ if (this.opts.allowUnknownVariables || allowSpecialCase) {
2867
3077
  // console.log('allowUnknownVars propertyString', propertyString)
2868
3078
  const varMatches = propertyString.match(this.variableSyntax)
2869
3079
  let allowUnknownVars = propertyString
@@ -2915,331 +3125,20 @@ Missing Value ${missingValue} - ${matchedString}
2915
3125
  })
2916
3126
  }
2917
3127
  async getValueFromFile(variableString, options) {
2918
- const opts = options || {}
2919
- const syntax = opts.asRawText ? textRefSyntax : fileRefSyntax
2920
- // console.log('From file', `"${variableString}"`)
2921
- let matchedFileString = variableString.match(syntax)[0]
2922
- // console.log('matchedFileString', matchedFileString)
2923
-
2924
- // Get function input params if any supplied https://regex101.com/r/qlNFVm/1
2925
- // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2926
- var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
2927
- // tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
2928
- var hasParams = funcParamsRegex.exec(matchedFileString)
2929
-
2930
- let argsToPass = []
2931
- if (hasParams) {
2932
- const splitter = splitCsv(hasParams[2])
2933
- const argsFound = splitter.map((arg) => {
2934
- const cleanArg = trim(arg).replace(/^'|"/, '').replace(/'|"$/, '')
2935
- return cleanArg
2936
- })
2937
- // console.log('argsFound', argsFound)
2938
-
2939
- // If function has more arguments than file path
2940
- if (argsFound.length && argsFound.length > 1) {
2941
- matchedFileString = argsFound[0]
2942
- argsToPass = argsFound.filter((arg, i) => {
2943
- return i !== 0
2944
- })
2945
- }
2946
- }
2947
- // console.log('argsToPass', argsToPass)
2948
-
2949
- const fileDetails = resolveFilePathFromMatch(matchedFileString, syntax, this.configPath)
2950
- // console.log('fileDetails', fileDetails)
2951
-
2952
- const { fullFilePath, resolvedPath, relativePath } = fileDetails
2953
-
2954
- const exists = fs.existsSync(fullFilePath)
2955
-
2956
- this.fileRefsFound.push({
2957
- // location: options.context.path.join('.'),
2958
- filePath: fullFilePath,
2959
- relativePath,
2960
- resolvedVariableString: options.context.value,
2961
- originalVariableString: options.context.originalSource,
2962
- containsVariables: options.context.value !== options.context.originalSource,
2963
- exists,
2964
- })
2965
-
2966
- let fileExtension = resolvedPath.split('.')
2967
-
2968
- fileExtension = fileExtension[fileExtension.length - 1]
2969
-
2970
- // Validate file exists
2971
- if (!exists) {
2972
- const originalVar = options.context && options.context.originalSource
2973
-
2974
- const findNestedResult = findNestedVariables(
2975
- originalVar,
2976
- this.variableSyntax,
2977
- this.variablesKnownTypes,
2978
- options.context.path,
2979
- this.variableTypes
2980
- )
2981
- // console.log('findNestedResult', findNestedResult)
2982
- let hasFallback = false
2983
- if (findNestedResult) {
2984
- const varDetails = findNestedResult[0]
2985
- // console.log('varDetails', varDetails)
2986
- hasFallback = varDetails.hasFallback
2987
- }
2988
-
2989
- // check if original var has fallback value
2990
- // console.log('NO FILE FOUND', fullFilePath)
2991
- // console.log('variableString', variableString)
2992
-
2993
- if (!hasFallback && !this.opts.allowUnknownFileRefs) {
2994
- const errorMsg = makeBox({
2995
- title: `File Not Found in ${originalVar}`,
2996
- minWidth: '100%',
2997
- text: `Variable ${variableString} cannot resolve due to missing file.
2998
-
2999
- File not found ${fullFilePath}
3000
-
3001
- Default fallback value will be used if provided.
3002
-
3003
- ${JSON.stringify(options.context, null, 2)}`,
3004
- })
3005
- console.log(errorMsg)
3006
- }
3007
- // TODO maybe reject. YAML does not allow for null/undefined values
3008
- // return Promise.reject(new Error(errorMsg))
3009
- return Promise.resolve(undefined)
3010
- }
3011
-
3012
-
3013
-
3014
- let valueToPopulate
3015
-
3016
- const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
3017
-
3018
- /* handle case for referencing raw JS files to inline them */
3019
- if (argsToPass.length
3020
- && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw')
3021
- || opts.asRawText
3022
- ) {
3023
- // Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
3024
- valueToPopulate = encodeJsSyntax(variableFileContents)
3025
- return Promise.resolve(valueToPopulate)
3026
- }
3027
-
3028
- // Process JS files
3029
- if (fileExtension === 'js' || fileExtension === 'cjs') {
3030
- // Possible alt importer tool https://github.com/humanwhocodes/module-importer
3031
- const jsFile = require(fullFilePath)
3032
- let returnValueFunction = jsFile
3033
- // TODO change how exported functions are referenced
3034
- const variableArray = variableString.split(':')
3035
-
3036
- if (variableArray[1]) {
3037
- let jsModule = variableArray[1]
3038
- jsModule = jsModule.split('.')[0]
3039
- returnValueFunction = jsFile[jsModule]
3040
- }
3041
-
3042
- if (typeof returnValueFunction !== 'function') {
3043
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3044
- Check if your javascript is exporting a function that returns a value.`
3045
- return Promise.reject(new Error(errorMessage))
3046
- }
3047
- // TODO update what is passed into function
3048
-
3049
- const valueForFunction = {
3050
- originalConfig: this.originalConfig,
3051
- config: this.config,
3052
- opts: this.opts,
3053
- }
3054
-
3055
- valueToPopulate = returnValueFunction.call(jsFile, valueForFunction, ...argsToPass)
3056
-
3057
- return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
3058
- let deepProperties = variableString.replace(matchedFileString, '')
3059
- deepProperties = deepProperties.slice(1).split('.')
3060
- deepProperties.splice(0, 1)
3061
- // Trim prop keys for starting/trailing spaces
3062
- deepProperties = deepProperties.map((prop) => {
3063
- return trim(prop)
3064
- })
3065
- return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
3066
- if (typeof deepValueToPopulateResolved === 'undefined') {
3067
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3068
- Check if your javascript is returning the correct data.`
3069
- return Promise.reject(new Error(errorMessage))
3070
- }
3071
- return Promise.resolve(deepValueToPopulateResolved)
3072
- })
3073
- })
3074
- }
3075
-
3076
- if (fileExtension === 'ts') {
3077
- const { executeTypeScriptFile } = require('./parsers/typescript')
3078
- let returnValueFunction
3079
- const variableArray = variableString.split(':')
3080
-
3081
- try {
3082
- const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
3083
- // console.log('fullFilePath', fullFilePath)
3084
- // console.log('tsFile', tsFile)
3085
- returnValueFunction = tsFile.config || tsFile.default || tsFile
3086
-
3087
- if (variableArray[1]) {
3088
- let tsModule = variableArray[1]
3089
- tsModule = tsModule.split('.')[0]
3090
- returnValueFunction = tsFile[tsModule]
3091
- }
3092
-
3093
- if (typeof returnValueFunction !== 'function') {
3094
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3095
- Check if your TypeScript is exporting a function that returns a value.`
3096
- return Promise.reject(new Error(errorMessage))
3097
- }
3098
-
3099
- const valueForFunction = {
3100
- originalConfig: this.originalConfig,
3101
- config: this.config,
3102
- opts: this.opts,
3103
- }
3104
-
3105
- valueToPopulate = returnValueFunction.call(tsFile, valueForFunction, ...argsToPass)
3106
-
3107
- return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
3108
- let deepProperties = variableString.replace(matchedFileString, '')
3109
- deepProperties = deepProperties.slice(1).split('.')
3110
- deepProperties.splice(0, 1)
3111
- // Trim prop keys for starting/trailing spaces
3112
- deepProperties = deepProperties.map((prop) => {
3113
- return trim(prop)
3114
- })
3115
- return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
3116
- if (typeof deepValueToPopulateResolved === 'undefined') {
3117
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3118
- Check if your TypeScript is returning the correct data.`
3119
- return Promise.reject(new Error(errorMessage))
3120
- }
3121
- return Promise.resolve(deepValueToPopulateResolved)
3122
- })
3123
- })
3124
- } catch (err) {
3125
- return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
3126
- }
3127
- }
3128
-
3129
- if (fileExtension === 'mjs' || fileExtension === 'esm') {
3130
- // Possible alt importer tool https://github.com/humanwhocodes/module-importer
3131
- const { executeESMFile } = require('./parsers/esm')
3132
- let returnValueFunction
3133
- const variableArray = variableString.split(':')
3134
-
3135
- try {
3136
- const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
3137
- // console.log('ESM fullFilePath', fullFilePath)
3138
- // console.log('ESM esmFile', esmFile, 'type:', typeof esmFile)
3139
- returnValueFunction = esmFile.config || esmFile.default || esmFile
3140
-
3141
- if (variableArray[1]) {
3142
- let esmModule = variableArray[1]
3143
- esmModule = esmModule.split('.')[0]
3144
- returnValueFunction = esmFile[esmModule]
3145
- }
3146
-
3147
- if (typeof returnValueFunction !== 'function') {
3148
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3149
- Check if your ESM is exporting a function that returns a value.`
3150
- return Promise.reject(new Error(errorMessage))
3151
- }
3152
-
3153
- const valueForFunction = {
3154
- originalConfig: this.originalConfig,
3155
- config: this.config,
3156
- opts: this.opts,
3157
- }
3158
-
3159
- valueToPopulate = returnValueFunction.call(esmFile, valueForFunction, ...argsToPass)
3160
-
3161
- return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
3162
- let deepProperties = variableString.replace(matchedFileString, '')
3163
- deepProperties = deepProperties.slice(1).split('.')
3164
- deepProperties.splice(0, 1)
3165
- // Trim prop keys for starting/trailing spaces
3166
- deepProperties = deepProperties.map((prop) => {
3167
- return trim(prop)
3168
- })
3169
- return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
3170
- if (typeof deepValueToPopulateResolved === 'undefined') {
3171
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3172
- Check if your ESM is returning the correct data.`
3173
- return Promise.reject(new Error(errorMessage))
3174
- }
3175
- return Promise.resolve(deepValueToPopulateResolved)
3176
- })
3177
- })
3178
- } catch (err) {
3179
- return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
3180
- }
3181
- }
3182
-
3183
- // Process everything except JS, TS, and ESM
3184
- if (fileExtension !== 'js' && fileExtension !== 'ts' && fileExtension !== 'mjs' && fileExtension !== 'esm') {
3185
- /* Read initial file */
3186
- valueToPopulate = variableFileContents
3187
-
3188
- // File reference has :subKey lookup. Must dig deeper
3189
- if (matchedFileString !== variableString) {
3190
- if (fileExtension === 'yml' || fileExtension === 'yaml') {
3191
- valueToPopulate = JSON.stringify(YAML.parse(valueToPopulate))
3192
- }
3193
- if (fileExtension === 'toml') {
3194
- valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
3195
- }
3196
- if (fileExtension === 'ini') {
3197
- valueToPopulate = INI.toJson(valueToPopulate)
3198
- }
3199
- // console.log('deep', variableString)
3200
- // console.log('matchedFileString', matchedFileString)
3201
- let deepProperties = variableString.replace(matchedFileString, '')
3202
- // TODO 2025-11-12 add file.path.support instead of just :
3203
- if (deepProperties.substring(0, 1) !== ':') {
3204
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
3205
- Please use ":" to reference sub properties. ${deepProperties}`
3206
- return Promise.reject(new Error(errorMessage))
3207
- }
3208
- deepProperties = deepProperties.slice(1).split('.')
3209
- return this.getDeeperValue(deepProperties, valueToPopulate)
3210
- }
3211
-
3212
- if (fileExtension === 'yml' || fileExtension === 'yaml') {
3213
- valueToPopulate = YAML.parse(valueToPopulate)
3214
- return Promise.resolve(valueToPopulate)
3215
- }
3216
-
3217
- if (fileExtension === 'toml') {
3218
- valueToPopulate = TOML.parse(valueToPopulate)
3219
- return Promise.resolve(valueToPopulate)
3220
- }
3221
-
3222
- if (fileExtension === 'ini') {
3223
- valueToPopulate = INI.parse(valueToPopulate)
3224
- return Promise.resolve(valueToPopulate)
3225
- }
3226
-
3227
- if (fileExtension === 'json') {
3228
- valueToPopulate = JSON.parse(valueToPopulate)
3229
- return Promise.resolve(valueToPopulate)
3230
- }
3231
- }
3232
- // console.log('fall thru', valueToPopulate)
3233
- return Promise.resolve(valueToPopulate)
3234
- }
3235
- getVariableFromDeep(variableString) {
3236
- const index = variableString.replace(deepIndexReplacePattern, '')
3237
- // const index = this.getDeepIndex(variableString)
3238
- /*
3239
- console.log('FIND INDEX', index)
3240
- console.log(this.deep, this.deep[index])
3241
- /** */
3242
- return this.deep[index]
3128
+ const ctx = {
3129
+ configPath: this.configPath,
3130
+ fileRefsFound: this.fileRefsFound,
3131
+ variableSyntax: this.variableSyntax,
3132
+ variablesKnownTypes: this.variablesKnownTypes,
3133
+ variableTypes: this.variableTypes,
3134
+ opts: this.opts,
3135
+ originalConfig: this.originalConfig,
3136
+ config: this.config,
3137
+ getDeeperValue: this.getDeeperValue.bind(this),
3138
+ fileRefSyntax: fileRefSyntax,
3139
+ textRefSyntax: textRefSyntax
3140
+ }
3141
+ return getValueFromFileResolver(ctx, variableString, options)
3243
3142
  }
3244
3143
  getValueFromDeep(variableString, pathValue) {
3245
3144
  const variable = this.getVariableFromDeep(variableString)
@@ -3271,6 +3170,19 @@ Please use ":" to reference sub properties. ${deepProperties}`
3271
3170
  }
3272
3171
  return ret
3273
3172
  }
3173
+
3174
+ // ############################
3175
+ // ## DEEP VARIABLE HANDLING ##
3176
+ // ############################
3177
+ getVariableFromDeep(variableString) {
3178
+ const index = variableString.replace(deepIndexReplacePattern, '')
3179
+ // const index = this.getDeepIndex(variableString)
3180
+ /*
3181
+ console.log('FIND INDEX', index)
3182
+ console.log(this.deep, this.deep[index])
3183
+ /** */
3184
+ return this.deep[index]
3185
+ }
3274
3186
  makeDeepVariable(variable, caller) {
3275
3187
  // variable = variable.replace("dev", '"dev"')
3276
3188
  let index = this.deep.findIndex((item) => variable === item)
@@ -3295,8 +3207,6 @@ Please use ":" to reference sub properties. ${deepProperties}`
3295
3207
  console.log('deepVar', deepVar)
3296
3208
  // process.exit(1)
3297
3209
  /** */
3298
- // TODO debugging space removal. Seems like this helps
3299
- // const deepVar = variableContainer.replace(/\s/g, '').replace(variableString, `deep:${index}`)
3300
3210
  return deepVar
3301
3211
  }
3302
3212
  /**
@@ -3367,66 +3277,68 @@ Please use ":" to reference sub properties. ${deepProperties}`
3367
3277
  return veryDeep
3368
3278
  }
3369
3279
 
3370
- warnIfNotFound(variableString, valueToPopulate) {
3371
- let variableTypeText
3372
- if (variableString.match(envRefSyntax)) {
3373
- variableTypeText = 'environment variable'
3374
- } else if (variableString.match(optRefSyntax)) {
3375
- variableTypeText = 'option'
3376
- } else if (variableString.match(selfRefSyntax)) {
3377
- variableTypeText = 'config attribute'
3378
- } else if (variableString.match(fileRefSyntax)) {
3379
- variableTypeText = 'file'
3380
- } else if (variableString.match(deepRefSyntax)) {
3381
- variableTypeText = 'deep'
3382
- } else if (variableString.match(textRefSyntax)) {
3383
- variableTypeText = 'text'
3280
+ // ###############
3281
+ // ## UTILITIES ##
3282
+ // ###############
3283
+ initialCall(func) {
3284
+ this.deep = []
3285
+ this.tracker.start()
3286
+ return func().finally(() => {
3287
+ this.tracker.stop()
3288
+ this.deep = []
3289
+ })
3290
+ }
3291
+ runFunction(variableString) {
3292
+ // console.log('runFunction', variableString)
3293
+ /* If json object value return it */
3294
+ if (variableString.match(/^\s*{/) && variableString.match(/}\s*$/)) {
3295
+ return variableString
3384
3296
  }
3385
- if (!isValidValue(valueToPopulate)) {
3386
- // console.log("MISSING", variableString)
3387
- // console.log(this.deep)
3388
- // console.log(valueToPopulate)
3389
- const notFoundMsg = `No ${variableTypeText} found to satisfy the '\${${variableString}}' variable. Attempting fallback value`
3390
- if (DEBUG) {
3391
- console.log(notFoundMsg)
3392
- }
3393
- // errors make fallbacks not function. throw new Error(errorMsg)
3297
+ // console.log('runFunction', variableString)
3298
+ var hasFunc = funcRegex.exec(variableString)
3299
+ // TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
3300
+ // console.log('hasFunc', hasFunc)
3301
+ if (!hasFunc || hasFunc && (hasFunc[1] === 'cron' || hasFunc[1] === 'eval')) {
3302
+ return variableString
3394
3303
  }
3395
- return valueToPopulate
3396
- }
3397
- }
3398
-
3399
- function ensureQuote(value, open = '"', close) {
3400
- let i = -1
3401
- const result = []
3402
- const end = close || open
3403
- if (typeof value === 'string') {
3404
- return startChar(value, open) + value + endChar(value, end)
3405
- }
3406
- while (++i < value.length) {
3407
- result[i] = startChar(value[i], open) + value[i] + endChar(value[i], end)
3408
- }
3409
- return result
3410
- }
3411
-
3412
- function startChar(str, char) {
3413
- return (str[0] === char) ? '' : char
3414
- }
3304
+ // test for object
3305
+ const functionName = hasFunc[1]
3306
+ const rawArgs = hasFunc[2]
3307
+ // TODO @DWELLS. Loop through all raw args and parse to correct datatype
3308
+ // argument is object
3309
+ let argsToPass
3310
+ if (rawArgs && rawArgs.match(/^{([^}]+)}$/)) {
3311
+ // console.log('OBJECT', hasFunc[2])
3312
+ // TODO use JSON5
3313
+ argsToPass = [JSON.parse(rawArgs)]
3314
+ } else {
3315
+ // TODO fix how commas + spaces are ned
3316
+ const splitter = splitCsv(rawArgs, ', ')
3317
+ // console.log('splitter', splitter)
3318
+ argsToPass = formatFunctionArgs(splitter)
3319
+ }
3320
+ // console.log('argsToPass runFunction', argsToPass)
3321
+ // TODO check for camelCase version. | toUpperCase messes with function name
3322
+ const theFunction = this.functions[functionName] || this.functions[functionName.toLowerCase()]
3415
3323
 
3416
- function endChar(str, char) {
3417
- return (str[str.length -1] === char) ? '' : char
3418
- }
3324
+ if (!theFunction) throw new Error(`Function "${functionName}" not found`)
3419
3325
 
3420
- function isSurroundedByQuotes(str) {
3421
- if (!str || str.length < 2) return false
3422
- const firstChar = str[0]
3423
- const lastChar = str[str.length - 1]
3424
- return (firstChar === "'" && lastChar === "'") || (firstChar === '"' && lastChar === '"')
3425
- }
3326
+ const funcValue = theFunction(...argsToPass)
3327
+ // console.log('funcValue', funcValue)
3328
+ // console.log('typeof funcValue', typeof funcValue)
3329
+ let replaceVal = funcValue
3330
+ if (typeof funcValue === 'string') {
3331
+ const replaceIt = variableString.replace(hasFunc[0], funcValue)
3332
+ replaceVal = cleanVariable(replaceIt, this.variableSyntax, true, `runFunction ${this.callCount}`)
3333
+ }
3426
3334
 
3427
- function startsWithQuotedPipe(str) {
3428
- // Matches either 'xyz' | or "xyz" |
3429
- return /^(['"])(.*?)\1\s*\|/.test(str)
3335
+ // If wrapped in outer function, recurse
3336
+ const hasMoreFunctions = funcRegex.exec(replaceVal)
3337
+ if (hasMoreFunctions) {
3338
+ return this.runFunction(replaceVal)
3339
+ }
3340
+ return replaceVal
3341
+ }
3430
3342
  }
3431
3343
 
3432
3344
  module.exports = Configorama