configorama 0.6.11 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +196 -24
  2. package/cli.js +3 -3
  3. package/package.json +1 -1
  4. package/src/index.js +22 -32
  5. package/src/main.js +775 -857
  6. package/src/parsers/yaml.js +3 -47
  7. package/src/resolvers/valueFromCron.js +3 -1
  8. package/src/resolvers/valueFromEnv.js +1 -0
  9. package/src/resolvers/valueFromEval.js +1 -0
  10. package/src/resolvers/valueFromFile.js +394 -0
  11. package/src/resolvers/valueFromGit.js +3 -2
  12. package/src/resolvers/valueFromOptions.js +1 -0
  13. package/src/resolvers/valueFromString.js +2 -1
  14. package/src/sync.js +12 -5
  15. package/src/utils/parsing/arrayToJsonPath.test.js +56 -0
  16. package/src/utils/{enrichMetadata.js → parsing/enrichMetadata.js} +244 -94
  17. package/src/utils/{parse.js → parsing/parse.js} +13 -13
  18. package/src/utils/parsing/preProcess.js +165 -0
  19. package/src/utils/paths/filePathUtils.js +136 -0
  20. package/src/utils/paths/filePathUtils.test.js +214 -0
  21. package/src/utils/paths/findLineForKey.js +47 -0
  22. package/src/utils/paths/findLineForKey.test.js +126 -0
  23. package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +22 -26
  24. package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
  25. package/src/utils/regex/index.js +23 -1
  26. package/src/utils/resolution/preResolveVariable.js +260 -0
  27. package/src/utils/resolution/preResolveVariable.test.js +98 -0
  28. package/src/utils/strings/bracketMatcher.js +86 -0
  29. package/src/utils/strings/bracketMatcher.test.js +135 -0
  30. package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
  31. package/src/utils/strings/formatFunctionArgs.test.js +77 -0
  32. package/src/utils/strings/quoteUtils.js +89 -0
  33. package/src/utils/strings/quoteUtils.test.js +217 -0
  34. package/src/utils/strings/replaceAll.test.js +82 -0
  35. package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
  36. package/src/utils/strings/splitCsv.js +38 -0
  37. package/src/utils/strings/splitCsv.test.js +96 -0
  38. package/src/utils/strings/textUtils.test.js +86 -0
  39. package/src/utils/{configWizard.js → ui/configWizard.js} +212 -60
  40. package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
  41. package/src/utils/{logs.js → ui/logs.js} +3 -3
  42. package/src/utils/validation/isValidValue.test.js +64 -0
  43. package/src/utils/validation/warnIfNotFound.js +52 -0
  44. package/src/utils/variables/appendDeepVariable.test.js +41 -0
  45. package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
  46. package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
  47. package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
  48. package/src/utils/variables/getVariableType.test.js +109 -0
  49. package/src/utils/variables/variableUtils.test.js +117 -0
  50. package/src/utils/isValidValue.js +0 -8
  51. package/src/utils/splitCsv.js +0 -29
  52. package/src/utils/trimSurroundingQuotes.js +0 -5
  53. /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
  54. /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
  55. /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.js} +0 -0
  56. /package/src/utils/{find-project-root.js → paths/findProjectRoot.js} +0 -0
  57. /package/src/utils/{resolveAlias.test.js → paths/resolveAlias.test.js} +0 -0
  58. /package/src/utils/{replaceAll.js → strings/replaceAll.js} +0 -0
  59. /package/src/utils/{splitByComma.test.js → strings/splitByComma.test.js} +0 -0
  60. /package/src/utils/{textUtils.js → strings/textUtils.js} +0 -0
  61. /package/src/utils/{chalk.js → ui/chalk.js} +0 -0
  62. /package/src/utils/{deep-log.js → ui/deep-log.js} +0 -0
  63. /package/src/utils/{appendDeepVariable.js → variables/appendDeepVariable.js} +0 -0
  64. /package/src/utils/{cleanVariable.test.js → variables/cleanVariable.test.js} +0 -0
  65. /package/src/utils/{getVariableType.js → variables/getVariableType.js} +0 -0
  66. /package/src/utils/{variableUtils.js → variables/variableUtils.js} +0 -0
@@ -1,6 +1,7 @@
1
1
  const YAML = require('js-yaml')
2
2
  const TOML = require('./toml')
3
3
  const JSON = require('./json5')
4
+ const { findOutermostVariables, findOutermostBracesDepthFirst } = require('../utils/strings/bracketMatcher')
4
5
 
5
6
  // Loader for custom CF syntax
6
7
  function load(contents, options) {
@@ -57,53 +58,8 @@ function toJson(ymlContents) {
57
58
  return json
58
59
  }
59
60
 
60
- // TODO only works for default var syntax ${}. Maybe fix?
61
- function findOutermostVariables(text) {
62
- let matches = [];
63
- let depth = 0;
64
- let startIndex = -1;
65
-
66
- for (let i = 0; i < text.length; i++) {
67
- if (text[i] === '$' && text[i + 1] === '{') {
68
- if (depth === 0) {
69
- startIndex = i;
70
- }
71
- depth++;
72
- i++; // Skip '{'
73
- } else if (text[i] === '}') {
74
- depth--;
75
- if (depth === 0 && startIndex !== -1) {
76
- matches.push(text.substring(startIndex, i + 1));
77
- startIndex = -1;
78
- }
79
- }
80
- }
81
- return matches;
82
- }
83
-
84
-
85
- function matchOutermostBraces(text) {
86
- let depth = 0
87
- let startIndex = -1
88
- let results = []
89
-
90
- for (let i = 0; i < text.length; i++) {
91
- if (text[i] === '{') {
92
- if (depth === 0) {
93
- startIndex = i
94
- }
95
- depth++
96
- } else if (text[i] === '}') {
97
- depth--
98
- if (depth === 0 && startIndex !== -1) {
99
- results.push(text.substring(startIndex, i + 1))
100
- startIndex = -1
101
- }
102
- }
103
- }
104
-
105
- return results
106
- }
61
+ // Alias for backward compatibility
62
+ const matchOutermostBraces = findOutermostBracesDepthFirst
107
63
 
108
64
 
109
65
  // https://regex101.com/r/XIltbc/1
@@ -1,3 +1,4 @@
1
+ const { trimSurroundingQuotes } = require('../utils/strings/quoteUtils')
1
2
  const cronRefSyntax = RegExp(/^cron\((~?[\{\}\:\$a-zA-Z0-9._\-\/,'"\*\` ]+?)?\)/g)
2
3
 
3
4
  /**
@@ -226,7 +227,7 @@ Examples:
226
227
  }
227
228
 
228
229
  // Remove surrounding quotes if present
229
- const cleanExpression = cronExpression.replace(/^['"`](.*)['"`]$/, '$1')
230
+ const cleanExpression = trimSurroundingQuotes(cronExpression, true)
230
231
 
231
232
  // If already a cron expression, return it
232
233
  if (cleanExpression.match(/^[\*\/,\-\d]+$/)) {
@@ -244,6 +245,7 @@ Examples:
244
245
 
245
246
  module.exports = {
246
247
  type: 'cron',
248
+ source: 'readonly',
247
249
  prefix: 'cron',
248
250
  syntax: '${cron(expression)}',
249
251
  description: 'Resolves cron expressions. Examples: ${cron("every 5 minutes"}, ${cron("weekdays")}, ${cron("at 9:30")}',
@@ -24,6 +24,7 @@ Example: \${env:MY_ENV_VAR}
24
24
 
25
25
  module.exports = {
26
26
  type: 'env',
27
+ source: 'user',
27
28
  syntax: '${env:ENV_VAR}',
28
29
  description: 'Resolves environment variables. Examples: ${env:MY_ENV_VAR}, ${env:MY_ENV_VAR_TWO, "fallbackValue"}',
29
30
  match: envRefSyntax,
@@ -32,6 +32,7 @@ async function getValueFromEval(variableString) {
32
32
 
33
33
  module.exports = {
34
34
  type: 'eval',
35
+ source: 'readonly',
35
36
  description: '${eval(expression)} - Evaluates mathematical expressions',
36
37
  match: evalRefSyntax,
37
38
  resolver: getValueFromEval
@@ -0,0 +1,394 @@
1
+ /**
2
+ * Resolves values from file references (file() and text() syntax)
3
+ */
4
+ const fs = require('fs')
5
+ const { trim } = require('../utils/lodash')
6
+ const { splitCsv } = require('../utils/strings/splitCsv')
7
+ const { resolveFilePathFromMatch } = require('../utils/paths/getFullFilePath')
8
+ const { findNestedVariables } = require('../utils/variables/findNestedVariables')
9
+ const { makeBox } = require('@davidwells/box-logger')
10
+ const { encodeJsSyntax } = require('../utils/encoders/js-fixes')
11
+
12
+ /* File Parsers */
13
+ const YAML = require('../parsers/yaml')
14
+ const TOML = require('../parsers/toml')
15
+ const INI = require('../parsers/ini')
16
+ const JSON5 = require('../parsers/json5')
17
+
18
+ /**
19
+ * Parse file contents based on file extension
20
+ * @param {string} content - Raw file contents
21
+ * @param {string} filePath - File path (used to determine extension)
22
+ * @returns {*} Parsed content
23
+ */
24
+ function parseFileContents(content, filePath) {
25
+ const ext = filePath.split('.').pop().toLowerCase()
26
+
27
+ if (ext === 'json' || ext === 'json5') {
28
+ return JSON5.parse(content)
29
+ }
30
+ if (ext === 'yml' || ext === 'yaml') {
31
+ return YAML.parse(content)
32
+ }
33
+ if (ext === 'toml' || ext === 'tml') {
34
+ return TOML.parse(content)
35
+ }
36
+ if (ext === 'ini') {
37
+ return INI.parse(content)
38
+ }
39
+
40
+ // Return raw content for other files
41
+ return content
42
+ }
43
+
44
+ /**
45
+ * Resolves a value from a file reference
46
+ * @param {object} ctx - Context object with instance properties
47
+ * @param {string} ctx.configPath - Base path for file resolution
48
+ * @param {Array} ctx.fileRefsFound - Mutable array tracking file refs
49
+ * @param {RegExp} ctx.variableSyntax - Regex for variable syntax
50
+ * @param {object} ctx.variablesKnownTypes - Known variable types
51
+ * @param {object} ctx.variableTypes - Variable types
52
+ * @param {object} ctx.opts - Options object
53
+ * @param {object} ctx.originalConfig - Original config
54
+ * @param {object} ctx.config - Current config
55
+ * @param {Function} ctx.getDeeperValue - Method for nested lookups
56
+ * @param {RegExp} ctx.fileRefSyntax - Regex for file() syntax
57
+ * @param {RegExp} ctx.textRefSyntax - Regex for text() syntax
58
+ * @param {string} variableString - The variable string to resolve
59
+ * @param {object} options - Resolution options
60
+ * @returns {Promise<any>}
61
+ */
62
+ async function getValueFromFile(ctx, variableString, options) {
63
+ const opts = options || {}
64
+ const syntax = opts.asRawText ? ctx.textRefSyntax : ctx.fileRefSyntax
65
+ // console.log('From file', `"${variableString}"`)
66
+ let matchedFileString = variableString.match(syntax)[0]
67
+ // console.log('matchedFileString', matchedFileString)
68
+
69
+ // Get function input params if any supplied https://regex101.com/r/qlNFVm/1
70
+ // var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*/g
71
+ var funcParamsRegex = /(\w+)\s*\(((?:[^()]+)*)?\s*\)/g
72
+ // tighter (?<![.\w-])\b(\w+)\s*\(((?:[^()]+)*)?\s*\)\s*
73
+ var hasParams = funcParamsRegex.exec(matchedFileString)
74
+
75
+ let argsToPass = []
76
+ if (hasParams) {
77
+ const splitter = splitCsv(hasParams[2])
78
+ const argsFound = splitter.map((arg) => {
79
+ const cleanArg = trim(arg).replace(/^'|"/, '').replace(/'|"$/, '')
80
+ return cleanArg
81
+ })
82
+ // console.log('argsFound', argsFound)
83
+
84
+ // If function has more arguments than file path
85
+ if (argsFound.length && argsFound.length > 1) {
86
+ matchedFileString = argsFound[0]
87
+ argsToPass = argsFound.filter((arg, i) => {
88
+ return i !== 0
89
+ })
90
+ }
91
+ }
92
+ // console.log('argsToPass', argsToPass)
93
+
94
+ const fileDetails = resolveFilePathFromMatch(matchedFileString, syntax, ctx.configPath)
95
+ // console.log('fileDetails', fileDetails)
96
+
97
+ const { fullFilePath, resolvedPath, relativePath } = fileDetails
98
+
99
+ const exists = fs.existsSync(fullFilePath)
100
+
101
+ ctx.fileRefsFound.push({
102
+ // location: options.context.path.join('.'),
103
+ filePath: fullFilePath,
104
+ relativePath,
105
+ resolvedVariableString: options.context.value,
106
+ originalVariableString: options.context.originalSource,
107
+ containsVariables: options.context.value !== options.context.originalSource,
108
+ exists,
109
+ })
110
+
111
+ let fileExtension = resolvedPath.split('.')
112
+
113
+ fileExtension = fileExtension[fileExtension.length - 1].toLowerCase()
114
+
115
+ // Validate file exists
116
+ if (!exists) {
117
+ const originalVar = options.context && options.context.originalSource
118
+
119
+ const findNestedResult = findNestedVariables(
120
+ originalVar,
121
+ ctx.variableSyntax,
122
+ ctx.variablesKnownTypes,
123
+ options.context.path,
124
+ ctx.variableTypes
125
+ )
126
+ // console.log('findNestedResult', findNestedResult)
127
+ let hasFallback = false
128
+ if (findNestedResult) {
129
+ const varDetails = findNestedResult[0]
130
+ // console.log('varDetails', varDetails)
131
+ hasFallback = varDetails.hasFallback
132
+ }
133
+
134
+ // check if original var has fallback value
135
+ // console.log('NO FILE FOUND', fullFilePath)
136
+ // console.log('variableString', variableString)
137
+
138
+ if (!hasFallback && !ctx.opts.allowUnknownFileRefs) {
139
+ const errorMsg = makeBox({
140
+ title: `File Not Found in ${originalVar}`,
141
+ minWidth: '100%',
142
+ text: `Variable ${variableString} cannot resolve due to missing file.
143
+
144
+ File not found ${fullFilePath}
145
+
146
+ Default fallback value will be used if provided.
147
+
148
+ ${JSON.stringify(options.context, null, 2)}`,
149
+ })
150
+ console.log(errorMsg)
151
+ }
152
+ // TODO maybe reject. YAML does not allow for null/undefined values
153
+ // return Promise.reject(new Error(errorMsg))
154
+ return Promise.resolve(undefined)
155
+ }
156
+
157
+ let valueToPopulate
158
+
159
+ const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
160
+
161
+ /* handle case for referencing raw JS files to inline them */
162
+ if (argsToPass.length
163
+ && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw')
164
+ || opts.asRawText
165
+ ) {
166
+ // Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
167
+ valueToPopulate = encodeJsSyntax(variableFileContents)
168
+ return Promise.resolve(valueToPopulate)
169
+ }
170
+
171
+ // Build context for executable files
172
+ const valueForFunction = {
173
+ originalConfig: ctx.originalConfig,
174
+ config: ctx.config,
175
+ opts: ctx.opts,
176
+ }
177
+
178
+ // Process JS files
179
+ if (fileExtension === 'js' || fileExtension === 'cjs') {
180
+ const jsFile = require(fullFilePath)
181
+ const { moduleName } = parseModuleReference(variableString, matchedFileString)
182
+ const returnValueFunction = moduleName ? jsFile[moduleName] : jsFile
183
+
184
+ return processExecutableFile({
185
+ fileModule: jsFile,
186
+ returnValueFunction,
187
+ valueForFunction,
188
+ argsToPass,
189
+ variableString,
190
+ matchedFileString,
191
+ relativePath,
192
+ fileType: 'javascript',
193
+ getDeeperValue: ctx.getDeeperValue
194
+ })
195
+ }
196
+
197
+ if (fileExtension === 'ts' || fileExtension === 'tsx' || fileExtension === 'mts' || fileExtension === 'cts') {
198
+ const { executeTypeScriptFile } = require('../parsers/typescript')
199
+ const { moduleName } = parseModuleReference(variableString, matchedFileString)
200
+
201
+ try {
202
+ const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
203
+ let returnValueFunction = tsFile.config || tsFile.default || tsFile
204
+ if (moduleName) {
205
+ returnValueFunction = tsFile[moduleName]
206
+ }
207
+
208
+ return processExecutableFile({
209
+ fileModule: tsFile,
210
+ returnValueFunction,
211
+ valueForFunction,
212
+ argsToPass,
213
+ variableString,
214
+ matchedFileString,
215
+ relativePath,
216
+ fileType: 'TypeScript',
217
+ getDeeperValue: ctx.getDeeperValue
218
+ })
219
+ } catch (err) {
220
+ return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
221
+ }
222
+ }
223
+
224
+ if (fileExtension === 'mjs' || fileExtension === 'esm') {
225
+ const { executeESMFile } = require('../parsers/esm')
226
+ const { moduleName } = parseModuleReference(variableString, matchedFileString)
227
+
228
+ try {
229
+ const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
230
+ let returnValueFunction = esmFile.config || esmFile.default || esmFile
231
+ if (moduleName) {
232
+ returnValueFunction = esmFile[moduleName]
233
+ }
234
+
235
+ return processExecutableFile({
236
+ fileModule: esmFile,
237
+ returnValueFunction,
238
+ valueForFunction,
239
+ argsToPass,
240
+ variableString,
241
+ matchedFileString,
242
+ relativePath,
243
+ fileType: 'ESM',
244
+ getDeeperValue: ctx.getDeeperValue
245
+ })
246
+ } catch (err) {
247
+ return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
248
+ }
249
+ }
250
+
251
+ // Process everything except JS, TS, and ESM
252
+ if (fileExtension !== 'js' && fileExtension !== 'ts' && fileExtension !== 'mjs' && fileExtension !== 'esm') {
253
+ /* Read initial file */
254
+ valueToPopulate = variableFileContents
255
+
256
+ // File reference has :subKey lookup. Must dig deeper
257
+ if (matchedFileString !== variableString) {
258
+ if (fileExtension === 'yml' || fileExtension === 'yaml') {
259
+ valueToPopulate = JSON.stringify(YAML.parse(valueToPopulate))
260
+ }
261
+ if (fileExtension === 'toml' || fileExtension === 'tml') {
262
+ valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
263
+ }
264
+ if (fileExtension === 'ini') {
265
+ valueToPopulate = INI.toJson(valueToPopulate)
266
+ }
267
+ // console.log('deep', variableString)
268
+ // console.log('matchedFileString', matchedFileString)
269
+ let deepProperties = variableString.replace(matchedFileString, '')
270
+ // Support both : and . as the separator for sub properties
271
+ const firstChar = deepProperties.substring(0, 1)
272
+ if (firstChar !== ':' && firstChar !== '.') {
273
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
274
+ Please use ":" or "." to reference sub properties. ${deepProperties}`
275
+ return Promise.reject(new Error(errorMessage))
276
+ }
277
+ deepProperties = deepProperties.slice(1).split('.')
278
+ return ctx.getDeeperValue(deepProperties, valueToPopulate)
279
+ }
280
+
281
+ if (fileExtension === 'yml' || fileExtension === 'yaml') {
282
+ valueToPopulate = YAML.parse(valueToPopulate)
283
+ return Promise.resolve(valueToPopulate)
284
+ }
285
+
286
+ if (fileExtension === 'toml' || fileExtension === 'tml') {
287
+ valueToPopulate = TOML.parse(valueToPopulate)
288
+ return Promise.resolve(valueToPopulate)
289
+ }
290
+
291
+ if (fileExtension === 'ini') {
292
+ valueToPopulate = INI.parse(valueToPopulate)
293
+ return Promise.resolve(valueToPopulate)
294
+ }
295
+
296
+ if (fileExtension === 'json' || fileExtension === 'json5') {
297
+ valueToPopulate = JSON5.parse(valueToPopulate)
298
+ return Promise.resolve(valueToPopulate)
299
+ }
300
+ }
301
+ // console.log('fall thru', valueToPopulate)
302
+ return Promise.resolve(valueToPopulate)
303
+ }
304
+
305
+ /**
306
+ * Parses variable string to extract module reference path
307
+ * Supports both : and . as separators for module references
308
+ * @param {string} variableString - The full variable string
309
+ * @param {string} matchedFileString - The matched file path portion
310
+ * @returns {{ variableArray: string[], moduleName: string|null }}
311
+ */
312
+ function parseModuleReference(variableString, matchedFileString) {
313
+ let variableArray = variableString.split(':')
314
+ if (variableArray.length === 1) {
315
+ const dotIndex = variableString.indexOf(matchedFileString) + matchedFileString.length
316
+ const afterMatch = variableString.substring(dotIndex)
317
+ if (afterMatch.startsWith('.')) {
318
+ variableArray = [variableString.substring(0, dotIndex), afterMatch.substring(1)]
319
+ }
320
+ }
321
+
322
+ let moduleName = null
323
+ if (variableArray[1]) {
324
+ moduleName = variableArray[1].split('.')[0]
325
+ }
326
+
327
+ return { variableArray, moduleName }
328
+ }
329
+
330
+ /**
331
+ * Extracts deep properties from variable string after file match
332
+ * @param {string} variableString - The full variable string
333
+ * @param {string} matchedFileString - The matched file path portion
334
+ * @returns {string[]} Array of property keys to traverse
335
+ */
336
+ function extractDeepProperties(variableString, matchedFileString) {
337
+ let deepProperties = variableString.replace(matchedFileString, '')
338
+ deepProperties = deepProperties.slice(1).split('.')
339
+ deepProperties.splice(0, 1)
340
+ return deepProperties.map((prop) => trim(prop))
341
+ }
342
+
343
+ /**
344
+ * Processes executable file (JS/TS/ESM) and resolves deep properties
345
+ * @param {object} params - Parameters
346
+ * @param {object} params.fileModule - The loaded module
347
+ * @param {Function} params.returnValueFunction - The function to call
348
+ * @param {object} params.valueForFunction - Context passed to the function
349
+ * @param {string[]} params.argsToPass - Additional args for the function
350
+ * @param {string} params.variableString - Original variable string
351
+ * @param {string} params.matchedFileString - Matched file path
352
+ * @param {string} params.relativePath - Relative file path for errors
353
+ * @param {string} params.fileType - Type of file (javascript/TypeScript/ESM)
354
+ * @param {Function} params.getDeeperValue - Function to resolve nested values
355
+ * @returns {Promise<any>}
356
+ */
357
+ async function processExecutableFile({
358
+ fileModule,
359
+ returnValueFunction,
360
+ valueForFunction,
361
+ argsToPass,
362
+ variableString,
363
+ matchedFileString,
364
+ relativePath,
365
+ fileType,
366
+ getDeeperValue
367
+ }) {
368
+ if (typeof returnValueFunction !== 'function') {
369
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
370
+ Check if your ${fileType} is exporting a function that returns a value.`
371
+ return Promise.reject(new Error(errorMessage))
372
+ }
373
+
374
+ const valueToPopulate = returnValueFunction.call(fileModule, valueForFunction, ...argsToPass)
375
+
376
+ return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
377
+ const deepProperties = extractDeepProperties(variableString, matchedFileString)
378
+ return getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
379
+ if (typeof deepValueToPopulateResolved === 'undefined') {
380
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
381
+ Check if your ${fileType} is returning the correct data.`
382
+ return Promise.reject(new Error(errorMessage))
383
+ }
384
+ return Promise.resolve(deepValueToPopulateResolved)
385
+ })
386
+ })
387
+ }
388
+
389
+ module.exports = {
390
+ getValueFromFile,
391
+ parseFileContents,
392
+ parseModuleReference,
393
+ extractDeepProperties
394
+ }
@@ -5,8 +5,8 @@ const path = require('path')
5
5
  const childProcess = require('child_process')
6
6
  const GitUrlParse = require('git-url-parse')
7
7
  const { functionRegex } = require('../utils/regex')
8
- const formatFunctionArgs = require('../utils/formatFunctionArgs')
9
- const { findProjectRoot } = require('../utils/find-project-root')
8
+ const formatFunctionArgs = require('../utils/strings/formatFunctionArgs')
9
+ const { findProjectRoot } = require('../utils/paths/findProjectRoot')
10
10
  const GIT_PREFIX = 'git'
11
11
  const gitVariableSyntax = RegExp(/^git:/g)
12
12
 
@@ -330,6 +330,7 @@ async function getGitRemote(name = 'origin') {
330
330
  module.exports = function createGitResolver(cwd) {
331
331
  return {
332
332
  type: 'git',
333
+ source: 'readonly',
333
334
  prefix: 'git',
334
335
  syntax: '${git:valueType}',
335
336
  description: `Resolves Git variables. Available valueTypes: ${Object.values(GIT_KEYS).join(', ')}`,
@@ -14,6 +14,7 @@ function getValueFromOptions(variableString, options) {
14
14
 
15
15
  module.exports = {
16
16
  type: 'options',
17
+ source: 'user',
17
18
  prefix: 'opt',
18
19
  syntax: '${opt:flagName}',
19
20
  description: 'Resolves CLI option flags. Examples: ${opt:stage}, ${opt:other, "fallbackValue"}',
@@ -1,8 +1,9 @@
1
+ const { trimSurroundingQuotes } = require('../utils/strings/quoteUtils')
1
2
 
2
3
  const stringRefSyntax = RegExp(/(?:('|").*?\1)/g)
3
4
 
4
5
  function getValueFromString(variableString) {
5
- const valueToPopulate = variableString.replace(/^['"]|['"]$/g, '')
6
+ const valueToPopulate = trimSurroundingQuotes(variableString, false)
6
7
  return Promise.resolve(valueToPopulate)
7
8
  }
8
9
 
package/src/sync.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const path = require('path')
2
2
  const fs = require('fs')
3
3
  const Configorama = require('./main')
4
- const getFullPath = require('./utils/getFullFilePath')
5
- const enrichMetadata = require('./utils/enrichMetadata')
4
+ const getFullPath = require('./utils/paths/getFullFilePath')
5
+ const enrichMetadata = require('./utils/parsing/enrichMetadata')
6
6
 
7
7
  /**
8
8
  * Force synchronous invocation of async API
@@ -54,19 +54,26 @@ module.exports = function configoramaSync(variableSources = []) {
54
54
  const metadata = instance.collectVariableMetadata()
55
55
 
56
56
  // Enrich metadata with resolution tracking data collected during execution
57
- const enrichedMetadata = enrichMetadata(
57
+ const enrichedMetadata = await enrichMetadata(
58
58
  metadata,
59
59
  instance.resolutionTracking,
60
60
  instance.variableSyntax,
61
61
  instance.fileRefsFound,
62
62
  instance.originalConfig,
63
63
  instance.configFilePath,
64
- Object.keys(instance.filters)
64
+ Object.keys(instance.filters),
65
+ result, // pass resolved config for post-resolution enrichment
66
+ options,
67
+ instance.variableTypes
65
68
  )
66
69
 
67
70
  return {
71
+ variableSyntax: instance.variableSyntax,
72
+ variableTypes: instance.variableTypes,
68
73
  config: result,
69
- metadata: enrichedMetadata
74
+ originalConfig: instance.originalConfig,
75
+ metadata: enrichedMetadata,
76
+ resolutionHistory: enrichedMetadata.resolutionHistory,
70
77
  }
71
78
  }
72
79
 
@@ -0,0 +1,56 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const { arrayToJsonPath } = require('./arrayToJsonPath')
4
+
5
+ test('arrayToJsonPath - should handle single string element', () => {
6
+ const result = arrayToJsonPath(['root'])
7
+ assert.equal(result, 'root')
8
+ })
9
+
10
+ test('arrayToJsonPath - should join string elements with dots', () => {
11
+ const result = arrayToJsonPath(['root', 'child', 'grandchild'])
12
+ assert.equal(result, 'root.child.grandchild')
13
+ })
14
+
15
+ test('arrayToJsonPath - should handle numeric indices with brackets', () => {
16
+ const result = arrayToJsonPath(['array', 0, 'property'])
17
+ assert.equal(result, 'array[0].property')
18
+ })
19
+
20
+ test('arrayToJsonPath - should handle mixed string and number paths', () => {
21
+ const result = arrayToJsonPath(['users', 5, 'name'])
22
+ assert.equal(result, 'users[5].name')
23
+ })
24
+
25
+ test('arrayToJsonPath - should handle consecutive numbers', () => {
26
+ const result = arrayToJsonPath(['matrix', 0, 1])
27
+ assert.equal(result, 'matrix[0][1]')
28
+ })
29
+
30
+ test('arrayToJsonPath - should handle single number', () => {
31
+ const result = arrayToJsonPath([0])
32
+ assert.equal(result, '0')
33
+ })
34
+
35
+ test('arrayToJsonPath - should handle complex nested path', () => {
36
+ const result = arrayToJsonPath(['data', 'items', 3, 'values', 2, 'name'])
37
+ assert.equal(result, 'data.items[3].values[2].name')
38
+ })
39
+
40
+ test('arrayToJsonPath - should handle empty array', () => {
41
+ const result = arrayToJsonPath([])
42
+ assert.equal(result, '')
43
+ })
44
+
45
+ test('arrayToJsonPath - should convert number to string for first element', () => {
46
+ const result = arrayToJsonPath([123, 'property'])
47
+ assert.equal(result, '123.property')
48
+ })
49
+
50
+ test('arrayToJsonPath - should handle property names with special chars', () => {
51
+ const result = arrayToJsonPath(['root', 'my-prop', 'my_prop'])
52
+ assert.equal(result, 'root.my-prop.my_prop')
53
+ })
54
+
55
+ // Run all tests
56
+ test.run()