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,7 +1,37 @@
1
- const { splitCsv } = require('./splitCsv')
2
1
  const dotProp = require('dot-prop')
3
2
  const fs = require('fs')
4
3
  const path = require('path')
4
+ const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVariables } = require('../paths/filePathUtils')
5
+ const { preResolveString, preResolveSingle } = require('../resolution/preResolveVariable')
6
+
7
+ // Type filters that indicate expected value types
8
+ const TYPE_FILTERS = ['Boolean', 'String', 'Number', 'Array', 'Object', 'Json']
9
+
10
+ /**
11
+ * Extract type filter from filters array
12
+ * @param {Array} filters - Filters array from variable instance
13
+ * @returns {string|undefined} Type filter if found
14
+ */
15
+ function extractTypeFromFilters(filters) {
16
+ if (!filters || !Array.isArray(filters)) return undefined
17
+ return filters.find(f => TYPE_FILTERS.includes(f))
18
+ }
19
+
20
+ /**
21
+ * Extract description from filters array (help filter)
22
+ * @param {Array} filters - Filters array from variable instance
23
+ * @returns {string|undefined} Description if found
24
+ */
25
+ function extractDescriptionFromFilters(filters) {
26
+ if (!filters || !Array.isArray(filters)) return undefined
27
+ for (const filter of filters) {
28
+ if (filter && typeof filter === 'string') {
29
+ const helpMatch = filter.match(/^help\(['"](.+)['"]\)$/)
30
+ if (helpMatch) return helpMatch[1]
31
+ }
32
+ }
33
+ return undefined
34
+ }
5
35
 
6
36
  /**
7
37
  * Create a standardized occurrence object
@@ -29,6 +59,9 @@ function createOccurrence(instance, varMatch, options = {}) {
29
59
  }
30
60
  }
31
61
 
62
+ // Extract type from filters
63
+ const type = extractTypeFromFilters(filters)
64
+
32
65
  const occurrence = {
33
66
  originalString: instance.originalStringValue,
34
67
  varMatch: varMatch,
@@ -40,6 +73,10 @@ function createOccurrence(instance, varMatch, options = {}) {
40
73
  hasFallback: options.hasFallback !== undefined ? options.hasFallback : (instance.hasFallback || false),
41
74
  }
42
75
 
76
+ if (type) {
77
+ occurrence.type = type
78
+ }
79
+
43
80
  if (description) {
44
81
  occurrence.description = description
45
82
  }
@@ -52,53 +89,15 @@ function createOccurrence(instance, varMatch, options = {}) {
52
89
  }
53
90
 
54
91
  /**
55
- * Extract file path from a file() or text() reference string
56
- * @param {string} propertyString - The property string containing file/text reference
57
- * @returns {object|null} Object with filePath, or null if no match
58
- */
59
- function extractFilePath(propertyString) {
60
- const fileMatch = propertyString.match(/^\$\{(?:file|text)\((.*?)\)/)
61
- if (!fileMatch || !fileMatch[1]) {
62
- return null
63
- }
64
-
65
- const fileContent = fileMatch[1].trim()
66
- const parts = splitCsv(fileContent)
67
- let filePath = parts[0].trim()
68
-
69
- // Remove quotes if present
70
- filePath = filePath.replace(/^['"]|['"]$/g, '')
71
-
72
- return { filePath }
73
- }
74
-
75
- /**
76
- * Normalize a file path (add ./ prefix, fix .//, skip deep refs)
77
- * @param {string} filePath - The file path to normalize
78
- * @returns {string|null} Normalized path, or null if should be skipped
92
+ * Get source type for a variable type
93
+ * @param {string} variableType - The variable type (e.g., 'env', 'opt', 'git')
94
+ * @param {Array} variableTypes - Array of variable type definitions
95
+ * @returns {string|undefined} The source type ('user', 'config', 'readonly', 'remote')
79
96
  */
80
- function normalizePath(filePath) {
81
- // Skip deep references
82
- if (filePath.includes('deep:')) {
83
- return null
84
- }
85
-
86
- let normalized = filePath
87
-
88
- // Add ./ prefix for relative paths
89
- if (!filePath.startsWith('./') &&
90
- !filePath.startsWith('../') &&
91
- !filePath.startsWith('/') &&
92
- !filePath.startsWith('~')) {
93
- normalized = './' + filePath
94
- }
95
-
96
- // Fix double slashes
97
- if (normalized.startsWith('.//')) {
98
- normalized = normalized.replace('.//', './')
99
- }
100
-
101
- return normalized
97
+ function getSourceForType(variableType, variableTypes) {
98
+ if (!variableTypes || !variableType) return undefined
99
+ const typeDef = variableTypes.find(vt => vt.type === variableType)
100
+ return typeDef?.source
102
101
  }
103
102
 
104
103
  /**
@@ -110,21 +109,31 @@ function normalizePath(filePath) {
110
109
  * @param {object} originalConfig - The original config object (before resolution) for self/dot.prop lookups.
111
110
  * @param {string} configPath - The path to the config file.
112
111
  * @param {Array} filterNames - Array of known filter names.
113
- * @returns {object} Enriched metadata with resolution details and a complete file reference list.
112
+ * @param {object} [resolvedConfig] - The resolved config object (optional, for post-resolution enrichment).
113
+ * @param {object} [options] - CLI options object for opt: resolution.
114
+ * @param {Array} [variableTypes] - Array of variable type definitions with source info.
115
+ * @returns {Promise<object>} Enriched metadata with resolution details and a complete file reference list.
114
116
  */
115
- function enrichMetadata(
117
+ async function enrichMetadata(
116
118
  metadata,
117
119
  resolutionTracking,
118
120
  variableSyntax,
119
121
  fileRefsFound = [],
120
122
  originalConfig = {},
121
123
  configPath,
122
- filterNames = []
124
+ filterNames = [],
125
+ resolvedConfig,
126
+ options = {},
127
+ variableTypes = []
123
128
  ) {
124
129
  if (!resolutionTracking) {
125
130
  return metadata
126
131
  }
127
132
 
133
+ // Create resolve context early for resolving descriptions
134
+ const configDir = configPath ? path.dirname(configPath) : undefined
135
+ const resolveContext = { config: originalConfig, variableSyntax, configDir, options }
136
+
128
137
  const varKeys = Object.keys(metadata.variables)
129
138
 
130
139
  for (const key of varKeys) {
@@ -138,6 +147,11 @@ function enrichMetadata(
138
147
  // For each resolveDetail, find the matching resolution history entry
139
148
  for (let i = 0; i < varData.resolveDetails.length; i++) {
140
149
  const detail = varData.resolveDetails[i]
150
+ // Add source type for this variable type
151
+ const sourceType = getSourceForType(detail.variableType, variableTypes)
152
+ if (sourceType) {
153
+ detail.variableSourceType = sourceType
154
+ }
141
155
  const isOutermost = i === varData.resolveDetails.length - 1
142
156
 
143
157
  if (isOutermost && trackingData.resolutionHistory.length > 0) {
@@ -173,6 +187,17 @@ function enrichMetadata(
173
187
  }
174
188
  }
175
189
  }
190
+
191
+ // Add type and description to individual variable instance
192
+ const instanceType = extractTypeFromFilters(varData.filters)
193
+ const instanceDescription = extractDescriptionFromFilters(varData.filters)
194
+ if (instanceType) {
195
+ varData.type = instanceType
196
+ }
197
+ if (instanceDescription) {
198
+ // Resolve any variables in the description (e.g., ${allowedValues} -> actual values)
199
+ varData.description = await preResolveString(instanceDescription, resolveContext)
200
+ }
176
201
  }
177
202
  }
178
203
 
@@ -198,7 +223,8 @@ function enrichMetadata(
198
223
  }
199
224
 
200
225
  // Update fileDependencies.resolvedPaths with the resolved file refs
201
- if (metadata.fileDependencies) {
226
+ // Only overwrite if we have enriched data, otherwise keep original from collectVariableMetadata
227
+ if (metadata.fileDependencies && resolvedFileRefs.length > 0) {
202
228
  metadata.fileDependencies.resolvedPaths = resolvedFileRefs
203
229
  }
204
230
 
@@ -304,10 +330,14 @@ function enrichMetadata(
304
330
  }
305
331
  }
306
332
 
307
- // Update fileDependencies with the enriched data
333
+ // Update fileDependencies with the enriched data (only if we have data)
308
334
  if (metadata.fileDependencies) {
309
- metadata.fileDependencies.byConfigPath = byConfigPath
310
- metadata.fileDependencies.references = references
335
+ if (byConfigPath.length > 0) {
336
+ metadata.fileDependencies.byConfigPath = byConfigPath
337
+ }
338
+ if (references.length > 0) {
339
+ metadata.fileDependencies.references = references
340
+ }
311
341
  }
312
342
 
313
343
  // Build uniqueVariables rollup - group by base variable (without fallbacks)
@@ -316,11 +346,32 @@ function enrichMetadata(
316
346
  for (const key of varKeys) {
317
347
  const varInstances = metadata.variables[key]
318
348
  const firstInstance = varInstances[0]
319
- const lastResolveDetail = firstInstance.resolveDetails[firstInstance.resolveDetails.length - 1]
349
+
350
+ // Extract variable name from key (e.g. "${self:service}" -> "self:service")
351
+ const keyVarName = key.slice(2, -1).split(',')[0].trim()
352
+
353
+ // Find the resolveDetail that matches THIS key's variable (not just outermost)
354
+ let matchingDetail = null
355
+ for (const instance of varInstances) {
356
+ if (instance.resolveDetails && instance.resolveDetails.length > 0) {
357
+ const found = instance.resolveDetails.find((detail) => {
358
+ const detailVar = detail.valueBeforeFallback || detail.variable
359
+ return detailVar === keyVarName
360
+ })
361
+ if (found) {
362
+ matchingDetail = found
363
+ break
364
+ }
365
+ }
366
+ }
367
+ // Fallback to last resolveDetail if no match found
368
+ if (!matchingDetail) {
369
+ matchingDetail = firstInstance.resolveDetails[firstInstance.resolveDetails.length - 1]
370
+ }
320
371
 
321
372
  // Get the base variable name without fallback
322
373
  // Use valueBeforeFallback if present, otherwise use the variable string
323
- let baseVar = lastResolveDetail.valueBeforeFallback || lastResolveDetail.variable
374
+ let baseVar = matchingDetail.valueBeforeFallback || matchingDetail.variable
324
375
 
325
376
  // Strip filters from baseVar using known filter names
326
377
  // e.g., "opt:stage | toUpperCase | help(...)" -> "opt:stage"
@@ -338,29 +389,13 @@ function enrichMetadata(
338
389
  }
339
390
 
340
391
  // Normalize file() and text() references
341
- if (baseVar.match(/^(?:file|text)\(/)) {
342
- // Strip sub-key accessors like :topLevel, :nested.value, etc.
343
- baseVar = baseVar.replace(/:[\w.[\]]+$/, '')
344
-
345
- // Normalize path - remove quotes and ensure it starts with ./
346
- baseVar = baseVar.replace(/^(file|text)\((.+?)\)/, (match, funcName, filePath) => {
347
- // Remove surrounding quotes (single or double)
348
- let cleanPath = filePath.trim().replace(/^["']|["']$/g, '')
349
-
350
- // Use normalizePath for consistent normalization (handles ./, .// etc)
351
- const normalized = normalizePath(cleanPath)
352
- if (normalized) {
353
- return `${funcName}(${normalized})`
354
- }
355
-
356
- return match
357
- })
358
- }
392
+ baseVar = normalizeFileVariable(baseVar)
359
393
 
360
394
  if (!uniqueVariablesMap.has(baseVar)) {
361
395
  uniqueVariablesMap.set(baseVar, {
362
396
  variable: baseVar,
363
- variableType: lastResolveDetail.variableType,
397
+ variableType: matchingDetail.variableType,
398
+ variableSourceType: getSourceForType(matchingDetail.variableType, variableTypes),
364
399
  occurrences: [],
365
400
  innerVariables: [],
366
401
  })
@@ -381,9 +416,25 @@ function enrichMetadata(
381
416
  // The outermost variable is the last one in resolveDetails
382
417
  const outermostDetail = instance.resolveDetails[instance.resolveDetails.length - 1]
383
418
 
419
+ // Find position of first | (filter separator) in the outermost variable's original string
420
+ // Variables after this position are filter-internal (e.g., inside help('...${var}...'))
421
+ const originalStr = outermostDetail.originalStringValue || ''
422
+ const outerVarStart = outermostDetail.start || 0
423
+ // Find first | that's inside the outermost variable (after its opening ${)
424
+ const filterSeparatorPos = originalStr.indexOf('|', outerVarStart + 2)
425
+
384
426
  for (let i = 0; i < instance.resolveDetails.length - 1; i++) {
385
427
  const detail = instance.resolveDetails[i]
386
428
 
429
+ // Check if this variable is inside filter arguments (after the first |)
430
+ // These are meta-variables used to build filter values, not user-configurable variables
431
+ const isInsideFilter = filterSeparatorPos !== -1 && detail.start > filterSeparatorPos
432
+ if (isInsideFilter) {
433
+ // Mark this detail as filter-internal and skip adding to uniqueVariables
434
+ detail.isFilterInnerVariable = true
435
+ continue
436
+ }
437
+
387
438
  // Check if this variable is actually INSIDE the outermost variable's boundaries
388
439
  // A variable is "inner" only if it's contained within the parent's start/end range
389
440
  const isInnerVariable = detail.start >= outermostDetail.start && detail.end <= outermostDetail.end
@@ -394,23 +445,14 @@ function enrichMetadata(
394
445
  const siblingBaseVar = detail.valueBeforeFallback || detail.variable
395
446
 
396
447
  // Normalize file/text references for sibling too
397
- let normalizedSiblingVar = siblingBaseVar
398
- if (normalizedSiblingVar.match(/^(?:file|text)\(/)) {
399
- // Strip sub-key accessor (e.g., :foo from file(./_inner.yml):foo)
400
- normalizedSiblingVar = normalizedSiblingVar.replace(/:[\w.[\]]+$/, '')
401
-
402
- normalizedSiblingVar = normalizedSiblingVar.replace(/^(file|text)\((.+?)\)/, (match, funcName, filePath) => {
403
- let cleanPath = filePath.trim().replace(/^["']|["']$/g, '')
404
- const normalized = normalizePath(cleanPath)
405
- return normalized ? `${funcName}(${normalized})` : match
406
- })
407
- }
448
+ const normalizedSiblingVar = normalizeFileVariable(siblingBaseVar)
408
449
 
409
450
  // Create or get entry for this sibling variable
410
451
  if (!uniqueVariablesMap.has(normalizedSiblingVar)) {
411
452
  uniqueVariablesMap.set(normalizedSiblingVar, {
412
453
  variable: normalizedSiblingVar,
413
454
  variableType: detail.variableType,
455
+ variableSourceType: getSourceForType(detail.variableType, variableTypes),
414
456
  occurrences: [],
415
457
  innerVariables: [],
416
458
  })
@@ -419,11 +461,29 @@ function enrichMetadata(
419
461
  const siblingEntry = uniqueVariablesMap.get(normalizedSiblingVar)
420
462
 
421
463
  // Add occurrence for this sibling variable
464
+ // Check if this self-reference resolves to a value in originalConfig
465
+ let siblingDefaultValue = detail.hasFallback ? (detail.fallbackValues?.[0]?.stringValue || detail.fallbackValues?.[0]?.variable) : undefined
466
+ let siblingDefaultValueSrc
467
+ let siblingIsRequired = !detail.hasFallback
468
+
469
+ if (detail.variableType === 'self' || detail.variableType === 'dot.prop') {
470
+ const varPath = (detail.valueBeforeFallback || detail.variable).replace('self:', '')
471
+ const resolvedValue = dotProp.get(originalConfig, varPath)
472
+ if (resolvedValue !== undefined) {
473
+ siblingDefaultValue = resolvedValue
474
+ siblingDefaultValueSrc = varPath
475
+ siblingIsRequired = false
476
+ }
477
+ }
478
+
422
479
  const siblingOccurrence = createOccurrence(instance, detail.varMatch, {
423
- isRequired: !detail.hasFallback,
480
+ isRequired: siblingIsRequired,
424
481
  hasFallback: !!detail.hasFallback,
425
- defaultValue: detail.hasFallback ? (detail.fallbackValues?.[0]?.stringValue || detail.fallbackValues?.[0]?.variable) : undefined,
482
+ defaultValue: siblingDefaultValue,
426
483
  })
484
+ if (siblingDefaultValueSrc) {
485
+ siblingOccurrence.defaultValueSrc = siblingDefaultValueSrc
486
+ }
427
487
 
428
488
  // Check if this exact occurrence already exists
429
489
  const occurrenceExists = siblingEntry.occurrences.some(occ =>
@@ -512,12 +572,7 @@ function enrichMetadata(
512
572
  }
513
573
 
514
574
  // Normalize file paths after variable substitution
515
- if (resolvedVariable.match(/^(?:file|text)\(/)) {
516
- resolvedVariable = resolvedVariable.replace(/^(file|text)\((.+?)\)/, (match, funcName, filePath) => {
517
- const normalized = normalizePath(filePath)
518
- return normalized ? `${funcName}(${normalized})` : match
519
- })
520
- }
575
+ resolvedVariable = normalizeFileVariable(resolvedVariable)
521
576
 
522
577
  // Update the variable to the resolved version and update map key
523
578
  if (resolvedVariable !== baseVar) {
@@ -577,9 +632,104 @@ function enrichMetadata(
577
632
  }
578
633
  }
579
634
 
635
+ // Aggregate types and descriptions for each uniqueVariable
636
+ for (const [varKey, entry] of uniqueVariablesMap) {
637
+ // Collect unique types from occurrences
638
+ const types = entry.occurrences
639
+ .map(occ => occ.type)
640
+ .filter((t, i, a) => t && a.indexOf(t) === i)
641
+ if (types.length > 0) {
642
+ entry.types = types
643
+ }
644
+
645
+ // Collect unique descriptions from occurrences and resolve any variables
646
+ const rawDescriptions = entry.occurrences
647
+ .map(occ => occ.description)
648
+ .filter((d, i, a) => d && a.indexOf(d) === i)
649
+
650
+ if (rawDescriptions.length > 0) {
651
+ // Build map from raw -> resolved description
652
+ const descriptionMap = new Map()
653
+ await Promise.all(
654
+ rawDescriptions.map(async desc => {
655
+ // Resolve any variables in the description
656
+ const resolved = await preResolveString(desc, resolveContext)
657
+ descriptionMap.set(desc, resolved)
658
+ })
659
+ )
660
+
661
+ // Update each occurrence's description with resolved version
662
+ for (const occ of entry.occurrences) {
663
+ if (occ.description && descriptionMap.has(occ.description)) {
664
+ occ.description = descriptionMap.get(occ.description)
665
+ }
666
+ }
667
+
668
+ entry.descriptions = Array.from(descriptionMap.values())
669
+ }
670
+
671
+ // Try to resolve defaultValue for variables that can be pre-resolved
672
+ // This applies to git:, env:, self: variables without existing defaults
673
+ const firstOcc = entry.occurrences[0]
674
+ if (firstOcc && firstOcc.isRequired && !firstOcc.defaultValue) {
675
+ const resolvableTypes = ['git', 'env', 'self', 'dot.prop']
676
+ if (resolvableTypes.includes(entry.variableType)) {
677
+ try {
678
+ const resolved = await preResolveSingle(entry.variable, resolveContext)
679
+ if (resolved !== undefined) {
680
+ const formattedValue = Array.isArray(resolved)
681
+ ? resolved.join(', ')
682
+ : (typeof resolved === 'object' ? JSON.stringify(resolved) : String(resolved))
683
+
684
+ // Update occurrences and entry
685
+ entry.occurrences.forEach(occ => {
686
+ occ.defaultValue = formattedValue
687
+ occ.isRequired = false
688
+ })
689
+ entry.resolvedValue = formattedValue
690
+ }
691
+ } catch (e) {
692
+ // Couldn't resolve, leave as required
693
+ }
694
+ }
695
+ }
696
+ }
697
+
580
698
  // Convert map to object for metadata
581
699
  metadata.uniqueVariables = Object.fromEntries(uniqueVariablesMap)
582
700
 
701
+ // Add resolvedPropertyValue to resolutionTracking if resolvedConfig provided
702
+ if (resolvedConfig) {
703
+ metadata.resolutionHistory = {}
704
+ for (const pathKey in resolutionTracking) {
705
+ const tracking = resolutionTracking[pathKey]
706
+ const keys = pathKey.split('.')
707
+ let resolvedValue = resolvedConfig
708
+
709
+ for (const key of keys) {
710
+ if (resolvedValue && typeof resolvedValue === 'object') {
711
+ resolvedValue = resolvedValue[key]
712
+ } else {
713
+ resolvedValue = undefined
714
+ break
715
+ }
716
+ }
717
+
718
+ // Unescape any __CONFIGVAR:...__ placeholders in tracking data
719
+ // (resolutionTracking uses escaped config during resolution)
720
+ const cleanTracking = JSON.parse(
721
+ JSON.stringify(tracking).replace(/__CONFIGVAR:([A-Za-z0-9+/=]+)__/g, (_, encoded) => {
722
+ return Buffer.from(encoded, 'base64').toString('utf8')
723
+ })
724
+ )
725
+
726
+ metadata.resolutionHistory[pathKey] = {
727
+ ...cleanTracking,
728
+ resolvedPropertyValue: resolvedValue
729
+ }
730
+ }
731
+ }
732
+
583
733
  return metadata
584
734
  }
585
735
 
@@ -1,9 +1,9 @@
1
- const YAML = require('../parsers/yaml')
2
- const TOML = require('../parsers/toml')
3
- const INI = require('../parsers/ini')
4
- const JSON5 = require('../parsers/json5')
5
- const { executeTypeScriptFileSync } = require('../parsers/typescript')
6
- const { executeESMFileSync } = require('../parsers/esm')
1
+ const YAML = require('../../parsers/yaml')
2
+ const TOML = require('../../parsers/toml')
3
+ const INI = require('../../parsers/ini')
4
+ const JSON5 = require('../../parsers/json5')
5
+ const { executeTypeScriptFileSync } = require('../../parsers/typescript')
6
+ const { executeESMFileSync } = require('../../parsers/esm')
7
7
  const cloudFormationSchema = require('./cloudformationSchema')
8
8
 
9
9
  /**
@@ -18,7 +18,7 @@ const cloudFormationSchema = require('./cloudformationSchema')
18
18
  function parseFileContents(fileContents, fileType, filePath, varRegex, opts = {}) {
19
19
  let configObject
20
20
 
21
- if (fileType.match(/\.(yml|yaml)/)) {
21
+ if (fileType.match(/\.(yml|yaml)/i)) {
22
22
  try {
23
23
  const ymlText = YAML.preProcess(fileContents, varRegex)
24
24
  configObject = YAML.parse(ymlText)
@@ -36,14 +36,14 @@ function parseFileContents(fileContents, fileType, filePath, varRegex, opts = {}
36
36
  configObject = result.data
37
37
  }
38
38
  }
39
- } else if (fileType.match(/\.(toml|tml)/)) {
39
+ } else if (fileType.match(/\.(toml|tml)/i)) {
40
40
  configObject = TOML.parse(fileContents)
41
- } else if (fileType.match(/\.(ini)/)) {
41
+ } else if (fileType.match(/\.(ini)/i)) {
42
42
  configObject = INI.parse(fileContents)
43
- } else if (fileType.match(/\.(json|json5)/)) {
43
+ } else if (fileType.match(/\.(json|json5)/i)) {
44
44
  configObject = JSON5.parse(fileContents)
45
45
  // TODO detect js syntax and use appropriate parser
46
- } else if (fileType.match(/\.(js|cjs)/)) {
46
+ } else if (fileType.match(/\.(js|cjs)/i)) {
47
47
  let jsFile
48
48
  try {
49
49
  jsFile = require(filePath)
@@ -60,7 +60,7 @@ function parseFileContents(fileContents, fileType, filePath, varRegex, opts = {}
60
60
  } catch (err) {
61
61
  throw new Error(err)
62
62
  }
63
- } else if (fileType.match(/\.(ts|tsx)/)) {
63
+ } else if (fileType.match(/\.(ts|tsx|mts|cts)/i)) {
64
64
  try {
65
65
  let jsArgs = opts.dynamicArgs || {}
66
66
  if (jsArgs && typeof jsArgs === 'function') {
@@ -76,7 +76,7 @@ function parseFileContents(fileContents, fileType, filePath, varRegex, opts = {}
76
76
  } catch (err) {
77
77
  throw new Error(`Failed to execute TypeScript file ${filePath}: ${err.message}`)
78
78
  }
79
- } else if (fileType.match(/\.(mjs|esm)/)) {
79
+ } else if (fileType.match(/\.(mjs|esm)/i)) {
80
80
  try {
81
81
  let jsArgs = opts.dynamicArgs || {}
82
82
  if (jsArgs && typeof jsArgs === 'function') {