configorama 0.6.9 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const os = require('os')
2
2
  const path = require('path')
3
3
  const fs = require('fs')
4
+ const enrichMetadata = require('./utils/enrichMetadata')
4
5
  /* // disable logs to find broken tests
5
6
  console.log = () => {}
6
7
  // process.exit(1)
@@ -14,6 +15,7 @@ const traverse = require('traverse')
14
15
  const dotProp = require('dot-prop')
15
16
  const chalk = require('./utils/chalk')
16
17
  const { resolveAlias } = require('./utils/resolveAlias')
18
+ const { resolveFilePathFromMatch } = require('./utils/getFullFilePath')
17
19
 
18
20
  /* Default Value resolvers */
19
21
  const getValueFromString = require('./resolvers/valueFromString')
@@ -27,6 +29,7 @@ const createGitResolver = require('./resolvers/valueFromGit')
27
29
  const YAML = require('./parsers/yaml')
28
30
  const TOML = require('./parsers/toml')
29
31
  const INI = require('./parsers/ini')
32
+ const JSON5 = require('./parsers/json5')
30
33
  /* functions */
31
34
  const md5Function = require('./functions/md5')
32
35
 
@@ -50,13 +53,16 @@ const { splitCsv } = require('./utils/splitCsv')
50
53
  const { replaceAll } = require('./utils/replaceAll')
51
54
  const { getTextAfterOccurrence, findNestedVariable } = require('./utils/textUtils')
52
55
  const { getFallbackString, verifyVariable } = require('./utils/variableUtils')
53
- const { encodeUnknown, decodeUnknown } = require('./utils/unknownValues')
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')
54
59
  const { mergeByKeys } = require('./utils/mergeByKeys')
55
60
  const { arrayToJsonPath } = require('./utils/arrayToJsonPath')
56
61
  const { findNestedVariables } = require('./utils/find-nested-variables')
57
62
  const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
58
63
  const { logHeader } = require('./utils/logs')
59
64
  const { createEditorLink } = require('./utils/createEditorLink')
65
+ const { runConfigWizard } = require('./utils/configWizard')
60
66
  /**
61
67
  * Maintainer's notes:
62
68
  *
@@ -90,6 +96,7 @@ const logLines = '────────────────────
90
96
 
91
97
  let DEBUG = process.argv.includes('--debug') ? true : false
92
98
  let VERBOSE = process.argv.includes('--verbose') ? true : false
99
+ let SETUP_MODE = process.argv.includes('--setup') ? true : false
93
100
  // DEBUG = true
94
101
  let DEBUG_TYPE = false
95
102
  const ENABLE_FUNCTIONS = true
@@ -123,6 +130,7 @@ function preProcess(configObject, variableSyntax) {
123
130
  if (typeof str !== 'string') return str
124
131
 
125
132
  let result = str
133
+ // result = result.replace(/\$\{self:/g, '${')
126
134
  let changed = true
127
135
 
128
136
  // Keep iterating until no more changes (to handle nested variables)
@@ -248,11 +256,18 @@ class Configorama {
248
256
  allowUnknownVars: false,
249
257
  // Allow undefined to be an end result.
250
258
  allowUndefinedValues: false,
259
+ // Allow unknown file refs to pass through without throwing errors
260
+ allowUnknownFileRefs: false,
261
+ // Return metadata
262
+ returnMetadata: false,
263
+ // Return preResolvedVariableDetails
264
+ returnPreResolvedVariableDetails: false,
251
265
  }, options)
252
266
 
253
267
  this.filterCache = {}
254
268
 
255
269
  this.foundVariables = []
270
+ this.fileRefsFound = []
256
271
 
257
272
  // Track variable resolutions for metadata (keyed by path)
258
273
  this.resolutionTracking = {}
@@ -343,6 +358,8 @@ class Configorama {
343
358
  {
344
359
  type: 'self',
345
360
  prefix: 'self',
361
+ syntax: '${self:pathToKeyInConfig}',
362
+ description: `Resolves values from the current config object. Supports sub-properties via :key lookup.`,
346
363
  match: selfRefSyntax,
347
364
  resolver: (varString, o, x, pathValue) => {
348
365
  return this.getValueFromSelf(varString, o, x, pathValue)
@@ -357,6 +374,8 @@ class Configorama {
357
374
  {
358
375
  type: 'file',
359
376
  prefix: 'file',
377
+ syntax: '${file(pathToFile.json)}',
378
+ description: `Resolves values from files. Supports sub-properties via :key lookup.`,
360
379
  match: fileRefSyntax,
361
380
  resolver: (varString, o, x, pathValue) => {
362
381
  return this.getValueFromFile(varString, { context: pathValue })
@@ -387,6 +406,7 @@ class Configorama {
387
406
  /* Resolve deep references */
388
407
  {
389
408
  type: 'deep',
409
+ internal: true,
390
410
  match: deepRefSyntax,
391
411
  resolver: (varString, o, x, pathValue) => {
392
412
  // console.log('>>>>>getValueFromDeep', varString)
@@ -399,23 +419,23 @@ class Configorama {
399
419
 
400
420
  /* Nicer self: references. Match key in object */
401
421
  const fallThroughSelfMatcher = {
402
- type: 'fallthrough',
422
+ type: 'dot.prop',
403
423
  match: (varString, fullObject, valueObject) => {
404
424
  /*
405
- console.log('fallthrough varString', varString)
406
- console.log('fallthrough valueObject', valueObject)
425
+ console.log('fallThroughSelfMatcher varString', varString)
426
+ console.log('fallThroughSelfMatcher valueObject', valueObject)
407
427
  console.log('fullObject', fullObject)
408
428
  /** */
409
429
  /* its file ref so we need to shift lookup for self in nested files */
410
430
  if (valueObject.isFileRef) {
411
431
  const exists = dotProp.get(fullObject, varString)
412
- // console.log('fallthrough exists', exists)
432
+ // console.log('fallThroughSelfMatcher exists', exists)
413
433
  if (!exists) {
414
434
  // @ Todo make recursive
415
435
  const deepProperties = [valueObject.path[0]].concat(varString)
416
436
  const dotPropPath = deepProperties.join('.')
417
437
  const deeperExists = dotProp.get(fullObject, dotPropPath)
418
- // console.log('fallthrough deeper', deeperExists)
438
+ // console.log('fallThroughSelfMatcher deeper', deeperExists)
419
439
  return deeperExists
420
440
  }
421
441
  }
@@ -426,10 +446,10 @@ class Configorama {
426
446
  },
427
447
  resolver: (varString, options, config, pathValue) => {
428
448
  /*
429
- console.log('fallthrough resolver', varString)
430
- console.log('fallthrough options', options)
431
- console.log('fallthrough config', config)
432
- console.log('fallthrough pathValue', pathValue)
449
+ console.log('fallThroughSelfMatcher resolver', varString)
450
+ console.log('fallThroughSelfMatcher options', options)
451
+ console.log('fallThroughSelfMatcher config', config)
452
+ console.log('fallThroughSelfMatcher pathValue', pathValue)
433
453
  /** */
434
454
  return this.getValueFromSelf(varString, options, config, pathValue)
435
455
  },
@@ -437,6 +457,19 @@ class Configorama {
437
457
 
438
458
  /* Apply user defined variable sources */
439
459
  if (options.variableSources) {
460
+
461
+ // ensure each variable source has a type
462
+ options.variableSources.forEach((v) => {
463
+ if (!v.type) {
464
+ console.log('Variable', v)
465
+ throw new Error('Variable source must have a type')
466
+ }
467
+ if (!v.match || !v.resolver) {
468
+ console.log('Variable', v)
469
+ throw new Error('Variable source must have a match and resolver functions')
470
+ }
471
+ })
472
+
440
473
  this.variableTypes = this.variableTypes.concat(options.variableSources)
441
474
  }
442
475
 
@@ -482,7 +515,7 @@ class Configorama {
482
515
  toKebabCase: (val) => {
483
516
  return kebabCase(val)
484
517
  },
485
- /* Type filters */
518
+ /* Type filters for coercion */
486
519
  toNumber: (val, from) => {
487
520
  const newVal = Number(val)
488
521
  return newVal
@@ -497,7 +530,37 @@ class Configorama {
497
530
  return JSON.stringify(val)
498
531
  },
499
532
  toObject: (val) => {
500
- return JSON.parse(val)
533
+ return JSON5.parse(val)
534
+ },
535
+ /* Type validation filters */
536
+ Number: (value) => {
537
+ const n = Number(value)
538
+ if (isNaN(n)) throw new Error(`Configorama Error: Expected Number, got "${value}"`)
539
+ return n
540
+ },
541
+ Boolean: (value) => {
542
+ if (typeof value === 'boolean') return value
543
+ 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
546
+ throw new Error(`Configorama Error: Expected Boolean, got "${value}"`)
547
+ },
548
+ String: (value) => {
549
+ if (value === undefined || value === null || value === 'null') return ''
550
+ return String(value)
551
+ },
552
+ Json: (value) => {
553
+ try {
554
+ return typeof value === 'string' ? JSON.parse(value) : value
555
+ } catch (e) {
556
+ throw new Error(`Configorama Error: Invalid JSON in variable`)
557
+ }
558
+ },
559
+ /* Help filter - identity function that preserves value but provides metadata for wizard */
560
+ help: (value, helpText) => {
561
+ // Identity function - returns value unchanged
562
+ // The helpText argument is extracted during metadata collection for the wizard
563
+ return value
501
564
  },
502
565
  }
503
566
 
@@ -507,7 +570,9 @@ class Configorama {
507
570
  }
508
571
 
509
572
  // (\|\s*(toUpperCase|toLowerCase|toCamelCase|toKebabCase|capitalize)\s*)+$
510
- this.filterMatch = new RegExp(`(\\|\\s*(${Object.keys(this.filters).join('|')})\\s*)+}?$`)
573
+ // Updated to support function-style filters like help('text') with nested parens
574
+ // 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*)+}?$`)
511
576
  // console.log('this.filterMatch', this.filterMatch)
512
577
 
513
578
  this.functions = {
@@ -603,7 +668,7 @@ class Configorama {
603
668
  this.opts
604
669
  )
605
670
  this.configFileContents = ''
606
- if (VERBOSE || showFoundVariables) {
671
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
607
672
  this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
608
673
  }
609
674
  /*
@@ -630,16 +695,37 @@ class Configorama {
630
695
  const variableSyntax = this.variableSyntax
631
696
  const variablesKnownTypes = this.variablesKnownTypes
632
697
 
633
- if (VERBOSE || showFoundVariables) {
698
+ if (VERBOSE || showFoundVariables || this.opts.returnPreResolvedVariableDetails) {
634
699
  // Use collectVariableMetadata to get variable info (DRY - don't duplicate logic)
635
700
  const metadata = this.collectVariableMetadata()
636
- /*
637
- deepLog('metadata', metadata)
638
- process.exit(1)
639
- /** */
640
-
701
+
702
+ const enrich = enrichMetadata(
703
+ metadata,
704
+ this.resolutionTracking,
705
+ this.variableSyntax,
706
+ this.fileRefsFound,
707
+ this.originalConfig,
708
+ this.configFilePath,
709
+ Object.keys(this.filters)
710
+ )
711
+
712
+ if (showFoundVariables) {
713
+ /*
714
+ deepLog('metadata', metadata)
715
+ fs.writeFileSync(`metadata-${path.basename(this.configFilePath)}.json`, JSON.stringify(metadata, null, 2))
716
+ deepLog('enrich', enrich)
717
+ // process.exit(1)
718
+ /** */
719
+ }
720
+
641
721
  const variableData = metadata.variables
722
+ const uniqueVariables = metadata.uniqueVariables
642
723
  const varKeys = Object.keys(variableData)
724
+ const uniqueVarKeys = Object.keys(uniqueVariables)
725
+
726
+ // if (this.opts.returnPreResolvedVariableDetails) {
727
+ // return metadata
728
+ // }
643
729
 
644
730
  if (!varKeys.length) {
645
731
  logHeader('No Variables Found in Config')
@@ -651,9 +737,9 @@ class Configorama {
651
737
 
652
738
  const varTypes = Object.keys(this.variableTypes)
653
739
  if (varTypes.length) {
654
- const exclude = ['fallthrough', 'deep']
740
+ const exclude = ['dot.prop', 'deep']
655
741
  console.log('\nAllowed variable types:')
656
- varTypes.filter((v) => v.type !== 'fallthrough').forEach((v) => {
742
+ varTypes.filter((v) => v.type !== 'dot.prop').forEach((v) => {
657
743
  const vData = this.variableTypes[v]
658
744
  if (exclude.includes(vData.type)) {
659
745
  return
@@ -676,166 +762,231 @@ class Configorama {
676
762
  const longestKey = varKeys.reduce((acc, k) => {
677
763
  return Math.max(acc, k.length)
678
764
  }, 0)
679
- // Count all references including nested ones within other variables
680
- const countAllReferences = (targetVariable) => {
681
- // Start with direct references
682
- let count = variableData[targetVariable].length
683
-
684
- // Check all other variables for nested references to this variable
685
- varKeys.forEach((otherKey) => {
686
- if (otherKey === targetVariable) return
687
-
688
- variableData[otherKey].forEach((instance) => {
689
- if (instance.resolveDetails) {
690
- instance.resolveDetails.forEach((detail) => {
691
- // Check if this resolveDetail references our target variable
692
- if (detail.fullMatch === targetVariable) {
693
- count++
694
- }
695
- })
696
- }
697
- })
698
- })
699
-
700
- return count
701
- }
702
765
 
703
- console.log(varKeys.map((k) => {
704
- const refCount = countAllReferences(k)
766
+ // Use uniqueVariables for simpler reference counting
767
+ 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()
771
+ const uniqueVar = uniqueVariables[varName]
772
+ const refCount = uniqueVar ? uniqueVar.occurrences.length : variableData[k].length
705
773
  const placesWord = refCount > 1 ? 'places' : 'place'
706
774
  return `- ${k.padEnd(longestKey).padEnd(longestKey + 10)} referenced ${refCount} ${placesWord}`
707
- }).join('\n'))
708
- console.log()
775
+ }).join('\n')
776
+
777
+ console.log(`${referenceData}\n`)
709
778
  }
710
779
 
711
780
  logHeader('Variable Details')
712
781
 
713
- const lines = this.configFileContents.split('\n')
714
- // console.log('lines', lines)
715
-
782
+ const lines = this.configFileContents ? this.configFileContents.split('\n') : []
783
+
716
784
  const indent = ''
717
785
  const boxes = varKeys.map((key, i) => {
718
786
  const variableInstances = variableData[key]
719
787
  // console.log('variableInstances', variableInstances)
720
-
721
788
  const firstInstance = variableInstances[0]
722
789
 
723
- let requiredText = ''
724
- let defaultValueSrc = ''
725
- if (typeof firstInstance.defaultValue === 'undefined') {
726
- // console.log('no default value', firstInstance)
727
-
728
- let dotPropArr = []
729
- if (firstInstance.defaultValueIsVar && (
730
- firstInstance.defaultValueIsVar.varType === 'self:' ||
731
- firstInstance.defaultValueIsVar.varType === 'dot.prop'
732
- )) {
733
- dotPropArr = [firstInstance.defaultValueIsVar]
734
- }
735
- /* Check if the fallback variable is a self reference */
736
- const hasDotPropOrSelf = variableInstances.reduce((acc, v) => {
737
- const dotProp = v.resolveDetails.find((d) => {
738
- // console.log('d', d)
739
- return d.varType === 'dot.prop'
740
- })
741
- if (dotProp) {
742
- acc.push(dotProp)
743
- }
744
- if (v.resolveDetails && v.resolveDetails.length === 1 && v.resolveDetails[0].varType === 'self:') {
745
- // console.log('dot.prop', v.resolveDetails)
746
- acc.push(v.resolveDetails[0])
747
- }
748
- return acc
749
- }, dotPropArr)
750
- // console.log('hasDotPropOrSelf', hasDotPropOrSelf)
751
-
752
- if (!hasDotPropOrSelf.length) {
753
- const debug = (false) ? JSON.stringify(firstInstance, null, 2) : ''
754
- requiredText = `[Required Variable] ${debug}`
755
- } else {
756
- const fallBackValues = variableInstances.filter((v) => v.resolveDetails.find((d) => d.hasFallback)).map((v) => v.resolveDetails)
757
- // console.log('fallBackValues', fallBackValues)
758
- if (fallBackValues.length) {
759
- // console.log('fallBackValues.resolveDetails', fallBackValues)
760
- }
790
+ // Get uniqueVariable data for description and other metadata
791
+ const varName = key.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
792
+ const uniqueVar = uniqueVariables[varName]
761
793
 
762
- const cleanPath = hasDotPropOrSelf[0].variable.replace('self:', '')
763
- defaultValueSrc = cleanPath
764
- // Find the dot prop value in the original config
765
- const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
766
- // console.log('dotPropValue', dotPropValue)
767
- if (typeof dotPropValue !== 'undefined') {
768
- requiredText = ''
769
- const niceString = typeof dotPropValue === 'object' ? JSON.stringify(dotPropValue) : dotPropValue
770
- // truncate niceString to 100 characters
771
- const truncatedString = niceString.length > 100 ? niceString.substring(0, 90) + '...' : niceString
772
- firstInstance.defaultValue = truncatedString
773
- } else {
774
- deepLog('Missing default var', firstInstance)
775
- throw new Error(
776
- `Variable misconfiguration at ${firstInstance.variable}\n\n"${hasDotPropOrSelf[0].variable}" resolves to undefined value.\n`
777
- )
778
- }
779
- }
780
- //this.originalConfig[key] = undefined
781
- }
794
+ // Build display message from enriched metadata
782
795
  const spacing = ' '
783
796
  const titleText = `Variable:${spacing}`
784
- const reqText = (requiredText) ? `${chalk.red.bold(requiredText)}\n` : ''
785
- let varMsg = `${reqText}`
786
- const VALUE_HEX = '#899499' // '#708090'
797
+ const VALUE_HEX = '#899499'
787
798
  const keyChalk = chalk.whiteBright
788
799
  const valueChalk = chalk.hex(VALUE_HEX)
789
800
 
801
+ let varMsg = ''
802
+ let requiredMessage = ''
803
+
804
+ // Show required status from metadata
805
+ if (firstInstance.isRequired) {
806
+ requiredMessage = `${chalk.red.bold('[Required]')}`
807
+ }
808
+
809
+ // 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
+ }
828
+ }
829
+
830
+ // 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
+ }
842
+ }
843
+
844
+
845
+
846
+ // Show default value from metadata
790
847
  if (typeof firstInstance.defaultValue !== 'undefined') {
791
- // console.log('firstInstance.defaultValue', firstInstance.defaultValue)
792
848
  const defaultValueRender = firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue
793
- const defaultValueText = `${indent}${keyChalk(`Default value:`.padEnd(titleText.length, ' '))}`
794
- // ensure padding is even
795
- varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}`
849
+ const defaultValueText = `${indent}${keyChalk('Default value:'.padEnd(titleText.length, ' '))}`
850
+ varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}\n`
796
851
  }
797
852
 
798
- if (defaultValueSrc) {
799
- varMsg += `\n${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
800
- varMsg += `${valueChalk(defaultValueSrc)}`
853
+ // Show default value source path from metadata
854
+ if (firstInstance.defaultValueSrc) {
855
+ varMsg += `${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
856
+ varMsg += `${valueChalk(firstInstance.defaultValueSrc)}\n`
801
857
  }
802
858
 
859
+ // Show resolve order from metadata
803
860
  if (firstInstance.resolveOrder.length > 1) {
804
- varMsg += `\n${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
861
+ varMsg += `${indent}${keyChalk('Resolve Order:'.padEnd(titleText.length, ' '))}`
805
862
  const resolveOrder = firstInstance.resolveOrder.join(', ')
806
- varMsg += ` ${valueChalk(resolveOrder)}`
863
+ varMsg += ` ${valueChalk(resolveOrder)}\n`
807
864
  }
808
865
 
866
+ // Show path(s) from metadata
809
867
  let locationRender = valueChalk(variableInstances[0].path)
810
-
811
- let locationLabel = `${indent}${keyChalk('Path:'.padEnd(titleText.length, ' '))}`
868
+ let locationLabel = `${indent}${keyChalk('Config Path:'.padEnd(titleText.length, ' '))}`
869
+ let typeText = ''
812
870
  if (variableInstances.length > 1) {
813
- locationRender = `\n${variableInstances.map((v) => valueChalk(`${indent}- ${v.path}`)).join('\n')}`
814
- const locationLabelText = `${indent}${keyChalk('Paths:')}`
815
- locationLabel = locationLabelText
871
+ const pathIndent = ' '.repeat(titleText.length + 1)
872
+ const pathItems = variableInstances.map((v, idx) => {
873
+ // Show type filter per path if different
874
+ if (uniqueVar && uniqueVar.occurrences.length > 1) {
875
+ 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
+
881
+ typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
882
+ const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
883
+ return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
884
+ }
885
+ const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
886
+ return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
887
+ })
888
+ locationRender = pathItems.join('\n')
889
+ locationLabel = `${indent}${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
890
+ } 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
+
897
+ typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
816
898
  }
899
+ varMsg += `${locationLabel} ${locationRender}`
900
+
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
817
930
 
818
- varMsg += `\n${locationLabel} ${locationRender}`
819
931
 
820
- // find the match in our lines
821
- const line = lines.findIndex((line) => line.includes(key))
822
- const lineNumber = line + 1
823
-
824
- // console.log(` ${chalk.bold(key)}`)
825
932
  return {
826
- text: varMsg,
933
+ content: {
934
+ left: varMsg,
935
+ backgroundColor: 'red',
936
+ width: '100%',
937
+ },
827
938
  title: {
828
- left: `▶ ${lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, key) : key}`,
829
- right: lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, `Line: ${lineNumber}`, 'gray') : '',
939
+ left: `▷ ${lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, key) : key}`,
940
+ right: lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, `${requiredMessage} ${lineNumber ? `Line: ${lineNumber.toString().padEnd(2, ' ')}` : ''}`, 'gray') : '',
941
+ center: typeText,
942
+ paddingBottom: 1,
943
+ paddingTop: (i === 0) ? 1 : 0,
944
+ truncate: true,
830
945
  },
946
+ width: '100%',
831
947
  }
832
948
  })
833
949
 
834
950
  console.log(makeStackedBoxes(boxes, {
951
+ borderText: 'Variable Details. Click on titles to open in editor.',
835
952
  borderColor: 'gray',
836
- minWidth: 120,
953
+ minWidth: '96%',
837
954
  borderStyle: 'bold',
955
+ disableTitleSeparator: true,
838
956
  }))
957
+ // process.exit(1)
958
+ }
959
+
960
+
961
+ // WALK through CLI prompt if --setup flag is set
962
+ if (SETUP_MODE) {
963
+ logHeader('Setup Mode')
964
+ // deepLog('enrich', enrich)
965
+ const userInputs = await runConfigWizard(enrich, this.originalConfig, this.configFilePath)
966
+
967
+ console.log('\n')
968
+ logHeader('User Inputs Summary')
969
+ console.log(JSON.stringify(userInputs, null, 2))
970
+
971
+ // TODO set values
972
+
973
+ // Apply user inputs to options and environment
974
+ if (userInputs.options) {
975
+ Object.assign(this.opts, userInputs.options)
976
+ }
977
+ if (userInputs.env) {
978
+ Object.assign(process.env, userInputs.env)
979
+ }
980
+ // Note: self references are in the config, so no need to apply them
981
+
982
+ console.log()
983
+ logHeader('Resolving Configuration')
984
+ console.log()
985
+
986
+ // process.exit(1)
987
+
988
+ // Continue with normal resolution flow using the new values
989
+ // Don't exit - let it fall through to resolve the config
839
990
  }
840
991
 
841
992
  /* Exit early if list or info flag is set */
@@ -931,9 +1082,8 @@ class Configorama {
931
1082
  }
932
1083
 
933
1084
  /* fix for file(JS-ref.js, raw) to keep parens and inline code */
934
- const OPEN_PAREN_PLACEHOLDER_PATTERN = /__PH_PAREN_OPEN__/g
935
- if (rawValue.match(OPEN_PAREN_PLACEHOLDER_PATTERN)) {
936
- rawValue = rawValue.replace(OPEN_PAREN_PLACEHOLDER_PATTERN, '(')
1085
+ if (hasParenthesesPlaceholder(rawValue)) {
1086
+ rawValue = decodeJsSyntax(rawValue)
937
1087
  this.update(rawValue)
938
1088
  }
939
1089
 
@@ -977,6 +1127,8 @@ class Configorama {
977
1127
  collectVariableMetadata() {
978
1128
  const variableSyntax = this.variableSyntax
979
1129
  const variablesKnownTypes = this.variablesKnownTypes
1130
+ const variableTypes = this.variableTypes
1131
+ const filterMatch = this.filterMatch
980
1132
  const foundVariables = []
981
1133
  const variableData = {}
982
1134
  const fileRefs = []
@@ -991,29 +1143,117 @@ class Configorama {
991
1143
  return
992
1144
  }
993
1145
 
994
- const nested = findNestedVariables(rawValue, variableSyntax, variablesKnownTypes, configValuePath)
1146
+ const nested = findNestedVariables(
1147
+ rawValue,
1148
+ variableSyntax,
1149
+ variablesKnownTypes,
1150
+ configValuePath,
1151
+ variableTypes
1152
+ )
995
1153
 
996
1154
  const lastItem = nested[nested.length - 1]
997
1155
  const lastKeyPath = this.path[this.path.length - 1]
998
1156
  const itemKey = (lastKeyPath.match(/[\d+]$/)) ? `${this.path[this.path.length - 2]}[${lastKeyPath}]` : lastKeyPath
999
- const key = lastItem.fullMatch
1157
+
1158
+ // Extract filters from varMatch
1159
+ const originalSrc = lastItem.varMatch || ''
1160
+ const hasFilters = filterMatch && originalSrc.match(filterMatch)
1161
+ let foundFilters = []
1162
+ let keyWithoutFilters = originalSrc
1163
+
1164
+ if (hasFilters) {
1165
+ // Extract filter names from the match (e.g., "| String}" -> ["String"])
1166
+ const filterPart = hasFilters[0].replace(/}?$/, '') // Remove trailing }
1167
+ foundFilters = filterPart
1168
+ .split('|')
1169
+ .map((filter) => filter.trim())
1170
+ .filter(Boolean)
1171
+
1172
+ // Remove filters from the key (replace "| String}" with "}")
1173
+ // Also clean up any trailing whitespace before the closing brace
1174
+ keyWithoutFilters = originalSrc.replace(filterMatch, '}').replace(/\s+}$/, '}')
1175
+ }
1176
+
1177
+ const key = keyWithoutFilters
1178
+
1179
+ // Strip filters from resolveDetails
1180
+ const cleanedResolveDetails = nested.map(detail => {
1181
+ const cleaned = { ...detail }
1182
+ if (cleaned.varMatch && filterMatch) {
1183
+ const match = cleaned.varMatch.match(filterMatch)
1184
+ if (match) {
1185
+ cleaned.varMatch = cleaned.varMatch.replace(filterMatch, '').replace(/\s+$/, '') + '}'
1186
+ }
1187
+ }
1188
+ if (cleaned.variable && filterMatch) {
1189
+ const match = cleaned.variable.match(filterMatch)
1190
+ if (match) {
1191
+ cleaned.variable = cleaned.variable.replace(filterMatch, '').replace(/\s+$/, '')
1192
+ }
1193
+ }
1194
+ if (cleaned.varString && filterMatch) {
1195
+ const match = cleaned.varString.match(filterMatch)
1196
+ if (match) {
1197
+ cleaned.varString = cleaned.varString.replace(filterMatch, '').trim()
1198
+ }
1199
+ }
1200
+ // Also clean fallbackValues if present
1201
+ if (cleaned.fallbackValues && Array.isArray(cleaned.fallbackValues)) {
1202
+ cleaned.fallbackValues = cleaned.fallbackValues.map(fb => {
1203
+ const cleanedFb = { ...fb }
1204
+ if (cleanedFb.varMatch && filterMatch) {
1205
+ const match = cleanedFb.varMatch.match(filterMatch)
1206
+ if (match) {
1207
+ cleanedFb.varMatch = cleanedFb.varMatch.replace(filterMatch, '').trim()
1208
+ }
1209
+ }
1210
+ if (cleanedFb.variable && filterMatch) {
1211
+ const match = cleanedFb.variable.match(filterMatch)
1212
+ if (match) {
1213
+ cleanedFb.variable = cleanedFb.variable.replace(filterMatch, '').trim()
1214
+ }
1215
+ }
1216
+ if (cleanedFb.stringValue && filterMatch) {
1217
+ const match = cleanedFb.stringValue.match(filterMatch)
1218
+ if (match) {
1219
+ cleanedFb.stringValue = cleanedFb.stringValue.replace(filterMatch, '').trim()
1220
+ }
1221
+ }
1222
+ return cleanedFb
1223
+ })
1224
+ }
1225
+ return cleaned
1226
+ })
1227
+
1000
1228
  const varData = {
1001
1229
  path: configValuePath,
1002
1230
  key: itemKey,
1003
- value: rawValue,
1004
- variable: lastItem.fullMatch,
1231
+ originalStringValue: rawValue,
1232
+ variable: keyWithoutFilters,
1233
+ variableWithFilters: originalSrc,
1005
1234
  isRequired: false,
1006
1235
  defaultValue: undefined,
1007
1236
  matchIndex: matchCount++,
1008
1237
  resolveOrder: [],
1009
- resolveDetails: nested,
1238
+ resolveDetails: cleanedResolveDetails,
1239
+ ...(foundFilters.length > 0 && { filters: foundFilters }),
1010
1240
  }
1011
1241
  let defaultValueIsVar = false
1012
1242
 
1013
1243
  function calculateResolveOrder(item) {
1244
+ // Helper to strip filters from variable strings
1245
+ const stripFilters = (str) => {
1246
+ if (!str || !filterMatch) return str
1247
+ const match = str.match(filterMatch)
1248
+ if (match) {
1249
+ return str.replace(filterMatch, '').trim()
1250
+ }
1251
+ return str
1252
+ }
1253
+
1014
1254
  if (item && item.fallbackValues) {
1015
1255
  let hasResolvedFallback
1016
- const order = ([item.valueBeforeFallback]).concat(item.fallbackValues.map((f, i) => {
1256
+ const order = ([stripFilters(item.valueBeforeFallback)]).concat(item.fallbackValues.map((f, i) => {
1017
1257
  if (f.fallbackValues) {
1018
1258
  const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
1019
1259
  if (!hasResolvedFallback && nestedResolvedFallback) {
@@ -1023,21 +1263,22 @@ class Configorama {
1023
1263
  }
1024
1264
 
1025
1265
  if (!hasResolvedFallback && f.isResolvedFallback) {
1026
- hasResolvedFallback = f.stringValue
1266
+ hasResolvedFallback = stripFilters(f.stringValue)
1027
1267
  }
1028
1268
  if (f.isResolvedFallback) {
1029
- hasResolvedFallback = f.stringValue
1269
+ hasResolvedFallback = stripFilters(f.stringValue)
1030
1270
  }
1031
1271
 
1032
1272
  if (!hasResolvedFallback && f.isVariable) {
1033
1273
  defaultValueIsVar = f
1034
1274
  }
1035
- return `${f.stringValue || f.variable}${f.isResolvedFallback ? ' (Resolved default fallback)' : ''}`
1275
+ const valueStr = stripFilters(f.stringValue || f.variable)
1276
+ return `${valueStr}${f.isResolvedFallback ? ' (default)' : ''}`
1036
1277
  })).flat()
1037
1278
 
1038
1279
  return [order, hasResolvedFallback]
1039
1280
  }
1040
- return [[item.variable], undefined]
1281
+ return [[stripFilters(item.variable)], undefined]
1041
1282
  }
1042
1283
 
1043
1284
  const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
@@ -1061,10 +1302,9 @@ class Configorama {
1061
1302
 
1062
1303
  // Extract file references
1063
1304
  nested.forEach((detail) => {
1064
- if (detail.varType &&
1065
- (detail.varType.startsWith('file(') || detail.varType.startsWith('text('))
1066
- ) {
1067
- const fileMatch = detail.varType.match(/^(?:file|text)\((.*?)\)/)
1305
+ // console.log('detail', detail)
1306
+ if (detail.variableType && (detail.variableType === 'file' || detail.variableType === 'text')) {
1307
+ const fileMatch = detail.variable.match(/^(?:file|text)\((.*?)\)/)
1068
1308
  if (fileMatch && fileMatch[1]) {
1069
1309
  let fileContent = fileMatch[1].trim()
1070
1310
 
@@ -1130,19 +1370,19 @@ class Configorama {
1130
1370
  // Check for self-references that resolve to config values
1131
1371
  let dotPropArr = []
1132
1372
  if (firstInstance.defaultValueIsVar && (
1133
- firstInstance.defaultValueIsVar.varType === 'self:' ||
1134
- firstInstance.defaultValueIsVar.varType === 'dot.prop'
1373
+ firstInstance.defaultValueIsVar.variableType === 'self:' ||
1374
+ firstInstance.defaultValueIsVar.variableType === 'dot.prop'
1135
1375
  )) {
1136
1376
  dotPropArr = [firstInstance.defaultValueIsVar]
1137
1377
  }
1138
1378
 
1139
1379
  const hasDotPropOrSelf = instances.reduce((acc, v) => {
1140
- const dotProp = v.resolveDetails.find((d) => d.varType === 'dot.prop')
1141
- if (dotProp) {
1142
- acc.push(dotProp)
1143
- }
1144
- if (v.resolveDetails && v.resolveDetails.length === 1 && v.resolveDetails[0].varType === 'self:') {
1145
- acc.push(v.resolveDetails[0])
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
+ }
1146
1386
  }
1147
1387
  return acc
1148
1388
  }, dotPropArr)
@@ -1155,10 +1395,20 @@ class Configorama {
1155
1395
  const dotPropValue = dotProp.get(this.originalConfig, cleanPath)
1156
1396
  if (typeof dotPropValue === 'undefined') {
1157
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
1158
1405
  }
1159
1406
  }
1160
1407
  }
1161
1408
 
1409
+ // Update isRequired based on computed isTrulyRequired
1410
+ firstInstance.isRequired = isTrulyRequired
1411
+
1162
1412
  if (isTrulyRequired) {
1163
1413
  requiredCount++
1164
1414
  } else {
@@ -1168,13 +1418,23 @@ class Configorama {
1168
1418
 
1169
1419
  return {
1170
1420
  variables: variableData,
1421
+ uniqueVariables: {},
1422
+ fileDependencies: {
1423
+ globPatterns: fileGlobPatterns,
1424
+ // all: fileRefs,
1425
+ 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,
1432
+ },
1171
1433
  summary: {
1172
1434
  totalVariables: varKeys.length,
1173
1435
  requiredVariables: requiredCount,
1174
1436
  variablesWithDefaults: withDefaultsCount
1175
1437
  },
1176
- fileRefs: fileRefs,
1177
- fileGlobPatterns: fileGlobPatterns,
1178
1438
  }
1179
1439
  }
1180
1440
  runFunction(variableString) {
@@ -1198,6 +1458,7 @@ class Configorama {
1198
1458
  let argsToPass
1199
1459
  if (rawArgs && rawArgs.match(/^{([^}]+)}$/)) {
1200
1460
  // console.log('OBJECT', hasFunc[2])
1461
+ // TODO use JSON5
1201
1462
  argsToPass = [JSON.parse(rawArgs)]
1202
1463
  } else {
1203
1464
  // TODO fix how commas + spaces are ned
@@ -1464,48 +1725,79 @@ class Configorama {
1464
1725
  if (matches.length === 1) {
1465
1726
  valueObject.currentVarDetails = matches[0]
1466
1727
  valueObject.currentVarDetails.result = results[0]
1728
+ }
1729
+
1730
+ // Initialize resolution history if needed
1731
+ if (!valueObject.resolutionHistory) {
1732
+ valueObject.resolutionHistory = []
1733
+ }
1734
+
1735
+ let result = valueObject.value
1736
+ for (let i = 0; i < matches.length; i += 1) {
1737
+ this.warnIfNotFound(matches[i].variable, results[i])
1467
1738
 
1468
1739
  // Extract metadata from result if present
1469
- let actualResult = results[0]
1740
+ let actualResult = results[i]
1470
1741
  let resolverType = undefined
1471
- if (results[0] && typeof results[0] === 'object') {
1472
- if (results[0].__internal_metadata) {
1473
- actualResult = results[0].value
1474
- resolverType = results[0].__resolverType
1475
- } else if (results[0].__internal_only_flag) {
1476
- // Don't extract value from __internal_only_flag objects, but grab resolverType if present
1477
- actualResult = results[0]
1478
- resolverType = results[0].__resolverType
1742
+ if (results[i] && typeof results[i] === 'object') {
1743
+ if (results[i].__internal_metadata) {
1744
+ actualResult = results[i].value
1745
+ resolverType = results[i].__resolverType
1746
+ } else if (results[i].__internal_only_flag) {
1747
+ actualResult = results[i]
1748
+ resolverType = results[i].__resolverType
1479
1749
  }
1480
1750
  }
1481
- // valueObject.currentVarDetails.varType = results[0].__resolverType
1482
1751
 
1483
- // Track resolution history
1484
- if (!valueObject.resolutionHistory) {
1485
- valueObject.resolutionHistory = []
1486
- }
1487
-
1488
1752
  // Extract clean result to avoid circular references
1489
- // For __internal_only_flag objects (like deep resolver results), extract the value
1490
- // For real data objects (like file contents), keep them as-is
1491
1753
  let cleanResult = actualResult
1492
1754
  if (actualResult && typeof actualResult === 'object' && actualResult.__internal_only_flag) {
1493
1755
  cleanResult = actualResult.value
1494
1756
  }
1495
-
1496
- const historyEntry = {
1497
- match: matches[0].match,
1498
- variable: matches[0].variable,
1499
- result: cleanResult,
1500
- resultType: typeof cleanResult,
1501
- valueBeforeResolution: valueObject.value,
1757
+
1758
+ let valueBeforeResolution = result
1759
+
1760
+ if (typeof valueBeforeResolution === 'object' && valueBeforeResolution.__internal_only_flag) {
1761
+ valueBeforeResolution = valueBeforeResolution.value
1762
+ }
1763
+
1764
+ const finalResult = decodeEncodedValue(cleanResult)
1765
+
1766
+ // Track this resolution step in history
1767
+ const historyEntry = {}
1768
+
1769
+ historyEntry.match = matches[i].match
1770
+ historyEntry.variable = matches[i].variable
1771
+ if (historyEntry.resultType === 'string' && historyEntry.result.match(/^>passthrough\[/)) {
1772
+ historyEntry.variableType = 'encodedUnknown'
1502
1773
  }
1503
1774
  if (resolverType) {
1504
- historyEntry.varType = resolverType
1775
+ historyEntry.variableType = resolverType
1776
+ }
1777
+ historyEntry.result = finalResult
1778
+
1779
+ const isDeepResult = typeof finalResult === 'string' && finalResult.match(/^\$\{deep:\d+\}$/)
1780
+
1781
+ if (isDeepResult) {
1782
+ historyEntry.resultAfterDeep = 'TBD'
1505
1783
  }
1506
1784
 
1785
+ historyEntry.resultType = typeof finalResult
1786
+ historyEntry.valueBeforeResolution = valueBeforeResolution
1787
+ historyEntry.from = 'renderMatches'
1788
+ if (isDeepResult) {
1789
+ historyEntry.resultIsDeep = true
1790
+ }
1791
+
1792
+ if (finalResult !== cleanResult) {
1793
+ historyEntry.resultEncoded = cleanResult
1794
+ }
1795
+
1796
+
1797
+
1798
+
1507
1799
  // Check if variable has fallback values (comma-separated)
1508
- const variableParts = splitByComma(matches[0].variable)
1800
+ const variableParts = splitByComma(matches[i].variable)
1509
1801
  if (variableParts.length > 1) {
1510
1802
  historyEntry.hasFallback = true
1511
1803
  historyEntry.valueBeforeFallback = variableParts[0]
@@ -1534,10 +1826,10 @@ class Configorama {
1534
1826
  fallbackData.isResolvedFallback = true
1535
1827
  }
1536
1828
  } else {
1537
- // Extract varType from variable references
1829
+ // Extract variableType from variable references
1538
1830
  const varTypeMatch = trimmedFallback.match(this.variablesKnownTypes)
1539
1831
  if (varTypeMatch && varTypeMatch[1]) {
1540
- fallbackData.varType = varTypeMatch[1]
1832
+ fallbackData.variableType = varTypeMatch[1]
1541
1833
  }
1542
1834
  }
1543
1835
 
@@ -1546,33 +1838,16 @@ class Configorama {
1546
1838
  }
1547
1839
 
1548
1840
  // Only add to history if not a duplicate (same match + variable)
1549
- const isDuplicate = valueObject.resolutionHistory.some(entry =>
1550
- entry.match === historyEntry.match &&
1841
+ const isDuplicate = valueObject.resolutionHistory.some(entry =>
1842
+ entry.match === historyEntry.match &&
1551
1843
  entry.variable === historyEntry.variable
1552
1844
  )
1553
-
1845
+
1554
1846
  if (!isDuplicate) {
1555
1847
  valueObject.resolutionHistory.push(historyEntry)
1556
1848
  }
1557
1849
 
1558
- // Save resolution history to tracking map for persistence across iterations
1559
- if (valueObject.path && valueObject.path.length) {
1560
- const pathKey = valueObject.path.join('.')
1561
- if (!this.resolutionTracking[pathKey]) {
1562
- this.resolutionTracking[pathKey] = {
1563
- path: pathKey,
1564
- originalPropertyString: valueObject.originalSource,
1565
- calls: []
1566
- }
1567
- }
1568
- this.resolutionTracking[pathKey].resolutionHistory = valueObject.resolutionHistory
1569
- }
1570
- }
1571
-
1572
- let result = valueObject.value
1573
- for (let i = 0; i < matches.length; i += 1) {
1574
- this.warnIfNotFound(matches[i].variable, results[i])
1575
- // console.log('Render MATCHES', results[i])
1850
+ // Process the match
1576
1851
  let valueToPop = results[i]
1577
1852
  // TODO refactor this. __internal_only_flag needed to stop clash with sync/async file resolution
1578
1853
  if (results[i] && typeof results[i] === 'object' && (results[i].__internal_only_flag || results[i].__internal_metadata)) {
@@ -1586,6 +1861,21 @@ class Configorama {
1586
1861
  console.log(this.deep)
1587
1862
  /** */
1588
1863
  }
1864
+
1865
+ // Save resolution history to tracking map for persistence across iterations
1866
+ if (valueObject.path && valueObject.path.length) {
1867
+ const pathKey = valueObject.path.join('.')
1868
+ if (!this.resolutionTracking[pathKey]) {
1869
+ this.resolutionTracking[pathKey] = {
1870
+ path: pathKey,
1871
+ originalPropertyString: valueObject.originalSource,
1872
+ resolvedPropertyValue: undefined,
1873
+ calls: []
1874
+ }
1875
+ }
1876
+ this.resolutionTracking[pathKey].resolutionHistory = valueObject.resolutionHistory
1877
+ }
1878
+
1589
1879
  return result
1590
1880
  }
1591
1881
  /**
@@ -1734,7 +2024,7 @@ class Configorama {
1734
2024
  if (currentDetails &&
1735
2025
  currentDetails.resultType === 'number' &&
1736
2026
  parentDetails && parentDetails.resultType === 'string' &&
1737
- parentDetails.result.match(/^\d+$/) && parentDetails.varType === 'env'
2027
+ parentDetails.result.match(/^\d+$/) && parentDetails.variableType === 'env'
1738
2028
  ) {
1739
2029
  if (Number(parentDetails.result) === currentDetails.result) {
1740
2030
  property = String(valueToPopulate)
@@ -1990,7 +2280,23 @@ Missing Value ${missingValue} - ${matchedString}
1990
2280
  })
1991
2281
  }
1992
2282
  property = foundFilters.reduce((acc, filter) => {
1993
- const newVal = this.filters[filter](acc, 'from populateVariable')
2283
+ // Check if filter has function-style arguments
2284
+ const funcMatch = filter.match(/^(\w+)\((.*)\)$/)
2285
+ let filterName = filter
2286
+ let filterArgs = []
2287
+
2288
+ if (funcMatch) {
2289
+ filterName = funcMatch[1]
2290
+ const rawArgs = funcMatch[2]
2291
+ if (rawArgs) {
2292
+ const splitter = splitCsv(rawArgs, ', ')
2293
+ filterArgs = formatFunctionArgs(splitter)
2294
+ }
2295
+ }
2296
+
2297
+ const newVal = filterArgs.length > 0
2298
+ ? this.filters[filterName](acc, ...filterArgs, 'from populateVariable')
2299
+ : this.filters[filterName](acc, 'from populateVariable')
1994
2300
  // console.log('PROPERTY', newVal)
1995
2301
  return newVal
1996
2302
  }, property)
@@ -2100,10 +2406,27 @@ Missing Value ${missingValue} - ${matchedString}
2100
2406
  this.resolutionTracking[pathKey] = {
2101
2407
  path: pathKey,
2102
2408
  originalPropertyString: propertyString,
2409
+ resolvedPropertyValue: undefined,
2103
2410
  calls: []
2104
2411
  }
2105
2412
  }
2106
2413
 
2414
+ // this.resolutionTracking[pathKey].resolutionHistory = this.resolutionTracking[pathKey].resolutionHistory || []
2415
+
2416
+ // const isDuplicate = this.resolutionTracking[pathKey].resolutionHistory.some(entry =>
2417
+ // entry.variableString === variableString
2418
+ // )
2419
+
2420
+ // if (!isDuplicate) {
2421
+ // this.resolutionTracking[pathKey].resolutionHistory.push({
2422
+ // variableString: variableString,
2423
+ // propertyString: propertyString,
2424
+ // caller: caller,
2425
+ // lol: 'what'
2426
+ // })
2427
+ // }
2428
+
2429
+
2107
2430
  this.resolutionTracking[pathKey].calls.push({
2108
2431
  variableString: variableString,
2109
2432
  propertyString: propertyString,
@@ -2210,7 +2533,6 @@ Missing Value ${missingValue} - ${matchedString}
2210
2533
  this.options,
2211
2534
  this.config,
2212
2535
  valueObject,
2213
-
2214
2536
  ).then((val) => {
2215
2537
  // Update the last call with the resolved value
2216
2538
  if (pathValue && pathValue.length) {
@@ -2219,7 +2541,8 @@ Missing Value ${missingValue} - ${matchedString}
2219
2541
  // Find the most recent call for this variableString
2220
2542
  for (let i = this.resolutionTracking[pathKey].calls.length - 1; i >= 0; i--) {
2221
2543
  if (this.resolutionTracking[pathKey].calls[i].variableString === variableString) {
2222
- this.resolutionTracking[pathKey].calls[i].resolvedValue = val
2544
+ const v = (typeof val === 'object' && val.__internal_only_flag) ? val.value : val
2545
+ this.resolutionTracking[pathKey].calls[i].resolvedValue = v
2223
2546
  this.resolutionTracking[pathKey].calls[i].resolverType = resolverType
2224
2547
  break
2225
2548
  }
@@ -2260,17 +2583,22 @@ Missing Value ${missingValue} - ${matchedString}
2260
2583
  // console.log('valueCount', valueCount)
2261
2584
  // TODO throw on empty values?
2262
2585
  // No fallback value found AND this is undefined, throw error
2263
- const nestedVars = findNestedVariables(propertyString, this.variableSyntax, this.variablesKnownTypes)
2586
+ const nestedVars = findNestedVariables(propertyString, this.variableSyntax, this.variablesKnownTypes, undefined, this.variableTypes)
2264
2587
  // console.log('nestedVars', nestedVars)
2265
2588
  const noNestedVars = nestedVars.length < 2
2589
+
2590
+ if (this.opts.allowUnknownFileRefs && variableString.match(fileRefSyntax)) {
2591
+ // Encode the unknown file variable to pass through resolution
2592
+ return Promise.resolve(encodeUnknown(propertyString))
2593
+ }
2594
+
2266
2595
  if (valueCount.length === 1 && noNestedVars) {
2267
- const configFilePath = (this.configFilePath) ? ` in ${this.configFilePath}` : ''
2268
- throw new Error(`
2269
- Unable to resolve configuration variable
2596
+ const configFilePathMsg = (this.configFilePath) ? `\nIn file ${this.configFilePath} ` : ''
2597
+ const fromLine = (propertyString !== valueObject.originalSource) ? `\n From "${valueObject.originalSource}"\n` : ''
2598
+
2270
2599
 
2271
- Value "${propertyString}"
2272
- From "${valueObject.originalSource}"
2273
- At location ${valueObject.path ? `"${arrayToJsonPath(valueObject.path)}"` : 'na'}${configFilePath}
2600
+
2601
+ throw new Error(`Unable to resolve config variable "${propertyString}".\n${configFilePathMsg}at location ${valueObject.path ? `"${arrayToJsonPath(valueObject.path)}"` : 'n/a'}${fromLine}
2274
2602
  \nFix this reference, your inputs and/or provide a valid fallback value.
2275
2603
  \nExample of setting a fallback value: \${${variableString}, "fallbackValue"\}\n`)
2276
2604
  }
@@ -2305,13 +2633,28 @@ Unable to resolve configuration variable
2305
2633
  }
2306
2634
 
2307
2635
  const newUse = newHasFilter.reduce((acc, currentFilter, i) => {
2308
- if (!this.filters[currentFilter]) {
2309
- throw new Error(`Filter "${currentFilter}" not found`)
2636
+ // Check if filter has function-style arguments: filterName(arg1, arg2)
2637
+ const funcMatch = currentFilter.match(/^(\w+)\((.*)\)$/)
2638
+ let filterName = currentFilter
2639
+ let filterArgs = null
2640
+
2641
+ if (funcMatch) {
2642
+ filterName = funcMatch[1]
2643
+ const rawArgs = funcMatch[2]
2644
+ // Parse arguments using the same logic as functions
2645
+ if (rawArgs) {
2646
+ const splitter = splitCsv(rawArgs, ', ')
2647
+ filterArgs = formatFunctionArgs(splitter)
2648
+ }
2649
+ }
2650
+
2651
+ if (!this.filters[filterName]) {
2652
+ throw new Error(`Filter "${filterName}" not found`)
2310
2653
  }
2311
2654
  return acc.concat({
2312
- filter: this.filters[currentFilter],
2313
- filterName: currentFilter,
2314
- // args: argsToPass
2655
+ filter: this.filters[filterName],
2656
+ filterName: filterName,
2657
+ args: filterArgs
2315
2658
  })
2316
2659
  }, [])
2317
2660
  // console.log('pathValue', pathValue)
@@ -2573,7 +2916,7 @@ Unable to resolve configuration variable
2573
2916
  // console.log('matchedFileString', matchedFileString)
2574
2917
 
2575
2918
  // Get function input params if any supplied https://regex101.com/r/qlNFVm/1
2576
- // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2919
+ // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2577
2920
  var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
2578
2921
  // tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
2579
2922
  var hasParams = funcParamsRegex.exec(matchedFileString)
@@ -2597,45 +2940,37 @@ Unable to resolve configuration variable
2597
2940
  }
2598
2941
  // console.log('argsToPass', argsToPass)
2599
2942
 
2600
- const relativePath = trimSurroundingQuotes(
2601
- matchedFileString.replace(syntax, (match, varName) => varName.trim()).replace('~', os.homedir()),
2602
- )
2603
-
2604
- // Resolve alias if the path contains alias syntax
2605
- const resolvedPath = resolveAlias(relativePath, this.configPath)
2606
- // console.log('resolvedPath', resolvedPath)
2943
+ const fileDetails = resolveFilePathFromMatch(matchedFileString, syntax, this.configPath)
2944
+ // console.log('fileDetails', fileDetails)
2607
2945
 
2608
- let fullFilePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.join(this.configPath, resolvedPath)
2946
+ const { fullFilePath, resolvedPath, relativePath } = fileDetails
2609
2947
 
2610
- // console.log('fullFilePath', fullFilePath)
2948
+ const exists = fs.existsSync(fullFilePath)
2611
2949
 
2612
- if (fs.existsSync(fullFilePath)) {
2613
- // Get real path to handle potential symlinks (but don't fatal error)
2614
- fullFilePath = fs.realpathSync(fullFilePath)
2615
-
2616
- // Only match files that are relative
2617
- } else if (resolvedPath.match(/\.\//)) {
2618
- // TODO test higher parent refs
2619
- const cleanName = path.basename(resolvedPath)
2620
- const findUpResult = findUp.sync(cleanName, { cwd: this.configPath })
2621
- if (findUpResult) {
2622
- fullFilePath = findUpResult
2623
- }
2624
- }
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
+ })
2625
2959
 
2626
2960
  let fileExtension = resolvedPath.split('.')
2627
2961
 
2628
2962
  fileExtension = fileExtension[fileExtension.length - 1]
2629
2963
 
2630
2964
  // Validate file exists
2631
- if (!fs.existsSync(fullFilePath)) {
2965
+ if (!exists) {
2632
2966
  const originalVar = options.context && options.context.originalSource
2633
2967
 
2634
2968
  const findNestedResult = findNestedVariables(
2635
- originalVar,
2636
- this.variableSyntax,
2637
- this.variablesKnownTypes,
2638
- options.context.path
2969
+ originalVar,
2970
+ this.variableSyntax,
2971
+ this.variablesKnownTypes,
2972
+ options.context.path,
2973
+ this.variableTypes
2639
2974
  )
2640
2975
  // console.log('findNestedResult', findNestedResult)
2641
2976
  let hasFallback = false
@@ -2649,9 +2984,10 @@ Unable to resolve configuration variable
2649
2984
  // console.log('NO FILE FOUND', fullFilePath)
2650
2985
  // console.log('variableString', variableString)
2651
2986
 
2652
- if (!hasFallback) {
2987
+ if (!hasFallback && !this.opts.allowUnknownFileRefs) {
2653
2988
  const errorMsg = makeBox({
2654
2989
  title: `File Not Found in ${originalVar}`,
2990
+ minWidth: '100%',
2655
2991
  text: `Variable ${variableString} cannot resolve due to missing file.
2656
2992
 
2657
2993
  File not found ${fullFilePath}
@@ -2667,13 +3003,19 @@ ${JSON.stringify(options.context, null, 2)}`,
2667
3003
  return Promise.resolve(undefined)
2668
3004
  }
2669
3005
 
3006
+
3007
+
2670
3008
  let valueToPopulate
2671
3009
 
2672
3010
  const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
2673
3011
 
2674
3012
  /* handle case for referencing raw JS files to inline them */
2675
- if (argsToPass.length && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw') || opts.asRawText) {
2676
- valueToPopulate = variableFileContents.replace(/\(/g, '__PH_PAREN_OPEN__')
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)
2677
3019
  return Promise.resolve(valueToPopulate)
2678
3020
  }
2679
3021
 
@@ -3020,25 +3362,25 @@ Please use ":" to reference sub properties. ${deepProperties}`
3020
3362
  }
3021
3363
 
3022
3364
  warnIfNotFound(variableString, valueToPopulate) {
3023
- let varType
3365
+ let variableTypeText
3024
3366
  if (variableString.match(envRefSyntax)) {
3025
- varType = 'environment variable'
3367
+ variableTypeText = 'environment variable'
3026
3368
  } else if (variableString.match(optRefSyntax)) {
3027
- varType = 'option'
3369
+ variableTypeText = 'option'
3028
3370
  } else if (variableString.match(selfRefSyntax)) {
3029
- varType = 'config attribute'
3371
+ variableTypeText = 'config attribute'
3030
3372
  } else if (variableString.match(fileRefSyntax)) {
3031
- varType = 'file'
3373
+ variableTypeText = 'file'
3032
3374
  } else if (variableString.match(deepRefSyntax)) {
3033
- varType = 'deep'
3375
+ variableTypeText = 'deep'
3034
3376
  } else if (variableString.match(textRefSyntax)) {
3035
- varType = 'text'
3377
+ variableTypeText = 'text'
3036
3378
  }
3037
3379
  if (!isValidValue(valueToPopulate)) {
3038
3380
  // console.log("MISSING", variableString)
3039
3381
  // console.log(this.deep)
3040
3382
  // console.log(valueToPopulate)
3041
- const notFoundMsg = `No ${varType} found to satisfy the '\${${variableString}}' variable. Attempting fallback value`
3383
+ const notFoundMsg = `No ${variableTypeText} found to satisfy the '\${${variableString}}' variable. Attempting fallback value`
3042
3384
  if (DEBUG) {
3043
3385
  console.log(notFoundMsg)
3044
3386
  }