configorama 0.6.12 → 0.6.14

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 +690 -778
  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 +423 -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} +210 -18
  17. package/src/utils/{parse.js → parsing/parse.js} +13 -13
  18. package/src/utils/parsing/preProcess.js +165 -0
  19. package/src/utils/{filePathUtils.js → paths/filePathUtils.js} +3 -2
  20. package/src/utils/paths/findLineForKey.js +47 -0
  21. package/src/utils/paths/findLineForKey.test.js +126 -0
  22. package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +24 -26
  23. package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
  24. package/src/utils/regex/index.js +23 -1
  25. package/src/utils/resolution/preResolveVariable.js +260 -0
  26. package/src/utils/resolution/preResolveVariable.test.js +98 -0
  27. package/src/utils/strings/bracketMatcher.js +86 -0
  28. package/src/utils/strings/bracketMatcher.test.js +135 -0
  29. package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
  30. package/src/utils/strings/formatFunctionArgs.test.js +77 -0
  31. package/src/utils/strings/quoteUtils.js +89 -0
  32. package/src/utils/strings/quoteUtils.test.js +217 -0
  33. package/src/utils/strings/replaceAll.test.js +82 -0
  34. package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
  35. package/src/utils/strings/splitCsv.js +38 -0
  36. package/src/utils/strings/splitCsv.test.js +96 -0
  37. package/src/utils/strings/textUtils.test.js +86 -0
  38. package/src/utils/{configWizard.js → ui/configWizard.js} +177 -38
  39. package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
  40. package/src/utils/{logs.js → ui/logs.js} +3 -3
  41. package/src/utils/validation/isValidValue.test.js +64 -0
  42. package/src/utils/validation/warnIfNotFound.js +52 -0
  43. package/src/utils/variables/appendDeepVariable.test.js +41 -0
  44. package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
  45. package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
  46. package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
  47. package/src/utils/variables/getVariableType.test.js +109 -0
  48. package/src/utils/variables/variableUtils.test.js +117 -0
  49. package/src/utils/isValidValue.js +0 -8
  50. package/src/utils/splitCsv.js +0 -29
  51. package/src/utils/trimSurroundingQuotes.js +0 -5
  52. /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
  53. /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
  54. /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.js} +0 -0
  55. /package/src/utils/{filePathUtils.test.js → paths/filePathUtils.test.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,423 @@
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, resolveFilePath } = 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
+ let { fullFilePath, resolvedPath, relativePath } = fileDetails
98
+
99
+ // Check for file path overrides
100
+ let wasOverridden = false
101
+ let originalFilePath = null
102
+ const filePathOverrides = ctx.opts && ctx.opts.filePathOverrides
103
+ if (filePathOverrides) {
104
+ // Try matching against relativePath (e.g., './env.yml')
105
+ const overrideKey = Object.keys(filePathOverrides).find((key) => {
106
+ // Normalize paths for comparison
107
+ const normalizedKey = key.replace(/^\.\//, '')
108
+ const normalizedRelPath = relativePath.replace(/^\.\//, '')
109
+ return normalizedKey === normalizedRelPath || key === relativePath
110
+ })
111
+
112
+ if (overrideKey) {
113
+ originalFilePath = fullFilePath
114
+ const overridePath = filePathOverrides[overrideKey]
115
+ // Resolve the override path (could be relative or absolute)
116
+ fullFilePath = resolveFilePath(overridePath, ctx.configPath)
117
+ resolvedPath = overridePath
118
+ wasOverridden = true
119
+ }
120
+ }
121
+
122
+ const exists = fs.existsSync(fullFilePath)
123
+
124
+ const fileRefEntry = {
125
+ filePath: fullFilePath,
126
+ relativePath,
127
+ resolvedVariableString: options.context.value,
128
+ originalVariableString: options.context.originalSource,
129
+ containsVariables: options.context.value !== options.context.originalSource,
130
+ exists,
131
+ }
132
+
133
+ if (wasOverridden) {
134
+ fileRefEntry.wasOverridden = true
135
+ fileRefEntry.originalFilePath = originalFilePath
136
+ }
137
+
138
+ ctx.fileRefsFound.push(fileRefEntry)
139
+
140
+ let fileExtension = resolvedPath.split('.')
141
+
142
+ fileExtension = fileExtension[fileExtension.length - 1].toLowerCase()
143
+
144
+ // Validate file exists
145
+ if (!exists) {
146
+ const originalVar = options.context && options.context.originalSource
147
+
148
+ const findNestedResult = findNestedVariables(
149
+ originalVar,
150
+ ctx.variableSyntax,
151
+ ctx.variablesKnownTypes,
152
+ options.context.path,
153
+ ctx.variableTypes
154
+ )
155
+ // console.log('findNestedResult', findNestedResult)
156
+ let hasFallback = false
157
+ if (findNestedResult) {
158
+ const varDetails = findNestedResult[0]
159
+ // console.log('varDetails', varDetails)
160
+ hasFallback = varDetails.hasFallback
161
+ }
162
+
163
+ // check if original var has fallback value
164
+ // console.log('NO FILE FOUND', fullFilePath)
165
+ // console.log('variableString', variableString)
166
+
167
+ if (!hasFallback && !ctx.opts.allowUnknownFileRefs) {
168
+ const errorMsg = makeBox({
169
+ title: `File Not Found in ${originalVar}`,
170
+ minWidth: '100%',
171
+ text: `Variable ${variableString} cannot resolve due to missing file.
172
+
173
+ File not found ${fullFilePath}
174
+
175
+ Default fallback value will be used if provided.
176
+
177
+ ${JSON.stringify(options.context, null, 2)}`,
178
+ })
179
+ console.log(errorMsg)
180
+ }
181
+ // TODO maybe reject. YAML does not allow for null/undefined values
182
+ // return Promise.reject(new Error(errorMsg))
183
+ return Promise.resolve(undefined)
184
+ }
185
+
186
+ let valueToPopulate
187
+
188
+ const variableFileContents = fs.readFileSync(fullFilePath, 'utf-8')
189
+
190
+ /* handle case for referencing raw JS files to inline them */
191
+ if (argsToPass.length
192
+ && (argsToPass && argsToPass[0] && argsToPass[0].toLowerCase() === 'raw')
193
+ || opts.asRawText
194
+ ) {
195
+ // Encode foo() to foo__PH_PAREN_OPEN__) to avoid function collisions
196
+ valueToPopulate = encodeJsSyntax(variableFileContents)
197
+ return Promise.resolve(valueToPopulate)
198
+ }
199
+
200
+ // Build context for executable files
201
+ const valueForFunction = {
202
+ originalConfig: ctx.originalConfig,
203
+ config: ctx.config,
204
+ opts: ctx.opts,
205
+ }
206
+
207
+ // Process JS files
208
+ if (fileExtension === 'js' || fileExtension === 'cjs') {
209
+ const jsFile = require(fullFilePath)
210
+ const { moduleName } = parseModuleReference(variableString, matchedFileString)
211
+ const returnValueFunction = moduleName ? jsFile[moduleName] : jsFile
212
+
213
+ return processExecutableFile({
214
+ fileModule: jsFile,
215
+ returnValueFunction,
216
+ valueForFunction,
217
+ argsToPass,
218
+ variableString,
219
+ matchedFileString,
220
+ relativePath,
221
+ fileType: 'javascript',
222
+ getDeeperValue: ctx.getDeeperValue
223
+ })
224
+ }
225
+
226
+ if (fileExtension === 'ts' || fileExtension === 'tsx' || fileExtension === 'mts' || fileExtension === 'cts') {
227
+ const { executeTypeScriptFile } = require('../parsers/typescript')
228
+ const { moduleName } = parseModuleReference(variableString, matchedFileString)
229
+
230
+ try {
231
+ const tsFile = await executeTypeScriptFile(fullFilePath, { dynamicArgs: () => argsToPass })
232
+ let returnValueFunction = tsFile.config || tsFile.default || tsFile
233
+ if (moduleName) {
234
+ returnValueFunction = tsFile[moduleName]
235
+ }
236
+
237
+ return processExecutableFile({
238
+ fileModule: tsFile,
239
+ returnValueFunction,
240
+ valueForFunction,
241
+ argsToPass,
242
+ variableString,
243
+ matchedFileString,
244
+ relativePath,
245
+ fileType: 'TypeScript',
246
+ getDeeperValue: ctx.getDeeperValue
247
+ })
248
+ } catch (err) {
249
+ return Promise.reject(new Error(`Error processing TypeScript file: ${err.message}`))
250
+ }
251
+ }
252
+
253
+ if (fileExtension === 'mjs' || fileExtension === 'esm') {
254
+ const { executeESMFile } = require('../parsers/esm')
255
+ const { moduleName } = parseModuleReference(variableString, matchedFileString)
256
+
257
+ try {
258
+ const esmFile = await executeESMFile(fullFilePath, { dynamicArgs: () => argsToPass })
259
+ let returnValueFunction = esmFile.config || esmFile.default || esmFile
260
+ if (moduleName) {
261
+ returnValueFunction = esmFile[moduleName]
262
+ }
263
+
264
+ return processExecutableFile({
265
+ fileModule: esmFile,
266
+ returnValueFunction,
267
+ valueForFunction,
268
+ argsToPass,
269
+ variableString,
270
+ matchedFileString,
271
+ relativePath,
272
+ fileType: 'ESM',
273
+ getDeeperValue: ctx.getDeeperValue
274
+ })
275
+ } catch (err) {
276
+ return Promise.reject(new Error(`Error processing ESM file: ${err.message}`))
277
+ }
278
+ }
279
+
280
+ // Process everything except JS, TS, and ESM
281
+ if (fileExtension !== 'js' && fileExtension !== 'ts' && fileExtension !== 'mjs' && fileExtension !== 'esm') {
282
+ /* Read initial file */
283
+ valueToPopulate = variableFileContents
284
+
285
+ // File reference has :subKey lookup. Must dig deeper
286
+ if (matchedFileString !== variableString) {
287
+ if (fileExtension === 'yml' || fileExtension === 'yaml') {
288
+ valueToPopulate = JSON.stringify(YAML.parse(valueToPopulate))
289
+ }
290
+ if (fileExtension === 'toml' || fileExtension === 'tml') {
291
+ valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
292
+ }
293
+ if (fileExtension === 'ini') {
294
+ valueToPopulate = INI.toJson(valueToPopulate)
295
+ }
296
+ // console.log('deep', variableString)
297
+ // console.log('matchedFileString', matchedFileString)
298
+ let deepProperties = variableString.replace(matchedFileString, '')
299
+ // Support both : and . as the separator for sub properties
300
+ const firstChar = deepProperties.substring(0, 1)
301
+ if (firstChar !== ':' && firstChar !== '.') {
302
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}" sub properties
303
+ Please use ":" or "." to reference sub properties. ${deepProperties}`
304
+ return Promise.reject(new Error(errorMessage))
305
+ }
306
+ deepProperties = deepProperties.slice(1).split('.')
307
+ return ctx.getDeeperValue(deepProperties, valueToPopulate)
308
+ }
309
+
310
+ if (fileExtension === 'yml' || fileExtension === 'yaml') {
311
+ valueToPopulate = YAML.parse(valueToPopulate)
312
+ return Promise.resolve(valueToPopulate)
313
+ }
314
+
315
+ if (fileExtension === 'toml' || fileExtension === 'tml') {
316
+ valueToPopulate = TOML.parse(valueToPopulate)
317
+ return Promise.resolve(valueToPopulate)
318
+ }
319
+
320
+ if (fileExtension === 'ini') {
321
+ valueToPopulate = INI.parse(valueToPopulate)
322
+ return Promise.resolve(valueToPopulate)
323
+ }
324
+
325
+ if (fileExtension === 'json' || fileExtension === 'json5') {
326
+ valueToPopulate = JSON5.parse(valueToPopulate)
327
+ return Promise.resolve(valueToPopulate)
328
+ }
329
+ }
330
+ // console.log('fall thru', valueToPopulate)
331
+ return Promise.resolve(valueToPopulate)
332
+ }
333
+
334
+ /**
335
+ * Parses variable string to extract module reference path
336
+ * Supports both : and . as separators for module references
337
+ * @param {string} variableString - The full variable string
338
+ * @param {string} matchedFileString - The matched file path portion
339
+ * @returns {{ variableArray: string[], moduleName: string|null }}
340
+ */
341
+ function parseModuleReference(variableString, matchedFileString) {
342
+ let variableArray = variableString.split(':')
343
+ if (variableArray.length === 1) {
344
+ const dotIndex = variableString.indexOf(matchedFileString) + matchedFileString.length
345
+ const afterMatch = variableString.substring(dotIndex)
346
+ if (afterMatch.startsWith('.')) {
347
+ variableArray = [variableString.substring(0, dotIndex), afterMatch.substring(1)]
348
+ }
349
+ }
350
+
351
+ let moduleName = null
352
+ if (variableArray[1]) {
353
+ moduleName = variableArray[1].split('.')[0]
354
+ }
355
+
356
+ return { variableArray, moduleName }
357
+ }
358
+
359
+ /**
360
+ * Extracts deep properties from variable string after file match
361
+ * @param {string} variableString - The full variable string
362
+ * @param {string} matchedFileString - The matched file path portion
363
+ * @returns {string[]} Array of property keys to traverse
364
+ */
365
+ function extractDeepProperties(variableString, matchedFileString) {
366
+ let deepProperties = variableString.replace(matchedFileString, '')
367
+ deepProperties = deepProperties.slice(1).split('.')
368
+ deepProperties.splice(0, 1)
369
+ return deepProperties.map((prop) => trim(prop))
370
+ }
371
+
372
+ /**
373
+ * Processes executable file (JS/TS/ESM) and resolves deep properties
374
+ * @param {object} params - Parameters
375
+ * @param {object} params.fileModule - The loaded module
376
+ * @param {Function} params.returnValueFunction - The function to call
377
+ * @param {object} params.valueForFunction - Context passed to the function
378
+ * @param {string[]} params.argsToPass - Additional args for the function
379
+ * @param {string} params.variableString - Original variable string
380
+ * @param {string} params.matchedFileString - Matched file path
381
+ * @param {string} params.relativePath - Relative file path for errors
382
+ * @param {string} params.fileType - Type of file (javascript/TypeScript/ESM)
383
+ * @param {Function} params.getDeeperValue - Function to resolve nested values
384
+ * @returns {Promise<any>}
385
+ */
386
+ async function processExecutableFile({
387
+ fileModule,
388
+ returnValueFunction,
389
+ valueForFunction,
390
+ argsToPass,
391
+ variableString,
392
+ matchedFileString,
393
+ relativePath,
394
+ fileType,
395
+ getDeeperValue
396
+ }) {
397
+ if (typeof returnValueFunction !== 'function') {
398
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
399
+ Check if your ${fileType} is exporting a function that returns a value.`
400
+ return Promise.reject(new Error(errorMessage))
401
+ }
402
+
403
+ const valueToPopulate = returnValueFunction.call(fileModule, valueForFunction, ...argsToPass)
404
+
405
+ return Promise.resolve(valueToPopulate).then((valueToPopulateResolved) => {
406
+ const deepProperties = extractDeepProperties(variableString, matchedFileString)
407
+ return getDeeperValue(deepProperties, valueToPopulateResolved).then((deepValueToPopulateResolved) => {
408
+ if (typeof deepValueToPopulateResolved === 'undefined') {
409
+ const errorMessage = `Invalid variable syntax when referencing file "${relativePath}".
410
+ Check if your ${fileType} is returning the correct data.`
411
+ return Promise.reject(new Error(errorMessage))
412
+ }
413
+ return Promise.resolve(deepValueToPopulateResolved)
414
+ })
415
+ })
416
+ }
417
+
418
+ module.exports = {
419
+ getValueFromFile,
420
+ parseFileContents,
421
+ parseModuleReference,
422
+ extractDeepProperties
423
+ }
@@ -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()