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.
- package/README.md +196 -24
- package/cli.js +3 -3
- package/package.json +1 -1
- package/src/index.js +22 -32
- package/src/main.js +690 -778
- package/src/parsers/yaml.js +3 -47
- package/src/resolvers/valueFromCron.js +3 -1
- package/src/resolvers/valueFromEnv.js +1 -0
- package/src/resolvers/valueFromEval.js +1 -0
- package/src/resolvers/valueFromFile.js +423 -0
- package/src/resolvers/valueFromGit.js +3 -2
- package/src/resolvers/valueFromOptions.js +1 -0
- package/src/resolvers/valueFromString.js +2 -1
- package/src/sync.js +12 -5
- package/src/utils/parsing/arrayToJsonPath.test.js +56 -0
- package/src/utils/{enrichMetadata.js → parsing/enrichMetadata.js} +210 -18
- package/src/utils/{parse.js → parsing/parse.js} +13 -13
- package/src/utils/parsing/preProcess.js +165 -0
- package/src/utils/{filePathUtils.js → paths/filePathUtils.js} +3 -2
- package/src/utils/paths/findLineForKey.js +47 -0
- package/src/utils/paths/findLineForKey.test.js +126 -0
- package/src/utils/{getFullFilePath.js → paths/getFullFilePath.js} +24 -26
- package/src/utils/{resolveAlias.js → paths/resolveAlias.js} +1 -1
- package/src/utils/regex/index.js +23 -1
- package/src/utils/resolution/preResolveVariable.js +260 -0
- package/src/utils/resolution/preResolveVariable.test.js +98 -0
- package/src/utils/strings/bracketMatcher.js +86 -0
- package/src/utils/strings/bracketMatcher.test.js +135 -0
- package/src/utils/{formatFunctionArgs.js → strings/formatFunctionArgs.js} +3 -2
- package/src/utils/strings/formatFunctionArgs.test.js +77 -0
- package/src/utils/strings/quoteUtils.js +89 -0
- package/src/utils/strings/quoteUtils.test.js +217 -0
- package/src/utils/strings/replaceAll.test.js +82 -0
- package/src/utils/{splitByComma.js → strings/splitByComma.js} +1 -1
- package/src/utils/strings/splitCsv.js +38 -0
- package/src/utils/strings/splitCsv.test.js +96 -0
- package/src/utils/strings/textUtils.test.js +86 -0
- package/src/utils/{configWizard.js → ui/configWizard.js} +177 -38
- package/src/utils/{createEditorLink.js → ui/createEditorLink.js} +11 -2
- package/src/utils/{logs.js → ui/logs.js} +3 -3
- package/src/utils/validation/isValidValue.test.js +64 -0
- package/src/utils/validation/warnIfNotFound.js +52 -0
- package/src/utils/variables/appendDeepVariable.test.js +41 -0
- package/src/utils/{cleanVariable.js → variables/cleanVariable.js} +5 -26
- package/src/utils/{find-nested-variables.js → variables/findNestedVariables.js} +2 -2
- package/src/utils/{find-nested-variables.test.js → variables/findNestedVariables.test.js} +5 -5
- package/src/utils/variables/getVariableType.test.js +109 -0
- package/src/utils/variables/variableUtils.test.js +117 -0
- package/src/utils/isValidValue.js +0 -8
- package/src/utils/splitCsv.js +0 -29
- package/src/utils/trimSurroundingQuotes.js +0 -5
- /package/src/utils/{arrayToJsonPath.js → parsing/arrayToJsonPath.js} +0 -0
- /package/src/utils/{cloudformationSchema.js → parsing/cloudformationSchema.js} +0 -0
- /package/src/utils/{mergeByKeys.js → parsing/mergeByKeys.js} +0 -0
- /package/src/utils/{filePathUtils.test.js → paths/filePathUtils.test.js} +0 -0
- /package/src/utils/{find-project-root.js → paths/findProjectRoot.js} +0 -0
- /package/src/utils/{resolveAlias.test.js → paths/resolveAlias.test.js} +0 -0
- /package/src/utils/{replaceAll.js → strings/replaceAll.js} +0 -0
- /package/src/utils/{splitByComma.test.js → strings/splitByComma.test.js} +0 -0
- /package/src/utils/{textUtils.js → strings/textUtils.js} +0 -0
- /package/src/utils/{chalk.js → ui/chalk.js} +0 -0
- /package/src/utils/{deep-log.js → ui/deep-log.js} +0 -0
- /package/src/utils/{appendDeepVariable.js → variables/appendDeepVariable.js} +0 -0
- /package/src/utils/{cleanVariable.test.js → variables/cleanVariable.test.js} +0 -0
- /package/src/utils/{getVariableType.js → variables/getVariableType.js} +0 -0
- /package/src/utils/{variableUtils.js → variables/variableUtils.js} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const dotProp = require('dot-prop')
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const path = require('path')
|
|
4
|
-
const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVariables } = require('
|
|
4
|
+
const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVariables } = require('../paths/filePathUtils')
|
|
5
|
+
const { preResolveString, preResolveSingle } = require('../resolution/preResolveVariable')
|
|
5
6
|
|
|
6
7
|
// Type filters that indicate expected value types
|
|
7
8
|
const TYPE_FILTERS = ['Boolean', 'String', 'Number', 'Array', 'Object', 'Json']
|
|
@@ -87,6 +88,18 @@ function createOccurrence(instance, varMatch, options = {}) {
|
|
|
87
88
|
return occurrence
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
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')
|
|
96
|
+
*/
|
|
97
|
+
function getSourceForType(variableType, variableTypes) {
|
|
98
|
+
if (!variableTypes || !variableType) return undefined
|
|
99
|
+
const typeDef = variableTypes.find(vt => vt.type === variableType)
|
|
100
|
+
return typeDef?.source
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
/**
|
|
91
104
|
* Enriches variable metadata with resolution tracking data.
|
|
92
105
|
* @param {object} metadata - The metadata object from collectVariableMetadata.
|
|
@@ -96,21 +109,31 @@ function createOccurrence(instance, varMatch, options = {}) {
|
|
|
96
109
|
* @param {object} originalConfig - The original config object (before resolution) for self/dot.prop lookups.
|
|
97
110
|
* @param {string} configPath - The path to the config file.
|
|
98
111
|
* @param {Array} filterNames - Array of known filter names.
|
|
99
|
-
* @
|
|
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.
|
|
100
116
|
*/
|
|
101
|
-
function enrichMetadata(
|
|
117
|
+
async function enrichMetadata(
|
|
102
118
|
metadata,
|
|
103
119
|
resolutionTracking,
|
|
104
120
|
variableSyntax,
|
|
105
121
|
fileRefsFound = [],
|
|
106
122
|
originalConfig = {},
|
|
107
123
|
configPath,
|
|
108
|
-
filterNames = []
|
|
124
|
+
filterNames = [],
|
|
125
|
+
resolvedConfig,
|
|
126
|
+
options = {},
|
|
127
|
+
variableTypes = []
|
|
109
128
|
) {
|
|
110
129
|
if (!resolutionTracking) {
|
|
111
130
|
return metadata
|
|
112
131
|
}
|
|
113
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
|
+
|
|
114
137
|
const varKeys = Object.keys(metadata.variables)
|
|
115
138
|
|
|
116
139
|
for (const key of varKeys) {
|
|
@@ -124,6 +147,11 @@ function enrichMetadata(
|
|
|
124
147
|
// For each resolveDetail, find the matching resolution history entry
|
|
125
148
|
for (let i = 0; i < varData.resolveDetails.length; i++) {
|
|
126
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
|
+
}
|
|
127
155
|
const isOutermost = i === varData.resolveDetails.length - 1
|
|
128
156
|
|
|
129
157
|
if (isOutermost && trackingData.resolutionHistory.length > 0) {
|
|
@@ -167,7 +195,8 @@ function enrichMetadata(
|
|
|
167
195
|
varData.type = instanceType
|
|
168
196
|
}
|
|
169
197
|
if (instanceDescription) {
|
|
170
|
-
|
|
198
|
+
// Resolve any variables in the description (e.g., ${allowedValues} -> actual values)
|
|
199
|
+
varData.description = await preResolveString(instanceDescription, resolveContext)
|
|
171
200
|
}
|
|
172
201
|
}
|
|
173
202
|
}
|
|
@@ -275,26 +304,37 @@ function enrichMetadata(
|
|
|
275
304
|
}
|
|
276
305
|
}
|
|
277
306
|
|
|
307
|
+
// Add overrideFilePath to references if file was overridden
|
|
308
|
+
for (const refData of references) {
|
|
309
|
+
const details = fileDetailsMap.get(refData.resolvedPath)
|
|
310
|
+
if (details && details.wasOverridden) {
|
|
311
|
+
refData.overrideFilePath = details.filePath
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
278
315
|
const byConfigPath = []
|
|
279
316
|
if (references.length > 0) {
|
|
280
317
|
for (const resolvedFileData of references) {
|
|
281
318
|
const details = fileDetailsMap.get(resolvedFileData.resolvedPath)
|
|
282
319
|
if (details) {
|
|
283
320
|
for (const ref of resolvedFileData.refs) {
|
|
321
|
+
// Use original file path if overridden, otherwise use the resolved path
|
|
322
|
+
const filePath = details.wasOverridden ? details.originalFilePath : details.filePath
|
|
284
323
|
const confDetails = {
|
|
285
324
|
location: ref.location,
|
|
286
|
-
filePath:
|
|
325
|
+
filePath: filePath,
|
|
287
326
|
relativePath: details.relativePath,
|
|
288
327
|
originalVariableString: ref.originalVariableString,
|
|
289
328
|
resolvedVariableString: details.resolvedVariableString,
|
|
290
329
|
containsVariables: !!ref.hasInnerVariable,
|
|
291
330
|
exists: details.exists,
|
|
292
|
-
// Get glob patterns from the individual ref, default to empty array
|
|
293
|
-
|
|
294
331
|
}
|
|
295
332
|
if (ref.pattern) {
|
|
296
333
|
confDetails.pattern = ref.pattern
|
|
297
334
|
}
|
|
335
|
+
if (details.wasOverridden) {
|
|
336
|
+
confDetails.overrideFilePath = details.filePath
|
|
337
|
+
}
|
|
298
338
|
byConfigPath.push(confDetails)
|
|
299
339
|
}
|
|
300
340
|
}
|
|
@@ -309,6 +349,25 @@ function enrichMetadata(
|
|
|
309
349
|
if (references.length > 0) {
|
|
310
350
|
metadata.fileDependencies.references = references
|
|
311
351
|
}
|
|
352
|
+
|
|
353
|
+
// Collect unique overridden files from fileRefsFound
|
|
354
|
+
const overriddenFilesMap = new Map()
|
|
355
|
+
for (const ref of fileRefsFound) {
|
|
356
|
+
if (ref.wasOverridden) {
|
|
357
|
+
// Use originalFilePath as key to dedupe
|
|
358
|
+
if (!overriddenFilesMap.has(ref.originalFilePath)) {
|
|
359
|
+
overriddenFilesMap.set(ref.originalFilePath, {
|
|
360
|
+
originalPath: ref.originalFilePath,
|
|
361
|
+
overridePath: ref.filePath,
|
|
362
|
+
relativePath: ref.relativePath,
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (overriddenFilesMap.size > 0) {
|
|
369
|
+
metadata.fileDependencies.overriddenFiles = Array.from(overriddenFilesMap.values())
|
|
370
|
+
}
|
|
312
371
|
}
|
|
313
372
|
|
|
314
373
|
// Build uniqueVariables rollup - group by base variable (without fallbacks)
|
|
@@ -317,11 +376,32 @@ function enrichMetadata(
|
|
|
317
376
|
for (const key of varKeys) {
|
|
318
377
|
const varInstances = metadata.variables[key]
|
|
319
378
|
const firstInstance = varInstances[0]
|
|
320
|
-
|
|
379
|
+
|
|
380
|
+
// Extract variable name from key (e.g. "${self:service}" -> "self:service")
|
|
381
|
+
const keyVarName = key.slice(2, -1).split(',')[0].trim()
|
|
382
|
+
|
|
383
|
+
// Find the resolveDetail that matches THIS key's variable (not just outermost)
|
|
384
|
+
let matchingDetail = null
|
|
385
|
+
for (const instance of varInstances) {
|
|
386
|
+
if (instance.resolveDetails && instance.resolveDetails.length > 0) {
|
|
387
|
+
const found = instance.resolveDetails.find((detail) => {
|
|
388
|
+
const detailVar = detail.valueBeforeFallback || detail.variable
|
|
389
|
+
return detailVar === keyVarName
|
|
390
|
+
})
|
|
391
|
+
if (found) {
|
|
392
|
+
matchingDetail = found
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Fallback to last resolveDetail if no match found
|
|
398
|
+
if (!matchingDetail) {
|
|
399
|
+
matchingDetail = firstInstance.resolveDetails[firstInstance.resolveDetails.length - 1]
|
|
400
|
+
}
|
|
321
401
|
|
|
322
402
|
// Get the base variable name without fallback
|
|
323
403
|
// Use valueBeforeFallback if present, otherwise use the variable string
|
|
324
|
-
let baseVar =
|
|
404
|
+
let baseVar = matchingDetail.valueBeforeFallback || matchingDetail.variable
|
|
325
405
|
|
|
326
406
|
// Strip filters from baseVar using known filter names
|
|
327
407
|
// e.g., "opt:stage | toUpperCase | help(...)" -> "opt:stage"
|
|
@@ -344,7 +424,8 @@ function enrichMetadata(
|
|
|
344
424
|
if (!uniqueVariablesMap.has(baseVar)) {
|
|
345
425
|
uniqueVariablesMap.set(baseVar, {
|
|
346
426
|
variable: baseVar,
|
|
347
|
-
variableType:
|
|
427
|
+
variableType: matchingDetail.variableType,
|
|
428
|
+
variableSourceType: getSourceForType(matchingDetail.variableType, variableTypes),
|
|
348
429
|
occurrences: [],
|
|
349
430
|
innerVariables: [],
|
|
350
431
|
})
|
|
@@ -365,9 +446,25 @@ function enrichMetadata(
|
|
|
365
446
|
// The outermost variable is the last one in resolveDetails
|
|
366
447
|
const outermostDetail = instance.resolveDetails[instance.resolveDetails.length - 1]
|
|
367
448
|
|
|
449
|
+
// Find position of first | (filter separator) in the outermost variable's original string
|
|
450
|
+
// Variables after this position are filter-internal (e.g., inside help('...${var}...'))
|
|
451
|
+
const originalStr = outermostDetail.originalStringValue || ''
|
|
452
|
+
const outerVarStart = outermostDetail.start || 0
|
|
453
|
+
// Find first | that's inside the outermost variable (after its opening ${)
|
|
454
|
+
const filterSeparatorPos = originalStr.indexOf('|', outerVarStart + 2)
|
|
455
|
+
|
|
368
456
|
for (let i = 0; i < instance.resolveDetails.length - 1; i++) {
|
|
369
457
|
const detail = instance.resolveDetails[i]
|
|
370
458
|
|
|
459
|
+
// Check if this variable is inside filter arguments (after the first |)
|
|
460
|
+
// These are meta-variables used to build filter values, not user-configurable variables
|
|
461
|
+
const isInsideFilter = filterSeparatorPos !== -1 && detail.start > filterSeparatorPos
|
|
462
|
+
if (isInsideFilter) {
|
|
463
|
+
// Mark this detail as filter-internal and skip adding to uniqueVariables
|
|
464
|
+
detail.isFilterInnerVariable = true
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
467
|
+
|
|
371
468
|
// Check if this variable is actually INSIDE the outermost variable's boundaries
|
|
372
469
|
// A variable is "inner" only if it's contained within the parent's start/end range
|
|
373
470
|
const isInnerVariable = detail.start >= outermostDetail.start && detail.end <= outermostDetail.end
|
|
@@ -385,6 +482,7 @@ function enrichMetadata(
|
|
|
385
482
|
uniqueVariablesMap.set(normalizedSiblingVar, {
|
|
386
483
|
variable: normalizedSiblingVar,
|
|
387
484
|
variableType: detail.variableType,
|
|
485
|
+
variableSourceType: getSourceForType(detail.variableType, variableTypes),
|
|
388
486
|
occurrences: [],
|
|
389
487
|
innerVariables: [],
|
|
390
488
|
})
|
|
@@ -393,11 +491,29 @@ function enrichMetadata(
|
|
|
393
491
|
const siblingEntry = uniqueVariablesMap.get(normalizedSiblingVar)
|
|
394
492
|
|
|
395
493
|
// Add occurrence for this sibling variable
|
|
494
|
+
// Check if this self-reference resolves to a value in originalConfig
|
|
495
|
+
let siblingDefaultValue = detail.hasFallback ? (detail.fallbackValues?.[0]?.stringValue || detail.fallbackValues?.[0]?.variable) : undefined
|
|
496
|
+
let siblingDefaultValueSrc
|
|
497
|
+
let siblingIsRequired = !detail.hasFallback
|
|
498
|
+
|
|
499
|
+
if (detail.variableType === 'self' || detail.variableType === 'dot.prop') {
|
|
500
|
+
const varPath = (detail.valueBeforeFallback || detail.variable).replace('self:', '')
|
|
501
|
+
const resolvedValue = dotProp.get(originalConfig, varPath)
|
|
502
|
+
if (resolvedValue !== undefined) {
|
|
503
|
+
siblingDefaultValue = resolvedValue
|
|
504
|
+
siblingDefaultValueSrc = varPath
|
|
505
|
+
siblingIsRequired = false
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
396
509
|
const siblingOccurrence = createOccurrence(instance, detail.varMatch, {
|
|
397
|
-
isRequired:
|
|
510
|
+
isRequired: siblingIsRequired,
|
|
398
511
|
hasFallback: !!detail.hasFallback,
|
|
399
|
-
defaultValue:
|
|
512
|
+
defaultValue: siblingDefaultValue,
|
|
400
513
|
})
|
|
514
|
+
if (siblingDefaultValueSrc) {
|
|
515
|
+
siblingOccurrence.defaultValueSrc = siblingDefaultValueSrc
|
|
516
|
+
}
|
|
401
517
|
|
|
402
518
|
// Check if this exact occurrence already exists
|
|
403
519
|
const occurrenceExists = siblingEntry.occurrences.some(occ =>
|
|
@@ -547,7 +663,7 @@ function enrichMetadata(
|
|
|
547
663
|
}
|
|
548
664
|
|
|
549
665
|
// Aggregate types and descriptions for each uniqueVariable
|
|
550
|
-
for (const [, entry] of uniqueVariablesMap) {
|
|
666
|
+
for (const [varKey, entry] of uniqueVariablesMap) {
|
|
551
667
|
// Collect unique types from occurrences
|
|
552
668
|
const types = entry.occurrences
|
|
553
669
|
.map(occ => occ.type)
|
|
@@ -556,18 +672,94 @@ function enrichMetadata(
|
|
|
556
672
|
entry.types = types
|
|
557
673
|
}
|
|
558
674
|
|
|
559
|
-
// Collect unique descriptions from occurrences
|
|
560
|
-
const
|
|
675
|
+
// Collect unique descriptions from occurrences and resolve any variables
|
|
676
|
+
const rawDescriptions = entry.occurrences
|
|
561
677
|
.map(occ => occ.description)
|
|
562
678
|
.filter((d, i, a) => d && a.indexOf(d) === i)
|
|
563
|
-
|
|
564
|
-
|
|
679
|
+
|
|
680
|
+
if (rawDescriptions.length > 0) {
|
|
681
|
+
// Build map from raw -> resolved description
|
|
682
|
+
const descriptionMap = new Map()
|
|
683
|
+
await Promise.all(
|
|
684
|
+
rawDescriptions.map(async desc => {
|
|
685
|
+
// Resolve any variables in the description
|
|
686
|
+
const resolved = await preResolveString(desc, resolveContext)
|
|
687
|
+
descriptionMap.set(desc, resolved)
|
|
688
|
+
})
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
// Update each occurrence's description with resolved version
|
|
692
|
+
for (const occ of entry.occurrences) {
|
|
693
|
+
if (occ.description && descriptionMap.has(occ.description)) {
|
|
694
|
+
occ.description = descriptionMap.get(occ.description)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
entry.descriptions = Array.from(descriptionMap.values())
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Try to resolve defaultValue for variables that can be pre-resolved
|
|
702
|
+
// This applies to git:, env:, self: variables without existing defaults
|
|
703
|
+
const firstOcc = entry.occurrences[0]
|
|
704
|
+
if (firstOcc && firstOcc.isRequired && !firstOcc.defaultValue) {
|
|
705
|
+
const resolvableTypes = ['git', 'env', 'self', 'dot.prop']
|
|
706
|
+
if (resolvableTypes.includes(entry.variableType)) {
|
|
707
|
+
try {
|
|
708
|
+
const resolved = await preResolveSingle(entry.variable, resolveContext)
|
|
709
|
+
if (resolved !== undefined) {
|
|
710
|
+
const formattedValue = Array.isArray(resolved)
|
|
711
|
+
? resolved.join(', ')
|
|
712
|
+
: (typeof resolved === 'object' ? JSON.stringify(resolved) : String(resolved))
|
|
713
|
+
|
|
714
|
+
// Update occurrences and entry
|
|
715
|
+
entry.occurrences.forEach(occ => {
|
|
716
|
+
occ.defaultValue = formattedValue
|
|
717
|
+
occ.isRequired = false
|
|
718
|
+
})
|
|
719
|
+
entry.resolvedValue = formattedValue
|
|
720
|
+
}
|
|
721
|
+
} catch (e) {
|
|
722
|
+
// Couldn't resolve, leave as required
|
|
723
|
+
}
|
|
724
|
+
}
|
|
565
725
|
}
|
|
566
726
|
}
|
|
567
727
|
|
|
568
728
|
// Convert map to object for metadata
|
|
569
729
|
metadata.uniqueVariables = Object.fromEntries(uniqueVariablesMap)
|
|
570
730
|
|
|
731
|
+
// Add resolvedPropertyValue to resolutionTracking if resolvedConfig provided
|
|
732
|
+
if (resolvedConfig) {
|
|
733
|
+
metadata.resolutionHistory = {}
|
|
734
|
+
for (const pathKey in resolutionTracking) {
|
|
735
|
+
const tracking = resolutionTracking[pathKey]
|
|
736
|
+
const keys = pathKey.split('.')
|
|
737
|
+
let resolvedValue = resolvedConfig
|
|
738
|
+
|
|
739
|
+
for (const key of keys) {
|
|
740
|
+
if (resolvedValue && typeof resolvedValue === 'object') {
|
|
741
|
+
resolvedValue = resolvedValue[key]
|
|
742
|
+
} else {
|
|
743
|
+
resolvedValue = undefined
|
|
744
|
+
break
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Unescape any __CONFIGVAR:...__ placeholders in tracking data
|
|
749
|
+
// (resolutionTracking uses escaped config during resolution)
|
|
750
|
+
const cleanTracking = JSON.parse(
|
|
751
|
+
JSON.stringify(tracking).replace(/__CONFIGVAR:([A-Za-z0-9+/=]+)__/g, (_, encoded) => {
|
|
752
|
+
return Buffer.from(encoded, 'base64').toString('utf8')
|
|
753
|
+
})
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
metadata.resolutionHistory[pathKey] = {
|
|
757
|
+
...cleanTracking,
|
|
758
|
+
resolvedPropertyValue: resolvedValue
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
571
763
|
return metadata
|
|
572
764
|
}
|
|
573
765
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const YAML = require('
|
|
2
|
-
const TOML = require('
|
|
3
|
-
const INI = require('
|
|
4
|
-
const JSON5 = require('
|
|
5
|
-
const { executeTypeScriptFileSync } = require('
|
|
6
|
-
const { executeESMFileSync } = require('
|
|
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') {
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preprocesses config to fix malformed fallback references
|
|
3
|
+
* and escape variables inside help() filter arguments
|
|
4
|
+
*/
|
|
5
|
+
const { splitByComma } = require('../strings/splitByComma')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Preprocess config to fix malformed fallback references
|
|
9
|
+
* @param {Object} configObject - The parsed configuration object
|
|
10
|
+
* @param {RegExp} variableSyntax - The variable syntax regex to use
|
|
11
|
+
* @returns {Object} The preprocessed configuration object
|
|
12
|
+
*/
|
|
13
|
+
function preProcess(configObject, variableSyntax) {
|
|
14
|
+
// Known reference prefixes that should be wrapped in ${}
|
|
15
|
+
const refPrefixes = ['self:', 'opt:', 'env:', 'file:', 'text:', 'deep:']
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escape variables inside help() filter arguments so main resolver skips them
|
|
19
|
+
* Uses base64 encoding to preserve exact original syntax (supports custom variable syntax)
|
|
20
|
+
* @param {string} str - String potentially containing help() with variables
|
|
21
|
+
* @returns {string} String with help() variables escaped
|
|
22
|
+
*/
|
|
23
|
+
function escapeHelpVariables(str) {
|
|
24
|
+
if (typeof str !== 'string') return str
|
|
25
|
+
if (!variableSyntax) return str
|
|
26
|
+
|
|
27
|
+
// Match help('...') or help("...") containing variables
|
|
28
|
+
const helpPattern = /help\(['"]([^'"]+)['"]\)/g
|
|
29
|
+
|
|
30
|
+
return str.replace(helpPattern, (match, helpContent) => {
|
|
31
|
+
// Check if help content contains variables
|
|
32
|
+
if (!helpContent.match(variableSyntax)) return match
|
|
33
|
+
|
|
34
|
+
// Replace each variable match with base64-encoded placeholder
|
|
35
|
+
const escaped = helpContent.replace(variableSyntax, (varMatch) => {
|
|
36
|
+
const encoded = Buffer.from(varMatch).toString('base64')
|
|
37
|
+
return `__CONFIGVAR:${encoded}__`
|
|
38
|
+
})
|
|
39
|
+
return `help('${escaped}')`
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fix malformed fallback references in a string
|
|
45
|
+
* @param {string} str - String potentially containing variables
|
|
46
|
+
* @returns {string} String with fixed fallback references
|
|
47
|
+
*/
|
|
48
|
+
function fixFallbacksInString(str) {
|
|
49
|
+
if (typeof str !== 'string') return str
|
|
50
|
+
|
|
51
|
+
let result = str
|
|
52
|
+
let changed = true
|
|
53
|
+
|
|
54
|
+
// Keep iterating until no more changes (to handle nested variables)
|
|
55
|
+
while (changed) {
|
|
56
|
+
changed = false
|
|
57
|
+
|
|
58
|
+
// Find innermost ${...} blocks (ones that don't contain other ${)
|
|
59
|
+
let i = 0
|
|
60
|
+
while (i < result.length) {
|
|
61
|
+
if (result[i] === '$' && result[i + 1] === '{') {
|
|
62
|
+
const start = i
|
|
63
|
+
let braceCount = 1
|
|
64
|
+
let j = i + 2
|
|
65
|
+
|
|
66
|
+
// Find the matching closing brace by counting { and }
|
|
67
|
+
while (j < result.length && braceCount > 0) {
|
|
68
|
+
if (result[j] === '{') {
|
|
69
|
+
braceCount++
|
|
70
|
+
} else if (result[j] === '}') {
|
|
71
|
+
braceCount--
|
|
72
|
+
}
|
|
73
|
+
j++
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (braceCount === 0) {
|
|
77
|
+
const end = j
|
|
78
|
+
const match = result.substring(start, end)
|
|
79
|
+
const content = result.substring(start + 2, end - 1)
|
|
80
|
+
|
|
81
|
+
// Only process if there's a comma (indicating fallback syntax)
|
|
82
|
+
if (content.includes(',')) {
|
|
83
|
+
// Split by comma
|
|
84
|
+
const parts = splitByComma(content, variableSyntax)
|
|
85
|
+
|
|
86
|
+
if (parts.length > 1) {
|
|
87
|
+
// Check if the first part has nested ${} - if so, skip this (process inner ones first)
|
|
88
|
+
const firstPart = parts[0]
|
|
89
|
+
if (firstPart.includes('${')) {
|
|
90
|
+
i = start + 2 // Move past ${ to find inner variables
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check each part after the first (these are fallback values)
|
|
95
|
+
const fixed = parts.map((part, index) => {
|
|
96
|
+
if (index === 0) {
|
|
97
|
+
return part // Keep the main reference as-is
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const trimmed = part.trim()
|
|
101
|
+
|
|
102
|
+
// Check if this looks like a reference but is not wrapped
|
|
103
|
+
const looksLikeRef = refPrefixes.some(prefix => trimmed.startsWith(prefix))
|
|
104
|
+
const alreadyWrapped = trimmed.startsWith('${') && trimmed.endsWith('}')
|
|
105
|
+
|
|
106
|
+
if (looksLikeRef && !alreadyWrapped) {
|
|
107
|
+
return ` \${${trimmed}}`
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return ` ${trimmed}`
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const replacement = `\${${fixed.join(',')}}`
|
|
114
|
+
if (replacement !== match) {
|
|
115
|
+
result = result.substring(0, start) + replacement + result.substring(end)
|
|
116
|
+
changed = true
|
|
117
|
+
break // Restart search from beginning
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
i = start + 2 // Move past ${ to continue searching for nested variables
|
|
123
|
+
} else {
|
|
124
|
+
i++
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
i++
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Recursively traverse and fix the config object
|
|
137
|
+
*/
|
|
138
|
+
function traverseAndFix(obj) {
|
|
139
|
+
if (typeof obj === 'string') {
|
|
140
|
+
// First escape help() variables, then fix fallbacks
|
|
141
|
+
const withHelpEscaped = escapeHelpVariables(obj)
|
|
142
|
+
return fixFallbacksInString(withHelpEscaped)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (Array.isArray(obj)) {
|
|
146
|
+
return obj.map(item => traverseAndFix(item))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (obj !== null && typeof obj === 'object') {
|
|
150
|
+
const result = {}
|
|
151
|
+
for (const key in obj) {
|
|
152
|
+
if (obj.hasOwnProperty(key)) {
|
|
153
|
+
result[key] = traverseAndFix(obj[key])
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return result
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return obj
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return traverseAndFix(configObject)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = preProcess
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Utilities for parsing and normalizing file paths in variable references
|
|
2
2
|
|
|
3
|
-
const { splitCsv } = require('
|
|
3
|
+
const { splitCsv } = require('../strings/splitCsv')
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Normalize a file path (add ./ prefix, fix .//, skip deep refs)
|
|
@@ -45,12 +45,13 @@ function extractFilePath(variableString) {
|
|
|
45
45
|
return null
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
const { trimSurroundingQuotes } = require('../strings/quoteUtils')
|
|
48
49
|
const fileContent = fileMatch[1].trim()
|
|
49
50
|
const parts = splitCsv(fileContent)
|
|
50
51
|
let filePath = parts[0].trim()
|
|
51
52
|
|
|
52
53
|
// Remove quotes if present
|
|
53
|
-
filePath = filePath
|
|
54
|
+
filePath = trimSurroundingQuotes(filePath, false)
|
|
54
55
|
|
|
55
56
|
return { filePath }
|
|
56
57
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds the line number for a given key in config file contents
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Find the line number where a key is defined in config file contents
|
|
7
|
+
* @param {string} keyToFind - The key to search for
|
|
8
|
+
* @param {string[]} lines - Array of file lines
|
|
9
|
+
* @param {string} fileType - File extension (e.g., '.yml', '.json', '.toml')
|
|
10
|
+
* @returns {number} Line number (1-indexed) or 0 if not found
|
|
11
|
+
*/
|
|
12
|
+
function findLineForKey(keyToFind, lines, fileType) {
|
|
13
|
+
if (!keyToFind || !lines || !lines.length) return 0
|
|
14
|
+
|
|
15
|
+
const escapedKey = keyToFind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
16
|
+
|
|
17
|
+
const lineIdx = lines.findIndex((line) => {
|
|
18
|
+
// YAML: key: or key :
|
|
19
|
+
if (fileType === '.yml' || fileType === '.yaml') {
|
|
20
|
+
return new RegExp(`^\\s*${escapedKey}\\s*:`).test(line)
|
|
21
|
+
}
|
|
22
|
+
// TOML: key = or key=
|
|
23
|
+
if (fileType === '.toml') {
|
|
24
|
+
return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
|
|
25
|
+
}
|
|
26
|
+
// JSON: "key": or "key" :
|
|
27
|
+
if (fileType === '.json' || fileType === '.json5') {
|
|
28
|
+
return new RegExp(`"${escapedKey}"\\s*:`).test(line)
|
|
29
|
+
}
|
|
30
|
+
// INI: key = or key=
|
|
31
|
+
if (fileType === '.ini') {
|
|
32
|
+
return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
|
|
33
|
+
}
|
|
34
|
+
// JS/TS/ESM: key: or "key": or 'key': or `key`: or [`key`]:
|
|
35
|
+
if (['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts'].includes(fileType)) {
|
|
36
|
+
return new RegExp(`(?:${escapedKey}|"${escapedKey}"|'${escapedKey}'|\`${escapedKey}\`|\\[\`${escapedKey}\`\\])\\s*:`).test(line)
|
|
37
|
+
}
|
|
38
|
+
// Default fallback: try YAML-style
|
|
39
|
+
return line.includes(`${keyToFind}:`)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return lineIdx !== -1 ? lineIdx + 1 : 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = {
|
|
46
|
+
findLineForKey
|
|
47
|
+
}
|