configorama 0.6.6 → 0.6.8

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.
@@ -21,11 +21,31 @@ async function _exec(cmd, options = { timeout: 1000 }) {
21
21
  })
22
22
  }
23
23
 
24
+ // TODO denote computed fields in metadata
25
+ /*
26
+ {
27
+ variables: {
28
+ repo: {
29
+ value: '${git:repo}',
30
+ type: 'string',
31
+ description: 'The repository owner and name',
32
+ }
33
+ },
34
+ computedVariables : {
35
+ hash: {
36
+ value: '${git:sha1}',
37
+ type: 'string',
38
+ description: 'The current commit hash',
39
+ }
40
+ }
41
+ }
42
+ */
43
+
24
44
  function createResolver(cwd) {
25
45
  async function _getValueFromGit(variableString) {
26
46
  const variable = variableString.split(`${GIT_PREFIX}:`)[1]
27
47
  let value = null
28
- // console.log('variableStringvariableString', variableString)
48
+ // console.log('createResolver variableString', variableString)
29
49
  if (variable.match(/^remote/i)) {
30
50
  const hasParams = functionRegex.exec(variableString)
31
51
  const remoteName = (hasParams && hasParams[2]) ? formatFunctionArgs(hasParams[2]) : 'origin'
package/src/sync.js CHANGED
@@ -2,6 +2,7 @@ const path = require('path')
2
2
  const fs = require('fs')
3
3
  const Configorama = require('./main')
4
4
  const getFullPath = require('./utils/getFullFilePath')
5
+ const enrichMetadata = require('./utils/enrichMetadata')
5
6
 
6
7
  /**
7
8
  * Force syncronous invocation of async API
@@ -37,7 +38,7 @@ module.exports = function configoramaSync(varSrcs = []) {
37
38
  resolver: resolverFunction
38
39
  }
39
40
  })
40
- return (args) => {
41
+ return async (args) => {
41
42
  const { filePath, settings = {} } = args
42
43
  const syncSettings = { sync: true }
43
44
  if (customVariableSources && customVariableSources.length) {
@@ -45,7 +46,21 @@ module.exports = function configoramaSync(varSrcs = []) {
45
46
  }
46
47
  const finalSettings = Object.assign({}, settings, syncSettings)
47
48
  const options = finalSettings.options || {}
48
- const config = new Configorama(filePath, finalSettings)
49
- return config.init(options)
49
+ const instance = new Configorama(filePath, finalSettings)
50
+ const result = await instance.init(options)
51
+
52
+ if (finalSettings.returnMetadata) {
53
+ const metadata = instance.collectVariableMetadata()
54
+
55
+ // Enrich metadata with resolution tracking data collected during execution
56
+ const enrichedMetadata = enrichMetadata(metadata, instance.resolutionTracking, instance.variableSyntax)
57
+
58
+ return {
59
+ config: result,
60
+ metadata: enrichedMetadata
61
+ }
62
+ }
63
+
64
+ return result
50
65
  }
51
66
  }
@@ -0,0 +1,229 @@
1
+ const { splitCsv } = require('./splitCsv')
2
+
3
+ /**
4
+ * Extract file path from a file() or text() reference string
5
+ * @param {string} propertyString - The property string containing file/text reference
6
+ * @returns {object|null} Object with filePath, or null if no match
7
+ */
8
+ function extractFilePath(propertyString) {
9
+ const fileMatch = propertyString.match(/^\$\{(?:file|text)\((.*?)\)/)
10
+ if (!fileMatch || !fileMatch[1]) {
11
+ return null
12
+ }
13
+
14
+ const fileContent = fileMatch[1].trim()
15
+ const parts = splitCsv(fileContent)
16
+ let filePath = parts[0].trim()
17
+
18
+ // Remove quotes if present
19
+ filePath = filePath.replace(/^['"]|['"]$/g, '')
20
+
21
+ return { filePath }
22
+ }
23
+
24
+ /**
25
+ * Normalize a file path (add ./ prefix, fix .//, skip deep refs)
26
+ * @param {string} filePath - The file path to normalize
27
+ * @returns {string|null} Normalized path, or null if should be skipped
28
+ */
29
+ function normalizePath(filePath) {
30
+ // Skip deep references
31
+ if (filePath.includes('deep:')) {
32
+ return null
33
+ }
34
+
35
+ let normalized = filePath
36
+
37
+ // Add ./ prefix for relative paths
38
+ if (!filePath.startsWith('./') &&
39
+ !filePath.startsWith('../') &&
40
+ !filePath.startsWith('/') &&
41
+ !filePath.startsWith('~')) {
42
+ normalized = './' + filePath
43
+ }
44
+
45
+ // Fix double slashes
46
+ if (normalized.startsWith('.//')) {
47
+ normalized = normalized.replace('.//', './')
48
+ }
49
+
50
+ return normalized
51
+ }
52
+
53
+ // Enriches variable metadata with resolution tracking data
54
+ /**
55
+ * @param {object} metadata - The metadata object from collectVariableMetadata
56
+ * @param {object} resolutionTracking - The resolution tracking data from Configorama instance
57
+ * @param {RegExp} variableSyntax - The variable syntax regex to detect variables in file paths
58
+ * @returns {object} Enriched metadata with afterInnerResolution and resolvedFileRefs
59
+ */
60
+ function enrichMetadata(metadata, resolutionTracking, variableSyntax) {
61
+ if (!resolutionTracking) {
62
+ return metadata
63
+ }
64
+
65
+ const varKeys = Object.keys(metadata.variables)
66
+
67
+ for (const key of varKeys) {
68
+ const varInstances = metadata.variables[key]
69
+
70
+ for (const varData of varInstances) {
71
+ const pathKey = varData.path
72
+ const trackingData = resolutionTracking[pathKey]
73
+
74
+ if (trackingData && trackingData.calls && varData.resolveDetails) {
75
+ // The last call represents the final state (all inner vars resolved)
76
+ const lastCall = trackingData.calls[trackingData.calls.length - 1]
77
+
78
+ // For each resolveDetail, find the matching call and set afterInnerResolution
79
+ for (let i = 0; i < varData.resolveDetails.length; i++) {
80
+ const detail = varData.resolveDetails[i]
81
+ const isOutermost = i === varData.resolveDetails.length - 1
82
+
83
+ if (isOutermost) {
84
+ // For the outermost variable, use the last call's propertyString
85
+ // This shows the state after all inner variables have been resolved
86
+ let afterResolution = lastCall.propertyString
87
+ if (afterResolution.startsWith('${') && afterResolution.endsWith('}')) {
88
+ afterResolution = afterResolution.slice(2, -1)
89
+ }
90
+ detail.afterInnerResolution = afterResolution
91
+
92
+ if (lastCall.resolvedValue !== undefined) {
93
+ detail.resolvedValue = lastCall.resolvedValue
94
+ }
95
+ } else {
96
+ // For inner variables, try to find a matching call
97
+ for (const call of trackingData.calls) {
98
+ const callVar = call.variableString
99
+ const detailVar = detail.variable
100
+
101
+ if (callVar === detailVar || callVar.includes(detail.varString)) {
102
+ let afterResolution = call.propertyString
103
+ if (afterResolution.startsWith('${') && afterResolution.endsWith('}')) {
104
+ afterResolution = afterResolution.slice(2, -1)
105
+ }
106
+ detail.afterInnerResolution = afterResolution
107
+
108
+ if (call.resolvedValue !== undefined) {
109
+ detail.resolvedValue = call.resolvedValue
110
+ }
111
+ break
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ // Build resolvedFileRefs array from tracking data
121
+ // Only use the LAST call for each path (final resolved state)
122
+ const resolvedFileRefs = []
123
+ const normalizedPaths = new Set()
124
+
125
+ for (const pathKey in resolutionTracking) {
126
+ const tracking = resolutionTracking[pathKey]
127
+ if (tracking.calls && tracking.calls.length) {
128
+ const lastCall = tracking.calls[tracking.calls.length - 1]
129
+
130
+ const extracted = extractFilePath(lastCall.propertyString)
131
+ if (extracted) {
132
+ const normalizedPath = normalizePath(extracted.filePath)
133
+
134
+ if (normalizedPath && !normalizedPaths.has(normalizedPath)) {
135
+ normalizedPaths.add(normalizedPath)
136
+ resolvedFileRefs.push(normalizedPath)
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ metadata.resolvedFileRefs = resolvedFileRefs
143
+
144
+ // Build resolvedFileRefsData array - maps resolved paths to their variable strings and glob patterns
145
+ const resolvedFileRefsDataMap = new Map()
146
+
147
+ // First pass: collect all resolved paths and their original variable strings
148
+ for (const pathKey in resolutionTracking) {
149
+ const tracking = resolutionTracking[pathKey]
150
+ if (!tracking.calls || !tracking.calls.length) {
151
+ continue
152
+ }
153
+
154
+ const lastCall = tracking.calls[tracking.calls.length - 1]
155
+ const extracted = extractFilePath(lastCall.propertyString)
156
+
157
+ if (!extracted) {
158
+ continue
159
+ }
160
+
161
+ const resolvedPath = normalizePath(extracted.filePath)
162
+ if (!resolvedPath) {
163
+ continue
164
+ }
165
+
166
+ // Get original variable string from tracking
167
+ const originalPropertyString = tracking.originalPropertyString
168
+ if (!originalPropertyString) {
169
+ continue
170
+ }
171
+
172
+ const origExtracted = extractFilePath(originalPropertyString)
173
+ if (!origExtracted) {
174
+ continue
175
+ }
176
+
177
+ const origPath = normalizePath(origExtracted.filePath)
178
+ if (!origPath) {
179
+ continue
180
+ }
181
+
182
+ // Initialize map entry if needed
183
+ if (!resolvedFileRefsDataMap.has(resolvedPath)) {
184
+ resolvedFileRefsDataMap.set(resolvedPath, {
185
+ resolvedPath: resolvedPath,
186
+ refs: [],
187
+ })
188
+ }
189
+
190
+ const entry = resolvedFileRefsDataMap.get(resolvedPath)
191
+
192
+ // Add original variable string with config path if not already present
193
+ const alreadyExists = entry.refs.some(ref => ref.path === pathKey && ref.value === origPath)
194
+ if (!alreadyExists) {
195
+ const refEntry = { path: pathKey, value: origPath }
196
+ // Check if the value contains variables
197
+ if (variableSyntax && origPath.match(variableSyntax)) {
198
+ refEntry.hasVariable = true
199
+ }
200
+ entry.refs.push(refEntry)
201
+ }
202
+ }
203
+
204
+ // Second pass: generate glob patterns for each resolved path
205
+ for (const [resolvedPath, data] of resolvedFileRefsDataMap) {
206
+ const globPatternSet = new Set()
207
+
208
+ for (const ref of data.refs) {
209
+ // Check if variable path contains variables
210
+ if (ref.value.match(variableSyntax)) {
211
+ const globPattern = ref.value.replace(variableSyntax, '*')
212
+ globPatternSet.add(globPattern)
213
+ }
214
+ }
215
+ const patterns = Array.from(globPatternSet)
216
+ if (patterns.length > 0) {
217
+ data.globPatterns = patterns
218
+ }
219
+ }
220
+
221
+ // Convert map to array
222
+ const resolvedFileRefsData = Array.from(resolvedFileRefsDataMap.values())
223
+
224
+ metadata.resolvedFileRefsData = resolvedFileRefsData
225
+
226
+ return metadata
227
+ }
228
+
229
+ module.exports = enrichMetadata
@@ -22,6 +22,7 @@ const VAR_MATCH_REGEX = /__VAR_\d+__/
22
22
  * @returns {Array} Array of match objects with fullMatch, variable, varString and other properties
23
23
  */
24
24
  function findNestedVariables(input, regex, variablesKnownTypes, location, debug = false) {
25
+ // console.log('variablesKnownTypes', variablesKnownTypes)
25
26
  // Create a copy of the input for replacement tracking
26
27
  let current = input
27
28
  // console.log('current', current)
@@ -182,7 +183,9 @@ function findNestedVariables(input, regex, variablesKnownTypes, location, debug
182
183
  // remove first element from split
183
184
  matches[i].fallbackValues = split.slice(1).map((item) => {
184
185
  // console.log('item', item)
185
- const isVariable = variablesKnownTypes.test(item) || VAR_MATCH_REGEX.test(item)
186
+ // Strip ${} wrapper if present to properly test against variablesKnownTypes
187
+ const innerContent = item.replace(/^\$\{(.*)\}$/, '$1')
188
+ const isVariable = variablesKnownTypes.test(innerContent) || VAR_MATCH_REGEX.test(item)
186
189
  const fallbackData = {
187
190
  isVariable,
188
191
  fullMatch: item,
@@ -195,7 +198,7 @@ function findNestedVariables(input, regex, variablesKnownTypes, location, debug
195
198
  }
196
199
 
197
200
  if (isVariable) {
198
- const varType = item.match(variablesKnownTypes)[1]
201
+ const varType = innerContent.match(variablesKnownTypes)[1]
199
202
  fallbackData.varType = varType
200
203
  // if (varType === 'self:') {
201
204
  // fallbackData.fullMatch = item.replace('self:', '')
@@ -290,13 +293,13 @@ function findNestedVariables(input, regex, variablesKnownTypes, location, debug
290
293
  * @param {boolean} debug - Whether to print debug information
291
294
  * @returns {Array} Array of match objects containing full match and captured group
292
295
  */
293
- function findNestedVariablesx(input, regex, variablesKnownTypes, debug = false) {
296
+ function findNestedVariablesOld(input, regex, variablesKnownTypes, debug = false) {
294
297
  let str = input
295
298
  let matches = []
296
299
  let match
297
300
  let iteration = 0
298
301
 
299
- console.log('input', input)
302
+ // console.log('input', input)
300
303
 
301
304
  if (debug) console.log(`Initial string: ${str}`)
302
305
 
@@ -106,12 +106,51 @@ test('findNestedVariables - mutliple fallback items', () => {
106
106
  assert.equal(result[result.length - 1].variable, 'file(./config.${opt:stage, ${opt:stageOne}, ${opt:stageTwo}, "three"}.json)');
107
107
  });
108
108
 
109
- test.skip('findNestedVariables - deep', () => {
110
- const input = '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}';
109
+ test('findNestedVariables - deep', () => {
110
+ const input =
111
+ '${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}';
111
112
  const result = findNestedVariables(input, regex, variablesKnownTypes, 'xyz');
112
113
  deepLog('result', result)
113
- // Check varString property for the outermost variable
114
- assert.equal(result[result.length - 1].variable, 'file(./config.${opt:stage, ${opt:stageOne}, ${opt:stageTwo}, "three"}.json)');
114
+
115
+ // Should have 5 variables total
116
+ assert.equal(result.length, 5);
117
+
118
+ // Check the innermost variable
119
+ assert.equal(result[0].fullMatch, '${env:foo}');
120
+ assert.equal(result[0].variable, 'env:foo');
121
+ assert.equal(result[0].varType, 'env:');
122
+
123
+ // Check opt:stageOne with env:foo fallback
124
+ assert.equal(result[1].fullMatch, '${opt:stageOne, ${env:foo}}');
125
+ assert.equal(result[1].variable, 'opt:stageOne, ${env:foo}');
126
+ assert.equal(result[1].varType, 'opt:');
127
+ assert.equal(result[1].hasFallback, true);
128
+ assert.equal(result[1].valueBeforeFallback, 'opt:stageOne');
129
+ assert.equal(result[1].fallbackValues.length, 1);
130
+ assert.equal(result[1].fallbackValues[0].isVariable, true);
131
+ assert.equal(result[1].fallbackValues[0].fullMatch, '${env:foo}');
132
+ assert.equal(result[1].fallbackValues[0].varType, 'env:');
133
+
134
+ // Check opt:stageTwo
135
+ assert.equal(result[2].fullMatch, '${opt:stageTwo}');
136
+ assert.equal(result[2].variable, 'opt:stageTwo');
137
+
138
+ // Check opt:stage with multiple fallbacks
139
+ assert.equal(result[3].fullMatch, '${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }');
140
+ assert.equal(result[3].variable, 'opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three"');
141
+ assert.equal(result[3].hasFallback, true);
142
+ assert.equal(result[3].valueBeforeFallback, 'opt:stage');
143
+ assert.equal(result[3].fallbackValues.length, 3);
144
+ assert.equal(result[3].fallbackValues[0].fullMatch, '${opt:stageOne, ${env:foo}}');
145
+ assert.equal(result[3].fallbackValues[0].isVariable, true);
146
+ assert.equal(result[3].fallbackValues[1].fullMatch, '${opt:stageTwo}');
147
+ assert.equal(result[3].fallbackValues[1].isVariable, true);
148
+ assert.equal(result[3].fallbackValues[2].fullMatch, '"three"');
149
+ assert.equal(result[3].fallbackValues[2].isVariable, false);
150
+ assert.equal(result[3].fallbackValues[2].stringValue, 'three');
151
+
152
+ // Check outermost file variable
153
+ assert.equal(result[4].variable, 'file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)');
115
154
  });
116
155
 
117
156
 
@@ -1,7 +1,7 @@
1
1
  const isEmpty = require('lodash.isempty')
2
2
 
3
3
  module.exports = function isValidValue(val) {
4
- if (typeof val === 'object' && val.hasOwnProperty('__internal_only_flag')) {
4
+ if (typeof val === 'object' && (val.hasOwnProperty('__internal_only_flag') || val.hasOwnProperty('__internal_metadata'))) {
5
5
  return false
6
6
  }
7
7
  return val !== null && typeof val !== 'undefined' && !(typeof val === 'object' && isEmpty(val))
@@ -34,11 +34,13 @@ function splitByComma(string, regexPattern) {
34
34
  let current = ""
35
35
  let inQuote = false
36
36
  let quoteChar = ""
37
- let bracketDepth = 0 // Includes both () and []
38
-
37
+ let bracketDepth = 0 // Includes (), [], and {}
38
+ let dollarBraceDepth = 0 // Track ${ ... } depth separately (only when regexPattern is provided)
39
+
39
40
  for (let i = 0; i < protectedString.length; i++) {
40
41
  const char = protectedString[i]
41
-
42
+ const prevChar = i > 0 ? protectedString[i-1] : ''
43
+
42
44
  // Handle quotes
43
45
  if ((char === "'" || char === '"') && (i === 0 || protectedString[i-1] !== "\\")) {
44
46
  if (!inQuote) {
@@ -48,13 +50,35 @@ function splitByComma(string, regexPattern) {
48
50
  inQuote = false
49
51
  }
50
52
  }
51
-
52
- // Handle parentheses and brackets
53
- if ((char === "(" || char === "[") && !inQuote) bracketDepth++
54
- if ((char === ")" || char === "]") && !inQuote) bracketDepth--
55
-
53
+
54
+ // Handle parentheses, brackets, and curly braces
55
+ if (!inQuote) {
56
+ if (char === "(" || char === "[") {
57
+ bracketDepth++
58
+ } else if (char === ")" || char === "]") {
59
+ bracketDepth--
60
+ } else if (regexPattern) {
61
+ // Only track {} when we have regexPattern (i.e., when protecting variables)
62
+ // TODO this doesn't support custom variable syntax regexes.
63
+ if (char === "{" && prevChar === "$") {
64
+ // Track ${ as a special unit
65
+ dollarBraceDepth++
66
+ } else if (char === "{") {
67
+ // Standalone { (not part of ${)
68
+ bracketDepth++
69
+ } else if (char === "}") {
70
+ // Check if this closes a ${ or a standalone {
71
+ if (dollarBraceDepth > 0) {
72
+ dollarBraceDepth--
73
+ } else if (bracketDepth > 0) {
74
+ bracketDepth--
75
+ }
76
+ }
77
+ }
78
+ }
79
+
56
80
  // Process comma
57
- if (char === "," && !inQuote && bracketDepth === 0) {
81
+ if (char === "," && !inQuote && bracketDepth === 0 && dollarBraceDepth === 0) {
58
82
  result.push(current.trim())
59
83
  current = ""
60
84
  } else {
@@ -46,7 +46,7 @@ test('splitByComma - should handle input with extra whitespace', () => {
46
46
 
47
47
  test('splitByComma - should handle mixed scenarios', () => {
48
48
  const result = splitByComma("normal, 'quoted, string', function(param1, param2)")
49
- console.log('result', result)
49
+ // console.log('result', result)
50
50
  assert.equal(result, ["normal", "'quoted, string'", "function(param1, param2)"])
51
51
  })
52
52
 
@@ -80,5 +80,50 @@ test('splitByComma - should handle backtick quotes', () => {
80
80
  assert.equal(result, ["normal", "`template ${with, commas} inside`"])
81
81
  })
82
82
 
83
- // Run all tests
83
+ test('splitByComma - should split ${} variables when NO regex provided', () => {
84
+ const result = splitByComma('${env:no, env:empty}')
85
+ assert.equal(result, ['${env:no', 'env:empty}'])
86
+ })
87
+
88
+ test('splitByComma - should protect ${} variables when regex provided', () => {
89
+ const result = splitByComma('opt:stage, ${opt:stageOne}, ${opt:stageTwo}', variableSyntax)
90
+ assert.equal(result, ['opt:stage', '${opt:stageOne}', '${opt:stageTwo}'])
91
+ })
92
+
93
+ test('splitByComma - should protect nested ${} variables when regex provided', () => {
94
+ const result = splitByComma('opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}', variableSyntax)
95
+ assert.equal(result, ['opt:stage', '${opt:stageOne, ${env:foo}}', '${opt:stageTwo}'])
96
+ })
97
+
98
+ test('splitByComma - should handle multiple nested levels with regex', () => {
99
+ const result = splitByComma('${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three"', variableSyntax)
100
+ assert.equal(result, ['${opt:stageOne, ${env:foo}}', '${opt:stageTwo}', '"three"'])
101
+ })
102
+
103
+ test('splitByComma - should handle standalone {} objects with regex', () => {
104
+ const result = splitByComma('func(arg1, {key: value}, arg2)', variableSyntax)
105
+ assert.equal(result, ['func(arg1, {key: value}, arg2)'])
106
+ })
107
+
108
+ test('splitByComma - should handle mixed ${} variables and {} objects with regex', () => {
109
+ const result = splitByComma('${self:config}, {key: "value"}, ${env:var}', variableSyntax)
110
+ assert.equal(result, ['${self:config}', '{key: "value"}', '${env:var}'])
111
+ })
112
+
113
+ test('splitByComma - should handle complex nested case from failCases', () => {
114
+ const result = splitByComma('${env:no, env:empty}')
115
+ assert.equal(result, ['${env:no', 'env:empty}'])
116
+ })
117
+
118
+ test('splitByComma - should handle deeply nested variables with regex', () => {
119
+ const input = 'opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three"'
120
+ const result = splitByComma(input, variableSyntax)
121
+ assert.equal(result.length, 4)
122
+ assert.equal(result[0], 'opt:stage')
123
+ assert.equal(result[1], '${opt:stageOne, ${env:foo}}')
124
+ assert.equal(result[2], '${opt:stageTwo}')
125
+ assert.equal(result[3], '"three"')
126
+ })
127
+
128
+ // Run all tests
84
129
  test.run()