configorama 0.6.3 → 0.6.5

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
@@ -13,16 +13,20 @@ const findUp = require('find-up')
13
13
  const traverse = require('traverse')
14
14
  const dotProp = require('dot-prop')
15
15
  const chalk = require('./utils/chalk')
16
+ const { resolveAlias } = require('./utils/resolveAlias')
16
17
 
17
18
  /* Default Value resolvers */
18
19
  const getValueFromString = require('./resolvers/valueFromString')
19
20
  const getValueFromNumber = require('./resolvers/valueFromNumber')
20
21
  const getValueFromEnv = require('./resolvers/valueFromEnv')
21
22
  const getValueFromOptions = require('./resolvers/valueFromOptions')
23
+ const getValueFromCron = require('./resolvers/valueFromCron')
24
+ const getValueFromEval = require('./resolvers/valueFromEval')
22
25
  const createGitResolver = require('./resolvers/valueFromGit')
23
26
  /* Default File Parsers */
24
27
  const YAML = require('./parsers/yaml')
25
28
  const TOML = require('./parsers/toml')
29
+ const INI = require('./parsers/ini')
26
30
  /* functions */
27
31
  const md5Function = require('./functions/md5')
28
32
 
@@ -44,14 +48,15 @@ const {
44
48
  const { parseFileContents } = require('./utils/parse')
45
49
  const { splitCsv } = require('./utils/splitCsv')
46
50
  const { replaceAll } = require('./utils/replaceAll')
47
- const { getTextAfterOccurance, findNestedVariable } = require('./utils/textUtils')
51
+ const { getTextAfterOccurrence, findNestedVariable } = require('./utils/textUtils')
48
52
  const { getFallbackString, verifyVariable } = require('./utils/variableUtils')
49
53
  const { encodeUnknown, decodeUnknown } = require('./utils/unknownValues')
50
54
  const { mergeByKeys } = require('./utils/mergeByKeys')
51
55
  const { arrayToJsonPath } = require('./utils/arrayToJsonPath')
52
56
  const { findNestedVariables } = require('./utils/find-nested-variables')
53
- const { makeBox } = require('@davidwells/box-logger')
57
+ const { makeBox, makeStackedBoxes } = require('@davidwells/box-logger')
54
58
  const { logHeader } = require('./utils/logs')
59
+ const { createEditorLink } = require('./utils/createEditorLink')
55
60
  /**
56
61
  * Maintainer's notes:
57
62
  *
@@ -70,7 +75,7 @@ const deepRefSyntax = RegExp(/(\${)?deep:\d+(\.[^}]+)*()}?/)
70
75
  const deepIndexReplacePattern = new RegExp(/^deep:|(\.[^}]+)*$/g)
71
76
  const deepIndexPattern = /deep\:(\d*)/
72
77
  const deepPrefixReplacePattern = /(?:^deep:)\d+\.?/g
73
- const fileRefSyntax = RegExp(/^file\((~?[\{\}\:\$a-zA-Z0-9._\-\/,'" ]+?)\)/g)
78
+ const fileRefSyntax = RegExp(/^file\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" ]+?)\)/g)
74
79
  // TODO update file regex ^file\((~?[a-zA-Z0-9._\-\/, ]+?)\)
75
80
  // To match file(asyncValue.js, lol) input params
76
81
  const envRefSyntax = RegExp(/^env:/g)
@@ -104,8 +109,6 @@ class Configorama {
104
109
  if (opts && !opts.sync) {
105
110
  handleSignalEvents()
106
111
  }
107
-
108
- const showFoundVariables = opts && opts.dynamicArgs && (opts.dynamicArgs.list || opts.dynamicArgs.info)
109
112
 
110
113
  const options = opts || {}
111
114
  // Set opts to pass into JS file calls
@@ -148,44 +151,16 @@ class Configorama {
148
151
  const fileDirectory = path.dirname(path.resolve(fileOrObject))
149
152
  const fileType = path.extname(fileOrObject)
150
153
 
151
- // Parse file contents using extracted function
152
- const configObject = parseFileContents(
153
- fileContents,
154
- fileType,
155
- fileOrObject,
156
- varRegex,
157
- this.opts
158
- )
159
-
160
154
  this.configFilePath = fileOrObject
161
- // set config objects
162
- this.config = configObject
155
+ // Set configFileType
156
+ this.configFileType = fileType
163
157
  // Keep a copy of the original file contents
164
158
  this.originalString = fileContents
165
- // Keep a copy
166
- this.originalConfig = cloneDeep(configObject)
167
-
168
- const useDotEnv = this.originalConfig.useDotenv || this.originalConfig.useDotEnv
169
- if ((useDotEnv && useDotEnv === true) || this.opts.useDotEnvFiles) {
170
- const loadStageEnv = require('env-stage-loader')
171
- let providerStage
172
- if (this.originalConfig && this.originalConfig.provider && this.originalConfig.provider.stage) {
173
- providerStage = this.originalConfig.provider.stage
174
- // @TODO check value to see if variable and needs pre-resolving to resolve stage vars
175
- }
176
- const stage = this.opts.stage || providerStage || 'dev'
177
- /* Load env variables into process.env */
178
- const values = loadStageEnv({
179
- // silent: true,
180
- // debug: true,
181
- env: stage,
182
- // defaultEnv: 'prod',
183
- // ignoreFiles: ['.env']
184
- })
185
- }
186
-
187
159
  // Set configPath for file references
188
160
  this.configPath = fileDirectory
161
+ // Initialize config as null - will be populated in init
162
+ this.config = null
163
+ this.originalConfig = null
189
164
  }
190
165
 
191
166
  // Track promise resolution
@@ -207,6 +182,23 @@ class Configorama {
207
182
  * ${opt:other, "fallbackValue"}
208
183
  */
209
184
  getValueFromOptions,
185
+
186
+ /**
187
+ * Cron expressions
188
+ * Usage:
189
+ * ${cron(every minute)}
190
+ * ${cron(weekdays)}
191
+ * ${cron(at 9:30)}
192
+ */
193
+ getValueFromCron,
194
+
195
+ /**
196
+ * Eval expressions
197
+ * Usage:
198
+ * ${eval(${self:valueTwo} > ${self:valueOne})}
199
+ */
200
+ getValueFromEval,
201
+
210
202
  /**
211
203
  * Self references
212
204
  * Usage:
@@ -284,6 +276,7 @@ class Configorama {
284
276
  return deeperExists
285
277
  }
286
278
  }
279
+ // console.log('fallthrough fullObject', fullObject)
287
280
  /* is simple ${whatever} reference in same file */
288
281
  const startOf = varString.split('.')
289
282
  return fullObject[startOf[0]]
@@ -308,8 +301,9 @@ class Configorama {
308
301
  this.variableTypes = this.variableTypes.concat(fallThroughSelfMatcher)
309
302
 
310
303
  // const variablesKnownTypes = new RegExp(`^(${this.variableTypes.map((v) => v.prefix || v.type).join('|')}):`)
311
- const variablesKnownTypes = combineRegexes(this.variableTypes.filter((v) => v.type !== 'string').map((v) => v.match))
312
- // console.log('variablesKnownTypes', variablesKnownTypes)
304
+ const variablesKnownTypes = combineRegexes(
305
+ this.variableTypes.filter((v) => v.type !== 'string').map((v) => v.match)
306
+ )
313
307
  this.variablesKnownTypes = variablesKnownTypes
314
308
 
315
309
  // this.allPatterns = combineRegexes(...this.variableTypes.map((v) => v.match))
@@ -430,6 +424,49 @@ class Configorama {
430
424
  this.functions = Object.assign({}, this.functions, options.functions)
431
425
  }
432
426
 
427
+ this.deep = []
428
+ this.leaves = []
429
+ this.callCount = 0
430
+ }
431
+
432
+ initialCall(func) {
433
+ this.deep = []
434
+ this.tracker.start()
435
+ return func().finally(() => {
436
+ this.tracker.stop()
437
+ this.deep = []
438
+ })
439
+ }
440
+
441
+ /**
442
+ * Populate all variables in the service, conveniently remove and restore the service attributes
443
+ * that confuse the population methods.
444
+ * @param cliOpts An options hive to use for ${opt:...} variables.
445
+ * @returns {Promise.<TResult>|*} A promise resolving to the populated service.
446
+ */
447
+ async init(cliOpts) {
448
+ this.options = cliOpts || {}
449
+ const configoramaOpts = this.opts
450
+
451
+ const showFoundVariables = configoramaOpts && configoramaOpts.dynamicArgs && (configoramaOpts.dynamicArgs.list || configoramaOpts.dynamicArgs.info)
452
+
453
+ // If we have a file path but no config yet, parse it now
454
+ if (this.configFilePath && !this.config) {
455
+ const configObject = await parseFileContents(
456
+ this.originalString,
457
+ this.configFileType,
458
+ this.configFilePath,
459
+ this.variableSyntax,
460
+ this.opts
461
+ )
462
+ this.configFileContents = ''
463
+ if (VERBOSE || showFoundVariables) {
464
+ this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
465
+ }
466
+ this.config = configObject
467
+ this.originalConfig = cloneDeep(configObject)
468
+ }
469
+
433
470
  if (VERBOSE) {
434
471
  logHeader('Config Input before processing')
435
472
  console.log()
@@ -437,10 +474,14 @@ class Configorama {
437
474
  console.log()
438
475
  }
439
476
 
477
+ const variableSyntax = this.variableSyntax
478
+ const variablesKnownTypes = this.variablesKnownTypes
479
+
440
480
  if (VERBOSE || showFoundVariables) {
441
481
  const foundVariables = []
442
482
  const variableData = {}
443
483
  let matchCount = 1
484
+ // console.log('this.originalConfig', this.originalConfig)
444
485
  traverse(this.originalConfig).forEach(function (rawValue) {
445
486
  if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
446
487
  const configValuePath = this.path.join('.')
@@ -472,45 +513,63 @@ class Configorama {
472
513
  resolveOrder: [],
473
514
  resolveDetails: nested,
474
515
  }
475
-
516
+ let defaultValueIsVar = false
476
517
  function calculateResolveOrder(item) {
477
518
  if (item && item.fallbackValues) {
478
519
  let hasResolvedFallback
520
+ // console.log('item.fallbackValues', item.fallbackValues)
479
521
  const order = ([item.valueBeforeFallback]).concat(item.fallbackValues.map((f, i) => {
522
+ // console.log('f', f)
480
523
  if (f.fallbackValues) {
481
524
  const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
525
+ // console.log('nestedOrder', nestedOrder)
526
+ // console.log('nestedResolvedFallback', nestedResolvedFallback)
482
527
  if (!hasResolvedFallback && nestedResolvedFallback) {
483
528
  hasResolvedFallback = nestedResolvedFallback
484
529
  }
485
530
  return nestedOrder // Return just the order part
486
531
  }
532
+
487
533
  if (!hasResolvedFallback && f.isResolvedFallback) {
488
534
  hasResolvedFallback = f.stringValue
489
535
  }
536
+ if (f.isResolvedFallback) {
537
+ hasResolvedFallback = f.stringValue
538
+ }
539
+
540
+ if (!hasResolvedFallback && f.isVariable) {
541
+ defaultValueIsVar = f
542
+ }
543
+ // console.log('hasResolvedFallback', hasResolvedFallback)
490
544
  return `${f.stringValue || f.variable}${f.isResolvedFallback ? ' (Resolved default fallback)' : ''}`
491
545
  })).flat()
492
546
 
493
547
  return [order, hasResolvedFallback]
494
548
  }
495
- return [[item.variable], false] // Return array instead of just the value
549
+ return [[item.variable], undefined] // Return array instead of just the value
496
550
  }
497
551
 
498
552
  const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
499
553
  varData.resolveOrder = resolveOrder
500
554
 
501
- if (hasResolvedFallback) {
502
- varData.defaultValue = hasResolvedFallback
555
+ if (defaultValueIsVar) {
556
+ varData.defaultValueIsVar = defaultValueIsVar
503
557
  }
504
558
 
559
+ // console.log('hasResolvedFallback', hasResolvedFallback)
560
+ if (typeof hasResolvedFallback !== 'undefined') {
561
+ varData.defaultValue = hasResolvedFallback
562
+ }
505
563
 
506
- if (!varData.defaultValue) {
564
+ // console.log('varData.defaultValue', varData.defaultValue)
565
+ if (typeof varData.defaultValue === 'undefined') {
507
566
  varData.isRequired = true
508
567
  }
509
568
 
510
-
511
569
  if (varData.resolveOrder.length > 1) {
512
570
  varData.hasFallback = true
513
571
  }
572
+ //console.log('varData', varData)
514
573
 
515
574
  variableData[key] = (variableData[key] || []).concat(varData)
516
575
 
@@ -518,7 +577,6 @@ class Configorama {
518
577
  }
519
578
  })
520
579
 
521
-
522
580
  if (!foundVariables.length) {
523
581
  logHeader('No Variables Found in Config')
524
582
  if (this.configFilePath) {
@@ -565,20 +623,35 @@ class Configorama {
565
623
  }
566
624
 
567
625
  logHeader('Variable Details')
626
+
627
+ const lines = this.configFileContents.split('\n')
628
+ // console.log('lines', lines)
568
629
 
569
630
  const indent = ''
570
- varKeys.forEach((key, i) => {
631
+ const boxes = varKeys.map((key, i) => {
571
632
  const variableInstances = variableData[key]
633
+ // console.log('variableInstances', variableInstances)
572
634
 
573
635
  const firstInstance = variableInstances[0]
574
636
 
575
637
  let requiredText = ''
576
638
  let defaultValueSrc = ''
577
- if (!firstInstance.defaultValue) {
639
+ if (typeof firstInstance.defaultValue === 'undefined') {
578
640
  // console.log('no default value', firstInstance)
641
+
642
+ let dotPropArr = []
643
+ if (firstInstance.defaultValueIsVar && (
644
+ firstInstance.defaultValueIsVar.varType === 'self:' ||
645
+ firstInstance.defaultValueIsVar.varType === 'dot.prop'
646
+ )) {
647
+ dotPropArr = [firstInstance.defaultValueIsVar]
648
+ }
579
649
  /* Check if the fallback variable is a self reference */
580
650
  const hasDotPropOrSelf = variableInstances.reduce((acc, v) => {
581
- const dotProp = v.resolveDetails.find((d) => d.varType === 'dot.prop')
651
+ const dotProp = v.resolveDetails.find((d) => {
652
+ // console.log('d', d)
653
+ return d.varType === 'dot.prop'
654
+ })
582
655
  if (dotProp) {
583
656
  acc.push(dotProp)
584
657
  }
@@ -587,10 +660,12 @@ class Configorama {
587
660
  acc.push(v.resolveDetails[0])
588
661
  }
589
662
  return acc
590
- }, [])
663
+ }, dotPropArr)
591
664
  // console.log('hasDotPropOrSelf', hasDotPropOrSelf)
665
+
592
666
  if (!hasDotPropOrSelf.length) {
593
- requiredText = '[Required] '
667
+ const debug = (false) ? JSON.stringify(firstInstance, null, 2) : ''
668
+ requiredText = `[Required Variable] ${debug}`
594
669
  } else {
595
670
  const fallBackValues = variableInstances.filter((v) => v.resolveDetails.find((d) => d.hasFallback)).map((v) => v.resolveDetails)
596
671
  // console.log('fallBackValues', fallBackValues)
@@ -609,6 +684,9 @@ class Configorama {
609
684
  // truncate niceString to 100 characters
610
685
  const truncatedString = niceString.length > 100 ? niceString.substring(0, 90) + '...' : niceString
611
686
  firstInstance.defaultValue = truncatedString
687
+ } else {
688
+ deepLog('Missing default var', firstInstance)
689
+ throw new Error(`Variable misconfiguration at ${firstInstance.variable}\n\n"${hasDotPropOrSelf[0].variable}" resolves to undefined value.\n`)
612
690
  }
613
691
  }
614
692
  //this.originalConfig[key] = undefined
@@ -621,14 +699,16 @@ class Configorama {
621
699
  const keyChalk = chalk.whiteBright
622
700
  const valueChalk = chalk.hex(VALUE_HEX)
623
701
 
624
- if (firstInstance.defaultValue) {
625
- const defaultValueText = `${indent}${keyChalk(`DefaultValue:`.padEnd(titleText.length, ' '))}`
702
+ if (typeof firstInstance.defaultValue !== 'undefined') {
703
+ // console.log('firstInstance.defaultValue', firstInstance.defaultValue)
704
+ const defaultValueRender = firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue
705
+ const defaultValueText = `${indent}${keyChalk(`Default value:`.padEnd(titleText.length, ' '))}`
626
706
  // ensure padding is even
627
- varMsg += `${defaultValueText} ${valueChalk(firstInstance.defaultValue)}`
707
+ varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}`
628
708
  }
629
709
 
630
- if(defaultValueSrc) {
631
- varMsg += `\n${indent}${keyChalk('DefaultValue path:'.padEnd(titleText.length, ' '))} `
710
+ if (defaultValueSrc) {
711
+ varMsg += `\n${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
632
712
  varMsg += `${valueChalk(defaultValueSrc)}`
633
713
  }
634
714
 
@@ -640,26 +720,34 @@ class Configorama {
640
720
 
641
721
  let locationRender = valueChalk(variableInstances[0].path)
642
722
 
643
- let locationLabel = `${indent}${keyChalk('Used at:'.padEnd(titleText.length, ' '))}`
723
+ let locationLabel = `${indent}${keyChalk('Path:'.padEnd(titleText.length, ' '))}`
644
724
  if (variableInstances.length > 1) {
645
725
  locationRender = `\n${variableInstances.map((v) => valueChalk(`${indent}- ${v.path}`)).join('\n')}`
646
- const locationLabelText = `${indent}${keyChalk('Used at:')}`
726
+ const locationLabelText = `${indent}${keyChalk('Paths:')}`
647
727
  locationLabel = locationLabelText
648
728
  }
649
729
 
650
730
  varMsg += `\n${locationLabel} ${locationRender}`
731
+
732
+ // find the match in our lines
733
+ const line = lines.findIndex((line) => line.includes(key))
734
+ const lineNumber = line + 1
651
735
 
652
736
  // console.log(` ${chalk.bold(key)}`)
653
- console.log(makeBox(varMsg, {
654
- title: `${key}`,
655
- borderColor: 'gray',
656
- // style: 'bold',
657
- minWidth: 120,
658
- }))
659
- if(i < varKeys.length - 1) {
660
- //console.log()
737
+ return {
738
+ text: varMsg,
739
+ title: {
740
+ left: `▶ ${lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, key) : key}`,
741
+ right: lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, `Line: ${lineNumber}`, 'gray') : '',
742
+ },
661
743
  }
662
744
  })
745
+
746
+ console.log(makeStackedBoxes(boxes, {
747
+ borderColor: 'gray',
748
+ minWidth: 120,
749
+ borderStyle: 'bold',
750
+ }))
663
751
  }
664
752
 
665
753
  /* Exit early if list or info flag is set */
@@ -668,33 +756,32 @@ class Configorama {
668
756
  }
669
757
  }
670
758
 
671
- this.deep = []
672
- this.callCount = 0
673
- }
674
-
675
- initialCall(func) {
676
- this.deep = []
677
- this.tracker.start()
678
- return func().finally(() => {
679
- this.tracker.stop()
680
- this.deep = []
681
- })
682
- }
683
-
684
- /**
685
- * Populate all variables in the service, conveniently remove and restore the service attributes
686
- * that confuse the population methods.
687
- * @param cliOpts An options hive to use for ${opt:...} variables.
688
- * @returns {Promise.<TResult>|*} A promise resolving to the populated service.
689
- */
690
- init(cliOpts) {
691
- this.options = cliOpts || {}
692
- const configoramaOpts = this.opts
693
759
  const originalConfig = this.originalConfig
694
760
 
695
761
  /* If no variables found just return early */
696
762
  if (this.originalString && !this.originalString.match(this.variableSyntax)) {
697
- return Promise.resolve(originalConfig)
763
+ return Promise.resolve(this.originalConfig)
764
+ }
765
+
766
+ const useDotEnv = this.originalConfig.useDotenv || this.originalConfig.useDotEnv
767
+ if ((useDotEnv && useDotEnv === true) || this.opts.useDotEnvFiles) {
768
+ let providerStage
769
+ /* has hardcoded stage */
770
+ if (
771
+ this.originalConfig && this.originalConfig.provider &&
772
+ this.originalConfig.provider.stage && !this.originalConfig.provider.stage.match(this.variableSyntax)
773
+ ) {
774
+ providerStage = this.originalConfig.provider.stage
775
+ }
776
+ const stage = cliOpts.stage || providerStage || process.env.NODE_ENV || 'dev'
777
+ /* Load env variables into process.env */
778
+ const values = require('env-stage-loader')({
779
+ // silent: true,
780
+ // debug: true,
781
+ env: stage,
782
+ // defaultEnv: 'prod',
783
+ // ignoreFiles: ['.env']
784
+ })
698
785
  }
699
786
 
700
787
  /* Parse variables */
@@ -706,18 +793,28 @@ class Configorama {
706
793
  // console.log('Final Config', this.config)
707
794
  const transform = this.runFunction.bind(this)
708
795
  const varSyntax = this.variableSyntax
796
+ const leaves = this.leaves
797
+ // console.log('leaves two', leaves)
709
798
  // Traverse resolved object and run functions
710
799
  // console.log('this.config', this.config)
711
800
  traverse(this.config).forEach(function (rawValue) {
712
801
  /* Pass through unknown variables */
713
802
  if (!configoramaOpts.allowUndefinedValues && typeof rawValue === 'undefined') {
714
803
  const configValuePath = this.path.join('.')
804
+ console.log(this.path)
715
805
  const ogValue = dotProp.get(originalConfig, configValuePath)
716
806
  const varDisplay = ogValue ? `"${ogValue}" variable` : 'variable'
807
+
808
+ const leaf = leaves.find((l) => l.path.join('.') === configValuePath)
809
+ // if (leaf) {
810
+ // deepLog('leaf', leaf)
811
+ // }
717
812
  const errorMessage = `
718
- Config error:
719
- "${configValuePath}" resolved to "undefined"
720
- Verify the ${varDisplay} in config at "${configValuePath}"`
813
+ Config error:\n
814
+ Path "${configValuePath}" resolved to "undefined".\n
815
+ Verify the ${varDisplay} in config at "${configValuePath}".\n
816
+ ${leaf ? `See:\n ${leaf.originalValuePath}: ${leaf.originalSource} ` : ''}
817
+ ${leaf && leaf.isFileRef ? `\n The error could be deeper in the referenced file at ${configValuePath.replace(leaf.originalValuePath, '').replace(/^\./, '')} key.\n` : ''}`
721
818
  throw new Error(errorMessage)
722
819
  }
723
820
  if (typeof rawValue === 'string') {
@@ -782,7 +879,7 @@ class Configorama {
782
879
  var hasFunc = funcRegex.exec(variableString)
783
880
  // TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
784
881
  // console.log('hasFunc', hasFunc)
785
- if (!hasFunc) {
882
+ if (!hasFunc || hasFunc && (hasFunc[1] === 'cron' || hasFunc[1] === 'eval')) {
786
883
  return variableString
787
884
  }
788
885
  // test for object
@@ -880,12 +977,23 @@ class Configorama {
880
977
  value: current,
881
978
  }
882
979
  const thePath = leaf.path.length > 1 ? leaf.path.join('.') : leaf.path[0]
980
+ // console.log('thePath', thePath)
981
+ // console.log('this.originalConfig', this.originalConfig)
883
982
  let originalValue = dotProp.get(this.originalConfig, thePath)
884
983
  // TODO @DWELLS make recursive
885
984
  if (!originalValue) {
886
- const parentArray = leaf.path.slice(0, -1)
887
- const parentPath = parentArray > 1 ? parentArray.join('.') : parentArray[0]
888
- originalValue = dotProp.get(this.originalConfig, parentPath)
985
+ // Recurse up the tree until we find a value
986
+ let currentPathArray = leaf.path.slice(0, -1)
987
+ while (currentPathArray.length > 0 && !originalValue) {
988
+ const currentPath = currentPathArray.length > 1 ? currentPathArray.join('.') : currentPathArray[0]
989
+ // console.log('checking parent path:', currentPath)
990
+ originalValue = dotProp.get(this.originalConfig, currentPath)
991
+ if (typeof originalValue !== 'undefined') {
992
+ leaf.originalValuePath = currentPath
993
+ leaf.currentConfig = this.config
994
+ }
995
+ currentPathArray = currentPathArray.slice(0, -1)
996
+ }
889
997
  }
890
998
  leaf.originalSource = originalValue
891
999
  if (originalValue && isString(originalValue)) {
@@ -912,11 +1020,27 @@ class Configorama {
912
1020
  */
913
1021
  populateVariables(properties) {
914
1022
  // console.log('properties', properties)
915
- const variables = properties.filter((property) => {
1023
+ let variables = properties.filter((property) => {
916
1024
  // Initial check if value has variable string in it
917
1025
  return isString(property.value) && property.value.match(this.variableSyntax)
918
1026
  })
919
1027
 
1028
+ /*
1029
+ console.log(`variables at call count ${this.callCount}`, variables)
1030
+ /** */
1031
+
1032
+ /* Exclude git messages from being processed */
1033
+ // Was failing on git msgs like "xyz cron:pattern to cron(pattern) for improved clarity"
1034
+ if (this.callCount > 1) {
1035
+ // filter out git vars
1036
+ variables = variables.filter(property => {
1037
+ if (property.originalSource && typeof property.originalSource === 'string') {
1038
+ return !property.originalSource.startsWith('${git:')
1039
+ }
1040
+ return true
1041
+ })
1042
+ }
1043
+
920
1044
  return map(variables, (valueObject) => {
921
1045
  // console.log('valueObject', valueObject)
922
1046
  return this.populateValue(valueObject, false, '_populateVariables').then((populated) => {
@@ -958,6 +1082,7 @@ class Configorama {
958
1082
  }
959
1083
 
960
1084
  const leaves = this.getProperties(objectToPopulate, true, objectToPopulate)
1085
+ this.leaves = leaves
961
1086
  // console.log('leaves', leaves)
962
1087
  const populations = this.populateVariables(leaves)
963
1088
  // console.log("FILL LEAVES", populations)
@@ -1301,7 +1426,19 @@ Missing Value ${missingValue} - ${matchedString}
1301
1426
 
1302
1427
  if (property && typeof property === 'string') {
1303
1428
  // console.log('property', property)
1304
- const prop = cleanVariable(property, this.variableSyntax, true, `populateVariable string ${this.callCount}`)
1429
+ let prop = cleanVariable(
1430
+ property,
1431
+ this.variableSyntax,
1432
+ true,
1433
+ `populateVariable string ${this.callCount}`,
1434
+ // true // recursive
1435
+ )
1436
+
1437
+ // Double processing needed for `${eval(${self:three} > ${self:four})}`
1438
+ if (prop.startsWith('${')) {
1439
+ prop = cleanVariable(prop, this.variableSyntax, true, `populateVariable string ${this.callCount}`)
1440
+ }
1441
+
1305
1442
  // console.log('prop', prop)
1306
1443
  if (property.match(/^> function /g) && prop) {
1307
1444
  // console.log('func prop', property)
@@ -1340,7 +1477,10 @@ Missing Value ${missingValue} - ${matchedString}
1340
1477
  }
1341
1478
  */
1342
1479
  // Does not match file refs with nested vars + args
1343
- if (!prop.match(/file\((~?[a-zA-Z0-9._\-\/,'"\{\}\.$: ]+?)\)/) && func) {
1480
+ // @TODO fix this for eval refs
1481
+ // console.log('prop', prop)
1482
+ // console.log('func', func)
1483
+ if (!prop.match(fileRefSyntax) && !prop.match(getValueFromEval.match) && func) {
1344
1484
  // console.log('IS FUNCTION')
1345
1485
  /* if matches function signature like ${merge('foo', 'bar')}
1346
1486
  rewrite the variable to run the function after inputs resolved
@@ -1491,7 +1631,7 @@ Missing Value ${missingValue} - ${matchedString}
1491
1631
  if (filters) {
1492
1632
  const string = cleanVariable(propertyString, this.variableSyntax, true, `getValueFromSrc filter ${this.callCount}`)
1493
1633
  // console.log('string', string)
1494
- const deeperValue = getTextAfterOccurance(string, variableString)
1634
+ const deeperValue = getTextAfterOccurrence(string, variableString)
1495
1635
  // console.log('deeperValue', deeperValue)
1496
1636
  // console.log('filters', filters)
1497
1637
  // console.log('variableString', variableString)
@@ -1844,7 +1984,7 @@ Unable to resolve configuration variable
1844
1984
  return res
1845
1985
  })
1846
1986
  }
1847
- getValueFromFile(variableString) {
1987
+ async getValueFromFile(variableString) {
1848
1988
  // console.log('From file', `"${variableString}"`)
1849
1989
  let matchedFileString = variableString.match(fileRefSyntax)[0]
1850
1990
  // console.log('matchedFileString', matchedFileString)
@@ -1875,7 +2015,11 @@ Unable to resolve configuration variable
1875
2015
  matchedFileString.replace(fileRefSyntax, (match, varName) => varName.trim()).replace('~', os.homedir()),
1876
2016
  )
1877
2017
 
1878
- let fullFilePath = path.isAbsolute(relativePath) ? relativePath : path.join(this.configPath, relativePath)
2018
+ // Resolve alias if the path contains alias syntax
2019
+ const resolvedPath = resolveAlias(relativePath, this.configPath)
2020
+ // console.log('resolvedPath', resolvedPath)
2021
+
2022
+ let fullFilePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.join(this.configPath, resolvedPath)
1879
2023
 
1880
2024
  // console.log('fullFilePath', fullFilePath)
1881
2025
 
@@ -1884,13 +2028,13 @@ Unable to resolve configuration variable
1884
2028
  fullFilePath = fs.realpathSync(fullFilePath)
1885
2029
 
1886
2030
  // Only match files that are relative
1887
- } else if (relativePath.match(/\.\//)) {
2031
+ } else if (resolvedPath.match(/\.\//)) {
1888
2032
  // TODO test higher parent refs
1889
- const cleanName = path.basename(relativePath)
2033
+ const cleanName = path.basename(resolvedPath)
1890
2034
  fullFilePath = findUp.sync(cleanName, { cwd: this.configPath })
1891
2035
  }
1892
2036
 
1893
- let fileExtension = relativePath.split('.')
2037
+ let fileExtension = resolvedPath.split('.')
1894
2038
 
1895
2039
  fileExtension = fileExtension[fileExtension.length - 1]
1896
2040
 
@@ -1940,6 +2084,7 @@ Check if your javascript is exporting a function that returns a value.`
1940
2084
  config: this.config,
1941
2085
  opts: this.opts,
1942
2086
  }
2087
+
1943
2088
 
1944
2089
  valueToPopulate = returnValueFunction.call(jsFile, valueForFunction, ...argsToPass)
1945
2090
 
@@ -1962,8 +2107,115 @@ Check if your javascript is returning the correct data.`
1962
2107
  })
1963
2108
  }
1964
2109
 
1965
- // Process everything except JS
1966
- if (fileExtension !== 'js') {
2110
+
2111
+ if (fileExtension === 'ts') {
2112
+ const { executeTypeScriptFile } = require('./parsers/typescript')
2113
+ let returnValueFunction
2114
+ const variableArray = variableString.split(':')
2115
+
2116
+ try {
2117
+ const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
2118
+ // console.log('fullFilePath', fullFilePath)
2119
+ // console.log('tsFile', tsFile)
2120
+ returnValueFunction = tsFile.config || tsFile.default || tsFile
2121
+
2122
+ if (variableArray[1]) {
2123
+ let tsModule = variableArray[1]
2124
+ tsModule = tsModule.split('.')[0]
2125
+ returnValueFunction = tsFile[tsModule]
2126
+ }
2127
+
2128
+ if (typeof returnValueFunction !== 'function') {
2129
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2130
+ Check if your TypeScript is exporting a function that returns a value.`
2131
+ return Promise.reject(new Error(errorMessage))
2132
+ }
2133
+
2134
+ const valueForFunction = {
2135
+ originalConfig: this.originalConfig,
2136
+ config: this.config,
2137
+ opts: this.opts,
2138
+ }
2139
+
2140
+ valueToPopulate = returnValueFunction.call(tsFile, valueForFunction, ...argsToPass)
2141
+
2142
+ return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
2143
+ let deepProperties = variableString.replace(matchedFileString, '')
2144
+ deepProperties = deepProperties.slice(1).split('.')
2145
+ deepProperties.splice(0, 1)
2146
+ // Trim prop keys for starting/trailing spaces
2147
+ deepProperties = deepProperties.map((prop) => {
2148
+ return trim(prop)
2149
+ })
2150
+ return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
2151
+ if (typeof deepValueToPopulateResolved === 'undefined') {
2152
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2153
+ Check if your TypeScript is returning the correct data.`
2154
+ return Promise.reject(new Error(errorMessage))
2155
+ }
2156
+ return Promise.resolve(deepValueToPopulateResolved)
2157
+ })
2158
+ })
2159
+ } catch (err) {
2160
+ return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
2161
+ }
2162
+ }
2163
+
2164
+ if (fileExtension === 'mjs' || fileExtension === 'esm') {
2165
+ const { executeESMFile } = require('./parsers/esm')
2166
+ let returnValueFunction
2167
+ const variableArray = variableString.split(':')
2168
+
2169
+ try {
2170
+ const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
2171
+ // console.log('ESM fullFilePath', fullFilePath)
2172
+ // console.log('ESM esmFile', esmFile, 'type:', typeof esmFile)
2173
+ returnValueFunction = esmFile.config || esmFile.default || esmFile
2174
+
2175
+ if (variableArray[1]) {
2176
+ let esmModule = variableArray[1]
2177
+ esmModule = esmModule.split('.')[0]
2178
+ returnValueFunction = esmFile[esmModule]
2179
+ }
2180
+
2181
+ if (typeof returnValueFunction !== 'function') {
2182
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2183
+ Check if your ESM is exporting a function that returns a value.`
2184
+ return Promise.reject(new Error(errorMessage))
2185
+ }
2186
+
2187
+ const valueForFunction = {
2188
+ originalConfig: this.originalConfig,
2189
+ config: this.config,
2190
+ opts: this.opts,
2191
+ }
2192
+
2193
+ valueToPopulate = returnValueFunction.call(esmFile, valueForFunction, ...argsToPass)
2194
+
2195
+ return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
2196
+ let deepProperties = variableString.replace(matchedFileString, '')
2197
+ deepProperties = deepProperties.slice(1).split('.')
2198
+ deepProperties.splice(0, 1)
2199
+ // Trim prop keys for starting/trailing spaces
2200
+ deepProperties = deepProperties.map((prop) => {
2201
+ return trim(prop)
2202
+ })
2203
+ return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
2204
+ if (typeof deepValueToPopulateResolved === 'undefined') {
2205
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2206
+ Check if your ESM is returning the correct data.`
2207
+ return Promise.reject(new Error(errorMessage))
2208
+ }
2209
+ return Promise.resolve(deepValueToPopulateResolved)
2210
+ })
2211
+ })
2212
+ } catch (err) {
2213
+ return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
2214
+ }
2215
+ }
2216
+
2217
+ // Process everything except JS, TS, and ESM
2218
+ if (fileExtension !== 'js' && fileExtension !== 'ts' && fileExtension !== 'mjs' && fileExtension !== 'esm') {
1967
2219
  /* Read initial file */
1968
2220
  valueToPopulate = fs.readFileSync(fullFilePath, 'utf-8')
1969
2221
 
@@ -1975,6 +2227,9 @@ Check if your javascript is returning the correct data.`
1975
2227
  if (fileExtension === 'toml') {
1976
2228
  valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
1977
2229
  }
2230
+ if (fileExtension === 'ini') {
2231
+ valueToPopulate = INI.toJson(valueToPopulate)
2232
+ }
1978
2233
  // console.log('deep', variableString)
1979
2234
  // console.log('matchedFileString', matchedFileString)
1980
2235
  let deepProperties = variableString.replace(matchedFileString, '')
@@ -1997,11 +2252,17 @@ Please use ":" to reference sub properties`
1997
2252
  return Promise.resolve(valueToPopulate)
1998
2253
  }
1999
2254
 
2255
+ if (fileExtension === 'ini') {
2256
+ valueToPopulate = INI.parse(valueToPopulate)
2257
+ return Promise.resolve(valueToPopulate)
2258
+ }
2259
+
2000
2260
  if (fileExtension === 'json') {
2001
2261
  valueToPopulate = JSON.parse(valueToPopulate)
2002
2262
  return Promise.resolve(valueToPopulate)
2003
2263
  }
2004
2264
  }
2265
+ console.log('fall thru', valueToPopulate)
2005
2266
  return Promise.resolve(valueToPopulate)
2006
2267
  }
2007
2268
  getVariableFromDeep(variableString) {