configorama 0.6.9 → 0.6.11

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.
@@ -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
- * @param {object} metadata - The metadata object from collectVariableMetadata
56
- * @param {object} resolutionTracking - The resolution tracking data from Configorama instance
57
- * @param {RegExp} variableSyntax - The variable syntax regex to detect variables in file paths
58
- * @returns {object} Enriched metadata with afterInnerResolution and resolvedFileRefs
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(metadata, resolutionTracking, variableSyntax) {
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.calls && varData.resolveDetails) {
75
- // The last call represents the final state (all inner vars resolved)
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 call's propertyString
85
- // This shows the state after all inner variables have been resolved
86
- let afterResolution = lastCall.propertyString
87
- if (afterResolution.startsWith('${') && afterResolution.endsWith('}')) {
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 call
97
- for (const call of trackingData.calls) {
98
- const callVar = call.variableString
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 (callVar === detailVar || callVar.includes(detail.varString)) {
102
- let afterResolution = call.propertyString
103
- if (afterResolution.startsWith('${') && afterResolution.endsWith('}')) {
104
- afterResolution = afterResolution.slice(2, -1)
105
- }
106
- detail.afterInnerResolution = afterResolution
107
-
108
- if (call.resolvedValue !== undefined) {
109
- detail.resolvedValue = call.resolvedValue
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
- metadata.resolvedFileRefs = resolvedFileRefs
200
+ // Update fileDependencies.resolvedPaths with the resolved file refs
201
+ if (metadata.fileDependencies) {
202
+ metadata.fileDependencies.resolvedPaths = resolvedFileRefs
203
+ }
143
204
 
144
- // Build resolvedFileRefsData array - maps resolved paths to their variable strings and glob patterns
205
+ // Build references array
145
206
  const resolvedFileRefsDataMap = new Map()
146
-
147
- // First pass: collect all resolved paths and their original variable strings
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 = { path: pathKey, value: origPath }
196
- // Check if the value contains variables
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.hasVariable = true
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 pass: generate glob patterns for each resolved path
205
- for (const [resolvedPath, data] of resolvedFileRefsDataMap) {
206
- const globPatternSet = new Set()
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
- // Check if variable path contains variables
210
- if (ref.value.match(variableSyntax)) {
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
- const patterns = Array.from(globPatternSet)
216
- if (patterns.length > 0) {
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 resolvedFileRefsData = Array.from(resolvedFileRefsDataMap.values())
223
-
224
- metadata.resolvedFileRefsData = resolvedFileRefsData
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