configorama 0.6.11 → 0.6.13

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 +775 -857
  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 +394 -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} +244 -94
  17. package/src/utils/{parse.js → parsing/parse.js} +13 -13
  18. package/src/utils/parsing/preProcess.js +165 -0
  19. package/src/utils/paths/filePathUtils.js +136 -0
  20. package/src/utils/paths/filePathUtils.test.js +214 -0
  21. package/src/utils/paths/findLineForKey.js +47 -0
  22. package/src/utils/paths/findLineForKey.test.js +126 -0
  23. package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +22 -26
  24. package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
  25. package/src/utils/regex/index.js +23 -1
  26. package/src/utils/resolution/preResolveVariable.js +260 -0
  27. package/src/utils/resolution/preResolveVariable.test.js +98 -0
  28. package/src/utils/strings/bracketMatcher.js +86 -0
  29. package/src/utils/strings/bracketMatcher.test.js +135 -0
  30. package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
  31. package/src/utils/strings/formatFunctionArgs.test.js +77 -0
  32. package/src/utils/strings/quoteUtils.js +89 -0
  33. package/src/utils/strings/quoteUtils.test.js +217 -0
  34. package/src/utils/strings/replaceAll.test.js +82 -0
  35. package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
  36. package/src/utils/strings/splitCsv.js +38 -0
  37. package/src/utils/strings/splitCsv.test.js +96 -0
  38. package/src/utils/strings/textUtils.test.js +86 -0
  39. package/src/utils/{configWizard.js → ui/configWizard.js} +212 -60
  40. package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
  41. package/src/utils/{logs.js → ui/logs.js} +3 -3
  42. package/src/utils/validation/isValidValue.test.js +64 -0
  43. package/src/utils/validation/warnIfNotFound.js +52 -0
  44. package/src/utils/variables/appendDeepVariable.test.js +41 -0
  45. package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
  46. package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
  47. package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
  48. package/src/utils/variables/getVariableType.test.js +109 -0
  49. package/src/utils/variables/variableUtils.test.js +117 -0
  50. package/src/utils/isValidValue.js +0 -8
  51. package/src/utils/splitCsv.js +0 -29
  52. package/src/utils/trimSurroundingQuotes.js +0 -5
  53. /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
  54. /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
  55. /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.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,23 +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
+
5
6
  /* // disable logs to find broken tests
6
7
  console.log = () => {}
7
8
  // process.exit(1)
8
9
  /** */
9
10
 
11
+ /* External dependencies */
10
12
  const promiseFinallyShim = require('promise.prototype.finally').shim()
11
- // @TODO only import lodash we need
12
-
13
13
  const findUp = require('find-up')
14
14
  const traverse = require('traverse')
15
15
  const dotProp = require('dot-prop')
16
- const chalk = require('./utils/chalk')
17
- const { resolveAlias } = require('./utils/resolveAlias')
18
- 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')
19
26
 
20
- /* Default Value resolvers */
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')
31
+
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 */
21
74
  const getValueFromString = require('./resolvers/valueFromString')
22
75
  const getValueFromNumber = require('./resolvers/valueFromNumber')
23
76
  const getValueFromEnv = require('./resolvers/valueFromEnv')
@@ -25,44 +78,16 @@ const getValueFromOptions = require('./resolvers/valueFromOptions')
25
78
  const getValueFromCron = require('./resolvers/valueFromCron')
26
79
  const getValueFromEval = require('./resolvers/valueFromEval')
27
80
  const createGitResolver = require('./resolvers/valueFromGit')
28
- /* Default File Parsers */
81
+ const { getValueFromFile: getValueFromFileResolver } = require('./resolvers/valueFromFile')
82
+
83
+ /* Parsers */
29
84
  const YAML = require('./parsers/yaml')
30
85
  const TOML = require('./parsers/toml')
31
86
  const INI = require('./parsers/ini')
32
87
  const JSON5 = require('./parsers/json5')
33
- /* functions */
34
- const md5Function = require('./functions/md5')
35
88
 
36
- /* Utility/helpers */
37
- const cleanVariable = require('./utils/cleanVariable')
38
- const appendDeepVariable = require('./utils/appendDeepVariable')
39
- const isValidValue = require('./utils/isValidValue')
40
- const PromiseTracker = require('./utils/PromiseTracker')
41
- const handleSignalEvents = require('./utils/handleSignalEvents')
42
- const formatFunctionArgs = require('./utils/formatFunctionArgs')
43
- const trimSurroundingQuotes = require('./utils/trimSurroundingQuotes')
44
- const deepLog = require('./utils/deep-log')
45
- const { splitByComma } = require('./utils/splitByComma')
46
- const {
47
- isArray, isString, isNumber, isObject, isDate, isRegExp, isFunction,
48
- isEmpty, trim, camelCase, kebabCase, capitalize, split, map, mapValues,
49
- assign, set, cloneDeep
50
- } = require('./utils/lodash')
51
- const { parseFileContents } = require('./utils/parse')
52
- const { splitCsv } = require('./utils/splitCsv')
53
- const { replaceAll } = require('./utils/replaceAll')
54
- const { getTextAfterOccurrence, findNestedVariable } = require('./utils/textUtils')
55
- const { getFallbackString, verifyVariable } = require('./utils/variableUtils')
56
- const { encodeUnknown, decodeUnknown } = require('./utils/encoders/unknown-values')
57
- const { decodeEncodedValue } = require('./utils/encoders')
58
- const { encodeJsSyntax, decodeJsSyntax, hasParenthesesPlaceholder } = require('./utils/encoders/js-fixes')
59
- const { mergeByKeys } = require('./utils/mergeByKeys')
60
- const { arrayToJsonPath } = require('./utils/arrayToJsonPath')
61
- const { findNestedVariables } = require('./utils/find-nested-variables')
62
- const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
63
- const { logHeader } = require('./utils/logs')
64
- const { createEditorLink } = require('./utils/createEditorLink')
65
- const { runConfigWizard } = require('./utils/configWizard')
89
+ /* Functions */
90
+ const md5Function = require('./functions/md5')
66
91
  /**
67
92
  * Maintainer's notes:
68
93
  *
@@ -88,9 +113,6 @@ const textRefSyntax = RegExp(/^text\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" ]+?)\)/g)
88
113
  const envRefSyntax = RegExp(/^env:/g)
89
114
  const optRefSyntax = RegExp(/^opt:/g)
90
115
  const selfRefSyntax = RegExp(/^self:/g)
91
- const funcRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
92
- const funcStartOfLineRegex = /^(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
93
- const subFunctionRegex = /(\w+):(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/
94
116
  const base64WrapperRegex = /\[_\[([A-Za-z0-9+/=\s]*)\]_\]/g
95
117
  const logLines = '─────────────────────────────────────────────────'
96
118
 
@@ -101,147 +123,6 @@ let SETUP_MODE = process.argv.includes('--setup') ? true : false
101
123
  let DEBUG_TYPE = false
102
124
  const ENABLE_FUNCTIONS = true
103
125
 
104
- function combineRegexes(regexes) {
105
- // Extract the pattern from each RegExp and join with OR operator
106
- const patterns = regexes.map(regex => {
107
- // Get source pattern string without flags
108
- return regex.source
109
- }).filter(Boolean)
110
- // Join patterns with the OR operator and create new RegExp
111
- return new RegExp(`(${patterns.join('|')})`)
112
- }
113
-
114
- /**
115
- * Preprocess config to fix malformed fallback references
116
- * @param {Object} configObject - The parsed configuration object
117
- * @param {RegExp} variableSyntax - The variable syntax regex to use
118
- * @returns {Object} The preprocessed configuration object
119
- */
120
- function preProcess(configObject, variableSyntax) {
121
- // Known reference prefixes that should be wrapped in ${}
122
- const refPrefixes = ['self:', 'opt:', 'env:', 'file:', 'text:', 'deep:']
123
-
124
- /**
125
- * Fix malformed fallback references in a string
126
- * @param {string} str - String potentially containing variables
127
- * @returns {string} String with fixed fallback references
128
- */
129
- function fixFallbacksInString(str) {
130
- if (typeof str !== 'string') return str
131
-
132
- let result = str
133
- // result = result.replace(/\$\{self:/g, '${')
134
- let changed = true
135
-
136
- // Keep iterating until no more changes (to handle nested variables)
137
- while (changed) {
138
- changed = false
139
-
140
- // Find innermost ${...} blocks (ones that don't contain other ${)
141
- let i = 0
142
- while (i < result.length) {
143
- if (result[i] === '$' && result[i + 1] === '{') {
144
- const start = i
145
- let braceCount = 1
146
- let j = i + 2
147
-
148
- // Find the matching closing brace by counting { and }
149
- while (j < result.length && braceCount > 0) {
150
- if (result[j] === '{') {
151
- braceCount++
152
- } else if (result[j] === '}') {
153
- braceCount--
154
- }
155
- j++
156
- }
157
-
158
- if (braceCount === 0) {
159
- const end = j
160
- const match = result.substring(start, end)
161
- const content = result.substring(start + 2, end - 1)
162
-
163
- // Only process if there's a comma (indicating fallback syntax)
164
- if (content.includes(',')) {
165
- // Split by comma
166
- const parts = splitByComma(content, variableSyntax)
167
-
168
- if (parts.length > 1) {
169
- // Check if the first part has nested ${} - if so, skip this (process inner ones first)
170
- const firstPart = parts[0]
171
- if (firstPart.includes('${')) {
172
- i = start + 2 // Move past ${ to find inner variables
173
- continue
174
- }
175
-
176
- // Check each part after the first (these are fallback values)
177
- const fixed = parts.map((part, index) => {
178
- if (index === 0) {
179
- return part // Keep the main reference as-is
180
- }
181
-
182
- const trimmed = part.trim()
183
-
184
- // Check if this looks like a reference but is not wrapped
185
- const looksLikeRef = refPrefixes.some(prefix => trimmed.startsWith(prefix))
186
- const alreadyWrapped = trimmed.startsWith('${') && trimmed.endsWith('}')
187
-
188
- if (looksLikeRef && !alreadyWrapped) {
189
- return ` \${${trimmed}}`
190
- }
191
-
192
- return ` ${trimmed}`
193
- })
194
-
195
- const replacement = `\${${fixed.join(',')}}`
196
- if (replacement !== match) {
197
- result = result.substring(0, start) + replacement + result.substring(end)
198
- changed = true
199
- break // Restart search from beginning
200
- }
201
- }
202
- }
203
-
204
- i = start + 2 // Move past ${ to continue searching for nested variables
205
- } else {
206
- i++
207
- }
208
- } else {
209
- i++
210
- }
211
- }
212
- }
213
-
214
- return result
215
- }
216
-
217
- /**
218
- * Recursively traverse and fix the config object
219
- */
220
- function traverseAndFix(obj) {
221
- if (typeof obj === 'string') {
222
- return fixFallbacksInString(obj)
223
- }
224
-
225
- if (Array.isArray(obj)) {
226
- return obj.map(item => traverseAndFix(item))
227
- }
228
-
229
- if (obj !== null && typeof obj === 'object') {
230
- const result = {}
231
- for (const key in obj) {
232
- if (obj.hasOwnProperty(key)) {
233
- result[key] = traverseAndFix(obj[key])
234
- }
235
- }
236
- return result
237
- }
238
-
239
- return obj
240
- }
241
-
242
- return traverseAndFix(configObject)
243
- }
244
-
245
126
  class Configorama {
246
127
  constructor(fileOrObject, opts) {
247
128
  /* attach sig events on async calls */
@@ -253,17 +134,24 @@ class Configorama {
253
134
  // Set opts to pass into JS file calls
254
135
  this.opts = Object.assign({}, {
255
136
  // Allow for unknown variable syntax to pass through without throwing errors
256
- allowUnknownVars: false,
137
+ allowUnknownVariables: false,
257
138
  // Allow undefined to be an end result.
258
139
  allowUndefinedValues: false,
259
140
  // Allow unknown file refs to pass through without throwing errors
260
141
  allowUnknownFileRefs: false,
142
+ // Allow known variable types that can't be resolved to pass through
143
+ allowUnresolvedVariables: false,
261
144
  // Return metadata
262
145
  returnMetadata: false,
263
146
  // Return preResolvedVariableDetails
264
147
  returnPreResolvedVariableDetails: false,
265
148
  }, options)
266
149
 
150
+ // Backward compat: allowUnknownVars -> allowUnknownVariables
151
+ if (options.allowUnknownVars !== undefined && options.allowUnknownVariables === undefined) {
152
+ this.opts.allowUnknownVariables = options.allowUnknownVars
153
+ }
154
+
267
155
  this.filterCache = {}
268
156
 
269
157
  this.foundVariables = []
@@ -357,6 +245,7 @@ class Configorama {
357
245
  */
358
246
  {
359
247
  type: 'self',
248
+ source: 'config',
360
249
  prefix: 'self',
361
250
  syntax: '${self:pathToKeyInConfig}',
362
251
  description: `Resolves values from the current config object. Supports sub-properties via :key lookup.`,
@@ -373,9 +262,10 @@ class Configorama {
373
262
  */
374
263
  {
375
264
  type: 'file',
265
+ source: 'config',
376
266
  prefix: 'file',
377
267
  syntax: '${file(pathToFile.json)}',
378
- 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.`,
379
269
  match: fileRefSyntax,
380
270
  resolver: (varString, o, x, pathValue) => {
381
271
  return this.getValueFromFile(varString, { context: pathValue })
@@ -385,6 +275,7 @@ class Configorama {
385
275
 
386
276
  {
387
277
  type: 'text',
278
+ source: 'config',
388
279
  prefix: 'text',
389
280
  match: textRefSyntax,
390
281
  resolver: (varString, o, x, pathValue) => {
@@ -420,6 +311,7 @@ class Configorama {
420
311
  /* Nicer self: references. Match key in object */
421
312
  const fallThroughSelfMatcher = {
422
313
  type: 'dot.prop',
314
+ source: 'config',
423
315
  match: (varString, fullObject, valueObject) => {
424
316
  /*
425
317
  console.log('fallThroughSelfMatcher varString', varString)
@@ -541,8 +433,8 @@ class Configorama {
541
433
  Boolean: (value) => {
542
434
  if (typeof value === 'boolean') return value
543
435
  const v = String(value).toLowerCase()
544
- if (['true', '1', 'yes', 'on'].includes(v)) return true
545
- 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
546
438
  throw new Error(`Configorama Error: Expected Boolean, got "${value}"`)
547
439
  },
548
440
  String: (value) => {
@@ -572,7 +464,9 @@ class Configorama {
572
464
  // (\|\s*(toUpperCase|toLowerCase|toCamelCase|toKebabCase|capitalize)\s*)+$
573
465
  // Updated to support function-style filters like help('text') with nested parens
574
466
  // Use a more permissive pattern that matches anything between parens including nested parens
575
- 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
+ )
576
470
  // console.log('this.filterMatch', this.filterMatch)
577
471
 
578
472
  this.functions = {
@@ -637,15 +531,9 @@ class Configorama {
637
531
  this.callCount = 0
638
532
  }
639
533
 
640
- initialCall(func) {
641
- this.deep = []
642
- this.tracker.start()
643
- return func().finally(() => {
644
- this.tracker.stop()
645
- this.deep = []
646
- })
647
- }
648
-
534
+ // ################
535
+ // ## PUBLIC API ##
536
+ // ################
649
537
  /**
650
538
  * Populate all variables in the service, conveniently remove and restore the service attributes
651
539
  * that confuse the population methods.
@@ -657,6 +545,7 @@ class Configorama {
657
545
  const configoramaOpts = this.opts
658
546
 
659
547
  const showFoundVariables = configoramaOpts && configoramaOpts.dynamicArgs && (configoramaOpts.dynamicArgs.list || configoramaOpts.dynamicArgs.info)
548
+
660
549
 
661
550
  // If we have a file path but no config yet, parse it now
662
551
  if (this.configFilePath && !this.config) {
@@ -668,13 +557,16 @@ class Configorama {
668
557
  this.opts
669
558
  )
670
559
  this.configFileContents = ''
671
- if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
560
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails || SETUP_MODE) {
672
561
  this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
673
562
  }
674
563
  /*
675
564
  console.log('before preprocess', configObject)
676
565
  /** */
677
- /* 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 */
678
570
  configObject = preProcess(configObject, this.variableSyntax)
679
571
  /*
680
572
  console.log('after preprocess', configObject)
@@ -695,22 +587,24 @@ class Configorama {
695
587
  const variableSyntax = this.variableSyntax
696
588
  const variablesKnownTypes = this.variablesKnownTypes
697
589
 
698
- if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
699
- // Use collectVariableMetadata to get variable info (DRY - don't duplicate logic)
590
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails || SETUP_MODE) {
700
591
  const metadata = this.collectVariableMetadata()
701
592
 
702
- const enrich = enrichMetadata(
593
+ const enrich = await enrichMetadata(
703
594
  metadata,
704
595
  this.resolutionTracking,
705
596
  this.variableSyntax,
706
597
  this.fileRefsFound,
707
598
  this.originalConfig,
708
599
  this.configFilePath,
709
- Object.keys(this.filters)
600
+ Object.keys(this.filters),
601
+ undefined, // resolvedConfig not available yet
602
+ this.opts.options,
603
+ this.variableTypes
710
604
  )
711
605
 
712
606
  if (showFoundVariables) {
713
- /*
607
+ //*
714
608
  deepLog('metadata', metadata)
715
609
  fs.writeFileSync(`metadata-${path.basename(this.configFilePath)}.json`, JSON.stringify(metadata, null, 2))
716
610
  deepLog('enrich', enrich)
@@ -724,7 +618,10 @@ class Configorama {
724
618
  const uniqueVarKeys = Object.keys(uniqueVariables)
725
619
 
726
620
  if (this.opts.returnPreResolvedVariableDetails) {
727
- return enrich
621
+ return Object.assign({}, {
622
+ resolved: false,
623
+ originalConfig: this.originalConfig
624
+ }, enrich)
728
625
  }
729
626
 
730
627
  if (!varKeys.length) {
@@ -750,9 +647,16 @@ class Configorama {
750
647
  console.log()
751
648
  }
752
649
 
650
+ const lines = this.configFileContents ? this.configFileContents.split('\n') : []
651
+ const fileType = this.configFileType
652
+ const configFilePath = this.configFilePath
653
+
753
654
  if (varKeys.length > 0) {
754
655
  const fileName = this.configFilePath ? ` in ${this.configFilePath}` : ''
755
656
 
657
+ // Extract base variable name from varMatch key (e.g., '${env:FOO, default}' -> 'env:FOO')
658
+ const getBaseVarName = (key) => key.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
659
+
756
660
  logHeader(`Found ${varKeys.length} Variables${fileName}`)
757
661
 
758
662
  // deepLog('variableData', variableData)
@@ -765,9 +669,7 @@ class Configorama {
765
669
 
766
670
  // Use uniqueVariables for simpler reference counting
767
671
  const referenceData = varKeys.map((k) => {
768
- // Map from varMatch (e.g., '${env:API_KEY}') to variable name (e.g., 'env:API_KEY')
769
- // Extract the variable name from the key by removing ${ and }
770
- const varName = k.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
672
+ const varName = getBaseVarName(k)
771
673
  const uniqueVar = uniqueVariables[varName]
772
674
  const refCount = uniqueVar ? uniqueVar.occurrences.length : variableData[k].length
773
675
  const placesWord = refCount > 1 ? 'places' : 'place'
@@ -779,8 +681,6 @@ class Configorama {
779
681
 
780
682
  logHeader('Variable Details')
781
683
 
782
- const lines = this.configFileContents ? this.configFileContents.split('\n') : []
783
-
784
684
  const indent = ''
785
685
  const boxes = varKeys.map((key, i) => {
786
686
  const variableInstances = variableData[key]
@@ -788,7 +688,7 @@ class Configorama {
788
688
  const firstInstance = variableInstances[0]
789
689
 
790
690
  // Get uniqueVariable data for description and other metadata
791
- const varName = key.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
691
+ const varName = getBaseVarName(key)
792
692
  const uniqueVar = uniqueVariables[varName]
793
693
 
794
694
  // Build display message from enriched metadata
@@ -807,41 +707,24 @@ class Configorama {
807
707
  }
808
708
 
809
709
  // Show type filter if present (Boolean, String, Number, etc.)
810
- if (uniqueVar && uniqueVar.occurrences.length > 0) {
811
- const typeFilters = ['Boolean', 'String', 'Number', 'Array', 'Object']
812
- const foundTypes = new Set()
813
-
814
- uniqueVar.occurrences.forEach(occ => {
815
- if (occ.filters && Array.isArray(occ.filters)) {
816
- occ.filters.forEach(filter => {
817
- if (typeFilters.includes(filter)) {
818
- foundTypes.add(filter)
819
- }
820
- })
821
- }
822
- })
823
-
824
- if (foundTypes.size > 0) {
825
- const typeText = `${indent}${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
826
- varMsg += `${typeText} ${valueChalk(Array.from(foundTypes).join(', '))}\n`
827
- }
710
+ if (uniqueVar && uniqueVar.types && uniqueVar.types.length > 0) {
711
+ const typeLabel = `${indent}${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
712
+ varMsg += `${typeLabel} ${valueChalk(uniqueVar.types.join(', '))}\n`
828
713
  }
829
714
 
830
715
  // Show description from uniqueVariables if available
831
- if (uniqueVar && uniqueVar.occurrences.length > 0) {
832
- // Collect unique descriptions from all occurrences
833
- const descriptions = uniqueVar.occurrences
834
- .map(occ => occ.description)
835
- .filter((desc, index, self) => desc && self.indexOf(desc) === index)
836
-
837
- if (descriptions.length > 0) {
838
- const descText = `${indent}${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
839
- const combinedDesc = descriptions.join('. ')
840
- varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
841
- }
716
+ if (uniqueVar && uniqueVar.descriptions && uniqueVar.descriptions.length > 0) {
717
+ const descText = `${indent}${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
718
+ const combinedDesc = uniqueVar.descriptions.join('. ')
719
+ varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
842
720
  }
843
721
 
844
-
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
+ }
845
728
 
846
729
  // Show default value from metadata
847
730
  if (typeof firstInstance.defaultValue !== 'undefined') {
@@ -852,82 +735,49 @@ class Configorama {
852
735
 
853
736
  // Show default value source path from metadata
854
737
  if (firstInstance.defaultValueSrc) {
855
- varMsg += `${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
856
- varMsg += `${valueChalk(firstInstance.defaultValueSrc)}\n`
857
- }
858
-
859
- // Show resolve order from metadata
860
- if (firstInstance.resolveOrder.length > 1) {
861
- varMsg += `${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
862
- const resolveOrder = firstInstance.resolveOrder.join(', ')
863
- 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
+ }
864
745
  }
865
746
 
866
747
  // Show path(s) from metadata
867
- 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)
868
752
  let locationLabel = `${indent}${keyChalk('Config Path:'.padEnd(titleText.length, ' '))}`
869
753
  let typeText = ''
870
754
  if (variableInstances.length > 1) {
871
755
  const pathIndent = ' '.repeat(titleText.length + 1)
872
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}`)
873
761
  // Show type filter per path if different
874
762
  if (uniqueVar && uniqueVar.occurrences.length > 1) {
875
763
  const occurrence = uniqueVar.occurrences.find(occ => occ.path === v.path)
876
- const typeFilters = ['Boolean', 'String', 'Number', 'Array', 'Object']
877
- const pathType = occurrence && occurrence.filters
878
- ? occurrence.filters.find(f => typeFilters.includes(f))
879
- : null
880
-
764
+ const pathType = occurrence && occurrence.type
881
765
  typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
882
766
  const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
883
- return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
767
+ return `${prefix}${pathLink}${typeText}`
884
768
  }
885
769
  const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
886
- return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
770
+ return `${prefix}${pathLink}${typeText}`
887
771
  })
888
772
  locationRender = pathItems.join('\n')
889
773
  locationLabel = `${indent}${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
890
774
  } else {
891
- // look for type filter in the first instance
892
- const typeFilters = ['Boolean', 'String', 'Number', 'Array', 'Object']
893
- const pathType = firstInstance.filters
894
- ? firstInstance.filters.find(f => typeFilters.includes(f))
895
- : null
896
-
775
+ const pathType = firstInstance.type
897
776
  typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
898
777
  }
899
778
  varMsg += `${locationLabel} ${locationRender}`
900
779
 
901
- // Find line number in config file based on format (YAML, TOML, JSON, INI)
902
- const configKey = firstInstance.key
903
- const line = lines.findIndex((line) => {
904
- const fileType = this.configFileType
905
- const escapedKey = configKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
906
- // YAML: key: or key :
907
- if (fileType === '.yml' || fileType === '.yaml') {
908
- return new RegExp(`^\\s*${escapedKey}\\s*:`).test(line)
909
- }
910
- // TOML: key = or key=
911
- if (fileType === '.toml') {
912
- return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
913
- }
914
- // JSON: "key": or "key" :
915
- if (fileType === '.json' || fileType === '.json5') {
916
- return new RegExp(`"${escapedKey}"\\s*:`).test(line)
917
- }
918
- // INI: key = or key=
919
- if (fileType === '.ini') {
920
- return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
921
- }
922
- // JS/TS/ESM: key: or "key": or 'key': or `key`: or [`key`]:
923
- if (['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(fileType)) {
924
- return new RegExp(`(?:${escapedKey}|"${escapedKey}"|'${escapedKey}'|\`${escapedKey}\`|\\[\`${escapedKey}\`\\])\\s*:`).test(line)
925
- }
926
- // Default fallback: try YAML-style
927
- return line.includes(`${configKey}:`)
928
- })
929
- const lineNumber = line !== -1 ? line + 1 : 0
930
-
780
+ const lineNumber = findLineForKey(firstInstance.key, lines, fileType)
931
781
 
932
782
  return {
933
783
  content: {
@@ -957,6 +807,276 @@ class Configorama {
957
807
  // process.exit(1)
958
808
  }
959
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
+
960
1080
 
961
1081
  // WALK through CLI prompt if --setup flag is set
962
1082
  if (SETUP_MODE) {
@@ -964,20 +1084,29 @@ class Configorama {
964
1084
  // deepLog('enrich', enrich)
965
1085
  const userInputs = await runConfigWizard(enrich, this.originalConfig, this.configFilePath)
966
1086
 
967
- console.log('\n')
968
1087
  logHeader('User Inputs Summary')
1088
+ console.log()
969
1089
  console.log(JSON.stringify(userInputs, null, 2))
970
1090
 
971
1091
  // TODO set values
972
1092
 
973
1093
  // Apply user inputs to options and environment
974
1094
  if (userInputs.options) {
975
- Object.assign(this.opts, userInputs.options)
1095
+ Object.assign(this.options, userInputs.options)
976
1096
  }
977
1097
  if (userInputs.env) {
978
1098
  Object.assign(process.env, userInputs.env)
979
1099
  }
980
- // 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
+ }
981
1110
 
982
1111
  console.log()
983
1112
  logHeader('Resolving Configuration')
@@ -991,7 +1120,8 @@ class Configorama {
991
1120
 
992
1121
  /* Exit early if list or info flag is set */
993
1122
  if (showFoundVariables) {
994
- process.exit(0)
1123
+ // TODO re-enable this
1124
+ // process.exit(0)
995
1125
  }
996
1126
  }
997
1127
 
@@ -1125,17 +1255,28 @@ class Configorama {
1125
1255
  * @returns {object} Metadata object containing variables, fileRefs, and summary
1126
1256
  */
1127
1257
  collectVariableMetadata() {
1258
+ // Return cached metadata if already computed
1259
+ if (this._cachedMetadata) {
1260
+ return this._cachedMetadata
1261
+ }
1262
+
1128
1263
  const variableSyntax = this.variableSyntax
1129
1264
  const variablesKnownTypes = this.variablesKnownTypes
1130
1265
  const variableTypes = this.variableTypes
1131
1266
  const filterMatch = this.filterMatch
1267
+ const configFilePath = this.configFilePath
1268
+ // Use rawOriginalConfig for metadata display (truly original, no escaping)
1269
+ const originalConfig = this.rawOriginalConfig || this.originalConfig
1132
1270
  const foundVariables = []
1133
1271
  const variableData = {}
1134
1272
  const fileRefs = []
1135
1273
  const fileGlobPatterns = []
1274
+ const preResolvedPaths = new Set()
1275
+ const byConfigPath = []
1276
+ const referencesMap = new Map()
1136
1277
  let matchCount = 1
1137
1278
 
1138
- traverse(this.originalConfig).forEach(function (rawValue) {
1279
+ traverse(originalConfig).forEach(function (rawValue) {
1139
1280
  if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
1140
1281
  const configValuePath = this.path.join('.')
1141
1282
  /* Skip Fn::Sub variables */
@@ -1176,6 +1317,21 @@ class Configorama {
1176
1317
 
1177
1318
  const key = keyWithoutFilters
1178
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
+
1179
1335
  // Strip filters from resolveDetails
1180
1336
  const cleanedResolveDetails = nested.map(detail => {
1181
1337
  const cleaned = { ...detail }
@@ -1197,6 +1353,14 @@ class Configorama {
1197
1353
  cleaned.varString = cleaned.varString.replace(filterMatch, '').trim()
1198
1354
  }
1199
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
+
1200
1364
  // Also clean fallbackValues if present
1201
1365
  if (cleaned.fallbackValues && Array.isArray(cleaned.fallbackValues)) {
1202
1366
  cleaned.fallbackValues = cleaned.fallbackValues.map(fb => {
@@ -1219,6 +1383,17 @@ class Configorama {
1219
1383
  cleanedFb.stringValue = cleanedFb.stringValue.replace(filterMatch, '').trim()
1220
1384
  }
1221
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
+
1222
1397
  return cleanedFb
1223
1398
  })
1224
1399
  }
@@ -1253,35 +1428,59 @@ class Configorama {
1253
1428
 
1254
1429
  if (item && item.fallbackValues) {
1255
1430
  let hasResolvedFallback
1431
+ let defaultValueSrc
1432
+ const isSingleFallback = item.fallbackValues.length === 1
1256
1433
  const order = ([stripFilters(item.valueBeforeFallback)]).concat(item.fallbackValues.map((f, i) => {
1257
1434
  if (f.fallbackValues) {
1258
- const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
1435
+ const [nestedOrder, nestedResolvedFallback, nestedDefaultSrc] = calculateResolveOrder(f)
1259
1436
  if (!hasResolvedFallback && nestedResolvedFallback) {
1260
1437
  hasResolvedFallback = nestedResolvedFallback
1438
+ defaultValueSrc = nestedDefaultSrc
1261
1439
  }
1262
1440
  return nestedOrder
1263
1441
  }
1264
1442
 
1443
+ const valueStr = stripFilters(f.stringValue || f.variable)
1444
+
1445
+ // Only set default from first resolvable fallback
1265
1446
  if (!hasResolvedFallback && f.isResolvedFallback) {
1266
- hasResolvedFallback = stripFilters(f.stringValue)
1267
- }
1268
- if (f.isResolvedFallback) {
1269
- 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
1270
1455
  }
1271
1456
 
1272
1457
  if (!hasResolvedFallback && f.isVariable) {
1273
1458
  defaultValueIsVar = f
1274
1459
  }
1275
- const valueStr = stripFilters(f.stringValue || f.variable)
1276
- 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
1277
1475
  })).flat()
1278
1476
 
1279
- return [order, hasResolvedFallback]
1477
+ return [order, hasResolvedFallback, defaultValueSrc]
1280
1478
  }
1281
- return [[stripFilters(item.variable)], undefined]
1479
+ return [[stripFilters(item.variable)], undefined, undefined]
1282
1480
  }
1283
1481
 
1284
- const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
1482
+ const lastCleanedItem = cleanedResolveDetails[cleanedResolveDetails.length - 1]
1483
+ const [resolveOrder, hasResolvedFallback, defaultValueSrc] = calculateResolveOrder(lastCleanedItem)
1285
1484
  varData.resolveOrder = resolveOrder
1286
1485
 
1287
1486
  if (defaultValueIsVar) {
@@ -1292,6 +1491,10 @@ class Configorama {
1292
1491
  varData.defaultValue = hasResolvedFallback
1293
1492
  }
1294
1493
 
1494
+ if (defaultValueSrc) {
1495
+ varData.defaultValueSrc = defaultValueSrc
1496
+ }
1497
+
1295
1498
  if (typeof varData.defaultValue === 'undefined') {
1296
1499
  varData.isRequired = true
1297
1500
  }
@@ -1304,46 +1507,75 @@ class Configorama {
1304
1507
  nested.forEach((detail) => {
1305
1508
  // console.log('detail', detail)
1306
1509
  if (detail.variableType && (detail.variableType === 'file' || detail.variableType === 'text')) {
1307
- const fileMatch = detail.variable.match(/^(?:file|text)\((.*?)\)/)
1308
- if (fileMatch && fileMatch[1]) {
1309
- let fileContent = fileMatch[1].trim()
1310
-
1311
- // Split by comma to separate file path from parameters/fallback values
1312
- const parts = splitCsv(fileContent)
1313
- let filePath = parts[0].trim()
1314
-
1315
- // Remove quotes if present
1316
- filePath = filePath.replace(/^['"]|['"]$/g, '')
1317
-
1318
- // Normalize path: ensure relative paths start with ./
1319
- let normalizedPath = filePath
1320
- if (
1321
- !filePath.startsWith('./') &&
1322
- !filePath.startsWith('../') &&
1323
- !filePath.startsWith('/') &&
1324
- !filePath.startsWith('~')
1325
- ) {
1326
- normalizedPath = './' + filePath
1327
- }
1510
+ const extracted = extractFilePath(detail.variable)
1511
+ if (extracted) {
1512
+ const normalizedPath = normalizePath(extracted.filePath)
1513
+ if (!normalizedPath) return
1328
1514
 
1329
- // file .//
1330
- if (normalizedPath.startsWith('.//')) {
1331
- normalizedPath = normalizedPath.replace('.//', './')
1332
- }
1333
-
1334
1515
  // Handle variables in file paths - just record the pattern
1335
1516
  if (!fileRefs.includes(normalizedPath)) {
1336
1517
  fileRefs.push(normalizedPath)
1337
1518
  }
1338
-
1519
+
1339
1520
  // Check if path contains variables and create glob pattern
1340
- if (normalizedPath.match(variableSyntax)) {
1521
+ const containsVariables = !!normalizedPath.match(variableSyntax)
1522
+ let globPattern
1523
+ if (containsVariables) {
1341
1524
  // Replace variable syntax ${...} with * for glob pattern
1342
- const globPattern = normalizedPath.replace(variableSyntax, '*')
1525
+ globPattern = normalizedPath.replace(variableSyntax, '*')
1343
1526
  if (!fileGlobPatterns.includes(globPattern)) {
1344
1527
  fileGlobPatterns.push(globPattern)
1345
1528
  }
1346
1529
  }
1530
+
1531
+ // Try to pre-resolve inner variables from originalConfig
1532
+ let resolvedPath = normalizedPath
1533
+ let resolvedVarString = rawValue
1534
+ if (containsVariables) {
1535
+ const pathResult = resolveInnerVariables(normalizedPath, variableSyntax, originalConfig, dotProp.get)
1536
+ const varStringResult = resolveInnerVariables(rawValue, variableSyntax, originalConfig, dotProp.get)
1537
+
1538
+ if (pathResult.didResolve) {
1539
+ resolvedPath = normalizePath(pathResult.resolved) || pathResult.resolved
1540
+ resolvedVarString = varStringResult.resolved
1541
+ preResolvedPaths.add(resolvedPath)
1542
+ }
1543
+ }
1544
+
1545
+ // Build byConfigPath entry
1546
+ const absolutePath = configFilePath
1547
+ ? path.resolve(path.dirname(configFilePath), resolvedPath)
1548
+ : resolvedPath
1549
+ const fileExists = configFilePath ? fs.existsSync(absolutePath) : false
1550
+
1551
+ const configPathEntry = {
1552
+ location: configValuePath,
1553
+ filePath: absolutePath,
1554
+ relativePath: resolvedPath,
1555
+ originalVariableString: rawValue,
1556
+ resolvedVariableString: resolvedVarString,
1557
+ containsVariables,
1558
+ exists: fileExists,
1559
+ }
1560
+ if (globPattern) {
1561
+ configPathEntry.pattern = globPattern
1562
+ }
1563
+ byConfigPath.push(configPathEntry)
1564
+
1565
+ // Build references entry (use resolvedPath as key when available)
1566
+ const refKey = resolvedPath
1567
+ if (!referencesMap.has(refKey)) {
1568
+ referencesMap.set(refKey, {
1569
+ resolvedPath: refKey,
1570
+ refs: [],
1571
+ })
1572
+ }
1573
+ const refEntry = referencesMap.get(refKey)
1574
+ refEntry.refs.push({
1575
+ location: configValuePath,
1576
+ value: normalizedPath,
1577
+ originalVariableString: rawValue,
1578
+ })
1347
1579
  }
1348
1580
  }
1349
1581
  })
@@ -1364,50 +1596,58 @@ class Configorama {
1364
1596
  const instances = variableData[key]
1365
1597
  const firstInstance = instances[0]
1366
1598
 
1367
- // Check if truly required using same logic as display code
1368
- let isTrulyRequired = false
1369
- if (typeof firstInstance.defaultValue === 'undefined') {
1370
- // Check for self-references that resolve to config values
1371
- let dotPropArr = []
1372
- if (firstInstance.defaultValueIsVar && (
1373
- firstInstance.defaultValueIsVar.variableType === 'self:' ||
1374
- firstInstance.defaultValueIsVar.variableType === 'dot.prop'
1375
- )) {
1376
- dotPropArr = [firstInstance.defaultValueIsVar]
1377
- }
1378
-
1379
- const hasDotPropOrSelf = instances.reduce((acc, v) => {
1380
- // Only check the outermost variable (last in resolveDetails)
1381
- if (v.resolveDetails && v.resolveDetails.length > 0) {
1382
- const outermostDetail = v.resolveDetails[v.resolveDetails.length - 1]
1383
- if (outermostDetail.variableType === 'dot.prop' || outermostDetail.variableType === 'self') {
1384
- acc.push(outermostDetail)
1385
- }
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
1386
1613
  }
1387
- return acc
1388
- }, dotPropArr)
1614
+ }
1615
+ }
1389
1616
 
1390
- if (!hasDotPropOrSelf.length) {
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
+ }
1624
+
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') {
1391
1634
  isTrulyRequired = true
1392
1635
  } else {
1393
- // Check if the self-reference resolves to a value
1394
- const cleanPath = hasDotPropOrSelf[0].variable.replace('self:', '')
1395
- const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1396
- if (typeof dotPropValue === 'undefined') {
1397
- isTrulyRequired = true
1398
- } else {
1399
- // Enrich with default value from self-reference
1400
- firstInstance.defaultValueSrc = cleanPath
1401
- const niceString = typeof dotPropValue === 'object' ? JSON.stringify(dotPropValue) : dotPropValue
1402
- const truncatedString = niceString.length > 100 ? niceString.substring(0, 90) + '...' : niceString
1403
- firstInstance.defaultValue = truncatedString
1404
- firstInstance.isRequired = false
1405
- }
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
+ })
1406
1642
  }
1643
+ } else if (typeof firstInstance.defaultValue === 'undefined') {
1644
+ isTrulyRequired = true
1407
1645
  }
1408
1646
 
1409
1647
  // Update isRequired based on computed isTrulyRequired
1410
- firstInstance.isRequired = isTrulyRequired
1648
+ instances.forEach((instance) => {
1649
+ instance.isRequired = isTrulyRequired
1650
+ })
1411
1651
 
1412
1652
  if (isTrulyRequired) {
1413
1653
  requiredCount++
@@ -1416,81 +1656,65 @@ class Configorama {
1416
1656
  }
1417
1657
  })
1418
1658
 
1419
- return {
1659
+ this._cachedMetadata = {
1420
1660
  variables: variableData,
1421
1661
  uniqueVariables: {},
1422
1662
  fileDependencies: {
1423
1663
  globPatterns: fileGlobPatterns,
1424
1664
  // all: fileRefs,
1425
1665
  dynamicPaths: fileRefs.filter(ref => ref.indexOf('*') !== -1 || ref.match(variableSyntax)),
1426
- // resolve files are those that are paths with no * and no inner variables
1427
- resolvedPaths: fileRefs.filter(ref => ref.indexOf('*') === -1 && !ref.match(variableSyntax)),
1428
- // Set in enrichMetadata
1429
- byConfigPath: undefined,
1430
- // Set in enrichMetadata
1431
- references: undefined,
1666
+ // Resolved paths: static paths + pre-resolved dynamic paths
1667
+ resolvedPaths: [
1668
+ ...fileRefs.filter(ref => ref.indexOf('*') === -1 && !ref.match(variableSyntax)),
1669
+ ...preResolvedPaths
1670
+ ],
1671
+ byConfigPath,
1672
+ references: Array.from(referencesMap.values()),
1432
1673
  },
1433
1674
  summary: {
1434
1675
  totalVariables: varKeys.length,
1435
1676
  requiredVariables: requiredCount,
1436
1677
  variablesWithDefaults: withDefaultsCount
1437
1678
  },
1438
- }
1439
- }
1440
- runFunction(variableString) {
1441
- // console.log('runFunction', variableString)
1442
- /* If json object value return it */
1443
- if (variableString.match(/^\s*{/) && variableString.match(/}\s*$/)) {
1444
- return variableString
1445
- }
1446
- // console.log('runFunction', variableString)
1447
- var hasFunc = funcRegex.exec(variableString)
1448
- // TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
1449
- // console.log('hasFunc', hasFunc)
1450
- if (!hasFunc || hasFunc && (hasFunc[1] === 'cron' || hasFunc[1] === 'eval')) {
1451
- return variableString
1452
- }
1453
- // test for object
1454
- const functionName = hasFunc[1]
1455
- const rawArgs = hasFunc[2]
1456
- // TODO @DWELLS. Loop through all raw args and parse to correct datatype
1457
- // argument is object
1458
- let argsToPass
1459
- if (rawArgs && rawArgs.match(/^{([^}]+)}$/)) {
1460
- // console.log('OBJECT', hasFunc[2])
1461
- // TODO use JSON5
1462
- argsToPass = [JSON.parse(rawArgs)]
1463
- } else {
1464
- // TODO fix how commas + spaces are ned
1465
- const splitter = splitCsv(rawArgs, ', ')
1466
- // console.log('splitter', splitter)
1467
- argsToPass = formatFunctionArgs(splitter)
1468
- }
1469
- // console.log('argsToPass runFunction', argsToPass)
1470
- // TODO check for camelCase version. | toUpperCase messes with function name
1471
- const theFunction = this.functions[functionName] || this.functions[functionName.toLowerCase()]
1679
+ }
1472
1680
 
1473
- if (!theFunction) throw new Error(`Function "${functionName}" not found`)
1681
+ return this._cachedMetadata
1682
+ }
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
1474
1693
 
1475
- const funcValue = theFunction(...argsToPass)
1476
- // console.log('funcValue', funcValue)
1477
- // console.log('typeof funcValue', typeof funcValue)
1478
- let replaceVal = funcValue
1479
- if (typeof funcValue === 'string') {
1480
- const replaceIt = variableString.replace(hasFunc[0], funcValue)
1481
- replaceVal = cleanVariable(replaceIt, this.variableSyntax, true, `runFunction ${this.callCount}`)
1694
+ if (DEBUG) {
1695
+ deepLog(`objectToPopulate called ${this.callCount} times`, objectToPopulate)
1696
+ // process.exit(0)
1482
1697
  }
1483
1698
 
1484
- // If wrapped in outer function, recurse
1485
- const hasMoreFunctions = funcRegex.exec(replaceVal)
1486
- if (hasMoreFunctions) {
1487
- return this.runFunction(replaceVal)
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)
1704
+
1705
+ if (populations.length === 0) {
1706
+ if (DEBUG) console.log('Config Population Finished')
1707
+ return Promise.resolve(objectToPopulate)
1488
1708
  }
1489
- return replaceVal
1709
+
1710
+ return this.assignProperties(objectToPopulate, populations).then(() => {
1711
+ return this.populateObjectImpl(objectToPopulate)
1712
+ })
1490
1713
  }
1491
- // ############
1492
- // ## OBJECT ##
1493
- // ############
1714
+
1715
+ // #######################
1716
+ // ## PROPERTY HANDLING ##
1717
+ // #######################
1494
1718
  /**
1495
1719
  * The declaration of a terminal property. This declaration includes the path and value of the
1496
1720
  * property.
@@ -1638,40 +1862,9 @@ class Configorama {
1638
1862
  })
1639
1863
  })
1640
1864
  }
1641
- /**
1642
- * Populate the variables in the given object.
1643
- * @param objectToPopulate The object to populate variables within.
1644
- * @returns {Promise.<TResult>|*} A promise resolving to the in-place populated object.
1645
- */
1646
- populateObject(objectToPopulate) {
1647
- return this.initialCall(() => this.populateObjectImpl(objectToPopulate))
1648
- }
1649
- populateObjectImpl(objectToPopulate) {
1650
- this.callCount = this.callCount + 1
1651
-
1652
- if (DEBUG) {
1653
- deepLog(`objectToPopulate called ${this.callCount} times`, objectToPopulate)
1654
- // process.exit(0)
1655
- }
1656
-
1657
- const leaves = this.getProperties(objectToPopulate, true, objectToPopulate)
1658
- this.leaves = leaves
1659
- // console.log('leaves', leaves)
1660
- const populations = this.populateVariables(leaves)
1661
- // console.log("FILL LEAVES", populations)
1662
-
1663
- if (populations.length === 0) {
1664
- if (DEBUG) console.log('Config Population Finished')
1665
- return Promise.resolve(objectToPopulate)
1666
- }
1667
-
1668
- return this.assignProperties(objectToPopulate, populations).then(() => {
1669
- return this.populateObjectImpl(objectToPopulate)
1670
- })
1671
- }
1672
- // ##############
1673
- // ## PROPERTY ##
1674
- // ##############
1865
+ // ##################
1866
+ // ## MATCH/RENDER ##
1867
+ // ##################
1675
1868
  /**
1676
1869
  * @typedef {Object} MatchResult
1677
1870
  * @property {String} match The original property value that matched the variable syntax
@@ -1734,7 +1927,17 @@ class Configorama {
1734
1927
 
1735
1928
  let result = valueObject.value
1736
1929
  for (let i = 0; i < matches.length; i += 1) {
1737
- 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
+ })
1738
1941
 
1739
1942
  // Extract metadata from result if present
1740
1943
  let actualResult = results[i]
@@ -1878,6 +2081,10 @@ class Configorama {
1878
2081
 
1879
2082
  return result
1880
2083
  }
2084
+
2085
+ // ######################
2086
+ // ## VALUE RESOLUTION ##
2087
+ // ######################
1881
2088
  /**
1882
2089
  * Populate the given value, recursively if root is true
1883
2090
  * @param valueObject The value to populate variables within
@@ -1995,7 +2202,8 @@ class Configorama {
1995
2202
  const hasFilters = originalSrc.match(this.filterMatch)
1996
2203
  let foundFilters = []
1997
2204
  if (hasFilters) {
1998
- foundFilters = hasFilters[1]
2205
+ foundFilters = hasFilters[0]
2206
+ .replace(/}$/, '') // remove trailing }
1999
2207
  .split('|')
2000
2208
  .map((filter) => filter.trim())
2001
2209
  .filter(Boolean)
@@ -2150,7 +2358,7 @@ class Configorama {
2150
2358
 
2151
2359
  if (nestedVar) {
2152
2360
  const fallbackStr = getFallbackString(splitVars, nestedVar)
2153
- if (!this.opts.allowUnknownVars) {
2361
+ if (!this.opts.allowUnknownVariables) {
2154
2362
  verifyVariable(nestedVar, valueObject, this.variableTypes, this.config)
2155
2363
  }
2156
2364
 
@@ -2262,14 +2470,13 @@ Missing Value ${missingValue} - ${matchedString}
2262
2470
  if (typeof valueToPopulate === 'number' && foundFilters.length) {
2263
2471
  runFilters = true
2264
2472
  } else if (
2265
- typeof valueToPopulate === 'string' &&
2266
- !valueToPopulate.match(deepRefSyntax) &&
2267
- foundFilters.length &&
2473
+ typeof valueToPopulate === 'string' &&
2474
+ !valueToPopulate.match(deepRefSyntax) &&
2475
+ foundFilters.length &&
2268
2476
  !property.match(this.variableSyntax)
2269
2477
  ) {
2270
2478
  runFilters = true
2271
2479
  }
2272
-
2273
2480
  /* Apply filters if found */
2274
2481
  //console.log('> property', property)
2275
2482
  if (runFilters) {
@@ -2389,6 +2596,10 @@ Missing Value ${missingValue} - ${matchedString}
2389
2596
  : Promise.resolve(extractedValues.find(isValidValue)) // resolve first valid value, else undefined
2390
2597
  })
2391
2598
  }
2599
+
2600
+ // ####################
2601
+ // ## SOURCE GETTERS ##
2602
+ // ####################
2392
2603
  /**
2393
2604
  * Given any variable string, return the value it should be populated with.
2394
2605
  * @param variableString The variable string to retrieve a value for.
@@ -2592,6 +2803,11 @@ Missing Value ${missingValue} - ${matchedString}
2592
2803
  return Promise.resolve(encodeUnknown(propertyString))
2593
2804
  }
2594
2805
 
2806
+ if (this.opts.allowUnresolvedVariables) {
2807
+ // Encode unresolved variable to pass through resolution
2808
+ return Promise.resolve(encodeUnknown(propertyString))
2809
+ }
2810
+
2595
2811
  if (valueCount.length === 1 && noNestedVars) {
2596
2812
  const configFilePathMsg = (this.configFilePath) ? `\nIn file ${this.configFilePath} ` : ''
2597
2813
  const fromLine = (propertyString !== valueObject.originalSource) ? `\n From "${valueObject.originalSource}"\n` : ''
@@ -2754,7 +2970,7 @@ Missing Value ${missingValue} - ${matchedString}
2754
2970
  // console.log('nestedVar', nestedVar)
2755
2971
 
2756
2972
  if (nestedVar) {
2757
- if (!this.opts.allowUnknownVars) {
2973
+ if (!this.opts.allowUnknownVariables) {
2758
2974
  verifyVariable(nestedVar, valueObject, this.variableTypes, this.config)
2759
2975
  }
2760
2976
  const fallbackStr = getFallbackString(split, nestedVar)
@@ -2857,7 +3073,7 @@ Missing Value ${missingValue} - ${matchedString}
2857
3073
 
2858
3074
 
2859
3075
  /* Pass through unknown variables */
2860
- if (this.opts.allowUnknownVars || allowSpecialCase) {
3076
+ if (this.opts.allowUnknownVariables || allowSpecialCase) {
2861
3077
  // console.log('allowUnknownVars propertyString', propertyString)
2862
3078
  const varMatches = propertyString.match(this.variableSyntax)
2863
3079
  let allowUnknownVars = propertyString
@@ -2909,331 +3125,20 @@ Missing Value ${missingValue} - ${matchedString}
2909
3125
  })
2910
3126
  }
2911
3127
  async getValueFromFile(variableString, options) {
2912
- const opts = options || {}
2913
- const syntax = opts.asRawText ? textRefSyntax : fileRefSyntax
2914
- // console.log('From file', `"${variableString}"`)
2915
- let matchedFileString = variableString.match(syntax)[0]
2916
- // console.log('matchedFileString', matchedFileString)
2917
-
2918
- // Get function input params if any supplied https://regex101.com/r/qlNFVm/1
2919
- // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2920
- var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
2921
- // tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
2922
- var hasParams = funcParamsRegex.exec(matchedFileString)
2923
-
2924
- let argsToPass = []
2925
- if (hasParams) {
2926
- const splitter = splitCsv(hasParams[2])
2927
- const argsFound = splitter.map((arg) => {
2928
- const cleanArg = trim(arg).replace(/^'|"/, '').replace(/'|"$/, '')
2929
- return cleanArg
2930
- })
2931
- // console.log('argsFound', argsFound)
2932
-
2933
- // If function has more arguments than file path
2934
- if (argsFound.length && argsFound.length > 1) {
2935
- matchedFileString = argsFound[0]
2936
- argsToPass = argsFound.filter((arg, i) => {
2937
- return i !== 0
2938
- })
2939
- }
2940
- }
2941
- // console.log('argsToPass', argsToPass)
2942
-
2943
- const fileDetails = resolveFilePathFromMatch(matchedFileString, syntax, this.configPath)
2944
- // console.log('fileDetails', fileDetails)
2945
-
2946
- const { fullFilePath, resolvedPath, relativePath } = fileDetails
2947
-
2948
- const exists = fs.existsSync(fullFilePath)
2949
-
2950
- this.fileRefsFound.push({
2951
- // location: options.context.path.join('.'),
2952
- filePath: fullFilePath,
2953
- relativePath,
2954
- resolvedVariableString: options.context.value,
2955
- originalVariableString: options.context.originalSource,
2956
- containsVariables: options.context.value !== options.context.originalSource,
2957
- exists,
2958
- })
2959
-
2960
- let fileExtension = resolvedPath.split('.')
2961
-
2962
- fileExtension = fileExtension[fileExtension.length - 1]
2963
-
2964
- // Validate file exists
2965
- if (!exists) {
2966
- const originalVar = options.context && options.context.originalSource
2967
-
2968
- const findNestedResult = findNestedVariables(
2969
- originalVar,
2970
- this.variableSyntax,
2971
- this.variablesKnownTypes,
2972
- options.context.path,
2973
- this.variableTypes
2974
- )
2975
- // console.log('findNestedResult', findNestedResult)
2976
- let hasFallback = false
2977
- if (findNestedResult) {
2978
- const varDetails = findNestedResult[0]
2979
- // console.log('varDetails', varDetails)
2980
- hasFallback = varDetails.hasFallback
2981
- }
2982
-
2983
- // check if original var has fallback value
2984
- // console.log('NO FILE FOUND', fullFilePath)
2985
- // console.log('variableString', variableString)
2986
-
2987
- if (!hasFallback && !this.opts.allowUnknownFileRefs) {
2988
- const errorMsg = makeBox({
2989
- title: `File Not Found in ${originalVar}`,
2990
- minWidth: '100%',
2991
- text: `Variable ${variableString} cannot resolve due to missing file.
2992
-
2993
- File not found ${fullFilePath}
2994
-
2995
- Default fallback value will be used if provided.
2996
-
2997
- ${JSON.stringify(options.context, null, 2)}`,
2998
- })
2999
- console.log(errorMsg)
3000
- }
3001
- // TODO maybe reject. YAML does not allow for null/undefined values
3002
- // return Promise.reject(new Error(errorMsg))
3003
- return Promise.resolve(undefined)
3004
- }
3005
-
3006
-
3007
-
3008
- let valueToPopulate
3009
-
3010
- const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
3011
-
3012
- /* handle case for referencing raw JS files to inline them */
3013
- if (argsToPass.length
3014
- && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw')
3015
- || opts.asRawText
3016
- ) {
3017
- // Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
3018
- valueToPopulate = encodeJsSyntax(variableFileContents)
3019
- return Promise.resolve(valueToPopulate)
3020
- }
3021
-
3022
- // Process JS files
3023
- if (fileExtension === 'js' || fileExtension === 'cjs') {
3024
- // Possible alt importer tool https://github.com/humanwhocodes/module-importer
3025
- const jsFile = require(fullFilePath)
3026
- let returnValueFunction = jsFile
3027
- // TODO change how exported functions are referenced
3028
- const variableArray = variableString.split(':')
3029
-
3030
- if (variableArray[1]) {
3031
- let jsModule = variableArray[1]
3032
- jsModule = jsModule.split('.')[0]
3033
- returnValueFunction = jsFile[jsModule]
3034
- }
3035
-
3036
- if (typeof returnValueFunction !== 'function') {
3037
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3038
- Check if your javascript is exporting a function that returns a value.`
3039
- return Promise.reject(new Error(errorMessage))
3040
- }
3041
- // TODO update what is passed into function
3042
-
3043
- const valueForFunction = {
3044
- originalConfig: this.originalConfig,
3045
- config: this.config,
3046
- opts: this.opts,
3047
- }
3048
-
3049
- valueToPopulate = returnValueFunction.call(jsFile, valueForFunction, ...argsToPass)
3050
-
3051
- return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
3052
- let deepProperties = variableString.replace(matchedFileString, '')
3053
- deepProperties = deepProperties.slice(1).split('.')
3054
- deepProperties.splice(0, 1)
3055
- // Trim prop keys for starting/trailing spaces
3056
- deepProperties = deepProperties.map((prop) => {
3057
- return trim(prop)
3058
- })
3059
- return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
3060
- if (typeof deepValueToPopulateResolved === 'undefined') {
3061
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3062
- Check if your javascript is returning the correct data.`
3063
- return Promise.reject(new Error(errorMessage))
3064
- }
3065
- return Promise.resolve(deepValueToPopulateResolved)
3066
- })
3067
- })
3068
- }
3069
-
3070
- if (fileExtension === 'ts') {
3071
- const { executeTypeScriptFile } = require('./parsers/typescript')
3072
- let returnValueFunction
3073
- const variableArray = variableString.split(':')
3074
-
3075
- try {
3076
- const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
3077
- // console.log('fullFilePath', fullFilePath)
3078
- // console.log('tsFile', tsFile)
3079
- returnValueFunction = tsFile.config || tsFile.default || tsFile
3080
-
3081
- if (variableArray[1]) {
3082
- let tsModule = variableArray[1]
3083
- tsModule = tsModule.split('.')[0]
3084
- returnValueFunction = tsFile[tsModule]
3085
- }
3086
-
3087
- if (typeof returnValueFunction !== 'function') {
3088
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3089
- Check if your TypeScript is exporting a function that returns a value.`
3090
- return Promise.reject(new Error(errorMessage))
3091
- }
3092
-
3093
- const valueForFunction = {
3094
- originalConfig: this.originalConfig,
3095
- config: this.config,
3096
- opts: this.opts,
3097
- }
3098
-
3099
- valueToPopulate = returnValueFunction.call(tsFile, valueForFunction, ...argsToPass)
3100
-
3101
- return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
3102
- let deepProperties = variableString.replace(matchedFileString, '')
3103
- deepProperties = deepProperties.slice(1).split('.')
3104
- deepProperties.splice(0, 1)
3105
- // Trim prop keys for starting/trailing spaces
3106
- deepProperties = deepProperties.map((prop) => {
3107
- return trim(prop)
3108
- })
3109
- return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
3110
- if (typeof deepValueToPopulateResolved === 'undefined') {
3111
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3112
- Check if your TypeScript is returning the correct data.`
3113
- return Promise.reject(new Error(errorMessage))
3114
- }
3115
- return Promise.resolve(deepValueToPopulateResolved)
3116
- })
3117
- })
3118
- } catch (err) {
3119
- return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
3120
- }
3121
- }
3122
-
3123
- if (fileExtension === 'mjs' || fileExtension === 'esm') {
3124
- // Possible alt importer tool https://github.com/humanwhocodes/module-importer
3125
- const { executeESMFile } = require('./parsers/esm')
3126
- let returnValueFunction
3127
- const variableArray = variableString.split(':')
3128
-
3129
- try {
3130
- const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
3131
- // console.log('ESM fullFilePath', fullFilePath)
3132
- // console.log('ESM esmFile', esmFile, 'type:', typeof esmFile)
3133
- returnValueFunction = esmFile.config || esmFile.default || esmFile
3134
-
3135
- if (variableArray[1]) {
3136
- let esmModule = variableArray[1]
3137
- esmModule = esmModule.split('.')[0]
3138
- returnValueFunction = esmFile[esmModule]
3139
- }
3140
-
3141
- if (typeof returnValueFunction !== 'function') {
3142
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3143
- Check if your ESM is exporting a function that returns a value.`
3144
- return Promise.reject(new Error(errorMessage))
3145
- }
3146
-
3147
- const valueForFunction = {
3148
- originalConfig: this.originalConfig,
3149
- config: this.config,
3150
- opts: this.opts,
3151
- }
3152
-
3153
- valueToPopulate = returnValueFunction.call(esmFile, valueForFunction, ...argsToPass)
3154
-
3155
- return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
3156
- let deepProperties = variableString.replace(matchedFileString, '')
3157
- deepProperties = deepProperties.slice(1).split('.')
3158
- deepProperties.splice(0, 1)
3159
- // Trim prop keys for starting/trailing spaces
3160
- deepProperties = deepProperties.map((prop) => {
3161
- return trim(prop)
3162
- })
3163
- return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
3164
- if (typeof deepValueToPopulateResolved === 'undefined') {
3165
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
3166
- Check if your ESM is returning the correct data.`
3167
- return Promise.reject(new Error(errorMessage))
3168
- }
3169
- return Promise.resolve(deepValueToPopulateResolved)
3170
- })
3171
- })
3172
- } catch (err) {
3173
- return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
3174
- }
3175
- }
3176
-
3177
- // Process everything except JS, TS, and ESM
3178
- if (fileExtension !== 'js' && fileExtension !== 'ts' && fileExtension !== 'mjs' && fileExtension !== 'esm') {
3179
- /* Read initial file */
3180
- valueToPopulate = variableFileContents
3181
-
3182
- // File reference has :subKey lookup. Must dig deeper
3183
- if (matchedFileString !== variableString) {
3184
- if (fileExtension === 'yml' || fileExtension === 'yaml') {
3185
- valueToPopulate = JSON.stringify(YAML.parse(valueToPopulate))
3186
- }
3187
- if (fileExtension === 'toml') {
3188
- valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
3189
- }
3190
- if (fileExtension === 'ini') {
3191
- valueToPopulate = INI.toJson(valueToPopulate)
3192
- }
3193
- // console.log('deep', variableString)
3194
- // console.log('matchedFileString', matchedFileString)
3195
- let deepProperties = variableString.replace(matchedFileString, '')
3196
- // TODO 2025-11-12 add file.path.support instead of just :
3197
- if (deepProperties.substring(0, 1) !== ':') {
3198
- const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
3199
- Please use ":" to reference sub properties. ${deepProperties}`
3200
- return Promise.reject(new Error(errorMessage))
3201
- }
3202
- deepProperties = deepProperties.slice(1).split('.')
3203
- return this.getDeeperValue(deepProperties, valueToPopulate)
3204
- }
3205
-
3206
- if (fileExtension === 'yml' || fileExtension === 'yaml') {
3207
- valueToPopulate = YAML.parse(valueToPopulate)
3208
- return Promise.resolve(valueToPopulate)
3209
- }
3210
-
3211
- if (fileExtension === 'toml') {
3212
- valueToPopulate = TOML.parse(valueToPopulate)
3213
- return Promise.resolve(valueToPopulate)
3214
- }
3215
-
3216
- if (fileExtension === 'ini') {
3217
- valueToPopulate = INI.parse(valueToPopulate)
3218
- return Promise.resolve(valueToPopulate)
3219
- }
3220
-
3221
- if (fileExtension === 'json') {
3222
- valueToPopulate = JSON.parse(valueToPopulate)
3223
- return Promise.resolve(valueToPopulate)
3224
- }
3225
- }
3226
- // console.log('fall thru', valueToPopulate)
3227
- return Promise.resolve(valueToPopulate)
3228
- }
3229
- getVariableFromDeep(variableString) {
3230
- const index = variableString.replace(deepIndexReplacePattern, '')
3231
- // const index = this.getDeepIndex(variableString)
3232
- /*
3233
- console.log('FIND INDEX', index)
3234
- console.log(this.deep, this.deep[index])
3235
- /** */
3236
- 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)
3237
3142
  }
3238
3143
  getValueFromDeep(variableString, pathValue) {
3239
3144
  const variable = this.getVariableFromDeep(variableString)
@@ -3265,6 +3170,19 @@ Please use ":" to reference sub properties. ${deepProperties}`
3265
3170
  }
3266
3171
  return ret
3267
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
+ }
3268
3186
  makeDeepVariable(variable, caller) {
3269
3187
  // variable = variable.replace("dev", '"dev"')
3270
3188
  let index = this.deep.findIndex((item) => variable === item)
@@ -3289,8 +3207,6 @@ Please use ":" to reference sub properties. ${deepProperties}`
3289
3207
  console.log('deepVar', deepVar)
3290
3208
  // process.exit(1)
3291
3209
  /** */
3292
- // TODO debugging space removal. Seems like this helps
3293
- // const deepVar = variableContainer.replace(/\s/g, '').replace(variableString, `deep:${index}`)
3294
3210
  return deepVar
3295
3211
  }
3296
3212
  /**
@@ -3361,66 +3277,68 @@ Please use ":" to reference sub properties. ${deepProperties}`
3361
3277
  return veryDeep
3362
3278
  }
3363
3279
 
3364
- warnIfNotFound(variableString, valueToPopulate) {
3365
- let variableTypeText
3366
- if (variableString.match(envRefSyntax)) {
3367
- variableTypeText = 'environment variable'
3368
- } else if (variableString.match(optRefSyntax)) {
3369
- variableTypeText = 'option'
3370
- } else if (variableString.match(selfRefSyntax)) {
3371
- variableTypeText = 'config attribute'
3372
- } else if (variableString.match(fileRefSyntax)) {
3373
- variableTypeText = 'file'
3374
- } else if (variableString.match(deepRefSyntax)) {
3375
- variableTypeText = 'deep'
3376
- } else if (variableString.match(textRefSyntax)) {
3377
- 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
3378
3296
  }
3379
- if (!isValidValue(valueToPopulate)) {
3380
- // console.log("MISSING", variableString)
3381
- // console.log(this.deep)
3382
- // console.log(valueToPopulate)
3383
- const notFoundMsg = `No ${variableTypeText} found to satisfy the '\${${variableString}}' variable. Attempting fallback value`
3384
- if (DEBUG) {
3385
- console.log(notFoundMsg)
3386
- }
3387
- // 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
3388
3303
  }
3389
- return valueToPopulate
3390
- }
3391
- }
3392
-
3393
- function ensureQuote(value, open = '"', close) {
3394
- let i = -1
3395
- const result = []
3396
- const end = close || open
3397
- if (typeof value === 'string') {
3398
- return startChar(value, open) + value + endChar(value, end)
3399
- }
3400
- while (++i < value.length) {
3401
- result[i] = startChar(value[i], open) + value[i] + endChar(value[i], end)
3402
- }
3403
- return result
3404
- }
3405
-
3406
- function startChar(str, char) {
3407
- return (str[0] === char) ? '' : char
3408
- }
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()]
3409
3323
 
3410
- function endChar(str, char) {
3411
- return (str[str.length -1] === char) ? '' : char
3412
- }
3324
+ if (!theFunction) throw new Error(`Function "${functionName}" not found`)
3413
3325
 
3414
- function isSurroundedByQuotes(str) {
3415
- if (!str || str.length < 2) return false
3416
- const firstChar = str[0]
3417
- const lastChar = str[str.length - 1]
3418
- return (firstChar === "'" && lastChar === "'") || (firstChar === '"' && lastChar === '"')
3419
- }
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
+ }
3420
3334
 
3421
- function startsWithQuotedPipe(str) {
3422
- // Matches either 'xyz' | or "xyz" |
3423
- 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
+ }
3424
3342
  }
3425
3343
 
3426
3344
  module.exports = Configorama