configorama 0.6.7 → 0.6.9
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/index.js +19 -1
- package/src/main.js +785 -190
- package/src/resolvers/valueFromGit.js +21 -1
- package/src/sync.js +18 -3
- package/src/utils/enrichMetadata.js +229 -0
- package/src/utils/find-nested-variables.js +7 -4
- package/src/utils/find-nested-variables.test.js +43 -4
- package/src/utils/isValidValue.js +1 -1
- package/src/utils/splitByComma.js +33 -9
- package/src/utils/splitByComma.test.js +47 -2
|
@@ -21,11 +21,31 @@ async function _exec(cmd, options = { timeout: 1000 }) {
|
|
|
21
21
|
})
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// TODO denote computed fields in metadata
|
|
25
|
+
/*
|
|
26
|
+
{
|
|
27
|
+
variables: {
|
|
28
|
+
repo: {
|
|
29
|
+
value: '${git:repo}',
|
|
30
|
+
type: 'string',
|
|
31
|
+
description: 'The repository owner and name',
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
computedVariables : {
|
|
35
|
+
hash: {
|
|
36
|
+
value: '${git:sha1}',
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: 'The current commit hash',
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
*/
|
|
43
|
+
|
|
24
44
|
function createResolver(cwd) {
|
|
25
45
|
async function _getValueFromGit(variableString) {
|
|
26
46
|
const variable = variableString.split(`${GIT_PREFIX}:`)[1]
|
|
27
47
|
let value = null
|
|
28
|
-
// console.log('
|
|
48
|
+
// console.log('createResolver variableString', variableString)
|
|
29
49
|
if (variable.match(/^remote/i)) {
|
|
30
50
|
const hasParams = functionRegex.exec(variableString)
|
|
31
51
|
const remoteName = (hasParams && hasParams[2]) ? formatFunctionArgs(hasParams[2]) : 'origin'
|
package/src/sync.js
CHANGED
|
@@ -2,6 +2,7 @@ const path = require('path')
|
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const Configorama = require('./main')
|
|
4
4
|
const getFullPath = require('./utils/getFullFilePath')
|
|
5
|
+
const enrichMetadata = require('./utils/enrichMetadata')
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Force syncronous invocation of async API
|
|
@@ -37,7 +38,7 @@ module.exports = function configoramaSync(varSrcs = []) {
|
|
|
37
38
|
resolver: resolverFunction
|
|
38
39
|
}
|
|
39
40
|
})
|
|
40
|
-
return (args) => {
|
|
41
|
+
return async (args) => {
|
|
41
42
|
const { filePath, settings = {} } = args
|
|
42
43
|
const syncSettings = { sync: true }
|
|
43
44
|
if (customVariableSources && customVariableSources.length) {
|
|
@@ -45,7 +46,21 @@ module.exports = function configoramaSync(varSrcs = []) {
|
|
|
45
46
|
}
|
|
46
47
|
const finalSettings = Object.assign({}, settings, syncSettings)
|
|
47
48
|
const options = finalSettings.options || {}
|
|
48
|
-
const
|
|
49
|
-
|
|
49
|
+
const instance = new Configorama(filePath, finalSettings)
|
|
50
|
+
const result = await instance.init(options)
|
|
51
|
+
|
|
52
|
+
if (finalSettings.returnMetadata) {
|
|
53
|
+
const metadata = instance.collectVariableMetadata()
|
|
54
|
+
|
|
55
|
+
// Enrich metadata with resolution tracking data collected during execution
|
|
56
|
+
const enrichedMetadata = enrichMetadata(metadata, instance.resolutionTracking, instance.variableSyntax)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
config: result,
|
|
60
|
+
metadata: enrichedMetadata
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result
|
|
50
65
|
}
|
|
51
66
|
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const { splitCsv } = require('./splitCsv')
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract file path from a file() or text() reference string
|
|
5
|
+
* @param {string} propertyString - The property string containing file/text reference
|
|
6
|
+
* @returns {object|null} Object with filePath, or null if no match
|
|
7
|
+
*/
|
|
8
|
+
function extractFilePath(propertyString) {
|
|
9
|
+
const fileMatch = propertyString.match(/^\$\{(?:file|text)\((.*?)\)/)
|
|
10
|
+
if (!fileMatch || !fileMatch[1]) {
|
|
11
|
+
return null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const fileContent = fileMatch[1].trim()
|
|
15
|
+
const parts = splitCsv(fileContent)
|
|
16
|
+
let filePath = parts[0].trim()
|
|
17
|
+
|
|
18
|
+
// Remove quotes if present
|
|
19
|
+
filePath = filePath.replace(/^['"]|['"]$/g, '')
|
|
20
|
+
|
|
21
|
+
return { filePath }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a file path (add ./ prefix, fix .//, skip deep refs)
|
|
26
|
+
* @param {string} filePath - The file path to normalize
|
|
27
|
+
* @returns {string|null} Normalized path, or null if should be skipped
|
|
28
|
+
*/
|
|
29
|
+
function normalizePath(filePath) {
|
|
30
|
+
// Skip deep references
|
|
31
|
+
if (filePath.includes('deep:')) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let normalized = filePath
|
|
36
|
+
|
|
37
|
+
// Add ./ prefix for relative paths
|
|
38
|
+
if (!filePath.startsWith('./') &&
|
|
39
|
+
!filePath.startsWith('../') &&
|
|
40
|
+
!filePath.startsWith('/') &&
|
|
41
|
+
!filePath.startsWith('~')) {
|
|
42
|
+
normalized = './' + filePath
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fix double slashes
|
|
46
|
+
if (normalized.startsWith('.//')) {
|
|
47
|
+
normalized = normalized.replace('.//', './')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return normalized
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Enriches variable metadata with resolution tracking data
|
|
54
|
+
/**
|
|
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
|
|
59
|
+
*/
|
|
60
|
+
function enrichMetadata(metadata, resolutionTracking, variableSyntax) {
|
|
61
|
+
if (!resolutionTracking) {
|
|
62
|
+
return metadata
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const varKeys = Object.keys(metadata.variables)
|
|
66
|
+
|
|
67
|
+
for (const key of varKeys) {
|
|
68
|
+
const varInstances = metadata.variables[key]
|
|
69
|
+
|
|
70
|
+
for (const varData of varInstances) {
|
|
71
|
+
const pathKey = varData.path
|
|
72
|
+
const trackingData = resolutionTracking[pathKey]
|
|
73
|
+
|
|
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
|
|
79
|
+
for (let i = 0; i < varData.resolveDetails.length; i++) {
|
|
80
|
+
const detail = varData.resolveDetails[i]
|
|
81
|
+
const isOutermost = i === varData.resolveDetails.length - 1
|
|
82
|
+
|
|
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
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// For inner variables, try to find a matching call
|
|
97
|
+
for (const call of trackingData.calls) {
|
|
98
|
+
const callVar = call.variableString
|
|
99
|
+
const detailVar = detail.variable
|
|
100
|
+
|
|
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
|
|
110
|
+
}
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Build resolvedFileRefs array from tracking data
|
|
121
|
+
// Only use the LAST call for each path (final resolved state)
|
|
122
|
+
const resolvedFileRefs = []
|
|
123
|
+
const normalizedPaths = new Set()
|
|
124
|
+
|
|
125
|
+
for (const pathKey in resolutionTracking) {
|
|
126
|
+
const tracking = resolutionTracking[pathKey]
|
|
127
|
+
if (tracking.calls && tracking.calls.length) {
|
|
128
|
+
const lastCall = tracking.calls[tracking.calls.length - 1]
|
|
129
|
+
|
|
130
|
+
const extracted = extractFilePath(lastCall.propertyString)
|
|
131
|
+
if (extracted) {
|
|
132
|
+
const normalizedPath = normalizePath(extracted.filePath)
|
|
133
|
+
|
|
134
|
+
if (normalizedPath && !normalizedPaths.has(normalizedPath)) {
|
|
135
|
+
normalizedPaths.add(normalizedPath)
|
|
136
|
+
resolvedFileRefs.push(normalizedPath)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
metadata.resolvedFileRefs = resolvedFileRefs
|
|
143
|
+
|
|
144
|
+
// Build resolvedFileRefsData array - maps resolved paths to their variable strings and glob patterns
|
|
145
|
+
const resolvedFileRefsDataMap = new Map()
|
|
146
|
+
|
|
147
|
+
// First pass: collect all resolved paths and their original variable strings
|
|
148
|
+
for (const pathKey in resolutionTracking) {
|
|
149
|
+
const tracking = resolutionTracking[pathKey]
|
|
150
|
+
if (!tracking.calls || !tracking.calls.length) {
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const lastCall = tracking.calls[tracking.calls.length - 1]
|
|
155
|
+
const extracted = extractFilePath(lastCall.propertyString)
|
|
156
|
+
|
|
157
|
+
if (!extracted) {
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resolvedPath = normalizePath(extracted.filePath)
|
|
162
|
+
if (!resolvedPath) {
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Get original variable string from tracking
|
|
167
|
+
const originalPropertyString = tracking.originalPropertyString
|
|
168
|
+
if (!originalPropertyString) {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const origExtracted = extractFilePath(originalPropertyString)
|
|
173
|
+
if (!origExtracted) {
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const origPath = normalizePath(origExtracted.filePath)
|
|
178
|
+
if (!origPath) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Initialize map entry if needed
|
|
183
|
+
if (!resolvedFileRefsDataMap.has(resolvedPath)) {
|
|
184
|
+
resolvedFileRefsDataMap.set(resolvedPath, {
|
|
185
|
+
resolvedPath: resolvedPath,
|
|
186
|
+
refs: [],
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const entry = resolvedFileRefsDataMap.get(resolvedPath)
|
|
191
|
+
|
|
192
|
+
// Add original variable string with config path if not already present
|
|
193
|
+
const alreadyExists = entry.refs.some(ref => ref.path === pathKey && ref.value === origPath)
|
|
194
|
+
if (!alreadyExists) {
|
|
195
|
+
const refEntry = { path: pathKey, value: origPath }
|
|
196
|
+
// Check if the value contains variables
|
|
197
|
+
if (variableSyntax && origPath.match(variableSyntax)) {
|
|
198
|
+
refEntry.hasVariable = true
|
|
199
|
+
}
|
|
200
|
+
entry.refs.push(refEntry)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Second pass: generate glob patterns for each resolved path
|
|
205
|
+
for (const [resolvedPath, data] of resolvedFileRefsDataMap) {
|
|
206
|
+
const globPatternSet = new Set()
|
|
207
|
+
|
|
208
|
+
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)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const patterns = Array.from(globPatternSet)
|
|
216
|
+
if (patterns.length > 0) {
|
|
217
|
+
data.globPatterns = patterns
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Convert map to array
|
|
222
|
+
const resolvedFileRefsData = Array.from(resolvedFileRefsDataMap.values())
|
|
223
|
+
|
|
224
|
+
metadata.resolvedFileRefsData = resolvedFileRefsData
|
|
225
|
+
|
|
226
|
+
return metadata
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = enrichMetadata
|
|
@@ -22,6 +22,7 @@ const VAR_MATCH_REGEX = /__VAR_\d+__/
|
|
|
22
22
|
* @returns {Array} Array of match objects with fullMatch, variable, varString and other properties
|
|
23
23
|
*/
|
|
24
24
|
function findNestedVariables(input, regex, variablesKnownTypes, location, debug = false) {
|
|
25
|
+
// console.log('variablesKnownTypes', variablesKnownTypes)
|
|
25
26
|
// Create a copy of the input for replacement tracking
|
|
26
27
|
let current = input
|
|
27
28
|
// console.log('current', current)
|
|
@@ -182,7 +183,9 @@ function findNestedVariables(input, regex, variablesKnownTypes, location, debug
|
|
|
182
183
|
// remove first element from split
|
|
183
184
|
matches[i].fallbackValues = split.slice(1).map((item) => {
|
|
184
185
|
// console.log('item', item)
|
|
185
|
-
|
|
186
|
+
// Strip ${} wrapper if present to properly test against variablesKnownTypes
|
|
187
|
+
const innerContent = item.replace(/^\$\{(.*)\}$/, '$1')
|
|
188
|
+
const isVariable = variablesKnownTypes.test(innerContent) || VAR_MATCH_REGEX.test(item)
|
|
186
189
|
const fallbackData = {
|
|
187
190
|
isVariable,
|
|
188
191
|
fullMatch: item,
|
|
@@ -195,7 +198,7 @@ function findNestedVariables(input, regex, variablesKnownTypes, location, debug
|
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
if (isVariable) {
|
|
198
|
-
const varType =
|
|
201
|
+
const varType = innerContent.match(variablesKnownTypes)[1]
|
|
199
202
|
fallbackData.varType = varType
|
|
200
203
|
// if (varType === 'self:') {
|
|
201
204
|
// fallbackData.fullMatch = item.replace('self:', '')
|
|
@@ -290,13 +293,13 @@ function findNestedVariables(input, regex, variablesKnownTypes, location, debug
|
|
|
290
293
|
* @param {boolean} debug - Whether to print debug information
|
|
291
294
|
* @returns {Array} Array of match objects containing full match and captured group
|
|
292
295
|
*/
|
|
293
|
-
function
|
|
296
|
+
function findNestedVariablesOld(input, regex, variablesKnownTypes, debug = false) {
|
|
294
297
|
let str = input
|
|
295
298
|
let matches = []
|
|
296
299
|
let match
|
|
297
300
|
let iteration = 0
|
|
298
301
|
|
|
299
|
-
console.log('input', input)
|
|
302
|
+
// console.log('input', input)
|
|
300
303
|
|
|
301
304
|
if (debug) console.log(`Initial string: ${str}`)
|
|
302
305
|
|
|
@@ -106,12 +106,51 @@ test('findNestedVariables - mutliple fallback items', () => {
|
|
|
106
106
|
assert.equal(result[result.length - 1].variable, 'file(./config.${opt:stage, ${opt:stageOne}, ${opt:stageTwo}, "three"}.json)');
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
test
|
|
110
|
-
const input =
|
|
109
|
+
test('findNestedVariables - deep', () => {
|
|
110
|
+
const input =
|
|
111
|
+
'${file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)}';
|
|
111
112
|
const result = findNestedVariables(input, regex, variablesKnownTypes, 'xyz');
|
|
112
113
|
deepLog('result', result)
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
|
|
115
|
+
// Should have 5 variables total
|
|
116
|
+
assert.equal(result.length, 5);
|
|
117
|
+
|
|
118
|
+
// Check the innermost variable
|
|
119
|
+
assert.equal(result[0].fullMatch, '${env:foo}');
|
|
120
|
+
assert.equal(result[0].variable, 'env:foo');
|
|
121
|
+
assert.equal(result[0].varType, 'env:');
|
|
122
|
+
|
|
123
|
+
// Check opt:stageOne with env:foo fallback
|
|
124
|
+
assert.equal(result[1].fullMatch, '${opt:stageOne, ${env:foo}}');
|
|
125
|
+
assert.equal(result[1].variable, 'opt:stageOne, ${env:foo}');
|
|
126
|
+
assert.equal(result[1].varType, 'opt:');
|
|
127
|
+
assert.equal(result[1].hasFallback, true);
|
|
128
|
+
assert.equal(result[1].valueBeforeFallback, 'opt:stageOne');
|
|
129
|
+
assert.equal(result[1].fallbackValues.length, 1);
|
|
130
|
+
assert.equal(result[1].fallbackValues[0].isVariable, true);
|
|
131
|
+
assert.equal(result[1].fallbackValues[0].fullMatch, '${env:foo}');
|
|
132
|
+
assert.equal(result[1].fallbackValues[0].varType, 'env:');
|
|
133
|
+
|
|
134
|
+
// Check opt:stageTwo
|
|
135
|
+
assert.equal(result[2].fullMatch, '${opt:stageTwo}');
|
|
136
|
+
assert.equal(result[2].variable, 'opt:stageTwo');
|
|
137
|
+
|
|
138
|
+
// Check opt:stage with multiple fallbacks
|
|
139
|
+
assert.equal(result[3].fullMatch, '${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }');
|
|
140
|
+
assert.equal(result[3].variable, 'opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three"');
|
|
141
|
+
assert.equal(result[3].hasFallback, true);
|
|
142
|
+
assert.equal(result[3].valueBeforeFallback, 'opt:stage');
|
|
143
|
+
assert.equal(result[3].fallbackValues.length, 3);
|
|
144
|
+
assert.equal(result[3].fallbackValues[0].fullMatch, '${opt:stageOne, ${env:foo}}');
|
|
145
|
+
assert.equal(result[3].fallbackValues[0].isVariable, true);
|
|
146
|
+
assert.equal(result[3].fallbackValues[1].fullMatch, '${opt:stageTwo}');
|
|
147
|
+
assert.equal(result[3].fallbackValues[1].isVariable, true);
|
|
148
|
+
assert.equal(result[3].fallbackValues[2].fullMatch, '"three"');
|
|
149
|
+
assert.equal(result[3].fallbackValues[2].isVariable, false);
|
|
150
|
+
assert.equal(result[3].fallbackValues[2].stringValue, 'three');
|
|
151
|
+
|
|
152
|
+
// Check outermost file variable
|
|
153
|
+
assert.equal(result[4].variable, 'file(./config.${opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three" }.json)');
|
|
115
154
|
});
|
|
116
155
|
|
|
117
156
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const isEmpty = require('lodash.isempty')
|
|
2
2
|
|
|
3
3
|
module.exports = function isValidValue(val) {
|
|
4
|
-
if (typeof val === 'object' && val.hasOwnProperty('__internal_only_flag')) {
|
|
4
|
+
if (typeof val === 'object' && (val.hasOwnProperty('__internal_only_flag') || val.hasOwnProperty('__internal_metadata'))) {
|
|
5
5
|
return false
|
|
6
6
|
}
|
|
7
7
|
return val !== null && typeof val !== 'undefined' && !(typeof val === 'object' && isEmpty(val))
|
|
@@ -34,11 +34,13 @@ function splitByComma(string, regexPattern) {
|
|
|
34
34
|
let current = ""
|
|
35
35
|
let inQuote = false
|
|
36
36
|
let quoteChar = ""
|
|
37
|
-
let bracketDepth = 0 // Includes
|
|
38
|
-
|
|
37
|
+
let bracketDepth = 0 // Includes (), [], and {}
|
|
38
|
+
let dollarBraceDepth = 0 // Track ${ ... } depth separately (only when regexPattern is provided)
|
|
39
|
+
|
|
39
40
|
for (let i = 0; i < protectedString.length; i++) {
|
|
40
41
|
const char = protectedString[i]
|
|
41
|
-
|
|
42
|
+
const prevChar = i > 0 ? protectedString[i-1] : ''
|
|
43
|
+
|
|
42
44
|
// Handle quotes
|
|
43
45
|
if ((char === "'" || char === '"') && (i === 0 || protectedString[i-1] !== "\\")) {
|
|
44
46
|
if (!inQuote) {
|
|
@@ -48,13 +50,35 @@ function splitByComma(string, regexPattern) {
|
|
|
48
50
|
inQuote = false
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
|
-
|
|
52
|
-
// Handle parentheses and
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
|
|
54
|
+
// Handle parentheses, brackets, and curly braces
|
|
55
|
+
if (!inQuote) {
|
|
56
|
+
if (char === "(" || char === "[") {
|
|
57
|
+
bracketDepth++
|
|
58
|
+
} else if (char === ")" || char === "]") {
|
|
59
|
+
bracketDepth--
|
|
60
|
+
} else if (regexPattern) {
|
|
61
|
+
// Only track {} when we have regexPattern (i.e., when protecting variables)
|
|
62
|
+
// TODO this doesn't support custom variable syntax regexes.
|
|
63
|
+
if (char === "{" && prevChar === "$") {
|
|
64
|
+
// Track ${ as a special unit
|
|
65
|
+
dollarBraceDepth++
|
|
66
|
+
} else if (char === "{") {
|
|
67
|
+
// Standalone { (not part of ${)
|
|
68
|
+
bracketDepth++
|
|
69
|
+
} else if (char === "}") {
|
|
70
|
+
// Check if this closes a ${ or a standalone {
|
|
71
|
+
if (dollarBraceDepth > 0) {
|
|
72
|
+
dollarBraceDepth--
|
|
73
|
+
} else if (bracketDepth > 0) {
|
|
74
|
+
bracketDepth--
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
// Process comma
|
|
57
|
-
if (char === "," && !inQuote && bracketDepth === 0) {
|
|
81
|
+
if (char === "," && !inQuote && bracketDepth === 0 && dollarBraceDepth === 0) {
|
|
58
82
|
result.push(current.trim())
|
|
59
83
|
current = ""
|
|
60
84
|
} else {
|
|
@@ -46,7 +46,7 @@ test('splitByComma - should handle input with extra whitespace', () => {
|
|
|
46
46
|
|
|
47
47
|
test('splitByComma - should handle mixed scenarios', () => {
|
|
48
48
|
const result = splitByComma("normal, 'quoted, string', function(param1, param2)")
|
|
49
|
-
console.log('result', result)
|
|
49
|
+
// console.log('result', result)
|
|
50
50
|
assert.equal(result, ["normal", "'quoted, string'", "function(param1, param2)"])
|
|
51
51
|
})
|
|
52
52
|
|
|
@@ -80,5 +80,50 @@ test('splitByComma - should handle backtick quotes', () => {
|
|
|
80
80
|
assert.equal(result, ["normal", "`template ${with, commas} inside`"])
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
test('splitByComma - should split ${} variables when NO regex provided', () => {
|
|
84
|
+
const result = splitByComma('${env:no, env:empty}')
|
|
85
|
+
assert.equal(result, ['${env:no', 'env:empty}'])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('splitByComma - should protect ${} variables when regex provided', () => {
|
|
89
|
+
const result = splitByComma('opt:stage, ${opt:stageOne}, ${opt:stageTwo}', variableSyntax)
|
|
90
|
+
assert.equal(result, ['opt:stage', '${opt:stageOne}', '${opt:stageTwo}'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('splitByComma - should protect nested ${} variables when regex provided', () => {
|
|
94
|
+
const result = splitByComma('opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}', variableSyntax)
|
|
95
|
+
assert.equal(result, ['opt:stage', '${opt:stageOne, ${env:foo}}', '${opt:stageTwo}'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('splitByComma - should handle multiple nested levels with regex', () => {
|
|
99
|
+
const result = splitByComma('${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three"', variableSyntax)
|
|
100
|
+
assert.equal(result, ['${opt:stageOne, ${env:foo}}', '${opt:stageTwo}', '"three"'])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('splitByComma - should handle standalone {} objects with regex', () => {
|
|
104
|
+
const result = splitByComma('func(arg1, {key: value}, arg2)', variableSyntax)
|
|
105
|
+
assert.equal(result, ['func(arg1, {key: value}, arg2)'])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('splitByComma - should handle mixed ${} variables and {} objects with regex', () => {
|
|
109
|
+
const result = splitByComma('${self:config}, {key: "value"}, ${env:var}', variableSyntax)
|
|
110
|
+
assert.equal(result, ['${self:config}', '{key: "value"}', '${env:var}'])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('splitByComma - should handle complex nested case from failCases', () => {
|
|
114
|
+
const result = splitByComma('${env:no, env:empty}')
|
|
115
|
+
assert.equal(result, ['${env:no', 'env:empty}'])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('splitByComma - should handle deeply nested variables with regex', () => {
|
|
119
|
+
const input = 'opt:stage, ${opt:stageOne, ${env:foo}}, ${opt:stageTwo}, "three"'
|
|
120
|
+
const result = splitByComma(input, variableSyntax)
|
|
121
|
+
assert.equal(result.length, 4)
|
|
122
|
+
assert.equal(result[0], 'opt:stage')
|
|
123
|
+
assert.equal(result[1], '${opt:stageOne, ${env:foo}}')
|
|
124
|
+
assert.equal(result[2], '${opt:stageTwo}')
|
|
125
|
+
assert.equal(result[3], '"three"')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Run all tests
|
|
84
129
|
test.run()
|