configorama 0.6.12 → 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.
- 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 +394 -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} +177 -15
- 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} +22 -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
|
}
|
|
@@ -317,11 +346,32 @@ function enrichMetadata(
|
|
|
317
346
|
for (const key of varKeys) {
|
|
318
347
|
const varInstances = metadata.variables[key]
|
|
319
348
|
const firstInstance = varInstances[0]
|
|
320
|
-
|
|
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
|
+
}
|
|
321
371
|
|
|
322
372
|
// Get the base variable name without fallback
|
|
323
373
|
// Use valueBeforeFallback if present, otherwise use the variable string
|
|
324
|
-
let baseVar =
|
|
374
|
+
let baseVar = matchingDetail.valueBeforeFallback || matchingDetail.variable
|
|
325
375
|
|
|
326
376
|
// Strip filters from baseVar using known filter names
|
|
327
377
|
// e.g., "opt:stage | toUpperCase | help(...)" -> "opt:stage"
|
|
@@ -344,7 +394,8 @@ function enrichMetadata(
|
|
|
344
394
|
if (!uniqueVariablesMap.has(baseVar)) {
|
|
345
395
|
uniqueVariablesMap.set(baseVar, {
|
|
346
396
|
variable: baseVar,
|
|
347
|
-
variableType:
|
|
397
|
+
variableType: matchingDetail.variableType,
|
|
398
|
+
variableSourceType: getSourceForType(matchingDetail.variableType, variableTypes),
|
|
348
399
|
occurrences: [],
|
|
349
400
|
innerVariables: [],
|
|
350
401
|
})
|
|
@@ -365,9 +416,25 @@ function enrichMetadata(
|
|
|
365
416
|
// The outermost variable is the last one in resolveDetails
|
|
366
417
|
const outermostDetail = instance.resolveDetails[instance.resolveDetails.length - 1]
|
|
367
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
|
+
|
|
368
426
|
for (let i = 0; i < instance.resolveDetails.length - 1; i++) {
|
|
369
427
|
const detail = instance.resolveDetails[i]
|
|
370
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
|
+
|
|
371
438
|
// Check if this variable is actually INSIDE the outermost variable's boundaries
|
|
372
439
|
// A variable is "inner" only if it's contained within the parent's start/end range
|
|
373
440
|
const isInnerVariable = detail.start >= outermostDetail.start && detail.end <= outermostDetail.end
|
|
@@ -385,6 +452,7 @@ function enrichMetadata(
|
|
|
385
452
|
uniqueVariablesMap.set(normalizedSiblingVar, {
|
|
386
453
|
variable: normalizedSiblingVar,
|
|
387
454
|
variableType: detail.variableType,
|
|
455
|
+
variableSourceType: getSourceForType(detail.variableType, variableTypes),
|
|
388
456
|
occurrences: [],
|
|
389
457
|
innerVariables: [],
|
|
390
458
|
})
|
|
@@ -393,11 +461,29 @@ function enrichMetadata(
|
|
|
393
461
|
const siblingEntry = uniqueVariablesMap.get(normalizedSiblingVar)
|
|
394
462
|
|
|
395
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
|
+
|
|
396
479
|
const siblingOccurrence = createOccurrence(instance, detail.varMatch, {
|
|
397
|
-
isRequired:
|
|
480
|
+
isRequired: siblingIsRequired,
|
|
398
481
|
hasFallback: !!detail.hasFallback,
|
|
399
|
-
defaultValue:
|
|
482
|
+
defaultValue: siblingDefaultValue,
|
|
400
483
|
})
|
|
484
|
+
if (siblingDefaultValueSrc) {
|
|
485
|
+
siblingOccurrence.defaultValueSrc = siblingDefaultValueSrc
|
|
486
|
+
}
|
|
401
487
|
|
|
402
488
|
// Check if this exact occurrence already exists
|
|
403
489
|
const occurrenceExists = siblingEntry.occurrences.some(occ =>
|
|
@@ -547,7 +633,7 @@ function enrichMetadata(
|
|
|
547
633
|
}
|
|
548
634
|
|
|
549
635
|
// Aggregate types and descriptions for each uniqueVariable
|
|
550
|
-
for (const [, entry] of uniqueVariablesMap) {
|
|
636
|
+
for (const [varKey, entry] of uniqueVariablesMap) {
|
|
551
637
|
// Collect unique types from occurrences
|
|
552
638
|
const types = entry.occurrences
|
|
553
639
|
.map(occ => occ.type)
|
|
@@ -556,18 +642,94 @@ function enrichMetadata(
|
|
|
556
642
|
entry.types = types
|
|
557
643
|
}
|
|
558
644
|
|
|
559
|
-
// Collect unique descriptions from occurrences
|
|
560
|
-
const
|
|
645
|
+
// Collect unique descriptions from occurrences and resolve any variables
|
|
646
|
+
const rawDescriptions = entry.occurrences
|
|
561
647
|
.map(occ => occ.description)
|
|
562
648
|
.filter((d, i, a) => d && a.indexOf(d) === i)
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
}
|
|
565
695
|
}
|
|
566
696
|
}
|
|
567
697
|
|
|
568
698
|
// Convert map to object for metadata
|
|
569
699
|
metadata.uniqueVariables = Object.fromEntries(uniqueVariablesMap)
|
|
570
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
|
+
|
|
571
733
|
return metadata
|
|
572
734
|
}
|
|
573
735
|
|
|
@@ -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
|
+
}
|