configorama 0.6.4 → 0.6.6

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,8 @@ 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)
79
+ const textRefSyntax = RegExp(/^text\((~?[@\{\}\:\$a-zA-Z0-9._\-\/,'" ]+?)\)/g)
74
80
  // TODO update file regex ^file\((~?[a-zA-Z0-9._\-\/, ]+?)\)
75
81
  // To match file(asyncValue.js, lol) input params
76
82
  const envRefSyntax = RegExp(/^env:/g)
@@ -104,8 +110,6 @@ class Configorama {
104
110
  if (opts && !opts.sync) {
105
111
  handleSignalEvents()
106
112
  }
107
-
108
- const showFoundVariables = opts && opts.dynamicArgs && (opts.dynamicArgs.list || opts.dynamicArgs.info)
109
113
 
110
114
  const options = opts || {}
111
115
  // Set opts to pass into JS file calls
@@ -148,24 +152,16 @@ class Configorama {
148
152
  const fileDirectory = path.dirname(path.resolve(fileOrObject))
149
153
  const fileType = path.extname(fileOrObject)
150
154
 
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
155
  this.configFilePath = fileOrObject
161
- // set config objects
162
- this.config = configObject
156
+ // Set configFileType
157
+ this.configFileType = fileType
163
158
  // Keep a copy of the original file contents
164
159
  this.originalString = fileContents
165
- // Keep a copy
166
- this.originalConfig = cloneDeep(configObject)
167
160
  // Set configPath for file references
168
161
  this.configPath = fileDirectory
162
+ // Initialize config as null - will be populated in init
163
+ this.config = null
164
+ this.originalConfig = null
169
165
  }
170
166
 
171
167
  // Track promise resolution
@@ -187,6 +183,23 @@ class Configorama {
187
183
  * ${opt:other, "fallbackValue"}
188
184
  */
189
185
  getValueFromOptions,
186
+
187
+ /**
188
+ * Cron expressions
189
+ * Usage:
190
+ * ${cron(every minute)}
191
+ * ${cron(weekdays)}
192
+ * ${cron(at 9:30)}
193
+ */
194
+ getValueFromCron,
195
+
196
+ /**
197
+ * Eval expressions
198
+ * Usage:
199
+ * ${eval(${self:valueTwo} > ${self:valueOne})}
200
+ */
201
+ getValueFromEval,
202
+
190
203
  /**
191
204
  * Self references
192
205
  * Usage:
@@ -218,6 +231,16 @@ class Configorama {
218
231
  },
219
232
  },
220
233
 
234
+
235
+ {
236
+ type: 'text',
237
+ prefix: 'text',
238
+ match: textRefSyntax,
239
+ resolver: (varString, o, x, pathValue) => {
240
+ return this.getValueFromFile(varString, { asRawText: true })
241
+ },
242
+ },
243
+
221
244
  // Git refs
222
245
  createGitResolver(this.configPath),
223
246
  /* Internal Resolvers */
@@ -264,6 +287,7 @@ class Configorama {
264
287
  return deeperExists
265
288
  }
266
289
  }
290
+ // console.log('fallthrough fullObject', fullObject)
267
291
  /* is simple ${whatever} reference in same file */
268
292
  const startOf = varString.split('.')
269
293
  return fullObject[startOf[0]]
@@ -288,8 +312,9 @@ class Configorama {
288
312
  this.variableTypes = this.variableTypes.concat(fallThroughSelfMatcher)
289
313
 
290
314
  // const variablesKnownTypes = new RegExp(`^(${this.variableTypes.map((v) => v.prefix || v.type).join('|')}):`)
291
- const variablesKnownTypes = combineRegexes(this.variableTypes.filter((v) => v.type !== 'string').map((v) => v.match))
292
- // console.log('variablesKnownTypes', variablesKnownTypes)
315
+ const variablesKnownTypes = combineRegexes(
316
+ this.variableTypes.filter((v) => v.type !== 'string').map((v) => v.match)
317
+ )
293
318
  this.variablesKnownTypes = variablesKnownTypes
294
319
 
295
320
  // this.allPatterns = combineRegexes(...this.variableTypes.map((v) => v.match))
@@ -410,6 +435,49 @@ class Configorama {
410
435
  this.functions = Object.assign({}, this.functions, options.functions)
411
436
  }
412
437
 
438
+ this.deep = []
439
+ this.leaves = []
440
+ this.callCount = 0
441
+ }
442
+
443
+ initialCall(func) {
444
+ this.deep = []
445
+ this.tracker.start()
446
+ return func().finally(() => {
447
+ this.tracker.stop()
448
+ this.deep = []
449
+ })
450
+ }
451
+
452
+ /**
453
+ * Populate all variables in the service, conveniently remove and restore the service attributes
454
+ * that confuse the population methods.
455
+ * @param cliOpts An options hive to use for ${opt:...} variables.
456
+ * @returns {Promise.<TResult>|*} A promise resolving to the populated service.
457
+ */
458
+ async init(cliOpts) {
459
+ this.options = cliOpts || {}
460
+ const configoramaOpts = this.opts
461
+
462
+ const showFoundVariables = configoramaOpts && configoramaOpts.dynamicArgs && (configoramaOpts.dynamicArgs.list || configoramaOpts.dynamicArgs.info)
463
+
464
+ // If we have a file path but no config yet, parse it now
465
+ if (this.configFilePath && !this.config) {
466
+ const configObject = await parseFileContents(
467
+ this.originalString,
468
+ this.configFileType,
469
+ this.configFilePath,
470
+ this.variableSyntax,
471
+ this.opts
472
+ )
473
+ this.configFileContents = ''
474
+ if (VERBOSE || showFoundVariables) {
475
+ this.configFileContents = fs.readFileSync(this.configFilePath, 'utf8')
476
+ }
477
+ this.config = configObject
478
+ this.originalConfig = cloneDeep(configObject)
479
+ }
480
+
413
481
  if (VERBOSE) {
414
482
  logHeader('Config Input before processing')
415
483
  console.log()
@@ -417,10 +485,14 @@ class Configorama {
417
485
  console.log()
418
486
  }
419
487
 
488
+ const variableSyntax = this.variableSyntax
489
+ const variablesKnownTypes = this.variablesKnownTypes
490
+
420
491
  if (VERBOSE || showFoundVariables) {
421
492
  const foundVariables = []
422
493
  const variableData = {}
423
494
  let matchCount = 1
495
+ // console.log('this.originalConfig', this.originalConfig)
424
496
  traverse(this.originalConfig).forEach(function (rawValue) {
425
497
  if (typeof rawValue === 'string' && rawValue.match(variableSyntax)) {
426
498
  const configValuePath = this.path.join('.')
@@ -452,45 +524,63 @@ class Configorama {
452
524
  resolveOrder: [],
453
525
  resolveDetails: nested,
454
526
  }
455
-
527
+ let defaultValueIsVar = false
456
528
  function calculateResolveOrder(item) {
457
529
  if (item && item.fallbackValues) {
458
530
  let hasResolvedFallback
531
+ // console.log('item.fallbackValues', item.fallbackValues)
459
532
  const order = ([item.valueBeforeFallback]).concat(item.fallbackValues.map((f, i) => {
533
+ // console.log('f', f)
460
534
  if (f.fallbackValues) {
461
535
  const [nestedOrder, nestedResolvedFallback] = calculateResolveOrder(f)
536
+ // console.log('nestedOrder', nestedOrder)
537
+ // console.log('nestedResolvedFallback', nestedResolvedFallback)
462
538
  if (!hasResolvedFallback && nestedResolvedFallback) {
463
539
  hasResolvedFallback = nestedResolvedFallback
464
540
  }
465
541
  return nestedOrder // Return just the order part
466
542
  }
543
+
467
544
  if (!hasResolvedFallback && f.isResolvedFallback) {
468
545
  hasResolvedFallback = f.stringValue
469
546
  }
547
+ if (f.isResolvedFallback) {
548
+ hasResolvedFallback = f.stringValue
549
+ }
550
+
551
+ if (!hasResolvedFallback && f.isVariable) {
552
+ defaultValueIsVar = f
553
+ }
554
+ // console.log('hasResolvedFallback', hasResolvedFallback)
470
555
  return `${f.stringValue || f.variable}${f.isResolvedFallback ? ' (Resolved default fallback)' : ''}`
471
556
  })).flat()
472
557
 
473
558
  return [order, hasResolvedFallback]
474
559
  }
475
- return [[item.variable], false] // Return array instead of just the value
560
+ return [[item.variable], undefined] // Return array instead of just the value
476
561
  }
477
562
 
478
563
  const [resolveOrder, hasResolvedFallback] = calculateResolveOrder(lastItem)
479
564
  varData.resolveOrder = resolveOrder
480
565
 
481
- if (hasResolvedFallback) {
482
- varData.defaultValue = hasResolvedFallback
566
+ if (defaultValueIsVar) {
567
+ varData.defaultValueIsVar = defaultValueIsVar
483
568
  }
484
569
 
570
+ // console.log('hasResolvedFallback', hasResolvedFallback)
571
+ if (typeof hasResolvedFallback !== 'undefined') {
572
+ varData.defaultValue = hasResolvedFallback
573
+ }
485
574
 
486
- if (!varData.defaultValue) {
575
+ // console.log('varData.defaultValue', varData.defaultValue)
576
+ if (typeof varData.defaultValue === 'undefined') {
487
577
  varData.isRequired = true
488
578
  }
489
579
 
490
-
491
580
  if (varData.resolveOrder.length > 1) {
492
581
  varData.hasFallback = true
493
582
  }
583
+ //console.log('varData', varData)
494
584
 
495
585
  variableData[key] = (variableData[key] || []).concat(varData)
496
586
 
@@ -498,7 +588,6 @@ class Configorama {
498
588
  }
499
589
  })
500
590
 
501
-
502
591
  if (!foundVariables.length) {
503
592
  logHeader('No Variables Found in Config')
504
593
  if (this.configFilePath) {
@@ -545,20 +634,35 @@ class Configorama {
545
634
  }
546
635
 
547
636
  logHeader('Variable Details')
637
+
638
+ const lines = this.configFileContents.split('\n')
639
+ // console.log('lines', lines)
548
640
 
549
641
  const indent = ''
550
- varKeys.forEach((key, i) => {
642
+ const boxes = varKeys.map((key, i) => {
551
643
  const variableInstances = variableData[key]
644
+ // console.log('variableInstances', variableInstances)
552
645
 
553
646
  const firstInstance = variableInstances[0]
554
647
 
555
648
  let requiredText = ''
556
649
  let defaultValueSrc = ''
557
- if (!firstInstance.defaultValue) {
650
+ if (typeof firstInstance.defaultValue === 'undefined') {
558
651
  // console.log('no default value', firstInstance)
652
+
653
+ let dotPropArr = []
654
+ if (firstInstance.defaultValueIsVar && (
655
+ firstInstance.defaultValueIsVar.varType === 'self:' ||
656
+ firstInstance.defaultValueIsVar.varType === 'dot.prop'
657
+ )) {
658
+ dotPropArr = [firstInstance.defaultValueIsVar]
659
+ }
559
660
  /* Check if the fallback variable is a self reference */
560
661
  const hasDotPropOrSelf = variableInstances.reduce((acc, v) => {
561
- const dotProp = v.resolveDetails.find((d) => d.varType === 'dot.prop')
662
+ const dotProp = v.resolveDetails.find((d) => {
663
+ // console.log('d', d)
664
+ return d.varType === 'dot.prop'
665
+ })
562
666
  if (dotProp) {
563
667
  acc.push(dotProp)
564
668
  }
@@ -567,10 +671,12 @@ class Configorama {
567
671
  acc.push(v.resolveDetails[0])
568
672
  }
569
673
  return acc
570
- }, [])
674
+ }, dotPropArr)
571
675
  // console.log('hasDotPropOrSelf', hasDotPropOrSelf)
676
+
572
677
  if (!hasDotPropOrSelf.length) {
573
- requiredText = '[Required] '
678
+ const debug = (false) ? JSON.stringify(firstInstance, null, 2) : ''
679
+ requiredText = `[Required Variable] ${debug}`
574
680
  } else {
575
681
  const fallBackValues = variableInstances.filter((v) => v.resolveDetails.find((d) => d.hasFallback)).map((v) => v.resolveDetails)
576
682
  // console.log('fallBackValues', fallBackValues)
@@ -589,6 +695,9 @@ class Configorama {
589
695
  // truncate niceString to 100 characters
590
696
  const truncatedString = niceString.length > 100 ? niceString.substring(0, 90) + '...' : niceString
591
697
  firstInstance.defaultValue = truncatedString
698
+ } else {
699
+ deepLog('Missing default var', firstInstance)
700
+ throw new Error(`Variable misconfiguration at ${firstInstance.variable}\n\n"${hasDotPropOrSelf[0].variable}" resolves to undefined value.\n`)
592
701
  }
593
702
  }
594
703
  //this.originalConfig[key] = undefined
@@ -601,14 +710,16 @@ class Configorama {
601
710
  const keyChalk = chalk.whiteBright
602
711
  const valueChalk = chalk.hex(VALUE_HEX)
603
712
 
604
- if (firstInstance.defaultValue) {
605
- const defaultValueText = `${indent}${keyChalk(`DefaultValue:`.padEnd(titleText.length, ' '))}`
713
+ if (typeof firstInstance.defaultValue !== 'undefined') {
714
+ // console.log('firstInstance.defaultValue', firstInstance.defaultValue)
715
+ const defaultValueRender = firstInstance.defaultValue === '' ? '""' : firstInstance.defaultValue
716
+ const defaultValueText = `${indent}${keyChalk(`Default value:`.padEnd(titleText.length, ' '))}`
606
717
  // ensure padding is even
607
- varMsg += `${defaultValueText} ${valueChalk(firstInstance.defaultValue)}`
718
+ varMsg += `${defaultValueText} ${valueChalk(defaultValueRender)}`
608
719
  }
609
720
 
610
- if(defaultValueSrc) {
611
- varMsg += `\n${indent}${keyChalk('DefaultValue path:'.padEnd(titleText.length, ' '))} `
721
+ if (defaultValueSrc) {
722
+ varMsg += `\n${indent}${keyChalk('Default value path:'.padEnd(titleText.length, ' '))} `
612
723
  varMsg += `${valueChalk(defaultValueSrc)}`
613
724
  }
614
725
 
@@ -620,26 +731,34 @@ class Configorama {
620
731
 
621
732
  let locationRender = valueChalk(variableInstances[0].path)
622
733
 
623
- let locationLabel = `${indent}${keyChalk('Used at:'.padEnd(titleText.length, ' '))}`
734
+ let locationLabel = `${indent}${keyChalk('Path:'.padEnd(titleText.length, ' '))}`
624
735
  if (variableInstances.length > 1) {
625
736
  locationRender = `\n${variableInstances.map((v) => valueChalk(`${indent}- ${v.path}`)).join('\n')}`
626
- const locationLabelText = `${indent}${keyChalk('Used at:')}`
737
+ const locationLabelText = `${indent}${keyChalk('Paths:')}`
627
738
  locationLabel = locationLabelText
628
739
  }
629
740
 
630
741
  varMsg += `\n${locationLabel} ${locationRender}`
742
+
743
+ // find the match in our lines
744
+ const line = lines.findIndex((line) => line.includes(key))
745
+ const lineNumber = line + 1
631
746
 
632
747
  // console.log(` ${chalk.bold(key)}`)
633
- console.log(makeBox(varMsg, {
634
- title: `${key}`,
635
- borderColor: 'gray',
636
- // style: 'bold',
637
- minWidth: 120,
638
- }))
639
- if(i < varKeys.length - 1) {
640
- //console.log()
748
+ return {
749
+ text: varMsg,
750
+ title: {
751
+ left: `▶ ${lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, key) : key}`,
752
+ right: lineNumber ? createEditorLink(this.configFilePath, lineNumber, 1, `Line: ${lineNumber}`, 'gray') : '',
753
+ },
641
754
  }
642
755
  })
756
+
757
+ console.log(makeStackedBoxes(boxes, {
758
+ borderColor: 'gray',
759
+ minWidth: 120,
760
+ borderStyle: 'bold',
761
+ }))
643
762
  }
644
763
 
645
764
  /* Exit early if list or info flag is set */
@@ -648,33 +767,11 @@ class Configorama {
648
767
  }
649
768
  }
650
769
 
651
- this.deep = []
652
- this.callCount = 0
653
- }
654
-
655
- initialCall(func) {
656
- this.deep = []
657
- this.tracker.start()
658
- return func().finally(() => {
659
- this.tracker.stop()
660
- this.deep = []
661
- })
662
- }
663
-
664
- /**
665
- * Populate all variables in the service, conveniently remove and restore the service attributes
666
- * that confuse the population methods.
667
- * @param cliOpts An options hive to use for ${opt:...} variables.
668
- * @returns {Promise.<TResult>|*} A promise resolving to the populated service.
669
- */
670
- init(cliOpts) {
671
- this.options = cliOpts || {}
672
- const configoramaOpts = this.opts
673
770
  const originalConfig = this.originalConfig
674
771
 
675
772
  /* If no variables found just return early */
676
773
  if (this.originalString && !this.originalString.match(this.variableSyntax)) {
677
- return Promise.resolve(originalConfig)
774
+ return Promise.resolve(this.originalConfig)
678
775
  }
679
776
 
680
777
  const useDotEnv = this.originalConfig.useDotenv || this.originalConfig.useDotEnv
@@ -707,18 +804,28 @@ class Configorama {
707
804
  // console.log('Final Config', this.config)
708
805
  const transform = this.runFunction.bind(this)
709
806
  const varSyntax = this.variableSyntax
807
+ const leaves = this.leaves
808
+ // console.log('leaves two', leaves)
710
809
  // Traverse resolved object and run functions
711
810
  // console.log('this.config', this.config)
712
811
  traverse(this.config).forEach(function (rawValue) {
713
812
  /* Pass through unknown variables */
714
813
  if (!configoramaOpts.allowUndefinedValues && typeof rawValue === 'undefined') {
715
814
  const configValuePath = this.path.join('.')
815
+ console.log(this.path)
716
816
  const ogValue = dotProp.get(originalConfig, configValuePath)
717
817
  const varDisplay = ogValue ? `"${ogValue}" variable` : 'variable'
818
+
819
+ const leaf = leaves.find((l) => l.path.join('.') === configValuePath)
820
+ // if (leaf) {
821
+ // deepLog('leaf', leaf)
822
+ // }
718
823
  const errorMessage = `
719
- Config error:
720
- "${configValuePath}" resolved to "undefined"
721
- Verify the ${varDisplay} in config at "${configValuePath}"`
824
+ Config error:\n
825
+ Path "${configValuePath}" resolved to "undefined".\n
826
+ Verify the ${varDisplay} in config at "${configValuePath}".\n
827
+ ${leaf ? `See:\n ${leaf.originalValuePath}: ${leaf.originalSource} ` : ''}
828
+ ${leaf && leaf.isFileRef ? `\n The error could be deeper in the referenced file at ${configValuePath.replace(leaf.originalValuePath, '').replace(/^\./, '')} key.\n` : ''}`
722
829
  throw new Error(errorMessage)
723
830
  }
724
831
  if (typeof rawValue === 'string') {
@@ -742,6 +849,13 @@ class Configorama {
742
849
  }
743
850
  }
744
851
 
852
+ /* fix for file(JS-ref.js, raw) to keep parens and inline code */
853
+ const OPEN_PAREN_PLACEHOLDER_PATTERN = /__PH_PAREN_OPEN__/g
854
+ if (rawValue.match(OPEN_PAREN_PLACEHOLDER_PATTERN)) {
855
+ rawValue = rawValue.replace(OPEN_PAREN_PLACEHOLDER_PATTERN, '(')
856
+ this.update(rawValue)
857
+ }
858
+
745
859
  /* Allow for unknown variables to pass through */
746
860
  if (rawValue.match(/>passthrough/)) {
747
861
  const newValues = decodeUnknown(rawValue)
@@ -783,7 +897,7 @@ class Configorama {
783
897
  var hasFunc = funcRegex.exec(variableString)
784
898
  // TODO finish Function handling. Need to move this down below resolver to resolve inner refs first
785
899
  // console.log('hasFunc', hasFunc)
786
- if (!hasFunc) {
900
+ if (!hasFunc || hasFunc && (hasFunc[1] === 'cron' || hasFunc[1] === 'eval')) {
787
901
  return variableString
788
902
  }
789
903
  // test for object
@@ -881,12 +995,23 @@ class Configorama {
881
995
  value: current,
882
996
  }
883
997
  const thePath = leaf.path.length > 1 ? leaf.path.join('.') : leaf.path[0]
998
+ // console.log('thePath', thePath)
999
+ // console.log('this.originalConfig', this.originalConfig)
884
1000
  let originalValue = dotProp.get(this.originalConfig, thePath)
885
1001
  // TODO @DWELLS make recursive
886
1002
  if (!originalValue) {
887
- const parentArray = leaf.path.slice(0, -1)
888
- const parentPath = parentArray > 1 ? parentArray.join('.') : parentArray[0]
889
- originalValue = dotProp.get(this.originalConfig, parentPath)
1003
+ // Recurse up the tree until we find a value
1004
+ let currentPathArray = leaf.path.slice(0, -1)
1005
+ while (currentPathArray.length > 0 && !originalValue) {
1006
+ const currentPath = currentPathArray.length > 1 ? currentPathArray.join('.') : currentPathArray[0]
1007
+ // console.log('checking parent path:', currentPath)
1008
+ originalValue = dotProp.get(this.originalConfig, currentPath)
1009
+ if (typeof originalValue !== 'undefined') {
1010
+ leaf.originalValuePath = currentPath
1011
+ leaf.currentConfig = this.config
1012
+ }
1013
+ currentPathArray = currentPathArray.slice(0, -1)
1014
+ }
890
1015
  }
891
1016
  leaf.originalSource = originalValue
892
1017
  if (originalValue && isString(originalValue)) {
@@ -913,11 +1038,27 @@ class Configorama {
913
1038
  */
914
1039
  populateVariables(properties) {
915
1040
  // console.log('properties', properties)
916
- const variables = properties.filter((property) => {
1041
+ let variables = properties.filter((property) => {
917
1042
  // Initial check if value has variable string in it
918
1043
  return isString(property.value) && property.value.match(this.variableSyntax)
919
1044
  })
920
1045
 
1046
+ /*
1047
+ console.log(`variables at call count ${this.callCount}`, variables)
1048
+ /** */
1049
+
1050
+ /* Exclude git messages from being processed */
1051
+ // Was failing on git msgs like "xyz cron:pattern to cron(pattern) for improved clarity"
1052
+ if (this.callCount > 1) {
1053
+ // filter out git vars
1054
+ variables = variables.filter(property => {
1055
+ if (property.originalSource && typeof property.originalSource === 'string') {
1056
+ return !property.originalSource.startsWith('${git:')
1057
+ }
1058
+ return true
1059
+ })
1060
+ }
1061
+
921
1062
  return map(variables, (valueObject) => {
922
1063
  // console.log('valueObject', valueObject)
923
1064
  return this.populateValue(valueObject, false, '_populateVariables').then((populated) => {
@@ -959,6 +1100,7 @@ class Configorama {
959
1100
  }
960
1101
 
961
1102
  const leaves = this.getProperties(objectToPopulate, true, objectToPopulate)
1103
+ this.leaves = leaves
962
1104
  // console.log('leaves', leaves)
963
1105
  const populations = this.populateVariables(leaves)
964
1106
  // console.log("FILL LEAVES", populations)
@@ -1302,7 +1444,19 @@ Missing Value ${missingValue} - ${matchedString}
1302
1444
 
1303
1445
  if (property && typeof property === 'string') {
1304
1446
  // console.log('property', property)
1305
- const prop = cleanVariable(property, this.variableSyntax, true, `populateVariable string ${this.callCount}`)
1447
+ let prop = cleanVariable(
1448
+ property,
1449
+ this.variableSyntax,
1450
+ true,
1451
+ `populateVariable string ${this.callCount}`,
1452
+ // true // recursive
1453
+ )
1454
+
1455
+ // Double processing needed for `${eval(${self:three} > ${self:four})}`
1456
+ if (prop.startsWith('${')) {
1457
+ prop = cleanVariable(prop, this.variableSyntax, true, `populateVariable string ${this.callCount}`)
1458
+ }
1459
+
1306
1460
  // console.log('prop', prop)
1307
1461
  if (property.match(/^> function /g) && prop) {
1308
1462
  // console.log('func prop', property)
@@ -1341,7 +1495,10 @@ Missing Value ${missingValue} - ${matchedString}
1341
1495
  }
1342
1496
  */
1343
1497
  // Does not match file refs with nested vars + args
1344
- if (!prop.match(/file\((~?[a-zA-Z0-9._\-\/,'"\{\}\.$: ]+?)\)/) && func) {
1498
+ // @TODO fix this for eval refs
1499
+ // console.log('prop', prop)
1500
+ // console.log('func', func)
1501
+ if (!prop.match(fileRefSyntax) && !prop.match(getValueFromEval.match) && func) {
1345
1502
  // console.log('IS FUNCTION')
1346
1503
  /* if matches function signature like ${merge('foo', 'bar')}
1347
1504
  rewrite the variable to run the function after inputs resolved
@@ -1492,7 +1649,7 @@ Missing Value ${missingValue} - ${matchedString}
1492
1649
  if (filters) {
1493
1650
  const string = cleanVariable(propertyString, this.variableSyntax, true, `getValueFromSrc filter ${this.callCount}`)
1494
1651
  // console.log('string', string)
1495
- const deeperValue = getTextAfterOccurance(string, variableString)
1652
+ const deeperValue = getTextAfterOccurrence(string, variableString)
1496
1653
  // console.log('deeperValue', deeperValue)
1497
1654
  // console.log('filters', filters)
1498
1655
  // console.log('variableString', variableString)
@@ -1845,15 +2002,19 @@ Unable to resolve configuration variable
1845
2002
  return res
1846
2003
  })
1847
2004
  }
1848
- getValueFromFile(variableString) {
2005
+ async getValueFromFile(variableString, options) {
2006
+ const opts = options || {}
2007
+ const syntax = opts.asRawText ? textRefSyntax : fileRefSyntax
1849
2008
  // console.log('From file', `"${variableString}"`)
1850
- let matchedFileString = variableString.match(fileRefSyntax)[0]
2009
+ let matchedFileString = variableString.match(syntax)[0]
1851
2010
  // console.log('matchedFileString', matchedFileString)
1852
2011
 
1853
- // Get function input params if any supplied
1854
- var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2012
+ // Get function input params if any supplied https://regex101.com/r/qlNFVm/1
2013
+ // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
2014
+ var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
2015
+ // tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
1855
2016
  var hasParams = funcParamsRegex.exec(matchedFileString)
1856
- // console.log('args', hasParams)
2017
+
1857
2018
  let argsToPass = []
1858
2019
  if (hasParams) {
1859
2020
  const splitter = splitCsv(hasParams[2])
@@ -1861,6 +2022,7 @@ Unable to resolve configuration variable
1861
2022
  const cleanArg = trim(arg).replace(/^'|"/, '').replace(/'|"$/, '')
1862
2023
  return cleanArg
1863
2024
  })
2025
+ // console.log('argsFound', argsFound)
1864
2026
 
1865
2027
  // If function has more arguments than file path
1866
2028
  if (argsFound.length && argsFound.length > 1) {
@@ -1873,10 +2035,14 @@ Unable to resolve configuration variable
1873
2035
  // console.log('argsToPass', argsToPass)
1874
2036
 
1875
2037
  const relativePath = trimSurroundingQuotes(
1876
- matchedFileString.replace(fileRefSyntax, (match, varName) => varName.trim()).replace('~', os.homedir()),
2038
+ matchedFileString.replace(syntax, (match, varName) => varName.trim()).replace('~', os.homedir()),
1877
2039
  )
1878
2040
 
1879
- let fullFilePath = path.isAbsolute(relativePath) ? relativePath : path.join(this.configPath, relativePath)
2041
+ // Resolve alias if the path contains alias syntax
2042
+ const resolvedPath = resolveAlias(relativePath, this.configPath)
2043
+ // console.log('resolvedPath', resolvedPath)
2044
+
2045
+ let fullFilePath = path.isAbsolute(resolvedPath) ? resolvedPath : path.join(this.configPath, resolvedPath)
1880
2046
 
1881
2047
  // console.log('fullFilePath', fullFilePath)
1882
2048
 
@@ -1885,13 +2051,13 @@ Unable to resolve configuration variable
1885
2051
  fullFilePath = fs.realpathSync(fullFilePath)
1886
2052
 
1887
2053
  // Only match files that are relative
1888
- } else if (relativePath.match(/\.\//)) {
2054
+ } else if (resolvedPath.match(/\.\//)) {
1889
2055
  // TODO test higher parent refs
1890
- const cleanName = path.basename(relativePath)
2056
+ const cleanName = path.basename(resolvedPath)
1891
2057
  fullFilePath = findUp.sync(cleanName, { cwd: this.configPath })
1892
2058
  }
1893
2059
 
1894
- let fileExtension = relativePath.split('.')
2060
+ let fileExtension = resolvedPath.split('.')
1895
2061
 
1896
2062
  fileExtension = fileExtension[fileExtension.length - 1]
1897
2063
 
@@ -1916,8 +2082,16 @@ ${logLines}
1916
2082
 
1917
2083
  let valueToPopulate
1918
2084
 
2085
+ const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
2086
+
2087
+ /* handle case for referencing raw JS files to inline them */
2088
+ if (argsToPass.length && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw') || opts.asRawText) {
2089
+ valueToPopulate = variableFileContents.replace(/\(/g, '__PH_PAREN_OPEN__')
2090
+ return Promise.resolve(valueToPopulate)
2091
+ }
2092
+
1919
2093
  // Process JS files
1920
- if (fileExtension === 'js') {
2094
+ if (fileExtension === 'js' || fileExtension === 'cjs') {
1921
2095
  const jsFile = require(fullFilePath)
1922
2096
  let returnValueFunction = jsFile
1923
2097
  // TODO change how exported functions are referenced
@@ -1941,7 +2115,7 @@ Check if your javascript is exporting a function that returns a value.`
1941
2115
  config: this.config,
1942
2116
  opts: this.opts,
1943
2117
  }
1944
-
2118
+
1945
2119
  valueToPopulate = returnValueFunction.call(jsFile, valueForFunction, ...argsToPass)
1946
2120
 
1947
2121
  return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
@@ -1963,10 +2137,116 @@ Check if your javascript is returning the correct data.`
1963
2137
  })
1964
2138
  }
1965
2139
 
1966
- // Process everything except JS
1967
- if (fileExtension !== 'js') {
2140
+ if (fileExtension === 'ts') {
2141
+ const { executeTypeScriptFile } = require('./parsers/typescript')
2142
+ let returnValueFunction
2143
+ const variableArray = variableString.split(':')
2144
+
2145
+ try {
2146
+ const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
2147
+ // console.log('fullFilePath', fullFilePath)
2148
+ // console.log('tsFile', tsFile)
2149
+ returnValueFunction = tsFile.config || tsFile.default || tsFile
2150
+
2151
+ if (variableArray[1]) {
2152
+ let tsModule = variableArray[1]
2153
+ tsModule = tsModule.split('.')[0]
2154
+ returnValueFunction = tsFile[tsModule]
2155
+ }
2156
+
2157
+ if (typeof returnValueFunction !== 'function') {
2158
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2159
+ Check if your TypeScript is exporting a function that returns a value.`
2160
+ return Promise.reject(new Error(errorMessage))
2161
+ }
2162
+
2163
+ const valueForFunction = {
2164
+ originalConfig: this.originalConfig,
2165
+ config: this.config,
2166
+ opts: this.opts,
2167
+ }
2168
+
2169
+ valueToPopulate = returnValueFunction.call(tsFile, valueForFunction, ...argsToPass)
2170
+
2171
+ return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
2172
+ let deepProperties = variableString.replace(matchedFileString, '')
2173
+ deepProperties = deepProperties.slice(1).split('.')
2174
+ deepProperties.splice(0, 1)
2175
+ // Trim prop keys for starting/trailing spaces
2176
+ deepProperties = deepProperties.map((prop) => {
2177
+ return trim(prop)
2178
+ })
2179
+ return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
2180
+ if (typeof deepValueToPopulateResolved === 'undefined') {
2181
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2182
+ Check if your TypeScript is returning the correct data.`
2183
+ return Promise.reject(new Error(errorMessage))
2184
+ }
2185
+ return Promise.resolve(deepValueToPopulateResolved)
2186
+ })
2187
+ })
2188
+ } catch (err) {
2189
+ return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
2190
+ }
2191
+ }
2192
+
2193
+ if (fileExtension === 'mjs' || fileExtension === 'esm') {
2194
+ const { executeESMFile } = require('./parsers/esm')
2195
+ let returnValueFunction
2196
+ const variableArray = variableString.split(':')
2197
+
2198
+ try {
2199
+ const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
2200
+ // console.log('ESM fullFilePath', fullFilePath)
2201
+ // console.log('ESM esmFile', esmFile, 'type:', typeof esmFile)
2202
+ returnValueFunction = esmFile.config || esmFile.default || esmFile
2203
+
2204
+ if (variableArray[1]) {
2205
+ let esmModule = variableArray[1]
2206
+ esmModule = esmModule.split('.')[0]
2207
+ returnValueFunction = esmFile[esmModule]
2208
+ }
2209
+
2210
+ if (typeof returnValueFunction !== 'function') {
2211
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2212
+ Check if your ESM is exporting a function that returns a value.`
2213
+ return Promise.reject(new Error(errorMessage))
2214
+ }
2215
+
2216
+ const valueForFunction = {
2217
+ originalConfig: this.originalConfig,
2218
+ config: this.config,
2219
+ opts: this.opts,
2220
+ }
2221
+
2222
+ valueToPopulate = returnValueFunction.call(esmFile, valueForFunction, ...argsToPass)
2223
+
2224
+ return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
2225
+ let deepProperties = variableString.replace(matchedFileString, '')
2226
+ deepProperties = deepProperties.slice(1).split('.')
2227
+ deepProperties.splice(0, 1)
2228
+ // Trim prop keys for starting/trailing spaces
2229
+ deepProperties = deepProperties.map((prop) => {
2230
+ return trim(prop)
2231
+ })
2232
+ return this.getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
2233
+ if (typeof deepValueToPopulateResolved === 'undefined') {
2234
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
2235
+ Check if your ESM is returning the correct data.`
2236
+ return Promise.reject(new Error(errorMessage))
2237
+ }
2238
+ return Promise.resolve(deepValueToPopulateResolved)
2239
+ })
2240
+ })
2241
+ } catch (err) {
2242
+ return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
2243
+ }
2244
+ }
2245
+
2246
+ // Process everything except JS, TS, and ESM
2247
+ if (fileExtension !== 'js' && fileExtension !== 'ts' && fileExtension !== 'mjs' && fileExtension !== 'esm') {
1968
2248
  /* Read initial file */
1969
- valueToPopulate = fs.readFileSync(fullFilePath, 'utf-8')
2249
+ valueToPopulate = variableFileContents
1970
2250
 
1971
2251
  // File reference has :subKey lookup. Must dig deeper
1972
2252
  if (matchedFileString !== variableString) {
@@ -1976,6 +2256,9 @@ Check if your javascript is returning the correct data.`
1976
2256
  if (fileExtension === 'toml') {
1977
2257
  valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
1978
2258
  }
2259
+ if (fileExtension === 'ini') {
2260
+ valueToPopulate = INI.toJson(valueToPopulate)
2261
+ }
1979
2262
  // console.log('deep', variableString)
1980
2263
  // console.log('matchedFileString', matchedFileString)
1981
2264
  let deepProperties = variableString.replace(matchedFileString, '')
@@ -1998,11 +2281,17 @@ Please use ":" to reference sub properties`
1998
2281
  return Promise.resolve(valueToPopulate)
1999
2282
  }
2000
2283
 
2284
+ if (fileExtension === 'ini') {
2285
+ valueToPopulate = INI.parse(valueToPopulate)
2286
+ return Promise.resolve(valueToPopulate)
2287
+ }
2288
+
2001
2289
  if (fileExtension === 'json') {
2002
2290
  valueToPopulate = JSON.parse(valueToPopulate)
2003
2291
  return Promise.resolve(valueToPopulate)
2004
2292
  }
2005
2293
  }
2294
+ console.log('fall thru', valueToPopulate)
2006
2295
  return Promise.resolve(valueToPopulate)
2007
2296
  }
2008
2297
  getVariableFromDeep(variableString) {
@@ -2140,6 +2429,8 @@ Please use ":" to reference sub properties`
2140
2429
  varType = 'file'
2141
2430
  } else if (variableString.match(deepRefSyntax)) {
2142
2431
  varType = 'deep'
2432
+ } else if (variableString.match(textRefSyntax)) {
2433
+ varType = 'text'
2143
2434
  }
2144
2435
  if (!isValidValue(valueToPopulate)) {
2145
2436
  // console.log("MISSING", variableString)