configorama 0.6.9 → 0.6.10
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/cli.js +57 -28
- package/package.json +4 -2
- package/src/index.js +39 -2
- package/src/main.js +611 -269
- package/src/resolvers/valueFromCron.js +2 -0
- package/src/resolvers/valueFromEnv.js +2 -0
- package/src/resolvers/valueFromEnv.test.js +78 -0
- package/src/resolvers/valueFromEval.js +1 -0
- package/src/resolvers/valueFromGit.js +24 -9
- package/src/resolvers/valueFromNumber.js +1 -0
- package/src/resolvers/valueFromOptions.js +2 -0
- package/src/resolvers/valueFromString.js +1 -0
- package/src/sync.js +13 -4
- package/src/utils/cleanVariable.js +3 -3
- package/src/utils/configWizard.js +567 -0
- package/src/utils/encoders/index.js +15 -0
- package/src/utils/encoders/js-fixes.js +22 -0
- package/src/utils/{unknownValues.js → encoders/unknown-values.js} +10 -1
- package/src/utils/enrichMetadata.js +439 -82
- package/src/utils/find-nested-variables.js +41 -38
- package/src/utils/find-nested-variables.test.js +119 -35
- package/src/utils/getFullFilePath.js +38 -0
- package/src/utils/getVariableType.js +55 -0
- package/src/utils/logs.js +1 -1
- package/src/utils/parse.js +6 -4
- package/src/utils/resolveAlias.js +3 -2
- package/src/utils/splitByComma.js +2 -1
- package/src/utils/splitCsv.js +6 -6
- package/src/utils/resolveAliasOld.js +0 -65
- package/src/utils/x.js +0 -173
|
@@ -1,4 +1,55 @@
|
|
|
1
1
|
const { splitCsv } = require('./splitCsv')
|
|
2
|
+
const dotProp = require('dot-prop')
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a standardized occurrence object
|
|
8
|
+
* @param {object} instance - The variable instance from metadata
|
|
9
|
+
* @param {string} varMatch - The variable match string
|
|
10
|
+
* @param {object} options - Optional override values
|
|
11
|
+
* @returns {object} Standardized occurrence object
|
|
12
|
+
*/
|
|
13
|
+
function createOccurrence(instance, varMatch, options = {}) {
|
|
14
|
+
// Extract help text from filters and separate it
|
|
15
|
+
let filters = instance.filters
|
|
16
|
+
let description = undefined
|
|
17
|
+
|
|
18
|
+
if (filters && Array.isArray(filters)) {
|
|
19
|
+
// Find and extract help() filter
|
|
20
|
+
const helpFilterIndex = filters.findIndex(f => f && f.match(/^help\(/))
|
|
21
|
+
if (helpFilterIndex !== -1) {
|
|
22
|
+
const helpFilter = filters[helpFilterIndex]
|
|
23
|
+
const helpMatch = helpFilter.match(/^help\(['"](.+)['"]\)$/)
|
|
24
|
+
if (helpMatch) {
|
|
25
|
+
description = helpMatch[1]
|
|
26
|
+
// Remove help filter from filters array
|
|
27
|
+
filters = filters.filter((_, i) => i !== helpFilterIndex)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const occurrence = {
|
|
33
|
+
originalString: instance.originalStringValue,
|
|
34
|
+
varMatch: varMatch,
|
|
35
|
+
path: instance.path,
|
|
36
|
+
filters: filters && filters.length > 0 ? filters : undefined,
|
|
37
|
+
defaultValue: options.defaultValue !== undefined ? options.defaultValue : instance.defaultValue,
|
|
38
|
+
isRequired: options.isRequired !== undefined ? options.isRequired : instance.isRequired,
|
|
39
|
+
hasFilters: !!(filters && filters.length > 0),
|
|
40
|
+
hasFallback: options.hasFallback !== undefined ? options.hasFallback : (instance.hasFallback || false),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (description) {
|
|
44
|
+
occurrence.description = description
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (instance.defaultValueSrc) {
|
|
48
|
+
occurrence.defaultValueSrc = instance.defaultValueSrc
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return occurrence
|
|
52
|
+
}
|
|
2
53
|
|
|
3
54
|
/**
|
|
4
55
|
* Extract file path from a file() or text() reference string
|
|
@@ -50,14 +101,26 @@ function normalizePath(filePath) {
|
|
|
50
101
|
return normalized
|
|
51
102
|
}
|
|
52
103
|
|
|
53
|
-
// Enriches variable metadata with resolution tracking data
|
|
54
104
|
/**
|
|
55
|
-
*
|
|
56
|
-
* @param {object}
|
|
57
|
-
* @param {
|
|
58
|
-
* @
|
|
105
|
+
* Enriches variable metadata with resolution tracking data.
|
|
106
|
+
* @param {object} metadata - The metadata object from collectVariableMetadata.
|
|
107
|
+
* @param {object} resolutionTracking - The resolution tracking data from Configorama instance.
|
|
108
|
+
* @param {RegExp} variableSyntax - The variable syntax regex.
|
|
109
|
+
* @param {Array} fileRefsFound - The (incomplete) list of file refs found during resolution.
|
|
110
|
+
* @param {object} originalConfig - The original config object (before resolution) for self/dot.prop lookups.
|
|
111
|
+
* @param {string} configPath - The path to the config file.
|
|
112
|
+
* @param {Array} filterNames - Array of known filter names.
|
|
113
|
+
* @returns {object} Enriched metadata with resolution details and a complete file reference list.
|
|
59
114
|
*/
|
|
60
|
-
function enrichMetadata(
|
|
115
|
+
function enrichMetadata(
|
|
116
|
+
metadata,
|
|
117
|
+
resolutionTracking,
|
|
118
|
+
variableSyntax,
|
|
119
|
+
fileRefsFound = [],
|
|
120
|
+
originalConfig = {},
|
|
121
|
+
configPath,
|
|
122
|
+
filterNames = []
|
|
123
|
+
) {
|
|
61
124
|
if (!resolutionTracking) {
|
|
62
125
|
return metadata
|
|
63
126
|
}
|
|
@@ -71,42 +134,38 @@ function enrichMetadata(metadata, resolutionTracking, variableSyntax) {
|
|
|
71
134
|
const pathKey = varData.path
|
|
72
135
|
const trackingData = resolutionTracking[pathKey]
|
|
73
136
|
|
|
74
|
-
if (trackingData && trackingData.
|
|
75
|
-
//
|
|
76
|
-
const lastCall = trackingData.calls[trackingData.calls.length - 1]
|
|
77
|
-
|
|
78
|
-
// For each resolveDetail, find the matching call and set afterInnerResolution
|
|
137
|
+
if (trackingData && trackingData.resolutionHistory && varData.resolveDetails) {
|
|
138
|
+
// For each resolveDetail, find the matching resolution history entry
|
|
79
139
|
for (let i = 0; i < varData.resolveDetails.length; i++) {
|
|
80
140
|
const detail = varData.resolveDetails[i]
|
|
81
141
|
const isOutermost = i === varData.resolveDetails.length - 1
|
|
82
142
|
|
|
83
|
-
if (isOutermost) {
|
|
84
|
-
// For the outermost variable, use the last
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
afterResolution = afterResolution.slice(2, -1)
|
|
89
|
-
}
|
|
90
|
-
detail.afterInnerResolution = afterResolution
|
|
91
|
-
|
|
92
|
-
if (lastCall.resolvedValue !== undefined) {
|
|
93
|
-
detail.resolvedValue = lastCall.resolvedValue
|
|
143
|
+
if (isOutermost && trackingData.resolutionHistory.length > 0) {
|
|
144
|
+
// For the outermost variable, use the last resolution history entry's result
|
|
145
|
+
const lastEntry = trackingData.resolutionHistory[trackingData.resolutionHistory.length - 1]
|
|
146
|
+
if (lastEntry.result !== undefined) {
|
|
147
|
+
detail.resolvedValue = lastEntry.result
|
|
94
148
|
}
|
|
95
149
|
} else {
|
|
96
|
-
// For inner variables, try to find a matching
|
|
97
|
-
for (const
|
|
98
|
-
const
|
|
150
|
+
// For inner variables, try to find a matching resolution history entry
|
|
151
|
+
for (const historyEntry of trackingData.resolutionHistory) {
|
|
152
|
+
const historyVar = historyEntry.variable
|
|
99
153
|
const detailVar = detail.variable
|
|
100
154
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
155
|
+
if (historyVar === detailVar || historyVar.includes(detail.varString)) {
|
|
156
|
+
if (historyEntry.result !== undefined) {
|
|
157
|
+
// detail.resolvedValue = historyEntry.result
|
|
158
|
+
let resolvedValue = historyEntry.result
|
|
159
|
+
// If result is a deep reference, look for the resolved value
|
|
160
|
+
if (typeof resolvedValue === 'string' && resolvedValue.match(/^\$\{deep:\d+\}$/)) {
|
|
161
|
+
const deepVar = resolvedValue.slice(2, -1) // e.g. "deep:1"
|
|
162
|
+
const deepEntry = trackingData.resolutionHistory.find(e => e.variable === deepVar)
|
|
163
|
+
if (deepEntry && deepEntry.result !== undefined) {
|
|
164
|
+
resolvedValue = deepEntry.result
|
|
165
|
+
historyEntry.resultAfterDeep = resolvedValue
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
detail.resolvedValue = resolvedValue
|
|
110
169
|
}
|
|
111
170
|
break
|
|
112
171
|
}
|
|
@@ -118,19 +177,18 @@ function enrichMetadata(metadata, resolutionTracking, variableSyntax) {
|
|
|
118
177
|
}
|
|
119
178
|
|
|
120
179
|
// Build resolvedFileRefs array from tracking data
|
|
121
|
-
// Only use the LAST call for each path (final resolved state)
|
|
122
180
|
const resolvedFileRefs = []
|
|
123
181
|
const normalizedPaths = new Set()
|
|
124
|
-
|
|
182
|
+
|
|
125
183
|
for (const pathKey in resolutionTracking) {
|
|
126
184
|
const tracking = resolutionTracking[pathKey]
|
|
127
185
|
if (tracking.calls && tracking.calls.length) {
|
|
128
186
|
const lastCall = tracking.calls[tracking.calls.length - 1]
|
|
129
|
-
|
|
187
|
+
|
|
130
188
|
const extracted = extractFilePath(lastCall.propertyString)
|
|
131
189
|
if (extracted) {
|
|
132
190
|
const normalizedPath = normalizePath(extracted.filePath)
|
|
133
|
-
|
|
191
|
+
|
|
134
192
|
if (normalizedPath && !normalizedPaths.has(normalizedPath)) {
|
|
135
193
|
normalizedPaths.add(normalizedPath)
|
|
136
194
|
resolvedFileRefs.push(normalizedPath)
|
|
@@ -139,45 +197,34 @@ function enrichMetadata(metadata, resolutionTracking, variableSyntax) {
|
|
|
139
197
|
}
|
|
140
198
|
}
|
|
141
199
|
|
|
142
|
-
|
|
200
|
+
// Update fileDependencies.resolvedPaths with the resolved file refs
|
|
201
|
+
if (metadata.fileDependencies) {
|
|
202
|
+
metadata.fileDependencies.resolvedPaths = resolvedFileRefs
|
|
203
|
+
}
|
|
143
204
|
|
|
144
|
-
// Build
|
|
205
|
+
// Build references array
|
|
145
206
|
const resolvedFileRefsDataMap = new Map()
|
|
146
|
-
|
|
147
|
-
// First
|
|
207
|
+
|
|
208
|
+
// First Pass: Collect all refs and attach glob patterns directly to each ref.
|
|
148
209
|
for (const pathKey in resolutionTracking) {
|
|
149
210
|
const tracking = resolutionTracking[pathKey]
|
|
150
|
-
if (!tracking.calls || !tracking.calls.length)
|
|
151
|
-
continue
|
|
152
|
-
}
|
|
211
|
+
if (!tracking.calls || !tracking.calls.length) continue
|
|
153
212
|
|
|
154
213
|
const lastCall = tracking.calls[tracking.calls.length - 1]
|
|
155
214
|
const extracted = extractFilePath(lastCall.propertyString)
|
|
156
|
-
|
|
157
|
-
if (!extracted) {
|
|
158
|
-
continue
|
|
159
|
-
}
|
|
215
|
+
if (!extracted) continue
|
|
160
216
|
|
|
161
217
|
const resolvedPath = normalizePath(extracted.filePath)
|
|
162
|
-
if (!resolvedPath)
|
|
163
|
-
continue
|
|
164
|
-
}
|
|
218
|
+
if (!resolvedPath) continue
|
|
165
219
|
|
|
166
|
-
// Get original variable string from tracking
|
|
167
220
|
const originalPropertyString = tracking.originalPropertyString
|
|
168
|
-
if (!originalPropertyString)
|
|
169
|
-
continue
|
|
170
|
-
}
|
|
221
|
+
if (!originalPropertyString) continue
|
|
171
222
|
|
|
172
223
|
const origExtracted = extractFilePath(originalPropertyString)
|
|
173
|
-
if (!origExtracted)
|
|
174
|
-
continue
|
|
175
|
-
}
|
|
224
|
+
if (!origExtracted) continue
|
|
176
225
|
|
|
177
226
|
const origPath = normalizePath(origExtracted.filePath)
|
|
178
|
-
if (!origPath)
|
|
179
|
-
continue
|
|
180
|
-
}
|
|
227
|
+
if (!origPath) continue
|
|
181
228
|
|
|
182
229
|
// Initialize map entry if needed
|
|
183
230
|
if (!resolvedFileRefsDataMap.has(resolvedPath)) {
|
|
@@ -189,41 +236,351 @@ function enrichMetadata(metadata, resolutionTracking, variableSyntax) {
|
|
|
189
236
|
|
|
190
237
|
const entry = resolvedFileRefsDataMap.get(resolvedPath)
|
|
191
238
|
|
|
192
|
-
// Add original variable string with config path if not already present
|
|
193
239
|
const alreadyExists = entry.refs.some(ref => ref.path === pathKey && ref.value === origPath)
|
|
194
240
|
if (!alreadyExists) {
|
|
195
|
-
const refEntry = {
|
|
196
|
-
|
|
241
|
+
const refEntry = {
|
|
242
|
+
location: pathKey,
|
|
243
|
+
value: origPath,
|
|
244
|
+
originalVariableString: originalPropertyString,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for inner variables and generate glob pattern for this specific ref
|
|
197
248
|
if (variableSyntax && origPath.match(variableSyntax)) {
|
|
198
|
-
refEntry.
|
|
249
|
+
refEntry.hasInnerVariable = true
|
|
250
|
+
refEntry.pattern = origPath.replace(variableSyntax, '*')
|
|
199
251
|
}
|
|
252
|
+
|
|
200
253
|
entry.refs.push(refEntry)
|
|
201
254
|
}
|
|
202
255
|
}
|
|
203
256
|
|
|
204
|
-
// Second
|
|
205
|
-
for (const
|
|
206
|
-
const
|
|
207
|
-
|
|
257
|
+
// Second Pass: Aggregate glob patterns for the top-level 'allGlobPatterns' summary.
|
|
258
|
+
for (const data of resolvedFileRefsDataMap.values()) {
|
|
259
|
+
const allGlobs = new Set()
|
|
208
260
|
for (const ref of data.refs) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const globPattern = ref.value.replace(variableSyntax, '*')
|
|
212
|
-
globPatternSet.add(globPattern)
|
|
261
|
+
if (ref.pattern) {
|
|
262
|
+
allGlobs.add(ref.pattern)
|
|
213
263
|
}
|
|
214
264
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
data.globPatterns = patterns
|
|
265
|
+
if (allGlobs.size > 0) {
|
|
266
|
+
data.globPatterns = Array.from(allGlobs)
|
|
218
267
|
}
|
|
219
268
|
}
|
|
220
269
|
|
|
221
|
-
// Convert map to array
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
270
|
+
// Convert map to array for the final metadata object.
|
|
271
|
+
const references = Array.from(resolvedFileRefsDataMap.values())
|
|
272
|
+
|
|
273
|
+
// Build the complete, flat list of all file references
|
|
274
|
+
const fileDetailsMap = new Map()
|
|
275
|
+
for (const fileRef of fileRefsFound) {
|
|
276
|
+
if (!fileDetailsMap.has(fileRef.relativePath)) {
|
|
277
|
+
fileDetailsMap.set(fileRef.relativePath, fileRef)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const byConfigPath = []
|
|
282
|
+
if (references.length > 0) {
|
|
283
|
+
for (const resolvedFileData of references) {
|
|
284
|
+
const details = fileDetailsMap.get(resolvedFileData.resolvedPath)
|
|
285
|
+
if (details) {
|
|
286
|
+
for (const ref of resolvedFileData.refs) {
|
|
287
|
+
const confDetails = {
|
|
288
|
+
location: ref.location,
|
|
289
|
+
filePath: details.filePath,
|
|
290
|
+
relativePath: details.relativePath,
|
|
291
|
+
originalVariableString: ref.originalVariableString,
|
|
292
|
+
resolvedVariableString: details.resolvedVariableString,
|
|
293
|
+
containsVariables: !!ref.hasInnerVariable,
|
|
294
|
+
exists: details.exists,
|
|
295
|
+
// Get glob patterns from the individual ref, default to empty array
|
|
296
|
+
|
|
297
|
+
}
|
|
298
|
+
if (ref.pattern) {
|
|
299
|
+
confDetails.pattern = ref.pattern
|
|
300
|
+
}
|
|
301
|
+
byConfigPath.push(confDetails)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Update fileDependencies with the enriched data
|
|
308
|
+
if (metadata.fileDependencies) {
|
|
309
|
+
metadata.fileDependencies.byConfigPath = byConfigPath
|
|
310
|
+
metadata.fileDependencies.references = references
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Build uniqueVariables rollup - group by base variable (without fallbacks)
|
|
314
|
+
const uniqueVariablesMap = new Map()
|
|
315
|
+
|
|
316
|
+
for (const key of varKeys) {
|
|
317
|
+
const varInstances = metadata.variables[key]
|
|
318
|
+
const firstInstance = varInstances[0]
|
|
319
|
+
const lastResolveDetail = firstInstance.resolveDetails[firstInstance.resolveDetails.length - 1]
|
|
320
|
+
|
|
321
|
+
// Get the base variable name without fallback
|
|
322
|
+
// Use valueBeforeFallback if present, otherwise use the variable string
|
|
323
|
+
let baseVar = lastResolveDetail.valueBeforeFallback || lastResolveDetail.variable
|
|
324
|
+
|
|
325
|
+
// Strip filters from baseVar using known filter names
|
|
326
|
+
// e.g., "opt:stage | toUpperCase | help(...)" -> "opt:stage"
|
|
327
|
+
if (baseVar && baseVar.includes(' |') && filterNames.length > 0) {
|
|
328
|
+
// Build a regex that matches known filters with optional arguments
|
|
329
|
+
// e.g., | filterName or | filterName(...)
|
|
330
|
+
const filterPattern = filterNames.map(name => {
|
|
331
|
+
// Escape special regex chars in filter name
|
|
332
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
333
|
+
return `\\s*\\|\\s*${escaped}(?:\\s*\\([^)]*(?:\\([^)]*\\))?[^)]*\\))?`
|
|
334
|
+
}).join('|')
|
|
335
|
+
|
|
336
|
+
const filterRegex = new RegExp(`(${filterPattern})+\\s*$`)
|
|
337
|
+
baseVar = baseVar.replace(filterRegex, '').trim()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 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
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!uniqueVariablesMap.has(baseVar)) {
|
|
361
|
+
uniqueVariablesMap.set(baseVar, {
|
|
362
|
+
variable: baseVar,
|
|
363
|
+
variableType: lastResolveDetail.variableType,
|
|
364
|
+
occurrences: [],
|
|
365
|
+
innerVariables: [],
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const entry = uniqueVariablesMap.get(baseVar)
|
|
370
|
+
|
|
371
|
+
// Collect inner variables from resolveDetails (variables nested inside this variable)
|
|
372
|
+
const innerVarsSet = new Set((entry.innerVariables || []).map(v => v.variable))
|
|
373
|
+
|
|
374
|
+
for (const instance of varInstances) {
|
|
375
|
+
// Add this occurrence with its full context
|
|
376
|
+
const occurrence = createOccurrence(instance, key)
|
|
377
|
+
entry.occurrences.push(occurrence)
|
|
378
|
+
|
|
379
|
+
// Find inner variables in resolveDetails (excluding the outermost variable itself)
|
|
380
|
+
if (instance.resolveDetails && instance.resolveDetails.length > 1) {
|
|
381
|
+
// The outermost variable is the last one in resolveDetails
|
|
382
|
+
const outermostDetail = instance.resolveDetails[instance.resolveDetails.length - 1]
|
|
383
|
+
|
|
384
|
+
for (let i = 0; i < instance.resolveDetails.length - 1; i++) {
|
|
385
|
+
const detail = instance.resolveDetails[i]
|
|
386
|
+
|
|
387
|
+
// Check if this variable is actually INSIDE the outermost variable's boundaries
|
|
388
|
+
// A variable is "inner" only if it's contained within the parent's start/end range
|
|
389
|
+
const isInnerVariable = detail.start >= outermostDetail.start && detail.end <= outermostDetail.end
|
|
390
|
+
|
|
391
|
+
if (!isInnerVariable) {
|
|
392
|
+
// This is a sibling variable at the same level, not an inner variable
|
|
393
|
+
// But we should still add it as its own uniqueVariables entry
|
|
394
|
+
const siblingBaseVar = detail.valueBeforeFallback || detail.variable
|
|
395
|
+
|
|
396
|
+
// 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
|
+
}
|
|
408
|
+
|
|
409
|
+
// Create or get entry for this sibling variable
|
|
410
|
+
if (!uniqueVariablesMap.has(normalizedSiblingVar)) {
|
|
411
|
+
uniqueVariablesMap.set(normalizedSiblingVar, {
|
|
412
|
+
variable: normalizedSiblingVar,
|
|
413
|
+
variableType: detail.variableType,
|
|
414
|
+
occurrences: [],
|
|
415
|
+
innerVariables: [],
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const siblingEntry = uniqueVariablesMap.get(normalizedSiblingVar)
|
|
420
|
+
|
|
421
|
+
// Add occurrence for this sibling variable
|
|
422
|
+
const siblingOccurrence = createOccurrence(instance, detail.varMatch, {
|
|
423
|
+
isRequired: !detail.hasFallback,
|
|
424
|
+
hasFallback: !!detail.hasFallback,
|
|
425
|
+
defaultValue: detail.hasFallback ? (detail.fallbackValues?.[0]?.stringValue || detail.fallbackValues?.[0]?.variable) : undefined,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// Check if this exact occurrence already exists
|
|
429
|
+
const occurrenceExists = siblingEntry.occurrences.some(occ =>
|
|
430
|
+
occ.varMatch === siblingOccurrence.varMatch &&
|
|
431
|
+
occ.path === siblingOccurrence.path
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if (!occurrenceExists) {
|
|
435
|
+
siblingEntry.occurrences.push(siblingOccurrence)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Get base variable (without fallback)
|
|
442
|
+
const innerBaseVar = detail.valueBeforeFallback || detail.variable
|
|
443
|
+
|
|
444
|
+
if (innerBaseVar && !innerVarsSet.has(innerBaseVar)) {
|
|
445
|
+
innerVarsSet.add(innerBaseVar)
|
|
446
|
+
|
|
447
|
+
let isRequired = !detail.hasFallback
|
|
448
|
+
let defaultValue = detail.hasFallback ? (detail.fallbackValues?.[0]?.stringValue || detail.fallbackValues?.[0]?.variable) : undefined
|
|
449
|
+
let defaultValueSrc
|
|
450
|
+
let hasValue = false
|
|
451
|
+
|
|
452
|
+
// For self: and dot.prop, check if value exists in original config
|
|
453
|
+
if (detail.variableType === 'self' || detail.variableType === 'dot.prop') {
|
|
454
|
+
const cleanPath = innerBaseVar.replace(/^self:/, '')
|
|
455
|
+
const configValue = dotProp.get(originalConfig, cleanPath)
|
|
456
|
+
|
|
457
|
+
if (configValue !== undefined) {
|
|
458
|
+
// Check if the value contains variables
|
|
459
|
+
const hasVariables = variableSyntax && typeof configValue === 'string' && configValue.match(variableSyntax)
|
|
460
|
+
|
|
461
|
+
if (!hasVariables) {
|
|
462
|
+
// Static value exists in config
|
|
463
|
+
hasValue = true
|
|
464
|
+
defaultValue = typeof configValue === 'object' ? JSON.stringify(configValue) : configValue
|
|
465
|
+
defaultValueSrc = cleanPath
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const innerVariable = {
|
|
471
|
+
variable: innerBaseVar,
|
|
472
|
+
variableType: detail.variableType,
|
|
473
|
+
isRequired,
|
|
474
|
+
hasValue,
|
|
475
|
+
defaultValue,
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (defaultValueSrc) {
|
|
479
|
+
innerVariable.defaultValueSrc = defaultValueSrc
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (entry.innerVariables) {
|
|
483
|
+
entry.innerVariables.push(innerVariable)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Remove innerVariables array if empty
|
|
492
|
+
if (entry.innerVariables && entry.innerVariables.length === 0) {
|
|
493
|
+
delete entry.innerVariables
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// If all inner variables have values, resolve them in the variable string
|
|
497
|
+
if (entry.innerVariables && entry.innerVariables.length > 0) {
|
|
498
|
+
const allHaveValues = entry.innerVariables.every(v => v.hasValue)
|
|
499
|
+
|
|
500
|
+
if (allHaveValues) {
|
|
501
|
+
let resolvedVariable = entry.variable
|
|
502
|
+
|
|
503
|
+
// Replace each inner variable with its default value
|
|
504
|
+
for (const innerVar of entry.innerVariables) {
|
|
505
|
+
// Match ${varName} or just varName (for dot.prop shorthand)
|
|
506
|
+
const varPattern = innerVar.variableType === 'self'
|
|
507
|
+
? `\\$\\{self:${innerVar.variable.replace('self:', '')}\\}`
|
|
508
|
+
: `\\$\\{${innerVar.variable}\\}`
|
|
509
|
+
|
|
510
|
+
const regex = new RegExp(varPattern, 'g')
|
|
511
|
+
resolvedVariable = resolvedVariable.replace(regex, innerVar.defaultValue)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// 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
|
+
}
|
|
521
|
+
|
|
522
|
+
// Update the variable to the resolved version and update map key
|
|
523
|
+
if (resolvedVariable !== baseVar) {
|
|
524
|
+
entry.variable = resolvedVariable
|
|
525
|
+
uniqueVariablesMap.delete(baseVar)
|
|
526
|
+
|
|
527
|
+
// Check if the resolved variable already exists in the map (merge if so)
|
|
528
|
+
if (uniqueVariablesMap.has(resolvedVariable)) {
|
|
529
|
+
const existingEntry = uniqueVariablesMap.get(resolvedVariable)
|
|
530
|
+
// Merge occurrences from both entries
|
|
531
|
+
existingEntry.occurrences.push(...entry.occurrences)
|
|
532
|
+
|
|
533
|
+
// Merge innerVariables if both have them
|
|
534
|
+
if (entry.innerVariables && entry.innerVariables.length > 0) {
|
|
535
|
+
if (!existingEntry.innerVariables) {
|
|
536
|
+
existingEntry.innerVariables = []
|
|
537
|
+
}
|
|
538
|
+
// Add unique inner variables only
|
|
539
|
+
for (const innerVar of entry.innerVariables) {
|
|
540
|
+
const exists = existingEntry.innerVariables.some(v => v.variable === innerVar.variable)
|
|
541
|
+
if (!exists) {
|
|
542
|
+
existingEntry.innerVariables.push(innerVar)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
uniqueVariablesMap.set(resolvedVariable, entry)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Check file existence for file/text variables with fully resolved paths
|
|
555
|
+
for (const [varKey, entry] of uniqueVariablesMap) {
|
|
556
|
+
if (entry.variableType === 'file' || entry.variableType === 'text') {
|
|
557
|
+
// Extract file path from variable string like "file(./config.other.json)"
|
|
558
|
+
const filePathMatch = entry.variable.match(/^(?:file|text)\((.+?)\)/)
|
|
559
|
+
if (filePathMatch) {
|
|
560
|
+
const filePath = filePathMatch[1]
|
|
561
|
+
|
|
562
|
+
// Check if the path contains variables (if so, we can't check existence yet)
|
|
563
|
+
const hasVariables = variableSyntax && filePath.match(variableSyntax)
|
|
564
|
+
|
|
565
|
+
if (!hasVariables) {
|
|
566
|
+
// Look up in fileRefsFound to see if file exists
|
|
567
|
+
const fileRef = fileRefsFound.find(ref => ref.relativePath === filePath)
|
|
568
|
+
if (fileRef) {
|
|
569
|
+
entry.fileExists = fileRef.exists
|
|
570
|
+
} else if (configPath) {
|
|
571
|
+
const thePath = path.resolve(path.dirname(configPath), filePath)
|
|
572
|
+
const fileExists = fs.existsSync(thePath)
|
|
573
|
+
entry.fileExists = fileExists
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Convert map to object for metadata
|
|
581
|
+
metadata.uniqueVariables = Object.fromEntries(uniqueVariablesMap)
|
|
225
582
|
|
|
226
583
|
return metadata
|
|
227
584
|
}
|
|
228
585
|
|
|
229
|
-
module.exports = enrichMetadata
|
|
586
|
+
module.exports = enrichMetadata
|