configorama 0.6.11 → 0.6.12
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/package.json +1 -1
- package/src/main.js +85 -79
- package/src/utils/configWizard.js +35 -22
- package/src/utils/enrichMetadata.js +78 -90
- package/src/utils/filePathUtils.js +135 -0
- package/src/utils/filePathUtils.test.js +214 -0
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -2,6 +2,7 @@ const os = require('os')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const enrichMetadata = require('./utils/enrichMetadata')
|
|
5
|
+
const { normalizePath, extractFilePath, resolveInnerVariables } = require('./utils/filePathUtils')
|
|
5
6
|
/* // disable logs to find broken tests
|
|
6
7
|
console.log = () => {}
|
|
7
8
|
// process.exit(1)
|
|
@@ -753,6 +754,9 @@ class Configorama {
|
|
|
753
754
|
if (varKeys.length > 0) {
|
|
754
755
|
const fileName = this.configFilePath ? ` in ${this.configFilePath}` : ''
|
|
755
756
|
|
|
757
|
+
// Extract base variable name from varMatch key (e.g., '${env:FOO, default}' -> 'env:FOO')
|
|
758
|
+
const getBaseVarName = (key) => key.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
|
|
759
|
+
|
|
756
760
|
logHeader(`Found ${varKeys.length} Variables${fileName}`)
|
|
757
761
|
|
|
758
762
|
// deepLog('variableData', variableData)
|
|
@@ -765,9 +769,7 @@ class Configorama {
|
|
|
765
769
|
|
|
766
770
|
// Use uniqueVariables for simpler reference counting
|
|
767
771
|
const referenceData = varKeys.map((k) => {
|
|
768
|
-
|
|
769
|
-
// Extract the variable name from the key by removing ${ and }
|
|
770
|
-
const varName = k.replace(/^\$\{/, '').replace(/\}$/, '').split(',')[0].trim()
|
|
772
|
+
const varName = getBaseVarName(k)
|
|
771
773
|
const uniqueVar = uniqueVariables[varName]
|
|
772
774
|
const refCount = uniqueVar ? uniqueVar.occurrences.length : variableData[k].length
|
|
773
775
|
const placesWord = refCount > 1 ? 'places' : 'place'
|
|
@@ -788,7 +790,7 @@ class Configorama {
|
|
|
788
790
|
const firstInstance = variableInstances[0]
|
|
789
791
|
|
|
790
792
|
// Get uniqueVariable data for description and other metadata
|
|
791
|
-
const varName = key
|
|
793
|
+
const varName = getBaseVarName(key)
|
|
792
794
|
const uniqueVar = uniqueVariables[varName]
|
|
793
795
|
|
|
794
796
|
// Build display message from enriched metadata
|
|
@@ -807,38 +809,16 @@ class Configorama {
|
|
|
807
809
|
}
|
|
808
810
|
|
|
809
811
|
// Show type filter if present (Boolean, String, Number, etc.)
|
|
810
|
-
if (uniqueVar && uniqueVar.
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
uniqueVar.occurrences.forEach(occ => {
|
|
815
|
-
if (occ.filters && Array.isArray(occ.filters)) {
|
|
816
|
-
occ.filters.forEach(filter => {
|
|
817
|
-
if (typeFilters.includes(filter)) {
|
|
818
|
-
foundTypes.add(filter)
|
|
819
|
-
}
|
|
820
|
-
})
|
|
821
|
-
}
|
|
822
|
-
})
|
|
823
|
-
|
|
824
|
-
if (foundTypes.size > 0) {
|
|
825
|
-
const typeText = `${indent}${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
|
|
826
|
-
varMsg += `${typeText} ${valueChalk(Array.from(foundTypes).join(', '))}\n`
|
|
827
|
-
}
|
|
812
|
+
if (uniqueVar && uniqueVar.types && uniqueVar.types.length > 0) {
|
|
813
|
+
const typeLabel = `${indent}${keyChalk('Type:'.padEnd(titleText.length, ' '))}`
|
|
814
|
+
varMsg += `${typeLabel} ${valueChalk(uniqueVar.types.join(', '))}\n`
|
|
828
815
|
}
|
|
829
816
|
|
|
830
817
|
// Show description from uniqueVariables if available
|
|
831
|
-
if (uniqueVar && uniqueVar.
|
|
832
|
-
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
.filter((desc, index, self) => desc && self.indexOf(desc) === index)
|
|
836
|
-
|
|
837
|
-
if (descriptions.length > 0) {
|
|
838
|
-
const descText = `${indent}${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
|
|
839
|
-
const combinedDesc = descriptions.join('. ')
|
|
840
|
-
varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
|
|
841
|
-
}
|
|
818
|
+
if (uniqueVar && uniqueVar.descriptions && uniqueVar.descriptions.length > 0) {
|
|
819
|
+
const descText = `${indent}${keyChalk('Description:'.padEnd(titleText.length, ' '))}`
|
|
820
|
+
const combinedDesc = uniqueVar.descriptions.join('. ')
|
|
821
|
+
varMsg += `${descText} ${valueChalk(combinedDesc)}\n`
|
|
842
822
|
}
|
|
843
823
|
|
|
844
824
|
|
|
@@ -873,11 +853,7 @@ class Configorama {
|
|
|
873
853
|
// Show type filter per path if different
|
|
874
854
|
if (uniqueVar && uniqueVar.occurrences.length > 1) {
|
|
875
855
|
const occurrence = uniqueVar.occurrences.find(occ => occ.path === v.path)
|
|
876
|
-
const
|
|
877
|
-
const pathType = occurrence && occurrence.filters
|
|
878
|
-
? occurrence.filters.find(f => typeFilters.includes(f))
|
|
879
|
-
: null
|
|
880
|
-
|
|
856
|
+
const pathType = occurrence && occurrence.type
|
|
881
857
|
typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
|
|
882
858
|
const prefix = idx === 0 ? '' : `${indent}${pathIndent}`
|
|
883
859
|
return `${prefix}${valueChalk(`- ${v.path}`)}${typeText}`
|
|
@@ -888,12 +864,7 @@ class Configorama {
|
|
|
888
864
|
locationRender = pathItems.join('\n')
|
|
889
865
|
locationLabel = `${indent}${keyChalk('Config Paths:'.padEnd(titleText.length, ' '))}`
|
|
890
866
|
} else {
|
|
891
|
-
|
|
892
|
-
const typeFilters = ['Boolean', 'String', 'Number', 'Array', 'Object']
|
|
893
|
-
const pathType = firstInstance.filters
|
|
894
|
-
? firstInstance.filters.find(f => typeFilters.includes(f))
|
|
895
|
-
: null
|
|
896
|
-
|
|
867
|
+
const pathType = firstInstance.type
|
|
897
868
|
typeText = pathType ? ` ${chalk.dim(`Type: ${pathType}`)}` : ''
|
|
898
869
|
}
|
|
899
870
|
varMsg += `${locationLabel} ${locationRender}`
|
|
@@ -1129,10 +1100,15 @@ class Configorama {
|
|
|
1129
1100
|
const variablesKnownTypes = this.variablesKnownTypes
|
|
1130
1101
|
const variableTypes = this.variableTypes
|
|
1131
1102
|
const filterMatch = this.filterMatch
|
|
1103
|
+
const configFilePath = this.configFilePath
|
|
1104
|
+
const originalConfig = this.originalConfig
|
|
1132
1105
|
const foundVariables = []
|
|
1133
1106
|
const variableData = {}
|
|
1134
1107
|
const fileRefs = []
|
|
1135
1108
|
const fileGlobPatterns = []
|
|
1109
|
+
const preResolvedPaths = new Set()
|
|
1110
|
+
const byConfigPath = []
|
|
1111
|
+
const referencesMap = new Map()
|
|
1136
1112
|
let matchCount = 1
|
|
1137
1113
|
|
|
1138
1114
|
traverse(this.originalConfig).forEach(function (rawValue) {
|
|
@@ -1304,46 +1280,75 @@ class Configorama {
|
|
|
1304
1280
|
nested.forEach((detail) => {
|
|
1305
1281
|
// console.log('detail', detail)
|
|
1306
1282
|
if (detail.variableType && (detail.variableType === 'file' || detail.variableType === 'text')) {
|
|
1307
|
-
const
|
|
1308
|
-
if (
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
// Split by comma to separate file path from parameters/fallback values
|
|
1312
|
-
const parts = splitCsv(fileContent)
|
|
1313
|
-
let filePath = parts[0].trim()
|
|
1314
|
-
|
|
1315
|
-
// Remove quotes if present
|
|
1316
|
-
filePath = filePath.replace(/^['"]|['"]$/g, '')
|
|
1317
|
-
|
|
1318
|
-
// Normalize path: ensure relative paths start with ./
|
|
1319
|
-
let normalizedPath = filePath
|
|
1320
|
-
if (
|
|
1321
|
-
!filePath.startsWith('./') &&
|
|
1322
|
-
!filePath.startsWith('../') &&
|
|
1323
|
-
!filePath.startsWith('/') &&
|
|
1324
|
-
!filePath.startsWith('~')
|
|
1325
|
-
) {
|
|
1326
|
-
normalizedPath = './' + filePath
|
|
1327
|
-
}
|
|
1283
|
+
const extracted = extractFilePath(detail.variable)
|
|
1284
|
+
if (extracted) {
|
|
1285
|
+
const normalizedPath = normalizePath(extracted.filePath)
|
|
1286
|
+
if (!normalizedPath) return
|
|
1328
1287
|
|
|
1329
|
-
// file .//
|
|
1330
|
-
if (normalizedPath.startsWith('.//')) {
|
|
1331
|
-
normalizedPath = normalizedPath.replace('.//', './')
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
1288
|
// Handle variables in file paths - just record the pattern
|
|
1335
1289
|
if (!fileRefs.includes(normalizedPath)) {
|
|
1336
1290
|
fileRefs.push(normalizedPath)
|
|
1337
1291
|
}
|
|
1338
|
-
|
|
1292
|
+
|
|
1339
1293
|
// Check if path contains variables and create glob pattern
|
|
1340
|
-
|
|
1294
|
+
const containsVariables = !!normalizedPath.match(variableSyntax)
|
|
1295
|
+
let globPattern
|
|
1296
|
+
if (containsVariables) {
|
|
1341
1297
|
// Replace variable syntax ${...} with * for glob pattern
|
|
1342
|
-
|
|
1298
|
+
globPattern = normalizedPath.replace(variableSyntax, '*')
|
|
1343
1299
|
if (!fileGlobPatterns.includes(globPattern)) {
|
|
1344
1300
|
fileGlobPatterns.push(globPattern)
|
|
1345
1301
|
}
|
|
1346
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
// Try to pre-resolve inner variables from originalConfig
|
|
1305
|
+
let resolvedPath = normalizedPath
|
|
1306
|
+
let resolvedVarString = rawValue
|
|
1307
|
+
if (containsVariables) {
|
|
1308
|
+
const pathResult = resolveInnerVariables(normalizedPath, variableSyntax, originalConfig, dotProp.get)
|
|
1309
|
+
const varStringResult = resolveInnerVariables(rawValue, variableSyntax, originalConfig, dotProp.get)
|
|
1310
|
+
|
|
1311
|
+
if (pathResult.didResolve) {
|
|
1312
|
+
resolvedPath = normalizePath(pathResult.resolved) || pathResult.resolved
|
|
1313
|
+
resolvedVarString = varStringResult.resolved
|
|
1314
|
+
preResolvedPaths.add(resolvedPath)
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// Build byConfigPath entry
|
|
1319
|
+
const absolutePath = configFilePath
|
|
1320
|
+
? path.resolve(path.dirname(configFilePath), resolvedPath)
|
|
1321
|
+
: resolvedPath
|
|
1322
|
+
const fileExists = configFilePath ? fs.existsSync(absolutePath) : false
|
|
1323
|
+
|
|
1324
|
+
const configPathEntry = {
|
|
1325
|
+
location: configValuePath,
|
|
1326
|
+
filePath: absolutePath,
|
|
1327
|
+
relativePath: resolvedPath,
|
|
1328
|
+
originalVariableString: rawValue,
|
|
1329
|
+
resolvedVariableString: resolvedVarString,
|
|
1330
|
+
containsVariables,
|
|
1331
|
+
exists: fileExists,
|
|
1332
|
+
}
|
|
1333
|
+
if (globPattern) {
|
|
1334
|
+
configPathEntry.pattern = globPattern
|
|
1335
|
+
}
|
|
1336
|
+
byConfigPath.push(configPathEntry)
|
|
1337
|
+
|
|
1338
|
+
// Build references entry (use resolvedPath as key when available)
|
|
1339
|
+
const refKey = resolvedPath
|
|
1340
|
+
if (!referencesMap.has(refKey)) {
|
|
1341
|
+
referencesMap.set(refKey, {
|
|
1342
|
+
resolvedPath: refKey,
|
|
1343
|
+
refs: [],
|
|
1344
|
+
})
|
|
1345
|
+
}
|
|
1346
|
+
const refEntry = referencesMap.get(refKey)
|
|
1347
|
+
refEntry.refs.push({
|
|
1348
|
+
location: configValuePath,
|
|
1349
|
+
value: normalizedPath,
|
|
1350
|
+
originalVariableString: rawValue,
|
|
1351
|
+
})
|
|
1347
1352
|
}
|
|
1348
1353
|
}
|
|
1349
1354
|
})
|
|
@@ -1423,12 +1428,13 @@ class Configorama {
|
|
|
1423
1428
|
globPatterns: fileGlobPatterns,
|
|
1424
1429
|
// all: fileRefs,
|
|
1425
1430
|
dynamicPaths: fileRefs.filter(ref => ref.indexOf('*') !== -1 || ref.match(variableSyntax)),
|
|
1426
|
-
//
|
|
1427
|
-
resolvedPaths:
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1431
|
+
// Resolved paths: static paths + pre-resolved dynamic paths
|
|
1432
|
+
resolvedPaths: [
|
|
1433
|
+
...fileRefs.filter(ref => ref.indexOf('*') === -1 && !ref.match(variableSyntax)),
|
|
1434
|
+
...preResolvedPaths
|
|
1435
|
+
],
|
|
1436
|
+
byConfigPath,
|
|
1437
|
+
references: Array.from(referencesMap.values()),
|
|
1432
1438
|
},
|
|
1433
1439
|
summary: {
|
|
1434
1440
|
totalVariables: varKeys.length,
|
|
@@ -223,17 +223,27 @@ function validateType(value, expectedType) {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
/**
|
|
226
|
-
* Extracts type from variable occurrences
|
|
227
|
-
* @param {
|
|
226
|
+
* Extracts type from variable data or occurrences
|
|
227
|
+
* @param {object} varData - Variable data with types array or occurrences
|
|
228
228
|
* @returns {string|null} Expected type or null
|
|
229
229
|
*/
|
|
230
|
-
function getExpectedType(
|
|
231
|
-
|
|
230
|
+
function getExpectedType(varData) {
|
|
231
|
+
// Use pre-computed types if available
|
|
232
|
+
if (varData && varData.types && varData.types.length > 0) {
|
|
233
|
+
return varData.types[0]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Fallback to checking occurrences
|
|
237
|
+
const occurrences = varData && varData.occurrences ? varData.occurrences : varData
|
|
238
|
+
if (!occurrences || !Array.isArray(occurrences) || occurrences.length === 0) return null
|
|
232
239
|
|
|
233
240
|
for (const occ of occurrences) {
|
|
241
|
+
// Check pre-computed type on occurrence
|
|
242
|
+
if (occ.type) return occ.type
|
|
243
|
+
|
|
244
|
+
// Fallback to filters
|
|
234
245
|
if (occ.filters && Array.isArray(occ.filters)) {
|
|
235
246
|
for (const filter of occ.filters) {
|
|
236
|
-
// Check if filter starts with uppercase letter
|
|
237
247
|
if (filter && typeof filter === 'string' && /^[A-Z]/.test(filter)) {
|
|
238
248
|
return filter
|
|
239
249
|
}
|
|
@@ -244,23 +254,28 @@ function getExpectedType(occurrences) {
|
|
|
244
254
|
}
|
|
245
255
|
|
|
246
256
|
/**
|
|
247
|
-
* Extracts help text from variable occurrences
|
|
248
|
-
* @param {
|
|
257
|
+
* Extracts help text from variable data or occurrences
|
|
258
|
+
* @param {object} varData - Variable data with descriptions array or occurrences
|
|
249
259
|
* @returns {string|null} Help text or null
|
|
250
260
|
*/
|
|
251
|
-
function getHelpText(
|
|
252
|
-
|
|
261
|
+
function getHelpText(varData) {
|
|
262
|
+
// Use pre-computed descriptions if available
|
|
263
|
+
if (varData && varData.descriptions && varData.descriptions.length > 0) {
|
|
264
|
+
return varData.descriptions.join('. ')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Fallback to checking occurrences
|
|
268
|
+
const occurrences = varData && varData.occurrences ? varData.occurrences : varData
|
|
269
|
+
if (!occurrences || !Array.isArray(occurrences) || occurrences.length === 0) return null
|
|
253
270
|
|
|
254
271
|
for (const occ of occurrences) {
|
|
255
|
-
// Check for description field first (preferred)
|
|
256
272
|
if (occ.description) {
|
|
257
273
|
return occ.description
|
|
258
274
|
}
|
|
259
275
|
|
|
260
|
-
// Fallback to checking filters array
|
|
276
|
+
// Fallback to checking filters array
|
|
261
277
|
if (occ.filters && Array.isArray(occ.filters)) {
|
|
262
278
|
for (const filter of occ.filters) {
|
|
263
|
-
// Check if filter has help() syntax
|
|
264
279
|
const helpMatch = filter.match(/^help\(['"](.+)['"]\)$/)
|
|
265
280
|
if (helpMatch) {
|
|
266
281
|
return helpMatch[1]
|
|
@@ -290,22 +305,20 @@ function createPromptMessage(varInfo) {
|
|
|
290
305
|
typeLabel = 'Value'
|
|
291
306
|
}
|
|
292
307
|
|
|
293
|
-
// Check for type
|
|
294
|
-
const expectedType = getExpectedType(
|
|
308
|
+
// Check for type - use pre-computed if available
|
|
309
|
+
const expectedType = getExpectedType(varInfo)
|
|
295
310
|
|
|
296
311
|
// Append type to label if found
|
|
297
312
|
if (expectedType) {
|
|
298
313
|
typeLabel = `${typeLabel}:${expectedType}`
|
|
299
314
|
}
|
|
300
315
|
|
|
301
|
-
//
|
|
302
|
-
|
|
303
|
-
if (occurrences && occurrences.length > 0) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
})
|
|
316
|
+
// Use pre-computed descriptions if available, otherwise collect from occurrences
|
|
317
|
+
let descriptions = varInfo.descriptions || []
|
|
318
|
+
if (descriptions.length === 0 && occurrences && occurrences.length > 0) {
|
|
319
|
+
descriptions = occurrences
|
|
320
|
+
.map(occ => occ.description)
|
|
321
|
+
.filter((d, i, a) => d && a.indexOf(d) === i)
|
|
309
322
|
}
|
|
310
323
|
|
|
311
324
|
// Build context from all occurrences
|
|
@@ -1,7 +1,36 @@
|
|
|
1
|
-
const { splitCsv } = require('./splitCsv')
|
|
2
1
|
const dotProp = require('dot-prop')
|
|
3
2
|
const fs = require('fs')
|
|
4
3
|
const path = require('path')
|
|
4
|
+
const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVariables } = require('./filePathUtils')
|
|
5
|
+
|
|
6
|
+
// Type filters that indicate expected value types
|
|
7
|
+
const TYPE_FILTERS = ['Boolean', 'String', 'Number', 'Array', 'Object', 'Json']
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract type filter from filters array
|
|
11
|
+
* @param {Array} filters - Filters array from variable instance
|
|
12
|
+
* @returns {string|undefined} Type filter if found
|
|
13
|
+
*/
|
|
14
|
+
function extractTypeFromFilters(filters) {
|
|
15
|
+
if (!filters || !Array.isArray(filters)) return undefined
|
|
16
|
+
return filters.find(f => TYPE_FILTERS.includes(f))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract description from filters array (help filter)
|
|
21
|
+
* @param {Array} filters - Filters array from variable instance
|
|
22
|
+
* @returns {string|undefined} Description if found
|
|
23
|
+
*/
|
|
24
|
+
function extractDescriptionFromFilters(filters) {
|
|
25
|
+
if (!filters || !Array.isArray(filters)) return undefined
|
|
26
|
+
for (const filter of filters) {
|
|
27
|
+
if (filter && typeof filter === 'string') {
|
|
28
|
+
const helpMatch = filter.match(/^help\(['"](.+)['"]\)$/)
|
|
29
|
+
if (helpMatch) return helpMatch[1]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
5
34
|
|
|
6
35
|
/**
|
|
7
36
|
* Create a standardized occurrence object
|
|
@@ -29,6 +58,9 @@ function createOccurrence(instance, varMatch, options = {}) {
|
|
|
29
58
|
}
|
|
30
59
|
}
|
|
31
60
|
|
|
61
|
+
// Extract type from filters
|
|
62
|
+
const type = extractTypeFromFilters(filters)
|
|
63
|
+
|
|
32
64
|
const occurrence = {
|
|
33
65
|
originalString: instance.originalStringValue,
|
|
34
66
|
varMatch: varMatch,
|
|
@@ -40,6 +72,10 @@ function createOccurrence(instance, varMatch, options = {}) {
|
|
|
40
72
|
hasFallback: options.hasFallback !== undefined ? options.hasFallback : (instance.hasFallback || false),
|
|
41
73
|
}
|
|
42
74
|
|
|
75
|
+
if (type) {
|
|
76
|
+
occurrence.type = type
|
|
77
|
+
}
|
|
78
|
+
|
|
43
79
|
if (description) {
|
|
44
80
|
occurrence.description = description
|
|
45
81
|
}
|
|
@@ -51,56 +87,6 @@ function createOccurrence(instance, varMatch, options = {}) {
|
|
|
51
87
|
return occurrence
|
|
52
88
|
}
|
|
53
89
|
|
|
54
|
-
/**
|
|
55
|
-
* Extract file path from a file() or text() reference string
|
|
56
|
-
* @param {string} propertyString - The property string containing file/text reference
|
|
57
|
-
* @returns {object|null} Object with filePath, or null if no match
|
|
58
|
-
*/
|
|
59
|
-
function extractFilePath(propertyString) {
|
|
60
|
-
const fileMatch = propertyString.match(/^\$\{(?:file|text)\((.*?)\)/)
|
|
61
|
-
if (!fileMatch || !fileMatch[1]) {
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const fileContent = fileMatch[1].trim()
|
|
66
|
-
const parts = splitCsv(fileContent)
|
|
67
|
-
let filePath = parts[0].trim()
|
|
68
|
-
|
|
69
|
-
// Remove quotes if present
|
|
70
|
-
filePath = filePath.replace(/^['"]|['"]$/g, '')
|
|
71
|
-
|
|
72
|
-
return { filePath }
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Normalize a file path (add ./ prefix, fix .//, skip deep refs)
|
|
77
|
-
* @param {string} filePath - The file path to normalize
|
|
78
|
-
* @returns {string|null} Normalized path, or null if should be skipped
|
|
79
|
-
*/
|
|
80
|
-
function normalizePath(filePath) {
|
|
81
|
-
// Skip deep references
|
|
82
|
-
if (filePath.includes('deep:')) {
|
|
83
|
-
return null
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
let normalized = filePath
|
|
87
|
-
|
|
88
|
-
// Add ./ prefix for relative paths
|
|
89
|
-
if (!filePath.startsWith('./') &&
|
|
90
|
-
!filePath.startsWith('../') &&
|
|
91
|
-
!filePath.startsWith('/') &&
|
|
92
|
-
!filePath.startsWith('~')) {
|
|
93
|
-
normalized = './' + filePath
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Fix double slashes
|
|
97
|
-
if (normalized.startsWith('.//')) {
|
|
98
|
-
normalized = normalized.replace('.//', './')
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return normalized
|
|
102
|
-
}
|
|
103
|
-
|
|
104
90
|
/**
|
|
105
91
|
* Enriches variable metadata with resolution tracking data.
|
|
106
92
|
* @param {object} metadata - The metadata object from collectVariableMetadata.
|
|
@@ -173,6 +159,16 @@ function enrichMetadata(
|
|
|
173
159
|
}
|
|
174
160
|
}
|
|
175
161
|
}
|
|
162
|
+
|
|
163
|
+
// Add type and description to individual variable instance
|
|
164
|
+
const instanceType = extractTypeFromFilters(varData.filters)
|
|
165
|
+
const instanceDescription = extractDescriptionFromFilters(varData.filters)
|
|
166
|
+
if (instanceType) {
|
|
167
|
+
varData.type = instanceType
|
|
168
|
+
}
|
|
169
|
+
if (instanceDescription) {
|
|
170
|
+
varData.description = instanceDescription
|
|
171
|
+
}
|
|
176
172
|
}
|
|
177
173
|
}
|
|
178
174
|
|
|
@@ -198,7 +194,8 @@ function enrichMetadata(
|
|
|
198
194
|
}
|
|
199
195
|
|
|
200
196
|
// Update fileDependencies.resolvedPaths with the resolved file refs
|
|
201
|
-
if
|
|
197
|
+
// Only overwrite if we have enriched data, otherwise keep original from collectVariableMetadata
|
|
198
|
+
if (metadata.fileDependencies && resolvedFileRefs.length > 0) {
|
|
202
199
|
metadata.fileDependencies.resolvedPaths = resolvedFileRefs
|
|
203
200
|
}
|
|
204
201
|
|
|
@@ -304,10 +301,14 @@ function enrichMetadata(
|
|
|
304
301
|
}
|
|
305
302
|
}
|
|
306
303
|
|
|
307
|
-
// Update fileDependencies with the enriched data
|
|
304
|
+
// Update fileDependencies with the enriched data (only if we have data)
|
|
308
305
|
if (metadata.fileDependencies) {
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
if (byConfigPath.length > 0) {
|
|
307
|
+
metadata.fileDependencies.byConfigPath = byConfigPath
|
|
308
|
+
}
|
|
309
|
+
if (references.length > 0) {
|
|
310
|
+
metadata.fileDependencies.references = references
|
|
311
|
+
}
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
// Build uniqueVariables rollup - group by base variable (without fallbacks)
|
|
@@ -338,24 +339,7 @@ function enrichMetadata(
|
|
|
338
339
|
}
|
|
339
340
|
|
|
340
341
|
// Normalize file() and text() references
|
|
341
|
-
|
|
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
|
-
}
|
|
342
|
+
baseVar = normalizeFileVariable(baseVar)
|
|
359
343
|
|
|
360
344
|
if (!uniqueVariablesMap.has(baseVar)) {
|
|
361
345
|
uniqueVariablesMap.set(baseVar, {
|
|
@@ -394,17 +378,7 @@ function enrichMetadata(
|
|
|
394
378
|
const siblingBaseVar = detail.valueBeforeFallback || detail.variable
|
|
395
379
|
|
|
396
380
|
// Normalize file/text references for sibling too
|
|
397
|
-
|
|
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
|
-
}
|
|
381
|
+
const normalizedSiblingVar = normalizeFileVariable(siblingBaseVar)
|
|
408
382
|
|
|
409
383
|
// Create or get entry for this sibling variable
|
|
410
384
|
if (!uniqueVariablesMap.has(normalizedSiblingVar)) {
|
|
@@ -512,12 +486,7 @@ function enrichMetadata(
|
|
|
512
486
|
}
|
|
513
487
|
|
|
514
488
|
// Normalize file paths after variable substitution
|
|
515
|
-
|
|
516
|
-
resolvedVariable = resolvedVariable.replace(/^(file|text)\((.+?)\)/, (match, funcName, filePath) => {
|
|
517
|
-
const normalized = normalizePath(filePath)
|
|
518
|
-
return normalized ? `${funcName}(${normalized})` : match
|
|
519
|
-
})
|
|
520
|
-
}
|
|
489
|
+
resolvedVariable = normalizeFileVariable(resolvedVariable)
|
|
521
490
|
|
|
522
491
|
// Update the variable to the resolved version and update map key
|
|
523
492
|
if (resolvedVariable !== baseVar) {
|
|
@@ -577,6 +546,25 @@ function enrichMetadata(
|
|
|
577
546
|
}
|
|
578
547
|
}
|
|
579
548
|
|
|
549
|
+
// Aggregate types and descriptions for each uniqueVariable
|
|
550
|
+
for (const [, entry] of uniqueVariablesMap) {
|
|
551
|
+
// Collect unique types from occurrences
|
|
552
|
+
const types = entry.occurrences
|
|
553
|
+
.map(occ => occ.type)
|
|
554
|
+
.filter((t, i, a) => t && a.indexOf(t) === i)
|
|
555
|
+
if (types.length > 0) {
|
|
556
|
+
entry.types = types
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Collect unique descriptions from occurrences
|
|
560
|
+
const descriptions = entry.occurrences
|
|
561
|
+
.map(occ => occ.description)
|
|
562
|
+
.filter((d, i, a) => d && a.indexOf(d) === i)
|
|
563
|
+
if (descriptions.length > 0) {
|
|
564
|
+
entry.descriptions = descriptions
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
580
568
|
// Convert map to object for metadata
|
|
581
569
|
metadata.uniqueVariables = Object.fromEntries(uniqueVariablesMap)
|
|
582
570
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Utilities for parsing and normalizing file paths in variable references
|
|
2
|
+
|
|
3
|
+
const { splitCsv } = require('./splitCsv')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a file path (add ./ prefix, fix .//, skip deep refs)
|
|
7
|
+
* @param {string} filePath - The file path to normalize
|
|
8
|
+
* @returns {string|null} Normalized path, or null if should be skipped
|
|
9
|
+
*/
|
|
10
|
+
function normalizePath(filePath) {
|
|
11
|
+
// Skip deep references
|
|
12
|
+
if (filePath.includes('deep:')) {
|
|
13
|
+
return null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let normalized = filePath
|
|
17
|
+
|
|
18
|
+
// Add ./ prefix for relative paths
|
|
19
|
+
if (
|
|
20
|
+
!filePath.startsWith('./') &&
|
|
21
|
+
!filePath.startsWith('../') &&
|
|
22
|
+
!filePath.startsWith('/') &&
|
|
23
|
+
!filePath.startsWith('~')
|
|
24
|
+
) {
|
|
25
|
+
normalized = './' + filePath
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fix double slashes
|
|
29
|
+
if (normalized.startsWith('.//')) {
|
|
30
|
+
normalized = normalized.replace('.//', './')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return normalized
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract file path from a file() or text() variable string
|
|
38
|
+
* @param {string} variableString - The variable string (with or without ${} wrapper)
|
|
39
|
+
* @returns {object|null} Object with filePath, or null if no match
|
|
40
|
+
*/
|
|
41
|
+
function extractFilePath(variableString) {
|
|
42
|
+
// Match both ${file(...)} and file(...) formats
|
|
43
|
+
const fileMatch = variableString.match(/^(?:\$\{)?(?:file|text)\((.*?)\)/)
|
|
44
|
+
if (!fileMatch || !fileMatch[1]) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fileContent = fileMatch[1].trim()
|
|
49
|
+
const parts = splitCsv(fileContent)
|
|
50
|
+
let filePath = parts[0].trim()
|
|
51
|
+
|
|
52
|
+
// Remove quotes if present
|
|
53
|
+
filePath = filePath.replace(/^['"]|['"]$/g, '')
|
|
54
|
+
|
|
55
|
+
return { filePath }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Normalize a file() or text() variable string
|
|
60
|
+
* Strips key accessors and normalizes the path inside
|
|
61
|
+
* @param {string} variableString - e.g. "file('./config.json'):key" or "file(config.json)"
|
|
62
|
+
* @returns {string} Normalized variable string, e.g. "file(./config.json)"
|
|
63
|
+
*/
|
|
64
|
+
function normalizeFileVariable(variableString) {
|
|
65
|
+
if (!variableString.match(/^(?:file|text)\(/)) {
|
|
66
|
+
return variableString
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Strip sub-key accessors like :topLevel, :nested.value, etc.
|
|
70
|
+
let normalized = variableString.replace(/:[\w.[\]]+$/, '')
|
|
71
|
+
|
|
72
|
+
// Normalize the path inside
|
|
73
|
+
normalized = normalized.replace(/^(file|text)\((.+?)\)/, (match, funcName, filePath) => {
|
|
74
|
+
let cleanPath = filePath.trim().replace(/^["']|["']$/g, '')
|
|
75
|
+
const normalizedPath = normalizePath(cleanPath)
|
|
76
|
+
return normalizedPath ? `${funcName}(${normalizedPath})` : match
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return normalized
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolve inner variables in a string from config values
|
|
84
|
+
* @param {string} str - String containing variables like ${self:stage}
|
|
85
|
+
* @param {RegExp} variableSyntax - Regex to match variable syntax
|
|
86
|
+
* @param {object} config - Config object to look up values
|
|
87
|
+
* @param {function} getProp - Function to get nested property (e.g. dotProp.get)
|
|
88
|
+
* @returns {{resolved: string, didResolve: boolean}} Resolved string and whether resolution happened
|
|
89
|
+
*/
|
|
90
|
+
function resolveInnerVariables(str, variableSyntax, config, getProp) {
|
|
91
|
+
const varMatches = str.match(variableSyntax)
|
|
92
|
+
if (!varMatches) {
|
|
93
|
+
return { resolved: str, didResolve: false }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let canResolve = true
|
|
97
|
+
let resolved = str
|
|
98
|
+
for (const varMatch of varMatches) {
|
|
99
|
+
const innerVar = varMatch.slice(2, -1) // Remove ${ and }
|
|
100
|
+
let configPath = null
|
|
101
|
+
|
|
102
|
+
// Handle self: prefix
|
|
103
|
+
if (innerVar.startsWith('self:')) {
|
|
104
|
+
configPath = innerVar.slice(5)
|
|
105
|
+
} else if (!innerVar.includes(':')) {
|
|
106
|
+
// dot.prop style
|
|
107
|
+
configPath = innerVar
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (configPath) {
|
|
111
|
+
const configValue = getProp(config, configPath)
|
|
112
|
+
// Only use if it's a static value (not another variable)
|
|
113
|
+
if (configValue !== undefined &&
|
|
114
|
+
typeof configValue === 'string' &&
|
|
115
|
+
!configValue.match(variableSyntax)) {
|
|
116
|
+
resolved = resolved.replace(varMatch, configValue)
|
|
117
|
+
} else {
|
|
118
|
+
canResolve = false
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
canResolve = false
|
|
123
|
+
break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { resolved: canResolve ? resolved : str, didResolve: canResolve }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
normalizePath,
|
|
132
|
+
extractFilePath,
|
|
133
|
+
normalizeFileVariable,
|
|
134
|
+
resolveInnerVariables,
|
|
135
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// Tests for file path parsing and normalization utilities
|
|
2
|
+
|
|
3
|
+
const { test } = require('uvu')
|
|
4
|
+
const assert = require('uvu/assert')
|
|
5
|
+
const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVariables } = require('./filePathUtils')
|
|
6
|
+
|
|
7
|
+
// normalizePath tests
|
|
8
|
+
|
|
9
|
+
test('normalizePath - returns null for deep: references', () => {
|
|
10
|
+
assert.is(normalizePath('deep:1'), null)
|
|
11
|
+
assert.is(normalizePath('some/deep:path'), null)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('normalizePath - adds ./ prefix to bare paths', () => {
|
|
15
|
+
assert.is(normalizePath('config.json'), './config.json')
|
|
16
|
+
assert.is(normalizePath('path/to/file.yml'), './path/to/file.yml')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('normalizePath - keeps ./ paths unchanged', () => {
|
|
20
|
+
assert.is(normalizePath('./config.json'), './config.json')
|
|
21
|
+
assert.is(normalizePath('./path/to/file.yml'), './path/to/file.yml')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('normalizePath - keeps ../ paths unchanged', () => {
|
|
25
|
+
assert.is(normalizePath('../config.json'), '../config.json')
|
|
26
|
+
assert.is(normalizePath('../../path/to/file.yml'), '../../path/to/file.yml')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('normalizePath - keeps absolute paths unchanged', () => {
|
|
30
|
+
assert.is(normalizePath('/etc/config.json'), '/etc/config.json')
|
|
31
|
+
assert.is(normalizePath('/home/user/file.yml'), '/home/user/file.yml')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('normalizePath - keeps ~ paths unchanged', () => {
|
|
35
|
+
assert.is(normalizePath('~/config.json'), '~/config.json')
|
|
36
|
+
assert.is(normalizePath('~/.config/file.yml'), '~/.config/file.yml')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('normalizePath - fixes .// to ./', () => {
|
|
40
|
+
assert.is(normalizePath('.//config.json'), './config.json')
|
|
41
|
+
assert.is(normalizePath('.//path/to/file.yml'), './path/to/file.yml')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// extractFilePath tests
|
|
45
|
+
|
|
46
|
+
test('extractFilePath - extracts path from file(...) format', () => {
|
|
47
|
+
const result = extractFilePath('file(./config.json)')
|
|
48
|
+
assert.is(result.filePath, './config.json')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('extractFilePath - extracts path from ${file(...)} format', () => {
|
|
52
|
+
const result = extractFilePath('${file(./config.json)}')
|
|
53
|
+
assert.is(result.filePath, './config.json')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('extractFilePath - extracts path from text(...) format', () => {
|
|
57
|
+
const result = extractFilePath('text(./readme.txt)')
|
|
58
|
+
assert.is(result.filePath, './readme.txt')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('extractFilePath - extracts path from ${text(...)} format', () => {
|
|
62
|
+
const result = extractFilePath('${text(./readme.txt)}')
|
|
63
|
+
assert.is(result.filePath, './readme.txt')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('extractFilePath - handles single-quoted paths', () => {
|
|
67
|
+
const result = extractFilePath("file('./config.json')")
|
|
68
|
+
assert.is(result.filePath, './config.json')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('extractFilePath - handles double-quoted paths', () => {
|
|
72
|
+
const result = extractFilePath('file("./config.json")')
|
|
73
|
+
assert.is(result.filePath, './config.json')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('extractFilePath - extracts first path when fallback present', () => {
|
|
77
|
+
const result = extractFilePath("file(./env.yml, 'default')")
|
|
78
|
+
assert.is(result.filePath, './env.yml')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('extractFilePath - handles path with key accessor', () => {
|
|
82
|
+
const result = extractFilePath('file(./env.yml):FOO')
|
|
83
|
+
assert.is(result.filePath, './env.yml')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('extractFilePath - handles complex fallback with key accessor', () => {
|
|
87
|
+
const result = extractFilePath("file(./env.yml):SECRET, 'default-value'")
|
|
88
|
+
assert.is(result.filePath, './env.yml')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('extractFilePath - returns null for non-file patterns', () => {
|
|
92
|
+
assert.is(extractFilePath('opt:stage'), null)
|
|
93
|
+
assert.is(extractFilePath('${self:provider.stage}'), null)
|
|
94
|
+
assert.is(extractFilePath('env:FOO'), null)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('extractFilePath - returns null for empty input', () => {
|
|
98
|
+
assert.is(extractFilePath(''), null)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('extractFilePath - handles bare filename', () => {
|
|
102
|
+
const result = extractFilePath('file(config.json)')
|
|
103
|
+
assert.is(result.filePath, 'config.json')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// normalizeFileVariable tests
|
|
107
|
+
|
|
108
|
+
test('normalizeFileVariable - returns non-file strings unchanged', () => {
|
|
109
|
+
assert.is(normalizeFileVariable('opt:stage'), 'opt:stage')
|
|
110
|
+
assert.is(normalizeFileVariable('self:provider.stage'), 'self:provider.stage')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('normalizeFileVariable - normalizes bare path in file()', () => {
|
|
114
|
+
assert.is(normalizeFileVariable('file(config.json)'), 'file(./config.json)')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('normalizeFileVariable - normalizes bare path in text()', () => {
|
|
118
|
+
assert.is(normalizeFileVariable('text(readme.txt)'), 'text(./readme.txt)')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('normalizeFileVariable - strips key accessor', () => {
|
|
122
|
+
assert.is(normalizeFileVariable('file(./env.yml):FOO'), 'file(./env.yml)')
|
|
123
|
+
assert.is(normalizeFileVariable('file(./config.json):nested.value'), 'file(./config.json)')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('normalizeFileVariable - strips key accessor with array notation', () => {
|
|
127
|
+
assert.is(normalizeFileVariable('file(./data.json):items[0]'), 'file(./data.json)')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('normalizeFileVariable - removes quotes from path', () => {
|
|
131
|
+
assert.is(normalizeFileVariable("file('./config.json')"), 'file(./config.json)')
|
|
132
|
+
assert.is(normalizeFileVariable('file("./config.json")'), 'file(./config.json)')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('normalizeFileVariable - handles combined normalization', () => {
|
|
136
|
+
assert.is(normalizeFileVariable("file('config.json'):key"), 'file(./config.json)')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('normalizeFileVariable - keeps ./ paths unchanged', () => {
|
|
140
|
+
assert.is(normalizeFileVariable('file(./already-normalized.yml)'), 'file(./already-normalized.yml)')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('normalizeFileVariable - keeps ../ paths unchanged', () => {
|
|
144
|
+
assert.is(normalizeFileVariable('file(../parent/config.yml)'), 'file(../parent/config.yml)')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// resolveInnerVariables tests
|
|
148
|
+
|
|
149
|
+
const variableSyntax = /\$\{([^}]+)\}/g
|
|
150
|
+
const getProp = (obj, path) => path.split('.').reduce((o, k) => o && o[k], obj)
|
|
151
|
+
|
|
152
|
+
test('resolveInnerVariables - returns unchanged when no variables', () => {
|
|
153
|
+
const result = resolveInnerVariables('./config.json', variableSyntax, {}, getProp)
|
|
154
|
+
assert.is(result.resolved, './config.json')
|
|
155
|
+
assert.is(result.didResolve, false)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('resolveInnerVariables - resolves self: variables from config', () => {
|
|
159
|
+
const config = { stage: 'prod' }
|
|
160
|
+
const result = resolveInnerVariables('./database-${self:stage}.json', variableSyntax, config, getProp)
|
|
161
|
+
assert.is(result.resolved, './database-prod.json')
|
|
162
|
+
assert.is(result.didResolve, true)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('resolveInnerVariables - resolves dot.prop style variables', () => {
|
|
166
|
+
const config = { stage: 'dev' }
|
|
167
|
+
const result = resolveInnerVariables('./database-${stage}.json', variableSyntax, config, getProp)
|
|
168
|
+
assert.is(result.resolved, './database-dev.json')
|
|
169
|
+
assert.is(result.didResolve, true)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('resolveInnerVariables - resolves nested config paths', () => {
|
|
173
|
+
const config = { provider: { stage: 'test' } }
|
|
174
|
+
const result = resolveInnerVariables('./db-${self:provider.stage}.json', variableSyntax, config, getProp)
|
|
175
|
+
assert.is(result.resolved, './db-test.json')
|
|
176
|
+
assert.is(result.didResolve, true)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('resolveInnerVariables - resolves multiple variables', () => {
|
|
180
|
+
const config = { stage: 'prod', region: 'us-east-1' }
|
|
181
|
+
const result = resolveInnerVariables('./config-${self:stage}-${self:region}.json', variableSyntax, config, getProp)
|
|
182
|
+
assert.is(result.resolved, './config-prod-us-east-1.json')
|
|
183
|
+
assert.is(result.didResolve, true)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('resolveInnerVariables - does not resolve if config value is a variable', () => {
|
|
187
|
+
const config = { stage: '${opt:stage}' }
|
|
188
|
+
const result = resolveInnerVariables('./database-${self:stage}.json', variableSyntax, config, getProp)
|
|
189
|
+
assert.is(result.resolved, './database-${self:stage}.json')
|
|
190
|
+
assert.is(result.didResolve, false)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('resolveInnerVariables - does not resolve if config value is undefined', () => {
|
|
194
|
+
const config = {}
|
|
195
|
+
const result = resolveInnerVariables('./database-${self:stage}.json', variableSyntax, config, getProp)
|
|
196
|
+
assert.is(result.resolved, './database-${self:stage}.json')
|
|
197
|
+
assert.is(result.didResolve, false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('resolveInnerVariables - does not resolve env: or opt: variables', () => {
|
|
201
|
+
const config = { stage: 'prod' }
|
|
202
|
+
const result = resolveInnerVariables('./database-${env:STAGE}.json', variableSyntax, config, getProp)
|
|
203
|
+
assert.is(result.resolved, './database-${env:STAGE}.json')
|
|
204
|
+
assert.is(result.didResolve, false)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('resolveInnerVariables - partial resolution fails completely', () => {
|
|
208
|
+
const config = { stage: 'prod' }
|
|
209
|
+
const result = resolveInnerVariables('./config-${self:stage}-${env:REGION}.json', variableSyntax, config, getProp)
|
|
210
|
+
assert.is(result.resolved, './config-${self:stage}-${env:REGION}.json')
|
|
211
|
+
assert.is(result.didResolve, false)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test.run()
|